@jsenv/navi 0.29.387 → 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.
Files changed (43) hide show
  1. package/README.md +4 -4
  2. package/dist/dev/jsenv_navi.js +556 -392
  3. package/dist/dev/jsenv_navi.js.map +81 -81
  4. package/dist/dev/jsenv_navi_side_effects.js +87 -78
  5. package/dist/dev/jsenv_navi_side_effects.js.map +4 -4
  6. package/dist/jsenv_navi.js +551 -387
  7. package/dist/jsenv_navi.js.map +81 -81
  8. package/dist/jsenv_navi_side_effects.js +87 -78
  9. package/dist/jsenv_navi_side_effects.js.map +4 -4
  10. package/docs/AI_INSTRUCTIONS.md +39 -23
  11. package/docs/actions.md +6 -4
  12. package/docs/autofocus.md +2 -1
  13. package/docs/badge_list.md +5 -3
  14. package/docs/control_group.md +4 -3
  15. package/docs/control_object.md +1 -1
  16. package/docs/control_value.md +8 -9
  17. package/docs/css_architecture.md +16 -14
  18. package/docs/dialog_shape.md +15 -10
  19. package/docs/drag_interactions.md +49 -41
  20. package/docs/drag_to_travel.md +32 -24
  21. package/docs/dynamic_import.md +7 -5
  22. package/docs/error_handling.md +31 -30
  23. package/docs/field_validation.md +35 -6
  24. package/docs/i18n.md +16 -7
  25. package/docs/interactions.md +28 -23
  26. package/docs/list_action.md +8 -5
  27. package/docs/list_refresh.md +8 -1
  28. package/docs/mobile_layout_pitfalls.md +6 -5
  29. package/docs/navigation.md +7 -5
  30. package/docs/network_policy.md +22 -7
  31. package/docs/pan_zoom.md +16 -9
  32. package/docs/popup_backdrop.md +11 -5
  33. package/docs/popup_lift.md +5 -3
  34. package/docs/popup_open.md +39 -28
  35. package/docs/resource.md +31 -20
  36. package/docs/route_transitions.md +13 -10
  37. package/docs/safe_area.md +59 -39
  38. package/docs/scroll.md +11 -9
  39. package/docs/testid.md +10 -1
  40. package/docs/typography.md +2 -3
  41. package/docs/view_transitions.md +9 -9
  42. package/docs/z_index.md +6 -5
  43. package/package.json +2 -2
@@ -554,12 +554,17 @@ 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:
561
564
  * - `"button.*"` — Clear, Reset, Send, Open, Close, Cancel, Confirm…
562
- * - `"time.*"` — what a clock writes between hours and minutes, and how the two ends of a span are named; the wording the formatters use is registered by @jsenv/humanize
565
+ * - `"confirm.*"` — the question a confirmation asks by default
566
+ * - `"time.*"` — what a clock writes between hours and minutes, and the labels of its hour and minute parts; the wording the formatters use is registered by @jsenv/humanize
567
+ * - `"time_range.*"` — how the two ends of a span are named ("From", "to")
563
568
  * - `"spin.*"` — the ends of a steppable range
564
569
  * - `"list.*"` — empty/no-match/failed-rows messages
565
570
  * - `"badge_list.*"` — the "+[count] more" overflow badge
