@jsenv/navi 0.29.477 → 0.29.479

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.
@@ -3,12 +3,12 @@
3
3
  * using @jsenv/navi as intended.
4
4
  */
5
5
  import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
- import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, getScrollContainer, watchWheelTravel, scrollRoomTowards, trapScrollInside, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, overflowGoesToViewport, parsePositionArea, snapToPixel, trapFocusInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange, getVirtualKeyboardOverlayHeight } from "@jsenv/dom";
6
+ import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, getScrollContainer, watchWheelTravel, scrollRoomTowards, trapScrollInside, scrollIntoViewScoped, isPressDisputedByDrag, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, overflowGoesToViewport, parsePositionArea, snapToPixel, trapFocusInside, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange, getVirtualKeyboardOverlayHeight } from "@jsenv/dom";
7
7
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
8
8
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
9
9
  import { options, isValidElement, createContext, render as render$1, h, toChildArray, createRef, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
10
10
  import { useErrorBoundary, useLayoutEffect, useContext, useState, useMemo, useRef, useReducer, useEffect, useCallback, useId } from "preact/hooks";
11
- import { humanizeI18n, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
11
+ import { humanizeI18n, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatMinuteDuration, formatTime, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
12
12
  export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
13
13
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
14
14
  import { CHAR_CLASS_PRESETS, createValidity, durationContainsNaN, compareTwoDurations, TYPE_RULE, durationToSeconds, resolveCharClass, getCharClassMessageKey, compileCharClassAnchored, compileCharClass, DISPLAYABLE_RULE, MAX_LINE_BREAKS_RULE, NO_EMOJI_RULE, SINGLE_SPACE_RULE, parseDuration, durationToISOString } from "@jsenv/validity";
@@ -3873,6 +3873,11 @@ const createAction = (callback, rootOptions = {}) => {
3873
3873
  * asks nothing.
3874
3874
  */
3875
3875
  const bindParams = (newParamsOrSignal, options = {}) => {
3876
+ if (newParamsOrSignal === null) {
3877
+ // A signal's `null` becomes `undefined` in mergeActionParams; a static
3878
+ // one never goes through it, so it is said the same way here.
3879
+ newParamsOrSignal = undefined;
3880
+ }
3876
3881
  const { debounce, ...optionsWithoutDebounce } = options;
3877
3882
  if (debounce) {
3878
3883
  if (!isSignal(newParamsOrSignal)) {
@@ -4460,7 +4465,9 @@ const createActionProxyFromSignal = (
4460
4465
  const inheritsAnswer =
4461
4466
  inheritData && getActionPrivateProperties(action).inheritData;
4462
4467
  const createTarget = (params) => {
4463
- if (inheritsAnswer) {
4468
+ // `undefined` is "nothing to ask": no answer is coming to replace a stand-in,
4469
+ // and that one instance is shared by every binding of the action.
4470
+ if (inheritsAnswer && params !== undefined) {
4464
4471
  const previousActionTarget = actionTargetPreviousWeakRef?.deref();
4465
4472
  const previousTarget = previousActionTarget || action;
4466
4473
  return action.bindParams(params, {
@@ -6274,9 +6281,11 @@ const CONTROL_ATTRIBUTE_SET = new Set([
6274
6281
  "spellcheck",
6275
6282
  "autoCorrect",
6276
6283
  "aria-controls",
6277
- // The name goes where the role is: the root box has none, so an aria-label
6278
- // left on it names nothing at all, while the host is what the user focuses
6279
- // and what a screen reader announces (and what getByRole({ name }) reads).
6284
+ // The name goes where the role is: for most controls the root box has none,
6285
+ // so an aria-label left on it names nothing at all, while the host is what
6286
+ // the user focuses and what a screen reader announces (and what
6287
+ // getByRole({ name }) reads). A control whose role is on its root says so
6288
+ // with `nameOn: "root"` (see useControlProps).
6280
6289
  "aria-label",
6281
6290
  "aria-labelledby",
6282
6291
  "tabIndex",
@@ -6803,7 +6812,7 @@ const useDebugUIState = useDebugNoop;
6803
6812
  * @param {boolean|Function} [props.debugPopup] - Popover/dialog open/close/positioning.
6804
6813
  * @param {boolean|Function} [props.debugAction] - Action lifecycle.
6805
6814
  * @param {boolean|Function} [props.debugUIState] - UI-state transitions and validation.
6806
- * @param {import("ignore:preact").ComponentChildren} props.children
6815
+ * @param {import("preact").ComponentChildren} props.children
6807
6816
  *
6808
6817
  * @example
6809
6818
  * // Log everything under this subtree
@@ -7020,9 +7029,11 @@ const generateSignalId = () => {
7020
7029
  * @param {Array} [options.oneOf] - Array of valid values for validation. Signal will be marked invalid if value is not in this array
7021
7030
  * @param {boolean} [options.autoFix=false] - Hold the repaired value instead of the refused one: a number
7022
7031
  * clamped to `min`/`max` and rounded to `step`, a value outside `oneOf` replaced by the signal's
7023
- * default value (an enumeration has no nearest value, and its order says nothing). Without it the
7024
- * refused value is held as-is, marked invalid, and `validSignal` reads `undefined`. When the value
7025
- * came from the address, the address is rewritten with the repaired one (a replace).
7032
+ * default value, or made absent when the signal has no default (an enumeration has no nearest
7033
+ * value, and its order says nothing). The default must therefore pass the rules itself: one of
7034
+ * `oneOf`. Without it the refused value is held as-is, marked invalid, and `validSignal` reads
7035
+ * `undefined`. When the value came from the address, the address is rewritten with the repaired
7036
+ * one (a replace).
7026
7037
  * @param {boolean} [options.weak=false] - The param qualifies one visit, not the screen: it is written into a
7027
7038
  * url only when explicitly named (`routeParams={{ edit: id }}`), never inherited from the signal's current
7028
7039
  * value, and it goes back to the default value when the route stops matching. Use it for params like an
@@ -7164,7 +7175,7 @@ const stateSignal = (defaultValue, options = {}) => {
7164
7175
 
7165
7176
  // Determine localStorage key: use id if persists=true, or legacy localStorage option
7166
7177
  const localStorageKey = signalIdString;
7167
- const [validity, updateValidity] = createValidity({
7178
+ const [validity, updateValidity, clearValidity] = createValidity({
7168
7179
  type,
7169
7180
  min,
7170
7181
  max,
@@ -7280,9 +7291,19 @@ const stateSignal = (defaultValue, options = {}) => {
7280
7291
  // Create signal with initial value: use stored value, or undefined to indicate no explicit value
7281
7292
  const processValue = (value) => {
7282
7293
  if (value === undefined) {
7294
+ // Absence is never validated, and the verdict on the previous value
7295
+ // (validSignal, validity.valid) must not outlive it.
7296
+ clearValidity();
7283
7297
  return undefined;
7284
7298
  }
7285
7299
  updateValidity(value);
7300
+ // A state without a default is repaired to absent. validity cannot suggest
7301
+ // it: `undefined` is "no suggestion" there and its rules refuse it.
7302
+ if (autoFix && !validity.valid && getDefaultValue(false) === undefined) {
7303
+ clearValidity();
7304
+ validity.autoFixed = true;
7305
+ return undefined;
7306
+ }
7286
7307
  // Always return the coerced value (type coercion applies), even if invalid.
7287
7308
  // Invalid values are preserved as-is so the UI can display them and the URL
7288
7309
  // can reflect the current input state without silently correcting it.
@@ -7573,6 +7594,18 @@ const readSignalForUrlBuild = (connection) => {
7573
7594
  return connection.signal.value;
7574
7595
  };
7575
7596
 
7597
+ /**
7598
+ * Whether a pattern catches the addresses below its own: "/map/", "/map/*" and
7599
+ * the root "/" do, "/places" does not. Only such a route lends its search
7600
+ * params to the routes below it: it is on screen wherever they are, so its
7601
+ * state holds there too. Declared on "/places", a search param belongs to
7602
+ * that address alone — "/places/:section" beside it also matches
7603
+ * "/places/clubs", and borrowing the list's search would hand it to the clubs.
7604
+ */
7605
+ const coversAddressesBelow = (patternObj) => {
7606
+ return patternObj.pattern.trailingSlash || patternObj.pattern.wildcard;
7607
+ };
7608
+
7576
7609
  /**
7577
7610
  * Creates a custom route pattern matcher
7578
7611
  */
@@ -7684,11 +7717,13 @@ const createRoutePattern = (
7684
7717
  }
7685
7718
  }
7686
7719
 
7687
- // Inherit search parameters from ancestry chain
7688
- // Search params are global and should be inherited from any matching ancestor
7689
- // regardless of path segment relationships
7720
+ // Search params lent by the ancestors covering this route
7690
7721
  let ancestorPatternObj = patternObject.parent;
7691
7722
  while (ancestorPatternObj) {
7723
+ if (!coversAddressesBelow(ancestorPatternObj)) {
7724
+ ancestorPatternObj = ancestorPatternObj.parent;
7725
+ continue;
7726
+ }
7692
7727
  for (const [
7693
7728
  paramName,
7694
7729
  ancestorConnection,
@@ -7773,7 +7808,12 @@ const createRoutePattern = (
7773
7808
  childParam,
7774
7809
  childConnection,
7775
7810
  ] of childPatternObj.queryConnectionMap) {
7776
- if (childParam in resolvedParams) {
7811
+ // a name this route declares is its own state's, whatever a child
7812
+ // declares under the same name
7813
+ if (
7814
+ childParam in resolvedParams ||
7815
+ queryConnectionMap.has(childParam)
7816
+ ) {
7777
7817
  continue;
7778
7818
  }
7779
7819
  const childSignalValue = readSignalForUrlBuild(childConnection);
@@ -7819,7 +7859,9 @@ const createRoutePattern = (
7819
7859
  * - own connection signals (path + query, inherited query included)
7820
7860
  * - ancestor path params pinned by this pattern's own literals: being on
7821
7861
  * "/admin/settings" MEANS section=settings, whatever the signal says
7822
- * - ancestor query signals holding custom values
7862
+ * - custom values of the search params lent by the ancestors covering this
7863
+ * route (coversAddressesBelow). Another ancestor's search params ride
7864
+ * only on that ancestor's own url (see buildMostPreciseUrl)
7823
7865
  * - reachable descendants' connection signals. A descendant is reachable
7824
7866
  * when its literals agree with the state collected so far (a tab value
7825
7867
  * is unreachable while section conflicts with the tab route's literals)
@@ -7945,6 +7987,10 @@ const createRoutePattern = (
7945
7987
  setEntry(seg.name, selfSeg.value, conn, seg.index, "ancestor", false);
7946
7988
  }
7947
7989
  }
7990
+ if (!coversAddressesBelow(ancestor)) {
7991
+ ancestor = ancestor.parent;
7992
+ continue;
7993
+ }
7948
7994
  for (const [name, conn] of ancestor.queryConnectionMap) {
7949
7995
  if (intended.has(name)) {
7950
7996
  continue;
@@ -8091,6 +8137,10 @@ const createRoutePattern = (
8091
8137
  if (!queryReachable || value === undefined) {
8092
8138
  continue;
8093
8139
  }
8140
+ // a name this route declares is its own state's (see resolveParams)
8141
+ if (queryConnectionMap.has(paramName)) {
8142
+ continue;
8143
+ }
8094
8144
  if (conn.isCustomValue(value)) {
8095
8145
  setEntry(paramName, value, conn, undefined, "descendant", false);
8096
8146
  }
@@ -8350,20 +8400,50 @@ const createRoutePattern = (
8350
8400
  );
8351
8401
  };
8352
8402
 
8403
+ // An ancestor's url is that ancestor's address: it carries the search params
8404
+ // declared there, including those it lends to nobody (coversAddressesBelow).
8405
+ // Each ancestor climbed past adds its own, so a higher one must keep them too.
8406
+ const withOwnSearchParams = (ancestorPattern, intended) => {
8407
+ let intendedWithOwn = intended;
8408
+ for (const [name, conn] of ancestorPattern.queryConnectionMap) {
8409
+ if (intendedWithOwn.has(name)) {
8410
+ continue;
8411
+ }
8412
+ const value = readSignalForUrlBuild(conn);
8413
+ if (value === undefined || !conn.isCustomValue(value)) {
8414
+ continue;
8415
+ }
8416
+ if (intendedWithOwn === intended) {
8417
+ intendedWithOwn = new Map(intended);
8418
+ }
8419
+ intendedWithOwn.set(name, {
8420
+ value,
8421
+ connection: conn,
8422
+ segmentIndex: undefined,
8423
+ owner: "ancestor",
8424
+ explicit: false,
8425
+ pageNaming: false,
8426
+ });
8427
+ }
8428
+ return intendedWithOwn;
8429
+ };
8430
+
8353
8431
  const buildMostPreciseUrl = (params = {}) => {
8354
8432
  const { intended, reachableDescendants } = buildIntendedState(params);
8355
8433
  // ancestors: start at the immediate parent, climb while urls keep round-
8356
8434
  // tripping, keep the highest one ("/" is never a target)
8357
8435
  let ancestorUrl = null;
8358
8436
  let ancestor = patternObject.parent;
8437
+ let ancestorIntended = intended;
8359
8438
  while (ancestor && ancestor.originalPattern !== "/") {
8360
8439
  if (!isAncestorCollapseAllowed(ancestor, intended)) {
8361
8440
  break;
8362
8441
  }
8363
- const url = buildCandidateUrl(ancestor, intended, {
8442
+ ancestorIntended = withOwnSearchParams(ancestor, ancestorIntended);
8443
+ const url = buildCandidateUrl(ancestor, ancestorIntended, {
8364
8444
  dropMissing: true,
8365
8445
  });
8366
- if (!url || !verifyUrl(url, intended)) {
8446
+ if (!url || !verifyUrl(url, ancestorIntended)) {
8367
8447
  break;
8368
8448
  }
8369
8449
  debug$3(
@@ -9527,11 +9607,15 @@ const setupRoutePatterns = (routePatterns) => {
9527
9607
  }
9528
9608
  routePattern.familyRoot = root;
9529
9609
  }
9530
- // Phase 3: Inherit search parameter connections from ancestors
9531
- // Search params are global and should be inherited by descendants regardless of path segments
9610
+ // Phase 3: Inherit the search param connections of the ancestors covering
9611
+ // the addresses below them (see coversAddressesBelow)
9532
9612
  for (const routePattern of routePatternSet) {
9533
9613
  let ancestorRoutePattern = routePattern.parent;
9534
9614
  while (ancestorRoutePattern) {
9615
+ if (!coversAddressesBelow(ancestorRoutePattern)) {
9616
+ ancestorRoutePattern = ancestorRoutePattern.parent;
9617
+ continue;
9618
+ }
9535
9619
  // For each ancestor's query connection, check if it should be inherited
9536
9620
  for (const [
9537
9621
  paramName,
@@ -9641,23 +9725,6 @@ const setupRoutePatterns = (routePatterns) => {
9641
9725
  continue;
9642
9726
  }
9643
9727
  const { index } = paramSegment;
9644
- const sharesPathUpTo = (otherSegments) => {
9645
- for (let i = 0; i < index; i++) {
9646
- const seg = routePattern.pattern.segments[i];
9647
- const otherSeg = otherSegments[i];
9648
- if (!otherSeg) {
9649
- return false;
9650
- }
9651
- if (seg.type === "literal" && otherSeg.type === "literal") {
9652
- if (seg.value !== otherSeg.value) {
9653
- return false;
9654
- }
9655
- } else if (seg.type !== otherSeg.type) {
9656
- return false;
9657
- }
9658
- }
9659
- return true;
9660
- };
9661
9728
  for (const otherPattern of routePatternSet) {
9662
9729
  if (otherPattern === routePattern) {
9663
9730
  continue;
@@ -9667,7 +9734,9 @@ const setupRoutePatterns = (routePatterns) => {
9667
9734
  if (!otherSegment || otherSegment.type !== "literal") {
9668
9735
  continue;
9669
9736
  }
9670
- if (!sharesPathUpTo(otherSegments)) {
9737
+ if (
9738
+ !sharesPathUpTo(routePattern.pattern.segments, otherSegments, index)
9739
+ ) {
9671
9740
  continue;
9672
9741
  }
9673
9742
  connection.namedByLiteralRoutes = true;
@@ -9675,11 +9744,90 @@ const setupRoutePatterns = (routePatterns) => {
9675
9744
  }
9676
9745
  }
9677
9746
  }
9747
+ // Phase 4c: A default with an address of its own. "/me/:section" defaulting
9748
+ // to "players", beside "/me/players" and "/me": the default section has a
9749
+ // segment, and the bare address is another page. The param is then required,
9750
+ // and the address writes every value, the default included. Both routes must
9751
+ // be declared: with none at the bare address, that address is the default's
9752
+ // ("/admin" opens on "/admin/settings/:tab"); with no literal for the default,
9753
+ // the bare address IS the default section ("/games/me").
9754
+ // The state keeps its default (getDefaultValue): what the signal holds while
9755
+ // another page of the family says nothing about it (see route.js).
9756
+ for (const routePattern of routePatternSet) {
9757
+ const { segments } = routePattern.pattern;
9758
+ for (const connection of routePattern.pathConnectionMap.values()) {
9759
+ const defaultValue = connection.getDefaultValue();
9760
+ if (defaultValue === undefined) {
9761
+ continue;
9762
+ }
9763
+ const paramSegment = segments.find(
9764
+ (seg) => seg.type === "param" && seg.name === connection.paramName,
9765
+ );
9766
+ if (!paramSegment) {
9767
+ continue;
9768
+ }
9769
+ const { index } = paramSegment;
9770
+ let defaultHasLiteral = false;
9771
+ let bareAddressTaken = false;
9772
+ for (const otherPattern of routePatternSet) {
9773
+ if (otherPattern === routePattern) {
9774
+ continue;
9775
+ }
9776
+ const otherSegments = otherPattern.pattern.segments;
9777
+ if (!sharesPathUpTo(segments, otherSegments, index)) {
9778
+ continue;
9779
+ }
9780
+ const otherSegment = otherSegments[index];
9781
+ if (otherSegment) {
9782
+ if (
9783
+ otherSegment.type === "literal" &&
9784
+ otherSegment.value === String(defaultValue)
9785
+ ) {
9786
+ defaultHasLiteral = true;
9787
+ }
9788
+ continue;
9789
+ }
9790
+ // a route covering the addresses below it is a frame around the
9791
+ // sections, not a page standing at the bare address
9792
+ if (!coversAddressesBelow(otherPattern)) {
9793
+ bareAddressTaken = true;
9794
+ }
9795
+ }
9796
+ if (!defaultHasLiteral || !bareAddressTaken) {
9797
+ continue;
9798
+ }
9799
+ paramSegment.optional = false;
9800
+ // Both answer for the address: the url build, the signal -> url sync,
9801
+ // and checkIfLiteralCanBeOptionalWithPatternObj ("/me" does not stand for
9802
+ // "/me/players" either)
9803
+ connection.isDefaultValue = (value) => value === undefined;
9804
+ connection.isCustomValue = (value) => value !== undefined;
9805
+ }
9806
+ }
9678
9807
  // Phase 5: Calculate depths for all patterns
9679
9808
  for (const routePattern of routePatternSet) {
9680
9809
  calculatePatternDepth(routePattern);
9681
9810
  }
9682
9811
  };
9812
+ // Whether two patterns spell the same path before `index`: the same literals,
9813
+ // and a param wherever the other has one
9814
+ const sharesPathUpTo = (segments, otherSegments, index) => {
9815
+ for (let i = 0; i < index; i++) {
9816
+ const seg = segments[i];
9817
+ const otherSeg = otherSegments[i];
9818
+ if (!otherSeg) {
9819
+ return false;
9820
+ }
9821
+ if (seg.type === "literal" && otherSeg.type === "literal") {
9822
+ if (seg.value !== otherSeg.value) {
9823
+ return false;
9824
+ }
9825
+ } else if (seg.type !== otherSeg.type) {
9826
+ return false;
9827
+ }
9828
+ }
9829
+ return true;
9830
+ };
9683
9831
  // Store the most specific parent (closest parent in hierarchy)
9684
9832
  const getPathSegmentCount = (pattern) => {
9685
9833
  // Only count path segments, not query parameters
@@ -9741,6 +9889,9 @@ let isUpdatingRoutesFromUrl = false;
9741
9889
  * are the same state: writing the signal rewrites the address, an address
9742
9890
  * arriving from outside writes the signal, and the param disappears from the
9743
9891
  * url while the signal holds its default.
9892
+ * They exist on this route's address only, unless the pattern catches the
9893
+ * addresses below it (`/map/`, `/map/*`, the root): then they hold on every
9894
+ * route below as well. `/places` does not lend its `?q=` to `/places/clubs`.
9744
9895
  * `{ signal, default }` says what that state is worth ON THIS ROUTE: the same
9745
9896
  * step opens a creation wizard on its first question and an edition one on
9746
9897
  * its summary, each address staying clean on the step its page opens on. One
@@ -9759,7 +9910,9 @@ let isUpdatingRoutesFromUrl = false;
9759
9910
  * whose param declines the segment does not match at all: no signal is
9760
9911
  * written, `<Route fallback>` is reachable, and `/:gameId` can sit at the root
9761
9912
  * without swallowing `/cgu`. A constrained param is also required — no segment
9762
- * is not one of the values it accepts — so `/:gameId` does not match `/`.
9913
+ * is not one of the values it accepts — so `/:gameId` does not match `/`,
9914
+ * unless its default stands for the bare address (see docs/navigation.md,
9915
+ * "Declaring the sections is what makes them places").
9763
9916
  *
9764
9917
  * Constrain the shape, not the existence: whether that game exists is for the
9765
9918
  * route action and the page to answer, on a route that did match.
@@ -10942,14 +11095,17 @@ const urlWithoutSearchAndHash = (url) => {
10942
11095
  *
10943
11096
  * - `isShown()` — whether the container would still show the page. False as
10944
11097
  * soon as the address leads elsewhere: the page is about to be taken down, or
10945
- * hidden until the route transition leaving it is over, and nothing it reads
10946
- * is worth a render any more. False for good once it is kept while leaving: a
10947
- * return to its route mounts the page anew. Asked outside any render — by a
10948
- * subscription deciding whether to re-render — and it reads the routes
11098
+ * hidden until the route transition leaving it is over, nothing it reads is
11099
+ * worth a render any more, and nothing it holds speaks for the page shown (a
11100
+ * list's hold on the document, see holdDocumentScroll). False for good once
11101
+ * it is kept while leaving: a return to its route mounts the page anew. Asked
11102
+ * outside any render — by a subscription deciding whether to re-render, by
11103
+ * the scroll restoration when a position is due — and it reads the routes
10949
11104
  * untracked, so asking subscribes nobody to them.
10950
11105
  * - `leavingSignal` — true once the page is hidden and kept while its movement
10951
- * plays: still mounted, so what it fills outside its own nodes (a slot in a
10952
- * bar) steps out rather than showing with the page arriving.
11106
+ * plays: still mounted, so what it fills or moves outside its own nodes (a
11107
+ * slot in a bar, the document a list scrolls) steps out rather than showing
11108
+ * with, or moving, the page arriving. Every box of it measures 0×0 by then.
10953
11109
  */
10954
11110
  const RoutePageContext = createContext(null);
10955
11111
 
@@ -11872,7 +12028,7 @@ const putBack = (restoration) => {
11872
12028
  // The page is not whole yet: the last release puts it back.
11873
12029
  return;
11874
12030
  }
11875
- if (documentHoldCount) {
12031
+ if (isDocumentHeld()) {
11876
12032
  pendingRestoration = null;
11877
12033
  return;
11878
12034
  }
@@ -11888,20 +12044,33 @@ const putBack = (restoration) => {
11888
12044
  // height, so the same pixels now fall on other rows. Put back after the list
11889
12045
  // has placed itself, they would undo it, in the very picture a route transition
11890
12046
  // takes. So the list holds the document while it is placing itself, and the
11891
- // url's offset is not put back meanwhile. Counted: two lists can share one
11892
- // document.
11893
- let documentHoldCount = 0;
11894
- const holdDocumentScroll = () => {
11895
- documentHoldCount++;
11896
- let released = false;
12047
+ // url's offset is not put back meanwhile. One hold per list: two lists can
12048
+ // share one document.
12049
+ //
12050
+ // A hold is for the page its list is on, and mounted is not shown: a page kept
12051
+ // while its route transition plays still has its list when the page returned to
12052
+ // renders, and that list says nothing about where the page arriving goes. So a
12053
+ // hold is taken with the list's page (RoutePageContext), asked whether it is
12054
+ // shown when a position is due — `null` outside any route container: that list
12055
+ // is on whatever page is shown. Not a url: which page an address shows is the
12056
+ // routes' to say, and one page can be shown under several (its params moving
12057
+ // under it).
12058
+ const documentHolds = new Set();
12059
+ const holdDocumentScroll = (routePage) => {
12060
+ const hold = { routePage };
12061
+ documentHolds.add(hold);
11897
12062
  return () => {
11898
- if (released) {
11899
- return;
11900
- }
11901
- released = true;
11902
- documentHoldCount--;
12063
+ documentHolds.delete(hold);
11903
12064
  };
11904
12065
  };
12066
+ const isDocumentHeld = () => {
12067
+ for (const { routePage } of documentHolds) {
12068
+ if (routePage === null || routePage.isShown()) {
12069
+ return true;
12070
+ }
12071
+ }
12072
+ return false;
12073
+ };
11905
12074
 
11906
12075
  // What a page holds instead: not the place, which stays the url's, but the
11907
12076
  // moment it is put back (see useHoldScrollRestoration). Counted: several parts
@@ -12159,9 +12328,9 @@ const restoreScrollPosition = (url) => {
12159
12328
  if (!position) {
12160
12329
  return false;
12161
12330
  }
12162
- if (documentHoldCount) {
12163
- // There is a place to go back to, and the list holding the document is
12164
- // putting it back (see holdDocumentScroll).
12331
+ if (isDocumentHeld()) {
12332
+ // There is a place to go back to, and the list holding the document on
12333
+ // that page is putting it back (see holdDocumentScroll).
12165
12334
  return true;
12166
12335
  }
12167
12336
  scrollTo(position);
@@ -12430,6 +12599,25 @@ const getHrefTargetInfo = (
12430
12599
  };
12431
12600
  };
12432
12601
 
12602
+ // The target of a link whose caller names none: a page of another site opens
12603
+ // in a new tab. Only a page: a `tel:`, `mailto:` or `sms:` address is handed to
12604
+ // another app, a `blob:` or `data:` one is this page's own, and a tab opened
12605
+ // for them would be left behind empty.
12606
+ const getDefaultHrefTarget = (
12607
+ href,
12608
+ currentUrlString = window.location.href,
12609
+ ) => {
12610
+ const { isEmpty, isSameSite } = getHrefTargetInfo(href, currentUrlString);
12611
+ if (isEmpty || isSameSite) {
12612
+ return undefined;
12613
+ }
12614
+ const { protocol } = new URL(href, currentUrlString);
12615
+ if (protocol !== "http:" && protocol !== "https:") {
12616
+ return undefined;
12617
+ }
12618
+ return "_blank";
12619
+ };
12620
+
12433
12621
  // A field of getHrefTargetInfo that depends on the page one is on (isCurrent,
12434
12622
  // isAnchor), read through a computed of that one boolean: the caller re-renders
12435
12623
  // when it flips, not at every address write — a search param bound to a wheel
@@ -12470,6 +12658,27 @@ const linkAsksForDocument = (linkElement) => {
12470
12658
  return linkElement.hasAttribute(LINK_DOCUMENT_ATTRIBUTE);
12471
12659
  };
12472
12660
 
12661
+ /*
12662
+ * A link does not always move the tab it is in. A `target` naming another
12663
+ * browsing context (a new tab, a named window) loads the address there, in a
12664
+ * document of its own; a `download` saves it. The browser answers both
12665
+ * without touching this page, and that is the point of writing them: reading
12666
+ * the terms beside a half-filled form costs nothing. Routing such a link
12667
+ * would take this tab instead, and give no tab and no file.
12668
+ *
12669
+ * `_parent` and `_top` count as elsewhere too. Inside a frame they are; at the
12670
+ * top level the browser treats them as `_self` and loads a document, which
12671
+ * costs a load and nothing else.
12672
+ */
12673
+
12674
+ const linkOpensElsewhere = (linkElement) => {
12675
+ const target = linkElement.getAttribute("target");
12676
+ if (target && target !== "_self") {
12677
+ return true;
12678
+ }
12679
+ return linkElement.hasAttribute("download");
12680
+ };
12681
+
12473
12682
  /*
12474
12683
  * A press aims at a place; it does not always go one step deeper. A row of tabs
12475
12684
  * is a lateral move — the neighbour is one finger away — so the whole row should
@@ -12799,8 +13008,10 @@ const setupBrowserIntegrationViaHistory = ({
12799
13008
  // Ignore non-left clicks
12800
13009
  return;
12801
13010
  }
12802
- if (e.metaKey) {
12803
- // Ignore clicks with meta key (e.g. open in new tab)
13011
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
13012
+ // A modifier asks the browser for something other than this tab: a new
13013
+ // tab (meta on macOS, ctrl elsewhere), a new window (shift), a download
13014
+ // (alt).
12804
13015
  return;
12805
13016
  }
12806
13017
  if (e.defaultPrevented) {
@@ -12811,6 +13022,10 @@ const setupBrowserIntegrationViaHistory = ({
12811
13022
  if (linkElement.hasAttribute("data-readonly")) {
12812
13023
  return;
12813
13024
  }
13025
+ if (linkOpensElsewhere(linkElement)) {
13026
+ // Another tab or window, or a download (see link_elsewhere.js).
13027
+ return;
13028
+ }
12814
13029
  const href = linkElement.href;
12815
13030
  const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
12816
13031
  getHrefTargetInfo(href);
@@ -16912,6 +17127,9 @@ const surfaceTextCss = /* css */ `
16912
17127
  }
16913
17128
  `;
16914
17129
 
17130
+ // What a callout's own JSX may know about the callout around it: how to close
17131
+ // it, and the element — whose id is what `commandFor` needs when the button
17132
+ // closing it is not inside it (inside, `--navi-close` finds it on its own).
16915
17133
  const CalloutContext = createContext();
16916
17134
  const useCalloutRequestClose = () => {
16917
17135
  return useContext(CalloutContext)?.requestClose;
@@ -17045,7 +17263,7 @@ const CALLOUT_STATUS_GLYPH_VIEWBOX = "0 0 540 540";
17045
17263
  const CALLOUT_STATUS_GLYPH_PATH = "m232.5,121 8,196h59l8-196zm36.5,224a37,37 0 1,0 2,0z";
17046
17264
 
17047
17265
  /**
17048
- * @type {import("ignore:preact").FunctionComponent<{
17266
+ * @type {import("preact").FunctionComponent<{
17049
17267
  * status?: "info" | "warning" | "error" | "success" | "none",
17050
17268
  * shape?: "square" | "circle",
17051
17269
  * }>}
@@ -21864,23 +22082,12 @@ defineInteractionDetector({
21864
22082
  // A link and an image are draggable without anyone asking, and a native
21865
22083
  // drag IS press-and-move: the browser claims the gesture, takes the pointer
21866
22084
  // events with it and paints a ghost of the row the hand is trying to swipe.
21867
- // Two things are needed, and neither covers the other:
21868
- // - `draggable` refuses it on the element itself, before it starts;
21869
- // - a swipe is usually a row with a link or a thumbnail INSIDE it, and
21870
- // those are draggable in their own right. `dragstart` bubbles, so
21871
- // refusing it here refuses theirs too — and it is the only refusal every
21872
- // browser honours (`-webkit-user-drag` is one engine's).
21873
- // The cost is stated rather than worked around: an element that takes a
21874
- // swipe cannot also be dragged out of the page, because there is one
21875
- // gesture and it cannot mean both.
22085
+ // `draggable` refuses it on the element itself, before it starts; the drag
22086
+ // of a link or a thumbnail INSIDE it is refused by the travel reading the
22087
+ // press (startDragToTravel). The cost is stated rather than worked around:
22088
+ // an element that takes a swipe cannot also be dragged out of the page,
22089
+ // because there is one gesture and it cannot mean both.
21876
22090
  mark("draggable", "false");
21877
- const onDragStart = (dragStartEvent) => {
21878
- dragStartEvent.preventDefault();
21879
- };
21880
- element.addEventListener("dragstart", onDragStart);
21881
- undo.push(() => {
21882
- element.removeEventListener("dragstart", onDragStart);
21883
- });
21884
22091
 
21885
22092
  // A touch this element may take has to be refusable before the finger
21886
22093
  // lands, or the browser can cancel an established swipe mid-gesture by
@@ -22915,9 +23122,10 @@ const isMac = detectMac();
22915
23122
  * place rather than about this box.
22916
23123
  *
22917
23124
  * `data-pan-after-hold="kept"` asks for that wait once rather than before every
22918
- * pan: the surface given the hand keeps it — panning on contact, like the same
22919
- * plan opened full screen — and asks again after a press has landed away from
22920
- * it, which is the hand saying it has moved on. What answers "away" is the same
23125
+ * pan: the surface a finger has given the hand keeps it — panning on contact,
23126
+ * like the same plan opened full screen — and asks again after a press has
23127
+ * landed away from it, which is the hand saying it has moved on. A mouse never
23128
+ * owed the wait, so it neither spends it nor marks it. What answers "away" is the same
22921
23129
  * thing that closes a popup, and the surface is the one that watches for it, so
22922
23130
  * an application never keeps a `pointerdown` listener on the window to know.
22923
23131
  * `[data-hand-kept]` is on the element for as long as it holds the hand that
@@ -24039,7 +24247,8 @@ const isSameKey = (browserEventKey, key) => {
24039
24247
  * and that field is the control the declaration speaks for: an ArrowUp declared
24040
24248
  * on a text field is the field saying what ArrowUp does there, not a container
24041
24249
  * taking the caret from a field inside it. A field inside a box that declares
24042
- * keys for something else (a row, a list) keeps every key it has a use for.
24250
+ * keys for something else (a row, a list) keeps every key it has a use for,
24251
+ * even when it is the only control in that box.
24043
24252
  *
24044
24253
  * A declared key that matches takes the key: its default does not happen as
24045
24254
  * well, whether the gate lets the interaction through or refuses it and says
@@ -24065,7 +24274,10 @@ defineInteractionDetector({
24065
24274
  };
24066
24275
  }
24067
24276
  const onKeyDown = createOnKeyDownForShortcuts(shortcuts, {
24068
- ownsTarget: (target) => target === findNearestControlHost(element),
24277
+ // Only the control's own root speaks for its host: a box that wraps a
24278
+ // single control is reached by that control's gate, not handed its keys.
24279
+ ownsTarget: (target) =>
24280
+ isControlRoot(element) && target === findNearestControlHost(element),
24069
24281
  });
24070
24282
  element.addEventListener("keydown", onKeyDown);
24071
24283
  return () => {
@@ -28133,6 +28345,127 @@ const useForceRender = () => {
28133
28345
 
28134
28346
  // https://github.com/preactjs/preact/issues/4756
28135
28347
 
28348
+ /**
28349
+ * Reads the current state of an action and returns `[data, loading, error, dismissError]`.
28350
+ *
28351
+ * By default (`loading` and `error` not set) the component suspends on load
28352
+ * and throws on failure, delegating both states to the nearest `<Loading>` and
28353
+ * `<ErrorBoundary>` ancestors. Pass `loading: true` or `error: true` to handle
28354
+ * either state directly inside the component instead.
28355
+ *
28356
+ * Return value: `[data, loading, error, dismissError]`
28357
+ * - `data` — the action's last successful data, or `undefined` if it has never completed
28358
+ * - `loading` — `true` while the action is running (only when `loading: true` is passed)
28359
+ * - `error` — the Error thrown by the action (only when `error: true` is passed)
28360
+ * - `dismissError` — takes that error out of what this hook returns, nothing else (only when `error: true` is passed)
28361
+ *
28362
+ * Stale data is always returned: the previous `data` remains available while
28363
+ * the action re-runs and after a run has failed, so a screen that had something
28364
+ * to show never has it taken away. What is drawn over it — a loading indicator,
28365
+ * the error, nothing — is entirely up to the component.
28366
+ *
28367
+ * `loading` covers the delay of a debounced binding
28368
+ * (`bindParams(paramsSignal, { debounce })`, `actionRunEffect({ debounce })`)
28369
+ * as well as the run itself: from the moment the params move, what is on screen
28370
+ * is the previous answer, and a screen showing "searching…" while someone types
28371
+ * needs to know it. Suspending has nothing to wait on during that delay, so
28372
+ * only `loading: true` reads it — the default keeps showing the previous answer
28373
+ * until the run really starts.
28374
+ *
28375
+ * When `loading` is not set (default), the component suspends until data is
28376
+ * ready, so `data` is always defined when the component renders and `loading`
28377
+ * is always `false`. With `loading: true` the component never suspends: every
28378
+ * state — running, settling, params nobody has started yet, a failure whose
28379
+ * message was dismissed — comes back as a value, so the subtree it draws is
28380
+ * never swapped for the boundary above and remounted.
28381
+ *
28382
+ * When `error` is not set (default), any action failure causes the component
28383
+ * to throw, so `error` is always `undefined` when the component renders. With
28384
+ * `error: true` the failure arrives beside the data the action already had — a
28385
+ * message on the content, not one in place of it. `dismissError()` removes the
28386
+ * message and nothing else: nothing is asked again, and what comes back is the
28387
+ * same state minus the error — the same data, or none at all when nothing ever
28388
+ * came back.
28389
+ *
28390
+ * `data === undefined` says there is nothing to show and `loading` says whether
28391
+ * anything is on its way: a skeleton reads both, or it claims a load nobody
28392
+ * started (see docs/data_states.md).
28393
+ *
28394
+ * @param {object | (() => Promise<any>) | Promise<any>} promiseOrAction -
28395
+ * an action, or the request this component owns written as a function:
28396
+ * made into an action once, on the first render, and kept for the life of
28397
+ * the instance — so it may be written inline — with what it resolves to as
28398
+ * the data and `run` implied. Code loaded on demand reads this way
28399
+ * (`() => import("./plan.jsx").then((m) => m.Plan)`, see
28400
+ * docs/dynamic_import.md), as does anything else asked for once per mount.
28401
+ * @param {{ loading?: true, error?: true, run?: true, prerun?: true, onLoad?: (data: any, context: {params: any}) => void }} [options]
28402
+ * @param {true} [options.run] - The fallback for data nothing else can ask for:
28403
+ * this component owns the request and starts it, from the render that reads
28404
+ * it. `loading` and `error` keep their meaning — delegated by default,
28405
+ * handled inline when asked for — so a run started here suspends into
28406
+ * `<Loading>` like any other.
28407
+ *
28408
+ * A failure is shown, not asked again: a render that retried it would retry
28409
+ * it in a loop while the network is down — that is `rerun()`'s job. Except a
28410
+ * failure a prerun left (a `prerun: true` head start, a press that preran
28411
+ * it): nothing on screen asked for that run, so this asks again, once.
28412
+ *
28413
+ * Prefer a `routeAction` wherever the parameter is one the address holds (a
28414
+ * path param, a search param bound to a `stateSignal`), popup included: it is
28415
+ * asked for when the address changes, with everything else that address
28416
+ * needs, whereas this one cannot start before the component that draws it
28417
+ * exists — one render late, and behind whatever gesture mounted it. What is
28418
+ * left for `run` is the parameter chosen inside the component and dying with
28419
+ * it — and any run that WRITES, whatever its parameter: an address asks again
28420
+ * on every arrival, which is right for a read and wrong for a write (see
28421
+ * docs/actions.md and docs/popup_open.md).
28422
+ * @param {true} [options.prerun] - A head start: this component starts a
28423
+ * read that another one, mounted later, owns with `run: true` — the read
28424
+ * leaves as soon as its params are known instead of once the owner exists.
28425
+ * Started from the render like `run`, as a prerun: it draws none of it, so
28426
+ * nothing suspends (no `<Loading>` needed) and a failure is neither thrown
28427
+ * nor reported — the owner asks again. A head start still running when the
28428
+ * owner reads it is the owner's wait.
28429
+ * @param {(data: any, context: {params: any}) => void} [options.onLoad] - what
28430
+ * this screen does with the data ONCE, when it becomes known: seed the fields
28431
+ * someone is about to edit, remember where a list was, focus something.
28432
+ *
28433
+ * Fired once per set of params — never again for a rerun that brings the same
28434
+ * thing back. That is the whole point of it: a save, a refresh, a poll all
28435
+ * hand the data over again, and copying it a second time would overwrite what
28436
+ * the person is in the middle of writing. The action knows what it ran with,
28437
+ * so nobody has to guess the dependency.
28438
+ *
28439
+ * Called from a layout effect, so what it writes belongs to the same tick as
28440
+ * the render that got the data — a `<Form pristineKey>` taking its reference
28441
+ * then sees the filled fields rather than the empty ones.
28442
+ * @returns {[data: unknown, loading: boolean, error: Error | undefined, dismissError: (() => void) | undefined]}
28443
+ *
28444
+ * @example <caption>Default — delegate both states (least code)</caption>
28445
+ * const [user] = useAsyncData(userAction);
28446
+ * // user is always defined here — never loading, never error
28447
+ *
28448
+ * @example <caption>Handle loading inline while keeping stale data</caption>
28449
+ * const [user, loading] = useAsyncData(userAction, { loading: true });
28450
+ * // First load: user === undefined, loading === true → show skeleton
28451
+ * // While refreshing: user === <stale>, loading === true → show stale + spinner
28452
+ * // Done: user === <fresh>, loading === false
28453
+ *
28454
+ * @example <caption>Handle error inline</caption>
28455
+ * const [user, , error] = useAsyncData(userAction, { error: true });
28456
+ * if (error) return <ErrorCard message={error.message} />;
28457
+ *
28458
+ * @example <caption>Say the failure over the rows already on screen</caption>
28459
+ * const [items, , error, dismissError] = useAsyncData(listAction, { error: true });
28460
+ * // First failure: items === undefined → only the message
28461
+ * // Failed refresh: items === <stale> → the message above the rows
28462
+ * return (
28463
+ * <>
28464
+ * {error && <ErrorStrip error={error} onClose={dismissError} />}
28465
+ * {items && <List items={items} />}
28466
+ * </>
28467
+ * );
28468
+ */
28136
28469
  const useAsyncData = (promiseOrAction, {
28137
28470
  loading = "delegate",
28138
28471
  error = "delegate",
@@ -28575,7 +28908,7 @@ const LoadingFallback = ({
28575
28908
  * ones that would render fine.
28576
28909
  *
28577
28910
  * @param {object} props
28578
- * @param {Function|import("ignore:preact").VNode} [props.fallback] - what is displayed
28911
+ * @param {Function|import("preact").VNode} [props.fallback] - what is displayed
28579
28912
  * instead of the children: an element, or a component receiving
28580
28913
  * `{ error, resetError }`. Without it the boundary is transparent.
28581
28914
  * @param {() => void} [props.onReset] - called when the fallback dismisses the
@@ -28934,6 +29267,12 @@ const hasWidthHeight = (context) => {
28934
29267
  (context.styles.height || context.remainingProps.height)
28935
29268
  );
28936
29269
  };
29270
+ // The `stretch` sizing keyword: the containing block's width minus the box's
29271
+ // own margins, so a box with a margin fills its row without leaving it (100%
29272
+ // adds the margins on top). Spelled with the prefix because an inline style
29273
+ // holds one value, and this is the one name Chromium, WebKit and Firefox all
29274
+ // read (Firefox 155 has no unprefixed `stretch`).
29275
+ const FILL_AVAILABLE_WIDTH = "-webkit-fill-available";
28937
29276
  const DIMENSION_PROPS = {
28938
29277
  boxSizing: PASS_THROUGH,
28939
29278
  width: PASS_THROUGH,
@@ -28986,16 +29325,16 @@ const DIMENSION_PROPS = {
28986
29325
  return { flexGrow: expandWeight(value), flexBasis: "0%" };
28987
29326
  }
28988
29327
  if (parentBoxFlow === "flex-y" || parentBoxFlow === "inline-flex-y") {
28989
- // width 100% is what fills the cross axis; align-self stretch would do
29328
+ // A width is what fills the cross axis; align-self stretch would do
28990
29329
  // the same but takes the parent's alignX away — a maxWidth-capped item
28991
29330
  // in an alignX="center" parent would then sit at the start instead of
28992
29331
  // in the middle.
28993
29332
  return {
28994
- width: "100%",
29333
+ width: FILL_AVAILABLE_WIDTH,
28995
29334
  };
28996
29335
  }
28997
29336
  // Can't use flexGrow — parent is not flex-x
28998
- return { width: "100%" };
29337
+ return { width: FILL_AVAILABLE_WIDTH };
28999
29338
  },
29000
29339
  expandY: (value, { parentBoxFlow }) => {
29001
29340
  if (!value) {
@@ -29556,6 +29895,7 @@ const getVisualChildStylePropStrategy = (name) => {
29556
29895
  };
29557
29896
 
29558
29897
  const isStyleProp = (name) => STYLE_PROP_NAME_SET.has(name);
29898
+ const isTypoProp = (name) => TYPO_PROP_NAME_SET.has(name);
29559
29899
 
29560
29900
  const getStringifier = (key) => {
29561
29901
  if (SPACING_PROP_SET.has(key)) {
@@ -30197,10 +30537,10 @@ const readCSSVarName = entry => Array.isArray(entry) ? entry[0] : entry;
30197
30537
  const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
30198
30538
 
30199
30539
  /**
30200
- * @type {import("ignore:preact").FunctionComponent<{
30540
+ * @type {import("preact").FunctionComponent<{
30201
30541
  * as?: string,
30202
30542
  * className?: string,
30203
- * style?: import("ignore:preact").JSX.CSSProperties & { [pseudo: string]: import("ignore:preact").JSX.CSSProperties },
30543
+ * style?: import("preact").JSX.CSSProperties & { [pseudo: string]: import("preact").JSX.CSSProperties },
30204
30544
  * styleCSSVars?: { [propName: string]: string | [string, string] },
30205
30545
  * inline?: boolean,
30206
30546
  * block?: boolean,
@@ -30215,7 +30555,7 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
30215
30555
  * hasChildUsingForwardedProps?: boolean,
30216
30556
  * childPropSet?: Set<string>,
30217
30557
  * preventInitialTransition?: boolean,
30218
- * separator?: import("ignore:preact").ComponentChildren | ((index: number) => import("ignore:preact").ComponentChildren),
30558
+ * separator?: import("preact").ComponentChildren | ((index: number) => import("preact").ComponentChildren),
30219
30559
  * selfInteractions?: string,
30220
30560
  * interactions?: { [type: string]: "request_action" | "request_ui_action" | "refuse" | ((event: Event) => void) | false | null | undefined },
30221
30561
  * alignX?: string,
@@ -30227,7 +30567,7 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
30227
30567
  * body?: boolean,
30228
30568
  * sticky?: boolean,
30229
30569
  * mount?: "always" | "after-paint",
30230
- * children?: import("ignore:preact").ComponentChildren,
30570
+ * children?: import("preact").ComponentChildren,
30231
30571
  * [key: string]: any,
30232
30572
  * }>}
30233
30573
  * @param {"x"|"y"|boolean} [flex] A flex container: `"x"` (or `true`) side by
@@ -32782,7 +33122,7 @@ const debug$1 = (...args) => {
32782
33122
  * matches. `routeFallback()` gives that page a name, which is what lets a
32783
33123
  * movement be written about it and its `matchingSignal` be read from outside
32784
33124
  * the tree (see route_fallback.js).
32785
- * @param {Function|import("ignore:preact").VNode} [props.element] - what the branch renders
33125
+ * @param {Function|import("preact").VNode} [props.element] - what the branch renders
32786
33126
  * @param {object} [props.elementProps] - props given to `element`
32787
33127
  *
32788
33128
  * A branch says what it renders, not what happens when it cannot: loading and
@@ -35131,7 +35471,7 @@ const css$13 = /* css */`@property --navi-route-transition-leave {
35131
35471
  * that. An application that already has an element holding its pages can mark
35132
35472
  * that one with `data-navi-route-transition-area` rather than nesting another.
35133
35473
  *
35134
- * @type {import("ignore:preact").FunctionComponent<{ children?: any, [key: string]: any }>}
35474
+ * @type {import("preact").FunctionComponent<{ children?: any, [key: string]: any }>}
35135
35475
  */
35136
35476
  const RouteTransitionArea = ({
35137
35477
  children,
@@ -35921,7 +36261,7 @@ const beginTransition = ({
35921
36261
  warnAboutNamesEscapingArea(areaElement, capturedNames);
35922
36262
  return;
35923
36263
  }
35924
- for (const name of capturedNames) {
36264
+ for (const name of capturedNames.keys()) {
35925
36265
  if (name === "root") {
35926
36266
  continue;
35927
36267
  }
@@ -36124,9 +36464,10 @@ const ignoreSkipped$1 = () => {};
36124
36464
 
36125
36465
  // The names the browser captured, read off the pictures themselves: what was
36126
36466
  // asked for in CSS and what was taken are not the same question (see
36127
- // viewTransitionReady).
36467
+ // viewTransitionReady). Each with the states it was taken in: a name taken in
36468
+ // only one of them pairs with nothing.
36128
36469
  const capturedViewTransitionNames = () => {
36129
- const names = new Set();
36470
+ const names = new Map();
36130
36471
  for (const animation of document.getAnimations()) {
36131
36472
  const pseudoElement = animation.effect?.pseudoElement;
36132
36473
  if (!pseudoElement || !pseudoElement.startsWith("::view-transition")) {
@@ -36136,43 +36477,59 @@ const capturedViewTransitionNames = () => {
36136
36477
  if (nameStart === -1) {
36137
36478
  continue;
36138
36479
  }
36139
- names.add(pseudoElement.slice(nameStart + 1, -1));
36480
+ const name = pseudoElement.slice(nameStart + 1, -1);
36481
+ let states = names.get(name);
36482
+ if (!states) {
36483
+ states = {
36484
+ old: false,
36485
+ new: false
36486
+ };
36487
+ names.set(name, states);
36488
+ }
36489
+ if (pseudoElement.startsWith("::view-transition-old(")) {
36490
+ states.old = true;
36491
+ } else if (pseudoElement.startsWith("::view-transition-new(")) {
36492
+ states.new = true;
36493
+ }
36140
36494
  }
36141
36495
  return names;
36142
36496
  };
36143
36497
 
36144
- // Nested groups keep a name written inside the area inside its picture (see
36145
- // the @supports block in the CSS above). Without them the name escapes to the
36146
- // top of the ::view-transition tree and the element it belongs to stands still,
36147
- // fading on its own, while the pages move under it.
36498
+ // A name written inside the area is a group of its own: it stands where it was
36499
+ // captured while the area's two pictures carry the movement, in every browser.
36500
+ // Nested groups only keep it cut at the pages' edge (see the @supports block in
36501
+ // the CSS above). Paired with the same name on the other page it is a morph;
36502
+ // taken in one state only it pairs with nothing, and is a piece of its page
36503
+ // left behind, fading, while that page moves.
36148
36504
  const NESTED_GROUPS_SUPPORTED = window.CSS.supports("view-transition-group", "contain");
36149
36505
  const warnAboutNamesEscapingArea = (areaElement, capturedNames) => {
36150
- if (NESTED_GROUPS_SUPPORTED) {
36151
- return;
36152
- }
36153
- let escapedName = null;
36154
- for (const name of capturedNames) {
36506
+ const unpairedNames = new Set();
36507
+ for (const [name, states] of capturedNames) {
36155
36508
  if (name === "root" || name === AREA_NAME || name.startsWith(FURNITURE_NAME_PREFIX)) {
36156
36509
  continue;
36157
36510
  }
36158
- escapedName = name;
36159
- break;
36511
+ if (states.old && states.new) {
36512
+ continue;
36513
+ }
36514
+ unpairedNames.add(name);
36160
36515
  }
36161
36516
  // A name captured next to the area is not necessarily inside it — a bar the
36162
36517
  // application animates on the same clock is named on purpose. The subtree is
36163
36518
  // walked only once something is there to find, so the common case reads
36164
- // nothing.
36165
- if (!escapedName) {
36519
+ // nothing. The page being left is still in it, hidden (keepLeavingPages in
36520
+ // route.jsx), so a name taken only in the old state is found too.
36521
+ if (unpairedNames.size === 0) {
36166
36522
  return;
36167
36523
  }
36168
36524
  for (const descendant of areaElement.querySelectorAll("*")) {
36169
36525
  const {
36170
36526
  viewTransitionName
36171
36527
  } = getComputedStyle(descendant);
36172
- if (!viewTransitionName || viewTransitionName === "none") {
36528
+ if (!unpairedNames.has(viewTransitionName)) {
36173
36529
  continue;
36174
36530
  }
36175
- warnOnce("names-escaping-area", `"${viewTransitionName}" is a view-transition-name written inside the element marked ${TRANSITION_AREA_ATTRIBUTE}, and this browser has no nested groups (view-transition-group: contain): the element it names is lifted out of the area's picture, so it stands still and fades on its own while the pages move. Give that name only for the length of the gesture it serves, or drop it while a route transition plays (:root[${TRANSITION_ATTRIBUTE}] { view-transition-name: none }).`);
36531
+ const unclipped = NESTED_GROUPS_SUPPORTED ? "" : " This browser has no nested groups (view-transition-group: contain), so it is not even cut at the pages' edge.";
36532
+ warnOnce("names-escaping-area", `"${viewTransitionName}" is a view-transition-name written inside the element marked ${TRANSITION_AREA_ATTRIBUTE}, and the page on the other side of the navigation has nothing of that name. A name lifts its element out of the pages' pictures, in every browser: with nothing to morph into, it stands where it was captured and fades on its own while the pages move.${unclipped} Give that name only for the length of the gesture it serves, or drop it while the pages move: :root:is([${TRANSITION_ATTRIBUTE}], [${ROUTE_TRAVEL_ATTRIBUTE}]) <that element> { view-transition-name: none } (with !important over an inline name).`);
36176
36533
  return;
36177
36534
  }
36178
36535
  };
@@ -36317,6 +36674,8 @@ const installRouteTransitionCss = () => {
36317
36674
  };
36318
36675
 
36319
36676
  installImportMetaCssBuild(import.meta);
36677
+ // The pictures carrying a travel, among everything else the movement takes
36678
+ // along (see findLeadAnimation).
36320
36679
  const TRAVEL_LEAD_NAMES = ["navi-route-travel"];
36321
36680
 
36322
36681
  // A browser with no view transitions of its own has no picture of the page
@@ -36584,7 +36943,7 @@ const css$12 = /* css */`.navi_route_travel {
36584
36943
  `;
36585
36944
 
36586
36945
  /**
36587
- * @type {import("ignore:preact").FunctionComponent<{
36946
+ * @type {import("preact").FunctionComponent<{
36588
36947
  * routes?: Array<object|{route: object, params?: object}>,
36589
36948
  * axis?: "x"|"y",
36590
36949
  * travelByDrag?: boolean,
@@ -39361,7 +39720,7 @@ const resetCommand = (source, event) => {
39361
39720
  // answers: "save" stays, "delete" goes back to the list. Same shape as the
39362
39721
  // browser's own formaction/formmethod, and the same shape the action already
39363
39722
  // has (it is told which button requested it).
39364
- const resolveAfterSend = (target, requester) => {
39723
+ const resolveAfterSend = (target, requester, surface) => {
39365
39724
  const askedForByRequester = requester?.getAttribute?.("data-after-send");
39366
39725
  if (askedForByRequester) {
39367
39726
  return askedForByRequester;
@@ -39370,11 +39729,6 @@ const resolveAfterSend = (target, requester) => {
39370
39729
  if (askedFor) {
39371
39730
  return askedFor;
39372
39731
  }
39373
- // From above the target: a popup that IS the send target is handled on its
39374
- // own (see the aria-expanded branch below), and must not answer twice.
39375
- const surface = target.parentElement?.closest(
39376
- `[data-slide], [aria-expanded]`,
39377
- );
39378
39732
  if (!surface) {
39379
39733
  return undefined;
39380
39734
  }
@@ -39386,6 +39740,11 @@ const resolveAfterSend = (target, requester) => {
39386
39740
  }
39387
39741
  return undefined;
39388
39742
  };
39743
+ // The surface a send answers when the control says nothing. Found from above
39744
+ // the target: a popup that IS the send target is handled on its own (see the
39745
+ // aria-expanded branch in sendCommand), and must not answer twice.
39746
+ const resolveSendSurface = (target) =>
39747
+ target.parentElement?.closest(`[data-slide], [aria-expanded]`);
39389
39748
  // What follows a send is caused by the send, not by the gesture: the send
39390
39749
  // answered the gesture and may have prevented it (an Enter must not also click
39391
39750
  // the submit button, nor activate the trigger of the popup it closed), and a
@@ -39467,20 +39826,30 @@ const sendCommand = (source, event, { requester }) => {
39467
39826
  // and the popup must stay open — with the form still in front of the
39468
39827
  // user, showing what it is waiting for.
39469
39828
  let invalid = false;
39470
- // What follows the send is read at the moment it runs, never before it:
39471
- // it depends on the requester (Enter in a field sends through the first
39472
- // submit button, and what follows is that button's answer), and on
39473
- // anything the send itself decided — an action that learned where to go
39474
- // from the response writes it on the form while it runs
39475
- // (data-after-send), and this is what picks it up.
39829
+ // What follows the send is aimed at the press, like a button's command
39830
+ // waiting for its action (see resolveNaviCommand): the send may take the
39831
+ // form out of the document (a form keyed on what it edits, a row that
39832
+ // moves once saved), and resolved from there it finds no popup to close.
39833
+ // Which command it is gets read again once the send went through: an
39834
+ // action that learned where to go from the response writes it on the
39835
+ // form while it runs (data-after-send). That one can only be aimed when
39836
+ // it is read.
39837
+ const surface = resolveSendSurface(target);
39838
+ const sentEvent = createSentEvent(event);
39839
+ const afterSendAtPress = resolveAfterSend(target, sendRequester, surface);
39840
+ const runAimedAtPress = afterSendAtPress
39841
+ ? resolveNaviCommand(source, afterSendAtPress, sentEvent)
39842
+ : undefined;
39476
39843
  const runAfterSend = () => {
39477
- const afterSend = resolveAfterSend(target, sendRequester);
39844
+ const afterSend = resolveAfterSend(target, sendRequester, surface);
39478
39845
  if (!afterSend) {
39479
39846
  return;
39480
39847
  }
39481
- triggerNaviCommand(source, afterSend, createSentEvent(event), {
39482
- optional: true,
39483
- });
39848
+ if (afterSend === afterSendAtPress) {
39849
+ runAimedAtPress?.();
39850
+ return;
39851
+ }
39852
+ triggerNaviCommand(source, afterSend, sentEvent);
39484
39853
  };
39485
39854
  const {
39486
39855
  result: sent,
@@ -40419,6 +40788,12 @@ const isDeclaredInsidePopup = (target, popupId) =>
40419
40788
  * Lives on the popup's own content element rather than in the backdrop's
40420
40789
  * handler, for both renderers: the backdrop is a sibling behind the popup
40421
40790
  * (never an ancestor), so a press inside the popup's box never reaches it.
40791
+ *
40792
+ * Inside a box that travels by drag — a row of slides walked by a swipe, a
40793
+ * sheet pushed back towards its edge — the region is also where that travel
40794
+ * starts, and at the mousedown nobody knows yet which of the two the press is.
40795
+ * The dismissal then waits for the click, which only comes if the press stayed
40796
+ * a press: a travel swallows the one it leaves behind.
40422
40797
  */
40423
40798
  const handlePressOnOutsideRegion = (
40424
40799
  mouseDownEvent,
@@ -40449,11 +40824,36 @@ const handlePressOnOutsideRegion = (
40449
40824
  mouseDownEvent.preventDefault();
40450
40825
  return;
40451
40826
  }
40452
- if (pressOutside === "close" || pressOutside === "cancel") {
40453
- openController.requestClose(mouseDownEvent, {
40454
- isCancel: pressOutside === "cancel",
40827
+ if (pressOutside !== "close" && pressOutside !== "cancel") {
40828
+ return;
40829
+ }
40830
+ const isCancel = pressOutside === "cancel";
40831
+ if (isPressDisputedByDrag(target)) {
40832
+ onClickOfThisPress(target, (clickEvent) => {
40833
+ openController.requestClose(clickEvent, { isCancel });
40455
40834
  });
40835
+ return;
40456
40836
  }
40837
+ openController.requestClose(mouseDownEvent, { isCancel });
40838
+ };
40839
+
40840
+ // The click ending the press whose mousedown landed on `element`. A click goes
40841
+ // to what the press and the release have in common, so it reaches `element`
40842
+ // only when the release stayed on it or inside it. A press that never clicks
40843
+ // leaves it armed until the next one, and no longer — a click always comes
40844
+ // after a press, so it cannot reach the click of another (the rule
40845
+ // click_suppression.js in @jsenv/dom goes by).
40846
+ const onClickOfThisPress = (element, callback) => {
40847
+ const onClick = (clickEvent) => {
40848
+ disarm();
40849
+ callback(clickEvent);
40850
+ };
40851
+ const disarm = () => {
40852
+ element.removeEventListener("click", onClick);
40853
+ window.removeEventListener("pointerdown", disarm, { capture: true });
40854
+ };
40855
+ element.addEventListener("click", onClick);
40856
+ window.addEventListener("pointerdown", disarm, { capture: true });
40457
40857
  };
40458
40858
 
40459
40859
  /**
@@ -45661,6 +46061,9 @@ const uiStateHoldsNothing = (uiState) => {
45661
46061
  * handles the timing centrally rather than each component having to manage its
45662
46062
  * own debounce logic.
45663
46063
  */
46064
+ // Sentinel used as the initial value of lastActionValueRef.
46065
+ // Distinct from undefined so that undefined (e.g. unchecked radio) can itself
46066
+ // be stored as a valid "last action value" and trigger the dedup logic.
45664
46067
  const NO_ACTION_YET = Symbol("no_action_yet");
45665
46068
 
45666
46069
  // The control types rendered as an element `<label for>` can name: HTML's
@@ -45760,13 +46163,18 @@ const LISTENERS_CALLED_BY_REACTIONS = new Set(["onMouseDown", "onClick", "onKeyD
45760
46163
  * the control opens, so that a listener hears the control as a whole.
45761
46164
  * `onMouseDown`, `onClick`, `onKeyDown`, `onPaste` and `onInput` stay the
45762
46165
  * host's either way: the control's own reactions to them call them.
46166
+ * @param {"host"|"root"} [options.nameOn] Where `aria-label` and
46167
+ * `aria-labelledby` land: where the role is. `"host"`, the default, for every
46168
+ * control whose host is what is announced; `"root"` for a control whose role
46169
+ * is on its root and whose host is an `aria-hidden` carrier (a wheel).
45763
46170
  * @returns {[controlRootProps, controlHostProps, { uiStateController }]}
45764
46171
  */
45765
46172
  const useControlProps = (props, {
45766
46173
  controlType,
45767
46174
  persists,
45768
46175
  uiActionInternal,
45769
- listenersOn = "host"
46176
+ listenersOn = "host",
46177
+ nameOn = "host"
45770
46178
  }) => {
45771
46179
  const debugUIState = useDebugUIState();
45772
46180
 
@@ -45854,6 +46262,7 @@ const useControlProps = (props, {
45854
46262
  boundAction,
45855
46263
  controlInfo,
45856
46264
  listenersOn,
46265
+ nameOn,
45857
46266
  listenersCalledByReactions: LISTENERS_CALLED_BY_REACTIONS
45858
46267
  });
45859
46268
  {
@@ -47082,6 +47491,7 @@ const useInteractiveProps = (props, {
47082
47491
  boundAction,
47083
47492
  controlInfo,
47084
47493
  listenersOn,
47494
+ nameOn,
47085
47495
  listenersCalledByReactions
47086
47496
  }) => {
47087
47497
  const {
@@ -47089,6 +47499,7 @@ const useInteractiveProps = (props, {
47089
47499
  } = props;
47090
47500
  const [controlRootProps, controlHostProps] = splitControlProps(props, {
47091
47501
  listenersOn,
47502
+ nameOn,
47092
47503
  listenersCalledByReactions
47093
47504
  });
47094
47505
  controlRootProps["navi-control"] = controlInfo.controlType;
@@ -47700,8 +48111,10 @@ const useInteractiveProps = (props, {
47700
48111
  }
47701
48112
  return [controlRootProps, controlHostProps];
47702
48113
  };
48114
+ const NAME_ATTRIBUTE_SET = new Set(["aria-label", "aria-labelledby"]);
47703
48115
  const splitControlProps = (props, {
47704
48116
  listenersOn,
48117
+ nameOn,
47705
48118
  listenersCalledByReactions
47706
48119
  }) => {
47707
48120
  const {
@@ -47712,6 +48125,10 @@ const splitControlProps = (props, {
47712
48125
  };
47713
48126
  const controlRootProps = {};
47714
48127
  for (const key of Object.keys(props)) {
48128
+ if (nameOn === "root" && NAME_ATTRIBUTE_SET.has(key)) {
48129
+ controlRootProps[key] = props[key];
48130
+ continue;
48131
+ }
47715
48132
  const attributeName = controlAttributeFromProp(key);
47716
48133
  if (attributeName) {
47717
48134
  controlHostProps[attributeName] = props[key];
@@ -49841,7 +50258,7 @@ const css$$ = /* css */`.navi_text_anchor {
49841
50258
  * @param {{ size?: number, verticalAlign?: string }} [lineLayout]
49842
50259
  * Describes the surrounding line context. Used as layout-effect dependencies so the correction
49843
50260
  * reruns when the surrounding text's font-size or vertical-align changes.
49844
- * @param {import("ignore:preact").RefObject} childRef — ref on the child element to reposition
50261
+ * @param {import("preact").RefObject} childRef — ref on the child element to reposition
49845
50262
  */
49846
50263
  const TextAnchor = ({
49847
50264
  childRef,
@@ -50503,25 +50920,25 @@ const shouldInjectSpacingBetween = (left, right) => {
50503
50920
  * See `docs/typography.md` for the decisions behind it — truncating, rows made
50504
50921
  * of an icon, a text and an icon, and where a line may break.
50505
50922
  *
50506
- * @type {import("ignore:preact").FunctionComponent<{
50507
- * children?: import("ignore:preact").ComponentChildren,
50923
+ * @type {import("preact").FunctionComponent<{
50924
+ * children?: import("preact").ComponentChildren,
50508
50925
  * as?: string,
50509
50926
  * className?: string,
50510
- * style?: import("ignore:preact").JSX.CSSProperties,
50927
+ * style?: import("preact").JSX.CSSProperties,
50511
50928
  * bold?: boolean,
50512
50929
  * noWrap?: boolean,
50513
50930
  * maxLines?: number,
50514
- * spacing?: string | number | import("ignore:preact").ComponentChildren,
50931
+ * spacing?: string | number | import("preact").ComponentChildren,
50515
50932
  * loading?: boolean,
50516
50933
  * skeleton?: boolean,
50517
50934
  * attachLastChild?: boolean,
50518
50935
  * preventSpaceUnderlines?: boolean,
50519
- * holdSpaceForStyle?: import("ignore:preact").JSX.CSSProperties,
50936
+ * holdSpaceForStyle?: import("preact").JSX.CSSProperties,
50520
50937
  * boldStable?: boolean,
50521
50938
  * shrinkWrap?: boolean,
50522
50939
  * capitalize?: boolean,
50523
50940
  * selectRange?: string | [number, number] | [string, string],
50524
- * childrenOutsideFlow?: import("ignore:preact").ComponentChildren,
50941
+ * childrenOutsideFlow?: import("preact").ComponentChildren,
50525
50942
  * [key: string]: any,
50526
50943
  * }>}
50527
50944
  *
@@ -50562,7 +50979,7 @@ const shouldInjectSpacingBetween = (left, right) => {
50562
50979
  * Replaces real space characters between children with padding-based spaces.
50563
50980
  * Useful inside `<a>` elements where browsers draw an underline under spaces.
50564
50981
  *
50565
- * @param {import("ignore:preact").JSX.CSSProperties} [holdSpaceForStyle]
50982
+ * @param {import("preact").JSX.CSSProperties} [holdSpaceForStyle]
50566
50983
  * Prevents layout shifts when text styles change (e.g. font-weight, font-size).
50567
50984
  * Pass the CSS properties representing the "largest" visual state of the text.
50568
50985
  * An invisible placeholder rendered with those styles reserves the space; the
@@ -50595,7 +51012,7 @@ const shouldInjectSpacingBetween = (left, right) => {
50595
51012
  * `[start, end]` character-offset tuple, or a `[startText, endText]` pair of
50596
51013
  * substrings selecting from the first to the end of the second.
50597
51014
  *
50598
- * @param {import("ignore:preact").ComponentChildren} [childrenOutsideFlow]
51015
+ * @param {import("preact").ComponentChildren} [childrenOutsideFlow]
50599
51016
  * Rendered after children but outside the text spacing/flow logic. Used
50600
51017
  * internally for overlays such as the skeleton container.
50601
51018
  */
@@ -50883,7 +51300,7 @@ const TextWithSelectRange = ({
50883
51300
  * @param {object} props
50884
51301
  * @param {string} [props.href] - URL/id of an external SVG symbol, rendered via
50885
51302
  * `<svg><use href></svg>`. Mutually exclusive with meaningful `children`.
50886
- * @param {import("ignore:preact").ComponentChildren} [props.children] - Inline icon
51303
+ * @param {import("preact").ComponentChildren} [props.children] - Inline icon
50887
51304
  * content: an `<svg>` element, or a string (renders as a text/emoji icon).
50888
51305
  * @param {boolean} [props.decorative] - Marks the icon `aria-hidden`. Defaults
50889
51306
  * to `true` for an unsized, non-interactive icon; pass `false` for a
@@ -51058,6 +51475,11 @@ const usePreloadOnIntent = (ref, href, prefetch = true) => {
51058
51475
  return undefined;
51059
51476
  }
51060
51477
  const preload = () => {
51478
+ // The tab or the window it opens loads its own code: what would be
51479
+ // fetched here is not what runs there.
51480
+ if (linkOpensElsewhere(element)) {
51481
+ return;
51482
+ }
51061
51483
  preloadUrl(href);
51062
51484
  };
51063
51485
  element.addEventListener("pointerenter", preload);
@@ -51266,6 +51688,15 @@ const useDimColorWhen = (elementRef, shouldDim) => {
51266
51688
  };
51267
51689
 
51268
51690
  installImportMetaCssBuild(import.meta);
51691
+ /*
51692
+ * Apply opacity to child content, not the link element itself.
51693
+ *
51694
+ * Why not apply opacity directly to .navi_link?
51695
+ * - Would make focus outlines semi-transparent too (accessibility issue)
51696
+ * - We want dimmed text but full-opacity focus indicators for visibility
51697
+ *
51698
+ * This approach dims the content while preserving focus outline visibility.
51699
+ */
51269
51700
  const css$Z = /* css */`@layer navi {
51270
51701
  .navi_link {
51271
51702
  --link-border-radius: unset;
@@ -51286,6 +51717,19 @@ const css$Z = /* css */`@layer navi {
51286
51717
  --link-current-indicator-spacing: 0;
51287
51718
  --link-current-indicator-color: var(--navi-link-current-indicator-color);
51288
51719
  }
51720
+
51721
+ .navi_link[data-variant="text"], .navi_link[data-variant="icon"] {
51722
+ --link-color: unset;
51723
+ --link-text-decoration: none;
51724
+ }
51725
+
51726
+ .navi_link[data-variant="tab"] {
51727
+ --link-background-hover: color-mix(in srgb,
51728
+ var(--link-background, transparent),
51729
+ var(--x-link-contrasting-color) 15%);
51730
+ --link-color: unset;
51731
+ --link-text-decoration: none;
51732
+ }
51289
51733
  }
51290
51734
 
51291
51735
  .navi_link {
@@ -51306,7 +51750,6 @@ const css$Z = /* css */`@layer navi {
51306
51750
  --x-link-padding-right: var(--link-padding-right, var(--link-padding-x, var(--link-padding, 0px)));
51307
51751
  --x-link-padding-bottom: var(--link-padding-bottom, var(--link-padding-y, var(--link-padding, 0px)));
51308
51752
  --x-link-padding-left: var(--link-padding-left, var(--link-padding-x, var(--link-padding, 0px)));
51309
- aspect-ratio: inherit;
51310
51753
  padding-top: var(--x-link-padding-top);
51311
51754
  padding-right: var(--x-link-padding-right);
51312
51755
  padding-bottom: var(--x-link-padding-bottom);
@@ -51495,17 +51938,7 @@ const css$Z = /* css */`@layer navi {
51495
51938
  margin-left: -.1em;
51496
51939
  }
51497
51940
 
51498
- &[data-variant="text"], &[data-variant="icon"] {
51499
- --link-color: unset;
51500
- --link-text-decoration: none;
51501
- }
51502
-
51503
51941
  &[data-variant="tab"] {
51504
- --link-background-hover: color-mix(in srgb,
51505
- var(--link-background, transparent),
51506
- var(--x-link-contrasting-color) 15%);
51507
- --link-color: unset;
51508
- --link-text-decoration: none;
51509
51942
  white-space: nowrap;
51510
51943
  user-select: none;
51511
51944
 
@@ -51592,8 +52025,9 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
51592
52025
  *
51593
52026
  * Href-derived behavior (all computed from `href`, recomputed on navigation):
51594
52027
  * - internal vs external target (`data-href-internal`/`data-href-external`),
51595
- * with `target`/`rel` auto-defaulted (`_self` internal, `_blank` +
51596
- * `noopener noreferrer` external) unless explicitly set.
52028
+ * with `target`/`rel` auto-defaulted unless explicitly set: `_blank` for a
52029
+ * page of another site (`http:`/`https:`), `noopener noreferrer` for any
52030
+ * external address, nothing otherwise.
51597
52031
  * - "current page" detection (`data-href-current`, `aria-current="page"`).
51598
52032
  * - anchor links (`href` starting with `#`) get `data-href-anchor`.
51599
52033
  * - an automatic end icon for `tel:`/`sms:`/`mailto:`/`github.com`, external
@@ -51655,14 +52089,14 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
51655
52089
  * text reads (see `holdSpaceForStyle` in `text.jsx`).
51656
52090
  * @param {boolean} [props.currentEffectShadow] - Inset-shadow effect while
51657
52091
  * current (used with `variant="tab"`).
51658
- * @param {boolean|import("ignore:preact").ComponentChild} [props.startIcon] - Icon
52092
+ * @param {boolean|import("preact").ComponentChild} [props.startIcon] - Icon
51659
52093
  * placed before the text.
51660
- * @param {boolean|import("ignore:preact").ComponentChild} [props.endIcon] - Icon
52094
+ * @param {boolean|import("preact").ComponentChild} [props.endIcon] - Icon
51661
52095
  * placed after the text; when omitted, an icon may be auto-chosen from the
51662
52096
  * href (see above).
51663
- * @param {boolean|import("ignore:preact").ComponentChild} [props.blankTargetIcon] -
52097
+ * @param {boolean|import("preact").ComponentChild} [props.blankTargetIcon] -
51664
52098
  * Override/suppress the auto external-link icon.
51665
- * @param {boolean|import("ignore:preact").ComponentChild} [props.anchorIcon] -
52099
+ * @param {boolean|import("preact").ComponentChild} [props.anchorIcon] -
51666
52100
  * Controls the auto anchor icon shown for a childless anchor link:
51667
52101
  * `true`/`undefined` uses the default chain SVG, `false` suppresses it, any
51668
52102
  * other node replaces it. To render a plain `#` instead, prefer writing it as
@@ -51733,7 +52167,7 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
51733
52167
  * @param {boolean} [props.preventDefault] - Call `event.preventDefault()` on
51734
52168
  * click (navigation suppressed; `onClick` still runs).
51735
52169
  * @param {(event: MouseEvent) => void} [props.onClick]
51736
- * @param {import("ignore:preact").ComponentChildren} [props.children] - Link content;
52170
+ * @param {import("preact").ComponentChildren} [props.children] - Link content;
51737
52171
  * falls back to the route's relative URL / `href` per the rules above.
51738
52172
  * @param {string} [props.name] - Control facade name (links may be nameless).
51739
52173
  * @param {boolean} [props.disabled]
@@ -51862,7 +52296,7 @@ const LinkPlain = props => {
51862
52296
  ":-navi-href-current": innerCurrent,
51863
52297
  ":-navi-selected": selected
51864
52298
  };
51865
- const innerTarget = target === undefined ? isSameSite ? "_self" : "_blank" : target;
52299
+ const innerTarget = target === undefined ? getDefaultHrefTarget(href, documentUrlSignal.peek()) : target;
51866
52300
  const innerRel = rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel;
51867
52301
  let innerEndIcon;
51868
52302
  if (endIcon === undefined) {
@@ -52029,7 +52463,7 @@ const LinkPlain = props => {
52029
52463
  },
52030
52464
  href: href,
52031
52465
  rel: innerRel,
52032
- target: innerTarget === "_self" ? undefined : target
52466
+ target: innerTarget === "_self" ? undefined : innerTarget
52033
52467
  // Which slide this tab is, and which box to say it to: read by the <Nav>
52034
52468
  // around it to place the row's own bar, and by the command above to find
52035
52469
  // the container across the document.
@@ -52281,6 +52715,9 @@ const useSlideContainer = (target) => {
52281
52715
  };
52282
52716
 
52283
52717
  installImportMetaCssBuild(import.meta);
52718
+ // A name of its own for the bar under the current tab, one per nav on the page:
52719
+ // two bars answering to the same name are two elements claiming one picture,
52720
+ // and the browser refuses the whole transition rather than pick.
52284
52721
  let navCount = 0;
52285
52722
  // Worn by a nav of routes while a route movement between two of its tabs is
52286
52723
  // pictured (see markIndicatorTakesPart, and the CSS below for what it decides).
@@ -52305,6 +52742,7 @@ const rememberProgressAnimation = (navElement, travel) => {
52305
52742
  };
52306
52743
  const css$Y = /* css */`@layer navi {
52307
52744
  .navi_nav {
52745
+ --nav-width: fit-content;
52308
52746
  --nav-border: none;
52309
52747
  --nav-padding: 0px;
52310
52748
  --nav-border-radius: 0px;
@@ -52328,7 +52766,7 @@ const css$Y = /* css */`@layer navi {
52328
52766
  var(--nav-indicator-length-delta));
52329
52767
  --x-nav-indicator-color: var(--nav-indicator-color, var(--nav-current-indicator-color));
52330
52768
  --x-nav-indicator-color-toward: var(--nav-indicator-color-toward, var(--x-nav-indicator-color));
52331
- z-index: 1;
52769
+ z-index: 2;
52332
52770
  background: color-mix(in srgb,
52333
52771
  var(--x-nav-indicator-color-toward)
52334
52772
  calc(var(--slide-travel-progress) * var(--nav-indicator-toward-sign, 0) *
@@ -52385,7 +52823,7 @@ const css$Y = /* css */`@layer navi {
52385
52823
  }
52386
52824
 
52387
52825
  .navi_nav {
52388
- width: fit-content;
52826
+ width: var(--nav-width);
52389
52827
  padding-top: var(--nav-padding-top, var(--nav-padding-y, var(--nav-padding, unset)));
52390
52828
  padding-right: var(--nav-padding-right, var(--nav-padding-x, var(--nav-padding, unset)));
52391
52829
  padding-bottom: var(--nav-padding-bottom, var(--nav-padding-y, var(--nav-padding, unset)));
@@ -52414,12 +52852,12 @@ const css$Y = /* css */`@layer navi {
52414
52852
  --x-nav-child-border-radius: calc(var(--nav-border-radius) - var(--nav-padding));
52415
52853
  --x-nav-link-border-radius: var(--link-border-radius, var(--x-nav-child-border-radius));
52416
52854
 
52417
- &:first-child {
52855
+ &:nth-child(1 of .navi_link) {
52418
52856
  border-top-left-radius: var(--x-nav-link-border-radius);
52419
52857
  border-bottom-left-radius: var(--x-nav-link-border-radius);
52420
52858
  }
52421
52859
 
52422
- &:last-child {
52860
+ &:nth-last-child(1 of .navi_link) {
52423
52861
  border-top-right-radius: var(--x-nav-link-border-radius);
52424
52862
  border-bottom-right-radius: var(--x-nav-link-border-radius);
52425
52863
  }
@@ -52522,7 +52960,7 @@ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
52522
52960
  };
52523
52961
 
52524
52962
  /**
52525
- * @type {import("ignore:preact").FunctionComponent<{
52963
+ * @type {import("preact").FunctionComponent<{
52526
52964
  * currentIndicator?: boolean|"top"|"bottom"|"left"|"right",
52527
52965
  * currentIndicatorSlides?: boolean,
52528
52966
  * slideContainer?: string,
@@ -53672,7 +54110,7 @@ const TABS_ALIGN_TO_JUSTIFY_CONTENT = {
53672
54110
  };
53673
54111
 
53674
54112
  /**
53675
- * @type {import("ignore:preact").FunctionComponent<{
54113
+ * @type {import("preact").FunctionComponent<{
53676
54114
  * value?: any,
53677
54115
  * defaultValue?: any,
53678
54116
  * onChange?: (value: any) => void,
@@ -53716,7 +54154,7 @@ const TABS_ALIGN_TO_JUSTIFY_CONTENT = {
53716
54154
  * grows the binder instead of closing in on the text.
53717
54155
  * @param {string} [props.tabBorderColor] - The inactive tabs alone; the rest of
53718
54156
  * the outline follows `borderColor`.
53719
- * @param {import("ignore:preact").ComponentChildren} props.children - `Binder.Item`s.
54157
+ * @param {import("preact").ComponentChildren} props.children - `Binder.Item`s.
53720
54158
  */
53721
54159
  const Binder = ({
53722
54160
  children,
@@ -53857,13 +54295,13 @@ const Binder = ({
53857
54295
  /**
53858
54296
  * @param {any} [props.value] - What identifies this item; defaults to its
53859
54297
  * position among its siblings.
53860
- * @param {import("ignore:preact").ComponentChildren} [props.label] - What the tab
54298
+ * @param {import("preact").ComponentChildren} [props.label] - What the tab
53861
54299
  * shows.
53862
54300
  * @param {object} [props.route] - Makes the tab a `Link`
53863
54301
  * to that route, and the binder follows the url: the open item is the one
53864
54302
  * whose route matches, no `value`/`onChange` needed.
53865
54303
  * @param {string} [props.href] - Same, for a plain url.
53866
- * @param {import("ignore:preact").ComponentChildren} [props.children] - The page.
54304
+ * @param {import("preact").ComponentChildren} [props.children] - The page.
53867
54305
  *
53868
54306
  * Every other prop goes to the tab (a `Box`, or a `Link` when routed): an item
53869
54307
  * styles and configures the control that opens it.
@@ -54273,7 +54711,7 @@ const FixedBarStyleCSSVars = {
54273
54711
  };
54274
54712
 
54275
54713
  /**
54276
- * @type {import("ignore:preact").FunctionComponent<{
54714
+ * @type {import("preact").FunctionComponent<{
54277
54715
  * area?: "top"|"bottom"|"left"|"right",
54278
54716
  * width?: string|number,
54279
54717
  * height?: string|number,
@@ -54664,7 +55102,7 @@ const css$U = /* css */`.navi_summary_marker {
54664
55102
  `;
54665
55103
 
54666
55104
  /**
54667
- * @type {import("ignore:preact").FunctionComponent<{
55105
+ * @type {import("preact").FunctionComponent<{
54668
55106
  * open?: boolean,
54669
55107
  * loading?: boolean,
54670
55108
  * openDirection?: "down" | "up" | "left",
@@ -55084,8 +55522,8 @@ const useExpandableContext = partName => {
55084
55522
  };
55085
55523
 
55086
55524
  /**
55087
- * @type {import("ignore:preact").FunctionComponent<{
55088
- * ui?: import("ignore:preact").ComponentChildren | ((state: { open: boolean }) => import("ignore:preact").ComponentChildren),
55525
+ * @type {import("preact").FunctionComponent<{
55526
+ * ui?: import("preact").ComponentChildren | ((state: { open: boolean }) => import("preact").ComponentChildren),
55089
55527
  * open?: boolean,
55090
55528
  * defaultOpen?: boolean,
55091
55529
  * signal?: import("@preact/signals").Signal,
@@ -55725,9 +56163,9 @@ const Expandable = props => {
55725
56163
  * Controls inside it keep their own behavior and do not toggle. A Box, so it
55726
56164
  * takes the layout and style props every Box takes.
55727
56165
  *
55728
- * @type {import("ignore:preact").FunctionComponent<{
55729
- * marker?: false | import("ignore:preact").ComponentChildren,
55730
- * children?: import("ignore:preact").ComponentChildren | ((state: { open: boolean }) => import("ignore:preact").ComponentChildren),
56166
+ * @type {import("preact").FunctionComponent<{
56167
+ * marker?: false | import("preact").ComponentChildren,
56168
+ * children?: import("preact").ComponentChildren | ((state: { open: boolean }) => import("preact").ComponentChildren),
55731
56169
  * }>}
55732
56170
  * @param marker - The chevron drawn before the label. `false` removes it (a UI
55733
56171
  * part that is already an icon has no room for one); any node replaces it.
@@ -55802,7 +56240,7 @@ const ExpandableUI = ({
55802
56240
  * Expandable, children may be a function `(data) => ui` or a branches object —
55803
56241
  * see ActionRenderer.
55804
56242
  *
55805
- * @type {import("ignore:preact").FunctionComponent<{}>}
56243
+ * @type {import("preact").FunctionComponent<{}>}
55806
56244
  */
55807
56245
  const ExpandableContent = ({
55808
56246
  children,
@@ -55958,6 +56396,40 @@ const createToggleEvent = open => {
55958
56396
  return toggleEvent;
55959
56397
  };
55960
56398
 
56399
+ /**
56400
+ * Creates a renderComponent function that passes props through a chain of resolvers.
56401
+ * Each resolver is a component function rendered in sequence (hooks are allowed).
56402
+ * To pass through to the next resolver, call useNextResolver() and render the
56403
+ * returned Next component with the desired props.
56404
+ * To terminate the chain early (e.g. render a specialized component), render
56405
+ * directly without calling Next.
56406
+ *
56407
+ * The last entry in the array is the final/target component — it receives null
56408
+ * from useNextResolver() indicating it is terminal.
56409
+ *
56410
+ * Usage:
56411
+ * const renderButton = createComponentResolver([ResolverA, ResolverB, ButtonTarget]);
56412
+ * // Then inside a component render:
56413
+ * renderButton(props)
56414
+ *
56415
+ * A chain step is one component: a runner, defined once per position, whose
56416
+ * render CALLS its resolver's body rather than rendering it as a child. The
56417
+ * body runs synchronously inside the runner's render — its hooks are the
56418
+ * runner's, at the same tree position on every render — and for exactly that
56419
+ * long, `useNextResolver()` answers with the runner after this one (null for
56420
+ * the last). Nothing is handed down through a context: a chain rendered by the
56421
+ * hundred (a list's rows) pays one component per step, not a provider and a
56422
+ * wrapper around each.
56423
+ *
56424
+ * A resolver that renders ANOTHER resolver — a type resolver picking the
56425
+ * component for `type="date"` — cannot render it as a JSX child: that child
56426
+ * would render later, outside any body, with no Next to read. It renders it
56427
+ * with `renderResolver(Resolver, props)`, which captures the Next of the body
56428
+ * it is called from and hands it to a runner made for that resolver — the
56429
+ * child continues the chain where its parent stood. Called anywhere else, or
56430
+ * a `useNextResolver()` reached outside a body, throws: the chain has no
56431
+ * silent answer to give there.
56432
+ */
55961
56433
  const createComponentResolver = (resolvers, {
55962
56434
  pure
55963
56435
  } = {}) => {
@@ -56167,10 +56639,21 @@ const useAccentColorAttributes = (
56167
56639
  };
56168
56640
 
56169
56641
  installImportMetaCssBuild(import.meta);
56642
+ /**
56643
+ * We need the content to visually shrink (scale down) but the button interactive area MUST remain intact
56644
+ * Otherwise a click on the edges of the button cannot not trigger the click event (mouseup occurs outside the button)
56645
+ **/
56646
+
56647
+ /**
56648
+ * We have to re-define the CSS of button because getComputedStyle(button).borderColor returns
56649
+ * rgb(0, 0, 0) while being visually grey in chrome
56650
+ * So we redefine chrome styles so that loader can keep up with the actual color visible to the user
56651
+ */
56170
56652
  const css$R = /* css */`@layer navi {
56171
56653
  .navi_button {
56172
56654
  --button-border-radius: var(--navi-control-border-radius);
56173
56655
  --button-border-width: var(--navi-control-border-width);
56656
+ --border-width: var(--button-border-width);
56174
56657
  --button-cta-background-color: var(--navi-accent-color);
56175
56658
  --button-outline-width: var(--navi-focus-outline-width);
56176
56659
  --button-outline-offset: calc(-.5 * var(--button-outline-width));
@@ -56179,7 +56662,7 @@ const css$R = /* css */`@layer navi {
56179
56662
  --button-padding-y-default: var(--navi-button-padding-y-default);
56180
56663
  --button-loader-color: var(--navi-loader-color);
56181
56664
  --button-border-color: var(--navi-control-border-color);
56182
- --button-background-color: var(--button-background, var(--navi-button-background-color));
56665
+ --button-background-color: var(--navi-button-background-color);
56183
56666
  --button-color: currentColor;
56184
56667
  --button-cursor: pointer;
56185
56668
  --button-font-size: var(--navi-control-font-size);
@@ -56212,6 +56695,92 @@ const css$R = /* css */`@layer navi {
56212
56695
  line-height: var(--navi-control-line-height);
56213
56696
  display: inline-block;
56214
56697
  }
56698
+
56699
+ .navi_button[data-variant="discrete"] {
56700
+ --button-border-width: 0;
56701
+ --button-border-color: transparent;
56702
+ --button-border-color-hover: var(--button-border-color);
56703
+ --button-border-color-current: var(--button-border-color);
56704
+ --button-border-color-readonly: var(--button-border-color);
56705
+ --button-border-color-disabled: var(--button-border-color);
56706
+ --button-background-color: transparent;
56707
+ --button-background-color-hover: color-mix(in srgb,
56708
+ currentColor 8%,
56709
+ var(--button-background-color));
56710
+ --button-background-color-readonly: var(--button-background-color);
56711
+ --button-background-color-disabled: var(--button-background-color);
56712
+ }
56713
+
56714
+ .navi_button[data-variant="bare"] {
56715
+ --button-padding-x-default: 0;
56716
+ --button-padding-y-default: 0;
56717
+ --button-border-width: 0;
56718
+ --button-border-color: transparent;
56719
+ --button-border-color-hover: var(--button-border-color);
56720
+ --button-border-color-current: var(--button-border-color);
56721
+ --button-border-color-readonly: var(--button-border-color);
56722
+ --button-border-color-disabled: var(--button-border-color);
56723
+ --button-background-color: transparent;
56724
+ --button-background-color-hover: var(--button-background-color);
56725
+ --button-background-color-readonly: var(--button-background-color);
56726
+ --button-background-color-disabled: var(--button-background-color);
56727
+ font-size: inherit;
56728
+ font-family: inherit;
56729
+ line-height: inherit;
56730
+ }
56731
+
56732
+ .navi_button[data-variant="discrete-border"] {
56733
+ --button-background-color: transparent;
56734
+ --button-background-color-hover: var(--button-background-color);
56735
+ --button-background-color-readonly: var(--button-background-color);
56736
+ --button-background-color-disabled: var(--button-background-color);
56737
+ }
56738
+
56739
+ .navi_button[data-variant="border"] {
56740
+ --button-background-color: transparent;
56741
+ --button-background-color-hover: color-mix(in srgb,
56742
+ currentColor 8%,
56743
+ var(--button-background-color));
56744
+ --button-background-color-readonly: var(--button-background-color);
56745
+ --button-background-color-disabled: var(--button-background-color);
56746
+ }
56747
+
56748
+ .navi_button[data-icon] {
56749
+ --button-padding: 0;
56750
+ }
56751
+
56752
+ .navi_button[data-cta] {
56753
+ --button-background-color: var(--button-cta-background-color);
56754
+ --button-border-color: var(--button-cta-background-color);
56755
+ --button-color: white;
56756
+ --button-background-color-hover: color-mix(in srgb,
56757
+ var(--button-background-color) 85%,
56758
+ white);
56759
+ --button-border-color-hover: color-mix(in srgb,
56760
+ var(--button-border-color) 85%,
56761
+ white);
56762
+ --button-background-color-readonly: color-mix(in srgb,
56763
+ var(--button-background-color) 50%,
56764
+ white);
56765
+ --button-border-color-readonly: color-mix(in srgb,
56766
+ var(--button-border-color) 50%,
56767
+ white);
56768
+ --button-background-color-disabled: color-mix(in srgb,
56769
+ var(--button-background-color) 40%,
56770
+ white);
56771
+ --button-border-color-disabled: color-mix(in srgb,
56772
+ var(--button-border-color) 40%,
56773
+ white);
56774
+ --button-color-disabled: color-mix(in srgb,
56775
+ var(--button-color) 60%,
56776
+ transparent);
56777
+ }
56778
+
56779
+ .navi_button[data-content-display="contents"] {
56780
+ background: none;
56781
+ border: none;
56782
+ padding: 0;
56783
+ }
56215
56784
  }
56216
56785
 
56217
56786
  a.navi_button {
@@ -56228,7 +56797,6 @@ a.navi_button {
56228
56797
  --x-button-color: var(--button-color);
56229
56798
  --x-button-cursor: var(--button-cursor);
56230
56799
  box-sizing: border-box;
56231
- aspect-ratio: inherit;
56232
56800
  color: var(--x-button-color);
56233
56801
  border-top-left-radius: var(--x-corner-top-left-radius, var(--button-border-radius));
56234
56802
  border-top-right-radius: var(--x-corner-top-right-radius, var(--button-border-radius));
@@ -56237,12 +56805,15 @@ a.navi_button {
56237
56805
  cursor: var(--x-button-cursor);
56238
56806
  touch-action: manipulation;
56239
56807
  user-select: none;
56240
- background: none;
56241
- border: none;
56242
56808
  outline: none;
56243
- padding: 0;
56244
56809
  position: relative;
56245
56810
 
56811
+ &:where(:not([data-content-display="contents"])) {
56812
+ background: none;
56813
+ border: none;
56814
+ padding: 0;
56815
+ }
56816
+
56246
56817
  & .navi_button_content {
56247
56818
  --x-corner-top-left-radius: initial;
56248
56819
  --x-corner-top-right-radius: initial;
@@ -56264,8 +56835,7 @@ a.navi_button {
56264
56835
  gap: inherit;
56265
56836
  color: inherit;
56266
56837
  vertical-align: inherit;
56267
- background: var(--x-button-background);
56268
- background-color: var(--x-button-background-color, var(--x-button-background));
56838
+ background: var(--x-button-background, var(--x-button-background-color));
56269
56839
  border: var(--x-button-border);
56270
56840
  border-radius: inherit;
56271
56841
  outline-width: var(--button-outline-width);
@@ -56288,6 +56858,10 @@ a.navi_button {
56288
56858
  }
56289
56859
  }
56290
56860
 
56861
+ &[data-background-color] .navi_button_content {
56862
+ background-color: var(--x-button-background-color);
56863
+ }
56864
+
56291
56865
  &[data-href-current] {
56292
56866
  --x-button-border-color: var(--button-border-color-current);
56293
56867
  --x-button-background-color: var(--button-background-color-current);
@@ -56332,7 +56906,7 @@ a.navi_button {
56332
56906
  }
56333
56907
 
56334
56908
  &[data-content-display="contents"] {
56335
- & [data-focus-visible] {
56909
+ &[data-focus-visible] {
56336
56910
  outline: var(--button-outline-width) solid var(--button-outline-color);
56337
56911
  outline-offset: var(--button-outline-offset);
56338
56912
  }
@@ -56358,19 +56932,6 @@ a.navi_button {
56358
56932
  }
56359
56933
 
56360
56934
  &[data-variant="discrete"] {
56361
- --button-border-width: 0;
56362
- --button-border-color: transparent;
56363
- --button-border-color-hover: var(--button-border-color);
56364
- --button-border-color-current: var(--button-border-color);
56365
- --button-border-color-readonly: var(--button-border-color);
56366
- --button-border-color-disabled: var(--button-border-color);
56367
- --button-background-color: var(--button-background, transparent);
56368
- --button-background-color-hover: color-mix(in srgb,
56369
- currentColor 8%,
56370
- var(--button-background-color));
56371
- --button-background-color-readonly: var(--button-background-color);
56372
- --button-background-color-disabled: var(--button-background-color);
56373
-
56374
56935
  @media (hover: hover) {
56375
56936
  &[data-pressed] {
56376
56937
  & .navi_button_content {
@@ -56381,17 +56942,6 @@ a.navi_button {
56381
56942
  }
56382
56943
 
56383
56944
  &[data-variant="bare"] {
56384
- --button-border-width: 0;
56385
- --button-border-color: transparent;
56386
- --button-border-color-hover: var(--button-border-color);
56387
- --button-border-color-current: var(--button-border-color);
56388
- --button-border-color-readonly: var(--button-border-color);
56389
- --button-border-color-disabled: var(--button-border-color);
56390
- --button-background-color: var(--button-background, transparent);
56391
- --button-background-color-hover: var(--button-background-color);
56392
- --button-background-color-readonly: var(--button-background-color);
56393
- --button-background-color-disabled: var(--button-background-color);
56394
-
56395
56945
  &[data-pressed] {
56396
56946
  & .navi_button_content {
56397
56947
  transform: none;
@@ -56400,10 +56950,6 @@ a.navi_button {
56400
56950
  }
56401
56951
 
56402
56952
  &[data-variant="discrete-border"] {
56403
- --button-background-color: var(--button-background, transparent);
56404
- --button-background-color-hover: var(--button-background-color);
56405
- --button-background-color-readonly: var(--button-background-color);
56406
- --button-background-color-disabled: var(--button-background-color);
56407
56953
  --x-button-border-color: transparent;
56408
56954
 
56409
56955
  &[data-hover] {
@@ -56415,15 +56961,6 @@ a.navi_button {
56415
56961
  }
56416
56962
  }
56417
56963
 
56418
- &[data-variant="border"] {
56419
- --button-background-color: var(--button-background, transparent);
56420
- --button-background-color-hover: color-mix(in srgb,
56421
- currentColor 8%,
56422
- var(--button-background-color));
56423
- --button-background-color-readonly: var(--button-background-color);
56424
- --button-background-color-disabled: var(--button-background-color);
56425
- }
56426
-
56427
56964
  &[data-press-effect="scale"][data-pressed] {
56428
56965
  & .navi_button_content {
56429
56966
  transform: scale(.9);
@@ -56437,37 +56974,9 @@ a.navi_button {
56437
56974
  }
56438
56975
 
56439
56976
  &[data-icon] {
56440
- --button-padding: 0;
56441
56977
  justify-content: center;
56442
56978
  align-items: center;
56443
56979
  }
56444
-
56445
- &[data-cta] {
56446
- --button-background-color: var(--button-background, var(--button-cta-background-color));
56447
- --button-border-color: var(--button-cta-background-color);
56448
- --button-color: white;
56449
- --button-background-color-hover: color-mix(in srgb,
56450
- var(--button-background-color) 85%,
56451
- white);
56452
- --button-border-color-hover: color-mix(in srgb,
56453
- var(--button-border-color) 85%,
56454
- white);
56455
- --button-background-color-readonly: color-mix(in srgb,
56456
- var(--button-background-color) 50%,
56457
- white);
56458
- --button-border-color-readonly: color-mix(in srgb,
56459
- var(--button-border-color) 50%,
56460
- white);
56461
- --button-background-color-disabled: color-mix(in srgb,
56462
- var(--button-background-color) 40%,
56463
- white);
56464
- --button-border-color-disabled: color-mix(in srgb,
56465
- var(--button-border-color) 40%,
56466
- white);
56467
- --button-color-disabled: color-mix(in srgb,
56468
- var(--button-color) 60%,
56469
- transparent);
56470
- }
56471
56980
  }
56472
56981
  `;
56473
56982
  const ButtonUI = props => {
@@ -56596,6 +57105,7 @@ const ButtonUI = props => {
56596
57105
  "data-press-effect": pressEffect,
56597
57106
  "data-icon": icon ? "" : undefined,
56598
57107
  "data-cta": cta ? "" : undefined,
57108
+ "data-background-color": props.backgroundColor === undefined ? undefined : "",
56599
57109
  "data-callout-arrow-x": "center"
56600
57110
  // style management
56601
57111
  ,
@@ -56908,7 +57418,7 @@ const ButtonWithHref = props => {
56908
57418
  } = getHrefTargetInfo(href, documentUrlSignal.peek());
56909
57419
  return jsx(Next, {
56910
57420
  ...props,
56911
- target: target === undefined ? isSameSite ? undefined : "_blank" : target,
57421
+ target: target === undefined ? getDefaultHrefTarget(href, documentUrlSignal.peek()) : target,
56912
57422
  rel: rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel
56913
57423
  // Under whatever the caller already says: a route knows better whether
56914
57424
  // it is the page one is on (see button_route.jsx), and a demo can force
@@ -57062,7 +57572,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
57062
57572
  };
57063
57573
 
57064
57574
  /**
57065
- * @type {import("ignore:preact").FunctionComponent<{
57575
+ * @type {import("preact").FunctionComponent<{
57066
57576
  * selfInteractions?: string,
57067
57577
  * whenSelfInteractionsBlocked?: "hide" | "refuse" | "ignore",
57068
57578
  * replace?: boolean,
@@ -57144,7 +57654,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
57144
57654
  * more: the work may already have happened on the other side (see
57145
57655
  * docs/actions.md#aborting-saves-resources-it-does-not-undo), so say it only
57146
57656
  * where the screen can be re-opened on what is actually there.
57147
- * @param {boolean|import("ignore:preact").ComponentChildren} [confirm] Ask before
57657
+ * @param {boolean|import("preact").ComponentChildren} [confirm] Ask before
57148
57658
  * doing it, in the place the button stands: the first press replaces the
57149
57659
  * button with this question and a "Confirmer"/"Annuler" pair, the second one
57150
57660
  * does what the button was for. `true` asks navi's default question. The
@@ -57155,9 +57665,9 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
57155
57665
  * question up, its message on the button that asked. Use `<Picker
57156
57666
  * type="confirm">` instead when the question is long enough to want a popup,
57157
57667
  * or when the row has no space for it.
57158
- * @param {import("ignore:preact").ComponentChildren} [confirmLabel] Label of the
57668
+ * @param {import("preact").ComponentChildren} [confirmLabel] Label of the
57159
57669
  * second press.
57160
- * @param {import("ignore:preact").ComponentChildren} [cancelLabel] Label of the way
57670
+ * @param {import("preact").ComponentChildren} [cancelLabel] Label of the way
57161
57671
  * out of the question.
57162
57672
  * @param {string} [confirmTestId] `data-testid` of the second press.
57163
57673
  * @param {string} [cancelTestId] `data-testid` of the way out.
@@ -57256,13 +57766,13 @@ const css$P = /* css */`.navi_control_swap {
57256
57766
  `;
57257
57767
 
57258
57768
  /**
57259
- * @type {import("ignore:preact").FunctionComponent<{
57769
+ * @type {import("preact").FunctionComponent<{
57260
57770
  * value?: string,
57261
57771
  * defaultValue?: string,
57262
57772
  * signal?: import("@preact/signals").Signal<string>,
57263
57773
  * onChange?: (name: string, event: Event) => void,
57264
57774
  * animation?: boolean,
57265
- * children?: import("ignore:preact").ComponentChildren,
57775
+ * children?: import("preact").ComponentChildren,
57266
57776
  * }>}
57267
57777
  * @param value - The name of the side holding the floor, driven from outside:
57268
57778
  * the row swaps to match every change of it, and a press on a cap can still
@@ -57406,13 +57916,13 @@ const ControlSwap = props => {
57406
57916
  * reads these and draws the caps at its ends, the controls between them. The
57407
57917
  * props below say what the side IS; every other prop describes its cap.
57408
57918
  *
57409
- * @type {import("ignore:preact").FunctionComponent<{
57919
+ * @type {import("preact").FunctionComponent<{
57410
57920
  * name?: string,
57411
- * icon: import("ignore:preact").ComponentChildren,
57921
+ * icon: import("preact").ComponentChildren,
57412
57922
  * label: string,
57413
- * badge?: boolean | import("ignore:preact").ComponentChildren,
57923
+ * badge?: boolean | import("preact").ComponentChildren,
57414
57924
  * autoFocus?: boolean,
57415
- * children?: import("ignore:preact").ComponentChildren,
57925
+ * children?: import("preact").ComponentChildren,
57416
57926
  * [key: string]: any,
57417
57927
  * }>}
57418
57928
  * @param name - How `value`/`signal`/`onChange` name this side. Its position
@@ -57620,6 +58130,51 @@ const createAvailableConstraint = (
57620
58130
  };
57621
58131
  };
57622
58132
 
58133
+ /**
58134
+ * ControlGroup — a named sub-group of form controls whose combined state is
58135
+ * aggregated into a single key/value object, exactly like a Form but without
58136
+ * the <form> element semantics.
58137
+ *
58138
+ * Use it when you need a self-contained cluster of inputs that:
58139
+ * - produce one composite value (an object) as their joint state
58140
+ * - can carry their own action / uiAction / command
58141
+ * - may live inside or outside a <Form>
58142
+ *
58143
+ * Props:
58144
+ * name — the key under which the composite value is registered in a
58145
+ * parent Form (optional when used standalone)
58146
+ * action — called with the aggregated object value when any child changes
58147
+ * command — navi command string (e.g. "--navi-send")
58148
+ * disabled — propagates to all children
58149
+ * readOnly — propagates to all children
58150
+ * as — HTML element to render (default "div")
58151
+ * children — the controls that belong to this group
58152
+ *
58153
+ * aggregateChildStates / distributeChildUIState
58154
+ * — what the group is worth, in both directions, when that is not
58155
+ * simply "one key per child": two wheels that add up to a number
58156
+ * of minutes, three fields that make one date. With them the
58157
+ * group takes and hands back a single value, so it can be driven
58158
+ * by one `value`/`signal` like any other control. Same mechanism
58159
+ * InputDuration uses for its own hour/minute/second fields (see
58160
+ * input_duration.jsx). `name` is unaffected: it still says under
58161
+ * which key the group's value — whatever shape it takes —
58162
+ * lands in the form around it.
58163
+ *
58164
+ * distributeChildStates
58165
+ * — the same way down, asked ONCE for all the children at a time:
58166
+ * `(groupValue, children) => Map<child, state>`. The plural of
58167
+ * `distributeChildUIState`, and the mirror of
58168
+ * `aggregateChildStates`, which already sees every child. For a
58169
+ * group whose children cannot be placed one at a time — four
58170
+ * seats where who sits down decides who moves, a row where the
58171
+ * first answer changes what the others may show. A child the Map
58172
+ * does not name is left where it is. Given both, this one wins.
58173
+ * It also closes the loop: the group holds ONE answer its
58174
+ * children are views of, so when one of them speaks the others
58175
+ * are placed again (the one that acted is left where the user
58176
+ * put it).
58177
+ */
57623
58178
  const ControlGroup = props => {
57624
58179
  const defaultRef = useRef();
57625
58180
  props.ref = props.ref || defaultRef;
@@ -57812,6 +58367,41 @@ const css$N = /* css */`@layer navi {
57812
58367
  var(--button-background-color) 95%,
57813
58368
  black);
57814
58369
  }
58370
+
58371
+ .navi_checkbox[data-variant="switch"] {
58372
+ --margin: var(--switch-margin);
58373
+ --width: var(--switch-outer-width);
58374
+ --height: unset;
58375
+ --border-radius: var(--switch-border-radius);
58376
+ --background-color: var(--switch-background-color);
58377
+ --background-color-hover: var(--switch-background-color-hover);
58378
+ --background-color-readonly: var(--switch-background-color-readonly);
58379
+ --background-color-disabled: var(--switch-background-color-disabled);
58380
+ --background-color-checked: var(--switch-background-color-checked);
58381
+ --background-color-hover-checked: var(--switch-background-color-hover-checked);
58382
+ --background-color-readonly-checked: var(--switch-background-color-readonly-checked);
58383
+ --background-color-disabled-checked: var(--switch-background-color-disabled-checked);
58384
+ }
58385
+
58386
+ .navi_checkbox[data-variant="icon"] {
58387
+ --margin: 0;
58388
+ --width: auto;
58389
+ --height: auto;
58390
+ }
58391
+
58392
+ .navi_checkbox[data-variant="button"] {
58393
+ --margin: 0;
58394
+ --width: auto;
58395
+ --height: auto;
58396
+ --border-color: var(--button-border-color);
58397
+ --border-color-hover: var(--button-border-color-hover);
58398
+ --background-color: var(--button-background-color);
58399
+ --background-color-hover: var(--button-background-color-hover);
58400
+ --background-color-readonly: var(--button-background-color-readonly);
58401
+ --background-color-disabled: var(--button-background-color-disabled);
58402
+ --border-color-checked: var(--button-border-color);
58403
+ --background-color-checked: var(--button-background-color);
58404
+ }
57815
58405
  }
57816
58406
 
57817
58407
  .navi_checkbox {
@@ -57952,18 +58542,6 @@ const css$N = /* css */`@layer navi {
57952
58542
 
57953
58543
  &[data-variant="switch"] {
57954
58544
  --switch-outer-width: calc(var(--switch-width) + var(--switch-padding));
57955
- --margin: var(--switch-margin);
57956
- --width: var(--switch-outer-width);
57957
- --height: unset;
57958
- --border-radius: var(--switch-border-radius);
57959
- --background-color: var(--switch-background-color);
57960
- --background-color-hover: var(--switch-background-color-hover);
57961
- --background-color-readonly: var(--switch-background-color-readonly);
57962
- --background-color-disabled: var(--switch-background-color-disabled);
57963
- --background-color-checked: var(--switch-background-color-checked);
57964
- --background-color-hover-checked: var(--switch-background-color-hover-checked);
57965
- --background-color-readonly-checked: var(--switch-background-color-readonly-checked);
57966
- --background-color-disabled-checked: var(--switch-background-color-disabled-checked);
57967
58545
  min-width: var(--switch-outer-width);
57968
58546
  padding: var(--switch-padding);
57969
58547
  background-color: var(--x-background-color);
@@ -57973,25 +58551,11 @@ const css$N = /* css */`@layer navi {
57973
58551
  }
57974
58552
 
57975
58553
  &[data-variant="icon"] {
57976
- --margin: 0;
57977
- --width: auto;
57978
- --height: auto;
57979
58554
  background: none;
57980
58555
  border: none;
57981
58556
  }
57982
58557
 
57983
58558
  &[data-variant="button"] {
57984
- --margin: 0;
57985
- --width: auto;
57986
- --height: auto;
57987
- --border-color: var(--button-border-color);
57988
- --border-color-hover: var(--button-border-color-hover);
57989
- --background-color: var(--button-background-color);
57990
- --background-color-hover: var(--button-background-color-hover);
57991
- --background-color-readonly: var(--button-background-color-readonly);
57992
- --background-color-disabled: var(--button-background-color-disabled);
57993
- --border-color-checked: var(--button-border-color);
57994
- --background-color-checked: var(--button-background-color);
57995
58559
  padding-top: var(--button-padding-top, var(--button-padding-y, var(--button-padding, var(--button-padding-y-default))));
57996
58560
  padding-right: var(--button-padding-right, var(--button-padding-x, var(--button-padding, var(--button-padding-x-default))));
57997
58561
  padding-bottom: var(--button-padding-bottom, var(--button-padding-y, var(--button-padding, var(--button-padding-y-default))));
@@ -58177,7 +58741,7 @@ const css$M = /* css */`@layer navi {
58177
58741
  [navi-field] {
58178
58742
  --spacing-with-control: var(--navi-xs);
58179
58743
 
58180
- & .navi_checkbox, & .navi_radio {
58744
+ & .navi_checkbox:not([data-variant="switch"]), & .navi_radio {
58181
58745
  --margin: 0;
58182
58746
  }
58183
58747
  }
@@ -58532,6 +59096,28 @@ const css$L = /* css */`@layer navi {
58532
59096
  --button-border-color-disabled: var(--border-color-readonly);
58533
59097
  --button-background-color-disabled: var(--background-color-readonly);
58534
59098
  }
59099
+
59100
+ .navi_radio[data-variant="icon"] {
59101
+ --width: auto;
59102
+ --height: auto;
59103
+ --outline-offset: 2px;
59104
+ --outline-width: 2px;
59105
+ }
59106
+
59107
+ .navi_radio[data-variant="button"] {
59108
+ --margin: 0;
59109
+ --outline-offset: 0px;
59110
+ --width: auto;
59111
+ --height: auto;
59112
+ --border-color: var(--button-border-color);
59113
+ --border-color-hover: var(--button-border-color-hover);
59114
+ --background-color: var(--button-background-color);
59115
+ --background-color-hover: var(--button-background-color-hover);
59116
+ --background-color-readonly: var(--button-background-color-readonly);
59117
+ --background-color-disabled: var(--button-background-color-disabled);
59118
+ --border-color-checked: var(--button-border-color);
59119
+ --background-color-checked: var(--button-background-color);
59120
+ }
58535
59121
  }
58536
59122
 
58537
59123
  .navi_radio {
@@ -58687,26 +59273,7 @@ const css$L = /* css */`@layer navi {
58687
59273
  }
58688
59274
  }
58689
59275
 
58690
- &[data-variant="icon"] {
58691
- --width: auto;
58692
- --height: auto;
58693
- --outline-offset: 2px;
58694
- --outline-width: 2px;
58695
- }
58696
-
58697
59276
  &[data-variant="button"] {
58698
- --margin: 0;
58699
- --outline-offset: 0px;
58700
- --width: auto;
58701
- --height: auto;
58702
- --border-color: var(--button-border-color);
58703
- --border-color-hover: var(--button-border-color-hover);
58704
- --background-color: var(--button-background-color);
58705
- --background-color-hover: var(--button-background-color-hover);
58706
- --background-color-readonly: var(--button-background-color-readonly);
58707
- --background-color-disabled: var(--button-background-color-disabled);
58708
- --border-color-checked: var(--button-border-color);
58709
- --background-color-checked: var(--button-background-color);
58710
59277
  padding-top: var(--button-padding-top, var(--button-padding-y, var(--button-padding, var(--button-padding-y-default))));
58711
59278
  padding-right: var(--button-padding-right, var(--button-padding-x, var(--button-padding, var(--button-padding-x-default))));
58712
59279
  padding-bottom: var(--button-padding-bottom, var(--button-padding-y, var(--button-padding, var(--button-padding-y-default))));
@@ -58930,6 +59497,7 @@ const css$K = /* css */`@layer navi {
58930
59497
  --outline-width: var(--navi-focus-outline-width);
58931
59498
  --outline-offset: var(--outline-width);
58932
59499
  --outline-color: var(--navi-focus-outline-color);
59500
+ --margin: 2px;
58933
59501
  --height: 8px;
58934
59502
  --thumb-size: 16px;
58935
59503
  --thumb-width: var(--thumb-size);
@@ -59005,6 +59573,7 @@ const css$K = /* css */`@layer navi {
59005
59573
  box-sizing: border-box;
59006
59574
  width: fit-content;
59007
59575
  height: var(--height);
59576
+ margin: var(--margin);
59008
59577
  font-size: var(--font-size);
59009
59578
  font-family: var(--font-family);
59010
59579
  border-top-left-radius: var(--x-corner-top-left-radius, var(--outline-border-radius));
@@ -59017,7 +59586,6 @@ const css$K = /* css */`@layer navi {
59017
59586
  outline-offset: var(--outline-offset);
59018
59587
  flex-direction: row;
59019
59588
  align-items: center;
59020
- margin: 2px;
59021
59589
  position: relative;
59022
59590
 
59023
59591
  & .navi_control_input {
@@ -59314,6 +59882,45 @@ const inputCss = /* css */ `@layer navi {
59314
59882
  grey);
59315
59883
  --color-disabled: var(--color-dimmed);
59316
59884
  }
59885
+
59886
+ .navi_input[data-background-transparent] {
59887
+ --background-color-hover: var(--background-color);
59888
+ --background-color-focus: var(--navi-surface-color);
59889
+ }
59890
+
59891
+ .navi_input[data-variant="discrete"], .navi_input[data-variant="discrete-border"] {
59892
+ --background-color: transparent;
59893
+ --background-color-hover: var(--background-color);
59894
+ --background-color-focus: var(--navi-surface-color);
59895
+
59896
+ &[data-background] {
59897
+ --background-color-focus: transparent;
59898
+ }
59899
+ }
59900
+
59901
+ .navi_input[data-variant="discrete"] {
59902
+ --border-color: transparent;
59903
+ }
59904
+
59905
+ .navi_input[data-variant="text"] {
59906
+ --background-color: transparent;
59907
+ --background-color-hover: var(--background-color);
59908
+ --background-color-focus: var(--background-color);
59909
+ --background-color-readonly: var(--background-color);
59910
+ --background-color-disabled: var(--background-color);
59911
+ --border-color: transparent;
59912
+ --border-color-hover: var(--border-color);
59913
+ --border-color-readonly: var(--border-color);
59914
+ --border-color-disabled: var(--border-color);
59915
+ }
59916
+
59917
+ .navi_input[data-variant="underline"] {
59918
+ --background-color: transparent;
59919
+ --background-color-hover: var(--background-color);
59920
+ --background-color-focus: var(--background-color);
59921
+ --background-color-readonly: var(--background-color);
59922
+ --background-color-disabled: var(--background-color);
59923
+ }
59317
59924
  }
59318
59925
 
59319
59926
  .navi_input {
@@ -59465,19 +60072,12 @@ const inputCss = /* css */ `@layer navi {
59465
60072
  }
59466
60073
 
59467
60074
  &[data-background-transparent] {
59468
- --background-color-hover: var(--background-color);
59469
- --background-color-focus: var(--navi-surface-color);
59470
-
59471
60075
  &[data-focus] {
59472
60076
  --x-background-color: var(--background-color-focus);
59473
60077
  }
59474
60078
  }
59475
60079
 
59476
60080
  &[data-variant="discrete"], &[data-variant="discrete-border"] {
59477
- --background-color: transparent;
59478
- --background-color-hover: var(--background-color);
59479
- --background-color-focus: var(--navi-surface-color);
59480
-
59481
60081
  &[data-focus] {
59482
60082
  --x-background-color: var(--background-color-focus);
59483
60083
  }
@@ -59485,15 +60085,9 @@ const inputCss = /* css */ `@layer navi {
59485
60085
  &[data-readonly], &[data-disabled] {
59486
60086
  --x-background-color: var(--background-color);
59487
60087
  }
59488
-
59489
- &[data-background] {
59490
- --background-color-focus: transparent;
59491
- }
59492
60088
  }
59493
60089
 
59494
60090
  &[data-variant="discrete"] {
59495
- --border-color: transparent;
59496
-
59497
60091
  &[data-focus] {
59498
60092
  --x-border-color: color-mix(in srgb,
59499
60093
  var(--border-color) 55%,
@@ -59502,24 +60096,10 @@ const inputCss = /* css */ `@layer navi {
59502
60096
  }
59503
60097
 
59504
60098
  &[data-variant="text"] {
59505
- --background-color: transparent;
59506
- --background-color-hover: var(--background-color);
59507
- --background-color-focus: var(--background-color);
59508
- --background-color-readonly: var(--background-color);
59509
- --background-color-disabled: var(--background-color);
59510
- --border-color: transparent;
59511
- --border-color-hover: var(--border-color);
59512
- --border-color-readonly: var(--border-color);
59513
- --border-color-disabled: var(--border-color);
59514
60099
  cursor: inherit;
59515
60100
  }
59516
60101
 
59517
60102
  &[data-variant="underline"] {
59518
- --background-color: transparent;
59519
- --background-color-hover: var(--background-color);
59520
- --background-color-focus: var(--background-color);
59521
- --background-color-readonly: var(--background-color);
59522
- --background-color-disabled: var(--background-color);
59523
60103
  border: none;
59524
60104
  border-radius: 0;
59525
60105
  padding-left: 0;
@@ -60715,6 +61295,38 @@ const InputStyleCSSVars = {
60715
61295
  const InputPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading", ":-navi-has-value"];
60716
61296
  const InputPseudoElements = ["::-navi-loader"];
60717
61297
 
61298
+ /**
61299
+ * @type {import("preact").FunctionComponent<{
61300
+ * type?: string,
61301
+ * icon?: import("preact").ComponentChildren | null,
61302
+ * clearable?: boolean,
61303
+ * value?: any,
61304
+ * boolean?: boolean,
61305
+ * selectOnPress?: boolean,
61306
+ * [key: string]: any,
61307
+ * }>}
61308
+ * @param {any} [value] On a checkbox, what it is worth checked (`"on"` when
61309
+ * left out); unchecked it is `undefined`. See control_value.md, "What a
61310
+ * checkbox is worth".
61311
+ * @param {boolean} [boolean] Makes a checkbox a yes/no: `true` checked, `false`
61312
+ * unchecked, everywhere it hands its value. Takes no `value`.
61313
+ * @param {import("preact").ComponentChildren|null} [icon] What the textual
61314
+ * types that draw an icon (`search`, `email`, `tel`) put in their slot: left
61315
+ * out, the type's own glyph; `null`, nothing — for a field an application
61316
+ * draws its own magnifier beside, where a second one two centimetres away
61317
+ * would be one too many; anything else is drawn instead. A `search` field
61318
+ * swaps that slot for the clear cross as soon as it holds a value, whatever
61319
+ * this says.
61320
+ * @param {boolean} [clearable] Gives any other type the same slot: the icon
61321
+ * while empty (none, unless `icon` says otherwise), the clear cross once
61322
+ * filled.
61323
+ * @param {boolean} [selectOnPress] For a textual field whose value is usually
61324
+ * replaced rather than edited, like a browser's address bar: the first press
61325
+ * after the field took the focus selects its whole text once released, with a
61326
+ * mouse or a finger, and a second press places the caret. A focus that came
61327
+ * without a press (a dialog opening, Tab, code) selects nothing; the first key
61328
+ * that types selects first, so it replaces the text.
61329
+ */
60718
61330
  const Input = props => {
60719
61331
  resolveInputProps(props);
60720
61332
  const {
@@ -60804,7 +61416,7 @@ const TextareaStyleCSSVars = {
60804
61416
  };
60805
61417
 
60806
61418
  /**
60807
- * @type {import("ignore:preact").FunctionComponent<{
61419
+ * @type {import("preact").FunctionComponent<{
60808
61420
  * value?: string,
60809
61421
  * defaultValue?: string,
60810
61422
  * signal?: import("@preact/signals").Signal<string>,
@@ -60911,7 +61523,7 @@ const Textarea = ({
60911
61523
  * draws: put it under the box, in a form footer, next to a label, and feed it
60912
61524
  * the same value or signal as the textarea.
60913
61525
  *
60914
- * @type {import("ignore:preact").FunctionComponent<{
61526
+ * @type {import("preact").FunctionComponent<{
60915
61527
  * value?: string,
60916
61528
  * signal?: import("@preact/signals").Signal<string>,
60917
61529
  * maxLength?: number,
@@ -61144,8 +61756,8 @@ const useEditionController = () => {
61144
61756
  * it in CSS (the `[data-editing] input` rules in `control/table/table_css.js`),
61145
61757
  * the demo `control/demos/action/8_editable_demo.html` does it with props.
61146
61758
  *
61147
- * @type {import("ignore:preact").FunctionComponent<{
61148
- * children?: import("ignore:preact").ComponentChildren,
61759
+ * @type {import("preact").FunctionComponent<{
61760
+ * children?: import("preact").ComponentChildren,
61149
61761
  * action: Function | object,
61150
61762
  * editing?: { event?: Event } | null,
61151
61763
  * onEditEnd?: (detail: { success?: boolean, cancelled?: boolean, event: Event }) => void,
@@ -61505,6 +62117,77 @@ const commitSubtree = (controller, e) => {
61505
62117
  * <form> element (FormNested).
61506
62118
  */
61507
62119
 
62120
+ /**
62121
+ * @param {object} props
62122
+ * @param {boolean} [props.standalone] - Its value is its own: the form does not
62123
+ * register with the control group around it (a Picker, a ControlGroup), so
62124
+ * what is typed in it never becomes part of that group's value, and nothing
62125
+ * that group does — distributing a value, resetting, cascading validation —
62126
+ * reaches it. For a form that lives INSIDE something else while answering a
62127
+ * different question — "create the thing I am about to pick" inside a picker,
62128
+ * say. Every control takes the same prop, for the same reason. A form inside
62129
+ * another form is that by construction and does not need the prop: the group
62130
+ * it would register into is a form, which never owns another form's fields.
62131
+ * @param {boolean} [props.canSendWhileUnchanged] - Send even when nothing changed. By
62132
+ * default a form only acts on an answer that is actually new: submitting a
62133
+ * form nobody touched — one just rendered, one reopened and left alone — runs
62134
+ * no action (a field still showing its `defaultValue` is an answer: confirming
62135
+ * a suggestion), and after the first submission it is that value the next
62136
+ * one is compared against. Everything around the action still happens either
62137
+ * way: the constraints are checked, and what follows the send still follows
62138
+ * it (the slide moves on, the popup closes) — the user is done regardless of
62139
+ * whether there was anything to send. Set this for a form where sending the
62140
+ * same thing twice is the point — a single button that fires off a
62141
+ * notification, an action whose duplicates are fine. See also
62142
+ * `readOnlyWhileFormUnchanged` on `Button`, for a submit that should say it
62143
+ * is waiting rather than accept a press that sends nothing.
62144
+ * @param {boolean} [props.actionAbortable] - The person waiting may give up on
62145
+ * the send: closing the popup this form holds calls the run off and goes
62146
+ * through, instead of being refused. For a send whose answer may never come —
62147
+ * a request over a network that stops answering — where a sheet with no way
62148
+ * out is worse than an answer lost. Aborting frees the client and nothing
62149
+ * more: the write may already have landed on the other side (see
62150
+ * docs/actions.md#aborting-saves-resources-it-does-not-undo), so say it only
62151
+ * where the screen can be re-opened on what is actually there. Every control
62152
+ * inside the form inherits it along with the wait itself.
62153
+ * @param {boolean} [props.optimistic] - The send is taken as done on the form's
62154
+ * own word: no busy state, and what follows it (`command="--navi-close"`, a
62155
+ * slide moving on) runs at once, the run going on behind. A failure then has
62156
+ * no sheet to be read in: `resetOnError` puts the form back and the error
62157
+ * callout is drawn on what surrounds the closed sheet. For a write not worth being
62158
+ * waited for — a score, a note — never for one whose refusal changes what the
62159
+ * person does next (see
62160
+ * docs/popup_open.md#closing-when-a-button-also-runs-an-action).
62161
+ * @param {any} [props.pristineKey] - What the form is measured against, taken
62162
+ * again every time this changes. A form knows what it holds as soon as its
62163
+ * fields have registered, which is the right moment for a form whose values
62164
+ * are there on the first render — and never the right one for a screen that
62165
+ * modifies something: the resource arrives a request later and fills the
62166
+ * fields, and a form comparing against what it held BEFORE that opens already
62167
+ * changed. Pass whatever says the filling is done (a boolean, the resource
62168
+ * itself, a count of what has loaded) and what the fields carry at that
62169
+ * moment becomes the reference — including the fields that settle in a render
62170
+ * of their own, so there is no tick to wait for on the caller's side. Change
62171
+ * it once, when the screen is ready: taken again after someone started
62172
+ * typing, it would call what they wrote the reference.
62173
+ * @param {string} [props.command] - What follows a submission that went
62174
+ * through: the form has answered its question, and this says what the screen
62175
+ * does about it. Nothing runs when the submission is refused — the form then
62176
+ * stays in front of the user, showing what it is waiting for.
62177
+ *
62178
+ * `"--navi-close"` dismisses the popup the form is in,
62179
+ * `"--navi-left"`/`"--navi-right"`/`"--navi-up"`/`"--navi-down"` move on the
62180
+ * slide map it is in, `"--navi-nav-to:/the/url"` takes the user to a page,
62181
+ * `"--navi-void"` stays put — the one for a form inside a popup that must
62182
+ * stay open once it is sent (a side panel one keeps editing in).
62183
+ * Any navi command, really: it is triggered from the form, so it finds its
62184
+ * target the way that command always does.
62185
+ *
62186
+ * Left out, the surface the form sits in decides (see resolveAfterSend in
62187
+ * commands.js): a popup closes, a slide goes on to the next one — or back to
62188
+ * the one it came from when there is no next — and a form on a page does
62189
+ * nothing.
62190
+ */
61508
62191
  const Form = props => {
61509
62192
  const defaultRef = useRef();
61510
62193
  props.ref = props.ref || defaultRef;
@@ -61821,6 +62504,7 @@ const useRequestSubmitThroughGates = ref => {
61821
62504
  installImportMetaCssBuild(import.meta);
61822
62505
  const css$H = /* css */`.navi_group {
61823
62506
  --group-border-width: var(--navi-control-border-width);
62507
+ --border-width: initial;
61824
62508
 
61825
62509
  & > :not([navi-out-of-flow]) {
61826
62510
  &:hover, &[data-hover] {
@@ -65053,6 +65737,122 @@ SlideContainer.Down = SlideDown;
65053
65737
  SlideContainer.First = SlideFirst;
65054
65738
  SlideContainer.Last = SlideLast;
65055
65739
 
65740
+ /**
65741
+ * Displays a date/time value in a human-readable format using the current locale.
65742
+ *
65743
+ * @param {Date|number|string} children
65744
+ * The date to display. Accepts:
65745
+ * - a `Date` instance
65746
+ * - a Unix timestamp (number, in ms)
65747
+ * - a date string `"YYYY-MM-DD"` or any string parseable by `Date`
65748
+ * If the value cannot be parsed, it is rendered as-is.
65749
+ * If undefined/null, renders a placeholder shaped like the type: `"–"` for
65750
+ * `"relative"`, `"--:--"` for `"time"`, `"--"` for the durations, the
65751
+ * locale's field pattern for `"date"`/`"month"`/`"week"`/`"datetime"`.
65752
+ *
65753
+ * @param {"date"|"month"|"week"|"datetime"|"time"|"hour"|"minute"|"second"|"duration"|"relative"} [type="relative"]
65754
+ * Controls the display format:
65755
+ * - `"date"` → "lundi 11 mai" (long by default), "samedi 1 janvier 2050" outside the current year (see `year`);
65756
+ * `format="short"` → "lun. 11 mai"; `format="numeric"` → "11/05/2026";
65757
+ * `format={{ weekday: "long", month: "short" }}` spells the two apart → "mercredi 2 sept.";
65758
+ * a part set to `false` is dropped — `format={{ day: false, month: false }}` → "mercredi"
65759
+ * - `"month"` → "juin 2026"
65760
+ * - `"week"` → the ISO week as given ("2026-W38"), in a `<time datetime>`
65761
+ * - `"datetime"` → "lun. 11 mai, 14:30" (long); `format="short"` → "11 mai, 14:30"; `format="narrow"` → "11/05, 14:30"
65762
+ * - `"time"` → time-of-day as duration by default (e.g. "14:30" → "14 heures 30");
65763
+ * `format="timestring"` → clock "14 h 30". Midnight (00:xx) is
65764
+ * special-cased regardless of `format` — "0 heures 5 minutes"
65765
+ * would otherwise collapse to "5 minutes", indistinguishable
65766
+ * from an actual 5-minute duration: `format="long"` (default)
65767
+ * says "minuit et 5 minutes" (`format="short"/"narrow"/"compact"`
65768
+ * keep the zero hour instead — "0 h et 5 min"/"0h 5min"/"00h05").
65769
+ * `format="compact"` also zero-pads any single-digit hour, not
65770
+ * just midnight (e.g. "5h30" → "05h30"), closer to "05:30";
65771
+ * `pad={false}` writes it the way one says it instead ("8h30",
65772
+ * "8h"). See `<TimeRange>` for a span between two of them.
65773
+ * - `"hour"` → hours as duration (e.g. 1.5 → "1 heure 30 minutes")
65774
+ * - `"minute"` → minutes as duration (e.g. 90 → "1 heure 30 minutes")
65775
+ * - `"second"` → seconds as duration (e.g. 90 → "1 minute 30 secondes")
65776
+ * These three promote to the largest fitting unit, days included (36 hours
65777
+ * → "1 jour et 12 heures"); see `forceUnit` to stay in the given unit.
65778
+ * - `"duration"` → duration string/object/number (seconds); `format="iso"` → ISO 8601
65779
+ * - `"relative"` → "dans 1 heure 30" / "En cours" / "il y a 2 heures"
65780
+ * Handles past, present, and future.
65781
+ * `format="short"` → "dans 1 h et 30 min"; `format="narrow"`
65782
+ * → "+3 h"; `format="compact"` → "dans 1h30".
65783
+ * `eventDuration` defaults to 0 (instantaneous: no "En cours" window).
65784
+ *
65785
+ * @param {number|string} [eventDuration=0]
65786
+ * Duration of the event in milliseconds, or a duration string (`"PT1H30M"`,
65787
+ * `"1hour30minute"`). Only used with `type="relative"`.
65788
+ * When omitted, the event is instantaneous (point in time, no "En cours" window).
65789
+ * @param {boolean} [bare]
65790
+ * When true, strips the past-tense literal ("il y a", "ago") and returns only integer + unit.
65791
+ * Only applies to the past state of `type="relative"`.
65792
+ * @param {"long"|"short"|"narrow"|"compact"|"numeric"|"timestring"|"iso"|{ weekday?: "long"|"short"|"narrow"|false, day?: boolean, month?: "long"|"short"|"narrow"|"numeric"|false }} [format="long"]
65793
+ * Controls the verbosity of the output. Defaults to `"long"` for all types.
65794
+ * `"long"`, `"short"`, `"narrow"` and `"compact"` are verbosities every `type`
65795
+ * writes. The three under them name a shape only some types have; a `type`
65796
+ * without that shape writes its `"long"` form and names the dropped word in
65797
+ * the console.
65798
+ * - `"short"` → Intl short (e.g. "2 h et 15 min", short month for dates/datetimes, no weekday for datetime)
65799
+ * - `"narrow"` → Intl narrow (e.g. "2h 15min", numeric month for datetime)
65800
+ * - `"compact"` → custom compact notation (e.g. "2h15", no minute symbol
65801
+ * when hours present). A date has nothing tighter than its
65802
+ * digits, so `type="date"`/`"month"`/`"datetime"` write the
65803
+ * all-digit spelling ("17/09/2026", "17/09 14:30")
65804
+ * - `"numeric"` → numeric date, for `type="date"` and `type="month"` (e.g. "11/09/2026", "09/2026")
65805
+ * - `"timestring"` → clock display for `type="time"`, `type="minute"`, `type="hour"`, and `type="second"` (e.g. "14:30"; `type="second"` always writes the hours, "00:01:30" for 90)
65806
+ * - `"iso"` → ISO 8601 string, only for `type="duration"` (e.g. "PT2H15M")
65807
+ * - an object → `type="date"` only, one verbosity per part
65808
+ * (`{ weekday: "long", month: "short" }`): a narrow card
65809
+ * keeps the weekday whole, since that is what the eye
65810
+ * lands on, and abbreviates the month, since that is
65811
+ * where the characters are. `false` drops a part:
65812
+ * `{ day: false, month: false }` is the weekday alone
65813
+ * ("mardi"), `{ month: false }` weekday + day-of-month
65814
+ * ("mardi 18").
65815
+ * @param {boolean} [forceUnit=false]
65816
+ * Keeps the value in the unit named by `type` however big it gets, for
65817
+ * `type="hour"`, `"minute"` and `"second"`: 36 hours reads "36 heures"
65818
+ * instead of the default "1 jour et 12 heures". The default promotes to the
65819
+ * largest fitting unit because it reads better; force the unit when the unit
65820
+ * itself is the information (a quota, a counter).
65821
+ * @param {number} [decimals]
65822
+ * `type="duration"` and `type="second"` only — how many decimals a fraction
65823
+ * of a second keeps, rounded: with `decimals={1}`, 1.234 reads "1,2 seconde"
65824
+ * and 1.96 "2 secondes"; a whole value is never padded ("12 secondes").
65825
+ * Without it the fraction is written to the millisecond ("1,234 seconde"). A
65826
+ * duration measured in milliseconds rarely comes out whole, and its last
65827
+ * digits are usually noise.
65828
+ * @param {boolean} [pad=true]
65829
+ * `type="time"` + `format="compact"` only — whether the clock is written at a
65830
+ * fixed width. `true` (default) zero-pads the hour and always writes the
65831
+ * minutes ("08h00", "08h30"), so a column of times lines up. `false` writes
65832
+ * the shape one says out loud instead: bare hour, minutes only when there are
65833
+ * any ("8h", "8h30"). The spelled-out formats put their units in words and
65834
+ * need neither, so they ignore it.
65835
+ * @param {"hour"|"minute"} [precision]
65836
+ * `type="time"` + `format="compact"` only, and rarely set by hand — whether a
65837
+ * zero minute is written ("8h00") or dropped ("8h"). Defaults to whatever
65838
+ * `pad` implies; the one reason to force `"minute"` on an unpadded clock is
65839
+ * to agree with a partner that has minutes of its own, which is what
65840
+ * `<TimeRange>` does for you.
65841
+ * @param {boolean|"auto"} [year]
65842
+ * `type="date"` only — whether the year is written. `"auto"` writes it only
65843
+ * when the date is not in the current year, the way a calendar does
65844
+ * ("lundi 11 mai", but "samedi 1 janvier 2050"); `false` drops it ("30/07",
65845
+ * the day/month order still following the locale). Defaults to `"auto"`
65846
+ * for the spelled formats and to `true` for `format="numeric"`. A format
65847
+ * without the month (`{ month: false }`) never writes it.
65848
+ * @param {boolean} [dayLabel]
65849
+ * When true and `type="date"`, appends the locale-aware relative label
65850
+ * ("hier", "aujourd'hui", "demain") when the date is yesterday, today, or tomorrow.
65851
+ * @param {string} [lang]
65852
+ * BCP 47 locale tag (e.g. `"fr"`, `"en-US"`).
65853
+ * Defaults to `languagesSignal.value`: the browser's languages, as
65854
+ * `setPreferredLanguage`/`setSupportedLanguages` narrow them.
65855
+ */
65056
65856
  const Time = props => {
65057
65857
  const {
65058
65858
  type
@@ -65317,6 +66117,20 @@ const TimeMinute = ({
65317
66117
  children: String(children)
65318
66118
  });
65319
66119
  }
66120
+ if (!Number.isInteger(minutes)) {
66121
+ // a fraction of a minute is read in seconds, the way formatMinuteDuration
66122
+ // writes it ("1 minute et 30 secondes")
66123
+ const clock = formatClock(Math.round(minutes * 60));
66124
+ return jsx(TimeText, {
66125
+ dateTime: clock,
66126
+ ...props,
66127
+ children: format === "timestring" ? clock : formatMinuteDuration(minutes, {
66128
+ lang,
66129
+ format,
66130
+ forceUnit
66131
+ })
66132
+ });
66133
+ }
65320
66134
  const totalHours = Math.floor(minutes / 60);
65321
66135
  const remainingMinutes = minutes % 60;
65322
66136
  const hh = String(totalHours).padStart(2, "0");
@@ -65346,6 +66160,7 @@ const TimeSecond = ({
65346
66160
  lang = languagesSignal.value,
65347
66161
  format = "long",
65348
66162
  forceUnit = false,
66163
+ decimals,
65349
66164
  ...props
65350
66165
  }) => {
65351
66166
  if (children === undefined || children === null) {
@@ -65361,19 +66176,17 @@ const TimeSecond = ({
65361
66176
  children: String(children)
65362
66177
  });
65363
66178
  }
65364
- const h = Math.floor(seconds / 3600);
65365
- const m = Math.floor(seconds % 3600 / 60);
65366
- const s = seconds % 60;
65367
- const dateTime = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
66179
+ const dateTime = formatClock(seconds);
65368
66180
  let text;
65369
66181
  if (format === "timestring") {
65370
66182
  // Always HH:MM:SS to avoid ambiguity with HH:MM time-of-day format
65371
- text = dateTime;
66183
+ text = formatClock(seconds, decimals);
65372
66184
  } else {
65373
66185
  text = formatSecondDuration(seconds, {
65374
66186
  lang,
65375
66187
  format,
65376
- forceUnit
66188
+ forceUnit,
66189
+ decimals
65377
66190
  });
65378
66191
  }
65379
66192
  return jsx(TimeText, {
@@ -65426,6 +66239,7 @@ const TimeDuration = ({
65426
66239
  children,
65427
66240
  lang = languagesSignal.value,
65428
66241
  format = "long",
66242
+ decimals,
65429
66243
  ...props
65430
66244
  }) => {
65431
66245
  if (children === undefined || children === null) {
@@ -65473,7 +66287,8 @@ const TimeDuration = ({
65473
66287
  ...props,
65474
66288
  children: formatDuration(duration, {
65475
66289
  lang,
65476
- format
66290
+ format,
66291
+ decimals
65477
66292
  })
65478
66293
  });
65479
66294
  }
@@ -65485,7 +66300,8 @@ const TimeDuration = ({
65485
66300
  }
65486
66301
  const text = formatDuration(duration, {
65487
66302
  lang,
65488
- format
66303
+ format,
66304
+ decimals
65489
66305
  });
65490
66306
  return jsx(TimeText, {
65491
66307
  dateTime: isoString,
@@ -65625,6 +66441,24 @@ const TimeRange = ({
65625
66441
  });
65626
66442
  };
65627
66443
 
66444
+ // "HH:MM:SS", the fraction of a second after a period ("00:00:01.234"): the
66445
+ // shape a `datetime` attribute reads, which takes up to 3 decimals.
66446
+ const formatClock = (seconds, decimals = 3) => {
66447
+ const factor = 10 ** decimals;
66448
+ const total = Math.round(seconds * factor);
66449
+ const fraction = total % factor;
66450
+ const totalSeconds = (total - fraction) / factor;
66451
+ const h = Math.floor(totalSeconds / 3600);
66452
+ const m = Math.floor(totalSeconds % 3600 / 60);
66453
+ const s = totalSeconds % 60;
66454
+ const clock = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
66455
+ if (fraction === 0) {
66456
+ return clock;
66457
+ }
66458
+ const digits = String(fraction).padStart(decimals, "0").replace(/0+$/, "");
66459
+ return `${clock}.${digits}`;
66460
+ };
66461
+
65628
66462
  /**
65629
66463
  * Wraps multiple inputs together and handles keyboard navigation and paste
65630
66464
  * distribution between them.
@@ -65851,6 +66685,11 @@ const renderSafe = (value) => {
65851
66685
  };
65852
66686
 
65853
66687
  installImportMetaCssBuild(import.meta);
66688
+ /**
66689
+ * The group a popup opens in, or null: what a Popover or a Dialog reads to know
66690
+ * that its wall is the group's. Every popup provides null again to what it
66691
+ * holds, so a popup opened from inside a popup is in front of it, not a peer.
66692
+ */
65854
66693
  const PopupGroupContext = createContext(null);
65855
66694
 
65856
66695
  /**
@@ -66009,13 +66848,13 @@ const css$F = /* css */`@property --x-popup-group-top {
66009
66848
  * `navi-out-of-flow` like everything navi adds to a tree. See
66010
66849
  * docs/popup_backdrop.md.
66011
66850
  *
66012
- * @type {import("ignore:preact").FunctionComponent<{
66851
+ * @type {import("preact").FunctionComponent<{
66013
66852
  * backdropVariant?: "auto" | "lift" | "discrete" | "invisible",
66014
66853
  * backdropColor?: string,
66015
66854
  * backdropFilter?: string,
66016
66855
  * themeColor?: string | false,
66017
66856
  * role?: string,
66018
- * children?: import("ignore:preact").ComponentChildren,
66857
+ * children?: import("preact").ComponentChildren,
66019
66858
  * [key: string]: any,
66020
66859
  * }>}
66021
66860
  * @param {"auto"|"lift"|"discrete"|"invisible"} [props.backdropVariant="auto"] -
@@ -68062,6 +68901,35 @@ const clipOf = (side, distance) => {
68062
68901
  return `inset(${UNCUT} ${UNCUT} ${UNCUT} ${cut})`;
68063
68902
  };
68064
68903
 
68904
+ /**
68905
+ * The cross that closes the popup it sits in — reached as `<Popup.Close />`,
68906
+ * `<Dialog.Close />` or `<Popover.Close />`. A discrete icon button sending
68907
+ * `--navi-close`, which the nearest popup above it answers; the caller only
68908
+ * decides where it goes (the right end of a header, most of the time).
68909
+ *
68910
+ * It is the way out of a popup, so it is the one affordance in there that the
68911
+ * surrounding control's read-only and disabled do not reach: a read-only picker
68912
+ * still opens (reading changes nothing) and hands its popup that read-only, and
68913
+ * a cross wearing it would trap the user in a popup they were invited to read.
68914
+ * A close button written by hand inherits it and refuses the press — which is
68915
+ * the reason to use this one rather than a `<Button command="--navi-close" />`.
68916
+ *
68917
+ * Exempt from the state around it, never from the popup's own answer: the cross
68918
+ * ASKS, and the popup decides — a dialog holding a form mid-send keeps the
68919
+ * close, a Picker holding a value that does not validate keeps it too.
68920
+ *
68921
+ * @type {import("preact").FunctionComponent<{
68922
+ * label?: string,
68923
+ * iconSize?: number | string,
68924
+ * } & Record<string, any>>}
68925
+ * @param {string} [props.label] - What assistive tech reads for the cross
68926
+ * (navi's own "Close" text by default, in the active language).
68927
+ * @param {number|string} [props.iconSize] - How big the cross is drawn, apart
68928
+ * from the box around it. Unset, the glyph follows the button's font size
68929
+ * like a character, and `size` moves glyph and padding together. Set it when
68930
+ * the cross stands in for an affordance the app draws elsewhere and has to
68931
+ * land on the same pixels: `padding="s" iconSize="m"`.
68932
+ */
68065
68933
  const PopupClose = ({
68066
68934
  label,
68067
68935
  iconSize,
@@ -68310,6 +69178,9 @@ const TitlePseudoClasses = [":hover"];
68310
69178
  * step of a form — so only the title knows when it is there.
68311
69179
  */
68312
69180
 
69181
+ // The popup a title names: the nearest one around it. A surface that is no
69182
+ // popup of this kind but holds content of its own (a picker's callout) sets it
69183
+ // back to null, so a title in there names nothing rather than the popup around.
68313
69184
  const PopupTitleContext = createContext(null);
68314
69185
 
68315
69186
  /**
@@ -68319,10 +69190,10 @@ const PopupTitleContext = createContext(null);
68319
69190
  * announces the popup by it, and `getByRole("dialog", { name })` finds it.
68320
69191
  * An `aria-label`/`aria-labelledby` written on the popup itself wins over it.
68321
69192
  *
68322
- * @type {import("ignore:preact").FunctionComponent<{
69193
+ * @type {import("preact").FunctionComponent<{
68323
69194
  * as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6",
68324
69195
  * id?: string,
68325
- * children?: import("ignore:preact").ComponentChildren,
69196
+ * children?: import("preact").ComponentChildren,
68326
69197
  * } & Record<string, any>>}
68327
69198
  */
68328
69199
  const PopupTitle = props => {
@@ -68383,6 +69254,12 @@ const usePopupName = ({
68383
69254
  const NO_TITLE = [];
68384
69255
 
68385
69256
  installImportMetaCssBuild(import.meta);
69257
+ // Same need popover.jsx has for its own local renderer (see its
69258
+ // openLocalPopoverCount): a layer="local" dialog is a plain positioned element
69259
+ // in normal flow, so it gets none of the top layer's free "last shown wins" and
69260
+ // would paint under anything declared after it. A live count, not an
69261
+ // ever-growing counter, so the order stays small and resets once nothing is
69262
+ // open.
68386
69263
  let openLocalDialogCount = 0;
68387
69264
  const css$C = /* css */`
68388
69265
  /* jsenv-css-opaque: popupCss overlaps these rules, own sheet = cascade
@@ -69457,7 +70334,7 @@ const css$C = /* css */`
69457
70334
  * the dialog: its accessible name when neither `aria-label`/`aria-labelledby`
69458
70335
  * nor a `<Dialog.Title>` names it. A Picker passes its trigger's.
69459
70336
  * @param {string} [props.openerAriaLabel] - The same, as a string.
69460
- * @param {import("ignore:preact").ComponentChildren} props.children
70337
+ * @param {import("preact").ComponentChildren} props.children
69461
70338
  */
69462
70339
  const Dialog = props => {
69463
70340
  import.meta.css = [css$C, "@jsenv/navi/src/layout/dialog.jsx"];
@@ -70293,8 +71170,10 @@ const useDialogProps = props => {
70293
71170
  // gap — the same thing its CSS width/height say (see [data-expand-x],
70294
71171
  // [data-expand-y]) — so the placement derives it instead of measuring
70295
71172
  // it: a measurement here lays out the dialog's whole content, rows and
70296
- // all, for a number already known.
70297
- const fill = expandX && expandY;
71173
+ // all, for a number already known. A cap (maxWidth/maxHeight, a Picker's
71174
+ // anchor ceiling) stretches it to less than that, and only a measured
71175
+ // box can be centred.
71176
+ const fill = expandX && expandY && !isSizeCapped(dialogEl);
70298
71177
  const pickOptions = {
70299
71178
  positionArea,
70300
71179
  container: positionedAncestor,
@@ -70808,6 +71687,14 @@ const switchTransitionsOn = dialogEl => {
70808
71687
  dialogEl.style.transitionProperty = "";
70809
71688
  dialogEl.removeAttribute("navi-transitions-off");
70810
71689
  };
71690
+
71691
+ // Reads style, not layout: an unset cap is the guaranteed-invalid value its
71692
+ // @property gives it, and reads back as "". Catches a cap written inline by
71693
+ // the prop as well as one set from a stylesheet.
71694
+ const isSizeCapped = dialogEl => {
71695
+ const computedStyle = getComputedStyle(dialogEl);
71696
+ return computedStyle.getPropertyValue("--dialog-max-width") !== "" || computedStyle.getPropertyValue("--dialog-max-height") !== "";
71697
+ };
70811
71698
  const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":focus-within"];
70812
71699
 
70813
71700
  // Lets consumers pass animationDuration="0.5s" as a regular prop; Box maps
@@ -70833,6 +71720,11 @@ Dialog.Close = PopupClose;
70833
71720
  Dialog.Title = PopupTitle;
70834
71721
 
70835
71722
  installImportMetaCssBuild(import.meta);
71723
+ // Custom renderer only (layer="local") — count of currently-open instances,
71724
+ // see --popover-stack-order's own CSS comment for why a plain "last shown"
71725
+ // default doesn't happen for free the way it does for the top-layer
71726
+ // renderer. A live count (not an ever-growing counter) so the assigned
71727
+ // order stays small and resets to 0 once nothing is open.
70836
71728
  let openLocalPopoverCount = 0;
70837
71729
  const css$B = /* css */`
70838
71730
  /* jsenv-css-opaque: popupCss overlaps these rules, own sheet = cascade
@@ -71440,7 +72332,7 @@ const css$B = /* css */`
71440
72332
  * something names — these, `aria-label`/`aria-labelledby`, a
71441
72333
  * `<Popover.Title>` — is a `dialog`, unless given another `role`: a name on
71442
72334
  * an element with no role is read by nobody.
71443
- * @param {import("ignore:preact").ComponentChildren} props.children
72335
+ * @param {import("preact").ComponentChildren} props.children
71444
72336
  */
71445
72337
  const Popover = props => {
71446
72338
  import.meta.css = [css$B, "@jsenv/navi/src/layout/popover.jsx"];
@@ -72798,7 +73690,7 @@ const css$A = /* css */`@layer navi {
72798
73690
  * content whose fresh state is its initial state: an edit left unsent at
72799
73691
  * the close does not come back at the next opening (an untouched field
72800
73692
  * needs none of this to show a new `defaultValue` — it follows one).
72801
- * @param {import("ignore:preact").ComponentChildren} props.children
73693
+ * @param {import("preact").ComponentChildren} props.children
72802
73694
  */
72803
73695
  const Popup = props => {
72804
73696
  import.meta.css = [css$A, "@jsenv/navi/src/layout/popup.jsx"];
@@ -74475,6 +75367,18 @@ const PickerConfirmBody = ({
74475
75367
  });
74476
75368
  };
74477
75369
 
75370
+ /**
75371
+ * PickerNaviMinute — a custom minute picker that opens a number input.
75372
+ *
75373
+ * Renders a popover containing an `<input type="number">` pre-filled with the
75374
+ * current value, plus Confirm / Clear / Cancel actions. The picker holds a
75375
+ * number of minutes: its own field is a text one told
75376
+ * `navi-input-type="minute"`, so the value is read back as a number and a
75377
+ * min/max/step message speaks of minutes.
75378
+ *
75379
+ * Usage:
75380
+ * <Picker type="navi_minute" min={15} max={300} step={15} value={90} />
75381
+ */
74478
75382
  const PickerNaviMinute = props => {
74479
75383
  const Next = useNextResolver();
74480
75384
  const {
@@ -74858,6 +75762,16 @@ const LoadingDotsSvg = () => {
74858
75762
  });
74859
75763
  };
74860
75764
 
75765
+ /**
75766
+ * A loading indicator sized to match the surrounding text (1em × 1em).
75767
+ * Inherits color from the current text color by default.
75768
+ *
75769
+ * @param {"circle"|"dots"} [props.variant="circle"] - Visual style of the indicator
75770
+ * @param {string} [props.color="currentColor"] - Color of the indicator; inherits from CSS `color` by default
75771
+ *
75772
+ * Every other prop goes to the `Icon` it is drawn in: `size` is a font size
75773
+ * (the indicator being 1em), `width`/`height` size it as a block.
75774
+ */
74861
75775
  const LoadingIndicator = ({
74862
75776
  variant = "circle",
74863
75777
  ...props
@@ -74920,7 +75834,7 @@ const css$x = /* css */`@layer navi {
74920
75834
  * Everything about how it looks goes through a CSS var, so it can be set per
74921
75835
  * separator as a prop or once for a whole surface in CSS.
74922
75836
  *
74923
- * @type {import("ignore:preact").FunctionComponent<{
75837
+ * @type {import("preact").FunctionComponent<{
74924
75838
  * vertical?: boolean,
74925
75839
  * size?: string|number,
74926
75840
  * color?: string,
@@ -76523,6 +77437,9 @@ const applySearchHighlight = (el, highlight) => {
76523
77437
  };
76524
77438
 
76525
77439
  installImportMetaCssBuild(import.meta);
77440
+ // Everything the list knows about its items — see list_items.js. Filled in by
77441
+ // the children as they render, read by everything that must reserve room for
77442
+ // what is not rendered, count what is, or know what stands above it.
76526
77443
  const ListItemsContext = createContext(null);
76527
77444
  // The group an item is declared in, by id (see ListItemGroup): what its count
76528
77445
  // and its separator are scoped to.
@@ -78038,6 +78955,19 @@ const useListScrollSync = ({
78038
78955
  // eslint-disable-next-line no-unused-expressions
78039
78956
  virtualItemSizeSignal.value;
78040
78957
  }
78958
+ // The hold ending — the user reaching for the list, something else moving
78959
+ // it, the item it opens on never coming — ends its hold on the document with
78960
+ // it (see holdDocumentScroll), rather than at the next render, which nothing
78961
+ // owes: an offset put back on this page meanwhile is the reader's.
78962
+ const releaseDocumentHoldRef = useRef(null);
78963
+ const letGo = () => {
78964
+ startPlaceRef.current.userTookOver = true;
78965
+ const releaseDocumentHold = releaseDocumentHoldRef.current;
78966
+ if (releaseDocumentHold) {
78967
+ releaseDocumentHoldRef.current = null;
78968
+ releaseDocumentHold();
78969
+ }
78970
+ };
78041
78971
  // Set around a scroll the list performs itself. What it protects against is
78042
78972
  // not the scroll event as such, but what the listener would conclude from it:
78043
78973
  // the position it is about to read was chosen to keep the items where they
@@ -78271,7 +79201,7 @@ const useListScrollSync = ({
78271
79201
  }
78272
79202
  openAt = scrolledFallback;
78273
79203
  if (openAt === "start" || openAt === undefined) {
78274
- startPlaceRef.current.userTookOver = true;
79204
+ letGo();
78275
79205
  return;
78276
79206
  }
78277
79207
  } else {
@@ -78423,13 +79353,39 @@ const useListScrollSync = ({
78423
79353
  }, []);
78424
79354
  // Held on an item of a list scrolling the document: where the document goes is
78425
79355
  // this list's to say, measured on the item, not the url's offset in pixels
78426
- // (see holdDocumentScroll).
79356
+ // (see holdDocumentScroll). The hold read again: a layout effect of this
79357
+ // commit may have let go already (see placeWhereHeld).
79358
+ const routePage = useContext(RoutePageContext);
78427
79359
  useLayoutEffect(() => {
78428
- if (!heldSomewhere || getScroller() !== document.scrollingElement) {
79360
+ if (!heldSomewhere || startPlaceRef.current.userTookOver || getScroller() !== document.scrollingElement) {
78429
79361
  return undefined;
78430
79362
  }
78431
- return holdDocumentScroll();
78432
- }, [heldSomewhere, scrollerElResolved]);
79363
+ const releaseDocumentHold = holdDocumentScroll(routePage);
79364
+ releaseDocumentHoldRef.current = releaseDocumentHold;
79365
+ return () => {
79366
+ releaseDocumentHoldRef.current = null;
79367
+ releaseDocumentHold();
79368
+ };
79369
+ }, [heldSomewhere, scrolledWanted, scrollerElResolved]);
79370
+ // A page kept while its route transition plays is hidden until it is taken
79371
+ // down: nothing of the list can be measured there any more, and its scroller
79372
+ // shows the page arriving (see route_page_context.js). So the list lets go of
79373
+ // it, and stops answering its resizes and scrolls: measured hidden, every
79374
+ // item stands at 0×0, and the top item put back where it was would scroll
79375
+ // the page arriving by that item's offset. Kept mounted on purpose, so the
79376
+ // listeners stay; what they would do is what ends.
79377
+ const pageLeftRef = useRef(false);
79378
+ useLayoutEffect(() => {
79379
+ if (!routePage) {
79380
+ return undefined;
79381
+ }
79382
+ return routePage.leavingSignal.subscribe(leaving => {
79383
+ if (leaving) {
79384
+ pageLeftRef.current = true;
79385
+ letGo();
79386
+ }
79387
+ });
79388
+ }, []);
78433
79389
  // What to do when the list's own geometry moves under it, kept fresh for the
78434
79390
  // observer below (which is installed once).
78435
79391
  const onGeometryChangeRef = useRef(null);
@@ -78463,18 +79419,15 @@ const useListScrollSync = ({
78463
79419
  return undefined;
78464
79420
  }
78465
79421
  const scrollerEl = getScroller();
78466
- const takeOver = () => {
78467
- startPlaceRef.current.userTookOver = true;
78468
- };
78469
79422
  const takeOverEvents = ["wheel", "touchstart", "pointerdown", "keydown"];
78470
79423
  for (const type of takeOverEvents) {
78471
- scrollerEl.addEventListener(type, takeOver, {
79424
+ scrollerEl.addEventListener(type, letGo, {
78472
79425
  passive: true
78473
79426
  });
78474
79427
  }
78475
79428
  return () => {
78476
79429
  for (const type of takeOverEvents) {
78477
- scrollerEl.removeEventListener(type, takeOver);
79430
+ scrollerEl.removeEventListener(type, letGo);
78478
79431
  }
78479
79432
  };
78480
79433
  }, [scrolledWanted, scrollerElResolved]);
@@ -78631,6 +79584,9 @@ const useListScrollSync = ({
78631
79584
  }
78632
79585
  };
78633
79586
  const observer = new ResizeObserver(entries => {
79587
+ if (pageLeftRef.current) {
79588
+ return;
79589
+ }
78634
79590
  // A box that grows may be a box that starts to scroll — the scroller is
78635
79591
  // resolved again before anything is done about the resize, so that a
78636
79592
  // list which has outgrown a bounded ancestor stops holding on to the
@@ -79035,6 +79991,9 @@ const useListScrollSync = ({
79035
79991
  }
79036
79992
  const scrollerEl = getScroller();
79037
79993
  const onScroll = () => {
79994
+ if (pageLeftRef.current) {
79995
+ return;
79996
+ }
79038
79997
  // Which way the user is going, read before the position is stored: what
79039
79998
  // the window keeps ahead is ahead of this.
79040
79999
  const previousScroll = currentScrollRef.current;
@@ -79092,7 +80051,7 @@ const useListScrollSync = ({
79092
80051
  const low = way.from < way.to ? way.from : way.to;
79093
80052
  const high = way.from < way.to ? way.to : way.from;
79094
80053
  if (position < low - 1 || position > high + 1) {
79095
- place.userTookOver = true;
80054
+ letGo();
79096
80055
  }
79097
80056
  };
79098
80057
  // A page-level scroller does not emit "scroll" on the element itself
@@ -81053,7 +82012,7 @@ const LIST_ITEM_STYLE_CSS_VARS = {
81053
82012
  * of it. Props not listed here are forwarded to the rendered <li> (Box layout
81054
82013
  * props included).
81055
82014
  *
81056
- * @type {import("ignore:preact").FunctionComponent<{
82015
+ * @type {import("preact").FunctionComponent<{
81057
82016
  * id?: string,
81058
82017
  * selectable?: boolean,
81059
82018
  * value?: any,
@@ -81062,14 +82021,14 @@ const LIST_ITEM_STYLE_CSS_VARS = {
81062
82021
  * pointed?: boolean,
81063
82022
  * selectableArea?: "all" | "manual",
81064
82023
  * skeleton?: boolean,
81065
- * error?: boolean | import("ignore:preact").ComponentChildren,
82024
+ * error?: boolean | import("preact").ComponentChildren,
81066
82025
  * onErrorDismiss?: (event: Event) => void,
81067
82026
  * loading?: boolean | "adding" | "removing" | "updating",
81068
82027
  * readOnly?: boolean,
81069
82028
  * filtered?: boolean,
81070
82029
  * hidden?: boolean,
81071
82030
  * matchInfo?: {match: boolean, matchScore?: number, matchRanges?: Array<[number, number]>},
81072
- * children?: import("ignore:preact").ComponentChildren,
82031
+ * children?: import("preact").ComponentChildren,
81073
82032
  * [key: string]: any,
81074
82033
  * }>}
81075
82034
  * @param {string} [props.id]
@@ -81104,7 +82063,7 @@ const LIST_ITEM_STYLE_CSS_VARS = {
81104
82063
  * real item. This is what a `renderSkeleton` returns for an item on its way;
81105
82064
  * Box layout props (padding…) pass through so the placeholder can match the
81106
82065
  * real items' metrics.
81107
- * @param {boolean|import("ignore:preact").ComponentChildren} [props.error]
82066
+ * @param {boolean|import("preact").ComponentChildren} [props.error]
81108
82067
  * What this item stood for failed: the message replaces its content, styled
81109
82068
  * like the list's own error. `true` shows a generic sentence. When
81110
82069
  * `onErrorDismiss` is given, a dismiss button is drawn next to the message
@@ -81244,21 +82203,21 @@ const VISIBILITY_HIDDEN_STYLE = {
81244
82203
  * Several runs can live in one list, next to plain `<List.Item>` children and
81245
82204
  * inside `<List.Group>`s; each takes its place in declaration order.
81246
82205
  *
81247
- * @type {import("ignore:preact").FunctionComponent<{
81248
- * renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("ignore:preact").ComponentChildren,
82206
+ * @type {import("preact").FunctionComponent<{
82207
+ * renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("preact").ComponentChildren,
81249
82208
  * findText?: (item: any, index: number) => string,
81250
82209
  * items?: any[],
81251
82210
  * itemsAction?: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
81252
82211
  * debounce?: number,
81253
82212
  * count?: number,
81254
82213
  * groupBy?: (item: any, index: number) => any,
81255
- * renderGroupLabel?: (item: any, index: number) => import("ignore:preact").ComponentChildren,
82214
+ * renderGroupLabel?: (item: any, index: number) => import("preact").ComponentChildren,
81256
82215
  * groupLabelProps?: (item: any, index: number) => object,
81257
82216
  * pageSize?: number,
81258
82217
  * memoryBudget?: number,
81259
- * renderSkeleton?: false | ((index: number) => import("ignore:preact").ComponentChildren),
82218
+ * renderSkeleton?: false | ((index: number) => import("preact").ComponentChildren),
81260
82219
  * itemSize?: (index: number) => number,
81261
- * renderError?: (failure: {error: any, retry: () => void, start: number, end: number}) => import("ignore:preact").ComponentChildren,
82220
+ * renderError?: (failure: {error: any, retry: () => void, start: number, end: number}) => import("preact").ComponentChildren,
81262
82221
  * onRequestStateChange?: (state: {busy: boolean, refreshing: boolean, range: {start: number, end: number}|null}) => void,
81263
82222
  * onCountChange?: (count: number|undefined) => void,
81264
82223
  * }>}
@@ -82853,7 +83812,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
82853
83812
  * List — generic virtualized scroll container.
82854
83813
  * Items must use <List.Item> to participate in tracking.
82855
83814
  *
82856
- * @type {import("ignore:preact").FunctionComponent<{
83815
+ * @type {import("preact").FunctionComponent<{
82857
83816
  * selectable?: boolean,
82858
83817
  * multiple?: boolean,
82859
83818
  * deselectable?: boolean,
@@ -82874,16 +83833,16 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
82874
83833
  * scroller?: "self" | "parent" | "document" | Element | {current: Element},
82875
83834
  * hoverWhileScrolling?: boolean,
82876
83835
  * scrollResetOnNavigation?: boolean,
82877
- * fallback?: import("ignore:preact").ComponentChildren,
82878
- * searchFallback?: import("ignore:preact").ComponentChildren,
83836
+ * fallback?: import("preact").ComponentChildren,
83837
+ * searchFallback?: import("preact").ComponentChildren,
82879
83838
  * searchText?: string,
82880
83839
  * searchNoMatchMode?: "remove" | "invisible_and_inert" | "muted",
82881
83840
  * loading?: boolean,
82882
- * loadingFallback?: "skeleton" | "loader" | import("ignore:preact").ComponentChildren,
83841
+ * loadingFallback?: "skeleton" | "loader" | import("preact").ComponentChildren,
82883
83842
  * loadingSkeletonCount?: number,
82884
- * renderSkeleton?: false | ((index: number) => import("ignore:preact").ComponentChildren),
82885
- * error?: boolean | import("ignore:preact").ComponentChildren,
82886
- * separator?: boolean | import("ignore:preact").ComponentChildren,
83843
+ * renderSkeleton?: false | ((index: number) => import("preact").ComponentChildren),
83844
+ * error?: boolean | import("preact").ComponentChildren,
83845
+ * separator?: boolean | import("preact").ComponentChildren,
82887
83846
  * itemTransition?: boolean,
82888
83847
  * lockSize?: boolean,
82889
83848
  * horizontal?: boolean,
@@ -82897,7 +83856,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
82897
83856
  * expandX?: boolean,
82898
83857
  * expandY?: boolean,
82899
83858
  * expand?: boolean,
82900
- * children?: import("ignore:preact").ComponentChildren,
83859
+ * children?: import("preact").ComponentChildren,
82901
83860
  * [key: string]: any,
82902
83861
  * }>}
82903
83862
  * @param {string} [props.columns]
@@ -82957,7 +83916,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
82957
83916
  * have, so nothing moves when they arrive. Used for the items a `<List.Items>`
82958
83917
  * stands for and does not hold yet, and for the placeholder items drawn while
82959
83918
  * the whole list is `loading`. Defaults to a bare `<List.Item skeleton>`.
82960
- * @param {"skeleton"|"loader"|import("ignore:preact").ComponentChildren} [props.loadingFallback="skeleton"]
83919
+ * @param {"skeleton"|"loader"|import("preact").ComponentChildren} [props.loadingFallback="skeleton"]
82961
83920
  * What to display in place of the items while `loading` — that is, while
82962
83921
  * there is nothing to show at all: `"skeleton"` renders
82963
83922
  * `loadingSkeletonCount` placeholder items (look:
@@ -83141,6 +84100,17 @@ const List = /*#__PURE__*/Object.assign(ListResolved, {
83141
84100
  Group: ListItemGroup
83142
84101
  });
83143
84102
 
84103
+ /**
84104
+ * PickerNaviTime — a fully custom time picker driven by min/max/step.
84105
+ *
84106
+ * Unlike the native `<input type="time">`, this picker only exposes the exact
84107
+ * slots generated from min/max/step, so the user can never pick an out-of-range
84108
+ * or out-of-step value. The popover content is a selectable `List` of the
84109
+ * available slots; picking one closes the popover and submits the value.
84110
+ *
84111
+ * Usage:
84112
+ * <Picker type="navi_time" min="08:00" max="18:00" step="00:30" />
84113
+ */
83144
84114
  const PickerNaviTime = props => {
83145
84115
  const Next = useNextResolver();
83146
84116
  const {
@@ -83248,18 +84218,20 @@ const createBadgeRegistry = () => {
83248
84218
  };
83249
84219
 
83250
84220
  installImportMetaCssBuild(import.meta);
83251
- const css$u = /* css */`@layer navi;
84221
+ const css$u = /* css */`@layer navi {
84222
+ .navi_badge {
84223
+ --font-size: .7em;
84224
+ --badge-padding-x-default: .8em;
84225
+ --badge-padding-y-default: .4em;
84226
+ }
84227
+ }
83252
84228
 
83253
84229
  .navi_badge {
83254
- --font-size: .7em;
83255
- --badge-padding-x-default: .8em;
83256
- --badge-padding-y-default: .4em;
83257
84230
  --x-badge-padding-top: var(--badge-padding-top, var(--badge-padding-y, var(--badge-padding, var(--badge-padding-y-default))));
83258
84231
  --x-badge-padding-right: var(--badge-padding-right, var(--badge-padding-x, var(--badge-padding, var(--badge-padding-x-default))));
83259
84232
  --x-badge-padding-bottom: var(--badge-padding-bottom, var(--badge-padding-y, var(--badge-padding, var(--badge-padding-y-default))));
83260
84233
  --x-badge-padding-left: var(--badge-padding-left, var(--badge-padding-x, var(--badge-padding, var(--badge-padding-x-default))));
83261
84234
  --x-background: var(--background, light-dark(#e0e0e0, #3a3a3a));
83262
- --x-background-color: var(--background-color, var(--x-background));
83263
84235
  --x-color: var(--color, white);
83264
84236
  max-width: 200px;
83265
84237
  padding-top: var(--x-badge-padding-top);
@@ -83270,7 +84242,6 @@ const css$u = /* css */`@layer navi;
83270
84242
  font-size: var(--font-size);
83271
84243
  text-box: trim-both cap alphabetic;
83272
84244
  background: var(--x-background);
83273
- background-color: var(--x-background-color);
83274
84245
  border-radius: 1em;
83275
84246
  display: inline;
83276
84247
  position: relative;
@@ -83396,7 +84367,6 @@ const BadgeStyleCSSVars = {
83396
84367
  paddingRight: "--badge-padding-right",
83397
84368
  paddingBottom: "--badge-padding-bottom",
83398
84369
  paddingLeft: "--badge-padding-left",
83399
- backgroundColor: "--background-color",
83400
84370
  background: "--background",
83401
84371
  borderColor: "--border-color",
83402
84372
  color: "--color",
@@ -83570,7 +84540,7 @@ const useBadgeRegistry = (children, enabled) => {
83570
84540
  * shrinkWrap on its own builds the measurement ghost: under maxLines the list
83571
84541
  * is measured in place, once the rows are known.
83572
84542
  *
83573
- * @param {import("ignore:preact").ComponentChildren} [fallback]
84543
+ * @param {import("preact").ComponentChildren} [fallback]
83574
84544
  * Rendered in place of the badges when there is none. Without it an empty
83575
84545
  * list renders nothing at all. In a <Picker> this is the only placeholder
83576
84546
  * the user gets — a picker given a `ui` does not draw its own — so pass the
@@ -84204,10 +85174,10 @@ const PickerArrayUI = /*#__PURE__*/asPickerOwnUI(() => {
84204
85174
  * — a row being read still says what was picked, it just no longer offers to
84205
85175
  * unpick it.
84206
85176
  *
84207
- * @type {import("ignore:preact").FunctionComponent<{
85177
+ * @type {import("preact").FunctionComponent<{
84208
85178
  * value: any,
84209
85179
  * commandFor?: string,
84210
- * children?: import("ignore:preact").ComponentChildren,
85180
+ * children?: import("preact").ComponentChildren,
84211
85181
  * [key: string]: any,
84212
85182
  * }>}
84213
85183
  * @param {any} value The value this chip stands for — one entry of what the
@@ -84473,6 +85443,7 @@ const css$r = /* css */`@layer navi {
84473
85443
  .navi_picker {
84474
85444
  --picker-border-radius: var(--navi-control-border-radius);
84475
85445
  --picker-border-width: var(--navi-control-border-width);
85446
+ --border-width: var(--picker-border-width);
84476
85447
  --picker-outline-width: var(--navi-focus-outline-width);
84477
85448
  --picker-outline-offset: calc(-.5 * var(--picker-outline-width));
84478
85449
  --picker-outline-color: var(--navi-focus-outline-color);
@@ -84512,6 +85483,111 @@ const css$r = /* css */`@layer navi {
84512
85483
  --picker-align-x-default: flex-start;
84513
85484
  --picker-align-y-default: center;
84514
85485
  --picker-text-align-default: initial;
85486
+ font-size: var(--picker-font-size);
85487
+ font-family: var(--picker-font-family);
85488
+ line-height: var(--navi-control-line-height);
85489
+ }
85490
+
85491
+ .navi_picker[data-variant="icon"] {
85492
+ --picker-border-width: 0px;
85493
+ --picker-border-color: transparent;
85494
+ --picker-border-color-hover: var(--picker-border-color);
85495
+ --picker-border-color-readonly: var(--picker-border-color);
85496
+ --picker-border-color-disabled: var(--picker-border-color);
85497
+ --picker-background-color: transparent;
85498
+ --picker-background-color-hover: var(--picker-background-color);
85499
+ --picker-background-color-readonly: var(--picker-background-color);
85500
+ --picker-background-color-disabled: var(--picker-background-color);
85501
+ --picker-icon-color: currentColor;
85502
+ }
85503
+
85504
+ .navi_picker[data-variant="discrete"] {
85505
+ --picker-border-width: 0px;
85506
+ --picker-border-color: transparent;
85507
+ --picker-border-color-hover: var(--picker-border-color);
85508
+ --picker-border-color-readonly: var(--picker-border-color);
85509
+ --picker-border-color-disabled: var(--picker-border-color);
85510
+ --picker-background-color: transparent;
85511
+ --picker-background-color-hover: color-mix(in srgb,
85512
+ currentColor 8%,
85513
+ var(--picker-background-color));
85514
+ --picker-background-color-readonly: var(--picker-background-color);
85515
+ --picker-background-color-disabled: var(--picker-background-color);
85516
+ }
85517
+
85518
+ .navi_picker[data-variant="headless"] {
85519
+ --picker-padding-x-default: 0;
85520
+ --picker-padding-y-default: 0;
85521
+ --picker-border-width: 0px;
85522
+ --picker-border-color: transparent;
85523
+ --picker-outline-width: 0px;
85524
+ --picker-border-color-hover: var(--picker-border-color);
85525
+ --picker-border-color-readonly: var(--picker-border-color);
85526
+ --picker-border-color-disabled: var(--picker-border-color);
85527
+ --picker-background-color: transparent;
85528
+ --picker-background-color-hover: var(--picker-background-color);
85529
+ --picker-background-color-readonly: var(--picker-background-color);
85530
+ --picker-background-color-disabled: var(--picker-background-color);
85531
+ --picker-icon-color: currentColor;
85532
+ }
85533
+
85534
+ .navi_picker[data-variant="bare"] {
85535
+ --picker-padding-x-default: 0;
85536
+ --picker-padding-y-default: 0;
85537
+ --picker-align-y-default: stretch;
85538
+ --picker-border-width: 0px;
85539
+ --picker-border-color: transparent;
85540
+ --picker-border-color-hover: var(--picker-border-color);
85541
+ --picker-border-color-readonly: var(--picker-border-color);
85542
+ --picker-border-color-disabled: var(--picker-border-color);
85543
+ --picker-background-color: transparent;
85544
+ --picker-background-color-hover: var(--picker-background-color);
85545
+ --picker-background-color-readonly: var(--picker-background-color);
85546
+ --picker-background-color-disabled: var(--picker-background-color);
85547
+ --picker-icon-color: currentColor;
85548
+ font-size: inherit;
85549
+ font-family: inherit;
85550
+ line-height: inherit;
85551
+ }
85552
+
85553
+ .navi_picker[data-variant="button"] {
85554
+ --picker-padding-x-default: var(--navi-button-padding-x-default);
85555
+ --picker-padding-y-default: var(--navi-button-padding-y-default);
85556
+ --picker-align-x-default: center;
85557
+ --picker-background-color: var(--navi-button-background-color);
85558
+ --picker-border-color-readonly: color-mix(in srgb,
85559
+ var(--picker-border-color) 30%,
85560
+ var(--navi-surface-color));
85561
+ --picker-background-color-readonly: var(--picker-background-color);
85562
+ --picker-color-readonly: color-mix(in srgb,
85563
+ var(--picker-color) 30%,
85564
+ transparent);
85565
+ --picker-border-color-disabled: var(--picker-border-color-readonly);
85566
+ --picker-background-color-disabled: var(--picker-background-color-readonly);
85567
+ --picker-color-disabled: var(--picker-color-readonly);
85568
+ --picker-text-align-default: center;
85569
+ }
85570
+
85571
+ .navi_picker[data-variant="text"] {
85572
+ --picker-padding-x-default: 0;
85573
+ --picker-padding-y-default: 0;
85574
+ --picker-border-width: 0px;
85575
+ --picker-border-color: transparent;
85576
+ --picker-border-color-hover: var(--picker-border-color);
85577
+ --picker-border-color-readonly: var(--picker-border-color);
85578
+ --picker-border-color-disabled: var(--picker-border-color);
85579
+ --picker-background-color: transparent;
85580
+ --picker-background-color-hover: var(--picker-background-color);
85581
+ --picker-background-color-readonly: var(--picker-background-color);
85582
+ --picker-background-color-disabled: var(--picker-background-color);
85583
+ font-size: inherit;
85584
+ font-family: inherit;
85585
+ }
85586
+
85587
+ .navi_picker[data-icon] {
85588
+ --picker-padding-x-default: 0;
85589
+ --picker-padding-y-default: 0;
85590
+ --picker-align-x-default: center;
84515
85591
  }
84516
85592
  }
84517
85593
 
@@ -84534,10 +85610,7 @@ const css$r = /* css */`@layer navi {
84534
85610
  box-sizing: border-box;
84535
85611
  max-width: 100%;
84536
85612
  color: var(--x-picker-color);
84537
- font-size: var(--picker-font-size);
84538
- font-family: var(--picker-font-family);
84539
85613
  text-align: inherit;
84540
- line-height: var(--navi-control-line-height);
84541
85614
  border-top-left-radius: var(--x-corner-top-left-radius, var(--picker-border-radius));
84542
85615
  border-top-right-radius: var(--x-corner-top-right-radius, var(--picker-border-radius));
84543
85616
  border-bottom-right-radius: var(--x-corner-bottom-right-radius, var(--picker-border-radius));
@@ -84779,48 +85852,7 @@ const css$r = /* css */`@layer navi {
84779
85852
  --x-picker-border-color: var(--callout-color);
84780
85853
  }
84781
85854
 
84782
- &[data-variant="icon"] {
84783
- --picker-border-width: 0px;
84784
- --picker-border-color: transparent;
84785
- --picker-border-color-hover: var(--picker-border-color);
84786
- --picker-border-color-readonly: var(--picker-border-color);
84787
- --picker-border-color-disabled: var(--picker-border-color);
84788
- --picker-background-color: transparent;
84789
- --picker-background-color-hover: var(--picker-background-color);
84790
- --picker-background-color-readonly: var(--picker-background-color);
84791
- --picker-background-color-disabled: var(--picker-background-color);
84792
- --picker-icon-color: currentColor;
84793
- }
84794
-
84795
- &[data-variant="discrete"] {
84796
- --picker-border-width: 0px;
84797
- --picker-border-color: transparent;
84798
- --picker-border-color-hover: var(--picker-border-color);
84799
- --picker-border-color-readonly: var(--picker-border-color);
84800
- --picker-border-color-disabled: var(--picker-border-color);
84801
- --picker-background-color: transparent;
84802
- --picker-background-color-hover: color-mix(in srgb,
84803
- currentColor 8%,
84804
- var(--picker-background-color));
84805
- --picker-background-color-readonly: var(--picker-background-color);
84806
- --picker-background-color-disabled: var(--picker-background-color);
84807
- }
84808
-
84809
85855
  &[data-variant="headless"] {
84810
- --picker-padding-x-default: 0;
84811
- --picker-padding-y-default: 0;
84812
- --picker-border-width: 0px;
84813
- --picker-border-color: transparent;
84814
- --picker-outline-width: 0px;
84815
- --picker-border-color-hover: var(--picker-border-color);
84816
- --picker-border-color-readonly: var(--picker-border-color);
84817
- --picker-border-color-disabled: var(--picker-border-color);
84818
- --picker-background-color: transparent;
84819
- --picker-background-color-hover: var(--picker-background-color);
84820
- --picker-background-color-readonly: var(--picker-background-color);
84821
- --picker-background-color-disabled: var(--picker-background-color);
84822
- --picker-icon-color: currentColor;
84823
-
84824
85856
  & > .navi_picker_box {
84825
85857
  z-index: -1;
84826
85858
  position: absolute;
@@ -84829,23 +85861,6 @@ const css$r = /* css */`@layer navi {
84829
85861
  }
84830
85862
 
84831
85863
  &[data-variant="bare"] {
84832
- --picker-padding-x-default: 0;
84833
- --picker-padding-y-default: 0;
84834
- --picker-align-y-default: stretch;
84835
- --picker-border-width: 0px;
84836
- --picker-border-color: transparent;
84837
- --picker-border-color-hover: var(--picker-border-color);
84838
- --picker-border-color-readonly: var(--picker-border-color);
84839
- --picker-border-color-disabled: var(--picker-border-color);
84840
- --picker-background-color: transparent;
84841
- --picker-background-color-hover: var(--picker-background-color);
84842
- --picker-background-color-readonly: var(--picker-background-color);
84843
- --picker-background-color-disabled: var(--picker-background-color);
84844
- --picker-icon-color: currentColor;
84845
- font-size: inherit;
84846
- font-family: inherit;
84847
- line-height: inherit;
84848
-
84849
85864
  & > .navi_picker_box {
84850
85865
  min-height: 0;
84851
85866
  }
@@ -84880,39 +85895,8 @@ const css$r = /* css */`@layer navi {
84880
85895
  }
84881
85896
  }
84882
85897
 
84883
- &[data-variant="button"] {
84884
- --picker-padding-x-default: var(--navi-button-padding-x-default);
84885
- --picker-padding-y-default: var(--navi-button-padding-y-default);
84886
- --picker-align-x-default: center;
84887
- --picker-background-color: var(--navi-button-background-color);
84888
- --picker-border-color-readonly: color-mix(in srgb,
84889
- var(--picker-border-color) 30%,
84890
- var(--navi-surface-color));
84891
- --picker-background-color-readonly: var(--picker-background-color);
84892
- --picker-color-readonly: color-mix(in srgb,
84893
- var(--picker-color) 30%,
84894
- transparent);
84895
- --picker-border-color-disabled: var(--picker-border-color-readonly);
84896
- --picker-background-color-disabled: var(--picker-background-color-readonly);
84897
- --picker-color-disabled: var(--picker-color-readonly);
84898
- --picker-text-align-default: center;
84899
- }
84900
-
84901
85898
  &[data-variant="text"] {
84902
- --picker-padding-x-default: 0;
84903
- --picker-padding-y-default: 0;
84904
- --picker-border-width: 0px;
84905
- --picker-border-color: transparent;
84906
- --picker-border-color-hover: var(--picker-border-color);
84907
- --picker-border-color-readonly: var(--picker-border-color);
84908
- --picker-border-color-disabled: var(--picker-border-color);
84909
- --picker-background-color: transparent;
84910
- --picker-background-color-hover: var(--picker-background-color);
84911
- --picker-background-color-readonly: var(--picker-background-color);
84912
- --picker-background-color-disabled: var(--picker-background-color);
84913
- font-size: inherit;
84914
85899
  text-underline-offset: .2em;
84915
- font-family: inherit;
84916
85900
  text-decoration: underline dotted;
84917
85901
 
84918
85902
  &[data-hover] {
@@ -84921,10 +85905,6 @@ const css$r = /* css */`@layer navi {
84921
85905
  }
84922
85906
 
84923
85907
  &[data-icon] {
84924
- --picker-padding-x-default: 0;
84925
- --picker-padding-y-default: 0;
84926
- --picker-align-x-default: center;
84927
-
84928
85908
  & > .navi_picker_box {
84929
85909
  min-height: 0;
84930
85910
  }
@@ -85359,13 +86339,13 @@ const PickerButton = props => {
85359
86339
  * It goes when there is nothing to take out and when nothing can be changed, so
85360
86340
  * a caller places it once and never asks whether to draw it.
85361
86341
  *
85362
- * @type {import("ignore:preact").FunctionComponent<{
86342
+ * @type {import("preact").FunctionComponent<{
85363
86343
  * size?: number | string,
85364
- * children?: import("ignore:preact").ComponentChildren,
86344
+ * children?: import("preact").ComponentChildren,
85365
86345
  * } & Record<string, any>>}
85366
86346
  * @param {number|string} [size="inherit"] How big the cross is drawn, as
85367
86347
  * `<Icon size>` reads it — "inherit" takes the size of the text around it.
85368
- * @param {import("ignore:preact").ComponentChildren} [children] The icon, in place of
86348
+ * @param {import("preact").ComponentChildren} [children] The icon, in place of
85369
86349
  * the cross. Anything else goes to the `<Button>` underneath (`variant`,
85370
86350
  * `paddingX`, `aria-label`, …).
85371
86351
  */
@@ -85696,6 +86676,580 @@ const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
85696
86676
  ColorSvg
85697
86677
  });
85698
86678
 
86679
+ /**
86680
+ * Button-like trigger that opens a picker (native or custom popup) when clicked.
86681
+ *
86682
+ * Without `children`, opens the browser-native picker for the given `type`.
86683
+ * With `children`, opens a popover (desktop) or dialog (mobile) containing the children.
86684
+ * Pass `mode="popover"` or `mode="dialog"` to override the automatic choice.
86685
+ * Either way the popup is a `dialog`, named by a `<Popup.Title>` in the
86686
+ * children (see `popupAriaLabelledBy`), and the trigger answers to `combobox` —
86687
+ * `button` under `picksNothing` — with `aria-haspopup="dialog"` and
86688
+ * `aria-expanded` (see docs/testid.md).
86689
+ *
86690
+ * `type="confirm"` is the picker that asks "are you sure?" before doing what it
86691
+ * stands for: its popup is the question, `ui` its label, and yes runs its
86692
+ * `action` and triggers its `command` once the popup has closed (the command
86693
+ * follows the action, and only if it succeeded) — see
86694
+ * picker_confirm.jsx, and `message`, `confirmLabel`, `cancelLabel`,
86695
+ * `focusOnOpen` below.
86696
+ *
86697
+ * @type {import("preact").FunctionComponent<{
86698
+ * type?: "date" | "month" | "week" | "time" | "datetime" | "duration" | "color" | "file" | "text" | "object" | "array" | "confirm" | "navi_time" | "navi_minute" | "navi_number" | "navi_percentage",
86699
+ * value?: any,
86700
+ * defaultValue?: any,
86701
+ * name?: string,
86702
+ * placeholder?: import("preact").ComponentChildren,
86703
+ * ui?: import("preact").ComponentChildren | "default" | import("preact").ComponentType<{ value: any, loading: boolean, interactive: boolean }>,
86704
+ * required?: boolean,
86705
+ * min?: Date | string | number,
86706
+ * max?: Date | string | number,
86707
+ * step?: string | number,
86708
+ * disabled?: boolean,
86709
+ * readOnly?: boolean,
86710
+ * openWhileReadOnly?: boolean,
86711
+ * error?: boolean | string,
86712
+ * uiAction?: (value: any, event: Event) => void,
86713
+ * action?: (value: any, { event, signal }: { event: Event, signal: AbortSignal }) => void,
86714
+ * children?: import("preact").ComponentChildren,
86715
+ * mode?: "popover" | "dialog" | "callout",
86716
+ * openOn?: "press" | "longpress" | "contextmenu" | string | string[],
86717
+ * animation?: boolean | "auto" | "fading" | "scaling" | "sliding" | "lifting" | `slide-from-${string}` | { open: boolean | "auto" | "fading" | "scaling" | "sliding" | `slide-from-${string}`, close: "lifting" },
86718
+ * lift?: "box" | "scene",
86719
+ * liftAnchor?: Element | { current: Element } | string,
86720
+ * animationDuration?: string,
86721
+ * calloutStatus?: "info" | "warning" | "error" | "success" | "none",
86722
+ * calloutIcon?: boolean,
86723
+ * calloutCloseButton?: boolean,
86724
+ * calloutCloseByScroll?: boolean | number,
86725
+ * popoverMode?: "nearby" | "overlay",
86726
+ * positionArea?: string,
86727
+ * popupWidthFitContent?: boolean,
86728
+ * loadingOutline?: "custom",
86729
+ * loadingOutlineInset?: number,
86730
+ * variant?: "icon" | "circle" | "headless" | "discrete" | "button" | "text" | "bare" | "picker",
86731
+ * icon?: boolean,
86732
+ * alignX?: "start" | "center" | "end",
86733
+ * alignY?: "start" | "center" | "end" | "stretch",
86734
+ * rightSlotIcon?: import("preact").ComponentChildren,
86735
+ * rightSlotIconSize?: number | string,
86736
+ * rightSlot?: import("preact").ComponentChildren,
86737
+ * chevron?: "end" | "start" | "hang-end",
86738
+ * clearConfirm?: string | import("preact").ComponentChildren,
86739
+ * message?: import("preact").ComponentChildren,
86740
+ * confirmLabel?: import("preact").ComponentChildren,
86741
+ * cancelLabel?: import("preact").ComponentChildren,
86742
+ * focusOnOpen?: "confirm" | "cancel" | "none",
86743
+ * onConfirm?: (confirmEvent: CustomEvent) => void,
86744
+ * onOpen?: (openEvent: CustomEvent) => void,
86745
+ * onClose?: (closeEvent: CustomEvent) => void,
86746
+ * maxLines?: number,
86747
+ * slotSpacing?: number | string,
86748
+ * popoverMaxHeight?: number | string,
86749
+ * dialogMinWidth?: number | string,
86750
+ * dialogMinHeight?: number | string,
86751
+ * dialogMaxWidth?: number | string,
86752
+ * dialogMaxHeight?: number | string,
86753
+ * popupBackgroundColor?: string,
86754
+ * popupBorderRadius?: number | string,
86755
+ * popupBoxShadow?: string,
86756
+ * dialogBorderWidth?: number | string,
86757
+ * clearable?: boolean,
86758
+ * picksNothing?: boolean,
86759
+ * standalone?: boolean,
86760
+ * popupLayer?: "top" | "local",
86761
+ * popupTestId?: string,
86762
+ * popupAriaLabel?: string,
86763
+ * popupAriaLabelledBy?: string,
86764
+ * popupAriaDescribedBy?: string,
86765
+ * confirmTestId?: string,
86766
+ * cancelTestId?: string,
86767
+ * mount?: "always" | "idle" | "from-first-open" | "while-opened",
86768
+ * dialogExpand?: boolean,
86769
+ * dialogExpandX?: boolean,
86770
+ * dialogExpandY?: boolean,
86771
+ * dialogSizeFromAnchor?: boolean,
86772
+ * dockedOnSmallTouchScreen?: boolean | "top" | "bottom",
86773
+ * marginWithContainer?: number | string,
86774
+ * anchor?: import("preact").RefObject<HTMLElement> | HTMLElement,
86775
+ * escapeEffect?: "cancel" | "close",
86776
+ * pressOutside?: "close" | "cancel" | "capture",
86777
+ * backdrop?: boolean,
86778
+ * backdropVariant?: "auto" | "lift" | "discrete" | "invisible",
86779
+ * backdropColor?: string,
86780
+ * backdropFilter?: string,
86781
+ * ref?: import("preact").RefObject<HTMLElement>,
86782
+ * [key: string]: any,
86783
+ * }>}
86784
+ * @param {string} [popupBackgroundColor] The popup's surface. Left out it is
86785
+ * the one every navi popup uses (`--navi-popup-background-color`, which
86786
+ * follows the theme) — the trigger's own `backgroundColor` is not it, so a
86787
+ * variant that paints the trigger transparent leaves the popup a real sheet.
86788
+ * @param {number|string} [popupBorderRadius] The popup's corners. Left out they
86789
+ * echo the trigger's `borderRadius`.
86790
+ * @param {string} [popupBoxShadow] The popup's shadow, popover or dialog. Left
86791
+ * out it is the one every navi popup casts. `"none"` for a popup that is
86792
+ * not a sheet — a card lifted out of a page draws its own edges, and the
86793
+ * popup only carries it (see `dialogSizeFromAnchor`).
86794
+ * @param {"press"|"longpress"|"contextmenu"|string|string[]} [openOn="press"]
86795
+ * What opens the popup. The press, by default — the way a native select
86796
+ * opens. Any other name is an interaction navi detects (see
86797
+ * docs/interactions.md: `"longpress"`, `"contextmenu"`, `"double_click"`…), or
86798
+ * a list of them — `["longpress", "contextmenu"]` is the hold on a phone and
86799
+ * the right click at a desk. The picker declares it on itself, so it is
86800
+ * arbitrated like any other gesture: a swipe on the same card takes the
86801
+ * press a hold would have taken. A tap then opens nothing — the press stays
86802
+ * free for what the `ui` holds: a link in the card navigates, a button in it
86803
+ * presses, a picker in it opens on its own click, with nothing to declare
86804
+ * (`selfInteractions` is for a drawing that DOES open on the press). The
86805
+ * trigger says so under the mouse too: it drops the hand cursor, which is
86806
+ * the press's promise, and every pixel of the drawing keeps the cursor it
86807
+ * would have on its own. That is what lets a whole card be a picker without
86808
+ * every touch on it opening the sheet — and the keyboard keeps its ways in
86809
+ * (Enter, Space, the arrows). A hold declared INSIDE the card (`interactions={{ longpress }}`
86810
+ * on something it holds) answers before this one: the nearer hold takes the
86811
+ * press, the way a click is the innermost target's. Being a gesture, the open goes through
86812
+ * the interaction gate as one: a `readOnly` picker refuses it and says so
86813
+ * where the finger is, whatever `openWhileReadOnly` says.
86814
+ * @param {number} [maxLines=1] How many lines the value may take before it is
86815
+ * cut with an ellipsis — `Text`'s `maxLines`, put on the value box, and
86816
+ * granted to a `BadgeList` in the `ui`, which caps its rows to it (see
86817
+ * max_lines_context.js). `variant="text"` and `variant="bare"` are not
86818
+ * clamped: their drawing is the caller's own, a `Text` in the `ui` cuts
86819
+ * itself with its own `maxLines`. See docs/typography.md.
86820
+ * @param {import("preact").ComponentChildren | "default" | import("preact").ComponentType} [ui] What the
86821
+ * trigger draws in place of the value's default rendering (a date, a list
86822
+ * joined by commas…) — and then all it draws: `placeholder` is not shown
86823
+ * next to it, and an empty value is not greyed as a placeholder either: what
86824
+ * this draws when the picker holds nothing may well be an answer ("no
86825
+ * filter", "anywhere"), so its empty look is the caller's. A `ui` must
86826
+ * therefore say on its own that the picker is empty, and take the same room
86827
+ * empty as it does filled. A <BadgeList> gets both from its `fallback` — the
86828
+ * placeholder text, as plain text, see docs/typography.md#badgelist.
86829
+ * `"default"` draws nothing in the slot at all.
86830
+ *
86831
+ * Under `variant="icon"` there is no value to draw, so this IS the trigger:
86832
+ * left out it is the icon the right slot would have shown (`rightSlotIcon`,
86833
+ * or the chevron), and given it is drawn instead — one icon of the caller's
86834
+ * own, and nothing else.
86835
+ *
86836
+ * Under `variant="bare"` it is the trigger as well, and the picker adds
86837
+ * nothing around it: whatever this draws is what the picker measures. Its
86838
+ * own padding, its own lines, its own corners — a `<Box>` here gives a
86839
+ * picker the exact box that `<Box>` has on its own. Anything interactive
86840
+ * inside it takes its clicks back with `data-self-interactions`, the press
86841
+ * everywhere else opening the popup. There is no right slot beside it either,
86842
+ * so what a field would have got from navi this drawing places itself: a
86843
+ * `<Picker.Clear />` where the cross belongs, laid out like anything else it
86844
+ * holds. What the picker catches the press over is that box and no more, so
86845
+ * a drawing that paints outside it on purpose — an affordance in the margin
86846
+ * beside it, out of the flow so the rest keeps its width — is drawn and
86847
+ * opens nothing; `pressPadding` is how far the press area follows it out
86848
+ * there. The chevron itself is the `chevron` prop's to draw and place.
86849
+ *
86850
+ * A drawing that follows what the picker HOLDS — the value the popup just
86851
+ * closed on, before the server has answered, the same state navi's own
86852
+ * default `ui` reads, rolled back if the action fails (`resetOnError`) —
86853
+ * reads it one of two ways. As a component, `ui={MyCard}`: the picker
86854
+ * renders it and hands it `value`, `loading` and `interactive` as props.
86855
+ * As an element, `ui={<MyCard game={game} />}`, with `usePickerState()`
86856
+ * inside it returning the same three — for a drawing that needs props of
86857
+ * its own as well. Either way a card shows the score the moment the sheet
86858
+ * leaves and takes it back on a refusal, with nothing held by the caller.
86859
+ * `loading` is the wait it wears meanwhile: navi draws its loading outline
86860
+ * on every trigger, two pixels out of the box. On a card-sized
86861
+ * `variant="bare"` that run sits on the card's own border and reads as a
86862
+ * border blinking; `loadingOutlineInset` moves it clear of it. And when the
86863
+ * picker's box is not the shape of what waits, `loadingOutline="custom"`
86864
+ * lets the drawing draw the wait itself, from that flag or from
86865
+ * `[data-loading]` on the picker root in css.
86866
+ * @param {boolean} [readOnly] Nothing in this picker can be changed — and it
86867
+ * still opens, so what is in the popup can be read: everything in there is
86868
+ * held read-only in turn, each control greying out and saying why on its own.
86869
+ * Which is the point of opening it at all: a picker's answer often exists
86870
+ * only in the shape the popup draws (a plan with one tile ringed, a wheel
86871
+ * stopped on a time), and the trigger's one line is a summary of it, not the
86872
+ * whole of it. The clear cross goes, closing commits nothing, and a `value`
86873
+ * the popup would otherwise have confirmed stays a suggestion.
86874
+ * A picker with no `children` opens the browser's own picker instead, and
86875
+ * that one cannot be held read-only — so it goes on refusing (see
86876
+ * PickerNative).
86877
+ * @param {boolean} [openWhileReadOnly=true] Pass false to make `readOnly`
86878
+ * refuse the open as well, for a popup with nothing to read: a form of
86879
+ * controls to fill in, a menu of gestures. Opening then only shows what is
86880
+ * refused, so the picker says why on the trigger instead — where the
86881
+ * interaction happened. A busy picker follows the same rule: it still
86882
+ * opens to be read while its action runs, and with `false` the press is
86883
+ * refused with the wait as its reason — what a card that is syncing wants
86884
+ * for the sheet that would write it again.
86885
+ * @param {boolean|string} [error] Something went wrong around this picker (its
86886
+ * content failed to load, its value could not be resolved…). Shown as a
86887
+ * callout on the trigger, open or closed — the caller has nothing to place.
86888
+ * Dismissing it discards that error; a new `error` value raises another one.
86889
+ * @param {string} [id] What the popup's open state is kept under in the
86890
+ * history entry: a screen left and come back to finds the picker open, and
86891
+ * in dialog mode the opening is an entry of its own, closed by the back
86892
+ * button before the screen is left. Left out, the key is a generated id,
86893
+ * which names one mount: the state survives neither leaving the screen nor a
86894
+ * reload. A picker whose popup leads somewhere (a link inside it) has one.
86895
+ * @param {"popover"|"dialog"|"callout"} [mode] Which popup the children open
86896
+ * in. Left out, a popover on a large screen and a dialog on a narrow one.
86897
+ * `"callout"` shows them in the picker's own callout — the speech bubble its
86898
+ * constraints speak in, with its status icon, its cross and its arrow on
86899
+ * the trigger — for a tooltip that opens on a press. It closes the way a
86900
+ * callout does: its cross, Escape, a click outside.
86901
+ * @param {"info"|"warning"|"error"|"success"|"none"} [calloutStatus]
86902
+ * `mode="callout"`: what the callout says about what it holds — its border,
86903
+ * its icon, and whether a click outside closes it (an "error" stays).
86904
+ * `"none"` is a plain tooltip: no icon, a neutral frame. "info" by default,
86905
+ * "none" under `variant="text"`. The trigger's default icon is the
86906
+ * callout's own status icon in that status (see `CalloutStatusIcon`) — left
86907
+ * out, the trigger is that icon; `variant="circle"` draws it round. The callout's arrow points at
86908
+ * the middle of the trigger (`data-callout-arrow-x="center"` unless the
86909
+ * picker says otherwise).
86910
+ * @param {boolean} [calloutIcon] `mode="callout"`: whether the callout shows
86911
+ * its status icon beside what it holds. On by default, off under
86912
+ * `variant="text"` — a word asks for a plain bubble — and never shown
86913
+ * without a status, whatever this says.
86914
+ * @param {boolean} [calloutCloseButton=true] `mode="callout"`: pass false to
86915
+ * take the cross off the callout. It still closes on Escape, a click
86916
+ * outside, and a `--navi-close` from what it holds — for a tooltip that is
86917
+ * read rather than dismissed. Off by nobody's default: a text trigger
86918
+ * keeps the cross unless told not to.
86919
+ * @param {boolean|number} [calloutCloseByScroll=false] `mode="callout"`: close
86920
+ * the callout once the page has scrolled under it — `true` for a short
86921
+ * distance, a number for that many pixels. For a tooltip read in place,
86922
+ * which scrolling away is done with.
86923
+ * @param {"nearby"|"overlay"} [popoverMode="nearby"] "overlay" lays the popover
86924
+ * over the trigger, "nearby" leaves a small gap below it.
86925
+ * @param {string} [positionArea] Where the popup goes — relative to the trigger
86926
+ * in popover mode, relative to the viewport in dialog mode. Same grammar as
86927
+ * Popover/Dialog's own `positionArea` ("top", "right-end", "inset(top-left)",
86928
+ * …). Defaults to "bottom-start" in popover mode ("inset(top-left)" when
86929
+ * popoverMode is "overlay"), and to Dialog's own "center" in dialog mode. A
86930
+ * popover still flips to the opposite side on its own when there isn't
86931
+ * enough room.
86932
+ * @param {boolean} [popupWidthFitContent] By default the popover is at least as
86933
+ * wide as the trigger. Set this to let the content size it instead, so a
86934
+ * popover narrower than the trigger stays narrow. Popover mode only — a
86935
+ * dialog never takes the trigger's width to begin with (see
86936
+ * `dialogMinWidth`).
86937
+ * @param {import("preact").ComponentChildren} [rightSlotIcon] What the right
86938
+ * slot draws in place of the chevron. It is the whole slot, not an addition
86939
+ * to it: the picker then no longer says on its own that it opens, so pass
86940
+ * something that does. Decoration only — it is wrapped in an `<Icon>`, which
86941
+ * is `aria-hidden`, so anything focusable in there would be reachable by tab
86942
+ * while invisible to assistive tech. Use `rightSlot` for that.
86943
+ *
86944
+ * The slot only exists beside a value. `variant="icon"` has none, so there is
86945
+ * no slot either: the icon becomes the trigger itself and is drawn as the
86946
+ * `ui` — which is also what a caller overrides to draw their own. Nothing
86947
+ * else is drawn there, the clear cross included: one icon has room for one
86948
+ * thing.
86949
+
86950
+ * `variant="bare"` has no slot either, for the same reason turned around: the
86951
+ * picker is the drawing's box to the pixel, so nothing may be added beside it
86952
+ * — the drawing holds what it needs (`<Picker.Clear />`) and places it. Its
86953
+ * `chevron` prop is the exception: the slot, put where the drawing is not.
86954
+ * @param {"end"|"start"|"hang-end"} [chevron] `variant="bare"` only: the
86955
+ * picker draws no chevron beside the drawing, so nothing says it opens; this
86956
+ * draws one — the icon the right slot would hold, `rightSlotIcon` or the
86957
+ * chevron, at `rightSlotIconSize`, `slotSpacing` away from the drawing (xxs
86958
+ * by default), taking the clear cross's place under `clearable` like any
86959
+ * slot — and says where, so the drawing keeps the box it has when nothing
86960
+ * opens. Which side depends on where the drawing is anchored in its line.
86961
+ * `"end"`: after it, in the flow — a label with room after it. `"start"`:
86962
+ * before it — a label pushed against the right edge, which the chevron
86963
+ * would otherwise push left by its width. `"hang-end"`: after it and OUT of
86964
+ * the flow, in the margin beside the box — a drawing filling its whole
86965
+ * line; the press follows it out there by its size plus its gap, on top of
86966
+ * any `pressPaddingRight`. The chevron is drawn in `--picker-icon-color`
86967
+ * (`currentColor` here) and `--picker-icon-color-hover`; a dimmed one is
86968
+ * `--picker-icon-color: color-mix(in srgb, currentColor 55%, transparent)`.
86969
+ * @param {import("preact").ComponentChildren} [rightSlot] Same place, rendered
86970
+ * as-is: no `<Icon>` around it, nothing `aria-hidden`. This is where an
86971
+ * interactive right slot goes. `null` takes the slot away entirely — the
86972
+ * gutter it carries with it, so the trigger measures its drawing and centers
86973
+ * on it.
86974
+ * @param {boolean} [picksNothing] The popup asks something rather than holding
86975
+ * an answer, so the trigger draws a label and never a value: no chevron, no
86976
+ * clear cross, in any variant — and the controls in the popup are their own
86977
+ * (a checkbox gating the yes), never the picker's value, so a bound `action`
86978
+ * keeps the params it was bound with. `type="confirm"` says it for itself; say it
86979
+ * for a popup of your own that is a menu of actions rather than a choice —
86980
+ * or a DOOR: a drawing that grows to be looked at (a weather scene, a plan),
86981
+ * with no `action` at all. The picker is then a trigger and a popup, and
86982
+ * nothing more: its close does nothing, and what it keeps from being a
86983
+ * control is what a door still needs — the role of a button, the keyboard
86984
+ * opening it, the anchor being itself, and one popup per instance without
86985
+ * an id to wire. See "The trigger wears the wait" in docs/popup_open.md for
86986
+ * when that beats a `Button` opening a shared `Dialog`.
86987
+ * @param {string|import("preact").ComponentChildren} [clearConfirm] The
86988
+ * question asked before the clear cross clears anything — the `message` of
86989
+ * the `<Picker type="confirm">` the cross then is, plain text or JSX. Asked
86990
+ * BEFORE the ui state is emptied, so answering no leaves the field exactly
86991
+ * as it was; answering yes clears and sends, and the picker's own action
86992
+ * receives the cleared value like any other choice. A cut (Ctrl+X) on such a
86993
+ * picker copies the value and leaves it in place: no clear without the
86994
+ * question.
86995
+ * @param {"icon"|"headless"|"discrete"|"button"|"text"|"bare"|"picker"} [variant]
86996
+ * How the trigger is drawn. `"button"` is a Button's drawing — surface,
86997
+ * padding, a centered label, no chevron — and what a `type="confirm"` picker
86998
+ * draws unless told otherwise: nothing is picked, the popup is a question.
86999
+ * `"text"` is a word in a sentence, underlined with dots, no box and no
87000
+ * slot: what `mode="callout"` uses for a callout on a term rather than on an
87001
+ * icon (`"icon"` being its default, with an info icon in place of the
87002
+ * chevron). `"bare"` draws no trigger of its own at all: the `ui` is it, and
87003
+ * the picker is exactly that drawing's box — no padding, no frame, no
87004
+ * control line, no clamp — for a picker that is a whole piece of a layout
87005
+ * rather than a field, and which must measure the same as the same drawing
87006
+ * placed anywhere else. No right slot comes with it: a cross belongs inside
87007
+ * that drawing, at the place and the size the drawing decides, so it is
87008
+ * `<Picker.Clear />` the `ui` holds rather than `clearable` the picker
87009
+ * takes. What navi still draws on that box — the focus ring, the loading
87010
+ * outline — follows the picker's `borderRadius`, which stays the control's
87011
+ * unless told: a drawing with corners of its own states them on the picker
87012
+ * as well. `"headless"` draws nothing either, and is not the `ui`'s box but
87013
+ * its parent's: it stretches to whatever positioned element holds it, for a
87014
+ * picker put inside the thing that opens it (a button, a row) so that the
87015
+ * popup hangs off that whole element. A picker opened from something it does
87016
+ * not sit in — an object on a map saying
87017
+ * `triggerNaviCommand(pickerEl, "--navi-open", event, { anchor, value })` —
87018
+ * therefore wants an element of its own to stretch into rather than the
87019
+ * nearest positioned ancestor it happens to find. `"picker"` (or an explicit
87020
+ * `variant={undefined}`) asks a confirm or callout picker for the field-like
87021
+ * drawing every other picker has.
87022
+ * @param {"custom"} [loadingOutline] Who draws the loading outline while the
87023
+ * action runs. Left unsaid, the picker does, around its own box. `"custom"`
87024
+ * says someone else does — what it means on a `Button` too — for a picker
87025
+ * that carries a wait drawn elsewhere: two nested pickers sharing one run
87026
+ * (a score picker drawn inside the card an edit picker wraps, see
87027
+ * `12_picker_card_demo.html`) are one wait, and one outline; and a
87028
+ * `variant="bare"` whose box is not
87029
+ * the shape of what waits (a card with a note pinned under it) leaves the
87030
+ * outline to the card, which knows its own box. The wait itself is still
87031
+ * worn: `aria-busy`, the busy refusal, the error callout, `[data-loading]`.
87032
+ * @param {number} [loadingOutlineInset=-2] Where the loading outline runs, in
87033
+ * px from the picker's box — negative is outside. A field wants it right on
87034
+ * its border; a card-sized trigger wants it a few pixels out, clear of the
87035
+ * card's own frame, so it reads as a run around the card rather than as its
87036
+ * border blinking.
87037
+ * @param {boolean} [icon] The trigger is one icon — the `ui`, or the chevron
87038
+ * when there is none. Its box is that icon's box plus the padding asked for
87039
+ * and nothing else: no right slot, and no control line to be as tall as, so
87040
+ * it measures the same as `<Button icon>` and stands in a row of icon
87041
+ * buttons. A shape, not a paint: the variant still decides what is drawn
87042
+ * around it, so `variant="discrete" icon` is the discrete hover wash on an
87043
+ * icon-sized square. `variant="icon"` is this shape with no surface at all.
87044
+ * @param {import("preact").ComponentChildren} [message] `type="confirm"`: the
87045
+ * question, in the popup's default body — text, or JSX when it needs an
87046
+ * emphasis, a link. Left out, a generic "are you sure?" in the current
87047
+ * language. To draw the whole popup instead, pass `children`: yes is then
87048
+ * whatever carries `--navi-confirm`, no whatever closes (`--navi-cancel`).
87049
+ * @param {import("preact").ComponentChildren} [confirmLabel] `type="confirm"`,
87050
+ * default body: what the yes button reads. Defaults to the translated
87051
+ * "Confirm".
87052
+ * @param {import("preact").ComponentChildren} [cancelLabel] Same, for the no
87053
+ * button. Defaults to the translated "Cancel".
87054
+ * @param {"confirm"|"cancel"|"none"} [focusOnOpen="confirm"] `type="confirm"`,
87055
+ * default body: which button the keyboard lands on when the popup opens.
87056
+ * `"cancel"` is the careful choice before something that cannot be undone —
87057
+ * a stray Enter then answers no.
87058
+ * @param {(confirmEvent: CustomEvent) => void} [onConfirm] Called once the
87059
+ * popup has closed on a `--navi-confirm` said inside it. What a confirm
87060
+ * picker uses to run its press; a picker of any type may listen too.
87061
+ * @param {boolean} [canSendWhileUnchanged] Run `action` when a choice pressed
87062
+ * inside the popup — a row's `--navi-send`, a `--navi-confirm` — is the
87063
+ * value the picker already held. By default that close runs nothing: the
87064
+ * answer has not moved, so there is nothing new to say, same as `Form`.
87065
+ * Set this for a picker whose value is a setting that can be valid while
87066
+ * what it points to is not — a channel whose account came unlinked — so
87067
+ * picking it again is how the person asks for it to be put right, and the
87068
+ * action sees the same value and re-asks what there is to re-ask. A press
87069
+ * outside, Escape and the cross still send nothing: nothing was pressed.
87070
+ * @param {(openEvent: CustomEvent) => void} [onOpen] Called when the popup
87071
+ * opens, whatever asked for it (a press, a command, history bringing it
87072
+ * back). You most likely do NOT need this pair: "the user validated" is
87073
+ * already `action` — it fires on every close that keeps (click outside, the
87074
+ * cross, Enter, a `--navi-send` inside the popup) and never on a cancel.
87075
+ * Reach for `onOpen`/`onClose` only for the one thing `action` cannot say:
87076
+ * the popup being up at all, independent of the value — e.g. a derived
87077
+ * computation held while the sheet is open, released on close.
87078
+ * @param {(closeEvent: CustomEvent) => void} [onClose] Called when the popup
87079
+ * actually closes — not preventable, and (see `onOpen`) not the way to know
87080
+ * the user validated: that is `action`. `closeEvent.detail.isCancel` tells a
87081
+ * cancel (Escape, the back gesture) from a close that keeps. Fires after the
87082
+ * picker's value has settled — committed, or restored on a cancel — so the
87083
+ * value read in here is the one the picker ends up holding, and before the
87084
+ * popup leaves the history entry it pushed, so a route param cleared in here
87085
+ * is part of the url a close that keeps carries back.
87086
+ * @param {number|string} [rightSlotIconSize="inherit"] How big what sits in the
87087
+ * right slot is drawn — the chevron, a `rightSlotIcon`, or the clear button's
87088
+ * cross. "inherit" takes the picker's own font size.
87089
+ * @param {number|string} [slotSpacing] Gap kept between what sits in the right
87090
+ * slot (the chevron, or the clear button) and the picker's own edge — same
87091
+ * prop name Input uses for its own slots. Accepts a spacing token ("s",
87092
+ * "m"…) like any other spacing prop, or a length. Defaults to half the
87093
+ * horizontal padding.
87094
+ * @param {number|string} [pressPadding] How far a press counts as the trigger
87095
+ * past the picker's own box — a spacing token ("s", "m"…) or a length, zero
87096
+ * by default. What the picker draws is transparent to the pointer: the press
87097
+ * lands on an invisible input covering the box, so the box is the whole
87098
+ * answer to "does this open". That is right while what is drawn stays inside
87099
+ * it, which is what `variant="bare"` measures. A `ui` that paints OUTSIDE
87100
+ * its box on purpose — a chevron pushed into the margin beside it, so the
87101
+ * label under it keeps the width it has when nothing opens — draws something
87102
+ * nobody can press, and this is how far the press area goes to reach it. The
87103
+ * press area only; nothing moves and nothing is drawn. Font-relative lengths
87104
+ * are read against the picker's own text, so "0.75em" is the drawing's em.
87105
+ * Only the sides that overhang: the area grows over whatever is next to the
87106
+ * picker, and takes the presses aimed at it.
87107
+ * @param {number|string} [pressPaddingX] Same, left and right only;
87108
+ * `pressPaddingY` top and bottom, `pressPaddingTop`/`Right`/`Bottom`/`Left`
87109
+ * one side each.
87110
+ * @param {number|string} [popoverMaxHeight] Soft cap on the popover's height
87111
+ * (default 300px). The popover shrinks below it when space is tight.
87112
+ * @param {number|string} [dialogMinWidth] Floor on the dialog's width. A dialog
87113
+ * picker is not attached to its trigger and does not take its width (unlike
87114
+ * the popover, which uses it as a floor) — this is how a dialog too narrow
87115
+ * for what it holds is widened. Clamped by `dialogMaxWidth`/the container.
87116
+ * @param {number|string} [dialogMinHeight] Same, on the height.
87117
+ * @param {number|string} [dialogMaxWidth] Ceiling on the dialog's width, the
87118
+ * one `dialogExpand`/`dialogExpandX` grows up to — what makes an expanded
87119
+ * dialog a large sheet rather than a full screen. Capped in turn by the
87120
+ * container minus `marginWithContainer`. Describes the centered shape only:
87121
+ * a dialog docked by `dockedOnSmallTouchScreen` withdraws it and stays
87122
+ * container-wide, so both can be stated at once.
87123
+ * @param {number|string} [dialogMaxHeight] Same, on the height.
87124
+ * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|"lifting"|`slide-from-${string}`} [animation]
87125
+ * The popup's own (see Popover/Dialog). `"lifting"` is the one a picker
87126
+ * whose `ui` is a whole piece of the page wants: the popup is the browser
87127
+ * morphing the trigger's box into what carries `data-lift` inside the popup
87128
+ * (the popup itself when it carries it), and back on close. The trigger IS
87129
+ * the anchor, so nothing has to be named for it — `dialogSizeFromAnchor`
87130
+ * for a card that must keep its width on the way, left out for a drawing
87131
+ * that opens precisely to get bigger. It brings an opaque, blurred backdrop
87132
+ * with it (`--navi-backdrop-lift-*`); `backdropVariant="discrete"` asks for
87133
+ * the light wash back.
87134
+ * @param {Element|{current: Element}|string} [liftAnchor] Dialog mode,
87135
+ * Dialog's own: where a lifting close lands when that is not the trigger
87136
+ * (`animation="lifting"`, or `animation={{ open, close: "lifting" }}` for a
87137
+ * popup that opens the plain way and only lands on close),
87138
+ * read once the close is made (after `onClose`).
87139
+ * @param {"box"|"scene"} [lift="box"] Dialog's own, under `animation="lifting"`:
87140
+ * `"box"` for a card that extends (its top stays, the box uncovers the rest),
87141
+ * `"scene"` for a thumbnail that is a band cut from the middle of the bigger
87142
+ * view, as wide as it (each picture at the box's width, centred in it).
87143
+ * @param {string} [animationDuration] The popup's own (`--popup-animation-duration`).
87144
+ * @param {boolean} [dialogSizeFromAnchor] Dialog mode: the dialog is as wide
87145
+ * as the trigger — its box as a floor (Dialog's own `sizeFromAnchor`) and
87146
+ * as a ceiling, unless a `dialogMaxWidth` says otherwise. What keeps a card
87147
+ * its own width once lifted out of the page (`animation="lifting"`, with
87148
+ * `data-lift` on the card inside the popup): it moves, and nothing else
87149
+ * about it changes. `dialogMaxWidth="var(--anchor-width)"` says the same
87150
+ * ceiling by hand: the popup props are written on the dialog element, where
87151
+ * that variable lives.
87152
+ * @param {boolean|"top"|"bottom"} [dockedOnSmallTouchScreen] Dialog mode,
87153
+ * Dialog's own: on a small touch screen the dialog becomes a sheet docked
87154
+ * full width to one edge — `true` or `"top"` the top edge, out of the
87155
+ * virtual keyboard's way; `"bottom"` the bottom one, for a sheet that is
87156
+ * read and tapped. Anywhere else it stays the centered box. Ignored in
87157
+ * popover mode.
87158
+ * @param {"cancel"|"close"} [escapeEffect="cancel"] What Escape does to an open
87159
+ * picker. "cancel" puts back the value the picker had at open, and a dialog
87160
+ * picker with an explicit `id` (its opening being an entry of its own)
87161
+ * also goes back in history — so anything written to the url while it was
87162
+ * open (a route `stateSignal`, a search param) goes back with it. "close"
87163
+ * makes Escape say what clicking outside says: keep what was chosen,
87164
+ * close — a last resort, see
87165
+ * docs/popup_open.md#escape-cancels-the-other-gestures-keep for why Escape
87166
+ * should go on meaning cancel, and
87167
+ * docs/popup_open.md#what-cancel-actually-undoes for what a cancel puts
87168
+ * back.
87169
+ * @param {import("preact").RefObject<HTMLElement>|HTMLElement} [anchor] What
87170
+ * the popup hangs off, when that is not the picker itself: a picker whose
87171
+ * trigger is one piece of a bigger control (the chevron half of a split
87172
+ * button) points at the whole control, so the popup lines up with it and is
87173
+ * at least as wide as it.
87174
+ * @param {boolean} [standalone] - This picker answers for itself: it does not
87175
+ * register with the control group around it, so what it holds never becomes
87176
+ * part of that group's value and nothing that group does — distributing,
87177
+ * resetting, validating — reaches it. For a door that only opens something,
87178
+ * and for a picker that commits on its own inside a form.
87179
+ * @param {"close"|"cancel"|"capture"} [pressOutside="close"]
87180
+ * What a press outside the popup does: close and keep ("close"), close and
87181
+ * put back the value at open ("cancel"), or nothing at all ("capture"). The
87182
+ * default is what gives a popup with no confirm button its way out that
87183
+ * keeps — see the same section.
87184
+ * @param {boolean} [backdrop=true] Whether anything is laid between the popup
87185
+ * and the page at all. `false` lets a press outside both close the popup and
87186
+ * reach whatever it landed on, in one gesture — for a picker opened over a
87187
+ * page that stays as pressable as it looks (a plan, a map, a canvas), where a
87188
+ * wall would spend the first press on dismissing. Honoured in every mode: a
87189
+ * dialog with no wall is shown through the Popover API rather than
87190
+ * `showModal()`, so a picker docked as a bottom sheet on a phone still
87191
+ * leaves the plan behind it live. `false` by default under `mode="callout"`,
87192
+ * a bubble that dims nothing; `true` there spends the press outside on
87193
+ * closing it — the link under it not followed, the card under it not held —
87194
+ * without a wall: the scroll a finger starts beside the bubble still
87195
+ * scrolls. Not read inside a `<PopupGroup>`, whose wall it is.
87196
+ * @param {"auto"|"lift"|"discrete"|"invisible"} [backdropVariant="auto"] How
87197
+ * visible the popup's backdrop is, independently of what a click outside
87198
+ * does: `"auto"` is the paint `pressOutside` implies,
87199
+ * `"lift"` the opaque blurred wall `animation="lifting"` brings, asked for
87200
+ * without the movement, `"discrete"` a barely-there dim, `"invisible"` fully
87201
+ * transparent — a wall that is not seen is still a wall, which is what
87202
+ * `backdrop` above answers. For a picker that closes on an outside click
87203
+ * without wanting to dim the page for it.
87204
+ * @param {string} [backdropColor] The wash the popup paints over what is
87205
+ * behind, for this picker alone. See Dialog's own doc.
87206
+ * @param {string} [backdropFilter] What that wash does to the picture
87207
+ * underneath (`"blur(4px)"`), independently of what a click outside does.
87208
+ * See Dialog's own doc.
87209
+ * @param {number|string} [marginWithContainer] Minimum gap kept between the
87210
+ * popup and the edges of what contains it (the viewport, or the picker's own
87211
+ * positioned ancestor for `popupLayer="local"`). Caps the popup's size as
87212
+ * well as its placement, so what an expanded dialog leaves visible around
87213
+ * itself is set here. Defaults to `popoverSpacing` in popover mode, and to
87214
+ * Dialog's own in dialog mode: `3appw` in the top layer, `3cqw` under
87215
+ * `popupLayer="local"`, 0 for a dialog docked by
87216
+ * `dockedOnSmallTouchScreen`.
87217
+ * @param {string} [popupTestId] The `data-testid` of the popup element — the
87218
+ * popover or dialog the children open in. A `data-testid` on the picker
87219
+ * itself names the trigger (see docs/testid.md); this one is for a test that
87220
+ * needs the popup's own box — its surface, its position, its backdrop — and
87221
+ * not only what it holds, which a testid inside the children already names.
87222
+ * Under `mode="callout"` it names the callout element — the control's own,
87223
+ * shared with its constraint messages — while the children are shown in it.
87224
+ * @param {string} [popupAriaLabelledBy] The id(s) of what names the popup:
87225
+ * `aria-labelledby` on the popup element, which is a dialog in either mode
87226
+ * (the trigger says `aria-haspopup="dialog"`). `aria-label`/`aria-labelledby`
87227
+ * on the picker itself name the trigger. Rarely needed: a `<Popup.Title>`
87228
+ * in the children names the popup with nothing to wire, and without one
87229
+ * the popup takes the trigger's own name — the picker's `aria-labelledby`,
87230
+ * the label a picker that picks nothing draws, its `aria-label`, or the
87231
+ * `<Label>`/`<label for>` pointing at it (one that has an id, and does not
87232
+ * wrap the picker). A `mode="callout"` popup is read as its message.
87233
+ * @param {string} [popupAriaLabel] The same, as a string, for a popup whose
87234
+ * title is not an element.
87235
+ * @param {string} [popupAriaDescribedBy] The id(s) of what describes the
87236
+ * popup: `aria-describedby` on the popup element.
87237
+ * @param {string} [confirmTestId] `type="confirm"`, default body: the
87238
+ * `data-testid` of the yes button; `cancelTestId` that of the no button.
87239
+ * Both are elements navi builds, which a test may not name by class (see
87240
+ * docs/testid.md).
87241
+ * @param {string} [cancelTestId]
87242
+ * @param {"always"|"idle"|"from-first-open"|"while-opened"} [mount] The popup's own
87243
+ * (see Popup): when the children are built and thrown away. Left out, a
87244
+ * picker told no value holds what the control they hold holds, and builds
87245
+ * them as soon as that value is read: right away (`"always"`) when navi draws
87246
+ * it (no `ui`, a `Picker.UI.*`, a component `ui`), when there is a clear
87247
+ * cross, or when a form or group around the picker collects it; when an
87248
+ * element `ui` calls `usePickerState()`. Read by nothing, it waits for the
87249
+ * first open, like a picker told a value. A `picksNothing` picker (and so
87250
+ * `type="confirm"`) never builds them early: its popup holds no value, there
87251
+ * is nothing to read.
87252
+ */
85699
87253
  // Assembled in pure expressions rather than written onto Picker afterwards
85700
87254
  // (`Picker.Chip = PickerChip`): what createComponentResolver returns is an object
85701
87255
  // a bundler cannot see through, so a property assigned to it later is a side
@@ -85952,7 +87506,7 @@ const css$q = /* css */`@layer navi {
85952
87506
  const TRAVELS_NONE = [];
85953
87507
 
85954
87508
  /**
85955
- * @type {import("ignore:preact").FunctionComponent<{
87509
+ * @type {import("preact").FunctionComponent<{
85956
87510
  * value?: any,
85957
87511
  * defaultValue?: any,
85958
87512
  * signal?: import("@preact/signals").Signal<any>,
@@ -85968,7 +87522,7 @@ const TRAVELS_NONE = [];
85968
87522
  * fallbackValue?: any,
85969
87523
  * valueAtStep: (value: any, count: number) => any,
85970
87524
  * compareValues?: (a: any, b: any) => number,
85971
- * renderValue?: (value: any, context: { maxLines?: number }) => import("ignore:preact").ComponentChildren,
87525
+ * renderValue?: (value: any, context: { maxLines?: number }) => import("preact").ComponentChildren,
85972
87526
  * controlProps?: object,
85973
87527
  * duration?: number,
85974
87528
  * vertical?: boolean,
@@ -86755,14 +88309,14 @@ const renderValueDefault = value => String(value ?? "");
86755
88309
  * single `value`/`signal` like any other control. `TimeSpin` is that, for a
86756
88310
  * time of day.
86757
88311
  *
86758
- * @type {import("ignore:preact").FunctionComponent<{
88312
+ * @type {import("preact").FunctionComponent<{
86759
88313
  * name?: string,
86760
88314
  * value?: any,
86761
88315
  * defaultValue?: any,
86762
88316
  * signal?: import("@preact/signals").Signal<any>,
86763
88317
  * aggregateChildStates?: (childUIStateControllers: any[]) => any,
86764
88318
  * distributeChildUIState?: (groupState: any, childUIStateController: any) => any,
86765
- * children?: import("ignore:preact").ComponentChildren,
88319
+ * children?: import("preact").ComponentChildren,
86766
88320
  * [key: string]: any,
86767
88321
  * }>}
86768
88322
  * Everything a box takes is taken here too — `width`, `borderWidth`,
@@ -86865,7 +88419,7 @@ SpinGroup.Separator = SpinGroupSeparator;
86865
88419
  * above and below it by default — sideways they would be where the caret
86866
88420
  * moves — with the bigger number up top.
86867
88421
  *
86868
- * @type {import("ignore:preact").FunctionComponent<{
88422
+ * @type {import("preact").FunctionComponent<{
86869
88423
  * value?: number|string,
86870
88424
  * defaultValue?: number|string,
86871
88425
  * min?: number,
@@ -86970,7 +88524,7 @@ const numberAtStep = (value, count, {
86970
88524
  * A day one steps through: the date plus what it is to today when there is a
86971
88525
  * word for it, and a calendar behind it for the day that is far from here.
86972
88526
  *
86973
- * @type {import("ignore:preact").FunctionComponent<{
88527
+ * @type {import("preact").FunctionComponent<{
86974
88528
  * value?: string,
86975
88529
  * defaultValue?: string,
86976
88530
  * signal?: import("@preact/signals").Signal<string>,
@@ -86981,7 +88535,7 @@ const numberAtStep = (value, count, {
86981
88535
  * maxMessage?: any,
86982
88536
  * step?: number,
86983
88537
  * lang?: string,
86984
- * renderDay?: (day: string) => import("ignore:preact").ComponentChildren,
88538
+ * renderDay?: (day: string) => import("preact").ComponentChildren,
86985
88539
  * [key: string]: any,
86986
88540
  * }>}
86987
88541
  * @param {string} [value] The day shown, as "YYYY-MM-DD".
@@ -86992,7 +88546,7 @@ const numberAtStep = (value, count, {
86992
88546
  * @param {"long"|"short"|"numeric"} [format="long"] How the date is written.
86993
88547
  * Long by default, since this is a control one reads rather than a column
86994
88548
  * one scans; `short` where the room is not there.
86995
- * @param {(day: string) => import("ignore:preact").ComponentChildren} [renderDay] What
88549
+ * @param {(day: string) => import("preact").ComponentChildren} [renderDay] What
86996
88550
  * to write for a day. Defaults to the date plus what it is to today when
86997
88551
  * there is a word for it ("samedi 8 août (demain)"), since a day near now is
86998
88552
  * read as a distance from now before it is read as a date.
@@ -87109,7 +88663,7 @@ const HOUR_MAX = 23;
87109
88663
  const MINUTE_MAX = 59;
87110
88664
 
87111
88665
  /**
87112
- * @type {import("ignore:preact").FunctionComponent<{
88666
+ * @type {import("preact").FunctionComponent<{
87113
88667
  * name?: string,
87114
88668
  * value?: string,
87115
88669
  * defaultValue?: string,
@@ -87117,7 +88671,7 @@ const MINUTE_MAX = 59;
87117
88671
  * minuteStep?: number,
87118
88672
  * pad?: number,
87119
88673
  * loop?: boolean,
87120
- * separator?: import("ignore:preact").ComponentChildren,
88674
+ * separator?: import("preact").ComponentChildren,
87121
88675
  * hourLabel?: string,
87122
88676
  * minuteLabel?: string,
87123
88677
  * [key: string]: any,
@@ -87130,7 +88684,7 @@ const MINUTE_MAX = 59;
87130
88684
  * of a day to stop at. Say `loop={false}` for two ends one cannot step past.
87131
88685
  * @param {number} [pad=2] How many digits an hour and a minute are written on:
87132
88686
  * a clock says "07:00", never "7:0". Say `pad={0}` for the bare numbers.
87133
- * @param {import("ignore:preact").ComponentChildren} [separator] What is written
88687
+ * @param {import("preact").ComponentChildren} [separator] What is written
87134
88688
  * between the hours and the minutes. "h" in French, ":" elsewhere.
87135
88689
  * Everything a box takes is taken here too — `width`, `borderRadius`, `size`.
87136
88690
  */
@@ -87196,7 +88750,7 @@ const distributeTime$1 = (groupState, childUIStateController) => {
87196
88750
  };
87197
88751
 
87198
88752
  /**
87199
- * @type {import("ignore:preact").FunctionComponent<{
88753
+ * @type {import("preact").FunctionComponent<{
87200
88754
  * name?: string,
87201
88755
  * value?: { start?: string, end?: string },
87202
88756
  * defaultValue?: { start?: string, end?: string },
@@ -87206,13 +88760,13 @@ const distributeTime$1 = (groupState, childUIStateController) => {
87206
88760
  * timeProps?: object,
87207
88761
  * loop?: boolean,
87208
88762
  * size?: string,
87209
- * startLabel?: import("ignore:preact").ComponentChildren,
87210
- * endLabel?: import("ignore:preact").ComponentChildren,
88763
+ * startLabel?: import("preact").ComponentChildren,
88764
+ * endLabel?: import("preact").ComponentChildren,
87211
88765
  * [key: string]: any,
87212
88766
  * }>}
87213
88767
  * @param {{ start?: string, end?: string }} [value] The span shown, as two
87214
88768
  * "HH:MM".
87215
- * @param {import("ignore:preact").ComponentChildren} [startLabel] What is written
88769
+ * @param {import("preact").ComponentChildren} [startLabel] What is written
87216
88770
  * before the first time ("De"), and `endLabel` between the two ("à"). Say
87217
88771
  * `null` for neither.
87218
88772
  * @param {number} [minuteStep=1] How many minutes a press on a minute chevron
@@ -87309,7 +88863,7 @@ const css$p = /* css */`.navi_checkbox_group {
87309
88863
  *
87310
88864
  * See `<RadioGroup>` for the same shape holding a single value.
87311
88865
  *
87312
- * @type {import("ignore:preact").FunctionComponent<{
88866
+ * @type {import("preact").FunctionComponent<{
87313
88867
  * maxLength?: number,
87314
88868
  * maxLengthGuard?: number,
87315
88869
  * [key: string]: any,
@@ -87370,6 +88924,19 @@ const CheckboxGroupInterface = props => {
87370
88924
  });
87371
88925
  };
87372
88926
 
88927
+ /**
88928
+ * @type {import("preact").FunctionComponent<{
88929
+ * unit: string,
88930
+ * plural?: boolean,
88931
+ * format?: "long" | "short" | "narrow",
88932
+ * lang?: string,
88933
+ * label?: string,
88934
+ * size?: string,
88935
+ * sizeRatio?: number,
88936
+ * style?: import("preact").JSX.CSSProperties,
88937
+ * [key: string]: any,
88938
+ * }>}
88939
+ */
87373
88940
  const Unit = ({
87374
88941
  unit,
87375
88942
  plural,
@@ -88127,7 +89694,7 @@ const css$m = /* css */`.navi_input.navi_select {
88127
89694
  `;
88128
89695
 
88129
89696
  /**
88130
- * @type {import("ignore:preact").FunctionComponent<{
89697
+ * @type {import("preact").FunctionComponent<{
88131
89698
  * value?: string,
88132
89699
  * defaultValue?: string,
88133
89700
  * signal?: import("@preact/signals").Signal<string>,
@@ -88206,30 +89773,40 @@ const css$l = /* css */`.navi_split_button {
88206
89773
 
88207
89774
  & > .navi_group {
88208
89775
  flex: auto;
89776
+
89777
+ & > .navi_button {
89778
+ flex: auto;
89779
+ }
88209
89780
  }
88210
89781
  }
88211
89782
 
88212
89783
  .navi_split_button_menu {
89784
+ --border-width: 0px;
88213
89785
  display: flex;
88214
89786
  position: relative;
89787
+
89788
+ & > .navi_button {
89789
+ margin-left: calc(var(--border-width) * -1);
89790
+ }
88215
89791
  }
88216
89792
  `;
88217
89793
 
88218
89794
  /**
88219
- * @type {import("ignore:preact").FunctionComponent<{
88220
- * options: Array<{value: any, label: import("ignore:preact").ComponentChildren, [key: string]: any}>,
89795
+ * @type {import("preact").FunctionComponent<{
89796
+ * options: Array<{value: any, label: import("preact").ComponentChildren, [key: string]: any}>,
88221
89797
  * value?: any,
88222
89798
  * defaultValue?: any,
88223
- * label?: import("ignore:preact").ComponentChildren,
88224
- * action?: (value: any, event: Event) => void | Promise<void>,
89799
+ * label?: import("preact").ComponentChildren,
89800
+ * action?: (value: any, { event, signal }: { event: Event, signal: AbortSignal }) => void | Promise<void>,
88225
89801
  * command?: string,
88226
89802
  * commandFor?: string,
88227
89803
  * prefetch?: boolean,
88228
- * onValueChange?: (value: any, event: Event) => void,
89804
+ * onValueChange?: (value: any, { event }: { event: Event }) => void,
88229
89805
  * chooseEffect?: "select" | "run",
88230
89806
  * menuLabel?: string,
88231
- * menuIcon?: import("ignore:preact").ComponentChildren,
89807
+ * menuIcon?: import("preact").ComponentChildren,
88232
89808
  * menuIconSize?: number | string,
89809
+ * menuTestId?: string,
88233
89810
  * loading?: boolean,
88234
89811
  * readOnly?: boolean,
88235
89812
  * disabled?: boolean,
@@ -88265,19 +89842,21 @@ const css$l = /* css */`.navi_split_button {
88265
89842
  * popupTestId?: string,
88266
89843
  * [key: string]: any,
88267
89844
  * }>}
88268
- * @param {Array<{value: any, label: import("ignore:preact").ComponentChildren}>} options
89845
+ * @param {Array<{value: any, label: import("preact").ComponentChildren}>} options
88269
89846
  * The ways of doing the thing, in the order the menu lists them. Anything
88270
89847
  * else on an entry (`disabled`, `icon`, `padding`…) reaches its row.
88271
89848
  * @param {any} [value] Which entry the button stands for — the one it runs, and
88272
89849
  * the one it is named after. `defaultValue` is the uncontrolled form; without
88273
89850
  * either, the first entry.
88274
- * @param {import("ignore:preact").ComponentChildren} [label] What the button says, when
89851
+ * @param {import("preact").ComponentChildren} [label] What the button says, when
88275
89852
  * that is not the name of the entry it stands for — a button that keeps one
88276
89853
  * name whatever the menu does.
88277
- * @param {(value: any, event: Event) => void} [action] Do the thing, for the
88278
- * entry given. Pressed on the button it is the entry the button stands for;
88279
- * with `chooseEffect="run"` it is also the entry just chosen. Awaited: the
88280
- * whole split button is busy until it settles.
89854
+ * @param {(value: any, { event, signal }) => void} [action] Do the thing, for
89855
+ * the entry given. Pressed on the button it is the entry the button stands
89856
+ * for; with `chooseEffect="run"` it is also the entry just chosen. Run as a
89857
+ * navi action: the second argument is the run's (see docs/actions.md), the
89858
+ * press being its `event`. Awaited: the whole split button is busy until it
89859
+ * settles.
88281
89860
  * @param {string} [command] What a press on the button does once its `action`
88282
89861
  * is done, as on a `<Button>` (with `commandFor`, and `command-value` when
88283
89862
  * what the command is about is not the entry the button stands for). A
@@ -88291,12 +89870,14 @@ const css$l = /* css */`.navi_split_button {
88291
89870
  * menu of one-offs around a primary action.
88292
89871
  * @param {string} [menuLabel] What the chevron half is called, for a screen
88293
89872
  * reader and for the tooltip a long press raises.
88294
- * @param {import("ignore:preact").ComponentChildren} [menuIcon] What the chevron half
89873
+ * @param {import("preact").ComponentChildren} [menuIcon] What the chevron half
88295
89874
  * draws, in place of the chevron. It is the whole of what that half says, so
88296
89875
  * pass something that still reads as "there is more here" — and say what,
88297
89876
  * through `menuLabel`.
88298
89877
  * @param {number|string} [menuIconSize] How big that icon is drawn. Defaults to
88299
89878
  * the button's own font size.
89879
+ * @param {string} [menuTestId] The `data-testid` of the chevron half, which
89880
+ * navi builds and the caller has no other way to name.
88300
89881
  * @param {"popover"|"dialog"} [mode="popover"] What the menu is drawn as. A
88301
89882
  * picker left to itself turns into a dialog on a small screen; a split
88302
89883
  * button's menu stays hung off the button there, so this says popover unless
@@ -88317,8 +89898,12 @@ const css$l = /* css */`.navi_split_button {
88317
89898
  * `popupBorderRadius`, `animation`. See picker.jsx for what each one says.
88318
89899
  * An `aria-*` attribute describes the button and reaches the main half —
88319
89900
  * `aria-pressed` for a split button whose main half toggles a mode;
88320
- * `aria-hidden` excepted, which hides the whole control. Anything else lands
88321
- * on the split button's own box.
89901
+ * `aria-hidden` excepted, which hides the whole control. How the words are
89902
+ * written (`size`, `bold`, `lineHeight`, `noWrap`…) reaches both halves, and so
89903
+ * does vertical padding (`paddingY`, `paddingTop`, `paddingBottom`); the rest
89904
+ * of the padding is the main half's — the chevron half keeps its own, which
89905
+ * gives it its width. Anything else (`className`, `width`, margins, `data-*`)
89906
+ * lands on the split button's own box.
88322
89907
  */
88323
89908
  const SplitButton = props => {
88324
89909
  import.meta.css = [css$l, "@jsenv/navi/src/control/input/split_button.jsx"];
@@ -88337,6 +89922,7 @@ const SplitButton = props => {
88337
89922
  menuLabel = buttonI18n("button.more_actions"),
88338
89923
  menuIcon = jsx(ChevronDownSvg$1, {}),
88339
89924
  menuIconSize,
89925
+ menuTestId,
88340
89926
  loading,
88341
89927
  readOnly,
88342
89928
  disabled,
@@ -88356,11 +89942,13 @@ const SplitButton = props => {
88356
89942
  ...rest
88357
89943
  } = props;
88358
89944
  // Everything the popup answers to travels to the Picker; an aria-* describes
88359
- // the button a screen reader meets, which is the main half; everything else
88360
- // is the split button's own box (margins, width, data-*). Sorted by name
88361
- // rather than named one by one so a Picker popup prop is forwarded by adding
88362
- // it to that list, not by threading it through here.
88363
- const [popupProps, buttonAriaProps, boxProps] = sortRestProps(rest);
89945
+ // the button a screen reader meets, which is the main half; how the words are
89946
+ // written and how far they sit from the edge belong to the halves, since a
89947
+ // <button> inherits neither from the box around it; everything else is the
89948
+ // split button's own box (margins, width, data-*). Sorted by name rather
89949
+ // than named one by one so a Picker popup prop is forwarded by adding it to
89950
+ // that list, not by threading it through here.
89951
+ const [popupProps, buttonAriaProps, halfStyleProps, mainPaddingProps, boxProps] = sortRestProps(rest);
88364
89952
  // A split button is a control on the page, not a place one goes: its menu
88365
89953
  // hangs off it even on a phone, where a picker left to itself would decide a
88366
89954
  // small screen means a dialog. Passing mode="dialog" asks for that back.
@@ -88405,6 +89993,7 @@ const SplitButton = props => {
88405
89993
  borderColor,
88406
89994
  borderWidth,
88407
89995
  borderRadius,
89996
+ ...halfStyleProps,
88408
89997
  pressEffect,
88409
89998
  loading: loadingResolved,
88410
89999
  readOnly,
@@ -88414,7 +90003,7 @@ const SplitButton = props => {
88414
90003
  };
88415
90004
  return jsxs(Box, {
88416
90005
  ref: rootRef,
88417
- className: "navi_split_button",
90006
+ baseClassName: "navi_split_button",
88418
90007
  borderRadius: borderRadius,
88419
90008
  ...boxProps,
88420
90009
  children: [jsx(LoadingOutline, {
@@ -88431,6 +90020,7 @@ const SplitButton = props => {
88431
90020
  "command-value": commandValue,
88432
90021
  prefetch: prefetch,
88433
90022
  ...buttonAriaProps,
90023
+ ...mainPaddingProps,
88434
90024
  ...halfProps,
88435
90025
  children: label === undefined ? optionShown?.label : label
88436
90026
  }), jsxs("div", {
@@ -88440,6 +90030,7 @@ const SplitButton = props => {
88440
90030
  paddingX: "s",
88441
90031
  expandY: true,
88442
90032
  "aria-label": menuLabel,
90033
+ "data-testid": menuTestId,
88443
90034
  command: "--navi-open",
88444
90035
  commandFor: menuId,
88445
90036
  ...halfProps,
@@ -88519,9 +90110,16 @@ const SplitButton = props => {
88519
90110
  // a caller reaches all of them through the split button (see picker.jsx's JSDoc
88520
90111
  // for what each one says).
88521
90112
  const POPUP_PROP_SET = new Set(["mode", "popupLayer", "popupTestId", "positionArea", "popoverMode", "popoverSpacing", "popupWidthFitContent", "popoverMaxHeight", "dialogMinWidth", "dialogMinHeight", "dialogMaxWidth", "dialogMaxHeight", "dialogExpand", "dialogExpandX", "dialogExpandY", "dockedOnSmallTouchScreen", "marginWithContainer", "backdrop", "backdropVariant", "backdropColor", "backdropFilter", "pressOutside", "escapeEffect", "closeOnFocusOut", "scrollCapture", "focusCapture", "popupBackgroundColor", "popupBorderRadius", "animation"]);
90113
+ // Vertical padding is said about both halves so they stay the same height; the
90114
+ // horizontal one is the main half's alone, the chevron half's own (paddingX="s")
90115
+ // being what gives it its width.
90116
+ const HALF_PADDING_PROP_SET = new Set(["paddingY", "paddingTop", "paddingBottom"]);
90117
+ const MAIN_PADDING_PROP_SET = new Set(["padding", "paddingX", "paddingLeft", "paddingRight"]);
88522
90118
  const sortRestProps = props => {
88523
90119
  const popupProps = {};
88524
90120
  const buttonAriaProps = {};
90121
+ const halfStyleProps = {};
90122
+ const mainPaddingProps = {};
88525
90123
  const boxProps = {};
88526
90124
  for (const key of Object.keys(props)) {
88527
90125
  if (POPUP_PROP_SET.has(key)) {
@@ -88530,11 +90128,15 @@ const sortRestProps = props => {
88530
90128
  // aria-hidden hides a subtree rather than describing a button: on the
88531
90129
  // main half alone it would leave the chevron half exposed.
88532
90130
  buttonAriaProps[key] = props[key];
90131
+ } else if (isTypoProp(key) || HALF_PADDING_PROP_SET.has(key)) {
90132
+ halfStyleProps[key] = props[key];
90133
+ } else if (MAIN_PADDING_PROP_SET.has(key)) {
90134
+ mainPaddingProps[key] = props[key];
88533
90135
  } else {
88534
90136
  boxProps[key] = props[key];
88535
90137
  }
88536
90138
  }
88537
- return [popupProps, buttonAriaProps, boxProps];
90139
+ return [popupProps, buttonAriaProps, halfStyleProps, mainPaddingProps, boxProps];
88538
90140
  };
88539
90141
 
88540
90142
  /**
@@ -89736,7 +91338,7 @@ const WheelGroupContext = createContext(null);
89736
91338
  * value, the value lives in one hidden input for the form. The visible rows are
89737
91339
  * presentational.
89738
91340
  *
89739
- * @type {import("ignore:preact").FunctionComponent<{
91341
+ * @type {import("preact").FunctionComponent<{
89740
91342
  * value?: any,
89741
91343
  * defaultValue?: any,
89742
91344
  * action?: (value: any) => void,
@@ -89754,7 +91356,7 @@ const WheelGroupContext = createContext(null);
89754
91356
  * readOnly?: boolean,
89755
91357
  * disabled?: boolean,
89756
91358
  * loading?: boolean,
89757
- * children?: import("ignore:preact").ComponentChildren,
91359
+ * children?: import("preact").ComponentChildren,
89758
91360
  * [key: string]: any,
89759
91361
  * }>}
89760
91362
  * @param {number} [props.visibleCount=3] - Odd number of rows visible in the viewport (the center one is the selection).
@@ -90382,8 +91984,9 @@ function WheelUI(props) {
90382
91984
  }, {
90383
91985
  controlType: "input",
90384
91986
  // The host is the hidden input carrying the value: the container is
90385
- // what is focused and spun.
90386
- listenersOn: "root"
91987
+ // what is focused and spun, and the spinbutton its name has to name.
91988
+ listenersOn: "root",
91989
+ nameOn: "root"
90387
91990
  });
90388
91991
  const uiStateController = getUIStateControllerById(controlHostProps.id);
90389
91992
  const {
@@ -91233,9 +92836,9 @@ const WheelWindow = ({
91233
92836
  * of rows and fills each from this tracked data — so the value list is the source
91234
92837
  * of truth, not a per-item DOM node. Selection lives on the wheel's value.
91235
92838
  *
91236
- * @type {import("ignore:preact").FunctionComponent<{
92839
+ * @type {import("preact").FunctionComponent<{
91237
92840
  * value: any,
91238
- * children?: import("ignore:preact").ComponentChildren,
92841
+ * children?: import("preact").ComponentChildren,
91239
92842
  * [key: string]: any,
91240
92843
  * }>}
91241
92844
  */
@@ -91270,9 +92873,9 @@ Wheel.Item = WheelItem;
91270
92873
  * each Wheel.Item (it's a Box: paddingX="0.5ch") so the gap stays scrollable.
91271
92874
  * `spacing` is just the Box flex gap between children (a layout gap, not padding).
91272
92875
  *
91273
- * @type {import("ignore:preact").FunctionComponent<{
92876
+ * @type {import("preact").FunctionComponent<{
91274
92877
  * horizontal?: boolean,
91275
- * children?: import("ignore:preact").ComponentChildren,
92878
+ * children?: import("preact").ComponentChildren,
91276
92879
  * [key: string]: any,
91277
92880
  * }>}
91278
92881
  * @param {boolean} [props.horizontal] - Stack the (horizontal) wheels vertically instead of in a row.
@@ -91469,14 +93072,14 @@ const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
91469
93072
  const END_OF_DAY = END_OF_DAY_HOUR * MINUTES_PER_HOUR;
91470
93073
 
91471
93074
  /**
91472
- * @type {import("ignore:preact").FunctionComponent<{
93075
+ * @type {import("preact").FunctionComponent<{
91473
93076
  * name?: string,
91474
93077
  * value?: string,
91475
93078
  * defaultValue?: string,
91476
93079
  * signal?: import("@preact/signals").Signal<string>,
91477
93080
  * minuteStep?: number,
91478
93081
  * loop?: boolean,
91479
- * separator?: import("ignore:preact").ComponentChildren,
93082
+ * separator?: import("preact").ComponentChildren,
91480
93083
  * hourLabel?: string,
91481
93084
  * minuteLabel?: string,
91482
93085
  * size?: string,
@@ -91497,7 +93100,7 @@ const END_OF_DAY = END_OF_DAY_HOUR * MINUTES_PER_HOUR;
91497
93100
  * @param {boolean} [loop=true] The wheels go round: 23h then 0h, 59 minutes
91498
93101
  * then 0. What a clock does. Say `loop={false}` for two ends one cannot turn
91499
93102
  * past.
91500
- * @param {import("ignore:preact").ComponentChildren} [separator] What is written
93103
+ * @param {import("preact").ComponentChildren} [separator] What is written
91501
93104
  * between the hours and the minutes. "h" in French, ":" elsewhere. In a time
91502
93105
  * of whole hours it follows each hour on its row instead ("8h"), and nothing
91503
93106
  * is written by default outside French.
@@ -91616,7 +93219,7 @@ const TimeWheel = ({
91616
93219
  };
91617
93220
 
91618
93221
  /**
91619
- * @type {import("ignore:preact").FunctionComponent<{
93222
+ * @type {import("preact").FunctionComponent<{
91620
93223
  * name?: string,
91621
93224
  * value?: { start?: string, end?: string },
91622
93225
  * defaultValue?: { start?: string, end?: string },
@@ -91625,15 +93228,15 @@ const TimeWheel = ({
91625
93228
  * minDuration?: number,
91626
93229
  * loop?: boolean,
91627
93230
  * size?: string,
91628
- * startLabel?: import("ignore:preact").ComponentChildren,
91629
- * endLabel?: import("ignore:preact").ComponentChildren,
93231
+ * startLabel?: import("preact").ComponentChildren,
93232
+ * endLabel?: import("preact").ComponentChildren,
91630
93233
  * labelPosition?: "before" | "above",
91631
93234
  * timeProps?: object,
91632
93235
  * [key: string]: any,
91633
93236
  * }>}
91634
93237
  * @param {{ start?: string, end?: string }} [value] The span shown, as two
91635
93238
  * "HH:MM".
91636
- * @param {import("ignore:preact").ComponentChildren} [startLabel] What is written
93239
+ * @param {import("preact").ComponentChildren} [startLabel] What is written
91637
93240
  * before the first time ("De"), and `endLabel` between the two ("à"). Say
91638
93241
  * `null` for neither.
91639
93242
  * @param {"before"|"above"} [labelPosition="before"] Where each label stands.
@@ -95618,17 +97221,25 @@ const Address = ({
95618
97221
  };
95619
97222
 
95620
97223
  installImportMetaCssBuild(import.meta);
95621
- const css$a = /* css */`@layer navi;
97224
+ const css$a = /* css */`@layer navi {
97225
+ .navi_text.navi_badge_count {
97226
+ --font-size: .7em;
97227
+ --badge-count-padding-x-default: .5em;
97228
+ --badge-count-padding-y-default: .3em;
97229
+
97230
+ &[data-ellipse][data-single-char], &[data-circle] {
97231
+ --badge-count-padding-x-default: 0;
97232
+ --badge-count-padding-y-default: 0;
97233
+ }
97234
+ }
97235
+ }
95622
97236
 
95623
97237
  .navi_text.navi_badge_count {
95624
97238
  white-space: nowrap;
95625
- --font-size: .7em;
95626
97239
  --x-background: var(--background);
95627
- --x-background-color: var(--background-color, var(--x-background));
97240
+ --x-background-color: var(--background-color);
95628
97241
  --x-color-contrasting: var(--navi-color-white);
95629
97242
  --x-color: var(--color, var(--x-color-contrasting));
95630
- --badge-count-padding-x-default: .5em;
95631
- --badge-count-padding-y-default: .3em;
95632
97243
  --x-badge-count-padding-top: var(--badge-count-padding-top, var(--badge-count-padding-y, var(--badge-count-padding, var(--badge-count-padding-y-default))));
95633
97244
  --x-badge-count-padding-right: var(--badge-count-padding-right, var(--badge-count-padding-x, var(--badge-count-padding, var(--badge-count-padding-x-default))));
95634
97245
  --x-badge-count-padding-bottom: var(--badge-count-padding-bottom, var(--badge-count-padding-y, var(--badge-count-padding, var(--badge-count-padding-y-default))));
@@ -95660,12 +97271,9 @@ const css$a = /* css */`@layer navi;
95660
97271
  padding-bottom: var(--x-badge-count-padding-bottom);
95661
97272
  padding-left: var(--x-badge-count-padding-left);
95662
97273
  background: var(--x-background);
95663
- background-color: var(--x-background-color);
95664
97274
  border-radius: 1em;
95665
97275
 
95666
97276
  &[data-single-char] {
95667
- --badge-count-padding-x-default: 0;
95668
- --badge-count-padding-y-default: 0;
95669
97277
  box-sizing: content-box;
95670
97278
  text-align: center;
95671
97279
  border-radius: 100em;
@@ -95678,8 +97286,6 @@ const css$a = /* css */`@layer navi;
95678
97286
 
95679
97287
  &[data-circle] {
95680
97288
  --x-number-font-size: var(--font-size);
95681
- --badge-count-padding-x-default: 0;
95682
- --badge-count-padding-y-default: 0;
95683
97289
  box-sizing: content-box;
95684
97290
  aspect-ratio: 1;
95685
97291
  width: var(--x-radius);
@@ -95689,7 +97295,6 @@ const css$a = /* css */`@layer navi;
95689
97295
  padding-bottom: var(--x-badge-count-padding-bottom);
95690
97296
  padding-left: var(--x-badge-count-padding-left);
95691
97297
  background: var(--x-background);
95692
- background-color: var(--x-background-color);
95693
97298
  border-radius: 100em;
95694
97299
  justify-content: center;
95695
97300
  align-items: center;
@@ -95714,6 +97319,10 @@ const css$a = /* css */`@layer navi;
95714
97319
  font-size: var(--x-number-font-size);
95715
97320
  }
95716
97321
  }
97322
+
97323
+ &[data-background-color] {
97324
+ background-color: var(--x-background-color);
97325
+ }
95717
97326
  }
95718
97327
  `;
95719
97328
  const BadgeCountOverflow = () => jsx("span", {
@@ -95850,6 +97459,7 @@ const BadgeCountEllipse = ({
95850
97459
  "data-ellipse": "",
95851
97460
  "data-value-overflow": hasOverflow ? "" : undefined,
95852
97461
  "data-loading": loading ? "" : undefined,
97462
+ "data-background-color": props.backgroundColor === undefined ? undefined : "",
95853
97463
  "data-single-char": charCount === 1 ? "" : undefined,
95854
97464
  ...props,
95855
97465
  styleCSSVars: BadgeCountStyleCSSVars,
@@ -95872,6 +97482,7 @@ const BadgeCountCircle = ({
95872
97482
  "data-circle": "",
95873
97483
  bold: true,
95874
97484
  "data-loading": loading ? "" : undefined,
97485
+ "data-background-color": props.backgroundColor === undefined ? undefined : "",
95875
97486
  "data-single-char": charCount === 1 ? "" : undefined,
95876
97487
  "data-two-chars": charCount === 2 ? "" : undefined,
95877
97488
  "data-three-chars": charCount === 3 ? "" : undefined,
@@ -96364,7 +97975,7 @@ const css$8 = /* css */`@layer navi;
96364
97975
  `;
96365
97976
 
96366
97977
  /**
96367
- * @type {import("ignore:preact").FunctionComponent<{
97978
+ * @type {import("preact").FunctionComponent<{
96368
97979
  * children?: number | string,
96369
97980
  * unit?: string | [string, string],
96370
97981
  * unitPosition?: "right" | "bottom",
@@ -96956,8 +98567,8 @@ const css$4 = /* css */`.svg_mask_content * {
96956
98567
  *
96957
98568
  * @param {Object} props - Component properties
96958
98569
  * @param {string} props.viewBox - The main viewBox for the composition (required as soon as there is an overlay)
96959
- * @param {import("ignore:preact").ComponentChildren} props.children - SVG elements (first is base, rest are overlays)
96960
- * @returns {import("ignore:preact").VNode} A composed SVG with all elements properly masked
98570
+ * @param {import("preact").ComponentChildren} props.children - SVG elements (first is base, rest are overlays)
98571
+ * @returns {import("preact").VNode} A composed SVG with all elements properly masked
96961
98572
  */
96962
98573
  const SVGMaskOverlay = ({
96963
98574
  viewBox,
@@ -97047,7 +98658,6 @@ const css$3 = /* css */`@layer navi {
97047
98658
  padding-bottom: var(--layout-padding-bottom, var(--layout-padding-y, var(--layout-padding)));
97048
98659
  padding-left: var(--layout-padding-left, var(--layout-padding-x, var(--layout-padding)));
97049
98660
  background: var(--layout-background);
97050
- background-color: var(--layout-background-color, var(--layout-background));
97051
98661
  border-width: var(--layout-border-width);
97052
98662
  border-style: solid;
97053
98663
  border-color: var(--layout-border-color);
@@ -97064,7 +98674,6 @@ const CardLayoutStyleCSSVars = {
97064
98674
  borderWidth: "--layout-border-width",
97065
98675
  borderColor: "--layout-border-color",
97066
98676
  background: "--layout-background",
97067
- backgroundColor: "--layout-background-color",
97068
98677
  padding: "--layout-padding",
97069
98678
  paddingTop: "--layout-padding-top",
97070
98679
  paddingBottom: "--layout-padding-bottom",
@@ -97079,7 +98688,7 @@ const CardLayoutStyleCSSVars = {
97079
98688
  * what holds it, so how tall that area is is up to the page around it, as for
97080
98689
  * `ViewportLayout`.
97081
98690
  *
97082
- * @type {import("ignore:preact").FunctionComponent<{
98691
+ * @type {import("preact").FunctionComponent<{
97083
98692
  * alignX?: string,
97084
98693
  * alignY?: string,
97085
98694
  * margin?: string|number,
@@ -97104,6 +98713,7 @@ const CardLayout = ({
97104
98713
  children,
97105
98714
  alignX = "center",
97106
98715
  alignY = "center",
98716
+ backgroundColor,
97107
98717
  ...props
97108
98718
  }) => {
97109
98719
  import.meta.css = [css$3, "@jsenv/navi/src/layout/card_layout.jsx"];
@@ -97122,6 +98732,7 @@ const CardLayout = ({
97122
98732
  className: "navi_card",
97123
98733
  alignX: alignX,
97124
98734
  alignY: alignY,
98735
+ backgroundColor: backgroundColor,
97125
98736
  children: children
97126
98737
  })
97127
98738
  });
@@ -97295,7 +98906,7 @@ const LINE_GAP = 5;
97295
98906
  const StepListContext = createContext(null);
97296
98907
 
97297
98908
  /**
97298
- * @type {import("ignore:preact").FunctionComponent<{
98909
+ * @type {import("preact").FunctionComponent<{
97299
98910
  * current?: string,
97300
98911
  * slideContainer?: string,
97301
98912
  * travelByClick?: boolean,
@@ -97652,7 +99263,7 @@ const sameShallow = (previousObject, nextObject) => {
97652
99263
  * It is both StepList.Item and an export of its own, the way Slide is to
97653
99264
  * SlideContainer.
97654
99265
  *
97655
- * @type {import("ignore:preact").FunctionComponent<{
99266
+ * @type {import("preact").FunctionComponent<{
97656
99267
  * value: string,
97657
99268
  * done?: boolean,
97658
99269
  * [key: string]: any,
@@ -97714,7 +99325,7 @@ const ViewportLayoutStyleCSSVars = {
97714
99325
  * same inside a route, a preview pane or a demo box, and it is up to the page
97715
99326
  * around it to be full-height.
97716
99327
  *
97717
- * @type {import("ignore:preact").FunctionComponent<{
99328
+ * @type {import("preact").FunctionComponent<{
97718
99329
  * padding?: string|number,
97719
99330
  * paddingTop?: string|number,
97720
99331
  * paddingBottom?: string|number,
@@ -97939,7 +99550,7 @@ const css = /* css */`.navi_side_panel {
97939
99550
  * `closeByPressOutside`/`pressOutside` only say what a
97940
99551
  * press on the wall does, not whether there is one (see `dialog.jsx`'s own
97941
99552
  * doc).
97942
- * @param {import("ignore:preact").ComponentChildren} props.children - No built-in
99553
+ * @param {import("preact").ComponentChildren} props.children - No built-in
97943
99554
  * close button — add one wherever it makes sense for the layout (e.g. a
97944
99555
  * plain `<Button command="--navi-close">` in `SidePanel.Head`), or rely on
97945
99556
  * `closeByPressOutside`/Escape instead.
@@ -98038,7 +99649,7 @@ const toCssLength = (value, propertyName) => value === undefined || value === nu
98038
99649
  * @param {object} props
98039
99650
  * @param {string} [props.className] - Merged with the shared
98040
99651
  * `"navi_side_panel_head"` class this file's own CSS targets.
98041
- * @param {import("ignore:preact").ComponentChildren} props.children
99652
+ * @param {import("preact").ComponentChildren} props.children
98042
99653
  */
98043
99654
  const SidePanelHead = props => jsx(Box, {
98044
99655
  baseClassName: "navi_side_panel_head",
@@ -98051,7 +99662,7 @@ const SidePanelHead = props => jsx(Box, {
98051
99662
  * @param {object} props
98052
99663
  * @param {string} [props.className] - Merged with the shared
98053
99664
  * `"navi_side_panel_foot"` class this file's own CSS targets.
98054
- * @param {import("ignore:preact").ComponentChildren} props.children
99665
+ * @param {import("preact").ComponentChildren} props.children
98055
99666
  */
98056
99667
  const SidePanelFoot = props => jsx(Box, {
98057
99668
  className: "navi_side_panel_foot",
@@ -98060,6 +99671,58 @@ const SidePanelFoot = props => jsx(Box, {
98060
99671
  SidePanel.Head = SidePanelHead;
98061
99672
  SidePanel.Foot = SidePanelFoot;
98062
99673
 
99674
+ /**
99675
+ * Creates a linked `[Slot, SlotFill]` pair so content rendered anywhere in
99676
+ * the tree (`SlotFill`) appears at a fixed location elsewhere (`Slot`) — a
99677
+ * lightweight, signal-based alternative to a DOM portal: `SlotFill` itself
99678
+ * renders nothing (no extra DOM node at its own call site), it just writes
99679
+ * its props into a shared signal that `Slot` reads reactively.
99680
+ *
99681
+ * Holds at most one filler at a time — there is no stacking/queueing. The
99682
+ * filler is the last `SlotFill` (from this same `createSlot()` call) to
99683
+ * mount, or the first to render into an empty slot; later renders of a
99684
+ * `SlotFill` that is not the filler leave the slot alone. A `SlotFill`
99685
+ * unmounting empties the slot only while it is still that one, and so does one
99686
+ * in a page a route transition keeps while leaving (see keepLeavingPages in
99687
+ * nav/route.jsx): still mounted, hidden, it must neither stay in the slot nor
99688
+ * take it back from the page arriving.
99689
+ *
99690
+ * **One `SlotFill`, rendered where the choice is made.** `isFilled` follows
99691
+ * the fills as they mount and unmount, in the order the tree walks them. A
99692
+ * slot fed from several places — a `SlotFill` in each card of a board, each
99693
+ * card deciding on its own whether it is the open one — reads as empty
99694
+ * between the leaver and the newcomer whenever they render separately, and
99695
+ * anything acting on `isFilled` acts on that empty frame (a `SidePanel` with
99696
+ * `open={isFilled}` closes for good: its `onClose` clears the state the
99697
+ * newcomer was waiting for). The slot cannot tell that gap from a real
99698
+ * emptying, and does not try to: render one `SlotFill` at the level that
99699
+ * holds the choice, with the chosen content as its children, and bind a
99700
+ * state that must not blink (a panel's `open`) to the app's own signal
99701
+ * rather than to `isFilled` — see docs/popup_open.md, "One panel, fed by a
99702
+ * slot".
99703
+ *
99704
+ * `Slot` keeps `SlotRenderer` mounted permanently — even while unfilled it
99705
+ * still renders it, with no props and `isFilled={false}` — instead of
99706
+ * unmounting it. This lets `SlotRenderer` be a persistent wrapper (a
99707
+ * `SidePanel` that opens and closes with an animation instead of being
99708
+ * mounted/unmounted alongside the filler — see 7_slot_demo.html's "Two
99709
+ * side-panel slots" section). A `SlotRenderer` that wants the
99710
+ * render-nothing-when-unfilled behavior can opt in with its own
99711
+ * `if (!isFilled) return null;` (the default `Box` renderer doesn't do
99712
+ * this — an unfilled default slot just renders an empty `<Box/>`).
99713
+ *
99714
+ * @param {import("preact").ComponentType} [SlotRenderer=Box] - Rendered by
99715
+ * `Slot`, always, with whatever props `SlotFill` last set (spread
99716
+ * directly) plus `isFilled`. Swap this out to reuse the same slot
99717
+ * mechanism for something other than a plain `Box` (e.g. a specific
99718
+ * component expecting its own particular props, or a persistent wrapper
99719
+ * like `SidePanel`).
99720
+ * @returns {[import("preact").ComponentType, import("preact").ComponentType<object>]}
99721
+ * `[Slot, SlotFill]` — `Slot` takes no props, render it once wherever the
99722
+ * content should appear. `SlotFill` takes whatever props `SlotRenderer`
99723
+ * expects, render it anywhere else in the tree to supply/update that
99724
+ * content.
99725
+ */
98063
99726
  const createSlot = (SlotRenderer = Box) => {
98064
99727
  const slotPropsSignal = signal();
98065
99728
  let filler = null;