@jsenv/navi 0.29.134 → 0.29.135

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.
@@ -373,6 +373,9 @@ installImportMetaCssBuild(import.meta);/**
373
373
  const URL_TARGET_ATTRIBUTE = "data-url-target";
374
374
  const css$14 = /* css */`
375
375
  @layer navi {
376
+ /* Layered whole: the mark is navi's suggestion for "here is what the URL
377
+ pointed at", not something it needs. An app replaces it, or removes it
378
+ with animation: none, from an unlayered rule of any weight. */
376
379
  [${URL_TARGET_ATTRIBUTE}] {
377
380
  animation: navi_url_target var(--navi-url-target-duration, 2000ms)
378
381
  ease-out;
@@ -5979,25 +5982,32 @@ const css$12 = /* css */`
5979
5982
  border-radius: 50%;
5980
5983
  }
5981
5984
 
5985
+ /* The svg takes the whole square and the glyph keeps its share of it
5986
+ inside the viewBox (see CALLOUT_STATUS_GLYPH_VIEWBOX). Sized as a
5987
+ fraction of the square instead, the svg box lands on a fractional
5988
+ device pixel and the rasteriser snaps the glyph a pixel off centre —
5989
+ visible on a small badge, where the glyph is only a few pixels wide. */
5982
5990
  svg {
5983
- width: auto;
5984
- height: 55%;
5991
+ width: 100%;
5992
+ height: 100%;
5985
5993
  }
5986
5994
  }
5987
- /* Inside an <Icon>, the icon box is the size: fill it. The glyph keeps its
5988
- own share of it — the Icon's rule sizing any svg it holds to the whole
5989
- box is for an svg that IS the icon, and this one sits in a square. */
5995
+ /* Inside an <Icon>, the icon box is the size: fill it. */
5990
5996
  .navi_icon > .navi_callout_status_icon {
5991
5997
  width: 100%;
5992
5998
  height: 100%;
5993
5999
  }
5994
- .navi_icon[data-icon-char] .navi_callout_status_icon svg {
5995
- width: auto;
5996
- height: 55%;
5997
- }
5998
6000
  `;
5999
- const CALLOUT_STATUS_GLYPH_VIEWBOX = "0 0 125 300";
6000
- const CALLOUT_STATUS_GLYPH_PATH = "m25,1 8,196h59l8-196zm37,224a37,37 0 1,0 2,0z";
6001
+
6002
+ /**
6003
+ * The "!" on a square, viewBox and path both. The box is square and holds the
6004
+ * glyph's margin: the ink is 298 of 540 units tall (55%), centered. Drawn this
6005
+ * way rather than tight around the ink so whoever shows it can give the svg the
6006
+ * whole square — the glyph is then placed by the vector rasteriser, which has
6007
+ * no device pixel grid to miss, at any square size down to a fractional one.
6008
+ */
6009
+ const CALLOUT_STATUS_GLYPH_VIEWBOX = "0 0 540 540";
6010
+ const CALLOUT_STATUS_GLYPH_PATH = "m232.5,121 8,196h59l8-196zm36.5,224a37,37 0 1,0 2,0z";
6001
6011
 
6002
6012
  /**
6003
6013
  * @type {import("ignore:preact").FunctionComponent<{
@@ -6048,7 +6058,8 @@ const css$11 = /* css */`
6048
6058
  /* A callout is parented to what it explains, so it inherits from it — and
6049
6059
  an element that suppressed text selection (a list row, a drag source)
6050
6060
  would make its own explanation unselectable. The message is text one
6051
- copies. */
6061
+ copies. Layered: an app that has a reason to lock its own callouts down
6062
+ says so and wins. */
6052
6063
  user-select: text;
6053
6064
 
6054
6065
  --callout-success-color: var(--navi-callout-success-color);
@@ -6208,9 +6219,11 @@ const css$11 = /* css */`
6208
6219
  display: none;
6209
6220
  }
6210
6221
 
6222
+ /* The glyph keeps its share of the square inside the viewBox
6223
+ (see CALLOUT_STATUS_GLYPH_VIEWBOX), so the svg takes all of it. */
6211
6224
  svg {
6212
- width: 16px;
6213
- height: 12px;
6225
+ width: 100%;
6226
+ height: 100%;
6214
6227
  color: white;
6215
6228
  }
6216
6229
  }
@@ -6957,6 +6970,7 @@ const calloutTemplate = /* html */`
6957
6970
  <div
6958
6971
  class="navi_callout"
6959
6972
  popover="manual"
6973
+ navi-out-of-flow=""
6960
6974
  aria-expanded="true"
6961
6975
  >
6962
6976
  <div class="navi_callout_box">
@@ -27055,12 +27069,115 @@ const releaseTransitionDestination = (owner) => {
27055
27069
  publishDestination(null);
27056
27070
  };
27057
27071
 