@@ -1472,19 +1477,25 @@ const isNetworkPolicyError = (error) => {
1472
1477
  * is already saying it in words the person understands.
1473
1478
  *
1474
1479
  * navi's own report leaves it alone (action_error_report.js). What is left is
1475
- * the screen displaying it: it does so by throwing the error to a boundary, and
1476
- * `preact/debug` re-emits on `window` every error a boundary caught — on
1477
- * purpose, for React devtools compatibility. Uncancelled, that lands as an
1478
- * uncaught error in the console, over a page calmly explaining why nothing was
1479
- * 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
1480
1484
  * the console line, and the jsenv supervisor skips prevented events too.
1481
1485
  *
1482
1486
  * Both shapes a failure travels in are covered, since which one it is depends
1483
1487
  * on whether the resource callback happened to be async — the throw reaching
1484
1488
  * `window`, and the rejection nobody caught.
1485
1489
  *
1486
- * Scoped to the policy's own error and to nothing else — every other error a
1487
- * 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
1488
1499
  * it is.
1489
1500
  */
1490
1501
  let networkPolicyErrorSilenced = false;
@@ -1572,11 +1583,10 @@ const errorIsDisplayed = (error) => {
1572
1583
  *
1573
1584
  * Whatever the reader does with it is already covered without any deadline: it
1574
1585
  * displays it (and marks it), or it throws it, and a thrown error either finds a
1575
- * boundary that displays it or reaches window on its own — `preact/debug`
1576
- * re-throws every error a boundary caught, and an unbounded one aborts the
1577
- * render loudly. Reporting it here as well would be a second voice saying the
1578
- * same thing, always the wrong one, since this module cannot see which of those
1579
- * happened.
1586
+ * boundary that displays it or reaches window on its own — preact re-throws
1587
+ * what no boundary handled. Reporting it here as well would be a second voice
1588
+ * saying the same thing, always the wrong one, since this module cannot see
1589
+ * which of those happened.
1580
1590
  */
1581
1591
  const errorTakenByRenderSet = new WeakSet();
1582
1592
  const markErrorAsTakenByRender = (error) => {
@@ -1651,8 +1661,8 @@ const markErrorAsStoppingRender = (error) => {
1651
1661
  * An error the network policy produced is never reported: the app declared the
1652
1662
  * state that produced it, the request never left, and there is nothing for a
1653
1663
  * developer to fix. It is data a screen shows, and it stays data whether or not
1654
- * one does (see network_policy.js, which also keeps it out of the browser
1655
- * console).
1664
+ * one does (see network_policy.js, which also cancels it when it reaches
1665
+ * window).
1656
1666
  */
1657
1667
  const errorIsAccountedFor = (error) => {
1658
1668
  return (
@@ -1666,7 +1676,7 @@ const errorIsAccountedFor = (error) => {
1666
1676
  /**
1667
1677
  * Rethrown rather than logged: an error nobody took is an unhandled error, and
1668
1678
  * the runtime already knows what to do with those (window "error" event, jsenv
1669
- * overlay in dev). Same trick preact/debug uses for the same reason.
1679
+ * overlay in dev). Same answer preact gives an error no boundary handled.
1670
1680
  */
1671
1681
  const errorReportedSet = new WeakSet();
1672
1682
  const reportErrorIfNobodyDisplaysIt = (error, { action } = {}) => {
@@ -2187,7 +2197,7 @@ const compareTwoJsValues = (
2187
2197
  }
2188
2198
  return true;
2189
2199
  }
2190
- // Ordered array comparison (original behavior)
2200
+ // Ordered array comparison
2191
2201
  let i = 0;
2192
2202
  while (i < a.length) {
2193
2203
  const aValue = a[i];
@@ -3370,6 +3380,8 @@ const actionWeakMap = new WeakMap();
3370
3380
  * long is this still true?" belongs to the question: a court list is good for
3371
3381
  * the session, a count of people watching grows on its own and is worth a few
3372
3382
  * minutes, a payment status is worth nothing at all.
3383
+ * @param {{ verb?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" }} [rootOptions.meta] -
3384
+ * `verb` puts the action under the network policy (docs/network_policy.md).
3373
3385
  */
3374
3386
  const createAction = (callback, rootOptions = {}) => {
3375
3387
  const existing = actionWeakMap.get(callback);
@@ -4484,7 +4496,7 @@ const runUnwatched = (startRun) => {
4484
4496
  /**
4485
4497
  * Reactively runs an action whenever the params derived from signals change.
4486
4498
  *
4487
- * @param {object} action - The action to run.
4499
+ * @param {object | Function} action - The action to run, or a function made into one.
4488
4500
  * @param {Function} deriveActionParamsFromSignals - A function that reads signals and returns
4489
4501
  * the params to pass to the action. It is re-evaluated automatically whenever a signal it
4490
4502
  * read changes. Return `false`/`null`/`undefined` to skip running the action.
@@ -4507,6 +4519,8 @@ const runUnwatched = (startRun) => {
4507
4519
  * ```
4508
4520
  * The action will not fire while the user is actively changing filters; it fires once
4509
4521
  * they pause for half a second.
4522
+ * @returns {object} the instance bound to the derived params: the one to read
4523
+ * (`useAsyncData`), since the answer lands there and never on `action` itself.
4510
4524
  */
4511
4525
  // The run is not awaited here, and a rejection nobody waits for is an unhandled
4512
4526
  // one — in dev, an error overlay thrown over a page that is already saying what
@@ -5021,11 +5035,12 @@ const isNumberInput = (type, naviInputType) => {
5021
5035
  */
5022
5036
  /**
5023
5037
  * A bound signal that carries a default of its own says the same thing on every
5024
- * control: the control starts there and stays uncontrolled, which is what makes
5025
- * a form read the value shown as a SUGGESTION rather than as something it
5026
- * already holds. Uncontrolled here is about what the control HOLDS, not about
5027
- * whether it follows the signal — the binding stays two-way either way (see
5028
- * stateFromSignal in control_hooks.jsx). Written once and used by everything
5038
+ * control: the control starts there, uncontrolled, and a reset goes back to it.
5039
+ * Uncontrolled here is about where the control starts, not about whether it
5040
+ * follows the signal — the binding stays two-way either way (see
5041
+ * stateFromSignal in control_hooks.jsx) — nor about what a form counts as
5042
+ * held: that is the signal's content (isUIStateHeld in held_ui_state.js), and a
5043
+ * stateSignal holds its default. Written once and used by everything
5029
5044
  * that takes a `signal`, so one signal cannot mean two different things
5030
5045
  * depending on which control it was handed to.
5031
5046
  */
@@ -5085,8 +5100,7 @@ const resolveInputProps = (props, { controlType = "input" } = {}) => {
5085
5100
  // If no explicit defaultChecked, derive it from the signal's default
5086
5101
  // value so that resetUIState restores to the original default.
5087
5102
  // Only a stateSignal carries a default of its own; a plain signal has
5088
- // no `options` at all, and asking it for one used to throw on mount —
5089
- // the same optional read every other branch here already does.
5103
+ // no `options` at all, hence the optional read.
5090
5104
  const defaultVal = signalOptions?.getDefaultValue(false);
5091
5105
  if (defaultVal === undefined) ; else if (props.type === "radio") {
5092
5106
  if (defaultVal === true) {
@@ -9169,8 +9183,8 @@ const isLanguageSupported = (lang, supportedLanguages) => {
9169
9183
  * supported), falls back to `supportedLanguagesSignal` itself so callers
9170
9184
  * still get *something* usable rather than an empty array.
9171
9185
  *
9172
- * Consumers that accept either a single lang or an ordered array (this
9173
- * package's own `matchBestLang`/`createI18n`, and native `Intl.NumberFormat`/
9186
+ * Consumers that accept either a single lang or an ordered array
9187
+ * (@jsenv/humanize's `createI18n` and formatters, and native `Intl.NumberFormat`/
9174
9188
  * `Intl.DateTimeFormat`) can pass this straight through: anything not
9175
9189
  * covered by the first entry falls through to the next, rather than
9176
9190
  * jumping straight to an unrelated default like "en".
@@ -10153,15 +10167,15 @@ const readOnlyMessage = (field) => {
10153
10167
  * - Action dispatch + validity: `control_action.js` / `dispatchRequestAction`
10154
10168
  * - Validity checking: `control_validation.js`
10155
10169
  *
10156
- * Each UI state controller gets its own `controlInteraction` instance (created by
10157
- * `createControlInteraction`) just like it gets a `controlValidity` instance.
10170
+ * Each UI state controller gets its own instance, `controller.rules.interaction`
10171
+ * (created by `createControlInteraction`), beside `controller.rules.validation`.
10158
10172
  *
10159
10173
  * Flow:
10160
10174
  * user interaction
10161
10175
  * → dispatchRequestInteraction
10162
10176
  * → "navi_request_interaction" event
10163
10177
  * → onRequestInteraction
10164
- * → check disabled / read-only / busy (via controller.controlInteraction)
10178
+ * → check disabled / read-only / busy (via controller.rules.interaction)
10165
10179
  * against the interaction's `intent` ("write" by default, "read" for one
10166
10180
  * that only shows what is already there — see READONLY_CONSTRAINT)
10167
10181
  * → if blocked → prevented()
@@ -11004,7 +11018,8 @@ const interactionsDisputeThePress = (interactions) => {
11004
11018
  * control (a Button) is its own; a Box inside one, or wrapping exactly one,
11005
11019
  * reaches it; a Box with no control anywhere near it — or laying out several,
11006
11020
  * which belongs to none of them — still answers with a callback of the caller's,
11007
- * and only "request_action" has nothing to ask.
11021
+ * and only the two requests ("request_action", "request_ui_action") have
11022
+ * nothing to ask.
11008
11023
  *
11009
11024
  * Set up once per element rather than on every render, which is what lets a
11010
11025
  * detector be a plain `setup`/teardown pair. So the interactions themselves are
@@ -11173,7 +11188,7 @@ const useInteractionsEffect = (ref, interactionsRef) => {
11173
11188
  cancelable: true,
11174
11189
  });
11175
11190
  // The event the interaction was read from stays reachable from it:
11176
- // `findEvent(actionEvent, "pointerdown")` still finds the press a swipe
11191
+ // `findEvent(actionEvent, "pointerdown")` still finds the press a hold
11177
11192
  // was made of.
11178
11193
  chainEvent(interactionEvent, originalEvent);
11179
11194
  if (!element.dispatchEvent(interactionEvent)) {
@@ -11939,7 +11954,7 @@ const swipeTypeOf = (axis, pulled) => {
11939
11954
  * …
11940
11955
  * }}}
11941
11956
  *
11942
- * `toId` is an element and never null: a copy over nothing is a release that meant
11957
+ * `toId` names an element and is never null: a copy over nothing is a release that meant
11943
11958
  * nothing, and the interaction does not happen at all — unless the element
11944
11959
  * declares `leave` (see below), which gives that release a meaning of its own.
11945
11960
  *
@@ -12634,8 +12649,9 @@ defineInteractionDetector({
12634
12649
  name: "surface",
12635
12650
  claims: (type) =>
12636
12651
  type === PAN || type === ZOOM || type === GRAB || type === RELEASE,
12637
- // A press on the surface may be a tap, a hold or the beginning of a pan, and
12638
- // 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.
12639
12655
  disputesPress: true,
12640
12656
  setup: (element, trigger, { types, readConfig }) => {
12641
12657
  const canPan = types.includes(PAN);
@@ -15757,21 +15773,33 @@ const getActionResultProperties = (action) => {
15757
15773
  return actionResultPropertiesMap.get(action);
15758
15774
  };
15759
15775
 
15760
- // PUT/PATCH results update the UI through the store, and DELETE resets the GET
15761
- // instead of rerunning it, so a GET rerun would only ever cost a request.
15762
- // A POST is the one case the client cannot decide alone: whether a new item
15763
- // belongs to a list depends on filters/pagination the backend owns.
15764
15776
  // Rationale in full, plus when to override: docs/list_refresh.md
15765
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.
15766
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.
15767
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
+ };
15768
15802
  };
15769
- // What makes a range reader stale (rerunOn.GET_RANGE overrides it). DELETE is
15770
- // in there, unlike for GET_MANY: an action holds ids and the store drops the
15771
- // deleted one out of every list holding it, while a list reading by slices
15772
- // holds places — the row that left takes the ones after it one rank up, and
15773
- // only the collection knows who fills the last one.
15774
- const defaultInvalidateRangeOn = ["POST", "DELETE"];
15775
15803
 
15776
15804
  // This handles ALL resource lifecycle logic (rerun/reset) across all resources
15777
15805
  const createResourceLifecycleManager = () => {
@@ -15781,7 +15809,7 @@ const createResourceLifecycleManager = () => {
15781
15809
 
15782
15810
  const registerResource = (resourceScope, config) => {
15783
15811
  const {
15784
- rerunOn = defaultRerunOn,
15812
+ rerunOn,
15785
15813
  paramScope = null,
15786
15814
  dependencies = [],
15787
15815
  uniqueKeys = [],
@@ -16064,8 +16092,7 @@ const createResourceLifecycleManager = () => {
16064
16092
  if (!isSameResource && !isDependent) {
16065
16093
  continue;
16066
16094
  }
16067
- const invalidateOn = config.rerunOn.GET_RANGE ?? defaultInvalidateRangeOn;
16068
- if (!shouldRerunAfter(invalidateOn, triggerVerb)) {
16095
+ if (!shouldRerunAfter(config.rerunOn.GET_RANGE, triggerVerb)) {
16069
16096
  continue;
16070
16097
  }
16071
16098
  for (const rangeReader of config.rangeReaderSet) {
@@ -16403,7 +16430,7 @@ const debug$3 = (...args) => {
16403
16430
  * @param {string} name - resource name, used in action names and error messages
16404
16431
  * @param {Object} restCallbacks - `{ idKey, uniqueKeys, rerunOn, dependencies, GET, GET_MANY, GET_RANGE, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
16405
16432
  * @param {string} [restCallbacks.idKey] - primary key property, defaults to `"id"`
16406
- * @param {string[]} [restCallbacks.uniqueKeys] - alternate keys the store can find an item by (e.g. `"username"`); a callback may return a different `id` to rename the item's primary key
16433
+ * @param {string[]} [restCallbacks.uniqueKeys] - alternate keys the store can find an item by (e.g. `"username"`)
16407
16434
  * @see docs/resource.md — relationships, callback return contracts, decision table
16408
16435
  *
16409
16436
  * @example
@@ -16566,7 +16593,7 @@ const resource = (
16566
16593
  addItemSetup,
16567
16594
  createRestAction: createRestActionForRoot,
16568
16595
  paramScope: getParamScope(undefined),
16569
- rerunOn,
16596
+ rerunOn: resolveRerunOn(rerunOn),
16570
16597
  dependencies,
16571
16598
  });
16572
16599
  };
@@ -16630,7 +16657,7 @@ const createResource = (
16630
16657
  * identical parameters, preventing cross-contamination between different parameter sets.
16631
16658
  *
16632
16659
  * @param {Object} params - Parameters to bind to all actions of this resource (required)
16633
- * @param {Object} options - Additional options for the parameterized resource
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
16634
16661
  * @returns {Object} A new resource instance with parameter-bound actions and isolated lifecycle
16635
16662
  * @see docs/resource.md — what a scope isolates, and `dependencies`
16636
16663
  *
@@ -16648,7 +16675,7 @@ const createResource = (
16648
16675
  * const ROLE_WITH_OWNERSHIP = role.withParams({ owners: true }, {
16649
16676
  * dependencies: [role, database, tables],
16650
16677
  * });
16651
- * // ROLE_WITH_OWNERSHIP.GET_MANY will autorerun when any table/database/role is POST/DELETE
16678
+ * // ROLE_WITH_OWNERSHIP.GET_MANY reruns after any write (POST/PUT/PATCH/DELETE) on a role, a database or a table
16652
16679
  */
16653
16680
  const withParams = (
16654
16681
  paramsToInject,
@@ -16676,7 +16703,7 @@ const createResource = (
16676
16703
  addItemSetup,
16677
16704
  createRestAction: createRestActionWithParams,
16678
16705
  paramScope: resolvedParamScope,
16679
- rerunOn: withParamsRerunOn ?? rerunOn,
16706
+ rerunOn: resolveRerunOn(withParamsRerunOn, rerunOn),
16680
16707
  dependencies: withParamsDeps ?? dependencies,
16681
16708
  });
16682
16709
  };
@@ -16694,13 +16721,15 @@ const createResource = (
16694
16721
  * Callback return contracts:
16695
16722
  * - GET / PUT → the parent object with the relationship nested inside:
16696
16723
  * `async ({ id }) => ({ id, session: { id: 10, token: "abc" } })`; `null` for no relationship
16697
- * - DELETE → the parent id (or `{ id }`); the property is set to `null`
16724
+ * - DELETE → the parent id, or `{ id }`; the property is set to `null`
16698
16725
  *
16699
16726
  * The backend may also embed the child inline in a parent GET/POST response — the
16700
16727
  * setter on the property upserts the nested object into the child store.
16701
16728
  *
16702
- * Returns the child relationship resource, itself chainable:
16703
- * `USER_SESSION.one("device", DEVICE)` adds a reactive `.device` property to each session.
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.
16704
16733
  *
16705
16734
  * @param {string} propertyName - property holding the child on each parent item
16706
16735
  * @param {Object} childResource - the independent resource created by `resource()`
@@ -16783,11 +16812,11 @@ const createResource = (
16783
16812
  const createRestActionForOne = (verb, callback, { onActionComplete }) => {
16784
16813
  const applyResultToValue =
16785
16814
  verb === "DELETE"
16786
- ? (itemId) => {
16787
- const item = store.select(itemId);
16815
+ ? (itemIdOrItemProps) => {
16816
+ const item = store.select(itemIdOrItemProps);
16788
16817
  const childItemId = item[propertyName][childIdKey];
16789
16818
  store.upsert({
16790
- [idKey]: itemId,
16819
+ [idKey]: item[idKey],
16791
16820
  [propertyName]: null,
16792
16821
  });
16793
16822
  return childItemId;
@@ -16815,7 +16844,7 @@ const createResource = (
16815
16844
  if (!isProps(result) && !primitiveCanBeId(result)) {
16816
16845
  throwInvalidResult(
16817
16846
  action.name,
16818
- `an object (that will be used to drop "${name}" resource)`,
16847
+ `the "${name}" id, or { ${idKey} } (the item whose "${propertyName}" becomes null)`,
16819
16848
  result,
16820
16849
  );
16821
16850
  }
@@ -16840,11 +16869,11 @@ const createResource = (
16840
16869
  PUT,
16841
16870
  DELETE,
16842
16871
  },
16843
- store,
16844
- addItemSetup,
16872
+ store: childStore,
16873
+ addItemSetup: childResource.addItemSetup,
16845
16874
  createRestAction: createRestActionForOne,
16846
16875
  paramScope,
16847
- rerunOn: oneRerunOn ?? rerunOn,
16876
+ rerunOn: resolveRerunOn(oneRerunOn, rerunOn),
16848
16877
  dependencies: oneDependencies ?? dependencies,
16849
16878
  });
16850
16879
  };
@@ -16867,6 +16896,9 @@ const createResource = (
16867
16896
  * - DELETE → `[parentId, childId]`
16868
16897
  * - DELETE_MANY → `[parentId, [childId, childId, …]]`
16869
16898
  *
16899
+ * Returns the relationship resource, itself chainable; its items are the child
16900
+ * resource's own, as for `.one()`.
16901
+ *
16870
16902
  * @param {string} propertyName - property holding the child array on each parent item
16871
16903
  * @param {Object} childResource - the independent resource created by `resource()`
16872
16904
  * @param {Object} [restCallbacks] - `{ rerunOn, dependencies, GET, GET_MANY, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
@@ -17102,11 +17134,11 @@ const createResource = (
17102
17134
  DELETE,
17103
17135
  DELETE_MANY,
17104
17136
  },
17105
- store,
17106
- addItemSetup,
17137
+ store: childStore,
17138
+ addItemSetup: childResource.addItemSetup,
17107
17139
  createRestAction: createRestActionForMany,
17108
17140
  paramScope,
17109
- rerunOn: manyRerunOn ?? rerunOn,
17141
+ rerunOn: resolveRerunOn(manyRerunOn, rerunOn),
17110
17142
  dependencies: manyDependencies ?? dependencies,
17111
17143
  });
17112
17144
  };
@@ -17235,7 +17267,7 @@ const createResource = (
17235
17267
  addItemSetup: childAddItemSetup,
17236
17268
  createRestAction: createRestActionForScopedOne,
17237
17269
  paramScope,
17238
- rerunOn: scopedOneRerunOn ?? rerunOn,
17270
+ rerunOn: resolveRerunOn(scopedOneRerunOn, rerunOn),
17239
17271
  dependencies: scopedOneDependencies ?? dependencies,
17240
17272
  });
17241
17273
  };
@@ -17470,7 +17502,7 @@ const createResource = (
17470
17502
  addItemSetup: childAddItemSetup,
17471
17503
  createRestAction: createRestActionForScopedMany,
17472
17504
  paramScope,
17473
- rerunOn: scopedManyRerunOn ?? rerunOn,
17505
+ rerunOn: resolveRerunOn(scopedManyRerunOn, rerunOn),
17474
17506
  dependencies: scopedManyDependencies ?? dependencies,
17475
17507
  });
17476
17508
  // When a scoped child collection is mutated (POST etc.), the parent GET must
@@ -17861,7 +17893,7 @@ Received an object with keys: ${keys.join(", ")}.`,
17861
17893
  * });
17862
17894
  *
17863
17895
  * syncResourceToSignals(USER, { username: usernameSignal });
17864
- * // Now when a user item's username is updated via USER.PUT,
17896
+ * // When a user item's username is updated via USER.PUT,
17865
17897
  * // usernameSignal.value is set to the new username,
17866
17898
  * // which in turn triggers the route Signal->URL sync and updates the browser URL.
17867
17899
  */
@@ -17874,6 +17906,17 @@ const syncResourceToSignals = (resource, propertyToSignalMap) => {
17874
17906
  syncStoreToSignals(resource.store, propertyToSignalMap);
17875
17907
  };
17876
17908
 
17909
+ /**
17910
+ * The same, for a `scopedOne`/`scopedMany` resource: its items live in one store
17911
+ * per owner, so the store to watch is the one of the owner `ownerSignal` names
17912
+ * (by id or by any unique key), and it is switched when the signal changes.
17913
+ * Nothing is synced while `ownerSignal` holds `null`/`undefined` or names an
17914
+ * owner nothing has been loaded for yet.
17915
+ *
17916
+ * @param {Object} resource - a resource made by `.scopedOne()` / `.scopedMany()`
17917
+ * @param {import("@preact/signals").Signal} ownerSignal - the owner whose children are watched
17918
+ * @param {Object} propertyToSignalMap - `{ [propertyName]: signal }`, as for `syncResourceToSignals`
17919
+ */
17877
17920
  const syncOwnedResourceToSignals = (
17878
17921
  resource,
17879
17922
  ownerSignal,
@@ -17921,7 +17964,7 @@ const generateSignalId = () => {
17921
17964
  * 1. Initially takes value from the default signal
17922
17965
  * 2. When explicitly set (programmatically or via localStorage), the explicit value takes precedence
17923
17966
  * 3. When default signal changes, it only updates if no explicit value was ever set
17924
- * 4. Calling reset() or setting to undefined makes the signal use the dynamic default again
17967
+ * 4. Setting it to undefined makes the signal use the dynamic default again
17925
17968
  * 5. If dynamic default is undefined and options.default is provided, uses the static fallback
17926
17969
  *
17927
17970
  * This is useful for:
@@ -17935,7 +17978,9 @@ const generateSignalId = () => {
17935
17978
  * @param {string|number} [options.id] - Custom ID for the signal. If not provided, an auto-generated ID will be used. Used for localStorage key and route pattern detection.
17936
17979
  * @param {any} [options.default] - Static fallback value used when defaultValue is a signal and that signal's value is undefined
17937
17980
  * @param {boolean} [options.persists=false] - Whether to persist the signal value in localStorage using the signal ID as key
17938
- * @param {"string" | "number" | "boolean" | "object"} [options.type="string"] - Type for localStorage serialization/deserialization
17981
+ * @param {string} [options.type] - Type the value is coerced to, and read back as from localStorage and the url:
17982
+ * any @jsenv/validity type (`"string"`, `"number"`, `"boolean"`, `"array"`, `"object"`, `"integer"`, `"date"`,
17983
+ * `"time"`…). Without it the value is not coerced.
17939
17984
  * @param {"string" | "number" | "boolean"} [options.itemType] - For array type: type of the array items.
17940
17985
  * Used when reading the value back from a url search param, where everything is a string:
17941
17986
  * `?level=3,4` becomes `[3, 4]` instead of `["3", "4"]`. Without it items stay strings.
@@ -19105,14 +19150,12 @@ const LoadingFallback = ({
19105
19150
  * Two things it gets right that a hand-written boundary rarely does, both
19106
19151
  * explained in docs/error_handling.md:
19107
19152
  *
19108
- * - It marks the error as displayed ONLY when it actually displays it.
19109
- * `preact/debug` rethrows every error a boundary caught in a `setTimeout`, on
19110
- * purpose (React devtools compatibility), so a handled error still reaches
19111
- * window and the jsenv overlay covers the app unless `__handled_by__` is set.
19112
- * Setting it before knowing whether anything is rendered turns a boundary into
19113
- * a bug swallower: a TypeError in a component becomes a blank page AND a
19114
- * silent one. Without a `fallback` there is nothing to display, so the error is
19115
- * left alone and continues up.
19153
+ * - It marks the error as displayed ONLY when it actually displays it. The
19154
+ * jsenv overlay skips an error carrying `__handled_by__`, so a mark set before
19155
+ * knowing whether anything is rendered mutes an error that then continues up:
19156
+ * a TypeError in a component becomes a blank page AND a silent one. Without a
19157
+ * `fallback` there is nothing to display, so the error is left alone and
19158
+ * continues up.
19116
19159
  *
19117
19160
  * - It resets on navigation, not only on rerun. Rerunning the failed action is
19118
19161
  * one way out; going somewhere else is the common one. Without a reset on the
@@ -20178,9 +20221,9 @@ const isSizeSpacingKey = (key) => {
20178
20221
  // "vvw"/"vvh" are navi's own: the *visual* viewport, which — unlike vw/dvw —
20179
20222
  // shrinks when the mobile virtual keyboard opens (see layout/responsive.js), so
20180
20223
  // they are what a popup meant to stay clear of the keyboard should use.
20181
- // "appw"/"apph" are the same thing narrowed to the app's own screen: identical
20182
- // to vvw/vvh until the app declares --navi-app-max-width, and a share of that
20183
- // 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 —
20184
20227
  // 3vvw on a 1500px window is a 45px gap around a 600px app.
20185
20228
  // Functions rather than the signals themselves: appw/apph are not a signal to
20186
20229
  // read but a value to compute (a signal, then a CSS var read back). Reading the
@@ -22222,9 +22265,48 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
22222
22265
  * separator?: import("ignore:preact").ComponentChildren | ((index: number) => import("ignore:preact").ComponentChildren),
22223
22266
  * selfInteractions?: string,
22224
22267
  * interactions?: { [type: string]: "request_action" | "request_ui_action" | "refuse" | ((event: Event) => void) | false | null | undefined },
22268
+ * alignX?: string,
22269
+ * alignY?: string,
22270
+ * spacing?: string | number,
22271
+ * overflow?: string,
22272
+ * header?: boolean,
22273
+ * footer?: boolean,
22274
+ * body?: boolean,
22275
+ * sticky?: boolean,
22225
22276
  * children?: import("ignore:preact").ComponentChildren,
22226
22277
  * [key: string]: any,
22227
22278
  * }>}
22279
+ * @param {"x"|"y"|boolean} [flex] A flex container: `"x"` (or `true`) side by
22280
+ * side, `"y"` stacked. Named by axis because `flex-direction: column` stacks
22281
+ * items vertically, the opposite of what "column" suggests.
22282
+ * @param {boolean} [inline] The inline form of the box's display
22283
+ * (`inline-flex`, `inline-grid`…). An inline box given a `width`/`height`
22284
+ * becomes an inline flex row, since an inline box ignores both.
22285
+ * @param {string} [alignX] Horizontal alignment of the content whatever the
22286
+ * flex direction: `justify-content` or `align-items` is picked from it
22287
+ * (`text-align` outside flex and grid).
22288
+ * @param {string} [alignY] Vertical alignment, the same way.
22289
+ * @param {string|number} [spacing] The gap between children in a flex or grid
22290
+ * box (`spacingX`/`spacingY` per axis) — there is no `gap` prop. Spacing
22291
+ * props (`spacing`, `margin*`, `padding*`) take the scale `"xxs"`…`"xxl"`
22292
+ * (`--navi-xxs`…), `"-s"` for a negative, or any CSS value.
22293
+ * @param {object|string} [style] CSS properties, plus pseudo keys holding
22294
+ * their own: `style={{ color: "blue", ":hover": { color: "red" } }}`
22295
+ * (`":-navi-pressed"`, `"::before"`…). A pseudo class used there is tracked
22296
+ * on the element; nothing re-renders when it changes.
22297
+ * @param {string} [overflow] `"auto"`/`"scroll"` (`overflowX`/`overflowY`
22298
+ * too) makes the box a scrolling area, which is what gives `header`/`body`/
22299
+ * `footer` their meaning; such a box is also `isolation: isolate` and gets a
22300
+ * `scroll-padding` (see docs/scroll.md).
22301
+ * @param {boolean} [header] A part that stays put at the top of the scrolling
22302
+ * box it is a direct child of — nothing at all without that box's overflow.
22303
+ * Rendered as `<header>` unless `as` says otherwise.
22304
+ * @param {boolean} [footer] The same at the bottom, rendered as `<footer>`.
22305
+ * @param {boolean} [body] The only part of its scrolling box that scrolls,
22306
+ * `header`/`footer` sitting outside it. It shrinks, it never grows.
22307
+ * @param {boolean} [sticky] `position: sticky` in the sticky z-index band
22308
+ * (`--navi-z-index-sticky`), stuck or not; an explicit `zIndex`, `"auto"`
22309
+ * included, wins (see docs/z_index.md). `top`/`bottom`/… take `true` for 0.
22228
22310
  * @param {string} [background] Any CSS background, plus two keywords for the
22229
22311
  * planes an app paints: `"surface"` is the paper content sits on,
22230
22312
  * `"chrome"` the frame around a screen — a top bar, a side menu, a toolbar.
@@ -23968,21 +24050,16 @@ const createUITransitionController = (
23968
24050
  /**
23969
24051
  * UITransition
23970
24052
  *
23971
- * A Preact component that enables smooth animated transitions between its children when the content changes.
23972
- * It observes content keys and phases to create different types of transitions.
24053
+ * A Preact component that animates the change between its children: the
24054
+ * outgoing content fades out while the incoming one fades in, and the box
24055
+ * resizes from the one to the other (the mechanics are in ui_transition.js).
23973
24056
  *
23974
- * Features:
23975
- * - Content transitions: Between different content keys (e.g., user profiles, search results)
23976
- * - Phase transitions: Between loading/content/error states for the same content key
23977
- * - Automatic size animation to accommodate content changes
23978
- * - Configurable transition types: "slide-left", "cross-fade"
23979
- * - Independent duration control for content and phase transitions
23980
- *
23981
- * Usage:
23982
- * - Wrap dynamic content in <UITransition> to animate between states
23983
- * - Set a unique `data-content-id` on your rendered content to identify each content variant
23984
- * - Use `data-content-phase` to mark loading/error states for phase transitions
23985
- * - Configure transition types and durations for both content and phase changes
24057
+ * Two kinds of change are told apart, from attributes on the rendered content:
24058
+ * - a content transition, when the content key changes — `data-content-key` on
24059
+ * the rendered element (a user id, a search), or `useUITransitionContentId`
24060
+ * called from a component inside (what `<Route>` does with its url pattern);
24061
+ * - a phase transition, when `data-content-phase` (a loading or error state)
24062
+ * changes for the same content key.
23986
24063
  *
23987
24064
  * Example:
23988
24065
  *
@@ -23991,9 +24068,6 @@ const createUITransitionController = (
23991
24068
  * ? <Spinner data-content-key={userId} data-content-phase />
23992
24069
  * : <UserProfile user={user} data-content-key={userId} />}
23993
24070
  * </UITransition>
23994
- *
23995
- * When `data-content-id` changes, UITransition animates content transitions.
23996
- * When `data-content-phase` changes for the same key, it animates phase transitions.
23997
24071
  */
23998
24072
 
23999
24073
  const UITransitionContentIdContext = createContext();
@@ -24115,16 +24189,12 @@ const UITransition = ({
24115
24189
  };
24116
24190
 
24117
24191
  /**
24118
- * The goal of this hook is to allow a component to set a "content key"
24119
- * Meaning all content within the component is identified by that key
24120
- *
24121
- * When the key changes, UITransition will be able to detect that and consider the content
24122
- * as changed even if the component is still the same
24123
- *
24124
- * This is used by <Route> to set the content key to the route path
24125
- * When the route becomes inactive it will call useUITransitionContentId(undefined)
24126
- * And if a sibling route becones active it will call useUITransitionContentId with its own path
24192
+ * Names the content a component renders, for the <UITransition> around it:
24193
+ * everything rendered inside is identified by that key, and the key changing
24194
+ * is read as a content change even though the component is the same.
24127
24195
  *
24196
+ * <Route> calls it with the url pattern of the branch it renders; the branch
24197
+ * unmounting removes its part, and a sibling branch mounting adds its own.
24128
24198
  */
24129
24199
  const useUITransitionContentId = value => {
24130
24200
  const contentId = useContext(UITransitionContentIdContext);
@@ -24149,8 +24219,8 @@ const useUITransitionContentId = value => {
24149
24219
  };
24150
24220
 
24151
24221
  /**
24152
- * Custom route pattern matching system
24153
- * Replaces URLPattern with a simpler, more predictable approach
24222
+ * Custom route pattern matching system.
24223
+ * Not built on URLPattern: matching here is simpler and more predictable.
24154
24224
  */
24155
24225
 
24156
24226
 
@@ -27482,7 +27552,7 @@ const useRouteStatus = (route) => {
27482
27552
  const assertRoute = (route) => {
27483
27553
  if (!route.isRoute) {
27484
27554
  throw new Error(
27485
- "The route prop must be a route object created with createRoute",
27555
+ "The route prop must be a route object created with route()",
27486
27556
  );
27487
27557
  }
27488
27558
  if (!setupRoutesCalled) {
@@ -30157,8 +30227,8 @@ const TRANSITION_FURNITURE_CSS = /* css */ `.navi_fixed_bar[data-navi-transition
30157
30227
  }
30158
30228
  `;
30159
30229
 
30160
- // Called from the render of whatever holds the pages, never at module scope:
30161
- // 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
30162
30232
  // build that sees no caller drops the css with the function.
30163
30233
  const installTransitionFurnitureCss = () => {
30164
30234
  import.meta.css = [TRANSITION_FURNITURE_CSS, "@jsenv/navi/src/nav/transition_furniture.js"];
@@ -30621,9 +30691,10 @@ const TRANSITION_WINDOW_CSS = /* css */ `@property --navi-transition-cover-top {
30621
30691
  }
30622
30692
  `;
30623
30693
 
30624
- // Called from the render of whatever needs the window, never at module scope:
30625
- // a page that never travels between routes must not carry this sheet, and a
30626
- // 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.
30627
30698
  const installTransitionWindowCss = () => {
30628
30699
  import.meta.css = [TRANSITION_WINDOW_CSS, "@jsenv/navi/src/nav/transition_window.js"];
30629
30700
  };
@@ -31436,9 +31507,6 @@ const RouteTransitionArea = ({
31436
31507
  children,
31437
31508
  ...rest
31438
31509
  }) => {
31439
- import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
31440
- installTransitionWindowCss();
31441
- installTransitionFurnitureCss();
31442
31510
  const props = {
31443
31511
  ...rest,
31444
31512
  [TRANSITION_AREA_ATTRIBUTE]: ""
@@ -31453,8 +31521,9 @@ const RouteTransitionArea = ({
31453
31521
  * Declare how a pair of routes moves against each other — or how one route is
31454
31522
  * entered, from wherever its door happens to be.
31455
31523
  *
31456
- * @param {object|null} from - a route, or `{ route, params }` when the page is
31457
- * a param of a route rather than a route of its own. `null` says the page is
31524
+ * @param {object|null} from - a route (or the `routeFallback()` page), or
31525
+ * `{ route, params }` when the page is a param of a route rather than a route
31526
+ * of its own. `null` says the page is
31458
31527
  * reached FROM ANYWHERE: its door is in the furniture (a gear in the top
31459
31528
  * bar, a "+" in the tab bar) rather than on a screen, so there is no pair to
31460
31529
  * write it on. Arriving at `to` from any page then plays forward and leaving
@@ -31502,13 +31571,10 @@ const RouteTransitionArea = ({
31502
31571
  * Whatever is written here is what EVERY crossing of the pair plays. One
31503
31572
  * crossing can ask for something else — `<Link routeTransition>`, or
31504
31573
  * navTo(url, { routeTransition }) — which overrides this field by field, for
31505
- * that
31506
- * navigation alone.
31574
+ * that navigation alone.
31507
31575
  * @returns {() => void} remove this relation.
31508
31576
  */
31509
31577
  const defineRouteTransition = (from, to, transition) => {
31510
- import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
31511
- installTransitionWindowCss();
31512
31578
  if (!to) {
31513
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.`);
31514
31580
  }
@@ -31548,8 +31614,6 @@ const defineRouteTransition = (from, to, transition) => {
31548
31614
  * @returns {() => void} remove this default.
31549
31615
  */
31550
31616
  const defineRouteDefaultTransition = transition => {
31551
- import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
31552
- installTransitionWindowCss();
31553
31617
  const value = normalizeTransition(transition);
31554
31618
  defaultTransition = value;
31555
31619
  return () => {
@@ -31586,9 +31650,8 @@ const normalizeTransition = transition => {
31586
31650
  *
31587
31651
  * Two mouths, one meaning: the element being pressed wears it (a `<Link
31588
31652
  * routeTransition>`, or the attribute by hand on any anchor), or navTo() is
31589
- * handed
31590
- * it. Both arrive here through the announcement the navigation makes before it
31591
- * writes anything (see before_routing.js).
31653
+ * handed it. Both arrive here through the announcement the navigation makes
31654
+ * before it writes anything (see before_routing.js).
31592
31655
  *
31593
31656
  * A request answers FIELD BY FIELD: what it does not say, the relation — or
31594
31657
  * the default — still answers for. So `{ direction: "back" }` keeps the pair's
@@ -32132,6 +32195,10 @@ const beginTransition = ({
32132
32195
  // Said before the picture is taken: whoever names something for a movement
32133
32196
  // between two pages decides on it now (see transition_destination.js).
32134
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"];
32135
32202
  documentElement.setAttribute(TRANSITION_ATTRIBUTE, direction);
32136
32203
  if (type) {
32137
32204
  documentElement.setAttribute(TRANSITION_TYPE_ATTRIBUTE, type);
@@ -32153,6 +32220,10 @@ const beginTransition = ({
32153
32220
  // left (see transition_window.js).
32154
32221
  let areaStateBefore = null;
32155
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();
32156
32227
  documentElement.setAttribute(TRANSITION_TARGET_ATTRIBUTE, "area");
32157
32228
  // Said before the picture is taken, like every name (see
32158
32229
  // transition_furniture.js): what the bars are wearing when the transition
@@ -32835,10 +32906,10 @@ const css$11 = /* css */`.navi_route_travel {
32835
32906
  * The pages are cut at the edge of this box while they travel, and at the app's
32836
32907
  * safe area the box runs under, which is written on the transition's own
32837
32908
  * pseudo-elements — no overflow of the document reaches pictures drawn in the
32838
- * top layer. It needs nothing of the browser beyond view
32839
- * transitions themselves: a browser without them (Firefox) navigates without the
32840
- * movement, and the gesture applies its change on release instead of dragging a
32841
- * picture that does not exist.
32909
+ * top layer. It needs nothing of the browser beyond view transitions
32910
+ * themselves: a browser without them navigates without the movement, and the
32911
+ * gesture applies its change on release instead of dragging a picture that does
32912
+ * not exist.
32842
32913
  *
32843
32914
  * While a travel plays, the rest of the page is taken as a picture too — this
32844
32915
  * box asks for `view-transition-name: root` back for that time, so an
@@ -34429,9 +34500,6 @@ const onNaviCommand = (e, { debugCommand = () => {} } = {}) => {
34429
34500
  `targeting`,
34430
34501
  commandTarget,
34431
34502
  );
34432
- // Timed once, for the wheel-in-dialog "Définir feels frozen on mobile" case;
34433
- // the line is gone now that the answer is known — a command that runs in 1ms
34434
- // said nothing, and it said it on every single interaction.
34435
34503
  return implementation();
34436
34504
  };
34437
34505
 
@@ -35076,8 +35144,8 @@ registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
35076
35144
  target,
35077
35145
  implementation: () => {
35078
35146
  // The popup reads `anchor` first and falls back to the source (see
35079
- // onnavi_request_open in popover.jsx), so saying nothing keeps the old
35080
- // behaviour: the element asking is the element opened against.
35147
+ // onnavi_request_open in popover.jsx): without an `anchor`, the element
35148
+ // asking is the element opened against.
35081
35149
  return dispatchCustomEvent(target, "navi_request_open", {
35082
35150
  event,
35083
35151
  source: resolveCommandProxySource(source),
@@ -37548,8 +37616,8 @@ const usePopupContentMount = (
37548
37616
  * are no-ops.
37549
37617
  *
37550
37618
  * Behavior:
37551
- * - No <dialog>/<details>/[popover] ancestor → runs like a normal
37552
- * useLayoutEffect with the provided deps.
37619
+ * - No <dialog>/<details>/[popover]/[aria-expanded] ancestor → runs like a
37620
+ * normal useLayoutEffect with the provided deps.
37553
37621
  * - Inside a closed/hidden ancestor → skips the initial run; instead runs
37554
37622
  * the callback once the ancestor opens — see @jsenv/dom's own
37555
37623
  * observeAncestorOpenState for exactly how that's detected, and why it
@@ -37563,8 +37631,9 @@ const usePopupContentMount = (
37563
37631
  * reveals nothing about them, so it does not re-run them either. See
37564
37632
  * isDisplayedDespiteClosedAncestor in @jsenv/dom.
37565
37633
  *
37566
- * The callback's second argument is always a `navi_displayed` CustomEvent,
37567
- * with `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
37634
+ * The callback is called with the element (`ref.current`), and a
37635
+ * `navi_displayed` CustomEvent as its second argument, with
37636
+ * `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
37568
37637
  * - No <dialog>/<details>/[popover]/[aria-expanded] ancestor at all →
37569
37638
  * `{ ancestor: document, ancestorType: "document" }`.
37570
37639
  * - Otherwise → `{ ancestor: <the matched element>, ancestorType: "dialog"
@@ -37729,6 +37798,8 @@ const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
37729
37798
  // see also https://github.com/preactjs/preact/issues/1255
37730
37799
 
37731
37800
 
37801
+ const NO_ELEMENT_REF = { current: null };
37802
+
37732
37803
  /**
37733
37804
  * Programmatic autofocus that runs after Preact layout effects are flushed.
37734
37805
  *
@@ -37781,13 +37852,12 @@ const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
37781
37852
  * the browser's built-in scroll-into-view that accompanies focus.
37782
37853
  * @param {boolean} [options.focusVisible]
37783
37854
  * Passed as `focusVisible` to `element.focus()`.
37784
- * @param {boolean} [options.autoFocusSelect]
37855
+ * @param {boolean} [options.autoSelect]
37785
37856
  * When true, also calls `element.select()` after focusing (useful for text inputs).
37786
- * @returns {Function} triggerAutofocus — can be called manually with a synthetic
37787
- * event to re-run the focus logic outside of the layout-effect lifecycle.
37857
+ * @returns {object} The attributes to spread on the element: `navi-autofocus`
37858
+ * (and `navi-autofocus-select`), with `autoFocus` set to undefined so the
37859
+ * native attribute never lands on the node.
37788
37860
  */
37789
- const NO_ELEMENT_REF = { current: null };
37790
-
37791
37861
  const useAutoFocus = (
37792
37862
  focusableElementRef,
37793
37863
  autoFocus,
@@ -37982,9 +38052,10 @@ document.body.addEventListener(
37982
38052
  * and nothing about it stops someone from starting a run on every row before
37983
38053
  * any of them comes back: a dozen requests in flight, a dozen rows waiting, and
37984
38054
  * a server asked to do a dozen things at once because a list happened to be
37985
- * long. The guard is what makes the surface say "not yet" instead — the rows
37986
- * that are not running go read-only and say what they are waiting for, and the
37987
- * next press is possible again the moment one of the runs comes back.
38055
+ * long. The guard is what makes the surface say "not yet" instead — the
38056
+ * controls that would start another run go read-only and say what they are
38057
+ * waiting for, and the next press is possible again the moment one of the runs
38058
+ * comes back.
37988
38059
  *
37989
38060
  * It counts runs, not values — which is what separates it from
37990
38061
  * `maxLengthGuard`, the one that says how many things a selection may HOLD.
@@ -38329,14 +38400,13 @@ const createControlGuard = (controller) => {
38329
38400
  };
38330
38401
 
38331
38402
  /**
38332
- * Orchestrates the three rule managers for a UI state controller.
38333
- *
38334
- * Instead of holding `controller.controlInteraction` + `controller.controlValidity`
38335
- * independently, a controller now has a single `controller.rules` object:
38403
+ * Orchestrates the rule managers of a UI state controller, held together in
38404
+ * one `controller.rules` object:
38336
38405
  *
38337
38406
  * controller.rules.callout — shared callout display manager
38338
38407
  * controller.rules.interaction — interactivity gate (disabled/readonly/busy)
38339
38408
  * controller.rules.validation — value validity gate (required/pattern/etc.)
38409
+ * controller.rules.guard — keystroke guard (charGuard/maxLengthGuard)
38340
38410
  *
38341
38411
  * The callout manager is created first so both interaction and validation
38342
38412
  * can use the same callout slot and the same lifecycle hooks.
@@ -40487,9 +40557,8 @@ const EMPTY_OBJECT = {};
40487
40557
  * child changes → we update picker input → navi_ui_state_change fires →
40488
40558
  * we see updatingRef=true → skip → no loop.
40489
40559
  *
40490
- * This removes the need for `command="--navi-update"` on controls placed
40491
- * inside the picker popup. It also means `commands.js` no longer has to
40492
- * manually re-dispatch to inner controls.
40560
+ * So a control placed inside the picker popup needs no
40561
+ * `command="--navi-update"` to reach the picker input.
40493
40562
  */
40494
40563
  const useUIFacadeStateController = (props, realUIStateController) => {
40495
40564
  const firstChildControllerRef = useRef(null);
@@ -40981,9 +41050,10 @@ const uiStateHoldsNothing = (uiState) => {
40981
41050
  * use `onInput` internally and route through `dispatchRequestAction` so the behavior
40982
41051
  * is consistent regardless of input type.
40983
41052
  *
40984
- * 2. Any field (text, checkbox, radio, picker…) can opt into debounce simply by passing
40985
- * a debounced action. The request-action event chain handles the timing centrally
40986
- * rather than each component having to manage its own debounce logic.
41053
+ * 2. Any field (text, checkbox, radio, picker…) can opt into debounce with
41054
+ * `actionDebounce` (ms). The input effect every field installs (input_effect.js)
41055
+ * handles the timing centrally rather than each component having to manage its
41056
+ * own debounce logic.
40987
41057
  */
40988
41058
  const NO_ACTION_YET = Symbol("no_action_yet");
40989
41059
 
@@ -41059,7 +41129,7 @@ const ControlgroupChildrenWrapper = ({
41059
41129
  });
41060
41130
 
41061
41131
  /**
41062
- * Core hook for interactive field components (InputText, InputCheckbox, etc.).
41132
+ * Core hook for interactive field components (InputTextual, InputCheckbox, etc.).
41063
41133
  *
41064
41134
  * Sets up the full field lifecycle:
41065
41135
  * - Creates a UI state controller that manages state divergence between props and user interactions
@@ -41070,7 +41140,7 @@ const ControlgroupChildrenWrapper = ({
41070
41140
  * - Handles constraint validation and message props
41071
41141
  *
41072
41142
  * All state changes route through DOM events on the field element so that
41073
- * external subscribers (e.g. useUIState, Selectable) receive every update.
41143
+ * external subscribers (e.g. useControlUIState, Selectable) receive every update.
41074
41144
  *
41075
41145
  * @returns {[controlRootProps, controlHostProps, { uiStateController }]}
41076
41146
  */
@@ -42163,10 +42233,10 @@ const useReadOnlyUncontrolled = (props, controlInfo) => {
42163
42233
  };
42164
42234
 
42165
42235
  /**
42166
- * Core hook for field group components (SelectableList, CheckboxList, etc.).
42236
+ * Core hook for field group components (List selectable, CheckboxGroup, etc.).
42167
42237
  * - Creates a UI group state controller that aggregates child states into one group state
42168
42238
  * - Binds the group's action to the aggregated state signal
42169
- * - Provides context to children: ParentUIStateController, FieldName, Disabled, ReadOnly,
42239
+ * - Provides context to children: ParentUIStateController, ControlName, Disabled, ReadOnly,
42170
42240
  * Required, Loading, Action, ActionRequester
42171
42241
  * - Overrides `onnavi_reset_ui_state` to cascade resets to all monitored children
42172
42242
  * by dispatching `navi_reset_ui_state` DOM events on each child's DOM element
@@ -44701,10 +44771,10 @@ const LoadingRectangleSvg = ({
44701
44771
  }) => {
44702
44772
  // The stroke is centered on the path, so half of it falls outside: drawing
44703
44773
  // at half the stroke width puts its outer edge exactly on the box, whatever
44704
- // the box. It used to be a share of the smaller dimension (3%, floored at
44705
- // 2px), which held the line further and further inside as the box grew — a
44706
- // caller placing the outline with `inset` got the distance it asked for on a
44707
- // field and something several pixels inward on a card. Nothing needs the
44774
+ // the box. Not a share of the smaller dimension (3%, floored at 2px): that
44775
+ // holds the line further and further inside as the box grows — a caller
44776
+ // placing the outline with `inset` gets the distance it asked for on a field
44777
+ // and something several pixels inward on a card. Nothing needs the
44708
44778
  // extra room: the svg is `overflow: visible` and whoever positions the
44709
44779
  // wrapper already reserves the half stroke.
44710
44780
  const margin = strokeWidth / 2;
@@ -45357,16 +45427,6 @@ const css$Z = /* css */`@layer navi {
45357
45427
  border-radius: .2em;
45358
45428
  }
45359
45429
 
45360
- &[data-capitalize] {
45361
- text-transform: capitalize;
45362
-
45363
- & .navi_text_sizer {
45364
- & .navi_text {
45365
- display: inline-block;
45366
- }
45367
- }
45368
- }
45369
-
45370
45430
  &[data-shrinkwrap] {
45371
45431
  display: inline-block;
45372
45432
  }
@@ -45389,12 +45449,6 @@ time.navi_text {
45389
45449
  }
45390
45450
 
45391
45451
  .navi_text {
45392
- &[data-capitalize] {
45393
- &:first-letter, & .navi_text_sizer_placeholder:first-letter, & .navi_text_sizer_overlay:first-letter {
45394
- text-transform: uppercase;
45395
- }
45396
- }
45397
-
45398
45452
  & .navi_text_sizer, & .navi_text_sizer_placeholder, & .navi_text_sizer_overlay {
45399
45453
  display: inherit;
45400
45454
  width: inherit;
@@ -45776,7 +45830,7 @@ const shouldInjectSpacingBetween = (left, right) => {
45776
45830
  * boldStable?: boolean,
45777
45831
  * shrinkWrap?: boolean,
45778
45832
  * capitalize?: boolean,
45779
- * selectRange?: string | [number, number],
45833
+ * selectRange?: string | [number, number] | [string, string],
45780
45834
  * childrenOutsideFlow?: import("ignore:preact").ComponentChildren,
45781
45835
  * [key: string]: any,
45782
45836
  * }>}
@@ -45842,11 +45896,14 @@ const shouldInjectSpacingBetween = (left, right) => {
45842
45896
  * text block from being wider than its content when inside a flex/grid container.
45843
45897
  *
45844
45898
  * @param {boolean} [capitalize]
45845
- * 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.
45846
45902
  *
45847
- * @param {string|[number,number]} [selectRange]
45848
- * Selects a portion of the text on mount. Pass a substring to search for, or
45849
- * a `[start, end]` character-offset tuple.
45903
+ * @param {string|[number,number]|[string,string]} [selectRange]
45904
+ * Selects a portion of the text on mount. Pass a substring to search for, a
45905
+ * `[start, end]` character-offset tuple, or a `[startText, endText]` pair of
45906
+ * substrings selecting from the first to the end of the second.
45850
45907
  *
45851
45908
  * @param {import("ignore:preact").ComponentChildren} [childrenOutsideFlow]
45852
45909
  * Rendered after children but outside the text spacing/flow logic. Used
@@ -45961,11 +46018,13 @@ const TextUI = props => {
45961
46018
  shrinkWrap,
45962
46019
  ...rest
45963
46020
  } = props;
46021
+ if (capitalize) {
46022
+ children = capitalizeFirstLetter(children);
46023
+ }
45964
46024
  const defaultSpace = preventSpaceUnderlines ? FAKE_SPACE : REGULAR_SPACE;
45965
46025
  const resolvedSpacing = spacing ?? defaultSpace;
45966
46026
  const boxProps = {
45967
46027
  "as": "span",
45968
- "data-capitalize": capitalize ? "" : undefined,
45969
46028
  "data-shrinkwrap": shrinkWrap ? "" : undefined,
45970
46029
  ...rest,
45971
46030
  ref,
@@ -46266,6 +46325,28 @@ const Icon = ({
46266
46325
  });
46267
46326
  };
46268
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
+
46269
46350
  /**
46270
46351
  * Where a link leads, asked for when the pointer or the focus arrives on it:
46271
46352
  * the moment the user signals a destination, before the press. Only what the
@@ -46716,7 +46797,7 @@ Object.assign(PSEUDO_CLASSES, {
46716
46797
  * @param {object} props
46717
46798
  * @param {string} [props.href] - Destination. Also the default `value`, and
46718
46799
  * (when `hrefFallback`) the default visible text.
46719
- * @param {import("../route.js").Route} [props.route] - Renders via `route`
46800
+ * @param {object} [props.route] - Renders via `route`
46720
46801
  * instead of a raw `href`: the URL is built from the route (see
46721
46802
  * `routeParams`) and "current" is derived from whether the route matches.
46722
46803
  * @param {object} [props.routeParams] - Params passed to `route.buildUrl`.
@@ -46740,14 +46821,14 @@ Object.assign(PSEUDO_CLASSES, {
46740
46821
  * (`navi_value`); defaults to `href`.
46741
46822
  * @param {boolean} [props.current] - Forces the "current" state on (otherwise
46742
46823
  * derived from the href/route).
46743
- * @param {import("../route.js").Route|import("../route.js").Route[]} [props.currentExcept] -
46824
+ * @param {object|object[]} [props.currentExcept] -
46744
46825
  * Route(s) inside this link's own that are NOT it: while one of them matches,
46745
46826
  * the link is not current even though its route still is. A bar entry
46746
46827
  * standing for a whole section, and one place under that section the entry
46747
46828
  * does not stand for — settings reached from everywhere and shown over
46748
46829
  * whatever the reader was on. Nothing about the routing changes: the url is
46749
46830
  * in the section, only the link stops claiming "you are here".
46750
- * @param {import("../route.js").Route|import("../route.js").Route[]} [props.currentAlso] -
46831
+ * @param {object|object[]} [props.currentAlso] -
46751
46832
  * Route(s) other than this link's own that are it: the link is current while
46752
46833
  * one of them matches. The other side of `currentExcept` — an entry standing
46753
46834
  * for more than the one page it opens. `currentExcept` wins over it.
@@ -47660,6 +47741,7 @@ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
47660
47741
  * currentIndicator?: boolean|"top"|"bottom"|"left"|"right",
47661
47742
  * currentIndicatorSlides?: boolean,
47662
47743
  * slideContainer?: string,
47744
+ * panelPosition?: "before"|"after",
47663
47745
  * }>}
47664
47746
  * @param {boolean|"top"|"bottom"|"left"|"right"} [props.currentIndicator] - the
47665
47747
  * bar that says which tab one is on, said once here rather than on every
@@ -47685,6 +47767,9 @@ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
47685
47767
  * container says which one is on screen, and pressing a tab travels there.
47686
47768
  * Tabs that are places in the same screen rather than pages of their own —
47687
47769
  * nothing is written to the URL and nothing is a link.
47770
+ * @param {"before"|"after"} [props.panelPosition] - turns the row into folder
47771
+ * tabs: the current tab and the panel it opens share one surface, and this
47772
+ * says which side of the row the panel sits on.
47688
47773
  */
47689
47774
  const Nav = ({
47690
47775
  children,
@@ -48807,6 +48892,8 @@ const TABS_ALIGN_TO_JUSTIFY_CONTENT = {
48807
48892
  * onChange?: (value: any) => void,
48808
48893
  * tabsPosition?: "top"|"bottom"|"left"|"right",
48809
48894
  * tabsAlign?: "start"|"center"|"end"|"stretch",
48895
+ * scrollablePage?: boolean,
48896
+ * maxLines?: number|false,
48810
48897
  * borderWidth?: string|number,
48811
48898
  * borderRadius?: string|number,
48812
48899
  * tabBorderRadius?: string|number,
@@ -48986,7 +49073,7 @@ const Binder = ({
48986
49073
  * position among its siblings.
48987
49074
  * @param {import("ignore:preact").ComponentChildren} [props.label] - What the tab
48988
49075
  * shows.
48989
- * @param {import("../route.js").Route} [props.route] - Makes the tab a `Link`
49076
+ * @param {object} [props.route] - Makes the tab a `Link`
48990
49077
  * to that route, and the binder follows the url: the open item is the one
48991
49078
  * whose route matches, no `value`/`onChange` needed.
48992
49079
  * @param {string} [props.href] - Same, for a plain url.
@@ -49425,6 +49512,9 @@ const FixedBarStyleCSSVars = {
49425
49512
  * `border={false}` for none.
49426
49513
  * @param {string|number} [props.maxWidth] - Keeps the bar lined up with a
49427
49514
  * content column narrower than the window (it stays centered).
49515
+ * @param {string|number} [props.padding=0] - Along the bar only: left/right on
49516
+ * a top or bottom bar, top/bottom on a side one. Across it, the size is
49517
+ * `height`/`width`.
49428
49518
  */
49429
49519
  const FixedBar = ({
49430
49520
  children,
@@ -50293,9 +50383,10 @@ const useExpandableContext = partName => {
50293
50383
  * height too. `"idle"` builds it in a browser idle moment after load.
50294
50384
  * `"while-opened"` throws the content away once the collapse
50295
50385
  * settles — after the closing animation, so it still plays on real
50296
- * content — and rebuilds it from scratch on every expansion. Whatever the
50297
- * value, intent on the UI part (pointer entering it, focus landing in it)
50298
- * builds the content ahead of the click.
50386
+ * content — and rebuilds it from scratch on every expansion. Under the
50387
+ * other three, intent on the UI part (pointer entering it, focus landing in
50388
+ * it) builds the content ahead of the click; `"while-opened"` content is
50389
+ * never warmed, it is built at the open.
50299
50390
  */
50300
50391
  const Expandable = props => {
50301
50392
  import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
@@ -51072,7 +51163,8 @@ const DARK_CONTRAST_LIGHTNESS_THRESHOLD = 0.65;
51072
51163
  * @param {string} accentColor - The accent color value. When it changes, attributes are recomputed.
51073
51164
  * @param {object} [options]
51074
51165
  * @param {string} [options.elementSelector] - CSS selector to find the element whose computed color is read.
51075
- * Defaults to the root element itself. Useful when the color is applied to a probe/child element.
51166
+ * Defaults to the root's own `data-visual-selector` attribute, else the root element itself.
51167
+ * Useful when the color is applied to a probe/child element.
51076
51168
  * @param {string} [options.colorProperty="backgroundColor"] - Computed style property to read (e.g. "color", "borderColor").
51077
51169
  */
51078
51170
  const useAccentColorAttributes = (
@@ -53117,16 +53209,18 @@ const css$L = /* css */`@layer navi {
53117
53209
  /**
53118
53210
  * Field — a semantic wrapper that connects a label to a form control.
53119
53211
  *
53120
- * It generates a stable `fieldId` (or accepts an explicit `id`) that is
53212
+ * It generates a stable `fieldId` (or accepts an explicit one) that is
53121
53213
  * automatically forwarded to the `Label` inside the field as `htmlFor` and to
53122
- * any interactive control (Picker, Input, …) as its `id`, so clicking the
53123
- * label focuses the control without requiring manual wiring.
53214
+ * the labelable control inside it (Input, Select, Picker, Button) as its `id`,
53215
+ * so clicking the label focuses the control without requiring manual wiring.
53124
53216
  *
53125
53217
  * It also tracks the readOnly / disabled / interactive state reported by its
53126
53218
  * child control and reflects it on the `Label` (dimmed color, cursor change).
53127
53219
  *
53128
53220
  * Props:
53129
- * id — optional explicit id used as the field id instead of the auto-generated one
53221
+ * fieldId — optional explicit field id instead of the auto-generated one (an `id`
53222
+ * is the wrapper's own DOM id)
53223
+ * as="label" — the field IS the label, wrapping its control: no id needed
53130
53224
  * flex="y" — vertical layout; automatically sets alignX="start" and data-vertical
53131
53225
  * children — any JSX; should contain a `Label` and a form control
53132
53226
  * ...rest — forwarded to the wrapping element (className, style, flex, spacing, …)
@@ -53333,6 +53427,7 @@ const InputUnitSlot = ({
53333
53427
  };
53334
53428
  const InputSlot = ({
53335
53429
  side,
53430
+ children,
53336
53431
  ...props
53337
53432
  }) => {
53338
53433
  const ctx = useContext(InputTextualContext);
@@ -53362,7 +53457,11 @@ const InputSlot = ({
53362
53457
  e.preventDefault();
53363
53458
  }
53364
53459
  },
53365
- ...props
53460
+ ...props,
53461
+ children: jsx(ControlIdContext.Provider, {
53462
+ value: undefined,
53463
+ children: children
53464
+ })
53366
53465
  });
53367
53466
  };
53368
53467
 
@@ -54827,9 +54926,9 @@ const InputWithListResolver = props => {
54827
54926
  };
54828
54927
 
54829
54928
  /**
54830
- * InputWithList — connects an input to a SelectableList via its id.
54929
+ * InputWithList — connects an input to a selectable List via its id.
54831
54930
  *
54832
- * Usage: <Input navi-list="my-list-id" /> next to <SelectableList id="my-list-id" />
54931
+ * Usage: <Input navi-list="my-list-id" /> next to <List selectable id="my-list-id">
54833
54932
  *
54834
54933
  * Behavior:
54835
54934
  * - ArrowDown / ArrowUp move the list's "current item" without moving focus
@@ -55197,11 +55296,14 @@ const useAutoSelectReadOnly = (props) => {
55197
55296
  *
55198
55297
  * - charGuard — restricts which characters can be typed, pasted, or set externally.
55199
55298
  * Accepts a preset name or a raw regex character class:
55200
- * "numeric" → digits only, sets inputMode="numeric" + pattern auto
55299
+ * true / "auto" → picked from the field: inputMode numeric/decimal, type tel/email
55300
+ * "numeric" → digits only, sets inputMode="numeric"
55301
+ * "decimal" → digits, minus, dot, comma, sets inputMode="decimal"
55201
55302
  * "alpha" → letters only
55202
55303
  * "alphanumeric" → letters and digits
55203
55304
  * "uppercase" → uppercase letters only
55204
55305
  * "tel" → phone chars (digits, +, -, parens, space), sets inputMode="tel"
55306
+ * "email" → what an email address is made of
55205
55307
  * "card" → credit card (digits and spaces), sets inputMode="numeric"
55206
55308
  * "hex" → hexadecimal digits
55207
55309
  * "pin" → numeric PIN, sets inputMode="numeric"
@@ -56272,18 +56374,13 @@ const commitSubtree = (controller, e) => {
56272
56374
  };
56273
56375
 
56274
56376
  /**
56275
- *
56276
- * Here we want the same behaviour as web standards:
56277
- *
56278
- * 1. When submitting the form URL does not change
56279
- * 2. When form submission id done user is redirected (by default the current one)
56280
- * (we can configure this using target)
56281
- * So for example user might be reidrect to a page with the resource he just created
56282
- * I could create an example where we would put a link on the page to let user see what he created
56283
- * but by default user stays on the form allowing to create multiple resources at once
56284
- * And an other where he is redirected to the resource he created
56285
- * 3. If form submission fails ideally we should display this somewhere on the UI
56286
- * right now it's just logged to the console I need to see how we can achieve this
56377
+ * <Form>: the <form> element as a navi control group. Its named fields
56378
+ * aggregate into one object; a submit checks their constraints, runs `action`
56379
+ * with that object when it changed (docs/form_changed.md), and `command` — or
56380
+ * the surface the form sits in — says what follows. The URL does not change on
56381
+ * a submit, and a failing action is drawn as an error callout on the button
56382
+ * that sent it (docs/error_handling.md). A form inside another form renders no
56383
+ * <form> element (FormNested).
56287
56384
  */
56288
56385
 
56289
56386
  const Form = props => {
@@ -56923,9 +57020,9 @@ const offsetToPx = (offset, box) => {
56923
57020
 
56924
57021
  // A press landing while the track is already travelling: what is playing is
56925
57022
  // sent home in a fifth of the time it has left, and the press it could not take
56926
- // yet is taken as soon as it lands. A press has to be FELT — nudging the pace
56927
- // of a travel already in flight (what this used to do) reads as "nothing
56928
- // happened", because the thing was moving before the click too. Getting there
57023
+ // yet is taken as soon as it lands. A press has to be FELT — merely nudging the
57024
+ // pace of a travel already in flight reads as "nothing happened", because the
57025
+ // thing was moving before the click too. Getting there
56929
57026
  // almost at once and setting off again is the click being answered.
56930
57027
  // Played out fast rather than cut short: ending it on the spot would jump.
56931
57028
  // Compounds, so two presses during one travel bring it home twice as sharply,
@@ -59523,13 +59620,12 @@ const SlideNavButton = ({
59523
59620
  autoFocus: "last-resort",
59524
59621
  icon: true,
59525
59622
  variant: "discrete"
59526
- // Takes the focus like any other button, on purpose. It used to refuse it
59623
+ // Takes the focus like any other button, on purpose. Do not refuse it
59527
59624
  // (mousedown.preventDefault) to keep the keyboard where the user had put
59528
- // it — but pressing a way out with nothing focused then left the keyboard
59529
- // on nothing at all: the travel below only hands the focus to the slide
59530
- // arriving when it was leaving a slide, so a click from document.body
59531
- // arrived on document.body, and the next Tab started from the top of the
59532
- // page rather than from what is on screen.
59625
+ // it: the travel below only hands the focus to the slide arriving when it
59626
+ // was leaving a slide, so a press with nothing focused would arrive on
59627
+ // document.body, and the next Tab would start from the top of the page
59628
+ // rather than from what is on screen.
59533
59629
  //
59534
59630
  // Letting it focus is both the plain behaviour of a button and what makes
59535
59631
  // the rest fall into place: the press lands on the chevron, the slide left
@@ -59546,12 +59642,6 @@ const SlideNavButton = ({
59546
59642
  })
59547
59643
  });
59548
59644
 
59549
- /**
59550
- * The way out of a slide, and the way into the next one. Nothing but the
59551
- * command plus the chevron that matches the travel: a row goes left/right, a
59552
- * column up/down — so the button points where the slide actually goes without
59553
- * the caller having to keep the two in sync.
59554
- */
59555
59645
  /**
59556
59646
  * A way out pointing where it goes. It says a direction, not a slide: what is
59557
59647
  * over there is the map's business, and moving a screen changes nothing here.
@@ -61193,7 +61283,6 @@ const createSwipeToClose = (side, { grip } = {}) => {
61193
61283
  panelEl.style.transform = "";
61194
61284
  panelEl.style.clipPath = "";
61195
61285
  panelEl.style.transitionProperty = "";
61196
- panelEl.style.userSelect = "";
61197
61286
  };
61198
61287
  // The resting value is written first and the animation only covers the way
61199
61288
  // to it: both end on the same number, so nothing is seen changing when the
@@ -61237,10 +61326,6 @@ const createSwipeToClose = (side, { grip } = {}) => {
61237
61326
  return false;
61238
61327
  }
61239
61328
  panelEl.style.transitionProperty = "none";
61240
- panelEl.style.userSelect = "none";
61241
- // What the first pixels of the gesture may have started selecting is
61242
- // not a selection, it is the beginning of this travel.
61243
- window.getSelection().removeAllRanges();
61244
61329
  // The way out is the only way there is anything: pulling the other way
61245
61330
  // leans against a wall and comes back.
61246
61331
  return {
@@ -61946,7 +62031,7 @@ const css$E = /* css */`
61946
62031
  --navi-app-width rather than 3vvw): the gap must read as a small
61947
62032
  margin around the dialog, and 3% of a 1500px window is a 45px gap
61948
62033
  around a 600px app. Identical to 3vvw until the app declares
61949
- --navi-app-max-width. */
62034
+ bands (--navi-app-max-width, or --navi-app-inset-* written by it). */
61950
62035
  --x-dialog-container-spacing: calc(0.03 * var(--navi-app-width));
61951
62036
 
61952
62037
  /* --navi-app-width, not --navi-vvw: a top-layer dialog is calibrated on
@@ -62663,7 +62748,7 @@ const css$E = /* css */`
62663
62748
  * within its container (the viewport for `layer="top"`, the positioned
62664
62749
  * ancestor for `layer="local"`) — Dialog is never anchored to a real
62665
62750
  * element for positioning purposes. Same grammar as `Popover`'s own
62666
- * `positionArea` (see `popup_shared.js`'s `parsePositionArea`): a single
62751
+ * `positionArea` (see `parsePositionArea` in @jsenv/dom): a single
62667
62752
  * compass token — `top`/`top-start`/`top-end`/`top-left`/`top-right`,
62668
62753
  * `right`/`right-start`/`right-end`, `bottom`/`bottom-start`/
62669
62754
  * `bottom-end`/`bottom-left`/`bottom-right`, `left`/`left-start`/
@@ -62682,12 +62767,12 @@ const css$E = /* css */`
62682
62767
  * between the dialog and the edges of its container, whatever its
62683
62768
  * `positionArea`: it both caps the dialog's own size (via
62684
62769
  * `--x-dialog-container-spacing`, written from this prop) and offsets a docked
62685
- * one from the edge it docks to. Accepts a spacing token ("s", "m"…), a
62686
- * number of pixels, or a viewport length — "appw"/"apph" being the app's own
62687
- * screen (the visual viewport, or the narrower one the app declared with
62688
- * --navi-app-max-width) and "vvw"/"vvh" the visual viewport itself, which
62689
- * shrinks when the mobile keyboard opens. Pass 0 for a dialog
62690
- * meant to sit flush (a side panel).
62770
+ * one from the edge it docks to. Accepts a number of pixels, a viewport
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
62773
+ * "vvw"/"vvh" the visual viewport itself, which shrinks when the mobile
62774
+ * keyboard opens — or a container length ("3cqw", the `layer="local"`
62775
+ * default). Pass 0 for a dialog meant to sit flush (a side panel).
62691
62776
  * @param {boolean} [props.backdrop=true] - Whether anything is laid between
62692
62777
  * the dialog and the page at all — asked before any question of what an
62693
62778
  * outside press does or how the backdrop is painted. `false` leaves the page
@@ -62700,10 +62785,12 @@ const css$E = /* css */`
62700
62785
  * needs. What it gives up is what only a modal gets natively: the hardware
62701
62786
  * back button no longer dismisses it. See docs/popup_backdrop.md.
62702
62787
  * @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="close"]
62703
- * - `"close"` closes the dialog on an outside press. `"capture"`/`"ignore"`
62704
- * both just absorb the press without closing (visually dimmed backdrop vs.
62705
- * not) — a dialog with a backdrop is modal one way or another, so there's
62706
- * always at least a press-absorbing backdrop regardless of this prop.
62788
+ * - `"close"` closes the dialog on an outside press, `"cancel"` closes it
62789
+ * as a cancel (what it holds reverts, as on Escape). `"capture"`/`"ignore"`
62790
+ * both just absorb the press without closing (`"capture"` paints the
62791
+ * blurred glass, `"ignore"` leaves the wall's paint alone) — a wall absorbs
62792
+ * the press whatever this prop says, natively for a modal and through the
62793
+ * sibling element for a local one.
62707
62794
  * "Outside" is the dialog's own border box; a see-through dialog whose box is
62708
62795
  * bigger than what it paints marks the difference with
62709
62796
  * `data-navi-popup-outside` (see docs/popup_backdrop.md).
@@ -62778,11 +62865,10 @@ const css$E = /* css */`
62778
62865
  * @param {string} [props.animationDuration] - Maps to
62779
62866
  * `--popup-animation-duration`.
62780
62867
  * @param {Element|{current: Element}|string} [props.anchor] - Never used for
62781
- * positioning (see this file's top comment), and ignored entirely unless
62782
- * `sizeFromAnchor` or `animation="lifting"` asks for it — the first sizes
62783
- * the dialog via the `--anchor-width`/`--anchor-height` CSS vars, the second
62784
- * makes the dialog come out of the anchor's own box. Used when the open
62785
- * itself names none — an anchor carried by the opening event
62868
+ * positioning (see this file's top comment); read by `sizeFromAnchor` (the
62869
+ * `--anchor-width`/`--anchor-height` CSS vars), by `animation="lifting"`
62870
+ * (the box the dialog comes out of) and by `mount`'s warm-on-intent. Used
62871
+ * when the open itself names none — an anchor carried by the opening event
62786
62872
  * (`e.detail.anchor`) is about that one opening and comes first; left out,
62787
62873
  * whatever triggered the open (`e.detail.source`). A string is resolved via
62788
62874
  * `document.getElementById` when the dialog opens — see popover.jsx's own
@@ -62844,6 +62930,9 @@ const css$E = /* css */`
62844
62930
  * focusable of its own.
62845
62931
  * - `"restore"` — the dialog stays out of the opening focus chain unless it
62846
62932
  * held focus when it closed.
62933
+ * - `false` — the dialog element itself is never where the focus lands; the
62934
+ * opening still hands the focus to what the dialog holds. Unlike
62935
+ * `Popover`'s `false`, it does not leave the keyboard where it was.
62847
62936
  * Wherever the keyboard is a virtual one (a touch device), the surface is
62848
62937
  * already what one arrives on: a popup is read before it is reached there, so
62849
62938
  * the focus only leaves it for something that asked by name (`autoFocus` on
@@ -63089,8 +63178,8 @@ const useDialogProps = props => {
63089
63178
  // `backdrop` — see this file's top comment.
63090
63179
  layer = "top",
63091
63180
  dockedOnSmallTouchScreen,
63092
- // Same grammar as Popover's own positionArea — see this file's top
63093
- // comment and popup_shared.js's parsePositionArea.
63181
+ // Same grammar as Popover's own positionArea — see parsePositionArea in
63182
+ // @jsenv/dom.
63094
63183
  positionArea: positionAreaProp,
63095
63184
  // A dialog docked against an edge must keep the same gap its own size cap
63096
63185
  // already guarantees a centered one — so this drives both (see
@@ -63130,8 +63219,8 @@ const useDialogProps = props => {
63130
63219
  sizing = "auto",
63131
63220
  animation,
63132
63221
  lift = "box",
63133
- // Inert unless sizeFromAnchor below (see this file's top comment) —
63134
- // Dialog's own positioning is never relative to it.
63222
+ // Never positioned against (see this file's top comment): read by
63223
+ // sizeFromAnchor, by a lift, and by the warm-on-intent of `mount`.
63135
63224
  anchor,
63136
63225
  // Where a lift comes back to, when that is no longer the box it came out
63137
63226
  // of. Read at the close, not kept from the opening — see
@@ -63141,8 +63230,8 @@ const useDialogProps = props => {
63141
63230
  // See this prop's own JSDoc above for why a dialog does not follow its
63142
63231
  // trigger's box by default.
63143
63232
  sizeFromAnchor = false,
63144
- // Same meaning as Popover's own prop, applied to the only thing an anchor
63145
- // can do here: sizing under sizeFromAnchor.
63233
+ // Same meaning as Popover's own prop, applied to what an anchor does here:
63234
+ // sizing under sizeFromAnchor, and a lift.
63146
63235
  anchorCustomEventDetail = "override",
63147
63236
  // Makes the dialog itself a valid focus target so
63148
63237
  // autoFocus="last-resort" below has somewhere to land when it contains
@@ -63201,8 +63290,8 @@ const useDialogProps = props => {
63201
63290
  // narrower one) — and the positioned ancestor for a local one, where
63202
63291
  // reading 3% of the screen gives an absurd gap inside a small box.
63203
63292
  isTopLayer ? "3appw" : "3cqw");
63204
- // "expand || expandX", the shorthand semantics Popup used to apply before
63205
- // handing them over — the docked default only applies when neither was said
63293
+ // `expand` is the shorthand for both axes; the docked default only applies
63294
+ // when neither was said.
63206
63295
  const expandXUnset = expand === undefined && expandXProp === undefined;
63207
63296
  const expandX = expandXUnset ? isDocked && DOCKED.expandX : Boolean(expand) || Boolean(expandXProp);
63208
63297
  const scrollCapture = scrollCaptureProp ?? (isDocked ? DOCKED.scrollCapture : false);
@@ -63941,8 +64030,8 @@ const useDialogProps = props => {
63941
64030
  // ("[navi-control='dialog'], [navi-control='popover']" — see openEffect
63942
64031
  // above), and --navi-open/--navi-close resolve their target this way.
63943
64032
  "navi-control": "dialog",
63944
- // The protocol every command target answers. It came with the control
63945
- // group before; a dialog is layout and still has to answer --navi-open,
64033
+ // The protocol every command target answers: a dialog is layout, not a
64034
+ // control, and still has to answer --navi-open,
63946
64035
  // --navi-close and --navi-toggle, which are dispatched here and do not
63947
64036
  // bubble.
63948
64037
  "onnavi_command": e => {
@@ -63964,14 +64053,12 @@ const useDialogProps = props => {
63964
64053
  "popover": isTopLayerPopover ? "manual" : undefined,
63965
64054
  "baseClassName": "navi_dialog",
63966
64055
  "pseudoClasses": DIALOG_PSEUDO_CLASSES,
63967
- // Distinguishes the two renderers for the CSS above (position: fixed
63968
- // vs. absolute) — positioning itself is entirely JS-driven now (see
63969
- // openEffect's own positionDialog above), no data-position-area
63970
- // attribute needed at all.
63971
64056
  // A popup scrolls, and asking Box for that overflow is also what lets what
63972
64057
  // it contains claim header/footer/body (see box.jsx) — a popup is always a
63973
64058
  // scrolling area, so it says so once, here.
63974
64059
  "overflow": "auto",
64060
+ // Tells the two renderers apart for the CSS above (position: fixed vs.
64061
+ // absolute); the placement itself is JS-driven (positionDialog).
63975
64062
  "data-layer": layer,
63976
64063
  // The sheet shape is live in CSS, not just a set of resolved defaults:
63977
64064
  // it is what withdraws the caller's --dialog-max-width (see the stylesheet
@@ -64475,8 +64562,8 @@ const css$D = /* css */`
64475
64562
  * whole viewport. A real `anchor` works with either.
64476
64563
  * @param {string} [props.positionArea="bottom"] - Where to place the popover
64477
64564
  * relative to its `anchor` (or its container, if there is none). Same
64478
- * grammar as `Dialog`'s own `positionArea` (see `popup_shared.js`'s
64479
- * `parsePositionArea`): a single compass token — `top`/`top-start`/
64565
+ * grammar as `Dialog`'s own `positionArea` (see `parsePositionArea` in
64566
+ * @jsenv/dom): a single compass token — `top`/`top-start`/
64480
64567
  * `top-end`/`top-left`/`top-right`, `right`/`right-start`/`right-end`,
64481
64568
  * `bottom`/`bottom-start`/`bottom-end`/`bottom-left`/`bottom-right`,
64482
64569
  * `left`/`left-start`/`left-end`, or `center`. A bare token means no
@@ -64505,8 +64592,9 @@ const css$D = /* css */`
64505
64592
  * See docs/popup_backdrop.md.
64506
64593
  * @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="ignore"]
64507
64594
  * - `"ignore"` (default): no backdrop at all, outside presses pass straight
64508
- * through. `"close"` closes the popover on an outside press. `"capture"`
64509
- * absorbs the press (dims the backdrop) without closing. Note this
64595
+ * through. `"close"` closes the popover on an outside press, `"cancel"`
64596
+ * closes it as a cancel (what it holds reverts, as on Escape). `"capture"`
64597
+ * absorbs the press (blurred glass) without closing. Note this
64510
64598
  * default differs from `Dialog`'s own (`"close"`) — a popover is
64511
64599
  * typically a lightweight, non-modal affordance. "Outside" is the popover's
64512
64600
  * own border box; a see-through popover whose box is bigger than what it
@@ -64540,9 +64628,11 @@ const css$D = /* css */`
64540
64628
  * popover so the page/container behind it can't scroll while it's open.
64541
64629
  * @param {boolean} [props.focusCapture] - Traps Tab navigation inside the
64542
64630
  * popover (see `focus_trap.js`).
64543
- * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`} [props.animation]
64544
- * - `true`/`"auto"` resolves to a concrete `"slide-from-*"` direction
64545
- * based on `positionArea`. Any other explicit value is used as-is.
64631
+ * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`|"expanding"|`expand-${string}`} [props.animation]
64632
+ * - `true`/`"auto"` resolves to `"scaling"` for a real anchor or a centered
64633
+ * `positionArea`, and to a concrete `"slide-from-*"` direction otherwise.
64634
+ * `"expanding"`/`"expand-*"` grow out of a real anchor (see popup_css.js).
64635
+ * Any other explicit value is used as-is.
64546
64636
  * @param {string} [props.animationDuration] - Maps to
64547
64637
  * `--popup-animation-duration`.
64548
64638
  * @param {Element|{current: Element}|string} [props.anchor] - The element the
@@ -64654,7 +64744,7 @@ const css$D = /* css */`
64654
64744
  * @param {object} [props.openController] - Advanced: an externally-owned
64655
64745
  * open controller (see `open_controller.js`) for a caller that wants to
64656
64746
  * drive open/close itself instead of `open`/`defaultOpen`/`onClose` (used
64657
- * by `picker_custom.jsx`/`side_panel.jsx`).
64747
+ * by `picker_custom.jsx`).
64658
64748
  * @param {"always"|"idle"|"from-first-open"|"while-opened"} [props.mount] - When
64659
64749
  * `children` are built and thrown away (see popup_content_mount.js).
64660
64750
  * `"from-first-open"` (the default) builds them on the first open and keeps
@@ -64966,12 +65056,10 @@ const usePopoverProps = props => {
64966
65056
  unfreezeSize(popoverEl);
64967
65057
  }
64968
65058
  }, [sizing]);
64969
- // The custom renderer's own starting-hidden state is a stylesheet default
64970
- // now (&:not([popover]) { display: none } on .navi_popover/
64971
- // .navi_popover_backdrop above) rather than set here imperatively — a
64972
- // plain div has no native default the way [popover]/<dialog> do, so
64973
- // leaving this to a layout effect meant an actual (if narrow) window
64974
- // where the browser could paint it visible before this ever ran.
65059
+ // The custom renderer starts hidden from the stylesheet ([navi-hidden] on
65060
+ // .navi_popover/.navi_popover_backdrop above), never from a layout effect:
65061
+ // a plain div has no native hidden default the way [popover]/<dialog> do,
65062
+ // and an effect leaves a window where the browser paints it visible first.
64975
65063
  // aria-expanded starts "false" the same way, but via a static literal
64976
65064
  // JSX prop on contentProps/backdropProps below instead of a layout
64977
65065
  // effect — see that prop's own comment for why a *constant* value there
@@ -64990,8 +65078,8 @@ const usePopoverProps = props => {
64990
65078
  openController.getElement = () => ref.current;
64991
65079
  openController.openEffect = e => {
64992
65080
  const popoverEl = ref.current;
64993
- // backdropEl is null when pressOutside is "ignore" —
64994
- // the backdrop isn't rendered at all in that case.
65081
+ // backdropEl is null when there is no wall (pressOutside "ignore", or
65082
+ // backdrop={false}): the backdrop isn't rendered at all then.
64995
65083
  const backdropEl = backdropRef.current;
64996
65084
  if (!popoverEl) {
64997
65085
  {
@@ -65052,10 +65140,8 @@ const usePopoverProps = props => {
65052
65140
  // needed here.
65053
65141
  const positionedAncestor = getPositionedParent(popoverEl);
65054
65142
  // Drives the via-attribute renderer's own position: fixed/absolute
65055
- // switch (see this file's top comment) — set here, well before any
65056
- // positioning/measurement runs, so there's no ordering subtlety to get
65057
- // wrong (unlike the CSS this replaced, which keyed off the resolved
65058
- // animation instead, known too late relative to the first measurement).
65143
+ // switch (see this file's top comment) — set here, before any
65144
+ // positioning/measurement runs, so the first measurement already sees it.
65059
65145
  // True only for a real anchor that itself scrolls with the document —
65060
65146
  // that's the one case `absolute` (scrolling in lockstep with it) is
65061
65147
  // correct; not just "has an anchor at all", since an anchor that's
@@ -65105,10 +65191,9 @@ const usePopoverProps = props => {
65105
65191
  disarmBackdropHideRef.current?.();
65106
65192
  disarmBackdropHideRef.current = null;
65107
65193
  // transitionProperty stays "none" here for both — reset later, in the
65108
- // final commit step alongside popoverEl's own (not resumed early the
65109
- // way it briefly was), so a `silent` open (see above) can keep both
65110
- // elements' transitions suppressed right up until after their
65111
- // aria-expanded flip, the same way it does for popoverEl.
65194
+ // final commit step alongside popoverEl's own, so a `silent` open (see
65195
+ // above) can keep both elements' transitions suppressed right up until
65196
+ // after their aria-expanded flip, the same way it does for popoverEl.
65112
65197
  backdropEl.style.transitionProperty = "none";
65113
65198
  if (isTopLayer) {
65114
65199
  // Hidden first if a previous close's deferred hidePopover() (see
@@ -65534,16 +65619,10 @@ const usePopoverProps = props => {
65534
65619
  // current unconditionally on every call, and treats their mere
65535
65620
  // *presence* as "already positioned this session — stay on this
65536
65621
  // side unless it no longer fits" (its own anti-oscillation guard for
65537
- // repositioning mid-open, e.g. on scroll/resize). Popover's own CSS
65538
- // doesn't read these anymore (slide direction is resolved in JS now,
65539
- // see resolveDirectionValue), which is why the clearing that used to
65540
- // live here was removed — but that removal missed this second,
65541
- // still-live purpose: without clearing them on close, a later reopen
65542
- // sees a stale, "already positioned" value left over from a
65543
- // *previous* open (e.g. the container-aligned collapse from an
65544
- // anchorless silent open) and wrongly stays sticky to it instead of
65545
- // resolving fresh from the current positionArea/anchor — a real,
65546
- // reproduced bug, not just a leftover no-op.
65622
+ // repositioning mid-open, e.g. on scroll/resize). Cleared on close so a
65623
+ // later reopen resolves fresh from the current positionArea/anchor
65624
+ // instead of staying sticky to a side left over from a previous open
65625
+ // (e.g. the container-aligned collapse from an anchorless silent open).
65547
65626
  popoverEl.removeAttribute("data-position-y-current");
65548
65627
  popoverEl.removeAttribute("data-position-x-current");
65549
65628
  cleanup();
@@ -65673,8 +65752,8 @@ const usePopoverProps = props => {
65673
65752
  // ("[navi-control='dialog'], [navi-control='popover']"), and
65674
65753
  // --navi-open/--navi-close resolve their target this way.
65675
65754
  "navi-control": "popover",
65676
- // The protocol every command target answers. It came with the control
65677
- // group before; a popover is layout and still has to answer --navi-open,
65755
+ // The protocol every command target answers: a popover is layout, not a
65756
+ // control, and still has to answer --navi-open,
65678
65757
  // --navi-close and --navi-toggle, which are dispatched here and do not
65679
65758
  // bubble.
65680
65759
  "onnavi_command": e => {
@@ -65714,7 +65793,7 @@ const usePopoverProps = props => {
65714
65793
  // already arrive here as plain props via ...rest (wired by
65715
65794
  // UncontrolledPopover above, forwarded through ControlledPopover's own
65716
65795
  // {...props} spread) — nothing extra to add here. A controlled caller
65717
- // (picker_custom.jsx/side_panel.jsx) wires its own equivalent handling
65796
+ // (picker_custom.jsx) wires its own equivalent handling
65718
65797
  // directly against its own openController instead.
65719
65798
  });
65720
65799
  return [hasBackdrop ? backdropProps : null, contentProps];
@@ -65746,9 +65825,6 @@ const POPUP_STYLE_CSS_VARS = {
65746
65825
  maxHeight: "--popover-max-height"
65747
65826
  };
65748
65827
 
65749
- // parsePositionArea/POSITION_AREA_X/Y_VALUES moved to popup_shared.js — same
65750
- // grammar Dialog's own layer="local"/"top" now shares.
65751
-
65752
65828
  /**
65753
65829
  * Shared by both renderers: parses `positionArea` and resolves
65754
65830
  * `animation="auto"`/`true`. `animationAnchor` is the real anchor element
@@ -65776,15 +65852,6 @@ const resolvePositionAreaAndAnimationKind = ({
65776
65852
  resolvedAnimationKind
65777
65853
  };
65778
65854
  };
65779
-
65780
- // resolveDirectionValue/resolveAutoAnimationKind moved to popup_shared.js —
65781
- // same logic Dialog's own auto-animation resolution now shares, since it
65782
- // never has a real anchor either (always the "anchor === undefined" path).
65783
-
65784
- // suppressPointerEventsDuringTransition/armPointerDownOutsideClose moved to
65785
- // popup_shared.js — same helpers Dialog's own custom renderer needs, no
65786
- // Popover-specific knowledge in either.
65787
-
65788
65855
  Popover.Close = PopupClose;
65789
65856
 
65790
65857
  installImportMetaCssBuild(import.meta);
@@ -65823,11 +65890,12 @@ const css$C = /* css */`@layer navi {
65823
65890
  * @param {"top"|"local"} [props.layer] - Forwarded as-is to whichever of
65824
65891
  * `Dialog`/`Popover` renders — see either component's own doc.
65825
65892
  * @param {Element|{current: Element}} [props.anchor] - Forwarded as-is —
65826
- * positioning for `Popover`, and for `Dialog` sizing only, and only when
65827
- * `sizeFromAnchor` is also passed (see each component's own doc).
65893
+ * positioning for `Popover`; for `Dialog`, the box it sizes from under
65894
+ * `sizeFromAnchor` and lifts out of under `animation="lifting"`. In both,
65895
+ * intent on the anchor warms the content (see `mount`).
65828
65896
  * @param {"override"|"ignore"} [props.anchorCustomEventDetail] - Forwarded
65829
65897
  * as-is to both — what it governs differs (positioning for `Popover`,
65830
- * `sizeFromAnchor` sizing for `Dialog`), but "ignore whatever anchor the
65898
+ * sizing and lifting for `Dialog`), but "ignore whatever anchor the
65831
65899
  * triggering event carried" has to mean the same thing in either mode.
65832
65900
  * @param {string} [props.marginWithAnchor] - **Popover-only**, destructured
65833
65901
  * out so it can't leak onto the real `<dialog>` element as a stray DOM
@@ -65845,6 +65913,8 @@ const css$C = /* css */`@layer navi {
65845
65913
  * `maxWidth` below.
65846
65914
  * @param {boolean} [props.sizeFromAnchor] - **Dialog-only**, same guard.
65847
65915
  * @param {"box"|"scene"} [props.lift] - **Dialog-only**, same guard.
65916
+ * @param {Element|{current: Element}|string} [props.liftAnchor] -
65917
+ * **Dialog-only**, same guard.
65848
65918
  * @param {string} [props.positionArea] - Forwarded as-is — `Dialog` and
65849
65919
  * `Popover` have different own defaults (`"center"` vs. `"bottom"`),
65850
65920
  * deliberately not homogenized here (each reads best for its own typical
@@ -65857,8 +65927,9 @@ const css$C = /* css */`@layer navi {
65857
65927
  * the screen-size check happens to pick, which defeats the point of
65858
65928
  * having one shared API in the first place. Note this only says what a
65859
65929
  * press outside *does*; whether it reaches the page at all is `backdrop`
65860
- * below, and `"ignore"`/`"capture"` describe a wall either way — the popup
65861
- * absorbs the press without closing, dimmed or not.
65930
+ * below. `"capture"` is a wall that absorbs the press without closing, in
65931
+ * either mode. `"ignore"` is not the same in both: a `Dialog` keeps its
65932
+ * wall, a `Popover` lays none and the press reaches the page.
65862
65933
  * @param {boolean} [props.backdrop] - Whether anything is laid between the
65863
65934
  * popup and the page at all: `false` lets a press outside both dismiss the
65864
65935
  * popup and reach whatever it landed on, in one gesture. Forwarded as-is and
@@ -65877,8 +65948,13 @@ const css$C = /* css */`@layer navi {
65877
65948
  * identically): the wash the backdrop paints over what is behind.
65878
65949
  * @param {string} [props.backdropFilter] - Forwarded as-is: what that wash
65879
65950
  * does to the picture underneath, `"blur(4px)"` and the like.
65880
- * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|"expanding"|`slide-from-${string}`|`expand-${string}`} [props.animation]
65881
- * - 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.
65882
65958
  * @param {string} [props.animationDuration] - Forwarded as-is.
65883
65959
  * @param {string} [props.maxWidth] - Forwarded as-is to both; also read
65884
65960
  * here directly to help decide the automatic `mode` (a fixed length under
@@ -65898,11 +65974,15 @@ const css$C = /* css */`@layer navi {
65898
65974
  * `--x-popover-max-width`) instead of its content width — same meaning
65899
65975
  * whichever mode the screen-size resolution picks.
65900
65976
  * @param {boolean} [props.expandY] - Same, vertically (`data-expand-y`).
65901
- * @param {boolean} [props.scrollCapture] - Forwarded as-is.
65977
+ * @param {boolean|"dialog"|"popover"} [props.scrollCapture] - Forwarded to
65978
+ * both; a mode name traps scroll only when that mode is the one rendered.
65902
65979
  * @param {boolean} [props.open] - Forwarded as-is (controlled).
65903
- * @param {import("@preact/signals").Signal<boolean>} [props.signal] -
65980
+ * @param {import("@preact/signals").Signal<any>} [props.signal] -
65904
65981
  * Forwarded as-is: one binding to both drive the popup's open state and
65905
65982
  * know where it is (see `Dialog`/`Popover`'s own `signal`).
65983
+ * @param {any} [props.value] - Forwarded as-is: what `signal` holds while
65984
+ * THIS popup is open, for several sharing one signal (see
65985
+ * `Dialog`/`Popover`'s own `value`).
65906
65986
  * @param {boolean} [props.defaultOpen] - Forwarded as-is (uncontrolled,
65907
65987
  * mount-only).
65908
65988
  * @param {(event: Event) => void} [props.onClose] - Forwarded as-is.
@@ -65917,6 +65997,8 @@ const css$C = /* css */`@layer navi {
65917
65997
  * them afterwards. `"always"` builds them right away, for content something
65918
65998
  * depends on while the popup is still closed: a value read off it, fields a
65919
65999
  * surrounding form collects on submit, a size measured from outside.
66000
+ * `"idle"` builds them in a browser idle moment after load — `"always"`
66001
+ * minus the cost on the critical render.
65920
66002
  * `"while-opened"` throws them away once the popup has finished closing, for
65921
66003
  * content whose fresh state is its initial state: an uncontrolled field
65922
66004
  * seeded from a `defaultValue` that changed while the popup was closed.
@@ -65938,6 +66020,8 @@ const Popup = props => {
65938
66020
  // mode the automatic screen-size resolution happens to pick.
65939
66021
  pressOutside = "close",
65940
66022
  backdrop,
66023
+ // A popover is handed only the values it understands (toPopoverAnimation).
66024
+ animation,
65941
66025
  // Popover-only (see this component's own doc) — destructured out so
65942
66026
  // they're never part of ...rest, and therefore never forwarded to
65943
66027
  // Dialog below, where they'd otherwise leak onto the real <dialog>
@@ -65952,6 +66036,7 @@ const Popup = props => {
65952
66036
  dockedOnSmallTouchScreen,
65953
66037
  sizeFromAnchor,
65954
66038
  lift,
66039
+ liftAnchor,
65955
66040
  ...rest
65956
66041
  } = props;
65957
66042
  const [mode] = useResolvedPopupMode(modeProp, maxWidth, {
@@ -65971,6 +66056,8 @@ const Popup = props => {
65971
66056
  dockedOnSmallTouchScreen: dockedOnSmallTouchScreen,
65972
66057
  sizeFromAnchor: sizeFromAnchor,
65973
66058
  lift: lift,
66059
+ liftAnchor: liftAnchor,
66060
+ animation: animation,
65974
66061
  maxWidth: maxWidth,
65975
66062
  pressOutside: pressOutside,
65976
66063
  backdrop: backdrop,
@@ -65984,6 +66071,7 @@ const Popup = props => {
65984
66071
  }
65985
66072
  return jsx(Popover, {
65986
66073
  ...rest,
66074
+ animation: toPopoverAnimation(animation),
65987
66075
  maxWidth: maxWidth,
65988
66076
  pressOutside: pressOutside,
65989
66077
  backdrop: backdrop,
@@ -66001,6 +66089,18 @@ const Popup = props => {
66001
66089
  };
66002
66090
  Popup.Close = PopupClose;
66003
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
+
66004
66104
  installImportMetaCssBuild(import.meta);
66005
66105
  const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
66006
66106
  display: none;
@@ -66515,7 +66615,7 @@ const PickerCustom = props => {
66515
66615
  // Not on pickerProps (the trigger): commands.js's own
66516
66616
  // resolveClosestExpandable() does `el.closest("[aria-expanded]")` to
66517
66617
  // find where to dispatch navi_request_open/navi_request_close — and
66518
- // the popup itself now carries its own aria-expanded (see
66618
+ // the popup itself carries its own aria-expanded (see
66519
66619
  // popover.jsx/dialog.jsx), which is *closer* than the picker's own
66520
66620
  // aria-expanded for anything dispatched from inside the popup's own
66521
66621
  // content (e.g. a `command="--navi-close"` button rendered as
@@ -66795,12 +66895,8 @@ const PickerContentInsidePopup = props => {
66795
66895
  mode,
66796
66896
  pointerLock,
66797
66897
  scrollCapture,
66798
- // No default here (matches Popover's own default of inactive) — the
66799
- // old, differently-named `focusTrap = true` prop never actually reached
66800
- // Popover's real `focusCapture` prop (see this file's history), so
66801
- // focus-trapping has never really been active for popover-mode pickers;
66802
- // defaulting the now-correctly-named prop to `true` would be a real,
66803
- // unintended behavior change riding along with the rename.
66898
+ // No default here: Popover's own `focusCapture` is inactive by default,
66899
+ // and a popover-mode picker traps the focus only when asked to.
66804
66900
  focusCapture,
66805
66901
  // Popup documents its own `layer` as forwarded as-is to Dialog/Popover,
66806
66902
  // but popupProps is built explicitly here, so it only travels if named.
@@ -67329,9 +67425,14 @@ const PickerNaviMinute = props => {
67329
67425
  step,
67330
67426
  value
67331
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.
67332
67432
  return jsx(Next, {
67333
67433
  ...props,
67334
- type: "minute",
67434
+ type: "text",
67435
+ "navi-input-type": "minute",
67335
67436
  children: jsxs(Box, {
67336
67437
  flex: "y",
67337
67438
  spacing: "s",
@@ -67437,6 +67538,8 @@ document.addEventListener(
67437
67538
  { capture: true, passive: true },
67438
67539
  );
67439
67540
 
67541
+ const FRAME_TIMEOUT_MS = 100;
67542
+
67440
67543
  /**
67441
67544
  * Calls back once the browser has painted what is committed now.
67442
67545
  *
@@ -67448,10 +67551,9 @@ document.addEventListener(
67448
67551
  * has to survive. A frame not coming at all (a background tab) still answers,
67449
67552
  * late, through the timeout.
67450
67553
  *
67554
+ * @param {() => void} callback
67451
67555
  * @returns {() => void} cancel
67452
67556
  */
67453
- const FRAME_TIMEOUT_MS = 100;
67454
-
67455
67557
  const afterPaint = (callback) => {
67456
67558
  let called = false;
67457
67559
  let timeoutId;
@@ -68998,7 +69100,7 @@ const SELECTABLE_INPUT_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-
68998
69100
  const SelectableInputProxy = props => {
68999
69101
  const selectableRealInputProps = useContext(SelectableRealInputContext);
69000
69102
  if (!selectableRealInputProps) {
69001
- throw new Error("Selectable.Input must be used within a Selectable component");
69103
+ throw new Error("SelectableInput must be used within a row of a selectable List");
69002
69104
  }
69003
69105
 
69004
69106
  // Reset FieldToInterfaceContext to ensure we don't read id or report our
@@ -69036,7 +69138,7 @@ const SelectableInput = SelectableInputProxy;
69036
69138
  * background-color: var(--search-match-background-color);
69037
69139
  * }
69038
69140
  *
69039
- * The `highlight` prop can be:
69141
+ * The `highlight` argument of `useSearchHighlight` (a row's `matchInfo.matchRanges`) can be:
69040
69142
  * - an array of [start, end] pairs — applied to all text nodes under the root element
69041
69143
  * - an object { [domSelector]: [[start, end], …] } — applied to each sub-element
69042
69144
  * matched by the selector (the format produced by createSearch)
@@ -72584,8 +72686,8 @@ const LIST_ITEM_STYLE_CSS_VARS = {
72584
72686
  *
72585
72687
  * All three are about the row as a thing the LIST holds, never about the
72586
72688
  * selection: a selectable row taken while its list sends says so on its own
72587
- * ("la sélection est en cours d'enregistrement"), and needs no `loading` for
72588
- * that.
72689
+ * ("le choix est en cours d'enregistrement", "la sélection…" in a `multiple`
72690
+ * list), and needs no `loading` for that.
72589
72691
  * @param {boolean} [props.readOnly]
72590
72692
  * The row cannot be acted on: dimmed and click-through-proof, buttons inside
72591
72693
  * it included.
@@ -73680,9 +73782,12 @@ const useItemStore = ({
73680
73782
  * (accessible via aria-labelledby) and a <ul role="group"> for the items.
73681
73783
  *
73682
73784
  * Props:
73683
- * label — group label content
73684
- * labelProps — props forwarded to the label <span>
73685
- * ...rest — forwarded to the outer <li role="presentation">
73785
+ * label — group label content
73786
+ * labelProps — props forwarded to the label <span>
73787
+ * hiddenWhileEmpty — the group leaves the flow (`display: none`) while it
73788
+ * holds no real row — a search that emptied it, rows not
73789
+ * arrived yet
73790
+ * ...rest — forwarded to the outer <li role="presentation">
73686
73791
  */
73687
73792
  const ListItemGroup = ({
73688
73793
  label,
@@ -73794,7 +73899,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
73794
73899
  * uiAction?: (value: any) => void,
73795
73900
  * popover?: boolean,
73796
73901
  * role?: string,
73797
- * renderBudget?: number | string,
73902
+ * renderBudget?: number | string | {initial?: number, after: number},
73798
73903
  * renderBudgetSkipCheck?: boolean,
73799
73904
  * virtualItemSize?: number,
73800
73905
  * onListVisibleItemsChange?: (visibleItems: any[]) => void,
@@ -73823,6 +73928,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
73823
73928
  * alignX?: string,
73824
73929
  * alignY?: string,
73825
73930
  * flexWrap?: boolean,
73931
+ * overflow?: string,
73826
73932
  * expandX?: boolean,
73827
73933
  * expandY?: boolean,
73828
73934
  * expand?: boolean,
@@ -74009,10 +74115,10 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
74009
74115
  * @param {number} [props.parallelGuard=4]
74010
74116
  * How many runs the rows may have in flight at once, for a list whose rows
74011
74117
  * carry their own `action` (a button per row). While that many are out, every
74012
- * row that is not running goes read-only and says how many it is waiting on;
74013
- * the next press is possible again as soon as one comes back. `Infinity`
74014
- * lifts it. Counts runs, not values — `maxLengthGuard` above is the one that
74015
- * says how many things the selection may hold.
74118
+ * control that would start another run goes read-only and says how many it is
74119
+ * waiting on; the next press is possible again as soon as one comes back.
74120
+ * `Infinity` lifts it. Counts runs, not values — `maxLengthGuard` above is
74121
+ * the one that says how many things the selection may hold.
74016
74122
  * @param {boolean} [props.standalone]
74017
74123
  * This list answers for itself: it does not register with the control group
74018
74124
  * or picker around it, so its selection stays out of that value and nothing
@@ -76481,7 +76587,7 @@ const PickerFirstResolver = props => {
76481
76587
  * `focusOnOpen` below.
76482
76588
  *
76483
76589
  * @type {import("ignore:preact").FunctionComponent<{
76484
- * 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",
76485
76591
  * value?: any,
76486
76592
  * defaultValue?: any,
76487
76593
  * name?: string,
@@ -76547,7 +76653,7 @@ const PickerFirstResolver = props => {
76547
76653
  * popupTestId?: string,
76548
76654
  * confirmTestId?: string,
76549
76655
  * cancelTestId?: string,
76550
- * mount?: "always" | "from-first-open" | "while-opened",
76656
+ * mount?: "always" | "idle" | "from-first-open" | "while-opened",
76551
76657
  * dialogExpand?: boolean,
76552
76658
  * dialogExpandX?: boolean,
76553
76659
  * dialogExpandY?: boolean,
@@ -76984,8 +77090,8 @@ const PickerFirstResolver = props => {
76984
77090
  * itself names the trigger (see docs/testid.md); this one is for a test that
76985
77091
  * needs the popup's own box — its surface, its position, its backdrop — and
76986
77092
  * not only what it holds, which a testid inside the children already names.
76987
- * A `mode="callout"` picker has no popup of its own — the callout is the
76988
- * control's, shared with its constraint messages — so it is not named.
77093
+ * Under `mode="callout"` it names the callout element — the control's own,
77094
+ * shared with its constraint messages — while the children are shown in it.
76989
77095
  * @param {string} [confirmTestId] `type="confirm"`, default body: the
76990
77096
  * `data-testid` of the yes button; `cancelTestId` that of the no button.
76991
77097
  * Both are elements navi builds, which a test may not name by class (see
@@ -77373,7 +77479,7 @@ const TRAVELS_NONE = [];
77373
77479
  * fallbackValue?: any,
77374
77480
  * valueAtStep: (value: any, count: number) => any,
77375
77481
  * compareValues?: (a: any, b: any) => number,
77376
- * renderValue?: (value: any) => import("ignore:preact").ComponentChildren,
77482
+ * renderValue?: (value: any, context: { maxLines?: number }) => import("ignore:preact").ComponentChildren,
77377
77483
  * controlProps?: object,
77378
77484
  * duration?: number,
77379
77485
  * vertical?: boolean,
@@ -78848,7 +78954,8 @@ const css$q = /* css */`.navi_input_duration {
78848
78954
  * @param {boolean} [props.required]
78849
78955
  * @param {boolean} [props.loading]
78850
78956
  * @param {Function} [props.uiAction] - Called on every change with the ISO 8601 value
78851
- * @param {Function} [props.action] - Called on form submission
78957
+ * @param {Function} [props.action] - Runs with the ISO 8601 value when it changes,
78958
+ * like any control's `action` (busy state, error callout)
78852
78959
  * @param {preact.ComponentChild} [props.unitHour] - Custom label for the hour sub-field
78853
78960
  * @param {"auto"|"left"|"center"|"right"} [props.textAlign="auto"] - Text alignment of sub-inputs.
78854
78961
  * "auto" aligns each field toward its neighbouring separator (first→right, last→left, middle/solo→center).
@@ -79859,11 +79966,12 @@ const splitPopupProps = props => {
79859
79966
  * multi-word, one word at start (all match) 0.75
79860
79967
  * phrase / word at word boundary 0.625
79861
79968
  * phrase / words mid-word 0.5
79969
+ * acronym (2+ chars, each the first letter of a word, in order) 0.4, 0.45 at start
79862
79970
  * + case-exact bonus +0.125
79863
79971
  * multi-word partial: score × (matched/total)
79864
79972
  *
79865
79973
  * matchRanges: [start, end] pairs (exclusive end) for CSS Highlight API.
79866
- * Intended to be passed to useSearch as the matchFn parameter.
79974
+ * Intended to be passed to useSearchText as the matchFn parameter.
79867
79975
  */
79868
79976
  const applySearch = (searchText, value) => {
79869
79977
  if (!searchText) {
@@ -80073,7 +80181,7 @@ const mergeRanges = (ranges) => {
80073
80181
  };
80074
80182
 
80075
80183
  /**
80076
- * createSearch — builds a matchFn compatible with useSearch that searches
80184
+ * createSearch — builds a matchFn compatible with useSearchText that searches
80077
80185
  * across multiple named fields of an item, each with its own DOM selector
80078
80186
  * and optional priority weight.
80079
80187
  *
@@ -80091,7 +80199,7 @@ const mergeRanges = (ranges) => {
80091
80199
  * },
80092
80200
  * });
80093
80201
  *
80094
- * const [orderedItems, getItemMatchInfo] = useSearch(search, items, searchPerson);
80202
+ * const [orderedItems, getItemMatchInfo] = useSearchText(search, items, searchPerson);
80095
80203
  * // getItemMatchInfo(item).matchRanges is { ".name": [[start,end],…], ".address": [[start,end],…] }
80096
80204
  * // Pass the whole thing: <ListItem matchInfo={getItemMatchInfo(item)} />
80097
80205
  * // — ListItem handles the per-selector object format for matchRanges.
@@ -80158,7 +80266,7 @@ const useSearchText = (searchText, items, matchFn = applySearch) => {
80158
80266
  );
80159
80267
  }
80160
80268
  if (items === undefined) {
80161
- throw new TypeError("useSearch: items is undefined");
80269
+ throw new TypeError("useSearchText: items is undefined");
80162
80270
  }
80163
80271
  const { orderedItems, matchInfoMap } = useMemo(() => {
80164
80272
  const { scoreEntries, nonMatched, matchInfoMap } = buildMatchInfo(
@@ -80283,17 +80391,19 @@ const buildMatchInfo = (searchText, items, matchFn) => {
80283
80391
  * - orderedKeys: number[] of visible item keys sorted by explicit order
80284
80392
  * - keyToOrderedIndex: Map key → orderedKeys index, gives O(1) indexOf equivalent
80285
80393
  * - keyToExplicitOrder: Map key → explicitly passed index, used to maintain sort order
80286
- * - allItemsSignal: signal(array), all items including hidden, ordered by explicit index
80287
- * - visibleItemsSignal: signal(array), non-hidden items only
80394
+ * - itemsSignal: signal(array), all items including hidden, ordered by explicit index
80395
+ * - visibleItemsSignal: signal(array), items neither hidden nor filtered
80288
80396
  * - countSignal: signal(number), count of all items including hidden
80289
80397
  * - visibleCountSignal: signal(number), updated in microtask batch, only when count changes
80290
- * - propSignals: Map propName → signal(array), updated in microtask batch with element equality
80398
+ * - noMatchCountSignal: signal(number), items registered with `match: false`
80291
80399
  * - onChangeRef: holds the latest onChange callback, called once per microtask batch
80292
80400
  *
80293
- * useTrackItem(id, data, index): registers the item with an explicitly provided index
80294
- * that determines its position among siblings. The caller (e.g. items.map) knows the
80295
- * correct order and passes it directly — no render-sequence deduction needed.
80296
- * Returns the visible rank (position among non-hidden items), or -1 when hidden.
80401
+ * useTrackItem(data): registers the item under `data.id`, at the position
80402
+ * `data.index` gives it among its siblings. The caller (e.g. items.map) knows
80403
+ * the correct order and passes it directly — no render-sequence deduction
80404
+ * needed. `data.role === "presentation"` keeps it out entirely.
80405
+ * Returns the visible rank (position among visible items), or -1 when hidden
80406
+ * or filtered.
80297
80407
  * Signals and onChange are deferred to a microtask so multiple items updating
80298
80408
  * in one commit cause only one notification.
80299
80409
  *
@@ -81061,7 +81171,7 @@ const WheelGroupContext = createContext(null);
81061
81171
  * [key: string]: any,
81062
81172
  * }>}
81063
81173
  * @param {number} [props.visibleCount=3] - Odd number of rows visible in the viewport (the center one is the selection).
81064
- * @param {number|string} [props.itemHeight] - Main-axis size of a row when vertical (number = px). Defaults to the CSS var (2.4em).
81174
+ * @param {number|string} [props.itemHeight] - Main-axis size of a row when vertical (number = px). Defaults to the CSS var (1.8em).
81065
81175
  * @param {number|string} [props.itemWidth] - Main-axis size of a cell when horizontal (number = px). Defaults to the CSS var (3.5ch).
81066
81176
  * @param {number|string} [props.size] - Font size of the wheel: a size token ("s", "m", "l", "xl", …), a number (px), or a CSS length. Scales the digits and, since the row size is em-based, the row height too — a simple way to make the whole wheel bigger. An explicit itemHeight/itemWidth still overrides the row size.
81067
81177
  * @param {boolean} [props.bounded] - Give the wheel fixed ends instead of wrapping: it stops at the first/last value. By default the wheel loops endlessly (past the last value the first reappears, and vice-versa).
@@ -82557,7 +82667,7 @@ Wheel.Item = WheelItem;
82557
82667
  * @param {boolean} [props.horizontal] - Stack the (horizontal) wheels vertically instead of in a row.
82558
82668
  * @param {boolean} [props.glass] - Frost every wheel's neighbouring rows (see Wheel's glass prop) with one prop for the whole group.
82559
82669
  * @param {boolean} [props.frameBorder] - Line every wheel's center-window edges with a faint frame (off by default; independent of glass).
82560
- * @param {boolean} [props.zoom] - Enlarge the centered value of every wheel (see Wheel's zoom prop) with one prop for the whole group.
82670
+ * @param {boolean|number} [props.zoom] - Enlarge the centered value of every wheel (see Wheel's zoom prop) with one prop for the whole group.
82561
82671
  */
82562
82672
  const WheelGroup = props => {
82563
82673
  import.meta.css = [css$m, "@jsenv/navi/src/control/wheel/wheel.jsx"];
@@ -83342,8 +83452,8 @@ const stringifyTableSelectionValue = (type, value) => {
83342
83452
 
83343
83453
  /**
83344
83454
  * Check if a specific cell is selected
83345
- * @param {Array} selection - The selection set or array
83346
- * @param {{rowIndex: number, columnIndex: number}} cellPosition - Cell coordinates
83455
+ * @param {string[]} selection - The table's `selection` array
83456
+ * @param {{columnId: string, rowId: string}} cell - The cell's column and row ids
83347
83457
  * @returns {boolean} True if the cell is selected
83348
83458
  */
83349
83459
  const isCellSelected = (selection, { columnId, rowId }) => {
@@ -83356,8 +83466,8 @@ const isCellSelected = (selection, { columnId, rowId }) => {
83356
83466
 
83357
83467
  /**
83358
83468
  * Check if a specific row is selected
83359
- * @param {Array} selection - The selection set or array
83360
- * @param {number} rowIndex - Row index
83469
+ * @param {string[]} selection - The table's `selection` array
83470
+ * @param {string} rowId - The row's id (the `id` of its `<Tr>`)
83361
83471
  * @returns {boolean} True if the row is selected
83362
83472
  */
83363
83473
  const isRowSelected = (selection, rowId) => {
@@ -83367,8 +83477,8 @@ const isRowSelected = (selection, rowId) => {
83367
83477
 
83368
83478
  /**
83369
83479
  * Check if a specific column is selected
83370
- * @param {Array} selection - The selection set or array
83371
- * @param {number} columnIndex - Column index
83480
+ * @param {string[]} selection - The table's `selection` array
83481
+ * @param {string} columnId - The column's id (the `id` of its `<Col>`)
83372
83482
  * @returns {boolean} True if the column is selected
83373
83483
  */
83374
83484
  const isColumnSelected = (selection, columnId) => {
@@ -83903,7 +84013,7 @@ const initDragTableColumnViaPointer = (pointerdownEvent, {
83903
84013
  /**
83904
84014
  * Creates a MutationObserver that syncs attribute changes from original table to clone
83905
84015
  * @param {HTMLElement} table - The original table element
83906
- * @param {HTMLElement} cloneTable - The cloned table element
84016
+ * @param {HTMLElement} tableClone - The cloned table element
83907
84017
  * @returns {MutationObserver} The observer instance with disconnect method
83908
84018
  */
83909
84019
  const createTableAttributeSync = (table, tableClone) => {
@@ -85868,9 +85978,10 @@ const TableUI = /*#__PURE__*/forwardRef((props, ref) => {
85868
85978
  * KEY PRINCIPLES:
85869
85979
  * - Use inset box-shadow to ensure borders appear above table cell backgrounds
85870
85980
  * - Use ::before pseudo-elements with position: absolute for flexible positioning
85871
- * - Each cell draws its own borders independently (no border-collapse by default)
85981
+ * - Each cell draws its own borders; `borderCollapse` (on by default) keeps only
85982
+ * the right and bottom ones, plus top on the first row and left on the first column
85872
85983
  * - Selection borders override table borders using higher CSS specificity
85873
- * - Sticky borders use thicker box-shadows in accent color (yellow)
85984
+ * - Sticky borders use thicker box-shadows in an accent color
85874
85985
  *
85875
85986
  * TECHNICAL IMPLEMENTATION:
85876
85987
  * - All borders use inset box-shadow with specific directional mapping:
@@ -85878,9 +85989,8 @@ const TableUI = /*#__PURE__*/forwardRef((props, ref) => {
85878
85989
  * * Right: inset -1px 0 0 0
85879
85990
  * * Bottom: inset 0 -1px 0 0
85880
85991
  * * Left: inset 1px 0 0 0
85881
- * - Selection borders (blue) override table borders (red) in same pseudo-element
85992
+ * - Selection borders override table borders in the same pseudo-element
85882
85993
  * - Sticky borders replace regular borders with thicker colored variants
85883
- * - Border-collapse mode available as optional feature for future use
85884
85994
  *
85885
85995
  * Note how border disappear for sticky elements when using border-collapse (https://bugzilla.mozilla.org/show_bug.cgi?id=1727594)
85886
85996
  *
@@ -87014,11 +87124,18 @@ const BadgeCountOverflow = () => jsx("span", {
87014
87124
  });
87015
87125
  const MAX_CHAR_AS_CIRCLE = 3;
87016
87126
  const MAX_FOR_CIRCLE = 99;
87127
+
87128
+ /**
87129
+ * @param {boolean} [circle] Drawn round rather than as a pill. A circle holds
87130
+ * three characters at most, so `max` defaults to 99 with it ("99+"); a `max`
87131
+ * above 99, or `"none"`, lets a bigger count turn the circle into a pill as
87132
+ * it grows.
87133
+ * @param {number|"none"} [max] The count above which `max` and a "+" are
87134
+ * written in its place. 99 under `circle`, no cap otherwise.
87135
+ */
87017
87136
  const BadgeCount = ({
87018
87137
  children,
87019
87138
  maxElement = jsx(BadgeCountOverflow, {}),
87020
- // When you use max="none" (or max > 99) it might be a good idea to force ellipse
87021
- // so that visually the interface do not suddently switch from circle to ellipse depending on the count
87022
87139
  circle,
87023
87140
  max = circle ? MAX_FOR_CIRCLE : Infinity,
87024
87141
  integer,
@@ -88201,12 +88318,13 @@ const css$7 = /* css */`.navi_text_box {
88201
88318
  * - Displays optional icon(s) before and/or after the text
88202
88319
  * - Shrinks its width to fit the longest rendered text line (no trailing blank space)
88203
88320
  * - Wraps long text by default (overflow-wrap: anywhere)
88204
- * - Shows ellipsis for a single overflowing unwrappable line
88321
+ * - Shows an ellipsis instead under `singleLine`
88205
88322
  *
88206
88323
  * Props:
88207
88324
  * iconBefore — element shown to the left of the text
88208
88325
  * iconAfter — element shown to the right of the text (stays on the same line)
88209
- * maxHeight — CSS max-height string; when set, content that cannot wrap gets ellipsis
88326
+ * singleLine — keeps the text on one line, cut with an ellipsis (no width shrinking)
88327
+ * maxHeight — CSS max-height string; content past it is clipped
88210
88328
  * children — the text content
88211
88329
  */
88212
88330
  const TextBox = ({
@@ -88385,6 +88503,41 @@ const css$4 = /* css */`.svg_mask_content * {
88385
88503
  stroke-opacity: 1 !important;
88386
88504
  }
88387
88505
  `;
88506
+
88507
+ /**
88508
+ * SVGMaskOverlay
88509
+ *
88510
+ * Creates composite SVGs by combining independent SVG elements with masking.
88511
+ *
88512
+ * This component solves the challenge of combining independently created SVGs into
88513
+ * a single visual composition. Each SVG can have its own coordinate system, viewBox,
88514
+ * and styling, allowing for maximum reusability of individual icons or graphics.
88515
+ *
88516
+ * When overlaying SVGs, each subsequent overlay "cuts out" its portion from the base SVG,
88517
+ * creating a seamless integration where SVGs appear to interact with each other visually.
88518
+ *
88519
+ * Key benefits:
88520
+ * - Maintains each SVG's independence - use them individually elsewhere
88521
+ * - Handles different viewBox dimensions automatically
88522
+ * - Works with any SVG components regardless of internal implementation
88523
+ * - Supports unlimited overlay elements
88524
+ * - Creates proper masking between elements for visual integration
88525
+ *
88526
+ * Usage example combining two independent icon components:
88527
+ * ```jsx
88528
+ * <SVGMaskOverlay viewBox="0 0 24 24">
88529
+ * <DatabaseSvg />
88530
+ * <svg x="12" y="12" width="16" height="16" overflow="visible">
88531
+ * <PlusSvg />
88532
+ * </svg>
88533
+ * </SVGMaskOverlay>
88534
+ * ```
88535
+ *
88536
+ * @param {Object} props - Component properties
88537
+ * @param {string} props.viewBox - The main viewBox for the composition (required as soon as there is an overlay)
88538
+ * @param {import("ignore:preact").ComponentChildren} props.children - SVG elements (first is base, rest are overlays)
88539
+ * @returns {import("ignore:preact").VNode} A composed SVG with all elements properly masked
88540
+ */
88388
88541
  const SVGMaskOverlay = ({
88389
88542
  viewBox,
88390
88543
  children
@@ -88397,7 +88550,7 @@ const SVGMaskOverlay = ({
88397
88550
  return children[0];
88398
88551
  }
88399
88552
  if (!viewBox) {
88400
- console.error("SVGComposition requires an explicit viewBox");
88553
+ console.error("SVGMaskOverlay requires an explicit viewBox");
88401
88554
  return null;
88402
88555
  }
88403
88556
 
@@ -88457,6 +88610,8 @@ const css$3 = /* css */`@layer navi {
88457
88610
  }
88458
88611
 
88459
88612
  .navi_card_layout {
88613
+ box-sizing: border-box;
88614
+ min-height: 100%;
88460
88615
  padding-top: var(--layout-margin-top, var(--layout-margin-y, var(--layout-margin)));
88461
88616
  padding-right: var(--layout-margin-right, var(--layout-margin-x, var(--layout-margin)));
88462
88617
  padding-bottom: var(--layout-margin-bottom, var(--layout-margin-y, var(--layout-margin)));
@@ -88498,6 +88653,11 @@ const CardLayoutStyleCSSVars = {
88498
88653
  minHeight: "--layout-min-height"
88499
88654
  };
88500
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
+ *
88501
88661
  * @type {import("ignore:preact").FunctionComponent<{
88502
88662
  * alignX?: string,
88503
88663
  * alignY?: string,
@@ -88527,6 +88687,11 @@ const CardLayout = ({
88527
88687
  }) => {
88528
88688
  import.meta.css = [css$3, "@jsenv/navi/src/layout/card_layout.jsx"];
88529
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",
88530
88695
  baseClassName: "navi_card_layout",
88531
88696
  styleCSSVars: CardLayoutStyleCSSVars,
88532
88697
  visualSelector: ".navi_card",
@@ -89338,7 +89503,8 @@ const css = /* css */`.navi_side_panel {
89338
89503
  * interaction only makes sense paired with not letting focus silently
89339
89504
  * leave the panel first. A box of the page whose press must not close the
89340
89505
  * panel (a card that fills it) names the panel:
89341
- * `data-navi-popup-inside={id}` — see docs/popup_backdrop.md.
89506
+ * `data-navi-popup-inside={id}`, read when the panel has no wall
89507
+ * (`backdrop={false}`) — see docs/popup_backdrop.md.
89342
89508
  * @param {boolean} [props.closeByDrag=true] - Pushing the panel back
89343
89509
  * towards the edge it is docked to closes it: the panel follows the
89344
89510
  * pointer and finishes leaving (or comes back to rest) when it is
@@ -89353,8 +89519,8 @@ const css = /* css */`.navi_side_panel {
89353
89519
  * doc).
89354
89520
  * @param {import("ignore:preact").ComponentChildren} props.children - No built-in
89355
89521
  * close button — add one wherever it makes sense for the layout (e.g. a
89356
- * plain `<Button command="--navi-close">`), use `SidePanel.Head`'s own
89357
- * `closeButton` prop, or rely on `closeByPressOutside`/Escape instead.
89522
+ * plain `<Button command="--navi-close">` in `SidePanel.Head`), or rely on
89523
+ * `closeByPressOutside`/Escape instead.
89358
89524
  * A form sent inside the panel closes it, as in any popup: a panel one
89359
89525
  * keeps editing in says `command="--navi-void"` on that form (see
89360
89526
  * docs/form_changed.md).
@@ -89445,9 +89611,7 @@ const toCssLength = (value, propertyName) => value === undefined || value === nu
89445
89611
  * Stuck to the top of the panel's own scrollable area (`position: sticky`)
89446
89612
  * regardless of `side` — only the panel's content in between scrolls. No
89447
89613
  * built-in padding or close button — add a `<Button command="--navi-close">`
89448
- * (optionally with the `"navi_side_panel_head_close_button"` className, a
89449
- * float-right utility this file's own CSS still provides) wherever it makes
89450
- * sense for the layout.
89614
+ * wherever it makes sense for the layout.
89451
89615
  *
89452
89616
  * @param {object} props
89453
89617
  * @param {string} [props.className] - Merged with the shared