@jsenv/navi 0.29.388 → 0.29.389

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.
@@ -554,7 +554,10 @@ effect(() => {
554
554
  * The active language is read from `languagesSignal` (see lang_signal.js —
555
555
  * combines the browser's own `navigator.languages`, an optional
556
556
  * `setPreferredLanguage()` user override, and an optional
557
- * `setSupportedLanguages()` app-wide allow-list), live on every lookup.
557
+ * `setSupportedLanguages()` app-wide allow-list), live on every lookup. Each
558
+ * key is looked up in those languages in order, then in English, which
559
+ * translates every key: a language shipped for part of the keys only (German
560
+ * has the `time.*` words) never shows a raw key.
558
561
  *
559
562
  * Built-in key namespaces, all overridable — the registrations below are the
560
563
  * exhaustive list, read them to find the exact key to override:
@@ -1474,19 +1477,25 @@ const isNetworkPolicyError = (error) => {
1474
1477
  * is already saying it in words the person understands.
1475
1478
  *
1476
1479
  * navi's own report leaves it alone (action_error_report.js). What is left is
1477
- * the screen displaying it: it does so by throwing the error to a boundary, and
1478
- * `preact/debug` re-emits on `window` every error a boundary caught — on
1479
- * purpose, for React devtools compatibility. Uncancelled, that lands as an
1480
- * uncaught error in the console, over a page calmly explaining why nothing was
1481
- * asked. Cancelling the event is what says it is handled: the browser drops
1480
+ * the error reaching `window` on its own — a run called by hand whose failure
1481
+ * nobody catches, a render throw no boundary took — where it lands as an
1482
+ * uncaught error in the console, about a request the app itself declared would
1483
+ * not leave. Cancelling the event is what says it is handled: the browser drops
1482
1484
  * the console line, and the jsenv supervisor skips prevented events too.
1483
1485
  *
1484
1486
  * Both shapes a failure travels in are covered, since which one it is depends
1485
1487
  * on whether the resource callback happened to be async — the throw reaching
1486
1488
  * `window`, and the rejection nobody caught.
1487
1489
  *
1488
- * Scoped to the policy's own error and to nothing else — every other error a
1489
- * boundary caught, and every other rejection let go, stays exactly as loud as
1490
+ * A boundary displaying it raises no event at all. In dev, `preact/debug` hands
1491
+ * every error a boundary caught to `console.error`, after the boundary took it
1492
+ * and before it rendered anything; it reads no option and no flag, so that line
1493
+ * stays. Muting it from here is not done: wrapping `console.error` rewrites a
1494
+ * global, and wrapping `options._catchError` reaches into preact's private
1495
+ * hooks, both to hide one class of error.
1496
+ *
1497
+ * Scoped to the policy's own error and to nothing else — every other error
1498
+ * thrown at window, and every other rejection let go, stays exactly as loud as
1490
1499
  * it is.
1491
1500
  */
1492
1501
  let networkPolicyErrorSilenced = false;
@@ -1652,8 +1661,8 @@ const markErrorAsStoppingRender = (error) => {
1652
1661
  * An error the network policy produced is never reported: the app declared the
1653
1662
  * state that produced it, the request never left, and there is nothing for a
1654
1663
  * developer to fix. It is data a screen shows, and it stays data whether or not
1655
- * one does (see network_policy.js, which also keeps it out of the browser
1656
- * console).
1664
+ * one does (see network_policy.js, which also cancels it when it reaches
1665
+ * window).
1657
1666
  */
1658
1667
  const errorIsAccountedFor = (error) => {
1659
1668
  return (
@@ -12640,8 +12649,9 @@ defineInteractionDetector({
12640
12649
  name: "surface",
12641
12650
  claims: (type) =>
12642
12651
  type === PAN || type === ZOOM || type === GRAB || type === RELEASE,
12643
- // A press on the surface may be a tap, a hold or the beginning of a pan, and
12644
- // nothing may read it as the first until the pointer has said which.
12652
+ // A press on the surface may be a tap, a hold or the first finger of a pan or
12653
+ // a pinch, and nothing may read it as the first until the pointer has said
12654
+ // which.
12645
12655
  disputesPress: true,
12646
12656
  setup: (element, trigger, { types, readConfig }) => {
12647
12657
  const canPan = types.includes(PAN);
@@ -15763,21 +15773,33 @@ const getActionResultProperties = (action) => {
15763
15773
  return actionResultPropertiesMap.get(action);
15764
15774
  };
15765
15775
 
15766
- // PUT/PATCH results update the UI through the store, and DELETE resets the GET
15767
- // instead of rerunning it, so a GET rerun would only ever cost a request.
15768
- // A POST is the one case the client cannot decide alone: whether a new item
15769
- // belongs to a list depends on filters/pagination the backend owns.
15770
15776
  // Rationale in full, plus when to override: docs/list_refresh.md
15771
15777
  const defaultRerunOn = {
15778
+ // PUT/PATCH results update the UI through the store, and DELETE resets the
15779
+ // GET rather than rerunning it, so a GET rerun would only ever cost a request.
15772
15780
  GET: false,
15781
+ // A POST is the one case the client cannot decide alone: whether a new item
15782
+ // belongs to a list depends on filters/pagination the backend owns.
15773
15783
  GET_MANY: ["POST"],
15784
+ // DELETE is in there, unlike for GET_MANY: an action holds ids and the store
15785
+ // drops the deleted one out of every list holding it, while a list reading
15786
+ // by slices holds places — the row that left takes the ones after it one
15787
+ // rank up, and only the collection knows who fills the last one.
15788
+ GET_RANGE: ["POST", "DELETE"],
15789
+ };
15790
+ // A key a `rerunOn` leaves out keeps the value it would have had without it:
15791
+ // the default for a resource, the value of the resource it is declared on for
15792
+ // a withParams() scope or a relation.
15793
+ const resolveRerunOn = (rerunOn, inheritedRerunOn = defaultRerunOn) => {
15794
+ if (!rerunOn) {
15795
+ return inheritedRerunOn;
15796
+ }
15797
+ return {
15798
+ GET: rerunOn.GET ?? inheritedRerunOn.GET,
15799
+ GET_MANY: rerunOn.GET_MANY ?? inheritedRerunOn.GET_MANY,
15800
+ GET_RANGE: rerunOn.GET_RANGE ?? inheritedRerunOn.GET_RANGE,
15801
+ };
15774
15802
  };
15775
- // What makes a range reader stale (rerunOn.GET_RANGE overrides it). DELETE is
15776
- // in there, unlike for GET_MANY: an action holds ids and the store drops the
15777
- // deleted one out of every list holding it, while a list reading by slices
15778
- // holds places — the row that left takes the ones after it one rank up, and
15779
- // only the collection knows who fills the last one.
15780
- const defaultInvalidateRangeOn = ["POST", "DELETE"];
15781
15803
 
15782
15804
  // This handles ALL resource lifecycle logic (rerun/reset) across all resources
15783
15805
  const createResourceLifecycleManager = () => {
@@ -15787,7 +15809,7 @@ const createResourceLifecycleManager = () => {
15787
15809
 
15788
15810
  const registerResource = (resourceScope, config) => {
15789
15811
  const {
15790
- rerunOn = defaultRerunOn,
15812
+ rerunOn,
15791
15813
  paramScope = null,
15792
15814
  dependencies = [],
15793
15815
  uniqueKeys = [],
@@ -16070,8 +16092,7 @@ const createResourceLifecycleManager = () => {
16070
16092
  if (!isSameResource && !isDependent) {
16071
16093
  continue;
16072
16094
  }
16073
- const invalidateOn = config.rerunOn.GET_RANGE ?? defaultInvalidateRangeOn;
16074
- if (!shouldRerunAfter(invalidateOn, triggerVerb)) {
16095
+ if (!shouldRerunAfter(config.rerunOn.GET_RANGE, triggerVerb)) {
16075
16096
  continue;
16076
16097
  }
16077
16098
  for (const rangeReader of config.rangeReaderSet) {
@@ -16572,7 +16593,7 @@ const resource = (
16572
16593
  addItemSetup,
16573
16594
  createRestAction: createRestActionForRoot,
16574
16595
  paramScope: getParamScope(undefined),
16575
- rerunOn,
16596
+ rerunOn: resolveRerunOn(rerunOn),
16576
16597
  dependencies,
16577
16598
  });
16578
16599
  };
@@ -16636,7 +16657,7 @@ const createResource = (
16636
16657
  * identical parameters, preventing cross-contamination between different parameter sets.
16637
16658
  *
16638
16659
  * @param {Object} params - Parameters to bind to all actions of this resource (required)
16639
- * @param {{ rerunOn?: Object, dependencies?: Object[] }} [options] - reruns of that scope; left out, the scope inherits the resource's
16660
+ * @param {{ rerunOn?: Object, dependencies?: Object[] }} [options] - reruns of that scope; a `rerunOn` key left out keeps the resource's value, and `dependencies` left out are the resource's
16640
16661
  * @returns {Object} A new resource instance with parameter-bound actions and isolated lifecycle
16641
16662
  * @see docs/resource.md — what a scope isolates, and `dependencies`
16642
16663
  *
@@ -16682,7 +16703,7 @@ const createResource = (
16682
16703
  addItemSetup,
16683
16704
  createRestAction: createRestActionWithParams,
16684
16705
  paramScope: resolvedParamScope,
16685
- rerunOn: withParamsRerunOn ?? rerunOn,
16706
+ rerunOn: resolveRerunOn(withParamsRerunOn, rerunOn),
16686
16707
  dependencies: withParamsDeps ?? dependencies,
16687
16708
  });
16688
16709
  };
@@ -16700,14 +16721,15 @@ const createResource = (
16700
16721
  * Callback return contracts:
16701
16722
  * - GET / PUT → the parent object with the relationship nested inside:
16702
16723
  * `async ({ id }) => ({ id, session: { id: 10, token: "abc" } })`; `null` for no relationship
16703
- * - DELETE → the parent id; the property is set to `null`
16724
+ * - DELETE → the parent id, or `{ id }`; the property is set to `null`
16704
16725
  *
16705
16726
  * The backend may also embed the child inline in a parent GET/POST response — the
16706
16727
  * setter on the property upserts the nested object into the child store.
16707
16728
  *
16708
- * Returns the relationship resource: its actions (`USER_SESSION.GET`, `.PUT`,
16709
- * `.DELETE`) write the parent's property. A relation of the child itself is
16710
- * declared on the child resource (`SESSION.one("device", DEVICE)`).
16729
+ * Returns the relationship resource, itself chainable. Its items are the child
16730
+ * resource's own, shared by every parent: `USER_SESSION.one("device", DEVICE)`
16731
+ * adds a reactive `.device` property to every session, as
16732
+ * `SESSION.one("device", DEVICE)` would.
16711
16733
  *
16712
16734
  * @param {string} propertyName - property holding the child on each parent item
16713
16735
  * @param {Object} childResource - the independent resource created by `resource()`
@@ -16790,11 +16812,11 @@ const createResource = (
16790
16812
  const createRestActionForOne = (verb, callback, { onActionComplete }) => {
16791
16813
  const applyResultToValue =
16792
16814
  verb === "DELETE"
16793
- ? (itemId) => {
16794
- const item = store.select(itemId);
16815
+ ? (itemIdOrItemProps) => {
16816
+ const item = store.select(itemIdOrItemProps);
16795
16817
  const childItemId = item[propertyName][childIdKey];
16796
16818
  store.upsert({
16797
- [idKey]: itemId,
16819
+ [idKey]: item[idKey],
16798
16820
  [propertyName]: null,
16799
16821
  });
16800
16822
  return childItemId;
@@ -16822,7 +16844,7 @@ const createResource = (
16822
16844
  if (!isProps(result) && !primitiveCanBeId(result)) {
16823
16845
  throwInvalidResult(
16824
16846
  action.name,
16825
- `an object (that will be used to drop "${name}" resource)`,
16847
+ `the "${name}" id, or { ${idKey} } (the item whose "${propertyName}" becomes null)`,
16826
16848
  result,
16827
16849
  );
16828
16850
  }
@@ -16847,11 +16869,11 @@ const createResource = (
16847
16869
  PUT,
16848
16870
  DELETE,
16849
16871
  },
16850
- store,
16851
- addItemSetup,
16872
+ store: childStore,
16873
+ addItemSetup: childResource.addItemSetup,
16852
16874
  createRestAction: createRestActionForOne,
16853
16875
  paramScope,
16854
- rerunOn: oneRerunOn ?? rerunOn,
16876
+ rerunOn: resolveRerunOn(oneRerunOn, rerunOn),
16855
16877
  dependencies: oneDependencies ?? dependencies,
16856
16878
  });
16857
16879
  };
@@ -16874,6 +16896,9 @@ const createResource = (
16874
16896
  * - DELETE → `[parentId, childId]`
16875
16897
  * - DELETE_MANY → `[parentId, [childId, childId, …]]`
16876
16898
  *
16899
+ * Returns the relationship resource, itself chainable; its items are the child
16900
+ * resource's own, as for `.one()`.
16901
+ *
16877
16902
  * @param {string} propertyName - property holding the child array on each parent item
16878
16903
  * @param {Object} childResource - the independent resource created by `resource()`
16879
16904
  * @param {Object} [restCallbacks] - `{ rerunOn, dependencies, GET, GET_MANY, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
@@ -17109,11 +17134,11 @@ const createResource = (
17109
17134
  DELETE,
17110
17135
  DELETE_MANY,
17111
17136
  },
17112
- store,
17113
- addItemSetup,
17137
+ store: childStore,
17138
+ addItemSetup: childResource.addItemSetup,
17114
17139
  createRestAction: createRestActionForMany,
17115
17140
  paramScope,
17116
- rerunOn: manyRerunOn ?? rerunOn,
17141
+ rerunOn: resolveRerunOn(manyRerunOn, rerunOn),
17117
17142
  dependencies: manyDependencies ?? dependencies,
17118
17143
  });
17119
17144
  };
@@ -17242,7 +17267,7 @@ const createResource = (
17242
17267
  addItemSetup: childAddItemSetup,
17243
17268
  createRestAction: createRestActionForScopedOne,
17244
17269
  paramScope,
17245
- rerunOn: scopedOneRerunOn ?? rerunOn,
17270
+ rerunOn: resolveRerunOn(scopedOneRerunOn, rerunOn),
17246
17271
  dependencies: scopedOneDependencies ?? dependencies,
17247
17272
  });
17248
17273
  };
@@ -17477,7 +17502,7 @@ const createResource = (
17477
17502
  addItemSetup: childAddItemSetup,
17478
17503
  createRestAction: createRestActionForScopedMany,
17479
17504
  paramScope,
17480
- rerunOn: scopedManyRerunOn ?? rerunOn,
17505
+ rerunOn: resolveRerunOn(scopedManyRerunOn, rerunOn),
17481
17506
  dependencies: scopedManyDependencies ?? dependencies,
17482
17507
  });
17483
17508
  // When a scoped child collection is mutated (POST etc.), the parent GET must
@@ -20196,9 +20221,9 @@ const isSizeSpacingKey = (key) => {
20196
20221
  // "vvw"/"vvh" are navi's own: the *visual* viewport, which — unlike vw/dvw —
20197
20222
  // shrinks when the mobile virtual keyboard opens (see layout/responsive.js), so
20198
20223
  // they are what a popup meant to stay clear of the keyboard should use.
20199
- // "appw"/"apph" are the same thing narrowed to the app's own screen: identical
20200
- // to vvw/vvh until the app declares --navi-app-max-width, and a share of that
20201
- // width afterwards. A gap meant to read as "a small margin" must use these —
20224
+ // "appw"/"apph" are the same thing narrowed to the app's own screen: the
20225
+ // visual viewport minus the app's bands (--navi-app-inset-*), so identical to
20226
+ // vvw/vvh until the app declares bands. A gap meant to read as "a small margin" must use these —
20202
20227
  // 3vvw on a 1500px window is a 45px gap around a 600px app.
20203
20228
  // Functions rather than the signals themselves: appw/apph are not a signal to
20204
20229
  // read but a value to compute (a signal, then a CSS var read back). Reading the
@@ -30202,8 +30227,8 @@ const TRANSITION_FURNITURE_CSS = /* css */ `.navi_fixed_bar[data-navi-transition
30202
30227
  }
30203
30228
  `;
30204
30229
 
30205
- // Called from the render of whatever holds the pages, never at module scope:
30206
- // a page that never travels between routes must not carry this sheet, and a
30230
+ // Called as a movement starts over a marked area, never at module scope: a
30231
+ // page that never travels between routes must not carry this sheet, and a
30207
30232
  // build that sees no caller drops the css with the function.
30208
30233
  const installTransitionFurnitureCss = () => {
30209
30234
  import.meta.css = [TRANSITION_FURNITURE_CSS, "@jsenv/navi/src/nav/transition_furniture.js"];
@@ -30666,9 +30691,10 @@ const TRANSITION_WINDOW_CSS = /* css */ `@property --navi-transition-cover-top {
30666
30691
  }
30667
30692
  `;
30668
30693
 
30669
- // Called from the render of whatever needs the window, never at module scope:
30670
- // a page that never travels between routes must not carry this sheet, and a
30671
- // build that sees no caller drops the css with the function.
30694
+ // Called by whatever needs the window — a render, or a movement starting —
30695
+ // never at module scope: a page that never travels between routes must not
30696
+ // carry this sheet, and a build that sees no caller drops the css with the
30697
+ // function.
30672
30698
  const installTransitionWindowCss = () => {
30673
30699
  import.meta.css = [TRANSITION_WINDOW_CSS, "@jsenv/navi/src/nav/transition_window.js"];
30674
30700
  };
@@ -31481,9 +31507,6 @@ const RouteTransitionArea = ({
31481
31507
  children,
31482
31508
  ...rest
31483
31509
  }) => {
31484
- import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
31485
- installTransitionWindowCss();
31486
- installTransitionFurnitureCss();
31487
31510
  const props = {
31488
31511
  ...rest,
31489
31512
  [TRANSITION_AREA_ATTRIBUTE]: ""
@@ -31552,8 +31575,6 @@ const RouteTransitionArea = ({
31552
31575
  * @returns {() => void} remove this relation.
31553
31576
  */
31554
31577
  const defineRouteTransition = (from, to, transition) => {
31555
- import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
31556
- installTransitionWindowCss();
31557
31578
  if (!to) {
31558
31579
  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.`);
31559
31580
  }
@@ -31593,8 +31614,6 @@ const defineRouteTransition = (from, to, transition) => {
31593
31614
  * @returns {() => void} remove this default.
31594
31615
  */
31595
31616
  const defineRouteDefaultTransition = transition => {
31596
- import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
31597
- installTransitionWindowCss();
31598
31617
  const value = normalizeTransition(transition);
31599
31618
  defaultTransition = value;
31600
31619
  return () => {
@@ -32176,6 +32195,10 @@ const beginTransition = ({
32176
32195
  // Said before the picture is taken: whoever names something for a movement
32177
32196
  // between two pages decides on it now (see transition_destination.js).
32178
32197
  holdTransitionDestination(transition, url);
32198
+ // Adopted by what starts a movement rather than by what declares one: a link
32199
+ // or a navTo() asks for a movement in an app that declared none, and an app
32200
+ // that never moves never carries the sheet.
32201
+ import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
32179
32202
  documentElement.setAttribute(TRANSITION_ATTRIBUTE, direction);
32180
32203
  if (type) {
32181
32204
  documentElement.setAttribute(TRANSITION_TYPE_ATTRIBUTE, type);
@@ -32197,6 +32220,10 @@ const beginTransition = ({
32197
32220
  // left (see transition_window.js).
32198
32221
  let areaStateBefore = null;
32199
32222
  if (areaElement) {
32223
+ // The area is the application's element as often as <RouteTransitionArea>,
32224
+ // so what a movement on it plays with is adopted here, where it is found.
32225
+ installTransitionWindowCss();
32226
+ installTransitionFurnitureCss();
32200
32227
  documentElement.setAttribute(TRANSITION_TARGET_ATTRIBUTE, "area");
32201
32228
  // Said before the picture is taken, like every name (see
32202
32229
  // transition_furniture.js): what the bars are wearing when the transition
@@ -45400,16 +45427,6 @@ const css$Z = /* css */`@layer navi {
45400
45427
  border-radius: .2em;
45401
45428
  }
45402
45429
 
45403
- &[data-capitalize] {
45404
- text-transform: capitalize;
45405
-
45406
- & .navi_text_sizer {
45407
- & .navi_text {
45408
- display: inline-block;
45409
- }
45410
- }
45411
- }
45412
-
45413
45430
  &[data-shrinkwrap] {
45414
45431
  display: inline-block;
45415
45432
  }
@@ -45432,12 +45449,6 @@ time.navi_text {
45432
45449
  }
45433
45450
 
45434
45451
  .navi_text {
45435
- &[data-capitalize] {
45436
- &:first-letter, & .navi_text_sizer_placeholder:first-letter, & .navi_text_sizer_overlay:first-letter {
45437
- text-transform: uppercase;
45438
- }
45439
- }
45440
-
45441
45452
  & .navi_text_sizer, & .navi_text_sizer_placeholder, & .navi_text_sizer_overlay {
45442
45453
  display: inherit;
45443
45454
  width: inherit;
@@ -45885,7 +45896,9 @@ const shouldInjectSpacingBetween = (left, right) => {
45885
45896
  * text block from being wider than its content when inside a flex/grid container.
45886
45897
  *
45887
45898
  * @param {boolean} [capitalize]
45888
- * Uppercases the first letter of the text content via CSS.
45899
+ * Uppercases the first letter of the text, and only that one: "lundi 11 mai"
45900
+ * reads "Lundi 11 mai". Applied to the string the text starts with; a text
45901
+ * starting with an element keeps its first letter as written.
45889
45902
  *
45890
45903
  * @param {string|[number,number]|[string,string]} [selectRange]
45891
45904
  * Selects a portion of the text on mount. Pass a substring to search for, a
@@ -46005,11 +46018,13 @@ const TextUI = props => {
46005
46018
  shrinkWrap,
46006
46019
  ...rest
46007
46020
  } = props;
46021
+ if (capitalize) {
46022
+ children = capitalizeFirstLetter(children);
46023
+ }
46008
46024
  const defaultSpace = preventSpaceUnderlines ? FAKE_SPACE : REGULAR_SPACE;
46009
46025
  const resolvedSpacing = spacing ?? defaultSpace;
46010
46026
  const boxProps = {
46011
46027
  "as": "span",
46012
- "data-capitalize": capitalize ? "" : undefined,
46013
46028
  "data-shrinkwrap": shrinkWrap ? "" : undefined,
46014
46029
  ...rest,
46015
46030
  ref,
@@ -46310,6 +46325,28 @@ const Icon = ({
46310
46325
  });
46311
46326
  };
46312
46327
 
46328
+ // On the string rather than with ::first-letter, which needs a block container:
46329
+ // an inline text made one is an atomic box, and the ellipsis of the line
46330
+ // holding it (a Picker's value) can then no longer cut it. The string is also
46331
+ // what every copy of the children (sizer, bold background) renders, so they
46332
+ // all read the same. Leading spaces and punctuation are skipped, and a text
46333
+ // starting with a digit is left as it is, as ::first-letter would.
46334
+ const capitalizeFirstLetter = children => {
46335
+ if (typeof children === "string") {
46336
+ return children.replace(FIRST_LOWERCASE_LETTER_REGEXP, (match, lead, letter) => `${lead}${letter.toLocaleUpperCase()}`);
46337
+ }
46338
+ if (Array.isArray(children) && children.length > 0) {
46339
+ const first = children[0];
46340
+ const firstCapitalized = capitalizeFirstLetter(first);
46341
+ if (firstCapitalized === first) {
46342
+ return children;
46343
+ }
46344
+ return [firstCapitalized, ...children.slice(1)];
46345
+ }
46346
+ return children;
46347
+ };
46348
+ const FIRST_LOWERCASE_LETTER_REGEXP = /^([\s\p{P}]*)(\p{Ll})/u;
46349
+
46313
46350
  /**
46314
46351
  * Where a link leads, asked for when the pointer or the focus arrives on it:
46315
46352
  * the moment the user signals a destination, before the press. Only what the
@@ -61246,7 +61283,6 @@ const createSwipeToClose = (side, { grip } = {}) => {
61246
61283
  panelEl.style.transform = "";
61247
61284
  panelEl.style.clipPath = "";
61248
61285
  panelEl.style.transitionProperty = "";
61249
- panelEl.style.userSelect = "";
61250
61286
  };
61251
61287
  // The resting value is written first and the animation only covers the way
61252
61288
  // to it: both end on the same number, so nothing is seen changing when the
@@ -61290,10 +61326,6 @@ const createSwipeToClose = (side, { grip } = {}) => {
61290
61326
  return false;
61291
61327
  }
61292
61328
  panelEl.style.transitionProperty = "none";
61293
- panelEl.style.userSelect = "none";
61294
- // What the first pixels of the gesture may have started selecting is
61295
- // not a selection, it is the beginning of this travel.
61296
- window.getSelection().removeAllRanges();
61297
61329
  // The way out is the only way there is anything: pulling the other way
61298
61330
  // leans against a wall and comes back.
61299
61331
  return {
@@ -61999,7 +62031,7 @@ const css$E = /* css */`
61999
62031
  --navi-app-width rather than 3vvw): the gap must read as a small
62000
62032
  margin around the dialog, and 3% of a 1500px window is a 45px gap
62001
62033
  around a 600px app. Identical to 3vvw until the app declares
62002
- --navi-app-max-width. */
62034
+ bands (--navi-app-max-width, or --navi-app-inset-* written by it). */
62003
62035
  --x-dialog-container-spacing: calc(0.03 * var(--navi-app-width));
62004
62036
 
62005
62037
  /* --navi-app-width, not --navi-vvw: a top-layer dialog is calibrated on
@@ -62736,8 +62768,8 @@ const css$E = /* css */`
62736
62768
  * `positionArea`: it both caps the dialog's own size (via
62737
62769
  * `--x-dialog-container-spacing`, written from this prop) and offsets a docked
62738
62770
  * one from the edge it docks to. Accepts a number of pixels, a viewport
62739
- * length — "appw"/"apph" being the app's own screen (the visual viewport, or
62740
- * the narrower one the app declared with --navi-app-max-width) and
62771
+ * length — "appw"/"apph" being the app's own screen (the visual viewport
62772
+ * minus the app's bands, see docs/safe_area.md) and
62741
62773
  * "vvw"/"vvh" the visual viewport itself, which shrinks when the mobile
62742
62774
  * keyboard opens — or a container length ("3cqw", the `layer="local"`
62743
62775
  * default). Pass 0 for a dialog meant to sit flush (a side panel).
@@ -65881,6 +65913,8 @@ const css$C = /* css */`@layer navi {
65881
65913
  * `maxWidth` below.
65882
65914
  * @param {boolean} [props.sizeFromAnchor] - **Dialog-only**, same guard.
65883
65915
  * @param {"box"|"scene"} [props.lift] - **Dialog-only**, same guard.
65916
+ * @param {Element|{current: Element}|string} [props.liftAnchor] -
65917
+ * **Dialog-only**, same guard.
65884
65918
  * @param {string} [props.positionArea] - Forwarded as-is — `Dialog` and
65885
65919
  * `Popover` have different own defaults (`"center"` vs. `"bottom"`),
65886
65920
  * deliberately not homogenized here (each reads best for its own typical
@@ -65914,8 +65948,13 @@ const css$C = /* css */`@layer navi {
65914
65948
  * identically): the wash the backdrop paints over what is behind.
65915
65949
  * @param {string} [props.backdropFilter] - Forwarded as-is: what that wash
65916
65950
  * does to the picture underneath, `"blur(4px)"` and the like.
65917
- * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|"expanding"|`slide-from-${string}`|`expand-${string}`} [props.animation]
65918
- * - Forwarded as-is.
65951
+ * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`|"expanding"|`expand-${string}`|"lifting"|{open: boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`, close: "lifting"}} [props.animation]
65952
+ * - Forwarded to whichever renders — see either component's own doc.
65953
+ * `"expanding"`/`"expand-*"` are `Popover`'s own; `"lifting"` and
65954
+ * `{ open, close: "lifting" }` are `Dialog`'s. A popover has no lift, so in
65955
+ * popover mode `"lifting"` plays no movement (what a dialog does with no
65956
+ * anchor to lift out of) and the object form opens with its `open`, which
65957
+ * the close plays backwards like every other kind.
65919
65958
  * @param {string} [props.animationDuration] - Forwarded as-is.
65920
65959
  * @param {string} [props.maxWidth] - Forwarded as-is to both; also read
65921
65960
  * here directly to help decide the automatic `mode` (a fixed length under
@@ -65981,6 +66020,8 @@ const Popup = props => {
65981
66020
  // mode the automatic screen-size resolution happens to pick.
65982
66021
  pressOutside = "close",
65983
66022
  backdrop,
66023
+ // A popover is handed only the values it understands (toPopoverAnimation).
66024
+ animation,
65984
66025
  // Popover-only (see this component's own doc) — destructured out so
65985
66026
  // they're never part of ...rest, and therefore never forwarded to
65986
66027
  // Dialog below, where they'd otherwise leak onto the real <dialog>
@@ -65995,6 +66036,7 @@ const Popup = props => {
65995
66036
  dockedOnSmallTouchScreen,
65996
66037
  sizeFromAnchor,
65997
66038
  lift,
66039
+ liftAnchor,
65998
66040
  ...rest
65999
66041
  } = props;
66000
66042
  const [mode] = useResolvedPopupMode(modeProp, maxWidth, {
@@ -66014,6 +66056,8 @@ const Popup = props => {
66014
66056
  dockedOnSmallTouchScreen: dockedOnSmallTouchScreen,
66015
66057
  sizeFromAnchor: sizeFromAnchor,
66016
66058
  lift: lift,
66059
+ liftAnchor: liftAnchor,
66060
+ animation: animation,
66017
66061
  maxWidth: maxWidth,
66018
66062
  pressOutside: pressOutside,
66019
66063
  backdrop: backdrop,
@@ -66027,6 +66071,7 @@ const Popup = props => {
66027
66071
  }
66028
66072
  return jsx(Popover, {
66029
66073
  ...rest,
66074
+ animation: toPopoverAnimation(animation),
66030
66075
  maxWidth: maxWidth,
66031
66076
  pressOutside: pressOutside,
66032
66077
  backdrop: backdrop,
@@ -66044,6 +66089,18 @@ const Popup = props => {
66044
66089
  };
66045
66090
  Popup.Close = PopupClose;
66046
66091
 
66092
+ // What Dialog's own animation values mean to a popover, which has no lift
66093
+ // (see the `animation` doc above).
66094
+ const toPopoverAnimation = animation => {
66095
+ if (animation === "lifting") {
66096
+ return undefined;
66097
+ }
66098
+ if (animation !== null && typeof animation === "object") {
66099
+ return animation.open;
66100
+ }
66101
+ return animation;
66102
+ };
66103
+
66047
66104
  installImportMetaCssBuild(import.meta);
66048
66105
  const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
66049
66106
  display: none;
@@ -67368,9 +67425,14 @@ const PickerNaviMinute = props => {
67368
67425
  step,
67369
67426
  value
67370
67427
  } = props;
67428
+
67429
+ // resolveInputProps has already run (PickerFirstResolver), so the type
67430
+ // handed on is one the picker renders as is — the text field a navi numeric
67431
+ // type resolves to, told what its value is.
67371
67432
  return jsx(Next, {
67372
67433
  ...props,
67373
- type: "minute",
67434
+ type: "text",
67435
+ "navi-input-type": "minute",
67374
67436
  children: jsxs(Box, {
67375
67437
  flex: "y",
67376
67438
  spacing: "s",
@@ -76525,7 +76587,7 @@ const PickerFirstResolver = props => {
76525
76587
  * `focusOnOpen` below.
76526
76588
  *
76527
76589
  * @type {import("ignore:preact").FunctionComponent<{
76528
- * type?: "date" | "month" | "week" | "time" | "datetime" | "duration" | "color" | "file" | "text" | "object" | "array" | "confirm" | "navi_time" | "navi_number" | "navi_percentage",
76590
+ * type?: "date" | "month" | "week" | "time" | "datetime" | "duration" | "color" | "file" | "text" | "object" | "array" | "confirm" | "navi_time" | "navi_minute" | "navi_number" | "navi_percentage",
76529
76591
  * value?: any,
76530
76592
  * defaultValue?: any,
76531
76593
  * name?: string,
@@ -88548,6 +88610,8 @@ const css$3 = /* css */`@layer navi {
88548
88610
  }
88549
88611
 
88550
88612
  .navi_card_layout {
88613
+ box-sizing: border-box;
88614
+ min-height: 100%;
88551
88615
  padding-top: var(--layout-margin-top, var(--layout-margin-y, var(--layout-margin)));
88552
88616
  padding-right: var(--layout-margin-right, var(--layout-margin-x, var(--layout-margin)));
88553
88617
  padding-bottom: var(--layout-margin-bottom, var(--layout-margin-y, var(--layout-margin)));
@@ -88589,6 +88653,11 @@ const CardLayoutStyleCSSVars = {
88589
88653
  minHeight: "--layout-min-height"
88590
88654
  };
88591
88655
  /**
88656
+ * The card is centered in the area both ways and is as wide as its content,
88657
+ * `minWidth` at least. Down the area, the layout takes at least the height of
88658
+ * what holds it, so how tall that area is is up to the page around it, as for
88659
+ * `ViewportLayout`.
88660
+ *
88592
88661
  * @type {import("ignore:preact").FunctionComponent<{
88593
88662
  * alignX?: string,
88594
88663
  * alignY?: string,
@@ -88618,6 +88687,11 @@ const CardLayout = ({
88618
88687
  }) => {
88619
88688
  import.meta.css = [css$3, "@jsenv/navi/src/layout/card_layout.jsx"];
88620
88689
  return jsx(Box, {
88690
+ flex: true
88691
+ // The card's place in the area; alignX/alignY below are its content's.
88692
+ ,
88693
+ alignX: "center",
88694
+ alignY: "center",
88621
88695
  baseClassName: "navi_card_layout",
88622
88696
  styleCSSVars: CardLayoutStyleCSSVars,
88623
88697
  visualSelector: ".navi_card",