@jsenv/navi 0.29.163 → 0.29.164

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.
@@ -28064,6 +28064,21 @@ installImportMetaCssBuild(import.meta);/**
28064
28064
  * the reverse of another, so B → A can be given a movement of its own, or
28065
28065
  * silenced with "none", by writing it (see findRelation).
28066
28066
  *
28067
+ * One shape of page has no pair to write: the one whose door is in the fixed
28068
+ * furniture — a gear in a top bar, a "+" in a tab bar — and which is therefore
28069
+ * opened from every screen and closed back onto whichever one the reader was
28070
+ * on. The relation is real and it is a single sentence; only the `from` is
28071
+ * missing, so it is left out:
28072
+ *
28073
+ * defineRouteTransition(null, SETTINGS_PAGE, "cover-top");
28074
+ *
28075
+ * Arriving there plays forward from wherever, leaving plays back to wherever,
28076
+ * and that is what lets the back button close it the way it opened — a
28077
+ * traversal carries no request, and nothing has to remember per history entry
28078
+ * what the press that created it had asked for. It is tried after every
28079
+ * written pair, and it is not defineRouteDefaultTransition: a default is about
28080
+ * every navigation nothing was said about, this is about ONE destination.
28081
+ *
28067
28082
  * The relation says WHEN something plays and which way; the transition says
28068
28083
  * WHAT plays — a movement navi ships, or a name the application defines in its
28069
28084
  * own CSS (see the JSDoc below). Said without one, the relation plays the
@@ -28252,7 +28267,7 @@ const css$11 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
28252
28267
  }
28253
28268
  }
28254
28269
 
28255
- &[data-navi-route-transition-type="cover-x"] {
28270
+ &[data-navi-route-transition-type="cover-right"] {
28256
28271
  &[data-navi-route-transition="forward"] {
28257
28272
  --navi-route-transition-leave: navi-route-transition-still;
28258
28273
  --navi-route-transition-enter: navi-route-transition-enter-from-end;
@@ -28261,14 +28276,22 @@ const css$11 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
28261
28276
  &[data-navi-route-transition="back"] {
28262
28277
  --navi-route-transition-leave: navi-route-transition-leave-towards-end;
28263
28278
  --navi-route-transition-enter: navi-route-transition-still;
28279
+ }
28280
+ }
28264
28281
 
28265
- &::view-transition-old(root), &::view-transition-old(navi-route-transition) {
28266
- z-index: 1;
28267
- }
28282
+ &[data-navi-route-transition-type="cover-left"] {
28283
+ &[data-navi-route-transition="forward"] {
28284
+ --navi-route-transition-leave: navi-route-transition-still;
28285
+ --navi-route-transition-enter: navi-route-transition-enter-from-start;
28286
+ }
28287
+
28288
+ &[data-navi-route-transition="back"] {
28289
+ --navi-route-transition-leave: navi-route-transition-leave-towards-start;
28290
+ --navi-route-transition-enter: navi-route-transition-still;
28268
28291
  }
28269
28292
  }
28270
28293
 
28271
- &[data-navi-route-transition-type="cover-y"] {
28294
+ &[data-navi-route-transition-type="cover-bottom"] {
28272
28295
  &[data-navi-route-transition="forward"] {
28273
28296
  --navi-route-transition-leave: navi-route-transition-still;
28274
28297
  --navi-route-transition-enter: navi-route-transition-enter-from-bottom;
@@ -28277,7 +28300,23 @@ const css$11 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
28277
28300
  &[data-navi-route-transition="back"] {
28278
28301
  --navi-route-transition-leave: navi-route-transition-leave-towards-bottom;
28279
28302
  --navi-route-transition-enter: navi-route-transition-still;
28303
+ }
28304
+ }
28280
28305
 
28306
+ &[data-navi-route-transition-type="cover-top"] {
28307
+ &[data-navi-route-transition="forward"] {
28308
+ --navi-route-transition-leave: navi-route-transition-still;
28309
+ --navi-route-transition-enter: navi-route-transition-enter-from-top;
28310
+ }
28311
+
28312
+ &[data-navi-route-transition="back"] {
28313
+ --navi-route-transition-leave: navi-route-transition-leave-towards-top;
28314
+ --navi-route-transition-enter: navi-route-transition-still;
28315
+ }
28316
+ }
28317
+
28318
+ &[data-navi-route-transition-type="cover-right"], &[data-navi-route-transition-type="cover-left"], &[data-navi-route-transition-type="cover-bottom"], &[data-navi-route-transition-type="cover-top"] {
28319
+ &[data-navi-route-transition="back"] {
28281
28320
  &::view-transition-old(root), &::view-transition-old(navi-route-transition) {
28282
28321
  z-index: 1;
28283
28322
  }
@@ -28300,7 +28339,7 @@ const css$11 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
28300
28339
  }
28301
28340
  }
28302
28341
 
28303
- &[data-navi-route-transition-type="slide-x"], &[data-navi-route-transition-type="slide-y"], &[data-navi-route-transition-type="cover-x"], &[data-navi-route-transition-type="cover-y"], &[data-navi-route-transition-type="zoom"] {
28342
+ &[data-navi-route-transition-type="slide-x"], &[data-navi-route-transition-type="slide-y"], &[data-navi-route-transition-type="cover-right"], &[data-navi-route-transition-type="cover-left"], &[data-navi-route-transition-type="cover-bottom"], &[data-navi-route-transition-type="cover-top"], &[data-navi-route-transition-type="zoom"] {
28304
28343
  &::view-transition-old(root), &::view-transition-old(navi-route-transition) {
28305
28344
  animation-name: var(--navi-route-transition-leave);
28306
28345
  }
@@ -28310,7 +28349,7 @@ const css$11 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
28310
28349
  }
28311
28350
  }
28312
28351
 
28313
- &[data-navi-route-transition-type="slide-x"], &[data-navi-route-transition-type="slide-y"], &[data-navi-route-transition-type="cover-x"], &[data-navi-route-transition-type="cover-y"] {
28352
+ &[data-navi-route-transition-type="slide-x"], &[data-navi-route-transition-type="slide-y"], &[data-navi-route-transition-type="cover-right"], &[data-navi-route-transition-type="cover-left"], &[data-navi-route-transition-type="cover-bottom"], &[data-navi-route-transition-type="cover-top"] {
28314
28353
  &::view-transition-old(root), &::view-transition-new(root), &::view-transition-old(navi-route-transition), &::view-transition-new(navi-route-transition) {
28315
28354
  animation-timing-function: ease;
28316
28355
  }
@@ -28435,10 +28474,17 @@ const RouteTransitionArea = ({
28435
28474
  };
28436
28475
 
28437
28476
  /**
28438
- * Declare how a pair of routes moves against each other.
28439
- *
28440
- * @param {object} from - a route, or `{ route, params }` when the page is a
28441
- * param of a route rather than a route of its own.
28477
+ * Declare how a pair of routes moves against each other — or how one route is
28478
+ * entered, from wherever its door happens to be.
28479
+ *
28480
+ * @param {object|null} from - a route, or `{ route, params }` when the page is
28481
+ * a param of a route rather than a route of its own. `null` says the page is
28482
+ * reached FROM ANYWHERE: its door is in the furniture (a gear in the top
28483
+ * bar, a "+" in the tab bar) rather than on a screen, so there is no pair to
28484
+ * write it on. Arriving at `to` from any page then plays forward and leaving
28485
+ * it for any page plays back, which is what makes the back button close it
28486
+ * the way it opened. Tried last, after every written pair: a pair naming the
28487
+ * same destination still owns its crossing.
28442
28488
  * @param {object} to - same forms. Going from `from` to `to` plays forward,
28443
28489
  * the reverse plays back — unless the reverse is written as a relation of
28444
28490
  * its own, which then owns that way (a movement of its own, or `"none"` for
@@ -28452,8 +28498,11 @@ const RouteTransitionArea = ({
28452
28498
  * Omitted, the browser's own cross-fade. Shipped with navi:
28453
28499
  * - `"slide-x"`, `"slide-y"`: the two pages slide past each other, forward
28454
28500
  * towards the start of the axis;
28455
- * - `"cover-x"`, `"cover-y"`: the page arriving slides in OVER one that does
28456
- * not move, and slides off it on the way back;
28501
+ * - `"cover-right"`, `"cover-left"`, `"cover-bottom"`, `"cover-top"`: the
28502
+ * page arriving slides in OVER one that does not move, and slides off it
28503
+ * on the way back. The edge named is the one it comes IN from, which is
28504
+ * where its door is: a page reached from a bottom bar covers from the
28505
+ * bottom, one pulled down from a top bar covers from the top;
28457
28506
  * - `"zoom"`: the deeper page is the closer one — it lands from slightly too
28458
28507
  * big, and grows away when left;
28459
28508
  * - `"none"`: nothing, said out loud — written on one way of a pair, it cuts
@@ -28484,6 +28533,9 @@ const RouteTransitionArea = ({
28484
28533
  const defineRouteTransition = (from, to, transition) => {
28485
28534
  import.meta.css = [css$11, "@jsenv/navi/src/nav/route_transition.jsx"];
28486
28535
  installTransitionWindowCss();
28536
+ if (!to) {
28537
+ throw new TypeError(`defineRouteTransition needs a destination: "to" is ${to}. The page reached from anywhere is written defineRouteTransition(null, THAT_PAGE, ...) — there is no relation the other way round, a page LEFT for anywhere being the back half of that one.`);
28538
+ }
28487
28539
  const {
28488
28540
  type,
28489
28541
  duration
@@ -28652,13 +28704,15 @@ const rebuildWatcher = () => {
28652
28704
  }
28653
28705
  // Every page any relation mentions, each once: the position of the current
28654
28706
  // page in this list is what turns "some signal moved" into "the document
28655
- // went from page A to page B".
28707
+ // went from page A to page B". A relation written from anywhere mentions one
28708
+ // page only — the other end is whatever the reader was on, and -1 is how it
28709
+ // reads here.
28656
28710
  const pages = [];
28657
28711
  for (const {
28658
28712
  from,
28659
28713
  to
28660
28714
  } of relations) {
28661
- if (pageIndexOf$1(pages, from) === -1) {
28715
+ if (from && pageIndexOf$1(pages, from) === -1) {
28662
28716
  pages.push(from);
28663
28717
  }
28664
28718
  if (pageIndexOf$1(pages, to) === -1) {
@@ -28678,10 +28732,15 @@ const rebuildWatcher = () => {
28678
28732
  firstReading = false;
28679
28733
  return;
28680
28734
  }
28681
- if (index === -1 || fromIndex === -1 || fromIndex === index) {
28735
+ if (fromIndex === index) {
28682
28736
  return;
28683
28737
  }
28684
- const found = findRelation(pages[fromIndex], pages[index]);
28738
+ // A page in no relation at all is a real end of the crossing, not a
28739
+ // missing one: it is the "anywhere" a page reached from the furniture is
28740
+ // opened over and closed back onto (see findRelation).
28741
+ const fromPage = fromIndex === -1 ? null : pages[fromIndex];
28742
+ const toPage = index === -1 ? null : pages[index];
28743
+ const found = findRelation(fromPage, toPage);
28685
28744
  if (!found && !navigationRequest) {
28686
28745
  // No relation says anything about these two and this navigation asked
28687
28746
  // for nothing: they are side by side, and silence is the fact — not a
@@ -28700,7 +28759,7 @@ const rebuildWatcher = () => {
28700
28759
  return;
28701
28760
  }
28702
28761
  beginTransition({
28703
- page: pages[index],
28762
+ page: toPage,
28704
28763
  url: navigationUrl,
28705
28764
  // Which way it plays: what the navigation itself said first — the link
28706
28765
  // being pressed is where the way the app is being walked is known — then
@@ -28777,13 +28836,23 @@ observeAfterRouting(() => {
28777
28836
  releaseRoutingRenderingHold();
28778
28837
  });
28779
28838
 
28780
- // The exact way travelled first, over the whole registry, and only then the
28781
- // reverses: a relation written B → A owns that way, and being the reverse of
28782
- // one written A → B never outranks it. This is what makes reciprocity a
28783
- // default rather than a decree — write the way back to give it a movement of
28784
- // its own, or "none" to silence it.
28839
+ // The exact way travelled first, over the whole registry, then the reverses,
28840
+ // and last the pages written from anywhere.
28841
+ //
28842
+ // A relation written B → A owns that way, and being the reverse of one written
28843
+ // A → B never outranks it. This is what makes reciprocity a default rather
28844
+ // than a decree — write the way back to give it a movement of its own, or
28845
+ // "none" to silence it.
28846
+ //
28847
+ // A page reached from anywhere is tried after every pair, so a pair naming the
28848
+ // same destination still owns its crossing — the map, where it was drawn, is
28849
+ // more precise than "from wherever". Arriving is read before leaving: between
28850
+ // two such pages, the one being opened says what plays.
28785
28851
  const findRelation = (fromPage, toPage) => {
28786
28852
  for (const relation of relations) {
28853
+ if (!relation.from) {
28854
+ continue;
28855
+ }
28787
28856
  if (samePage$1(relation.from, fromPage) && samePage$1(relation.to, toPage)) {
28788
28857
  return {
28789
28858
  direction: "forward",
@@ -28792,6 +28861,9 @@ const findRelation = (fromPage, toPage) => {
28792
28861
  }
28793
28862
  }
28794
28863
  for (const relation of relations) {
28864
+ if (!relation.from) {
28865
+ continue;
28866
+ }
28795
28867
  if (samePage$1(relation.from, toPage) && samePage$1(relation.to, fromPage)) {
28796
28868
  return {
28797
28869
  direction: "back",
@@ -28799,6 +28871,28 @@ const findRelation = (fromPage, toPage) => {
28799
28871
  };
28800
28872
  }
28801
28873
  }
28874
+ for (const relation of relations) {
28875
+ if (relation.from) {
28876
+ continue;
28877
+ }
28878
+ if (samePage$1(relation.to, toPage)) {
28879
+ return {
28880
+ direction: "forward",
28881
+ relation
28882
+ };
28883
+ }
28884
+ }
28885
+ for (const relation of relations) {
28886
+ if (relation.from) {
28887
+ continue;
28888
+ }
28889
+ if (samePage$1(relation.to, fromPage)) {
28890
+ return {
28891
+ direction: "back",
28892
+ relation
28893
+ };
28894
+ }
28895
+ }
28802
28896
  return null;
28803
28897
  };
28804
28898
 
@@ -28906,10 +29000,11 @@ const beginTransition = ({
28906
29000
  // rendered has already resolved the wait by the next line.
28907
29001
  releaseRendering();
28908
29002
  if (page === null) {
28909
- // A default transition: which page is arriving is unknown, and some
28910
- // navigations render no route at all (a search param bound to a
28911
- // signal) — waited on, those would freeze the page until the browser
28912
- // gives up. The wait is raced with a short timer instead.
29003
+ // Which page is arriving is unknown — a default transition, or a page
29004
+ // reached from anywhere being left for wherever — and some navigations
29005
+ // render no route at all (a search param bound to a signal): waited
29006
+ // on, those would freeze the page until the browser gives up. The wait
29007
+ // is raced with a short timer instead.
28913
29008
  await Promise.race([renderWait.rendered, waitMs(50)]);
28914
29009
  } else if (pageIsCurrent$1(page)) {
28915
29010
  await renderWait.rendered;
@@ -29034,10 +29129,18 @@ const armRouteRenderWait$1 = () => {
29034
29129
  };
29035
29130
  };
29036
29131
  const waitMs = ms => new Promise(resolve => setTimeout(resolve, ms));
29037
- const normalizePage$1 = page => page.isRoute ? {
29038
- route: page,
29039
- params: undefined
29040
- } : page;
29132
+
29133
+ // A page written as the route itself, and the page written nowhere: `from`
29134
+ // may be left out, which is a relation about arriving at `to` from anywhere.
29135
+ const normalizePage$1 = page => {
29136
+ if (!page) {
29137
+ return null;
29138
+ }
29139
+ return page.isRoute ? {
29140
+ route: page,
29141
+ params: undefined
29142
+ } : page;
29143
+ };
29041
29144
 
29042
29145
  // Two pages are the same page when they select the same thing, not when they
29043
29146
  // were written by the same hand.
@@ -30594,6 +30697,117 @@ const unnameAfterTravel = element => {
30594
30697
  element.style.viewTransitionName = "";
30595
30698
  };
30596
30699
 
30700
+ /**
30701
+ * What covers the box a route movement plays in, from INSIDE the document: a
30702
+ * sticky row of tabs above a `<RouteTravel>`, a header pinned to the top of a
30703
+ * scroller. The element says it is there, this measures what it takes and
30704
+ * publishes it under --navi-transition-cover-* — the slot transition_window.js
30705
+ * declares and explains, and which nothing but the element covering the box can
30706
+ * fill: the safe area answers for what is pinned to the WINDOW's edges, and a
30707
+ * sticky row is none of those.
30708
+ *
30709
+ * The pictures of a transition are drawn in the top layer, above every z-index
30710
+ * of the document, so a band left uncounted here is a band the pictures slide
30711
+ * over instead of being cut at.
30712
+ *
30713
+ * Same shape as the room a fixed bar gives back (layout/fixed_bar/
30714
+ * fixed_bar_space.js): measured rather than declared, and the largest of the
30715
+ * elements sharing an edge rather than their sum — during a route movement the
30716
+ * outgoing and the incoming row are both mounted, pinned to the same edge, one
30717
+ * over the other. Nothing takes its layout from these variables, though, so the
30718
+ * write happens where it is measured, with no frame to wait for.
30719
+ */
30720
+
30721
+
30722
+ const sizeMapByEdge = new Map();
30723
+ // What is currently on <html> for each edge (absent = the variable is not set).
30724
+ // Several elements sharing an edge means most calls compute the same largest
30725
+ // size again, and a size that has not changed is a style recalculation of the
30726
+ // whole document for nothing.
30727
+ const writtenValueByEdge = new Map();
30728
+
30729
+ /**
30730
+ * @param {import("preact").RefObject<Element>} elementRef - The element
30731
+ * covering the box.
30732
+ * @param {"top"|"right"|"bottom"|"left"} [edge="top"] - Which side of the box
30733
+ * it covers. Its size across that edge is what gets published; the edge's own
30734
+ * band (the safe area, the fixed bars) is already counted, so an element
30735
+ * states its own size and nothing else.
30736
+ */
30737
+ const useTransitionCover = (elementRef, edge = "top") => {
30738
+ installTransitionWindowCss();
30739
+
30740
+ const measureCover = (element) => {
30741
+ const { width, height } = element.getBoundingClientRect();
30742
+ return edge === "left" || edge === "right" ? width : height;
30743
+ };
30744
+
30745
+ // Anything a render can change — the row's content, a size prop, a theme
30746
+ // variable — is measured in that same commit: a movement starting on the
30747
+ // render that grew the row is cut at the row as it now is.
30748
+ useLayoutEffect(() => {
30749
+ const element = elementRef.current;
30750
+ if (!element) {
30751
+ return;
30752
+ }
30753
+ writeCover(edge, element, measureCover(element));
30754
+ });
30755
+
30756
+ // What no render caused: a font arriving late, content coming from outside, a
30757
+ // rotation.
30758
+ useLayoutEffect(() => {
30759
+ const element = elementRef.current;
30760
+ if (!element) {
30761
+ return undefined;
30762
+ }
30763
+ const resizeObserver = new ResizeObserver(() => {
30764
+ writeCover(edge, element, measureCover(element));
30765
+ });
30766
+ resizeObserver.observe(element);
30767
+ return () => {
30768
+ resizeObserver.disconnect();
30769
+ writeCover(edge, element, null);
30770
+ };
30771
+ }, [edge]);
30772
+ };
30773
+
30774
+ const writeCover = (edge, element, size) => {
30775
+ let sizeMap = sizeMapByEdge.get(edge);
30776
+ if (!sizeMap) {
30777
+ sizeMap = new Map();
30778
+ sizeMapByEdge.set(edge, sizeMap);
30779
+ }
30780
+ if (size === null) {
30781
+ sizeMap.delete(element);
30782
+ } else {
30783
+ sizeMap.set(element, size);
30784
+ }
30785
+ let largestSize = 0;
30786
+ for (const coverSize of sizeMap.values()) {
30787
+ if (coverSize > largestSize) {
30788
+ largestSize = coverSize;
30789
+ }
30790
+ }
30791
+ const property = `--navi-transition-cover-${edge}`;
30792
+ const { style } = document.documentElement;
30793
+ if (sizeMap.size === 0) {
30794
+ // Back to the zero the sheet declares, rather than a zero written over it:
30795
+ // a page with no sticky row must read what navi says, not what the last one
30796
+ // left behind.
30797
+ if (writtenValueByEdge.has(edge)) {
30798
+ writtenValueByEdge.delete(edge);
30799
+ style.removeProperty(property);
30800
+ }
30801
+ return;
30802
+ }
30803
+ const value = `${largestSize}px`;
30804
+ if (writtenValueByEdge.get(edge) === value) {
30805
+ return;
30806
+ }
30807
+ writtenValueByEdge.set(edge, value);
30808
+ style.setProperty(property, value);
30809
+ };
30810
+
30597
30811
  const routeAction = (
30598
30812
  routeOrRoutes,
30599
30813
  action,
@@ -39632,6 +39846,12 @@ const shouldInjectSpacingBetween = (left, right) => {
39632
39846
  * An invisible placeholder rendered with those styles reserves the space; the
39633
39847
  * real visible text is layered on top via `position: absolute`.
39634
39848
  * Best combined with `noWrap` — does not work reliably on multi-line text.
39849
+ * The placeholder is a SECOND copy of the children: it is `aria-hidden`, so
39850
+ * the accessible name is right, but the text appears twice in `textContent`
39851
+ * — which is what a test asserting on the element's text reads. Where the
39852
+ * element's text must stay single (a tab, a label a test matches on), reserve
39853
+ * the space some other way; `Link`'s `currentEffectBold` paints the weight by
39854
+ * default and comes here only when asked to (`"hold-space"`).
39635
39855
  *
39636
39856
  * @param {boolean} [boldStable]
39637
39857
  * Alternative to `holdSpaceForStyle` for multi-line text. Keeps a consistent
@@ -40131,6 +40351,7 @@ installImportMetaCssBuild(import.meta);const css$X = /* css */`@layer navi {
40131
40351
  --link-text-decoration-hover: var(--link-text-decoration);
40132
40352
  --link-cursor: pointer;
40133
40353
  --link-outline-width: 2px;
40354
+ --link-current-effect-bold-thickness: .02em;
40134
40355
  --link-current-indicator-size: 2px;
40135
40356
  --link-current-indicator-spacing: 0;
40136
40357
  --link-current-indicator-color: var(--navi-link-current-indicator-color);
@@ -40266,7 +40487,12 @@ installImportMetaCssBuild(import.meta);const css$X = /* css */`@layer navi {
40266
40487
  --x-link-color: var(--link-color-current);
40267
40488
  }
40268
40489
 
40269
- &[data-current-effect-bold] {
40490
+ &[data-current-effect-bold="text-shadow"] {
40491
+ text-shadow: var(--link-current-effect-bold-thickness) 0 0 currentColor,
40492
+ calc(-1 * var(--link-current-effect-bold-thickness)) 0 0 currentColor;
40493
+ }
40494
+
40495
+ &[data-current-effect-bold="hold-space"] {
40270
40496
  font-weight: bold;
40271
40497
  }
40272
40498
 
@@ -40479,8 +40705,17 @@ Object.assign(PSEUDO_CLASSES, {
40479
40705
  * `"tab"` renders a tab-like affordance.
40480
40706
  * @param {boolean|"top"|"bottom"|"left"|"right"} [props.currentIndicator] - A
40481
40707
  * bar drawn on the given edge (or bottom when `true`) while current.
40482
- * @param {boolean} [props.currentEffectBold] - Bold the text while current
40483
- * (reserving the bold width so layout doesn't shift).
40708
+ * @param {boolean|"text-shadow"|"hold-space"} [props.currentEffectBold] - Bold
40709
+ * the label while current, without its width changing — a row of tabs must
40710
+ * not jump when one becomes current. Which is a choice of two:
40711
+ * - `"text-shadow"` (what `true` means): the weight is PAINTED, the glyphs
40712
+ * drawn a hair to each side of themselves
40713
+ * (`--link-current-effect-bold-thickness`). The label stays one piece of
40714
+ * text, so `textContent` and anything reading it still see it once.
40715
+ * - `"hold-space"`: the real bold face, its width held by an invisible copy
40716
+ * of the label rendered bold underneath. True bold — and the label then
40717
+ * appears TWICE in `textContent`, which a test asserting on the element's
40718
+ * text reads (see `holdSpaceForStyle` in `text.jsx`).
40484
40719
  * @param {boolean} [props.currentEffectShadow] - Inset-shadow effect while
40485
40720
  * current (used with `variant="tab"`).
40486
40721
  * @param {boolean|import("ignore:preact").ComponentChild} [props.startIcon] - Icon
@@ -40710,6 +40945,11 @@ const LinkPlain = props => {
40710
40945
  const startIconEl = startIcon;
40711
40946
  const endIconEl = innerEndIcon;
40712
40947
 
40948
+ // Bold by painting the glyphs unless asked for the bold face itself, because
40949
+ // that one costs a second copy of the label in the element's text (see the
40950
+ // css above).
40951
+ const currentEffectBoldTechnique = currentEffectBold === true ? "text-shadow" : currentEffectBold || undefined;
40952
+
40713
40953
  // Where the bar goes: said here, or once for the whole row by the <Nav>
40714
40954
  // around this link.
40715
40955
  const currentIndicatorAsked = currentIndicator ?? nav?.currentIndicator;
@@ -40799,7 +41039,7 @@ const LinkPlain = props => {
40799
41039
  onnavi_value: e => {
40800
41040
  e.detail.setValue(value);
40801
41041
  },
40802
- holdSpaceForStyle: currentEffectBold ? {
41042
+ holdSpaceForStyle: currentEffectBoldTechnique === "hold-space" ? {
40803
41043
  fontWeight: "bold"
40804
41044
  } : undefined,
40805
41045
  preventSpaceUnderlines: true
@@ -40813,7 +41053,7 @@ const LinkPlain = props => {
40813
41053
  ,
40814
41054
 
40815
41055
  "data-variant": variant,
40816
- "data-current-effect-bold": currentEffectBold ? "" : undefined,
41056
+ "data-current-effect-bold": currentEffectBoldTechnique,
40817
41057
  "data-current-effect-shadow": currentEffectShadow ? "" : undefined,
40818
41058
  "data-current-indicator-position": currentIndicatorPosition,
40819
41059
  "data-anchor": anchor ? "" : undefined,
@@ -41051,6 +41291,10 @@ const css$W = /* css */`@layer navi {
41051
41291
  --nav-current-indicator-size: 2px;
41052
41292
  --nav-current-indicator-color: var(--navi-link-current-indicator-color);
41053
41293
  }
41294
+
41295
+ .navi_nav .navi_link {
41296
+ --link-current-indicator-color: var(--nav-current-indicator-color);
41297
+ }
41054
41298
  }
41055
41299
 
41056
41300
  .navi_nav[data-nav-indicator] {
@@ -41061,8 +41305,14 @@ const css$W = /* css */`@layer navi {
41061
41305
  var(--nav-indicator-position-delta));
41062
41306
  --x-nav-indicator-length: calc(var(--nav-indicator-length) + var(--slide-travel-progress) *
41063
41307
  var(--nav-indicator-length-delta));
41308
+ --x-nav-indicator-color: var(--nav-indicator-color, var(--nav-current-indicator-color));
41309
+ --x-nav-indicator-color-toward: var(--nav-indicator-color-toward, var(--x-nav-indicator-color));
41064
41310
  z-index: 1;
41065
- background: var(--nav-current-indicator-color);
41311
+ background: color-mix(in srgb,
41312
+ var(--x-nav-indicator-color-toward)
41313
+ calc(var(--slide-travel-progress) * var(--nav-indicator-toward-sign, 0) *
41314
+ 100%),
41315
+ var(--x-nav-indicator-color));
41066
41316
  pointer-events: none;
41067
41317
  border-radius: .1px;
41068
41318
  position: absolute;
@@ -41127,6 +41377,11 @@ const css$W = /* css */`@layer navi {
41127
41377
 
41128
41378
  &[data-scrollable] {
41129
41379
  max-width: 100%;
41380
+
41381
+ & .navi_link {
41382
+ white-space: nowrap;
41383
+ flex: none;
41384
+ }
41130
41385
  }
41131
41386
 
41132
41387
  &[data-scrollable][data-vertical] {
@@ -41162,13 +41417,13 @@ const css$W = /* css */`@layer navi {
41162
41417
  &[data-expand-x] {
41163
41418
  flex-grow: 1;
41164
41419
 
41165
- &:not([data-vertical]) .navi_link {
41420
+ &:not([data-vertical]):not([data-scrollable]) .navi_link {
41166
41421
  flex: 1;
41167
41422
  justify-content: center;
41168
41423
  }
41169
41424
  }
41170
41425
 
41171
- &[data-expand-y][data-vertical] {
41426
+ &[data-expand-y][data-vertical]:not([data-scrollable]) {
41172
41427
  & .navi_link {
41173
41428
  flex: 1;
41174
41429
  }
@@ -41256,6 +41511,10 @@ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
41256
41511
  * @param {boolean|"top"|"bottom"|"left"|"right"} [props.currentIndicator] - the
41257
41512
  * bar that says which tab one is on, said once here rather than on every
41258
41513
  * `<Link>`. A link may still say otherwise for itself.
41514
+ * Its colour belongs to the tab it is under: `currentIndicatorColor` here is
41515
+ * what every tab inherits, and a tab declaring
41516
+ * `--nav-current-indicator-color` for itself is arrived at in its own colour
41517
+ * — the bar changes colour as it travels rather than once it lands.
41259
41518
  * @param {boolean} [props.currentIndicatorSlides=true] - whether that bar
41260
41519
  * travels from the tab it was under to the tab it is under now, instead of
41261
41520
  * going out on one and coming back on the other. For a nav made of routes it
@@ -41334,11 +41593,11 @@ const Nav = ({
41334
41593
  return navElement.querySelector(`[data-slide-target="${CSS.escape(towardArea)}"]`);
41335
41594
  };
41336
41595
 
41337
- // Where the trait is and where it is headed, as four numbers of pixels the
41338
- // CSS above interpolates between (see the .navi_nav_indicator rules). Written
41339
- // by hand rather than rendered: it is read off the row as it stands, and the
41340
- // travel it must agree with starts in the same frame the container publishes
41341
- // it — a render would land after the movement had begun.
41596
+ // Where the trait is, where it is headed, and in which colour at each end —
41597
+ // the numbers the CSS above interpolates between (see the .navi_nav_indicator
41598
+ // rules). Written by hand rather than rendered: it is read off the row as it
41599
+ // stands, and the travel it must agree with starts in the same frame the
41600
+ // container publishes it — a render would land after the movement had begun.
41342
41601
  const paintIndicatorGeometry = () => {
41343
41602
  const navElement = navRef.current;
41344
41603
  const containerElement = slideContainerElementRef.current;
@@ -41360,13 +41619,29 @@ const Nav = ({
41360
41619
  position: tabElement.offsetLeft,
41361
41620
  length: tabElement.offsetWidth
41362
41621
  };
41622
+ // The colour a tab asks the bar to be under it. Read from the tab and not
41623
+ // from the row, so a tab that says nothing gives the row's value back —
41624
+ // it inherits it (see the css above) — and one that says something gives
41625
+ // its own. Empty when neither resolves to anything: the css then falls
41626
+ // back on its own, rather than being handed a colour that is not one.
41627
+ const colorOf = tabElement => getComputedStyle(tabElement).getPropertyValue("--link-current-indicator-color").trim();
41628
+ const writeColor = (property, color) => {
41629
+ if (color) {
41630
+ navElement.style.setProperty(property, color);
41631
+ } else {
41632
+ navElement.style.removeProperty(property);
41633
+ }
41634
+ };
41363
41635
  const currentMeasure = measure(tabElements[currentIndex]);
41364
41636
  const towardArea = containerElement.getAttribute("data-slide-travel-toward");
41365
41637
  const towardIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === towardArea);
41366
41638
  let positionDelta = 0;
41367
41639
  let lengthDelta = 0;
41640
+ let towardSign = 0;
41641
+ let towardColor = "";
41368
41642
  if (towardIndex !== -1 && towardIndex !== currentIndex) {
41369
- const towardMeasure = measure(tabElements[towardIndex]);
41643
+ const towardTabElement = tabElements[towardIndex];
41644
+ const towardMeasure = measure(towardTabElement);
41370
41645
  // What one box of travel is worth in pixels of this row, signed so that
41371
41646
  // the trait is exactly on the other tab when the progress is at its own
41372
41647
  // end: the container counts +1 when the picture leans on a slide sitting
@@ -41374,6 +41649,8 @@ const Nav = ({
41374
41649
  const sign = towardIndex > currentIndex ? -1 : 1;
41375
41650
  positionDelta = (towardMeasure.position - currentMeasure.position) * sign;
41376
41651
  lengthDelta = (towardMeasure.length - currentMeasure.length) * sign;
41652
+ towardSign = sign;
41653
+ towardColor = colorOf(towardTabElement);
41377
41654
  }
41378
41655
  const {
41379
41656
  style
@@ -41382,6 +41659,9 @@ const Nav = ({
41382
41659
  style.setProperty("--nav-indicator-length", currentMeasure.length);
41383
41660
  style.setProperty("--nav-indicator-position-delta", positionDelta);
41384
41661
  style.setProperty("--nav-indicator-length-delta", lengthDelta);
41662
+ style.setProperty("--nav-indicator-toward-sign", towardSign);
41663
+ writeColor("--nav-indicator-color", colorOf(tabElements[currentIndex]));
41664
+ writeColor("--nav-indicator-color-toward", towardColor);
41385
41665
  navElement.setAttribute("data-nav-indicator-measured", "");
41386
41666
  };
41387
41667
  // Reached through a ref by everything watching the DOM below: those watchers
@@ -42857,7 +43137,15 @@ installImportMetaCssBuild(import.meta);/**
42857
43137
  * keeps its bars against its own edges.
42858
43138
  * 2. **It gives its space back.** Being fixed it covers the content: without a
42859
43139
  * reserve the end of a long page stays under it, unreachable. It publishes
42860
- * what it takes on <html> — see fixed_bar_space.js.
43140
+ * what it takes on <html> — see fixed_bar_space.js. An edge holds ONE bar:
43141
+ * bars sharing an edge are pinned to it one over the other, so the room
43142
+ * given back is the largest of them and never their sum — which is what the
43143
+ * two bars of a page transition need, and what makes a deliberately stacked
43144
+ * second bar unexpressible. A second strip under the first belongs to the
43145
+ * flow: `position: sticky` at `top: var(--navi-fixed-bar-space-top)`, which
43146
+ * puts it under whatever the bar of the moment is worth, and
43147
+ * `useTransitionCover` (nav/transition_cover.js) so the pictures of a route
43148
+ * movement are cut at it.
42861
43149
  * 3. **It reaches under the notch.** `env(safe-area-inset-*)` is padding, so
42862
43150
  * the content is pushed in while the background keeps running to the edge of
42863
43151
  * the screen. Across the bar that inset is added to `width`/`height` too, so
@@ -42972,7 +43260,9 @@ const FixedBarStyleCSSVars = {
42972
43260
  * borderColor?: string,
42973
43261
  * }>}
42974
43262
  * @param {"top"|"bottom"|"left"|"right"} [props.area="top"] - Which edge of
42975
- * the window it is pinned to.
43263
+ * the window it is pinned to. One bar per edge: a second one does not stack
43264
+ * under the first, it sits on it (see this file's top comment for the strip
43265
+ * that does stack).
42976
43266
  * @param {string|number} [props.height] - For a bar on the top or bottom
42977
43267
  * @param {string|number} [props.width] - …and for one on a side. The safe-area
42978
43268
  * inset is NOT part of it: it is added on top, so the content keeps the size
@@ -43945,23 +44235,34 @@ installImportMetaCssBuild(import.meta);/**
43945
44235
  * Expandable: an in-flow disclosure — a UI part that reveals a content part.
43946
44236
  * It covers the same ground as <Details> with structural differences:
43947
44237
  *
43948
- * - the two parts are explicit and free to order/orient:
44238
+ * - the two parts are explicit, and are Boxes:
43949
44239
  *
43950
44240
  * <Expandable>
43951
44241
  * <Expandable.UI>See more</Expandable.UI>
43952
44242
  * <Expandable.Content>…</Expandable.Content>
43953
44243
  * </Expandable>
43954
44244
  *
43955
- * Content after UI expands below (the <details> shape), Content before UI
43956
- * expands above; `layout="column"` puts the parts side by side (sharing
43957
- * their height), the content then expanding horizontally. The marker
43958
- * chevron follows: it points right while closed and toward where the
43959
- * content went while open (down, up, or left). The common shape has a
43960
- * shorthand: `ui` prop + children as content.
44245
+ * `openDirection` decides where the content is revealed — down (the
44246
+ * <details> shape) or up, left or right in `layout="column"`, where the
44247
+ * parts sit side by side sharing their height. It places the parts too
44248
+ * (CSS `order`), so an expandable revealing upward can still be written UI
44249
+ * first, and an expandable with no UI part at all — driven from
44250
+ * `open`/`signal` by a toggle of the app's own — can say which way it
44251
+ * opens. The marker chevron follows: it points right while closed and
44252
+ * toward where the content went while open (down, up, or left). The common
44253
+ * shape has a shorthand: `ui` prop + children as content.
44254
+ *
43961
44255
  * - the UI part is the focusable toggle itself (role button, Space/Enter,
43962
44256
  * arrow keys) and accepts any markup: controls inside it keep their own
43963
44257
  * behavior, the marker is purely decorative.
43964
44258
  *
44259
+ * The root never looks at its children: which parts are there, in which order,
44260
+ * is none of its business. It publishes a context and the parts take what they
44261
+ * need from it — including WHEN to arm the reveal, which <Expandable.Content>
44262
+ * asks for from its own layout effect. That is what lets a part re-render
44263
+ * later than the root (a memoized subtree does exactly that): the measurement
44264
+ * runs in the commit that put the content in the DOM, whichever one that is.
44265
+ *
43965
44266
  * Reach for it knowingly: expanding in-flow SHIFTS the layout — everything
43966
44267
  * below (or beside) moves when it opens. A Popover, Dialog, Picker or Callout
43967
44268
  * answers the same click on its own layer, moving nothing, which is usually
@@ -44001,6 +44302,10 @@ const css$Q = /* css */`.navi_expandable {
44001
44302
  display: flex;
44002
44303
  position: relative;
44003
44304
 
44305
+ &[data-open-direction="down"] > .navi_expandable_content_container, &[data-open-direction="right"] > .navi_expandable_content_container, &[data-open-direction="up"] > .navi_expandable_ui, &[data-open-direction="left"] > .navi_expandable_ui {
44306
+ order: 1;
44307
+ }
44308
+
44004
44309
  & > .navi_expandable_ui {
44005
44310
  cursor: pointer;
44006
44311
  user-select: none;
@@ -44046,7 +44351,7 @@ const css$Q = /* css */`.navi_expandable {
44046
44351
  grid-template-rows: 1fr;
44047
44352
  }
44048
44353
 
44049
- &[data-content-first]:not([data-layout="column"]) > .navi_expandable_content_container {
44354
+ &[data-open-direction="up"] > .navi_expandable_content_container {
44050
44355
  clip-path: inset(0 -9999px -9999px);
44051
44356
  align-content: end;
44052
44357
 
@@ -44087,7 +44392,7 @@ const css$Q = /* css */`.navi_expandable {
44087
44392
  grid-template-rows: none;
44088
44393
  }
44089
44394
 
44090
- &[data-content-first] > .navi_expandable_content_container {
44395
+ &[data-open-direction="left"] > .navi_expandable_content_container {
44091
44396
  clip-path: inset(-9999px -9999px 0 0);
44092
44397
  justify-content: end;
44093
44398
 
@@ -44143,6 +44448,7 @@ const useExpandableContext = partName => {
44143
44448
  * loading?: boolean,
44144
44449
  * animation?: boolean,
44145
44450
  * layout?: "row" | "column",
44451
+ * openDirection?: "down" | "up" | "right" | "left",
44146
44452
  * autoFocus?: boolean,
44147
44453
  * maxContentHeight?: string | number,
44148
44454
  * mount?: "always" | "from-first-open" | "while-opened",
@@ -44175,6 +44481,12 @@ const useExpandableContext = partName => {
44175
44481
  * @param layout - `"row"` (default): the parts stack, the content expands
44176
44482
  * vertically. `"column"`: the parts sit side by side sharing their height,
44177
44483
  * the content expands horizontally next to the UI part.
44484
+ * @param openDirection - Where the content is revealed: `"down"` (default) or
44485
+ * `"up"` in the stacked layout, `"right"` (default) or `"left"` in
44486
+ * `layout="column"`. It also places the parts, so the direction holds
44487
+ * whatever order they are written in — and an expandable with no
44488
+ * `<Expandable.UI>` at all, driven from `open`/`signal` by a toggle of the
44489
+ * app's own, can still say which way it opens.
44178
44490
  * @param autoFocus - Off by default (the focus stays on the UI part when
44179
44491
  * opening). `true` moves the focus into the content on open — the
44180
44492
  * `[autofocus]` element if any, the first focusable otherwise. Whatever the
@@ -44204,6 +44516,7 @@ const Expandable = props => {
44204
44516
  loading,
44205
44517
  animation = false,
44206
44518
  layout,
44519
+ openDirection,
44207
44520
  autoFocus,
44208
44521
  maxContentHeight,
44209
44522
  mount = MOUNT_DEFAULT,
@@ -44345,30 +44658,44 @@ const Expandable = props => {
44345
44658
  toggleTo(openRequested);
44346
44659
  }, [openRequested]);
44347
44660
 
44348
- // A state change: tell the world (the "toggle" event), move the focus, and
44349
- // set up the reveal. Skipped on mount — nothing changed, so neither the
44350
- // event nor a transition exists (and a page must not have its focus stolen
44351
- // by an expandable that was simply already open).
44661
+ // A state change: tell the world (the "toggle" event) and take the keyboard
44662
+ // back if the closing content held it. Skipped on mount — nothing changed,
44663
+ // so no event exists (and a page must not have its focus stolen by an
44664
+ // expandable that was simply already open).
44352
44665
  const isFirstOpenedRunRef = useRef(true);
44353
44666
  useLayoutEffect(() => {
44354
44667
  if (isFirstOpenedRunRef.current) {
44355
44668
  isFirstOpenedRunRef.current = false;
44356
- return undefined;
44669
+ return;
44357
44670
  }
44358
44671
  const root = rootRef.current;
44359
44672
  root.dispatchEvent(createToggleEvent(opened));
44360
44673
  if (opened) {
44361
- if (autoFocus) {
44362
- const firstFocusableElement = findFirstFocusableInContent();
44363
- if (firstFocusableElement) {
44364
- firstFocusableElement.focus();
44365
- }
44674
+ return;
44675
+ }
44676
+ const focusedBeforeClose = focusedBeforeCloseRef.current;
44677
+ focusedBeforeCloseRef.current = null;
44678
+ if (focusedBeforeClose && contentContainerRef.current && contentContainerRef.current.contains(focusedBeforeClose)) {
44679
+ const uiElement = uiRef.current;
44680
+ if (uiElement) {
44681
+ uiElement.focus();
44682
+ } else {
44683
+ // Nothing of the expandable's own can hold the keyboard (no UI part):
44684
+ // the focus only has to leave the content becoming inert.
44685
+ focusedBeforeClose.blur();
44366
44686
  }
44367
- } else {
44368
- const focusedBeforeClose = focusedBeforeCloseRef.current;
44369
- focusedBeforeCloseRef.current = null;
44370
- if (focusedBeforeClose && contentContainerRef.current && contentContainerRef.current.contains(focusedBeforeClose)) {
44371
- uiRef.current.focus();
44687
+ }
44688
+ }, [opened]);
44689
+
44690
+ // Everything a state change owes the content: the focus it may take, and the
44691
+ // reveal, which measures it. Ran by <Expandable.Content> from its own layout
44692
+ // effect — the commit where the content really is in the DOM — and returns
44693
+ // that effect's cleanup.
44694
+ const applyOpenedToContent = () => {
44695
+ if (opened && autoFocus) {
44696
+ const firstFocusableElement = findFirstFocusableInContent();
44697
+ if (firstFocusableElement) {
44698
+ firstFocusableElement.focus();
44372
44699
  }
44373
44700
  }
44374
44701
  if (!animation) {
@@ -44380,9 +44707,9 @@ const Expandable = props => {
44380
44707
  // The reveal needs the content at its final size before the track
44381
44708
  // starts moving, and the final size only exists in the open state —
44382
44709
  // the reflow trick (see instructions.md, CSS section), with transitions
44383
- // suppressed BEFORE the first layout read: this effect runs pre-paint,
44384
- // so any earlier read would itself be the first recalc of the open
44385
- // state and would start the track transition (see revealStartSizeRef).
44710
+ // suppressed BEFORE the first layout read: this runs pre-paint, so any
44711
+ // earlier read would itself be the first recalc of the open state and
44712
+ // would start the track transition (see revealStartSizeRef).
44386
44713
  contentContainer.style.transitionProperty = "none";
44387
44714
  const finalRect = contentElement.getBoundingClientRect();
44388
44715
  if (isColumn) {
@@ -44422,7 +44749,7 @@ const Expandable = props => {
44422
44749
  setSettled(true);
44423
44750
  });
44424
44751
  return cancel;
44425
- }, [opened]);
44752
+ };
44426
44753
  useLayoutEffect(() => {
44427
44754
  if (settled && !opened && mountedWhileOpened) {
44428
44755
  setContentMounted(false);
@@ -44468,6 +44795,12 @@ const Expandable = props => {
44468
44795
  if (!arrowKeyShortcuts) {
44469
44796
  return;
44470
44797
  }
44798
+ // The shortcuts all speak about the UI part — opening from it, stepping
44799
+ // into the content, stepping back to it. Without one the expandable is
44800
+ // driven from outside and the keys belong to whatever drives it.
44801
+ if (!uiRef.current) {
44802
+ return;
44803
+ }
44471
44804
  // A nested expandable (deeper, so heard first) already answered this key.
44472
44805
  if (keyboardEvent.defaultPrevented) {
44473
44806
  return;
@@ -44549,22 +44882,21 @@ const Expandable = props => {
44549
44882
  }
44550
44883
  };
44551
44884
 
44552
- // Where the content went, so the marker can point at it while open (closed
44553
- // always points right): below by default, above when the content part comes
44554
- // first, beside for layout="column" (the chevron then points back toward
44555
- // the UI: left).
44556
- const childArray = toChildArray(children);
44557
- const firstPart = childArray.find(child => child && (child.type === ExpandableUI || child.type === ExpandableContent));
44558
- const hasParts = Boolean(firstPart);
44559
- const contentFirst = hasParts && firstPart.type === ExpandableContent;
44560
- const openDirection = isColumn ? "left" : contentFirst ? "up" : "down";
44885
+ // Only the two directions the layout has room for: a column layout reveals
44886
+ // sideways, a stacked one vertically.
44887
+ const revealDirection = isColumn ? openDirection === "left" ? "left" : "right" : openDirection === "up" ? "up" : "down";
44888
+ // While open the marker points at the content, while closed always right.
44889
+ // In a column layout it points back toward the UI part whichever side the
44890
+ // content took, "right" being the closed direction already.
44891
+ const markerDirection = isColumn ? "left" : revealDirection;
44561
44892
  const expandableContextValue = {
44562
44893
  opened,
44563
44894
  loading: loading || hasAction && actionLoading,
44564
44895
  contentMounted,
44565
44896
  hasAction,
44566
44897
  effectiveAction,
44567
- openDirection,
44898
+ markerDirection,
44899
+ applyOpenedToContent,
44568
44900
  toggleTo,
44569
44901
  onUIClick,
44570
44902
  onUIPointerDown,
@@ -44574,12 +44906,9 @@ const Expandable = props => {
44574
44906
  contentId
44575
44907
  };
44576
44908
 
44577
- // Explicit parts win; the `ui` prop + children is the shorthand for the
44578
- // common shape (UI above, content below). Parts are cloned on every render:
44579
- // reference-stable children would be bailed out of the commit, leaving
44580
- // their context subscription to re-render them asynchronously — after the
44581
- // [opened] effect above, which measures the content they render.
44582
- const body = hasParts ? childArray.map(child => child && (child.type === ExpandableUI || child.type === ExpandableContent) ? cloneElement(child) : child) : jsxs(Fragment$1, {
44909
+ // The `ui` prop is the shorthand for the common shape: it names the UI part
44910
+ // and children are the content. Without it the children ARE the parts.
44911
+ const body = ui === undefined ? children : jsxs(Fragment$1, {
44583
44912
  children: [jsx(ExpandableUI, {
44584
44913
  children: ui
44585
44914
  }), jsx(ExpandableContent, {
@@ -44591,7 +44920,7 @@ const Expandable = props => {
44591
44920
  baseClassName: "navi_expandable",
44592
44921
  "aria-expanded": opened ? "true" : "false",
44593
44922
  "data-layout": isColumn ? "column" : undefined,
44594
- "data-content-first": contentFirst ? "" : undefined,
44923
+ "data-open-direction": revealDirection,
44595
44924
  "data-animation": animation ? "" : undefined,
44596
44925
  "data-settled": settled ? "" : undefined,
44597
44926
  "data-content-scrolls": maxContentHeight === undefined ? undefined : "",
@@ -44631,22 +44960,25 @@ const Expandable = props => {
44631
44960
  * The always-visible part that reveals the content: the focusable toggle
44632
44961
  * itself (role button — click, Space/Enter, arrow keys), holding the marker
44633
44962
  * plus whatever it is given — any markup, a function of `{ open }` included.
44634
- * Controls inside it keep their own behavior and do not toggle. Its position
44635
- * among the parts decides where the content goes (before the content: content
44636
- * below/right; after it: content above/left).
44963
+ * Controls inside it keep their own behavior and do not toggle. A Box, so it
44964
+ * takes the layout and style props every Box takes.
44637
44965
  *
44638
44966
  * @type {import("ignore:preact").FunctionComponent<{
44967
+ * marker?: false | import("ignore:preact").ComponentChildren,
44639
44968
  * children?: import("ignore:preact").ComponentChildren | ((state: { open: boolean }) => import("ignore:preact").ComponentChildren),
44640
44969
  * }>}
44970
+ * @param marker - The chevron drawn before the label. `false` removes it (a UI
44971
+ * part that is already an icon has no room for one); any node replaces it.
44641
44972
  */
44642
44973
  const ExpandableUI = ({
44974
+ marker,
44643
44975
  children,
44644
44976
  ...rest
44645
44977
  }) => {
44646
44978
  const {
44647
44979
  opened,
44648
44980
  loading,
44649
- openDirection,
44981
+ markerDirection,
44650
44982
  toggleTo,
44651
44983
  onUIClick,
44652
44984
  onUIPointerDown,
@@ -44654,9 +44986,9 @@ const ExpandableUI = ({
44654
44986
  uiRef,
44655
44987
  contentId
44656
44988
  } = useExpandableContext("UI");
44657
- return jsxs("div", {
44989
+ return jsxs(Box, {
44658
44990
  ref: uiRef,
44659
- className: "navi_expandable_ui",
44991
+ baseClassName: "navi_expandable_ui",
44660
44992
  role: "button",
44661
44993
  tabIndex: 0,
44662
44994
  "aria-expanded": opened,
@@ -44681,14 +45013,14 @@ const ExpandableUI = ({
44681
45013
  toggleTo(false);
44682
45014
  },
44683
45015
  ...rest,
44684
- children: [jsx("span", {
45016
+ children: [marker === false ? null : jsx("span", {
44685
45017
  className: "navi_expandable_marker",
44686
45018
  "aria-hidden": "true",
44687
- children: jsx(SummaryMarker, {
45019
+ children: marker === undefined ? jsx(SummaryMarker, {
44688
45020
  open: opened,
44689
45021
  loading: loading,
44690
- openDirection: openDirection
44691
- })
45022
+ openDirection: markerDirection
45023
+ }) : marker
44692
45024
  }), jsx("div", {
44693
45025
  className: "navi_expandable_ui_label",
44694
45026
  children: typeof children === "function" ? children({
@@ -44699,8 +45031,9 @@ const ExpandableUI = ({
44699
45031
  };
44700
45032
 
44701
45033
  /**
44702
- * The revealed part. With an `action` on the Expandable, children may be a
44703
- * function `(data) => ui` or a branches object — see ActionRenderer.
45034
+ * The revealed part, a Box like the UI part. With an `action` on the
45035
+ * Expandable, children may be a function `(data) => ui` or a branches object —
45036
+ * see ActionRenderer.
44704
45037
  *
44705
45038
  * @type {import("ignore:preact").FunctionComponent<{}>}
44706
45039
  */
@@ -44713,9 +45046,24 @@ const ExpandableContent = ({
44713
45046
  contentMounted,
44714
45047
  hasAction,
44715
45048
  effectiveAction,
45049
+ applyOpenedToContent,
44716
45050
  contentContainerRef,
44717
45051
  contentId
44718
45052
  } = useExpandableContext("Content");
45053
+
45054
+ // The focus move and the reveal are the root's, but they measure the content
45055
+ // and can only run once it is in the DOM — which is this commit, the one
45056
+ // that rendered it. Skipped on mount: nothing has changed yet, and an
45057
+ // expandable that was simply already open must neither animate nor take the
45058
+ // focus.
45059
+ const isFirstOpenedRunRef = useRef(true);
45060
+ useLayoutEffect(() => {
45061
+ if (isFirstOpenedRunRef.current) {
45062
+ isFirstOpenedRunRef.current = false;
45063
+ return undefined;
45064
+ }
45065
+ return applyOpenedToContent();
45066
+ }, [opened]);
44719
45067
  let content = children;
44720
45068
  if (hasAction) {
44721
45069
  content = jsx(ActionRenderer, {
@@ -44723,10 +45071,10 @@ const ExpandableContent = ({
44723
45071
  children: children
44724
45072
  });
44725
45073
  }
44726
- return jsx("div", {
45074
+ return jsx(Box, {
44727
45075
  ref: contentContainerRef,
44728
45076
  id: contentId,
44729
- className: "navi_expandable_content_container",
45077
+ baseClassName: "navi_expandable_content_container",
44730
45078
  inert: opened ? undefined : true,
44731
45079
  ...rest,
44732
45080
  children: jsx("div", {
@@ -80746,5 +81094,5 @@ const UserSvg = () => jsx("svg", {
80746
81094
  })
80747
81095
  });
80748
81096
 
80749
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, OfflineError, 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, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isOfflineError, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
81097
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, OfflineError, 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, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isOfflineError, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
80750
81098
  //# sourceMappingURL=jsenv_navi.js.map