27072
+ /**
27073
+ * The furniture around the pages — the fixed bars — photographed with them for
27074
+ * the length of a route transition, so that a piece of it belonging to ONE of
27075
+ * the two states takes part in the movement instead of appearing or vanishing
27076
+ * in a frame.
27077
+ *
27078
+ * Whether a bar is the frame or part of what changes is a fact about the PAIR
27079
+ * of states, never about the bar: the top bar of a list is the frame while one
27080
+ * goes into a row of that list, and part of what changes when the next page
27081
+ * takes the whole screen. So nothing is declared — a name is written on every
27082
+ * bar for the length of the movement, and the browser derives the rest:
27083
+ *
27084
+ * - a bar the two states SHARE is one element wearing one name on both sides.
27085
+ * The browser pairs the two pictures into one group and holds it where it
27086
+ * stands: the frame, and the pages move behind it exactly as before.
27087
+ * - a bar only one state has never meets a counterpart. It stands where it was
27088
+ * photographed while the pages move OVER it (see the z-order in
27089
+ * route_transition.jsx) — covered progressively as a page comes over it,
27090
+ * uncovered as one leaves — rather than going out with the render.
27091
+ *
27092
+ * The name is per ELEMENT and kept for the element's whole life, which is what
27093
+ * makes "shared" mean shared: several bars can live on one edge
27094
+ * (layout/fixed_bar/fixed_bar_space.js), so a bar leaving and another one
27095
+ * arriving on that same edge are two names and two movements, not one bar
27096
+ * changing its mind.
27097
+ *
27098
+ * Worn only while a transition of navi's plays: named the rest of the time, a
27099
+ * bar would be captured during every view transition the APPLICATION starts —
27100
+ * dead to the pointer and drawn in the top layer for the length of a movement
27101
+ * that has nothing to do with it. Being unable to answer a press is the price
27102
+ * of being photographed, and a route transition is where it costs nothing:
27103
+ * both pages are pictures for those few hundred milliseconds anyway.
27104
+ */
27105
+
27106
+ // What counts as furniture: what is pinned to an edge of the window and gives
27107
+ // its room back to the content (layout/fixed_bar/fixed_bar.jsx). A sticky row
27108
+ // inside the pages needs none of this — it lives in the area, so it is already
27109
+ // part of the pages' own picture.
27110
+ const FURNITURE_SELECTOR = ".navi_fixed_bar";
27111
+ const NAME_PROPERTY = "view-transition-name";
27112
+ const FURNITURE_NAME_PREFIX = "navi-transition-furniture-";
27113
+
27114
+ const nameByElement = new WeakMap();
27115
+ let nameCount = 0;
27116
+
27117
+ // Whose movement the names belong to, for the same reason the window has an
27118
+ // owner (transition_window.js): a movement ending after another has replaced
27119
+ // it must not strip what the new one is wearing.
27120
+ let furnitureOwner = null;
27121
+ let namedElements = new Set();
27122
+
27123
+ /**
27124
+ * Name what stands around the area. Called twice for one movement: before the
27125
+ * picture of the state being left is taken, and again once the state arriving
27126
+ * has rendered — a bar that just mounted has to wear its name before the
27127
+ * second picture, and one that survived the render is left with the name it
27128
+ * already has.
27129
+ */
27130
+ const nameTransitionFurniture = (owner, areaElement) => {
27131
+ if (owner !== furnitureOwner) {
27132
+ furnitureOwner = owner;
27133
+ namedElements = new Set();
27134
+ }
27135
+ for (const element of document.querySelectorAll(FURNITURE_SELECTOR)) {
27136
+ if (namedElements.has(element)) {
27137
+ continue;
27138
+ }
27139
+ // Inside the area it is not furniture, it is the page: naming it would
27140
+ // punch a hole in the picture the movement is played on.
27141
+ if (areaElement.contains(element)) {
27142
+ continue;
27143
+ }
27144
+ let name = nameByElement.get(element);
27145
+ if (!name) {
27146
+ // A name the application wrote itself answers for that element, and it
27147
+ // is saying something navi is not: it wants that bar moved on the pages'
27148
+ // clock, by its own CSS. Asked only of a bar navi has never named — a
27149
+ // bar wearing one of ITS names is one this movement inherited from the
27150
+ // one it interrupted, and taking it for the application's would leave it
27151
+ // named for the rest of the document's life.
27152
+ if (getComputedStyle(element).viewTransitionName !== "none") {
27153
+ continue;
27154
+ }
27155
+ nameCount++;
27156
+ name = `${FURNITURE_NAME_PREFIX}${nameCount}`;
27157
+ nameByElement.set(element, name);
27158
+ }
27159
+ element.style.setProperty(NAME_PROPERTY, name);
27160
+ namedElements.add(element);
27161
+ }
27162
+ };
27163
+
27164
+ const releaseTransitionFurniture = (owner) => {
27165
+ if (owner !== furnitureOwner) {
27166
+ return;
27167
+ }
27168
+ furnitureOwner = null;
27169
+ for (const element of namedElements) {
27170
+ element.style.removeProperty(NAME_PROPERTY);
27171
+ }
27172
+ namedElements = new Set();
27173
+ };
27174
+
27058
27175
  /**
27059
27176
  * The window two pages are seen through while one replaces the other, measured
27060
27177
  * once and published for the length of the movement.
27061
27178
  *
27062
- * Both ways of moving from one route to another need the same six numbers, so
27063
- * they are written under the same names and read by the same CSS formulas —
27179
+ * Both ways of moving from one route to another need the same numbers, so they
27180
+ * are written under the same names and read by the same CSS formulas —
27064
27181
  * `route_travel.jsx` for a row a finger pushes, `route_transition.jsx` for a
27065
27182
  * relation between two pages. Only one of them ever plays at a time (there is
27066
27183
  * one view transition per document), which is what lets them share the names.
@@ -27071,16 +27188,25 @@ const releaseTransitionDestination = (owner) => {
27071
27188
  * pictures of a transition are drawn in the top layer, above everything, so
27072
27189
  * no overflow of the document reaches them: where to cut them can only be
27073
27190
  * said from outside the document, and this is where that outside is known.
27074
- * - **the height it is held at**, the taller of the two states. Left to the
27075
- * browser the window's height animates from one to the other, and a window
27076
- * that changes size under the pictures cuts the page being left from the
27077
- * bottom, progressively. It does end up at the arriving height, and that is
27078
- * right; what must not happen is the user watching it get there.
27079
- * - **where the page being left WAS**, which is not where the window stands:
27080
- * the two states are at the same place in the layout without being at the
27081
- * same place in the window — one page is scrolled and the other is not.
27082
- * Drawn at the window's own corner, the page being left would be seen
27083
- * jumping to its top before it begins to leave.
27191
+ * - **the box is the two states TOGETHER**, not either one of them. The
27192
+ * browser puts the group where the arriving state stands and animates it
27193
+ * from where the state being left stood; both are wrong for the same reason
27194
+ * — a window that is one of the two states cuts the other. Held at the
27195
+ * rectangle that contains both, nothing either picture holds is ever cut by
27196
+ * a box it never had, and the user never watches the window get anywhere.
27197
+ * - **where each state WAS**, which is not where the window stands: the two
27198
+ * states are at the same place in the layout without being at the same place
27199
+ * in the window — one page is scrolled and the other is not, one has a top
27200
+ * bar over it and the other has the screen. Each picture is placed at its
27201
+ * own corner inside the window.
27202
+ * - **the band the state being left kept free**, next to the one the arriving
27203
+ * state keeps free. A band is furniture — a fixed bar, a sticky row — and
27204
+ * the pictures are cut at it so they are not watched painting over it. Read
27205
+ * live it describes the arriving state alone, and a piece of furniture that
27206
+ * belongs to only ONE of the two states is then treated as the frame of
27207
+ * both: the page being left is cut at a bar it never had. Photographed here
27208
+ * at the one moment the state being left still exists, the cut can be taken
27209
+ * at what is furniture on BOTH sides (see the clip formulas).
27084
27210
  *
27085
27211
  * Only the measuring needs JS, and only for the one moment both states exist:
27086
27212
  * the page arriving is in the DOM and the transition has not started playing.
@@ -27107,8 +27233,32 @@ const releaseTransitionDestination = (owner) => {
27107
27233
  * Each one is a distance inward from the band the safe area already leaves
27108
27234
  * free, so a row states its own height and nothing else: what the bars above
27109
27235
  * it take is already counted.
27236
+ *
27237
+ * Registered as lengths for the same reason the safe area is (safe_area.js):
27238
+ * the band has to be readable in pixels at the moment a page leaves.
27110
27239
  */
