@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.
@@ -532,7 +532,9 @@ effect(() => {
532
532
  * Built-in key namespaces, all overridable — the registrations below are the
533
533
  * exhaustive list, read them to find the exact key to override:
534
534
  * - `"button.*"` — Clear, Reset, Send, Open, Close, Cancel, Confirm…
535
- * - `"time.*"` — what a clock writes between hours and minutes, and how the two ends of a span are named; the wording the formatters use is registered by @jsenv/humanize
535
+ * - `"confirm.*"` — the question a confirmation asks by default
536
+ * - `"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
537
+ * - `"time_range.*"` — how the two ends of a span are named ("From", "to")
536
538
  * - `"spin.*"` — the ends of a steppable range
537
539
  * - `"list.*"` — empty/no-match/failed-rows messages
538
540
  * - `"badge_list.*"` — the "+[count] more" overflow badge
@@ -1540,11 +1542,10 @@ const errorIsDisplayed = (error) => {
1540
1542
  *
1541
1543
  * Whatever the reader does with it is already covered without any deadline: it
1542
1544
  * displays it (and marks it), or it throws it, and a thrown error either finds a
1543
- * boundary that displays it or reaches window on its own — `preact/debug`
1544
- * re-throws every error a boundary caught, and an unbounded one aborts the
1545
- * render loudly. Reporting it here as well would be a second voice saying the
1546
- * same thing, always the wrong one, since this module cannot see which of those
1547
- * happened.
1545
+ * boundary that displays it or reaches window on its own — preact re-throws
1546
+ * what no boundary handled. Reporting it here as well would be a second voice
1547
+ * saying the same thing, always the wrong one, since this module cannot see
1548
+ * which of those happened.
1548
1549
  */
1549
1550
  const errorTakenByRenderSet = new WeakSet();
1550
1551
  const markErrorAsTakenByRender = (error) => {
@@ -1634,7 +1635,7 @@ const errorIsAccountedFor = (error) => {
1634
1635
  /**
1635
1636
  * Rethrown rather than logged: an error nobody took is an unhandled error, and
1636
1637
  * the runtime already knows what to do with those (window "error" event, jsenv
1637
- * overlay in dev). Same trick preact/debug uses for the same reason.
1638
+ * overlay in dev). Same answer preact gives an error no boundary handled.
1638
1639
  */
1639
1640
  const errorReportedSet = new WeakSet();
1640
1641
  const reportErrorIfNobodyDisplaysIt = (error, { action } = {}) => {
@@ -2155,7 +2156,7 @@ const compareTwoJsValues = (
2155
2156
  }
2156
2157
  return true;
2157
2158
  }
2158
- // Ordered array comparison (original behavior)
2159
+ // Ordered array comparison
2159
2160
  let i = 0;
2160
2161
  while (i < a.length) {
2161
2162
  const aValue = a[i];
@@ -3222,6 +3223,8 @@ const actionWeakMap = new WeakMap();
3222
3223
  * long is this still true?" belongs to the question: a court list is good for
3223
3224
  * the session, a count of people watching grows on its own and is worth a few
3224
3225
  * minutes, a payment status is worth nothing at all.
3226
+ * @param {{ verb?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" }} [rootOptions.meta] -
3227
+ * `verb` puts the action under the network policy (docs/network_policy.md).
3225
3228
  */
3226
3229
  const createAction = (callback, rootOptions = {}) => {
3227
3230
  const existing = actionWeakMap.get(callback);
@@ -4323,7 +4326,7 @@ const runUnwatched = (startRun) => {
4323
4326
  /**
4324
4327
  * Reactively runs an action whenever the params derived from signals change.
4325
4328
  *
4326
- * @param {object} action - The action to run.
4329
+ * @param {object | Function} action - The action to run, or a function made into one.
4327
4330
  * @param {Function} deriveActionParamsFromSignals - A function that reads signals and returns
4328
4331
  * the params to pass to the action. It is re-evaluated automatically whenever a signal it
4329
4332
  * read changes. Return `false`/`null`/`undefined` to skip running the action.
@@ -4346,6 +4349,8 @@ const runUnwatched = (startRun) => {
4346
4349
  * ```
4347
4350
  * The action will not fire while the user is actively changing filters; it fires once
4348
4351
  * they pause for half a second.
4352
+ * @returns {object} the instance bound to the derived params: the one to read
4353
+ * (`useAsyncData`), since the answer lands there and never on `action` itself.
4349
4354
  */
4350
4355
  // The run is not awaited here, and a rejection nobody waits for is an unhandled
4351
4356
  // one — in dev, an error overlay thrown over a page that is already saying what
@@ -4860,11 +4865,12 @@ const isNumberInput = (type, naviInputType) => {
4860
4865
  */
4861
4866
  /**
4862
4867
  * A bound signal that carries a default of its own says the same thing on every
4863
- * control: the control starts there and stays uncontrolled, which is what makes
4864
- * a form read the value shown as a SUGGESTION rather than as something it
4865
- * already holds. Uncontrolled here is about what the control HOLDS, not about
4866
- * whether it follows the signal — the binding stays two-way either way (see
4867
- * stateFromSignal in control_hooks.jsx). Written once and used by everything
4868
+ * control: the control starts there, uncontrolled, and a reset goes back to it.
4869
+ * Uncontrolled here is about where the control starts, not about whether it
4870
+ * follows the signal — the binding stays two-way either way (see
4871
+ * stateFromSignal in control_hooks.jsx) — nor about what a form counts as
4872
+ * held: that is the signal's content (isUIStateHeld in held_ui_state.js), and a
4873
+ * stateSignal holds its default. Written once and used by everything
4868
4874
  * that takes a `signal`, so one signal cannot mean two different things
4869
4875
  * depending on which control it was handed to.
4870
4876
  */
@@ -4924,8 +4930,7 @@ const resolveInputProps = (props, { controlType = "input" } = {}) => {
4924
4930
  // If no explicit defaultChecked, derive it from the signal's default
4925
4931
  // value so that resetUIState restores to the original default.
4926
4932
  // Only a stateSignal carries a default of its own; a plain signal has
4927
- // no `options` at all, and asking it for one used to throw on mount —
4928
- // the same optional read every other branch here already does.
4933
+ // no `options` at all, hence the optional read.
4929
4934
  const defaultVal = signalOptions?.getDefaultValue(false);
4930
4935
  if (defaultVal === undefined) ; else if (props.type === "radio") {
4931
4936
  if (defaultVal === true) {
@@ -8835,8 +8840,8 @@ const isLanguageSupported = (lang, supportedLanguages) => {
8835
8840
  * supported), falls back to `supportedLanguagesSignal` itself so callers
8836
8841
  * still get *something* usable rather than an empty array.
8837
8842
  *
8838
- * Consumers that accept either a single lang or an ordered array (this
8839
- * package's own `matchBestLang`/`createI18n`, and native `Intl.NumberFormat`/
8843
+ * Consumers that accept either a single lang or an ordered array
8844
+ * (@jsenv/humanize's `createI18n` and formatters, and native `Intl.NumberFormat`/
8840
8845
  * `Intl.DateTimeFormat`) can pass this straight through: anything not
8841
8846
  * covered by the first entry falls through to the next, rather than
8842
8847
  * jumping straight to an unrelated default like "en".
@@ -9819,15 +9824,15 @@ const readOnlyMessage = (field) => {
9819
9824
  * - Action dispatch + validity: `control_action.js` / `dispatchRequestAction`
9820
9825
  * - Validity checking: `control_validation.js`
9821
9826
  *
9822
- * Each UI state controller gets its own `controlInteraction` instance (created by
9823
- * `createControlInteraction`) just like it gets a `controlValidity` instance.
9827
+ * Each UI state controller gets its own instance, `controller.rules.interaction`
9828
+ * (created by `createControlInteraction`), beside `controller.rules.validation`.
9824
9829
  *
9825
9830
  * Flow:
9826
9831
  * user interaction
9827
9832
  * → dispatchRequestInteraction
9828
9833
  * → "navi_request_interaction" event
9829
9834
  * → onRequestInteraction
9830
- * → check disabled / read-only / busy (via controller.controlInteraction)
9835
+ * → check disabled / read-only / busy (via controller.rules.interaction)
9831
9836
  * against the interaction's `intent` ("write" by default, "read" for one
9832
9837
  * that only shows what is already there — see READONLY_CONSTRAINT)
9833
9838
  * → if blocked → prevented()
@@ -10647,7 +10652,8 @@ const interactionsDisputeThePress = (interactions) => {
10647
10652
  * control (a Button) is its own; a Box inside one, or wrapping exactly one,
10648
10653
  * reaches it; a Box with no control anywhere near it — or laying out several,
10649
10654
  * which belongs to none of them — still answers with a callback of the caller's,
10650
- * and only "request_action" has nothing to ask.
10655
+ * and only the two requests ("request_action", "request_ui_action") have
10656
+ * nothing to ask.
10651
10657
  *
10652
10658
  * Set up once per element rather than on every render, which is what lets a
10653
10659
  * detector be a plain `setup`/teardown pair. So the interactions themselves are
@@ -10806,7 +10812,7 @@ const useInteractionsEffect = (ref, interactionsRef) => {
10806
10812
  cancelable: true,
10807
10813
  });
10808
10814
  // The event the interaction was read from stays reachable from it:
10809
- // `findEvent(actionEvent, "pointerdown")` still finds the press a swipe
10815
+ // `findEvent(actionEvent, "pointerdown")` still finds the press a hold
10810
10816
  // was made of.
10811
10817
  chainEvent(interactionEvent, originalEvent);
10812
10818
  if (!element.dispatchEvent(interactionEvent)) {
@@ -11554,7 +11560,7 @@ const swipeTypeOf = (axis, pulled) => {
11554
11560
  * …
11555
11561
  * }}}
11556
11562
  *
11557
- * `toId` is an element and never null: a copy over nothing is a release that meant
11563
+ * `toId` names an element and is never null: a copy over nothing is a release that meant
11558
11564
  * nothing, and the interaction does not happen at all — unless the element
11559
11565
  * declares `leave` (see below), which gives that release a meaning of its own.
11560
11566
  *
@@ -15806,7 +15812,7 @@ const debug$3 = (...args) => {
15806
15812
  * @param {string} name - resource name, used in action names and error messages
15807
15813
  * @param {Object} restCallbacks - `{ idKey, uniqueKeys, rerunOn, dependencies, GET, GET_MANY, GET_RANGE, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
15808
15814
  * @param {string} [restCallbacks.idKey] - primary key property, defaults to `"id"`
15809
- * @param {string[]} [restCallbacks.uniqueKeys] - alternate keys the store can find an item by (e.g. `"username"`); a callback may return a different `id` to rename the item's primary key
15815
+ * @param {string[]} [restCallbacks.uniqueKeys] - alternate keys the store can find an item by (e.g. `"username"`)
15810
15816
  * @see docs/resource.md — relationships, callback return contracts, decision table
15811
15817
  *
15812
15818
  * @example
@@ -16033,7 +16039,7 @@ const createResource = (
16033
16039
  * identical parameters, preventing cross-contamination between different parameter sets.
16034
16040
  *
16035
16041
  * @param {Object} params - Parameters to bind to all actions of this resource (required)
16036
- * @param {Object} options - Additional options for the parameterized resource
16042
+ * @param {{ rerunOn?: Object, dependencies?: Object[] }} [options] - reruns of that scope; left out, the scope inherits the resource's
16037
16043
  * @returns {Object} A new resource instance with parameter-bound actions and isolated lifecycle
16038
16044
  * @see docs/resource.md — what a scope isolates, and `dependencies`
16039
16045
  *
@@ -16051,7 +16057,7 @@ const createResource = (
16051
16057
  * const ROLE_WITH_OWNERSHIP = role.withParams({ owners: true }, {
16052
16058
  * dependencies: [role, database, tables],
16053
16059
  * });
16054
- * // ROLE_WITH_OWNERSHIP.GET_MANY will autorerun when any table/database/role is POST/DELETE
16060
+ * // ROLE_WITH_OWNERSHIP.GET_MANY reruns after any write (POST/PUT/PATCH/DELETE) on a role, a database or a table
16055
16061
  */
16056
16062
  const withParams = (
16057
16063
  paramsToInject,
@@ -16097,13 +16103,14 @@ const createResource = (
16097
16103
  * Callback return contracts:
16098
16104
  * - GET / PUT → the parent object with the relationship nested inside:
16099
16105
  * `async ({ id }) => ({ id, session: { id: 10, token: "abc" } })`; `null` for no relationship
16100
- * - DELETE → the parent id (or `{ id }`); the property is set to `null`
16106
+ * - DELETE → the parent id; the property is set to `null`
16101
16107
  *
16102
16108
  * The backend may also embed the child inline in a parent GET/POST response — the
16103
16109
  * setter on the property upserts the nested object into the child store.
16104
16110
  *
16105
- * Returns the child relationship resource, itself chainable:
16106
- * `USER_SESSION.one("device", DEVICE)` adds a reactive `.device` property to each session.
16111
+ * Returns the relationship resource: its actions (`USER_SESSION.GET`, `.PUT`,
16112
+ * `.DELETE`) write the parent's property. A relation of the child itself is
16113
+ * declared on the child resource (`SESSION.one("device", DEVICE)`).
16107
16114
  *
16108
16115
  * @param {string} propertyName - property holding the child on each parent item
16109
16116
  * @param {Object} childResource - the independent resource created by `resource()`
@@ -17264,7 +17271,7 @@ Received an object with keys: ${keys.join(", ")}.`,
17264
17271
  * });
17265
17272
  *
17266
17273
  * syncResourceToSignals(USER, { username: usernameSignal });
17267
- * // Now when a user item's username is updated via USER.PUT,
17274
+ * // When a user item's username is updated via USER.PUT,
17268
17275
  * // usernameSignal.value is set to the new username,
17269
17276
  * // which in turn triggers the route Signal->URL sync and updates the browser URL.
17270
17277
  */
@@ -17277,6 +17284,17 @@ const syncResourceToSignals = (resource, propertyToSignalMap) => {
17277
17284
  syncStoreToSignals(resource.store, propertyToSignalMap);
17278
17285
  };
17279
17286
 
17287
+ /**
17288
+ * The same, for a `scopedOne`/`scopedMany` resource: its items live in one store
17289
+ * per owner, so the store to watch is the one of the owner `ownerSignal` names
17290
+ * (by id or by any unique key), and it is switched when the signal changes.
17291
+ * Nothing is synced while `ownerSignal` holds `null`/`undefined` or names an
17292
+ * owner nothing has been loaded for yet.
17293
+ *
17294
+ * @param {Object} resource - a resource made by `.scopedOne()` / `.scopedMany()`
17295
+ * @param {import("@preact/signals").Signal} ownerSignal - the owner whose children are watched
17296
+ * @param {Object} propertyToSignalMap - `{ [propertyName]: signal }`, as for `syncResourceToSignals`
17297
+ */
17280
17298
  const syncOwnedResourceToSignals = (
17281
17299
  resource,
17282
17300
  ownerSignal,
@@ -17324,7 +17342,7 @@ const generateSignalId = () => {
17324
17342
  * 1. Initially takes value from the default signal
17325
17343
  * 2. When explicitly set (programmatically or via localStorage), the explicit value takes precedence
17326
17344
  * 3. When default signal changes, it only updates if no explicit value was ever set
17327
- * 4. Calling reset() or setting to undefined makes the signal use the dynamic default again
17345
+ * 4. Setting it to undefined makes the signal use the dynamic default again
17328
17346
  * 5. If dynamic default is undefined and options.default is provided, uses the static fallback
17329
17347
  *
17330
17348
  * This is useful for:
@@ -17338,7 +17356,9 @@ const generateSignalId = () => {
17338
17356
  * @param {string|number} [options.id] - Custom ID for the signal. If not provided, an auto-generated ID will be used. Used for localStorage key and route pattern detection.
17339
17357
  * @param {any} [options.default] - Static fallback value used when defaultValue is a signal and that signal's value is undefined
17340
17358
  * @param {boolean} [options.persists=false] - Whether to persist the signal value in localStorage using the signal ID as key
17341
- * @param {"string" | "number" | "boolean" | "object"} [options.type="string"] - Type for localStorage serialization/deserialization
17359
+ * @param {string} [options.type] - Type the value is coerced to, and read back as from localStorage and the url:
17360
+ * any @jsenv/validity type (`"string"`, `"number"`, `"boolean"`, `"array"`, `"object"`, `"integer"`, `"date"`,
17361
+ * `"time"`…). Without it the value is not coerced.
17342
17362
  * @param {"string" | "number" | "boolean"} [options.itemType] - For array type: type of the array items.
17343
17363
  * Used when reading the value back from a url search param, where everything is a string:
17344
17364
  * `?level=3,4` becomes `[3, 4]` instead of `["3", "4"]`. Without it items stay strings.
@@ -18501,14 +18521,12 @@ const LoadingFallback = ({
18501
18521
  * Two things it gets right that a hand-written boundary rarely does, both
18502
18522
  * explained in docs/error_handling.md:
18503
18523
  *
18504
- * - It marks the error as displayed ONLY when it actually displays it.
18505
- * `preact/debug` rethrows every error a boundary caught in a `setTimeout`, on
18506
- * purpose (React devtools compatibility), so a handled error still reaches
18507
- * window and the jsenv overlay covers the app unless `__handled_by__` is set.
18508
- * Setting it before knowing whether anything is rendered turns a boundary into
18509
- * a bug swallower: a TypeError in a component becomes a blank page AND a
18510
- * silent one. Without a `fallback` there is nothing to display, so the error is
18511
- * left alone and continues up.
18524
+ * - It marks the error as displayed ONLY when it actually displays it. The
18525
+ * jsenv overlay skips an error carrying `__handled_by__`, so a mark set before
18526
+ * knowing whether anything is rendered mutes an error that then continues up:
18527
+ * a TypeError in a component becomes a blank page AND a silent one. Without a
18528
+ * `fallback` there is nothing to display, so the error is left alone and
18529
+ * continues up.
18512
18530
  *
18513
18531
  * - It resets on navigation, not only on rerun. Rerunning the failed action is
18514
18532
  * one way out; going somewhere else is the common one. Without a reset on the
@@ -21590,9 +21608,48 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
21590
21608
  * separator?: import("ignore:preact").ComponentChildren | ((index: number) => import("ignore:preact").ComponentChildren),
21591
21609
  * selfInteractions?: string,
21592
21610
  * interactions?: { [type: string]: "request_action" | "request_ui_action" | "refuse" | ((event: Event) => void) | false | null | undefined },
21611
+ * alignX?: string,
21612
+ * alignY?: string,
21613
+ * spacing?: string | number,
21614
+ * overflow?: string,
21615
+ * header?: boolean,
21616
+ * footer?: boolean,
21617
+ * body?: boolean,
21618
+ * sticky?: boolean,
21593
21619
  * children?: import("ignore:preact").ComponentChildren,
21594
21620
  * [key: string]: any,
21595
21621
  * }>}
21622
+ * @param {"x"|"y"|boolean} [flex] A flex container: `"x"` (or `true`) side by
21623
+ * side, `"y"` stacked. Named by axis because `flex-direction: column` stacks
21624
+ * items vertically, the opposite of what "column" suggests.
21625
+ * @param {boolean} [inline] The inline form of the box's display
21626
+ * (`inline-flex`, `inline-grid`…). An inline box given a `width`/`height`
21627
+ * becomes an inline flex row, since an inline box ignores both.
21628
+ * @param {string} [alignX] Horizontal alignment of the content whatever the
21629
+ * flex direction: `justify-content` or `align-items` is picked from it
21630
+ * (`text-align` outside flex and grid).
21631
+ * @param {string} [alignY] Vertical alignment, the same way.
21632
+ * @param {string|number} [spacing] The gap between children in a flex or grid
21633
+ * box (`spacingX`/`spacingY` per axis) — there is no `gap` prop. Spacing
21634
+ * props (`spacing`, `margin*`, `padding*`) take the scale `"xxs"`…`"xxl"`
21635
+ * (`--navi-xxs`…), `"-s"` for a negative, or any CSS value.
21636
+ * @param {object|string} [style] CSS properties, plus pseudo keys holding
21637
+ * their own: `style={{ color: "blue", ":hover": { color: "red" } }}`
21638
+ * (`":-navi-pressed"`, `"::before"`…). A pseudo class used there is tracked
21639
+ * on the element; nothing re-renders when it changes.
21640
+ * @param {string} [overflow] `"auto"`/`"scroll"` (`overflowX`/`overflowY`
21641
+ * too) makes the box a scrolling area, which is what gives `header`/`body`/
21642
+ * `footer` their meaning; such a box is also `isolation: isolate` and gets a
21643
+ * `scroll-padding` (see docs/scroll.md).
21644
+ * @param {boolean} [header] A part that stays put at the top of the scrolling
21645
+ * box it is a direct child of — nothing at all without that box's overflow.
21646
+ * Rendered as `<header>` unless `as` says otherwise.
21647
+ * @param {boolean} [footer] The same at the bottom, rendered as `<footer>`.
21648
+ * @param {boolean} [body] The only part of its scrolling box that scrolls,
21649
+ * `header`/`footer` sitting outside it. It shrinks, it never grows.
21650
+ * @param {boolean} [sticky] `position: sticky` in the sticky z-index band
21651
+ * (`--navi-z-index-sticky`), stuck or not; an explicit `zIndex`, `"auto"`
21652
+ * included, wins (see docs/z_index.md). `top`/`bottom`/… take `true` for 0.
21596
21653
  * @param {string} [background] Any CSS background, plus two keywords for the
21597
21654
  * planes an app paints: `"surface"` is the paper content sits on,
21598
21655
  * `"chrome"` the frame around a screen — a top bar, a side menu, a toolbar.
@@ -23284,21 +23341,16 @@ const createUITransitionController = (
23284
23341
  /**
23285
23342
  * UITransition
23286
23343
  *
23287
- * A Preact component that enables smooth animated transitions between its children when the content changes.
23288
- * It observes content keys and phases to create different types of transitions.
23344
+ * A Preact component that animates the change between its children: the
23345
+ * outgoing content fades out while the incoming one fades in, and the box
23346
+ * resizes from the one to the other (the mechanics are in ui_transition.js).
23289
23347
  *
23290
- * Features:
23291
- * - Content transitions: Between different content keys (e.g., user profiles, search results)
23292
- * - Phase transitions: Between loading/content/error states for the same content key
23293
- * - Automatic size animation to accommodate content changes
23294
- * - Configurable transition types: "slide-left", "cross-fade"
23295
- * - Independent duration control for content and phase transitions
23296
- *
23297
- * Usage:
23298
- * - Wrap dynamic content in <UITransition> to animate between states
23299
- * - Set a unique `data-content-id` on your rendered content to identify each content variant
23300
- * - Use `data-content-phase` to mark loading/error states for phase transitions
23301
- * - Configure transition types and durations for both content and phase changes
23348
+ * Two kinds of change are told apart, from attributes on the rendered content:
23349
+ * - a content transition, when the content key changes — `data-content-key` on
23350
+ * the rendered element (a user id, a search), or `useUITransitionContentId`
23351
+ * called from a component inside (what `<Route>` does with its url pattern);
23352
+ * - a phase transition, when `data-content-phase` (a loading or error state)
23353
+ * changes for the same content key.
23302
23354
  *
23303
23355
  * Example:
23304
23356
  *
@@ -23307,9 +23359,6 @@ const createUITransitionController = (
23307
23359
  * ? <Spinner data-content-key={userId} data-content-phase />
23308
23360
  * : <UserProfile user={user} data-content-key={userId} />}
23309
23361
  * </UITransition>
23310
- *
23311
- * When `data-content-id` changes, UITransition animates content transitions.
23312
- * When `data-content-phase` changes for the same key, it animates phase transitions.
23313
23362
  */
23314
23363
 
23315
23364
  const UITransitionContentIdContext = createContext();
@@ -23431,16 +23480,12 @@ const UITransition = ({
23431
23480
  };
23432
23481
 
23433
23482
  /**
23434
- * The goal of this hook is to allow a component to set a "content key"
23435
- * Meaning all content within the component is identified by that key
23436
- *
23437
- * When the key changes, UITransition will be able to detect that and consider the content
23438
- * as changed even if the component is still the same
23439
- *
23440
- * This is used by <Route> to set the content key to the route path
23441
- * When the route becomes inactive it will call useUITransitionContentId(undefined)
23442
- * And if a sibling route becones active it will call useUITransitionContentId with its own path
23483
+ * Names the content a component renders, for the <UITransition> around it:
23484
+ * everything rendered inside is identified by that key, and the key changing
23485
+ * is read as a content change even though the component is the same.
23443
23486
  *
23487
+ * <Route> calls it with the url pattern of the branch it renders; the branch
23488
+ * unmounting removes its part, and a sibling branch mounting adds its own.
23444
23489
  */
23445
23490
  const useUITransitionContentId = value => {
23446
23491
  const contentId = useContext(UITransitionContentIdContext);
@@ -23465,8 +23510,8 @@ const useUITransitionContentId = value => {
23465
23510
  };
23466
23511
 
23467
23512
  /**
23468
- * Custom route pattern matching system
23469
- * Replaces URLPattern with a simpler, more predictable approach
23513
+ * Custom route pattern matching system.
23514
+ * Not built on URLPattern: matching here is simpler and more predictable.
23470
23515
  */
23471
23516
 
23472
23517
  const debug$2 = (...args) => {
@@ -30547,8 +30592,9 @@ const RouteTransitionArea = ({
30547
30592
  * Declare how a pair of routes moves against each other — or how one route is
30548
30593
  * entered, from wherever its door happens to be.
30549
30594
  *
30550
- * @param {object|null} from - a route, or `{ route, params }` when the page is
30551
- * a param of a route rather than a route of its own. `null` says the page is
30595
+ * @param {object|null} from - a route (or the `routeFallback()` page), or
30596
+ * `{ route, params }` when the page is a param of a route rather than a route
30597
+ * of its own. `null` says the page is
30552
30598
  * reached FROM ANYWHERE: its door is in the furniture (a gear in the top
30553
30599
  * bar, a "+" in the tab bar) rather than on a screen, so there is no pair to
30554
30600
  * write it on. Arriving at `to` from any page then plays forward and leaving
@@ -30596,8 +30642,7 @@ const RouteTransitionArea = ({
30596
30642
  * Whatever is written here is what EVERY crossing of the pair plays. One
30597
30643
  * crossing can ask for something else — `<Link routeTransition>`, or
30598
30644
  * navTo(url, { routeTransition }) — which overrides this field by field, for
30599
- * that
30600
- * navigation alone.
30645
+ * that navigation alone.
30601
30646
  * @returns {() => void} remove this relation.
30602
30647
  */
30603
30648
  const defineRouteTransition = (from, to, transition) => {
@@ -30680,9 +30725,8 @@ const normalizeTransition = transition => {
30680
30725
  *
30681
30726
  * Two mouths, one meaning: the element being pressed wears it (a `<Link
30682
30727
  * routeTransition>`, or the attribute by hand on any anchor), or navTo() is
30683
- * handed
30684
- * it. Both arrive here through the announcement the navigation makes before it
30685
- * writes anything (see before_routing.js).
30728
+ * handed it. Both arrive here through the announcement the navigation makes
30729
+ * before it writes anything (see before_routing.js).
30686
30730
  *
30687
30731
  * A request answers FIELD BY FIELD: what it does not say, the relation — or
30688
30732
  * the default — still answers for. So `{ direction: "back" }` keeps the pair's
@@ -31929,10 +31973,10 @@ const css$11 = /* css */`.navi_route_travel {
31929
31973
  * The pages are cut at the edge of this box while they travel, and at the app's
31930
31974
  * safe area the box runs under, which is written on the transition's own
31931
31975
  * pseudo-elements — no overflow of the document reaches pictures drawn in the
31932
- * top layer. It needs nothing of the browser beyond view
31933
- * transitions themselves: a browser without them (Firefox) navigates without the
31934
- * movement, and the gesture applies its change on release instead of dragging a
31935
- * picture that does not exist.
31976
+ * top layer. It needs nothing of the browser beyond view transitions
31977
+ * themselves: a browser without them navigates without the movement, and the
31978
+ * gesture applies its change on release instead of dragging a picture that does
31979
+ * not exist.
31936
31980
  *
31937
31981
  * While a travel plays, the rest of the page is taken as a picture too — this
31938
31982
  * box asks for `view-transition-name: root` back for that time, so an
@@ -33508,9 +33552,6 @@ const onNaviCommand = (e, { debugCommand = () => {} } = {}) => {
33508
33552
  `targeting`,
33509
33553
  commandTarget,
33510
33554
  );
33511
- // Timed once, for the wheel-in-dialog "Définir feels frozen on mobile" case;
33512
- // the line is gone now that the answer is known — a command that runs in 1ms
33513
- // said nothing, and it said it on every single interaction.
33514
33555
  return implementation();
33515
33556
  };
33516
33557
 
@@ -34154,8 +34195,8 @@ registerNaviCommand("--navi-open", (source, event, { anchor, value } = {}) => {
34154
34195
  target,
34155
34196
  implementation: () => {
34156
34197
  // The popup reads `anchor` first and falls back to the source (see
34157
- // onnavi_request_open in popover.jsx), so saying nothing keeps the old
34158
- // behaviour: the element asking is the element opened against.
34198
+ // onnavi_request_open in popover.jsx): without an `anchor`, the element
34199
+ // asking is the element opened against.
34159
34200
  return dispatchCustomEvent(target, "navi_request_open", {
34160
34201
  event,
34161
34202
  source: resolveCommandProxySource(source),
@@ -36570,8 +36611,8 @@ const usePopupContentMount = (
36570
36611
  * are no-ops.
36571
36612
  *
36572
36613
  * Behavior:
36573
- * - No <dialog>/<details>/[popover] ancestor → runs like a normal
36574
- * useLayoutEffect with the provided deps.
36614
+ * - No <dialog>/<details>/[popover]/[aria-expanded] ancestor → runs like a
36615
+ * normal useLayoutEffect with the provided deps.
36575
36616
  * - Inside a closed/hidden ancestor → skips the initial run; instead runs
36576
36617
  * the callback once the ancestor opens — see @jsenv/dom's own
36577
36618
  * observeAncestorOpenState for exactly how that's detected, and why it
@@ -36585,8 +36626,9 @@ const usePopupContentMount = (
36585
36626
  * reveals nothing about them, so it does not re-run them either. See
36586
36627
  * isDisplayedDespiteClosedAncestor in @jsenv/dom.
36587
36628
  *
36588
- * The callback's second argument is always a `navi_displayed` CustomEvent,
36589
- * with `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
36629
+ * The callback is called with the element (`ref.current`), and a
36630
+ * `navi_displayed` CustomEvent as its second argument, with
36631
+ * `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
36590
36632
  * - No <dialog>/<details>/[popover]/[aria-expanded] ancestor at all →
36591
36633
  * `{ ancestor: document, ancestorType: "document" }`.
36592
36634
  * - Otherwise → `{ ancestor: <the matched element>, ancestorType: "dialog"
@@ -36751,6 +36793,8 @@ const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
36751
36793
  // see also https://github.com/preactjs/preact/issues/1255
36752
36794
 
36753
36795
 
36796
+ const NO_ELEMENT_REF = { current: null };
36797
+
36754
36798
  /**
36755
36799
  * Programmatic autofocus that runs after Preact layout effects are flushed.
36756
36800
  *
@@ -36803,13 +36847,12 @@ const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
36803
36847
  * the browser's built-in scroll-into-view that accompanies focus.
36804
36848
  * @param {boolean} [options.focusVisible]
36805
36849
  * Passed as `focusVisible` to `element.focus()`.
36806
- * @param {boolean} [options.autoFocusSelect]
36850
+ * @param {boolean} [options.autoSelect]
36807
36851
  * When true, also calls `element.select()` after focusing (useful for text inputs).
36808
- * @returns {Function} triggerAutofocus — can be called manually with a synthetic
36809
- * event to re-run the focus logic outside of the layout-effect lifecycle.
36852
+ * @returns {object} The attributes to spread on the element: `navi-autofocus`
36853
+ * (and `navi-autofocus-select`), with `autoFocus` set to undefined so the
36854
+ * native attribute never lands on the node.
36810
36855
  */
36811
- const NO_ELEMENT_REF = { current: null };
36812
-
36813
36856
  const useAutoFocus = (
36814
36857
  focusableElementRef,
36815
36858
  autoFocus,
@@ -37002,9 +37045,10 @@ document.body.addEventListener(
37002
37045
  * and nothing about it stops someone from starting a run on every row before
37003
37046
  * any of them comes back: a dozen requests in flight, a dozen rows waiting, and
37004
37047
  * a server asked to do a dozen things at once because a list happened to be
37005
- * long. The guard is what makes the surface say "not yet" instead — the rows
37006
- * that are not running go read-only and say what they are waiting for, and the
37007
- * next press is possible again the moment one of the runs comes back.
37048
+ * long. The guard is what makes the surface say "not yet" instead — the
37049
+ * controls that would start another run go read-only and say what they are
37050
+ * waiting for, and the next press is possible again the moment one of the runs
37051
+ * comes back.
37008
37052
  *
37009
37053
  * It counts runs, not values — which is what separates it from
37010
37054
  * `maxLengthGuard`, the one that says how many things a selection may HOLD.
@@ -37349,14 +37393,13 @@ const createControlGuard = (controller) => {
37349
37393
  };
37350
37394
 
37351
37395
  /**
37352
- * Orchestrates the three rule managers for a UI state controller.
37353
- *
37354
- * Instead of holding `controller.controlInteraction` + `controller.controlValidity`
37355
- * independently, a controller now has a single `controller.rules` object:
37396
+ * Orchestrates the rule managers of a UI state controller, held together in
37397
+ * one `controller.rules` object:
37356
37398
  *
37357
37399
  * controller.rules.callout — shared callout display manager
37358
37400
  * controller.rules.interaction — interactivity gate (disabled/readonly/busy)
37359
37401
  * controller.rules.validation — value validity gate (required/pattern/etc.)
37402
+ * controller.rules.guard — keystroke guard (charGuard/maxLengthGuard)
37360
37403
  *
37361
37404
  * The callout manager is created first so both interaction and validation
37362
37405
  * can use the same callout slot and the same lifecycle hooks.
@@ -39472,9 +39515,8 @@ const EMPTY_OBJECT = {};
39472
39515
  * child changes → we update picker input → navi_ui_state_change fires →
39473
39516
  * we see updatingRef=true → skip → no loop.
39474
39517
  *
39475
- * This removes the need for `command="--navi-update"` on controls placed
39476
- * inside the picker popup. It also means `commands.js` no longer has to
39477
- * manually re-dispatch to inner controls.
39518
+ * So a control placed inside the picker popup needs no
39519
+ * `command="--navi-update"` to reach the picker input.
39478
39520
  */
39479
39521
  const useUIFacadeStateController = (props, realUIStateController) => {
39480
39522
  const firstChildControllerRef = useRef(null);
@@ -39886,9 +39928,10 @@ const uiStateHoldsNothing = (uiState) => {
39886
39928
  * use `onInput` internally and route through `dispatchRequestAction` so the behavior
39887
39929
  * is consistent regardless of input type.
39888
39930
  *
39889
- * 2. Any field (text, checkbox, radio, picker…) can opt into debounce simply by passing
39890
- * a debounced action. The request-action event chain handles the timing centrally
39891
- * rather than each component having to manage its own debounce logic.
39931
+ * 2. Any field (text, checkbox, radio, picker…) can opt into debounce with
39932
+ * `actionDebounce` (ms). The input effect every field installs (input_effect.js)
39933
+ * handles the timing centrally rather than each component having to manage its
39934
+ * own debounce logic.
39892
39935
  */
39893
39936
  const NO_ACTION_YET = Symbol("no_action_yet");
39894
39937
 
@@ -39964,7 +40007,7 @@ const ControlgroupChildrenWrapper = ({
39964
40007
  });
39965
40008
 
39966
40009
  /**
39967
- * Core hook for interactive field components (InputText, InputCheckbox, etc.).
40010
+ * Core hook for interactive field components (InputTextual, InputCheckbox, etc.).
39968
40011
  *
39969
40012
  * Sets up the full field lifecycle:
39970
40013
  * - Creates a UI state controller that manages state divergence between props and user interactions
@@ -39975,7 +40018,7 @@ const ControlgroupChildrenWrapper = ({
39975
40018
  * - Handles constraint validation and message props
39976
40019
  *
39977
40020
  * All state changes route through DOM events on the field element so that
39978
- * external subscribers (e.g. useUIState, Selectable) receive every update.
40021
+ * external subscribers (e.g. useControlUIState, Selectable) receive every update.
39979
40022
  *
39980
40023
  * @returns {[controlRootProps, controlHostProps, { uiStateController }]}
39981
40024
  */
@@ -41048,10 +41091,10 @@ const useReadOnlyUncontrolled = (props, controlInfo) => {
41048
41091
  };
41049
41092
 
41050
41093
  /**
41051
- * Core hook for field group components (SelectableList, CheckboxList, etc.).
41094
+ * Core hook for field group components (List selectable, CheckboxGroup, etc.).
41052
41095
  * - Creates a UI group state controller that aggregates child states into one group state
41053
41096
  * - Binds the group's action to the aggregated state signal
41054
- * - Provides context to children: ParentUIStateController, FieldName, Disabled, ReadOnly,
41097
+ * - Provides context to children: ParentUIStateController, ControlName, Disabled, ReadOnly,
41055
41098
  * Required, Loading, Action, ActionRequester
41056
41099
  * - Overrides `onnavi_reset_ui_state` to cascade resets to all monitored children
41057
41100
  * by dispatching `navi_reset_ui_state` DOM events on each child's DOM element
@@ -43580,10 +43623,10 @@ const LoadingRectangleSvg = ({
43580
43623
  }) => {
43581
43624
  // The stroke is centered on the path, so half of it falls outside: drawing
43582
43625
  // at half the stroke width puts its outer edge exactly on the box, whatever
43583
- // the box. It used to be a share of the smaller dimension (3%, floored at
43584
- // 2px), which held the line further and further inside as the box grew — a
43585
- // caller placing the outline with `inset` got the distance it asked for on a
43586
- // field and something several pixels inward on a card. Nothing needs the
43626
+ // the box. Not a share of the smaller dimension (3%, floored at 2px): that
43627
+ // holds the line further and further inside as the box grows — a caller
43628
+ // placing the outline with `inset` gets the distance it asked for on a field
43629
+ // and something several pixels inward on a card. Nothing needs the
43587
43630
  // extra room: the svg is `overflow: visible` and whoever positions the
43588
43631
  // wrapper already reserves the half stroke.
43589
43632
  const margin = strokeWidth / 2;
@@ -44655,7 +44698,7 @@ const shouldInjectSpacingBetween = (left, right) => {
44655
44698
  * boldStable?: boolean,
44656
44699
  * shrinkWrap?: boolean,
44657
44700
  * capitalize?: boolean,
44658
- * selectRange?: string | [number, number],
44701
+ * selectRange?: string | [number, number] | [string, string],
44659
44702
  * childrenOutsideFlow?: import("ignore:preact").ComponentChildren,
44660
44703
  * [key: string]: any,
44661
44704
  * }>}
@@ -44723,9 +44766,10 @@ const shouldInjectSpacingBetween = (left, right) => {
44723
44766
  * @param {boolean} [capitalize]
44724
44767
  * Uppercases the first letter of the text content via CSS.
44725
44768
  *
44726
- * @param {string|[number,number]} [selectRange]
44727
- * Selects a portion of the text on mount. Pass a substring to search for, or
44728
- * a `[start, end]` character-offset tuple.
44769
+ * @param {string|[number,number]|[string,string]} [selectRange]
44770
+ * Selects a portion of the text on mount. Pass a substring to search for, a
44771
+ * `[start, end]` character-offset tuple, or a `[startText, endText]` pair of
44772
+ * substrings selecting from the first to the end of the second.
44729
44773
  *
44730
44774
  * @param {import("ignore:preact").ComponentChildren} [childrenOutsideFlow]
44731
44775
  * Rendered after children but outside the text spacing/flow logic. Used
@@ -45595,7 +45639,7 @@ Object.assign(PSEUDO_CLASSES, {
45595
45639
  * @param {object} props
45596
45640
  * @param {string} [props.href] - Destination. Also the default `value`, and
45597
45641
  * (when `hrefFallback`) the default visible text.
45598
- * @param {import("../route.js").Route} [props.route] - Renders via `route`
45642
+ * @param {object} [props.route] - Renders via `route`
45599
45643
  * instead of a raw `href`: the URL is built from the route (see
45600
45644
  * `routeParams`) and "current" is derived from whether the route matches.
45601
45645
  * @param {object} [props.routeParams] - Params passed to `route.buildUrl`.
@@ -45619,14 +45663,14 @@ Object.assign(PSEUDO_CLASSES, {
45619
45663
  * (`navi_value`); defaults to `href`.
45620
45664
  * @param {boolean} [props.current] - Forces the "current" state on (otherwise
45621
45665
  * derived from the href/route).
45622
- * @param {import("../route.js").Route|import("../route.js").Route[]} [props.currentExcept] -
45666
+ * @param {object|object[]} [props.currentExcept] -
45623
45667
  * Route(s) inside this link's own that are NOT it: while one of them matches,
45624
45668
  * the link is not current even though its route still is. A bar entry
45625
45669
  * standing for a whole section, and one place under that section the entry
45626
45670
  * does not stand for — settings reached from everywhere and shown over
45627
45671
  * whatever the reader was on. Nothing about the routing changes: the url is
45628
45672
  * in the section, only the link stops claiming "you are here".
45629
- * @param {import("../route.js").Route|import("../route.js").Route[]} [props.currentAlso] -
45673
+ * @param {object|object[]} [props.currentAlso] -
45630
45674
  * Route(s) other than this link's own that are it: the link is current while
45631
45675
  * one of them matches. The other side of `currentExcept` — an entry standing
45632
45676
  * for more than the one page it opens. `currentExcept` wins over it.
@@ -46530,6 +46574,7 @@ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
46530
46574
  * currentIndicator?: boolean|"top"|"bottom"|"left"|"right",
46531
46575
  * currentIndicatorSlides?: boolean,
46532
46576
  * slideContainer?: string,
46577
+ * panelPosition?: "before"|"after",
46533
46578
  * }>}
46534
46579
  * @param {boolean|"top"|"bottom"|"left"|"right"} [props.currentIndicator] - the
46535
46580
  * bar that says which tab one is on, said once here rather than on every
@@ -46555,6 +46600,9 @@ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
46555
46600
  * container says which one is on screen, and pressing a tab travels there.
46556
46601
  * Tabs that are places in the same screen rather than pages of their own —
46557
46602
  * nothing is written to the URL and nothing is a link.
46603
+ * @param {"before"|"after"} [props.panelPosition] - turns the row into folder
46604
+ * tabs: the current tab and the panel it opens share one surface, and this
46605
+ * says which side of the row the panel sits on.
46558
46606
  */
46559
46607
  const Nav = ({
46560
46608
  children,
@@ -47677,6 +47725,8 @@ const TABS_ALIGN_TO_JUSTIFY_CONTENT = {
47677
47725
  * onChange?: (value: any) => void,
47678
47726
  * tabsPosition?: "top"|"bottom"|"left"|"right",
47679
47727
  * tabsAlign?: "start"|"center"|"end"|"stretch",
47728
+ * scrollablePage?: boolean,
47729
+ * maxLines?: number|false,
47680
47730
  * borderWidth?: string|number,
47681
47731
  * borderRadius?: string|number,
47682
47732
  * tabBorderRadius?: string|number,
@@ -47856,7 +47906,7 @@ const Binder = ({
47856
47906
  * position among its siblings.
47857
47907
  * @param {import("ignore:preact").ComponentChildren} [props.label] - What the tab
47858
47908
  * shows.
47859
- * @param {import("../route.js").Route} [props.route] - Makes the tab a `Link`
47909
+ * @param {object} [props.route] - Makes the tab a `Link`
47860
47910
  * to that route, and the binder follows the url: the open item is the one
47861
47911
  * whose route matches, no `value`/`onChange` needed.
47862
47912
  * @param {string} [props.href] - Same, for a plain url.
@@ -48295,6 +48345,9 @@ const FixedBarStyleCSSVars = {
48295
48345
  * `border={false}` for none.
48296
48346
  * @param {string|number} [props.maxWidth] - Keeps the bar lined up with a
48297
48347
  * content column narrower than the window (it stays centered).
48348
+ * @param {string|number} [props.padding=0] - Along the bar only: left/right on
48349
+ * a top or bottom bar, top/bottom on a side one. Across it, the size is
48350
+ * `height`/`width`.
48298
48351
  */
48299
48352
  const FixedBar = ({
48300
48353
  children,
@@ -49163,9 +49216,10 @@ const useExpandableContext = partName => {
49163
49216
  * height too. `"idle"` builds it in a browser idle moment after load.
49164
49217
  * `"while-opened"` throws the content away once the collapse
49165
49218
  * settles — after the closing animation, so it still plays on real
49166
- * content — and rebuilds it from scratch on every expansion. Whatever the
49167
- * value, intent on the UI part (pointer entering it, focus landing in it)
49168
- * builds the content ahead of the click.
49219
+ * content — and rebuilds it from scratch on every expansion. Under the
49220
+ * other three, intent on the UI part (pointer entering it, focus landing in
49221
+ * it) builds the content ahead of the click; `"while-opened"` content is
49222
+ * never warmed, it is built at the open.
49169
49223
  */
49170
49224
  const Expandable = props => {
49171
49225
  import.meta.css = [css$R, "@jsenv/navi/src/control/expandable/expandable.jsx"];
@@ -49942,7 +49996,8 @@ const DARK_CONTRAST_LIGHTNESS_THRESHOLD = 0.65;
49942
49996
  * @param {string} accentColor - The accent color value. When it changes, attributes are recomputed.
49943
49997
  * @param {object} [options]
49944
49998
  * @param {string} [options.elementSelector] - CSS selector to find the element whose computed color is read.
49945
- * Defaults to the root element itself. Useful when the color is applied to a probe/child element.
49999
+ * Defaults to the root's own `data-visual-selector` attribute, else the root element itself.
50000
+ * Useful when the color is applied to a probe/child element.
49946
50001
  * @param {string} [options.colorProperty="backgroundColor"] - Computed style property to read (e.g. "color", "borderColor").
49947
50002
  */
49948
50003
  const useAccentColorAttributes = (
@@ -51981,16 +52036,18 @@ const css$L = /* css */`@layer navi {
51981
52036
  /**
51982
52037
  * Field — a semantic wrapper that connects a label to a form control.
51983
52038
  *
51984
- * It generates a stable `fieldId` (or accepts an explicit `id`) that is
52039
+ * It generates a stable `fieldId` (or accepts an explicit one) that is
51985
52040
  * automatically forwarded to the `Label` inside the field as `htmlFor` and to
51986
- * any interactive control (Picker, Input, …) as its `id`, so clicking the
51987
- * label focuses the control without requiring manual wiring.
52041
+ * the labelable control inside it (Input, Select, Picker, Button) as its `id`,
52042
+ * so clicking the label focuses the control without requiring manual wiring.
51988
52043
  *
51989
52044
  * It also tracks the readOnly / disabled / interactive state reported by its
51990
52045
  * child control and reflects it on the `Label` (dimmed color, cursor change).
51991
52046
  *
51992
52047
  * Props:
51993
- * id — optional explicit id used as the field id instead of the auto-generated one
52048
+ * fieldId — optional explicit field id instead of the auto-generated one (an `id`
52049
+ * is the wrapper's own DOM id)
52050
+ * as="label" — the field IS the label, wrapping its control: no id needed
51994
52051
  * flex="y" — vertical layout; automatically sets alignX="start" and data-vertical
51995
52052
  * children — any JSX; should contain a `Label` and a form control
51996
52053
  * ...rest — forwarded to the wrapping element (className, style, flex, spacing, …)
@@ -52197,6 +52254,7 @@ const InputUnitSlot = ({
52197
52254
  };
52198
52255
  const InputSlot = ({
52199
52256
  side,
52257
+ children,
52200
52258
  ...props
52201
52259
  }) => {
52202
52260
  const ctx = useContext(InputTextualContext);
@@ -52226,7 +52284,11 @@ const InputSlot = ({
52226
52284
  e.preventDefault();
52227
52285
  }
52228
52286
  },
52229
- ...props
52287
+ ...props,
52288
+ children: jsx(ControlIdContext.Provider, {
52289
+ value: undefined,
52290
+ children: children
52291
+ })
52230
52292
  });
52231
52293
  };
52232
52294
 
@@ -53691,9 +53753,9 @@ const InputWithListResolver = props => {
53691
53753
  };
53692
53754
 
53693
53755
  /**
53694
- * InputWithList — connects an input to a SelectableList via its id.
53756
+ * InputWithList — connects an input to a selectable List via its id.
53695
53757
  *
53696
- * Usage: <Input navi-list="my-list-id" /> next to <SelectableList id="my-list-id" />
53758
+ * Usage: <Input navi-list="my-list-id" /> next to <List selectable id="my-list-id">
53697
53759
  *
53698
53760
  * Behavior:
53699
53761
  * - ArrowDown / ArrowUp move the list's "current item" without moving focus
@@ -54061,11 +54123,14 @@ const useAutoSelectReadOnly = (props) => {
54061
54123
  *
54062
54124
  * - charGuard — restricts which characters can be typed, pasted, or set externally.
54063
54125
  * Accepts a preset name or a raw regex character class:
54064
- * "numeric" → digits only, sets inputMode="numeric" + pattern auto
54126
+ * true / "auto" → picked from the field: inputMode numeric/decimal, type tel/email
54127
+ * "numeric" → digits only, sets inputMode="numeric"
54128
+ * "decimal" → digits, minus, dot, comma, sets inputMode="decimal"
54065
54129
  * "alpha" → letters only
54066
54130
  * "alphanumeric" → letters and digits
54067
54131
  * "uppercase" → uppercase letters only
54068
54132
  * "tel" → phone chars (digits, +, -, parens, space), sets inputMode="tel"
54133
+ * "email" → what an email address is made of
54069
54134
  * "card" → credit card (digits and spaces), sets inputMode="numeric"
54070
54135
  * "hex" → hexadecimal digits
54071
54136
  * "pin" → numeric PIN, sets inputMode="numeric"
@@ -55133,18 +55198,13 @@ const commitSubtree = (controller, e) => {
55133
55198
  };
55134
55199
 
55135
55200
  /**
55136
- *
55137
- * Here we want the same behaviour as web standards:
55138
- *
55139
- * 1. When submitting the form URL does not change
55140
- * 2. When form submission id done user is redirected (by default the current one)
55141
- * (we can configure this using target)
55142
- * So for example user might be reidrect to a page with the resource he just created
55143
- * I could create an example where we would put a link on the page to let user see what he created
55144
- * but by default user stays on the form allowing to create multiple resources at once
55145
- * And an other where he is redirected to the resource he created
55146
- * 3. If form submission fails ideally we should display this somewhere on the UI
55147
- * right now it's just logged to the console I need to see how we can achieve this
55201
+ * <Form>: the <form> element as a navi control group. Its named fields
55202
+ * aggregate into one object; a submit checks their constraints, runs `action`
55203
+ * with that object when it changed (docs/form_changed.md), and `command` — or
55204
+ * the surface the form sits in — says what follows. The URL does not change on
55205
+ * a submit, and a failing action is drawn as an error callout on the button
55206
+ * that sent it (docs/error_handling.md). A form inside another form renders no
55207
+ * <form> element (FormNested).
55148
55208
  */
55149
55209
 
55150
55210
  const Form = props => {
@@ -55764,9 +55824,9 @@ const offsetToPx = (offset, box) => {
55764
55824
 
55765
55825
  // A press landing while the track is already travelling: what is playing is
55766
55826
  // sent home in a fifth of the time it has left, and the press it could not take
55767
- // yet is taken as soon as it lands. A press has to be FELT — nudging the pace
55768
- // of a travel already in flight (what this used to do) reads as "nothing
55769
- // happened", because the thing was moving before the click too. Getting there
55827
+ // yet is taken as soon as it lands. A press has to be FELT — merely nudging the
55828
+ // pace of a travel already in flight reads as "nothing happened", because the
55829
+ // thing was moving before the click too. Getting there
55770
55830
  // almost at once and setting off again is the click being answered.
55771
55831
  // Played out fast rather than cut short: ending it on the spot would jump.
55772
55832
  // Compounds, so two presses during one travel bring it home twice as sharply,
@@ -58337,13 +58397,12 @@ const SlideNavButton = ({
58337
58397
  autoFocus: "last-resort",
58338
58398
  icon: true,
58339
58399
  variant: "discrete"
58340
- // Takes the focus like any other button, on purpose. It used to refuse it
58400
+ // Takes the focus like any other button, on purpose. Do not refuse it
58341
58401
  // (mousedown.preventDefault) to keep the keyboard where the user had put
58342
- // it — but pressing a way out with nothing focused then left the keyboard
58343
- // on nothing at all: the travel below only hands the focus to the slide
58344
- // arriving when it was leaving a slide, so a click from document.body
58345
- // arrived on document.body, and the next Tab started from the top of the
58346
- // page rather than from what is on screen.
58402
+ // it: the travel below only hands the focus to the slide arriving when it
58403
+ // was leaving a slide, so a press with nothing focused would arrive on
58404
+ // document.body, and the next Tab would start from the top of the page
58405
+ // rather than from what is on screen.
58347
58406
  //
58348
58407
  // Letting it focus is both the plain behaviour of a button and what makes
58349
58408
  // the rest fall into place: the press lands on the chevron, the slide left
@@ -58360,12 +58419,6 @@ const SlideNavButton = ({
58360
58419
  })
58361
58420
  });
58362
58421
 
58363
- /**
58364
- * The way out of a slide, and the way into the next one. Nothing but the
58365
- * command plus the chevron that matches the travel: a row goes left/right, a
58366
- * column up/down — so the button points where the slide actually goes without
58367
- * the caller having to keep the two in sync.
58368
- */
58369
58422
  /**
58370
58423
  * A way out pointing where it goes. It says a direction, not a slide: what is
58371
58424
  * over there is the map's business, and moving a screen changes nothing here.
@@ -61415,7 +61468,7 @@ const css$E = /* css */`
61415
61468
  * within its container (the viewport for `layer="top"`, the positioned
61416
61469
  * ancestor for `layer="local"`) — Dialog is never anchored to a real
61417
61470
  * element for positioning purposes. Same grammar as `Popover`'s own
61418
- * `positionArea` (see `popup_shared.js`'s `parsePositionArea`): a single
61471
+ * `positionArea` (see `parsePositionArea` in @jsenv/dom): a single
61419
61472
  * compass token — `top`/`top-start`/`top-end`/`top-left`/`top-right`,
61420
61473
  * `right`/`right-start`/`right-end`, `bottom`/`bottom-start`/
61421
61474
  * `bottom-end`/`bottom-left`/`bottom-right`, `left`/`left-start`/
@@ -61434,12 +61487,12 @@ const css$E = /* css */`
61434
61487
  * between the dialog and the edges of its container, whatever its
61435
61488
  * `positionArea`: it both caps the dialog's own size (via
61436
61489
  * `--x-dialog-container-spacing`, written from this prop) and offsets a docked
61437
- * one from the edge it docks to. Accepts a spacing token ("s", "m"…), a
61438
- * number of pixels, or a viewport length — "appw"/"apph" being the app's own
61439
- * screen (the visual viewport, or the narrower one the app declared with
61440
- * --navi-app-max-width) and "vvw"/"vvh" the visual viewport itself, which
61441
- * shrinks when the mobile keyboard opens. Pass 0 for a dialog
61442
- * meant to sit flush (a side panel).
61490
+ * one from the edge it docks to. Accepts a number of pixels, a viewport
61491
+ * length — "appw"/"apph" being the app's own screen (the visual viewport, or
61492
+ * the narrower one the app declared with --navi-app-max-width) and
61493
+ * "vvw"/"vvh" the visual viewport itself, which shrinks when the mobile
61494
+ * keyboard opens — or a container length ("3cqw", the `layer="local"`
61495
+ * default). Pass 0 for a dialog meant to sit flush (a side panel).
61443
61496
  * @param {boolean} [props.backdrop=true] - Whether anything is laid between
61444
61497
  * the dialog and the page at all — asked before any question of what an
61445
61498
  * outside press does or how the backdrop is painted. `false` leaves the page
@@ -61452,10 +61505,12 @@ const css$E = /* css */`
61452
61505
  * needs. What it gives up is what only a modal gets natively: the hardware
61453
61506
  * back button no longer dismisses it. See docs/popup_backdrop.md.
61454
61507
  * @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="close"]
61455
- * - `"close"` closes the dialog on an outside press. `"capture"`/`"ignore"`
61456
- * both just absorb the press without closing (visually dimmed backdrop vs.
61457
- * not) — a dialog with a backdrop is modal one way or another, so there's
61458
- * always at least a press-absorbing backdrop regardless of this prop.
61508
+ * - `"close"` closes the dialog on an outside press, `"cancel"` closes it
61509
+ * as a cancel (what it holds reverts, as on Escape). `"capture"`/`"ignore"`
61510
+ * both just absorb the press without closing (`"capture"` paints the
61511
+ * blurred glass, `"ignore"` leaves the wall's paint alone) — a wall absorbs
61512
+ * the press whatever this prop says, natively for a modal and through the
61513
+ * sibling element for a local one.
61459
61514
  * "Outside" is the dialog's own border box; a see-through dialog whose box is
61460
61515
  * bigger than what it paints marks the difference with
61461
61516
  * `data-navi-popup-outside` (see docs/popup_backdrop.md).
@@ -61530,11 +61585,10 @@ const css$E = /* css */`
61530
61585
  * @param {string} [props.animationDuration] - Maps to
61531
61586
  * `--popup-animation-duration`.
61532
61587
  * @param {Element|{current: Element}|string} [props.anchor] - Never used for
61533
- * positioning (see this file's top comment), and ignored entirely unless
61534
- * `sizeFromAnchor` or `animation="lifting"` asks for it — the first sizes
61535
- * the dialog via the `--anchor-width`/`--anchor-height` CSS vars, the second
61536
- * makes the dialog come out of the anchor's own box. Used when the open
61537
- * itself names none — an anchor carried by the opening event
61588
+ * positioning (see this file's top comment); read by `sizeFromAnchor` (the
61589
+ * `--anchor-width`/`--anchor-height` CSS vars), by `animation="lifting"`
61590
+ * (the box the dialog comes out of) and by `mount`'s warm-on-intent. Used
61591
+ * when the open itself names none — an anchor carried by the opening event
61538
61592
  * (`e.detail.anchor`) is about that one opening and comes first; left out,
61539
61593
  * whatever triggered the open (`e.detail.source`). A string is resolved via
61540
61594
  * `document.getElementById` when the dialog opens — see popover.jsx's own
@@ -61596,6 +61650,9 @@ const css$E = /* css */`
61596
61650
  * focusable of its own.
61597
61651
  * - `"restore"` — the dialog stays out of the opening focus chain unless it
61598
61652
  * held focus when it closed.
61653
+ * - `false` — the dialog element itself is never where the focus lands; the
61654
+ * opening still hands the focus to what the dialog holds. Unlike
61655
+ * `Popover`'s `false`, it does not leave the keyboard where it was.
61599
61656
  * Wherever the keyboard is a virtual one (a touch device), the surface is
61600
61657
  * already what one arrives on: a popup is read before it is reached there, so
61601
61658
  * the focus only leaves it for something that asked by name (`autoFocus` on
@@ -61839,8 +61896,8 @@ const useDialogProps = props => {
61839
61896
  // `backdrop` — see this file's top comment.
61840
61897
  layer = "top",
61841
61898
  dockedOnSmallTouchScreen,
61842
- // Same grammar as Popover's own positionArea — see this file's top
61843
- // comment and popup_shared.js's parsePositionArea.
61899
+ // Same grammar as Popover's own positionArea — see parsePositionArea in
61900
+ // @jsenv/dom.
61844
61901
  positionArea: positionAreaProp,
61845
61902
  // A dialog docked against an edge must keep the same gap its own size cap
61846
61903
  // already guarantees a centered one — so this drives both (see
@@ -61880,8 +61937,8 @@ const useDialogProps = props => {
61880
61937
  sizing = "auto",
61881
61938
  animation,
61882
61939
  lift = "box",
61883
- // Inert unless sizeFromAnchor below (see this file's top comment) —
61884
- // Dialog's own positioning is never relative to it.
61940
+ // Never positioned against (see this file's top comment): read by
61941
+ // sizeFromAnchor, by a lift, and by the warm-on-intent of `mount`.
61885
61942
  anchor,
61886
61943
  // Where a lift comes back to, when that is no longer the box it came out
61887
61944
  // of. Read at the close, not kept from the opening — see
@@ -61891,8 +61948,8 @@ const useDialogProps = props => {
61891
61948
  // See this prop's own JSDoc above for why a dialog does not follow its
61892
61949
  // trigger's box by default.
61893
61950
  sizeFromAnchor = false,
61894
- // Same meaning as Popover's own prop, applied to the only thing an anchor
61895
- // can do here: sizing under sizeFromAnchor.
61951
+ // Same meaning as Popover's own prop, applied to what an anchor does here:
61952
+ // sizing under sizeFromAnchor, and a lift.
61896
61953
  anchorCustomEventDetail = "override",
61897
61954
  // Makes the dialog itself a valid focus target so
61898
61955
  // autoFocus="last-resort" below has somewhere to land when it contains
@@ -61948,8 +62005,8 @@ const useDialogProps = props => {
61948
62005
  // narrower one) — and the positioned ancestor for a local one, where
61949
62006
  // reading 3% of the screen gives an absurd gap inside a small box.
61950
62007
  isTopLayer ? "3appw" : "3cqw");
61951
- // "expand || expandX", the shorthand semantics Popup used to apply before
61952
- // handing them over — the docked default only applies when neither was said
62008
+ // `expand` is the shorthand for both axes; the docked default only applies
62009
+ // when neither was said.
61953
62010
  const expandXUnset = expand === undefined && expandXProp === undefined;
61954
62011
  const expandX = expandXUnset ? isDocked && DOCKED.expandX : Boolean(expand) || Boolean(expandXProp);
61955
62012
  const scrollCapture = scrollCaptureProp ?? (isDocked ? DOCKED.scrollCapture : false);
@@ -62638,8 +62695,8 @@ const useDialogProps = props => {
62638
62695
  // ("[navi-control='dialog'], [navi-control='popover']" — see openEffect
62639
62696
  // above), and --navi-open/--navi-close resolve their target this way.
62640
62697
  "navi-control": "dialog",
62641
- // The protocol every command target answers. It came with the control
62642
- // group before; a dialog is layout and still has to answer --navi-open,
62698
+ // The protocol every command target answers: a dialog is layout, not a
62699
+ // control, and still has to answer --navi-open,
62643
62700
  // --navi-close and --navi-toggle, which are dispatched here and do not
62644
62701
  // bubble.
62645
62702
  "onnavi_command": e => {
@@ -62661,14 +62718,12 @@ const useDialogProps = props => {
62661
62718
  "popover": isTopLayerPopover ? "manual" : undefined,
62662
62719
  "baseClassName": "navi_dialog",
62663
62720
  "pseudoClasses": DIALOG_PSEUDO_CLASSES,
62664
- // Distinguishes the two renderers for the CSS above (position: fixed
62665
- // vs. absolute) — positioning itself is entirely JS-driven now (see
62666
- // openEffect's own positionDialog above), no data-position-area
62667
- // attribute needed at all.
62668
62721
  // A popup scrolls, and asking Box for that overflow is also what lets what
62669
62722
  // it contains claim header/footer/body (see box.jsx) — a popup is always a
62670
62723
  // scrolling area, so it says so once, here.
62671
62724
  "overflow": "auto",
62725
+ // Tells the two renderers apart for the CSS above (position: fixed vs.
62726
+ // absolute); the placement itself is JS-driven (positionDialog).
62672
62727
  "data-layer": layer,
62673
62728
  // The sheet shape is live in CSS, not just a set of resolved defaults:
62674
62729
  // it is what withdraws the caller's --dialog-max-width (see the stylesheet
@@ -63145,8 +63200,8 @@ const css$D = /* css */`
63145
63200
  * whole viewport. A real `anchor` works with either.
63146
63201
  * @param {string} [props.positionArea="bottom"] - Where to place the popover
63147
63202
  * relative to its `anchor` (or its container, if there is none). Same
63148
- * grammar as `Dialog`'s own `positionArea` (see `popup_shared.js`'s
63149
- * `parsePositionArea`): a single compass token — `top`/`top-start`/
63203
+ * grammar as `Dialog`'s own `positionArea` (see `parsePositionArea` in
63204
+ * @jsenv/dom): a single compass token — `top`/`top-start`/
63150
63205
  * `top-end`/`top-left`/`top-right`, `right`/`right-start`/`right-end`,
63151
63206
  * `bottom`/`bottom-start`/`bottom-end`/`bottom-left`/`bottom-right`,
63152
63207
  * `left`/`left-start`/`left-end`, or `center`. A bare token means no
@@ -63175,8 +63230,9 @@ const css$D = /* css */`
63175
63230
  * See docs/popup_backdrop.md.
63176
63231
  * @param {"close"|"cancel"|"capture"|"ignore"} [props.pressOutside="ignore"]
63177
63232
  * - `"ignore"` (default): no backdrop at all, outside presses pass straight
63178
- * through. `"close"` closes the popover on an outside press. `"capture"`
63179
- * absorbs the press (dims the backdrop) without closing. Note this
63233
+ * through. `"close"` closes the popover on an outside press, `"cancel"`
63234
+ * closes it as a cancel (what it holds reverts, as on Escape). `"capture"`
63235
+ * absorbs the press (blurred glass) without closing. Note this
63180
63236
  * default differs from `Dialog`'s own (`"close"`) — a popover is
63181
63237
  * typically a lightweight, non-modal affordance. "Outside" is the popover's
63182
63238
  * own border box; a see-through popover whose box is bigger than what it
@@ -63210,9 +63266,11 @@ const css$D = /* css */`
63210
63266
  * popover so the page/container behind it can't scroll while it's open.
63211
63267
  * @param {boolean} [props.focusCapture] - Traps Tab navigation inside the
63212
63268
  * popover (see `focus_trap.js`).
63213
- * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`} [props.animation]
63214
- * - `true`/`"auto"` resolves to a concrete `"slide-from-*"` direction
63215
- * based on `positionArea`. Any other explicit value is used as-is.
63269
+ * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`|"expanding"|`expand-${string}`} [props.animation]
63270
+ * - `true`/`"auto"` resolves to `"scaling"` for a real anchor or a centered
63271
+ * `positionArea`, and to a concrete `"slide-from-*"` direction otherwise.
63272
+ * `"expanding"`/`"expand-*"` grow out of a real anchor (see popup_css.js).
63273
+ * Any other explicit value is used as-is.
63216
63274
  * @param {string} [props.animationDuration] - Maps to
63217
63275
  * `--popup-animation-duration`.
63218
63276
  * @param {Element|{current: Element}|string} [props.anchor] - The element the
@@ -63324,7 +63382,7 @@ const css$D = /* css */`
63324
63382
  * @param {object} [props.openController] - Advanced: an externally-owned
63325
63383
  * open controller (see `open_controller.js`) for a caller that wants to
63326
63384
  * drive open/close itself instead of `open`/`defaultOpen`/`onClose` (used
63327
- * by `picker_custom.jsx`/`side_panel.jsx`).
63385
+ * by `picker_custom.jsx`).
63328
63386
  * @param {"always"|"idle"|"from-first-open"|"while-opened"} [props.mount] - When
63329
63387
  * `children` are built and thrown away (see popup_content_mount.js).
63330
63388
  * `"from-first-open"` (the default) builds them on the first open and keeps
@@ -63634,12 +63692,10 @@ const usePopoverProps = props => {
63634
63692
  unfreezeSize(popoverEl);
63635
63693
  }
63636
63694
  }, [sizing]);
63637
- // The custom renderer's own starting-hidden state is a stylesheet default
63638
- // now (&:not([popover]) { display: none } on .navi_popover/
63639
- // .navi_popover_backdrop above) rather than set here imperatively — a
63640
- // plain div has no native default the way [popover]/<dialog> do, so
63641
- // leaving this to a layout effect meant an actual (if narrow) window
63642
- // where the browser could paint it visible before this ever ran.
63695
+ // The custom renderer starts hidden from the stylesheet ([navi-hidden] on
63696
+ // .navi_popover/.navi_popover_backdrop above), never from a layout effect:
63697
+ // a plain div has no native hidden default the way [popover]/<dialog> do,
63698
+ // and an effect leaves a window where the browser paints it visible first.
63643
63699
  // aria-expanded starts "false" the same way, but via a static literal
63644
63700
  // JSX prop on contentProps/backdropProps below instead of a layout
63645
63701
  // effect — see that prop's own comment for why a *constant* value there
@@ -63658,8 +63714,8 @@ const usePopoverProps = props => {
63658
63714
  openController.getElement = () => ref.current;
63659
63715
  openController.openEffect = e => {
63660
63716
  const popoverEl = ref.current;
63661
- // backdropEl is null when pressOutside is "ignore" —
63662
- // the backdrop isn't rendered at all in that case.
63717
+ // backdropEl is null when there is no wall (pressOutside "ignore", or
63718
+ // backdrop={false}): the backdrop isn't rendered at all then.
63663
63719
  const backdropEl = backdropRef.current;
63664
63720
  if (!popoverEl) {
63665
63721
  return undefined;
@@ -63714,10 +63770,8 @@ const usePopoverProps = props => {
63714
63770
  // needed here.
63715
63771
  const positionedAncestor = getPositionedParent(popoverEl);
63716
63772
  // Drives the via-attribute renderer's own position: fixed/absolute
63717
- // switch (see this file's top comment) — set here, well before any
63718
- // positioning/measurement runs, so there's no ordering subtlety to get
63719
- // wrong (unlike the CSS this replaced, which keyed off the resolved
63720
- // animation instead, known too late relative to the first measurement).
63773
+ // switch (see this file's top comment) — set here, before any
63774
+ // positioning/measurement runs, so the first measurement already sees it.
63721
63775
  // True only for a real anchor that itself scrolls with the document —
63722
63776
  // that's the one case `absolute` (scrolling in lockstep with it) is
63723
63777
  // correct; not just "has an anchor at all", since an anchor that's
@@ -63767,10 +63821,9 @@ const usePopoverProps = props => {
63767
63821
  disarmBackdropHideRef.current?.();
63768
63822
  disarmBackdropHideRef.current = null;
63769
63823
  // transitionProperty stays "none" here for both — reset later, in the
63770
- // final commit step alongside popoverEl's own (not resumed early the
63771
- // way it briefly was), so a `silent` open (see above) can keep both
63772
- // elements' transitions suppressed right up until after their
63773
- // aria-expanded flip, the same way it does for popoverEl.
63824
+ // final commit step alongside popoverEl's own, so a `silent` open (see
63825
+ // above) can keep both elements' transitions suppressed right up until
63826
+ // after their aria-expanded flip, the same way it does for popoverEl.
63774
63827
  backdropEl.style.transitionProperty = "none";
63775
63828
  if (isTopLayer) {
63776
63829
  // Hidden first if a previous close's deferred hidePopover() (see
@@ -64195,16 +64248,10 @@ const usePopoverProps = props => {
64195
64248
  // current unconditionally on every call, and treats their mere
64196
64249
  // *presence* as "already positioned this session — stay on this
64197
64250
  // side unless it no longer fits" (its own anti-oscillation guard for
64198
- // repositioning mid-open, e.g. on scroll/resize). Popover's own CSS
64199
- // doesn't read these anymore (slide direction is resolved in JS now,
64200
- // see resolveDirectionValue), which is why the clearing that used to
64201
- // live here was removed — but that removal missed this second,
64202
- // still-live purpose: without clearing them on close, a later reopen
64203
- // sees a stale, "already positioned" value left over from a
64204
- // *previous* open (e.g. the container-aligned collapse from an
64205
- // anchorless silent open) and wrongly stays sticky to it instead of
64206
- // resolving fresh from the current positionArea/anchor — a real,
64207
- // reproduced bug, not just a leftover no-op.
64251
+ // repositioning mid-open, e.g. on scroll/resize). Cleared on close so a
64252
+ // later reopen resolves fresh from the current positionArea/anchor
64253
+ // instead of staying sticky to a side left over from a previous open
64254
+ // (e.g. the container-aligned collapse from an anchorless silent open).
64208
64255
  popoverEl.removeAttribute("data-position-y-current");
64209
64256
  popoverEl.removeAttribute("data-position-x-current");
64210
64257
  cleanup();
@@ -64334,8 +64381,8 @@ const usePopoverProps = props => {
64334
64381
  // ("[navi-control='dialog'], [navi-control='popover']"), and
64335
64382
  // --navi-open/--navi-close resolve their target this way.
64336
64383
  "navi-control": "popover",
64337
- // The protocol every command target answers. It came with the control
64338
- // group before; a popover is layout and still has to answer --navi-open,
64384
+ // The protocol every command target answers: a popover is layout, not a
64385
+ // control, and still has to answer --navi-open,
64339
64386
  // --navi-close and --navi-toggle, which are dispatched here and do not
64340
64387
  // bubble.
64341
64388
  "onnavi_command": e => {
@@ -64375,7 +64422,7 @@ const usePopoverProps = props => {
64375
64422
  // already arrive here as plain props via ...rest (wired by
64376
64423
  // UncontrolledPopover above, forwarded through ControlledPopover's own
64377
64424
  // {...props} spread) — nothing extra to add here. A controlled caller
64378
- // (picker_custom.jsx/side_panel.jsx) wires its own equivalent handling
64425
+ // (picker_custom.jsx) wires its own equivalent handling
64379
64426
  // directly against its own openController instead.
64380
64427
  });
64381
64428
  return [hasBackdrop ? backdropProps : null, contentProps];
@@ -64407,9 +64454,6 @@ const POPUP_STYLE_CSS_VARS = {
64407
64454
  maxHeight: "--popover-max-height"
64408
64455
  };
64409
64456
 
64410
- // parsePositionArea/POSITION_AREA_X/Y_VALUES moved to popup_shared.js — same
64411
- // grammar Dialog's own layer="local"/"top" now shares.
64412
-
64413
64457
  /**
64414
64458
  * Shared by both renderers: parses `positionArea` and resolves
64415
64459
  * `animation="auto"`/`true`. `animationAnchor` is the real anchor element
@@ -64437,15 +64481,6 @@ const resolvePositionAreaAndAnimationKind = ({
64437
64481
  resolvedAnimationKind
64438
64482
  };
64439
64483
  };
64440
-
64441
- // resolveDirectionValue/resolveAutoAnimationKind moved to popup_shared.js —
64442
- // same logic Dialog's own auto-animation resolution now shares, since it
64443
- // never has a real anchor either (always the "anchor === undefined" path).
64444
-
64445
- // suppressPointerEventsDuringTransition/armPointerDownOutsideClose moved to
64446
- // popup_shared.js — same helpers Dialog's own custom renderer needs, no
64447
- // Popover-specific knowledge in either.
64448
-
64449
64484
  Popover.Close = PopupClose;
64450
64485
 
64451
64486
  installImportMetaCssBuild(import.meta);
@@ -64484,11 +64519,12 @@ const css$C = /* css */`@layer navi {
64484
64519
  * @param {"top"|"local"} [props.layer] - Forwarded as-is to whichever of
64485
64520
  * `Dialog`/`Popover` renders — see either component's own doc.
64486
64521
  * @param {Element|{current: Element}} [props.anchor] - Forwarded as-is —
64487
- * positioning for `Popover`, and for `Dialog` sizing only, and only when
64488
- * `sizeFromAnchor` is also passed (see each component's own doc).
64522
+ * positioning for `Popover`; for `Dialog`, the box it sizes from under
64523
+ * `sizeFromAnchor` and lifts out of under `animation="lifting"`. In both,
64524
+ * intent on the anchor warms the content (see `mount`).
64489
64525
  * @param {"override"|"ignore"} [props.anchorCustomEventDetail] - Forwarded
64490
64526
  * as-is to both — what it governs differs (positioning for `Popover`,
64491
- * `sizeFromAnchor` sizing for `Dialog`), but "ignore whatever anchor the
64527
+ * sizing and lifting for `Dialog`), but "ignore whatever anchor the
64492
64528
  * triggering event carried" has to mean the same thing in either mode.
64493
64529
  * @param {string} [props.marginWithAnchor] - **Popover-only**, destructured
64494
64530
  * out so it can't leak onto the real `<dialog>` element as a stray DOM
@@ -64518,8 +64554,9 @@ const css$C = /* css */`@layer navi {
64518
64554
  * the screen-size check happens to pick, which defeats the point of
64519
64555
  * having one shared API in the first place. Note this only says what a
64520
64556
  * press outside *does*; whether it reaches the page at all is `backdrop`
64521
- * below, and `"ignore"`/`"capture"` describe a wall either way — the popup
64522
- * absorbs the press without closing, dimmed or not.
64557
+ * below. `"capture"` is a wall that absorbs the press without closing, in
64558
+ * either mode. `"ignore"` is not the same in both: a `Dialog` keeps its
64559
+ * wall, a `Popover` lays none and the press reaches the page.
64523
64560
  * @param {boolean} [props.backdrop] - Whether anything is laid between the
64524
64561
  * popup and the page at all: `false` lets a press outside both dismiss the
64525
64562
  * popup and reach whatever it landed on, in one gesture. Forwarded as-is and
@@ -64559,11 +64596,15 @@ const css$C = /* css */`@layer navi {
64559
64596
  * `--x-popover-max-width`) instead of its content width — same meaning
64560
64597
  * whichever mode the screen-size resolution picks.
64561
64598
  * @param {boolean} [props.expandY] - Same, vertically (`data-expand-y`).
64562
- * @param {boolean} [props.scrollCapture] - Forwarded as-is.
64599
+ * @param {boolean|"dialog"|"popover"} [props.scrollCapture] - Forwarded to
64600
+ * both; a mode name traps scroll only when that mode is the one rendered.
64563
64601
  * @param {boolean} [props.open] - Forwarded as-is (controlled).
64564
- * @param {import("@preact/signals").Signal<boolean>} [props.signal] -
64602
+ * @param {import("@preact/signals").Signal<any>} [props.signal] -
64565
64603
  * Forwarded as-is: one binding to both drive the popup's open state and
64566
64604
  * know where it is (see `Dialog`/`Popover`'s own `signal`).
64605
+ * @param {any} [props.value] - Forwarded as-is: what `signal` holds while
64606
+ * THIS popup is open, for several sharing one signal (see
64607
+ * `Dialog`/`Popover`'s own `value`).
64567
64608
  * @param {boolean} [props.defaultOpen] - Forwarded as-is (uncontrolled,
64568
64609
  * mount-only).
64569
64610
  * @param {(event: Event) => void} [props.onClose] - Forwarded as-is.
@@ -64578,6 +64619,8 @@ const css$C = /* css */`@layer navi {
64578
64619
  * them afterwards. `"always"` builds them right away, for content something
64579
64620
  * depends on while the popup is still closed: a value read off it, fields a
64580
64621
  * surrounding form collects on submit, a size measured from outside.
64622
+ * `"idle"` builds them in a browser idle moment after load — `"always"`
64623
+ * minus the cost on the critical render.
64581
64624
  * `"while-opened"` throws them away once the popup has finished closing, for
64582
64625
  * content whose fresh state is its initial state: an uncontrolled field
64583
64626
  * seeded from a `defaultValue` that changed while the popup was closed.
@@ -65163,7 +65206,7 @@ const PickerCustom = props => {
65163
65206
  // Not on pickerProps (the trigger): commands.js's own
65164
65207
  // resolveClosestExpandable() does `el.closest("[aria-expanded]")` to
65165
65208
  // find where to dispatch navi_request_open/navi_request_close — and
65166
- // the popup itself now carries its own aria-expanded (see
65209
+ // the popup itself carries its own aria-expanded (see
65167
65210
  // popover.jsx/dialog.jsx), which is *closer* than the picker's own
65168
65211
  // aria-expanded for anything dispatched from inside the popup's own
65169
65212
  // content (e.g. a `command="--navi-close"` button rendered as
@@ -65442,12 +65485,8 @@ const PickerContentInsidePopup = props => {
65442
65485
  mode,
65443
65486
  pointerLock,
65444
65487
  scrollCapture,
65445
- // No default here (matches Popover's own default of inactive) — the
65446
- // old, differently-named `focusTrap = true` prop never actually reached
65447
- // Popover's real `focusCapture` prop (see this file's history), so
65448
- // focus-trapping has never really been active for popover-mode pickers;
65449
- // defaulting the now-correctly-named prop to `true` would be a real,
65450
- // unintended behavior change riding along with the rename.
65488
+ // No default here: Popover's own `focusCapture` is inactive by default,
65489
+ // and a popover-mode picker traps the focus only when asked to.
65451
65490
  focusCapture,
65452
65491
  // Popup documents its own `layer` as forwarded as-is to Dialog/Popover,
65453
65492
  // but popupProps is built explicitly here, so it only travels if named.
@@ -66084,6 +66123,8 @@ document.addEventListener(
66084
66123
  { capture: true, passive: true },
66085
66124
  );
66086
66125
 
66126
+ const FRAME_TIMEOUT_MS = 100;
66127
+
66087
66128
  /**
66088
66129
  * Calls back once the browser has painted what is committed now.
66089
66130
  *
@@ -66095,10 +66136,9 @@ document.addEventListener(
66095
66136
  * has to survive. A frame not coming at all (a background tab) still answers,
66096
66137
  * late, through the timeout.
66097
66138
  *
66139
+ * @param {() => void} callback
66098
66140
  * @returns {() => void} cancel
66099
66141
  */
66100
- const FRAME_TIMEOUT_MS = 100;
66101
-
66102
66142
  const afterPaint = (callback) => {
66103
66143
  let called = false;
66104
66144
  let timeoutId;
@@ -67645,7 +67685,7 @@ const SELECTABLE_INPUT_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-
67645
67685
  const SelectableInputProxy = props => {
67646
67686
  const selectableRealInputProps = useContext(SelectableRealInputContext);
67647
67687
  if (!selectableRealInputProps) {
67648
- throw new Error("Selectable.Input must be used within a Selectable component");
67688
+ throw new Error("SelectableInput must be used within a row of a selectable List");
67649
67689
  }
67650
67690
 
67651
67691
  // Reset FieldToInterfaceContext to ensure we don't read id or report our
@@ -67683,7 +67723,7 @@ const SelectableInput = SelectableInputProxy;
67683
67723
  * background-color: var(--search-match-background-color);
67684
67724
  * }
67685
67725
  *
67686
- * The `highlight` prop can be:
67726
+ * The `highlight` argument of `useSearchHighlight` (a row's `matchInfo.matchRanges`) can be:
67687
67727
  * - an array of [start, end] pairs — applied to all text nodes under the root element
67688
67728
  * - an object { [domSelector]: [[start, end], …] } — applied to each sub-element
67689
67729
  * matched by the selector (the format produced by createSearch)
@@ -71129,8 +71169,8 @@ const LIST_ITEM_STYLE_CSS_VARS = {
71129
71169
  *
71130
71170
  * All three are about the row as a thing the LIST holds, never about the
71131
71171
  * selection: a selectable row taken while its list sends says so on its own
71132
- * ("la sélection est en cours d'enregistrement"), and needs no `loading` for
71133
- * that.
71172
+ * ("le choix est en cours d'enregistrement", "la sélection…" in a `multiple`
71173
+ * list), and needs no `loading` for that.
71134
71174
  * @param {boolean} [props.readOnly]
71135
71175
  * The row cannot be acted on: dimmed and click-through-proof, buttons inside
71136
71176
  * it included.
@@ -72216,9 +72256,12 @@ const useItemStore = ({
72216
72256
  * (accessible via aria-labelledby) and a <ul role="group"> for the items.
72217
72257
  *
72218
72258
  * Props:
72219
- * label — group label content
72220
- * labelProps — props forwarded to the label <span>
72221
- * ...rest — forwarded to the outer <li role="presentation">
72259
+ * label — group label content
72260
+ * labelProps — props forwarded to the label <span>
72261
+ * hiddenWhileEmpty — the group leaves the flow (`display: none`) while it
72262
+ * holds no real row — a search that emptied it, rows not
72263
+ * arrived yet
72264
+ * ...rest — forwarded to the outer <li role="presentation">
72222
72265
  */
72223
72266
  const ListItemGroup = ({
72224
72267
  label,
@@ -72330,7 +72373,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
72330
72373
  * uiAction?: (value: any) => void,
72331
72374
  * popover?: boolean,
72332
72375
  * role?: string,
72333
- * renderBudget?: number | string,
72376
+ * renderBudget?: number | string | {initial?: number, after: number},
72334
72377
  * renderBudgetSkipCheck?: boolean,
72335
72378
  * virtualItemSize?: number,
72336
72379
  * onListVisibleItemsChange?: (visibleItems: any[]) => void,
@@ -72359,6 +72402,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
72359
72402
  * alignX?: string,
72360
72403
  * alignY?: string,
72361
72404
  * flexWrap?: boolean,
72405
+ * overflow?: string,
72362
72406
  * expandX?: boolean,
72363
72407
  * expandY?: boolean,
72364
72408
  * expand?: boolean,
@@ -72545,10 +72589,10 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
72545
72589
  * @param {number} [props.parallelGuard=4]
72546
72590
  * How many runs the rows may have in flight at once, for a list whose rows
72547
72591
  * carry their own `action` (a button per row). While that many are out, every
72548
- * row that is not running goes read-only and says how many it is waiting on;
72549
- * the next press is possible again as soon as one comes back. `Infinity`
72550
- * lifts it. Counts runs, not values — `maxLengthGuard` above is the one that
72551
- * says how many things the selection may hold.
72592
+ * control that would start another run goes read-only and says how many it is
72593
+ * waiting on; the next press is possible again as soon as one comes back.
72594
+ * `Infinity` lifts it. Counts runs, not values — `maxLengthGuard` above is
72595
+ * the one that says how many things the selection may hold.
72552
72596
  * @param {boolean} [props.standalone]
72553
72597
  * This list answers for itself: it does not register with the control group
72554
72598
  * or picker around it, so its selection stays out of that value and nothing
@@ -75275,7 +75319,7 @@ const TRAVELS_NONE = [];
75275
75319
  * fallbackValue?: any,
75276
75320
  * valueAtStep: (value: any, count: number) => any,
75277
75321
  * compareValues?: (a: any, b: any) => number,
75278
- * renderValue?: (value: any) => import("ignore:preact").ComponentChildren,
75322
+ * renderValue?: (value: any, context: { maxLines?: number }) => import("ignore:preact").ComponentChildren,
75279
75323
  * controlProps?: object,
75280
75324
  * duration?: number,
75281
75325
  * vertical?: boolean,
@@ -76750,7 +76794,8 @@ const css$q = /* css */`.navi_input_duration {
76750
76794
  * @param {boolean} [props.required]
76751
76795
  * @param {boolean} [props.loading]
76752
76796
  * @param {Function} [props.uiAction] - Called on every change with the ISO 8601 value
76753
- * @param {Function} [props.action] - Called on form submission
76797
+ * @param {Function} [props.action] - Runs with the ISO 8601 value when it changes,
76798
+ * like any control's `action` (busy state, error callout)
76754
76799
  * @param {preact.ComponentChild} [props.unitHour] - Custom label for the hour sub-field
76755
76800
  * @param {"auto"|"left"|"center"|"right"} [props.textAlign="auto"] - Text alignment of sub-inputs.
76756
76801
  * "auto" aligns each field toward its neighbouring separator (first→right, last→left, middle/solo→center).
@@ -77758,11 +77803,12 @@ const splitPopupProps = props => {
77758
77803
  * multi-word, one word at start (all match) 0.75
77759
77804
  * phrase / word at word boundary 0.625
77760
77805
  * phrase / words mid-word 0.5
77806
+ * acronym (2+ chars, each the first letter of a word, in order) 0.4, 0.45 at start
77761
77807
  * + case-exact bonus +0.125
77762
77808
  * multi-word partial: score × (matched/total)
77763
77809
  *
77764
77810
  * matchRanges: [start, end] pairs (exclusive end) for CSS Highlight API.
77765
- * Intended to be passed to useSearch as the matchFn parameter.
77811
+ * Intended to be passed to useSearchText as the matchFn parameter.
77766
77812
  */
77767
77813
  const applySearch = (searchText, value) => {
77768
77814
  if (!searchText) {
@@ -77972,7 +78018,7 @@ const mergeRanges = (ranges) => {
77972
78018
  };
77973
78019
 
77974
78020
  /**
77975
- * createSearch — builds a matchFn compatible with useSearch that searches
78021
+ * createSearch — builds a matchFn compatible with useSearchText that searches
77976
78022
  * across multiple named fields of an item, each with its own DOM selector
77977
78023
  * and optional priority weight.
77978
78024
  *
@@ -77990,7 +78036,7 @@ const mergeRanges = (ranges) => {
77990
78036
  * },
77991
78037
  * });
77992
78038
  *
77993
- * const [orderedItems, getItemMatchInfo] = useSearch(search, items, searchPerson);
78039
+ * const [orderedItems, getItemMatchInfo] = useSearchText(search, items, searchPerson);
77994
78040
  * // getItemMatchInfo(item).matchRanges is { ".name": [[start,end],…], ".address": [[start,end],…] }
77995
78041
  * // Pass the whole thing: <ListItem matchInfo={getItemMatchInfo(item)} />
77996
78042
  * // — ListItem handles the per-selector object format for matchRanges.
@@ -78057,7 +78103,7 @@ const useSearchText = (searchText, items, matchFn = applySearch) => {
78057
78103
  );
78058
78104
  }
78059
78105
  if (items === undefined) {
78060
- throw new TypeError("useSearch: items is undefined");
78106
+ throw new TypeError("useSearchText: items is undefined");
78061
78107
  }
78062
78108
  const { orderedItems, matchInfoMap } = useMemo(() => {
78063
78109
  const { scoreEntries, nonMatched, matchInfoMap } = buildMatchInfo(
@@ -78182,17 +78228,19 @@ const buildMatchInfo = (searchText, items, matchFn) => {
78182
78228
  * - orderedKeys: number[] of visible item keys sorted by explicit order
78183
78229
  * - keyToOrderedIndex: Map key → orderedKeys index, gives O(1) indexOf equivalent
78184
78230
  * - keyToExplicitOrder: Map key → explicitly passed index, used to maintain sort order
78185
- * - allItemsSignal: signal(array), all items including hidden, ordered by explicit index
78186
- * - visibleItemsSignal: signal(array), non-hidden items only
78231
+ * - itemsSignal: signal(array), all items including hidden, ordered by explicit index
78232
+ * - visibleItemsSignal: signal(array), items neither hidden nor filtered
78187
78233
  * - countSignal: signal(number), count of all items including hidden
78188
78234
  * - visibleCountSignal: signal(number), updated in microtask batch, only when count changes
78189
- * - propSignals: Map propName → signal(array), updated in microtask batch with element equality
78235
+ * - noMatchCountSignal: signal(number), items registered with `match: false`
78190
78236
  * - onChangeRef: holds the latest onChange callback, called once per microtask batch
78191
78237
  *
78192
- * useTrackItem(id, data, index): registers the item with an explicitly provided index
78193
- * that determines its position among siblings. The caller (e.g. items.map) knows the
78194
- * correct order and passes it directly — no render-sequence deduction needed.
78195
- * Returns the visible rank (position among non-hidden items), or -1 when hidden.
78238
+ * useTrackItem(data): registers the item under `data.id`, at the position
78239
+ * `data.index` gives it among its siblings. The caller (e.g. items.map) knows
78240
+ * the correct order and passes it directly — no render-sequence deduction
78241
+ * needed. `data.role === "presentation"` keeps it out entirely.
78242
+ * Returns the visible rank (position among visible items), or -1 when hidden
78243
+ * or filtered.
78196
78244
  * Signals and onChange are deferred to a microtask so multiple items updating
78197
78245
  * in one commit cause only one notification.
78198
78246
  *
@@ -78960,7 +79008,7 @@ const WheelGroupContext = createContext(null);
78960
79008
  * [key: string]: any,
78961
79009
  * }>}
78962
79010
  * @param {number} [props.visibleCount=3] - Odd number of rows visible in the viewport (the center one is the selection).
78963
- * @param {number|string} [props.itemHeight] - Main-axis size of a row when vertical (number = px). Defaults to the CSS var (2.4em).
79011
+ * @param {number|string} [props.itemHeight] - Main-axis size of a row when vertical (number = px). Defaults to the CSS var (1.8em).
78964
79012
  * @param {number|string} [props.itemWidth] - Main-axis size of a cell when horizontal (number = px). Defaults to the CSS var (3.5ch).
78965
79013
  * @param {number|string} [props.size] - Font size of the wheel: a size token ("s", "m", "l", "xl", …), a number (px), or a CSS length. Scales the digits and, since the row size is em-based, the row height too — a simple way to make the whole wheel bigger. An explicit itemHeight/itemWidth still overrides the row size.
78966
79014
  * @param {boolean} [props.bounded] - Give the wheel fixed ends instead of wrapping: it stops at the first/last value. By default the wheel loops endlessly (past the last value the first reappears, and vice-versa).
@@ -80448,7 +80496,7 @@ Wheel.Item = WheelItem;
80448
80496
  * @param {boolean} [props.horizontal] - Stack the (horizontal) wheels vertically instead of in a row.
80449
80497
  * @param {boolean} [props.glass] - Frost every wheel's neighbouring rows (see Wheel's glass prop) with one prop for the whole group.
80450
80498
  * @param {boolean} [props.frameBorder] - Line every wheel's center-window edges with a faint frame (off by default; independent of glass).
80451
- * @param {boolean} [props.zoom] - Enlarge the centered value of every wheel (see Wheel's zoom prop) with one prop for the whole group.
80499
+ * @param {boolean|number} [props.zoom] - Enlarge the centered value of every wheel (see Wheel's zoom prop) with one prop for the whole group.
80452
80500
  */
80453
80501
  const WheelGroup = props => {
80454
80502
  import.meta.css = [css$m, "@jsenv/navi/src/control/wheel/wheel.jsx"];
@@ -81233,8 +81281,8 @@ const stringifyTableSelectionValue = (type, value) => {
81233
81281
 
81234
81282
  /**
81235
81283
  * Check if a specific cell is selected
81236
- * @param {Array} selection - The selection set or array
81237
- * @param {{rowIndex: number, columnIndex: number}} cellPosition - Cell coordinates
81284
+ * @param {string[]} selection - The table's `selection` array
81285
+ * @param {{columnId: string, rowId: string}} cell - The cell's column and row ids
81238
81286
  * @returns {boolean} True if the cell is selected
81239
81287
  */
81240
81288
  const isCellSelected = (selection, { columnId, rowId }) => {
@@ -81247,8 +81295,8 @@ const isCellSelected = (selection, { columnId, rowId }) => {
81247
81295
 
81248
81296
  /**
81249
81297
  * Check if a specific row is selected
81250
- * @param {Array} selection - The selection set or array
81251
- * @param {number} rowIndex - Row index
81298
+ * @param {string[]} selection - The table's `selection` array
81299
+ * @param {string} rowId - The row's id (the `id` of its `<Tr>`)
81252
81300
  * @returns {boolean} True if the row is selected
81253
81301
  */
81254
81302
  const isRowSelected = (selection, rowId) => {
@@ -81258,8 +81306,8 @@ const isRowSelected = (selection, rowId) => {
81258
81306
 
81259
81307
  /**
81260
81308
  * Check if a specific column is selected
81261
- * @param {Array} selection - The selection set or array
81262
- * @param {number} columnIndex - Column index
81309
+ * @param {string[]} selection - The table's `selection` array
81310
+ * @param {string} columnId - The column's id (the `id` of its `<Col>`)
81263
81311
  * @returns {boolean} True if the column is selected
81264
81312
  */
81265
81313
  const isColumnSelected = (selection, columnId) => {
@@ -81794,7 +81842,7 @@ const initDragTableColumnViaPointer = (pointerdownEvent, {
81794
81842
  /**
81795
81843
  * Creates a MutationObserver that syncs attribute changes from original table to clone
81796
81844
  * @param {HTMLElement} table - The original table element
81797
- * @param {HTMLElement} cloneTable - The cloned table element
81845
+ * @param {HTMLElement} tableClone - The cloned table element
81798
81846
  * @returns {MutationObserver} The observer instance with disconnect method
81799
81847
  */
81800
81848
  const createTableAttributeSync = (table, tableClone) => {
@@ -83759,9 +83807,10 @@ const TableUI = /*#__PURE__*/forwardRef((props, ref) => {
83759
83807
  * KEY PRINCIPLES:
83760
83808
  * - Use inset box-shadow to ensure borders appear above table cell backgrounds
83761
83809
  * - Use ::before pseudo-elements with position: absolute for flexible positioning
83762
- * - Each cell draws its own borders independently (no border-collapse by default)
83810
+ * - Each cell draws its own borders; `borderCollapse` (on by default) keeps only
83811
+ * the right and bottom ones, plus top on the first row and left on the first column
83763
83812
  * - Selection borders override table borders using higher CSS specificity
83764
- * - Sticky borders use thicker box-shadows in accent color (yellow)
83813
+ * - Sticky borders use thicker box-shadows in an accent color
83765
83814
  *
83766
83815
  * TECHNICAL IMPLEMENTATION:
83767
83816
  * - All borders use inset box-shadow with specific directional mapping:
@@ -83769,9 +83818,8 @@ const TableUI = /*#__PURE__*/forwardRef((props, ref) => {
83769
83818
  * * Right: inset -1px 0 0 0
83770
83819
  * * Bottom: inset 0 -1px 0 0
83771
83820
  * * Left: inset 1px 0 0 0
83772
- * - Selection borders (blue) override table borders (red) in same pseudo-element
83821
+ * - Selection borders override table borders in the same pseudo-element
83773
83822
  * - Sticky borders replace regular borders with thicker colored variants
83774
- * - Border-collapse mode available as optional feature for future use
83775
83823
  *
83776
83824
  * Note how border disappear for sticky elements when using border-collapse (https://bugzilla.mozilla.org/show_bug.cgi?id=1727594)
83777
83825
  *
@@ -84905,11 +84953,18 @@ const BadgeCountOverflow = () => jsx("span", {
84905
84953
  });
84906
84954
  const MAX_CHAR_AS_CIRCLE = 3;
84907
84955
  const MAX_FOR_CIRCLE = 99;
84956
+
84957
+ /**
84958
+ * @param {boolean} [circle] Drawn round rather than as a pill. A circle holds
84959
+ * three characters at most, so `max` defaults to 99 with it ("99+"); a `max`
84960
+ * above 99, or `"none"`, lets a bigger count turn the circle into a pill as
84961
+ * it grows.
84962
+ * @param {number|"none"} [max] The count above which `max` and a "+" are
84963
+ * written in its place. 99 under `circle`, no cap otherwise.
84964
+ */
84908
84965
  const BadgeCount = ({
84909
84966
  children,
84910
84967
  maxElement = jsx(BadgeCountOverflow, {}),
84911
- // When you use max="none" (or max > 99) it might be a good idea to force ellipse
84912
- // so that visually the interface do not suddently switch from circle to ellipse depending on the count
84913
84968
  circle,
84914
84969
  max = circle ? MAX_FOR_CIRCLE : Infinity,
84915
84970
  integer,
@@ -86092,12 +86147,13 @@ const css$7 = /* css */`.navi_text_box {
86092
86147
  * - Displays optional icon(s) before and/or after the text
86093
86148
  * - Shrinks its width to fit the longest rendered text line (no trailing blank space)
86094
86149
  * - Wraps long text by default (overflow-wrap: anywhere)
86095
- * - Shows ellipsis for a single overflowing unwrappable line
86150
+ * - Shows an ellipsis instead under `singleLine`
86096
86151
  *
86097
86152
  * Props:
86098
86153
  * iconBefore — element shown to the left of the text
86099
86154
  * iconAfter — element shown to the right of the text (stays on the same line)
86100
- * maxHeight — CSS max-height string; when set, content that cannot wrap gets ellipsis
86155
+ * singleLine — keeps the text on one line, cut with an ellipsis (no width shrinking)
86156
+ * maxHeight — CSS max-height string; content past it is clipped
86101
86157
  * children — the text content
86102
86158
  */
86103
86159
  const TextBox = ({
@@ -86276,6 +86332,41 @@ const css$4 = /* css */`.svg_mask_content * {
86276
86332
  stroke-opacity: 1 !important;
86277
86333
  }
86278
86334
  `;
86335
+
86336
+ /**
86337
+ * SVGMaskOverlay
86338
+ *
86339
+ * Creates composite SVGs by combining independent SVG elements with masking.
86340
+ *
86341
+ * This component solves the challenge of combining independently created SVGs into
86342
+ * a single visual composition. Each SVG can have its own coordinate system, viewBox,
86343
+ * and styling, allowing for maximum reusability of individual icons or graphics.
86344
+ *
86345
+ * When overlaying SVGs, each subsequent overlay "cuts out" its portion from the base SVG,
86346
+ * creating a seamless integration where SVGs appear to interact with each other visually.
86347
+ *
86348
+ * Key benefits:
86349
+ * - Maintains each SVG's independence - use them individually elsewhere
86350
+ * - Handles different viewBox dimensions automatically
86351
+ * - Works with any SVG components regardless of internal implementation
86352
+ * - Supports unlimited overlay elements
86353
+ * - Creates proper masking between elements for visual integration
86354
+ *
86355
+ * Usage example combining two independent icon components:
86356
+ * ```jsx
86357
+ * <SVGMaskOverlay viewBox="0 0 24 24">
86358
+ * <DatabaseSvg />
86359
+ * <svg x="12" y="12" width="16" height="16" overflow="visible">
86360
+ * <PlusSvg />
86361
+ * </svg>
86362
+ * </SVGMaskOverlay>
86363
+ * ```
86364
+ *
86365
+ * @param {Object} props - Component properties
86366
+ * @param {string} props.viewBox - The main viewBox for the composition (required as soon as there is an overlay)
86367
+ * @param {import("ignore:preact").ComponentChildren} props.children - SVG elements (first is base, rest are overlays)
86368
+ * @returns {import("ignore:preact").VNode} A composed SVG with all elements properly masked
86369
+ */
86279
86370
  const SVGMaskOverlay = ({
86280
86371
  viewBox,
86281
86372
  children
@@ -86288,7 +86379,7 @@ const SVGMaskOverlay = ({
86288
86379
  return children[0];
86289
86380
  }
86290
86381
  if (!viewBox) {
86291
- console.error("SVGComposition requires an explicit viewBox");
86382
+ console.error("SVGMaskOverlay requires an explicit viewBox");
86292
86383
  return null;
86293
86384
  }
86294
86385
 
@@ -87229,7 +87320,8 @@ const css = /* css */`.navi_side_panel {
87229
87320
  * interaction only makes sense paired with not letting focus silently
87230
87321
  * leave the panel first. A box of the page whose press must not close the
87231
87322
  * panel (a card that fills it) names the panel:
87232
- * `data-navi-popup-inside={id}` — see docs/popup_backdrop.md.
87323
+ * `data-navi-popup-inside={id}`, read when the panel has no wall
87324
+ * (`backdrop={false}`) — see docs/popup_backdrop.md.
87233
87325
  * @param {boolean} [props.closeByDrag=true] - Pushing the panel back
87234
87326
  * towards the edge it is docked to closes it: the panel follows the
87235
87327
  * pointer and finishes leaving (or comes back to rest) when it is
@@ -87244,8 +87336,8 @@ const css = /* css */`.navi_side_panel {
87244
87336
  * doc).
87245
87337
  * @param {import("ignore:preact").ComponentChildren} props.children - No built-in
87246
87338
  * close button — add one wherever it makes sense for the layout (e.g. a
87247
- * plain `<Button command="--navi-close">`), use `SidePanel.Head`'s own
87248
- * `closeButton` prop, or rely on `closeByPressOutside`/Escape instead.
87339
+ * plain `<Button command="--navi-close">` in `SidePanel.Head`), or rely on
87340
+ * `closeByPressOutside`/Escape instead.
87249
87341
  * A form sent inside the panel closes it, as in any popup: a panel one
87250
87342
  * keeps editing in says `command="--navi-void"` on that form (see
87251
87343
  * docs/form_changed.md).
@@ -87336,9 +87428,7 @@ const toCssLength = (value, propertyName) => value === undefined || value === nu
87336
87428
  * Stuck to the top of the panel's own scrollable area (`position: sticky`)
87337
87429
  * regardless of `side` — only the panel's content in between scrolls. No
87338
87430
  * built-in padding or close button — add a `<Button command="--navi-close">`
87339
- * (optionally with the `"navi_side_panel_head_close_button"` className, a
87340
- * float-right utility this file's own CSS still provides) wherever it makes
87341
- * sense for the layout.
87431
+ * wherever it makes sense for the layout.
87342
87432
  *
87343
87433
  * @param {object} props
87344
87434
  * @param {string} [props.className] - Merged with the shared