@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.
@@ -527,7 +527,10 @@ effect(() => {
527
527
  * The active language is read from `languagesSignal` (see lang_signal.js —
528
528
  * combines the browser's own `navigator.languages`, an optional
529
529
  * `setPreferredLanguage()` user override, and an optional
530
- * `setSupportedLanguages()` app-wide allow-list), live on every lookup.
530
+ * `setSupportedLanguages()` app-wide allow-list), live on every lookup. Each
531
+ * key is looked up in those languages in order, then in English, which
532
+ * translates every key: a language shipped for part of the keys only (German
533
+ * has the `time.*` words) never shows a raw key.
531
534
  *
532
535
  * Built-in key namespaces, all overridable — the registrations below are the
533
536
  * exhaustive list, read them to find the exact key to override:
@@ -1447,19 +1450,25 @@ const isNetworkPolicyError = (error) => {
1447
1450
  * is already saying it in words the person understands.
1448
1451
  *
1449
1452
  * navi's own report leaves it alone (action_error_report.js). What is left is
1450
- * the screen displaying it: it does so by throwing the error to a boundary, and
1451
- * `preact/debug` re-emits on `window` every error a boundary caught — on
1452
- * purpose, for React devtools compatibility. Uncancelled, that lands as an
1453
- * uncaught error in the console, over a page calmly explaining why nothing was
1454
- * asked. Cancelling the event is what says it is handled: the browser drops
1453
+ * the error reaching `window` on its own — a run called by hand whose failure
1454
+ * nobody catches, a render throw no boundary took — where it lands as an
1455
+ * uncaught error in the console, about a request the app itself declared would
1456
+ * not leave. Cancelling the event is what says it is handled: the browser drops
1455
1457
  * the console line, and the jsenv supervisor skips prevented events too.
1456
1458
  *
1457
1459
  * Both shapes a failure travels in are covered, since which one it is depends
1458
1460
  * on whether the resource callback happened to be async — the throw reaching
1459
1461
  * `window`, and the rejection nobody caught.
1460
1462
  *
1461
- * Scoped to the policy's own error and to nothing else — every other error a
1462
- * boundary caught, and every other rejection let go, stays exactly as loud as
1463
+ * A boundary displaying it raises no event at all. In dev, `preact/debug` hands
1464
+ * every error a boundary caught to `console.error`, after the boundary took it
1465
+ * and before it rendered anything; it reads no option and no flag, so that line
1466
+ * stays. Muting it from here is not done: wrapping `console.error` rewrites a
1467
+ * global, and wrapping `options._catchError` reaches into preact's private
1468
+ * hooks, both to hide one class of error.
1469
+ *
1470
+ * Scoped to the policy's own error and to nothing else — every other error
1471
+ * thrown at window, and every other rejection let go, stays exactly as loud as
1463
1472
  * it is.
1464
1473
  */
1465
1474
  let networkPolicyErrorSilenced = false;
@@ -1620,8 +1629,8 @@ const markErrorAsStoppingRender = (error) => {
1620
1629
  * An error the network policy produced is never reported: the app declared the
1621
1630
  * state that produced it, the request never left, and there is nothing for a
1622
1631
  * developer to fix. It is data a screen shows, and it stays data whether or not
1623
- * one does (see network_policy.js, which also keeps it out of the browser
1624
- * console).
1632
+ * one does (see network_policy.js, which also cancels it when it reaches
1633
+ * window).
1625
1634
  */
1626
1635
  const errorIsAccountedFor = (error) => {
1627
1636
  return (
@@ -12162,8 +12171,9 @@ defineInteractionDetector({
12162
12171
  name: "surface",
12163
12172
  claims: (type) =>
12164
12173
  type === PAN || type === ZOOM || type === GRAB || type === RELEASE,
12165
- // A press on the surface may be a tap, a hold or the beginning of a pan, and
12166
- // nothing may read it as the first until the pointer has said which.
12174
+ // A press on the surface may be a tap, a hold or the first finger of a pan or
12175
+ // a pinch, and nothing may read it as the first until the pointer has said
12176
+ // which.
12167
12177
  disputesPress: true,
12168
12178
  setup: (element, trigger, { types, readConfig }) => {
12169
12179
  const canPan = types.includes(PAN);
@@ -15166,21 +15176,33 @@ const getActionResultProperties = (action) => {
15166
15176
  return actionResultPropertiesMap.get(action);
15167
15177
  };
15168
15178
 
15169
- // PUT/PATCH results update the UI through the store, and DELETE resets the GET
15170
- // instead of rerunning it, so a GET rerun would only ever cost a request.
15171
- // A POST is the one case the client cannot decide alone: whether a new item
15172
- // belongs to a list depends on filters/pagination the backend owns.
15173
15179
  // Rationale in full, plus when to override: docs/list_refresh.md
15174
15180
  const defaultRerunOn = {
15181
+ // PUT/PATCH results update the UI through the store, and DELETE resets the
15182
+ // GET rather than rerunning it, so a GET rerun would only ever cost a request.
15175
15183
  GET: false,
15184
+ // A POST is the one case the client cannot decide alone: whether a new item
15185
+ // belongs to a list depends on filters/pagination the backend owns.
15176
15186
  GET_MANY: ["POST"],
15187
+ // DELETE is in there, unlike for GET_MANY: an action holds ids and the store
15188
+ // drops the deleted one out of every list holding it, while a list reading
15189
+ // by slices holds places — the row that left takes the ones after it one
15190
+ // rank up, and only the collection knows who fills the last one.
15191
+ GET_RANGE: ["POST", "DELETE"],
15192
+ };
15193
+ // A key a `rerunOn` leaves out keeps the value it would have had without it:
15194
+ // the default for a resource, the value of the resource it is declared on for
15195
+ // a withParams() scope or a relation.
15196
+ const resolveRerunOn = (rerunOn, inheritedRerunOn = defaultRerunOn) => {
15197
+ if (!rerunOn) {
15198
+ return inheritedRerunOn;
15199
+ }
15200
+ return {
15201
+ GET: rerunOn.GET ?? inheritedRerunOn.GET,
15202
+ GET_MANY: rerunOn.GET_MANY ?? inheritedRerunOn.GET_MANY,
15203
+ GET_RANGE: rerunOn.GET_RANGE ?? inheritedRerunOn.GET_RANGE,
15204
+ };
15177
15205
  };
15178
- // What makes a range reader stale (rerunOn.GET_RANGE overrides it). DELETE is
15179
- // in there, unlike for GET_MANY: an action holds ids and the store drops the
15180
- // deleted one out of every list holding it, while a list reading by slices
15181
- // holds places — the row that left takes the ones after it one rank up, and
15182
- // only the collection knows who fills the last one.
15183
- const defaultInvalidateRangeOn = ["POST", "DELETE"];
15184
15206
 
15185
15207
  // This handles ALL resource lifecycle logic (rerun/reset) across all resources
15186
15208
  const createResourceLifecycleManager = () => {
@@ -15190,7 +15212,7 @@ const createResourceLifecycleManager = () => {
15190
15212
 
15191
15213
  const registerResource = (resourceScope, config) => {
15192
15214
  const {
15193
- rerunOn = defaultRerunOn,
15215
+ rerunOn,
15194
15216
  paramScope = null,
15195
15217
  dependencies = [],
15196
15218
  uniqueKeys = [],
@@ -15473,8 +15495,7 @@ const createResourceLifecycleManager = () => {
15473
15495
  if (!isSameResource && !isDependent) {
15474
15496
  continue;
15475
15497
  }
15476
- const invalidateOn = config.rerunOn.GET_RANGE ?? defaultInvalidateRangeOn;
15477
- if (!shouldRerunAfter(invalidateOn, triggerVerb)) {
15498
+ if (!shouldRerunAfter(config.rerunOn.GET_RANGE, triggerVerb)) {
15478
15499
  continue;
15479
15500
  }
15480
15501
  for (const rangeReader of config.rangeReaderSet) {
@@ -15975,7 +15996,7 @@ const resource = (
15975
15996
  addItemSetup,
15976
15997
  createRestAction: createRestActionForRoot,
15977
15998
  paramScope: getParamScope(undefined),
15978
- rerunOn,
15999
+ rerunOn: resolveRerunOn(rerunOn),
15979
16000
  dependencies,
15980
16001
  });
15981
16002
  };
@@ -16039,7 +16060,7 @@ const createResource = (
16039
16060
  * identical parameters, preventing cross-contamination between different parameter sets.
16040
16061
  *
16041
16062
  * @param {Object} params - Parameters to bind to all actions of this resource (required)
16042
- * @param {{ rerunOn?: Object, dependencies?: Object[] }} [options] - reruns of that scope; left out, the scope inherits the resource's
16063
+ * @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
16043
16064
  * @returns {Object} A new resource instance with parameter-bound actions and isolated lifecycle
16044
16065
  * @see docs/resource.md — what a scope isolates, and `dependencies`
16045
16066
  *
@@ -16085,7 +16106,7 @@ const createResource = (
16085
16106
  addItemSetup,
16086
16107
  createRestAction: createRestActionWithParams,
16087
16108
  paramScope: resolvedParamScope,
16088
- rerunOn: withParamsRerunOn ?? rerunOn,
16109
+ rerunOn: resolveRerunOn(withParamsRerunOn, rerunOn),
16089
16110
  dependencies: withParamsDeps ?? dependencies,
16090
16111
  });
16091
16112
  };
@@ -16103,14 +16124,15 @@ const createResource = (
16103
16124
  * Callback return contracts:
16104
16125
  * - GET / PUT → the parent object with the relationship nested inside:
16105
16126
  * `async ({ id }) => ({ id, session: { id: 10, token: "abc" } })`; `null` for no relationship
16106
- * - DELETE → the parent id; the property is set to `null`
16127
+ * - DELETE → the parent id, or `{ id }`; the property is set to `null`
16107
16128
  *
16108
16129
  * The backend may also embed the child inline in a parent GET/POST response — the
16109
16130
  * setter on the property upserts the nested object into the child store.
16110
16131
  *
16111
- * Returns the relationship resource: its actions (`USER_SESSION.GET`, `.PUT`,
16112
- * `.DELETE`) write the parent's property. A relation of the child itself is
16113
- * declared on the child resource (`SESSION.one("device", DEVICE)`).
16132
+ * Returns the relationship resource, itself chainable. Its items are the child
16133
+ * resource's own, shared by every parent: `USER_SESSION.one("device", DEVICE)`
16134
+ * adds a reactive `.device` property to every session, as
16135
+ * `SESSION.one("device", DEVICE)` would.
16114
16136
  *
16115
16137
  * @param {string} propertyName - property holding the child on each parent item
16116
16138
  * @param {Object} childResource - the independent resource created by `resource()`
@@ -16193,11 +16215,11 @@ const createResource = (
16193
16215
  const createRestActionForOne = (verb, callback, { onActionComplete }) => {
16194
16216
  const applyResultToValue =
16195
16217
  verb === "DELETE"
16196
- ? (itemId) => {
16197
- const item = store.select(itemId);
16218
+ ? (itemIdOrItemProps) => {
16219
+ const item = store.select(itemIdOrItemProps);
16198
16220
  const childItemId = item[propertyName][childIdKey];
16199
16221
  store.upsert({
16200
- [idKey]: itemId,
16222
+ [idKey]: item[idKey],
16201
16223
  [propertyName]: null,
16202
16224
  });
16203
16225
  return childItemId;
@@ -16225,7 +16247,7 @@ const createResource = (
16225
16247
  if (!isProps(result) && !primitiveCanBeId(result)) {
16226
16248
  throwInvalidResult(
16227
16249
  action.name,
16228
- `an object (that will be used to drop "${name}" resource)`,
16250
+ `the "${name}" id, or { ${idKey} } (the item whose "${propertyName}" becomes null)`,
16229
16251
  result,
16230
16252
  );
16231
16253
  }
@@ -16250,11 +16272,11 @@ const createResource = (
16250
16272
  PUT,
16251
16273
  DELETE,
16252
16274
  },
16253
- store,
16254
- addItemSetup,
16275
+ store: childStore,
16276
+ addItemSetup: childResource.addItemSetup,
16255
16277
  createRestAction: createRestActionForOne,
16256
16278
  paramScope,
16257
- rerunOn: oneRerunOn ?? rerunOn,
16279
+ rerunOn: resolveRerunOn(oneRerunOn, rerunOn),
16258
16280
  dependencies: oneDependencies ?? dependencies,
16259
16281
  });
16260
16282
  };
@@ -16277,6 +16299,9 @@ const createResource = (
16277
16299
  * - DELETE → `[parentId, childId]`
16278
16300
  * - DELETE_MANY → `[parentId, [childId, childId, …]]`
16279
16301
  *
16302
+ * Returns the relationship resource, itself chainable; its items are the child
16303
+ * resource's own, as for `.one()`.
16304
+ *
16280
16305
  * @param {string} propertyName - property holding the child array on each parent item
16281
16306
  * @param {Object} childResource - the independent resource created by `resource()`
16282
16307
  * @param {Object} [restCallbacks] - `{ rerunOn, dependencies, GET, GET_MANY, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
@@ -16512,11 +16537,11 @@ const createResource = (
16512
16537
  DELETE,
16513
16538
  DELETE_MANY,
16514
16539
  },
16515
- store,
16516
- addItemSetup,
16540
+ store: childStore,
16541
+ addItemSetup: childResource.addItemSetup,
16517
16542
  createRestAction: createRestActionForMany,
16518
16543
  paramScope,
16519
- rerunOn: manyRerunOn ?? rerunOn,
16544
+ rerunOn: resolveRerunOn(manyRerunOn, rerunOn),
16520
16545
  dependencies: manyDependencies ?? dependencies,
16521
16546
  });
16522
16547
  };
@@ -16645,7 +16670,7 @@ const createResource = (
16645
16670
  addItemSetup: childAddItemSetup,
16646
16671
  createRestAction: createRestActionForScopedOne,
16647
16672
  paramScope,
16648
- rerunOn: scopedOneRerunOn ?? rerunOn,
16673
+ rerunOn: resolveRerunOn(scopedOneRerunOn, rerunOn),
16649
16674
  dependencies: scopedOneDependencies ?? dependencies,
16650
16675
  });
16651
16676
  };
@@ -16880,7 +16905,7 @@ const createResource = (
16880
16905
  addItemSetup: childAddItemSetup,
16881
16906
  createRestAction: createRestActionForScopedMany,
16882
16907
  paramScope,
16883
- rerunOn: scopedManyRerunOn ?? rerunOn,
16908
+ rerunOn: resolveRerunOn(scopedManyRerunOn, rerunOn),
16884
16909
  dependencies: scopedManyDependencies ?? dependencies,
16885
16910
  });
16886
16911
  // When a scoped child collection is mutated (POST etc.), the parent GET must
@@ -19592,9 +19617,9 @@ const isSizeSpacingKey = (key) => {
19592
19617
  // "vvw"/"vvh" are navi's own: the *visual* viewport, which — unlike vw/dvw —
19593
19618
  // shrinks when the mobile virtual keyboard opens (see layout/responsive.js), so
19594
19619
  // they are what a popup meant to stay clear of the keyboard should use.
19595
- // "appw"/"apph" are the same thing narrowed to the app's own screen: identical
19596
- // to vvw/vvh until the app declares --navi-app-max-width, and a share of that
19597
- // width afterwards. A gap meant to read as "a small margin" must use these —
19620
+ // "appw"/"apph" are the same thing narrowed to the app's own screen: the
19621
+ // visual viewport minus the app's bands (--navi-app-inset-*), so identical to
19622
+ // vvw/vvh until the app declares bands. A gap meant to read as "a small margin" must use these —
19598
19623
  // 3vvw on a 1500px window is a 45px gap around a 600px app.
19599
19624
  // Functions rather than the signals themselves: appw/apph are not a signal to
19600
19625
  // read but a value to compute (a signal, then a CSS var read back). Reading the
@@ -29296,8 +29321,8 @@ const TRANSITION_FURNITURE_CSS = /* css */ `.navi_fixed_bar[data-navi-transition
29296
29321
  }
29297
29322
  `;
29298
29323
 
29299
- // Called from the render of whatever holds the pages, never at module scope:
29300
- // a page that never travels between routes must not carry this sheet, and a
29324
+ // Called as a movement starts over a marked area, never at module scope: a
29325
+ // page that never travels between routes must not carry this sheet, and a
29301
29326
  // build that sees no caller drops the css with the function.
29302
29327
  const installTransitionFurnitureCss = () => {
29303
29328
  import.meta.css = [TRANSITION_FURNITURE_CSS, "@jsenv/navi/src/nav/transition_furniture.js"];
@@ -29760,9 +29785,10 @@ const TRANSITION_WINDOW_CSS = /* css */ `@property --navi-transition-cover-top {
29760
29785
  }
29761
29786
  `;
29762
29787
 
29763
- // Called from the render of whatever needs the window, never at module scope:
29764
- // a page that never travels between routes must not carry this sheet, and a
29765
- // build that sees no caller drops the css with the function.
29788
+ // Called by whatever needs the window — a render, or a movement starting —
29789
+ // never at module scope: a page that never travels between routes must not
29790
+ // carry this sheet, and a build that sees no caller drops the css with the
29791
+ // function.
29766
29792
  const installTransitionWindowCss = () => {
29767
29793
  import.meta.css = [TRANSITION_WINDOW_CSS, "@jsenv/navi/src/nav/transition_window.js"];
29768
29794
  };
@@ -30575,9 +30601,6 @@ const RouteTransitionArea = ({
30575
30601
  children,
30576
30602
  ...rest
30577
30603
  }) => {
30578
- import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
30579
- installTransitionWindowCss();
30580
- installTransitionFurnitureCss();
30581
30604
  const props = {
30582
30605
  ...rest,
30583
30606
  [TRANSITION_AREA_ATTRIBUTE]: ""
@@ -30646,8 +30669,6 @@ const RouteTransitionArea = ({
30646
30669
  * @returns {() => void} remove this relation.
30647
30670
  */
30648
30671
  const defineRouteTransition = (from, to, transition) => {
30649
- import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
30650
- installTransitionWindowCss();
30651
30672
  if (!to) {
30652
30673
  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.`);
30653
30674
  }
@@ -30687,8 +30708,6 @@ const defineRouteTransition = (from, to, transition) => {
30687
30708
  * @returns {() => void} remove this default.
30688
30709
  */
30689
30710
  const defineRouteDefaultTransition = transition => {
30690
- import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
30691
- installTransitionWindowCss();
30692
30711
  const value = normalizeTransition(transition);
30693
30712
  defaultTransition = value;
30694
30713
  return () => {
@@ -31270,6 +31289,10 @@ const beginTransition = ({
31270
31289
  // Said before the picture is taken: whoever names something for a movement
31271
31290
  // between two pages decides on it now (see transition_destination.js).
31272
31291
  holdTransitionDestination(transition, url);
31292
+ // Adopted by what starts a movement rather than by what declares one: a link
31293
+ // or a navTo() asks for a movement in an app that declared none, and an app
31294
+ // that never moves never carries the sheet.
31295
+ import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
31273
31296
  documentElement.setAttribute(TRANSITION_ATTRIBUTE, direction);
31274
31297
  if (type) {
31275
31298
  documentElement.setAttribute(TRANSITION_TYPE_ATTRIBUTE, type);
@@ -31291,6 +31314,10 @@ const beginTransition = ({
31291
31314
  // left (see transition_window.js).
31292
31315
  let areaStateBefore = null;
31293
31316
  if (areaElement) {
31317
+ // The area is the application's element as often as <RouteTransitionArea>,
31318
+ // so what a movement on it plays with is adopted here, where it is found.
31319
+ installTransitionWindowCss();
31320
+ installTransitionFurnitureCss();
31294
31321
  documentElement.setAttribute(TRANSITION_TARGET_ATTRIBUTE, "area");
31295
31322
  // Said before the picture is taken, like every name (see
31296
31323
  // transition_furniture.js): what the bars are wearing when the transition
@@ -44279,16 +44306,6 @@ const css$Z = /* css */`@layer navi {
44279
44306
  border-radius: .2em;
44280
44307
  }
44281
44308
 
44282
- &[data-capitalize] {
44283
- text-transform: capitalize;
44284
-
44285
- & .navi_text_sizer {
44286
- & .navi_text {
44287
- display: inline-block;
44288
- }
44289
- }
44290
- }
44291
-
44292
44309
  &[data-shrinkwrap] {
44293
44310
  display: inline-block;
44294
44311
  }
@@ -44311,12 +44328,6 @@ time.navi_text {
44311
44328
  }
44312
44329
 
44313
44330
  .navi_text {
44314
- &[data-capitalize] {
44315
- &:first-letter, & .navi_text_sizer_placeholder:first-letter, & .navi_text_sizer_overlay:first-letter {
44316
- text-transform: uppercase;
44317
- }
44318
- }
44319
-
44320
44331
  & .navi_text_sizer, & .navi_text_sizer_placeholder, & .navi_text_sizer_overlay {
44321
44332
  display: inherit;
44322
44333
  width: inherit;
@@ -44764,7 +44775,9 @@ const shouldInjectSpacingBetween = (left, right) => {
44764
44775
  * text block from being wider than its content when inside a flex/grid container.
44765
44776
  *
44766
44777
  * @param {boolean} [capitalize]
44767
- * Uppercases the first letter of the text content via CSS.
44778
+ * Uppercases the first letter of the text, and only that one: "lundi 11 mai"
44779
+ * reads "Lundi 11 mai". Applied to the string the text starts with; a text
44780
+ * starting with an element keeps its first letter as written.
44768
44781
  *
44769
44782
  * @param {string|[number,number]|[string,string]} [selectRange]
44770
44783
  * Selects a portion of the text on mount. Pass a substring to search for, a
@@ -44884,11 +44897,13 @@ const TextUI = props => {
44884
44897
  shrinkWrap,
44885
44898
  ...rest
44886
44899
  } = props;
44900
+ if (capitalize) {
44901
+ children = capitalizeFirstLetter(children);
44902
+ }
44887
44903
  const defaultSpace = preventSpaceUnderlines ? FAKE_SPACE : REGULAR_SPACE;
44888
44904
  const resolvedSpacing = spacing ?? defaultSpace;
44889
44905
  const boxProps = {
44890
44906
  "as": "span",
44891
- "data-capitalize": capitalize ? "" : undefined,
44892
44907
  "data-shrinkwrap": shrinkWrap ? "" : undefined,
44893
44908
  ...rest,
44894
44909
  ref,
@@ -45189,6 +45204,28 @@ const Icon = ({
45189
45204
  });
45190
45205
  };
45191
45206
 
45207
+ // On the string rather than with ::first-letter, which needs a block container:
45208
+ // an inline text made one is an atomic box, and the ellipsis of the line
45209
+ // holding it (a Picker's value) can then no longer cut it. The string is also
45210
+ // what every copy of the children (sizer, bold background) renders, so they
45211
+ // all read the same. Leading spaces and punctuation are skipped, and a text
45212
+ // starting with a digit is left as it is, as ::first-letter would.
45213
+ const capitalizeFirstLetter = children => {
45214
+ if (typeof children === "string") {
45215
+ return children.replace(FIRST_LOWERCASE_LETTER_REGEXP, (match, lead, letter) => `${lead}${letter.toLocaleUpperCase()}`);
45216
+ }
45217
+ if (Array.isArray(children) && children.length > 0) {
45218
+ const first = children[0];
45219
+ const firstCapitalized = capitalizeFirstLetter(first);
45220
+ if (firstCapitalized === first) {
45221
+ return children;
45222
+ }
45223
+ return [firstCapitalized, ...children.slice(1)];
45224
+ }
45225
+ return children;
45226
+ };
45227
+ const FIRST_LOWERCASE_LETTER_REGEXP = /^([\s\p{P}]*)(\p{Ll})/u;
45228
+
45192
45229
  /**
45193
45230
  * Where a link leads, asked for when the pointer or the focus arrives on it:
45194
45231
  * the moment the user signals a destination, before the press. Only what the
@@ -60033,7 +60070,6 @@ const createSwipeToClose = (side, { grip } = {}) => {
60033
60070
  panelEl.style.transform = "";
60034
60071
  panelEl.style.clipPath = "";
60035
60072
  panelEl.style.transitionProperty = "";
60036
- panelEl.style.userSelect = "";
60037
60073
  };
60038
60074
  // The resting value is written first and the animation only covers the way
60039
60075
  // to it: both end on the same number, so nothing is seen changing when the
@@ -60077,10 +60113,6 @@ const createSwipeToClose = (side, { grip } = {}) => {
60077
60113
  return false;
60078
60114
  }
60079
60115
  panelEl.style.transitionProperty = "none";
60080
- panelEl.style.userSelect = "none";
60081
- // What the first pixels of the gesture may have started selecting is
60082
- // not a selection, it is the beginning of this travel.
60083
- window.getSelection().removeAllRanges();
60084
60116
  // The way out is the only way there is anything: pulling the other way
60085
60117
  // leans against a wall and comes back.
60086
60118
  return {
@@ -60751,7 +60783,7 @@ const css$E = /* css */`
60751
60783
  --navi-app-width rather than 3vvw): the gap must read as a small
60752
60784
  margin around the dialog, and 3% of a 1500px window is a 45px gap
60753
60785
  around a 600px app. Identical to 3vvw until the app declares
60754
- --navi-app-max-width. */
60786
+ bands (--navi-app-max-width, or --navi-app-inset-* written by it). */
60755
60787
  --x-dialog-container-spacing: calc(0.03 * var(--navi-app-width));
60756
60788
 
60757
60789
  /* --navi-app-width, not --navi-vvw: a top-layer dialog is calibrated on
@@ -61488,8 +61520,8 @@ const css$E = /* css */`
61488
61520
  * `positionArea`: it both caps the dialog's own size (via
61489
61521
  * `--x-dialog-container-spacing`, written from this prop) and offsets a docked
61490
61522
  * one from the edge it docks to. Accepts a number of pixels, a viewport
61491
- * length — "appw"/"apph" being the app's own screen (the visual viewport, or
61492
- * the narrower one the app declared with --navi-app-max-width) and
61523
+ * length — "appw"/"apph" being the app's own screen (the visual viewport
61524
+ * minus the app's bands, see docs/safe_area.md) and
61493
61525
  * "vvw"/"vvh" the visual viewport itself, which shrinks when the mobile
61494
61526
  * keyboard opens — or a container length ("3cqw", the `layer="local"`
61495
61527
  * default). Pass 0 for a dialog meant to sit flush (a side panel).
@@ -64542,6 +64574,8 @@ const css$C = /* css */`@layer navi {
64542
64574
  * `maxWidth` below.
64543
64575
  * @param {boolean} [props.sizeFromAnchor] - **Dialog-only**, same guard.
64544
64576
  * @param {"box"|"scene"} [props.lift] - **Dialog-only**, same guard.
64577
+ * @param {Element|{current: Element}|string} [props.liftAnchor] -
64578
+ * **Dialog-only**, same guard.
64545
64579
  * @param {string} [props.positionArea] - Forwarded as-is — `Dialog` and
64546
64580
  * `Popover` have different own defaults (`"center"` vs. `"bottom"`),
64547
64581
  * deliberately not homogenized here (each reads best for its own typical
@@ -64575,8 +64609,13 @@ const css$C = /* css */`@layer navi {
64575
64609
  * identically): the wash the backdrop paints over what is behind.
64576
64610
  * @param {string} [props.backdropFilter] - Forwarded as-is: what that wash
64577
64611
  * does to the picture underneath, `"blur(4px)"` and the like.
64578
- * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|"expanding"|`slide-from-${string}`|`expand-${string}`} [props.animation]
64579
- * - Forwarded as-is.
64612
+ * @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]
64613
+ * - Forwarded to whichever renders — see either component's own doc.
64614
+ * `"expanding"`/`"expand-*"` are `Popover`'s own; `"lifting"` and
64615
+ * `{ open, close: "lifting" }` are `Dialog`'s. A popover has no lift, so in
64616
+ * popover mode `"lifting"` plays no movement (what a dialog does with no
64617
+ * anchor to lift out of) and the object form opens with its `open`, which
64618
+ * the close plays backwards like every other kind.
64580
64619
  * @param {string} [props.animationDuration] - Forwarded as-is.
64581
64620
  * @param {string} [props.maxWidth] - Forwarded as-is to both; also read
64582
64621
  * here directly to help decide the automatic `mode` (a fixed length under
@@ -64642,6 +64681,8 @@ const Popup = props => {
64642
64681
  // mode the automatic screen-size resolution happens to pick.
64643
64682
  pressOutside = "close",
64644
64683
  backdrop,
64684
+ // A popover is handed only the values it understands (toPopoverAnimation).
64685
+ animation,
64645
64686
  // Popover-only (see this component's own doc) — destructured out so
64646
64687
  // they're never part of ...rest, and therefore never forwarded to
64647
64688
  // Dialog below, where they'd otherwise leak onto the real <dialog>
@@ -64656,6 +64697,7 @@ const Popup = props => {
64656
64697
  dockedOnSmallTouchScreen,
64657
64698
  sizeFromAnchor,
64658
64699
  lift,
64700
+ liftAnchor,
64659
64701
  ...rest
64660
64702
  } = props;
64661
64703
  const [mode] = useResolvedPopupMode(modeProp, maxWidth, {
@@ -64675,6 +64717,8 @@ const Popup = props => {
64675
64717
  dockedOnSmallTouchScreen: dockedOnSmallTouchScreen,
64676
64718
  sizeFromAnchor: sizeFromAnchor,
64677
64719
  lift: lift,
64720
+ liftAnchor: liftAnchor,
64721
+ animation: animation,
64678
64722
  maxWidth: maxWidth,
64679
64723
  pressOutside: pressOutside,
64680
64724
  backdrop: backdrop,
@@ -64688,6 +64732,7 @@ const Popup = props => {
64688
64732
  }
64689
64733
  return jsx(Popover, {
64690
64734
  ...rest,
64735
+ animation: toPopoverAnimation(animation),
64691
64736
  maxWidth: maxWidth,
64692
64737
  pressOutside: pressOutside,
64693
64738
  backdrop: backdrop,
@@ -64705,6 +64750,18 @@ const Popup = props => {
64705
64750
  };
64706
64751
  Popup.Close = PopupClose;
64707
64752
 
64753
+ // What Dialog's own animation values mean to a popover, which has no lift
64754
+ // (see the `animation` doc above).
64755
+ const toPopoverAnimation = animation => {
64756
+ if (animation === "lifting") {
64757
+ return undefined;
64758
+ }
64759
+ if (animation !== null && typeof animation === "object") {
64760
+ return animation.open;
64761
+ }
64762
+ return animation;
64763
+ };
64764
+
64708
64765
  installImportMetaCssBuild(import.meta);
64709
64766
  const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
64710
64767
  display: none;
@@ -66015,9 +66072,14 @@ const PickerNaviMinute = props => {
66015
66072
  step,
66016
66073
  value
66017
66074
  } = props;
66075
+
66076
+ // resolveInputProps has already run (PickerFirstResolver), so the type
66077
+ // handed on is one the picker renders as is — the text field a navi numeric
66078
+ // type resolves to, told what its value is.
66018
66079
  return jsx(Next, {
66019
66080
  ...props,
66020
- type: "minute",
66081
+ type: "text",
66082
+ "navi-input-type": "minute",
66021
66083
  children: jsxs(Box, {
66022
66084
  flex: "y",
66023
66085
  spacing: "s",
@@ -86439,6 +86501,8 @@ const css$3 = /* css */`@layer navi {
86439
86501
  }
86440
86502
 
86441
86503
  .navi_card_layout {
86504
+ box-sizing: border-box;
86505
+ min-height: 100%;
86442
86506
  padding-top: var(--layout-margin-top, var(--layout-margin-y, var(--layout-margin)));
86443
86507
  padding-right: var(--layout-margin-right, var(--layout-margin-x, var(--layout-margin)));
86444
86508
  padding-bottom: var(--layout-margin-bottom, var(--layout-margin-y, var(--layout-margin)));
@@ -86480,6 +86544,11 @@ const CardLayoutStyleCSSVars = {
86480
86544
  minHeight: "--layout-min-height"
86481
86545
  };
86482
86546
  /**
86547
+ * The card is centered in the area both ways and is as wide as its content,
86548
+ * `minWidth` at least. Down the area, the layout takes at least the height of
86549
+ * what holds it, so how tall that area is is up to the page around it, as for
86550
+ * `ViewportLayout`.
86551
+ *
86483
86552
  * @type {import("ignore:preact").FunctionComponent<{
86484
86553
  * alignX?: string,
86485
86554
  * alignY?: string,
@@ -86509,6 +86578,11 @@ const CardLayout = ({
86509
86578
  }) => {
86510
86579
  import.meta.css = [css$3, "@jsenv/navi/src/layout/card_layout.jsx"];
86511
86580
  return jsx(Box, {
86581
+ flex: true
86582
+ // The card's place in the area; alignX/alignY below are its content's.
86583
+ ,
86584
+ alignX: "center",
86585
+ alignY: "center",
86512
86586
  baseClassName: "navi_card_layout",
86513
86587
  styleCSSVars: CardLayoutStyleCSSVars,
86514
86588
  visualSelector: ".navi_card",