27111
27240
  const TRANSITION_WINDOW_CSS = /* css */ `
27241
+ @property --navi-transition-cover-top {
27242
+ syntax: "<length>";
27243
+ inherits: true;
27244
+ initial-value: 0px;
27245
+ }
27246
+ @property --navi-transition-cover-right {
27247
+ syntax: "<length>";
27248
+ inherits: true;
27249
+ initial-value: 0px;
27250
+ }
27251
+ @property --navi-transition-cover-bottom {
27252
+ syntax: "<length>";
27253
+ inherits: true;
27254
+ initial-value: 0px;
27255
+ }
27256
+ @property --navi-transition-cover-left {
27257
+ syntax: "<length>";
27258
+ inherits: true;
27259
+ initial-value: 0px;
27260
+ }
27261
+
27112
27262
  @layer navi {
27113
27263
  :root {
27114
27264
  --navi-transition-cover-top: 0px;
@@ -27125,6 +27275,12 @@ const WINDOW_WIDTH_PROPERTY = "--navi-transition-window-width";
27125
27275
  const WINDOW_HEIGHT_PROPERTY = "--navi-transition-window-height";
27126
27276
  const WINDOW_OLD_TOP_PROPERTY = "--navi-transition-window-old-top";
27127
27277
  const WINDOW_OLD_LEFT_PROPERTY = "--navi-transition-window-old-left";
27278
+ const WINDOW_NEW_TOP_PROPERTY = "--navi-transition-window-new-top";
27279
+ const WINDOW_NEW_LEFT_PROPERTY = "--navi-transition-window-new-left";
27280
+ const OLD_BAND_TOP_PROPERTY = "--navi-transition-old-band-top";
27281
+ const OLD_BAND_RIGHT_PROPERTY = "--navi-transition-old-band-right";
27282
+ const OLD_BAND_BOTTOM_PROPERTY = "--navi-transition-old-band-bottom";
27283
+ const OLD_BAND_LEFT_PROPERTY = "--navi-transition-old-band-left";
27128
27284
  const WINDOW_PROPERTIES = [
27129
27285
  WINDOW_TOP_PROPERTY,
27130
27286
  WINDOW_LEFT_PROPERTY,
@@ -27132,6 +27288,12 @@ const WINDOW_PROPERTIES = [
27132
27288
  WINDOW_HEIGHT_PROPERTY,
27133
27289
  WINDOW_OLD_TOP_PROPERTY,
27134
27290
  WINDOW_OLD_LEFT_PROPERTY,
27291
+ WINDOW_NEW_TOP_PROPERTY,
27292
+ WINDOW_NEW_LEFT_PROPERTY,
27293
+ OLD_BAND_TOP_PROPERTY,
27294
+ OLD_BAND_RIGHT_PROPERTY,
27295
+ OLD_BAND_BOTTOM_PROPERTY,
27296
+ OLD_BAND_LEFT_PROPERTY,
27135
27297
  ];
27136
27298
 
27137
27299
  // Whose numbers are currently published. The window belongs to the movement
@@ -27140,21 +27302,50 @@ const WINDOW_PROPERTIES = [
27140
27302
  // on.
27141
27303
  let windowOwner = null;
27142
27304
 
27143
- const holdTransitionWindow = (owner, element, rectBefore) => {
27305
+ /**
27306
+ * The state being left, taken while rendering is held — the page arriving is
27307
+ * not in the DOM yet, so this is still the one going away. Where its box
27308
+ * stands and what its furniture left free, which the arriving state answers
27309
+ * for differently and which nothing can be read back from once it is gone.
27310
+ */
27311
+ const measureTransitionWindowState = (element) => {
27312
+ return {
27313
+ rect: element.getBoundingClientRect(),
27314
+ band: readBand(),
27315
+ };
27316
+ };
27317
+
27318
+ const holdTransitionWindow = (owner, element, stateBefore) => {
27319
+ const rectBefore = stateBefore.rect;
27320
+ const bandBefore = stateBefore.band;
27144
27321
  const rectAfter = element.getBoundingClientRect();
27145
- // It cannot be measured from one side alone: a page arriving shorter than
27146
- // the one it replaces would cut the one leaving, a page arriving taller
27147
- // would be cut itself.
27148
- const height =
27149
- rectBefore.height > rectAfter.height ? rectBefore.height : rectAfter.height;
27322
+ const bandAfter = readBand();
27323
+ // The rectangle that contains both states. It cannot be measured from one
27324
+ // side alone: a page arriving shorter than the one it replaces would cut the
27325
+ // one leaving, a page arriving taller would be cut itself, and either one
27326
+ // standing higher up than the other would be cut across.
27327
+ const top = rectBefore.top < rectAfter.top ? rectBefore.top : rectAfter.top;
27328
+ const left =
27329
+ rectBefore.left < rectAfter.left ? rectBefore.left : rectAfter.left;
27330
+ const bottom =
27331
+ rectBefore.bottom > rectAfter.bottom ? rectBefore.bottom : rectAfter.bottom;
27332
+ const right =
27333
+ rectBefore.right > rectAfter.right ? rectBefore.right : rectAfter.right;
27150
27334
  windowOwner = owner;
27151
27335
  const { style } = document.documentElement;
27152
- style.setProperty(WINDOW_TOP_PROPERTY, `${rectAfter.top}px`);
27153
- style.setProperty(WINDOW_LEFT_PROPERTY, `${rectAfter.left}px`);
27154
- style.setProperty(WINDOW_WIDTH_PROPERTY, `${rectAfter.width}px`);
27155
- style.setProperty(WINDOW_HEIGHT_PROPERTY, `${height}px`);
27336
+ style.setProperty(WINDOW_TOP_PROPERTY, `${top}px`);
27337
+ style.setProperty(WINDOW_LEFT_PROPERTY, `${left}px`);
27338
+ style.setProperty(WINDOW_WIDTH_PROPERTY, `${right - left}px`);
27339
+ style.setProperty(WINDOW_HEIGHT_PROPERTY, `${bottom - top}px`);
27156
27340
  style.setProperty(WINDOW_OLD_TOP_PROPERTY, `${rectBefore.top}px`);
27157
27341
  style.setProperty(WINDOW_OLD_LEFT_PROPERTY, `${rectBefore.left}px`);
27342
+ style.setProperty(WINDOW_NEW_TOP_PROPERTY, `${rectAfter.top}px`);
27343
+ style.setProperty(WINDOW_NEW_LEFT_PROPERTY, `${rectAfter.left}px`);
27344
+ const oldBand = bandTheCutMayBeRelaxedTo(bandBefore, bandAfter, rectAfter);
27345
+ style.setProperty(OLD_BAND_TOP_PROPERTY, `${oldBand.top}px`);
27346
+ style.setProperty(OLD_BAND_RIGHT_PROPERTY, `${oldBand.right}px`);
27347
+ style.setProperty(OLD_BAND_BOTTOM_PROPERTY, `${oldBand.bottom}px`);
27348
+ style.setProperty(OLD_BAND_LEFT_PROPERTY, `${oldBand.left}px`);
27158
27349
  };
27159
27350
 
27160
27351
  // The live layout takes the box back. A discontinuity by construction — the
@@ -27172,6 +27363,65 @@ const releaseTransitionWindow = (owner) => {
27172
27363
  }
27173
27364
  };
27174
27365
 
27366
+ // A band nothing can be wider than, published for an edge nothing is known
27367
+ // about: taken as the smaller of the two, it leaves the live band standing on
27368
+ // its own, which is the movement navi played before it knew any of this.
27369
+ const BAND_UNKNOWN = 1e6;
27370
+
27371
+ // What the furniture leaves free on each edge, as a distance inward from that
27372
+ // edge of the window: the app's safe area (everything pinned to the window's
27373
+ // edges) plus what covers the box from inside the document.
27374
+ const readBand = () => {
27375
+ const computedStyle = getComputedStyle(document.documentElement);
27376
+ const readEdge = (edge) => {
27377
+ const safeArea = parseFloat(
27378
+ computedStyle.getPropertyValue(`--navi-safe-area-inset-${edge}`),
27379
+ );
27380
+ const cover = parseFloat(
27381
+ computedStyle.getPropertyValue(`--navi-transition-cover-${edge}`),
27382
+ );
27383
+ // A browser that cannot register a custom property hands back the calc()
27384
+ // it was written as rather than what it computes to (see safe_area.js).
27385
+ if (!Number.isFinite(safeArea) || !Number.isFinite(cover)) {
27386
+ return BAND_UNKNOWN;
27387
+ }
27388
+ return safeArea + cover;
27389
+ };
27390
+ return {
27391
+ top: readEdge("top"),
27392
+ right: readEdge("right"),
27393
+ bottom: readEdge("bottom"),
27394
+ left: readEdge("left"),
27395
+ };
27396
+ };
27397
+
27398
+ // How far the cut may be opened for the picture being left — the band its own
27399
+ // state kept free, which is the whole point of photographing it: furniture
27400
+ // present in only one of the two states is not the frame, it is part of what
27401
+ // changes, and cutting the page being left at a bar it never had shows its
27402
+ // header being sliced instead of leaving.
27403
+ //
27404
+ // Never past what the picture ARRIVING needs, though: the cut is one line for
27405
+ // both pictures, so opening it for one opens it for the other. A box that runs
27406
+ // under the furniture it arrives beside — a page scrolled below a top bar —
27407
+ // would then be watched painting over that bar for the length of the movement,
27408
+ // which is what the cut exists to prevent. On such an edge the arriving band
27409
+ // stands, and the page being left is cut as it always was.
27410
+ const bandTheCutMayBeRelaxedTo = (bandBefore, bandAfter, rectAfter) => {
27411
+ return {
27412
+ top: rectAfter.top < bandAfter.top ? bandAfter.top : bandBefore.top,
27413
+ right:
27414
+ rectAfter.right > window.innerWidth - bandAfter.right
27415
+ ? bandAfter.right
27416
+ : bandBefore.right,
27417
+ bottom:
27418
+ rectAfter.bottom > window.innerHeight - bandAfter.bottom
27419
+ ? bandAfter.bottom
27420
+ : bandBefore.bottom,
27421
+ left: rectAfter.left < bandAfter.left ? bandAfter.left : bandBefore.left,
27422
+ };
27423
+ };
27424
+
27175
27425
  installImportMetaCssBuild(import.meta);/**
27176
27426
  * How two routes move against each other, said one relation at a time —
27177
27427
  * without putting them in a row, and without a box in the tree.
@@ -27302,18 +27552,37 @@ const css$10 = /* css */`
27302
27552
  the document animates, and the document belongs to the application the
27303
27553
  rest of the time. */
