@jsenv/navi 0.29.235 → 0.29.238

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.
@@ -6034,6 +6034,7 @@ installImportMetaCssBuild(import.meta);
6034
6034
  let calloutCount = 0;
6035
6035
 
6036
6036
  const css$13 = /* css */ `
6037
+ /* jsenv-css-opaque: same as dialog.jsx, cascade order. */
6037
6038
  @layer navi {
6038
6039
  .navi_callout {
6039
6040
  /* A callout is parented to what it explains, so it inherits from it — and
@@ -6983,7 +6984,17 @@ const openCallout = (
6983
6984
  `append callout into ${getElementSignature(calloutContainer)}`,
6984
6985
  );
6985
6986
  }
6987
+ let checkCalloutInsertionImpact;
6988
+ {
6989
+ checkCalloutInsertionImpact = startCalloutInsertionImpactCheck(
6990
+ calloutContainer,
6991
+ anchorElement,
6992
+ );
6993
+ }
6986
6994
  calloutContainer.appendChild(calloutElement);
6995
+ if (checkCalloutInsertionImpact) {
6996
+ checkCalloutInsertionImpact();
6997
+ }
6987
6998
  calloutElement.showPopover();
6988
6999
  addTeardown(() => {
6989
7000
  calloutElement.remove();
@@ -7549,6 +7560,54 @@ const canContainCallout = (element) => {
7549
7560
  return true;
7550
7561
  };
7551
7562
 
7563
+ // A callout is a popover with no layout box of its own, so putting it in the
7564
+ // tree must move nothing. Anything that does move means an app rule counting
7565
+ // element children — :last-child, :nth-child, :only-child, :empty, > * + * —
7566
+ // took the callout for one of them and changed subject. Invisible from the
7567
+ // app, where the rule and the markup both read fine.
7568
+ // Two boxes rather than every child of the container: a callout on a list row
7569
+ // would otherwise measure the whole list on each press.
7570
+ const startCalloutInsertionImpactCheck = (calloutContainer, anchorElement) => {
7571
+ if (!anchorElement) {
7572
+ return null;
7573
+ }
7574
+ const anchorBoxBefore = anchorElement.getBoundingClientRect();
7575
+ const containerBoxBefore = calloutContainer.getBoundingClientRect();
7576
+ return () => {
7577
+ const anchorBoxAfter = anchorElement.getBoundingClientRect();
7578
+ const containerBoxAfter = calloutContainer.getBoundingClientRect();
7579
+ let what;
7580
+ let boxBefore;
7581
+ let boxAfter;
7582
+ if (!isSameBox(anchorBoxBefore, anchorBoxAfter)) {
7583
+ what = "moved it";
7584
+ boxBefore = anchorBoxBefore;
7585
+ boxAfter = anchorBoxAfter;
7586
+ } else if (!isSameBox(containerBoxBefore, containerBoxAfter)) {
7587
+ what = `moved ${getElementSignature(calloutContainer)} around it`;
7588
+ boxBefore = containerBoxBefore;
7589
+ boxAfter = containerBoxAfter;
7590
+ } else {
7591
+ return;
7592
+ }
7593
+ console.warn(
7594
+ `opening a callout on ${getElementSignature(anchorElement)} ${what}: ${formatBox(boxBefore)} -> ${formatBox(boxAfter)}.
7595
+ A callout draws nothing in the flow, but while it is open it is an element child of ${getElementSignature(calloutContainer)}: a rule counting children there (:last-child, :nth-child, :only-child, :empty, > * + *) counts it too and matches something else than what it aims at.
7596
+ Say which children the rule counts: :nth-last-child(1 of S) / :nth-child(An+B of S), where S names yours (a class, or :not([navi-out-of-flow]) — the marker navi's out-of-flow elements carry). A sibling rule takes the marker between its compounds: > *:not([navi-out-of-flow]) + *:not([navi-out-of-flow]).
7597
+ See @jsenv/navi docs/css_architecture.md.`,
7598
+ );
7599
+ };
7600
+ };
7601
+ const isSameBox = (a, b) => {
7602
+ return (
7603
+ a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height
7604
+ );
7605
+ };
7606
+ const formatBox = ({ x, y, width, height }) => {
7607
+ const round = (value) => Math.round(value * 100) / 100;
7608
+ return `${round(width)}x${round(height)} at ${round(x)},${round(y)}`;
7609
+ };
7610
+
7552
7611
  const escapeHtml = (string) => {
7553
7612
  return string
7554
7613
  .replace(/&/g, "&")
@@ -18949,6 +19008,20 @@ const resolveSpacingSize = (size, element, property = "padding") => {
18949
19008
  }
18950
19009
  return normalizeStyle(SIZE_MAP[size] || size, property, "js", element);
18951
19010
  };
19011
+ // Whether a spacing is nothing at all, answered without resolving it. A
19012
+ // viewport or container length resolves through a computed-style read, and
19013
+ // asked during a render that read forces the browser to compute the style of
19014
+ // a tree still being built, stylesheets still being adopted. Zero is a fact
19015
+ // about the number written, whatever the unit; a scale keyword is never zero.
19016
+ const isZeroSpacingSize = (size) => {
19017
+ if (typeof size === "number") {
19018
+ return size === 0;
19019
+ }
19020
+ if (typeof size !== "string") {
19021
+ return false;
19022
+ }
19023
+ return parseFloat(size) === 0;
19024
+ };
18952
19025
 
18953
19026
  const COLOR_KEYWORD_MAP = {
18954
19027
  primary: "var(--navi-color-primary)",
@@ -26130,6 +26203,47 @@ const resolveEffectiveDocumentState = (
26130
26203
  return { ...state, ...sharedState };
26131
26204
  };
26132
26205
 
26206
+ // Preact's own useId() (see preact/hooks) returns "P<mask0>-<mask1>", where the
26207
+ // mask is derived from render order within the nearest root/async boundary:
26208
+ // stable across re-renders of the same mount, but not across two mounts on the
26209
+ // same page, and not across two documents.
26210
+ const PREACT_GENERATED_ID_REGEX = /^P\d+-\d+/;
26211
+ const isLikelyPreactGeneratedId = (id) => {
26212
+ return PREACT_GENERATED_ID_REGEX.test(id);
26213
+ };
26214
+
26215
+ /**
26216
+ * A history entry outlives the document that wrote it: `history.state` comes
26217
+ * back as it was left on a reload, and on a back/forward into a document that
26218
+ * has been unloaded since.
26219
+ *
26220
+ * A key named by a generated id names a position in the render order of the
26221
+ * document that wrote it, and nothing else. Read back in another document the
26222
+ * same key names whatever now renders at that position — so an open popup left
26223
+ * behind by the previous document opens a component nobody ever touched (see
26224
+ * `useNavState`, whose key presence IS the open state). Only a caller-given id
26225
+ * means the same thing on both sides of a load.
26226
+ *
26227
+ * Returns the state untouched when it holds no such key, so the caller can tell
26228
+ * whether the entry needs rewriting.
26229
+ */
26230
+ const dropGeneratedIdKeys = (state) => {
26231
+ if (!state) {
26232
+ return state;
26233
+ }
26234
+ let stateWithoutGeneratedIdKeys = state;
26235
+ for (const key of Object.keys(state)) {
26236
+ if (!isLikelyPreactGeneratedId(key)) {
26237
+ continue;
26238
+ }
26239
+ if (stateWithoutGeneratedIdKeys === state) {
26240
+ stateWithoutGeneratedIdKeys = { ...state };
26241
+ }
26242
+ delete stateWithoutGeneratedIdKeys[key];
26243
+ }
26244
+ return stateWithoutGeneratedIdKeys;
26245
+ };
26246
+
26133
26247
  /**
26134
26248
  * The document's rendering, held for the one frame a view transition needs.
26135
26249
  *
@@ -26986,7 +27100,14 @@ const setupBrowserIntegrationViaHistory = ({
26986
27100
 
26987
27101
  const init = () => {
26988
27102
  const url = window.location.href;
26989
- const state = history.state;
27103
+ const stateOnEntry = window.history.state;
27104
+ const state = dropGeneratedIdKeys(stateOnEntry);
27105
+ if (state !== stateOnEntry) {
27106
+ // The entry itself has to lose them too, not just the document state:
27107
+ // getDocumentState() reads the entry back, and every state write copies
27108
+ // what it finds there onto the next one.
27109
+ window.history.replaceState(state, null, url);
27110
+ }
26990
27111
  handleRoutingTask(url, {
26991
27112
  reason: "routing initialization",
26992
27113
  navigationType: "load",
@@ -27220,18 +27341,6 @@ Consider using unique IDs for each component instance.`,
27220
27341
  return useNavStateBasic(id, options);
27221
27342
  };
