@jsenv/navi 0.29.478 → 0.29.480

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
@@ -9734,13 +9882,18 @@ let isUpdatingRoutesFromUrl = false;
9734
9882
  * Declares a route from an url pattern.
9735
9883
  *
9736
9884
  * @param {string} pattern - The url pattern, where `:name` is a path param and
9737
- * `:name=${signal}` binds that param to a signal.
9885
+ * `:name=${signal}` binds that param to a signal. A pattern ending in `/` is a
9886
+ * frame: it matches every address below it too, and its search params hold
9887
+ * there (see docs/navigation.md, "A page or a frame: the trailing slash").
9738
9888
  * @param {object} [options]
9739
9889
  * @param {Object<string, import("@preact/signals").Signal | {signal: import("@preact/signals").Signal, default?: any}>} [options.searchParams]
9740
9890
  * Search params this route two-way syncs with, by name. The signal and the url
9741
9891
  * are the same state: writing the signal rewrites the address, an address
9742
9892
  * arriving from outside writes the signal, and the param disappears from the
9743
9893
  * url while the signal holds its default.
9894
+ * They exist on this route's address only, unless the pattern is a frame
9895
+ * (`/map/`, `/map/*`, the root): then on every address below it too.
9896
+ * `/places` does not lend its `?q=` to `/places/clubs`.
9744
9897
  * `{ signal, default }` says what that state is worth ON THIS ROUTE: the same
9745
9898
  * step opens a creation wizard on its first question and an edition one on
9746
9899
  * its summary, each address staying clean on the step its page opens on. One
@@ -9759,7 +9912,9 @@ let isUpdatingRoutesFromUrl = false;
9759
9912
  * whose param declines the segment does not match at all: no signal is
9760
9913
  * written, `<Route fallback>` is reachable, and `/:gameId` can sit at the root
9761
9914
  * 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 `/`.
9915
+ * is not one of the values it accepts — so `/:gameId` does not match `/`,
9916
+ * unless its default stands for the bare address (see docs/navigation.md,
9917
+ * "Declaring the sections is what makes them places").
9763
9918
  *
9764
9919
  * Constrain the shape, not the existence: whether that game exists is for the
9765
9920
  * route action and the page to answer, on a route that did match.