27304
27554
  :root[data-navi-route-transition] {
27305
- /* With an area marked, the page AROUND it is not taken as a picture — so
27306
- exactly one of the two names below exists at a time, and the movements
27307
- can be written once for both. It is also what a fixed bar wants: a
27308
- captured element is not painted where it stands and cannot be pointed
27309
- at either, so a bar photographed with the document is dead for the
27310
- length of every transition. Left live, it answers as it always did, and
27311
- nothing shows through where the pages are — the two pictures cover the
27312
- area's rectangle between them at every moment. Anything around that must
27313
- ANIMATE rather than stand still gets a view-transition-name of its own,
27314
- and the browser moves it on the same clock. */
27555
+ /* With an area marked, the page AROUND it is not taken as one picture —
27556
+ so exactly one of the two names below exists at a time, and the
27557
+ movements can be written once for both. It is also the only way the
27558
+ furniture can take part in the movement: photographed with the whole
27559
+ document it is one picture the size of the screen, and a bar can be
27560
+ neither held where it stands nor moved with the page it belongs to
27561
+ inside it. Each bar is photographed on its own instead (see
27562
+ transition_furniture.js), and nothing shows through where the pages are
27563
+ — their two pictures cover the area's rectangle between them at every
27564
+ moment. */
27315
27565
  &[data-navi-route-transition-target="area"] {
27316
27566
  view-transition-name: none;
27567
+
27568
+ /* The pages travel OVER the furniture. Everything else captured while an
27569
+ area is marked is a fixed bar wearing a name of navi's own for the
27570
+ length of the movement (transition_furniture.js): a bar the two states
27571
+ share is one group the browser holds where it stands, and a bar only
27572
+ one of them has stands there too, with no counterpart to move to. Both
27573
+ belong under the pages — that is what lets a page come over a bar that
27574
+ is going away, and a page leaving uncover the bar arriving behind it.
27575
+
27576
+ Ordered here rather than left to the DOM, which decides it otherwise:
27577
+ where an application puts its bars relative to the area is its own
27578
+ business. A name and \`*\` weigh the same, so the two rules are read in
27579
+ the order they are written. */
27580
+ &::view-transition-group(*) {
27581
+ z-index: 0;
27582
+ }
27583
+ &::view-transition-group(navi-route-transition) {
27584
+ z-index: 1;
27585
+ }
27317
27586
  }
27318
27587
 
27319
27588
  &::view-transition-old(root),
@@ -27342,11 +27611,22 @@ const css$10 = /* css */`
27342
27611
  overflow: clip;
27343
27612
  }
27344
27613
  &::view-transition-group(navi-route-transition) {
27345
- /* Held still for the whole transition, at the taller of the two states,
27346
- and standing where the area stands (see transition_window.js). Held by
27347
- dropping the group's animation rather than by winning against it with
27348
- !important — which also drops its position animation, fine for an area
27349
- that stays in the same place from one page to the next. */
27614
+ /* Held still for the whole transition, at the rectangle that contains
27615
+ both states (see transition_window.js). Held by dropping the group's
27616
+ animation rather than by winning against it with !important. The
27617
+ browser puts the group where the ARRIVING area stands, so it is moved
27618
+ from there back to the window's own corner. */
27619
+ top: calc(
27620
+ var(--navi-transition-window-top) - var(
27621
+ --navi-transition-window-new-top
27622
+ )
27623
+ );
27624
+ left: calc(
27625
+ var(--navi-transition-window-left) - var(
27626
+ --navi-transition-window-new-left
27627
+ )
27628
+ );
27629
+ width: var(--navi-transition-window-width);
27350
27630
  height: var(--navi-transition-window-height);
27351
27631
  animation-name: none;
27352
27632
 
@@ -27365,31 +27645,51 @@ const css$10 = /* css */`
27365
27645
  the pages covers the top of the area exactly as a fixed bar covers the
27366
27646
  top of the screen. Both are read rather than asked for, so one that
27367
27647
  grows, shrinks or unmounts mid-transition is followed without anything
27368
- being told. What the window cannot know is only where it itself stands,
27369
- and that is the measured half. */
27648
+ being told.
27649
+
27650
+ Read live, though, they describe the state ARRIVING and nothing else,
27651
+ so the cut is taken at the smaller of that and the band the state
27652
+ being left kept free (--navi-transition-old-band-*, photographed while
27653
+ both still existed). Furniture standing in BOTH states is the frame:
27654
+ the pages move behind it and are cut at it. Furniture standing in one
27655
+ of them is part of what changes, and cutting the page being left at a
27656
+ bar it never had shows its own header being sliced instead of
27657
+ leaving. */
27370
27658
  --navi-route-transition-clip-top: max(
27371
27659
  0px,
27372
- var(--navi-safe-area-inset-top) +
27373
- var(--navi-transition-cover-top) - var(--navi-transition-window-top)
27660
+ min(
27661
+ var(--navi-safe-area-inset-top) + var(--navi-transition-cover-top),
27662
+ var(--navi-transition-old-band-top)
27663
+ ) - var(--navi-transition-window-top)
27374
27664
  );
27375
27665
  --navi-route-transition-clip-left: max(
27376
27666
  0px,
27377
- var(--navi-safe-area-inset-left) +
27378
- var(--navi-transition-cover-left) - var(--navi-transition-window-left)
27667
+ min(
27668
+ var(--navi-safe-area-inset-left) + var(--navi-transition-cover-left),
27669
+ var(--navi-transition-old-band-left)
27670
+ ) - var(--navi-transition-window-left)
27379
27671
  );
27380
27672
  --navi-route-transition-clip-bottom: max(
27381
27673
  0px,
27382
27674
  var(--navi-transition-window-top) +
27383
27675
  var(--navi-transition-window-height) +
27384
- var(--navi-safe-area-inset-bottom) +
27385
- var(--navi-transition-cover-bottom) - 100dvh
27676
+ min(
27677
+ var(--navi-safe-area-inset-bottom) +
27678
+ var(--navi-transition-cover-bottom),
27679
+ var(--navi-transition-old-band-bottom)
27680
+ ) -
27681
+ 100dvh
27386
27682
  );
27387
27683
  --navi-route-transition-clip-right: max(
27388
27684
  0px,
27389
27685
  var(--navi-transition-window-left) +
27390
27686
  var(--navi-transition-window-width) +
27391
- var(--navi-safe-area-inset-right) +
27392
- var(--navi-transition-cover-right) - 100dvw
27687
+ min(
27688
+ var(--navi-safe-area-inset-right) +
27689
+ var(--navi-transition-cover-right),
27690
+ var(--navi-transition-old-band-right)
27691
+ ) -
27692
+ 100dvw
27393
27693
  );
27394
27694
  clip-path: inset(
27395
27695
  var(--navi-route-transition-clip-top)
@@ -27416,10 +27716,12 @@ const css$10 = /* css */`
27416
27716
  ) - var(--navi-route-transition-clip-bottom)
27417
27717
  );
27418
27718
  }
27719
+ /* Each picture at the corner its own state stood at, which is not the
27720
+ window's: the window contains both states, and a state that is scrolled
27721
+ — or that stands under a bar the other one does not have — is somewhere
27722
+ inside it (see transition_window.js). Offset here rather than by
27723
+ \`translate\`, which the movement itself uses. */
27419
27724
  &::view-transition-old(navi-route-transition) {
27420
- /* Where the area WAS on screen, which is not where the window stands
27421
- (see transition_window.js). Offset here rather than by \`translate\`,
27422
- which the movement itself uses. */
27423
27725
  top: calc(
27424
27726
  var(--navi-transition-window-old-top) - var(
27425
27727
  --navi-transition-window-top
@@ -27431,6 +27733,18 @@ const css$10 = /* css */`
27431
27733
  )
27432
27734
  );
27433
27735
  }
27736
+ &::view-transition-new(navi-route-transition) {
27737
+ top: calc(
27738
+ var(--navi-transition-window-new-top) - var(
27739
+ --navi-transition-window-top
27740
+ )
27741
+ );
27742
+ left: calc(
27743
+ var(--navi-transition-window-new-left) - var(
27744
+ --navi-transition-window-left
27745
+ )
27746
+ );
27747
+ }
27434
27748
 
