@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
@@ -527,12 +527,17 @@ effect(() => {
527
527
  * The active language is read from `languagesSignal` (see lang_signal.js —
528
528
  * combines the browser's own `navigator.languages`, an optional
529
529
  * `setPreferredLanguage()` user override, and an optional
530
- * `setSupportedLanguages()` app-wide allow-list), live on every lookup.
530
+ * `setSupportedLanguages()` app-wide allow-list), live on every lookup. Each
531
+ * key is looked up in those languages in order, then in English, which
532
+ * translates every key: a language shipped for part of the keys only (German
533
+ * has the `time.*` words) never shows a raw key.
531
534
  *
532
535
  * Built-in key namespaces, all overridable — the registrations below are the
533
536
  * exhaustive list, read them to find the exact key to override:
534
537
  * - `"button.*"` — Clear, Reset, Send, Open, Close, Cancel, Confirm…
535
- * - `"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
538
+ * - `"confirm.*"` — the question a confirmation asks by default
539
+ * - `"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
540
+ * - `"time_range.*"` — how the two ends of a span are named ("From", "to")
536
541
  * - `"spin.*"` — the ends of a steppable range
537
542
  * - `"list.*"` — empty/no-match/failed-rows messages
538
543
  * - `"badge_list.*"` — the "+[count] more" overflow badge
@@ -1445,19 +1450,25 @@ const isNetworkPolicyError = (error) => {
1445
1450
  * is already saying it in words the person understands.
1446
1451
  *
1447
1452
  * navi's own report leaves it alone (action_error_report.js). What is left is
1448
- * the screen displaying it: it does so by throwing the error to a boundary, and
1449
- * `preact/debug` re-emits on `window` every error a boundary caught — on
1450
- * purpose, for React devtools compatibility. Uncancelled, that lands as an
1451
- * uncaught error in the console, over a page calmly explaining why nothing was
1452
- * asked. Cancelling the event is what says it is handled: the browser drops
1453
+ * the error reaching `window` on its own — a run called by hand whose failure
1454
+ * nobody catches, a render throw no boundary took — where it lands as an
1455
+ * uncaught error in the console, about a request the app itself declared would
1456
+ * not leave. Cancelling the event is what says it is handled: the browser drops
1453
1457
  * the console line, and the jsenv supervisor skips prevented events too.
1454
1458
  *
1455
1459
  * Both shapes a failure travels in are covered, since which one it is depends
1456
1460
  * on whether the resource callback happened to be async — the throw reaching
1457
1461
  * `window`, and the rejection nobody caught.
1458
1462
  *
1459
- * Scoped to the policy's own error and to nothing else — every other error a
1460
- * boundary caught, and every other rejection let go, stays exactly as loud as
1463
+ * A boundary displaying it raises no event at all. In dev, `preact/debug` hands
1464
+ * every error a boundary caught to `console.error`, after the boundary took it
1465
+ * and before it rendered anything; it reads no option and no flag, so that line
1466
+ * stays. Muting it from here is not done: wrapping `console.error` rewrites a
1467
+ * global, and wrapping `options._catchError` reaches into preact's private
1468
+ * hooks, both to hide one class of error.
1469
+ *
1470
+ * Scoped to the policy's own error and to nothing else — every other error
1471
+ * thrown at window, and every other rejection let go, stays exactly as loud as
1461
1472
  * it is.
1462
1473
  */
1463
1474
  let networkPolicyErrorSilenced = false;
@@ -1540,11 +1551,10 @@ const errorIsDisplayed = (error) => {
1540
1551
  *
1541
1552
  * Whatever the reader does with it is already covered without any deadline: it
1542
1553
  * displays it (and marks it), or it throws it, and a thrown error either finds a
1543
- * boundary that displays it or reaches window on its own — `preact/debug`
1544
- * re-throws every error a boundary caught, and an unbounded one aborts the
1545
- * render loudly. Reporting it here as well would be a second voice saying the
1546
- * same thing, always the wrong one, since this module cannot see which of those
1547
- * happened.
1554
+ * boundary that displays it or reaches window on its own — preact re-throws
1555
+ * what no boundary handled. Reporting it here as well would be a second voice
1556
+ * saying the same thing, always the wrong one, since this module cannot see
1557
+ * which of those happened.
1548
1558
  */
1549
1559
  const errorTakenByRenderSet = new WeakSet();
1550
1560
  const markErrorAsTakenByRender = (error) => {
@@ -1619,8 +1629,8 @@ const markErrorAsStoppingRender = (error) => {
1619
1629
  * An error the network policy produced is never reported: the app declared the
1620
1630
  * state that produced it, the request never left, and there is nothing for a
1621
1631
  * developer to fix. It is data a screen shows, and it stays data whether or not
1622
- * one does (see network_policy.js, which also keeps it out of the browser
1623
- * console).
1632
+ * one does (see network_policy.js, which also cancels it when it reaches
1633
+ * window).
1624
1634
  */
1625
1635
  const errorIsAccountedFor = (error) => {
1626
1636
  return (
@@ -1634,7 +1644,7 @@ const errorIsAccountedFor = (error) => {
1634
1644
  /**
1635
1645
  * Rethrown rather than logged: an error nobody took is an unhandled error, and
1636
1646
  * the runtime already knows what to do with those (window "error" event, jsenv
1637
- * overlay in dev). Same trick preact/debug uses for the same reason.
1647
+ * overlay in dev). Same answer preact gives an error no boundary handled.
1638
1648
  */
1639
1649
  const errorReportedSet = new WeakSet();
1640
1650
  const reportErrorIfNobodyDisplaysIt = (error, { action } = {}) => {
@@ -2155,7 +2165,7 @@ const compareTwoJsValues = (
2155
2165
  }
2156
2166
  return true;
2157
2167
  }
2158
- // Ordered array comparison (original behavior)
2168
+ // Ordered array comparison
2159
2169
  let i = 0;
2160
2170
  while (i < a.length) {
2161
2171
  const aValue = a[i];
@@ -3222,6 +3232,8 @@ const actionWeakMap = new WeakMap();
3222
3232
  * long is this still true?" belongs to the question: a court list is good for
3223
3233
  * the session, a count of people watching grows on its own and is worth a few
3224
3234
  * minutes, a payment status is worth nothing at all.
3235
+ * @param {{ verb?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" }} [rootOptions.meta] -
3236
+ * `verb` puts the action under the network policy (docs/network_policy.md).
3225
3237
  */
3226
3238
  const createAction = (callback, rootOptions = {}) => {
3227
3239
  const existing = actionWeakMap.get(callback);
@@ -4323,7 +4335,7 @@ const runUnwatched = (startRun) => {
4323
4335
  /**
4324
4336
  * Reactively runs an action whenever the params derived from signals change.
4325
4337
  *
4326
- * @param {object} action - The action to run.
4338
+ * @param {object | Function} action - The action to run, or a function made into one.
4327
4339
  * @param {Function} deriveActionParamsFromSignals - A function that reads signals and returns
4328
4340
  * the params to pass to the action. It is re-evaluated automatically whenever a signal it
4329
4341
  * read changes. Return `false`/`null`/`undefined` to skip running the action.
@@ -4346,6 +4358,8 @@ const runUnwatched = (startRun) => {
4346
4358
  * ```
4347
4359
  * The action will not fire while the user is actively changing filters; it fires once
4348
4360
  * they pause for half a second.
4361
+ * @returns {object} the instance bound to the derived params: the one to read
4362
+ * (`useAsyncData`), since the answer lands there and never on `action` itself.
4349
4363
  */
4350
4364
  // The run is not awaited here, and a rejection nobody waits for is an unhandled
4351
4365
  // one — in dev, an error overlay thrown over a page that is already saying what
@@ -4860,11 +4874,12 @@ const isNumberInput = (type, naviInputType) => {
4860
4874
  */
4861
4875
  /**
4862
4876
  * A bound signal that carries a default of its own says the same thing on every
4863
- * control: the control starts there and stays uncontrolled, which is what makes
4864
- * a form read the value shown as a SUGGESTION rather than as something it
4865
- * already holds. Uncontrolled here is about what the control HOLDS, not about
4866
- * whether it follows the signal — the binding stays two-way either way (see
4867
- * stateFromSignal in control_hooks.jsx). Written once and used by everything
4877
+ * control: the control starts there, uncontrolled, and a reset goes back to it.
4878
+ * Uncontrolled here is about where the control starts, not about whether it
4879
+ * follows the signal — the binding stays two-way either way (see
4880
+ * stateFromSignal in control_hooks.jsx) — nor about what a form counts as
4881
+ * held: that is the signal's content (isUIStateHeld in held_ui_state.js), and a
4882
+ * stateSignal holds its default. Written once and used by everything
4868
4883
  * that takes a `signal`, so one signal cannot mean two different things
4869
4884
  * depending on which control it was handed to.
4870
4885
  */
@@ -4924,8 +4939,7 @@ const resolveInputProps = (props, { controlType = "input" } = {}) => {
4924
4939
  // If no explicit defaultChecked, derive it from the signal's default
4925
4940
  // value so that resetUIState restores to the original default.
4926
4941
  // Only a stateSignal carries a default of its own; a plain signal has
4927
- // no `options` at all, and asking it for one used to throw on mount —
4928
- // the same optional read every other branch here already does.
4942
+ // no `options` at all, hence the optional read.
4929
4943
  const defaultVal = signalOptions?.getDefaultValue(false);
4930
4944
  if (defaultVal === undefined) ; else if (props.type === "radio") {
4931
4945
  if (defaultVal === true) {
@@ -8835,8 +8849,8 @@ const isLanguageSupported = (lang, supportedLanguages) => {
8835
8849
  * supported), falls back to `supportedLanguagesSignal` itself so callers
8836
8850
  * still get *something* usable rather than an empty array.
8837
8851
  *
8838
- * Consumers that accept either a single lang or an ordered array (this
8839
- * package's own `matchBestLang`/`createI18n`, and native `Intl.NumberFormat`/
8852
+ * Consumers that accept either a single lang or an ordered array
8853
+ * (@jsenv/humanize's `createI18n` and formatters, and native `Intl.NumberFormat`/
8840
8854
  * `Intl.DateTimeFormat`) can pass this straight through: anything not
8841
8855
  * covered by the first entry falls through to the next, rather than
8842
8856
  * jumping straight to an unrelated default like "en".
@@ -9819,15 +9833,15 @@ const readOnlyMessage = (field) => {
9819
9833
  * - Action dispatch + validity: `control_action.js` / `dispatchRequestAction`
9820
9834
  * - Validity checking: `control_validation.js`
9821
9835
  *
9822
- * Each UI state controller gets its own `controlInteraction` instance (created by
9823
- * `createControlInteraction`) just like it gets a `controlValidity` instance.
9836
+ * Each UI state controller gets its own instance, `controller.rules.interaction`
9837
+ * (created by `createControlInteraction`), beside `controller.rules.validation`.
9824
9838
  *
9825
9839
  * Flow:
9826
9840
  * user interaction
9827
9841
  * → dispatchRequestInteraction
9828
9842
  * → "navi_request_interaction" event
9829
9843
  * → onRequestInteraction
9830
- * → check disabled / read-only / busy (via controller.controlInteraction)
9844
+ * → check disabled / read-only / busy (via controller.rules.interaction)
9831
9845
  * against the interaction's `intent` ("write" by default, "read" for one
9832
9846
  * that only shows what is already there — see READONLY_CONSTRAINT)
9833
9847
  * → if blocked → prevented()
@@ -10647,7 +10661,8 @@ const interactionsDisputeThePress = (interactions) => {
10647
10661
  * control (a Button) is its own; a Box inside one, or wrapping exactly one,
10648
10662
  * reaches it; a Box with no control anywhere near it — or laying out several,
10649
10663
  * which belongs to none of them — still answers with a callback of the caller's,
10650
- * and only "request_action" has nothing to ask.
10664
+ * and only the two requests ("request_action", "request_ui_action") have
10665
+ * nothing to ask.
10651
10666
  *
10652
10667
  * Set up once per element rather than on every render, which is what lets a
10653
10668
  * detector be a plain `setup`/teardown pair. So the interactions themselves are
@@ -10806,7 +10821,7 @@ const useInteractionsEffect = (ref, interactionsRef) => {
10806
10821
  cancelable: true,
10807
10822
  });
10808
10823
  // The event the interaction was read from stays reachable from it:
10809
- // `findEvent(actionEvent, "pointerdown")` still finds the press a swipe
10824
+ // `findEvent(actionEvent, "pointerdown")` still finds the press a hold
10810
10825
  // was made of.
10811
10826
  chainEvent(interactionEvent, originalEvent);
10812
10827
  if (!element.dispatchEvent(interactionEvent)) {
@@ -11554,7 +11569,7 @@ const swipeTypeOf = (axis, pulled) => {
11554
11569
  * …
11555
11570
  * }}}
11556
11571
  *
11557
- * `toId` is an element and never null: a copy over nothing is a release that meant
11572
+ * `toId` names an element and is never null: a copy over nothing is a release that meant
11558
11573
  * nothing, and the interaction does not happen at all — unless the element
11559
11574
  * declares `leave` (see below), which gives that release a meaning of its own.
11560
11575
  *
@@ -12156,8 +12171,9 @@ defineInteractionDetector({
12156
12171
  name: "surface",
12157
12172
  claims: (type) =>
12158
12173
  type === PAN || type === ZOOM || type === GRAB || type === RELEASE,
12159
- // A press on the surface may be a tap, a hold or the beginning of a pan, and
12160
- // nothing may read it as the first until the pointer has said which.
12174
+ // A press on the surface may be a tap, a hold or the first finger of a pan or
12175
+ // a pinch, and nothing may read it as the first until the pointer has said
12176
+ // which.
12161
12177
  disputesPress: true,
12162
12178
  setup: (element, trigger, { types, readConfig }) => {
12163
12179
  const canPan = types.includes(PAN);
@@ -15160,21 +15176,33 @@ const getActionResultProperties = (action) => {
15160
15176
  return actionResultPropertiesMap.get(action);
15161
15177
  };
15162
15178
 
15163
- // PUT/PATCH results update the UI through the store, and DELETE resets the GET
15164
- // instead of rerunning it, so a GET rerun would only ever cost a request.
15165
- // A POST is the one case the client cannot decide alone: whether a new item
15166
- // belongs to a list depends on filters/pagination the backend owns.
15167
15179
  // Rationale in full, plus when to override: docs/list_refresh.md
15168
15180
  const defaultRerunOn = {
15181
+ // PUT/PATCH results update the UI through the store, and DELETE resets the
15182
+ // GET rather than rerunning it, so a GET rerun would only ever cost a request.
15169
15183
  GET: false,
15184
+ // A POST is the one case the client cannot decide alone: whether a new item
15185
+ // belongs to a list depends on filters/pagination the backend owns.
15170
15186
  GET_MANY: ["POST"],
15187
+ // DELETE is in there, unlike for GET_MANY: an action holds ids and the store
15188
+ // drops the deleted one out of every list holding it, while a list reading
15189
+ // by slices holds places — the row that left takes the ones after it one
15190
+ // rank up, and only the collection knows who fills the last one.
15191
+ GET_RANGE: ["POST", "DELETE"],
15192
+ };
15193
+ // A key a `rerunOn` leaves out keeps the value it would have had without it:
15194
+ // the default for a resource, the value of the resource it is declared on for
15195
+ // a withParams() scope or a relation.
15196
+ const resolveRerunOn = (rerunOn, inheritedRerunOn = defaultRerunOn) => {
15197
+ if (!rerunOn) {
15198
+ return inheritedRerunOn;
15199
+ }
15200
+ return {
15201
+ GET: rerunOn.GET ?? inheritedRerunOn.GET,
15202
+ GET_MANY: rerunOn.GET_MANY ?? inheritedRerunOn.GET_MANY,
15203
+ GET_RANGE: rerunOn.GET_RANGE ?? inheritedRerunOn.GET_RANGE,
15204
+ };
15171
15205
  };
15172
- // What makes a range reader stale (rerunOn.GET_RANGE overrides it). DELETE is
15173
- // in there, unlike for GET_MANY: an action holds ids and the store drops the
15174
- // deleted one out of every list holding it, while a list reading by slices
15175
- // holds places — the row that left takes the ones after it one rank up, and
15176
- // only the collection knows who fills the last one.
15177
- const defaultInvalidateRangeOn = ["POST", "DELETE"];
15178
15206
 
15179
15207
  // This handles ALL resource lifecycle logic (rerun/reset) across all resources
15180
15208
  const createResourceLifecycleManager = () => {
@@ -15184,7 +15212,7 @@ const createResourceLifecycleManager = () => {
15184
15212
 
15185
15213
  const registerResource = (resourceScope, config) => {
15186
15214
  const {
15187
- rerunOn = defaultRerunOn,
15215
+ rerunOn,
15188
15216
  paramScope = null,
15189
15217
  dependencies = [],
15190
15218
  uniqueKeys = [],
@@ -15467,8 +15495,7 @@ const createResourceLifecycleManager = () => {
15467
15495
  if (!isSameResource && !isDependent) {
15468
15496
  continue;
15469
15497
  }
15470
- const invalidateOn = config.rerunOn.GET_RANGE ?? defaultInvalidateRangeOn;
15471
- if (!shouldRerunAfter(invalidateOn, triggerVerb)) {
15498
+ if (!shouldRerunAfter(config.rerunOn.GET_RANGE, triggerVerb)) {
15472
15499
  continue;
15473
15500
  }
15474
15501
  for (const rangeReader of config.rangeReaderSet) {
@@ -15806,7 +15833,7 @@ const debug$3 = (...args) => {
15806
15833
  * @param {string} name - resource name, used in action names and error messages
15807
15834
  * @param {Object} restCallbacks - `{ idKey, uniqueKeys, rerunOn, dependencies, GET, GET_MANY, GET_RANGE, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
15808
15835
  * @param {string} [restCallbacks.idKey] - primary key property, defaults to `"id"`
15809
- * @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
15836
+ * @param {string[]} [restCallbacks.uniqueKeys] - alternate keys the store can find an item by (e.g. `"username"`)
15810
15837
  * @see docs/resource.md — relationships, callback return contracts, decision table
15811
15838
  *
15812
15839
  * @example
@@ -15969,7 +15996,7 @@ const resource = (
15969
15996
  addItemSetup,
15970
15997
  createRestAction: createRestActionForRoot,
15971
15998
  paramScope: getParamScope(undefined),
15972
- rerunOn,
15999
+ rerunOn: resolveRerunOn(rerunOn),
15973
16000
  dependencies,
15974
16001
  });
15975
16002
  };
@@ -16033,7 +16060,7 @@ const createResource = (
16033
16060
  * identical parameters, preventing cross-contamination between different parameter sets.
16034
16061
  *
16035
16062
  * @param {Object} params - Parameters to bind to all actions of this resource (required)
16036
- * @param {Object} options - Additional options for the parameterized resource
16063
+ * @param {{ rerunOn?: Object, dependencies?: Object[] }} [options] - reruns of that scope; a `rerunOn` key left out keeps the resource's value, and `dependencies` left out are the resource's
16037
16064
  * @returns {Object} A new resource instance with parameter-bound actions and isolated lifecycle
16038
16065
  * @see docs/resource.md — what a scope isolates, and `dependencies`
16039
16066
  *
@@ -16051,7 +16078,7 @@ const createResource = (
16051
16078
  * const ROLE_WITH_OWNERSHIP = role.withParams({ owners: true }, {
16052
16079
  * dependencies: [role, database, tables],
16053
16080
  * });
16054
- * // ROLE_WITH_OWNERSHIP.GET_MANY will autorerun when any table/database/role is POST/DELETE
16081
+ * // ROLE_WITH_OWNERSHIP.GET_MANY reruns after any write (POST/PUT/PATCH/DELETE) on a role, a database or a table
16055
16082
  */
16056
16083
  const withParams = (
16057
16084
  paramsToInject,
@@ -16079,7 +16106,7 @@ const createResource = (
16079
16106
  addItemSetup,
16080
16107
  createRestAction: createRestActionWithParams,
16081
16108
  paramScope: resolvedParamScope,
16082
- rerunOn: withParamsRerunOn ?? rerunOn,
16109
+ rerunOn: resolveRerunOn(withParamsRerunOn, rerunOn),
16083
16110
  dependencies: withParamsDeps ?? dependencies,
16084
16111
  });
16085
16112
  };
@@ -16097,13 +16124,15 @@ const createResource = (
16097
16124
  * Callback return contracts:
16098
16125
  * - GET / PUT → the parent object with the relationship nested inside:
16099
16126
  * `async ({ id }) => ({ id, session: { id: 10, token: "abc" } })`; `null` for no relationship
16100
- * - DELETE → the parent id (or `{ id }`); the property is set to `null`
16127
+ * - DELETE → the parent id, or `{ id }`; the property is set to `null`
16101
16128
  *
16102
16129
  * The backend may also embed the child inline in a parent GET/POST response — the
16103
16130
  * setter on the property upserts the nested object into the child store.
16104
16131
  *
16105
- * Returns the child relationship resource, itself chainable:
16106
- * `USER_SESSION.one("device", DEVICE)` adds a reactive `.device` property to each session.
16132
+ * Returns the relationship resource, itself chainable. Its items are the child
16133
+ * resource's own, shared by every parent: `USER_SESSION.one("device", DEVICE)`
16134
+ * adds a reactive `.device` property to every session, as
16135
+ * `SESSION.one("device", DEVICE)` would.
16107
16136
  *
16108
16137
  * @param {string} propertyName - property holding the child on each parent item
16109
16138
  * @param {Object} childResource - the independent resource created by `resource()`
@@ -16186,11 +16215,11 @@ const createResource = (
16186
16215
  const createRestActionForOne = (verb, callback, { onActionComplete }) => {
16187
16216
  const applyResultToValue =
16188
16217
  verb === "DELETE"
16189
- ? (itemId) => {
16190
- const item = store.select(itemId);
16218
+ ? (itemIdOrItemProps) => {
16219
+ const item = store.select(itemIdOrItemProps);
16191
16220
  const childItemId = item[propertyName][childIdKey];
16192
16221
  store.upsert({
16193
- [idKey]: itemId,
16222
+ [idKey]: item[idKey],
16194
16223
  [propertyName]: null,
16195
16224
  });
16196
16225
  return childItemId;
@@ -16218,7 +16247,7 @@ const createResource = (
16218
16247
  if (!isProps(result) && !primitiveCanBeId(result)) {
16219
16248
  throwInvalidResult(
16220
16249
  action.name,
16221
- `an object (that will be used to drop "${name}" resource)`,
16250
+ `the "${name}" id, or { ${idKey} } (the item whose "${propertyName}" becomes null)`,
16222
16251
  result,
16223
16252
  );
16224
16253
  }
@@ -16243,11 +16272,11 @@ const createResource = (
16243
16272
  PUT,
16244
16273
  DELETE,
16245
16274
  },
16246
- store,
16247
- addItemSetup,
16275
+ store: childStore,
16276
+ addItemSetup: childResource.addItemSetup,
16248
16277
  createRestAction: createRestActionForOne,
16249
16278
  paramScope,
16250
- rerunOn: oneRerunOn ?? rerunOn,
16279
+ rerunOn: resolveRerunOn(oneRerunOn, rerunOn),
16251
16280
  dependencies: oneDependencies ?? dependencies,
16252
16281
  });
16253
16282
  };
@@ -16270,6 +16299,9 @@ const createResource = (
16270
16299
  * - DELETE → `[parentId, childId]`
16271
16300
  * - DELETE_MANY → `[parentId, [childId, childId, …]]`
16272
16301
  *
16302
+ * Returns the relationship resource, itself chainable; its items are the child
16303
+ * resource's own, as for `.one()`.
16304
+ *
16273
16305
  * @param {string} propertyName - property holding the child array on each parent item
16274
16306
  * @param {Object} childResource - the independent resource created by `resource()`
16275
16307
  * @param {Object} [restCallbacks] - `{ rerunOn, dependencies, GET, GET_MANY, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
@@ -16505,11 +16537,11 @@ const createResource = (
16505
16537
  DELETE,
16506
16538
  DELETE_MANY,
16507
16539
  },
16508
- store,
16509
- addItemSetup,
16540
+ store: childStore,
16541
+ addItemSetup: childResource.addItemSetup,
16510
16542
  createRestAction: createRestActionForMany,
16511
16543
  paramScope,
16512
- rerunOn: manyRerunOn ?? rerunOn,
16544
+ rerunOn: resolveRerunOn(manyRerunOn, rerunOn),
16513
16545
  dependencies: manyDependencies ?? dependencies,
16514
16546
  });
16515
16547
  };
@@ -16638,7 +16670,7 @@ const createResource = (
16638
16670
  addItemSetup: childAddItemSetup,
16639
16671
  createRestAction: createRestActionForScopedOne,
16640
16672
  paramScope,
16641
- rerunOn: scopedOneRerunOn ?? rerunOn,
16673
+ rerunOn: resolveRerunOn(scopedOneRerunOn, rerunOn),
16642
16674
  dependencies: scopedOneDependencies ?? dependencies,
16643
16675
  });
16644
16676
  };
@@ -16873,7 +16905,7 @@ const createResource = (
16873
16905
  addItemSetup: childAddItemSetup,
16874
16906
  createRestAction: createRestActionForScopedMany,
16875
16907
  paramScope,
16876
- rerunOn: scopedManyRerunOn ?? rerunOn,
16908
+ rerunOn: resolveRerunOn(scopedManyRerunOn, rerunOn),
16877
16909
  dependencies: scopedManyDependencies ?? dependencies,
16878
16910
  });
16879
16911
  // When a scoped child collection is mutated (POST etc.), the parent GET must
@@ -17264,7 +17296,7 @@ Received an object with keys: ${keys.join(", ")}.`,
17264
17296
  * });
17265
17297
  *
17266
17298
  * syncResourceToSignals(USER, { username: usernameSignal });
17267
- * // Now when a user item's username is updated via USER.PUT,
17299
+ * // When a user item's username is updated via USER.PUT,
17268
17300
  * // usernameSignal.value is set to the new username,
17269
17301
  * // which in turn triggers the route Signal->URL sync and updates the browser URL.
17270
17302
  */
@@ -17277,6 +17309,17 @@ const syncResourceToSignals = (resource, propertyToSignalMap) => {
17277
17309
  syncStoreToSignals(resource.store, propertyToSignalMap);
17278
17310
  };
17279
17311
 
17312
+ /**
17313
+ * The same, for a `scopedOne`/`scopedMany` resource: its items live in one store
17314
+ * per owner, so the store to watch is the one of the owner `ownerSignal` names
17315
+ * (by id or by any unique key), and it is switched when the signal changes.
17316
+ * Nothing is synced while `ownerSignal` holds `null`/`undefined` or names an
17317
+ * owner nothing has been loaded for yet.
17318
+ *
17319
+ * @param {Object} resource - a resource made by `.scopedOne()` / `.scopedMany()`
17320
+ * @param {import("@preact/signals").Signal} ownerSignal - the owner whose children are watched
17321
+ * @param {Object} propertyToSignalMap - `{ [propertyName]: signal }`, as for `syncResourceToSignals`
17322
+ */
17280
17323
  const syncOwnedResourceToSignals = (
17281
17324
  resource,
17282
17325
  ownerSignal,
@@ -17324,7 +17367,7 @@ const generateSignalId = () => {
17324
17367
  * 1. Initially takes value from the default signal
17325
17368
  * 2. When explicitly set (programmatically or via localStorage), the explicit value takes precedence
17326
17369
  * 3. When default signal changes, it only updates if no explicit value was ever set
17327
- * 4. Calling reset() or setting to undefined makes the signal use the dynamic default again
17370
+ * 4. Setting it to undefined makes the signal use the dynamic default again
17328
17371
  * 5. If dynamic default is undefined and options.default is provided, uses the static fallback
17329
17372
  *
17330
17373
  * This is useful for:
@@ -17338,7 +17381,9 @@ const generateSignalId = () => {
17338
17381
  * @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.
17339
17382
  * @param {any} [options.default] - Static fallback value used when defaultValue is a signal and that signal's value is undefined
17340
17383
  * @param {boolean} [options.persists=false] - Whether to persist the signal value in localStorage using the signal ID as key
17341
- * @param {"string" | "number" | "boolean" | "object"} [options.type="string"] - Type for localStorage serialization/deserialization
17384
+ * @param {string} [options.type] - Type the value is coerced to, and read back as from localStorage and the url:
17385
+ * any @jsenv/validity type (`"string"`, `"number"`, `"boolean"`, `"array"`, `"object"`, `"integer"`, `"date"`,
17386
+ * `"time"`…). Without it the value is not coerced.
17342
17387
  * @param {"string" | "number" | "boolean"} [options.itemType] - For array type: type of the array items.
17343
17388
  * Used when reading the value back from a url search param, where everything is a string:
17344
17389
  * `?level=3,4` becomes `[3, 4]` instead of `["3", "4"]`. Without it items stay strings.
@@ -18501,14 +18546,12 @@ const LoadingFallback = ({
18501
18546
  * Two things it gets right that a hand-written boundary rarely does, both
18502
18547
  * explained in docs/error_handling.md:
18503
18548
  *
18504
- * - It marks the error as displayed ONLY when it actually displays it.
18505
- * `preact/debug` rethrows every error a boundary caught in a `setTimeout`, on
18506
- * purpose (React devtools compatibility), so a handled error still reaches
18507
- * window and the jsenv overlay covers the app unless `__handled_by__` is set.
18508
- * Setting it before knowing whether anything is rendered turns a boundary into
18509
- * a bug swallower: a TypeError in a component becomes a blank page AND a
18510
- * silent one. Without a `fallback` there is nothing to display, so the error is
18511
- * left alone and continues up.
18549
+ * - It marks the error as displayed ONLY when it actually displays it. The
18550
+ * jsenv overlay skips an error carrying `__handled_by__`, so a mark set before
18551
+ * knowing whether anything is rendered mutes an error that then continues up:
18552
+ * a TypeError in a component becomes a blank page AND a silent one. Without a
18553
+ * `fallback` there is nothing to display, so the error is left alone and
18554
+ * continues up.
18512
18555
  *
18513
18556
  * - It resets on navigation, not only on rerun. Rerunning the failed action is
18514
18557
  * one way out; going somewhere else is the common one. Without a reset on the
@@ -19574,9 +19617,9 @@ const isSizeSpacingKey = (key) => {
19574
19617
  // "vvw"/"vvh" are navi's own: the *visual* viewport, which — unlike vw/dvw —
19575
19618
  // shrinks when the mobile virtual keyboard opens (see layout/responsive.js), so
19576
19619
  // they are what a popup meant to stay clear of the keyboard should use.
19577
- // "appw"/"apph" are the same thing narrowed to the app's own screen: identical
19578
- // to vvw/vvh until the app declares --navi-app-max-width, and a share of that
19579
- // width afterwards. A gap meant to read as "a small margin" must use these —
19620
+ // "appw"/"apph" are the same thing narrowed to the app's own screen: the
19621
+ // visual viewport minus the app's bands (--navi-app-inset-*), so identical to
19622
+ // vvw/vvh until the app declares bands. A gap meant to read as "a small margin" must use these —
19580
19623
  // 3vvw on a 1500px window is a 45px gap around a 600px app.
19581
19624
  // Functions rather than the signals themselves: appw/apph are not a signal to
19582
19625
  // read but a value to compute (a signal, then a CSS var read back). Reading the
@@ -21590,9 +21633,48 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
21590
21633
  * separator?: import("ignore:preact").ComponentChildren | ((index: number) => import("ignore:preact").ComponentChildren),
21591
21634
  * selfInteractions?: string,
21592
21635
  * interactions?: { [type: string]: "request_action" | "request_ui_action" | "refuse" | ((event: Event) => void) | false | null | undefined },
21636
+ * alignX?: string,
21637
+ * alignY?: string,
21638
+ * spacing?: string | number,
21639
+ * overflow?: string,
21640
+ * header?: boolean,
21641
+ * footer?: boolean,
21642
+ * body?: boolean,
21643
+ * sticky?: boolean,
21593
21644
  * children?: import("ignore:preact").ComponentChildren,
21594
21645
  * [key: string]: any,
21595
21646
  * }>}
21647
+ * @param {"x"|"y"|boolean} [flex] A flex container: `"x"` (or `true`) side by
21648
+ * side, `"y"` stacked. Named by axis because `flex-direction: column` stacks
21649
+ * items vertically, the opposite of what "column" suggests.
21650
+ * @param {boolean} [inline] The inline form of the box's display
21651
+ * (`inline-flex`, `inline-grid`…). An inline box given a `width`/`height`
21652
+ * becomes an inline flex row, since an inline box ignores both.
21653
+ * @param {string} [alignX] Horizontal alignment of the content whatever the
21654
+ * flex direction: `justify-content` or `align-items` is picked from it
21655
+ * (`text-align` outside flex and grid).
21656
+ * @param {string} [alignY] Vertical alignment, the same way.
21657
+ * @param {string|number} [spacing] The gap between children in a flex or grid
21658
+ * box (`spacingX`/`spacingY` per axis) — there is no `gap` prop. Spacing
21659
+ * props (`spacing`, `margin*`, `padding*`) take the scale `"xxs"`…`"xxl"`
21660
+ * (`--navi-xxs`…), `"-s"` for a negative, or any CSS value.
21661
+ * @param {object|string} [style] CSS properties, plus pseudo keys holding
21662
+ * their own: `style={{ color: "blue", ":hover": { color: "red" } }}`
21663
+ * (`":-navi-pressed"`, `"::before"`…). A pseudo class used there is tracked
21664
+ * on the element; nothing re-renders when it changes.
21665
+ * @param {string} [overflow] `"auto"`/`"scroll"` (`overflowX`/`overflowY`
21666
+ * too) makes the box a scrolling area, which is what gives `header`/`body`/
21667
+ * `footer` their meaning; such a box is also `isolation: isolate` and gets a
21668
+ * `scroll-padding` (see docs/scroll.md).
21669
+ * @param {boolean} [header] A part that stays put at the top of the scrolling
21670
+ * box it is a direct child of — nothing at all without that box's overflow.
21671
+ * Rendered as `<header>` unless `as` says otherwise.
21672
+ * @param {boolean} [footer] The same at the bottom, rendered as `<footer>`.
21673
+ * @param {boolean} [body] The only part of its scrolling box that scrolls,
21674
+ * `header`/`footer` sitting outside it. It shrinks, it never grows.
21675
+ * @param {boolean} [sticky] `position: sticky` in the sticky z-index band
21676
+ * (`--navi-z-index-sticky`), stuck or not; an explicit `zIndex`, `"auto"`
21677
+ * included, wins (see docs/z_index.md). `top`/`bottom`/… take `true` for 0.
21596
21678
  * @param {string} [background] Any CSS background, plus two keywords for the
21597
21679
  * planes an app paints: `"surface"` is the paper content sits on,
21598
21680
  * `"chrome"` the frame around a screen — a top bar, a side menu, a toolbar.
@@ -23284,21 +23366,16 @@ const createUITransitionController = (
23284
23366
  /**
23285
23367
  * UITransition
23286
23368
  *
23287
- * A Preact component that enables smooth animated transitions between its children when the content changes.
23288
- * It observes content keys and phases to create different types of transitions.
23289
- *
23290
- * Features:
23291
- * - Content transitions: Between different content keys (e.g., user profiles, search results)
23292
- * - Phase transitions: Between loading/content/error states for the same content key
23293
- * - Automatic size animation to accommodate content changes
23294
- * - Configurable transition types: "slide-left", "cross-fade"
23295
- * - Independent duration control for content and phase transitions
23369
+ * A Preact component that animates the change between its children: the
23370
+ * outgoing content fades out while the incoming one fades in, and the box
23371
+ * resizes from the one to the other (the mechanics are in ui_transition.js).
23296
23372
  *
23297
- * Usage:
23298
- * - Wrap dynamic content in <UITransition> to animate between states
23299
- * - Set a unique `data-content-id` on your rendered content to identify each content variant
23300
- * - Use `data-content-phase` to mark loading/error states for phase transitions
23301
- * - Configure transition types and durations for both content and phase changes
23373
+ * Two kinds of change are told apart, from attributes on the rendered content:
23374
+ * - a content transition, when the content key changes — `data-content-key` on
23375
+ * the rendered element (a user id, a search), or `useUITransitionContentId`
23376
+ * called from a component inside (what `<Route>` does with its url pattern);
23377
+ * - a phase transition, when `data-content-phase` (a loading or error state)
23378
+ * changes for the same content key.
23302
23379
  *
23303
23380
  * Example:
23304
23381
  *
@@ -23307,9 +23384,6 @@ const createUITransitionController = (
23307
23384
  * ? <Spinner data-content-key={userId} data-content-phase />
23308
23385
  * : <UserProfile user={user} data-content-key={userId} />}
23309
23386
  * </UITransition>
23310
- *
23311
- * When `data-content-id` changes, UITransition animates content transitions.
23312
- * When `data-content-phase` changes for the same key, it animates phase transitions.
23313
23387
  */
23314
23388
 
23315
23389
  const UITransitionContentIdContext = createContext();
@@ -23431,16 +23505,12 @@ const UITransition = ({
23431
23505
  };
23432
23506
 
23433
23507
  /**
23434
- * The goal of this hook is to allow a component to set a "content key"
23435
- * Meaning all content within the component is identified by that key
23436
- *
23437
- * When the key changes, UITransition will be able to detect that and consider the content
23438
- * as changed even if the component is still the same
23439
- *
23440
- * This is used by <Route> to set the content key to the route path
23441
- * When the route becomes inactive it will call useUITransitionContentId(undefined)
23442
- * And if a sibling route becones active it will call useUITransitionContentId with its own path
23508
+ * Names the content a component renders, for the <UITransition> around it:
23509
+ * everything rendered inside is identified by that key, and the key changing
23510
+ * is read as a content change even though the component is the same.
23443
23511
  *
23512
+ * <Route> calls it with the url pattern of the branch it renders; the branch
23513
+ * unmounting removes its part, and a sibling branch mounting adds its own.
23444
23514
  */
23445
23515
  const useUITransitionContentId = value => {
23446
23516
  const contentId = useContext(UITransitionContentIdContext);
@@ -23465,8 +23535,8 @@ const useUITransitionContentId = value => {
23465
23535
  };
23466
23536
 
23467
23537
  /**
23468
- * Custom route pattern matching system
23469
- * Replaces URLPattern with a simpler, more predictable approach
23538
+ * Custom route pattern matching system.
23539
+ * Not built on URLPattern: matching here is simpler and more predictable.
23470
23540
  */
23471
23541
 
23472
23542
  const debug$2 = (...args) => {
@@ -29251,8 +29321,8 @@ const TRANSITION_FURNITURE_CSS = /* css */ `.navi_fixed_bar[data-navi-transition
29251
29321
  }
29252
29322
  `;
29253
29323
 
29254
- // Called from the render of whatever holds the pages, never at module scope:
29255
- // a page that never travels between routes must not carry this sheet, and a
29324
+ // Called as a movement starts over a marked area, never at module scope: a
29325
+ // page that never travels between routes must not carry this sheet, and a
29256
29326
  // build that sees no caller drops the css with the function.
29257
29327
  const installTransitionFurnitureCss = () => {
29258
29328
  import.meta.css = [TRANSITION_FURNITURE_CSS, "@jsenv/navi/src/nav/transition_furniture.js"];
@@ -29715,9 +29785,10 @@ const TRANSITION_WINDOW_CSS = /* css */ `@property --navi-transition-cover-top {
29715
29785
  }
29716
29786
  `;
29717
29787
 
29718
- // Called from the render of whatever needs the window, never at module scope:
29719
- // a page that never travels between routes must not carry this sheet, and a
29720
- // build that sees no caller drops the css with the function.
29788
+ // Called by whatever needs the window — a render, or a movement starting —
29789
+ // never at module scope: a page that never travels between routes must not
29790
+ // carry this sheet, and a build that sees no caller drops the css with the
29791
+ // function.
29721
29792
  const installTransitionWindowCss = () => {
29722
29793
  import.meta.css = [TRANSITION_WINDOW_CSS, "@jsenv/navi/src/nav/transition_window.js"];
29723
29794
  };
@@ -30530,9 +30601,6 @@ const RouteTransitionArea = ({
30530
30601
  children,
30531
30602
  ...rest
30532
30603
  }) => {
30533
- import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
30534
- installTransitionWindowCss();
30535
- installTransitionFurnitureCss();
30536
30604
  const props = {
30537
30605
  ...rest,
30538
30606
  [TRANSITION_AREA_ATTRIBUTE]: ""
@@ -30547,8 +30615,9 @@ const RouteTransitionArea = ({
30547
30615
  * Declare how a pair of routes moves against each other — or how one route is
30548
30616
  * entered, from wherever its door happens to be.
30549
30617
  *
30550
- * @param {object|null} from - a route, or `{ route, params }` when the page is
30551
- * a param of a route rather than a route of its own. `null` says the page is
30618
+ * @param {object|null} from - a route (or the `routeFallback()` page), or
30619
+ * `{ route, params }` when the page is a param of a route rather than a route
30620
+ * of its own. `null` says the page is
30552
30621
  * reached FROM ANYWHERE: its door is in the furniture (a gear in the top
30553
30622
  * bar, a "+" in the tab bar) rather than on a screen, so there is no pair to
30554
30623
  * write it on. Arriving at `to` from any page then plays forward and leaving
@@ -30596,13 +30665,10 @@ const RouteTransitionArea = ({
30596
30665
  * Whatever is written here is what EVERY crossing of the pair plays. One
30597
30666
  * crossing can ask for something else — `<Link routeTransition>`, or
30598
30667
  * navTo(url, { routeTransition }) — which overrides this field by field, for
30599
- * that
30600
- * navigation alone.
30668
+ * that navigation alone.
30601
30669
  * @returns {() => void} remove this relation.
30602
30670
  */
30603
30671
  const defineRouteTransition = (from, to, transition) => {
30604
- import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
30605
- installTransitionWindowCss();
30606
30672
  if (!to) {
30607
30673
  throw new TypeError(`defineRouteTransition needs a destination: "to" is ${to}. The page reached from anywhere is written defineRouteTransition(null, THAT_PAGE, ...) — there is no relation the other way round, a page LEFT for anywhere being the back half of that one.`);
30608
30674
  }
@@ -30642,8 +30708,6 @@ const defineRouteTransition = (from, to, transition) => {
30642
30708
  * @returns {() => void} remove this default.
30643
30709
  */
30644
30710
  const defineRouteDefaultTransition = transition => {
30645
- import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
30646
- installTransitionWindowCss();
30647
30711
  const value = normalizeTransition(transition);
30648
30712
  defaultTransition = value;
30649
30713
  return () => {
@@ -30680,9 +30744,8 @@ const normalizeTransition = transition => {
30680
30744
  *
30681
30745
  * Two mouths, one meaning: the element being pressed wears it (a `<Link
30682
30746
  * routeTransition>`, or the attribute by hand on any anchor), or navTo() is
30683
- * handed
30684
- * it. Both arrive here through the announcement the navigation makes before it
30685
- * writes anything (see before_routing.js).
30747
+ * handed it. Both arrive here through the announcement the navigation makes
30748
+ * before it writes anything (see before_routing.js).
30686
30749
  *
30687
30750
  * A request answers FIELD BY FIELD: what it does not say, the relation — or
30688
30751
  * the default — still answers for. So `{ direction: "back" }` keeps the pair's
@@ -31226,6 +31289,10 @@ const beginTransition = ({
31226
31289
  // Said before the picture is taken: whoever names something for a movement
31227
31290
  // between two pages decides on it now (see transition_destination.js).
31228
31291
  holdTransitionDestination(transition, url);
31292
+ // Adopted by what starts a movement rather than by what declares one: a link
31293
+ // or a navTo() asks for a movement in an app that declared none, and an app
31294
+ // that never moves never carries the sheet.
31295
+ import.meta.css = [css$12, "@jsenv/navi/src/nav/route_transition.jsx"];
31229
31296
  documentElement.setAttribute(TRANSITION_ATTRIBUTE, direction);
31230
31297
  if (type) {
31231
31298
  documentElement.setAttribute(TRANSITION_TYPE_ATTRIBUTE, type);
@@ -31247,6 +31314,10 @@ const beginTransition = ({
31247
31314
  // left (see transition_window.js).
31248
31315
  let areaStateBefore = null;
31249
31316
  if (areaElement) {
31317
+ // The area is the application's element as often as <RouteTransitionArea>,
31318
+ // so what a movement on it plays with is adopted here, where it is found.
31319
+ installTransitionWindowCss();
31320
+ installTransitionFurnitureCss();
31250
31321
  documentElement.setAttribute(TRANSITION_TARGET_ATTRIBUTE, "area");
31251
31322
  // Said before the picture is taken, like every name (see
31252
31323
  // transition_furniture.js): what the bars are wearing when the transition
@@ -31929,10 +32000,10 @@ const css$11 = /* css */`.navi_route_travel {
31929
32000
  * The pages are cut at the edge of this box while they travel, and at the app's
31930
32001
  * safe area the box runs under, which is written on the transition's own
31931
32002
  * pseudo-elements — no overflow of the document reaches pictures drawn in the
31932
- * top layer. It needs nothing of the browser beyond view
31933
- * transitions themselves: a browser without them (Firefox) navigates without the
31934
- * movement, and the gesture applies its change on release instead of dragging a
31935
- * picture that does not exist.
32003
+ * top layer. It needs nothing of the browser beyond view transitions
32004
+ * themselves: a browser without them navigates without the movement, and the
32005
+ * gesture applies its change on release instead of dragging a picture that does
32006
+ * not exist.
31936
32007
  *
31937
32008
  * While a travel plays, the rest of the page is taken as a picture too — this
31938
32009
  * box asks for `view-transition-name: root` back for that time, so an
@@ -33508,9 +33579,6 @@ const onNaviCommand = (e, { debugCommand = () => {} } = {}) => {
33508
33579
  `targeting`,
33509
33580
  commandTarget,
33510
33581
  );
33511
- // Timed once, for the wheel-in-dialog "Définir feels frozen on mobile" case;
33512
- // the line is gone now that the answer is known — a command that runs in 1ms
33513
- // said nothing, and it said it on every single interaction.
33514
33582
  return implementation();
33515
33583
  };
33516
33584
 
@@ -34154,8 +34222,8 @@ registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
34154
34222
  target,
34155
34223
  implementation: () => {
34156
34224
  // The popup reads `anchor` first and falls back to the source (see
34157
- // onnavi_request_open in popover.jsx), so saying nothing keeps the old
34158
- // behaviour: the element asking is the element opened against.
34225
+ // onnavi_request_open in popover.jsx): without an `anchor`, the element
34226
+ // asking is the element opened against.
34159
34227
  return dispatchCustomEvent(target, "navi_request_open", {
34160
34228
  event,
34161
34229
  source: resolveCommandProxySource(source),
@@ -36570,8 +36638,8 @@ const usePopupContentMount = (
36570
36638
  * are no-ops.
36571
36639
  *
36572
36640
  * Behavior:
36573
- * - No <dialog>/<details>/[popover] ancestor → runs like a normal
36574
- * useLayoutEffect with the provided deps.
36641
+ * - No <dialog>/<details>/[popover]/[aria-expanded] ancestor → runs like a
36642
+ * normal useLayoutEffect with the provided deps.
36575
36643
  * - Inside a closed/hidden ancestor → skips the initial run; instead runs
36576
36644
  * the callback once the ancestor opens — see @jsenv/dom's own
36577
36645
  * observeAncestorOpenState for exactly how that's detected, and why it
@@ -36585,8 +36653,9 @@ const usePopupContentMount = (
36585
36653
  * reveals nothing about them, so it does not re-run them either. See
36586
36654
  * isDisplayedDespiteClosedAncestor in @jsenv/dom.
36587
36655
  *
36588
- * The callback's second argument is always a `navi_displayed` CustomEvent,
36589
- * with `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
36656
+ * The callback is called with the element (`ref.current`), and a
36657
+ * `navi_displayed` CustomEvent as its second argument, with
36658
+ * `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
36590
36659
  * - No <dialog>/<details>/[popover]/[aria-expanded] ancestor at all →
36591
36660
  * `{ ancestor: document, ancestorType: "document" }`.
36592
36661
  * - Otherwise → `{ ancestor: <the matched element>, ancestorType: "dialog"
@@ -36751,6 +36820,8 @@ const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
36751
36820
  // see also https://github.com/preactjs/preact/issues/1255
36752
36821
 
36753
36822
 
36823
+ const NO_ELEMENT_REF = { current: null };
36824
+
36754
36825
  /**
36755
36826
  * Programmatic autofocus that runs after Preact layout effects are flushed.
36756
36827
  *
@@ -36803,13 +36874,12 @@ const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
36803
36874
  * the browser's built-in scroll-into-view that accompanies focus.
36804
36875
  * @param {boolean} [options.focusVisible]
36805
36876
  * Passed as `focusVisible` to `element.focus()`.
36806
- * @param {boolean} [options.autoFocusSelect]
36877
+ * @param {boolean} [options.autoSelect]
36807
36878
  * When true, also calls `element.select()` after focusing (useful for text inputs).
36808
- * @returns {Function} triggerAutofocus — can be called manually with a synthetic
36809
- * event to re-run the focus logic outside of the layout-effect lifecycle.
36879
+ * @returns {object} The attributes to spread on the element: `navi-autofocus`
36880
+ * (and `navi-autofocus-select`), with `autoFocus` set to undefined so the
36881
+ * native attribute never lands on the node.
36810
36882
  */
36811
- const NO_ELEMENT_REF = { current: null };
36812
-
36813
36883
  const useAutoFocus = (
36814
36884
  focusableElementRef,
36815
36885
  autoFocus,
@@ -37002,9 +37072,10 @@ document.body.addEventListener(
37002
37072
  * and nothing about it stops someone from starting a run on every row before
37003
37073
  * any of them comes back: a dozen requests in flight, a dozen rows waiting, and
37004
37074
  * a server asked to do a dozen things at once because a list happened to be
37005
- * long. The guard is what makes the surface say "not yet" instead — the rows
37006
- * that are not running go read-only and say what they are waiting for, and the
37007
- * next press is possible again the moment one of the runs comes back.
37075
+ * long. The guard is what makes the surface say "not yet" instead — the
37076
+ * controls that would start another run go read-only and say what they are
37077
+ * waiting for, and the next press is possible again the moment one of the runs
37078
+ * comes back.
37008
37079
  *
37009
37080
  * It counts runs, not values — which is what separates it from
37010
37081
  * `maxLengthGuard`, the one that says how many things a selection may HOLD.
@@ -37349,14 +37420,13 @@ const createControlGuard = (controller) => {
37349
37420
  };
37350
37421
 
37351
37422
  /**
37352
- * Orchestrates the three rule managers for a UI state controller.
37353
- *
37354
- * Instead of holding `controller.controlInteraction` + `controller.controlValidity`
37355
- * independently, a controller now has a single `controller.rules` object:
37423
+ * Orchestrates the rule managers of a UI state controller, held together in
37424
+ * one `controller.rules` object:
37356
37425
  *
37357
37426
  * controller.rules.callout — shared callout display manager
37358
37427
  * controller.rules.interaction — interactivity gate (disabled/readonly/busy)
37359
37428
  * controller.rules.validation — value validity gate (required/pattern/etc.)
37429
+ * controller.rules.guard — keystroke guard (charGuard/maxLengthGuard)
37360
37430
  *
37361
37431
  * The callout manager is created first so both interaction and validation
37362
37432
  * can use the same callout slot and the same lifecycle hooks.
@@ -39472,9 +39542,8 @@ const EMPTY_OBJECT = {};
39472
39542
  * child changes → we update picker input → navi_ui_state_change fires →
39473
39543
  * we see updatingRef=true → skip → no loop.
39474
39544
  *
39475
- * This removes the need for `command="--navi-update"` on controls placed
39476
- * inside the picker popup. It also means `commands.js` no longer has to
39477
- * manually re-dispatch to inner controls.
39545
+ * So a control placed inside the picker popup needs no
39546
+ * `command="--navi-update"` to reach the picker input.
39478
39547
  */
39479
39548
  const useUIFacadeStateController = (props, realUIStateController) => {
39480
39549
  const firstChildControllerRef = useRef(null);
@@ -39886,9 +39955,10 @@ const uiStateHoldsNothing = (uiState) => {
39886
39955
  * use `onInput` internally and route through `dispatchRequestAction` so the behavior
39887
39956
  * is consistent regardless of input type.
39888
39957
  *
39889
- * 2. Any field (text, checkbox, radio, picker…) can opt into debounce simply by passing
39890
- * a debounced action. The request-action event chain handles the timing centrally
39891
- * rather than each component having to manage its own debounce logic.
39958
+ * 2. Any field (text, checkbox, radio, picker…) can opt into debounce with
39959
+ * `actionDebounce` (ms). The input effect every field installs (input_effect.js)
39960
+ * handles the timing centrally rather than each component having to manage its
39961
+ * own debounce logic.
39892
39962
  */
39893
39963
  const NO_ACTION_YET = Symbol("no_action_yet");
39894
39964
 
@@ -39964,7 +40034,7 @@ const ControlgroupChildrenWrapper = ({
39964
40034
  });
39965
40035
 
39966
40036
  /**
39967
- * Core hook for interactive field components (InputText, InputCheckbox, etc.).
40037
+ * Core hook for interactive field components (InputTextual, InputCheckbox, etc.).
39968
40038
  *
39969
40039
  * Sets up the full field lifecycle:
39970
40040
  * - Creates a UI state controller that manages state divergence between props and user interactions
@@ -39975,7 +40045,7 @@ const ControlgroupChildrenWrapper = ({
39975
40045
  * - Handles constraint validation and message props
39976
40046
  *
39977
40047
  * All state changes route through DOM events on the field element so that
39978
- * external subscribers (e.g. useUIState, Selectable) receive every update.
40048
+ * external subscribers (e.g. useControlUIState, Selectable) receive every update.
39979
40049
  *
39980
40050
  * @returns {[controlRootProps, controlHostProps, { uiStateController }]}
39981
40051
  */
@@ -41048,10 +41118,10 @@ const useReadOnlyUncontrolled = (props, controlInfo) => {
41048
41118
  };
41049
41119
 
41050
41120
  /**
41051
- * Core hook for field group components (SelectableList, CheckboxList, etc.).
41121
+ * Core hook for field group components (List selectable, CheckboxGroup, etc.).
41052
41122
  * - Creates a UI group state controller that aggregates child states into one group state
41053
41123
  * - Binds the group's action to the aggregated state signal
41054
- * - Provides context to children: ParentUIStateController, FieldName, Disabled, ReadOnly,
41124
+ * - Provides context to children: ParentUIStateController, ControlName, Disabled, ReadOnly,
41055
41125
  * Required, Loading, Action, ActionRequester
41056
41126
  * - Overrides `onnavi_reset_ui_state` to cascade resets to all monitored children
41057
41127
  * by dispatching `navi_reset_ui_state` DOM events on each child's DOM element
@@ -43580,10 +43650,10 @@ const LoadingRectangleSvg = ({
43580
43650
  }) => {
43581
43651
  // The stroke is centered on the path, so half of it falls outside: drawing
43582
43652
  // at half the stroke width puts its outer edge exactly on the box, whatever
43583
- // the box. It used to be a share of the smaller dimension (3%, floored at
43584
- // 2px), which held the line further and further inside as the box grew — a
43585
- // caller placing the outline with `inset` got the distance it asked for on a
43586
- // field and something several pixels inward on a card. Nothing needs the
43653
+ // the box. Not a share of the smaller dimension (3%, floored at 2px): that
43654
+ // holds the line further and further inside as the box grows — a caller
43655
+ // placing the outline with `inset` gets the distance it asked for on a field
43656
+ // and something several pixels inward on a card. Nothing needs the
43587
43657
  // extra room: the svg is `overflow: visible` and whoever positions the
43588
43658
  // wrapper already reserves the half stroke.
43589
43659
  const margin = strokeWidth / 2;
@@ -44236,16 +44306,6 @@ const css$Z = /* css */`@layer navi {
44236
44306
  border-radius: .2em;
44237
44307
  }
44238
44308
 
44239
- &[data-capitalize] {
44240
- text-transform: capitalize;
44241
-
44242
- & .navi_text_sizer {
44243
- & .navi_text {
44244
- display: inline-block;
44245
- }
44246
- }
44247
- }
44248
-
44249
44309
  &[data-shrinkwrap] {
44250
44310
  display: inline-block;
44251
44311
  }
@@ -44268,12 +44328,6 @@ time.navi_text {
44268
44328
  }
44269
44329
 
44270
44330
  .navi_text {
44271
- &[data-capitalize] {
44272
- &:first-letter, & .navi_text_sizer_placeholder:first-letter, & .navi_text_sizer_overlay:first-letter {
44273
- text-transform: uppercase;
44274
- }
44275
- }
44276
-
44277
44331
  & .navi_text_sizer, & .navi_text_sizer_placeholder, & .navi_text_sizer_overlay {
44278
44332
  display: inherit;
44279
44333
  width: inherit;
@@ -44655,7 +44709,7 @@ const shouldInjectSpacingBetween = (left, right) => {
44655
44709
  * boldStable?: boolean,
44656
44710
  * shrinkWrap?: boolean,
44657
44711
  * capitalize?: boolean,
44658
- * selectRange?: string | [number, number],
44712
+ * selectRange?: string | [number, number] | [string, string],
44659
44713
  * childrenOutsideFlow?: import("ignore:preact").ComponentChildren,
44660
44714
  * [key: string]: any,
44661
44715
  * }>}
@@ -44721,11 +44775,14 @@ const shouldInjectSpacingBetween = (left, right) => {
44721
44775
  * text block from being wider than its content when inside a flex/grid container.
44722
44776
  *
44723
44777
  * @param {boolean} [capitalize]
44724
- * Uppercases the first letter of the text content via CSS.
44778
+ * Uppercases the first letter of the text, and only that one: "lundi 11 mai"
44779
+ * reads "Lundi 11 mai". Applied to the string the text starts with; a text
44780
+ * starting with an element keeps its first letter as written.
44725
44781
  *
44726
- * @param {string|[number,number]} [selectRange]
44727
- * Selects a portion of the text on mount. Pass a substring to search for, or
44728
- * a `[start, end]` character-offset tuple.
44782
+ * @param {string|[number,number]|[string,string]} [selectRange]
44783
+ * Selects a portion of the text on mount. Pass a substring to search for, a
44784
+ * `[start, end]` character-offset tuple, or a `[startText, endText]` pair of
44785
+ * substrings selecting from the first to the end of the second.
44729
44786
  *
44730
44787
  * @param {import("ignore:preact").ComponentChildren} [childrenOutsideFlow]
44731
44788
  * Rendered after children but outside the text spacing/flow logic. Used
@@ -44840,11 +44897,13 @@ const TextUI = props => {
44840
44897
  shrinkWrap,
44841
44898
  ...rest
44842
44899
  } = props;
44900
+ if (capitalize) {
44901
+ children = capitalizeFirstLetter(children);
44902
+ }
44843
44903
  const defaultSpace = preventSpaceUnderlines ? FAKE_SPACE : REGULAR_SPACE;
44844
44904
  const resolvedSpacing = spacing ?? defaultSpace;
44845
44905
  const boxProps = {
44846
44906
  "as": "span",
44847
- "data-capitalize": capitalize ? "" : undefined,
44848
44907
  "data-shrinkwrap": shrinkWrap ? "" : undefined,
44849
44908
  ...rest,
44850
44909
  ref,
@@ -45145,6 +45204,28 @@ const Icon = ({
45145
45204
  });
45146
45205
  };
45147
45206
 
45207
+ // On the string rather than with ::first-letter, which needs a block container:
45208
+ // an inline text made one is an atomic box, and the ellipsis of the line
45209
+ // holding it (a Picker's value) can then no longer cut it. The string is also
45210
+ // what every copy of the children (sizer, bold background) renders, so they
45211
+ // all read the same. Leading spaces and punctuation are skipped, and a text
45212
+ // starting with a digit is left as it is, as ::first-letter would.
45213
+ const capitalizeFirstLetter = children => {
45214
+ if (typeof children === "string") {
45215
+ return children.replace(FIRST_LOWERCASE_LETTER_REGEXP, (match, lead, letter) => `${lead}${letter.toLocaleUpperCase()}`);
45216
+ }
45217
+ if (Array.isArray(children) && children.length > 0) {
45218
+ const first = children[0];
45219
+ const firstCapitalized = capitalizeFirstLetter(first);
45220
+ if (firstCapitalized === first) {
45221
+ return children;
45222
+ }
45223
+ return [firstCapitalized, ...children.slice(1)];
45224
+ }
45225
+ return children;
45226
+ };
45227
+ const FIRST_LOWERCASE_LETTER_REGEXP = /^([\s\p{P}]*)(\p{Ll})/u;
45228
+
45148
45229
  /**
45149
45230
  * Where a link leads, asked for when the pointer or the focus arrives on it:
45150
45231
  * the moment the user signals a destination, before the press. Only what the
@@ -45595,7 +45676,7 @@ Object.assign(PSEUDO_CLASSES, {
45595
45676
  * @param {object} props
45596
45677
  * @param {string} [props.href] - Destination. Also the default `value`, and
45597
45678
  * (when `hrefFallback`) the default visible text.
45598
- * @param {import("../route.js").Route} [props.route] - Renders via `route`
45679
+ * @param {object} [props.route] - Renders via `route`
45599
45680
  * instead of a raw `href`: the URL is built from the route (see
45600
45681
  * `routeParams`) and "current" is derived from whether the route matches.
45601
45682
  * @param {object} [props.routeParams] - Params passed to `route.buildUrl`.
@@ -45619,14 +45700,14 @@ Object.assign(PSEUDO_CLASSES, {
45619
45700
  * (`navi_value`); defaults to `href`.
45620
45701
  * @param {boolean} [props.current] - Forces the "current" state on (otherwise
45621
45702
  * derived from the href/route).
45622
- * @param {import("../route.js").Route|import("../route.js").Route[]} [props.currentExcept] -
45703
+ * @param {object|object[]} [props.currentExcept] -
45623
45704
  * Route(s) inside this link's own that are NOT it: while one of them matches,
45624
45705
  * the link is not current even though its route still is. A bar entry
45625
45706
  * standing for a whole section, and one place under that section the entry
45626
45707
  * does not stand for — settings reached from everywhere and shown over
45627
45708
  * whatever the reader was on. Nothing about the routing changes: the url is
45628
45709
  * in the section, only the link stops claiming "you are here".
45629
- * @param {import("../route.js").Route|import("../route.js").Route[]} [props.currentAlso] -
45710
+ * @param {object|object[]} [props.currentAlso] -
45630
45711
  * Route(s) other than this link's own that are it: the link is current while
45631
45712
  * one of them matches. The other side of `currentExcept` — an entry standing
45632
45713
  * for more than the one page it opens. `currentExcept` wins over it.
@@ -46530,6 +46611,7 @@ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
46530
46611
  * currentIndicator?: boolean|"top"|"bottom"|"left"|"right",
46531
46612
  * currentIndicatorSlides?: boolean,
46532
46613
  * slideContainer?: string,
46614
+ * panelPosition?: "before"|"after",
46533
46615
  * }>}
46534
46616
  * @param {boolean|"top"|"bottom"|"left"|"right"} [props.currentIndicator] - the
46535
46617
  * bar that says which tab one is on, said once here rather than on every
@@ -46555,6 +46637,9 @@ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
46555
46637
  * container says which one is on screen, and pressing a tab travels there.
46556
46638
  * Tabs that are places in the same screen rather than pages of their own —
46557
46639
  * nothing is written to the URL and nothing is a link.
46640
+ * @param {"before"|"after"} [props.panelPosition] - turns the row into folder
46641
+ * tabs: the current tab and the panel it opens share one surface, and this
46642
+ * says which side of the row the panel sits on.
46558
46643
  */
46559
46644
  const Nav = ({
46560
46645
  children,
@@ -47677,6 +47762,8 @@ const TABS_ALIGN_TO_JUSTIFY_CONTENT = {
47677
47762
  * onChange?: (value: any) => void,
47678
47763
  * tabsPosition?: "top"|"bottom"|"left"|"right",
47679
47764
  * tabsAlign?: "start"|"center"|"end"|"stretch",
47765
+ * scrollablePage?: boolean,
47766
+ * maxLines?: number|false,
47680
47767
  * borderWidth?: string|number,
47681
47768
  * borderRadius?: string|number,
47682
47769
  * tabBorderRadius?: string|number,
@@ -47856,7 +47943,7 @@ const Binder = ({
47856
47943
  * position among its siblings.
47857
47944
  * @param {import("ignore:preact").ComponentChildren} [props.label] - What the tab
47858
47945
  * shows.
47859
- * @param {import("../route.js").Route} [props.route] - Makes the tab a `Link`
47946
+ * @param {object} [props.route] - Makes the tab a `Link`
47860
47947
  * to that route, and the binder follows the url: the open item is the one
47861
47948
  * whose route matches, no `value`/`onChange` needed.
47862
47949
  * @param {string} [props.href] - Same, for a plain url.
@@ -48295,6 +48382,9 @@ const FixedBarStyleCSSVars = {
48295
48382
  * `border={false}` for none.
48296
48383
  * @param {string|number} [props.maxWidth] - Keeps the bar lined up with a
48297
48384
  * content column narrower than the window (it stays centered).
48385
+ * @param {string|number} [props.padding=0] - Along the bar only: left/right on
48386
+ * a top or bottom bar, top/bottom on a side one. Across it, the size is
48387
+ * `height`/`width`.
48298
48388
  */
48299
48389
  const FixedBar = ({
48300
48390
  children,
@@ -49163,9 +49253,10 @@ const useExpandableContext = partName => {
49163
49253
  * height too. `"idle"` builds it in a browser idle moment after load.
49164
49254
  * `"while-opened"` throws the content away once the collapse
49165
49255
  * settles — after the closing animation, so it still plays on real
49166
- * content — and rebuilds it from scratch on every expansion. Whatever the
49167
- * value, intent on the UI part (pointer entering it, focus landing in it)
49168
- * builds the content ahead of the click.
49256
+ * content — and rebuilds it from scratch on every expansion. Under the
49257
+ * other three, intent on the UI part (pointer entering it, focus landing in
49258
+ * it) builds the content ahead of the click; `"while-opened"` content is
49259
+ * never warmed, it is built at the open.
49169
49260
  */
49170
49261
  const Expandable = props => {
49171
49262
  import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
@@ -49942,7 +50033,8 @@ const DARK_CONTRAST_LIGHTNESS_THRESHOLD = 0.65;
49942
50033
  * @param {string} accentColor - The accent color value. When it changes, attributes are recomputed.
49943
50034
  * @param {object} [options]
49944
50035
  * @param {string} [options.elementSelector] - CSS selector to find the element whose computed color is read.
49945
- * Defaults to the root element itself. Useful when the color is applied to a probe/child element.
50036
+ * Defaults to the root's own `data-visual-selector` attribute, else the root element itself.
50037
+ * Useful when the color is applied to a probe/child element.
49946
50038
  * @param {string} [options.colorProperty="backgroundColor"] - Computed style property to read (e.g. "color", "borderColor").
49947
50039
  */
49948
50040
  const useAccentColorAttributes = (
@@ -51981,16 +52073,18 @@ const css$L = /* css */`@layer navi {
51981
52073
  /**
51982
52074
  * Field — a semantic wrapper that connects a label to a form control.
51983
52075
  *
51984
- * It generates a stable `fieldId` (or accepts an explicit `id`) that is
52076
+ * It generates a stable `fieldId` (or accepts an explicit one) that is
51985
52077
  * automatically forwarded to the `Label` inside the field as `htmlFor` and to
51986
- * any interactive control (Picker, Input, …) as its `id`, so clicking the
51987
- * label focuses the control without requiring manual wiring.
52078
+ * the labelable control inside it (Input, Select, Picker, Button) as its `id`,
52079
+ * so clicking the label focuses the control without requiring manual wiring.
51988
52080
  *
51989
52081
  * It also tracks the readOnly / disabled / interactive state reported by its
51990
52082
  * child control and reflects it on the `Label` (dimmed color, cursor change).
51991
52083
  *
51992
52084
  * Props:
51993
- * id — optional explicit id used as the field id instead of the auto-generated one
52085
+ * fieldId — optional explicit field id instead of the auto-generated one (an `id`
52086
+ * is the wrapper's own DOM id)
52087
+ * as="label" — the field IS the label, wrapping its control: no id needed
51994
52088
  * flex="y" — vertical layout; automatically sets alignX="start" and data-vertical
51995
52089
  * children — any JSX; should contain a `Label` and a form control
51996
52090
  * ...rest — forwarded to the wrapping element (className, style, flex, spacing, …)
@@ -52197,6 +52291,7 @@ const InputUnitSlot = ({
52197
52291
  };
52198
52292
  const InputSlot = ({
52199
52293
  side,
52294
+ children,
52200
52295
  ...props
52201
52296
  }) => {
52202
52297
  const ctx = useContext(InputTextualContext);
@@ -52226,7 +52321,11 @@ const InputSlot = ({
52226
52321
  e.preventDefault();
52227
52322
  }
52228
52323
  },
52229
- ...props
52324
+ ...props,
52325
+ children: jsx(ControlIdContext.Provider, {
52326
+ value: undefined,
52327
+ children: children
52328
+ })
52230
52329
  });
52231
52330
  };
52232
52331
 
@@ -53691,9 +53790,9 @@ const InputWithListResolver = props => {
53691
53790
  };
53692
53791
 
53693
53792
  /**
53694
- * InputWithList — connects an input to a SelectableList via its id.
53793
+ * InputWithList — connects an input to a selectable List via its id.
53695
53794
  *
53696
- * Usage: <Input navi-list="my-list-id" /> next to <SelectableList id="my-list-id" />
53795
+ * Usage: <Input navi-list="my-list-id" /> next to <List selectable id="my-list-id">
53697
53796
  *
53698
53797
  * Behavior:
53699
53798
  * - ArrowDown / ArrowUp move the list's "current item" without moving focus
@@ -54061,11 +54160,14 @@ const useAutoSelectReadOnly = (props) => {
54061
54160
  *
54062
54161
  * - charGuard — restricts which characters can be typed, pasted, or set externally.
54063
54162
  * Accepts a preset name or a raw regex character class:
54064
- * "numeric" → digits only, sets inputMode="numeric" + pattern auto
54163
+ * true / "auto" → picked from the field: inputMode numeric/decimal, type tel/email
54164
+ * "numeric" → digits only, sets inputMode="numeric"
54165
+ * "decimal" → digits, minus, dot, comma, sets inputMode="decimal"
54065
54166
  * "alpha" → letters only
54066
54167
  * "alphanumeric" → letters and digits
54067
54168
  * "uppercase" → uppercase letters only
54068
54169
  * "tel" → phone chars (digits, +, -, parens, space), sets inputMode="tel"
54170
+ * "email" → what an email address is made of
54069
54171
  * "card" → credit card (digits and spaces), sets inputMode="numeric"
54070
54172
  * "hex" → hexadecimal digits
54071
54173
  * "pin" → numeric PIN, sets inputMode="numeric"
@@ -55133,18 +55235,13 @@ const commitSubtree = (controller, e) => {
55133
55235
  };
55134
55236
 
55135
55237
  /**
55136
- *
55137
- * Here we want the same behaviour as web standards:
55138
- *
55139
- * 1. When submitting the form URL does not change
55140
- * 2. When form submission id done user is redirected (by default the current one)
55141
- * (we can configure this using target)
55142
- * So for example user might be reidrect to a page with the resource he just created
55143
- * I could create an example where we would put a link on the page to let user see what he created
55144
- * but by default user stays on the form allowing to create multiple resources at once
55145
- * And an other where he is redirected to the resource he created
55146
- * 3. If form submission fails ideally we should display this somewhere on the UI
55147
- * right now it's just logged to the console I need to see how we can achieve this
55238
+ * <Form>: the <form> element as a navi control group. Its named fields
55239
+ * aggregate into one object; a submit checks their constraints, runs `action`
55240
+ * with that object when it changed (docs/form_changed.md), and `command` — or
55241
+ * the surface the form sits in — says what follows. The URL does not change on
55242
+ * a submit, and a failing action is drawn as an error callout on the button
55243
+ * that sent it (docs/error_handling.md). A form inside another form renders no
55244
+ * <form> element (FormNested).
55148
55245
  */
55149
55246
 
55150
55247
  const Form = props => {
@@ -55764,9 +55861,9 @@ const offsetToPx = (offset, box) => {
55764
55861
 
55765
55862
  // A press landing while the track is already travelling: what is playing is
55766
55863
  // sent home in a fifth of the time it has left, and the press it could not take
55767
- // yet is taken as soon as it lands. A press has to be FELT — nudging the pace
55768
- // of a travel already in flight (what this used to do) reads as "nothing
55769
- // happened", because the thing was moving before the click too. Getting there
55864
+ // yet is taken as soon as it lands. A press has to be FELT — merely nudging the
55865
+ // pace of a travel already in flight reads as "nothing happened", because the
55866
+ // thing was moving before the click too. Getting there
55770
55867
  // almost at once and setting off again is the click being answered.
55771
55868
  // Played out fast rather than cut short: ending it on the spot would jump.
55772
55869
  // Compounds, so two presses during one travel bring it home twice as sharply,
@@ -58337,13 +58434,12 @@ const SlideNavButton = ({
58337
58434
  autoFocus: "last-resort",
58338
58435
  icon: true,
58339
58436
  variant: "discrete"
58340
- // Takes the focus like any other button, on purpose. It used to refuse it
58437
+ // Takes the focus like any other button, on purpose. Do not refuse it
58341
58438
  // (mousedown.preventDefault) to keep the keyboard where the user had put
58342
- // it — but pressing a way out with nothing focused then left the keyboard
58343
- // on nothing at all: the travel below only hands the focus to the slide
58344
- // arriving when it was leaving a slide, so a click from document.body
58345
- // arrived on document.body, and the next Tab started from the top of the
58346
- // page rather than from what is on screen.
58439
+ // it: the travel below only hands the focus to the slide arriving when it
58440
+ // was leaving a slide, so a press with nothing focused would arrive on
58441
+ // document.body, and the next Tab would start from the top of the page
58442
+ // rather than from what is on screen.
58347
58443
  //
58348
58444
  // Letting it focus is both the plain behaviour of a button and what makes
58349
58445
  // the rest fall into place: the press lands on the chevron, the slide left
@@ -58360,12 +58456,6 @@ const SlideNavButton = ({
58360
58456
  })
58361
58457
  });
58362
58458
 
58363
- /**
58364
- * The way out of a slide, and the way into the next one. Nothing but the
58365
- * command plus the chevron that matches the travel: a row goes left/right, a
58366
- * column up/down — so the button points where the slide actually goes without
58367
- * the caller having to keep the two in sync.
58368
- */
58369
58459
  /**
58370
58460
  * A way out pointing where it goes. It says a direction, not a slide: what is
58371
58461
  * over there is the map's business, and moving a screen changes nothing here.
@@ -59980,7 +60070,6 @@ const createSwipeToClose = (side, { grip } = {}) => {
59980
60070
  panelEl.style.transform = "";
59981
60071
  panelEl.style.clipPath = "";
59982
60072
  panelEl.style.transitionProperty = "";
59983
- panelEl.style.userSelect = "";
59984
60073
  };
59985
60074
  // The resting value is written first and the animation only covers the way
59986
60075
  // to it: both end on the same number, so nothing is seen changing when the
@@ -60024,10 +60113,6 @@ const createSwipeToClose = (side, { grip } = {}) => {
60024
60113
  return false;
60025
60114
  }
60026
60115
  panelEl.style.transitionProperty = "none";
60027
- panelEl.style.userSelect = "none";
60028
- // What the first pixels of the gesture may have started selecting is
60029
- // not a selection, it is the beginning of this travel.
60030
- window.getSelection().removeAllRanges();
60031
60116
  // The way out is the only way there is anything: pulling the other way
60032
60117
  // leans against a wall and comes back.
60033
60118
  return {
@@ -60698,7 +60783,7 @@ const css$E = /* css */`
60698
60783
  --navi-app-width rather than 3vvw): the gap must read as a small
60699
60784
  margin around the dialog, and 3% of a 1500px window is a 45px gap
60700
60785
  around a 600px app. Identical to 3vvw until the app declares
60701
- --navi-app-max-width. */
60786
+ bands (--navi-app-max-width, or --navi-app-inset-* written by it). */
60702
60787
  --x-dialog-container-spacing: calc(0.03 * var(--navi-app-width));
60703
60788
 
60704
60789
  /* --navi-app-width, not --navi-vvw: a top-layer dialog is calibrated on
@@ -61415,7 +61500,7 @@ const css$E = /* css */`
61415
61500
  * within its container (the viewport for `layer="top"`, the positioned
61416
61501
  * ancestor for `layer="local"`) — Dialog is never anchored to a real
61417
61502
  * element for positioning purposes. Same grammar as `Popover`'s own
61418
- * `positionArea` (see `popup_shared.js`'s `parsePositionArea`): a single
61503
+ * `positionArea` (see `parsePositionArea` in @jsenv/dom): a single
61419
61504
  * compass token — `top`/`top-start`/`top-end`/`top-left`/`top-right`,
61420
61505
  * `right`/`right-start`/`right-end`, `bottom`/`bottom-start`/
61421
61506
  * `bottom-end`/`bottom-left`/`bottom-right`, `left`/`left-start`/
@@ -61434,12 +61519,12 @@ const css$E = /* css */`
61434
61519
  * between the dialog and the edges of its container, whatever its
61435
61520
  * `positionArea`: it both caps the dialog's own size (via
61436
61521
  * `--x-dialog-container-spacing`, written from this prop) and offsets a docked
61437
- * one from the edge it docks to. Accepts a spacing token ("s", "m"…), a
61438
- * number of pixels, or a viewport length — "appw"/"apph" being the app's own
61439
- * screen (the visual viewport, or the narrower one the app declared with
61440
- * --navi-app-max-width) and "vvw"/"vvh" the visual viewport itself, which
61441
- * shrinks when the mobile keyboard opens. Pass 0 for a dialog
61442
- * meant to sit flush (a side panel).
61522
+ * one from the edge it docks to. Accepts a number of pixels, a viewport
61523
+ * length — "appw"/"apph" being the app's own screen (the visual viewport
61524
+ * minus the app's bands, see docs/safe_area.md) and
61525
+ * "vvw"/"vvh" the visual viewport itself, which shrinks when the mobile
61526
+ * keyboard opens — or a container length ("3cqw", the `layer="local"`
61527
+ * default). Pass 0 for a dialog meant to sit flush (a side panel).
61443
61528
  * @param {boolean} [props.backdrop=true] - Whether anything is laid between
61444
61529
  * the dialog and the page at all — asked before any question of what an
61445
61530
  * outside press does or how the backdrop is painted. `false` leaves the page
@@ -61452,10 +61537,12 @@ const css$E = /* css */`
61452
61537
  * needs. What it gives up is what only a modal gets natively: the hardware
61453
61538
  * back button no longer dismisses it. See docs/popup_backdrop.md.
61454
61539
  * @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="close"]
61455
- * - `"close"` closes the dialog on an outside press. `"capture"`/`"ignore"`
61456
- * both just absorb the press without closing (visually dimmed backdrop vs.
61457
- * not) — a dialog with a backdrop is modal one way or another, so there's
61458
- * always at least a press-absorbing backdrop regardless of this prop.
61540
+ * - `"close"` closes the dialog on an outside press, `"cancel"` closes it
61541
+ * as a cancel (what it holds reverts, as on Escape). `"capture"`/`"ignore"`
61542
+ * both just absorb the press without closing (`"capture"` paints the
61543
+ * blurred glass, `"ignore"` leaves the wall's paint alone) — a wall absorbs
61544
+ * the press whatever this prop says, natively for a modal and through the
61545
+ * sibling element for a local one.
61459
61546
  * "Outside" is the dialog's own border box; a see-through dialog whose box is
61460
61547
  * bigger than what it paints marks the difference with
61461
61548
  * `data-navi-popup-outside` (see docs/popup_backdrop.md).
@@ -61530,11 +61617,10 @@ const css$E = /* css */`
61530
61617
  * @param {string} [props.animationDuration] - Maps to
61531
61618
  * `--popup-animation-duration`.
61532
61619
  * @param {Element|{current: Element}|string} [props.anchor] - Never used for
61533
- * positioning (see this file's top comment), and ignored entirely unless
61534
- * `sizeFromAnchor` or `animation="lifting"` asks for it — the first sizes
61535
- * the dialog via the `--anchor-width`/`--anchor-height` CSS vars, the second
61536
- * makes the dialog come out of the anchor's own box. Used when the open
61537
- * itself names none — an anchor carried by the opening event
61620
+ * positioning (see this file's top comment); read by `sizeFromAnchor` (the
61621
+ * `--anchor-width`/`--anchor-height` CSS vars), by `animation="lifting"`
61622
+ * (the box the dialog comes out of) and by `mount`'s warm-on-intent. Used
61623
+ * when the open itself names none — an anchor carried by the opening event
61538
61624
  * (`e.detail.anchor`) is about that one opening and comes first; left out,
61539
61625
  * whatever triggered the open (`e.detail.source`). A string is resolved via
61540
61626
  * `document.getElementById` when the dialog opens — see popover.jsx's own
@@ -61596,6 +61682,9 @@ const css$E = /* css */`
61596
61682
  * focusable of its own.
61597
61683
  * - `"restore"` — the dialog stays out of the opening focus chain unless it
61598
61684
  * held focus when it closed.
61685
+ * - `false` — the dialog element itself is never where the focus lands; the
61686
+ * opening still hands the focus to what the dialog holds. Unlike
61687
+ * `Popover`'s `false`, it does not leave the keyboard where it was.
61599
61688
  * Wherever the keyboard is a virtual one (a touch device), the surface is
61600
61689
  * already what one arrives on: a popup is read before it is reached there, so
61601
61690
  * the focus only leaves it for something that asked by name (`autoFocus` on
@@ -61839,8 +61928,8 @@ const useDialogProps = props => {
61839
61928
  // `backdrop` — see this file's top comment.
61840
61929
  layer = "top",
61841
61930
  dockedOnSmallTouchScreen,
61842
- // Same grammar as Popover's own positionArea — see this file's top
61843
- // comment and popup_shared.js's parsePositionArea.
61931
+ // Same grammar as Popover's own positionArea — see parsePositionArea in
61932
+ // @jsenv/dom.
61844
61933
  positionArea: positionAreaProp,
61845
61934
  // A dialog docked against an edge must keep the same gap its own size cap
61846
61935
  // already guarantees a centered one — so this drives both (see
@@ -61880,8 +61969,8 @@ const useDialogProps = props => {
61880
61969
  sizing = "auto",
61881
61970
  animation,
61882
61971
  lift = "box",
61883
- // Inert unless sizeFromAnchor below (see this file's top comment) —
61884
- // Dialog's own positioning is never relative to it.
61972
+ // Never positioned against (see this file's top comment): read by
61973
+ // sizeFromAnchor, by a lift, and by the warm-on-intent of `mount`.
61885
61974
  anchor,
61886
61975
  // Where a lift comes back to, when that is no longer the box it came out
61887
61976
  // of. Read at the close, not kept from the opening — see
@@ -61891,8 +61980,8 @@ const useDialogProps = props => {
61891
61980
  // See this prop's own JSDoc above for why a dialog does not follow its
61892
61981
  // trigger's box by default.
61893
61982
  sizeFromAnchor = false,
61894
- // Same meaning as Popover's own prop, applied to the only thing an anchor
61895
- // can do here: sizing under sizeFromAnchor.
61983
+ // Same meaning as Popover's own prop, applied to what an anchor does here:
61984
+ // sizing under sizeFromAnchor, and a lift.
61896
61985
  anchorCustomEventDetail = "override",
61897
61986
  // Makes the dialog itself a valid focus target so
61898
61987
  // autoFocus="last-resort" below has somewhere to land when it contains
@@ -61948,8 +62037,8 @@ const useDialogProps = props => {
61948
62037
  // narrower one) — and the positioned ancestor for a local one, where
61949
62038
  // reading 3% of the screen gives an absurd gap inside a small box.
61950
62039
  isTopLayer ? "3appw" : "3cqw");
61951
- // "expand || expandX", the shorthand semantics Popup used to apply before
61952
- // handing them over — the docked default only applies when neither was said
62040
+ // `expand` is the shorthand for both axes; the docked default only applies
62041
+ // when neither was said.
61953
62042
  const expandXUnset = expand === undefined && expandXProp === undefined;
61954
62043
  const expandX = expandXUnset ? isDocked && DOCKED.expandX : Boolean(expand) || Boolean(expandXProp);
61955
62044
  const scrollCapture = scrollCaptureProp ?? (isDocked ? DOCKED.scrollCapture : false);
@@ -62638,8 +62727,8 @@ const useDialogProps = props => {
62638
62727
  // ("[navi-control='dialog'], [navi-control='popover']" — see openEffect
62639
62728
  // above), and --navi-open/--navi-close resolve their target this way.
62640
62729
  "navi-control": "dialog",
62641
- // The protocol every command target answers. It came with the control
62642
- // group before; a dialog is layout and still has to answer --navi-open,
62730
+ // The protocol every command target answers: a dialog is layout, not a
62731
+ // control, and still has to answer --navi-open,
62643
62732
  // --navi-close and --navi-toggle, which are dispatched here and do not
62644
62733
  // bubble.
62645
62734
  "onnavi_command": e => {
@@ -62661,14 +62750,12 @@ const useDialogProps = props => {
62661
62750
  "popover": isTopLayerPopover ? "manual" : undefined,
62662
62751
  "baseClassName": "navi_dialog",
62663
62752
  "pseudoClasses": DIALOG_PSEUDO_CLASSES,
62664
- // Distinguishes the two renderers for the CSS above (position: fixed
62665
- // vs. absolute) — positioning itself is entirely JS-driven now (see
62666
- // openEffect's own positionDialog above), no data-position-area
62667
- // attribute needed at all.
62668
62753
  // A popup scrolls, and asking Box for that overflow is also what lets what
62669
62754
  // it contains claim header/footer/body (see box.jsx) — a popup is always a
62670
62755
  // scrolling area, so it says so once, here.
62671
62756
  "overflow": "auto",
62757
+ // Tells the two renderers apart for the CSS above (position: fixed vs.
62758
+ // absolute); the placement itself is JS-driven (positionDialog).
62672
62759
  "data-layer": layer,
62673
62760
  // The sheet shape is live in CSS, not just a set of resolved defaults:
62674
62761
  // it is what withdraws the caller's --dialog-max-width (see the stylesheet
@@ -63145,8 +63232,8 @@ const css$D = /* css */`
63145
63232
  * whole viewport. A real `anchor` works with either.
63146
63233
  * @param {string} [props.positionArea="bottom"] - Where to place the popover
63147
63234
  * relative to its `anchor` (or its container, if there is none). Same
63148
- * grammar as `Dialog`'s own `positionArea` (see `popup_shared.js`'s
63149
- * `parsePositionArea`): a single compass token — `top`/`top-start`/
63235
+ * grammar as `Dialog`'s own `positionArea` (see `parsePositionArea` in
63236
+ * @jsenv/dom): a single compass token — `top`/`top-start`/
63150
63237
  * `top-end`/`top-left`/`top-right`, `right`/`right-start`/`right-end`,
63151
63238
  * `bottom`/`bottom-start`/`bottom-end`/`bottom-left`/`bottom-right`,
63152
63239
  * `left`/`left-start`/`left-end`, or `center`. A bare token means no
@@ -63175,8 +63262,9 @@ const css$D = /* css */`
63175
63262
  * See docs/popup_backdrop.md.
63176
63263
  * @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="ignore"]
63177
63264
  * - `"ignore"` (default): no backdrop at all, outside presses pass straight
63178
- * through. `"close"` closes the popover on an outside press. `"capture"`
63179
- * absorbs the press (dims the backdrop) without closing. Note this
63265
+ * through. `"close"` closes the popover on an outside press, `"cancel"`
63266
+ * closes it as a cancel (what it holds reverts, as on Escape). `"capture"`
63267
+ * absorbs the press (blurred glass) without closing. Note this
63180
63268
  * default differs from `Dialog`'s own (`"close"`) — a popover is
63181
63269
  * typically a lightweight, non-modal affordance. "Outside" is the popover's
63182
63270
  * own border box; a see-through popover whose box is bigger than what it
@@ -63210,9 +63298,11 @@ const css$D = /* css */`
63210
63298
  * popover so the page/container behind it can't scroll while it's open.
63211
63299
  * @param {boolean} [props.focusCapture] - Traps Tab navigation inside the
63212
63300
  * popover (see `focus_trap.js`).
63213
- * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`} [props.animation]
63214
- * - `true`/`"auto"` resolves to a concrete `"slide-from-*"` direction
63215
- * based on `positionArea`. Any other explicit value is used as-is.
63301
+ * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`|"expanding"|`expand-${string}`} [props.animation]
63302
+ * - `true`/`"auto"` resolves to `"scaling"` for a real anchor or a centered
63303
+ * `positionArea`, and to a concrete `"slide-from-*"` direction otherwise.
63304
+ * `"expanding"`/`"expand-*"` grow out of a real anchor (see popup_css.js).
63305
+ * Any other explicit value is used as-is.
63216
63306
  * @param {string} [props.animationDuration] - Maps to
63217
63307
  * `--popup-animation-duration`.
63218
63308
  * @param {Element|{current: Element}|string} [props.anchor] - The element the
@@ -63324,7 +63414,7 @@ const css$D = /* css */`
63324
63414
  * @param {object} [props.openController] - Advanced: an externally-owned
63325
63415
  * open controller (see `open_controller.js`) for a caller that wants to
63326
63416
  * drive open/close itself instead of `open`/`defaultOpen`/`onClose` (used
63327
- * by `picker_custom.jsx`/`side_panel.jsx`).
63417
+ * by `picker_custom.jsx`).
63328
63418
  * @param {"always"|"idle"|"from-first-open"|"while-opened"} [props.mount] - When
63329
63419
  * `children` are built and thrown away (see popup_content_mount.js).
63330
63420
  * `"from-first-open"` (the default) builds them on the first open and keeps
@@ -63634,12 +63724,10 @@ const usePopoverProps = props => {
63634
63724
  unfreezeSize(popoverEl);
63635
63725
  }
63636
63726
  }, [sizing]);
63637
- // The custom renderer's own starting-hidden state is a stylesheet default
63638
- // now (&:not([popover]) { display: none } on .navi_popover/
63639
- // .navi_popover_backdrop above) rather than set here imperatively — a
63640
- // plain div has no native default the way [popover]/<dialog> do, so
63641
- // leaving this to a layout effect meant an actual (if narrow) window
63642
- // where the browser could paint it visible before this ever ran.
63727
+ // The custom renderer starts hidden from the stylesheet ([navi-hidden] on
63728
+ // .navi_popover/.navi_popover_backdrop above), never from a layout effect:
63729
+ // a plain div has no native hidden default the way [popover]/<dialog> do,
63730
+ // and an effect leaves a window where the browser paints it visible first.
63643
63731
  // aria-expanded starts "false" the same way, but via a static literal
63644
63732
  // JSX prop on contentProps/backdropProps below instead of a layout
63645
63733
  // effect — see that prop's own comment for why a *constant* value there
@@ -63658,8 +63746,8 @@ const usePopoverProps = props => {
63658
63746
  openController.getElement = () => ref.current;
63659
63747
  openController.openEffect = e => {
63660
63748
  const popoverEl = ref.current;
63661
- // backdropEl is null when pressOutside is "ignore" —
63662
- // the backdrop isn't rendered at all in that case.
63749
+ // backdropEl is null when there is no wall (pressOutside "ignore", or
63750
+ // backdrop={false}): the backdrop isn't rendered at all then.
63663
63751
  const backdropEl = backdropRef.current;
63664
63752
  if (!popoverEl) {
63665
63753
  return undefined;
@@ -63714,10 +63802,8 @@ const usePopoverProps = props => {
63714
63802
  // needed here.
63715
63803
  const positionedAncestor = getPositionedParent(popoverEl);
63716
63804
  // Drives the via-attribute renderer's own position: fixed/absolute
63717
- // switch (see this file's top comment) — set here, well before any
63718
- // positioning/measurement runs, so there's no ordering subtlety to get
63719
- // wrong (unlike the CSS this replaced, which keyed off the resolved
63720
- // animation instead, known too late relative to the first measurement).
63805
+ // switch (see this file's top comment) — set here, before any
63806
+ // positioning/measurement runs, so the first measurement already sees it.
63721
63807
  // True only for a real anchor that itself scrolls with the document —
63722
63808
  // that's the one case `absolute` (scrolling in lockstep with it) is
63723
63809
  // correct; not just "has an anchor at all", since an anchor that's
@@ -63767,10 +63853,9 @@ const usePopoverProps = props => {
63767
63853
  disarmBackdropHideRef.current?.();
63768
63854
  disarmBackdropHideRef.current = null;
63769
63855
  // transitionProperty stays "none" here for both — reset later, in the
63770
- // final commit step alongside popoverEl's own (not resumed early the
63771
- // way it briefly was), so a `silent` open (see above) can keep both
63772
- // elements' transitions suppressed right up until after their
63773
- // aria-expanded flip, the same way it does for popoverEl.
63856
+ // final commit step alongside popoverEl's own, so a `silent` open (see
63857
+ // above) can keep both elements' transitions suppressed right up until
63858
+ // after their aria-expanded flip, the same way it does for popoverEl.
63774
63859
  backdropEl.style.transitionProperty = "none";
63775
63860
  if (isTopLayer) {
63776
63861
  // Hidden first if a previous close's deferred hidePopover() (see
@@ -64195,16 +64280,10 @@ const usePopoverProps = props => {
64195
64280
  // current unconditionally on every call, and treats their mere
64196
64281
  // *presence* as "already positioned this session — stay on this
64197
64282
  // side unless it no longer fits" (its own anti-oscillation guard for
64198
- // repositioning mid-open, e.g. on scroll/resize). Popover's own CSS
64199
- // doesn't read these anymore (slide direction is resolved in JS now,
64200
- // see resolveDirectionValue), which is why the clearing that used to
64201
- // live here was removed — but that removal missed this second,
64202
- // still-live purpose: without clearing them on close, a later reopen
64203
- // sees a stale, "already positioned" value left over from a
64204
- // *previous* open (e.g. the container-aligned collapse from an
64205
- // anchorless silent open) and wrongly stays sticky to it instead of
64206
- // resolving fresh from the current positionArea/anchor — a real,
64207
- // reproduced bug, not just a leftover no-op.
64283
+ // repositioning mid-open, e.g. on scroll/resize). Cleared on close so a
64284
+ // later reopen resolves fresh from the current positionArea/anchor
64285
+ // instead of staying sticky to a side left over from a previous open
64286
+ // (e.g. the container-aligned collapse from an anchorless silent open).
64208
64287
  popoverEl.removeAttribute("data-position-y-current");
64209
64288
  popoverEl.removeAttribute("data-position-x-current");
64210
64289
  cleanup();
@@ -64334,8 +64413,8 @@ const usePopoverProps = props => {
64334
64413
  // ("[navi-control='dialog'], [navi-control='popover']"), and
64335
64414
  // --navi-open/--navi-close resolve their target this way.
64336
64415
  "navi-control": "popover",
64337
- // The protocol every command target answers. It came with the control
64338
- // group before; a popover is layout and still has to answer --navi-open,
64416
+ // The protocol every command target answers: a popover is layout, not a
64417
+ // control, and still has to answer --navi-open,
64339
64418
  // --navi-close and --navi-toggle, which are dispatched here and do not
64340
64419
  // bubble.
64341
64420
  "onnavi_command": e => {
@@ -64375,7 +64454,7 @@ const usePopoverProps = props => {
64375
64454
  // already arrive here as plain props via ...rest (wired by
64376
64455
  // UncontrolledPopover above, forwarded through ControlledPopover's own
64377
64456
  // {...props} spread) — nothing extra to add here. A controlled caller
64378
- // (picker_custom.jsx/side_panel.jsx) wires its own equivalent handling
64457
+ // (picker_custom.jsx) wires its own equivalent handling
64379
64458
  // directly against its own openController instead.
64380
64459
  });
64381
64460
  return [hasBackdrop ? backdropProps : null, contentProps];
@@ -64407,9 +64486,6 @@ const POPUP_STYLE_CSS_VARS = {
64407
64486
  maxHeight: "--popover-max-height"
64408
64487
  };
64409
64488
 
64410
- // parsePositionArea/POSITION_AREA_X/Y_VALUES moved to popup_shared.js — same
64411
- // grammar Dialog's own layer="local"/"top" now shares.
64412
-
64413
64489
  /**
64414
64490
  * Shared by both renderers: parses `positionArea` and resolves
64415
64491
  * `animation="auto"`/`true`. `animationAnchor` is the real anchor element
@@ -64437,15 +64513,6 @@ const resolvePositionAreaAndAnimationKind = ({
64437
64513
  resolvedAnimationKind
64438
64514
  };
64439
64515
  };
64440
-
64441
- // resolveDirectionValue/resolveAutoAnimationKind moved to popup_shared.js —
64442
- // same logic Dialog's own auto-animation resolution now shares, since it
64443
- // never has a real anchor either (always the "anchor === undefined" path).
64444
-
64445
- // suppressPointerEventsDuringTransition/armPointerDownOutsideClose moved to
64446
- // popup_shared.js — same helpers Dialog's own custom renderer needs, no
64447
- // Popover-specific knowledge in either.
64448
-
64449
64516
  Popover.Close = PopupClose;
64450
64517
 
64451
64518
  installImportMetaCssBuild(import.meta);
@@ -64484,11 +64551,12 @@ const css$C = /* css */`@layer navi {
64484
64551
  * @param {"top"|"local"} [props.layer] - Forwarded as-is to whichever of
64485
64552
  * `Dialog`/`Popover` renders — see either component's own doc.
64486
64553
  * @param {Element|{current: Element}} [props.anchor] - Forwarded as-is —
64487
- * positioning for `Popover`, and for `Dialog` sizing only, and only when
64488
- * `sizeFromAnchor` is also passed (see each component's own doc).
64554
+ * positioning for `Popover`; for `Dialog`, the box it sizes from under
64555
+ * `sizeFromAnchor` and lifts out of under `animation="lifting"`. In both,
64556
+ * intent on the anchor warms the content (see `mount`).
64489
64557
  * @param {"override"|"ignore"} [props.anchorCustomEventDetail] - Forwarded
64490
64558
  * as-is to both — what it governs differs (positioning for `Popover`,
64491
- * `sizeFromAnchor` sizing for `Dialog`), but "ignore whatever anchor the
64559
+ * sizing and lifting for `Dialog`), but "ignore whatever anchor the
64492
64560
  * triggering event carried" has to mean the same thing in either mode.
64493
64561
  * @param {string} [props.marginWithAnchor] - **Popover-only**, destructured
64494
64562
  * out so it can't leak onto the real `<dialog>` element as a stray DOM
@@ -64506,6 +64574,8 @@ const css$C = /* css */`@layer navi {
64506
64574
  * `maxWidth` below.
64507
64575
  * @param {boolean} [props.sizeFromAnchor] - **Dialog-only**, same guard.
64508
64576
  * @param {"box"|"scene"} [props.lift] - **Dialog-only**, same guard.
64577
+ * @param {Element|{current: Element}|string} [props.liftAnchor] -
64578
+ * **Dialog-only**, same guard.
64509
64579
  * @param {string} [props.positionArea] - Forwarded as-is — `Dialog` and
64510
64580
  * `Popover` have different own defaults (`"center"` vs. `"bottom"`),
64511
64581
  * deliberately not homogenized here (each reads best for its own typical
@@ -64518,8 +64588,9 @@ const css$C = /* css */`@layer navi {
64518
64588
  * the screen-size check happens to pick, which defeats the point of
64519
64589
  * having one shared API in the first place. Note this only says what a
64520
64590
  * press outside *does*; whether it reaches the page at all is `backdrop`
64521
- * below, and `"ignore"`/`"capture"` describe a wall either way — the popup
64522
- * absorbs the press without closing, dimmed or not.
64591
+ * below. `"capture"` is a wall that absorbs the press without closing, in
64592
+ * either mode. `"ignore"` is not the same in both: a `Dialog` keeps its
64593
+ * wall, a `Popover` lays none and the press reaches the page.
64523
64594
  * @param {boolean} [props.backdrop] - Whether anything is laid between the
64524
64595
  * popup and the page at all: `false` lets a press outside both dismiss the
64525
64596
  * popup and reach whatever it landed on, in one gesture. Forwarded as-is and
@@ -64538,8 +64609,13 @@ const css$C = /* css */`@layer navi {
64538
64609
  * identically): the wash the backdrop paints over what is behind.
64539
64610
  * @param {string} [props.backdropFilter] - Forwarded as-is: what that wash
64540
64611
  * does to the picture underneath, `"blur(4px)"` and the like.
64541
- * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|"expanding"|`slide-from-${string}`|`expand-${string}`} [props.animation]
64542
- * - Forwarded as-is.
64612
+ * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`|"expanding"|`expand-${string}`|"lifting"|{open: boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`, close: "lifting"}} [props.animation]
64613
+ * - Forwarded to whichever renders — see either component's own doc.
64614
+ * `"expanding"`/`"expand-*"` are `Popover`'s own; `"lifting"` and
64615
+ * `{ open, close: "lifting" }` are `Dialog`'s. A popover has no lift, so in
64616
+ * popover mode `"lifting"` plays no movement (what a dialog does with no
64617
+ * anchor to lift out of) and the object form opens with its `open`, which
64618
+ * the close plays backwards like every other kind.
64543
64619
  * @param {string} [props.animationDuration] - Forwarded as-is.
64544
64620
  * @param {string} [props.maxWidth] - Forwarded as-is to both; also read
64545
64621
  * here directly to help decide the automatic `mode` (a fixed length under
@@ -64559,11 +64635,15 @@ const css$C = /* css */`@layer navi {
64559
64635
  * `--x-popover-max-width`) instead of its content width — same meaning
64560
64636
  * whichever mode the screen-size resolution picks.
64561
64637
  * @param {boolean} [props.expandY] - Same, vertically (`data-expand-y`).
64562
- * @param {boolean} [props.scrollCapture] - Forwarded as-is.
64638
+ * @param {boolean|"dialog"|"popover"} [props.scrollCapture] - Forwarded to
64639
+ * both; a mode name traps scroll only when that mode is the one rendered.
64563
64640
  * @param {boolean} [props.open] - Forwarded as-is (controlled).
64564
- * @param {import("@preact/signals").Signal<boolean>} [props.signal] -
64641
+ * @param {import("@preact/signals").Signal<any>} [props.signal] -
64565
64642
  * Forwarded as-is: one binding to both drive the popup's open state and
64566
64643
  * know where it is (see `Dialog`/`Popover`'s own `signal`).
64644
+ * @param {any} [props.value] - Forwarded as-is: what `signal` holds while
64645
+ * THIS popup is open, for several sharing one signal (see
64646
+ * `Dialog`/`Popover`'s own `value`).
64567
64647
  * @param {boolean} [props.defaultOpen] - Forwarded as-is (uncontrolled,
64568
64648
  * mount-only).
64569
64649
  * @param {(event: Event) => void} [props.onClose] - Forwarded as-is.
@@ -64578,6 +64658,8 @@ const css$C = /* css */`@layer navi {
64578
64658
  * them afterwards. `"always"` builds them right away, for content something
64579
64659
  * depends on while the popup is still closed: a value read off it, fields a
64580
64660
  * surrounding form collects on submit, a size measured from outside.
64661
+ * `"idle"` builds them in a browser idle moment after load — `"always"`
64662
+ * minus the cost on the critical render.
64581
64663
  * `"while-opened"` throws them away once the popup has finished closing, for
64582
64664
  * content whose fresh state is its initial state: an uncontrolled field
64583
64665
  * seeded from a `defaultValue` that changed while the popup was closed.
@@ -64599,6 +64681,8 @@ const Popup = props => {
64599
64681
  // mode the automatic screen-size resolution happens to pick.
64600
64682
  pressOutside = "close",
64601
64683
  backdrop,
64684
+ // A popover is handed only the values it understands (toPopoverAnimation).
64685
+ animation,
64602
64686
  // Popover-only (see this component's own doc) — destructured out so
64603
64687
  // they're never part of ...rest, and therefore never forwarded to
64604
64688
  // Dialog below, where they'd otherwise leak onto the real <dialog>
@@ -64613,6 +64697,7 @@ const Popup = props => {
64613
64697
  dockedOnSmallTouchScreen,
64614
64698
  sizeFromAnchor,
64615
64699
  lift,
64700
+ liftAnchor,
64616
64701
  ...rest
64617
64702
  } = props;
64618
64703
  const [mode] = useResolvedPopupMode(modeProp, maxWidth, {
@@ -64632,6 +64717,8 @@ const Popup = props => {
64632
64717
  dockedOnSmallTouchScreen: dockedOnSmallTouchScreen,
64633
64718
  sizeFromAnchor: sizeFromAnchor,
64634
64719
  lift: lift,
64720
+ liftAnchor: liftAnchor,
64721
+ animation: animation,
64635
64722
  maxWidth: maxWidth,
64636
64723
  pressOutside: pressOutside,
64637
64724
  backdrop: backdrop,
@@ -64645,6 +64732,7 @@ const Popup = props => {
64645
64732
  }
64646
64733
  return jsx(Popover, {
64647
64734
  ...rest,
64735
+ animation: toPopoverAnimation(animation),
64648
64736
  maxWidth: maxWidth,
64649
64737
  pressOutside: pressOutside,
64650
64738
  backdrop: backdrop,
@@ -64662,6 +64750,18 @@ const Popup = props => {
64662
64750
  };
64663
64751
  Popup.Close = PopupClose;
64664
64752
 
64753
+ // What Dialog's own animation values mean to a popover, which has no lift
64754
+ // (see the `animation` doc above).
64755
+ const toPopoverAnimation = animation => {
64756
+ if (animation === "lifting") {
64757
+ return undefined;
64758
+ }
64759
+ if (animation !== null && typeof animation === "object") {
64760
+ return animation.open;
64761
+ }
64762
+ return animation;
64763
+ };
64764
+
64665
64765
  installImportMetaCssBuild(import.meta);
64666
64766
  const css$B = /* css */`.navi_picker_callout_dock > [data-picker-content] {
64667
64767
  display: none;
@@ -65163,7 +65263,7 @@ const PickerCustom = props => {
65163
65263
  // Not on pickerProps (the trigger): commands.js's own
65164
65264
  // resolveClosestExpandable() does `el.closest("[aria-expanded]")` to
65165
65265
  // find where to dispatch navi_request_open/navi_request_close — and
65166
- // the popup itself now carries its own aria-expanded (see
65266
+ // the popup itself carries its own aria-expanded (see
65167
65267
  // popover.jsx/dialog.jsx), which is *closer* than the picker's own
65168
65268
  // aria-expanded for anything dispatched from inside the popup's own
65169
65269
  // content (e.g. a `command="--navi-close"` button rendered as
@@ -65442,12 +65542,8 @@ const PickerContentInsidePopup = props => {
65442
65542
  mode,
65443
65543
  pointerLock,
65444
65544
  scrollCapture,
65445
- // No default here (matches Popover's own default of inactive) — the
65446
- // old, differently-named `focusTrap = true` prop never actually reached
65447
- // Popover's real `focusCapture` prop (see this file's history), so
65448
- // focus-trapping has never really been active for popover-mode pickers;
65449
- // defaulting the now-correctly-named prop to `true` would be a real,
65450
- // unintended behavior change riding along with the rename.
65545
+ // No default here: Popover's own `focusCapture` is inactive by default,
65546
+ // and a popover-mode picker traps the focus only when asked to.
65451
65547
  focusCapture,
65452
65548
  // Popup documents its own `layer` as forwarded as-is to Dialog/Popover,
65453
65549
  // but popupProps is built explicitly here, so it only travels if named.
@@ -65976,9 +66072,14 @@ const PickerNaviMinute = props => {
65976
66072
  step,
65977
66073
  value
65978
66074
  } = props;
66075
+
66076
+ // resolveInputProps has already run (PickerFirstResolver), so the type
66077
+ // handed on is one the picker renders as is — the text field a navi numeric
66078
+ // type resolves to, told what its value is.
65979
66079
  return jsx(Next, {
65980
66080
  ...props,
65981
- type: "minute",
66081
+ type: "text",
66082
+ "navi-input-type": "minute",
65982
66083
  children: jsxs(Box, {
65983
66084
  flex: "y",
65984
66085
  spacing: "s",
@@ -66084,6 +66185,8 @@ document.addEventListener(
66084
66185
  { capture: true, passive: true },
66085
66186
  );
66086
66187
 
66188
+ const FRAME_TIMEOUT_MS = 100;
66189
+
66087
66190
  /**
66088
66191
  * Calls back once the browser has painted what is committed now.
66089
66192
  *
@@ -66095,10 +66198,9 @@ document.addEventListener(
66095
66198
  * has to survive. A frame not coming at all (a background tab) still answers,
66096
66199
  * late, through the timeout.
66097
66200
  *
66201
+ * @param {() => void} callback
66098
66202
  * @returns {() => void} cancel
66099
66203
  */
66100
- const FRAME_TIMEOUT_MS = 100;
66101
-
66102
66204
  const afterPaint = (callback) => {
66103
66205
  let called = false;
66104
66206
  let timeoutId;
@@ -67645,7 +67747,7 @@ const SELECTABLE_INPUT_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-
67645
67747
  const SelectableInputProxy = props => {
67646
67748
  const selectableRealInputProps = useContext(SelectableRealInputContext);
67647
67749
  if (!selectableRealInputProps) {
67648
- throw new Error("Selectable.Input must be used within a Selectable component");
67750
+ throw new Error("SelectableInput must be used within a row of a selectable List");
67649
67751
  }
67650
67752
 
67651
67753
  // Reset FieldToInterfaceContext to ensure we don't read id or report our
@@ -67683,7 +67785,7 @@ const SelectableInput = SelectableInputProxy;
67683
67785
  * background-color: var(--search-match-background-color);
67684
67786
  * }
67685
67787
  *
67686
- * The `highlight` prop can be:
67788
+ * The `highlight` argument of `useSearchHighlight` (a row's `matchInfo.matchRanges`) can be:
67687
67789
  * - an array of [start, end] pairs — applied to all text nodes under the root element
67688
67790
  * - an object { [domSelector]: [[start, end], …] } — applied to each sub-element
67689
67791
  * matched by the selector (the format produced by createSearch)
@@ -71129,8 +71231,8 @@ const LIST_ITEM_STYLE_CSS_VARS = {
71129
71231
  *
71130
71232
  * All three are about the row as a thing the LIST holds, never about the
71131
71233
  * selection: a selectable row taken while its list sends says so on its own
71132
- * ("la sélection est en cours d'enregistrement"), and needs no `loading` for
71133
- * that.
71234
+ * ("le choix est en cours d'enregistrement", "la sélection…" in a `multiple`
71235
+ * list), and needs no `loading` for that.
71134
71236
  * @param {boolean} [props.readOnly]
71135
71237
  * The row cannot be acted on: dimmed and click-through-proof, buttons inside
71136
71238
  * it included.
@@ -72216,9 +72318,12 @@ const useItemStore = ({
72216
72318
  * (accessible via aria-labelledby) and a <ul role="group"> for the items.
72217
72319
  *
72218
72320
  * Props:
72219
- * label — group label content
72220
- * labelProps — props forwarded to the label <span>
72221
- * ...rest — forwarded to the outer <li role="presentation">
72321
+ * label — group label content
72322
+ * labelProps — props forwarded to the label <span>
72323
+ * hiddenWhileEmpty — the group leaves the flow (`display: none`) while it
72324
+ * holds no real row — a search that emptied it, rows not
72325
+ * arrived yet
72326
+ * ...rest — forwarded to the outer <li role="presentation">
72222
72327
  */
72223
72328
  const ListItemGroup = ({
72224
72329
  label,
@@ -72330,7 +72435,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
72330
72435
  * uiAction?: (value: any) => void,
72331
72436
  * popover?: boolean,
72332
72437
  * role?: string,
72333
- * renderBudget?: number | string,
72438
+ * renderBudget?: number | string | {initial?: number, after: number},
72334
72439
  * renderBudgetSkipCheck?: boolean,
72335
72440
  * virtualItemSize?: number,
72336
72441
  * onListVisibleItemsChange?: (visibleItems: any[]) => void,
@@ -72359,6 +72464,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
72359
72464
  * alignX?: string,
72360
72465
  * alignY?: string,
72361
72466
  * flexWrap?: boolean,
72467
+ * overflow?: string,
72362
72468
  * expandX?: boolean,
72363
72469
  * expandY?: boolean,
72364
72470
  * expand?: boolean,
@@ -72545,10 +72651,10 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
72545
72651
  * @param {number} [props.parallelGuard=4]
72546
72652
  * How many runs the rows may have in flight at once, for a list whose rows
72547
72653
  * carry their own `action` (a button per row). While that many are out, every
72548
- * row that is not running goes read-only and says how many it is waiting on;
72549
- * the next press is possible again as soon as one comes back. `Infinity`
72550
- * lifts it. Counts runs, not values — `maxLengthGuard` above is the one that
72551
- * says how many things the selection may hold.
72654
+ * control that would start another run goes read-only and says how many it is
72655
+ * waiting on; the next press is possible again as soon as one comes back.
72656
+ * `Infinity` lifts it. Counts runs, not values — `maxLengthGuard` above is
72657
+ * the one that says how many things the selection may hold.
72552
72658
  * @param {boolean} [props.standalone]
72553
72659
  * This list answers for itself: it does not register with the control group
72554
72660
  * or picker around it, so its selection stays out of that value and nothing
@@ -75275,7 +75381,7 @@ const TRAVELS_NONE = [];
75275
75381
  * fallbackValue?: any,
75276
75382
  * valueAtStep: (value: any, count: number) => any,
75277
75383
  * compareValues?: (a: any, b: any) => number,
75278
- * renderValue?: (value: any) => import("ignore:preact").ComponentChildren,
75384
+ * renderValue?: (value: any, context: { maxLines?: number }) => import("ignore:preact").ComponentChildren,
75279
75385
  * controlProps?: object,
75280
75386
  * duration?: number,
75281
75387
  * vertical?: boolean,
@@ -76750,7 +76856,8 @@ const css$q = /* css */`.navi_input_duration {
76750
76856
  * @param {boolean} [props.required]
76751
76857
  * @param {boolean} [props.loading]
76752
76858
  * @param {Function} [props.uiAction] - Called on every change with the ISO 8601 value
76753
- * @param {Function} [props.action] - Called on form submission
76859
+ * @param {Function} [props.action] - Runs with the ISO 8601 value when it changes,
76860
+ * like any control's `action` (busy state, error callout)
76754
76861
  * @param {preact.ComponentChild} [props.unitHour] - Custom label for the hour sub-field
76755
76862
  * @param {"auto"|"left"|"center"|"right"} [props.textAlign="auto"] - Text alignment of sub-inputs.
76756
76863
  * "auto" aligns each field toward its neighbouring separator (first→right, last→left, middle/solo→center).
@@ -77758,11 +77865,12 @@ const splitPopupProps = props => {
77758
77865
  * multi-word, one word at start (all match) 0.75
77759
77866
  * phrase / word at word boundary 0.625
77760
77867
  * phrase / words mid-word 0.5
77868
+ * acronym (2+ chars, each the first letter of a word, in order) 0.4, 0.45 at start
77761
77869
  * + case-exact bonus +0.125
77762
77870
  * multi-word partial: score × (matched/total)
77763
77871
  *
77764
77872
  * matchRanges: [start, end] pairs (exclusive end) for CSS Highlight API.
77765
- * Intended to be passed to useSearch as the matchFn parameter.
77873
+ * Intended to be passed to useSearchText as the matchFn parameter.
77766
77874
  */
77767
77875
  const applySearch = (searchText, value) => {
77768
77876
  if (!searchText) {
@@ -77972,7 +78080,7 @@ const mergeRanges = (ranges) => {
77972
78080
  };
77973
78081
 
77974
78082
  /**
77975
- * createSearch — builds a matchFn compatible with useSearch that searches
78083
+ * createSearch — builds a matchFn compatible with useSearchText that searches
77976
78084
  * across multiple named fields of an item, each with its own DOM selector
77977
78085
  * and optional priority weight.
77978
78086
  *
@@ -77990,7 +78098,7 @@ const mergeRanges = (ranges) => {
77990
78098
  * },
77991
78099
  * });
77992
78100
  *
77993
- * const [orderedItems, getItemMatchInfo] = useSearch(search, items, searchPerson);
78101
+ * const [orderedItems, getItemMatchInfo] = useSearchText(search, items, searchPerson);
77994
78102
  * // getItemMatchInfo(item).matchRanges is { ".name": [[start,end],…], ".address": [[start,end],…] }
77995
78103
  * // Pass the whole thing: <ListItem matchInfo={getItemMatchInfo(item)} />
77996
78104
  * // — ListItem handles the per-selector object format for matchRanges.
@@ -78057,7 +78165,7 @@ const useSearchText = (searchText, items, matchFn = applySearch) => {
78057
78165
  );
78058
78166
  }
78059
78167
  if (items === undefined) {
78060
- throw new TypeError("useSearch: items is undefined");
78168
+ throw new TypeError("useSearchText: items is undefined");
78061
78169
  }
78062
78170
  const { orderedItems, matchInfoMap } = useMemo(() => {
78063
78171
  const { scoreEntries, nonMatched, matchInfoMap } = buildMatchInfo(
@@ -78182,17 +78290,19 @@ const buildMatchInfo = (searchText, items, matchFn) => {
78182
78290
  * - orderedKeys: number[] of visible item keys sorted by explicit order
78183
78291
  * - keyToOrderedIndex: Map key → orderedKeys index, gives O(1) indexOf equivalent
78184
78292
  * - keyToExplicitOrder: Map key → explicitly passed index, used to maintain sort order
78185
- * - allItemsSignal: signal(array), all items including hidden, ordered by explicit index
78186
- * - visibleItemsSignal: signal(array), non-hidden items only
78293
+ * - itemsSignal: signal(array), all items including hidden, ordered by explicit index
78294
+ * - visibleItemsSignal: signal(array), items neither hidden nor filtered
78187
78295
  * - countSignal: signal(number), count of all items including hidden
78188
78296
  * - visibleCountSignal: signal(number), updated in microtask batch, only when count changes
78189
- * - propSignals: Map propName → signal(array), updated in microtask batch with element equality
78297
+ * - noMatchCountSignal: signal(number), items registered with `match: false`
78190
78298
  * - onChangeRef: holds the latest onChange callback, called once per microtask batch
78191
78299
  *
78192
- * useTrackItem(id, data, index): registers the item with an explicitly provided index
78193
- * that determines its position among siblings. The caller (e.g. items.map) knows the
78194
- * correct order and passes it directly — no render-sequence deduction needed.
78195
- * Returns the visible rank (position among non-hidden items), or -1 when hidden.
78300
+ * useTrackItem(data): registers the item under `data.id`, at the position
78301
+ * `data.index` gives it among its siblings. The caller (e.g. items.map) knows
78302
+ * the correct order and passes it directly — no render-sequence deduction
78303
+ * needed. `data.role === "presentation"` keeps it out entirely.
78304
+ * Returns the visible rank (position among visible items), or -1 when hidden
78305
+ * or filtered.
78196
78306
  * Signals and onChange are deferred to a microtask so multiple items updating
78197
78307
  * in one commit cause only one notification.
78198
78308
  *
@@ -78960,7 +79070,7 @@ const WheelGroupContext = createContext(null);
78960
79070
  * [key: string]: any,
78961
79071
  * }>}
78962
79072
  * @param {number} [props.visibleCount=3] - Odd number of rows visible in the viewport (the center one is the selection).
78963
- * @param {number|string} [props.itemHeight] - Main-axis size of a row when vertical (number = px). Defaults to the CSS var (2.4em).
79073
+ * @param {number|string} [props.itemHeight] - Main-axis size of a row when vertical (number = px). Defaults to the CSS var (1.8em).
78964
79074
  * @param {number|string} [props.itemWidth] - Main-axis size of a cell when horizontal (number = px). Defaults to the CSS var (3.5ch).
78965
79075
  * @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.
78966
79076
  * @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).
@@ -80448,7 +80558,7 @@ Wheel.Item = WheelItem;
80448
80558
  * @param {boolean} [props.horizontal] - Stack the (horizontal) wheels vertically instead of in a row.
80449
80559
  * @param {boolean} [props.glass] - Frost every wheel's neighbouring rows (see Wheel's glass prop) with one prop for the whole group.
80450
80560
  * @param {boolean} [props.frameBorder] - Line every wheel's center-window edges with a faint frame (off by default; independent of glass).
80451
- * @param {boolean} [props.zoom] - Enlarge the centered value of every wheel (see Wheel's zoom prop) with one prop for the whole group.
80561
+ * @param {boolean|number} [props.zoom] - Enlarge the centered value of every wheel (see Wheel's zoom prop) with one prop for the whole group.
80452
80562
  */
80453
80563
  const WheelGroup = props => {
80454
80564
  import.meta.css = [css$m, "@jsenv/navi/src/control/wheel/wheel.jsx"];
@@ -81233,8 +81343,8 @@ const stringifyTableSelectionValue = (type, value) => {
81233
81343
 
81234
81344
  /**
81235
81345
  * Check if a specific cell is selected
81236
- * @param {Array} selection - The selection set or array
81237
- * @param {{rowIndex: number, columnIndex: number}} cellPosition - Cell coordinates
81346
+ * @param {string[]} selection - The table's `selection` array
81347
+ * @param {{columnId: string, rowId: string}} cell - The cell's column and row ids
81238
81348
  * @returns {boolean} True if the cell is selected
81239
81349
  */
81240
81350
  const isCellSelected = (selection, { columnId, rowId }) => {
@@ -81247,8 +81357,8 @@ const isCellSelected = (selection, { columnId, rowId }) => {
81247
81357
 
81248
81358
  /**
81249
81359
  * Check if a specific row is selected
81250
- * @param {Array} selection - The selection set or array
81251
- * @param {number} rowIndex - Row index
81360
+ * @param {string[]} selection - The table's `selection` array
81361
+ * @param {string} rowId - The row's id (the `id` of its `<Tr>`)
81252
81362
  * @returns {boolean} True if the row is selected
81253
81363
  */
81254
81364
  const isRowSelected = (selection, rowId) => {
@@ -81258,8 +81368,8 @@ const isRowSelected = (selection, rowId) => {
81258
81368
 
81259
81369
  /**
81260
81370
  * Check if a specific column is selected
81261
- * @param {Array} selection - The selection set or array
81262
- * @param {number} columnIndex - Column index
81371
+ * @param {string[]} selection - The table's `selection` array
81372
+ * @param {string} columnId - The column's id (the `id` of its `<Col>`)
81263
81373
  * @returns {boolean} True if the column is selected
81264
81374
  */
81265
81375
  const isColumnSelected = (selection, columnId) => {
@@ -81794,7 +81904,7 @@ const initDragTableColumnViaPointer = (pointerdownEvent, {
81794
81904
  /**
81795
81905
  * Creates a MutationObserver that syncs attribute changes from original table to clone
81796
81906
  * @param {HTMLElement} table - The original table element
81797
- * @param {HTMLElement} cloneTable - The cloned table element
81907
+ * @param {HTMLElement} tableClone - The cloned table element
81798
81908
  * @returns {MutationObserver} The observer instance with disconnect method
81799
81909
  */
81800
81910
  const createTableAttributeSync = (table, tableClone) => {
@@ -83759,9 +83869,10 @@ const TableUI = /*#__PURE__*/forwardRef((props, ref) => {
83759
83869
  * KEY PRINCIPLES:
83760
83870
  * - Use inset box-shadow to ensure borders appear above table cell backgrounds
83761
83871
  * - Use ::before pseudo-elements with position: absolute for flexible positioning
83762
- * - Each cell draws its own borders independently (no border-collapse by default)
83872
+ * - Each cell draws its own borders; `borderCollapse` (on by default) keeps only
83873
+ * the right and bottom ones, plus top on the first row and left on the first column
83763
83874
  * - Selection borders override table borders using higher CSS specificity
83764
- * - Sticky borders use thicker box-shadows in accent color (yellow)
83875
+ * - Sticky borders use thicker box-shadows in an accent color
83765
83876
  *
83766
83877
  * TECHNICAL IMPLEMENTATION:
83767
83878
  * - All borders use inset box-shadow with specific directional mapping:
@@ -83769,9 +83880,8 @@ const TableUI = /*#__PURE__*/forwardRef((props, ref) => {
83769
83880
  * * Right: inset -1px 0 0 0
83770
83881
  * * Bottom: inset 0 -1px 0 0
83771
83882
  * * Left: inset 1px 0 0 0
83772
- * - Selection borders (blue) override table borders (red) in same pseudo-element
83883
+ * - Selection borders override table borders in the same pseudo-element
83773
83884
  * - Sticky borders replace regular borders with thicker colored variants
83774
- * - Border-collapse mode available as optional feature for future use
83775
83885
  *
83776
83886
  * Note how border disappear for sticky elements when using border-collapse (https://bugzilla.mozilla.org/show_bug.cgi?id=1727594)
83777
83887
  *
@@ -84905,11 +85015,18 @@ const BadgeCountOverflow = () => jsx("span", {
84905
85015
  });
84906
85016
  const MAX_CHAR_AS_CIRCLE = 3;
84907
85017
  const MAX_FOR_CIRCLE = 99;
85018
+
85019
+ /**
85020
+ * @param {boolean} [circle] Drawn round rather than as a pill. A circle holds
85021
+ * three characters at most, so `max` defaults to 99 with it ("99+"); a `max`
85022
+ * above 99, or `"none"`, lets a bigger count turn the circle into a pill as
85023
+ * it grows.
85024
+ * @param {number|"none"} [max] The count above which `max` and a "+" are
85025
+ * written in its place. 99 under `circle`, no cap otherwise.
85026
+ */
84908
85027
  const BadgeCount = ({
84909
85028
  children,
84910
85029
  maxElement = jsx(BadgeCountOverflow, {}),
84911
- // When you use max="none" (or max > 99) it might be a good idea to force ellipse
84912
- // so that visually the interface do not suddently switch from circle to ellipse depending on the count
84913
85030
  circle,
84914
85031
  max = circle ? MAX_FOR_CIRCLE : Infinity,
84915
85032
  integer,
@@ -86092,12 +86209,13 @@ const css$7 = /* css */`.navi_text_box {
86092
86209
  * - Displays optional icon(s) before and/or after the text
86093
86210
  * - Shrinks its width to fit the longest rendered text line (no trailing blank space)
86094
86211
  * - Wraps long text by default (overflow-wrap: anywhere)
86095
- * - Shows ellipsis for a single overflowing unwrappable line
86212
+ * - Shows an ellipsis instead under `singleLine`
86096
86213
  *
86097
86214
  * Props:
86098
86215
  * iconBefore — element shown to the left of the text
86099
86216
  * iconAfter — element shown to the right of the text (stays on the same line)
86100
- * maxHeight — CSS max-height string; when set, content that cannot wrap gets ellipsis
86217
+ * singleLine — keeps the text on one line, cut with an ellipsis (no width shrinking)
86218
+ * maxHeight — CSS max-height string; content past it is clipped
86101
86219
  * children — the text content
86102
86220
  */
86103
86221
  const TextBox = ({
@@ -86276,6 +86394,41 @@ const css$4 = /* css */`.svg_mask_content * {
86276
86394
  stroke-opacity: 1 !important;
86277
86395
  }
86278
86396
  `;
86397
+
86398
+ /**
86399
+ * SVGMaskOverlay
86400
+ *
86401
+ * Creates composite SVGs by combining independent SVG elements with masking.
86402
+ *
86403
+ * This component solves the challenge of combining independently created SVGs into
86404
+ * a single visual composition. Each SVG can have its own coordinate system, viewBox,
86405
+ * and styling, allowing for maximum reusability of individual icons or graphics.
86406
+ *
86407
+ * When overlaying SVGs, each subsequent overlay "cuts out" its portion from the base SVG,
86408
+ * creating a seamless integration where SVGs appear to interact with each other visually.
86409
+ *
86410
+ * Key benefits:
86411
+ * - Maintains each SVG's independence - use them individually elsewhere
86412
+ * - Handles different viewBox dimensions automatically
86413
+ * - Works with any SVG components regardless of internal implementation
86414
+ * - Supports unlimited overlay elements
86415
+ * - Creates proper masking between elements for visual integration
86416
+ *
86417
+ * Usage example combining two independent icon components:
86418
+ * ```jsx
86419
+ * <SVGMaskOverlay viewBox="0 0 24 24">
86420
+ * <DatabaseSvg />
86421
+ * <svg x="12" y="12" width="16" height="16" overflow="visible">
86422
+ * <PlusSvg />
86423
+ * </svg>
86424
+ * </SVGMaskOverlay>
86425
+ * ```
86426
+ *
86427
+ * @param {Object} props - Component properties
86428
+ * @param {string} props.viewBox - The main viewBox for the composition (required as soon as there is an overlay)
86429
+ * @param {import("ignore:preact").ComponentChildren} props.children - SVG elements (first is base, rest are overlays)
86430
+ * @returns {import("ignore:preact").VNode} A composed SVG with all elements properly masked
86431
+ */
86279
86432
  const SVGMaskOverlay = ({
86280
86433
  viewBox,
86281
86434
  children
@@ -86288,7 +86441,7 @@ const SVGMaskOverlay = ({
86288
86441
  return children[0];
86289
86442
  }
86290
86443
  if (!viewBox) {
86291
- console.error("SVGComposition requires an explicit viewBox");
86444
+ console.error("SVGMaskOverlay requires an explicit viewBox");
86292
86445
  return null;
86293
86446
  }
86294
86447
 
@@ -86348,6 +86501,8 @@ const css$3 = /* css */`@layer navi {
86348
86501
  }
86349
86502
 
86350
86503
  .navi_card_layout {
86504
+ box-sizing: border-box;
86505
+ min-height: 100%;
86351
86506
  padding-top: var(--layout-margin-top, var(--layout-margin-y, var(--layout-margin)));
86352
86507
  padding-right: var(--layout-margin-right, var(--layout-margin-x, var(--layout-margin)));
86353
86508
  padding-bottom: var(--layout-margin-bottom, var(--layout-margin-y, var(--layout-margin)));
@@ -86389,6 +86544,11 @@ const CardLayoutStyleCSSVars = {
86389
86544
  minHeight: "--layout-min-height"
86390
86545
  };
86391
86546
  /**
86547
+ * The card is centered in the area both ways and is as wide as its content,
86548
+ * `minWidth` at least. Down the area, the layout takes at least the height of
86549
+ * what holds it, so how tall that area is is up to the page around it, as for
86550
+ * `ViewportLayout`.
86551
+ *
86392
86552
  * @type {import("ignore:preact").FunctionComponent<{
86393
86553
  * alignX?: string,
86394
86554
  * alignY?: string,
@@ -86418,6 +86578,11 @@ const CardLayout = ({
86418
86578
  }) => {
86419
86579
  import.meta.css = [css$3, "@jsenv/navi/src/layout/card_layout.jsx"];
86420
86580
  return jsx(Box, {
86581
+ flex: true
86582
+ // The card's place in the area; alignX/alignY below are its content's.
86583
+ ,
86584
+ alignX: "center",
86585
+ alignY: "center",
86421
86586
  baseClassName: "navi_card_layout",
86422
86587
  styleCSSVars: CardLayoutStyleCSSVars,
86423
86588
  visualSelector: ".navi_card",
@@ -87229,7 +87394,8 @@ const css = /* css */`.navi_side_panel {
87229
87394
  * interaction only makes sense paired with not letting focus silently
87230
87395
  * leave the panel first. A box of the page whose press must not close the
87231
87396
  * panel (a card that fills it) names the panel:
87232
- * `data-navi-popup-inside={id}` — see docs/popup_backdrop.md.
87397
+ * `data-navi-popup-inside={id}`, read when the panel has no wall
87398
+ * (`backdrop={false}`) — see docs/popup_backdrop.md.
87233
87399
  * @param {boolean} [props.closeByDrag=true] - Pushing the panel back
87234
87400
  * towards the edge it is docked to closes it: the panel follows the
87235
87401
  * pointer and finishes leaving (or comes back to rest) when it is
@@ -87244,8 +87410,8 @@ const css = /* css */`.navi_side_panel {
87244
87410
  * doc).
87245
87411
  * @param {import("ignore:preact").ComponentChildren} props.children - No built-in
87246
87412
  * close button — add one wherever it makes sense for the layout (e.g. a
87247
- * plain `<Button command="--navi-close">`), use `SidePanel.Head`'s own
87248
- * `closeButton` prop, or rely on `closeByPressOutside`/Escape instead.
87413
+ * plain `<Button command="--navi-close">` in `SidePanel.Head`), or rely on
87414
+ * `closeByPressOutside`/Escape instead.
87249
87415
  * A form sent inside the panel closes it, as in any popup: a panel one
87250
87416
  * keeps editing in says `command="--navi-void"` on that form (see
87251
87417
  * docs/form_changed.md).
@@ -87336,9 +87502,7 @@ const toCssLength = (value, propertyName) => value === undefined || value === nu
87336
87502
  * Stuck to the top of the panel's own scrollable area (`position: sticky`)
87337
87503
  * regardless of `side` — only the panel's content in between scrolls. No
87338
87504
  * built-in padding or close button — add a `<Button command="--navi-close">`
87339
- * (optionally with the `"navi_side_panel_head_close_button"` className, a
87340
- * float-right utility this file's own CSS still provides) wherever it makes
87341
- * sense for the layout.
87505
+ * wherever it makes sense for the layout.
87342
87506
  *
87343
87507
  * @param {object} props
87344
87508
  * @param {string} [props.className] - Merged with the shared