27222
27343
 
27223
- // Preact's own useId() (see preact/hooks) returns "P<mask0>-<mask1>", where
27224
- // the mask is derived from render order within the nearest root/async
27225
- // boundary — stable across re-renders of the *same* mount, but not across a
27226
- // reload (render order can differ) or even across two mounts on the same
27227
- // page (two components hitting useId() in the same relative order get the
27228
- // same string). Storing one of these under type: "push" bakes it into a
27229
- // history entry: reload the page and the entry's key may now belong to a
27230
- // completely different component (or none), silently auto-opening whatever
27231
- // happens to render at that same position instead.
27232
- const PREACT_GENERATED_ID_REGEX = /^P\d+-\d+/;
27233
- const isLikelyPreactGeneratedId = (id) => PREACT_GENERATED_ID_REGEX.test(id);
27234
-
27235
27344
  const NO_OP = () => {};
27236
27345
  const NO_ID_GIVEN = [undefined, NO_OP, NO_OP];
27237
27346
  const useNavStateBasic = (
@@ -27263,6 +27372,10 @@ const useNavStateBasic = (
27263
27372
  }
27264
27373
 
27265
27374
  let effectiveType = type;
27375
+ // A push bakes the key into a history entry of its own, and an id naming a
27376
+ // render position (see isLikelyPreactGeneratedId) can be claimed by two
27377
+ // mounts of this same page. A replace keeps the key on the entry the document
27378
+ // is already on, where the id at least names one render.
27266
27379
  if (type === "push" && isLikelyPreactGeneratedId(id)) {
27267
27380
  {
27268
27381
  console.warn(
@@ -39948,47 +40061,35 @@ const LoadingIndicatorFluid = ({
39948
40061
  const [containerWidth, setContainerWidth] = useState(0);
39949
40062
  const [containerHeight, setContainerHeight] = useState(0);
39950
40063
  const [containerRadius, setContainerRadius] = useState(0);
40064
+
40065
+ // Sized by the observer alone, first frame included: an element gets an
40066
+ // observation for the size it has when first observed, delivered after the
40067
+ // frame's own layout and before its paint. Measuring here instead would
40068
+ // force a layout in the middle of the commit, on a tree the commit is still
40069
+ // writing to, and the frame would lay it out again. Inside the callback style
40070
+ // and layout are clean, so the radius read there is a lookup, not a recalc.
39951
40071
  useLayoutEffect(() => {
39952
40072
  const indicatorEl = ref.current;
39953
40073
  if (!indicatorEl) {
39954
40074
  return null;
39955
40075
  }
39956
- const {
39957
- width,
39958
- height
39959
- } = indicatorEl.getBoundingClientRect();
39960
- setContainerWidth(width);
39961
- setContainerHeight(height);
39962
- if (radius === undefined || radius === "inherit") {
39963
- const parentEl = indicatorEl.parentElement;
39964
- // Prefer the inline style (always available, even before layout is computed).
39965
- // Fall back to computed longhands — getComputedStyle shorthand may return ""
39966
- // if the browser hasn't resolved the layout yet.
39967
- const radius = parentEl.style.borderRadius || parentEl.style.borderTopLeftRadius || getComputedStyle(parentEl).borderTopLeftRadius || "0px";
39968
- setContainerRadius(radius);
39969
- }
39970
- let animationFrameId = null;
39971
- // Create a resize observer to detect changes in the container's dimensions
39972
- const resizeObserver = new ResizeObserver(entries => {
39973
- // Use requestAnimationFrame to debounce updates
39974
- if (animationFrameId) {
39975
- cancelAnimationFrame(animationFrameId);
39976
- }
39977
- animationFrameId = requestAnimationFrame(() => {
39978
- const [containerEntry] = entries;
39979
- const {
39980
- width,
39981
- height
39982
- } = containerEntry.contentRect;
39983
- setContainerWidth(width);
39984
- setContainerHeight(height);
39985
- });
40076
+ const resizeObserver = new ResizeObserver(([containerEntry]) => {
40077
+ const {
40078
+ width,
40079
+ height
40080
+ } = containerEntry.contentRect;
40081
+ setContainerWidth(width);
40082
+ setContainerHeight(height);
40083
+ if (radius === undefined || radius === "inherit") {
40084
+ const parentEl = indicatorEl.parentElement;
40085
+ // The inline style says the radius as the caller wrote it; the
40086
+ // computed longhand otherwise, the shorthand computing to "" when the
40087
+ // four corners differ.
40088
+ setContainerRadius(parentEl.style.borderRadius || parentEl.style.borderTopLeftRadius || getComputedStyle(parentEl).borderTopLeftRadius || "0px");
40089
+ }
39986
40090
  });
39987
40091
  resizeObserver.observe(indicatorEl);
39988
40092
  return () => {
39989
- if (animationFrameId) {
39990
- cancelAnimationFrame(animationFrameId);
39991
- }
39992
40093
  resizeObserver.disconnect();
39993
40094
  };
39994
40095
  }, []);
@@ -44330,6 +44431,13 @@ const measureDrawing = ({
44330
44431
  * Measured rather than declared: a bar's size comes from a prop, a theme
44331
44432
  * variable, its own content or the device's notch, and only the used value
44332
44433
  * knows all four.
44434
+ *
44435
+ * Written once per task, after every bar has measured. The variable resizes
44436
+ * an ancestor of the bars — the scroll container takes its padding from it —
44437
+ * so a write landing between two measurements turns the second one into a
44438
+ * style recalc and a layout of the whole page. Sizes are queued, and the queue
44439
+ * lands in a microtask: still before the browser paints, so the content is
44440
+ * never seen under a bar, and after every read of the commit that queued them.
44333
44441
  */
44334
44442
 
44335
44443
  // Several bars can share an edge — during a page transition the outgoing and
@@ -44343,10 +44451,14 @@ const sizeMapByArea = new Map();
44343
44451
  // size again — the ones for the smaller bars, and the second half of a page
44344
44452
  // transition.
44345
44453
  const writtenValueByArea = new Map();
44454
+ // Sizes said and not yet written. One queue for both callers below: a bar
44455
+ // saying its size twice before the write keeps the last word, whoever said it.
44456
+ const pendingSizeMapByArea = new Map();
44457
+ let flushMicrotaskQueued = false;
44458
+ let flushFrame = null;
44346
44459
 
44347
- // A size that must land before the next paint: a render changed the bar, so
44348
- // the room it takes is given back in that same commit and the content is never
44349
- // painted under it.
44460
+ // A size a render produced: a prop, the children or a theme variable changed
44461
+ // the bar, and the room it takes is given back before that commit is painted.
44350
44462
  /**
44351
44463
  * @param {"top"|"bottom"|"left"|"right"} area
44352
44464
  * @param {Element} barElement - Which bar this size belongs to.
@@ -44354,41 +44466,47 @@ const writtenValueByArea = new Map();
44354
44466
  * content.
44355
44467
  */
44356
44468
  const setFixedBarSpace = (area, barElement, size) => {
44357
- dropPendingSize(area, barElement);
44358
- storeSize(area, barElement, size);
44359
- writeSpace(area);
44360
- };
44361
-
44362
- // A size nothing asked for, coming from a ResizeObserver. Writing the variable
44363
- // resizes an ANCESTOR of the bars — the scroll container takes its padding
44364
- // from it — and mutating layout from inside a resize callback is what makes
44365
- // the browser report "ResizeObserver loop completed with undelivered
44366
- // notifications". So the write waits for the frame that resize produced.
44367
- // Queued here rather than deferred by each bar on its own, so the bars sharing
44368
- // an edge resolve to a single write instead of one per bar.
44469
+ queueSize(area, barElement, size);
44470
+ if (flushMicrotaskQueued) {
44471
+ return;
44472
+ }
44473
+ flushMicrotaskQueued = true;
44474
+ queueMicrotask(() => {
44475
+ flushMicrotaskQueued = false;
44476
+ flushPendingSizes();
44477
+ });
44478
+ };
44479
+
44480
+ // A size nothing asked for, coming from a ResizeObserver. Mutating layout from
44481
+ // inside a resize callback is what makes the browser report "ResizeObserver
44482
+ // loop completed with undelivered notifications", so this write waits for the
44483
+ // frame that resize produced.
44369
44484
  /**
44370
44485
  * @param {"top"|"bottom"|"left"|"right"} area
44371
44486
  * @param {Element} barElement - Which bar this size belongs to.
44372
44487
  * @param {number} size - In px.
44373
44488
  */
44374
44489
  const requestFixedBarSpace = (area, barElement, size) => {
44490
+ queueSize(area, barElement, size);
44491
+ if (flushFrame !== null) {
44492
+ return;
44493
+ }
44494
+ flushFrame = requestAnimationFrame(() => {
44495
+ flushFrame = null;
44496
+ flushPendingSizes();
44497
+ });
44498
+ };
44499
+
44500
+ const queueSize = (area, barElement, size) => {
44375
44501
  let pendingSizeMap = pendingSizeMapByArea.get(area);
44376
44502
  if (!pendingSizeMap) {
44377
44503
  pendingSizeMap = new Map();
44378
44504
  pendingSizeMapByArea.set(area, pendingSizeMap);
44379
44505
  }
44380
44506
  pendingSizeMap.set(barElement, size);
44381
- if (flushFrame !== null) {
44382
- return;
44383
- }
44384
- flushFrame = requestAnimationFrame(flushPendingSizes);
44385
44507
  };
44386
44508
 
44387
- const pendingSizeMapByArea = new Map();
44388
- let flushFrame = null;
44389
-
44390
44509
  const flushPendingSizes = () => {
44391
- flushFrame = null;
44392
44510
  for (const [area, pendingSizeMap] of pendingSizeMapByArea) {
44393
44511
  for (const [barElement, size] of pendingSizeMap) {
44394
44512
  storeSize(area, barElement, size);
@@ -44398,20 +44516,6 @@ const flushPendingSizes = () => {
44398
44516
  pendingSizeMapByArea.clear();
44399
44517
  };
44400
44518
 
44401
- // What the bar itself just said wins over what its observer had queued about
44402
- // it: a bar unmounting gives its room back, and a size queued for it before
44403
- // that must not put it back.
44404
- const dropPendingSize = (area, barElement) => {
44405
- const pendingSizeMap = pendingSizeMapByArea.get(area);
44406
- if (!pendingSizeMap) {
44407
- return;
44408
- }
44409
- pendingSizeMap.delete(barElement);
44410
- if (pendingSizeMap.size === 0) {
44411
- pendingSizeMapByArea.delete(area);
44412
- }
44413
- };
44414
-
44415
44519
  const storeSize = (area, barElement, size) => {
44416
44520
  let sizeMap = sizeMapByArea.get(area);
44417
44521
  if (!sizeMap) {
@@ -58569,6 +58673,8 @@ const PopupClose = ({
58569
58673
  installImportMetaCssBuild(import.meta);
58570
58674
  let openLocalDialogCount = 0;
58571
58675
  const css$E = /* css */`
58676
+ /* jsenv-css-opaque: popupCss overlaps these rules, own sheet = cascade
58677
+ order depending on which popup renders first. */
58572
58678
  @layer navi {
58573
58679
  .navi_dialog {
58574
58680
  /* Min gap between the dialog and the edges of its container. Written
@@ -59619,7 +59725,7 @@ const useDialogProps = props => {
59619
59725
  bottom: false,
59620
59726
  left: false
59621
59727
  };
59622
- if (resolveSpacingSize(marginWithContainer) === 0) {
59728
+ if (isZeroSpacingSize(marginWithContainer)) {
59623
59729
  const {
59624
59730
  y,
59625
59731
  x
@@ -60342,6 +60448,8 @@ Dialog.Close = PopupClose;
60342
60448
  installImportMetaCssBuild(import.meta);
60343
60449
  let openLocalPopoverCount = 0;
60344
60450
  const css$D = /* css */`
60451
+ /* jsenv-css-opaque: popupCss overlaps these rules, own sheet = cascade
60452
+ order depending on which popup renders first. */
60345
60453
  @layer navi {
60346
60454
  .navi_popover {
60347
60455
  /* soft: user-configurable preferred max-height. Kept as a *default*