27435
27749
  /* ------------------------------------------------------------------
27436
27750
  The movements. One of \`root\` and \`navi-route-transition\` exists at a
@@ -28113,12 +28427,17 @@ const beginTransition = ({
28113
28427
  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.`);
28114
28428
  }
28115
28429
  const areaElement = areaElements.length > 0 ? areaElements[0] : null;
28116
- // The area as it stands before anything moves: rendering is held, so this is
28117
- // still the page being left (see holdAreaGeometry).
28118
- let areaRectBefore = null;
28430
+ // The area as it stands before anything moves, and the band the furniture
28431
+ // around it leaves free: rendering is held, so both are still the page being
28432
+ // left (see transition_window.js).
28433
+ let areaStateBefore = null;
28119
28434
  if (areaElement) {
28120
28435
  documentElement.setAttribute(TRANSITION_TARGET_ATTRIBUTE, "area");
28121
- areaRectBefore = areaElement.getBoundingClientRect();
28436
+ // Said before the picture is taken, like every name (see
28437
+ // transition_furniture.js): what the bars are wearing when the transition
28438
+ // starts is what the browser photographs.
28439
+ nameTransitionFurniture(transition, areaElement);
28440
+ areaStateBefore = measureTransitionWindowState(areaElement);
28122
28441
  }
28123
28442
  // A duration of this relation's own, worn for the length of the transition —
28124
28443
  // and whatever the application had written inline put back afterwards, not
@@ -28188,7 +28507,11 @@ const beginTransition = ({
28188
28507
  // The page arriving is in the DOM and the transition has not started
28189
28508
  // playing: the one moment both states of the area can be known.
28190
28509
  if (areaElement) {
28191
- holdTransitionWindow(transition, areaElement, areaRectBefore);
28510
+ // A bar the arriving state mounted has to wear its name before the
28511
+ // second picture is taken; one that survived the render keeps the name
28512
+ // it already has, which is what pairs its two pictures.
28513
+ nameTransitionFurniture(transition, areaElement);
28514
+ holdTransitionWindow(transition, areaElement, areaStateBefore);
28192
28515
  }
28193
28516
  });
28194
28517
  const end = () => {
@@ -28206,6 +28529,7 @@ const beginTransition = ({
28206
28529
  documentElement.removeAttribute(TRANSITION_TARGET_ATTRIBUTE);
28207
28530
  releaseTransitionWindow(transition);
28208
28531
  releaseTransitionDestination(transition);
28532
+ releaseTransitionFurniture(transition);
28209
28533
  if (restoreDuration) {
28210
28534
  restoreDuration();
28211
28535
  }
@@ -28248,7 +28572,7 @@ const warnAboutNamesEscapingArea = (areaElement, capturedNames) => {
28248
28572
  }
28249
28573
  let escapedName = null;
28250
28574
  for (const name of capturedNames) {
28251
- if (name === "root" || name === AREA_NAME) {
28575
+ if (name === "root" || name === AREA_NAME || name.startsWith(FURNITURE_NAME_PREFIX)) {
28252
28576
  continue;
28253
28577
  }
28254
28578
  escapedName = name;
@@ -28333,16 +28657,37 @@ const pageIsCurrent$1 = ({
28333
28657
  };
28334
28658
  // The FIRST page that answers, and every page read all the same: a page that
28335
28659
  // is not the current one today is the one that must wake the reader tomorrow.
28660
+ //
28661
+ // Two of them answering at once is the one thing this reading cannot get right.
28662
+ // Relations are declared one pair at a time, so the row read here is the order
28663
+ // they happened to be written in — an order nothing in the application shows —
28664
+ // and the movement then played is the one written for whichever page was
28665
+ // mentioned first. A movement is a movement: it looks deliberate, which is why
28666
+ // the overlap is said out loud here, where both pages are known.
28336
28667
  const currentPageIndex$1 = pages => {
28337
28668
  let currentIndex = -1;
28338
28669
  for (let i = 0; i < pages.length; i++) {
28339
28670
  const isCurrent = pageIsCurrent$1(pages[i]);
28340
- if (isCurrent && currentIndex === -1) {
28671
+ if (!isCurrent) {
28672
+ continue;
28673
+ }
28674
+ if (currentIndex === -1) {
28341
28675
  currentIndex = i;
28676
+ continue;
28342
28677
  }
28678
+ warnPagesBothCurrent(pages[currentIndex], pages[i]);
28343
28679
  }
28344
28680
  return currentIndex;
28345
28681
  };
28682
+ const warnPagesBothCurrent = (pageKept, pageIgnored) => {
28683
+ const kept = describePage(pageKept);
28684
+ const ignored = describePage(pageIgnored);
28685
+ warnOnce(`both-current:${kept}|${ignored}`, `${kept} and ${ignored} are both current on "${window.location.pathname}": two relations claim this url. A relation is resolved through which page is current, so the one mentioned first in a defineRouteTransition call wins — ${kept} — and its movement plays whichever of the two the application is really showing. Make one of them decline this url with a param constraint (see navigation.md, "Which values a param accepts").`);
28686
+ };
28687
+ const describePage = ({
28688
+ route,
28689
+ params
28690
+ }) => params ? `${route} with ${JSON.stringify(params)}` : `${route}`;
28346
28691
 
28347
28692
  installImportMetaCssBuild(import.meta);/**
28348
28693
  * Dragging from one route to the next, when the tabs of a page are URLs.
@@ -28480,17 +28825,16 @@ const css$$ = /* css */`
28480
28825
  same page changing its mind. */
28481
28826
  mix-blend-mode: normal;
28482
28827
  }
28828
+ /* Each picture at the corner its own box stood at, which is not the
28829
+ window's: the window contains both boxes, and the two are at the same
28830
+ place in the layout without being at the same place in the window — one
28831
+ page is scrolled and the other is not, so the box being left starts
28832
+ higher up. Left at the window's own corner the page being left would be
28833
+ seen jumping back to its top before it even begins to leave. Offset here
28834
+ rather than by \`translate\`, which the movement itself uses, and at its
28835
+ own size rather than the group's so that nothing is cut off the far side
28836
+ of the shift (see transition_window.js). */
28483
28837
  &::view-transition-old(navi-route-travel) {
28484
- /* Where the page being left WAS on screen, which is not where the group
28485
- stands: the group is at the arriving box (its position animation is
28486
- dropped along with its height one, below), and the two boxes are at the
28487
- same place in the layout without being at the same place in the window
28488
- — one page is scrolled and the other is not, so the box being left
28489
- starts higher up. Left at the group's own corner the page being left
28490
- would be seen jumping back to its top before it even begins to leave.
28491
- Offset here rather than by \`translate\`, which the movement itself uses,
28492
- and at its own size rather than the group's so that nothing is cut off
28493
- the far side of the shift (see transition_window.js). */
28494
28838
  top: calc(
28495
28839
  var(--navi-transition-window-old-top) - var(
28496
28840
  --navi-transition-window-top
@@ -28503,6 +28847,19 @@ const css$$ = /* css */`
28503
28847
  );
28504
28848
  width: auto;
28505
28849
  }
