@jsenv/navi 0.29.387 → 0.29.388

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.
@@ -559,7 +559,9 @@ effect(() => {
559
559
  * Built-in key namespaces, all overridable — the registrations below are the
560
560
  * exhaustive list, read them to find the exact key to override:
561
561
  * - `"button.*"` — Clear, Reset, Send, Open, Close, Cancel, Confirm…
562
- * - `"time.*"` — what a clock writes between hours and minutes, and how the two ends of a span are named; the wording the formatters use is registered by @jsenv/humanize
562
+ * - `"confirm.*"` — the question a confirmation asks by default
563
+ * - `"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
564
+ * - `"time_range.*"` — how the two ends of a span are named ("From", "to")
563
565
  * - `"spin.*"` — the ends of a steppable range
564
566
  * - `"list.*"` — empty/no-match/failed-rows messages
565
567
  * - `"badge_list.*"` — the "+[count] more" overflow badge
@@ -1572,11 +1574,10 @@ const errorIsDisplayed = (error) => {
1572
1574
  *
1573
1575
  * Whatever the reader does with it is already covered without any deadline: it
1574
1576
  * displays it (and marks it), or it throws it, and a thrown error either finds a
1575
- * boundary that displays it or reaches window on its own — `preact/debug`
1576
- * re-throws every error a boundary caught, and an unbounded one aborts the
1577
- * render loudly. Reporting it here as well would be a second voice saying the
1578
- * same thing, always the wrong one, since this module cannot see which of those
1579
- * happened.
1577
+ * boundary that displays it or reaches window on its own — preact re-throws
1578
+ * what no boundary handled. Reporting it here as well would be a second voice
1579
+ * saying the same thing, always the wrong one, since this module cannot see
1580
+ * which of those happened.
1580
1581
  */
1581
1582
  const errorTakenByRenderSet = new WeakSet();
1582
1583
  const markErrorAsTakenByRender = (error) => {
@@ -1666,7 +1667,7 @@ const errorIsAccountedFor = (error) => {
1666
1667
  /**
1667
1668
  * Rethrown rather than logged: an error nobody took is an unhandled error, and
1668
1669
  * the runtime already knows what to do with those (window "error" event, jsenv
1669
- * overlay in dev). Same trick preact/debug uses for the same reason.
1670
+ * overlay in dev). Same answer preact gives an error no boundary handled.
1670
1671
  */
1671
1672
  const errorReportedSet = new WeakSet();
1672
1673
  const reportErrorIfNobodyDisplaysIt = (error, { action } = {}) => {
@@ -2187,7 +2188,7 @@ const compareTwoJsValues = (
2187
2188
  }
2188
2189
  return true;
2189
2190
  }
2190
- // Ordered array comparison (original behavior)
2191
+ // Ordered array comparison
2191
2192
  let i = 0;
2192
2193
  while (i < a.length) {
2193
2194
  const aValue = a[i];
@@ -3370,6 +3371,8 @@ const actionWeakMap = new WeakMap();
3370
3371
  * long is this still true?" belongs to the question: a court list is good for
3371
3372
  * the session, a count of people watching grows on its own and is worth a few
3372
3373
  * minutes, a payment status is worth nothing at all.
3374
+ * @param {{ verb?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" }} [rootOptions.meta] -
3375
+ * `verb` puts the action under the network policy (docs/network_policy.md).
3373
3376
  */
3374
3377
  const createAction = (callback, rootOptions = {}) => {
3375
3378
  const existing = actionWeakMap.get(callback);
@@ -4484,7 +4487,7 @@ const runUnwatched = (startRun) => {
4484
4487
  /**
4485
4488
  * Reactively runs an action whenever the params derived from signals change.
4486
4489
  *
4487
- * @param {object} action - The action to run.
4490
+ * @param {object | Function} action - The action to run, or a function made into one.
4488
4491
  * @param {Function} deriveActionParamsFromSignals - A function that reads signals and returns
4489
4492
  * the params to pass to the action. It is re-evaluated automatically whenever a signal it
4490
4493
  * read changes. Return `false`/`null`/`undefined` to skip running the action.
@@ -4507,6 +4510,8 @@ const runUnwatched = (startRun) => {
4507
4510
  * ```
4508
4511
  * The action will not fire while the user is actively changing filters; it fires once
4509
4512
  * they pause for half a second.
4513
+ * @returns {object} the instance bound to the derived params: the one to read
4514
+ * (`useAsyncData`), since the answer lands there and never on `action` itself.
4510
4515
  */
4511
4516
  // The run is not awaited here, and a rejection nobody waits for is an unhandled
4512
4517
  // one — in dev, an error overlay thrown over a page that is already saying what
@@ -5021,11 +5026,12 @@ const isNumberInput = (type, naviInputType) => {
5021
5026
  */
5022
5027
  /**
5023
5028
  * A bound signal that carries a default of its own says the same thing on every
5024
- * control: the control starts there and stays uncontrolled, which is what makes
5025
- * a form read the value shown as a SUGGESTION rather than as something it
5026
- * already holds. Uncontrolled here is about what the control HOLDS, not about
5027
- * whether it follows the signal — the binding stays two-way either way (see
5028
- * stateFromSignal in control_hooks.jsx). Written once and used by everything
5029
+ * control: the control starts there, uncontrolled, and a reset goes back to it.
5030
+ * Uncontrolled here is about where the control starts, not about whether it
5031
+ * follows the signal — the binding stays two-way either way (see
5032
+ * stateFromSignal in control_hooks.jsx) — nor about what a form counts as
5033
+ * held: that is the signal's content (isUIStateHeld in held_ui_state.js), and a
5034
+ * stateSignal holds its default. Written once and used by everything
5029
5035
  * that takes a `signal`, so one signal cannot mean two different things
5030
5036
  * depending on which control it was handed to.
5031
5037
  */
@@ -5085,8 +5091,7 @@ const resolveInputProps = (props, { controlType = "input" } = {}) => {
5085
5091
  // If no explicit defaultChecked, derive it from the signal's default
5086
5092
  // value so that resetUIState restores to the original default.
5087
5093
  // Only a stateSignal carries a default of its own; a plain signal has
5088
- // no `options` at all, and asking it for one used to throw on mount —
5089
- // the same optional read every other branch here already does.
5094
+ // no `options` at all, hence the optional read.
5090
5095
  const defaultVal = signalOptions?.getDefaultValue(false);
5091
5096
  if (defaultVal === undefined) ; else if (props.type === "radio") {
5092
5097
  if (defaultVal === true) {
@@ -9169,8 +9174,8 @@ const isLanguageSupported = (lang, supportedLanguages) => {
9169
9174
  * supported), falls back to `supportedLanguagesSignal` itself so callers
9170
9175
  * still get *something* usable rather than an empty array.
9171
9176
  *
9172
- * Consumers that accept either a single lang or an ordered array (this
9173
- * package's own `matchBestLang`/`createI18n`, and native `Intl.NumberFormat`/
9177
+ * Consumers that accept either a single lang or an ordered array
9178
+ * (@jsenv/humanize's `createI18n` and formatters, and native `Intl.NumberFormat`/
9174
9179
  * `Intl.DateTimeFormat`) can pass this straight through: anything not
9175
9180
  * covered by the first entry falls through to the next, rather than
9176
9181
  * jumping straight to an unrelated default like "en".
@@ -10153,15 +10158,15 @@ const readOnlyMessage = (field) => {
10153
10158
  * - Action dispatch + validity: `control_action.js` / `dispatchRequestAction`
10154
10159
  * - Validity checking: `control_validation.js`
10155
10160
  *
10156
- * Each UI state controller gets its own `controlInteraction` instance (created by
10157
- * `createControlInteraction`) just like it gets a `controlValidity` instance.
10161
+ * Each UI state controller gets its own instance, `controller.rules.interaction`
10162
+ * (created by `createControlInteraction`), beside `controller.rules.validation`.
10158
10163
  *
10159
10164
  * Flow:
10160
10165
  * user interaction
10161
10166
  * → dispatchRequestInteraction
10162
10167
  * → "navi_request_interaction" event
10163
10168
  * → onRequestInteraction
10164
- * → check disabled / read-only / busy (via controller.controlInteraction)
10169
+ * → check disabled / read-only / busy (via controller.rules.interaction)
10165
10170
  * against the interaction's `intent` ("write" by default, "read" for one
10166
10171
  * that only shows what is already there — see READONLY_CONSTRAINT)
10167
10172
  * → if blocked → prevented()
@@ -11004,7 +11009,8 @@ const interactionsDisputeThePress = (interactions) => {
11004
11009
  * control (a Button) is its own; a Box inside one, or wrapping exactly one,
11005
11010
  * reaches it; a Box with no control anywhere near it — or laying out several,
11006
11011
  * which belongs to none of them — still answers with a callback of the caller's,
11007
- * and only "request_action" has nothing to ask.
11012
+ * and only the two requests ("request_action", "request_ui_action") have
11013
+ * nothing to ask.
11008
11014
  *
11009
11015
  * Set up once per element rather than on every render, which is what lets a
11010
11016
  * detector be a plain `setup`/teardown pair. So the interactions themselves are
@@ -11173,7 +11179,7 @@ const useInteractionsEffect = (ref, interactionsRef) => {
11173
11179
  cancelable: true,
11174
11180
  });
11175
11181
  // The event the interaction was read from stays reachable from it:
11176
- // `findEvent(actionEvent, "pointerdown")` still finds the press a swipe
11182
+ // `findEvent(actionEvent, "pointerdown")` still finds the press a hold
11177
11183
  // was made of.
11178
11184
  chainEvent(interactionEvent, originalEvent);
11179
11185
  if (!element.dispatchEvent(interactionEvent)) {
@@ -11939,7 +11945,7 @@ const swipeTypeOf = (axis, pulled) => {
11939
11945
  * …
11940
11946
  * }}}
11941
11947
  *
11942
- * `toId` is an element and never null: a copy over nothing is a release that meant
11948
+ * `toId` names an element and is never null: a copy over nothing is a release that meant
11943
11949
  * nothing, and the interaction does not happen at all — unless the element
11944
11950
  * declares `leave` (see below), which gives that release a meaning of its own.
11945
11951
  *
@@ -16403,7 +16409,7 @@ const debug$3 = (...args) => {
16403
16409
  * @param {string} name - resource name, used in action names and error messages
16404
16410
  * @param {Object} restCallbacks - `{ idKey, uniqueKeys, rerunOn, dependencies, GET, GET_MANY, GET_RANGE, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
16405
16411
  * @param {string} [restCallbacks.idKey] - primary key property, defaults to `"id"`
16406
- * @param {string[]} [restCallbacks.uniqueKeys] - alternate keys the store can find an item by (e.g. `"username"`); a callback may return a different `id` to rename the item's primary key
16412
+ * @param {string[]} [restCallbacks.uniqueKeys] - alternate keys the store can find an item by (e.g. `"username"`)
16407
16413
  * @see docs/resource.md — relationships, callback return contracts, decision table
16408
16414
  *
16409
16415
  * @example
@@ -16630,7 +16636,7 @@ const createResource = (
16630
16636
  * identical parameters, preventing cross-contamination between different parameter sets.
16631
16637
  *
16632
16638
  * @param {Object} params - Parameters to bind to all actions of this resource (required)
16633
- * @param {Object} options - Additional options for the parameterized resource
16639
+ * @param {{ rerunOn?: Object, dependencies?: Object[] }} [options] - reruns of that scope; left out, the scope inherits the resource's
16634
16640
  * @returns {Object} A new resource instance with parameter-bound actions and isolated lifecycle
16635
16641
  * @see docs/resource.md — what a scope isolates, and `dependencies`
16636
16642
  *
@@ -16648,7 +16654,7 @@ const createResource = (
16648
16654
  * const ROLE_WITH_OWNERSHIP = role.withParams({ owners: true }, {
16649
16655
  * dependencies: [role, database, tables],
16650
16656
  * });
16651
- * // ROLE_WITH_OWNERSHIP.GET_MANY will autorerun when any table/database/role is POST/DELETE
16657
+ * // ROLE_WITH_OWNERSHIP.GET_MANY reruns after any write (POST/PUT/PATCH/DELETE) on a role, a database or a table
16652
16658
  */
16653
16659
  const withParams = (
16654
16660
  paramsToInject,
@@ -16694,13 +16700,14 @@ const createResource = (
16694
16700
  * Callback return contracts:
16695
16701
  * - GET / PUT → the parent object with the relationship nested inside:
16696
16702
  * `async ({ id }) => ({ id, session: { id: 10, token: "abc" } })`; `null` for no relationship
16697
- * - DELETE → the parent id (or `{ id }`); the property is set to `null`
16703
+ * - DELETE → the parent id; the property is set to `null`
16698
16704
  *
16699
16705
  * The backend may also embed the child inline in a parent GET/POST response — the
16700
16706
  * setter on the property upserts the nested object into the child store.
16701
16707
  *
16702
- * Returns the child relationship resource, itself chainable:
16703
- * `USER_SESSION.one("device", DEVICE)` adds a reactive `.device` property to each session.
16708
+ * Returns the relationship resource: its actions (`USER_SESSION.GET`, `.PUT`,
16709
+ * `.DELETE`) write the parent's property. A relation of the child itself is
16710
+ * declared on the child resource (`SESSION.one("device", DEVICE)`).
16704
16711
  *
16705
16712
  * @param {string} propertyName - property holding the child on each parent item
16706
16713
  * @param {Object} childResource - the independent resource created by `resource()`
@@ -17861,7 +17868,7 @@ Received an object with keys: ${keys.join(", ")}.`,
17861
17868
  * });
17862
17869
  *
17863
17870
  * syncResourceToSignals(USER, { username: usernameSignal });
17864
- * // Now when a user item's username is updated via USER.PUT,
17871
+ * // When a user item's username is updated via USER.PUT,
17865
17872
  * // usernameSignal.value is set to the new username,
17866
17873
  * // which in turn triggers the route Signal->URL sync and updates the browser URL.
17867
17874
  */
@@ -17874,6 +17881,17 @@ const syncResourceToSignals = (resource, propertyToSignalMap) => {
17874
17881
  syncStoreToSignals(resource.store, propertyToSignalMap);
17875
17882
  };
17876
17883
 
17884
+ /**
17885
+ * The same, for a `scopedOne`/`scopedMany` resource: its items live in one store
17886
+ * per owner, so the store to watch is the one of the owner `ownerSignal` names
17887
+ * (by id or by any unique key), and it is switched when the signal changes.
17888
+ * Nothing is synced while `ownerSignal` holds `null`/`undefined` or names an
17889
+ * owner nothing has been loaded for yet.
17890
+ *
17891
+ * @param {Object} resource - a resource made by `.scopedOne()` / `.scopedMany()`
17892
+ * @param {import("@preact/signals").Signal} ownerSignal - the owner whose children are watched
17893
+ * @param {Object} propertyToSignalMap - `{ [propertyName]: signal }`, as for `syncResourceToSignals`
17894
+ */
17877
17895
  const syncOwnedResourceToSignals = (
17878
17896
  resource,
17879
17897
  ownerSignal,
@@ -17921,7 +17939,7 @@ const generateSignalId = () => {
17921
17939
  * 1. Initially takes value from the default signal
17922
17940
  * 2. When explicitly set (programmatically or via localStorage), the explicit value takes precedence
17923
17941
  * 3. When default signal changes, it only updates if no explicit value was ever set
17924
- * 4. Calling reset() or setting to undefined makes the signal use the dynamic default again
17942
+ * 4. Setting it to undefined makes the signal use the dynamic default again
17925
17943
  * 5. If dynamic default is undefined and options.default is provided, uses the static fallback
17926
17944
  *
17927
17945
  * This is useful for:
@@ -17935,7 +17953,9 @@ const generateSignalId = () => {
17935
17953
  * @param {string|number} [options.id] - Custom ID for the signal. If not provided, an auto-generated ID will be used. Used for localStorage key and route pattern detection.
17936
17954
  * @param {any} [options.default] - Static fallback value used when defaultValue is a signal and that signal's value is undefined
17937
17955
  * @param {boolean} [options.persists=false] - Whether to persist the signal value in localStorage using the signal ID as key
17938
- * @param {"string" | "number" | "boolean" | "object"} [options.type="string"] - Type for localStorage serialization/deserialization
17956
+ * @param {string} [options.type] - Type the value is coerced to, and read back as from localStorage and the url:
17957
+ * any @jsenv/validity type (`"string"`, `"number"`, `"boolean"`, `"array"`, `"object"`, `"integer"`, `"date"`,
17958
+ * `"time"`…). Without it the value is not coerced.
17939
17959
  * @param {"string" | "number" | "boolean"} [options.itemType] - For array type: type of the array items.
17940
17960
  * Used when reading the value back from a url search param, where everything is a string:
17941
17961
  * `?level=3,4` becomes `[3, 4]` instead of `["3", "4"]`. Without it items stay strings.
@@ -19105,14 +19125,12 @@ const LoadingFallback = ({
19105
19125
  * Two things it gets right that a hand-written boundary rarely does, both
19106
19126
  * explained in docs/error_handling.md:
19107
19127
  *
19108
- * - It marks the error as displayed ONLY when it actually displays it.
19109
- * `preact/debug` rethrows every error a boundary caught in a `setTimeout`, on
19110
- * purpose (React devtools compatibility), so a handled error still reaches
19111
- * window and the jsenv overlay covers the app unless `__handled_by__` is set.
19112
- * Setting it before knowing whether anything is rendered turns a boundary into
19113
- * a bug swallower: a TypeError in a component becomes a blank page AND a
19114
- * silent one. Without a `fallback` there is nothing to display, so the error is
19115
- * left alone and continues up.
19128
+ * - It marks the error as displayed ONLY when it actually displays it. The
19129
+ * jsenv overlay skips an error carrying `__handled_by__`, so a mark set before
19130
+ * knowing whether anything is rendered mutes an error that then continues up:
19131
+ * a TypeError in a component becomes a blank page AND a silent one. Without a
19132
+ * `fallback` there is nothing to display, so the error is left alone and
19133
+ * continues up.
19116
19134
  *
19117
19135
  * - It resets on navigation, not only on rerun. Rerunning the failed action is
19118
19136
  * one way out; going somewhere else is the common one. Without a reset on the
@@ -22222,9 +22240,48 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
22222
22240
  * separator?: import("ignore:preact").ComponentChildren | ((index: number) => import("ignore:preact").ComponentChildren),
22223
22241
  * selfInteractions?: string,
22224
22242
  * interactions?: { [type: string]: "request_action" | "request_ui_action" | "refuse" | ((event: Event) => void) | false | null | undefined },
22243
+ * alignX?: string,
22244
+ * alignY?: string,
22245
+ * spacing?: string | number,
22246
+ * overflow?: string,
22247
+ * header?: boolean,
22248
+ * footer?: boolean,
22249
+ * body?: boolean,
22250
+ * sticky?: boolean,
22225
22251
  * children?: import("ignore:preact").ComponentChildren,
22226
22252
  * [key: string]: any,
22227
22253
  * }>}
22254
+ * @param {"x"|"y"|boolean} [flex] A flex container: `"x"` (or `true`) side by
22255
+ * side, `"y"` stacked. Named by axis because `flex-direction: column` stacks
22256
+ * items vertically, the opposite of what "column" suggests.
22257
+ * @param {boolean} [inline] The inline form of the box's display
22258
+ * (`inline-flex`, `inline-grid`…). An inline box given a `width`/`height`
22259
+ * becomes an inline flex row, since an inline box ignores both.
22260
+ * @param {string} [alignX] Horizontal alignment of the content whatever the
22261
+ * flex direction: `justify-content` or `align-items` is picked from it
22262
+ * (`text-align` outside flex and grid).
22263
+ * @param {string} [alignY] Vertical alignment, the same way.
22264
+ * @param {string|number} [spacing] The gap between children in a flex or grid
22265
+ * box (`spacingX`/`spacingY` per axis) — there is no `gap` prop. Spacing
22266
+ * props (`spacing`, `margin*`, `padding*`) take the scale `"xxs"`…`"xxl"`
22267
+ * (`--navi-xxs`…), `"-s"` for a negative, or any CSS value.
22268
+ * @param {object|string} [style] CSS properties, plus pseudo keys holding
22269
+ * their own: `style={{ color: "blue", ":hover": { color: "red" } }}`
22270
+ * (`":-navi-pressed"`, `"::before"`…). A pseudo class used there is tracked
22271
+ * on the element; nothing re-renders when it changes.
22272
+ * @param {string} [overflow] `"auto"`/`"scroll"` (`overflowX`/`overflowY`
22273
+ * too) makes the box a scrolling area, which is what gives `header`/`body`/
22274
+ * `footer` their meaning; such a box is also `isolation: isolate` and gets a
22275
+ * `scroll-padding` (see docs/scroll.md).
22276
+ * @param {boolean} [header] A part that stays put at the top of the scrolling
22277
+ * box it is a direct child of — nothing at all without that box's overflow.
22278
+ * Rendered as `<header>` unless `as` says otherwise.
22279
+ * @param {boolean} [footer] The same at the bottom, rendered as `<footer>`.
22280
+ * @param {boolean} [body] The only part of its scrolling box that scrolls,
22281
+ * `header`/`footer` sitting outside it. It shrinks, it never grows.
22282
+ * @param {boolean} [sticky] `position: sticky` in the sticky z-index band
22283
+ * (`--navi-z-index-sticky`), stuck or not; an explicit `zIndex`, `"auto"`
22284
+ * included, wins (see docs/z_index.md). `top`/`bottom`/… take `true` for 0.
22228
22285
  * @param {string} [background] Any CSS background, plus two keywords for the
22229
22286
  * planes an app paints: `"surface"` is the paper content sits on,
22230
22287
  * `"chrome"` the frame around a screen — a top bar, a side menu, a toolbar.
@@ -23968,21 +24025,16 @@ const createUITransitionController = (
23968
24025
  /**
23969
24026
  * UITransition
23970
24027
  *
23971
- * A Preact component that enables smooth animated transitions between its children when the content changes.
23972
- * It observes content keys and phases to create different types of transitions.
24028
+ * A Preact component that animates the change between its children: the
24029
+ * outgoing content fades out while the incoming one fades in, and the box
24030
+ * resizes from the one to the other (the mechanics are in ui_transition.js).
23973
24031
  *
23974
- * Features:
23975
- * - Content transitions: Between different content keys (e.g., user profiles, search results)
23976
- * - Phase transitions: Between loading/content/error states for the same content key
23977
- * - Automatic size animation to accommodate content changes
23978
- * - Configurable transition types: "slide-left", "cross-fade"
23979
- * - Independent duration control for content and phase transitions
23980
- *
23981
- * Usage:
23982
- * - Wrap dynamic content in <UITransition> to animate between states
23983
- * - Set a unique `data-content-id` on your rendered content to identify each content variant
23984
- * - Use `data-content-phase` to mark loading/error states for phase transitions
23985
- * - Configure transition types and durations for both content and phase changes
24032
+ * Two kinds of change are told apart, from attributes on the rendered content:
24033
+ * - a content transition, when the content key changes — `data-content-key` on
24034
+ * the rendered element (a user id, a search), or `useUITransitionContentId`
24035
+ * called from a component inside (what `<Route>` does with its url pattern);
24036
+ * - a phase transition, when `data-content-phase` (a loading or error state)
24037
+ * changes for the same content key.
23986
24038
  *
23987
24039
  * Example:
23988
24040
  *
@@ -23991,9 +24043,6 @@ const createUITransitionController = (
23991
24043
  * ? <Spinner data-content-key={userId} data-content-phase />
23992
24044
  * : <UserProfile user={user} data-content-key={userId} />}
23993
24045
  * </UITransition>
23994
- *
23995
- * When `data-content-id` changes, UITransition animates content transitions.
23996
- * When `data-content-phase` changes for the same key, it animates phase transitions.
23997
24046
  */
23998
24047
 
23999
24048
  const UITransitionContentIdContext = createContext();
@@ -24115,16 +24164,12 @@ const UITransition = ({
24115
24164
  };
24116
24165
 
24117
24166
  /**
24118
- * The goal of this hook is to allow a component to set a "content key"
24119
- * Meaning all content within the component is identified by that key
24120
- *
24121
- * When the key changes, UITransition will be able to detect that and consider the content
24122
- * as changed even if the component is still the same
24123
- *
24124
- * This is used by <Route> to set the content key to the route path
24125
- * When the route becomes inactive it will call useUITransitionContentId(undefined)
24126
- * And if a sibling route becones active it will call useUITransitionContentId with its own path
24167
+ * Names the content a component renders, for the <UITransition> around it:
24168
+ * everything rendered inside is identified by that key, and the key changing
24169
+ * is read as a content change even though the component is the same.
24127
24170
  *
24171
+ * <Route> calls it with the url pattern of the branch it renders; the branch
24172
+ * unmounting removes its part, and a sibling branch mounting adds its own.
24128
24173
  */
24129
24174
  const useUITransitionContentId = value => {
24130
24175
  const contentId = useContext(UITransitionContentIdContext);
@@ -24149,8 +24194,8 @@ const useUITransitionContentId = value => {
24149
24194
  };
24150
24195
 
24151
24196
  /**
24152
- * Custom route pattern matching system
24153
- * Replaces URLPattern with a simpler, more predictable approach
24197
+ * Custom route pattern matching system.
24198
+ * Not built on URLPattern: matching here is simpler and more predictable.
24154
24199
  */
24155
24200
 
24156
24201
 
@@ -27482,7 +27527,7 @@ const useRouteStatus = (route) => {
27482
27527
  const assertRoute = (route) => {
27483
27528
  if (!route.isRoute) {
27484
27529
  throw new Error(
27485
- "The route prop must be a route object created with createRoute",
27530
+ "The route prop must be a route object created with route()",
27486
27531
  );
27487
27532
  }
27488
27533
  if (!setupRoutesCalled) {
@@ -31453,8 +31498,9 @@ const RouteTransitionArea = ({
31453
31498
  * Declare how a pair of routes moves against each other — or how one route is
31454
31499
  * entered, from wherever its door happens to be.
31455
31500
  *
31456
- * @param {object|null} from - a route, or `{ route, params }` when the page is
31457
- * a param of a route rather than a route of its own. `null` says the page is
31501
+ * @param {object|null} from - a route (or the `routeFallback()` page), or
31502
+ * `{ route, params }` when the page is a param of a route rather than a route
31503
+ * of its own. `null` says the page is
31458
31504
  * reached FROM ANYWHERE: its door is in the furniture (a gear in the top
31459
31505
  * bar, a "+" in the tab bar) rather than on a screen, so there is no pair to
31460
31506
  * write it on. Arriving at `to` from any page then plays forward and leaving
@@ -31502,8 +31548,7 @@ const RouteTransitionArea = ({
31502
31548
  * Whatever is written here is what EVERY crossing of the pair plays. One
31503
31549
  * crossing can ask for something else — `<Link routeTransition>`, or
31504
31550
  * navTo(url, { routeTransition }) — which overrides this field by field, for
31505
- * that
31506
- * navigation alone.
31551
+ * that navigation alone.
31507
31552
  * @returns {() => void} remove this relation.
31508
31553
  */
31509
31554
  const defineRouteTransition = (from, to, transition) => {
@@ -31586,9 +31631,8 @@ const normalizeTransition = transition => {
31586
31631
  *
31587
31632
  * Two mouths, one meaning: the element being pressed wears it (a `<Link
31588
31633
  * routeTransition>`, or the attribute by hand on any anchor), or navTo() is
31589
- * handed
31590
- * it. Both arrive here through the announcement the navigation makes before it
31591
- * writes anything (see before_routing.js).
31634
+ * handed it. Both arrive here through the announcement the navigation makes
31635
+ * before it writes anything (see before_routing.js).
31592
31636
  *
31593
31637
  * A request answers FIELD BY FIELD: what it does not say, the relation — or
31594
31638
  * the default — still answers for. So `{ direction: "back" }` keeps the pair's
@@ -32835,10 +32879,10 @@ const css$11 = /* css */`.navi_route_travel {
32835
32879
  * The pages are cut at the edge of this box while they travel, and at the app's
32836
32880
  * safe area the box runs under, which is written on the transition's own
32837
32881
  * pseudo-elements — no overflow of the document reaches pictures drawn in the
32838
- * top layer. It needs nothing of the browser beyond view
32839
- * transitions themselves: a browser without them (Firefox) navigates without the
32840
- * movement, and the gesture applies its change on release instead of dragging a
32841
- * picture that does not exist.
32882
+ * top layer. It needs nothing of the browser beyond view transitions
32883
+ * themselves: a browser without them navigates without the movement, and the
32884
+ * gesture applies its change on release instead of dragging a picture that does
32885
+ * not exist.
32842
32886
  *
32843
32887
  * While a travel plays, the rest of the page is taken as a picture too — this
32844
32888
  * box asks for `view-transition-name: root` back for that time, so an
@@ -34429,9 +34473,6 @@ const onNaviCommand = (e, { debugCommand = () => {} } = {}) => {
34429
34473
  `targeting`,
34430
34474
  commandTarget,
34431
34475
  );
34432
- // Timed once, for the wheel-in-dialog "Définir feels frozen on mobile" case;
34433
- // the line is gone now that the answer is known — a command that runs in 1ms
34434
- // said nothing, and it said it on every single interaction.
34435
34476
  return implementation();
34436
34477
  };
34437
34478
 
@@ -35076,8 +35117,8 @@ registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
35076
35117
  target,
35077
35118
  implementation: () => {
35078
35119
  // The popup reads `anchor` first and falls back to the source (see
35079
- // onnavi_request_open in popover.jsx), so saying nothing keeps the old
35080
- // behaviour: the element asking is the element opened against.
35120
+ // onnavi_request_open in popover.jsx): without an `anchor`, the element
35121
+ // asking is the element opened against.
35081
35122
  return dispatchCustomEvent(target, "navi_request_open", {
35082
35123
  event,
35083
35124
  source: resolveCommandProxySource(source),
@@ -37548,8 +37589,8 @@ const usePopupContentMount = (
37548
37589
  * are no-ops.
37549
37590
  *
37550
37591
  * Behavior:
37551
- * - No <dialog>/<details>/[popover] ancestor → runs like a normal
37552
- * useLayoutEffect with the provided deps.
37592
+ * - No <dialog>/<details>/[popover]/[aria-expanded] ancestor → runs like a
37593
+ * normal useLayoutEffect with the provided deps.
37553
37594
  * - Inside a closed/hidden ancestor → skips the initial run; instead runs
37554
37595
  * the callback once the ancestor opens — see @jsenv/dom's own
37555
37596
  * observeAncestorOpenState for exactly how that's detected, and why it
@@ -37563,8 +37604,9 @@ const usePopupContentMount = (
37563
37604
  * reveals nothing about them, so it does not re-run them either. See
37564
37605
  * isDisplayedDespiteClosedAncestor in @jsenv/dom.
37565
37606
  *
37566
- * The callback's second argument is always a `navi_displayed` CustomEvent,
37567
- * with `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
37607
+ * The callback is called with the element (`ref.current`), and a
37608
+ * `navi_displayed` CustomEvent as its second argument, with
37609
+ * `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
37568
37610
  * - No <dialog>/<details>/[popover]/[aria-expanded] ancestor at all →
37569
37611
  * `{ ancestor: document, ancestorType: "document" }`.
37570
37612
  * - Otherwise → `{ ancestor: <the matched element>, ancestorType: "dialog"
@@ -37729,6 +37771,8 @@ const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
37729
37771
  // see also https://github.com/preactjs/preact/issues/1255
37730
37772
 
37731
37773
 
37774
+ const NO_ELEMENT_REF = { current: null };
37775
+
37732
37776
  /**
37733
37777
  * Programmatic autofocus that runs after Preact layout effects are flushed.
37734
37778
  *
@@ -37781,13 +37825,12 @@ const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
37781
37825
  * the browser's built-in scroll-into-view that accompanies focus.
37782
37826
  * @param {boolean} [options.focusVisible]
37783
37827
  * Passed as `focusVisible` to `element.focus()`.
37784
- * @param {boolean} [options.autoFocusSelect]
37828
+ * @param {boolean} [options.autoSelect]
37785
37829
  * When true, also calls `element.select()` after focusing (useful for text inputs).
37786
- * @returns {Function} triggerAutofocus — can be called manually with a synthetic
37787
- * event to re-run the focus logic outside of the layout-effect lifecycle.
37830
+ * @returns {object} The attributes to spread on the element: `navi-autofocus`
37831
+ * (and `navi-autofocus-select`), with `autoFocus` set to undefined so the
37832
+ * native attribute never lands on the node.
37788
37833
  */
37789
- const NO_ELEMENT_REF = { current: null };
37790
-
37791
37834
  const useAutoFocus = (
37792
37835
  focusableElementRef,
37793
37836
  autoFocus,
@@ -37982,9 +38025,10 @@ document.body.addEventListener(
37982
38025
  * and nothing about it stops someone from starting a run on every row before
37983
38026
  * any of them comes back: a dozen requests in flight, a dozen rows waiting, and
37984
38027
  * a server asked to do a dozen things at once because a list happened to be
37985
- * long. The guard is what makes the surface say "not yet" instead — the rows
37986
- * that are not running go read-only and say what they are waiting for, and the
37987
- * next press is possible again the moment one of the runs comes back.
38028
+ * long. The guard is what makes the surface say "not yet" instead — the
38029
+ * controls that would start another run go read-only and say what they are
38030
+ * waiting for, and the next press is possible again the moment one of the runs
38031
+ * comes back.
37988
38032
  *
37989
38033
  * It counts runs, not values — which is what separates it from
37990
38034
  * `maxLengthGuard`, the one that says how many things a selection may HOLD.
@@ -38329,14 +38373,13 @@ const createControlGuard = (controller) => {
38329
38373
  };
38330
38374
 
38331
38375
  /**
38332
- * Orchestrates the three rule managers for a UI state controller.
38333
- *
38334
- * Instead of holding `controller.controlInteraction` + `controller.controlValidity`
38335
- * independently, a controller now has a single `controller.rules` object:
38376
+ * Orchestrates the rule managers of a UI state controller, held together in
38377
+ * one `controller.rules` object:
38336
38378
  *
38337
38379
  * controller.rules.callout — shared callout display manager
38338
38380
  * controller.rules.interaction — interactivity gate (disabled/readonly/busy)
38339
38381
  * controller.rules.validation — value validity gate (required/pattern/etc.)
38382
+ * controller.rules.guard — keystroke guard (charGuard/maxLengthGuard)
38340
38383
  *
38341
38384
  * The callout manager is created first so both interaction and validation
38342
38385
  * can use the same callout slot and the same lifecycle hooks.
@@ -40487,9 +40530,8 @@ const EMPTY_OBJECT = {};
40487
40530
  * child changes → we update picker input → navi_ui_state_change fires →
40488
40531
  * we see updatingRef=true → skip → no loop.
40489
40532
  *
40490
- * This removes the need for `command="--navi-update"` on controls placed
40491
- * inside the picker popup. It also means `commands.js` no longer has to
40492
- * manually re-dispatch to inner controls.
40533
+ * So a control placed inside the picker popup needs no
40534
+ * `command="--navi-update"` to reach the picker input.
40493
40535
  */
40494
40536
  const useUIFacadeStateController = (props, realUIStateController) => {
40495
40537
  const firstChildControllerRef = useRef(null);
@@ -40981,9 +41023,10 @@ const uiStateHoldsNothing = (uiState) => {
40981
41023
  * use `onInput` internally and route through `dispatchRequestAction` so the behavior
40982
41024
  * is consistent regardless of input type.
40983
41025
  *
40984
- * 2. Any field (text, checkbox, radio, picker…) can opt into debounce simply by passing
40985
- * a debounced action. The request-action event chain handles the timing centrally
40986
- * rather than each component having to manage its own debounce logic.
41026
+ * 2. Any field (text, checkbox, radio, picker…) can opt into debounce with
41027
+ * `actionDebounce` (ms). The input effect every field installs (input_effect.js)
41028
+ * handles the timing centrally rather than each component having to manage its
41029
+ * own debounce logic.
40987
41030
  */
40988
41031
  const NO_ACTION_YET = Symbol("no_action_yet");
40989
41032
 
@@ -41059,7 +41102,7 @@ const ControlgroupChildrenWrapper = ({
41059
41102
  });
41060
41103
 
41061
41104
  /**
41062
- * Core hook for interactive field components (InputText, InputCheckbox, etc.).
41105
+ * Core hook for interactive field components (InputTextual, InputCheckbox, etc.).
41063
41106
  *
41064
41107
  * Sets up the full field lifecycle:
41065
41108
  * - Creates a UI state controller that manages state divergence between props and user interactions
@@ -41070,7 +41113,7 @@ const ControlgroupChildrenWrapper = ({
41070
41113
  * - Handles constraint validation and message props
41071
41114
  *
41072
41115
  * All state changes route through DOM events on the field element so that
41073
- * external subscribers (e.g. useUIState, Selectable) receive every update.
41116
+ * external subscribers (e.g. useControlUIState, Selectable) receive every update.
41074
41117
  *
41075
41118
  * @returns {[controlRootProps, controlHostProps, { uiStateController }]}
41076
41119
  */
@@ -42163,10 +42206,10 @@ const useReadOnlyUncontrolled = (props, controlInfo) => {
42163
42206
  };
42164
42207
 
42165
42208
  /**
42166
- * Core hook for field group components (SelectableList, CheckboxList, etc.).
42209
+ * Core hook for field group components (List selectable, CheckboxGroup, etc.).
42167
42210
  * - Creates a UI group state controller that aggregates child states into one group state
42168
42211
  * - Binds the group's action to the aggregated state signal
42169
- * - Provides context to children: ParentUIStateController, FieldName, Disabled, ReadOnly,
42212
+ * - Provides context to children: ParentUIStateController, ControlName, Disabled, ReadOnly,
42170
42213
  * Required, Loading, Action, ActionRequester
42171
42214
  * - Overrides `onnavi_reset_ui_state` to cascade resets to all monitored children
42172
42215
  * by dispatching `navi_reset_ui_state` DOM events on each child's DOM element
@@ -44701,10 +44744,10 @@ const LoadingRectangleSvg = ({
44701
44744
  }) => {
44702
44745
  // The stroke is centered on the path, so half of it falls outside: drawing
44703
44746
  // at half the stroke width puts its outer edge exactly on the box, whatever
44704
- // the box. It used to be a share of the smaller dimension (3%, floored at
44705
- // 2px), which held the line further and further inside as the box grew — a
44706
- // caller placing the outline with `inset` got the distance it asked for on a
44707
- // field and something several pixels inward on a card. Nothing needs the
44747
+ // the box. Not a share of the smaller dimension (3%, floored at 2px): that
44748
+ // holds the line further and further inside as the box grows — a caller
44749
+ // placing the outline with `inset` gets the distance it asked for on a field
44750
+ // and something several pixels inward on a card. Nothing needs the
44708
44751
  // extra room: the svg is `overflow: visible` and whoever positions the
44709
44752
  // wrapper already reserves the half stroke.
44710
44753
  const margin = strokeWidth / 2;
@@ -45776,7 +45819,7 @@ const shouldInjectSpacingBetween = (left, right) => {
45776
45819
  * boldStable?: boolean,
45777
45820
  * shrinkWrap?: boolean,
45778
45821
  * capitalize?: boolean,
45779
- * selectRange?: string | [number, number],
45822
+ * selectRange?: string | [number, number] | [string, string],
45780
45823
  * childrenOutsideFlow?: import("ignore:preact").ComponentChildren,
45781
45824
  * [key: string]: any,
45782
45825
  * }>}
@@ -45844,9 +45887,10 @@ const shouldInjectSpacingBetween = (left, right) => {
45844
45887
  * @param {boolean} [capitalize]
45845
45888
  * Uppercases the first letter of the text content via CSS.
45846
45889
  *
45847
- * @param {string|[number,number]} [selectRange]
45848
- * Selects a portion of the text on mount. Pass a substring to search for, or
45849
- * a `[start, end]` character-offset tuple.
45890
+ * @param {string|[number,number]|[string,string]} [selectRange]
45891
+ * Selects a portion of the text on mount. Pass a substring to search for, a
45892
+ * `[start, end]` character-offset tuple, or a `[startText, endText]` pair of
45893
+ * substrings selecting from the first to the end of the second.
45850
45894
  *
45851
45895
  * @param {import("ignore:preact").ComponentChildren} [childrenOutsideFlow]
45852
45896
  * Rendered after children but outside the text spacing/flow logic. Used
@@ -46716,7 +46760,7 @@ Object.assign(PSEUDO_CLASSES, {
46716
46760
  * @param {object} props
46717
46761
  * @param {string} [props.href] - Destination. Also the default `value`, and
46718
46762
  * (when `hrefFallback`) the default visible text.
46719
- * @param {import("../route.js").Route} [props.route] - Renders via `route`
46763
+ * @param {object} [props.route] - Renders via `route`
46720
46764
  * instead of a raw `href`: the URL is built from the route (see
46721
46765
  * `routeParams`) and "current" is derived from whether the route matches.
46722
46766
  * @param {object} [props.routeParams] - Params passed to `route.buildUrl`.
@@ -46740,14 +46784,14 @@ Object.assign(PSEUDO_CLASSES, {
46740
46784
  * (`navi_value`); defaults to `href`.
46741
46785
  * @param {boolean} [props.current] - Forces the "current" state on (otherwise
46742
46786
  * derived from the href/route).
46743
- * @param {import("../route.js").Route|import("../route.js").Route[]} [props.currentExcept] -
46787
+ * @param {object|object[]} [props.currentExcept] -
46744
46788
  * Route(s) inside this link's own that are NOT it: while one of them matches,
46745
46789
  * the link is not current even though its route still is. A bar entry
46746
46790
  * standing for a whole section, and one place under that section the entry
46747
46791
  * does not stand for — settings reached from everywhere and shown over
46748
46792
  * whatever the reader was on. Nothing about the routing changes: the url is
46749
46793
  * in the section, only the link stops claiming "you are here".
46750
- * @param {import("../route.js").Route|import("../route.js").Route[]} [props.currentAlso] -
46794
+ * @param {object|object[]} [props.currentAlso] -
46751
46795
  * Route(s) other than this link's own that are it: the link is current while
46752
46796
  * one of them matches. The other side of `currentExcept` — an entry standing
46753
46797
  * for more than the one page it opens. `currentExcept` wins over it.
@@ -47660,6 +47704,7 @@ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
47660
47704
  * currentIndicator?: boolean|"top"|"bottom"|"left"|"right",
47661
47705
  * currentIndicatorSlides?: boolean,
47662
47706
  * slideContainer?: string,
47707
+ * panelPosition?: "before"|"after",
47663
47708
  * }>}
47664
47709
  * @param {boolean|"top"|"bottom"|"left"|"right"} [props.currentIndicator] - the
47665
47710
  * bar that says which tab one is on, said once here rather than on every
@@ -47685,6 +47730,9 @@ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
47685
47730
  * container says which one is on screen, and pressing a tab travels there.
47686
47731
  * Tabs that are places in the same screen rather than pages of their own —
47687
47732
  * nothing is written to the URL and nothing is a link.
47733
+ * @param {"before"|"after"} [props.panelPosition] - turns the row into folder
47734
+ * tabs: the current tab and the panel it opens share one surface, and this
47735
+ * says which side of the row the panel sits on.
47688
47736
  */
47689
47737
  const Nav = ({
47690
47738
  children,
@@ -48807,6 +48855,8 @@ const TABS_ALIGN_TO_JUSTIFY_CONTENT = {
48807
48855
  * onChange?: (value: any) => void,
48808
48856
  * tabsPosition?: "top"|"bottom"|"left"|"right",
48809
48857
  * tabsAlign?: "start"|"center"|"end"|"stretch",
48858
+ * scrollablePage?: boolean,
48859
+ * maxLines?: number|false,
48810
48860
  * borderWidth?: string|number,
48811
48861
  * borderRadius?: string|number,
48812
48862
  * tabBorderRadius?: string|number,
@@ -48986,7 +49036,7 @@ const Binder = ({
48986
49036
  * position among its siblings.
48987
49037
  * @param {import("ignore:preact").ComponentChildren} [props.label] - What the tab
48988
49038
  * shows.
48989
- * @param {import("../route.js").Route} [props.route] - Makes the tab a `Link`
49039
+ * @param {object} [props.route] - Makes the tab a `Link`
48990
49040
  * to that route, and the binder follows the url: the open item is the one
48991
49041
  * whose route matches, no `value`/`onChange` needed.
48992
49042
  * @param {string} [props.href] - Same, for a plain url.
@@ -49425,6 +49475,9 @@ const FixedBarStyleCSSVars = {
49425
49475
  * `border={false}` for none.
49426
49476
  * @param {string|number} [props.maxWidth] - Keeps the bar lined up with a
49427
49477
  * content column narrower than the window (it stays centered).
49478
+ * @param {string|number} [props.padding=0] - Along the bar only: left/right on
49479
+ * a top or bottom bar, top/bottom on a side one. Across it, the size is
49480
+ * `height`/`width`.
49428
49481
  */
49429
49482
  const FixedBar = ({
49430
49483
  children,
@@ -50293,9 +50346,10 @@ const useExpandableContext = partName => {
50293
50346
  * height too. `"idle"` builds it in a browser idle moment after load.
50294
50347
  * `"while-opened"` throws the content away once the collapse
50295
50348
  * settles — after the closing animation, so it still plays on real
50296
- * content — and rebuilds it from scratch on every expansion. Whatever the
50297
- * value, intent on the UI part (pointer entering it, focus landing in it)
50298
- * builds the content ahead of the click.
50349
+ * content — and rebuilds it from scratch on every expansion. Under the
50350
+ * other three, intent on the UI part (pointer entering it, focus landing in
50351
+ * it) builds the content ahead of the click; `"while-opened"` content is
50352
+ * never warmed, it is built at the open.
50299
50353
  */
50300
50354
  const Expandable = props => {
50301
50355
  import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
@@ -51072,7 +51126,8 @@ const DARK_CONTRAST_LIGHTNESS_THRESHOLD = 0.65;
51072
51126
  * @param {string} accentColor - The accent color value. When it changes, attributes are recomputed.
51073
51127
  * @param {object} [options]
51074
51128
  * @param {string} [options.elementSelector] - CSS selector to find the element whose computed color is read.
51075
- * Defaults to the root element itself. Useful when the color is applied to a probe/child element.
51129
+ * Defaults to the root's own `data-visual-selector` attribute, else the root element itself.
51130
+ * Useful when the color is applied to a probe/child element.
51076
51131
  * @param {string} [options.colorProperty="backgroundColor"] - Computed style property to read (e.g. "color", "borderColor").
51077
51132
  */
51078
51133
  const useAccentColorAttributes = (
@@ -53117,16 +53172,18 @@ const css$L = /* css */`@layer navi {
53117
53172
  /**
53118
53173
  * Field — a semantic wrapper that connects a label to a form control.
53119
53174
  *
53120
- * It generates a stable `fieldId` (or accepts an explicit `id`) that is
53175
+ * It generates a stable `fieldId` (or accepts an explicit one) that is
53121
53176
  * automatically forwarded to the `Label` inside the field as `htmlFor` and to
53122
- * any interactive control (Picker, Input, …) as its `id`, so clicking the
53123
- * label focuses the control without requiring manual wiring.
53177
+ * the labelable control inside it (Input, Select, Picker, Button) as its `id`,
53178
+ * so clicking the label focuses the control without requiring manual wiring.
53124
53179
  *
53125
53180
  * It also tracks the readOnly / disabled / interactive state reported by its
53126
53181
  * child control and reflects it on the `Label` (dimmed color, cursor change).
53127
53182
  *
53128
53183
  * Props:
53129
- * id — optional explicit id used as the field id instead of the auto-generated one
53184
+ * fieldId — optional explicit field id instead of the auto-generated one (an `id`
53185
+ * is the wrapper's own DOM id)
53186
+ * as="label" — the field IS the label, wrapping its control: no id needed
53130
53187
  * flex="y" — vertical layout; automatically sets alignX="start" and data-vertical
53131
53188
  * children — any JSX; should contain a `Label` and a form control
53132
53189
  * ...rest — forwarded to the wrapping element (className, style, flex, spacing, …)
@@ -53333,6 +53390,7 @@ const InputUnitSlot = ({
53333
53390
  };
53334
53391
  const InputSlot = ({
53335
53392
  side,
53393
+ children,
53336
53394
  ...props
53337
53395
  }) => {
53338
53396
  const ctx = useContext(InputTextualContext);
@@ -53362,7 +53420,11 @@ const InputSlot = ({
53362
53420
  e.preventDefault();
53363
53421
  }
53364
53422
  },
53365
- ...props
53423
+ ...props,
53424
+ children: jsx(ControlIdContext.Provider, {
53425
+ value: undefined,
53426
+ children: children
53427
+ })
53366
53428
  });
53367
53429
  };
53368
53430
 
@@ -54827,9 +54889,9 @@ const InputWithListResolver = props => {
54827
54889
  };
54828
54890
 
54829
54891
  /**
54830
- * InputWithList — connects an input to a SelectableList via its id.
54892
+ * InputWithList — connects an input to a selectable List via its id.
54831
54893
  *
54832
- * Usage: <Input navi-list="my-list-id" /> next to <SelectableList id="my-list-id" />
54894
+ * Usage: <Input navi-list="my-list-id" /> next to <List selectable id="my-list-id">
54833
54895
  *
54834
54896
  * Behavior:
54835
54897
  * - ArrowDown / ArrowUp move the list's "current item" without moving focus
@@ -55197,11 +55259,14 @@ const useAutoSelectReadOnly = (props) => {
55197
55259
  *
55198
55260
  * - charGuard — restricts which characters can be typed, pasted, or set externally.
55199
55261
  * Accepts a preset name or a raw regex character class:
55200
- * "numeric" → digits only, sets inputMode="numeric" + pattern auto
55262
+ * true / "auto" → picked from the field: inputMode numeric/decimal, type tel/email
55263
+ * "numeric" → digits only, sets inputMode="numeric"
55264
+ * "decimal" → digits, minus, dot, comma, sets inputMode="decimal"
55201
55265
  * "alpha" → letters only
55202
55266
  * "alphanumeric" → letters and digits
55203
55267
  * "uppercase" → uppercase letters only
55204
55268
  * "tel" → phone chars (digits, +, -, parens, space), sets inputMode="tel"
55269
+ * "email" → what an email address is made of
55205
55270
  * "card" → credit card (digits and spaces), sets inputMode="numeric"
55206
55271
  * "hex" → hexadecimal digits
55207
55272
  * "pin" → numeric PIN, sets inputMode="numeric"
@@ -56272,18 +56337,13 @@ const commitSubtree = (controller, e) => {
56272
56337
  };
56273
56338
 
56274
56339
  /**
56275
- *
56276
- * Here we want the same behaviour as web standards:
56277
- *
56278
- * 1. When submitting the form URL does not change
56279
- * 2. When form submission id done user is redirected (by default the current one)
56280
- * (we can configure this using target)
56281
- * So for example user might be reidrect to a page with the resource he just created
56282
- * I could create an example where we would put a link on the page to let user see what he created
56283
- * but by default user stays on the form allowing to create multiple resources at once
56284
- * And an other where he is redirected to the resource he created
56285
- * 3. If form submission fails ideally we should display this somewhere on the UI
56286
- * right now it's just logged to the console I need to see how we can achieve this
56340
+ * <Form>: the <form> element as a navi control group. Its named fields
56341
+ * aggregate into one object; a submit checks their constraints, runs `action`
56342
+ * with that object when it changed (docs/form_changed.md), and `command` — or
56343
+ * the surface the form sits in — says what follows. The URL does not change on
56344
+ * a submit, and a failing action is drawn as an error callout on the button
56345
+ * that sent it (docs/error_handling.md). A form inside another form renders no
56346
+ * <form> element (FormNested).
56287
56347
  */
56288
56348
 
56289
56349
  const Form = props => {
@@ -56923,9 +56983,9 @@ const offsetToPx = (offset, box) => {
56923
56983
 
56924
56984
  // A press landing while the track is already travelling: what is playing is
56925
56985
  // sent home in a fifth of the time it has left, and the press it could not take
56926
- // yet is taken as soon as it lands. A press has to be FELT — nudging the pace
56927
- // of a travel already in flight (what this used to do) reads as "nothing
56928
- // happened", because the thing was moving before the click too. Getting there
56986
+ // yet is taken as soon as it lands. A press has to be FELT — merely nudging the
56987
+ // pace of a travel already in flight reads as "nothing happened", because the
56988
+ // thing was moving before the click too. Getting there
56929
56989
  // almost at once and setting off again is the click being answered.
56930
56990
  // Played out fast rather than cut short: ending it on the spot would jump.
56931
56991
  // Compounds, so two presses during one travel bring it home twice as sharply,
@@ -59523,13 +59583,12 @@ const SlideNavButton = ({
59523
59583
  autoFocus: "last-resort",
59524
59584
  icon: true,
59525
59585
  variant: "discrete"
59526
- // Takes the focus like any other button, on purpose. It used to refuse it
59586
+ // Takes the focus like any other button, on purpose. Do not refuse it
59527
59587
  // (mousedown.preventDefault) to keep the keyboard where the user had put
59528
- // it — but pressing a way out with nothing focused then left the keyboard
59529
- // on nothing at all: the travel below only hands the focus to the slide
59530
- // arriving when it was leaving a slide, so a click from document.body
59531
- // arrived on document.body, and the next Tab started from the top of the
59532
- // page rather than from what is on screen.
59588
+ // it: the travel below only hands the focus to the slide arriving when it
59589
+ // was leaving a slide, so a press with nothing focused would arrive on
59590
+ // document.body, and the next Tab would start from the top of the page
59591
+ // rather than from what is on screen.
59533
59592
  //
59534
59593
  // Letting it focus is both the plain behaviour of a button and what makes
59535
59594
  // the rest fall into place: the press lands on the chevron, the slide left
@@ -59546,12 +59605,6 @@ const SlideNavButton = ({
59546
59605
  })
59547
59606
  });
59548
59607
 
59549
- /**
59550
- * The way out of a slide, and the way into the next one. Nothing but the
59551
- * command plus the chevron that matches the travel: a row goes left/right, a
59552
- * column up/down — so the button points where the slide actually goes without
59553
- * the caller having to keep the two in sync.
59554
- */
59555
59608
  /**
59556
59609
  * A way out pointing where it goes. It says a direction, not a slide: what is
59557
59610
  * over there is the map's business, and moving a screen changes nothing here.
@@ -62663,7 +62716,7 @@ const css$E = /* css */`
62663
62716
  * within its container (the viewport for `layer="top"`, the positioned
62664
62717
  * ancestor for `layer="local"`) — Dialog is never anchored to a real
62665
62718
  * element for positioning purposes. Same grammar as `Popover`'s own
62666
- * `positionArea` (see `popup_shared.js`'s `parsePositionArea`): a single
62719
+ * `positionArea` (see `parsePositionArea` in @jsenv/dom): a single
62667
62720
  * compass token — `top`/`top-start`/`top-end`/`top-left`/`top-right`,
62668
62721
  * `right`/`right-start`/`right-end`, `bottom`/`bottom-start`/
62669
62722
  * `bottom-end`/`bottom-left`/`bottom-right`, `left`/`left-start`/
@@ -62682,12 +62735,12 @@ const css$E = /* css */`
62682
62735
  * between the dialog and the edges of its container, whatever its
62683
62736
  * `positionArea`: it both caps the dialog's own size (via
62684
62737
  * `--x-dialog-container-spacing`, written from this prop) and offsets a docked
62685
- * one from the edge it docks to. Accepts a spacing token ("s", "m"…), a
62686
- * number of pixels, or a viewport length — "appw"/"apph" being the app's own
62687
- * screen (the visual viewport, or the narrower one the app declared with
62688
- * --navi-app-max-width) and "vvw"/"vvh" the visual viewport itself, which
62689
- * shrinks when the mobile keyboard opens. Pass 0 for a dialog
62690
- * meant to sit flush (a side panel).
62738
+ * one from the edge it docks to. Accepts a number of pixels, a viewport
62739
+ * length — "appw"/"apph" being the app's own screen (the visual viewport, or
62740
+ * the narrower one the app declared with --navi-app-max-width) and
62741
+ * "vvw"/"vvh" the visual viewport itself, which shrinks when the mobile
62742
+ * keyboard opens — or a container length ("3cqw", the `layer="local"`
62743
+ * default). Pass 0 for a dialog meant to sit flush (a side panel).
62691
62744
  * @param {boolean} [props.backdrop=true] - Whether anything is laid between
62692
62745
  * the dialog and the page at all — asked before any question of what an
62693
62746
  * outside press does or how the backdrop is painted. `false` leaves the page
@@ -62700,10 +62753,12 @@ const css$E = /* css */`
62700
62753
  * needs. What it gives up is what only a modal gets natively: the hardware
62701
62754
  * back button no longer dismisses it. See docs/popup_backdrop.md.
62702
62755
  * @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="close"]
62703
- * - `"close"` closes the dialog on an outside press. `"capture"`/`"ignore"`
62704
- * both just absorb the press without closing (visually dimmed backdrop vs.
62705
- * not) — a dialog with a backdrop is modal one way or another, so there's
62706
- * always at least a press-absorbing backdrop regardless of this prop.
62756
+ * - `"close"` closes the dialog on an outside press, `"cancel"` closes it
62757
+ * as a cancel (what it holds reverts, as on Escape). `"capture"`/`"ignore"`
62758
+ * both just absorb the press without closing (`"capture"` paints the
62759
+ * blurred glass, `"ignore"` leaves the wall's paint alone) — a wall absorbs
62760
+ * the press whatever this prop says, natively for a modal and through the
62761
+ * sibling element for a local one.
62707
62762
  * "Outside" is the dialog's own border box; a see-through dialog whose box is
62708
62763
  * bigger than what it paints marks the difference with
62709
62764
  * `data-navi-popup-outside` (see docs/popup_backdrop.md).
@@ -62778,11 +62833,10 @@ const css$E = /* css */`
62778
62833
  * @param {string} [props.animationDuration] - Maps to
62779
62834
  * `--popup-animation-duration`.
62780
62835
  * @param {Element|{current: Element}|string} [props.anchor] - Never used for
62781
- * positioning (see this file's top comment), and ignored entirely unless
62782
- * `sizeFromAnchor` or `animation="lifting"` asks for it — the first sizes
62783
- * the dialog via the `--anchor-width`/`--anchor-height` CSS vars, the second
62784
- * makes the dialog come out of the anchor's own box. Used when the open
62785
- * itself names none — an anchor carried by the opening event
62836
+ * positioning (see this file's top comment); read by `sizeFromAnchor` (the
62837
+ * `--anchor-width`/`--anchor-height` CSS vars), by `animation="lifting"`
62838
+ * (the box the dialog comes out of) and by `mount`'s warm-on-intent. Used
62839
+ * when the open itself names none — an anchor carried by the opening event
62786
62840
  * (`e.detail.anchor`) is about that one opening and comes first; left out,
62787
62841
  * whatever triggered the open (`e.detail.source`). A string is resolved via
62788
62842
  * `document.getElementById` when the dialog opens — see popover.jsx's own
@@ -62844,6 +62898,9 @@ const css$E = /* css */`
62844
62898
  * focusable of its own.
62845
62899
  * - `"restore"` — the dialog stays out of the opening focus chain unless it
62846
62900
  * held focus when it closed.
62901
+ * - `false` — the dialog element itself is never where the focus lands; the
62902
+ * opening still hands the focus to what the dialog holds. Unlike
62903
+ * `Popover`'s `false`, it does not leave the keyboard where it was.
62847
62904
  * Wherever the keyboard is a virtual one (a touch device), the surface is
62848
62905
  * already what one arrives on: a popup is read before it is reached there, so
62849
62906
  * the focus only leaves it for something that asked by name (`autoFocus` on
@@ -63089,8 +63146,8 @@ const useDialogProps = props => {
63089
63146
  // `backdrop` — see this file's top comment.
63090
63147
  layer = "top",
63091
63148
  dockedOnSmallTouchScreen,
63092
- // Same grammar as Popover's own positionArea — see this file's top
63093
- // comment and popup_shared.js's parsePositionArea.
63149
+ // Same grammar as Popover's own positionArea — see parsePositionArea in
63150
+ // @jsenv/dom.
63094
63151
  positionArea: positionAreaProp,
63095
63152
  // A dialog docked against an edge must keep the same gap its own size cap
63096
63153
  // already guarantees a centered one — so this drives both (see
@@ -63130,8 +63187,8 @@ const useDialogProps = props => {
63130
63187
  sizing = "auto",
63131
63188
  animation,
63132
63189
  lift = "box",
63133
- // Inert unless sizeFromAnchor below (see this file's top comment) —
63134
- // Dialog's own positioning is never relative to it.
63190
+ // Never positioned against (see this file's top comment): read by
63191
+ // sizeFromAnchor, by a lift, and by the warm-on-intent of `mount`.
63135
63192
  anchor,
63136
63193
  // Where a lift comes back to, when that is no longer the box it came out
63137
63194
  // of. Read at the close, not kept from the opening — see
@@ -63141,8 +63198,8 @@ const useDialogProps = props => {
63141
63198
  // See this prop's own JSDoc above for why a dialog does not follow its
63142
63199
  // trigger's box by default.
63143
63200
  sizeFromAnchor = false,
63144
- // Same meaning as Popover's own prop, applied to the only thing an anchor
63145
- // can do here: sizing under sizeFromAnchor.
63201
+ // Same meaning as Popover's own prop, applied to what an anchor does here:
63202
+ // sizing under sizeFromAnchor, and a lift.
63146
63203
  anchorCustomEventDetail = "override",
63147
63204
  // Makes the dialog itself a valid focus target so
63148
63205
  // autoFocus="last-resort" below has somewhere to land when it contains
@@ -63201,8 +63258,8 @@ const useDialogProps = props => {
63201
63258
  // narrower one) — and the positioned ancestor for a local one, where
63202
63259
  // reading 3% of the screen gives an absurd gap inside a small box.
63203
63260
  isTopLayer ? "3appw" : "3cqw");
63204
- // "expand || expandX", the shorthand semantics Popup used to apply before
63205
- // handing them over — the docked default only applies when neither was said
63261
+ // `expand` is the shorthand for both axes; the docked default only applies
63262
+ // when neither was said.
63206
63263
  const expandXUnset = expand === undefined && expandXProp === undefined;
63207
63264
  const expandX = expandXUnset ? isDocked && DOCKED.expandX : Boolean(expand) || Boolean(expandXProp);
63208
63265
  const scrollCapture = scrollCaptureProp ?? (isDocked ? DOCKED.scrollCapture : false);
@@ -63941,8 +63998,8 @@ const useDialogProps = props => {
63941
63998
  // ("[navi-control='dialog'], [navi-control='popover']" — see openEffect
63942
63999
  // above), and --navi-open/--navi-close resolve their target this way.
63943
64000
  "navi-control": "dialog",
63944
- // The protocol every command target answers. It came with the control
63945
- // group before; a dialog is layout and still has to answer --navi-open,
64001
+ // The protocol every command target answers: a dialog is layout, not a
64002
+ // control, and still has to answer --navi-open,
63946
64003
  // --navi-close and --navi-toggle, which are dispatched here and do not
63947
64004
  // bubble.
63948
64005
  "onnavi_command": e => {
@@ -63964,14 +64021,12 @@ const useDialogProps = props => {
63964
64021
  "popover": isTopLayerPopover ? "manual" : undefined,
63965
64022
  "baseClassName": "navi_dialog",
63966
64023
  "pseudoClasses": DIALOG_PSEUDO_CLASSES,
63967
- // Distinguishes the two renderers for the CSS above (position: fixed
63968
- // vs. absolute) — positioning itself is entirely JS-driven now (see
63969
- // openEffect's own positionDialog above), no data-position-area
63970
- // attribute needed at all.
63971
64024
  // A popup scrolls, and asking Box for that overflow is also what lets what
63972
64025
  // it contains claim header/footer/body (see box.jsx) — a popup is always a
63973
64026
  // scrolling area, so it says so once, here.
63974
64027
  "overflow": "auto",
64028
+ // Tells the two renderers apart for the CSS above (position: fixed vs.
64029
+ // absolute); the placement itself is JS-driven (positionDialog).
63975
64030
  "data-layer": layer,
63976
64031
  // The sheet shape is live in CSS, not just a set of resolved defaults:
63977
64032
  // it is what withdraws the caller's --dialog-max-width (see the stylesheet
@@ -64475,8 +64530,8 @@ const css$D = /* css */`
64475
64530
  * whole viewport. A real `anchor` works with either.
64476
64531
  * @param {string} [props.positionArea="bottom"] - Where to place the popover
64477
64532
  * relative to its `anchor` (or its container, if there is none). Same
64478
- * grammar as `Dialog`'s own `positionArea` (see `popup_shared.js`'s
64479
- * `parsePositionArea`): a single compass token — `top`/`top-start`/
64533
+ * grammar as `Dialog`'s own `positionArea` (see `parsePositionArea` in
64534
+ * @jsenv/dom): a single compass token — `top`/`top-start`/
64480
64535
  * `top-end`/`top-left`/`top-right`, `right`/`right-start`/`right-end`,
64481
64536
  * `bottom`/`bottom-start`/`bottom-end`/`bottom-left`/`bottom-right`,
64482
64537
  * `left`/`left-start`/`left-end`, or `center`. A bare token means no
@@ -64505,8 +64560,9 @@ const css$D = /* css */`
64505
64560
  * See docs/popup_backdrop.md.
64506
64561
  * @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="ignore"]
64507
64562
  * - `"ignore"` (default): no backdrop at all, outside presses pass straight
64508
- * through. `"close"` closes the popover on an outside press. `"capture"`
64509
- * absorbs the press (dims the backdrop) without closing. Note this
64563
+ * through. `"close"` closes the popover on an outside press, `"cancel"`
64564
+ * closes it as a cancel (what it holds reverts, as on Escape). `"capture"`
64565
+ * absorbs the press (blurred glass) without closing. Note this
64510
64566
  * default differs from `Dialog`'s own (`"close"`) — a popover is
64511
64567
  * typically a lightweight, non-modal affordance. "Outside" is the popover's
64512
64568
  * own border box; a see-through popover whose box is bigger than what it
@@ -64540,9 +64596,11 @@ const css$D = /* css */`
64540
64596
  * popover so the page/container behind it can't scroll while it's open.
64541
64597
  * @param {boolean} [props.focusCapture] - Traps Tab navigation inside the
64542
64598
  * popover (see `focus_trap.js`).
64543
- * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`} [props.animation]
64544
- * - `true`/`"auto"` resolves to a concrete `"slide-from-*"` direction
64545
- * based on `positionArea`. Any other explicit value is used as-is.
64599
+ * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`|"expanding"|`expand-${string}`} [props.animation]
64600
+ * - `true`/`"auto"` resolves to `"scaling"` for a real anchor or a centered
64601
+ * `positionArea`, and to a concrete `"slide-from-*"` direction otherwise.
64602
+ * `"expanding"`/`"expand-*"` grow out of a real anchor (see popup_css.js).
64603
+ * Any other explicit value is used as-is.
64546
64604
  * @param {string} [props.animationDuration] - Maps to
64547
64605
  * `--popup-animation-duration`.
64548
64606
  * @param {Element|{current: Element}|string} [props.anchor] - The element the
@@ -64654,7 +64712,7 @@ const css$D = /* css */`
64654
64712
  * @param {object} [props.openController] - Advanced: an externally-owned
64655
64713
  * open controller (see `open_controller.js`) for a caller that wants to
64656
64714
  * drive open/close itself instead of `open`/`defaultOpen`/`onClose` (used
64657
- * by `picker_custom.jsx`/`side_panel.jsx`).
64715
+ * by `picker_custom.jsx`).
64658
64716
  * @param {"always"|"idle"|"from-first-open"|"while-opened"} [props.mount] - When
64659
64717
  * `children` are built and thrown away (see popup_content_mount.js).
64660
64718
  * `"from-first-open"` (the default) builds them on the first open and keeps
@@ -64966,12 +65024,10 @@ const usePopoverProps = props => {
64966
65024
  unfreezeSize(popoverEl);
64967
65025
  }
64968
65026
  }, [sizing]);
64969
- // The custom renderer's own starting-hidden state is a stylesheet default
64970
- // now (&:not([popover]) { display: none } on .navi_popover/
64971
- // .navi_popover_backdrop above) rather than set here imperatively — a
64972
- // plain div has no native default the way [popover]/<dialog> do, so
64973
- // leaving this to a layout effect meant an actual (if narrow) window
64974
- // where the browser could paint it visible before this ever ran.
65027
+ // The custom renderer starts hidden from the stylesheet ([navi-hidden] on
65028
+ // .navi_popover/.navi_popover_backdrop above), never from a layout effect:
65029
+ // a plain div has no native hidden default the way [popover]/<dialog> do,
65030
+ // and an effect leaves a window where the browser paints it visible first.
64975
65031
  // aria-expanded starts "false" the same way, but via a static literal
64976
65032
  // JSX prop on contentProps/backdropProps below instead of a layout
64977
65033
  // effect — see that prop's own comment for why a *constant* value there
@@ -64990,8 +65046,8 @@ const usePopoverProps = props => {
64990
65046
  openController.getElement = () => ref.current;
64991
65047
  openController.openEffect = e => {
64992
65048
  const popoverEl = ref.current;
64993
- // backdropEl is null when pressOutside is "ignore" —
64994
- // the backdrop isn't rendered at all in that case.
65049
+ // backdropEl is null when there is no wall (pressOutside "ignore", or
65050
+ // backdrop={false}): the backdrop isn't rendered at all then.
64995
65051
  const backdropEl = backdropRef.current;
64996
65052
  if (!popoverEl) {
64997
65053
  {
@@ -65052,10 +65108,8 @@ const usePopoverProps = props => {
65052
65108
  // needed here.
65053
65109
  const positionedAncestor = getPositionedParent(popoverEl);
65054
65110
  // Drives the via-attribute renderer's own position: fixed/absolute
65055
- // switch (see this file's top comment) — set here, well before any
65056
- // positioning/measurement runs, so there's no ordering subtlety to get
65057
- // wrong (unlike the CSS this replaced, which keyed off the resolved
65058
- // animation instead, known too late relative to the first measurement).
65111
+ // switch (see this file's top comment) — set here, before any
65112
+ // positioning/measurement runs, so the first measurement already sees it.
65059
65113
  // True only for a real anchor that itself scrolls with the document —
65060
65114
  // that's the one case `absolute` (scrolling in lockstep with it) is
65061
65115
  // correct; not just "has an anchor at all", since an anchor that's
@@ -65105,10 +65159,9 @@ const usePopoverProps = props => {
65105
65159
  disarmBackdropHideRef.current?.();
65106
65160
  disarmBackdropHideRef.current = null;
65107
65161
  // transitionProperty stays "none" here for both — reset later, in the
65108
- // final commit step alongside popoverEl's own (not resumed early the
65109
- // way it briefly was), so a `silent` open (see above) can keep both
65110
- // elements' transitions suppressed right up until after their
65111
- // aria-expanded flip, the same way it does for popoverEl.
65162
+ // final commit step alongside popoverEl's own, so a `silent` open (see
65163
+ // above) can keep both elements' transitions suppressed right up until
65164
+ // after their aria-expanded flip, the same way it does for popoverEl.
65112
65165
  backdropEl.style.transitionProperty = "none";
65113
65166
  if (isTopLayer) {
65114
65167
  // Hidden first if a previous close's deferred hidePopover() (see
@@ -65534,16 +65587,10 @@ const usePopoverProps = props => {
65534
65587
  // current unconditionally on every call, and treats their mere
65535
65588
  // *presence* as "already positioned this session — stay on this
65536
65589
  // side unless it no longer fits" (its own anti-oscillation guard for
65537
- // repositioning mid-open, e.g. on scroll/resize). Popover's own CSS
65538
- // doesn't read these anymore (slide direction is resolved in JS now,
65539
- // see resolveDirectionValue), which is why the clearing that used to
65540
- // live here was removed — but that removal missed this second,
65541
- // still-live purpose: without clearing them on close, a later reopen
65542
- // sees a stale, "already positioned" value left over from a
65543
- // *previous* open (e.g. the container-aligned collapse from an
65544
- // anchorless silent open) and wrongly stays sticky to it instead of
65545
- // resolving fresh from the current positionArea/anchor — a real,
65546
- // reproduced bug, not just a leftover no-op.
65590
+ // repositioning mid-open, e.g. on scroll/resize). Cleared on close so a
65591
+ // later reopen resolves fresh from the current positionArea/anchor
65592
+ // instead of staying sticky to a side left over from a previous open
65593
+ // (e.g. the container-aligned collapse from an anchorless silent open).
65547
65594
  popoverEl.removeAttribute("data-position-y-current");
65548
65595
  popoverEl.removeAttribute("data-position-x-current");
65549
65596
  cleanup();
@@ -65673,8 +65720,8 @@ const usePopoverProps = props => {
65673
65720
  // ("[navi-control='dialog'], [navi-control='popover']"), and
65674
65721
  // --navi-open/--navi-close resolve their target this way.
65675
65722
  "navi-control": "popover",
65676
- // The protocol every command target answers. It came with the control
65677
- // group before; a popover is layout and still has to answer --navi-open,
65723
+ // The protocol every command target answers: a popover is layout, not a
65724
+ // control, and still has to answer --navi-open,
65678
65725
  // --navi-close and --navi-toggle, which are dispatched here and do not
65679
65726
  // bubble.
65680
65727
  "onnavi_command": e => {
@@ -65714,7 +65761,7 @@ const usePopoverProps = props => {
65714
65761
  // already arrive here as plain props via ...rest (wired by
65715
65762
  // UncontrolledPopover above, forwarded through ControlledPopover's own
65716
65763
  // {...props} spread) — nothing extra to add here. A controlled caller
65717
- // (picker_custom.jsx/side_panel.jsx) wires its own equivalent handling
65764
+ // (picker_custom.jsx) wires its own equivalent handling
65718
65765
  // directly against its own openController instead.
65719
65766
  });
65720
65767
  return [hasBackdrop ? backdropProps : null, contentProps];
@@ -65746,9 +65793,6 @@ const POPUP_STYLE_CSS_VARS = {
65746
65793
  maxHeight: "--popover-max-height"
65747
65794
  };
65748
65795
 
65749
- // parsePositionArea/POSITION_AREA_X/Y_VALUES moved to popup_shared.js — same
65750
- // grammar Dialog's own layer="local"/"top" now shares.
65751
-
65752
65796
  /**
65753
65797
  * Shared by both renderers: parses `positionArea` and resolves
65754
65798
  * `animation="auto"`/`true`. `animationAnchor` is the real anchor element
@@ -65776,15 +65820,6 @@ const resolvePositionAreaAndAnimationKind = ({
65776
65820
  resolvedAnimationKind
65777
65821
  };
65778
65822
  };
65779
-
65780
- // resolveDirectionValue/resolveAutoAnimationKind moved to popup_shared.js —
65781
- // same logic Dialog's own auto-animation resolution now shares, since it
65782
- // never has a real anchor either (always the "anchor === undefined" path).
65783
-
65784
- // suppressPointerEventsDuringTransition/armPointerDownOutsideClose moved to
65785
- // popup_shared.js — same helpers Dialog's own custom renderer needs, no
65786
- // Popover-specific knowledge in either.
65787
-
65788
65823
  Popover.Close = PopupClose;
65789
65824
 
65790
65825
  installImportMetaCssBuild(import.meta);
@@ -65823,11 +65858,12 @@ const css$C = /* css */`@layer navi {
65823
65858
  * @param {"top"|"local"} [props.layer] - Forwarded as-is to whichever of
65824
65859
  * `Dialog`/`Popover` renders — see either component's own doc.
65825
65860
  * @param {Element|{current: Element}} [props.anchor] - Forwarded as-is —
65826
- * positioning for `Popover`, and for `Dialog` sizing only, and only when
65827
- * `sizeFromAnchor` is also passed (see each component's own doc).
65861
+ * positioning for `Popover`; for `Dialog`, the box it sizes from under
65862
+ * `sizeFromAnchor` and lifts out of under `animation="lifting"`. In both,
65863
+ * intent on the anchor warms the content (see `mount`).
65828
65864
  * @param {"override"|"ignore"} [props.anchorCustomEventDetail] - Forwarded
65829
65865
  * as-is to both — what it governs differs (positioning for `Popover`,
65830
- * `sizeFromAnchor` sizing for `Dialog`), but "ignore whatever anchor the
65866
+ * sizing and lifting for `Dialog`), but "ignore whatever anchor the
65831
65867
  * triggering event carried" has to mean the same thing in either mode.
65832
65868
  * @param {string} [props.marginWithAnchor] - **Popover-only**, destructured
65833
65869
  * out so it can't leak onto the real `<dialog>` element as a stray DOM
@@ -65857,8 +65893,9 @@ const css$C = /* css */`@layer navi {
65857
65893
  * the screen-size check happens to pick, which defeats the point of
65858
65894
  * having one shared API in the first place. Note this only says what a
65859
65895
  * press outside *does*; whether it reaches the page at all is `backdrop`
65860
- * below, and `"ignore"`/`"capture"` describe a wall either way — the popup
65861
- * absorbs the press without closing, dimmed or not.
65896
+ * below. `"capture"` is a wall that absorbs the press without closing, in
65897
+ * either mode. `"ignore"` is not the same in both: a `Dialog` keeps its
65898
+ * wall, a `Popover` lays none and the press reaches the page.
65862
65899
  * @param {boolean} [props.backdrop] - Whether anything is laid between the
65863
65900
  * popup and the page at all: `false` lets a press outside both dismiss the
65864
65901
  * popup and reach whatever it landed on, in one gesture. Forwarded as-is and
@@ -65898,11 +65935,15 @@ const css$C = /* css */`@layer navi {
65898
65935
  * `--x-popover-max-width`) instead of its content width — same meaning
65899
65936
  * whichever mode the screen-size resolution picks.
65900
65937
  * @param {boolean} [props.expandY] - Same, vertically (`data-expand-y`).
65901
- * @param {boolean} [props.scrollCapture] - Forwarded as-is.
65938
+ * @param {boolean|"dialog"|"popover"} [props.scrollCapture] - Forwarded to
65939
+ * both; a mode name traps scroll only when that mode is the one rendered.
65902
65940
  * @param {boolean} [props.open] - Forwarded as-is (controlled).
65903
- * @param {import("@preact/signals").Signal<boolean>} [props.signal] -
65941
+ * @param {import("@preact/signals").Signal<any>} [props.signal] -
65904
65942
  * Forwarded as-is: one binding to both drive the popup's open state and
65905
65943
  * know where it is (see `Dialog`/`Popover`'s own `signal`).
65944
+ * @param {any} [props.value] - Forwarded as-is: what `signal` holds while
65945
+ * THIS popup is open, for several sharing one signal (see
65946
+ * `Dialog`/`Popover`'s own `value`).
65906
65947
  * @param {boolean} [props.defaultOpen] - Forwarded as-is (uncontrolled,
65907
65948
  * mount-only).
65908
65949
  * @param {(event: Event) => void} [props.onClose] - Forwarded as-is.
@@ -65917,6 +65958,8 @@ const css$C = /* css */`@layer navi {
65917
65958
  * them afterwards. `"always"` builds them right away, for content something
65918
65959
  * depends on while the popup is still closed: a value read off it, fields a
65919
65960
  * surrounding form collects on submit, a size measured from outside.
65961
+ * `"idle"` builds them in a browser idle moment after load — `"always"`
65962
+ * minus the cost on the critical render.
65920
65963
  * `"while-opened"` throws them away once the popup has finished closing, for
65921
65964
  * content whose fresh state is its initial state: an uncontrolled field
65922
65965
  * seeded from a `defaultValue` that changed while the popup was closed.
@@ -66515,7 +66558,7 @@ const PickerCustom = props => {
66515
66558
  // Not on pickerProps (the trigger): commands.js's own
66516
66559
  // resolveClosestExpandable() does `el.closest("[aria-expanded]")` to
66517
66560
  // find where to dispatch navi_request_open/navi_request_close — and
66518
- // the popup itself now carries its own aria-expanded (see
66561
+ // the popup itself carries its own aria-expanded (see
66519
66562
  // popover.jsx/dialog.jsx), which is *closer* than the picker's own
66520
66563
  // aria-expanded for anything dispatched from inside the popup's own
66521
66564
  // content (e.g. a `command="--navi-close"` button rendered as
@@ -66795,12 +66838,8 @@ const PickerContentInsidePopup = props => {
66795
66838
  mode,
66796
66839
  pointerLock,
66797
66840
  scrollCapture,
66798
- // No default here (matches Popover's own default of inactive) — the
66799
- // old, differently-named `focusTrap = true` prop never actually reached
66800
- // Popover's real `focusCapture` prop (see this file's history), so
66801
- // focus-trapping has never really been active for popover-mode pickers;
66802
- // defaulting the now-correctly-named prop to `true` would be a real,
66803
- // unintended behavior change riding along with the rename.
66841
+ // No default here: Popover's own `focusCapture` is inactive by default,
66842
+ // and a popover-mode picker traps the focus only when asked to.
66804
66843
  focusCapture,
66805
66844
  // Popup documents its own `layer` as forwarded as-is to Dialog/Popover,
66806
66845
  // but popupProps is built explicitly here, so it only travels if named.
@@ -67437,6 +67476,8 @@ document.addEventListener(
67437
67476
  { capture: true, passive: true },
67438
67477
  );
67439
67478
 
67479
+ const FRAME_TIMEOUT_MS = 100;
67480
+
67440
67481
  /**
67441
67482
  * Calls back once the browser has painted what is committed now.
67442
67483
  *
@@ -67448,10 +67489,9 @@ document.addEventListener(
67448
67489
  * has to survive. A frame not coming at all (a background tab) still answers,
67449
67490
  * late, through the timeout.
67450
67491
  *
67492
+ * @param {() => void} callback
67451
67493
  * @returns {() => void} cancel
67452
67494
  */
67453
- const FRAME_TIMEOUT_MS = 100;
67454
-
67455
67495
  const afterPaint = (callback) => {
67456
67496
  let called = false;
67457
67497
  let timeoutId;
@@ -68998,7 +69038,7 @@ const SELECTABLE_INPUT_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-
68998
69038
  const SelectableInputProxy = props => {
68999
69039
  const selectableRealInputProps = useContext(SelectableRealInputContext);
69000
69040
  if (!selectableRealInputProps) {
69001
- throw new Error("Selectable.Input must be used within a Selectable component");
69041
+ throw new Error("SelectableInput must be used within a row of a selectable List");
69002
69042
  }
69003
69043
 
69004
69044
  // Reset FieldToInterfaceContext to ensure we don't read id or report our
@@ -69036,7 +69076,7 @@ const SelectableInput = SelectableInputProxy;
69036
69076
  * background-color: var(--search-match-background-color);
69037
69077
  * }
69038
69078
  *
69039
- * The `highlight` prop can be:
69079
+ * The `highlight` argument of `useSearchHighlight` (a row's `matchInfo.matchRanges`) can be:
69040
69080
  * - an array of [start, end] pairs — applied to all text nodes under the root element
69041
69081
  * - an object { [domSelector]: [[start, end], …] } — applied to each sub-element
69042
69082
  * matched by the selector (the format produced by createSearch)
@@ -72584,8 +72624,8 @@ const LIST_ITEM_STYLE_CSS_VARS = {
72584
72624
  *
72585
72625
  * All three are about the row as a thing the LIST holds, never about the
72586
72626
  * selection: a selectable row taken while its list sends says so on its own
72587
- * ("la sélection est en cours d'enregistrement"), and needs no `loading` for
72588
- * that.
72627
+ * ("le choix est en cours d'enregistrement", "la sélection…" in a `multiple`
72628
+ * list), and needs no `loading` for that.
72589
72629
  * @param {boolean} [props.readOnly]
72590
72630
  * The row cannot be acted on: dimmed and click-through-proof, buttons inside
72591
72631
  * it included.
@@ -73680,9 +73720,12 @@ const useItemStore = ({
73680
73720
  * (accessible via aria-labelledby) and a <ul role="group"> for the items.
73681
73721
  *
73682
73722
  * Props:
73683
- * label — group label content
73684
- * labelProps — props forwarded to the label <span>
73685
- * ...rest — forwarded to the outer <li role="presentation">
73723
+ * label — group label content
73724
+ * labelProps — props forwarded to the label <span>
73725
+ * hiddenWhileEmpty — the group leaves the flow (`display: none`) while it
73726
+ * holds no real row — a search that emptied it, rows not
73727
+ * arrived yet
73728
+ * ...rest — forwarded to the outer <li role="presentation">
73686
73729
  */
73687
73730
  const ListItemGroup = ({
73688
73731
  label,
@@ -73794,7 +73837,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
73794
73837
  * uiAction?: (value: any) => void,
73795
73838
  * popover?: boolean,
73796
73839
  * role?: string,
73797
- * renderBudget?: number | string,
73840
+ * renderBudget?: number | string | {initial?: number, after: number},
73798
73841
  * renderBudgetSkipCheck?: boolean,
73799
73842
  * virtualItemSize?: number,
73800
73843
  * onListVisibleItemsChange?: (visibleItems: any[]) => void,
@@ -73823,6 +73866,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
73823
73866
  * alignX?: string,
73824
73867
  * alignY?: string,
73825
73868
  * flexWrap?: boolean,
73869
+ * overflow?: string,
73826
73870
  * expandX?: boolean,
73827
73871
  * expandY?: boolean,
73828
73872
  * expand?: boolean,
@@ -74009,10 +74053,10 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
74009
74053
  * @param {number} [props.parallelGuard=4]
74010
74054
  * How many runs the rows may have in flight at once, for a list whose rows
74011
74055
  * carry their own `action` (a button per row). While that many are out, every
74012
- * row that is not running goes read-only and says how many it is waiting on;
74013
- * the next press is possible again as soon as one comes back. `Infinity`
74014
- * lifts it. Counts runs, not values — `maxLengthGuard` above is the one that
74015
- * says how many things the selection may hold.
74056
+ * control that would start another run goes read-only and says how many it is
74057
+ * waiting on; the next press is possible again as soon as one comes back.
74058
+ * `Infinity` lifts it. Counts runs, not values — `maxLengthGuard` above is
74059
+ * the one that says how many things the selection may hold.
74016
74060
  * @param {boolean} [props.standalone]
74017
74061
  * This list answers for itself: it does not register with the control group
74018
74062
  * or picker around it, so its selection stays out of that value and nothing
@@ -76547,7 +76591,7 @@ const PickerFirstResolver = props => {
76547
76591
  * popupTestId?: string,
76548
76592
  * confirmTestId?: string,
76549
76593
  * cancelTestId?: string,
76550
- * mount?: "always" | "from-first-open" | "while-opened",
76594
+ * mount?: "always" | "idle" | "from-first-open" | "while-opened",
76551
76595
  * dialogExpand?: boolean,
76552
76596
  * dialogExpandX?: boolean,
76553
76597
  * dialogExpandY?: boolean,
@@ -76984,8 +77028,8 @@ const PickerFirstResolver = props => {
76984
77028
  * itself names the trigger (see docs/testid.md); this one is for a test that
76985
77029
  * needs the popup's own box — its surface, its position, its backdrop — and
76986
77030
  * not only what it holds, which a testid inside the children already names.
76987
- * A `mode="callout"` picker has no popup of its own — the callout is the
76988
- * control's, shared with its constraint messages — so it is not named.
77031
+ * Under `mode="callout"` it names the callout element — the control's own,
77032
+ * shared with its constraint messages — while the children are shown in it.
76989
77033
  * @param {string} [confirmTestId] `type="confirm"`, default body: the
76990
77034
  * `data-testid` of the yes button; `cancelTestId` that of the no button.
76991
77035
  * Both are elements navi builds, which a test may not name by class (see
@@ -77373,7 +77417,7 @@ const TRAVELS_NONE = [];
77373
77417
  * fallbackValue?: any,
77374
77418
  * valueAtStep: (value: any, count: number) => any,
77375
77419
  * compareValues?: (a: any, b: any) => number,
77376
- * renderValue?: (value: any) => import("ignore:preact").ComponentChildren,
77420
+ * renderValue?: (value: any, context: { maxLines?: number }) => import("ignore:preact").ComponentChildren,
77377
77421
  * controlProps?: object,
77378
77422
  * duration?: number,
77379
77423
  * vertical?: boolean,
@@ -78848,7 +78892,8 @@ const css$q = /* css */`.navi_input_duration {
78848
78892
  * @param {boolean} [props.required]
78849
78893
  * @param {boolean} [props.loading]
78850
78894
  * @param {Function} [props.uiAction] - Called on every change with the ISO 8601 value
78851
- * @param {Function} [props.action] - Called on form submission
78895
+ * @param {Function} [props.action] - Runs with the ISO 8601 value when it changes,
78896
+ * like any control's `action` (busy state, error callout)
78852
78897
  * @param {preact.ComponentChild} [props.unitHour] - Custom label for the hour sub-field
78853
78898
  * @param {"auto"|"left"|"center"|"right"} [props.textAlign="auto"] - Text alignment of sub-inputs.
78854
78899
  * "auto" aligns each field toward its neighbouring separator (first→right, last→left, middle/solo→center).
@@ -79859,11 +79904,12 @@ const splitPopupProps = props => {
79859
79904
  * multi-word, one word at start (all match) 0.75
79860
79905
  * phrase / word at word boundary 0.625
79861
79906
  * phrase / words mid-word 0.5
79907
+ * acronym (2+ chars, each the first letter of a word, in order) 0.4, 0.45 at start
79862
79908
  * + case-exact bonus +0.125
79863
79909
  * multi-word partial: score × (matched/total)
79864
79910
  *
79865
79911
  * matchRanges: [start, end] pairs (exclusive end) for CSS Highlight API.
79866
- * Intended to be passed to useSearch as the matchFn parameter.
79912
+ * Intended to be passed to useSearchText as the matchFn parameter.
79867
79913
  */
79868
79914
  const applySearch = (searchText, value) => {
79869
79915
  if (!searchText) {
@@ -80073,7 +80119,7 @@ const mergeRanges = (ranges) => {
80073
80119
  };
80074
80120
 
80075
80121
  /**
80076
- * createSearch — builds a matchFn compatible with useSearch that searches
80122
+ * createSearch — builds a matchFn compatible with useSearchText that searches
80077
80123
  * across multiple named fields of an item, each with its own DOM selector
80078
80124
  * and optional priority weight.
80079
80125
  *
@@ -80091,7 +80137,7 @@ const mergeRanges = (ranges) => {
80091
80137
  * },
80092
80138
  * });
80093
80139
  *
80094
- * const [orderedItems, getItemMatchInfo] = useSearch(search, items, searchPerson);
80140
+ * const [orderedItems, getItemMatchInfo] = useSearchText(search, items, searchPerson);
80095
80141
  * // getItemMatchInfo(item).matchRanges is { ".name": [[start,end],…], ".address": [[start,end],…] }
80096
80142
  * // Pass the whole thing: <ListItem matchInfo={getItemMatchInfo(item)} />
80097
80143
  * // — ListItem handles the per-selector object format for matchRanges.
@@ -80158,7 +80204,7 @@ const useSearchText = (searchText, items, matchFn = applySearch) => {
80158
80204
  );
80159
80205
  }
80160
80206
  if (items === undefined) {
80161
- throw new TypeError("useSearch: items is undefined");
80207
+ throw new TypeError("useSearchText: items is undefined");
80162
80208
  }
80163
80209
  const { orderedItems, matchInfoMap } = useMemo(() => {
80164
80210
  const { scoreEntries, nonMatched, matchInfoMap } = buildMatchInfo(
@@ -80283,17 +80329,19 @@ const buildMatchInfo = (searchText, items, matchFn) => {
80283
80329
  * - orderedKeys: number[] of visible item keys sorted by explicit order
80284
80330
  * - keyToOrderedIndex: Map key → orderedKeys index, gives O(1) indexOf equivalent
80285
80331
  * - keyToExplicitOrder: Map key → explicitly passed index, used to maintain sort order
80286
- * - allItemsSignal: signal(array), all items including hidden, ordered by explicit index
80287
- * - visibleItemsSignal: signal(array), non-hidden items only
80332
+ * - itemsSignal: signal(array), all items including hidden, ordered by explicit index
80333
+ * - visibleItemsSignal: signal(array), items neither hidden nor filtered
80288
80334
  * - countSignal: signal(number), count of all items including hidden
80289
80335
  * - visibleCountSignal: signal(number), updated in microtask batch, only when count changes
80290
- * - propSignals: Map propName → signal(array), updated in microtask batch with element equality
80336
+ * - noMatchCountSignal: signal(number), items registered with `match: false`
80291
80337
  * - onChangeRef: holds the latest onChange callback, called once per microtask batch
80292
80338
  *
80293
- * useTrackItem(id, data, index): registers the item with an explicitly provided index
80294
- * that determines its position among siblings. The caller (e.g. items.map) knows the
80295
- * correct order and passes it directly — no render-sequence deduction needed.
80296
- * Returns the visible rank (position among non-hidden items), or -1 when hidden.
80339
+ * useTrackItem(data): registers the item under `data.id`, at the position
80340
+ * `data.index` gives it among its siblings. The caller (e.g. items.map) knows
80341
+ * the correct order and passes it directly — no render-sequence deduction
80342
+ * needed. `data.role === "presentation"` keeps it out entirely.
80343
+ * Returns the visible rank (position among visible items), or -1 when hidden
80344
+ * or filtered.
80297
80345
  * Signals and onChange are deferred to a microtask so multiple items updating
80298
80346
  * in one commit cause only one notification.
80299
80347
  *
@@ -81061,7 +81109,7 @@ const WheelGroupContext = createContext(null);
81061
81109
  * [key: string]: any,
81062
81110
  * }>}
81063
81111
  * @param {number} [props.visibleCount=3] - Odd number of rows visible in the viewport (the center one is the selection).
81064
- * @param {number|string} [props.itemHeight] - Main-axis size of a row when vertical (number = px). Defaults to the CSS var (2.4em).
81112
+ * @param {number|string} [props.itemHeight] - Main-axis size of a row when vertical (number = px). Defaults to the CSS var (1.8em).
81065
81113
  * @param {number|string} [props.itemWidth] - Main-axis size of a cell when horizontal (number = px). Defaults to the CSS var (3.5ch).
81066
81114
  * @param {number|string} [props.size] - Font size of the wheel: a size token ("s", "m", "l", "xl", …), a number (px), or a CSS length. Scales the digits and, since the row size is em-based, the row height too — a simple way to make the whole wheel bigger. An explicit itemHeight/itemWidth still overrides the row size.
81067
81115
  * @param {boolean} [props.bounded] - Give the wheel fixed ends instead of wrapping: it stops at the first/last value. By default the wheel loops endlessly (past the last value the first reappears, and vice-versa).
@@ -82557,7 +82605,7 @@ Wheel.Item = WheelItem;
82557
82605
  * @param {boolean} [props.horizontal] - Stack the (horizontal) wheels vertically instead of in a row.
82558
82606
  * @param {boolean} [props.glass] - Frost every wheel's neighbouring rows (see Wheel's glass prop) with one prop for the whole group.
82559
82607
  * @param {boolean} [props.frameBorder] - Line every wheel's center-window edges with a faint frame (off by default; independent of glass).
82560
- * @param {boolean} [props.zoom] - Enlarge the centered value of every wheel (see Wheel's zoom prop) with one prop for the whole group.
82608
+ * @param {boolean|number} [props.zoom] - Enlarge the centered value of every wheel (see Wheel's zoom prop) with one prop for the whole group.
82561
82609
  */
82562
82610
  const WheelGroup = props => {
82563
82611
  import.meta.css = [css$m, "@jsenv/navi/src/control/wheel/wheel.jsx"];
@@ -83342,8 +83390,8 @@ const stringifyTableSelectionValue = (type, value) => {
83342
83390
 
83343
83391
  /**
83344
83392
  * Check if a specific cell is selected
83345
- * @param {Array} selection - The selection set or array
83346
- * @param {{rowIndex: number, columnIndex: number}} cellPosition - Cell coordinates
83393
+ * @param {string[]} selection - The table's `selection` array
83394
+ * @param {{columnId: string, rowId: string}} cell - The cell's column and row ids
83347
83395
  * @returns {boolean} True if the cell is selected
83348
83396
  */
83349
83397
  const isCellSelected = (selection, { columnId, rowId }) => {
@@ -83356,8 +83404,8 @@ const isCellSelected = (selection, { columnId, rowId }) => {
83356
83404
 
83357
83405
  /**
83358
83406
  * Check if a specific row is selected
83359
- * @param {Array} selection - The selection set or array
83360
- * @param {number} rowIndex - Row index
83407
+ * @param {string[]} selection - The table's `selection` array
83408
+ * @param {string} rowId - The row's id (the `id` of its `<Tr>`)
83361
83409
  * @returns {boolean} True if the row is selected
83362
83410
  */
83363
83411
  const isRowSelected = (selection, rowId) => {
@@ -83367,8 +83415,8 @@ const isRowSelected = (selection, rowId) => {
83367
83415
 
83368
83416
  /**
83369
83417
  * Check if a specific column is selected
83370
- * @param {Array} selection - The selection set or array
83371
- * @param {number} columnIndex - Column index
83418
+ * @param {string[]} selection - The table's `selection` array
83419
+ * @param {string} columnId - The column's id (the `id` of its `<Col>`)
83372
83420
  * @returns {boolean} True if the column is selected
83373
83421
  */
83374
83422
  const isColumnSelected = (selection, columnId) => {
@@ -83903,7 +83951,7 @@ const initDragTableColumnViaPointer = (pointerdownEvent, {
83903
83951
  /**
83904
83952
  * Creates a MutationObserver that syncs attribute changes from original table to clone
83905
83953
  * @param {HTMLElement} table - The original table element
83906
- * @param {HTMLElement} cloneTable - The cloned table element
83954
+ * @param {HTMLElement} tableClone - The cloned table element
83907
83955
  * @returns {MutationObserver} The observer instance with disconnect method
83908
83956
  */
83909
83957
  const createTableAttributeSync = (table, tableClone) => {
@@ -85868,9 +85916,10 @@ const TableUI = /*#__PURE__*/forwardRef((props, ref) => {
85868
85916
  * KEY PRINCIPLES:
85869
85917
  * - Use inset box-shadow to ensure borders appear above table cell backgrounds
85870
85918
  * - Use ::before pseudo-elements with position: absolute for flexible positioning
85871
- * - Each cell draws its own borders independently (no border-collapse by default)
85919
+ * - Each cell draws its own borders; `borderCollapse` (on by default) keeps only
85920
+ * the right and bottom ones, plus top on the first row and left on the first column
85872
85921
  * - Selection borders override table borders using higher CSS specificity
85873
- * - Sticky borders use thicker box-shadows in accent color (yellow)
85922
+ * - Sticky borders use thicker box-shadows in an accent color
85874
85923
  *
85875
85924
  * TECHNICAL IMPLEMENTATION:
85876
85925
  * - All borders use inset box-shadow with specific directional mapping:
@@ -85878,9 +85927,8 @@ const TableUI = /*#__PURE__*/forwardRef((props, ref) => {
85878
85927
  * * Right: inset -1px 0 0 0
85879
85928
  * * Bottom: inset 0 -1px 0 0
85880
85929
  * * Left: inset 1px 0 0 0
85881
- * - Selection borders (blue) override table borders (red) in same pseudo-element
85930
+ * - Selection borders override table borders in the same pseudo-element
85882
85931
  * - Sticky borders replace regular borders with thicker colored variants
85883
- * - Border-collapse mode available as optional feature for future use
85884
85932
  *
85885
85933
  * Note how border disappear for sticky elements when using border-collapse (https://bugzilla.mozilla.org/show_bug.cgi?id=1727594)
85886
85934
  *
@@ -87014,11 +87062,18 @@ const BadgeCountOverflow = () => jsx("span", {
87014
87062
  });
87015
87063
  const MAX_CHAR_AS_CIRCLE = 3;
87016
87064
  const MAX_FOR_CIRCLE = 99;
87065
+
87066
+ /**
87067
+ * @param {boolean} [circle] Drawn round rather than as a pill. A circle holds
87068
+ * three characters at most, so `max` defaults to 99 with it ("99+"); a `max`
87069
+ * above 99, or `"none"`, lets a bigger count turn the circle into a pill as
87070
+ * it grows.
87071
+ * @param {number|"none"} [max] The count above which `max` and a "+" are
87072
+ * written in its place. 99 under `circle`, no cap otherwise.
87073
+ */
87017
87074
  const BadgeCount = ({
87018
87075
  children,
87019
87076
  maxElement = jsx(BadgeCountOverflow, {}),
87020
- // When you use max="none" (or max > 99) it might be a good idea to force ellipse
87021
- // so that visually the interface do not suddently switch from circle to ellipse depending on the count
87022
87077
  circle,
87023
87078
  max = circle ? MAX_FOR_CIRCLE : Infinity,
87024
87079
  integer,
@@ -88201,12 +88256,13 @@ const css$7 = /* css */`.navi_text_box {
88201
88256
  * - Displays optional icon(s) before and/or after the text
88202
88257
  * - Shrinks its width to fit the longest rendered text line (no trailing blank space)
88203
88258
  * - Wraps long text by default (overflow-wrap: anywhere)
88204
- * - Shows ellipsis for a single overflowing unwrappable line
88259
+ * - Shows an ellipsis instead under `singleLine`
88205
88260
  *
88206
88261
  * Props:
88207
88262
  * iconBefore — element shown to the left of the text
88208
88263
  * iconAfter — element shown to the right of the text (stays on the same line)
88209
- * maxHeight — CSS max-height string; when set, content that cannot wrap gets ellipsis
88264
+ * singleLine — keeps the text on one line, cut with an ellipsis (no width shrinking)
88265
+ * maxHeight — CSS max-height string; content past it is clipped
88210
88266
  * children — the text content
88211
88267
  */
88212
88268
  const TextBox = ({
@@ -88385,6 +88441,41 @@ const css$4 = /* css */`.svg_mask_content * {
88385
88441
  stroke-opacity: 1 !important;
88386
88442
  }
88387
88443
  `;
88444
+
88445
+ /**
88446
+ * SVGMaskOverlay
88447
+ *
88448
+ * Creates composite SVGs by combining independent SVG elements with masking.
88449
+ *
88450
+ * This component solves the challenge of combining independently created SVGs into
88451
+ * a single visual composition. Each SVG can have its own coordinate system, viewBox,
88452
+ * and styling, allowing for maximum reusability of individual icons or graphics.
88453
+ *
88454
+ * When overlaying SVGs, each subsequent overlay "cuts out" its portion from the base SVG,
88455
+ * creating a seamless integration where SVGs appear to interact with each other visually.
88456
+ *
88457
+ * Key benefits:
88458
+ * - Maintains each SVG's independence - use them individually elsewhere
88459
+ * - Handles different viewBox dimensions automatically
88460
+ * - Works with any SVG components regardless of internal implementation
88461
+ * - Supports unlimited overlay elements
88462
+ * - Creates proper masking between elements for visual integration
88463
+ *
88464
+ * Usage example combining two independent icon components:
88465
+ * ```jsx
88466
+ * <SVGMaskOverlay viewBox="0 0 24 24">
88467
+ * <DatabaseSvg />
88468
+ * <svg x="12" y="12" width="16" height="16" overflow="visible">
88469
+ * <PlusSvg />
88470
+ * </svg>
88471
+ * </SVGMaskOverlay>
88472
+ * ```
88473
+ *
88474
+ * @param {Object} props - Component properties
88475
+ * @param {string} props.viewBox - The main viewBox for the composition (required as soon as there is an overlay)
88476
+ * @param {import("ignore:preact").ComponentChildren} props.children - SVG elements (first is base, rest are overlays)
88477
+ * @returns {import("ignore:preact").VNode} A composed SVG with all elements properly masked
88478
+ */
88388
88479
  const SVGMaskOverlay = ({
88389
88480
  viewBox,
88390
88481
  children
@@ -88397,7 +88488,7 @@ const SVGMaskOverlay = ({
88397
88488
  return children[0];
88398
88489
  }
88399
88490
  if (!viewBox) {
88400
- console.error("SVGComposition requires an explicit viewBox");
88491
+ console.error("SVGMaskOverlay requires an explicit viewBox");
88401
88492
  return null;
88402
88493
  }
88403
88494
 
@@ -89338,7 +89429,8 @@ const css = /* css */`.navi_side_panel {
89338
89429
  * interaction only makes sense paired with not letting focus silently
89339
89430
  * leave the panel first. A box of the page whose press must not close the
89340
89431
  * panel (a card that fills it) names the panel:
89341
- * `data-navi-popup-inside={id}` — see docs/popup_backdrop.md.
89432
+ * `data-navi-popup-inside={id}`, read when the panel has no wall
89433
+ * (`backdrop={false}`) — see docs/popup_backdrop.md.
89342
89434
  * @param {boolean} [props.closeByDrag=true] - Pushing the panel back
89343
89435
  * towards the edge it is docked to closes it: the panel follows the
89344
89436
  * pointer and finishes leaving (or comes back to rest) when it is
@@ -89353,8 +89445,8 @@ const css = /* css */`.navi_side_panel {
89353
89445
  * doc).
89354
89446
  * @param {import("ignore:preact").ComponentChildren} props.children - No built-in
89355
89447
  * close button — add one wherever it makes sense for the layout (e.g. a
89356
- * plain `<Button command="--navi-close">`), use `SidePanel.Head`'s own
89357
- * `closeButton` prop, or rely on `closeByPressOutside`/Escape instead.
89448
+ * plain `<Button command="--navi-close">` in `SidePanel.Head`), or rely on
89449
+ * `closeByPressOutside`/Escape instead.
89358
89450
  * A form sent inside the panel closes it, as in any popup: a panel one
89359
89451
  * keeps editing in says `command="--navi-void"` on that form (see
89360
89452
  * docs/form_changed.md).
@@ -89445,9 +89537,7 @@ const toCssLength = (value, propertyName) => value === undefined || value === nu
89445
89537
  * Stuck to the top of the panel's own scrollable area (`position: sticky`)
89446
89538
  * regardless of `side` — only the panel's content in between scrolls. No
89447
89539
  * built-in padding or close button — add a `<Button command="--navi-close">`
89448
- * (optionally with the `"navi_side_panel_head_close_button"` className, a
89449
- * float-right utility this file's own CSS still provides) wherever it makes
89450
- * sense for the layout.
89540
+ * wherever it makes sense for the layout.
89451
89541
  *
89452
89542
  * @param {object} props
89453
89543
  * @param {string} [props.className] - Merged with the shared