@@ -10942,14 +11097,17 @@ const urlWithoutSearchAndHash = (url) => {
10942
11097
  *
10943
11098
  * - `isShown()` — whether the container would still show the page. False as
10944
11099
  * 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
11100
+ * hidden until the route transition leaving it is over, nothing it reads is
11101
+ * worth a render any more, and nothing it holds speaks for the page shown (a
11102
+ * list's hold on the document, see holdDocumentScroll). False for good once
11103
+ * it is kept while leaving: a return to its route mounts the page anew. Asked
11104
+ * outside any render — by a subscription deciding whether to re-render, by
11105
+ * the scroll restoration when a position is due — and it reads the routes
10949
11106
  * untracked, so asking subscribes nobody to them.
10950
11107
  * - `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.
11108
+ * plays: still mounted, so what it fills or moves outside its own nodes (a
11109
+ * slot in a bar, the document a list scrolls) steps out rather than showing
11110
+ * with, or moving, the page arriving. Every box of it measures 0×0 by then.
10953
11111
  */
10954
11112
  const RoutePageContext = createContext(null);
10955
11113
 
@@ -12443,6 +12601,25 @@ const getHrefTargetInfo = (
12443
12601
  };
12444
12602
  };
12445
12603
 
12604
+ // The target of a link whose caller names none: a page of another site opens
12605
+ // in a new tab. Only a page: a `tel:`, `mailto:` or `sms:` address is handed to
12606
+ // another app, a `blob:` or `data:` one is this page's own, and a tab opened
12607
+ // for them would be left behind empty.
12608
+ const getDefaultHrefTarget = (
12609
+ href,
12610
+ currentUrlString = window.location.href,
12611
+ ) => {
12612
+ const { isEmpty, isSameSite } = getHrefTargetInfo(href, currentUrlString);
12613
+ if (isEmpty || isSameSite) {
12614
+ return undefined;
12615
+ }
12616
+ const { protocol } = new URL(href, currentUrlString);
12617
+ if (protocol !== "http:" && protocol !== "https:") {
12618
+ return undefined;
12619
+ }
12620
+ return "_blank";
12621
+ };
12622
+
12446
12623
  // A field of getHrefTargetInfo that depends on the page one is on (isCurrent,
12447
12624
  // isAnchor), read through a computed of that one boolean: the caller re-renders
12448
12625
  // when it flips, not at every address write — a search param bound to a wheel
@@ -12483,6 +12660,27 @@ const linkAsksForDocument = (linkElement) => {
12483
12660
  return linkElement.hasAttribute(LINK_DOCUMENT_ATTRIBUTE);
12484
12661
  };
12485
12662
 
12663
+ /*
12664
+ * A link does not always move the tab it is in. A `target` naming another
12665
+ * browsing context (a new tab, a named window) loads the address there, in a
12666
+ * document of its own; a `download` saves it. The browser answers both
12667
+ * without touching this page, and that is the point of writing them: reading
12668
+ * the terms beside a half-filled form costs nothing. Routing such a link
12669
+ * would take this tab instead, and give no tab and no file.
12670
+ *
12671
+ * `_parent` and `_top` count as elsewhere too. Inside a frame they are; at the
12672
+ * top level the browser treats them as `_self` and loads a document, which
12673
+ * costs a load and nothing else.
12674
+ */
12675
+
12676
+ const linkOpensElsewhere = (linkElement) => {
12677
+ const target = linkElement.getAttribute("target");
12678
+ if (target && target !== "_self") {
12679
+ return true;
12680
+ }
12681
+ return linkElement.hasAttribute("download");
12682
+ };
12683
+
12486
12684
  /*
12487
12685
  * A press aims at a place; it does not always go one step deeper. A row of tabs
12488
12686
  * is a lateral move — the neighbour is one finger away — so the whole row should
@@ -12812,8 +13010,10 @@ const setupBrowserIntegrationViaHistory = ({
12812
13010
  // Ignore non-left clicks
12813
13011
  return;
12814
13012
  }
12815
- if (e.metaKey) {
12816
- // Ignore clicks with meta key (e.g. open in new tab)
13013
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
13014
+ // A modifier asks the browser for something other than this tab: a new
13015
+ // tab (meta on macOS, ctrl elsewhere), a new window (shift), a download
13016
+ // (alt).
12817
13017
  return;
12818
13018
  }
12819
13019
  if (e.defaultPrevented) {
@@ -12824,6 +13024,10 @@ const setupBrowserIntegrationViaHistory = ({
12824
13024
  if (linkElement.hasAttribute("data-readonly")) {
12825
13025
  return;
12826
13026
  }
13027
+ if (linkOpensElsewhere(linkElement)) {
13028
+ // Another tab or window, or a download (see link_elsewhere.js).
13029
+ return;
13030
+ }
12827
13031
  const href = linkElement.href;
12828
13032
  const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
12829
13033
  getHrefTargetInfo(href);
@@ -16925,6 +17129,9 @@ const surfaceTextCss = /* css */ `
16925
17129
  }
16926
17130
  `;
16927
17131
 
17132
+ // What a callout's own JSX may know about the callout around it: how to close
17133
+ // it, and the element — whose id is what `commandFor` needs when the button
17134
+ // closing it is not inside it (inside, `--navi-close` finds it on its own).
16928
17135
  const CalloutContext = createContext();
16929
17136
  const useCalloutRequestClose = () => {
16930
17137
  return useContext(CalloutContext)?.requestClose;
@@ -17058,7 +17265,7 @@ const CALLOUT_STATUS_GLYPH_VIEWBOX = "0 0 540 540";
17058
17265
  const CALLOUT_STATUS_GLYPH_PATH = "m232.5,121 8,196h59l8-196zm36.5,224a37,37 0 1,0 2,0z";
17059
17266
 
17060
17267
  /**
17061
- * @type {import("ignore:preact").FunctionComponent<{
17268
+ * @type {import("preact").FunctionComponent<{
17062
17269
  * status?: "info" | "warning" | "error" | "success" | "none",
17063
17270
  * shape?: "square" | "circle",
17064
17271
  * }>}
@@ -21877,23 +22084,12 @@ defineInteractionDetector({
21877
22084
  // A link and an image are draggable without anyone asking, and a native
21878
22085
  // drag IS press-and-move: the browser claims the gesture, takes the pointer
21879
22086
  // events with it and paints a ghost of the row the hand is trying to swipe.
21880
- // Two things are needed, and neither covers the other:
21881
- // - `draggable` refuses it on the element itself, before it starts;
21882
- // - a swipe is usually a row with a link or a thumbnail INSIDE it, and
21883
- // those are draggable in their own right. `dragstart` bubbles, so
21884
- // refusing it here refuses theirs too — and it is the only refusal every
21885
- // browser honours (`-webkit-user-drag` is one engine's).
21886
- // The cost is stated rather than worked around: an element that takes a
21887
- // swipe cannot also be dragged out of the page, because there is one
21888
- // gesture and it cannot mean both.
22087
+ // `draggable` refuses it on the element itself, before it starts; the drag
22088
+ // of a link or a thumbnail INSIDE it is refused by the travel reading the
22089
+ // press (startDragToTravel). The cost is stated rather than worked around:
22090
+ // an element that takes a swipe cannot also be dragged out of the page,
22091
+ // because there is one gesture and it cannot mean both.
21889
22092
  mark("draggable", "false");
21890
- const onDragStart = (dragStartEvent) => {
21891
- dragStartEvent.preventDefault();
21892
- };
21893
- element.addEventListener("dragstart", onDragStart);
21894
- undo.push(() => {
21895
- element.removeEventListener("dragstart", onDragStart);
21896
- });
21897
22093
 
21898
22094
  // A touch this element may take has to be refusable before the finger
21899
22095
  // lands, or the browser can cancel an established swipe mid-gesture by
@@ -22928,9 +23124,10 @@ const isMac = detectMac();
22928
23124
  * place rather than about this box.
22929
23125
  *
22930
23126
  * `data-pan-after-hold="kept"` asks for that wait once rather than before every
22931
- * pan: the surface given the hand keeps it — panning on contact, like the same
22932
- * plan opened full screen — and asks again after a press has landed away from
22933
- * it, which is the hand saying it has moved on. What answers "away" is the same
23127
+ * pan: the surface a finger has given the hand keeps it — panning on contact,
23128
+ * like the same plan opened full screen — and asks again after a press has
23129
+ * landed away from it, which is the hand saying it has moved on. A mouse never
23130
+ * owed the wait, so it neither spends it nor marks it. What answers "away" is the same
22934
23131
  * thing that closes a popup, and the surface is the one that watches for it, so
22935
23132
  * an application never keeps a `pointerdown` listener on the window to know.
22936
23133
  * `[data-hand-kept]` is on the element for as long as it holds the hand that
@@ -24052,7 +24249,8 @@ const isSameKey = (browserEventKey, key) => {
24052
24249
  * and that field is the control the declaration speaks for: an ArrowUp declared
24053
24250
  * on a text field is the field saying what ArrowUp does there, not a container
24054
24251
  * taking the caret from a field inside it. A field inside a box that declares
24055
- * keys for something else (a row, a list) keeps every key it has a use for.
24252
+ * keys for something else (a row, a list) keeps every key it has a use for,
24253
+ * even when it is the only control in that box.
24056
24254
  *
24057
24255
  * A declared key that matches takes the key: its default does not happen as
24058
24256
  * well, whether the gate lets the interaction through or refuses it and says
@@ -24078,7 +24276,10 @@ defineInteractionDetector({
24078
24276
  };
24079
24277
  }
24080
24278
  const onKeyDown = createOnKeyDownForShortcuts(shortcuts, {
24081
- ownsTarget: (target) => target === findNearestControlHost(element),
24279
+ // Only the control's own root speaks for its host: a box that wraps a
24280
+ // single control is reached by that control's gate, not handed its keys.
24281
+ ownsTarget: (target) =>
24282
+ isControlRoot(element) && target === findNearestControlHost(element),
24082
24283
  });
24083
24284
  element.addEventListener("keydown", onKeyDown);
24084
24285
  return () => {
@@ -28146,6 +28347,127 @@ const useForceRender = () => {
28146
28347
 
28147
28348
  // https://github.com/preactjs/preact/issues/4756
28148
28349
 
28350
+ /**
28351
+ * Reads the current state of an action and returns `[data, loading, error, dismissError]`.
28352
+ *
28353
+ * By default (`loading` and `error` not set) the component suspends on load
28354
+ * and throws on failure, delegating both states to the nearest `<Loading>` and
28355
+ * `<ErrorBoundary>` ancestors. Pass `loading: true` or `error: true` to handle
28356
+ * either state directly inside the component instead.
28357
+ *
28358
+ * Return value: `[data, loading, error, dismissError]`
28359
+ * - `data` — the action's last successful data, or `undefined` if it has never completed
28360
+ * - `loading` — `true` while the action is running (only when `loading: true` is passed)
28361
+ * - `error` — the Error thrown by the action (only when `error: true` is passed)
28362
+ * - `dismissError` — takes that error out of what this hook returns, nothing else (only when `error: true` is passed)
28363
+ *
28364
+ * Stale data is always returned: the previous `data` remains available while
28365
+ * the action re-runs and after a run has failed, so a screen that had something
28366
+ * to show never has it taken away. What is drawn over it — a loading indicator,
28367
+ * the error, nothing — is entirely up to the component.
28368
+ *
28369
+ * `loading` covers the delay of a debounced binding
28370
+ * (`bindParams(paramsSignal, { debounce })`, `actionRunEffect({ debounce })`)
28371
+ * as well as the run itself: from the moment the params move, what is on screen
28372
+ * is the previous answer, and a screen showing "searching…" while someone types
28373
+ * needs to know it. Suspending has nothing to wait on during that delay, so
28374
+ * only `loading: true` reads it — the default keeps showing the previous answer
28375
+ * until the run really starts.
28376
+ *
28377
+ * When `loading` is not set (default), the component suspends until data is
28378
+ * ready, so `data` is always defined when the component renders and `loading`
28379
+ * is always `false`. With `loading: true` the component never suspends: every
28380
+ * state — running, settling, params nobody has started yet, a failure whose
28381
+ * message was dismissed — comes back as a value, so the subtree it draws is
28382
+ * never swapped for the boundary above and remounted.
28383
+ *
28384
+ * When `error` is not set (default), any action failure causes the component
28385
+ * to throw, so `error` is always `undefined` when the component renders. With
28386
+ * `error: true` the failure arrives beside the data the action already had — a
28387
+ * message on the content, not one in place of it. `dismissError()` removes the
28388
+ * message and nothing else: nothing is asked again, and what comes back is the
28389
+ * same state minus the error — the same data, or none at all when nothing ever
28390
+ * came back.
28391
+ *
28392
+ * `data === undefined` says there is nothing to show and `loading` says whether
28393
+ * anything is on its way: a skeleton reads both, or it claims a load nobody
28394
+ * started (see docs/data_states.md).
28395
+ *
28396
+ * @param {object | (() => Promise<any>) | Promise<any>} promiseOrAction -
28397
+ * an action, or the request this component owns written as a function:
28398
+ * made into an action once, on the first render, and kept for the life of
28399
+ * the instance — so it may be written inline — with what it resolves to as
28400
+ * the data and `run` implied. Code loaded on demand reads this way
28401
+ * (`() => import("./plan.jsx").then((m) => m.Plan)`, see
28402
+ * docs/dynamic_import.md), as does anything else asked for once per mount.
28403
+ * @param {{ loading?: true, error?: true, run?: true, prerun?: true, onLoad?: (data: any, context: {params: any}) => void }} [options]
28404
+ * @param {true} [options.run] - The fallback for data nothing else can ask for:
28405
+ * this component owns the request and starts it, from the render that reads
28406
+ * it. `loading` and `error` keep their meaning — delegated by default,
28407
+ * handled inline when asked for — so a run started here suspends into
28408
+ * `<Loading>` like any other.
28409
+ *
28410
+ * A failure is shown, not asked again: a render that retried it would retry
28411
+ * it in a loop while the network is down — that is `rerun()`'s job. Except a
28412
+ * failure a prerun left (a `prerun: true` head start, a press that preran
28413
+ * it): nothing on screen asked for that run, so this asks again, once.
28414
+ *
28415
+ * Prefer a `routeAction` wherever the parameter is one the address holds (a
28416
+ * path param, a search param bound to a `stateSignal`), popup included: it is
28417
+ * asked for when the address changes, with everything else that address
28418
+ * needs, whereas this one cannot start before the component that draws it
28419
+ * exists — one render late, and behind whatever gesture mounted it. What is
28420
+ * left for `run` is the parameter chosen inside the component and dying with
28421
+ * it — and any run that WRITES, whatever its parameter: an address asks again
28422
+ * on every arrival, which is right for a read and wrong for a write (see
28423
+ * docs/actions.md and docs/popup_open.md).
28424
+ * @param {true} [options.prerun] - A head start: this component starts a
28425
+ * read that another one, mounted later, owns with `run: true` — the read
28426
+ * leaves as soon as its params are known instead of once the owner exists.
28427
+ * Started from the render like `run`, as a prerun: it draws none of it, so
28428
+ * nothing suspends (no `<Loading>` needed) and a failure is neither thrown
28429
+ * nor reported — the owner asks again. A head start still running when the
28430
+ * owner reads it is the owner's wait.
28431
+ * @param {(data: any, context: {params: any}) => void} [options.onLoad] - what
28432
+ * this screen does with the data ONCE, when it becomes known: seed the fields
28433
+ * someone is about to edit, remember where a list was, focus something.
28434
+ *
28435
+ * Fired once per set of params — never again for a rerun that brings the same
28436
+ * thing back. That is the whole point of it: a save, a refresh, a poll all
28437
+ * hand the data over again, and copying it a second time would overwrite what
28438
+ * the person is in the middle of writing. The action knows what it ran with,
28439
+ * so nobody has to guess the dependency.
28440
+ *
28441
+ * Called from a layout effect, so what it writes belongs to the same tick as
28442
+ * the render that got the data — a `<Form pristineKey>` taking its reference
28443
+ * then sees the filled fields rather than the empty ones.
28444
+ * @returns {[data: unknown, loading: boolean, error: Error | undefined, dismissError: (() => void) | undefined]}
28445
+ *
28446
+ * @example <caption>Default — delegate both states (least code)</caption>
28447
+ * const [user] = useAsyncData(userAction);
28448
+ * // user is always defined here — never loading, never error
28449
+ *
28450
+ * @example <caption>Handle loading inline while keeping stale data</caption>
28451
+ * const [user, loading] = useAsyncData(userAction, { loading: true });
28452
+ * // First load: user === undefined, loading === true → show skeleton
28453
+ * // While refreshing: user === <stale>, loading === true → show stale + spinner
28454
+ * // Done: user === <fresh>, loading === false
28455
+ *
28456
+ * @example <caption>Handle error inline</caption>
28457
+ * const [user, , error] = useAsyncData(userAction, { error: true });
28458
+ * if (error) return <ErrorCard message={error.message} />;
28459
+ *
28460
+ * @example <caption>Say the failure over the rows already on screen</caption>
28461
+ * const [items, , error, dismissError] = useAsyncData(listAction, { error: true });
28462
+ * // First failure: items === undefined → only the message
28463
+ * // Failed refresh: items === <stale> → the message above the rows
28464
+ * return (
28465
+ * <>
28466
+ * {error && <ErrorStrip error={error} onClose={dismissError} />}
28467
+ * {items && <List items={items} />}
28468
+ * </>
28469
+ * );
28470
+ */
28149
28471
  const useAsyncData = (promiseOrAction, {
28150
28472
  loading = "delegate",
28151
28473
  error = "delegate",
@@ -28588,7 +28910,7 @@ const LoadingFallback = ({
28588
28910
  * ones that would render fine.
28589
28911
  *
28590
28912
  * @param {object} props
28591
- * @param {Function|import("ignore:preact").VNode} [props.fallback] - what is displayed
28913
+ * @param {Function|import("preact").VNode} [props.fallback] - what is displayed
28592
28914
  * instead of the children: an element, or a component receiving
28593
28915
  * `{ error, resetError }`. Without it the boundary is transparent.
28594
28916
  * @param {() => void} [props.onReset] - called when the fallback dismisses the
@@ -28947,6 +29269,12 @@ const hasWidthHeight = (context) => {
28947
29269
  (context.styles.height || context.remainingProps.height)
28948
29270
  );
28949
29271
  };
29272
+ // The `stretch` sizing keyword: the containing block's width minus the box's
29273
+ // own margins, so a box with a margin fills its row without leaving it (100%
29274
+ // adds the margins on top). Spelled with the prefix because an inline style
29275
+ // holds one value, and this is the one name Chromium, WebKit and Firefox all
29276
+ // read (Firefox 155 has no unprefixed `stretch`).
29277
+ const FILL_AVAILABLE_WIDTH = "-webkit-fill-available";
28950
29278
  const DIMENSION_PROPS = {
28951
29279
  boxSizing: PASS_THROUGH,
28952
29280
  width: PASS_THROUGH,
@@ -28999,16 +29327,16 @@ const DIMENSION_PROPS = {
28999
29327
  return { flexGrow: expandWeight(value), flexBasis: "0%" };
29000
29328
  }
29001
29329
  if (parentBoxFlow === "flex-y" || parentBoxFlow === "inline-flex-y") {
29002
- // width 100% is what fills the cross axis; align-self stretch would do
29330
+ // A width is what fills the cross axis; align-self stretch would do
29003
29331
  // the same but takes the parent's alignX away — a maxWidth-capped item
29004
29332
  // in an alignX="center" parent would then sit at the start instead of
29005
29333
  // in the middle.
29006
29334
  return {
29007
- width: "100%",
29335
+ width: FILL_AVAILABLE_WIDTH,
29008
29336
  };
29009
29337
  }
29010
29338
  // Can't use flexGrow — parent is not flex-x
29011
- return { width: "100%" };
29339
+ return { width: FILL_AVAILABLE_WIDTH };
29012
29340
  },
29013
29341
  expandY: (value, { parentBoxFlow }) => {
29014
29342
  if (!value) {
@@ -29569,6 +29897,7 @@ const getVisualChildStylePropStrategy = (name) => {
29569
29897
  };
29570
29898
 
29571
29899
  const isStyleProp = (name) => STYLE_PROP_NAME_SET.has(name);
29900
+ const isTypoProp = (name) => TYPO_PROP_NAME_SET.has(name);
29572
29901
 
29573
29902
  const getStringifier = (key) => {
29574
29903
  if (SPACING_PROP_SET.has(key)) {
@@ -30210,10 +30539,10 @@ const readCSSVarName = entry => Array.isArray(entry) ? entry[0] : entry;
30210
30539
  const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
30211
30540
 
30212
30541
  /**
30213
- * @type {import("ignore:preact").FunctionComponent<{
30542
+ * @type {import("preact").FunctionComponent<{
30214
30543
  * as?: string,
30215
30544
  * className?: string,
30216
- * style?: import("ignore:preact").JSX.CSSProperties & { [pseudo: string]: import("ignore:preact").JSX.CSSProperties },
30545
+ * style?: import("preact").JSX.CSSProperties & { [pseudo: string]: import("preact").JSX.CSSProperties },
30217
30546
  * styleCSSVars?: { [propName: string]: string | [string, string] },
30218
30547
  * inline?: boolean,
30219
30548
  * block?: boolean,
@@ -30228,7 +30557,7 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
30228
30557
  * hasChildUsingForwardedProps?: boolean,
30229
30558
  * childPropSet?: Set<string>,
30230
30559
  * preventInitialTransition?: boolean,
30231
- * separator?: import("ignore:preact").ComponentChildren | ((index: number) => import("ignore:preact").ComponentChildren),
30560
+ * separator?: import("preact").ComponentChildren | ((index: number) => import("preact").ComponentChildren),
30232
30561
  * selfInteractions?: string,
30233
30562
  * interactions?: { [type: string]: "request_action" | "request_ui_action" | "refuse" | ((event: Event) => void) | false | null | undefined },
30234
30563
  * alignX?: string,
@@ -30240,7 +30569,7 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
30240
30569
  * body?: boolean,
30241
30570
  * sticky?: boolean,
30242
30571
  * mount?: "always" | "after-paint",
30243
- * children?: import("ignore:preact").ComponentChildren,
30572
+ * children?: import("preact").ComponentChildren,
30244
30573
  * [key: string]: any,
30245
30574
  * }>}
30246
30575
  * @param {"x"|"y"|boolean} [flex] A flex container: `"x"` (or `true`) side by
@@ -32795,7 +33124,7 @@ const debug$1 = (...args) => {
32795
33124
  * matches. `routeFallback()` gives that page a name, which is what lets a
32796
33125
  * movement be written about it and its `matchingSignal` be read from outside
32797
33126
  * the tree (see route_fallback.js).
32798
- * @param {Function|import("ignore:preact").VNode} [props.element] - what the branch renders
33127
+ * @param {Function|import("preact").VNode} [props.element] - what the branch renders
32799
33128
  * @param {object} [props.elementProps] - props given to `element`
32800
33129
  *
32801
33130
  * A branch says what it renders, not what happens when it cannot: loading and
@@ -35144,7 +35473,7 @@ const css$13 = /* css */`@property --navi-route-transition-leave {
35144
35473
  * that. An application that already has an element holding its pages can mark
35145
35474
  * that one with `data-navi-route-transition-area` rather than nesting another.
35146
35475
  *
35147
- * @type {import("ignore:preact").FunctionComponent<{ children?: any, [key: string]: any }>}
35476
+ * @type {import("preact").FunctionComponent<{ children?: any, [key: string]: any }>}
35148
35477
  */
35149
35478
  const RouteTransitionArea = ({
35150
35479
  children,
@@ -35934,7 +36263,7 @@ const beginTransition = ({
35934
36263
  warnAboutNamesEscapingArea(areaElement, capturedNames);
35935
36264
  return;
35936
36265
  }
35937
- for (const name of capturedNames) {
36266
+ for (const name of capturedNames.keys()) {
35938
36267
  if (name === "root") {
35939
36268
  continue;
35940
36269
  }
@@ -36137,9 +36466,10 @@ const ignoreSkipped$1 = () => {};
36137
36466
 
36138
36467
  // The names the browser captured, read off the pictures themselves: what was
36139
36468
  // asked for in CSS and what was taken are not the same question (see
36140
- // viewTransitionReady).
36469
+ // viewTransitionReady). Each with the states it was taken in: a name taken in
36470
+ // only one of them pairs with nothing.
36141
36471
  const capturedViewTransitionNames = () => {
36142
- const names = new Set();
36472
+ const names = new Map();
36143
36473
  for (const animation of document.getAnimations()) {
36144
36474
  const pseudoElement = animation.effect?.pseudoElement;
36145
36475
  if (!pseudoElement || !pseudoElement.startsWith("::view-transition")) {
@@ -36149,43 +36479,59 @@ const capturedViewTransitionNames = () => {
36149
36479
  if (nameStart === -1) {
36150
36480
  continue;
36151
36481
  }
36152
- names.add(pseudoElement.slice(nameStart + 1, -1));
36482
+ const name = pseudoElement.slice(nameStart + 1, -1);
36483
+ let states = names.get(name);
36484
+ if (!states) {
36485
+ states = {
36486
+ old: false,
36487
+ new: false
36488
+ };
36489
+ names.set(name, states);
36490
+ }
36491
+ if (pseudoElement.startsWith("::view-transition-old(")) {
36492
+ states.old = true;
36493
+ } else if (pseudoElement.startsWith("::view-transition-new(")) {
36494
+ states.new = true;
36495
+ }
36153
36496
  }
36154
36497
  return names;
36155
36498
  };
36156
36499
 
36157
- // Nested groups keep a name written inside the area inside its picture (see
36158
- // the @supports block in the CSS above). Without them the name escapes to the
36159
- // top of the ::view-transition tree and the element it belongs to stands still,
36160
- // fading on its own, while the pages move under it.
36500
+ // A name written inside the area is a group of its own: it stands where it was
36501
+ // captured while the area's two pictures carry the movement, in every browser.
36502
+ // Nested groups only keep it cut at the pages' edge (see the @supports block in
36503
+ // the CSS above). Paired with the same name on the other page it is a morph;
36504
+ // taken in one state only it pairs with nothing, and is a piece of its page
36505
+ // left behind, fading, while that page moves.
36161
36506
  const NESTED_GROUPS_SUPPORTED = window.CSS.supports("view-transition-group", "contain");
36162
36507
  const warnAboutNamesEscapingArea = (areaElement, capturedNames) => {
36163
- if (NESTED_GROUPS_SUPPORTED) {
36164
- return;
36165
- }
36166
- let escapedName = null;
36167
- for (const name of capturedNames) {
36508
+ const unpairedNames = new Set();
36509
+ for (const [name, states] of capturedNames) {
36168
36510
  if (name === "root" || name === AREA_NAME || name.startsWith(FURNITURE_NAME_PREFIX)) {
36169
36511
  continue;
36170
36512
  }
36171
- escapedName = name;
36172
- break;
36513
+ if (states.old && states.new) {
36514
+ continue;
36515
+ }
36516
+ unpairedNames.add(name);
36173
36517
  }
36174
36518
  // A name captured next to the area is not necessarily inside it — a bar the
36175
36519
  // application animates on the same clock is named on purpose. The subtree is
36176
36520
  // walked only once something is there to find, so the common case reads
36177
- // nothing.
36178
- if (!escapedName) {
36521
+ // nothing. The page being left is still in it, hidden (keepLeavingPages in
36522
+ // route.jsx), so a name taken only in the old state is found too.
36523
+ if (unpairedNames.size === 0) {
36179
36524
  return;
36180
36525
  }
36181
36526
  for (const descendant of areaElement.querySelectorAll("*")) {
36182
36527
  const {
36183
36528
  viewTransitionName
36184
36529
  } = getComputedStyle(descendant);
36185
- if (!viewTransitionName || viewTransitionName === "none") {
36530
+ if (!unpairedNames.has(viewTransitionName)) {
36186
36531
  continue;
36187
36532
  }
36188
- 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 }).`);
36533
+ 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.";
36534
+ 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).`);
36189
36535
  return;
36190
36536
  }
36191
36537
  };
@@ -36330,6 +36676,8 @@ const installRouteTransitionCss = () => {
36330
36676
  };
36331
36677
 
36332
36678
  installImportMetaCssBuild(import.meta);
36679
+ // The pictures carrying a travel, among everything else the movement takes
36680
+ // along (see findLeadAnimation).
36333
36681
  const TRAVEL_LEAD_NAMES = ["navi-route-travel"];
36334
36682
 
36335
36683
  // A browser with no view transitions of its own has no picture of the page
@@ -36597,7 +36945,7 @@ const css$12 = /* css */`.navi_route_travel {
36597
36945
  `;
36598
36946
 
36599
36947
  /**
36600
- * @type {import("ignore:preact").FunctionComponent<{
36948
+ * @type {import("preact").FunctionComponent<{
36601
36949
  * routes?: Array<object|{route: object, params?: object}>,
36602
36950
  * axis?: "x"|"y",
36603
36951
  * travelByDrag?: boolean,
@@ -39374,7 +39722,7 @@ const resetCommand = (source, event) => {
39374
39722
  // answers: "save" stays, "delete" goes back to the list. Same shape as the
39375
39723
  // browser's own formaction/formmethod, and the same shape the action already
39376
39724
  // has (it is told which button requested it).
39377
- const resolveAfterSend = (target, requester) => {
39725
+ const resolveAfterSend = (target, requester, surface) => {
39378
39726
  const askedForByRequester = requester?.getAttribute?.("data-after-send");
39379
39727
  if (askedForByRequester) {
39380
39728
  return askedForByRequester;
@@ -39383,11 +39731,6 @@ const resolveAfterSend = (target, requester) => {
39383
39731
  if (askedFor) {
39384
39732
  return askedFor;
39385
39733
  }
39386
- // From above the target: a popup that IS the send target is handled on its
39387
- // own (see the aria-expanded branch below), and must not answer twice.
39388
- const surface = target.parentElement?.closest(
39389
- `[data-slide], [aria-expanded]`,
39390
- );
39391
39734
  if (!surface) {
39392
39735
  return undefined;
39393
39736
  }
@@ -39399,6 +39742,11 @@ const resolveAfterSend = (target, requester) => {
39399
39742
  }
39400
39743
  return undefined;
39401
39744
  };
39745
+ // The surface a send answers when the control says nothing. Found from above
39746
+ // the target: a popup that IS the send target is handled on its own (see the
39747
+ // aria-expanded branch in sendCommand), and must not answer twice.
39748
+ const resolveSendSurface = (target) =>
39749
+ target.parentElement?.closest(`[data-slide], [aria-expanded]`);
39402
39750
  // What follows a send is caused by the send, not by the gesture: the send
39403
39751
  // answered the gesture and may have prevented it (an Enter must not also click
39404
39752
  // the submit button, nor activate the trigger of the popup it closed), and a
@@ -39480,20 +39828,30 @@ const sendCommand = (source, event, { requester }) => {
39480
39828
  // and the popup must stay open — with the form still in front of the
39481
39829
  // user, showing what it is waiting for.
39482
39830
  let invalid = false;
39483
- // What follows the send is read at the moment it runs, never before it:
39484
- // it depends on the requester (Enter in a field sends through the first
39485
- // submit button, and what follows is that button's answer), and on
39486
- // anything the send itself decided — an action that learned where to go
39487
- // from the response writes it on the form while it runs
39488
- // (data-after-send), and this is what picks it up.
39831
+ // What follows the send is aimed at the press, like a button's command
39832
+ // waiting for its action (see resolveNaviCommand): the send may take the
39833
+ // form out of the document (a form keyed on what it edits, a row that
39834
+ // moves once saved), and resolved from there it finds no popup to close.
39835
+ // Which command it is gets read again once the send went through: an
39836
+ // action that learned where to go from the response writes it on the
39837
+ // form while it runs (data-after-send). That one can only be aimed when
39838
+ // it is read.
39839
+ const surface = resolveSendSurface(target);
39840
+ const sentEvent = createSentEvent(event);
39841
+ const afterSendAtPress = resolveAfterSend(target, sendRequester, surface);
39842
+ const runAimedAtPress = afterSendAtPress
39843
+ ? resolveNaviCommand(source, afterSendAtPress, sentEvent)
39844
+ : undefined;
39489
39845
  const runAfterSend = () => {
39490
- const afterSend = resolveAfterSend(target, sendRequester);
39846
+ const afterSend = resolveAfterSend(target, sendRequester, surface);
39491
39847
  if (!afterSend) {
39492
39848
  return;
39493
39849
  }
39494
- triggerNaviCommand(source, afterSend, createSentEvent(event), {
39495
- optional: true,
39496
- });
39850
+ if (afterSend === afterSendAtPress) {
39851
+ runAimedAtPress?.();
39852
+ return;
39853
+ }
39854
+ triggerNaviCommand(source, afterSend, sentEvent);
39497
39855
  };
39498
39856
  const {
39499
39857
  result: sent,
@@ -40432,6 +40790,12 @@ const isDeclaredInsidePopup = (target, popupId) =>
40432
40790
  * Lives on the popup's own content element rather than in the backdrop's
40433
40791
  * handler, for both renderers: the backdrop is a sibling behind the popup
40434
40792
  * (never an ancestor), so a press inside the popup's box never reaches it.
40793
+ *
40794
+ * Inside a box that travels by drag — a row of slides walked by a swipe, a
40795
+ * sheet pushed back towards its edge — the region is also where that travel
40796
+ * starts, and at the mousedown nobody knows yet which of the two the press is.
40797
+ * The dismissal then waits for the click, which only comes if the press stayed
40798
+ * a press: a travel swallows the one it leaves behind.
40435
40799
  */
40436
40800
  const handlePressOnOutsideRegion = (
40437
40801
  mouseDownEvent,
@@ -40462,11 +40826,36 @@ const handlePressOnOutsideRegion = (
40462
40826
  mouseDownEvent.preventDefault();
40463
40827
  return;
40464
40828
  }
40465
- if (pressOutside === "close" || pressOutside === "cancel") {
40466
- openController.requestClose(mouseDownEvent, {
40467
- isCancel: pressOutside === "cancel",
40829
+ if (pressOutside !== "close" && pressOutside !== "cancel") {
40830
+ return;
40831
+ }
40832
+ const isCancel = pressOutside === "cancel";
40833
+ if (isPressDisputedByDrag(target)) {
40834
+ onClickOfThisPress(target, (clickEvent) => {
40835
+ openController.requestClose(clickEvent, { isCancel });
40468
40836
  });
40837
+ return;
40469
40838
  }
40839
+ openController.requestClose(mouseDownEvent, { isCancel });
40840
+ };
40841
+
40842
+ // The click ending the press whose mousedown landed on `element`. A click goes
40843
+ // to what the press and the release have in common, so it reaches `element`
40844
+ // only when the release stayed on it or inside it. A press that never clicks
40845
+ // leaves it armed until the next one, and no longer — a click always comes
40846
+ // after a press, so it cannot reach the click of another (the rule
40847
+ // click_suppression.js in @jsenv/dom goes by).
40848
+ const onClickOfThisPress = (element, callback) => {
40849
+ const onClick = (clickEvent) => {
40850
+ disarm();
40851
+ callback(clickEvent);
40852
+ };
40853
+ const disarm = () => {
40854
+ element.removeEventListener("click", onClick);
40855
+ window.removeEventListener("pointerdown", disarm, { capture: true });
40856
+ };
40857
+ element.addEventListener("click", onClick);
40858
+ window.addEventListener("pointerdown", disarm, { capture: true });
40470
40859
  };
40471
40860
 
40472
40861
  /**
@@ -45674,6 +46063,9 @@ const uiStateHoldsNothing = (uiState) => {
45674
46063
  * handles the timing centrally rather than each component having to manage its
45675
46064
  * own debounce logic.
45676
46065
  */
46066
+ // Sentinel used as the initial value of lastActionValueRef.
46067
+ // Distinct from undefined so that undefined (e.g. unchecked radio) can itself
46068
+ // be stored as a valid "last action value" and trigger the dedup logic.
45677
46069
  const NO_ACTION_YET = Symbol("no_action_yet");
45678
46070
 
45679
46071
  // The control types rendered as an element `<label for>` can name: HTML's
@@ -45773,13 +46165,18 @@ const LISTENERS_CALLED_BY_REACTIONS = new Set(["onMouseDown", "onClick", "onKeyD
45773
46165
  * the control opens, so that a listener hears the control as a whole.
45774
46166
  * `onMouseDown`, `onClick`, `onKeyDown`, `onPaste` and `onInput` stay the
45775
46167
  * host's either way: the control's own reactions to them call them.
46168
+ * @param {"host"|"root"} [options.nameOn] Where `aria-label` and
46169
+ * `aria-labelledby` land: where the role is. `"host"`, the default, for every
46170
+ * control whose host is what is announced; `"root"` for a control whose role
46171
+ * is on its root and whose host is an `aria-hidden` carrier (a wheel).
45776
46172
  * @returns {[controlRootProps, controlHostProps, { uiStateController }]}
45777
46173
  */
45778
46174
  const useControlProps = (props, {
45779
46175
  controlType,
45780
46176
  persists,
45781
46177
  uiActionInternal,
45782
- listenersOn = "host"
46178
+ listenersOn = "host",
46179
+ nameOn = "host"
45783
46180
  }) => {
45784
46181
  const debugUIState = useDebugUIState();
45785
46182
 
@@ -45867,6 +46264,7 @@ const useControlProps = (props, {
45867
46264
  boundAction,
45868
46265
  controlInfo,
45869
46266
  listenersOn,
46267
+ nameOn,
45870
46268
  listenersCalledByReactions: LISTENERS_CALLED_BY_REACTIONS
45871
46269
  });
45872
46270
  {
@@ -47095,6 +47493,7 @@ const useInteractiveProps = (props, {
47095
47493
  boundAction,
47096
47494
  controlInfo,
47097
47495
  listenersOn,
47496
+ nameOn,
47098
47497
  listenersCalledByReactions
47099
47498
  }) => {
47100
47499
  const {
@@ -47102,6 +47501,7 @@ const useInteractiveProps = (props, {
47102
47501
  } = props;
47103
47502
  const [controlRootProps, controlHostProps] = splitControlProps(props, {
47104
47503
  listenersOn,
47504
+ nameOn,
47105
47505
  listenersCalledByReactions
47106
47506
  });
47107
47507
  controlRootProps["navi-control"] = controlInfo.controlType;
@@ -47713,8 +48113,10 @@ const useInteractiveProps = (props, {
47713
48113
  }
47714
48114
  return [controlRootProps, controlHostProps];
47715
48115
  };
48116
+ const NAME_ATTRIBUTE_SET = new Set(["aria-label", "aria-labelledby"]);
47716
48117
  const splitControlProps = (props, {
47717
48118
  listenersOn,
48119
+ nameOn,
47718
48120
  listenersCalledByReactions
47719
48121
  }) => {
47720
48122
  const {
@@ -47725,6 +48127,10 @@ const splitControlProps = (props, {
47725
48127
  };
47726
48128
  const controlRootProps = {};
47727
48129
  for (const key of Object.keys(props)) {
48130
+ if (nameOn === "root" && NAME_ATTRIBUTE_SET.has(key)) {
48131
+ controlRootProps[key] = props[key];
48132
+ continue;
48133
+ }
47728
48134
  const attributeName = controlAttributeFromProp(key);
47729
48135
  if (attributeName) {
47730
48136
  controlHostProps[attributeName] = props[key];
@@ -49854,7 +50260,7 @@ const css$$ = /* css */`.navi_text_anchor {
49854
50260
  * @param {{ size?: number, verticalAlign?: string }} [lineLayout]
49855
50261
  * Describes the surrounding line context. Used as layout-effect dependencies so the correction
49856
50262
  * reruns when the surrounding text's font-size or vertical-align changes.
49857
- * @param {import("ignore:preact").RefObject} childRef — ref on the child element to reposition
50263
+ * @param {import("preact").RefObject} childRef — ref on the child element to reposition
49858
50264
  */
49859
50265
  const TextAnchor = ({
49860
50266
  childRef,
@@ -50516,25 +50922,25 @@ const shouldInjectSpacingBetween = (left, right) => {
50516
50922
  * See `docs/typography.md` for the decisions behind it — truncating, rows made
50517
50923
  * of an icon, a text and an icon, and where a line may break.
50518
50924
  *
50519
- * @type {import("ignore:preact").FunctionComponent<{
50520
- * children?: import("ignore:preact").ComponentChildren,
50925
+ * @type {import("preact").FunctionComponent<{
50926
+ * children?: import("preact").ComponentChildren,
50521
50927
  * as?: string,
50522
50928
  * className?: string,
50523
- * style?: import("ignore:preact").JSX.CSSProperties,
50929
+ * style?: import("preact").JSX.CSSProperties,
50524
50930
  * bold?: boolean,
50525
50931
  * noWrap?: boolean,
50526
50932
  * maxLines?: number,
50527
- * spacing?: string | number | import("ignore:preact").ComponentChildren,
50933
+ * spacing?: string | number | import("preact").ComponentChildren,
50528
50934
  * loading?: boolean,
50529
50935
  * skeleton?: boolean,
50530
50936
  * attachLastChild?: boolean,
50531
50937
  * preventSpaceUnderlines?: boolean,
50532
- * holdSpaceForStyle?: import("ignore:preact").JSX.CSSProperties,
50938
+ * holdSpaceForStyle?: import("preact").JSX.CSSProperties,
50533
50939
  * boldStable?: boolean,
50534
50940
  * shrinkWrap?: boolean,
50535
50941
  * capitalize?: boolean,
50536
50942
  * selectRange?: string | [number, number] | [string, string],
50537
- * childrenOutsideFlow?: import("ignore:preact").ComponentChildren,
50943
+ * childrenOutsideFlow?: import("preact").ComponentChildren,
50538
50944
  * [key: string]: any,
50539
50945
  * }>}
50540
50946
  *
@@ -50575,7 +50981,7 @@ const shouldInjectSpacingBetween = (left, right) => {
50575
50981
  * Replaces real space characters between children with padding-based spaces.
50576
50982
  * Useful inside `<a>` elements where browsers draw an underline under spaces.
50577
50983
  *
50578
- * @param {import("ignore:preact").JSX.CSSProperties} [holdSpaceForStyle]
50984
+ * @param {import("preact").JSX.CSSProperties} [holdSpaceForStyle]
50579
50985
  * Prevents layout shifts when text styles change (e.g. font-weight, font-size).
50580
50986
  * Pass the CSS properties representing the "largest" visual state of the text.
50581
50987
  * An invisible placeholder rendered with those styles reserves the space; the
@@ -50608,7 +51014,7 @@ const shouldInjectSpacingBetween = (left, right) => {
50608
51014
  * `[start, end]` character-offset tuple, or a `[startText, endText]` pair of
50609
51015
  * substrings selecting from the first to the end of the second.
50610
51016
  *
50611
- * @param {import("ignore:preact").ComponentChildren} [childrenOutsideFlow]
51017
+ * @param {import("preact").ComponentChildren} [childrenOutsideFlow]
50612
51018
  * Rendered after children but outside the text spacing/flow logic. Used
50613
51019
  * internally for overlays such as the skeleton container.
50614
51020
  */
@@ -50896,7 +51302,7 @@ const TextWithSelectRange = ({
50896
51302
  * @param {object} props
50897
51303
  * @param {string} [props.href] - URL/id of an external SVG symbol, rendered via
50898
51304
  * `<svg><use href></svg>`. Mutually exclusive with meaningful `children`.
50899
- * @param {import("ignore:preact").ComponentChildren} [props.children] - Inline icon
51305
+ * @param {import("preact").ComponentChildren} [props.children] - Inline icon
50900
51306
  * content: an `<svg>` element, or a string (renders as a text/emoji icon).
50901
51307
  * @param {boolean} [props.decorative] - Marks the icon `aria-hidden`. Defaults
50902
51308
  * to `true` for an unsized, non-interactive icon; pass `false` for a
@@ -51071,6 +51477,11 @@ const usePreloadOnIntent = (ref, href, prefetch = true) => {
51071
51477
  return undefined;
51072
51478
  }
51073
51479
  const preload = () => {
51480
+ // The tab or the window it opens loads its own code: what would be
51481
+ // fetched here is not what runs there.
51482
+ if (linkOpensElsewhere(element)) {
51483
+ return;
51484
+ }
51074
51485
  preloadUrl(href);
51075
51486
  };
51076
51487
  element.addEventListener("pointerenter", preload);
@@ -51279,6 +51690,15 @@ const useDimColorWhen = (elementRef, shouldDim) => {
51279
51690
  };
51280
51691
 
51281
51692
  installImportMetaCssBuild(import.meta);
51693
+ /*
51694
+ * Apply opacity to child content, not the link element itself.
51695
+ *
51696
+ * Why not apply opacity directly to .navi_link?
51697
+ * - Would make focus outlines semi-transparent too (accessibility issue)
51698
+ * - We want dimmed text but full-opacity focus indicators for visibility
51699
+ *
51700
+ * This approach dims the content while preserving focus outline visibility.
51701
+ */
51282
51702
  const css$Z = /* css */`@layer navi {
51283
51703
  .navi_link {
51284
51704
  --link-border-radius: unset;
@@ -51299,6 +51719,19 @@ const css$Z = /* css */`@layer navi {
51299
51719
  --link-current-indicator-spacing: 0;
51300
51720
  --link-current-indicator-color: var(--navi-link-current-indicator-color);
51301
51721
  }
51722
+
51723
+ .navi_link[data-variant="text"], .navi_link[data-variant="icon"] {
51724
+ --link-color: unset;
51725
+ --link-text-decoration: none;
51726
+ }
51727
+
51728
+ .navi_link[data-variant="tab"] {
51729
+ --link-background-hover: color-mix(in srgb,
51730
+ var(--link-background, transparent),
51731
+ var(--x-link-contrasting-color) 15%);
51732
+ --link-color: unset;
51733
+ --link-text-decoration: none;
51734
+ }
51302
51735
  }
51303
51736
 
51304
51737
  .navi_link {
@@ -51319,7 +51752,6 @@ const css$Z = /* css */`@layer navi {
51319
51752
  --x-link-padding-right: var(--link-padding-right, var(--link-padding-x, var(--link-padding, 0px)));
51320
51753
  --x-link-padding-bottom: var(--link-padding-bottom, var(--link-padding-y, var(--link-padding, 0px)));
51321
51754
  --x-link-padding-left: var(--link-padding-left, var(--link-padding-x, var(--link-padding, 0px)));
51322
- aspect-ratio: inherit;
51323
51755
  padding-top: var(--x-link-padding-top);
51324
51756
  padding-right: var(--x-link-padding-right);
51325
51757
  padding-bottom: var(--x-link-padding-bottom);
@@ -51508,17 +51940,7 @@ const css$Z = /* css */`@layer navi {
51508
51940
  margin-left: -.1em;
51509
51941
  }
51510
51942
 
51511
- &[data-variant="text"], &[data-variant="icon"] {
51512
- --link-color: unset;
51513
- --link-text-decoration: none;
51514
- }
51515
-
51516
51943
  &[data-variant="tab"] {
51517
- --link-background-hover: color-mix(in srgb,
51518
- var(--link-background, transparent),
51519
- var(--x-link-contrasting-color) 15%);
51520
- --link-color: unset;
51521
- --link-text-decoration: none;
51522
51944
  white-space: nowrap;
51523
51945
  user-select: none;
51524
51946
 
@@ -51605,8 +52027,9 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
51605
52027
  *
51606
52028
  * Href-derived behavior (all computed from `href`, recomputed on navigation):
51607
52029
  * - internal vs external target (`data-href-internal`/`data-href-external`),
51608
- * with `target`/`rel` auto-defaulted (`_self` internal, `_blank` +
51609
- * `noopener noreferrer` external) unless explicitly set.
52030
+ * with `target`/`rel` auto-defaulted unless explicitly set: `_blank` for a
52031
+ * page of another site (`http:`/`https:`), `noopener noreferrer` for any
52032
+ * external address, nothing otherwise.
51610
52033
  * - "current page" detection (`data-href-current`, `aria-current="page"`).
51611
52034
  * - anchor links (`href` starting with `#`) get `data-href-anchor`.
51612
52035
  * - an automatic end icon for `tel:`/`sms:`/`mailto:`/`github.com`, external
@@ -51668,14 +52091,14 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
51668
52091
  * text reads (see `holdSpaceForStyle` in `text.jsx`).
51669
52092
  * @param {boolean} [props.currentEffectShadow] - Inset-shadow effect while
51670
52093
  * current (used with `variant="tab"`).
51671
- * @param {boolean|import("ignore:preact").ComponentChild} [props.startIcon] - Icon
52094
+ * @param {boolean|import("preact").ComponentChild} [props.startIcon] - Icon
51672
52095
  * placed before the text.
51673
- * @param {boolean|import("ignore:preact").ComponentChild} [props.endIcon] - Icon
52096
+ * @param {boolean|import("preact").ComponentChild} [props.endIcon] - Icon
51674
52097
  * placed after the text; when omitted, an icon may be auto-chosen from the
51675
52098
  * href (see above).
51676
- * @param {boolean|import("ignore:preact").ComponentChild} [props.blankTargetIcon] -
52099
+ * @param {boolean|import("preact").ComponentChild} [props.blankTargetIcon] -
51677
52100
  * Override/suppress the auto external-link icon.
51678
- * @param {boolean|import("ignore:preact").ComponentChild} [props.anchorIcon] -
52101
+ * @param {boolean|import("preact").ComponentChild} [props.anchorIcon] -
51679
52102
  * Controls the auto anchor icon shown for a childless anchor link:
51680
52103
  * `true`/`undefined` uses the default chain SVG, `false` suppresses it, any
51681
52104
  * other node replaces it. To render a plain `#` instead, prefer writing it as
@@ -51746,7 +52169,7 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
51746
52169
  * @param {boolean} [props.preventDefault] - Call `event.preventDefault()` on
51747
52170
  * click (navigation suppressed; `onClick` still runs).
51748
52171
  * @param {(event: MouseEvent) => void} [props.onClick]
51749
- * @param {import("ignore:preact").ComponentChildren} [props.children] - Link content;
52172
+ * @param {import("preact").ComponentChildren} [props.children] - Link content;
51750
52173
  * falls back to the route's relative URL / `href` per the rules above.
51751
52174
  * @param {string} [props.name] - Control facade name (links may be nameless).
51752
52175
  * @param {boolean} [props.disabled]
@@ -51875,7 +52298,7 @@ const LinkPlain = props => {
51875
52298
  ":-navi-href-current": innerCurrent,
51876
52299
  ":-navi-selected": selected
51877
52300
  };
51878
- const innerTarget = target === undefined ? isSameSite ? "_self" : "_blank" : target;
52301
+ const innerTarget = target === undefined ? getDefaultHrefTarget(href, documentUrlSignal.peek()) : target;
51879
52302
  const innerRel = rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel;
51880
52303
  let innerEndIcon;
51881
52304
  if (endIcon === undefined) {
@@ -52042,7 +52465,7 @@ const LinkPlain = props => {
52042
52465
  },
52043
52466
  href: href,
52044
52467
  rel: innerRel,
52045
- target: innerTarget === "_self" ? undefined : target
52468
+ target: innerTarget === "_self" ? undefined : innerTarget
52046
52469
  // Which slide this tab is, and which box to say it to: read by the <Nav>
52047
52470
  // around it to place the row's own bar, and by the command above to find
52048
52471
  // the container across the document.
@@ -52294,6 +52717,9 @@ const useSlideContainer = (target) => {
52294
52717
  };
52295
52718
 
52296
52719
  installImportMetaCssBuild(import.meta);
52720
+ // A name of its own for the bar under the current tab, one per nav on the page:
52721
+ // two bars answering to the same name are two elements claiming one picture,
52722
+ // and the browser refuses the whole transition rather than pick.
52297
52723
  let navCount = 0;
52298
52724
  // Worn by a nav of routes while a route movement between two of its tabs is
52299
52725
  // pictured (see markIndicatorTakesPart, and the CSS below for what it decides).
@@ -52318,6 +52744,7 @@ const rememberProgressAnimation = (navElement, travel) => {
52318
52744
  };
52319
52745
  const css$Y = /* css */`@layer navi {
52320
52746
  .navi_nav {
52747
+ --nav-width: fit-content;
52321
52748
  --nav-border: none;
52322
52749
  --nav-padding: 0px;
52323
52750
  --nav-border-radius: 0px;
@@ -52341,7 +52768,7 @@ const css$Y = /* css */`@layer navi {
52341
52768
  var(--nav-indicator-length-delta));
52342
52769
  --x-nav-indicator-color: var(--nav-indicator-color, var(--nav-current-indicator-color));
52343
52770
  --x-nav-indicator-color-toward: var(--nav-indicator-color-toward, var(--x-nav-indicator-color));
52344
- z-index: 1;
52771
+ z-index: 2;
52345
52772
  background: color-mix(in srgb,
52346
52773
  var(--x-nav-indicator-color-toward)
52347
52774
  calc(var(--slide-travel-progress) * var(--nav-indicator-toward-sign, 0) *
@@ -52398,7 +52825,7 @@ const css$Y = /* css */`@layer navi {
52398
52825
  }
52399
52826
 
52400
52827
  .navi_nav {
52401
- width: fit-content;
52828
+ width: var(--nav-width);
52402
52829
  padding-top: var(--nav-padding-top, var(--nav-padding-y, var(--nav-padding, unset)));
52403
52830
  padding-right: var(--nav-padding-right, var(--nav-padding-x, var(--nav-padding, unset)));
52404
52831
  padding-bottom: var(--nav-padding-bottom, var(--nav-padding-y, var(--nav-padding, unset)));
@@ -52427,12 +52854,12 @@ const css$Y = /* css */`@layer navi {
52427
52854
  --x-nav-child-border-radius: calc(var(--nav-border-radius) - var(--nav-padding));
52428
52855
  --x-nav-link-border-radius: var(--link-border-radius, var(--x-nav-child-border-radius));
52429
52856
 
52430
- &:first-child {
52857
+ &:nth-child(1 of .navi_link) {
52431
52858
  border-top-left-radius: var(--x-nav-link-border-radius);
52432
52859
  border-bottom-left-radius: var(--x-nav-link-border-radius);
52433
52860
  }
52434
52861
 
52435
- &:last-child {
52862
+ &:nth-last-child(1 of .navi_link) {
52436
52863
  border-top-right-radius: var(--x-nav-link-border-radius);
52437
52864
  border-bottom-right-radius: var(--x-nav-link-border-radius);
52438
52865
  }
@@ -52535,7 +52962,7 @@ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
52535
52962
  };
52536
52963
 
52537
52964
  /**
52538
- * @type {import("ignore:preact").FunctionComponent<{
52965
+ * @type {import("preact").FunctionComponent<{
52539
52966
  * currentIndicator?: boolean|"top"|"bottom"|"left"|"right",
52540
52967
  * currentIndicatorSlides?: boolean,
52541
52968
  * slideContainer?: string,
@@ -53685,7 +54112,7 @@ const TABS_ALIGN_TO_JUSTIFY_CONTENT = {
53685
54112
  };
53686
54113
 
53687
54114
  /**
53688
- * @type {import("ignore:preact").FunctionComponent<{
54115
+ * @type {import("preact").FunctionComponent<{
53689
54116
  * value?: any,
53690
54117
  * defaultValue?: any,
53691
54118
  * onChange?: (value: any) => void,
@@ -53729,7 +54156,7 @@ const TABS_ALIGN_TO_JUSTIFY_CONTENT = {
53729
54156
  * grows the binder instead of closing in on the text.
53730
54157
  * @param {string} [props.tabBorderColor] - The inactive tabs alone; the rest of
53731
54158
  * the outline follows `borderColor`.
53732
- * @param {import("ignore:preact").ComponentChildren} props.children - `Binder.Item`s.
54159
+ * @param {import("preact").ComponentChildren} props.children - `Binder.Item`s.
53733
54160
  */
53734
54161
  const Binder = ({
53735
54162
  children,
@@ -53870,13 +54297,13 @@ const Binder = ({
53870
54297
  /**
53871
54298
  * @param {any} [props.value] - What identifies this item; defaults to its
53872
54299
  * position among its siblings.
53873
- * @param {import("ignore:preact").ComponentChildren} [props.label] - What the tab
54300
+ * @param {import("preact").ComponentChildren} [props.label] - What the tab
53874
54301
  * shows.
53875
54302
  * @param {object} [props.route] - Makes the tab a `Link`
53876
54303
  * to that route, and the binder follows the url: the open item is the one
53877
54304
  * whose route matches, no `value`/`onChange` needed.
53878
54305
  * @param {string} [props.href] - Same, for a plain url.
53879
- * @param {import("ignore:preact").ComponentChildren} [props.children] - The page.
54306
+ * @param {import("preact").ComponentChildren} [props.children] - The page.
53880
54307
  *
53881
54308
  * Every other prop goes to the tab (a `Box`, or a `Link` when routed): an item
53882
54309
  * styles and configures the control that opens it.
@@ -54286,7 +54713,7 @@ const FixedBarStyleCSSVars = {
54286
54713
  };
54287
54714
 
54288
54715
  /**
54289
- * @type {import("ignore:preact").FunctionComponent<{
54716
+ * @type {import("preact").FunctionComponent<{
54290
54717
  * area?: "top"|"bottom"|"left"|"right",
54291
54718
  * width?: string|number,
54292
54719
  * height?: string|number,
@@ -54677,7 +55104,7 @@ const css$U = /* css */`.navi_summary_marker {
54677
55104
  `;
54678
55105
 
54679
55106
  /**
54680
- * @type {import("ignore:preact").FunctionComponent<{
55107
+ * @type {import("preact").FunctionComponent<{
54681
55108
  * open?: boolean,
54682
55109
  * loading?: boolean,
54683
55110
  * openDirection?: "down" | "up" | "left",
@@ -55097,8 +55524,8 @@ const useExpandableContext = partName => {
55097
55524
  };
55098
55525
 
55099
55526
  /**
55100
- * @type {import("ignore:preact").FunctionComponent<{
55101
- * ui?: import("ignore:preact").ComponentChildren | ((state: { open: boolean }) => import("ignore:preact").ComponentChildren),
55527
+ * @type {import("preact").FunctionComponent<{
55528
+ * ui?: import("preact").ComponentChildren | ((state: { open: boolean }) => import("preact").ComponentChildren),
55102
55529
  * open?: boolean,
55103
55530
  * defaultOpen?: boolean,
55104
55531
  * signal?: import("@preact/signals").Signal,
@@ -55738,9 +56165,9 @@ const Expandable = props => {
55738
56165
  * Controls inside it keep their own behavior and do not toggle. A Box, so it
55739
56166
  * takes the layout and style props every Box takes.
55740
56167
  *
55741
- * @type {import("ignore:preact").FunctionComponent<{
55742
- * marker?: false | import("ignore:preact").ComponentChildren,
55743
- * children?: import("ignore:preact").ComponentChildren | ((state: { open: boolean }) => import("ignore:preact").ComponentChildren),
56168
+ * @type {import("preact").FunctionComponent<{
56169
+ * marker?: false | import("preact").ComponentChildren,
56170
+ * children?: import("preact").ComponentChildren | ((state: { open: boolean }) => import("preact").ComponentChildren),
55744
56171
  * }>}
55745
56172
  * @param marker - The chevron drawn before the label. `false` removes it (a UI
55746
56173
  * part that is already an icon has no room for one); any node replaces it.
@@ -55815,7 +56242,7 @@ const ExpandableUI = ({
55815
56242
  * Expandable, children may be a function `(data) => ui` or a branches object —
55816
56243
  * see ActionRenderer.
55817
56244
  *
55818
- * @type {import("ignore:preact").FunctionComponent<{}>}
56245
+ * @type {import("preact").FunctionComponent<{}>}
55819
56246
  */
55820
56247
  const ExpandableContent = ({
55821
56248
  children,
@@ -55971,6 +56398,40 @@ const createToggleEvent = open => {
55971
56398
  return toggleEvent;
55972
56399
  };
55973
56400
 
56401
+ /**
56402
+ * Creates a renderComponent function that passes props through a chain of resolvers.
56403
+ * Each resolver is a component function rendered in sequence (hooks are allowed).
56404
+ * To pass through to the next resolver, call useNextResolver() and render the
56405
+ * returned Next component with the desired props.
56406
+ * To terminate the chain early (e.g. render a specialized component), render
56407
+ * directly without calling Next.
56408
+ *
56409
+ * The last entry in the array is the final/target component — it receives null
56410
+ * from useNextResolver() indicating it is terminal.
56411
+ *
56412
+ * Usage:
56413
+ * const renderButton = createComponentResolver([ResolverA, ResolverB, ButtonTarget]);
56414
+ * // Then inside a component render:
56415
+ * renderButton(props)
56416
+ *
56417
+ * A chain step is one component: a runner, defined once per position, whose
56418
+ * render CALLS its resolver's body rather than rendering it as a child. The
56419
+ * body runs synchronously inside the runner's render — its hooks are the
56420
+ * runner's, at the same tree position on every render — and for exactly that
56421
+ * long, `useNextResolver()` answers with the runner after this one (null for
56422
+ * the last). Nothing is handed down through a context: a chain rendered by the
56423
+ * hundred (a list's rows) pays one component per step, not a provider and a
56424
+ * wrapper around each.
56425
+ *
56426
+ * A resolver that renders ANOTHER resolver — a type resolver picking the
56427
+ * component for `type="date"` — cannot render it as a JSX child: that child
56428
+ * would render later, outside any body, with no Next to read. It renders it
56429
+ * with `renderResolver(Resolver, props)`, which captures the Next of the body
56430
+ * it is called from and hands it to a runner made for that resolver — the
56431
+ * child continues the chain where its parent stood. Called anywhere else, or
56432
+ * a `useNextResolver()` reached outside a body, throws: the chain has no
56433
+ * silent answer to give there.
56434
+ */
55974
56435
  const createComponentResolver = (resolvers, {
55975
56436
  pure
55976
56437
  } = {}) => {
@@ -56180,10 +56641,21 @@ const useAccentColorAttributes = (
56180
56641
  };
56181
56642
 
56182
56643
  installImportMetaCssBuild(import.meta);
56644
+ /**
56645
+ * We need the content to visually shrink (scale down) but the button interactive area MUST remain intact
56646
+ * Otherwise a click on the edges of the button cannot not trigger the click event (mouseup occurs outside the button)
56647
+ **/
56648
+
56649
+ /**
56650
+ * We have to re-define the CSS of button because getComputedStyle(button).borderColor returns
56651
+ * rgb(0, 0, 0) while being visually grey in chrome
56652
+ * So we redefine chrome styles so that loader can keep up with the actual color visible to the user
56653
+ */
56183
56654
  const css$R = /* css */`@layer navi {
56184
56655
  .navi_button {
56185
56656
  --button-border-radius: var(--navi-control-border-radius);
56186
56657
  --button-border-width: var(--navi-control-border-width);
56658
+ --border-width: var(--button-border-width);
56187
56659
  --button-cta-background-color: var(--navi-accent-color);
56188
56660
  --button-outline-width: var(--navi-focus-outline-width);
56189
56661
  --button-outline-offset: calc(-.5 * var(--button-outline-width));
@@ -56192,7 +56664,7 @@ const css$R = /* css */`@layer navi {
56192
56664
  --button-padding-y-default: var(--navi-button-padding-y-default);
56193
56665
  --button-loader-color: var(--navi-loader-color);
56194
56666
  --button-border-color: var(--navi-control-border-color);
56195
- --button-background-color: var(--button-background, var(--navi-button-background-color));
56667
+ --button-background-color: var(--navi-button-background-color);
56196
56668
  --button-color: currentColor;
56197
56669
  --button-cursor: pointer;
56198
56670
  --button-font-size: var(--navi-control-font-size);
@@ -56225,6 +56697,92 @@ const css$R = /* css */`@layer navi {
56225
56697
  line-height: var(--navi-control-line-height);
56226
56698
  display: inline-block;
56227
56699
  }
56700
+
56701
+ .navi_button[data-variant="discrete"] {
56702
+ --button-border-width: 0;
56703
+ --button-border-color: transparent;
56704
+ --button-border-color-hover: var(--button-border-color);
56705
+ --button-border-color-current: var(--button-border-color);
56706
+ --button-border-color-readonly: var(--button-border-color);
56707
+ --button-border-color-disabled: var(--button-border-color);
56708
+ --button-background-color: transparent;
56709
+ --button-background-color-hover: color-mix(in srgb,
56710
+ currentColor 8%,
56711
+ var(--button-background-color));
56712
+ --button-background-color-readonly: var(--button-background-color);
56713
+ --button-background-color-disabled: var(--button-background-color);
56714
+ }
56715
+
56716
+ .navi_button[data-variant="bare"] {
56717
+ --button-padding-x-default: 0;
56718
+ --button-padding-y-default: 0;
56719
+ --button-border-width: 0;
56720
+ --button-border-color: transparent;
56721
+ --button-border-color-hover: var(--button-border-color);
56722
+ --button-border-color-current: var(--button-border-color);
56723
+ --button-border-color-readonly: var(--button-border-color);
56724
+ --button-border-color-disabled: var(--button-border-color);
56725
+ --button-background-color: transparent;
56726
+ --button-background-color-hover: var(--button-background-color);
56727
+ --button-background-color-readonly: var(--button-background-color);
56728
+ --button-background-color-disabled: var(--button-background-color);
56729
+ font-size: inherit;
56730
+ font-family: inherit;
56731
+ line-height: inherit;
56732
+ }
56733
+
56734
+ .navi_button[data-variant="discrete-border"] {
56735
+ --button-background-color: transparent;
56736
+ --button-background-color-hover: var(--button-background-color);
56737
+ --button-background-color-readonly: var(--button-background-color);
56738
+ --button-background-color-disabled: var(--button-background-color);
56739
+ }
56740
+
56741
+ .navi_button[data-variant="border"] {
56742
+ --button-background-color: transparent;
56743
+ --button-background-color-hover: color-mix(in srgb,
56744
+ currentColor 8%,
56745
+ var(--button-background-color));
56746
+ --button-background-color-readonly: var(--button-background-color);
56747
+ --button-background-color-disabled: var(--button-background-color);
56748
+ }
56749
+
56750
+ .navi_button[data-icon] {
56751
+ --button-padding: 0;
56752
+ }
56753
+
56754
+ .navi_button[data-cta] {
56755
+ --button-background-color: var(--button-cta-background-color);
56756
+ --button-border-color: var(--button-cta-background-color);
56757
+ --button-color: white;
56758
+ --button-background-color-hover: color-mix(in srgb,
56759
+ var(--button-background-color) 85%,
56760
+ white);
56761
+ --button-border-color-hover: color-mix(in srgb,
56762
+ var(--button-border-color) 85%,
56763
+ white);
56764
+ --button-background-color-readonly: color-mix(in srgb,
56765
+ var(--button-background-color) 50%,
56766
+ white);
56767
+ --button-border-color-readonly: color-mix(in srgb,
56768
+ var(--button-border-color) 50%,
56769
+ white);
56770
+ --button-background-color-disabled: color-mix(in srgb,
56771
+ var(--button-background-color) 40%,
56772
+ white);
56773
+ --button-border-color-disabled: color-mix(in srgb,
56774
+ var(--button-border-color) 40%,
56775
+ white);
56776
+ --button-color-disabled: color-mix(in srgb,
56777
+ var(--button-color) 60%,
56778
+ transparent);
56779
+ }
56780
+
56781
+ .navi_button[data-content-display="contents"] {
56782
+ background: none;
56783
+ border: none;
56784
+ padding: 0;
56785
+ }
56228
56786
  }
56229
56787
 
56230
56788
  a.navi_button {
@@ -56241,7 +56799,6 @@ a.navi_button {
56241
56799
  --x-button-color: var(--button-color);
56242
56800
  --x-button-cursor: var(--button-cursor);
56243
56801
  box-sizing: border-box;
56244
- aspect-ratio: inherit;
56245
56802
  color: var(--x-button-color);
56246
56803
  border-top-left-radius: var(--x-corner-top-left-radius, var(--button-border-radius));
56247
56804
  border-top-right-radius: var(--x-corner-top-right-radius, var(--button-border-radius));
@@ -56250,12 +56807,15 @@ a.navi_button {
56250
56807
  cursor: var(--x-button-cursor);
56251
56808
  touch-action: manipulation;
56252
56809
  user-select: none;
56253
- background: none;
56254
- border: none;
56255
56810
  outline: none;
56256
- padding: 0;
56257
56811
  position: relative;
56258
56812
 
56813
+ &:where(:not([data-content-display="contents"])) {
56814
+ background: none;
56815
+ border: none;
56816
+ padding: 0;
56817
+ }
56818
+
56259
56819
  & .navi_button_content {
56260
56820
  --x-corner-top-left-radius: initial;
56261
56821
  --x-corner-top-right-radius: initial;
@@ -56277,8 +56837,7 @@ a.navi_button {
56277
56837
  gap: inherit;
56278
56838
  color: inherit;
56279
56839
  vertical-align: inherit;
56280
- background: var(--x-button-background);
56281
- background-color: var(--x-button-background-color, var(--x-button-background));
56840
+ background: var(--x-button-background, var(--x-button-background-color));
56282
56841
  border: var(--x-button-border);
56283
56842
  border-radius: inherit;
56284
56843
  outline-width: var(--button-outline-width);
@@ -56301,6 +56860,10 @@ a.navi_button {
56301
56860
  }
56302
56861
  }
56303
56862
 
56863
+ &[data-background-color] .navi_button_content {
56864
+ background-color: var(--x-button-background-color);
56865
+ }
56866
+
56304
56867
  &[data-href-current] {
56305
56868
  --x-button-border-color: var(--button-border-color-current);
56306
56869
  --x-button-background-color: var(--button-background-color-current);
@@ -56345,7 +56908,7 @@ a.navi_button {
56345
56908
  }
56346
56909
 
56347
56910
  &[data-content-display="contents"] {
56348
- & [data-focus-visible] {
56911
+ &[data-focus-visible] {
56349
56912
  outline: var(--button-outline-width) solid var(--button-outline-color);
56350
56913
  outline-offset: var(--button-outline-offset);
56351
56914
  }
@@ -56371,19 +56934,6 @@ a.navi_button {
56371
56934
  }
56372
56935
 
56373
56936
  &[data-variant="discrete"] {
56374
- --button-border-width: 0;
56375
- --button-border-color: transparent;
56376
- --button-border-color-hover: var(--button-border-color);
56377
- --button-border-color-current: var(--button-border-color);
56378
- --button-border-color-readonly: var(--button-border-color);
56379
- --button-border-color-disabled: var(--button-border-color);
56380
- --button-background-color: var(--button-background, transparent);
56381
- --button-background-color-hover: color-mix(in srgb,
56382
- currentColor 8%,
56383
- var(--button-background-color));
56384
- --button-background-color-readonly: var(--button-background-color);
56385
- --button-background-color-disabled: var(--button-background-color);
56386
-
56387
56937
  @media (hover: hover) {
56388
56938
  &[data-pressed] {
56389
56939
  & .navi_button_content {
@@ -56394,17 +56944,6 @@ a.navi_button {
56394
56944
  }
56395
56945
 
56396
56946
  &[data-variant="bare"] {
56397
- --button-border-width: 0;
56398
- --button-border-color: transparent;
56399
- --button-border-color-hover: var(--button-border-color);
56400
- --button-border-color-current: var(--button-border-color);
56401
- --button-border-color-readonly: var(--button-border-color);
56402
- --button-border-color-disabled: var(--button-border-color);
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
-
56408
56947
  &[data-pressed] {
56409
56948
  & .navi_button_content {
56410
56949
  transform: none;
@@ -56413,10 +56952,6 @@ a.navi_button {
56413
56952
  }
56414
56953
 
56415
56954
  &[data-variant="discrete-border"] {
56416
- --button-background-color: var(--button-background, transparent);
56417
- --button-background-color-hover: var(--button-background-color);
56418
- --button-background-color-readonly: var(--button-background-color);
56419
- --button-background-color-disabled: var(--button-background-color);
56420
56955
  --x-button-border-color: transparent;
56421
56956
 
56422
56957
  &[data-hover] {
@@ -56428,15 +56963,6 @@ a.navi_button {
56428
56963
  }
56429
56964
  }
56430
56965
 
56431
- &[data-variant="border"] {
56432
- --button-background-color: var(--button-background, transparent);
56433
- --button-background-color-hover: color-mix(in srgb,
56434
- currentColor 8%,
56435
- var(--button-background-color));
56436
- --button-background-color-readonly: var(--button-background-color);
56437
- --button-background-color-disabled: var(--button-background-color);
56438
- }
56439
-
56440
56966
  &[data-press-effect="scale"][data-pressed] {
56441
56967
  & .navi_button_content {
56442
56968
  transform: scale(.9);
@@ -56450,37 +56976,9 @@ a.navi_button {
56450
56976
  }
56451
56977
 
56452
56978
  &[data-icon] {
56453
- --button-padding: 0;
56454
56979
  justify-content: center;
56455
56980
  align-items: center;
56456
56981
  }
56457
-
56458
- &[data-cta] {
56459
- --button-background-color: var(--button-background, var(--button-cta-background-color));
56460
- --button-border-color: var(--button-cta-background-color);
56461
- --button-color: white;
56462
- --button-background-color-hover: color-mix(in srgb,
56463
- var(--button-background-color) 85%,
56464
- white);
56465
- --button-border-color-hover: color-mix(in srgb,
56466
- var(--button-border-color) 85%,
56467
- white);
56468
- --button-background-color-readonly: color-mix(in srgb,
56469
- var(--button-background-color) 50%,
56470
- white);
56471
- --button-border-color-readonly: color-mix(in srgb,
56472
- var(--button-border-color) 50%,
56473
- white);
56474
- --button-background-color-disabled: color-mix(in srgb,
56475
- var(--button-background-color) 40%,
56476
- white);
56477
- --button-border-color-disabled: color-mix(in srgb,
56478
- var(--button-border-color) 40%,
56479
- white);
56480
- --button-color-disabled: color-mix(in srgb,
56481
- var(--button-color) 60%,
56482
- transparent);
56483
- }
56484
56982
  }
56485
56983
  `;
56486
56984
  const ButtonUI = props => {
@@ -56609,6 +57107,7 @@ const ButtonUI = props => {
56609
57107
  "data-press-effect": pressEffect,
56610
57108
  "data-icon": icon ? "" : undefined,
56611
57109
  "data-cta": cta ? "" : undefined,
57110
+ "data-background-color": props.backgroundColor === undefined ? undefined : "",
56612
57111
  "data-callout-arrow-x": "center"
56613
57112
  // style management
56614
57113
  ,
@@ -56921,7 +57420,7 @@ const ButtonWithHref = props => {
56921
57420
  } = getHrefTargetInfo(href, documentUrlSignal.peek());
56922
57421
  return jsx(Next, {
56923
57422
  ...props,
56924
- target: target === undefined ? isSameSite ? undefined : "_blank" : target,
57423
+ target: target === undefined ? getDefaultHrefTarget(href, documentUrlSignal.peek()) : target,
56925
57424
  rel: rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel
56926
57425
  // Under whatever the caller already says: a route knows better whether
56927
57426
  // it is the page one is on (see button_route.jsx), and a demo can force
@@ -57075,7 +57574,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
57075
57574
  };
57076
57575
 
57077
57576
  /**
57078
- * @type {import("ignore:preact").FunctionComponent<{
57577
+ * @type {import("preact").FunctionComponent<{
57079
57578
  * selfInteractions?: string,
57080
57579
  * whenSelfInteractionsBlocked?: "hide" | "refuse" | "ignore",
57081
57580
  * replace?: boolean,
@@ -57157,7 +57656,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
57157
57656
  * more: the work may already have happened on the other side (see
57158
57657
  * docs/actions.md#aborting-saves-resources-it-does-not-undo), so say it only
57159
57658
  * where the screen can be re-opened on what is actually there.
57160
- * @param {boolean|import("ignore:preact").ComponentChildren} [confirm] Ask before
57659
+ * @param {boolean|import("preact").ComponentChildren} [confirm] Ask before
57161
57660
  * doing it, in the place the button stands: the first press replaces the
57162
57661
  * button with this question and a "Confirmer"/"Annuler" pair, the second one
57163
57662
  * does what the button was for. `true` asks navi's default question. The
@@ -57168,9 +57667,9 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
57168
57667
  * question up, its message on the button that asked. Use `<Picker
57169
57668
  * type="confirm">` instead when the question is long enough to want a popup,
57170
57669
  * or when the row has no space for it.
57171
- * @param {import("ignore:preact").ComponentChildren} [confirmLabel] Label of the
57670
+ * @param {import("preact").ComponentChildren} [confirmLabel] Label of the
57172
57671
  * second press.
57173
- * @param {import("ignore:preact").ComponentChildren} [cancelLabel] Label of the way
57672
+ * @param {import("preact").ComponentChildren} [cancelLabel] Label of the way
57174
57673
  * out of the question.
57175
57674
  * @param {string} [confirmTestId] `data-testid` of the second press.
57176
57675
  * @param {string} [cancelTestId] `data-testid` of the way out.
@@ -57269,13 +57768,13 @@ const css$P = /* css */`.navi_control_swap {
57269
57768
  `;
57270
57769
 
57271
57770
  /**
57272
- * @type {import("ignore:preact").FunctionComponent<{
57771
+ * @type {import("preact").FunctionComponent<{
57273
57772
  * value?: string,
57274
57773
  * defaultValue?: string,
57275
57774
  * signal?: import("@preact/signals").Signal<string>,
57276
57775
  * onChange?: (name: string, event: Event) => void,
57277
57776
  * animation?: boolean,
57278
- * children?: import("ignore:preact").ComponentChildren,
57777
+ * children?: import("preact").ComponentChildren,
57279
57778
  * }>}
57280
57779
  * @param value - The name of the side holding the floor, driven from outside:
57281
57780
  * the row swaps to match every change of it, and a press on a cap can still
@@ -57419,13 +57918,13 @@ const ControlSwap = props => {
57419
57918
  * reads these and draws the caps at its ends, the controls between them. The
57420
57919
  * props below say what the side IS; every other prop describes its cap.
57421
57920
  *
57422
- * @type {import("ignore:preact").FunctionComponent<{
57921
+ * @type {import("preact").FunctionComponent<{
57423
57922
  * name?: string,
57424
- * icon: import("ignore:preact").ComponentChildren,
57923
+ * icon: import("preact").ComponentChildren,
57425
57924
  * label: string,
57426
- * badge?: boolean | import("ignore:preact").ComponentChildren,
57925
+ * badge?: boolean | import("preact").ComponentChildren,
57427
57926
  * autoFocus?: boolean,
57428
- * children?: import("ignore:preact").ComponentChildren,
57927
+ * children?: import("preact").ComponentChildren,
57429
57928
  * [key: string]: any,
57430
57929
  * }>}
57431
57930
  * @param name - How `value`/`signal`/`onChange` name this side. Its position
@@ -57633,6 +58132,51 @@ const createAvailableConstraint = (
57633
58132
  };
57634
58133
  };
57635
58134
 
58135
+ /**
58136
+ * ControlGroup — a named sub-group of form controls whose combined state is
58137
+ * aggregated into a single key/value object, exactly like a Form but without
58138
+ * the <form> element semantics.
58139
+ *
58140
+ * Use it when you need a self-contained cluster of inputs that:
58141
+ * - produce one composite value (an object) as their joint state
58142
+ * - can carry their own action / uiAction / command
58143
+ * - may live inside or outside a <Form>
58144
+ *
58145
+ * Props:
58146
+ * name — the key under which the composite value is registered in a
58147
+ * parent Form (optional when used standalone)
58148
+ * action — called with the aggregated object value when any child changes
58149
+ * command — navi command string (e.g. "--navi-send")
58150
+ * disabled — propagates to all children
58151
+ * readOnly — propagates to all children
58152
+ * as — HTML element to render (default "div")
58153
+ * children — the controls that belong to this group
58154
+ *
58155
+ * aggregateChildStates / distributeChildUIState
58156
+ * — what the group is worth, in both directions, when that is not
58157
+ * simply "one key per child": two wheels that add up to a number
58158
+ * of minutes, three fields that make one date. With them the
58159
+ * group takes and hands back a single value, so it can be driven
58160
+ * by one `value`/`signal` like any other control. Same mechanism
58161
+ * InputDuration uses for its own hour/minute/second fields (see
58162
+ * input_duration.jsx). `name` is unaffected: it still says under
58163
+ * which key the group's value — whatever shape it takes —
58164
+ * lands in the form around it.
58165
+ *
58166
+ * distributeChildStates
58167
+ * — the same way down, asked ONCE for all the children at a time:
58168
+ * `(groupValue, children) => Map<child, state>`. The plural of
58169
+ * `distributeChildUIState`, and the mirror of
58170
+ * `aggregateChildStates`, which already sees every child. For a
58171
+ * group whose children cannot be placed one at a time — four
58172
+ * seats where who sits down decides who moves, a row where the
58173
+ * first answer changes what the others may show. A child the Map
58174
+ * does not name is left where it is. Given both, this one wins.
58175
+ * It also closes the loop: the group holds ONE answer its
58176
+ * children are views of, so when one of them speaks the others
58177
+ * are placed again (the one that acted is left where the user
58178
+ * put it).
58179
+ */
57636
58180
  const ControlGroup = props => {
57637
58181
  const defaultRef = useRef();
57638
58182
  props.ref = props.ref || defaultRef;
@@ -57825,6 +58369,41 @@ const css$N = /* css */`@layer navi {
57825
58369
  var(--button-background-color) 95%,
57826
58370
  black);
57827
58371
  }
58372
+
58373
+ .navi_checkbox[data-variant="switch"] {
58374
+ --margin: var(--switch-margin);
58375
+ --width: var(--switch-outer-width);
58376
+ --height: unset;
58377
+ --border-radius: var(--switch-border-radius);
58378
+ --background-color: var(--switch-background-color);
58379
+ --background-color-hover: var(--switch-background-color-hover);
58380
+ --background-color-readonly: var(--switch-background-color-readonly);
58381
+ --background-color-disabled: var(--switch-background-color-disabled);
58382
+ --background-color-checked: var(--switch-background-color-checked);
58383
+ --background-color-hover-checked: var(--switch-background-color-hover-checked);
58384
+ --background-color-readonly-checked: var(--switch-background-color-readonly-checked);
58385
+ --background-color-disabled-checked: var(--switch-background-color-disabled-checked);
58386
+ }
58387
+
58388
+ .navi_checkbox[data-variant="icon"] {
58389
+ --margin: 0;
58390
+ --width: auto;
58391
+ --height: auto;
58392
+ }
58393
+
58394
+ .navi_checkbox[data-variant="button"] {
58395
+ --margin: 0;
58396
+ --width: auto;
58397
+ --height: auto;
58398
+ --border-color: var(--button-border-color);
58399
+ --border-color-hover: var(--button-border-color-hover);
58400
+ --background-color: var(--button-background-color);
58401
+ --background-color-hover: var(--button-background-color-hover);
58402
+ --background-color-readonly: var(--button-background-color-readonly);
58403
+ --background-color-disabled: var(--button-background-color-disabled);
58404
+ --border-color-checked: var(--button-border-color);
58405
+ --background-color-checked: var(--button-background-color);
58406
+ }
57828
58407
  }
57829
58408
 
57830
58409
  .navi_checkbox {
@@ -57965,18 +58544,6 @@ const css$N = /* css */`@layer navi {
57965
58544
 
57966
58545
  &[data-variant="switch"] {
57967
58546
  --switch-outer-width: calc(var(--switch-width) + var(--switch-padding));
57968
- --margin: var(--switch-margin);
57969
- --width: var(--switch-outer-width);
57970
- --height: unset;
57971
- --border-radius: var(--switch-border-radius);
57972
- --background-color: var(--switch-background-color);
57973
- --background-color-hover: var(--switch-background-color-hover);
57974
- --background-color-readonly: var(--switch-background-color-readonly);
57975
- --background-color-disabled: var(--switch-background-color-disabled);
57976
- --background-color-checked: var(--switch-background-color-checked);
57977
- --background-color-hover-checked: var(--switch-background-color-hover-checked);
57978
- --background-color-readonly-checked: var(--switch-background-color-readonly-checked);
57979
- --background-color-disabled-checked: var(--switch-background-color-disabled-checked);
57980
58547
  min-width: var(--switch-outer-width);
57981
58548
  padding: var(--switch-padding);
57982
58549
  background-color: var(--x-background-color);
@@ -57986,25 +58553,11 @@ const css$N = /* css */`@layer navi {
57986
58553
  }
57987
58554
 
57988
58555
  &[data-variant="icon"] {
57989
- --margin: 0;
57990
- --width: auto;
57991
- --height: auto;
57992
58556
  background: none;
57993
58557
  border: none;
57994
58558
  }
57995
58559
 
57996
58560
  &[data-variant="button"] {
57997
- --margin: 0;
57998
- --width: auto;
57999
- --height: auto;
58000
- --border-color: var(--button-border-color);
58001
- --border-color-hover: var(--button-border-color-hover);
58002
- --background-color: var(--button-background-color);
58003
- --background-color-hover: var(--button-background-color-hover);
58004
- --background-color-readonly: var(--button-background-color-readonly);
58005
- --background-color-disabled: var(--button-background-color-disabled);
58006
- --border-color-checked: var(--button-border-color);
58007
- --background-color-checked: var(--button-background-color);
58008
58561
  padding-top: var(--button-padding-top, var(--button-padding-y, var(--button-padding, var(--button-padding-y-default))));
58009
58562
  padding-right: var(--button-padding-right, var(--button-padding-x, var(--button-padding, var(--button-padding-x-default))));
58010
58563
  padding-bottom: var(--button-padding-bottom, var(--button-padding-y, var(--button-padding, var(--button-padding-y-default))));
@@ -58190,7 +58743,7 @@ const css$M = /* css */`@layer navi {
58190
58743
  [navi-field] {
58191
58744
  --spacing-with-control: var(--navi-xs);
58192
58745
 
58193
- & .navi_checkbox, & .navi_radio {
58746
+ & .navi_checkbox:not([data-variant="switch"]), & .navi_radio {
58194
58747
  --margin: 0;
58195
58748
  }
58196
58749
  }
@@ -58545,6 +59098,28 @@ const css$L = /* css */`@layer navi {
58545
59098
  --button-border-color-disabled: var(--border-color-readonly);
58546
59099
  --button-background-color-disabled: var(--background-color-readonly);
58547
59100
  }
59101
+
59102
+ .navi_radio[data-variant="icon"] {
59103
+ --width: auto;
59104
+ --height: auto;
59105
+ --outline-offset: 2px;
59106
+ --outline-width: 2px;
59107
+ }
59108
+
59109
+ .navi_radio[data-variant="button"] {
59110
+ --margin: 0;
59111
+ --outline-offset: 0px;
59112
+ --width: auto;
59113
+ --height: auto;
59114
+ --border-color: var(--button-border-color);
59115
+ --border-color-hover: var(--button-border-color-hover);
59116
+ --background-color: var(--button-background-color);
59117
+ --background-color-hover: var(--button-background-color-hover);
59118
+ --background-color-readonly: var(--button-background-color-readonly);
59119
+ --background-color-disabled: var(--button-background-color-disabled);
59120
+ --border-color-checked: var(--button-border-color);
59121
+ --background-color-checked: var(--button-background-color);
59122
+ }
58548
59123
  }
58549
59124
 
58550
59125
  .navi_radio {
@@ -58700,26 +59275,7 @@ const css$L = /* css */`@layer navi {
58700
59275
  }
58701
59276
  }
58702
59277
 
58703
- &[data-variant="icon"] {
58704
- --width: auto;
58705
- --height: auto;
58706
- --outline-offset: 2px;
58707
- --outline-width: 2px;
58708
- }
58709
-
58710
59278
  &[data-variant="button"] {
58711
- --margin: 0;
58712
- --outline-offset: 0px;
58713
- --width: auto;
58714
- --height: auto;
58715
- --border-color: var(--button-border-color);
58716
- --border-color-hover: var(--button-border-color-hover);
58717
- --background-color: var(--button-background-color);
58718
- --background-color-hover: var(--button-background-color-hover);
58719
- --background-color-readonly: var(--button-background-color-readonly);
58720
- --background-color-disabled: var(--button-background-color-disabled);
58721
- --border-color-checked: var(--button-border-color);
58722
- --background-color-checked: var(--button-background-color);
58723
59279
  padding-top: var(--button-padding-top, var(--button-padding-y, var(--button-padding, var(--button-padding-y-default))));
58724
59280
  padding-right: var(--button-padding-right, var(--button-padding-x, var(--button-padding, var(--button-padding-x-default))));
58725
59281
  padding-bottom: var(--button-padding-bottom, var(--button-padding-y, var(--button-padding, var(--button-padding-y-default))));
@@ -58943,6 +59499,7 @@ const css$K = /* css */`@layer navi {
58943
59499
  --outline-width: var(--navi-focus-outline-width);
58944
59500
  --outline-offset: var(--outline-width);
58945
59501
  --outline-color: var(--navi-focus-outline-color);
59502
+ --margin: 2px;
58946
59503
  --height: 8px;
58947
59504
  --thumb-size: 16px;
58948
59505
  --thumb-width: var(--thumb-size);
@@ -59018,6 +59575,7 @@ const css$K = /* css */`@layer navi {
59018
59575
  box-sizing: border-box;
59019
59576
  width: fit-content;
59020
59577
  height: var(--height);
59578
+ margin: var(--margin);
59021
59579
  font-size: var(--font-size);
59022
59580
  font-family: var(--font-family);
59023
59581
  border-top-left-radius: var(--x-corner-top-left-radius, var(--outline-border-radius));
@@ -59030,7 +59588,6 @@ const css$K = /* css */`@layer navi {
59030
59588
  outline-offset: var(--outline-offset);
59031
59589
  flex-direction: row;
59032
59590
  align-items: center;
59033
- margin: 2px;
59034
59591
  position: relative;
59035
59592
 
59036
59593
  & .navi_control_input {
@@ -59327,6 +59884,45 @@ const inputCss = /* css */ `@layer navi {
59327
59884
  grey);
59328
59885
  --color-disabled: var(--color-dimmed);
59329
59886
  }
59887
+
59888
+ .navi_input[data-background-transparent] {
59889
+ --background-color-hover: var(--background-color);
59890
+ --background-color-focus: var(--navi-surface-color);
59891
+ }
59892
+
59893
+ .navi_input[data-variant="discrete"], .navi_input[data-variant="discrete-border"] {
59894
+ --background-color: transparent;
59895
+ --background-color-hover: var(--background-color);
59896
+ --background-color-focus: var(--navi-surface-color);
59897
+
59898
+ &[data-background] {
59899
+ --background-color-focus: transparent;
59900
+ }
59901
+ }
59902
+
59903
+ .navi_input[data-variant="discrete"] {
59904
+ --border-color: transparent;
59905
+ }
59906
+
59907
+ .navi_input[data-variant="text"] {
59908
+ --background-color: transparent;
59909
+ --background-color-hover: var(--background-color);
59910
+ --background-color-focus: var(--background-color);
59911
+ --background-color-readonly: var(--background-color);
59912
+ --background-color-disabled: var(--background-color);
59913
+ --border-color: transparent;
59914
+ --border-color-hover: var(--border-color);
59915
+ --border-color-readonly: var(--border-color);
59916
+ --border-color-disabled: var(--border-color);
59917
+ }
59918
+
59919
+ .navi_input[data-variant="underline"] {
59920
+ --background-color: transparent;
59921
+ --background-color-hover: var(--background-color);
59922
+ --background-color-focus: var(--background-color);
59923
+ --background-color-readonly: var(--background-color);
59924
+ --background-color-disabled: var(--background-color);
59925
+ }
59330
59926
  }
59331
59927
 
59332
59928
  .navi_input {
@@ -59478,19 +60074,12 @@ const inputCss = /* css */ `@layer navi {
59478
60074
  }
59479
60075
 
59480
60076
  &[data-background-transparent] {
59481
- --background-color-hover: var(--background-color);
59482
- --background-color-focus: var(--navi-surface-color);
59483
-
59484
60077
  &[data-focus] {
59485
60078
  --x-background-color: var(--background-color-focus);
59486
60079
  }
59487
60080
  }
59488
60081
 
59489
60082
  &[data-variant="discrete"], &[data-variant="discrete-border"] {
59490
- --background-color: transparent;
59491
- --background-color-hover: var(--background-color);
59492
- --background-color-focus: var(--navi-surface-color);
59493
-
59494
60083
  &[data-focus] {
59495
60084
  --x-background-color: var(--background-color-focus);
59496
60085
  }
@@ -59498,15 +60087,9 @@ const inputCss = /* css */ `@layer navi {
59498
60087
  &[data-readonly], &[data-disabled] {
59499
60088
  --x-background-color: var(--background-color);
59500
60089
  }
59501
-
59502
- &[data-background] {
59503
- --background-color-focus: transparent;
59504
- }
59505
60090
  }
59506
60091
 
59507
60092
  &[data-variant="discrete"] {
59508
- --border-color: transparent;
59509
-
59510
60093
  &[data-focus] {
59511
60094
  --x-border-color: color-mix(in srgb,
59512
60095
  var(--border-color) 55%,
@@ -59515,24 +60098,10 @@ const inputCss = /* css */ `@layer navi {
59515
60098
  }
59516
60099
 
59517
60100
  &[data-variant="text"] {
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
- --border-color: transparent;
59524
- --border-color-hover: var(--border-color);
59525
- --border-color-readonly: var(--border-color);
59526
- --border-color-disabled: var(--border-color);
59527
60101
  cursor: inherit;
59528
60102
  }
59529
60103
 
59530
60104
  &[data-variant="underline"] {
59531
- --background-color: transparent;
59532
- --background-color-hover: var(--background-color);
59533
- --background-color-focus: var(--background-color);
59534
- --background-color-readonly: var(--background-color);
59535
- --background-color-disabled: var(--background-color);
59536
60105
  border: none;
59537
60106
  border-radius: 0;
59538
60107
  padding-left: 0;
@@ -60728,6 +61297,38 @@ const InputStyleCSSVars = {
60728
61297
  const InputPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading", ":-navi-has-value"];
60729
61298
  const InputPseudoElements = ["::-navi-loader"];
60730
61299
 
61300
+ /**
61301
+ * @type {import("preact").FunctionComponent<{
61302
+ * type?: string,
61303
+ * icon?: import("preact").ComponentChildren | null,
61304
+ * clearable?: boolean,
61305
+ * value?: any,
61306
+ * boolean?: boolean,
61307
+ * selectOnPress?: boolean,
61308
+ * [key: string]: any,
61309
+ * }>}
61310
+ * @param {any} [value] On a checkbox, what it is worth checked (`"on"` when
61311
+ * left out); unchecked it is `undefined`. See control_value.md, "What a
61312
+ * checkbox is worth".
61313
+ * @param {boolean} [boolean] Makes a checkbox a yes/no: `true` checked, `false`
61314
+ * unchecked, everywhere it hands its value. Takes no `value`.
61315
+ * @param {import("preact").ComponentChildren|null} [icon] What the textual
61316
+ * types that draw an icon (`search`, `email`, `tel`) put in their slot: left
61317
+ * out, the type's own glyph; `null`, nothing — for a field an application
61318
+ * draws its own magnifier beside, where a second one two centimetres away
61319
+ * would be one too many; anything else is drawn instead. A `search` field
61320
+ * swaps that slot for the clear cross as soon as it holds a value, whatever
61321
+ * this says.
61322
+ * @param {boolean} [clearable] Gives any other type the same slot: the icon
61323
+ * while empty (none, unless `icon` says otherwise), the clear cross once
61324
+ * filled.
61325
+ * @param {boolean} [selectOnPress] For a textual field whose value is usually
61326
+ * replaced rather than edited, like a browser's address bar: the first press
61327
+ * after the field took the focus selects its whole text once released, with a
61328
+ * mouse or a finger, and a second press places the caret. A focus that came
61329
+ * without a press (a dialog opening, Tab, code) selects nothing; the first key
61330
+ * that types selects first, so it replaces the text.
61331
+ */
60731
61332
  const Input = props => {
60732
61333
  resolveInputProps(props);
60733
61334
  const {
@@ -60817,7 +61418,7 @@ const TextareaStyleCSSVars = {
60817
61418
  };
60818
61419
 
60819
61420
  /**
60820
- * @type {import("ignore:preact").FunctionComponent<{
61421
+ * @type {import("preact").FunctionComponent<{
60821
61422
  * value?: string,
60822
61423
  * defaultValue?: string,
60823
61424
  * signal?: import("@preact/signals").Signal<string>,
@@ -60924,7 +61525,7 @@ const Textarea = ({
60924
61525
  * draws: put it under the box, in a form footer, next to a label, and feed it
60925
61526
  * the same value or signal as the textarea.
60926
61527
  *
60927
- * @type {import("ignore:preact").FunctionComponent<{
61528
+ * @type {import("preact").FunctionComponent<{
60928
61529
  * value?: string,
60929
61530
  * signal?: import("@preact/signals").Signal<string>,
60930
61531
  * maxLength?: number,
@@ -61157,8 +61758,8 @@ const useEditionController = () => {
61157
61758
  * it in CSS (the `[data-editing] input` rules in `control/table/table_css.js`),
61158
61759
  * the demo `control/demos/action/8_editable_demo.html` does it with props.
61159
61760
  *
61160
- * @type {import("ignore:preact").FunctionComponent<{
61161
- * children?: import("ignore:preact").ComponentChildren,
61761
+ * @type {import("preact").FunctionComponent<{
61762
+ * children?: import("preact").ComponentChildren,
61162
61763
  * action: Function | object,
61163
61764
  * editing?: { event?: Event } | null,
61164
61765
  * onEditEnd?: (detail: { success?: boolean, cancelled?: boolean, event: Event }) => void,
@@ -61518,6 +62119,77 @@ const commitSubtree = (controller, e) => {
61518
62119
  * <form> element (FormNested).
61519
62120
  */
61520
62121
 
62122
+ /**
62123
+ * @param {object} props
62124
+ * @param {boolean} [props.standalone] - Its value is its own: the form does not
62125
+ * register with the control group around it (a Picker, a ControlGroup), so
62126
+ * what is typed in it never becomes part of that group's value, and nothing
62127
+ * that group does — distributing a value, resetting, cascading validation —
62128
+ * reaches it. For a form that lives INSIDE something else while answering a
62129
+ * different question — "create the thing I am about to pick" inside a picker,
62130
+ * say. Every control takes the same prop, for the same reason. A form inside
62131
+ * another form is that by construction and does not need the prop: the group
62132
+ * it would register into is a form, which never owns another form's fields.
62133
+ * @param {boolean} [props.canSendWhileUnchanged] - Send even when nothing changed. By
62134
+ * default a form only acts on an answer that is actually new: submitting a
62135
+ * form nobody touched — one just rendered, one reopened and left alone — runs
62136
+ * no action (a field still showing its `defaultValue` is an answer: confirming
62137
+ * a suggestion), and after the first submission it is that value the next
62138
+ * one is compared against. Everything around the action still happens either
62139
+ * way: the constraints are checked, and what follows the send still follows
62140
+ * it (the slide moves on, the popup closes) — the user is done regardless of
62141
+ * whether there was anything to send. Set this for a form where sending the
62142
+ * same thing twice is the point — a single button that fires off a
62143
+ * notification, an action whose duplicates are fine. See also
62144
+ * `readOnlyWhileFormUnchanged` on `Button`, for a submit that should say it
62145
+ * is waiting rather than accept a press that sends nothing.
62146
+ * @param {boolean} [props.actionAbortable] - The person waiting may give up on
62147
+ * the send: closing the popup this form holds calls the run off and goes
62148
+ * through, instead of being refused. For a send whose answer may never come —
62149
+ * a request over a network that stops answering — where a sheet with no way
62150
+ * out is worse than an answer lost. Aborting frees the client and nothing
62151
+ * more: the write may already have landed on the other side (see
62152
+ * docs/actions.md#aborting-saves-resources-it-does-not-undo), so say it only
62153
+ * where the screen can be re-opened on what is actually there. Every control
62154
+ * inside the form inherits it along with the wait itself.
62155
+ * @param {boolean} [props.optimistic] - The send is taken as done on the form's
62156
+ * own word: no busy state, and what follows it (`command="--navi-close"`, a
62157
+ * slide moving on) runs at once, the run going on behind. A failure then has
62158
+ * no sheet to be read in: `resetOnError` puts the form back and the error
62159
+ * callout is drawn on what surrounds the closed sheet. For a write not worth being
62160
+ * waited for — a score, a note — never for one whose refusal changes what the
62161
+ * person does next (see
62162
+ * docs/popup_open.md#closing-when-a-button-also-runs-an-action).
62163
+ * @param {any} [props.pristineKey] - What the form is measured against, taken
62164
+ * again every time this changes. A form knows what it holds as soon as its
62165
+ * fields have registered, which is the right moment for a form whose values
62166
+ * are there on the first render — and never the right one for a screen that
62167
+ * modifies something: the resource arrives a request later and fills the
62168
+ * fields, and a form comparing against what it held BEFORE that opens already
62169
+ * changed. Pass whatever says the filling is done (a boolean, the resource
62170
+ * itself, a count of what has loaded) and what the fields carry at that
62171
+ * moment becomes the reference — including the fields that settle in a render
62172
+ * of their own, so there is no tick to wait for on the caller's side. Change
62173
+ * it once, when the screen is ready: taken again after someone started
62174
+ * typing, it would call what they wrote the reference.
62175
+ * @param {string} [props.command] - What follows a submission that went
62176
+ * through: the form has answered its question, and this says what the screen
62177
+ * does about it. Nothing runs when the submission is refused — the form then
62178
+ * stays in front of the user, showing what it is waiting for.
62179
+ *
62180
+ * `"--navi-close"` dismisses the popup the form is in,
62181
+ * `"--navi-left"`/`"--navi-right"`/`"--navi-up"`/`"--navi-down"` move on the
62182
+ * slide map it is in, `"--navi-nav-to:/the/url"` takes the user to a page,
62183
+ * `"--navi-void"` stays put — the one for a form inside a popup that must
62184
+ * stay open once it is sent (a side panel one keeps editing in).
62185
+ * Any navi command, really: it is triggered from the form, so it finds its
62186
+ * target the way that command always does.
62187
+ *
62188
+ * Left out, the surface the form sits in decides (see resolveAfterSend in
62189
+ * commands.js): a popup closes, a slide goes on to the next one — or back to
62190
+ * the one it came from when there is no next — and a form on a page does
62191
+ * nothing.
62192
+ */
61521
62193
  const Form = props => {
61522
62194
  const defaultRef = useRef();
61523
62195
  props.ref = props.ref || defaultRef;
@@ -61834,6 +62506,7 @@ const useRequestSubmitThroughGates = ref => {
61834
62506
  installImportMetaCssBuild(import.meta);
61835
62507
  const css$H = /* css */`.navi_group {
61836
62508
  --group-border-width: var(--navi-control-border-width);
62509
+ --border-width: initial;
61837
62510
 
61838
62511
  & > :not([navi-out-of-flow]) {
61839
62512
  &:hover, &[data-hover] {
@@ -65066,6 +65739,122 @@ SlideContainer.Down = SlideDown;
65066
65739
  SlideContainer.First = SlideFirst;
65067
65740
  SlideContainer.Last = SlideLast;
65068
65741
 
65742
+ /**
65743
+ * Displays a date/time value in a human-readable format using the current locale.
65744
+ *
65745
+ * @param {Date|number|string} children
65746
+ * The date to display. Accepts:
65747
+ * - a `Date` instance
65748
+ * - a Unix timestamp (number, in ms)
65749
+ * - a date string `"YYYY-MM-DD"` or any string parseable by `Date`
65750
+ * If the value cannot be parsed, it is rendered as-is.
65751
+ * If undefined/null, renders a placeholder shaped like the type: `"–"` for
65752
+ * `"relative"`, `"--:--"` for `"time"`, `"--"` for the durations, the
65753
+ * locale's field pattern for `"date"`/`"month"`/`"week"`/`"datetime"`.
65754
+ *
65755
+ * @param {"date"|"month"|"week"|"datetime"|"time"|"hour"|"minute"|"second"|"duration"|"relative"} [type="relative"]
65756
+ * Controls the display format:
65757
+ * - `"date"` → "lundi 11 mai" (long by default), "samedi 1 janvier 2050" outside the current year (see `year`);
65758
+ * `format="short"` → "lun. 11 mai"; `format="numeric"` → "11/05/2026";
65759
+ * `format={{ weekday: "long", month: "short" }}` spells the two apart → "mercredi 2 sept.";
65760
+ * a part set to `false` is dropped — `format={{ day: false, month: false }}` → "mercredi"
65761
+ * - `"month"` → "juin 2026"
65762
+ * - `"week"` → the ISO week as given ("2026-W38"), in a `<time datetime>`
65763
+ * - `"datetime"` → "lun. 11 mai, 14:30" (long); `format="short"` → "11 mai, 14:30"; `format="narrow"` → "11/05, 14:30"
65764
+ * - `"time"` → time-of-day as duration by default (e.g. "14:30" → "14 heures 30");
65765
+ * `format="timestring"` → clock "14 h 30". Midnight (00:xx) is
65766
+ * special-cased regardless of `format` — "0 heures 5 minutes"
65767
+ * would otherwise collapse to "5 minutes", indistinguishable
65768
+ * from an actual 5-minute duration: `format="long"` (default)
65769
+ * says "minuit et 5 minutes" (`format="short"/"narrow"/"compact"`
65770
+ * keep the zero hour instead — "0 h et 5 min"/"0h 5min"/"00h05").
65771
+ * `format="compact"` also zero-pads any single-digit hour, not
65772
+ * just midnight (e.g. "5h30" → "05h30"), closer to "05:30";
65773
+ * `pad={false}` writes it the way one says it instead ("8h30",
65774
+ * "8h"). See `<TimeRange>` for a span between two of them.
65775
+ * - `"hour"` → hours as duration (e.g. 1.5 → "1 heure 30 minutes")
65776
+ * - `"minute"` → minutes as duration (e.g. 90 → "1 heure 30 minutes")
65777
+ * - `"second"` → seconds as duration (e.g. 90 → "1 minute 30 secondes")
65778
+ * These three promote to the largest fitting unit, days included (36 hours
65779
+ * → "1 jour et 12 heures"); see `forceUnit` to stay in the given unit.
65780
+ * - `"duration"` → duration string/object/number (seconds); `format="iso"` → ISO 8601
65781
+ * - `"relative"` → "dans 1 heure 30" / "En cours" / "il y a 2 heures"
65782
+ * Handles past, present, and future.
65783
+ * `format="short"` → "dans 1 h et 30 min"; `format="narrow"`
65784
+ * → "+3 h"; `format="compact"` → "dans 1h30".
65785
+ * `eventDuration` defaults to 0 (instantaneous: no "En cours" window).
65786
+ *
65787
+ * @param {number|string} [eventDuration=0]
65788
+ * Duration of the event in milliseconds, or a duration string (`"PT1H30M"`,
65789
+ * `"1hour30minute"`). Only used with `type="relative"`.
65790
+ * When omitted, the event is instantaneous (point in time, no "En cours" window).
65791
+ * @param {boolean} [bare]
65792
+ * When true, strips the past-tense literal ("il y a", "ago") and returns only integer + unit.
65793
+ * Only applies to the past state of `type="relative"`.
65794
+ * @param {"long"|"short"|"narrow"|"compact"|"numeric"|"timestring"|"iso"|{ weekday?: "long"|"short"|"narrow"|false, day?: boolean, month?: "long"|"short"|"narrow"|"numeric"|false }} [format="long"]
65795
+ * Controls the verbosity of the output. Defaults to `"long"` for all types.
65796
+ * `"long"`, `"short"`, `"narrow"` and `"compact"` are verbosities every `type`
65797
+ * writes. The three under them name a shape only some types have; a `type`
65798
+ * without that shape writes its `"long"` form and names the dropped word in
65799
+ * the console.
65800
+ * - `"short"` → Intl short (e.g. "2 h et 15 min", short month for dates/datetimes, no weekday for datetime)
65801
+ * - `"narrow"` → Intl narrow (e.g. "2h 15min", numeric month for datetime)
65802
+ * - `"compact"` → custom compact notation (e.g. "2h15", no minute symbol
65803
+ * when hours present). A date has nothing tighter than its
65804
+ * digits, so `type="date"`/`"month"`/`"datetime"` write the
65805
+ * all-digit spelling ("17/09/2026", "17/09 14:30")
65806
+ * - `"numeric"` → numeric date, for `type="date"` and `type="month"` (e.g. "11/09/2026", "09/2026")
65807
+ * - `"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)
65808
+ * - `"iso"` → ISO 8601 string, only for `type="duration"` (e.g. "PT2H15M")
65809
+ * - an object → `type="date"` only, one verbosity per part
65810
+ * (`{ weekday: "long", month: "short" }`): a narrow card
65811
+ * keeps the weekday whole, since that is what the eye
65812
+ * lands on, and abbreviates the month, since that is
65813
+ * where the characters are. `false` drops a part:
65814
+ * `{ day: false, month: false }` is the weekday alone
65815
+ * ("mardi"), `{ month: false }` weekday + day-of-month
65816
+ * ("mardi 18").
65817
+ * @param {boolean} [forceUnit=false]
65818
+ * Keeps the value in the unit named by `type` however big it gets, for
65819
+ * `type="hour"`, `"minute"` and `"second"`: 36 hours reads "36 heures"
65820
+ * instead of the default "1 jour et 12 heures". The default promotes to the
65821
+ * largest fitting unit because it reads better; force the unit when the unit
65822
+ * itself is the information (a quota, a counter).
65823
+ * @param {number} [decimals]
65824
+ * `type="duration"` and `type="second"` only — how many decimals a fraction
65825
+ * of a second keeps, rounded: with `decimals={1}`, 1.234 reads "1,2 seconde"
65826
+ * and 1.96 "2 secondes"; a whole value is never padded ("12 secondes").
65827
+ * Without it the fraction is written to the millisecond ("1,234 seconde"). A
65828
+ * duration measured in milliseconds rarely comes out whole, and its last
65829
+ * digits are usually noise.
65830
+ * @param {boolean} [pad=true]
65831
+ * `type="time"` + `format="compact"` only — whether the clock is written at a
65832
+ * fixed width. `true` (default) zero-pads the hour and always writes the
65833
+ * minutes ("08h00", "08h30"), so a column of times lines up. `false` writes
65834
+ * the shape one says out loud instead: bare hour, minutes only when there are
65835
+ * any ("8h", "8h30"). The spelled-out formats put their units in words and
65836
+ * need neither, so they ignore it.
65837
+ * @param {"hour"|"minute"} [precision]
65838
+ * `type="time"` + `format="compact"` only, and rarely set by hand — whether a
65839
+ * zero minute is written ("8h00") or dropped ("8h"). Defaults to whatever
65840
+ * `pad` implies; the one reason to force `"minute"` on an unpadded clock is
65841
+ * to agree with a partner that has minutes of its own, which is what
65842
+ * `<TimeRange>` does for you.
65843
+ * @param {boolean|"auto"} [year]
65844
+ * `type="date"` only — whether the year is written. `"auto"` writes it only
65845
+ * when the date is not in the current year, the way a calendar does
65846
+ * ("lundi 11 mai", but "samedi 1 janvier 2050"); `false` drops it ("30/07",
65847
+ * the day/month order still following the locale). Defaults to `"auto"`
65848
+ * for the spelled formats and to `true` for `format="numeric"`. A format
65849
+ * without the month (`{ month: false }`) never writes it.
65850
+ * @param {boolean} [dayLabel]
65851
+ * When true and `type="date"`, appends the locale-aware relative label
65852
+ * ("hier", "aujourd'hui", "demain") when the date is yesterday, today, or tomorrow.
65853
+ * @param {string} [lang]
65854
+ * BCP 47 locale tag (e.g. `"fr"`, `"en-US"`).
65855
+ * Defaults to `languagesSignal.value`: the browser's languages, as
65856
+ * `setPreferredLanguage`/`setSupportedLanguages` narrow them.
65857
+ */
65069
65858
  const Time = props => {
65070
65859
  const {
65071
65860
  type
@@ -65330,6 +66119,20 @@ const TimeMinute = ({
65330
66119
  children: String(children)
65331
66120
  });
65332
66121
  }
66122
+ if (!Number.isInteger(minutes)) {
66123
+ // a fraction of a minute is read in seconds, the way formatMinuteDuration
66124
+ // writes it ("1 minute et 30 secondes")
66125
+ const clock = formatClock(Math.round(minutes * 60));
66126
+ return jsx(TimeText, {
66127
+ dateTime: clock,
66128
+ ...props,
66129
+ children: format === "timestring" ? clock : formatMinuteDuration(minutes, {
66130
+ lang,
66131
+ format,
66132
+ forceUnit
66133
+ })
66134
+ });
66135
+ }
65333
66136
  const totalHours = Math.floor(minutes / 60);
65334
66137
  const remainingMinutes = minutes % 60;
65335
66138
  const hh = String(totalHours).padStart(2, "0");
@@ -65359,6 +66162,7 @@ const TimeSecond = ({
65359
66162
  lang = languagesSignal.value,
65360
66163
  format = "long",
65361
66164
  forceUnit = false,
66165
+ decimals,
65362
66166
  ...props
65363
66167
  }) => {
65364
66168
  if (children === undefined || children === null) {
@@ -65374,19 +66178,17 @@ const TimeSecond = ({
65374
66178
  children: String(children)
65375
66179
  });
65376
66180
  }
65377
- const h = Math.floor(seconds / 3600);
65378
- const m = Math.floor(seconds % 3600 / 60);
65379
- const s = seconds % 60;
65380
- const dateTime = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
66181
+ const dateTime = formatClock(seconds);
65381
66182
  let text;
65382
66183
  if (format === "timestring") {
65383
66184
  // Always HH:MM:SS to avoid ambiguity with HH:MM time-of-day format
65384
- text = dateTime;
66185
+ text = formatClock(seconds, decimals);
65385
66186
  } else {
65386
66187
  text = formatSecondDuration(seconds, {
65387
66188
  lang,
65388
66189
  format,
65389
- forceUnit
66190
+ forceUnit,
66191
+ decimals
65390
66192
  });
65391
66193
  }
65392
66194
  return jsx(TimeText, {
@@ -65439,6 +66241,7 @@ const TimeDuration = ({
65439
66241
  children,
65440
66242
  lang = languagesSignal.value,
65441
66243
  format = "long",
66244
+ decimals,
65442
66245
  ...props
65443
66246
  }) => {
65444
66247
  if (children === undefined || children === null) {
@@ -65486,7 +66289,8 @@ const TimeDuration = ({
65486
66289
  ...props,
65487
66290
  children: formatDuration(duration, {
65488
66291
  lang,
65489
- format
66292
+ format,
66293
+ decimals
65490
66294
  })
65491
66295
  });
65492
66296
  }
@@ -65498,7 +66302,8 @@ const TimeDuration = ({
65498
66302
  }
65499
66303
  const text = formatDuration(duration, {
65500
66304
  lang,
65501
- format
66305
+ format,
66306
+ decimals
65502
66307
  });
65503
66308
  return jsx(TimeText, {
65504
66309
  dateTime: isoString,
@@ -65638,6 +66443,24 @@ const TimeRange = ({
65638
66443
  });
65639
66444
  };
65640
66445
 
66446
+ // "HH:MM:SS", the fraction of a second after a period ("00:00:01.234"): the
66447
+ // shape a `datetime` attribute reads, which takes up to 3 decimals.
66448
+ const formatClock = (seconds, decimals = 3) => {
66449
+ const factor = 10 ** decimals;
66450
+ const total = Math.round(seconds * factor);
66451
+ const fraction = total % factor;
66452
+ const totalSeconds = (total - fraction) / factor;
66453
+ const h = Math.floor(totalSeconds / 3600);
66454
+ const m = Math.floor(totalSeconds % 3600 / 60);
66455
+ const s = totalSeconds % 60;
66456
+ const clock = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
66457
+ if (fraction === 0) {
66458
+ return clock;
66459
+ }
66460
+ const digits = String(fraction).padStart(decimals, "0").replace(/0+$/, "");
66461
+ return `${clock}.${digits}`;
66462
+ };
66463
+
65641
66464
  /**
65642
66465
  * Wraps multiple inputs together and handles keyboard navigation and paste
65643
66466
  * distribution between them.
@@ -65864,6 +66687,11 @@ const renderSafe = (value) => {
65864
66687
  };
65865
66688
 
65866
66689
  installImportMetaCssBuild(import.meta);
66690
+ /**
66691
+ * The group a popup opens in, or null: what a Popover or a Dialog reads to know
66692
+ * that its wall is the group's. Every popup provides null again to what it
66693
+ * holds, so a popup opened from inside a popup is in front of it, not a peer.
66694
+ */
65867
66695
  const PopupGroupContext = createContext(null);
65868
66696
 
65869
66697
  /**
@@ -66022,13 +66850,13 @@ const css$F = /* css */`@property --x-popup-group-top {
66022
66850
  * `navi-out-of-flow` like everything navi adds to a tree. See
66023
66851
  * docs/popup_backdrop.md.
66024
66852
  *
66025
- * @type {import("ignore:preact").FunctionComponent<{
66853
+ * @type {import("preact").FunctionComponent<{
66026
66854
  * backdropVariant?: "auto" | "lift" | "discrete" | "invisible",
66027
66855
  * backdropColor?: string,
66028
66856
  * backdropFilter?: string,
66029
66857
  * themeColor?: string | false,
66030
66858
  * role?: string,
66031
- * children?: import("ignore:preact").ComponentChildren,
66859
+ * children?: import("preact").ComponentChildren,
66032
66860
  * [key: string]: any,
66033
66861
  * }>}
66034
66862
  * @param {"auto"|"lift"|"discrete"|"invisible"} [props.backdropVariant="auto"] -
@@ -68075,6 +68903,35 @@ const clipOf = (side, distance) => {
68075
68903
  return `inset(${UNCUT} ${UNCUT} ${UNCUT} ${cut})`;
68076
68904
  };
68077
68905
 
68906
+ /**
68907
+ * The cross that closes the popup it sits in — reached as `<Popup.Close />`,
68908
+ * `<Dialog.Close />` or `<Popover.Close />`. A discrete icon button sending
68909
+ * `--navi-close`, which the nearest popup above it answers; the caller only
68910
+ * decides where it goes (the right end of a header, most of the time).
68911
+ *
68912
+ * It is the way out of a popup, so it is the one affordance in there that the
68913
+ * surrounding control's read-only and disabled do not reach: a read-only picker
68914
+ * still opens (reading changes nothing) and hands its popup that read-only, and
68915
+ * a cross wearing it would trap the user in a popup they were invited to read.
68916
+ * A close button written by hand inherits it and refuses the press — which is
68917
+ * the reason to use this one rather than a `<Button command="--navi-close" />`.
68918
+ *
68919
+ * Exempt from the state around it, never from the popup's own answer: the cross
68920
+ * ASKS, and the popup decides — a dialog holding a form mid-send keeps the
68921
+ * close, a Picker holding a value that does not validate keeps it too.
68922
+ *
68923
+ * @type {import("preact").FunctionComponent<{
68924
+ * label?: string,
68925
+ * iconSize?: number | string,
68926
+ * } & Record<string, any>>}
68927
+ * @param {string} [props.label] - What assistive tech reads for the cross
68928
+ * (navi's own "Close" text by default, in the active language).
68929
+ * @param {number|string} [props.iconSize] - How big the cross is drawn, apart
68930
+ * from the box around it. Unset, the glyph follows the button's font size
68931
+ * like a character, and `size` moves glyph and padding together. Set it when
68932
+ * the cross stands in for an affordance the app draws elsewhere and has to
68933
+ * land on the same pixels: `padding="s" iconSize="m"`.
68934
+ */
68078
68935
  const PopupClose = ({
68079
68936
  label,
68080
68937
  iconSize,
@@ -68323,6 +69180,9 @@ const TitlePseudoClasses = [":hover"];
68323
69180
  * step of a form — so only the title knows when it is there.
68324
69181
  */
68325
69182
 
69183
+ // The popup a title names: the nearest one around it. A surface that is no
69184
+ // popup of this kind but holds content of its own (a picker's callout) sets it
69185
+ // back to null, so a title in there names nothing rather than the popup around.
68326
69186
  const PopupTitleContext = createContext(null);
68327
69187
 
68328
69188
  /**
@@ -68332,10 +69192,10 @@ const PopupTitleContext = createContext(null);
68332
69192
  * announces the popup by it, and `getByRole("dialog", { name })` finds it.
68333
69193
  * An `aria-label`/`aria-labelledby` written on the popup itself wins over it.
68334
69194
  *
68335
- * @type {import("ignore:preact").FunctionComponent<{
69195
+ * @type {import("preact").FunctionComponent<{
68336
69196
  * as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6",
68337
69197
  * id?: string,
68338
- * children?: import("ignore:preact").ComponentChildren,
69198
+ * children?: import("preact").ComponentChildren,
68339
69199
  * } & Record<string, any>>}
68340
69200
  */
68341
69201
  const PopupTitle = props => {
@@ -68396,6 +69256,12 @@ const usePopupName = ({
68396
69256
  const NO_TITLE = [];
68397
69257
 
68398
69258
  installImportMetaCssBuild(import.meta);
69259
+ // Same need popover.jsx has for its own local renderer (see its
69260
+ // openLocalPopoverCount): a layer="local" dialog is a plain positioned element
69261
+ // in normal flow, so it gets none of the top layer's free "last shown wins" and
69262
+ // would paint under anything declared after it. A live count, not an
69263
+ // ever-growing counter, so the order stays small and resets once nothing is
69264
+ // open.
68399
69265
  let openLocalDialogCount = 0;
68400
69266
  const css$C = /* css */`
68401
69267
  /* jsenv-css-opaque: popupCss overlaps these rules, own sheet = cascade
@@ -69470,7 +70336,7 @@ const css$C = /* css */`
69470
70336
  * the dialog: its accessible name when neither `aria-label`/`aria-labelledby`
69471
70337
  * nor a `<Dialog.Title>` names it. A Picker passes its trigger's.
69472
70338
  * @param {string} [props.openerAriaLabel] - The same, as a string.
69473
- * @param {import("ignore:preact").ComponentChildren} props.children
70339
+ * @param {import("preact").ComponentChildren} props.children
69474
70340
  */
69475
70341
  const Dialog = props => {
69476
70342
  import.meta.css = [css$C, "@jsenv/navi/src/layout/dialog.jsx"];
@@ -70306,8 +71172,10 @@ const useDialogProps = props => {
70306
71172
  // gap — the same thing its CSS width/height say (see [data-expand-x],
70307
71173
  // [data-expand-y]) — so the placement derives it instead of measuring
70308
71174
  // it: a measurement here lays out the dialog's whole content, rows and
70309
- // all, for a number already known.
70310
- const fill = expandX && expandY;
71175
+ // all, for a number already known. A cap (maxWidth/maxHeight, a Picker's
71176
+ // anchor ceiling) stretches it to less than that, and only a measured
71177
+ // box can be centred.
71178
+ const fill = expandX && expandY && !isSizeCapped(dialogEl);
70311
71179
  const pickOptions = {
70312
71180
  positionArea,
70313
71181
  container: positionedAncestor,
@@ -70821,6 +71689,14 @@ const switchTransitionsOn = dialogEl => {
70821
71689
  dialogEl.style.transitionProperty = "";
70822
71690
  dialogEl.removeAttribute("navi-transitions-off");
70823
71691
  };
71692
+
71693
+ // Reads style, not layout: an unset cap is the guaranteed-invalid value its
71694
+ // @property gives it, and reads back as "". Catches a cap written inline by
71695
+ // the prop as well as one set from a stylesheet.
71696
+ const isSizeCapped = dialogEl => {
71697
+ const computedStyle = getComputedStyle(dialogEl);
71698
+ return computedStyle.getPropertyValue("--dialog-max-width") !== "" || computedStyle.getPropertyValue("--dialog-max-height") !== "";
71699
+ };
70824
71700
  const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":focus-within"];
70825
71701
 
70826
71702
  // Lets consumers pass animationDuration="0.5s" as a regular prop; Box maps
@@ -70846,6 +71722,11 @@ Dialog.Close = PopupClose;
70846
71722
  Dialog.Title = PopupTitle;
70847
71723
 
70848
71724
  installImportMetaCssBuild(import.meta);
71725
+ // Custom renderer only (layer="local") — count of currently-open instances,
71726
+ // see --popover-stack-order's own CSS comment for why a plain "last shown"
71727
+ // default doesn't happen for free the way it does for the top-layer
71728
+ // renderer. A live count (not an ever-growing counter) so the assigned
71729
+ // order stays small and resets to 0 once nothing is open.
70849
71730
  let openLocalPopoverCount = 0;
70850
71731
  const css$B = /* css */`
70851
71732
  /* jsenv-css-opaque: popupCss overlaps these rules, own sheet = cascade
@@ -71453,7 +72334,7 @@ const css$B = /* css */`
71453
72334
  * something names — these, `aria-label`/`aria-labelledby`, a
71454
72335
  * `<Popover.Title>` — is a `dialog`, unless given another `role`: a name on
71455
72336
  * an element with no role is read by nobody.
71456
- * @param {import("ignore:preact").ComponentChildren} props.children
72337
+ * @param {import("preact").ComponentChildren} props.children
71457
72338
  */
71458
72339
  const Popover = props => {
71459
72340
  import.meta.css = [css$B, "@jsenv/navi/src/layout/popover.jsx"];
@@ -72811,7 +73692,7 @@ const css$A = /* css */`@layer navi {
72811
73692
  * content whose fresh state is its initial state: an edit left unsent at
72812
73693
  * the close does not come back at the next opening (an untouched field
72813
73694
  * needs none of this to show a new `defaultValue` — it follows one).
72814
- * @param {import("ignore:preact").ComponentChildren} props.children
73695
+ * @param {import("preact").ComponentChildren} props.children
72815
73696
  */
72816
73697
  const Popup = props => {
72817
73698
  import.meta.css = [css$A, "@jsenv/navi/src/layout/popup.jsx"];
@@ -74488,6 +75369,18 @@ const PickerConfirmBody = ({
74488
75369
  });
74489
75370
  };
74490
75371
 
75372
+ /**
75373
+ * PickerNaviMinute — a custom minute picker that opens a number input.
75374
+ *
75375
+ * Renders a popover containing an `<input type="number">` pre-filled with the
75376
+ * current value, plus Confirm / Clear / Cancel actions. The picker holds a
75377
+ * number of minutes: its own field is a text one told
75378
+ * `navi-input-type="minute"`, so the value is read back as a number and a
75379
+ * min/max/step message speaks of minutes.
75380
+ *
75381
+ * Usage:
75382
+ * <Picker type="navi_minute" min={15} max={300} step={15} value={90} />
75383
+ */
74491
75384
  const PickerNaviMinute = props => {
74492
75385
  const Next = useNextResolver();
74493
75386
  const {
@@ -74871,6 +75764,16 @@ const LoadingDotsSvg = () => {
74871
75764
  });
74872
75765
  };
74873
75766
 
75767
+ /**
75768
+ * A loading indicator sized to match the surrounding text (1em × 1em).
75769
+ * Inherits color from the current text color by default.
75770
+ *
75771
+ * @param {"circle"|"dots"} [props.variant="circle"] - Visual style of the indicator
75772
+ * @param {string} [props.color="currentColor"] - Color of the indicator; inherits from CSS `color` by default
75773
+ *
75774
+ * Every other prop goes to the `Icon` it is drawn in: `size` is a font size
75775
+ * (the indicator being 1em), `width`/`height` size it as a block.
75776
+ */
74874
75777
  const LoadingIndicator = ({
74875
75778
  variant = "circle",
74876
75779
  ...props
@@ -74933,7 +75836,7 @@ const css$x = /* css */`@layer navi {
74933
75836
  * Everything about how it looks goes through a CSS var, so it can be set per
74934
75837
  * separator as a prop or once for a whole surface in CSS.
74935
75838
  *
74936
- * @type {import("ignore:preact").FunctionComponent<{
75839
+ * @type {import("preact").FunctionComponent<{
74937
75840
  * vertical?: boolean,
74938
75841
  * size?: string|number,
74939
75842
  * color?: string,
@@ -76536,6 +77439,9 @@ const applySearchHighlight = (el, highlight) => {
76536
77439
  };
76537
77440
 
76538
77441
  installImportMetaCssBuild(import.meta);
77442
+ // Everything the list knows about its items — see list_items.js. Filled in by
77443
+ // the children as they render, read by everything that must reserve room for
77444
+ // what is not rendered, count what is, or know what stands above it.
76539
77445
  const ListItemsContext = createContext(null);
76540
77446
  // The group an item is declared in, by id (see ListItemGroup): what its count
76541
77447
  // and its separator are scoped to.
@@ -81108,7 +82014,7 @@ const LIST_ITEM_STYLE_CSS_VARS = {
81108
82014
  * of it. Props not listed here are forwarded to the rendered <li> (Box layout
81109
82015
  * props included).
81110
82016
  *
81111
- * @type {import("ignore:preact").FunctionComponent<{
82017
+ * @type {import("preact").FunctionComponent<{
81112
82018
  * id?: string,
81113
82019
  * selectable?: boolean,
81114
82020
  * value?: any,
@@ -81117,14 +82023,14 @@ const LIST_ITEM_STYLE_CSS_VARS = {
81117
82023
  * pointed?: boolean,
81118
82024
  * selectableArea?: "all" | "manual",
81119
82025
  * skeleton?: boolean,
81120
- * error?: boolean | import("ignore:preact").ComponentChildren,
82026
+ * error?: boolean | import("preact").ComponentChildren,
81121
82027
  * onErrorDismiss?: (event: Event) => void,
81122
82028
  * loading?: boolean | "adding" | "removing" | "updating",
81123
82029
  * readOnly?: boolean,
81124
82030
  * filtered?: boolean,
81125
82031
  * hidden?: boolean,
81126
82032
  * matchInfo?: {match: boolean, matchScore?: number, matchRanges?: Array<[number, number]>},
81127
- * children?: import("ignore:preact").ComponentChildren,
82033
+ * children?: import("preact").ComponentChildren,
81128
82034
  * [key: string]: any,
81129
82035
  * }>}
81130
82036
  * @param {string} [props.id]
@@ -81159,7 +82065,7 @@ const LIST_ITEM_STYLE_CSS_VARS = {
81159
82065
  * real item. This is what a `renderSkeleton` returns for an item on its way;
81160
82066
  * Box layout props (padding…) pass through so the placeholder can match the
81161
82067
  * real items' metrics.
81162
- * @param {boolean|import("ignore:preact").ComponentChildren} [props.error]
82068
+ * @param {boolean|import("preact").ComponentChildren} [props.error]
81163
82069
  * What this item stood for failed: the message replaces its content, styled
81164
82070
  * like the list's own error. `true` shows a generic sentence. When
81165
82071
  * `onErrorDismiss` is given, a dismiss button is drawn next to the message
@@ -81299,21 +82205,21 @@ const VISIBILITY_HIDDEN_STYLE = {
81299
82205
  * Several runs can live in one list, next to plain `<List.Item>` children and
81300
82206
  * inside `<List.Group>`s; each takes its place in declaration order.
81301
82207
  *
81302
- * @type {import("ignore:preact").FunctionComponent<{
81303
- * renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("ignore:preact").ComponentChildren,
82208
+ * @type {import("preact").FunctionComponent<{
82209
+ * renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("preact").ComponentChildren,
81304
82210
  * findText?: (item: any, index: number) => string,
81305
82211
  * items?: any[],
81306
82212
  * itemsAction?: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
81307
82213
  * debounce?: number,
81308
82214
  * count?: number,
81309
82215
  * groupBy?: (item: any, index: number) => any,
81310
- * renderGroupLabel?: (item: any, index: number) => import("ignore:preact").ComponentChildren,
82216
+ * renderGroupLabel?: (item: any, index: number) => import("preact").ComponentChildren,
81311
82217
  * groupLabelProps?: (item: any, index: number) => object,
81312
82218
  * pageSize?: number,
81313
82219
  * memoryBudget?: number,
81314
- * renderSkeleton?: false | ((index: number) => import("ignore:preact").ComponentChildren),
82220
+ * renderSkeleton?: false | ((index: number) => import("preact").ComponentChildren),
81315
82221
  * itemSize?: (index: number) => number,
81316
- * renderError?: (failure: {error: any, retry: () => void, start: number, end: number}) => import("ignore:preact").ComponentChildren,
82222
+ * renderError?: (failure: {error: any, retry: () => void, start: number, end: number}) => import("preact").ComponentChildren,
81317
82223
  * onRequestStateChange?: (state: {busy: boolean, refreshing: boolean, range: {start: number, end: number}|null}) => void,
81318
82224
  * onCountChange?: (count: number|undefined) => void,
81319
82225
  * }>}
@@ -82908,7 +83814,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
82908
83814
  * List — generic virtualized scroll container.
82909
83815
  * Items must use <List.Item> to participate in tracking.
82910
83816
  *
82911
- * @type {import("ignore:preact").FunctionComponent<{
83817
+ * @type {import("preact").FunctionComponent<{
82912
83818
  * selectable?: boolean,
82913
83819
  * multiple?: boolean,
82914
83820
  * deselectable?: boolean,
@@ -82929,16 +83835,16 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
82929
83835
  * scroller?: "self" | "parent" | "document" | Element | {current: Element},
82930
83836
  * hoverWhileScrolling?: boolean,
82931
83837
  * scrollResetOnNavigation?: boolean,
82932
- * fallback?: import("ignore:preact").ComponentChildren,
82933
- * searchFallback?: import("ignore:preact").ComponentChildren,
83838
+ * fallback?: import("preact").ComponentChildren,
83839
+ * searchFallback?: import("preact").ComponentChildren,
82934
83840
  * searchText?: string,
82935
83841
  * searchNoMatchMode?: "remove" | "invisible_and_inert" | "muted",
82936
83842
  * loading?: boolean,
82937
- * loadingFallback?: "skeleton" | "loader" | import("ignore:preact").ComponentChildren,
83843
+ * loadingFallback?: "skeleton" | "loader" | import("preact").ComponentChildren,
82938
83844
  * loadingSkeletonCount?: number,
82939
- * renderSkeleton?: false | ((index: number) => import("ignore:preact").ComponentChildren),
82940
- * error?: boolean | import("ignore:preact").ComponentChildren,
82941
- * separator?: boolean | import("ignore:preact").ComponentChildren,
83845
+ * renderSkeleton?: false | ((index: number) => import("preact").ComponentChildren),
83846
+ * error?: boolean | import("preact").ComponentChildren,
83847
+ * separator?: boolean | import("preact").ComponentChildren,
82942
83848
  * itemTransition?: boolean,
82943
83849
  * lockSize?: boolean,
82944
83850
  * horizontal?: boolean,
@@ -82952,7 +83858,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
82952
83858
  * expandX?: boolean,
82953
83859
  * expandY?: boolean,
82954
83860
  * expand?: boolean,
82955
- * children?: import("ignore:preact").ComponentChildren,
83861
+ * children?: import("preact").ComponentChildren,
82956
83862
  * [key: string]: any,
82957
83863
  * }>}
82958
83864
  * @param {string} [props.columns]
@@ -83012,7 +83918,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
83012
83918
  * have, so nothing moves when they arrive. Used for the items a `<List.Items>`
83013
83919
  * stands for and does not hold yet, and for the placeholder items drawn while
83014
83920
  * the whole list is `loading`. Defaults to a bare `<List.Item skeleton>`.
83015
- * @param {"skeleton"|"loader"|import("ignore:preact").ComponentChildren} [props.loadingFallback="skeleton"]
83921
+ * @param {"skeleton"|"loader"|import("preact").ComponentChildren} [props.loadingFallback="skeleton"]
83016
83922
  * What to display in place of the items while `loading` — that is, while
83017
83923
  * there is nothing to show at all: `"skeleton"` renders
83018
83924
  * `loadingSkeletonCount` placeholder items (look:
@@ -83196,6 +84102,17 @@ const List = /*#__PURE__*/Object.assign(ListResolved, {
83196
84102
  Group: ListItemGroup
83197
84103
  });
83198
84104
 
84105
+ /**
84106
+ * PickerNaviTime — a fully custom time picker driven by min/max/step.
84107
+ *
84108
+ * Unlike the native `<input type="time">`, this picker only exposes the exact
84109
+ * slots generated from min/max/step, so the user can never pick an out-of-range
84110
+ * or out-of-step value. The popover content is a selectable `List` of the
84111
+ * available slots; picking one closes the popover and submits the value.
84112
+ *
84113
+ * Usage:
84114
+ * <Picker type="navi_time" min="08:00" max="18:00" step="00:30" />
84115
+ */
83199
84116
  const PickerNaviTime = props => {
83200
84117
  const Next = useNextResolver();
83201
84118
  const {
@@ -83303,18 +84220,20 @@ const createBadgeRegistry = () => {
83303
84220
  };
83304
84221
 
83305
84222
  installImportMetaCssBuild(import.meta);
83306
- const css$u = /* css */`@layer navi;
84223
+ const css$u = /* css */`@layer navi {
84224
+ .navi_badge {
84225
+ --font-size: .7em;
84226
+ --badge-padding-x-default: .8em;
84227
+ --badge-padding-y-default: .4em;
84228
+ }
84229
+ }
83307
84230
 
83308
84231
  .navi_badge {
83309
- --font-size: .7em;
83310
- --badge-padding-x-default: .8em;
83311
- --badge-padding-y-default: .4em;
83312
84232
  --x-badge-padding-top: var(--badge-padding-top, var(--badge-padding-y, var(--badge-padding, var(--badge-padding-y-default))));
83313
84233
  --x-badge-padding-right: var(--badge-padding-right, var(--badge-padding-x, var(--badge-padding, var(--badge-padding-x-default))));
83314
84234
  --x-badge-padding-bottom: var(--badge-padding-bottom, var(--badge-padding-y, var(--badge-padding, var(--badge-padding-y-default))));
83315
84235
  --x-badge-padding-left: var(--badge-padding-left, var(--badge-padding-x, var(--badge-padding, var(--badge-padding-x-default))));
83316
84236
  --x-background: var(--background, light-dark(#e0e0e0, #3a3a3a));
83317
- --x-background-color: var(--background-color, var(--x-background));
83318
84237
  --x-color: var(--color, white);
83319
84238
  max-width: 200px;
83320
84239
  padding-top: var(--x-badge-padding-top);
@@ -83325,7 +84244,6 @@ const css$u = /* css */`@layer navi;
83325
84244
  font-size: var(--font-size);
83326
84245
  text-box: trim-both cap alphabetic;
83327
84246
  background: var(--x-background);
83328
- background-color: var(--x-background-color);
83329
84247
  border-radius: 1em;
83330
84248
  display: inline;
83331
84249
  position: relative;
@@ -83451,7 +84369,6 @@ const BadgeStyleCSSVars = {
83451
84369
  paddingRight: "--badge-padding-right",
83452
84370
  paddingBottom: "--badge-padding-bottom",
83453
84371
  paddingLeft: "--badge-padding-left",
83454
- backgroundColor: "--background-color",
83455
84372
  background: "--background",
83456
84373
  borderColor: "--border-color",
83457
84374
  color: "--color",
@@ -83625,7 +84542,7 @@ const useBadgeRegistry = (children, enabled) => {
83625
84542
  * shrinkWrap on its own builds the measurement ghost: under maxLines the list
83626
84543
  * is measured in place, once the rows are known.
83627
84544
  *
83628
- * @param {import("ignore:preact").ComponentChildren} [fallback]
84545
+ * @param {import("preact").ComponentChildren} [fallback]
83629
84546
  * Rendered in place of the badges when there is none. Without it an empty
83630
84547
  * list renders nothing at all. In a <Picker> this is the only placeholder
83631
84548
  * the user gets — a picker given a `ui` does not draw its own — so pass the
@@ -84259,10 +85176,10 @@ const PickerArrayUI = /*#__PURE__*/asPickerOwnUI(() => {
84259
85176
  * — a row being read still says what was picked, it just no longer offers to
84260
85177
  * unpick it.
84261
85178
  *
84262
- * @type {import("ignore:preact").FunctionComponent<{
85179
+ * @type {import("preact").FunctionComponent<{
84263
85180
  * value: any,
84264
85181
  * commandFor?: string,
84265
- * children?: import("ignore:preact").ComponentChildren,
85182
+ * children?: import("preact").ComponentChildren,
84266
85183
  * [key: string]: any,
84267
85184
  * }>}
84268
85185
  * @param {any} value The value this chip stands for — one entry of what the
@@ -84528,6 +85445,7 @@ const css$r = /* css */`@layer navi {
84528
85445
  .navi_picker {
84529
85446
  --picker-border-radius: var(--navi-control-border-radius);
84530
85447
  --picker-border-width: var(--navi-control-border-width);
85448
+ --border-width: var(--picker-border-width);
84531
85449
  --picker-outline-width: var(--navi-focus-outline-width);
84532
85450
  --picker-outline-offset: calc(-.5 * var(--picker-outline-width));
84533
85451
  --picker-outline-color: var(--navi-focus-outline-color);
@@ -84567,6 +85485,111 @@ const css$r = /* css */`@layer navi {
84567
85485
  --picker-align-x-default: flex-start;
84568
85486
  --picker-align-y-default: center;
84569
85487
  --picker-text-align-default: initial;
85488
+ font-size: var(--picker-font-size);
85489
+ font-family: var(--picker-font-family);
85490
+ line-height: var(--navi-control-line-height);
85491
+ }
85492
+
85493
+ .navi_picker[data-variant="icon"] {
85494
+ --picker-border-width: 0px;
85495
+ --picker-border-color: transparent;
85496
+ --picker-border-color-hover: var(--picker-border-color);
85497
+ --picker-border-color-readonly: var(--picker-border-color);
85498
+ --picker-border-color-disabled: var(--picker-border-color);
85499
+ --picker-background-color: transparent;
85500
+ --picker-background-color-hover: var(--picker-background-color);
85501
+ --picker-background-color-readonly: var(--picker-background-color);
85502
+ --picker-background-color-disabled: var(--picker-background-color);
85503
+ --picker-icon-color: currentColor;
85504
+ }
85505
+
85506
+ .navi_picker[data-variant="discrete"] {
85507
+ --picker-border-width: 0px;
85508
+ --picker-border-color: transparent;
85509
+ --picker-border-color-hover: var(--picker-border-color);
85510
+ --picker-border-color-readonly: var(--picker-border-color);
85511
+ --picker-border-color-disabled: var(--picker-border-color);
85512
+ --picker-background-color: transparent;
85513
+ --picker-background-color-hover: color-mix(in srgb,
85514
+ currentColor 8%,
85515
+ var(--picker-background-color));
85516
+ --picker-background-color-readonly: var(--picker-background-color);
85517
+ --picker-background-color-disabled: var(--picker-background-color);
85518
+ }
85519
+
85520
+ .navi_picker[data-variant="headless"] {
85521
+ --picker-padding-x-default: 0;
85522
+ --picker-padding-y-default: 0;
85523
+ --picker-border-width: 0px;
85524
+ --picker-border-color: transparent;
85525
+ --picker-outline-width: 0px;
85526
+ --picker-border-color-hover: var(--picker-border-color);
85527
+ --picker-border-color-readonly: var(--picker-border-color);
85528
+ --picker-border-color-disabled: var(--picker-border-color);
85529
+ --picker-background-color: transparent;
85530
+ --picker-background-color-hover: var(--picker-background-color);
85531
+ --picker-background-color-readonly: var(--picker-background-color);
85532
+ --picker-background-color-disabled: var(--picker-background-color);
85533
+ --picker-icon-color: currentColor;
85534
+ }
85535
+
85536
+ .navi_picker[data-variant="bare"] {
85537
+ --picker-padding-x-default: 0;
85538
+ --picker-padding-y-default: 0;
85539
+ --picker-align-y-default: stretch;
85540
+ --picker-border-width: 0px;
85541
+ --picker-border-color: transparent;
85542
+ --picker-border-color-hover: var(--picker-border-color);
85543
+ --picker-border-color-readonly: var(--picker-border-color);
85544
+ --picker-border-color-disabled: var(--picker-border-color);
85545
+ --picker-background-color: transparent;
85546
+ --picker-background-color-hover: var(--picker-background-color);
85547
+ --picker-background-color-readonly: var(--picker-background-color);
85548
+ --picker-background-color-disabled: var(--picker-background-color);
85549
+ --picker-icon-color: currentColor;
85550
+ font-size: inherit;
85551
+ font-family: inherit;
85552
+ line-height: inherit;
85553
+ }
85554
+
85555
+ .navi_picker[data-variant="button"] {
85556
+ --picker-padding-x-default: var(--navi-button-padding-x-default);
85557
+ --picker-padding-y-default: var(--navi-button-padding-y-default);
85558
+ --picker-align-x-default: center;
85559
+ --picker-background-color: var(--navi-button-background-color);
85560
+ --picker-border-color-readonly: color-mix(in srgb,
85561
+ var(--picker-border-color) 30%,
85562
+ var(--navi-surface-color));
85563
+ --picker-background-color-readonly: var(--picker-background-color);
85564
+ --picker-color-readonly: color-mix(in srgb,
85565
+ var(--picker-color) 30%,
85566
+ transparent);
85567
+ --picker-border-color-disabled: var(--picker-border-color-readonly);
85568
+ --picker-background-color-disabled: var(--picker-background-color-readonly);
85569
+ --picker-color-disabled: var(--picker-color-readonly);
85570
+ --picker-text-align-default: center;
85571
+ }
85572
+
85573
+ .navi_picker[data-variant="text"] {
85574
+ --picker-padding-x-default: 0;
85575
+ --picker-padding-y-default: 0;
85576
+ --picker-border-width: 0px;
85577
+ --picker-border-color: transparent;
85578
+ --picker-border-color-hover: var(--picker-border-color);
85579
+ --picker-border-color-readonly: var(--picker-border-color);
85580
+ --picker-border-color-disabled: var(--picker-border-color);
85581
+ --picker-background-color: transparent;
85582
+ --picker-background-color-hover: var(--picker-background-color);
85583
+ --picker-background-color-readonly: var(--picker-background-color);
85584
+ --picker-background-color-disabled: var(--picker-background-color);
85585
+ font-size: inherit;
85586
+ font-family: inherit;
85587
+ }
85588
+
85589
+ .navi_picker[data-icon] {
85590
+ --picker-padding-x-default: 0;
85591
+ --picker-padding-y-default: 0;
85592
+ --picker-align-x-default: center;
84570
85593
  }
84571
85594
  }
84572
85595
 
@@ -84589,10 +85612,7 @@ const css$r = /* css */`@layer navi {
84589
85612
  box-sizing: border-box;
84590
85613
  max-width: 100%;
84591
85614
  color: var(--x-picker-color);
84592
- font-size: var(--picker-font-size);
84593
- font-family: var(--picker-font-family);
84594
85615
  text-align: inherit;
84595
- line-height: var(--navi-control-line-height);
84596
85616
  border-top-left-radius: var(--x-corner-top-left-radius, var(--picker-border-radius));
84597
85617
  border-top-right-radius: var(--x-corner-top-right-radius, var(--picker-border-radius));
84598
85618
  border-bottom-right-radius: var(--x-corner-bottom-right-radius, var(--picker-border-radius));
@@ -84834,48 +85854,7 @@ const css$r = /* css */`@layer navi {
84834
85854
  --x-picker-border-color: var(--callout-color);
84835
85855
  }
84836
85856
 
84837
- &[data-variant="icon"] {
84838
- --picker-border-width: 0px;
84839
- --picker-border-color: transparent;
84840
- --picker-border-color-hover: var(--picker-border-color);
84841
- --picker-border-color-readonly: var(--picker-border-color);
84842
- --picker-border-color-disabled: var(--picker-border-color);
84843
- --picker-background-color: transparent;
84844
- --picker-background-color-hover: var(--picker-background-color);
84845
- --picker-background-color-readonly: var(--picker-background-color);
84846
- --picker-background-color-disabled: var(--picker-background-color);
84847
- --picker-icon-color: currentColor;
84848
- }
84849
-
84850
- &[data-variant="discrete"] {
84851
- --picker-border-width: 0px;
84852
- --picker-border-color: transparent;
84853
- --picker-border-color-hover: var(--picker-border-color);
84854
- --picker-border-color-readonly: var(--picker-border-color);
84855
- --picker-border-color-disabled: var(--picker-border-color);
84856
- --picker-background-color: transparent;
84857
- --picker-background-color-hover: color-mix(in srgb,
84858
- currentColor 8%,
84859
- var(--picker-background-color));
84860
- --picker-background-color-readonly: var(--picker-background-color);
84861
- --picker-background-color-disabled: var(--picker-background-color);
84862
- }
84863
-
84864
85857
  &[data-variant="headless"] {
84865
- --picker-padding-x-default: 0;
84866
- --picker-padding-y-default: 0;
84867
- --picker-border-width: 0px;
84868
- --picker-border-color: transparent;
84869
- --picker-outline-width: 0px;
84870
- --picker-border-color-hover: var(--picker-border-color);
84871
- --picker-border-color-readonly: var(--picker-border-color);
84872
- --picker-border-color-disabled: var(--picker-border-color);
84873
- --picker-background-color: transparent;
84874
- --picker-background-color-hover: var(--picker-background-color);
84875
- --picker-background-color-readonly: var(--picker-background-color);
84876
- --picker-background-color-disabled: var(--picker-background-color);
84877
- --picker-icon-color: currentColor;
84878
-
84879
85858
  & > .navi_picker_box {
84880
85859
  z-index: -1;
84881
85860
  position: absolute;
@@ -84884,23 +85863,6 @@ const css$r = /* css */`@layer navi {
84884
85863
  }
84885
85864
 
84886
85865
  &[data-variant="bare"] {
84887
- --picker-padding-x-default: 0;
84888
- --picker-padding-y-default: 0;
84889
- --picker-align-y-default: stretch;
84890
- --picker-border-width: 0px;
84891
- --picker-border-color: transparent;
84892
- --picker-border-color-hover: var(--picker-border-color);
84893
- --picker-border-color-readonly: var(--picker-border-color);
84894
- --picker-border-color-disabled: var(--picker-border-color);
84895
- --picker-background-color: transparent;
84896
- --picker-background-color-hover: var(--picker-background-color);
84897
- --picker-background-color-readonly: var(--picker-background-color);
84898
- --picker-background-color-disabled: var(--picker-background-color);
84899
- --picker-icon-color: currentColor;
84900
- font-size: inherit;
84901
- font-family: inherit;
84902
- line-height: inherit;
84903
-
84904
85866
  & > .navi_picker_box {
84905
85867
  min-height: 0;
84906
85868
  }
@@ -84935,39 +85897,8 @@ const css$r = /* css */`@layer navi {
84935
85897
  }
84936
85898
  }
84937
85899
 
84938
- &[data-variant="button"] {
84939
- --picker-padding-x-default: var(--navi-button-padding-x-default);
84940
- --picker-padding-y-default: var(--navi-button-padding-y-default);
84941
- --picker-align-x-default: center;
84942
- --picker-background-color: var(--navi-button-background-color);
84943
- --picker-border-color-readonly: color-mix(in srgb,
84944
- var(--picker-border-color) 30%,
84945
- var(--navi-surface-color));
84946
- --picker-background-color-readonly: var(--picker-background-color);
84947
- --picker-color-readonly: color-mix(in srgb,
84948
- var(--picker-color) 30%,
84949
- transparent);
84950
- --picker-border-color-disabled: var(--picker-border-color-readonly);
84951
- --picker-background-color-disabled: var(--picker-background-color-readonly);
84952
- --picker-color-disabled: var(--picker-color-readonly);
84953
- --picker-text-align-default: center;
84954
- }
84955
-
84956
85900
  &[data-variant="text"] {
84957
- --picker-padding-x-default: 0;
84958
- --picker-padding-y-default: 0;
84959
- --picker-border-width: 0px;
84960
- --picker-border-color: transparent;
84961
- --picker-border-color-hover: var(--picker-border-color);
84962
- --picker-border-color-readonly: var(--picker-border-color);
84963
- --picker-border-color-disabled: var(--picker-border-color);
84964
- --picker-background-color: transparent;
84965
- --picker-background-color-hover: var(--picker-background-color);
84966
- --picker-background-color-readonly: var(--picker-background-color);
84967
- --picker-background-color-disabled: var(--picker-background-color);
84968
- font-size: inherit;
84969
85901
  text-underline-offset: .2em;
84970
- font-family: inherit;
84971
85902
  text-decoration: underline dotted;
84972
85903
 
84973
85904
  &[data-hover] {
@@ -84976,10 +85907,6 @@ const css$r = /* css */`@layer navi {
84976
85907
  }
84977
85908
 
84978
85909
  &[data-icon] {
84979
- --picker-padding-x-default: 0;
84980
- --picker-padding-y-default: 0;
84981
- --picker-align-x-default: center;
84982
-
84983
85910
  & > .navi_picker_box {
84984
85911
  min-height: 0;
84985
85912
  }
@@ -85414,13 +86341,13 @@ const PickerButton = props => {
85414
86341
  * It goes when there is nothing to take out and when nothing can be changed, so
85415
86342
  * a caller places it once and never asks whether to draw it.
85416
86343
  *
85417
- * @type {import("ignore:preact").FunctionComponent<{
86344
+ * @type {import("preact").FunctionComponent<{
85418
86345
  * size?: number | string,
85419
- * children?: import("ignore:preact").ComponentChildren,
86346
+ * children?: import("preact").ComponentChildren,
85420
86347
  * } & Record<string, any>>}
85421
86348
  * @param {number|string} [size="inherit"] How big the cross is drawn, as
85422
86349
  * `<Icon size>` reads it — "inherit" takes the size of the text around it.
85423
- * @param {import("ignore:preact").ComponentChildren} [children] The icon, in place of
86350
+ * @param {import("preact").ComponentChildren} [children] The icon, in place of
85424
86351
  * the cross. Anything else goes to the `<Button>` underneath (`variant`,
85425
86352
  * `paddingX`, `aria-label`, …).
85426
86353
  */
@@ -85751,6 +86678,580 @@ const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
85751
86678
  ColorSvg
85752
86679
  });
85753
86680
 
86681
+ /**
86682
+ * Button-like trigger that opens a picker (native or custom popup) when clicked.
86683
+ *
86684
+ * Without `children`, opens the browser-native picker for the given `type`.
86685
+ * With `children`, opens a popover (desktop) or dialog (mobile) containing the children.
86686
+ * Pass `mode="popover"` or `mode="dialog"` to override the automatic choice.
86687
+ * Either way the popup is a `dialog`, named by a `<Popup.Title>` in the
86688
+ * children (see `popupAriaLabelledBy`), and the trigger answers to `combobox` —
86689
+ * `button` under `picksNothing` — with `aria-haspopup="dialog"` and
86690
+ * `aria-expanded` (see docs/testid.md).
86691
+ *
86692
+ * `type="confirm"` is the picker that asks "are you sure?" before doing what it
86693
+ * stands for: its popup is the question, `ui` its label, and yes runs its
86694
+ * `action` and triggers its `command` once the popup has closed (the command
86695
+ * follows the action, and only if it succeeded) — see
86696
+ * picker_confirm.jsx, and `message`, `confirmLabel`, `cancelLabel`,
86697
+ * `focusOnOpen` below.
86698
+ *
86699
+ * @type {import("preact").FunctionComponent<{
86700
+ * type?: "date" | "month" | "week" | "time" | "datetime" | "duration" | "color" | "file" | "text" | "object" | "array" | "confirm" | "navi_time" | "navi_minute" | "navi_number" | "navi_percentage",
86701
+ * value?: any,
86702
+ * defaultValue?: any,
86703
+ * name?: string,
86704
+ * placeholder?: import("preact").ComponentChildren,
86705
+ * ui?: import("preact").ComponentChildren | "default" | import("preact").ComponentType<{ value: any, loading: boolean, interactive: boolean }>,
86706
+ * required?: boolean,
86707
+ * min?: Date | string | number,
86708
+ * max?: Date | string | number,
86709
+ * step?: string | number,
86710
+ * disabled?: boolean,
86711
+ * readOnly?: boolean,
86712
+ * openWhileReadOnly?: boolean,
86713
+ * error?: boolean | string,
86714
+ * uiAction?: (value: any, event: Event) => void,
86715
+ * action?: (value: any, { event, signal }: { event: Event, signal: AbortSignal }) => void,
86716
+ * children?: import("preact").ComponentChildren,
86717
+ * mode?: "popover" | "dialog" | "callout",
86718
+ * openOn?: "press" | "longpress" | "contextmenu" | string | string[],
86719
+ * animation?: boolean | "auto" | "fading" | "scaling" | "sliding" | "lifting" | `slide-from-${string}` | { open: boolean | "auto" | "fading" | "scaling" | "sliding" | `slide-from-${string}`, close: "lifting" },
86720
+ * lift?: "box" | "scene",
86721
+ * liftAnchor?: Element | { current: Element } | string,
86722
+ * animationDuration?: string,
86723
+ * calloutStatus?: "info" | "warning" | "error" | "success" | "none",
86724
+ * calloutIcon?: boolean,
86725
+ * calloutCloseButton?: boolean,
86726
+ * calloutCloseByScroll?: boolean | number,
86727
+ * popoverMode?: "nearby" | "overlay",
86728
+ * positionArea?: string,
86729
+ * popupWidthFitContent?: boolean,
86730
+ * loadingOutline?: "custom",
86731
+ * loadingOutlineInset?: number,
86732
+ * variant?: "icon" | "circle" | "headless" | "discrete" | "button" | "text" | "bare" | "picker",
86733
+ * icon?: boolean,
86734
+ * alignX?: "start" | "center" | "end",
86735
+ * alignY?: "start" | "center" | "end" | "stretch",
86736
+ * rightSlotIcon?: import("preact").ComponentChildren,
86737
+ * rightSlotIconSize?: number | string,
86738
+ * rightSlot?: import("preact").ComponentChildren,
86739
+ * chevron?: "end" | "start" | "hang-end",
86740
+ * clearConfirm?: string | import("preact").ComponentChildren,
86741
+ * message?: import("preact").ComponentChildren,
86742
+ * confirmLabel?: import("preact").ComponentChildren,
86743
+ * cancelLabel?: import("preact").ComponentChildren,
86744
+ * focusOnOpen?: "confirm" | "cancel" | "none",
86745
+ * onConfirm?: (confirmEvent: CustomEvent) => void,
86746
+ * onOpen?: (openEvent: CustomEvent) => void,
86747
+ * onClose?: (closeEvent: CustomEvent) => void,
86748
+ * maxLines?: number,
86749
+ * slotSpacing?: number | string,
86750
+ * popoverMaxHeight?: number | string,
86751
+ * dialogMinWidth?: number | string,
86752
+ * dialogMinHeight?: number | string,
86753
+ * dialogMaxWidth?: number | string,
86754
+ * dialogMaxHeight?: number | string,
86755
+ * popupBackgroundColor?: string,
86756
+ * popupBorderRadius?: number | string,
86757
+ * popupBoxShadow?: string,
86758
+ * dialogBorderWidth?: number | string,
86759
+ * clearable?: boolean,
86760
+ * picksNothing?: boolean,
86761
+ * standalone?: boolean,
86762
+ * popupLayer?: "top" | "local",
86763
+ * popupTestId?: string,
86764
+ * popupAriaLabel?: string,
86765
+ * popupAriaLabelledBy?: string,
86766
+ * popupAriaDescribedBy?: string,
86767
+ * confirmTestId?: string,
86768
+ * cancelTestId?: string,
86769
+ * mount?: "always" | "idle" | "from-first-open" | "while-opened",
86770
+ * dialogExpand?: boolean,
86771
+ * dialogExpandX?: boolean,
86772
+ * dialogExpandY?: boolean,
86773
+ * dialogSizeFromAnchor?: boolean,
86774
+ * dockedOnSmallTouchScreen?: boolean | "top" | "bottom",
86775
+ * marginWithContainer?: number | string,
86776
+ * anchor?: import("preact").RefObject<HTMLElement> | HTMLElement,
86777
+ * escapeEffect?: "cancel" | "close",
86778
+ * pressOutside?: "close" | "cancel" | "capture",
86779
+ * backdrop?: boolean,
86780
+ * backdropVariant?: "auto" | "lift" | "discrete" | "invisible",
86781
+ * backdropColor?: string,
86782
+ * backdropFilter?: string,
86783
+ * ref?: import("preact").RefObject<HTMLElement>,
86784
+ * [key: string]: any,
86785
+ * }>}
86786
+ * @param {string} [popupBackgroundColor] The popup's surface. Left out it is
86787
+ * the one every navi popup uses (`--navi-popup-background-color`, which
86788
+ * follows the theme) — the trigger's own `backgroundColor` is not it, so a
86789
+ * variant that paints the trigger transparent leaves the popup a real sheet.
86790
+ * @param {number|string} [popupBorderRadius] The popup's corners. Left out they
86791
+ * echo the trigger's `borderRadius`.
86792
+ * @param {string} [popupBoxShadow] The popup's shadow, popover or dialog. Left
86793
+ * out it is the one every navi popup casts. `"none"` for a popup that is
86794
+ * not a sheet — a card lifted out of a page draws its own edges, and the
86795
+ * popup only carries it (see `dialogSizeFromAnchor`).
86796
+ * @param {"press"|"longpress"|"contextmenu"|string|string[]} [openOn="press"]
86797
+ * What opens the popup. The press, by default — the way a native select
86798
+ * opens. Any other name is an interaction navi detects (see
86799
+ * docs/interactions.md: `"longpress"`, `"contextmenu"`, `"double_click"`…), or
86800
+ * a list of them — `["longpress", "contextmenu"]` is the hold on a phone and
86801
+ * the right click at a desk. The picker declares it on itself, so it is
86802
+ * arbitrated like any other gesture: a swipe on the same card takes the
86803
+ * press a hold would have taken. A tap then opens nothing — the press stays
86804
+ * free for what the `ui` holds: a link in the card navigates, a button in it
86805
+ * presses, a picker in it opens on its own click, with nothing to declare
86806
+ * (`selfInteractions` is for a drawing that DOES open on the press). The
86807
+ * trigger says so under the mouse too: it drops the hand cursor, which is
86808
+ * the press's promise, and every pixel of the drawing keeps the cursor it
86809
+ * would have on its own. That is what lets a whole card be a picker without
86810
+ * every touch on it opening the sheet — and the keyboard keeps its ways in
86811
+ * (Enter, Space, the arrows). A hold declared INSIDE the card (`interactions={{ longpress }}`
86812
+ * on something it holds) answers before this one: the nearer hold takes the
86813
+ * press, the way a click is the innermost target's. Being a gesture, the open goes through
86814
+ * the interaction gate as one: a `readOnly` picker refuses it and says so
86815
+ * where the finger is, whatever `openWhileReadOnly` says.
86816
+ * @param {number} [maxLines=1] How many lines the value may take before it is
86817
+ * cut with an ellipsis — `Text`'s `maxLines`, put on the value box, and
86818
+ * granted to a `BadgeList` in the `ui`, which caps its rows to it (see
86819
+ * max_lines_context.js). `variant="text"` and `variant="bare"` are not
86820
+ * clamped: their drawing is the caller's own, a `Text` in the `ui` cuts
86821
+ * itself with its own `maxLines`. See docs/typography.md.
86822
+ * @param {import("preact").ComponentChildren | "default" | import("preact").ComponentType} [ui] What the
86823
+ * trigger draws in place of the value's default rendering (a date, a list
86824
+ * joined by commas…) — and then all it draws: `placeholder` is not shown
86825
+ * next to it, and an empty value is not greyed as a placeholder either: what
86826
+ * this draws when the picker holds nothing may well be an answer ("no
86827
+ * filter", "anywhere"), so its empty look is the caller's. A `ui` must
86828
+ * therefore say on its own that the picker is empty, and take the same room
86829
+ * empty as it does filled. A <BadgeList> gets both from its `fallback` — the
86830
+ * placeholder text, as plain text, see docs/typography.md#badgelist.
86831
+ * `"default"` draws nothing in the slot at all.
86832
+ *
86833
+ * Under `variant="icon"` there is no value to draw, so this IS the trigger:
86834
+ * left out it is the icon the right slot would have shown (`rightSlotIcon`,
86835
+ * or the chevron), and given it is drawn instead — one icon of the caller's
86836
+ * own, and nothing else.
86837
+ *
86838
+ * Under `variant="bare"` it is the trigger as well, and the picker adds
86839
+ * nothing around it: whatever this draws is what the picker measures. Its
86840
+ * own padding, its own lines, its own corners — a `<Box>` here gives a
86841
+ * picker the exact box that `<Box>` has on its own. Anything interactive
86842
+ * inside it takes its clicks back with `data-self-interactions`, the press
86843
+ * everywhere else opening the popup. There is no right slot beside it either,
86844
+ * so what a field would have got from navi this drawing places itself: a
86845
+ * `<Picker.Clear />` where the cross belongs, laid out like anything else it
86846
+ * holds. What the picker catches the press over is that box and no more, so
86847
+ * a drawing that paints outside it on purpose — an affordance in the margin
86848
+ * beside it, out of the flow so the rest keeps its width — is drawn and
86849
+ * opens nothing; `pressPadding` is how far the press area follows it out
86850
+ * there. The chevron itself is the `chevron` prop's to draw and place.
86851
+ *
86852
+ * A drawing that follows what the picker HOLDS — the value the popup just
86853
+ * closed on, before the server has answered, the same state navi's own
86854
+ * default `ui` reads, rolled back if the action fails (`resetOnError`) —
86855
+ * reads it one of two ways. As a component, `ui={MyCard}`: the picker
86856
+ * renders it and hands it `value`, `loading` and `interactive` as props.
86857
+ * As an element, `ui={<MyCard game={game} />}`, with `usePickerState()`
86858
+ * inside it returning the same three — for a drawing that needs props of
86859
+ * its own as well. Either way a card shows the score the moment the sheet
86860
+ * leaves and takes it back on a refusal, with nothing held by the caller.
86861
+ * `loading` is the wait it wears meanwhile: navi draws its loading outline
86862
+ * on every trigger, two pixels out of the box. On a card-sized
86863
+ * `variant="bare"` that run sits on the card's own border and reads as a
86864
+ * border blinking; `loadingOutlineInset` moves it clear of it. And when the
86865
+ * picker's box is not the shape of what waits, `loadingOutline="custom"`
86866
+ * lets the drawing draw the wait itself, from that flag or from
86867
+ * `[data-loading]` on the picker root in css.
86868
+ * @param {boolean} [readOnly] Nothing in this picker can be changed — and it
86869
+ * still opens, so what is in the popup can be read: everything in there is
86870
+ * held read-only in turn, each control greying out and saying why on its own.
86871
+ * Which is the point of opening it at all: a picker's answer often exists
86872
+ * only in the shape the popup draws (a plan with one tile ringed, a wheel
86873
+ * stopped on a time), and the trigger's one line is a summary of it, not the
86874
+ * whole of it. The clear cross goes, closing commits nothing, and a `value`
86875
+ * the popup would otherwise have confirmed stays a suggestion.
86876
+ * A picker with no `children` opens the browser's own picker instead, and
86877
+ * that one cannot be held read-only — so it goes on refusing (see
86878
+ * PickerNative).
86879
+ * @param {boolean} [openWhileReadOnly=true] Pass false to make `readOnly`
86880
+ * refuse the open as well, for a popup with nothing to read: a form of
86881
+ * controls to fill in, a menu of gestures. Opening then only shows what is
86882
+ * refused, so the picker says why on the trigger instead — where the
86883
+ * interaction happened. A busy picker follows the same rule: it still
86884
+ * opens to be read while its action runs, and with `false` the press is
86885
+ * refused with the wait as its reason — what a card that is syncing wants
86886
+ * for the sheet that would write it again.
86887
+ * @param {boolean|string} [error] Something went wrong around this picker (its
86888
+ * content failed to load, its value could not be resolved…). Shown as a
86889
+ * callout on the trigger, open or closed — the caller has nothing to place.
86890
+ * Dismissing it discards that error; a new `error` value raises another one.
86891
+ * @param {string} [id] What the popup's open state is kept under in the
86892
+ * history entry: a screen left and come back to finds the picker open, and
86893
+ * in dialog mode the opening is an entry of its own, closed by the back
86894
+ * button before the screen is left. Left out, the key is a generated id,
86895
+ * which names one mount: the state survives neither leaving the screen nor a
86896
+ * reload. A picker whose popup leads somewhere (a link inside it) has one.
86897
+ * @param {"popover"|"dialog"|"callout"} [mode] Which popup the children open
86898
+ * in. Left out, a popover on a large screen and a dialog on a narrow one.
86899
+ * `"callout"` shows them in the picker's own callout — the speech bubble its
86900
+ * constraints speak in, with its status icon, its cross and its arrow on
86901
+ * the trigger — for a tooltip that opens on a press. It closes the way a
86902
+ * callout does: its cross, Escape, a click outside.
86903
+ * @param {"info"|"warning"|"error"|"success"|"none"} [calloutStatus]
86904
+ * `mode="callout"`: what the callout says about what it holds — its border,
86905
+ * its icon, and whether a click outside closes it (an "error" stays).
86906
+ * `"none"` is a plain tooltip: no icon, a neutral frame. "info" by default,
86907
+ * "none" under `variant="text"`. The trigger's default icon is the
86908
+ * callout's own status icon in that status (see `CalloutStatusIcon`) — left
86909
+ * out, the trigger is that icon; `variant="circle"` draws it round. The callout's arrow points at
86910
+ * the middle of the trigger (`data-callout-arrow-x="center"` unless the
86911
+ * picker says otherwise).
86912
+ * @param {boolean} [calloutIcon] `mode="callout"`: whether the callout shows
86913
+ * its status icon beside what it holds. On by default, off under
86914
+ * `variant="text"` — a word asks for a plain bubble — and never shown
86915
+ * without a status, whatever this says.
86916
+ * @param {boolean} [calloutCloseButton=true] `mode="callout"`: pass false to
86917
+ * take the cross off the callout. It still closes on Escape, a click
86918
+ * outside, and a `--navi-close` from what it holds — for a tooltip that is
86919
+ * read rather than dismissed. Off by nobody's default: a text trigger
86920
+ * keeps the cross unless told not to.
86921
+ * @param {boolean|number} [calloutCloseByScroll=false] `mode="callout"`: close
86922
+ * the callout once the page has scrolled under it — `true` for a short
86923
+ * distance, a number for that many pixels. For a tooltip read in place,
86924
+ * which scrolling away is done with.
86925
+ * @param {"nearby"|"overlay"} [popoverMode="nearby"] "overlay" lays the popover
86926
+ * over the trigger, "nearby" leaves a small gap below it.
86927
+ * @param {string} [positionArea] Where the popup goes — relative to the trigger
86928
+ * in popover mode, relative to the viewport in dialog mode. Same grammar as
86929
+ * Popover/Dialog's own `positionArea` ("top", "right-end", "inset(top-left)",
86930
+ * …). Defaults to "bottom-start" in popover mode ("inset(top-left)" when
86931
+ * popoverMode is "overlay"), and to Dialog's own "center" in dialog mode. A
86932
+ * popover still flips to the opposite side on its own when there isn't
86933
+ * enough room.
86934
+ * @param {boolean} [popupWidthFitContent] By default the popover is at least as
86935
+ * wide as the trigger. Set this to let the content size it instead, so a
86936
+ * popover narrower than the trigger stays narrow. Popover mode only — a
86937
+ * dialog never takes the trigger's width to begin with (see
86938
+ * `dialogMinWidth`).
86939
+ * @param {import("preact").ComponentChildren} [rightSlotIcon] What the right
86940
+ * slot draws in place of the chevron. It is the whole slot, not an addition
86941
+ * to it: the picker then no longer says on its own that it opens, so pass
86942
+ * something that does. Decoration only — it is wrapped in an `<Icon>`, which
86943
+ * is `aria-hidden`, so anything focusable in there would be reachable by tab
86944
+ * while invisible to assistive tech. Use `rightSlot` for that.
86945
+ *
86946
+ * The slot only exists beside a value. `variant="icon"` has none, so there is
86947
+ * no slot either: the icon becomes the trigger itself and is drawn as the
86948
+ * `ui` — which is also what a caller overrides to draw their own. Nothing
86949
+ * else is drawn there, the clear cross included: one icon has room for one
86950
+ * thing.
86951
+
86952
+ * `variant="bare"` has no slot either, for the same reason turned around: the
86953
+ * picker is the drawing's box to the pixel, so nothing may be added beside it
86954
+ * — the drawing holds what it needs (`<Picker.Clear />`) and places it. Its
86955
+ * `chevron` prop is the exception: the slot, put where the drawing is not.
86956
+ * @param {"end"|"start"|"hang-end"} [chevron] `variant="bare"` only: the
86957
+ * picker draws no chevron beside the drawing, so nothing says it opens; this
86958
+ * draws one — the icon the right slot would hold, `rightSlotIcon` or the
86959
+ * chevron, at `rightSlotIconSize`, `slotSpacing` away from the drawing (xxs
86960
+ * by default), taking the clear cross's place under `clearable` like any
86961
+ * slot — and says where, so the drawing keeps the box it has when nothing
86962
+ * opens. Which side depends on where the drawing is anchored in its line.
86963
+ * `"end"`: after it, in the flow — a label with room after it. `"start"`:
86964
+ * before it — a label pushed against the right edge, which the chevron
86965
+ * would otherwise push left by its width. `"hang-end"`: after it and OUT of
86966
+ * the flow, in the margin beside the box — a drawing filling its whole
86967
+ * line; the press follows it out there by its size plus its gap, on top of
86968
+ * any `pressPaddingRight`. The chevron is drawn in `--picker-icon-color`
86969
+ * (`currentColor` here) and `--picker-icon-color-hover`; a dimmed one is
86970
+ * `--picker-icon-color: color-mix(in srgb, currentColor 55%, transparent)`.
86971
+ * @param {import("preact").ComponentChildren} [rightSlot] Same place, rendered
86972
+ * as-is: no `<Icon>` around it, nothing `aria-hidden`. This is where an
86973
+ * interactive right slot goes. `null` takes the slot away entirely — the
86974
+ * gutter it carries with it, so the trigger measures its drawing and centers
86975
+ * on it.
86976
+ * @param {boolean} [picksNothing] The popup asks something rather than holding
86977
+ * an answer, so the trigger draws a label and never a value: no chevron, no
86978
+ * clear cross, in any variant — and the controls in the popup are their own
86979
+ * (a checkbox gating the yes), never the picker's value, so a bound `action`
86980
+ * keeps the params it was bound with. `type="confirm"` says it for itself; say it
86981
+ * for a popup of your own that is a menu of actions rather than a choice —
86982
+ * or a DOOR: a drawing that grows to be looked at (a weather scene, a plan),
86983
+ * with no `action` at all. The picker is then a trigger and a popup, and
86984
+ * nothing more: its close does nothing, and what it keeps from being a
86985
+ * control is what a door still needs — the role of a button, the keyboard
86986
+ * opening it, the anchor being itself, and one popup per instance without
86987
+ * an id to wire. See "The trigger wears the wait" in docs/popup_open.md for
86988
+ * when that beats a `Button` opening a shared `Dialog`.
86989
+ * @param {string|import("preact").ComponentChildren} [clearConfirm] The
86990
+ * question asked before the clear cross clears anything — the `message` of
86991
+ * the `<Picker type="confirm">` the cross then is, plain text or JSX. Asked
86992
+ * BEFORE the ui state is emptied, so answering no leaves the field exactly
86993
+ * as it was; answering yes clears and sends, and the picker's own action
86994
+ * receives the cleared value like any other choice. A cut (Ctrl+X) on such a
86995
+ * picker copies the value and leaves it in place: no clear without the
86996
+ * question.
86997
+ * @param {"icon"|"headless"|"discrete"|"button"|"text"|"bare"|"picker"} [variant]
86998
+ * How the trigger is drawn. `"button"` is a Button's drawing — surface,
86999
+ * padding, a centered label, no chevron — and what a `type="confirm"` picker
87000
+ * draws unless told otherwise: nothing is picked, the popup is a question.
87001
+ * `"text"` is a word in a sentence, underlined with dots, no box and no
87002
+ * slot: what `mode="callout"` uses for a callout on a term rather than on an
87003
+ * icon (`"icon"` being its default, with an info icon in place of the
87004
+ * chevron). `"bare"` draws no trigger of its own at all: the `ui` is it, and
87005
+ * the picker is exactly that drawing's box — no padding, no frame, no
87006
+ * control line, no clamp — for a picker that is a whole piece of a layout
87007
+ * rather than a field, and which must measure the same as the same drawing
87008
+ * placed anywhere else. No right slot comes with it: a cross belongs inside
87009
+ * that drawing, at the place and the size the drawing decides, so it is
87010
+ * `<Picker.Clear />` the `ui` holds rather than `clearable` the picker
87011
+ * takes. What navi still draws on that box — the focus ring, the loading
87012
+ * outline — follows the picker's `borderRadius`, which stays the control's
87013
+ * unless told: a drawing with corners of its own states them on the picker
87014
+ * as well. `"headless"` draws nothing either, and is not the `ui`'s box but
87015
+ * its parent's: it stretches to whatever positioned element holds it, for a
87016
+ * picker put inside the thing that opens it (a button, a row) so that the
87017
+ * popup hangs off that whole element. A picker opened from something it does
87018
+ * not sit in — an object on a map saying
87019
+ * `triggerNaviCommand(pickerEl, "--navi-open", event, { anchor, value })` —
87020
+ * therefore wants an element of its own to stretch into rather than the
87021
+ * nearest positioned ancestor it happens to find. `"picker"` (or an explicit
87022
+ * `variant={undefined}`) asks a confirm or callout picker for the field-like
87023
+ * drawing every other picker has.
87024
+ * @param {"custom"} [loadingOutline] Who draws the loading outline while the
87025
+ * action runs. Left unsaid, the picker does, around its own box. `"custom"`
87026
+ * says someone else does — what it means on a `Button` too — for a picker
87027
+ * that carries a wait drawn elsewhere: two nested pickers sharing one run
87028
+ * (a score picker drawn inside the card an edit picker wraps, see
87029
+ * `12_picker_card_demo.html`) are one wait, and one outline; and a
87030
+ * `variant="bare"` whose box is not
87031
+ * the shape of what waits (a card with a note pinned under it) leaves the
87032
+ * outline to the card, which knows its own box. The wait itself is still
87033
+ * worn: `aria-busy`, the busy refusal, the error callout, `[data-loading]`.
87034
+ * @param {number} [loadingOutlineInset=-2] Where the loading outline runs, in
87035
+ * px from the picker's box — negative is outside. A field wants it right on
87036
+ * its border; a card-sized trigger wants it a few pixels out, clear of the
87037
+ * card's own frame, so it reads as a run around the card rather than as its
87038
+ * border blinking.
87039
+ * @param {boolean} [icon] The trigger is one icon — the `ui`, or the chevron
87040
+ * when there is none. Its box is that icon's box plus the padding asked for
87041
+ * and nothing else: no right slot, and no control line to be as tall as, so
87042
+ * it measures the same as `<Button icon>` and stands in a row of icon
87043
+ * buttons. A shape, not a paint: the variant still decides what is drawn
87044
+ * around it, so `variant="discrete" icon` is the discrete hover wash on an
87045
+ * icon-sized square. `variant="icon"` is this shape with no surface at all.
87046
+ * @param {import("preact").ComponentChildren} [message] `type="confirm"`: the
87047
+ * question, in the popup's default body — text, or JSX when it needs an
87048
+ * emphasis, a link. Left out, a generic "are you sure?" in the current
87049
+ * language. To draw the whole popup instead, pass `children`: yes is then
87050
+ * whatever carries `--navi-confirm`, no whatever closes (`--navi-cancel`).
87051
+ * @param {import("preact").ComponentChildren} [confirmLabel] `type="confirm"`,
87052
+ * default body: what the yes button reads. Defaults to the translated
87053
+ * "Confirm".
87054
+ * @param {import("preact").ComponentChildren} [cancelLabel] Same, for the no
87055
+ * button. Defaults to the translated "Cancel".
87056
+ * @param {"confirm"|"cancel"|"none"} [focusOnOpen="confirm"] `type="confirm"`,
87057
+ * default body: which button the keyboard lands on when the popup opens.
87058
+ * `"cancel"` is the careful choice before something that cannot be undone —
87059
+ * a stray Enter then answers no.
87060
+ * @param {(confirmEvent: CustomEvent) => void} [onConfirm] Called once the
87061
+ * popup has closed on a `--navi-confirm` said inside it. What a confirm
87062
+ * picker uses to run its press; a picker of any type may listen too.
87063
+ * @param {boolean} [canSendWhileUnchanged] Run `action` when a choice pressed
87064
+ * inside the popup — a row's `--navi-send`, a `--navi-confirm` — is the
87065
+ * value the picker already held. By default that close runs nothing: the
87066
+ * answer has not moved, so there is nothing new to say, same as `Form`.
87067
+ * Set this for a picker whose value is a setting that can be valid while
87068
+ * what it points to is not — a channel whose account came unlinked — so
87069
+ * picking it again is how the person asks for it to be put right, and the
87070
+ * action sees the same value and re-asks what there is to re-ask. A press
87071
+ * outside, Escape and the cross still send nothing: nothing was pressed.
87072
+ * @param {(openEvent: CustomEvent) => void} [onOpen] Called when the popup
87073
+ * opens, whatever asked for it (a press, a command, history bringing it
87074
+ * back). You most likely do NOT need this pair: "the user validated" is
87075
+ * already `action` — it fires on every close that keeps (click outside, the
87076
+ * cross, Enter, a `--navi-send` inside the popup) and never on a cancel.
87077
+ * Reach for `onOpen`/`onClose` only for the one thing `action` cannot say:
87078
+ * the popup being up at all, independent of the value — e.g. a derived
87079
+ * computation held while the sheet is open, released on close.
87080
+ * @param {(closeEvent: CustomEvent) => void} [onClose] Called when the popup
87081
+ * actually closes — not preventable, and (see `onOpen`) not the way to know
87082
+ * the user validated: that is `action`. `closeEvent.detail.isCancel` tells a
87083
+ * cancel (Escape, the back gesture) from a close that keeps. Fires after the
87084
+ * picker's value has settled — committed, or restored on a cancel — so the
87085
+ * value read in here is the one the picker ends up holding, and before the
87086
+ * popup leaves the history entry it pushed, so a route param cleared in here
87087
+ * is part of the url a close that keeps carries back.
87088
+ * @param {number|string} [rightSlotIconSize="inherit"] How big what sits in the
87089
+ * right slot is drawn — the chevron, a `rightSlotIcon`, or the clear button's
87090
+ * cross. "inherit" takes the picker's own font size.
87091
+ * @param {number|string} [slotSpacing] Gap kept between what sits in the right
87092
+ * slot (the chevron, or the clear button) and the picker's own edge — same
87093
+ * prop name Input uses for its own slots. Accepts a spacing token ("s",
87094
+ * "m"…) like any other spacing prop, or a length. Defaults to half the
87095
+ * horizontal padding.
87096
+ * @param {number|string} [pressPadding] How far a press counts as the trigger
87097
+ * past the picker's own box — a spacing token ("s", "m"…) or a length, zero
87098
+ * by default. What the picker draws is transparent to the pointer: the press
87099
+ * lands on an invisible input covering the box, so the box is the whole
87100
+ * answer to "does this open". That is right while what is drawn stays inside
87101
+ * it, which is what `variant="bare"` measures. A `ui` that paints OUTSIDE
87102
+ * its box on purpose — a chevron pushed into the margin beside it, so the
87103
+ * label under it keeps the width it has when nothing opens — draws something
87104
+ * nobody can press, and this is how far the press area goes to reach it. The
87105
+ * press area only; nothing moves and nothing is drawn. Font-relative lengths
87106
+ * are read against the picker's own text, so "0.75em" is the drawing's em.
87107
+ * Only the sides that overhang: the area grows over whatever is next to the
87108
+ * picker, and takes the presses aimed at it.
87109
+ * @param {number|string} [pressPaddingX] Same, left and right only;
87110
+ * `pressPaddingY` top and bottom, `pressPaddingTop`/`Right`/`Bottom`/`Left`
87111
+ * one side each.
87112
+ * @param {number|string} [popoverMaxHeight] Soft cap on the popover's height
87113
+ * (default 300px). The popover shrinks below it when space is tight.
87114
+ * @param {number|string} [dialogMinWidth] Floor on the dialog's width. A dialog
87115
+ * picker is not attached to its trigger and does not take its width (unlike
87116
+ * the popover, which uses it as a floor) — this is how a dialog too narrow
87117
+ * for what it holds is widened. Clamped by `dialogMaxWidth`/the container.
87118
+ * @param {number|string} [dialogMinHeight] Same, on the height.
87119
+ * @param {number|string} [dialogMaxWidth] Ceiling on the dialog's width, the
87120
+ * one `dialogExpand`/`dialogExpandX` grows up to — what makes an expanded
87121
+ * dialog a large sheet rather than a full screen. Capped in turn by the
87122
+ * container minus `marginWithContainer`. Describes the centered shape only:
87123
+ * a dialog docked by `dockedOnSmallTouchScreen` withdraws it and stays
87124
+ * container-wide, so both can be stated at once.
87125
+ * @param {number|string} [dialogMaxHeight] Same, on the height.
87126
+ * @param {boolean|"auto"|"fading"|"scaling"|"sliding"|"lifting"|`slide-from-${string}`} [animation]
87127
+ * The popup's own (see Popover/Dialog). `"lifting"` is the one a picker
87128
+ * whose `ui` is a whole piece of the page wants: the popup is the browser
87129
+ * morphing the trigger's box into what carries `data-lift` inside the popup
87130
+ * (the popup itself when it carries it), and back on close. The trigger IS
87131
+ * the anchor, so nothing has to be named for it — `dialogSizeFromAnchor`
87132
+ * for a card that must keep its width on the way, left out for a drawing
87133
+ * that opens precisely to get bigger. It brings an opaque, blurred backdrop
87134
+ * with it (`--navi-backdrop-lift-*`); `backdropVariant="discrete"` asks for
87135
+ * the light wash back.
87136
+ * @param {Element|{current: Element}|string} [liftAnchor] Dialog mode,
87137
+ * Dialog's own: where a lifting close lands when that is not the trigger
87138
+ * (`animation="lifting"`, or `animation={{ open, close: "lifting" }}` for a
87139
+ * popup that opens the plain way and only lands on close),
87140
+ * read once the close is made (after `onClose`).
87141
+ * @param {"box"|"scene"} [lift="box"] Dialog's own, under `animation="lifting"`:
87142
+ * `"box"` for a card that extends (its top stays, the box uncovers the rest),
87143
+ * `"scene"` for a thumbnail that is a band cut from the middle of the bigger
87144
+ * view, as wide as it (each picture at the box's width, centred in it).
87145
+ * @param {string} [animationDuration] The popup's own (`--popup-animation-duration`).
87146
+ * @param {boolean} [dialogSizeFromAnchor] Dialog mode: the dialog is as wide
87147
+ * as the trigger — its box as a floor (Dialog's own `sizeFromAnchor`) and
87148
+ * as a ceiling, unless a `dialogMaxWidth` says otherwise. What keeps a card
87149
+ * its own width once lifted out of the page (`animation="lifting"`, with
87150
+ * `data-lift` on the card inside the popup): it moves, and nothing else
87151
+ * about it changes. `dialogMaxWidth="var(--anchor-width)"` says the same
87152
+ * ceiling by hand: the popup props are written on the dialog element, where
87153
+ * that variable lives.
87154
+ * @param {boolean|"top"|"bottom"} [dockedOnSmallTouchScreen] Dialog mode,
87155
+ * Dialog's own: on a small touch screen the dialog becomes a sheet docked
87156
+ * full width to one edge — `true` or `"top"` the top edge, out of the
87157
+ * virtual keyboard's way; `"bottom"` the bottom one, for a sheet that is
87158
+ * read and tapped. Anywhere else it stays the centered box. Ignored in
87159
+ * popover mode.
87160
+ * @param {"cancel"|"close"} [escapeEffect="cancel"] What Escape does to an open
87161
+ * picker. "cancel" puts back the value the picker had at open, and a dialog
87162
+ * picker with an explicit `id` (its opening being an entry of its own)
87163
+ * also goes back in history — so anything written to the url while it was
87164
+ * open (a route `stateSignal`, a search param) goes back with it. "close"
87165
+ * makes Escape say what clicking outside says: keep what was chosen,
87166
+ * close — a last resort, see
87167
+ * docs/popup_open.md#escape-cancels-the-other-gestures-keep for why Escape
87168
+ * should go on meaning cancel, and
87169
+ * docs/popup_open.md#what-cancel-actually-undoes for what a cancel puts
87170
+ * back.
87171
+ * @param {import("preact").RefObject<HTMLElement>|HTMLElement} [anchor] What
87172
+ * the popup hangs off, when that is not the picker itself: a picker whose
87173
+ * trigger is one piece of a bigger control (the chevron half of a split
87174
+ * button) points at the whole control, so the popup lines up with it and is
87175
+ * at least as wide as it.
87176
+ * @param {boolean} [standalone] - This picker answers for itself: it does not
87177
+ * register with the control group around it, so what it holds never becomes
87178
+ * part of that group's value and nothing that group does — distributing,
87179
+ * resetting, validating — reaches it. For a door that only opens something,
87180
+ * and for a picker that commits on its own inside a form.
87181
+ * @param {"close"|"cancel"|"capture"} [pressOutside="close"]
87182
+ * What a press outside the popup does: close and keep ("close"), close and
87183
+ * put back the value at open ("cancel"), or nothing at all ("capture"). The
87184
+ * default is what gives a popup with no confirm button its way out that
87185
+ * keeps — see the same section.
87186
+ * @param {boolean} [backdrop=true] Whether anything is laid between the popup
87187
+ * and the page at all. `false` lets a press outside both close the popup and
87188
+ * reach whatever it landed on, in one gesture — for a picker opened over a
87189
+ * page that stays as pressable as it looks (a plan, a map, a canvas), where a
87190
+ * wall would spend the first press on dismissing. Honoured in every mode: a
87191
+ * dialog with no wall is shown through the Popover API rather than
87192
+ * `showModal()`, so a picker docked as a bottom sheet on a phone still
87193
+ * leaves the plan behind it live. `false` by default under `mode="callout"`,
87194
+ * a bubble that dims nothing; `true` there spends the press outside on
87195
+ * closing it — the link under it not followed, the card under it not held —
87196
+ * without a wall: the scroll a finger starts beside the bubble still
87197
+ * scrolls. Not read inside a `<PopupGroup>`, whose wall it is.
87198
+ * @param {"auto"|"lift"|"discrete"|"invisible"} [backdropVariant="auto"] How
87199
+ * visible the popup's backdrop is, independently of what a click outside
87200
+ * does: `"auto"` is the paint `pressOutside` implies,
87201
+ * `"lift"` the opaque blurred wall `animation="lifting"` brings, asked for
87202
+ * without the movement, `"discrete"` a barely-there dim, `"invisible"` fully
87203
+ * transparent — a wall that is not seen is still a wall, which is what
87204
+ * `backdrop` above answers. For a picker that closes on an outside click
87205
+ * without wanting to dim the page for it.
87206
+ * @param {string} [backdropColor] The wash the popup paints over what is
87207
+ * behind, for this picker alone. See Dialog's own doc.
87208
+ * @param {string} [backdropFilter] What that wash does to the picture
87209
+ * underneath (`"blur(4px)"`), independently of what a click outside does.
87210
+ * See Dialog's own doc.
87211
+ * @param {number|string} [marginWithContainer] Minimum gap kept between the
87212
+ * popup and the edges of what contains it (the viewport, or the picker's own
87213
+ * positioned ancestor for `popupLayer="local"`). Caps the popup's size as
87214
+ * well as its placement, so what an expanded dialog leaves visible around
87215
+ * itself is set here. Defaults to `popoverSpacing` in popover mode, and to
87216
+ * Dialog's own in dialog mode: `3appw` in the top layer, `3cqw` under
87217
+ * `popupLayer="local"`, 0 for a dialog docked by
87218
+ * `dockedOnSmallTouchScreen`.
87219
+ * @param {string} [popupTestId] The `data-testid` of the popup element — the
87220
+ * popover or dialog the children open in. A `data-testid` on the picker
87221
+ * itself names the trigger (see docs/testid.md); this one is for a test that
87222
+ * needs the popup's own box — its surface, its position, its backdrop — and
87223
+ * not only what it holds, which a testid inside the children already names.
87224
+ * Under `mode="callout"` it names the callout element — the control's own,
87225
+ * shared with its constraint messages — while the children are shown in it.
87226
+ * @param {string} [popupAriaLabelledBy] The id(s) of what names the popup:
87227
+ * `aria-labelledby` on the popup element, which is a dialog in either mode
87228
+ * (the trigger says `aria-haspopup="dialog"`). `aria-label`/`aria-labelledby`
87229
+ * on the picker itself name the trigger. Rarely needed: a `<Popup.Title>`
87230
+ * in the children names the popup with nothing to wire, and without one
87231
+ * the popup takes the trigger's own name — the picker's `aria-labelledby`,
87232
+ * the label a picker that picks nothing draws, its `aria-label`, or the
87233
+ * `<Label>`/`<label for>` pointing at it (one that has an id, and does not
87234
+ * wrap the picker). A `mode="callout"` popup is read as its message.
87235
+ * @param {string} [popupAriaLabel] The same, as a string, for a popup whose
87236
+ * title is not an element.
87237
+ * @param {string} [popupAriaDescribedBy] The id(s) of what describes the
87238
+ * popup: `aria-describedby` on the popup element.
87239
+ * @param {string} [confirmTestId] `type="confirm"`, default body: the
87240
+ * `data-testid` of the yes button; `cancelTestId` that of the no button.
87241
+ * Both are elements navi builds, which a test may not name by class (see
87242
+ * docs/testid.md).
87243
+ * @param {string} [cancelTestId]
87244
+ * @param {"always"|"idle"|"from-first-open"|"while-opened"} [mount] The popup's own
87245
+ * (see Popup): when the children are built and thrown away. Left out, a
87246
+ * picker told no value holds what the control they hold holds, and builds
87247
+ * them as soon as that value is read: right away (`"always"`) when navi draws
87248
+ * it (no `ui`, a `Picker.UI.*`, a component `ui`), when there is a clear
87249
+ * cross, or when a form or group around the picker collects it; when an
87250
+ * element `ui` calls `usePickerState()`. Read by nothing, it waits for the
87251
+ * first open, like a picker told a value. A `picksNothing` picker (and so
87252
+ * `type="confirm"`) never builds them early: its popup holds no value, there
87253
+ * is nothing to read.
87254
+ */
85754
87255
  // Assembled in pure expressions rather than written onto Picker afterwards
85755
87256
  // (`Picker.Chip = PickerChip`): what createComponentResolver returns is an object
85756
87257
  // a bundler cannot see through, so a property assigned to it later is a side
@@ -86007,7 +87508,7 @@ const css$q = /* css */`@layer navi {
86007
87508
  const TRAVELS_NONE = [];
86008
87509
 
86009
87510
  /**
86010
- * @type {import("ignore:preact").FunctionComponent<{
87511
+ * @type {import("preact").FunctionComponent<{
86011
87512
  * value?: any,
86012
87513
  * defaultValue?: any,
86013
87514
  * signal?: import("@preact/signals").Signal<any>,
@@ -86023,7 +87524,7 @@ const TRAVELS_NONE = [];
86023
87524
  * fallbackValue?: any,
86024
87525
  * valueAtStep: (value: any, count: number) => any,
86025
87526
  * compareValues?: (a: any, b: any) => number,
86026
- * renderValue?: (value: any, context: { maxLines?: number }) => import("ignore:preact").ComponentChildren,
87527
+ * renderValue?: (value: any, context: { maxLines?: number }) => import("preact").ComponentChildren,
86027
87528
  * controlProps?: object,
86028
87529
  * duration?: number,
86029
87530
  * vertical?: boolean,
@@ -86810,14 +88311,14 @@ const renderValueDefault = value => String(value ?? "");
86810
88311
  * single `value`/`signal` like any other control. `TimeSpin` is that, for a
86811
88312
  * time of day.
86812
88313
  *
86813
- * @type {import("ignore:preact").FunctionComponent<{
88314
+ * @type {import("preact").FunctionComponent<{
86814
88315
  * name?: string,
86815
88316
  * value?: any,
86816
88317
  * defaultValue?: any,
86817
88318
  * signal?: import("@preact/signals").Signal<any>,
86818
88319
  * aggregateChildStates?: (childUIStateControllers: any[]) => any,
86819
88320
  * distributeChildUIState?: (groupState: any, childUIStateController: any) => any,
86820
- * children?: import("ignore:preact").ComponentChildren,
88321
+ * children?: import("preact").ComponentChildren,
86821
88322
  * [key: string]: any,
86822
88323
  * }>}
86823
88324
  * Everything a box takes is taken here too — `width`, `borderWidth`,
@@ -86920,7 +88421,7 @@ SpinGroup.Separator = SpinGroupSeparator;
86920
88421
  * above and below it by default — sideways they would be where the caret
86921
88422
  * moves — with the bigger number up top.
86922
88423
  *
86923
- * @type {import("ignore:preact").FunctionComponent<{
88424
+ * @type {import("preact").FunctionComponent<{
86924
88425
  * value?: number|string,
86925
88426
  * defaultValue?: number|string,
86926
88427
  * min?: number,
@@ -87025,7 +88526,7 @@ const numberAtStep = (value, count, {
87025
88526
  * A day one steps through: the date plus what it is to today when there is a
87026
88527
  * word for it, and a calendar behind it for the day that is far from here.
87027
88528
  *
87028
- * @type {import("ignore:preact").FunctionComponent<{
88529
+ * @type {import("preact").FunctionComponent<{
87029
88530
  * value?: string,
87030
88531
  * defaultValue?: string,
87031
88532
  * signal?: import("@preact/signals").Signal<string>,
@@ -87036,7 +88537,7 @@ const numberAtStep = (value, count, {
87036
88537
  * maxMessage?: any,
87037
88538
  * step?: number,
87038
88539
  * lang?: string,
87039
- * renderDay?: (day: string) => import("ignore:preact").ComponentChildren,
88540
+ * renderDay?: (day: string) => import("preact").ComponentChildren,
87040
88541
  * [key: string]: any,
87041
88542
  * }>}
87042
88543
  * @param {string} [value] The day shown, as "YYYY-MM-DD".
@@ -87047,7 +88548,7 @@ const numberAtStep = (value, count, {
87047
88548
  * @param {"long"|"short"|"numeric"} [format="long"] How the date is written.
87048
88549
  * Long by default, since this is a control one reads rather than a column
87049
88550
  * one scans; `short` where the room is not there.
87050
- * @param {(day: string) => import("ignore:preact").ComponentChildren} [renderDay] What
88551
+ * @param {(day: string) => import("preact").ComponentChildren} [renderDay] What
87051
88552
  * to write for a day. Defaults to the date plus what it is to today when
87052
88553
  * there is a word for it ("samedi 8 août (demain)"), since a day near now is
87053
88554
  * read as a distance from now before it is read as a date.
@@ -87164,7 +88665,7 @@ const HOUR_MAX = 23;
87164
88665
  const MINUTE_MAX = 59;
87165
88666
 
87166
88667
  /**
87167
- * @type {import("ignore:preact").FunctionComponent<{
88668
+ * @type {import("preact").FunctionComponent<{
87168
88669
  * name?: string,
87169
88670
  * value?: string,
87170
88671
  * defaultValue?: string,
@@ -87172,7 +88673,7 @@ const MINUTE_MAX = 59;
87172
88673
  * minuteStep?: number,
87173
88674
  * pad?: number,
87174
88675
  * loop?: boolean,
87175
- * separator?: import("ignore:preact").ComponentChildren,
88676
+ * separator?: import("preact").ComponentChildren,
87176
88677
  * hourLabel?: string,
87177
88678
  * minuteLabel?: string,
87178
88679
  * [key: string]: any,
@@ -87185,7 +88686,7 @@ const MINUTE_MAX = 59;
87185
88686
  * of a day to stop at. Say `loop={false}` for two ends one cannot step past.
87186
88687
  * @param {number} [pad=2] How many digits an hour and a minute are written on:
87187
88688
  * a clock says "07:00", never "7:0". Say `pad={0}` for the bare numbers.
87188
- * @param {import("ignore:preact").ComponentChildren} [separator] What is written
88689
+ * @param {import("preact").ComponentChildren} [separator] What is written
87189
88690
  * between the hours and the minutes. "h" in French, ":" elsewhere.
87190
88691
  * Everything a box takes is taken here too — `width`, `borderRadius`, `size`.
87191
88692
  */
@@ -87251,7 +88752,7 @@ const distributeTime$1 = (groupState, childUIStateController) => {
87251
88752
  };
87252
88753
 
87253
88754
  /**
87254
- * @type {import("ignore:preact").FunctionComponent<{
88755
+ * @type {import("preact").FunctionComponent<{
87255
88756
  * name?: string,
87256
88757
  * value?: { start?: string, end?: string },
87257
88758
  * defaultValue?: { start?: string, end?: string },
@@ -87261,13 +88762,13 @@ const distributeTime$1 = (groupState, childUIStateController) => {
87261
88762
  * timeProps?: object,
87262
88763
  * loop?: boolean,
87263
88764
  * size?: string,
87264
- * startLabel?: import("ignore:preact").ComponentChildren,
87265
- * endLabel?: import("ignore:preact").ComponentChildren,
88765
+ * startLabel?: import("preact").ComponentChildren,
88766
+ * endLabel?: import("preact").ComponentChildren,
87266
88767
  * [key: string]: any,
87267
88768
  * }>}
87268
88769
  * @param {{ start?: string, end?: string }} [value] The span shown, as two
87269
88770
  * "HH:MM".
87270
- * @param {import("ignore:preact").ComponentChildren} [startLabel] What is written
88771
+ * @param {import("preact").ComponentChildren} [startLabel] What is written
87271
88772
  * before the first time ("De"), and `endLabel` between the two ("à"). Say
87272
88773
  * `null` for neither.
87273
88774
  * @param {number} [minuteStep=1] How many minutes a press on a minute chevron
@@ -87364,7 +88865,7 @@ const css$p = /* css */`.navi_checkbox_group {
87364
88865
  *
87365
88866
  * See `<RadioGroup>` for the same shape holding a single value.
87366
88867
  *
87367
- * @type {import("ignore:preact").FunctionComponent<{
88868
+ * @type {import("preact").FunctionComponent<{
87368
88869
  * maxLength?: number,
87369
88870
  * maxLengthGuard?: number,
87370
88871
  * [key: string]: any,
@@ -87425,6 +88926,19 @@ const CheckboxGroupInterface = props => {
87425
88926
  });
87426
88927
  };
87427
88928
 
88929
+ /**
88930
+ * @type {import("preact").FunctionComponent<{
88931
+ * unit: string,
88932
+ * plural?: boolean,
88933
+ * format?: "long" | "short" | "narrow",
88934
+ * lang?: string,
88935
+ * label?: string,
88936
+ * size?: string,
88937
+ * sizeRatio?: number,
88938
+ * style?: import("preact").JSX.CSSProperties,
88939
+ * [key: string]: any,
88940
+ * }>}
88941
+ */
87428
88942
  const Unit = ({
87429
88943
  unit,
87430
88944
  plural,
@@ -88182,7 +89696,7 @@ const css$m = /* css */`.navi_input.navi_select {
88182
89696
  `;
88183
89697
 
88184
89698
  /**
88185
- * @type {import("ignore:preact").FunctionComponent<{
89699
+ * @type {import("preact").FunctionComponent<{
88186
89700
  * value?: string,
88187
89701
  * defaultValue?: string,
88188
89702
  * signal?: import("@preact/signals").Signal<string>,
@@ -88261,30 +89775,40 @@ const css$l = /* css */`.navi_split_button {
88261
89775
 
88262
89776
  & > .navi_group {
88263
89777
  flex: auto;
89778
+
89779
+ & > .navi_button {
89780
+ flex: auto;
89781
+ }
88264
89782
  }
88265
89783
  }
88266
89784
 
88267
89785
  .navi_split_button_menu {
89786
+ --border-width: 0px;
88268
89787
  display: flex;
88269
89788
  position: relative;
89789
+
89790
+ & > .navi_button {
89791
+ margin-left: calc(var(--border-width) * -1);
89792
+ }
88270
89793
  }
88271
89794
  `;
88272
89795
 
88273
89796
  /**
88274
- * @type {import("ignore:preact").FunctionComponent<{
88275
- * options: Array<{value: any, label: import("ignore:preact").ComponentChildren, [key: string]: any}>,
89797
+ * @type {import("preact").FunctionComponent<{
89798
+ * options: Array<{value: any, label: import("preact").ComponentChildren, [key: string]: any}>,
88276
89799
  * value?: any,
88277
89800
  * defaultValue?: any,
88278
- * label?: import("ignore:preact").ComponentChildren,
88279
- * action?: (value: any, event: Event) => void | Promise<void>,
89801
+ * label?: import("preact").ComponentChildren,
89802
+ * action?: (value: any, { event, signal }: { event: Event, signal: AbortSignal }) => void | Promise<void>,
88280
89803
  * command?: string,
88281
89804
  * commandFor?: string,
88282
89805
  * prefetch?: boolean,
88283
- * onValueChange?: (value: any, event: Event) => void,
89806
+ * onValueChange?: (value: any, { event }: { event: Event }) => void,
88284
89807
  * chooseEffect?: "select" | "run",
88285
89808
  * menuLabel?: string,
88286
- * menuIcon?: import("ignore:preact").ComponentChildren,
89809
+ * menuIcon?: import("preact").ComponentChildren,
88287
89810
  * menuIconSize?: number | string,
89811
+ * menuTestId?: string,
88288
89812
  * loading?: boolean,
88289
89813
  * readOnly?: boolean,
88290
89814
  * disabled?: boolean,
@@ -88320,19 +89844,21 @@ const css$l = /* css */`.navi_split_button {
88320
89844
  * popupTestId?: string,
88321
89845
  * [key: string]: any,
88322
89846
  * }>}
88323
- * @param {Array<{value: any, label: import("ignore:preact").ComponentChildren}>} options
89847
+ * @param {Array<{value: any, label: import("preact").ComponentChildren}>} options
88324
89848
  * The ways of doing the thing, in the order the menu lists them. Anything
88325
89849
  * else on an entry (`disabled`, `icon`, `padding`…) reaches its row.
88326
89850
  * @param {any} [value] Which entry the button stands for — the one it runs, and
88327
89851
  * the one it is named after. `defaultValue` is the uncontrolled form; without
88328
89852
  * either, the first entry.
88329
- * @param {import("ignore:preact").ComponentChildren} [label] What the button says, when
89853
+ * @param {import("preact").ComponentChildren} [label] What the button says, when
88330
89854
  * that is not the name of the entry it stands for — a button that keeps one
88331
89855
  * name whatever the menu does.
88332
- * @param {(value: any, event: Event) => void} [action] Do the thing, for the
88333
- * entry given. Pressed on the button it is the entry the button stands for;
88334
- * with `chooseEffect="run"` it is also the entry just chosen. Awaited: the
88335
- * whole split button is busy until it settles.
89856
+ * @param {(value: any, { event, signal }) => void} [action] Do the thing, for
89857
+ * the entry given. Pressed on the button it is the entry the button stands
89858
+ * for; with `chooseEffect="run"` it is also the entry just chosen. Run as a
89859
+ * navi action: the second argument is the run's (see docs/actions.md), the
89860
+ * press being its `event`. Awaited: the whole split button is busy until it
89861
+ * settles.
88336
89862
  * @param {string} [command] What a press on the button does once its `action`
88337
89863
  * is done, as on a `<Button>` (with `commandFor`, and `command-value` when
88338
89864
  * what the command is about is not the entry the button stands for). A
@@ -88346,12 +89872,14 @@ const css$l = /* css */`.navi_split_button {
88346
89872
  * menu of one-offs around a primary action.
88347
89873
  * @param {string} [menuLabel] What the chevron half is called, for a screen
88348
89874
  * reader and for the tooltip a long press raises.
88349
- * @param {import("ignore:preact").ComponentChildren} [menuIcon] What the chevron half
89875
+ * @param {import("preact").ComponentChildren} [menuIcon] What the chevron half
88350
89876
  * draws, in place of the chevron. It is the whole of what that half says, so
88351
89877
  * pass something that still reads as "there is more here" — and say what,
88352
89878
  * through `menuLabel`.
88353
89879
  * @param {number|string} [menuIconSize] How big that icon is drawn. Defaults to
88354
89880
  * the button's own font size.
89881
+ * @param {string} [menuTestId] The `data-testid` of the chevron half, which
89882
+ * navi builds and the caller has no other way to name.
88355
89883
  * @param {"popover"|"dialog"} [mode="popover"] What the menu is drawn as. A
88356
89884
  * picker left to itself turns into a dialog on a small screen; a split
88357
89885
  * button's menu stays hung off the button there, so this says popover unless
@@ -88372,8 +89900,12 @@ const css$l = /* css */`.navi_split_button {
88372
89900
  * `popupBorderRadius`, `animation`. See picker.jsx for what each one says.
88373
89901
  * An `aria-*` attribute describes the button and reaches the main half —
88374
89902
  * `aria-pressed` for a split button whose main half toggles a mode;
88375
- * `aria-hidden` excepted, which hides the whole control. Anything else lands
88376
- * on the split button's own box.
89903
+ * `aria-hidden` excepted, which hides the whole control. How the words are
89904
+ * written (`size`, `bold`, `lineHeight`, `noWrap`…) reaches both halves, and so
89905
+ * does vertical padding (`paddingY`, `paddingTop`, `paddingBottom`); the rest
89906
+ * of the padding is the main half's — the chevron half keeps its own, which
89907
+ * gives it its width. Anything else (`className`, `width`, margins, `data-*`)
89908
+ * lands on the split button's own box.
88377
89909
  */
88378
89910
  const SplitButton = props => {
88379
89911
  import.meta.css = [css$l, "@jsenv/navi/src/control/input/split_button.jsx"];
@@ -88392,6 +89924,7 @@ const SplitButton = props => {
88392
89924
  menuLabel = buttonI18n("button.more_actions"),
88393
89925
  menuIcon = jsx(ChevronDownSvg$1, {}),
88394
89926
  menuIconSize,
89927
+ menuTestId,
88395
89928
  loading,
88396
89929
  readOnly,
88397
89930
  disabled,
@@ -88411,11 +89944,13 @@ const SplitButton = props => {
88411
89944
  ...rest
88412
89945
  } = props;
88413
89946
  // Everything the popup answers to travels to the Picker; an aria-* describes
88414
- // the button a screen reader meets, which is the main half; everything else
88415
- // is the split button's own box (margins, width, data-*). Sorted by name
88416
- // rather than named one by one so a Picker popup prop is forwarded by adding
88417
- // it to that list, not by threading it through here.
88418
- const [popupProps, buttonAriaProps, boxProps] = sortRestProps(rest);
89947
+ // the button a screen reader meets, which is the main half; how the words are
89948
+ // written and how far they sit from the edge belong to the halves, since a
89949
+ // <button> inherits neither from the box around it; everything else is the
89950
+ // split button's own box (margins, width, data-*). Sorted by name rather
89951
+ // than named one by one so a Picker popup prop is forwarded by adding it to
89952
+ // that list, not by threading it through here.
89953
+ const [popupProps, buttonAriaProps, halfStyleProps, mainPaddingProps, boxProps] = sortRestProps(rest);
88419
89954
  // A split button is a control on the page, not a place one goes: its menu
88420
89955
  // hangs off it even on a phone, where a picker left to itself would decide a
88421
89956
  // small screen means a dialog. Passing mode="dialog" asks for that back.
@@ -88460,6 +89995,7 @@ const SplitButton = props => {
88460
89995
  borderColor,
88461
89996
  borderWidth,
88462
89997
  borderRadius,
89998
+ ...halfStyleProps,
88463
89999
  pressEffect,
88464
90000
  loading: loadingResolved,
88465
90001
  readOnly,
@@ -88469,7 +90005,7 @@ const SplitButton = props => {
88469
90005
  };
88470
90006
  return jsxs(Box, {
88471
90007
  ref: rootRef,
88472
- className: "navi_split_button",
90008
+ baseClassName: "navi_split_button",
88473
90009
  borderRadius: borderRadius,
88474
90010
  ...boxProps,
88475
90011
  children: [jsx(LoadingOutline, {
@@ -88486,6 +90022,7 @@ const SplitButton = props => {
88486
90022
  "command-value": commandValue,
88487
90023
  prefetch: prefetch,
88488
90024
  ...buttonAriaProps,
90025
+ ...mainPaddingProps,
88489
90026
  ...halfProps,
88490
90027
  children: label === undefined ? optionShown?.label : label
88491
90028
  }), jsxs("div", {
@@ -88495,6 +90032,7 @@ const SplitButton = props => {
88495
90032
  paddingX: "s",
88496
90033
  expandY: true,
88497
90034
  "aria-label": menuLabel,
90035
+ "data-testid": menuTestId,
88498
90036
  command: "--navi-open",
88499
90037
  commandFor: menuId,
88500
90038
  ...halfProps,
@@ -88574,9 +90112,16 @@ const SplitButton = props => {
88574
90112
  // a caller reaches all of them through the split button (see picker.jsx's JSDoc
88575
90113
  // for what each one says).
88576
90114
  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"]);
90115
+ // Vertical padding is said about both halves so they stay the same height; the
90116
+ // horizontal one is the main half's alone, the chevron half's own (paddingX="s")
90117
+ // being what gives it its width.
90118
+ const HALF_PADDING_PROP_SET = new Set(["paddingY", "paddingTop", "paddingBottom"]);
90119
+ const MAIN_PADDING_PROP_SET = new Set(["padding", "paddingX", "paddingLeft", "paddingRight"]);
88577
90120
  const sortRestProps = props => {
88578
90121
  const popupProps = {};
88579
90122
  const buttonAriaProps = {};
90123
+ const halfStyleProps = {};
90124
+ const mainPaddingProps = {};
88580
90125
  const boxProps = {};
88581
90126
  for (const key of Object.keys(props)) {
88582
90127
  if (POPUP_PROP_SET.has(key)) {
@@ -88585,11 +90130,15 @@ const sortRestProps = props => {
88585
90130
  // aria-hidden hides a subtree rather than describing a button: on the
88586
90131
  // main half alone it would leave the chevron half exposed.
88587
90132
  buttonAriaProps[key] = props[key];
90133
+ } else if (isTypoProp(key) || HALF_PADDING_PROP_SET.has(key)) {
90134
+ halfStyleProps[key] = props[key];
90135
+ } else if (MAIN_PADDING_PROP_SET.has(key)) {
90136
+ mainPaddingProps[key] = props[key];
88588
90137
  } else {
88589
90138
  boxProps[key] = props[key];
88590
90139
  }
88591
90140
  }
88592
- return [popupProps, buttonAriaProps, boxProps];
90141
+ return [popupProps, buttonAriaProps, halfStyleProps, mainPaddingProps, boxProps];
88593
90142
  };
88594
90143
 
88595
90144
  /**
@@ -89791,7 +91340,7 @@ const WheelGroupContext = createContext(null);
89791
91340
  * value, the value lives in one hidden input for the form. The visible rows are
89792
91341
  * presentational.
89793
91342
  *
89794
- * @type {import("ignore:preact").FunctionComponent<{
91343
+ * @type {import("preact").FunctionComponent<{
89795
91344
  * value?: any,
89796
91345
  * defaultValue?: any,
89797
91346
  * action?: (value: any) => void,
@@ -89809,7 +91358,7 @@ const WheelGroupContext = createContext(null);
89809
91358
  * readOnly?: boolean,
89810
91359
  * disabled?: boolean,
89811
91360
  * loading?: boolean,
89812
- * children?: import("ignore:preact").ComponentChildren,
91361
+ * children?: import("preact").ComponentChildren,
89813
91362
  * [key: string]: any,
89814
91363
  * }>}
89815
91364
  * @param {number} [props.visibleCount=3] - Odd number of rows visible in the viewport (the center one is the selection).
@@ -90437,8 +91986,9 @@ function WheelUI(props) {
90437
91986
  }, {
90438
91987
  controlType: "input",
90439
91988
  // The host is the hidden input carrying the value: the container is
90440
- // what is focused and spun.
90441
- listenersOn: "root"
91989
+ // what is focused and spun, and the spinbutton its name has to name.
91990
+ listenersOn: "root",
91991
+ nameOn: "root"
90442
91992
  });
90443
91993
  const uiStateController = getUIStateControllerById(controlHostProps.id);
90444
91994
  const {
@@ -91288,9 +92838,9 @@ const WheelWindow = ({
91288
92838
  * of rows and fills each from this tracked data — so the value list is the source
91289
92839
  * of truth, not a per-item DOM node. Selection lives on the wheel's value.
91290
92840
  *
91291
- * @type {import("ignore:preact").FunctionComponent<{
92841
+ * @type {import("preact").FunctionComponent<{
91292
92842
  * value: any,
91293
- * children?: import("ignore:preact").ComponentChildren,
92843
+ * children?: import("preact").ComponentChildren,
91294
92844
  * [key: string]: any,
91295
92845
  * }>}
91296
92846
  */
@@ -91325,9 +92875,9 @@ Wheel.Item = WheelItem;
91325
92875
  * each Wheel.Item (it's a Box: paddingX="0.5ch") so the gap stays scrollable.
91326
92876
  * `spacing` is just the Box flex gap between children (a layout gap, not padding).
91327
92877
  *
91328
- * @type {import("ignore:preact").FunctionComponent<{
92878
+ * @type {import("preact").FunctionComponent<{
91329
92879
  * horizontal?: boolean,
91330
- * children?: import("ignore:preact").ComponentChildren,
92880
+ * children?: import("preact").ComponentChildren,
91331
92881
  * [key: string]: any,
91332
92882
  * }>}
91333
92883
  * @param {boolean} [props.horizontal] - Stack the (horizontal) wheels vertically instead of in a row.
@@ -91524,14 +93074,14 @@ const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
91524
93074
  const END_OF_DAY = END_OF_DAY_HOUR * MINUTES_PER_HOUR;
91525
93075
 
91526
93076
  /**
91527
- * @type {import("ignore:preact").FunctionComponent<{
93077
+ * @type {import("preact").FunctionComponent<{
91528
93078
  * name?: string,
91529
93079
  * value?: string,
91530
93080
  * defaultValue?: string,
91531
93081
  * signal?: import("@preact/signals").Signal<string>,
91532
93082
  * minuteStep?: number,
91533
93083
  * loop?: boolean,
91534
- * separator?: import("ignore:preact").ComponentChildren,
93084
+ * separator?: import("preact").ComponentChildren,
91535
93085
  * hourLabel?: string,
91536
93086
  * minuteLabel?: string,
91537
93087
  * size?: string,
@@ -91552,7 +93102,7 @@ const END_OF_DAY = END_OF_DAY_HOUR * MINUTES_PER_HOUR;
91552
93102
  * @param {boolean} [loop=true] The wheels go round: 23h then 0h, 59 minutes
91553
93103
  * then 0. What a clock does. Say `loop={false}` for two ends one cannot turn
91554
93104
  * past.
91555
- * @param {import("ignore:preact").ComponentChildren} [separator] What is written
93105
+ * @param {import("preact").ComponentChildren} [separator] What is written
91556
93106
  * between the hours and the minutes. "h" in French, ":" elsewhere. In a time
91557
93107
  * of whole hours it follows each hour on its row instead ("8h"), and nothing
91558
93108
  * is written by default outside French.
@@ -91671,7 +93221,7 @@ const TimeWheel = ({
91671
93221
  };
91672
93222
 
91673
93223
  /**
91674
- * @type {import("ignore:preact").FunctionComponent<{
93224
+ * @type {import("preact").FunctionComponent<{
91675
93225
  * name?: string,
91676
93226
  * value?: { start?: string, end?: string },
91677
93227
  * defaultValue?: { start?: string, end?: string },
@@ -91680,15 +93230,15 @@ const TimeWheel = ({
91680
93230
  * minDuration?: number,
91681
93231
  * loop?: boolean,
91682
93232
  * size?: string,
91683
- * startLabel?: import("ignore:preact").ComponentChildren,
91684
- * endLabel?: import("ignore:preact").ComponentChildren,
93233
+ * startLabel?: import("preact").ComponentChildren,
93234
+ * endLabel?: import("preact").ComponentChildren,
91685
93235
  * labelPosition?: "before" | "above",
91686
93236
  * timeProps?: object,
91687
93237
  * [key: string]: any,
91688
93238
  * }>}
91689
93239
  * @param {{ start?: string, end?: string }} [value] The span shown, as two
91690
93240
  * "HH:MM".
91691
- * @param {import("ignore:preact").ComponentChildren} [startLabel] What is written
93241
+ * @param {import("preact").ComponentChildren} [startLabel] What is written
91692
93242
  * before the first time ("De"), and `endLabel` between the two ("à"). Say
91693
93243
  * `null` for neither.
91694
93244
  * @param {"before"|"above"} [labelPosition="before"] Where each label stands.
@@ -95673,17 +97223,25 @@ const Address = ({
95673
97223
  };
95674
97224
 
95675
97225
  installImportMetaCssBuild(import.meta);
95676
- const css$a = /* css */`@layer navi;
97226
+ const css$a = /* css */`@layer navi {
97227
+ .navi_text.navi_badge_count {
97228
+ --font-size: .7em;
97229
+ --badge-count-padding-x-default: .5em;
97230
+ --badge-count-padding-y-default: .3em;
97231
+
97232
+ &[data-ellipse][data-single-char], &[data-circle] {
97233
+ --badge-count-padding-x-default: 0;
97234
+ --badge-count-padding-y-default: 0;
97235
+ }
97236
+ }
97237
+ }
95677
97238
 
95678
97239
  .navi_text.navi_badge_count {
95679
97240
  white-space: nowrap;
95680
- --font-size: .7em;
95681
97241
  --x-background: var(--background);
95682
- --x-background-color: var(--background-color, var(--x-background));
97242
+ --x-background-color: var(--background-color);
95683
97243
  --x-color-contrasting: var(--navi-color-white);
95684
97244
  --x-color: var(--color, var(--x-color-contrasting));
95685
- --badge-count-padding-x-default: .5em;
95686
- --badge-count-padding-y-default: .3em;
95687
97245
  --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))));
95688
97246
  --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))));
95689
97247
  --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))));
@@ -95715,12 +97273,9 @@ const css$a = /* css */`@layer navi;
95715
97273
  padding-bottom: var(--x-badge-count-padding-bottom);
95716
97274
  padding-left: var(--x-badge-count-padding-left);
95717
97275
  background: var(--x-background);
95718
- background-color: var(--x-background-color);
95719
97276
  border-radius: 1em;
95720
97277
 
95721
97278
  &[data-single-char] {
95722
- --badge-count-padding-x-default: 0;
95723
- --badge-count-padding-y-default: 0;
95724
97279
  box-sizing: content-box;
95725
97280
  text-align: center;
95726
97281
  border-radius: 100em;
@@ -95733,8 +97288,6 @@ const css$a = /* css */`@layer navi;
95733
97288
 
95734
97289
  &[data-circle] {
95735
97290
  --x-number-font-size: var(--font-size);
95736
- --badge-count-padding-x-default: 0;
95737
- --badge-count-padding-y-default: 0;
95738
97291
  box-sizing: content-box;
95739
97292
  aspect-ratio: 1;
95740
97293
  width: var(--x-radius);
@@ -95744,7 +97297,6 @@ const css$a = /* css */`@layer navi;
95744
97297
  padding-bottom: var(--x-badge-count-padding-bottom);
95745
97298
  padding-left: var(--x-badge-count-padding-left);
95746
97299
  background: var(--x-background);
95747
- background-color: var(--x-background-color);
95748
97300
  border-radius: 100em;
95749
97301
  justify-content: center;
95750
97302
  align-items: center;
@@ -95769,6 +97321,10 @@ const css$a = /* css */`@layer navi;
95769
97321
  font-size: var(--x-number-font-size);
95770
97322
  }
95771
97323
  }
97324
+
97325
+ &[data-background-color] {
97326
+ background-color: var(--x-background-color);
97327
+ }
95772
97328
  }
95773
97329
  `;
95774
97330
  const BadgeCountOverflow = () => jsx("span", {
@@ -95905,6 +97461,7 @@ const BadgeCountEllipse = ({
95905
97461
  "data-ellipse": "",
95906
97462
  "data-value-overflow": hasOverflow ? "" : undefined,
95907
97463
  "data-loading": loading ? "" : undefined,
97464
+ "data-background-color": props.backgroundColor === undefined ? undefined : "",
95908
97465
  "data-single-char": charCount === 1 ? "" : undefined,
95909
97466
  ...props,
95910
97467
  styleCSSVars: BadgeCountStyleCSSVars,
@@ -95927,6 +97484,7 @@ const BadgeCountCircle = ({
95927
97484
  "data-circle": "",
95928
97485
  bold: true,
95929
97486
  "data-loading": loading ? "" : undefined,
97487
+ "data-background-color": props.backgroundColor === undefined ? undefined : "",
95930
97488
  "data-single-char": charCount === 1 ? "" : undefined,
95931
97489
  "data-two-chars": charCount === 2 ? "" : undefined,
95932
97490
  "data-three-chars": charCount === 3 ? "" : undefined,
@@ -96419,7 +97977,7 @@ const css$8 = /* css */`@layer navi;
96419
97977
  `;
96420
97978
 
96421
97979
  /**
96422
- * @type {import("ignore:preact").FunctionComponent<{
97980
+ * @type {import("preact").FunctionComponent<{
96423
97981
  * children?: number | string,
96424
97982
  * unit?: string | [string, string],
96425
97983
  * unitPosition?: "right" | "bottom",
@@ -97011,8 +98569,8 @@ const css$4 = /* css */`.svg_mask_content * {
97011
98569
  *
97012
98570
  * @param {Object} props - Component properties
97013
98571
  * @param {string} props.viewBox - The main viewBox for the composition (required as soon as there is an overlay)
97014
- * @param {import("ignore:preact").ComponentChildren} props.children - SVG elements (first is base, rest are overlays)
97015
- * @returns {import("ignore:preact").VNode} A composed SVG with all elements properly masked
98572
+ * @param {import("preact").ComponentChildren} props.children - SVG elements (first is base, rest are overlays)
98573
+ * @returns {import("preact").VNode} A composed SVG with all elements properly masked
97016
98574
  */
97017
98575
  const SVGMaskOverlay = ({
97018
98576
  viewBox,
@@ -97102,7 +98660,6 @@ const css$3 = /* css */`@layer navi {
97102
98660
  padding-bottom: var(--layout-padding-bottom, var(--layout-padding-y, var(--layout-padding)));
97103
98661
  padding-left: var(--layout-padding-left, var(--layout-padding-x, var(--layout-padding)));
97104
98662
  background: var(--layout-background);
97105
- background-color: var(--layout-background-color, var(--layout-background));
97106
98663
  border-width: var(--layout-border-width);
97107
98664
  border-style: solid;
97108
98665
  border-color: var(--layout-border-color);
@@ -97119,7 +98676,6 @@ const CardLayoutStyleCSSVars = {
97119
98676
  borderWidth: "--layout-border-width",
97120
98677
  borderColor: "--layout-border-color",
97121
98678
  background: "--layout-background",
97122
- backgroundColor: "--layout-background-color",
97123
98679
  padding: "--layout-padding",
97124
98680
  paddingTop: "--layout-padding-top",
97125
98681
  paddingBottom: "--layout-padding-bottom",
@@ -97134,7 +98690,7 @@ const CardLayoutStyleCSSVars = {
97134
98690
  * what holds it, so how tall that area is is up to the page around it, as for
97135
98691
  * `ViewportLayout`.
97136
98692
  *
97137
- * @type {import("ignore:preact").FunctionComponent<{
98693
+ * @type {import("preact").FunctionComponent<{
97138
98694
  * alignX?: string,
97139
98695
  * alignY?: string,
97140
98696
  * margin?: string|number,
@@ -97159,6 +98715,7 @@ const CardLayout = ({
97159
98715
  children,
97160
98716
  alignX = "center",
97161
98717
  alignY = "center",
98718
+ backgroundColor,
97162
98719
  ...props
97163
98720
  }) => {
97164
98721
  import.meta.css = [css$3, "@jsenv/navi/src/layout/card_layout.jsx"];
@@ -97177,6 +98734,7 @@ const CardLayout = ({
97177
98734
  className: "navi_card",
97178
98735
  alignX: alignX,
97179
98736
  alignY: alignY,
98737
+ backgroundColor: backgroundColor,
97180
98738
  children: children
97181
98739
  })
97182
98740
  });
@@ -97350,7 +98908,7 @@ const LINE_GAP = 5;
97350
98908
  const StepListContext = createContext(null);
97351
98909
 
97352
98910
  /**
97353
- * @type {import("ignore:preact").FunctionComponent<{
98911
+ * @type {import("preact").FunctionComponent<{
97354
98912
  * current?: string,
97355
98913
  * slideContainer?: string,
97356
98914
  * travelByClick?: boolean,
@@ -97707,7 +99265,7 @@ const sameShallow = (previousObject, nextObject) => {
97707
99265
  * It is both StepList.Item and an export of its own, the way Slide is to
97708
99266
  * SlideContainer.
97709
99267
  *
97710
- * @type {import("ignore:preact").FunctionComponent<{
99268
+ * @type {import("preact").FunctionComponent<{
97711
99269
  * value: string,
97712
99270
  * done?: boolean,
97713
99271
  * [key: string]: any,
@@ -97769,7 +99327,7 @@ const ViewportLayoutStyleCSSVars = {
97769
99327
  * same inside a route, a preview pane or a demo box, and it is up to the page
97770
99328
  * around it to be full-height.
97771
99329
  *
97772
- * @type {import("ignore:preact").FunctionComponent<{
99330
+ * @type {import("preact").FunctionComponent<{
97773
99331
  * padding?: string|number,
97774
99332
  * paddingTop?: string|number,
97775
99333
  * paddingBottom?: string|number,
@@ -97994,7 +99552,7 @@ const css = /* css */`.navi_side_panel {
97994
99552
  * `closeByPressOutside`/`pressOutside` only say what a
97995
99553
  * press on the wall does, not whether there is one (see `dialog.jsx`'s own
97996
99554
  * doc).
97997
- * @param {import("ignore:preact").ComponentChildren} props.children - No built-in
99555
+ * @param {import("preact").ComponentChildren} props.children - No built-in
97998
99556
  * close button — add one wherever it makes sense for the layout (e.g. a
97999
99557
  * plain `<Button command="--navi-close">` in `SidePanel.Head`), or rely on
98000
99558
  * `closeByPressOutside`/Escape instead.
@@ -98093,7 +99651,7 @@ const toCssLength = (value, propertyName) => value === undefined || value === nu
98093
99651
  * @param {object} props
98094
99652
  * @param {string} [props.className] - Merged with the shared
98095
99653
  * `"navi_side_panel_head"` class this file's own CSS targets.
98096
- * @param {import("ignore:preact").ComponentChildren} props.children
99654
+ * @param {import("preact").ComponentChildren} props.children
98097
99655
  */
98098
99656
  const SidePanelHead = props => jsx(Box, {
98099
99657
  baseClassName: "navi_side_panel_head",
@@ -98106,7 +99664,7 @@ const SidePanelHead = props => jsx(Box, {
98106
99664
  * @param {object} props
98107
99665
  * @param {string} [props.className] - Merged with the shared
98108
99666
  * `"navi_side_panel_foot"` class this file's own CSS targets.
98109
- * @param {import("ignore:preact").ComponentChildren} props.children
99667
+ * @param {import("preact").ComponentChildren} props.children
98110
99668
  */
98111
99669
  const SidePanelFoot = props => jsx(Box, {
98112
99670
  className: "navi_side_panel_foot",
@@ -98115,6 +99673,58 @@ const SidePanelFoot = props => jsx(Box, {
98115
99673
  SidePanel.Head = SidePanelHead;
98116
99674
  SidePanel.Foot = SidePanelFoot;
98117
99675
 
99676
+ /**
99677
+ * Creates a linked `[Slot, SlotFill]` pair so content rendered anywhere in
99678
+ * the tree (`SlotFill`) appears at a fixed location elsewhere (`Slot`) — a
99679
+ * lightweight, signal-based alternative to a DOM portal: `SlotFill` itself
99680
+ * renders nothing (no extra DOM node at its own call site), it just writes
99681
+ * its props into a shared signal that `Slot` reads reactively.
99682
+ *
99683
+ * Holds at most one filler at a time — there is no stacking/queueing. The
99684
+ * filler is the last `SlotFill` (from this same `createSlot()` call) to
99685
+ * mount, or the first to render into an empty slot; later renders of a
99686
+ * `SlotFill` that is not the filler leave the slot alone. A `SlotFill`
99687
+ * unmounting empties the slot only while it is still that one, and so does one
99688
+ * in a page a route transition keeps while leaving (see keepLeavingPages in
99689
+ * nav/route.jsx): still mounted, hidden, it must neither stay in the slot nor
99690
+ * take it back from the page arriving.
99691
+ *
99692
+ * **One `SlotFill`, rendered where the choice is made.** `isFilled` follows
99693
+ * the fills as they mount and unmount, in the order the tree walks them. A
99694
+ * slot fed from several places — a `SlotFill` in each card of a board, each
99695
+ * card deciding on its own whether it is the open one — reads as empty
99696
+ * between the leaver and the newcomer whenever they render separately, and
99697
+ * anything acting on `isFilled` acts on that empty frame (a `SidePanel` with
99698
+ * `open={isFilled}` closes for good: its `onClose` clears the state the
99699
+ * newcomer was waiting for). The slot cannot tell that gap from a real
99700
+ * emptying, and does not try to: render one `SlotFill` at the level that
99701
+ * holds the choice, with the chosen content as its children, and bind a
99702
+ * state that must not blink (a panel's `open`) to the app's own signal
99703
+ * rather than to `isFilled` — see docs/popup_open.md, "One panel, fed by a
99704
+ * slot".
99705
+ *
99706
+ * `Slot` keeps `SlotRenderer` mounted permanently — even while unfilled it
99707
+ * still renders it, with no props and `isFilled={false}` — instead of
99708
+ * unmounting it. This lets `SlotRenderer` be a persistent wrapper (a
99709
+ * `SidePanel` that opens and closes with an animation instead of being
99710
+ * mounted/unmounted alongside the filler — see 7_slot_demo.html's "Two
99711
+ * side-panel slots" section). A `SlotRenderer` that wants the
99712
+ * render-nothing-when-unfilled behavior can opt in with its own
99713
+ * `if (!isFilled) return null;` (the default `Box` renderer doesn't do
99714
+ * this — an unfilled default slot just renders an empty `<Box/>`).
99715
+ *
99716
+ * @param {import("preact").ComponentType} [SlotRenderer=Box] - Rendered by
99717
+ * `Slot`, always, with whatever props `SlotFill` last set (spread
99718
+ * directly) plus `isFilled`. Swap this out to reuse the same slot
99719
+ * mechanism for something other than a plain `Box` (e.g. a specific
99720
+ * component expecting its own particular props, or a persistent wrapper
99721
+ * like `SidePanel`).
99722
+ * @returns {[import("preact").ComponentType, import("preact").ComponentType<object>]}
99723
+ * `[Slot, SlotFill]` — `Slot` takes no props, render it once wherever the
99724
+ * content should appear. `SlotFill` takes whatever props `SlotRenderer`
99725
+ * expects, render it anywhere else in the tree to supply/update that
99726
+ * content.
99727
+ */
98118
99728
  const createSlot = (SlotRenderer = Box) => {
98119
99729
  const slotPropsSignal = signal();
98120
99730
  let filler = null;