28850
+ &::view-transition-new(navi-route-travel) {
28851
+ top: calc(
28852
+ var(--navi-transition-window-new-top) - var(
28853
+ --navi-transition-window-top
28854
+ )
28855
+ );
28856
+ left: calc(
28857
+ var(--navi-transition-window-new-left) - var(
28858
+ --navi-transition-window-left
28859
+ )
28860
+ );
28861
+ width: auto;
28862
+ }
28506
28863
  /* The pages are cut at the edge of the box they travel in. Said HERE and
28507
28864
  nowhere else: these pictures are drawn in the top layer, so no overflow
28508
28865
  on any element of the document — not the box's own, not a frame around
@@ -28514,17 +28871,28 @@ const css$$ = /* css */`
28514
28871
  }
28515
28872
  &::view-transition-group(navi-route-travel) {
28516
28873
  /* The window the two pictures are seen through, held still for the whole
28517
- travel at the taller of the two boxes (see transition_window.js): the group
28518
- is what CLIPS, and the browser animates its height from the box being
28519
- left to the box arriving — so the window shrinks under the pictures and
28520
- cuts the page leaving from the bottom, progressively. The box does end
28521
- up at the arriving page's height, and that is right; what must not
28522
- happen is the user watching it get there.
28523
-
28524
- The height is held by dropping the group's animation rather than by
28525
- winning against it with !important — which also drops its position
28526
- animation, fine while a travel box stands in the same place from one
28527
- route to the next. */
28874
+ travel at the rectangle that contains both boxes (see
28875
+ transition_window.js): the group is what CLIPS, and the browser
28876
+ animates it from the box being left to the box arriving — so the
28877
+ window moves and shrinks under the pictures, cutting the page leaving
28878
+ from the bottom, progressively. The box does end up at the arriving
28879
+ page's height, and that is right; what must not happen is the user
28880
+ watching it get there.
28881
+
28882
+ Held by dropping the group's animation rather than by winning against
28883
+ it with !important. The browser puts the group where the ARRIVING box
28884
+ stands, so it is moved from there back to the window's own corner. */
28885
+ top: calc(
28886
+ var(--navi-transition-window-top) - var(
28887
+ --navi-transition-window-new-top
28888
+ )
28889
+ );
28890
+ left: calc(
28891
+ var(--navi-transition-window-left) - var(
28892
+ --navi-transition-window-new-left
28893
+ )
28894
+ );
28895
+ width: var(--navi-transition-window-width);
28528
28896
  height: var(--navi-transition-window-height);
28529
28897
 
28530
28898
  /* Cut at what covers the box, on top of being cut at the box. The
@@ -28542,31 +28910,51 @@ const css$$ = /* css */`
28542
28910
  above the pages covers the top of the box exactly as a fixed bar covers
28543
28911
  the top of the screen. Both are read rather than asked for, so one that
28544
28912
  grows, shrinks or unmounts mid-travel is followed without anything
28545
- being told. What the group cannot know is only where it itself stands,
28546
- and that is the measured half. */
28913
+ being told.
28914
+
28915
+ Read live, though, they describe the state ARRIVING and nothing else,
28916
+ so the cut is taken at the smaller of that and the band the state
28917
+ being left kept free (--navi-transition-old-band-*, photographed while
28918
+ both still existed). Furniture standing in BOTH states is the frame:
28919
+ the pages move behind it and are cut at it. Furniture standing in one
28920
+ of them is part of what changes, and cutting the page being left at a
28921
+ bar it never had shows its own header being sliced instead of
28922
+ leaving. */
28547
28923
  --navi-route-travel-clip-top: max(
28548
28924
  0px,
28549
- var(--navi-safe-area-inset-top) +
28550
- var(--navi-transition-cover-top) - var(--navi-transition-window-top)
28925
+ min(
28926
+ var(--navi-safe-area-inset-top) + var(--navi-transition-cover-top),
28927
+ var(--navi-transition-old-band-top)
28928
+ ) - var(--navi-transition-window-top)
28551
28929
  );
28552
28930
  --navi-route-travel-clip-left: max(
28553
28931
  0px,
28554
- var(--navi-safe-area-inset-left) +
28555
- var(--navi-transition-cover-left) - var(--navi-transition-window-left)
28932
+ min(
28933
+ var(--navi-safe-area-inset-left) + var(--navi-transition-cover-left),
28934
+ var(--navi-transition-old-band-left)
28935
+ ) - var(--navi-transition-window-left)
28556
28936
  );
28557
28937
  --navi-route-travel-clip-bottom: max(
28558
28938
  0px,
28559
28939
  var(--navi-transition-window-top) +
28560
28940
  var(--navi-transition-window-height) +
28561
- var(--navi-safe-area-inset-bottom) +
28562
- var(--navi-transition-cover-bottom) - 100dvh
28941
+ min(
28942
+ var(--navi-safe-area-inset-bottom) +
28943
+ var(--navi-transition-cover-bottom),
28944
+ var(--navi-transition-old-band-bottom)
28945
+ ) -
28946
+ 100dvh
28563
28947
  );
28564
28948
  --navi-route-travel-clip-right: max(
28565
28949
  0px,
28566
28950
  var(--navi-transition-window-left) +
28567
28951
  var(--navi-transition-window-width) +
28568
- var(--navi-safe-area-inset-right) +
28569
- var(--navi-transition-cover-right) - 100dvw
28952
+ min(
28953
+ var(--navi-safe-area-inset-right) +
28954
+ var(--navi-transition-cover-right),
28955
+ var(--navi-transition-old-band-right)
28956
+ ) -
28957
+ 100dvw
28570
28958
  );
28571
28959
  clip-path: inset(
28572
28960
  var(--navi-route-travel-clip-top) var(--navi-route-travel-clip-right)
@@ -28903,9 +29291,10 @@ const RouteTravel = ({
28903
29291
  document.documentElement.setAttribute(DRAGGED_ATTRIBUTE, "");
28904
29292
  }
28905
29293
  pageAskedForRef.current = page;
28906
- // The box as it stands before anything moves: rendering is held, so this is
28907
- // still the page being left (see transition_window.js).
28908
- const rectBefore = elementRef.current.getBoundingClientRect();
29294
+ // The box as it stands before anything moves, and the band the furniture
29295
+ // around it leaves free: rendering is held, so both are still the page
29296
+ // being left (see transition_window.js).
29297
+ const stateBefore = measureTransitionWindowState(elementRef.current);
28909
29298
  // The hold a navigation already took, if this travel is the answer to one:
28910
29299
  // taking another would be taking a hold on a page that is holding still.
28911
29300
  const releaseRendering = takeoverRoutingRenderingHold();
@@ -28948,7 +29337,7 @@ const RouteTravel = ({
28948
29337
  // which is what the box is measured through: the two states are at the
28949
29338
  // same place in the layout without being at the same place in the window
28950
29339
  // (see transition_window.js).
28951
- holdTransitionWindow(travel, elementRef.current, rectBefore);
29340
+ holdTransitionWindow(travel, elementRef.current, stateBefore);
28952
29341
  });
28953
29342
  travel.viewTransition = viewTransition;
28954
29343
  if (scrub) {
@@ -38375,6 +38764,9 @@ const selectByTextStrings = (element, range, startText, endText) => {
38375
38764
  installImportMetaCssBuild(import.meta);// https://jsfiddle.net/v5xzJ/4/
38376
38765
  const css$X = /* css */`
38377
38766
  @layer navi {
38767
+ /* Same reason as .navi_icon below: the display here is a starting point,
38768
+ and box.jsx's unlayered [navi-box-flow] attributes have to win over it.
38769
+ The rest is appearance a caller may want back. */
38378
38770
  .navi_text {
38379
38771
  &[data-skeleton] {
38380
38772
  border-radius: 0.2em;
@@ -44646,6 +45038,11 @@ const CheckboxPseudoElements = ["::-navi-loader", "::-navi-checkmark"];
44646
45038
 
44647
45039
  installImportMetaCssBuild(import.meta);const css$M = /* css */`
44648
45040
  @layer navi {
45041
+ /* Layered on purpose, rules included: a Field and its Label are chrome an
45042
+ app restyles wholesale (its own label color, its own spacing), and
45043
+ nothing declared here is structural — a cursor, a dimmed color, and
45044
+ spacing that already has --spacing-with-control as its knob. So an app
45045
+ rule of any weight takes them back, no specificity games. */
44649
45046
  .navi_label {
44650
45047
  --label-required-indicator-color: var(--navi-color-danger, #b42318);
44651
45048
 
@@ -46296,7 +46693,11 @@ installImportMetaCssBuild(import.meta);const css$J = /* css */`
46296
46693
  --button-color-disabled: var(--button-color-readonly);
46297
46694
 
46298
46695
  /* Here to be easy to override */
46299
- display: inline-block; /* So box css can override when wanting to put button inline flex */
46696
+ /* Layered, this one and the three below: display so box.jsx's unlayered
46697
+ [navi-box-flow] can put the button inline-flex, the font and the line
46698
+ so an app's own button typography wins without having to go through
46699
+ the --button-* variables. */
46700
+ display: inline-block;
46300
46701
  font-size: var(--button-font-size);
46301
46702
  font-family: var(--button-font-family);
46302
46703
  /* A form control comes with a line of its own from the browser, and that
@@ -49850,70 +50251,95 @@ const css$G = /* css */`
49850
50251
  controls answer it, an app never writes it. Whoever answers it also
49851
50252
  stops it (see .navi_button_content in button_ui.jsx), so a button
49852
50253
  deeper in — the clear cross in a picker's slot, the Save of a form in
49853
- a popup the member opens — never mistakes itself for the seam. */
49854
-
49855
- /* Members overlap by the width of one border, so along each seam one of the
49856
- two borders covers the other. Whichever member the user is on has to be
49857
- the one on top: it is the one whose border changes color, and the one
49858
- whose focus ring goes all the way around — half a ring, cut by the
49859
- neighbour painted after it, is what this avoids. z-index needs a
49860
- positioned element to mean anything, hence position: relative.
49861
- Deliberately not paired with isolation: isolate — a stacking context
49862
- here would also trap the popup of a picker held in the group, which
49863
- counts on its own band reaching the whole page. What keeps these
49864
- values from escaping is instead that everything they could reach is a
49865
- band above them (see navi_z_indexes.js). */
49866
- > *:hover,
49867
- > *[data-hover] {
49868
- position: relative;
49869
- z-index: var(--navi-z-index-control-hovered);
49870
- }
49871
- /* Three spellings for one thing — the member showing a focus ring. Some
49872
- controls take the focus on their own root (a button); others wrap a real
49873
- input and draw the ring on their frame while the keyboard is held
49874
- somewhere inside (a picker, a spin). The ring is what must not be sliced,
49875
- so the member holding it is raised whether it wears the state itself or
49876
- merely contains it. */
49877
- > *:focus-visible,
49878
- > *[data-focus-visible],
49879
- > *:has([data-focus-visible]) {
49880
- position: relative;
49881
- z-index: var(--navi-z-index-control-focused);
49882
- }
49883
- /* The member holding something open. Neither of the two above covers it:
49884
- the click that opened the popup gives no focus ring, the focus itself
49885
- left for the popup's content, and the pointer is free to travel to a
49886
- neighbour — yet the member keeps the border color its open state gives
49887
- it, and that border is exactly what the neighbour painted after it
49888
- slices. Read as a state, not as a pseudo-class: :active only lasts as
49889
- long as the button is held down, and while it is held :hover is true
49890
- anyway, so it would add nothing here.
49891
-
49892
- :has, for the same reason as focus-visible above — the group member can
49893
- be an enrobage around the control that expands — and reaching a popup
49894
- held inline (a Popover with layer="local" renders inside its member)
49895
- costs nothing: that popup only reads expanded while its own member is,
49896
- which is the member this raises. */
49897
- > *[aria-expanded="true"],
49898
- > *:has([aria-expanded="true"]) {
49899
- position: relative;
49900
- z-index: var(--navi-z-index-control-expanded);
50254
+ a popup the member opens — never mistakes itself for the seam.
50255
+ Both are asked of members only (see just below for what one is): the
50256
+ second one alone being reset by a popup would leave the first one
50257
+ reaching it. */
50258
+
50259
+ /* What the group counts as a member — a child on its line, spelled
50260
+ *:not([navi-out-of-flow]) here and at every seam below. Not every child
50261
+ is one. A popup renders inside its opener's own subtree — a Dialog
50262
+ written next to the Button that opens it lands as a child of the group
50263
+ itself, and brings its backdrop (and, for layer="local", its clip
50264
+ wrapper) along; a callout anchored on a <button> is mounted in that
50265
+ button's parent, which is the group too. All of them are out of flow:
50266
+ none is ever at a seam, and none is between two members either. Counted
50267
+ as members they take a corner meant for a real one — a dialog in the
50268
+ middle of a row comes out square — and, worse, they move the real ones:
50269
+ a row of one button plus the dialog it opens has no :only-child left, so
50270
+ the lone button squares the side it joins nothing on. Read off a marker
50271
+ the elements set themselves (see popover.jsx, dialog.jsx, callout.js)
50272
+ rather than named class by class here: a layout component knowing the
50273
+ private classes of every component that can open something is exactly
50274
+ what the corner claims above exist to avoid. */
50275
+ > *:not([navi-out-of-flow]) {
50276
+ /* Members overlap by the width of one border, so along each seam one of
50277
+ the two borders covers the other. Whichever member the user is on has
50278
+ to be the one on top: it is the one whose border changes color, and
50279
+ the one whose focus ring goes all the way around — half a ring, cut by
50280
+ the neighbour painted after it, is what this avoids. z-index needs a
50281
+ positioned element to mean anything, hence position: relative.
50282
+ Deliberately not paired with isolation: isolate — a stacking context
50283
+ here would also trap the popup of a picker held in the group, which
50284
+ counts on its own band reaching the whole page. What keeps these
50285
+ values from escaping is instead that everything they could reach is a
50286
+ band above them (see navi_z_indexes.js). */
50287
+ &:hover,
50288
+ &[data-hover] {
50289
+ position: relative;
50290
+ z-index: var(--navi-z-index-control-hovered);
50291
+ }
50292
+
50293
+ /* Three spellings for one thing — the member showing a focus ring. Some
50294
+ controls take the focus on their own root (a button); others wrap a
50295
+ real input and draw the ring on their frame while the keyboard is held
50296
+ somewhere inside (a picker, a spin). The ring is what must not be
50297
+ sliced, so the member holding it is raised whether it wears the state
50298
+ itself or merely contains it. */
50299
+ &:focus-visible,
50300
+ &[data-focus-visible],
50301
+ &:has([data-focus-visible]) {
50302
+ position: relative;
50303
+ z-index: var(--navi-z-index-control-focused);
50304
+ }
50305
+
50306
+ /* The member holding something open. Neither of the two above covers it:
50307
+ the click that opened the popup gives no focus ring, the focus itself
50308
+ left for the popup's content, and the pointer is free to travel to a
50309
+ neighbour — yet the member keeps the border color its open state gives
50310
+ it, and that border is exactly what the neighbour painted after it
50311
+ slices. Read as a state, not as a pseudo-class: :active only lasts as
50312
+ long as the button is held down, and while it is held :hover is true
50313
+ anyway, so it would add nothing here.
50314
+
50315
+ :has, for the same reason as focus-visible above — the group member
50316
+ can be an enrobage around the control that expands — and reaching a
50317
+ popup held inline (a Popover with layer="local" renders inside its
50318
+ member) costs nothing: that popup only reads expanded while its own
50319
+ member is, which is the member this raises. A popup that is a child of
50320
+ the group itself wears aria-expanded too, and is not a member:
50321
+ position/z-index here would fight the placement it does its own way. */
50322
+ &[aria-expanded="true"],
50323
+ &:has([aria-expanded="true"]) {
50324
+ position: relative;
50325
+ z-index: var(--navi-z-index-control-expanded);
50326
+ }
49901
50327
  }
49902
50328
 
49903
- /* Horizontal (default): Cumulative margin for border overlap */
50329
+ /* Where two members meet, stated as the relationship itself rather than as
50330
+ positions in the child list: the corner a member loses is the one facing
50331
+ a member, and the child list holds more than members (see above).
50332
+ A group of one — or of one member and the popup it opens — matches
50333
+ neither rule and keeps the radius it has on its own. */
50334
+
50335
+ /* Horizontal (default) */
49904
50336
  &:not([data-vertical]) {
49905
- > *:not(:first-child) {
50337
+ /* A member with a member before it: its left corners are on that seam,
50338
+ and it is the one pulled back so the two borders there become one
50339
+ line — same relationship, so the same rule. */
50340
+ > *:not([navi-out-of-flow]) ~ *:not([navi-out-of-flow]) {
49906
50341
  margin-left: calc(var(--border-width, var(--group-border-width)) * -1);
49907
- }
49908
- > *:first-child:not(:only-child) {
49909
- --x-corner-top-right-radius: 0;
49910
- --x-corner-bottom-right-radius: 0;
49911
50342
 
49912
- border-top-right-radius: 0 !important;
49913
- border-bottom-right-radius: 0 !important;
49914
- }
49915
-
49916
- > *:last-child:not(:only-child) {
49917
50343
  --x-corner-top-left-radius: 0;
49918
50344
  --x-corner-bottom-left-radius: 0;
49919
50345
 
@@ -49921,30 +50347,23 @@ const css$G = /* css */`
49921
50347
  border-bottom-left-radius: 0 !important;
49922
50348
  }
49923
50349
 
49924
- > *:not(:first-child):not(:last-child) {
49925
- --x-corner-top-left-radius: 0;
50350
+ /* A member with a member after it: its right corners are on that seam. */
50351
+ > *:not([navi-out-of-flow]):has(~ *:not([navi-out-of-flow])) {
49926
50352
  --x-corner-top-right-radius: 0;
49927
50353
  --x-corner-bottom-right-radius: 0;
49928
- --x-corner-bottom-left-radius: 0;
49929
50354
 
49930
- border-radius: 0 !important;
50355
+ border-top-right-radius: 0 !important;
50356
+ border-bottom-right-radius: 0 !important;
49931
50357
  }
49932
50358
  }
49933
50359
 
49934
- /* Vertical: Cumulative margin for border overlap */
50360
+ /* Vertical */
49935
50361
  &[data-vertical] {
49936
- > *:not(:first-child) {
50362
+ /* A member with a member above it — see the horizontal block's own
50363
+ comments, this is the same thing turned a quarter. */
50364
+ > *:not([navi-out-of-flow]) ~ *:not([navi-out-of-flow]) {
49937
50365
  margin-top: calc(var(--border-width, var(--group-border-width)) * -1);
49938
- }
49939
- > *:first-child:not(:only-child) {
49940
- --x-corner-bottom-right-radius: 0;
49941
- --x-corner-bottom-left-radius: 0;
49942
50366
 
49943
- border-bottom-right-radius: 0 !important;
49944
- border-bottom-left-radius: 0 !important;
49945
- }
49946
-
49947
- > *:last-child:not(:only-child) {
49948
50367
  --x-corner-top-left-radius: 0;
49949
50368
  --x-corner-top-right-radius: 0;
49950
50369
 
@@ -49952,13 +50371,13 @@ const css$G = /* css */`
49952
50371
  border-top-right-radius: 0 !important;
49953
50372
  }
49954
50373
 
49955
- > *:not(:first-child):not(:last-child) {
49956
- --x-corner-top-left-radius: 0;
49957
- --x-corner-top-right-radius: 0;
50374
+ /* A member with a member below it. */
50375
+ > *:not([navi-out-of-flow]):has(~ *:not([navi-out-of-flow])) {
49958
50376
  --x-corner-bottom-right-radius: 0;
49959
50377
  --x-corner-bottom-left-radius: 0;
49960
50378
 
49961
- border-radius: 0 !important;
50379
+ border-bottom-right-radius: 0 !important;
50380
+ border-bottom-left-radius: 0 !important;
49962
50381
  }
49963
50382
  }
49964
50383
  }
@@ -55974,7 +56393,13 @@ const DialogLocal = props => {
55974
56393
  children: [backdropProps && jsx(Box, {
55975
56394
  ...backdropProps
55976
56395
  }), jsx("div", {
55977
- className: "navi_dialog_clip_wrapper",
56396
+ className: "navi_dialog_clip_wrapper"
56397
+ // Out of flow like the dialog it holds — see the dialog's own
56398
+ // contentProps for what reads the marker.
56399
+ // eslint-disable-next-line react/no-unknown-property
56400
+ ,
56401
+
56402
+ "navi-out-of-flow": "",
55978
56403
  children: jsx(Box, {
55979
56404
  ...contentProps
55980
56405
  })
@@ -56653,6 +57078,9 @@ const useDialogProps = props => {
56653
57078
  // real element we render ourselves.
56654
57079
  Object.assign(backdropProps, {
56655
57080
  "ref": backdropRef,
57081
+ // Out of flow like the popup it belongs to — see the content element's own
57082
+ // note for what the marker is read by.
57083
+ "navi-out-of-flow": "",
56656
57084
  "baseClassName": "navi_dialog_backdrop",
56657
57085
  "aria-hidden": "true",
56658
57086
  // Recomputed fresh on every render from openController.opened (not
@@ -56726,6 +57154,13 @@ const useDialogProps = props => {
56726
57154
  onNaviCommand(e);
56727
57155
  rest.onnavi_command?.(e);
56728
57156
  },
57157
+ // Not a child on the line of whatever holds it: a popup renders inside its
57158
+ // opener's own subtree, so a Dialog written next to the control that opens it
57159
+ // is a child of that control's own parent. A Group frames the children on
57160
+ // its line and this is not one of them — it is in the top layer, or
57161
+ // absolutely placed by its own code — so it says so once, here (group.jsx
57162
+ // reads this attribute to tell a member from anything else on its line).
57163
+ "navi-out-of-flow": "",
56729
57164
  "baseClassName": "navi_dialog",
56730
57165
  "pseudoClasses": DIALOG_PSEUDO_CLASSES,
56731
57166
  // Distinguishes the two renderers for the CSS above (position: fixed
@@ -57436,7 +57871,13 @@ const PopoverCustom = props => {
57436
57871
  children: [backdropProps && jsx(Box, {
57437
57872
  ...backdropProps
57438
57873
  }), jsx("div", {
57439
- className: "navi_popover_clip_wrapper",
57874
+ className: "navi_popover_clip_wrapper"
57875
+ // Out of flow like the popover it holds — see the popover's own
57876
+ // contentProps for what reads the marker.
57877
+ // eslint-disable-next-line react/no-unknown-property
57878
+ ,
57879
+
57880
+ "navi-out-of-flow": "",
57440
57881
  children: jsx(Box, {
57441
57882
  ...contentProps
57442
57883
  })
@@ -58172,6 +58613,9 @@ const usePopoverProps = props => {
58172
58613
  Object.assign(backdropProps, {
58173
58614
  "ref": backdropRef,
58174
58615
  "id": backdropId,
58616
+ // Out of flow like the popup it belongs to — see the content element's own
58617
+ // note for what the marker is read by.
58618
+ "navi-out-of-flow": "",
58175
58619
  "baseClassName": "navi_popover_backdrop",
58176
58620
  "aria-hidden": "true",
58177
58621
  // Recomputed fresh on every render from openController.opened (not a
@@ -58267,6 +58711,13 @@ const usePopoverProps = props => {
58267
58711
  // it contains claim header/footer/body (see box.jsx) — a popup is always a
58268
58712
  // scrolling area, so it says so once, here.
58269
58713
  "overflow": "auto",
58714
+ // Not a child on the line of whatever holds it: a popup renders inside its
58715
+ // opener's own subtree, so a Popover written next to the control that opens it
58716
+ // is a child of that control's own parent. A Group frames the children on
58717
+ // its line and this is not one of them — it is in the top layer, or
58718
+ // absolutely placed by its own code — so it says so once, here (group.jsx
58719
+ // reads this attribute to tell a member from anything else on its line).
58720
+ "navi-out-of-flow": "",
58270
58721
  "baseClassName": "navi_popover",
58271
58722
  "pseudoClasses": POPOVER_PSEUDO_CLASSES,
58272
58723
  "onKeyDown": e => {
@@ -58377,7 +58828,10 @@ const css$C = /* css */`
58377
58828
  opens it (a Picker holds its popup children inside its root), and
58378
58829
  line-height inherits as computed: the control's line is a length
58379
58830
  (--navi-control-line-height), and inherited it would arrive as that
58380
- control's pixels — a 12px caption on an 18px picker's 23px rows. */
58831
+ control's pixels — a 12px caption on an 18px picker's 23px rows.
58832
+ Layered, like the dialog padding below: both are what navi puts there
58833
+ in the absence of anything else, and an app writing its own line or
58834
+ its own popup padding is meant to win. */
58381
58835
  line-height: var(--navi-line-height);
58382
58836
 
58383
58837
  &.navi_popover {