@jsenv/navi 0.29.478 → 0.29.479

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,12 +3,12 @@
3
3
  * using @jsenv/navi as intended.
4
4
  */
5
5
  import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
- import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, createEventGroupLogger, 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, getVirtualKeyboardOverlayHeight, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange } from "@jsenv/dom";
6
+ import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, createEventGroupLogger, 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, getVirtualKeyboardOverlayHeight, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange } 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, useEffect, useReducer, useCallback, useId } from "preact/hooks";
11
- import { humanizeI18n, prefixFirstAndIndentRemainingLines, 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, prefixFirstAndIndentRemainingLines, 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";
@@ -4021,6 +4021,11 @@ const createAction = (callback, rootOptions = {}) => {
4021
4021
  * asks nothing.
4022
4022
  */
4023
4023
  const bindParams = (newParamsOrSignal, options = {}) => {
4024
+ if (newParamsOrSignal === null) {
4025
+ // A signal's `null` becomes `undefined` in mergeActionParams; a static
4026
+ // one never goes through it, so it is said the same way here.
4027
+ newParamsOrSignal = undefined;
4028
+ }
4024
4029
  const { debounce, ...optionsWithoutDebounce } = options;
4025
4030
  if (debounce) {
4026
4031
  if (!isSignal(newParamsOrSignal)) {
@@ -4621,7 +4626,9 @@ const createActionProxyFromSignal = (
4621
4626
  const inheritsAnswer =
4622
4627
  inheritData && getActionPrivateProperties(action).inheritData;
4623
4628
  const createTarget = (params) => {
4624
- if (inheritsAnswer) {
4629
+ // `undefined` is "nothing to ask": no answer is coming to replace a stand-in,
4630
+ // and that one instance is shared by every binding of the action.
4631
+ if (inheritsAnswer && params !== undefined) {
4625
4632
  const previousActionTarget = actionTargetPreviousWeakRef?.deref();
4626
4633
  const previousTarget = previousActionTarget || action;
4627
4634
  return action.bindParams(params, {
@@ -6495,9 +6502,11 @@ const CONTROL_ATTRIBUTE_SET = new Set([
6495
6502
  "spellcheck",
6496
6503
  "autoCorrect",
6497
6504
  "aria-controls",
6498
- // The name goes where the role is: the root box has none, so an aria-label
6499
- // left on it names nothing at all, while the host is what the user focuses
6500
- // and what a screen reader announces (and what getByRole({ name }) reads).
6505
+ // The name goes where the role is: for most controls the root box has none,
6506
+ // so an aria-label left on it names nothing at all, while the host is what
6507
+ // the user focuses and what a screen reader announces (and what
6508
+ // getByRole({ name }) reads). A control whose role is on its root says so
6509
+ // with `nameOn: "root"` (see useControlProps).
6501
6510
  "aria-label",
6502
6511
  "aria-labelledby",
6503
6512
  "tabIndex",
@@ -7155,7 +7164,7 @@ const useDebugUIState = naviDebug.useDebugUIState ;
7155
7164
  * @param {boolean|Function} [props.debugPopup] - Popover/dialog open/close/positioning.
7156
7165
  * @param {boolean|Function} [props.debugAction] - Action lifecycle.
7157
7166
  * @param {boolean|Function} [props.debugUIState] - UI-state transitions and validation.
7158
- * @param {import("ignore:preact").ComponentChildren} props.children
7167
+ * @param {import("preact").ComponentChildren} props.children
7159
7168
  *
7160
7169
  * @example
7161
7170
  * // Log everything under this subtree
@@ -7377,9 +7386,11 @@ const generateSignalId = () => {
7377
7386
  * @param {Array} [options.oneOf] - Array of valid values for validation. Signal will be marked invalid if value is not in this array
7378
7387
  * @param {boolean} [options.autoFix=false] - Hold the repaired value instead of the refused one: a number
7379
7388
  * clamped to `min`/`max` and rounded to `step`, a value outside `oneOf` replaced by the signal's
7380
- * default value (an enumeration has no nearest value, and its order says nothing). Without it the
7381
- * refused value is held as-is, marked invalid, and `validSignal` reads `undefined`. When the value
7382
- * came from the address, the address is rewritten with the repaired one (a replace).
7389
+ * default value, or made absent when the signal has no default (an enumeration has no nearest
7390
+ * value, and its order says nothing). The default must therefore pass the rules itself: one of
7391
+ * `oneOf`. Without it the refused value is held as-is, marked invalid, and `validSignal` reads
7392
+ * `undefined`. When the value came from the address, the address is rewritten with the repaired
7393
+ * one (a replace).
7383
7394
  * @param {boolean} [options.weak=false] - The param qualifies one visit, not the screen: it is written into a
7384
7395
  * url only when explicitly named (`routeParams={{ edit: id }}`), never inherited from the signal's current
7385
7396
  * value, and it goes back to the default value when the route stops matching. Use it for params like an
@@ -7521,7 +7532,7 @@ const stateSignal = (defaultValue, options = {}) => {
7521
7532
 
7522
7533
  // Determine localStorage key: use id if persists=true, or legacy localStorage option
7523
7534
  const localStorageKey = signalIdString;
7524
- const [validity, updateValidity] = createValidity({
7535
+ const [validity, updateValidity, clearValidity] = createValidity({
7525
7536
  type,
7526
7537
  min,
7527
7538
  max,
@@ -7534,6 +7545,21 @@ const stateSignal = (defaultValue, options = {}) => {
7534
7545
  // processValue, below) so a dynamic default is read where it stands.
7535
7546
  fallback: () => getDefaultValue(false),
7536
7547
  });
7548
+ if (autoFix && staticDefaultValue !== undefined) {
7549
+ const [defaultValidity, validateDefault] = createValidity({
7550
+ type,
7551
+ min,
7552
+ max,
7553
+ step,
7554
+ oneOf,
7555
+ });
7556
+ validateDefault(staticDefaultValue);
7557
+ if (!defaultValidity.valid) {
7558
+ console.warn(
7559
+ `stateSignal "${signalIdString}": autoFix has nothing to repair to, the default ${JSON.stringify(staticDefaultValue)} is refused by the signal's own rules. Declare a default they accept (one of oneOf), or none.`,
7560
+ );
7561
+ }
7562
+ }
7537
7563
  const readFromLocalStorage = persists
7538
7564
  ? () => {
7539
7565
  const raw = window.localStorage.getItem(localStorageKey);
@@ -7637,9 +7663,19 @@ const stateSignal = (defaultValue, options = {}) => {
7637
7663
  // Create signal with initial value: use stored value, or undefined to indicate no explicit value
7638
7664
  const processValue = (value) => {
7639
7665
  if (value === undefined) {
7666
+ // Absence is never validated, and the verdict on the previous value
7667
+ // (validSignal, validity.valid) must not outlive it.
7668
+ clearValidity();
7640
7669
  return undefined;
7641
7670
  }
7642
7671
  updateValidity(value);
7672
+ // A state without a default is repaired to absent. validity cannot suggest
7673
+ // it: `undefined` is "no suggestion" there and its rules refuse it.
7674
+ if (autoFix && !validity.valid && getDefaultValue(false) === undefined) {
7675
+ clearValidity();
7676
+ validity.autoFixed = true;
7677
+ return undefined;
7678
+ }
7643
7679
  // Always return the coerced value (type coercion applies), even if invalid.
7644
7680
  // Invalid values are preserved as-is so the UI can display them and the URL
7645
7681
  // can reflect the current input state without silently correcting it.
@@ -7938,6 +7974,18 @@ const readSignalForUrlBuild = (connection) => {
7938
7974
  return connection.signal.value;
7939
7975
  };
7940
7976
 
7977
+ /**
7978
+ * Whether a pattern catches the addresses below its own: "/map/", "/map/*" and
7979
+ * the root "/" do, "/places" does not. Only such a route lends its search
7980
+ * params to the routes below it: it is on screen wherever they are, so its
7981
+ * state holds there too. Declared on "/places", a search param belongs to
7982
+ * that address alone — "/places/:section" beside it also matches
7983
+ * "/places/clubs", and borrowing the list's search would hand it to the clubs.
7984
+ */
7985
+ const coversAddressesBelow = (patternObj) => {
7986
+ return patternObj.pattern.trailingSlash || patternObj.pattern.wildcard;
7987
+ };
7988
+
7941
7989
  /**
7942
7990
  * Creates a custom route pattern matcher
7943
7991
  */
@@ -8060,11 +8108,13 @@ const createRoutePattern = (
8060
8108
  }
8061
8109
  }
8062
8110
 
8063
- // Inherit search parameters from ancestry chain
8064
- // Search params are global and should be inherited from any matching ancestor
8065
- // regardless of path segment relationships
8111
+ // Search params lent by the ancestors covering this route
8066
8112
  let ancestorPatternObj = patternObject.parent;
8067
8113
  while (ancestorPatternObj) {
8114
+ if (!coversAddressesBelow(ancestorPatternObj)) {
8115
+ ancestorPatternObj = ancestorPatternObj.parent;
8116
+ continue;
8117
+ }
8068
8118
  for (const [
8069
8119
  paramName,
8070
8120
  ancestorConnection,
@@ -8149,7 +8199,12 @@ const createRoutePattern = (
8149
8199
  childParam,
8150
8200
  childConnection,
8151
8201
  ] of childPatternObj.queryConnectionMap) {
8152
- if (childParam in resolvedParams) {
8202
+ // a name this route declares is its own state's, whatever a child
8203
+ // declares under the same name
8204
+ if (
8205
+ childParam in resolvedParams ||
8206
+ queryConnectionMap.has(childParam)
8207
+ ) {
8153
8208
  continue;
8154
8209
  }
8155
8210
  const childSignalValue = readSignalForUrlBuild(childConnection);
@@ -8195,7 +8250,9 @@ const createRoutePattern = (
8195
8250
  * - own connection signals (path + query, inherited query included)
8196
8251
  * - ancestor path params pinned by this pattern's own literals: being on
8197
8252
  * "/admin/settings" MEANS section=settings, whatever the signal says
8198
- * - ancestor query signals holding custom values
8253
+ * - custom values of the search params lent by the ancestors covering this
8254
+ * route (coversAddressesBelow). Another ancestor's search params ride
8255
+ * only on that ancestor's own url (see buildMostPreciseUrl)
8199
8256
  * - reachable descendants' connection signals. A descendant is reachable
8200
8257
  * when its literals agree with the state collected so far (a tab value
8201
8258
  * is unreachable while section conflicts with the tab route's literals)
@@ -8321,6 +8378,10 @@ const createRoutePattern = (
8321
8378
  setEntry(seg.name, selfSeg.value, conn, seg.index, "ancestor", false);
8322
8379
  }
8323
8380
  }
8381
+ if (!coversAddressesBelow(ancestor)) {
8382
+ ancestor = ancestor.parent;
8383
+ continue;
8384
+ }
8324
8385
  for (const [name, conn] of ancestor.queryConnectionMap) {
8325
8386
  if (intended.has(name)) {
8326
8387
  continue;
@@ -8467,6 +8528,10 @@ const createRoutePattern = (
8467
8528
  if (!queryReachable || value === undefined) {
8468
8529
  continue;
8469
8530
  }
8531
+ // a name this route declares is its own state's (see resolveParams)
8532
+ if (queryConnectionMap.has(paramName)) {
8533
+ continue;
8534
+ }
8470
8535
  if (conn.isCustomValue(value)) {
8471
8536
  setEntry(paramName, value, conn, undefined, "descendant", false);
8472
8537
  }
@@ -8726,20 +8791,50 @@ const createRoutePattern = (
8726
8791
  );
8727
8792
  };
8728
8793
 
8794
+ // An ancestor's url is that ancestor's address: it carries the search params
8795
+ // declared there, including those it lends to nobody (coversAddressesBelow).
8796
+ // Each ancestor climbed past adds its own, so a higher one must keep them too.
8797
+ const withOwnSearchParams = (ancestorPattern, intended) => {
8798
+ let intendedWithOwn = intended;
8799
+ for (const [name, conn] of ancestorPattern.queryConnectionMap) {
8800
+ if (intendedWithOwn.has(name)) {
8801
+ continue;
8802
+ }
8803
+ const value = readSignalForUrlBuild(conn);
8804
+ if (value === undefined || !conn.isCustomValue(value)) {
8805
+ continue;
8806
+ }
8807
+ if (intendedWithOwn === intended) {
8808
+ intendedWithOwn = new Map(intended);
8809
+ }
8810
+ intendedWithOwn.set(name, {
8811
+ value,
8812
+ connection: conn,
8813
+ segmentIndex: undefined,
8814
+ owner: "ancestor",
8815
+ explicit: false,
8816
+ pageNaming: false,
8817
+ });
8818
+ }
8819
+ return intendedWithOwn;
8820
+ };
8821
+
8729
8822
  const buildMostPreciseUrl = (params = {}) => {
8730
8823
  const { intended, reachableDescendants } = buildIntendedState(params);
8731
8824
  // ancestors: start at the immediate parent, climb while urls keep round-
8732
8825
  // tripping, keep the highest one ("/" is never a target)
8733
8826
  let ancestorUrl = null;
8734
8827
  let ancestor = patternObject.parent;
8828
+ let ancestorIntended = intended;
8735
8829
  while (ancestor && ancestor.originalPattern !== "/") {
8736
8830
  if (!isAncestorCollapseAllowed(ancestor, intended)) {
8737
8831
  break;
8738
8832
  }
8739
- const url = buildCandidateUrl(ancestor, intended, {
8833
+ ancestorIntended = withOwnSearchParams(ancestor, ancestorIntended);
8834
+ const url = buildCandidateUrl(ancestor, ancestorIntended, {
8740
8835
  dropMissing: true,
8741
8836
  });
8742
- if (!url || !verifyUrl(url, intended)) {
8837
+ if (!url || !verifyUrl(url, ancestorIntended)) {
8743
8838
  break;
8744
8839
  }
8745
8840
  debug$3(
@@ -9922,11 +10017,15 @@ const setupRoutePatterns = (routePatterns) => {
9922
10017
  }
9923
10018
  routePattern.familyRoot = root;
9924
10019
  }
9925
- // Phase 3: Inherit search parameter connections from ancestors
9926
- // Search params are global and should be inherited by descendants regardless of path segments
10020
+ // Phase 3: Inherit the search param connections of the ancestors covering
10021
+ // the addresses below them (see coversAddressesBelow)
9927
10022
  for (const routePattern of routePatternSet) {
9928
10023
  let ancestorRoutePattern = routePattern.parent;
9929
10024
  while (ancestorRoutePattern) {
10025
+ if (!coversAddressesBelow(ancestorRoutePattern)) {
10026
+ ancestorRoutePattern = ancestorRoutePattern.parent;
10027
+ continue;
10028
+ }
9930
10029
  // For each ancestor's query connection, check if it should be inherited
9931
10030
  for (const [
9932
10031
  paramName,
@@ -10036,23 +10135,6 @@ const setupRoutePatterns = (routePatterns) => {
10036
10135
  continue;
10037
10136
  }
10038
10137
  const { index } = paramSegment;
10039
- const sharesPathUpTo = (otherSegments) => {
10040
- for (let i = 0; i < index; i++) {
10041
- const seg = routePattern.pattern.segments[i];
10042
- const otherSeg = otherSegments[i];
10043
- if (!otherSeg) {
10044
- return false;
10045
- }
10046
- if (seg.type === "literal" && otherSeg.type === "literal") {
10047
- if (seg.value !== otherSeg.value) {
10048
- return false;
10049
- }
10050
- } else if (seg.type !== otherSeg.type) {
10051
- return false;
10052
- }
10053
- }
10054
- return true;
10055
- };
10056
10138
  for (const otherPattern of routePatternSet) {
10057
10139
  if (otherPattern === routePattern) {
10058
10140
  continue;
@@ -10062,7 +10144,9 @@ const setupRoutePatterns = (routePatterns) => {
10062
10144
  if (!otherSegment || otherSegment.type !== "literal") {
10063
10145
  continue;
10064
10146
  }
10065
- if (!sharesPathUpTo(otherSegments)) {
10147
+ if (
10148
+ !sharesPathUpTo(routePattern.pattern.segments, otherSegments, index)
10149
+ ) {
10066
10150
  continue;
10067
10151
  }
10068
10152
  connection.namedByLiteralRoutes = true;
@@ -10070,12 +10154,91 @@ const setupRoutePatterns = (routePatterns) => {
10070
10154
  }
10071
10155
  }
10072
10156
  }
10157
+ // Phase 4c: A default with an address of its own. "/me/:section" defaulting
10158
+ // to "players", beside "/me/players" and "/me": the default section has a
10159
+ // segment, and the bare address is another page. The param is then required,
10160
+ // and the address writes every value, the default included. Both routes must
10161
+ // be declared: with none at the bare address, that address is the default's
10162
+ // ("/admin" opens on "/admin/settings/:tab"); with no literal for the default,
10163
+ // the bare address IS the default section ("/games/me").
10164
+ // The state keeps its default (getDefaultValue): what the signal holds while
10165
+ // another page of the family says nothing about it (see route.js).
10166
+ for (const routePattern of routePatternSet) {
10167
+ const { segments } = routePattern.pattern;
10168
+ for (const connection of routePattern.pathConnectionMap.values()) {
10169
+ const defaultValue = connection.getDefaultValue();
10170
+ if (defaultValue === undefined) {
10171
+ continue;
10172
+ }
10173
+ const paramSegment = segments.find(
10174
+ (seg) => seg.type === "param" && seg.name === connection.paramName,
10175
+ );
10176
+ if (!paramSegment) {
10177
+ continue;
10178
+ }
10179
+ const { index } = paramSegment;
10180
+ let defaultHasLiteral = false;
10181
+ let bareAddressTaken = false;
10182
+ for (const otherPattern of routePatternSet) {
10183
+ if (otherPattern === routePattern) {
10184
+ continue;
10185
+ }
10186
+ const otherSegments = otherPattern.pattern.segments;
10187
+ if (!sharesPathUpTo(segments, otherSegments, index)) {
10188
+ continue;
10189
+ }
10190
+ const otherSegment = otherSegments[index];
10191
+ if (otherSegment) {
10192
+ if (
10193
+ otherSegment.type === "literal" &&
10194
+ otherSegment.value === String(defaultValue)
10195
+ ) {
10196
+ defaultHasLiteral = true;
10197
+ }
10198
+ continue;
10199
+ }
10200
+ // a route covering the addresses below it is a frame around the
10201
+ // sections, not a page standing at the bare address
10202
+ if (!coversAddressesBelow(otherPattern)) {
10203
+ bareAddressTaken = true;
10204
+ }
10205
+ }
10206
+ if (!defaultHasLiteral || !bareAddressTaken) {
10207
+ continue;
10208
+ }
10209
+ paramSegment.optional = false;
10210
+ // Both answer for the address: the url build, the signal -> url sync,
10211
+ // and checkIfLiteralCanBeOptionalWithPatternObj ("/me" does not stand for
10212
+ // "/me/players" either)
10213
+ connection.isDefaultValue = (value) => value === undefined;
10214
+ connection.isCustomValue = (value) => value !== undefined;
10215
+ }
10216
+ }
10073
10217
  // Phase 5: Calculate depths for all patterns
10074
10218
  for (const routePattern of routePatternSet) {
10075
10219
  calculatePatternDepth(routePattern);
10076
10220
  }
10077
10221
  debug$3("Pattern registry updated");
10078
10222
  };
10223
+ // Whether two patterns spell the same path before `index`: the same literals,
10224
+ // and a param wherever the other has one
10225
+ const sharesPathUpTo = (segments, otherSegments, index) => {
10226
+ for (let i = 0; i < index; i++) {
10227
+ const seg = segments[i];
10228
+ const otherSeg = otherSegments[i];
10229
+ if (!otherSeg) {
10230
+ return false;
10231
+ }
10232
+ if (seg.type === "literal" && otherSeg.type === "literal") {
10233
+ if (seg.value !== otherSeg.value) {
10234
+ return false;
10235
+ }
10236
+ } else if (seg.type !== otherSeg.type) {
10237
+ return false;
10238
+ }
10239
+ }
10240
+ return true;
10241
+ };
10079
10242
  // Store the most specific parent (closest parent in hierarchy)
10080
10243
  const getPathSegmentCount = (pattern) => {
10081
10244
  // Only count path segments, not query parameters
@@ -10137,6 +10300,9 @@ let isUpdatingRoutesFromUrl = false;
10137
10300
  * are the same state: writing the signal rewrites the address, an address
10138
10301
  * arriving from outside writes the signal, and the param disappears from the
10139
10302
  * url while the signal holds its default.
10303
+ * They exist on this route's address only, unless the pattern catches the
10304
+ * addresses below it (`/map/`, `/map/*`, the root): then they hold on every
10305
+ * route below as well. `/places` does not lend its `?q=` to `/places/clubs`.
10140
10306
  * `{ signal, default }` says what that state is worth ON THIS ROUTE: the same
10141
10307
  * step opens a creation wizard on its first question and an edition one on
10142
10308
  * its summary, each address staying clean on the step its page opens on. One
@@ -10155,7 +10321,9 @@ let isUpdatingRoutesFromUrl = false;
10155
10321
  * whose param declines the segment does not match at all: no signal is
10156
10322
  * written, `<Route fallback>` is reachable, and `/:gameId` can sit at the root
10157
10323
  * without swallowing `/cgu`. A constrained param is also required — no segment
10158
- * is not one of the values it accepts — so `/:gameId` does not match `/`.
10324
+ * is not one of the values it accepts — so `/:gameId` does not match `/`,
10325
+ * unless its default stands for the bare address (see docs/navigation.md,
10326
+ * "Declaring the sections is what makes them places").
10159
10327
  *
10160
10328
  * Constrain the shape, not the existence: whether that game exists is for the
10161
10329
  * route action and the page to answer, on a route that did match.
@@ -11370,14 +11538,17 @@ const urlWithoutSearchAndHash = (url) => {
11370
11538
  *
11371
11539
  * - `isShown()` — whether the container would still show the page. False as
11372
11540
  * soon as the address leads elsewhere: the page is about to be taken down, or
11373
- * hidden until the route transition leaving it is over, and nothing it reads
11374
- * is worth a render any more. False for good once it is kept while leaving: a
11375
- * return to its route mounts the page anew. Asked outside any render — by a
11376
- * subscription deciding whether to re-render — and it reads the routes
11541
+ * hidden until the route transition leaving it is over, nothing it reads is
11542
+ * worth a render any more, and nothing it holds speaks for the page shown (a
11543
+ * list's hold on the document, see holdDocumentScroll). False for good once
11544
+ * it is kept while leaving: a return to its route mounts the page anew. Asked
11545
+ * outside any render — by a subscription deciding whether to re-render, by
11546
+ * the scroll restoration when a position is due — and it reads the routes
11377
11547
  * untracked, so asking subscribes nobody to them.
11378
11548
  * - `leavingSignal` — true once the page is hidden and kept while its movement
11379
- * plays: still mounted, so what it fills outside its own nodes (a slot in a
11380
- * bar) steps out rather than showing with the page arriving.
11549
+ * plays: still mounted, so what it fills or moves outside its own nodes (a
11550
+ * slot in a bar, the document a list scrolls) steps out rather than showing
11551
+ * with, or moving, the page arriving. Every box of it measures 0×0 by then.
11381
11552
  */
11382
11553
  const RoutePageContext = createContext(null);
11383
11554
 
@@ -12871,6 +13042,25 @@ const getHrefTargetInfo = (
12871
13042
  };
12872
13043
  };
12873
13044
 
13045
+ // The target of a link whose caller names none: a page of another site opens
13046
+ // in a new tab. Only a page: a `tel:`, `mailto:` or `sms:` address is handed to
13047
+ // another app, a `blob:` or `data:` one is this page's own, and a tab opened
13048
+ // for them would be left behind empty.
13049
+ const getDefaultHrefTarget = (
13050
+ href,
13051
+ currentUrlString = window.location.href,
13052
+ ) => {
13053
+ const { isEmpty, isSameSite } = getHrefTargetInfo(href, currentUrlString);
13054
+ if (isEmpty || isSameSite) {
13055
+ return undefined;
13056
+ }
13057
+ const { protocol } = new URL(href, currentUrlString);
13058
+ if (protocol !== "http:" && protocol !== "https:") {
13059
+ return undefined;
13060
+ }
13061
+ return "_blank";
13062
+ };
13063
+
12874
13064
  // A field of getHrefTargetInfo that depends on the page one is on (isCurrent,
12875
13065
  // isAnchor), read through a computed of that one boolean: the caller re-renders
12876
13066
  // when it flips, not at every address write — a search param bound to a wheel
@@ -12911,6 +13101,27 @@ const linkAsksForDocument = (linkElement) => {
12911
13101
  return linkElement.hasAttribute(LINK_DOCUMENT_ATTRIBUTE);
12912
13102
  };
12913
13103
 
13104
+ /*
13105
+ * A link does not always move the tab it is in. A `target` naming another
13106
+ * browsing context (a new tab, a named window) loads the address there, in a
13107
+ * document of its own; a `download` saves it. The browser answers both
13108
+ * without touching this page, and that is the point of writing them: reading
13109
+ * the terms beside a half-filled form costs nothing. Routing such a link
13110
+ * would take this tab instead, and give no tab and no file.
13111
+ *
13112
+ * `_parent` and `_top` count as elsewhere too. Inside a frame they are; at the
13113
+ * top level the browser treats them as `_self` and loads a document, which
13114
+ * costs a load and nothing else.
13115
+ */
13116
+
13117
+ const linkOpensElsewhere = (linkElement) => {
13118
+ const target = linkElement.getAttribute("target");
13119
+ if (target && target !== "_self") {
13120
+ return true;
13121
+ }
13122
+ return linkElement.hasAttribute("download");
13123
+ };
13124
+
12914
13125
  /*
12915
13126
  * A press aims at a place; it does not always go one step deeper. A row of tabs
12916
13127
  * is a lateral move — the neighbour is one finger away — so the whole row should
@@ -13240,8 +13451,10 @@ const setupBrowserIntegrationViaHistory = ({
13240
13451
  // Ignore non-left clicks
13241
13452
  return;
13242
13453
  }
13243
- if (e.metaKey) {
13244
- // Ignore clicks with meta key (e.g. open in new tab)
13454
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
13455
+ // A modifier asks the browser for something other than this tab: a new
13456
+ // tab (meta on macOS, ctrl elsewhere), a new window (shift), a download
13457
+ // (alt).
13245
13458
  return;
13246
13459
  }
13247
13460
  if (e.defaultPrevented) {
@@ -13252,6 +13465,10 @@ const setupBrowserIntegrationViaHistory = ({
13252
13465
  if (linkElement.hasAttribute("data-readonly")) {
13253
13466
  return;
13254
13467
  }
13468
+ if (linkOpensElsewhere(linkElement)) {
13469
+ // Another tab or window, or a download (see link_elsewhere.js).
13470
+ return;
13471
+ }
13255
13472
  const href = linkElement.href;
13256
13473
  const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
13257
13474
  getHrefTargetInfo(href);
@@ -17538,6 +17755,9 @@ const surfaceTextCss = /* css */ `
17538
17755
  }
17539
17756
  `;
17540
17757
 
17758
+ // What a callout's own JSX may know about the callout around it: how to close
17759
+ // it, and the element — whose id is what `commandFor` needs when the button
17760
+ // closing it is not inside it (inside, `--navi-close` finds it on its own).
17541
17761
  const CalloutContext = createContext();
17542
17762
  const useCalloutRequestClose = () => {
17543
17763
  return useContext(CalloutContext)?.requestClose;
@@ -17671,7 +17891,7 @@ const CALLOUT_STATUS_GLYPH_VIEWBOX = "0 0 540 540";
17671
17891
  const CALLOUT_STATUS_GLYPH_PATH = "m232.5,121 8,196h59l8-196zm36.5,224a37,37 0 1,0 2,0z";
17672
17892
 
17673
17893
  /**
17674
- * @type {import("ignore:preact").FunctionComponent<{
17894
+ * @type {import("preact").FunctionComponent<{
17675
17895
  * status?: "info" | "warning" | "error" | "success" | "none",
17676
17896
  * shape?: "square" | "circle",
17677
17897
  * }>}
@@ -22617,23 +22837,12 @@ defineInteractionDetector({
22617
22837
  // A link and an image are draggable without anyone asking, and a native
22618
22838
  // drag IS press-and-move: the browser claims the gesture, takes the pointer
22619
22839
  // events with it and paints a ghost of the row the hand is trying to swipe.
22620
- // Two things are needed, and neither covers the other:
22621
- // - `draggable` refuses it on the element itself, before it starts;
22622
- // - a swipe is usually a row with a link or a thumbnail INSIDE it, and
22623
- // those are draggable in their own right. `dragstart` bubbles, so
22624
- // refusing it here refuses theirs too — and it is the only refusal every
22625
- // browser honours (`-webkit-user-drag` is one engine's).
22626
- // The cost is stated rather than worked around: an element that takes a
22627
- // swipe cannot also be dragged out of the page, because there is one
22628
- // gesture and it cannot mean both.
22840
+ // `draggable` refuses it on the element itself, before it starts; the drag
22841
+ // of a link or a thumbnail INSIDE it is refused by the travel reading the
22842
+ // press (startDragToTravel). The cost is stated rather than worked around:
22843
+ // an element that takes a swipe cannot also be dragged out of the page,
22844
+ // because there is one gesture and it cannot mean both.
22629
22845
  mark("draggable", "false");
22630
- const onDragStart = (dragStartEvent) => {
22631
- dragStartEvent.preventDefault();
22632
- };
22633
- element.addEventListener("dragstart", onDragStart);
22634
- undo.push(() => {
22635
- element.removeEventListener("dragstart", onDragStart);
22636
- });
22637
22846
 
22638
22847
  // A touch this element may take has to be refusable before the finger
22639
22848
  // lands, or the browser can cancel an established swipe mid-gesture by
@@ -23780,9 +23989,10 @@ const isMac = detectMac();
23780
23989
  * place rather than about this box.
23781
23990
  *
23782
23991
  * `data-pan-after-hold="kept"` asks for that wait once rather than before every
23783
- * pan: the surface given the hand keeps it — panning on contact, like the same
23784
- * plan opened full screen — and asks again after a press has landed away from
23785
- * it, which is the hand saying it has moved on. What answers "away" is the same
23992
+ * pan: the surface a finger has given the hand keeps it — panning on contact,
23993
+ * like the same plan opened full screen — and asks again after a press has
23994
+ * landed away from it, which is the hand saying it has moved on. A mouse never
23995
+ * owed the wait, so it neither spends it nor marks it. What answers "away" is the same
23786
23996
  * thing that closes a popup, and the surface is the one that watches for it, so
23787
23997
  * an application never keeps a `pointerdown` listener on the window to know.
23788
23998
  * `[data-hand-kept]` is on the element for as long as it holds the hand that
@@ -24911,7 +25121,8 @@ const isSameKey = (browserEventKey, key) => {
24911
25121
  * and that field is the control the declaration speaks for: an ArrowUp declared
24912
25122
  * on a text field is the field saying what ArrowUp does there, not a container
24913
25123
  * taking the caret from a field inside it. A field inside a box that declares
24914
- * keys for something else (a row, a list) keeps every key it has a use for.
25124
+ * keys for something else (a row, a list) keeps every key it has a use for,
25125
+ * even when it is the only control in that box.
24915
25126
  *
24916
25127
  * A declared key that matches takes the key: its default does not happen as
24917
25128
  * well, whether the gate lets the interaction through or refuses it and says
@@ -24937,7 +25148,10 @@ defineInteractionDetector({
24937
25148
  };
24938
25149
  }
24939
25150
  const onKeyDown = createOnKeyDownForShortcuts(shortcuts, {
24940
- ownsTarget: (target) => target === findNearestControlHost(element),
25151
+ // Only the control's own root speaks for its host: a box that wraps a
25152
+ // single control is reached by that control's gate, not handed its keys.
25153
+ ownsTarget: (target) =>
25154
+ isControlRoot(element) && target === findNearestControlHost(element),
24941
25155
  });
24942
25156
  element.addEventListener("keydown", onKeyDown);
24943
25157
  return () => {
@@ -29005,6 +29219,127 @@ const useForceRender = () => {
29005
29219
 
29006
29220
  // https://github.com/preactjs/preact/issues/4756
29007
29221
 
29222
+ /**
29223
+ * Reads the current state of an action and returns `[data, loading, error, dismissError]`.
29224
+ *
29225
+ * By default (`loading` and `error` not set) the component suspends on load
29226
+ * and throws on failure, delegating both states to the nearest `<Loading>` and
29227
+ * `<ErrorBoundary>` ancestors. Pass `loading: true` or `error: true` to handle
29228
+ * either state directly inside the component instead.
29229
+ *
29230
+ * Return value: `[data, loading, error, dismissError]`
29231
+ * - `data` — the action's last successful data, or `undefined` if it has never completed
29232
+ * - `loading` — `true` while the action is running (only when `loading: true` is passed)
29233
+ * - `error` — the Error thrown by the action (only when `error: true` is passed)
29234
+ * - `dismissError` — takes that error out of what this hook returns, nothing else (only when `error: true` is passed)
29235
+ *
29236
+ * Stale data is always returned: the previous `data` remains available while
29237
+ * the action re-runs and after a run has failed, so a screen that had something
29238
+ * to show never has it taken away. What is drawn over it — a loading indicator,
29239
+ * the error, nothing — is entirely up to the component.
29240
+ *
29241
+ * `loading` covers the delay of a debounced binding
29242
+ * (`bindParams(paramsSignal, { debounce })`, `actionRunEffect({ debounce })`)
29243
+ * as well as the run itself: from the moment the params move, what is on screen
29244
+ * is the previous answer, and a screen showing "searching…" while someone types
29245
+ * needs to know it. Suspending has nothing to wait on during that delay, so
29246
+ * only `loading: true` reads it — the default keeps showing the previous answer
29247
+ * until the run really starts.
29248
+ *
29249
+ * When `loading` is not set (default), the component suspends until data is
29250
+ * ready, so `data` is always defined when the component renders and `loading`
29251
+ * is always `false`. With `loading: true` the component never suspends: every
29252
+ * state — running, settling, params nobody has started yet, a failure whose
29253
+ * message was dismissed — comes back as a value, so the subtree it draws is
29254
+ * never swapped for the boundary above and remounted.
29255
+ *
29256
+ * When `error` is not set (default), any action failure causes the component
29257
+ * to throw, so `error` is always `undefined` when the component renders. With
29258
+ * `error: true` the failure arrives beside the data the action already had — a
29259
+ * message on the content, not one in place of it. `dismissError()` removes the
29260
+ * message and nothing else: nothing is asked again, and what comes back is the
29261
+ * same state minus the error — the same data, or none at all when nothing ever
29262
+ * came back.
29263
+ *
29264
+ * `data === undefined` says there is nothing to show and `loading` says whether
29265
+ * anything is on its way: a skeleton reads both, or it claims a load nobody
29266
+ * started (see docs/data_states.md).
29267
+ *
29268
+ * @param {object | (() => Promise<any>) | Promise<any>} promiseOrAction -
29269
+ * an action, or the request this component owns written as a function:
29270
+ * made into an action once, on the first render, and kept for the life of
29271
+ * the instance — so it may be written inline — with what it resolves to as
29272
+ * the data and `run` implied. Code loaded on demand reads this way
29273
+ * (`() => import("./plan.jsx").then((m) => m.Plan)`, see
29274
+ * docs/dynamic_import.md), as does anything else asked for once per mount.
29275
+ * @param {{ loading?: true, error?: true, run?: true, prerun?: true, onLoad?: (data: any, context: {params: any}) => void }} [options]
29276
+ * @param {true} [options.run] - The fallback for data nothing else can ask for:
29277
+ * this component owns the request and starts it, from the render that reads
29278
+ * it. `loading` and `error` keep their meaning — delegated by default,
29279
+ * handled inline when asked for — so a run started here suspends into
29280
+ * `<Loading>` like any other.
29281
+ *
29282
+ * A failure is shown, not asked again: a render that retried it would retry
29283
+ * it in a loop while the network is down — that is `rerun()`'s job. Except a
29284
+ * failure a prerun left (a `prerun: true` head start, a press that preran
29285
+ * it): nothing on screen asked for that run, so this asks again, once.
29286
+ *
29287
+ * Prefer a `routeAction` wherever the parameter is one the address holds (a
29288
+ * path param, a search param bound to a `stateSignal`), popup included: it is
29289
+ * asked for when the address changes, with everything else that address
29290
+ * needs, whereas this one cannot start before the component that draws it
29291
+ * exists — one render late, and behind whatever gesture mounted it. What is
29292
+ * left for `run` is the parameter chosen inside the component and dying with
29293
+ * it — and any run that WRITES, whatever its parameter: an address asks again
29294
+ * on every arrival, which is right for a read and wrong for a write (see
29295
+ * docs/actions.md and docs/popup_open.md).
29296
+ * @param {true} [options.prerun] - A head start: this component starts a
29297
+ * read that another one, mounted later, owns with `run: true` — the read
29298
+ * leaves as soon as its params are known instead of once the owner exists.
29299
+ * Started from the render like `run`, as a prerun: it draws none of it, so
29300
+ * nothing suspends (no `<Loading>` needed) and a failure is neither thrown
29301
+ * nor reported — the owner asks again. A head start still running when the
29302
+ * owner reads it is the owner's wait.
29303
+ * @param {(data: any, context: {params: any}) => void} [options.onLoad] - what
29304
+ * this screen does with the data ONCE, when it becomes known: seed the fields
29305
+ * someone is about to edit, remember where a list was, focus something.
29306
+ *
29307
+ * Fired once per set of params — never again for a rerun that brings the same
29308
+ * thing back. That is the whole point of it: a save, a refresh, a poll all
29309
+ * hand the data over again, and copying it a second time would overwrite what
29310
+ * the person is in the middle of writing. The action knows what it ran with,
29311
+ * so nobody has to guess the dependency.
29312
+ *
29313
+ * Called from a layout effect, so what it writes belongs to the same tick as
29314
+ * the render that got the data — a `<Form pristineKey>` taking its reference
29315
+ * then sees the filled fields rather than the empty ones.
29316
+ * @returns {[data: unknown, loading: boolean, error: Error | undefined, dismissError: (() => void) | undefined]}
29317
+ *
29318
+ * @example <caption>Default — delegate both states (least code)</caption>
29319
+ * const [user] = useAsyncData(userAction);
29320
+ * // user is always defined here — never loading, never error
29321
+ *
29322
+ * @example <caption>Handle loading inline while keeping stale data</caption>
29323
+ * const [user, loading] = useAsyncData(userAction, { loading: true });
29324
+ * // First load: user === undefined, loading === true → show skeleton
29325
+ * // While refreshing: user === <stale>, loading === true → show stale + spinner
29326
+ * // Done: user === <fresh>, loading === false
29327
+ *
29328
+ * @example <caption>Handle error inline</caption>
29329
+ * const [user, , error] = useAsyncData(userAction, { error: true });
29330
+ * if (error) return <ErrorCard message={error.message} />;
29331
+ *
29332
+ * @example <caption>Say the failure over the rows already on screen</caption>
29333
+ * const [items, , error, dismissError] = useAsyncData(listAction, { error: true });
29334
+ * // First failure: items === undefined → only the message
29335
+ * // Failed refresh: items === <stale> → the message above the rows
29336
+ * return (
29337
+ * <>
29338
+ * {error && <ErrorStrip error={error} onClose={dismissError} />}
29339
+ * {items && <List items={items} />}
29340
+ * </>
29341
+ * );
29342
+ */
29008
29343
  const useAsyncData = (promiseOrAction, {
29009
29344
  loading = "delegate",
29010
29345
  error = "delegate",
@@ -29454,7 +29789,7 @@ const LoadingFallback = ({
29454
29789
  * ones that would render fine.
29455
29790
  *
29456
29791
  * @param {object} props
29457
- * @param {Function|import("ignore:preact").VNode} [props.fallback] - what is displayed
29792
+ * @param {Function|import("preact").VNode} [props.fallback] - what is displayed
29458
29793
  * instead of the children: an element, or a component receiving
29459
29794
  * `{ error, resetError }`. Without it the boundary is transparent.
29460
29795
  * @param {() => void} [props.onReset] - called when the fallback dismisses the
@@ -29813,6 +30148,12 @@ const hasWidthHeight = (context) => {
29813
30148
  (context.styles.height || context.remainingProps.height)
29814
30149
  );
29815
30150
  };
30151
+ // The `stretch` sizing keyword: the containing block's width minus the box's
30152
+ // own margins, so a box with a margin fills its row without leaving it (100%
30153
+ // adds the margins on top). Spelled with the prefix because an inline style
30154
+ // holds one value, and this is the one name Chromium, WebKit and Firefox all
30155
+ // read (Firefox 155 has no unprefixed `stretch`).
30156
+ const FILL_AVAILABLE_WIDTH = "-webkit-fill-available";
29816
30157
  const DIMENSION_PROPS = {
29817
30158
  boxSizing: PASS_THROUGH,
29818
30159
  width: PASS_THROUGH,
@@ -29865,16 +30206,16 @@ const DIMENSION_PROPS = {
29865
30206
  return { flexGrow: expandWeight(value), flexBasis: "0%" };
29866
30207
  }
29867
30208
  if (parentBoxFlow === "flex-y" || parentBoxFlow === "inline-flex-y") {
29868
- // width 100% is what fills the cross axis; align-self stretch would do
30209
+ // A width is what fills the cross axis; align-self stretch would do
29869
30210
  // the same but takes the parent's alignX away — a maxWidth-capped item
29870
30211
  // in an alignX="center" parent would then sit at the start instead of
29871
30212
  // in the middle.
29872
30213
  return {
29873
- width: "100%",
30214
+ width: FILL_AVAILABLE_WIDTH,
29874
30215
  };
29875
30216
  }
29876
30217
  // Can't use flexGrow — parent is not flex-x
29877
- return { width: "100%" };
30218
+ return { width: FILL_AVAILABLE_WIDTH };
29878
30219
  },
29879
30220
  expandY: (value, { parentBoxFlow }) => {
29880
30221
  if (!value) {
@@ -30435,6 +30776,7 @@ const getVisualChildStylePropStrategy = (name) => {
30435
30776
  };
30436
30777
 
30437
30778
  const isStyleProp = (name) => STYLE_PROP_NAME_SET.has(name);
30779
+ const isTypoProp = (name) => TYPO_PROP_NAME_SET.has(name);
30438
30780
 
30439
30781
  const getStringifier = (key) => {
30440
30782
  if (SPACING_PROP_SET.has(key)) {
@@ -31076,10 +31418,10 @@ const readCSSVarName = entry => Array.isArray(entry) ? entry[0] : entry;
31076
31418
  const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
31077
31419
 
31078
31420
  /**
31079
- * @type {import("ignore:preact").FunctionComponent<{
31421
+ * @type {import("preact").FunctionComponent<{
31080
31422
  * as?: string,
31081
31423
  * className?: string,
31082
- * style?: import("ignore:preact").JSX.CSSProperties & { [pseudo: string]: import("ignore:preact").JSX.CSSProperties },
31424
+ * style?: import("preact").JSX.CSSProperties & { [pseudo: string]: import("preact").JSX.CSSProperties },
31083
31425
  * styleCSSVars?: { [propName: string]: string | [string, string] },
31084
31426
  * inline?: boolean,
31085
31427
  * block?: boolean,
@@ -31094,7 +31436,7 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
31094
31436
  * hasChildUsingForwardedProps?: boolean,
31095
31437
  * childPropSet?: Set<string>,
31096
31438
  * preventInitialTransition?: boolean,
31097
- * separator?: import("ignore:preact").ComponentChildren | ((index: number) => import("ignore:preact").ComponentChildren),
31439
+ * separator?: import("preact").ComponentChildren | ((index: number) => import("preact").ComponentChildren),
31098
31440
  * selfInteractions?: string,
31099
31441
  * interactions?: { [type: string]: "request_action" | "request_ui_action" | "refuse" | ((event: Event) => void) | false | null | undefined },
31100
31442
  * alignX?: string,
@@ -31106,7 +31448,7 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
31106
31448
  * body?: boolean,
31107
31449
  * sticky?: boolean,
31108
31450
  * mount?: "always" | "after-paint",
31109
- * children?: import("ignore:preact").ComponentChildren,
31451
+ * children?: import("preact").ComponentChildren,
31110
31452
  * [key: string]: any,
31111
31453
  * }>}
31112
31454
  * @param {"x"|"y"|boolean} [flex] A flex container: `"x"` (or `true`) side by
@@ -33713,7 +34055,7 @@ const debug$1 = (...args) => {
33713
34055
  * matches. `routeFallback()` gives that page a name, which is what lets a
33714
34056
  * movement be written about it and its `matchingSignal` be read from outside
33715
34057
  * the tree (see route_fallback.js).
33716
- * @param {Function|import("ignore:preact").VNode} [props.element] - what the branch renders
34058
+ * @param {Function|import("preact").VNode} [props.element] - what the branch renders
33717
34059
  * @param {object} [props.elementProps] - props given to `element`
33718
34060
  *
33719
34061
  * A branch says what it renders, not what happens when it cannot: loading and
@@ -36102,7 +36444,7 @@ const css$13 = /* css */`@property --navi-route-transition-leave {
36102
36444
  * that. An application that already has an element holding its pages can mark
36103
36445
  * that one with `data-navi-route-transition-area` rather than nesting another.
36104
36446
  *
36105
- * @type {import("ignore:preact").FunctionComponent<{ children?: any, [key: string]: any }>}
36447
+ * @type {import("preact").FunctionComponent<{ children?: any, [key: string]: any }>}
36106
36448
  */
36107
36449
  const RouteTransitionArea = ({
36108
36450
  children,
@@ -36892,7 +37234,7 @@ const beginTransition = ({
36892
37234
  warnAboutNamesEscapingArea(areaElement, capturedNames);
36893
37235
  return;
36894
37236
  }
36895
- for (const name of capturedNames) {
37237
+ for (const name of capturedNames.keys()) {
36896
37238
  if (name === "root") {
36897
37239
  continue;
36898
37240
  }
@@ -37095,9 +37437,10 @@ const ignoreSkipped$1 = () => {};
37095
37437
 
37096
37438
  // The names the browser captured, read off the pictures themselves: what was
37097
37439
  // asked for in CSS and what was taken are not the same question (see
37098
- // viewTransitionReady).
37440
+ // viewTransitionReady). Each with the states it was taken in: a name taken in
37441
+ // only one of them pairs with nothing.
37099
37442
  const capturedViewTransitionNames = () => {
37100
- const names = new Set();
37443
+ const names = new Map();
37101
37444
  for (const animation of document.getAnimations()) {
37102
37445
  const pseudoElement = animation.effect?.pseudoElement;
37103
37446
  if (!pseudoElement || !pseudoElement.startsWith("::view-transition")) {
@@ -37107,43 +37450,59 @@ const capturedViewTransitionNames = () => {
37107
37450
  if (nameStart === -1) {
37108
37451
  continue;
37109
37452
  }
37110
- names.add(pseudoElement.slice(nameStart + 1, -1));
37453
+ const name = pseudoElement.slice(nameStart + 1, -1);
37454
+ let states = names.get(name);
37455
+ if (!states) {
37456
+ states = {
37457
+ old: false,
37458
+ new: false
37459
+ };
37460
+ names.set(name, states);
37461
+ }
37462
+ if (pseudoElement.startsWith("::view-transition-old(")) {
37463
+ states.old = true;
37464
+ } else if (pseudoElement.startsWith("::view-transition-new(")) {
37465
+ states.new = true;
37466
+ }
37111
37467
  }
37112
37468
  return names;
37113
37469
  };
37114
37470
 
37115
- // Nested groups keep a name written inside the area inside its picture (see
37116
- // the @supports block in the CSS above). Without them the name escapes to the
37117
- // top of the ::view-transition tree and the element it belongs to stands still,
37118
- // fading on its own, while the pages move under it.
37471
+ // A name written inside the area is a group of its own: it stands where it was
37472
+ // captured while the area's two pictures carry the movement, in every browser.
37473
+ // Nested groups only keep it cut at the pages' edge (see the @supports block in
37474
+ // the CSS above). Paired with the same name on the other page it is a morph;
37475
+ // taken in one state only it pairs with nothing, and is a piece of its page
37476
+ // left behind, fading, while that page moves.
37119
37477
  const NESTED_GROUPS_SUPPORTED = window.CSS.supports("view-transition-group", "contain");
37120
37478
  const warnAboutNamesEscapingArea = (areaElement, capturedNames) => {
37121
- if (NESTED_GROUPS_SUPPORTED) {
37122
- return;
37123
- }
37124
- let escapedName = null;
37125
- for (const name of capturedNames) {
37479
+ const unpairedNames = new Set();
37480
+ for (const [name, states] of capturedNames) {
37126
37481
  if (name === "root" || name === AREA_NAME || name.startsWith(FURNITURE_NAME_PREFIX)) {
37127
37482
  continue;
37128
37483
  }
37129
- escapedName = name;
37130
- break;
37484
+ if (states.old && states.new) {
37485
+ continue;
37486
+ }
37487
+ unpairedNames.add(name);
37131
37488
  }
37132
37489
  // A name captured next to the area is not necessarily inside it — a bar the
37133
37490
  // application animates on the same clock is named on purpose. The subtree is
37134
37491
  // walked only once something is there to find, so the common case reads
37135
- // nothing.
37136
- if (!escapedName) {
37492
+ // nothing. The page being left is still in it, hidden (keepLeavingPages in
37493
+ // route.jsx), so a name taken only in the old state is found too.
37494
+ if (unpairedNames.size === 0) {
37137
37495
  return;
37138
37496
  }
37139
37497
  for (const descendant of areaElement.querySelectorAll("*")) {
37140
37498
  const {
37141
37499
  viewTransitionName
37142
37500
  } = getComputedStyle(descendant);
37143
- if (!viewTransitionName || viewTransitionName === "none") {
37501
+ if (!unpairedNames.has(viewTransitionName)) {
37144
37502
  continue;
37145
37503
  }
37146
- 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 }).`);
37504
+ 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.";
37505
+ 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).`);
37147
37506
  return;
37148
37507
  }
37149
37508
  };
@@ -37288,6 +37647,8 @@ const installRouteTransitionCss = () => {
37288
37647
  };
37289
37648
 
37290
37649
  installImportMetaCssBuild(import.meta);
37650
+ // The pictures carrying a travel, among everything else the movement takes
37651
+ // along (see findLeadAnimation).
37291
37652
  const TRAVEL_LEAD_NAMES = ["navi-route-travel"];
37292
37653
 
37293
37654
  // A browser with no view transitions of its own has no picture of the page
@@ -37555,7 +37916,7 @@ const css$12 = /* css */`.navi_route_travel {
37555
37916
  `;
37556
37917
 
37557
37918
  /**
37558
- * @type {import("ignore:preact").FunctionComponent<{
37919
+ * @type {import("preact").FunctionComponent<{
37559
37920
  * routes?: Array<object|{route: object, params?: object}>,
37560
37921
  * axis?: "x"|"y",
37561
37922
  * travelByDrag?: boolean,
@@ -40383,7 +40744,7 @@ const resetCommand = (source, event) => {
40383
40744
  // answers: "save" stays, "delete" goes back to the list. Same shape as the
40384
40745
  // browser's own formaction/formmethod, and the same shape the action already
40385
40746
  // has (it is told which button requested it).
40386
- const resolveAfterSend = (target, requester) => {
40747
+ const resolveAfterSend = (target, requester, surface) => {
40387
40748
  const askedForByRequester = requester?.getAttribute?.("data-after-send");
40388
40749
  if (askedForByRequester) {
40389
40750
  return askedForByRequester;
@@ -40392,11 +40753,6 @@ const resolveAfterSend = (target, requester) => {
40392
40753
  if (askedFor) {
40393
40754
  return askedFor;
40394
40755
  }
40395
- // From above the target: a popup that IS the send target is handled on its
40396
- // own (see the aria-expanded branch below), and must not answer twice.
40397
- const surface = target.parentElement?.closest(
40398
- `[data-slide], [aria-expanded]`,
40399
- );
40400
40756
  if (!surface) {
40401
40757
  return undefined;
40402
40758
  }
@@ -40408,6 +40764,11 @@ const resolveAfterSend = (target, requester) => {
40408
40764
  }
40409
40765
  return undefined;
40410
40766
  };
40767
+ // The surface a send answers when the control says nothing. Found from above
40768
+ // the target: a popup that IS the send target is handled on its own (see the
40769
+ // aria-expanded branch in sendCommand), and must not answer twice.
40770
+ const resolveSendSurface = (target) =>
40771
+ target.parentElement?.closest(`[data-slide], [aria-expanded]`);
40411
40772
  // What follows a send is caused by the send, not by the gesture: the send
40412
40773
  // answered the gesture and may have prevented it (an Enter must not also click
40413
40774
  // the submit button, nor activate the trigger of the popup it closed), and a
@@ -40489,20 +40850,30 @@ const sendCommand = (source, event, { requester }) => {
40489
40850
  // and the popup must stay open — with the form still in front of the
40490
40851
  // user, showing what it is waiting for.
40491
40852
  let invalid = false;
40492
- // What follows the send is read at the moment it runs, never before it:
40493
- // it depends on the requester (Enter in a field sends through the first
40494
- // submit button, and what follows is that button's answer), and on
40495
- // anything the send itself decided — an action that learned where to go
40496
- // from the response writes it on the form while it runs
40497
- // (data-after-send), and this is what picks it up.
40853
+ // What follows the send is aimed at the press, like a button's command
40854
+ // waiting for its action (see resolveNaviCommand): the send may take the
40855
+ // form out of the document (a form keyed on what it edits, a row that
40856
+ // moves once saved), and resolved from there it finds no popup to close.
40857
+ // Which command it is gets read again once the send went through: an
40858
+ // action that learned where to go from the response writes it on the
40859
+ // form while it runs (data-after-send). That one can only be aimed when
40860
+ // it is read.
40861
+ const surface = resolveSendSurface(target);
40862
+ const sentEvent = createSentEvent(event);
40863
+ const afterSendAtPress = resolveAfterSend(target, sendRequester, surface);
40864
+ const runAimedAtPress = afterSendAtPress
40865
+ ? resolveNaviCommand(source, afterSendAtPress, sentEvent)
40866
+ : undefined;
40498
40867
  const runAfterSend = () => {
40499
- const afterSend = resolveAfterSend(target, sendRequester);
40868
+ const afterSend = resolveAfterSend(target, sendRequester, surface);
40500
40869
  if (!afterSend) {
40501
40870
  return;
40502
40871
  }
40503
- triggerNaviCommand(source, afterSend, createSentEvent(event), {
40504
- optional: true,
40505
- });
40872
+ if (afterSend === afterSendAtPress) {
40873
+ runAimedAtPress?.();
40874
+ return;
40875
+ }
40876
+ triggerNaviCommand(source, afterSend, sentEvent);
40506
40877
  };
40507
40878
  const {
40508
40879
  result: sent,
@@ -41457,6 +41828,12 @@ const isDeclaredInsidePopup = (target, popupId) =>
41457
41828
  * Lives on the popup's own content element rather than in the backdrop's
41458
41829
  * handler, for both renderers: the backdrop is a sibling behind the popup
41459
41830
  * (never an ancestor), so a press inside the popup's box never reaches it.
41831
+ *
41832
+ * Inside a box that travels by drag — a row of slides walked by a swipe, a
41833
+ * sheet pushed back towards its edge — the region is also where that travel
41834
+ * starts, and at the mousedown nobody knows yet which of the two the press is.
41835
+ * The dismissal then waits for the click, which only comes if the press stayed
41836
+ * a press: a travel swallows the one it leaves behind.
41460
41837
  */
41461
41838
  const handlePressOnOutsideRegion = (
41462
41839
  mouseDownEvent,
@@ -41487,11 +41864,36 @@ const handlePressOnOutsideRegion = (
41487
41864
  mouseDownEvent.preventDefault();
41488
41865
  return;
41489
41866
  }
41490
- if (pressOutside === "close" || pressOutside === "cancel") {
41491
- openController.requestClose(mouseDownEvent, {
41492
- isCancel: pressOutside === "cancel",
41867
+ if (pressOutside !== "close" && pressOutside !== "cancel") {
41868
+ return;
41869
+ }
41870
+ const isCancel = pressOutside === "cancel";
41871
+ if (isPressDisputedByDrag(target)) {
41872
+ onClickOfThisPress(target, (clickEvent) => {
41873
+ openController.requestClose(clickEvent, { isCancel });
41493
41874
  });
41875
+ return;
41494
41876
  }
41877
+ openController.requestClose(mouseDownEvent, { isCancel });
41878
+ };
41879
+
41880
+ // The click ending the press whose mousedown landed on `element`. A click goes
41881
+ // to what the press and the release have in common, so it reaches `element`
41882
+ // only when the release stayed on it or inside it. A press that never clicks
41883
+ // leaves it armed until the next one, and no longer — a click always comes
41884
+ // after a press, so it cannot reach the click of another (the rule
41885
+ // click_suppression.js in @jsenv/dom goes by).
41886
+ const onClickOfThisPress = (element, callback) => {
41887
+ const onClick = (clickEvent) => {
41888
+ disarm();
41889
+ callback(clickEvent);
41890
+ };
41891
+ const disarm = () => {
41892
+ element.removeEventListener("click", onClick);
41893
+ window.removeEventListener("pointerdown", disarm, { capture: true });
41894
+ };
41895
+ element.addEventListener("click", onClick);
41896
+ window.addEventListener("pointerdown", disarm, { capture: true });
41495
41897
  };
41496
41898
 
41497
41899
  /**
@@ -46838,6 +47240,9 @@ const uiStateHoldsNothing = (uiState) => {
46838
47240
  * handles the timing centrally rather than each component having to manage its
46839
47241
  * own debounce logic.
46840
47242
  */
47243
+ // Sentinel used as the initial value of lastActionValueRef.
47244
+ // Distinct from undefined so that undefined (e.g. unchecked radio) can itself
47245
+ // be stored as a valid "last action value" and trigger the dedup logic.
46841
47246
  const NO_ACTION_YET = Symbol("no_action_yet");
46842
47247
 
46843
47248
  // The control types rendered as an element `<label for>` can name: HTML's
@@ -46937,13 +47342,18 @@ const LISTENERS_CALLED_BY_REACTIONS = new Set(["onMouseDown", "onClick", "onKeyD
46937
47342
  * the control opens, so that a listener hears the control as a whole.
46938
47343
  * `onMouseDown`, `onClick`, `onKeyDown`, `onPaste` and `onInput` stay the
46939
47344
  * host's either way: the control's own reactions to them call them.
47345
+ * @param {"host"|"root"} [options.nameOn] Where `aria-label` and
47346
+ * `aria-labelledby` land: where the role is. `"host"`, the default, for every
47347
+ * control whose host is what is announced; `"root"` for a control whose role
47348
+ * is on its root and whose host is an `aria-hidden` carrier (a wheel).
46940
47349
  * @returns {[controlRootProps, controlHostProps, { uiStateController }]}
46941
47350
  */
46942
47351
  const useControlProps = (props, {
46943
47352
  controlType,
46944
47353
  persists,
46945
47354
  uiActionInternal,
46946
- listenersOn = "host"
47355
+ listenersOn = "host",
47356
+ nameOn = "host"
46947
47357
  }) => {
46948
47358
  const debugUIState = useDebugUIState();
46949
47359
  const debugAction = useDebugAction();
@@ -47032,6 +47442,7 @@ const useControlProps = (props, {
47032
47442
  boundAction,
47033
47443
  controlInfo,
47034
47444
  listenersOn,
47445
+ nameOn,
47035
47446
  listenersCalledByReactions: LISTENERS_CALLED_BY_REACTIONS
47036
47447
  });
47037
47448
  {
@@ -48283,6 +48694,7 @@ const useInteractiveProps = (props, {
48283
48694
  boundAction,
48284
48695
  controlInfo,
48285
48696
  listenersOn,
48697
+ nameOn,
48286
48698
  listenersCalledByReactions
48287
48699
  }) => {
48288
48700
  const {
@@ -48290,6 +48702,7 @@ const useInteractiveProps = (props, {
48290
48702
  } = props;
48291
48703
  const [controlRootProps, controlHostProps] = splitControlProps(props, {
48292
48704
  listenersOn,
48705
+ nameOn,
48293
48706
  listenersCalledByReactions
48294
48707
  });
48295
48708
  controlRootProps["navi-control"] = controlInfo.controlType;
@@ -48903,8 +49316,10 @@ const useInteractiveProps = (props, {
48903
49316
  }
48904
49317
  return [controlRootProps, controlHostProps];
48905
49318
  };
49319
+ const NAME_ATTRIBUTE_SET = new Set(["aria-label", "aria-labelledby"]);
48906
49320
  const splitControlProps = (props, {
48907
49321
  listenersOn,
49322
+ nameOn,
48908
49323
  listenersCalledByReactions
48909
49324
  }) => {
48910
49325
  const {
@@ -48915,6 +49330,10 @@ const splitControlProps = (props, {
48915
49330
  };
48916
49331
  const controlRootProps = {};
48917
49332
  for (const key of Object.keys(props)) {
49333
+ if (nameOn === "root" && NAME_ATTRIBUTE_SET.has(key)) {
49334
+ controlRootProps[key] = props[key];
49335
+ continue;
49336
+ }
48918
49337
  const attributeName = controlAttributeFromProp(key);
48919
49338
  if (attributeName) {
48920
49339
  controlHostProps[attributeName] = props[key];
@@ -51044,7 +51463,7 @@ const css$$ = /* css */`.navi_text_anchor {
51044
51463
  * @param {{ size?: number, verticalAlign?: string }} [lineLayout]
51045
51464
  * Describes the surrounding line context. Used as layout-effect dependencies so the correction
51046
51465
  * reruns when the surrounding text's font-size or vertical-align changes.
51047
- * @param {import("ignore:preact").RefObject} childRef — ref on the child element to reposition
51466
+ * @param {import("preact").RefObject} childRef — ref on the child element to reposition
51048
51467
  */
51049
51468
  const TextAnchor = ({
51050
51469
  childRef,
@@ -51706,25 +52125,25 @@ const shouldInjectSpacingBetween = (left, right) => {
51706
52125
  * See `docs/typography.md` for the decisions behind it — truncating, rows made
51707
52126
  * of an icon, a text and an icon, and where a line may break.
51708
52127
  *
51709
- * @type {import("ignore:preact").FunctionComponent<{
51710
- * children?: import("ignore:preact").ComponentChildren,
52128
+ * @type {import("preact").FunctionComponent<{
52129
+ * children?: import("preact").ComponentChildren,
51711
52130
  * as?: string,
51712
52131
  * className?: string,
51713
- * style?: import("ignore:preact").JSX.CSSProperties,
52132
+ * style?: import("preact").JSX.CSSProperties,
51714
52133
  * bold?: boolean,
51715
52134
  * noWrap?: boolean,
51716
52135
  * maxLines?: number,
51717
- * spacing?: string | number | import("ignore:preact").ComponentChildren,
52136
+ * spacing?: string | number | import("preact").ComponentChildren,
51718
52137
  * loading?: boolean,
51719
52138
  * skeleton?: boolean,
51720
52139
  * attachLastChild?: boolean,
51721
52140
  * preventSpaceUnderlines?: boolean,
51722
- * holdSpaceForStyle?: import("ignore:preact").JSX.CSSProperties,
52141
+ * holdSpaceForStyle?: import("preact").JSX.CSSProperties,
51723
52142
  * boldStable?: boolean,
51724
52143
  * shrinkWrap?: boolean,
51725
52144
  * capitalize?: boolean,
51726
52145
  * selectRange?: string | [number, number] | [string, string],
51727
- * childrenOutsideFlow?: import("ignore:preact").ComponentChildren,
52146
+ * childrenOutsideFlow?: import("preact").ComponentChildren,
51728
52147
  * [key: string]: any,
51729
52148
  * }>}
51730
52149
  *
@@ -51765,7 +52184,7 @@ const shouldInjectSpacingBetween = (left, right) => {
51765
52184
  * Replaces real space characters between children with padding-based spaces.
51766
52185
  * Useful inside `<a>` elements where browsers draw an underline under spaces.
51767
52186
  *
51768
- * @param {import("ignore:preact").JSX.CSSProperties} [holdSpaceForStyle]
52187
+ * @param {import("preact").JSX.CSSProperties} [holdSpaceForStyle]
51769
52188
  * Prevents layout shifts when text styles change (e.g. font-weight, font-size).
51770
52189
  * Pass the CSS properties representing the "largest" visual state of the text.
51771
52190
  * An invisible placeholder rendered with those styles reserves the space; the
@@ -51798,7 +52217,7 @@ const shouldInjectSpacingBetween = (left, right) => {
51798
52217
  * `[start, end]` character-offset tuple, or a `[startText, endText]` pair of
51799
52218
  * substrings selecting from the first to the end of the second.
51800
52219
  *
51801
- * @param {import("ignore:preact").ComponentChildren} [childrenOutsideFlow]
52220
+ * @param {import("preact").ComponentChildren} [childrenOutsideFlow]
51802
52221
  * Rendered after children but outside the text spacing/flow logic. Used
51803
52222
  * internally for overlays such as the skeleton container.
51804
52223
  */
@@ -52086,7 +52505,7 @@ const TextWithSelectRange = ({
52086
52505
  * @param {object} props
52087
52506
  * @param {string} [props.href] - URL/id of an external SVG symbol, rendered via
52088
52507
  * `<svg><use href></svg>`. Mutually exclusive with meaningful `children`.
52089
- * @param {import("ignore:preact").ComponentChildren} [props.children] - Inline icon
52508
+ * @param {import("preact").ComponentChildren} [props.children] - Inline icon
52090
52509
  * content: an `<svg>` element, or a string (renders as a text/emoji icon).
52091
52510
  * @param {boolean} [props.decorative] - Marks the icon `aria-hidden`. Defaults
52092
52511
  * to `true` for an unsized, non-interactive icon; pass `false` for a
@@ -52261,6 +52680,11 @@ const usePreloadOnIntent = (ref, href, prefetch = true) => {
52261
52680
  return undefined;
52262
52681
  }
52263
52682
  const preload = () => {
52683
+ // The tab or the window it opens loads its own code: what would be
52684
+ // fetched here is not what runs there.
52685
+ if (linkOpensElsewhere(element)) {
52686
+ return;
52687
+ }
52264
52688
  preloadUrl(href);
52265
52689
  };
52266
52690
  element.addEventListener("pointerenter", preload);
@@ -52469,6 +52893,15 @@ const useDimColorWhen = (elementRef, shouldDim) => {
52469
52893
  };
52470
52894
 
52471
52895
  installImportMetaCssBuild(import.meta);
52896
+ /*
52897
+ * Apply opacity to child content, not the link element itself.
52898
+ *
52899
+ * Why not apply opacity directly to .navi_link?
52900
+ * - Would make focus outlines semi-transparent too (accessibility issue)
52901
+ * - We want dimmed text but full-opacity focus indicators for visibility
52902
+ *
52903
+ * This approach dims the content while preserving focus outline visibility.
52904
+ */
52472
52905
  const css$Z = /* css */`@layer navi {
52473
52906
  .navi_link {
52474
52907
  --link-border-radius: unset;
@@ -52489,6 +52922,19 @@ const css$Z = /* css */`@layer navi {
52489
52922
  --link-current-indicator-spacing: 0;
52490
52923
  --link-current-indicator-color: var(--navi-link-current-indicator-color);
52491
52924
  }
52925
+
52926
+ .navi_link[data-variant="text"], .navi_link[data-variant="icon"] {
52927
+ --link-color: unset;
52928
+ --link-text-decoration: none;
52929
+ }
52930
+
52931
+ .navi_link[data-variant="tab"] {
52932
+ --link-background-hover: color-mix(in srgb,
52933
+ var(--link-background, transparent),
52934
+ var(--x-link-contrasting-color) 15%);
52935
+ --link-color: unset;
52936
+ --link-text-decoration: none;
52937
+ }
52492
52938
  }
52493
52939
 
52494
52940
  .navi_link {
@@ -52509,7 +52955,6 @@ const css$Z = /* css */`@layer navi {
52509
52955
  --x-link-padding-right: var(--link-padding-right, var(--link-padding-x, var(--link-padding, 0px)));
52510
52956
  --x-link-padding-bottom: var(--link-padding-bottom, var(--link-padding-y, var(--link-padding, 0px)));
52511
52957
  --x-link-padding-left: var(--link-padding-left, var(--link-padding-x, var(--link-padding, 0px)));
52512
- aspect-ratio: inherit;
52513
52958
  padding-top: var(--x-link-padding-top);
52514
52959
  padding-right: var(--x-link-padding-right);
52515
52960
  padding-bottom: var(--x-link-padding-bottom);
@@ -52698,17 +53143,7 @@ const css$Z = /* css */`@layer navi {
52698
53143
  margin-left: -.1em;
52699
53144
  }
52700
53145
 
52701
- &[data-variant="text"], &[data-variant="icon"] {
52702
- --link-color: unset;
52703
- --link-text-decoration: none;
52704
- }
52705
-
52706
53146
  &[data-variant="tab"] {
52707
- --link-background-hover: color-mix(in srgb,
52708
- var(--link-background, transparent),
52709
- var(--x-link-contrasting-color) 15%);
52710
- --link-color: unset;
52711
- --link-text-decoration: none;
52712
53147
  white-space: nowrap;
52713
53148
  user-select: none;
52714
53149
 
@@ -52795,8 +53230,9 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
52795
53230
  *
52796
53231
  * Href-derived behavior (all computed from `href`, recomputed on navigation):
52797
53232
  * - internal vs external target (`data-href-internal`/`data-href-external`),
52798
- * with `target`/`rel` auto-defaulted (`_self` internal, `_blank` +
52799
- * `noopener noreferrer` external) unless explicitly set.
53233
+ * with `target`/`rel` auto-defaulted unless explicitly set: `_blank` for a
53234
+ * page of another site (`http:`/`https:`), `noopener noreferrer` for any
53235
+ * external address, nothing otherwise.
52800
53236
  * - "current page" detection (`data-href-current`, `aria-current="page"`).
52801
53237
  * - anchor links (`href` starting with `#`) get `data-href-anchor`.
52802
53238
  * - an automatic end icon for `tel:`/`sms:`/`mailto:`/`github.com`, external
@@ -52858,14 +53294,14 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
52858
53294
  * text reads (see `holdSpaceForStyle` in `text.jsx`).
52859
53295
  * @param {boolean} [props.currentEffectShadow] - Inset-shadow effect while
52860
53296
  * current (used with `variant="tab"`).
52861
- * @param {boolean|import("ignore:preact").ComponentChild} [props.startIcon] - Icon
53297
+ * @param {boolean|import("preact").ComponentChild} [props.startIcon] - Icon
52862
53298
  * placed before the text.
52863
- * @param {boolean|import("ignore:preact").ComponentChild} [props.endIcon] - Icon
53299
+ * @param {boolean|import("preact").ComponentChild} [props.endIcon] - Icon
52864
53300
  * placed after the text; when omitted, an icon may be auto-chosen from the
52865
53301
  * href (see above).
52866
- * @param {boolean|import("ignore:preact").ComponentChild} [props.blankTargetIcon] -
53302
+ * @param {boolean|import("preact").ComponentChild} [props.blankTargetIcon] -
52867
53303
  * Override/suppress the auto external-link icon.
52868
- * @param {boolean|import("ignore:preact").ComponentChild} [props.anchorIcon] -
53304
+ * @param {boolean|import("preact").ComponentChild} [props.anchorIcon] -
52869
53305
  * Controls the auto anchor icon shown for a childless anchor link:
52870
53306
  * `true`/`undefined` uses the default chain SVG, `false` suppresses it, any
52871
53307
  * other node replaces it. To render a plain `#` instead, prefer writing it as
@@ -52936,7 +53372,7 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
52936
53372
  * @param {boolean} [props.preventDefault] - Call `event.preventDefault()` on
52937
53373
  * click (navigation suppressed; `onClick` still runs).
52938
53374
  * @param {(event: MouseEvent) => void} [props.onClick]
52939
- * @param {import("ignore:preact").ComponentChildren} [props.children] - Link content;
53375
+ * @param {import("preact").ComponentChildren} [props.children] - Link content;
52940
53376
  * falls back to the route's relative URL / `href` per the rules above.
52941
53377
  * @param {string} [props.name] - Control facade name (links may be nameless).
52942
53378
  * @param {boolean} [props.disabled]
@@ -53081,7 +53517,7 @@ const LinkPlain = props => {
53081
53517
  ":-navi-href-current": innerCurrent,
53082
53518
  ":-navi-selected": selected
53083
53519
  };
53084
- const innerTarget = target === undefined ? isSameSite ? "_self" : "_blank" : target;
53520
+ const innerTarget = target === undefined ? getDefaultHrefTarget(href, documentUrlSignal.peek()) : target;
53085
53521
  const innerRel = rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel;
53086
53522
  let innerEndIcon;
53087
53523
  if (endIcon === undefined) {
@@ -53248,7 +53684,7 @@ const LinkPlain = props => {
53248
53684
  },
53249
53685
  href: href,
53250
53686
  rel: innerRel,
53251
- target: innerTarget === "_self" ? undefined : target
53687
+ target: innerTarget === "_self" ? undefined : innerTarget
53252
53688
  // Which slide this tab is, and which box to say it to: read by the <Nav>
53253
53689
  // around it to place the row's own bar, and by the command above to find
53254
53690
  // the container across the document.
@@ -53500,6 +53936,9 @@ const useSlideContainer = (target) => {
53500
53936
  };
53501
53937
 
53502
53938
  installImportMetaCssBuild(import.meta);
53939
+ // A name of its own for the bar under the current tab, one per nav on the page:
53940
+ // two bars answering to the same name are two elements claiming one picture,
53941
+ // and the browser refuses the whole transition rather than pick.
53503
53942
  let navCount = 0;
53504
53943
  // Worn by a nav of routes while a route movement between two of its tabs is
53505
53944
  // pictured (see markIndicatorTakesPart, and the CSS below for what it decides).
@@ -53524,6 +53963,7 @@ const rememberProgressAnimation = (navElement, travel) => {
53524
53963
  };
53525
53964
  const css$Y = /* css */`@layer navi {
53526
53965
  .navi_nav {
53966
+ --nav-width: fit-content;
53527
53967
  --nav-border: none;
53528
53968
  --nav-padding: 0px;
53529
53969
  --nav-border-radius: 0px;
@@ -53547,7 +53987,7 @@ const css$Y = /* css */`@layer navi {
53547
53987
  var(--nav-indicator-length-delta));
53548
53988
  --x-nav-indicator-color: var(--nav-indicator-color, var(--nav-current-indicator-color));
53549
53989
  --x-nav-indicator-color-toward: var(--nav-indicator-color-toward, var(--x-nav-indicator-color));
53550
- z-index: 1;
53990
+ z-index: 2;
53551
53991
  background: color-mix(in srgb,
53552
53992
  var(--x-nav-indicator-color-toward)
53553
53993
  calc(var(--slide-travel-progress) * var(--nav-indicator-toward-sign, 0) *
@@ -53604,7 +54044,7 @@ const css$Y = /* css */`@layer navi {
53604
54044
  }
53605
54045
 
53606
54046
  .navi_nav {
53607
- width: fit-content;
54047
+ width: var(--nav-width);
53608
54048
  padding-top: var(--nav-padding-top, var(--nav-padding-y, var(--nav-padding, unset)));
53609
54049
  padding-right: var(--nav-padding-right, var(--nav-padding-x, var(--nav-padding, unset)));
53610
54050
  padding-bottom: var(--nav-padding-bottom, var(--nav-padding-y, var(--nav-padding, unset)));
@@ -53633,12 +54073,12 @@ const css$Y = /* css */`@layer navi {
53633
54073
  --x-nav-child-border-radius: calc(var(--nav-border-radius) - var(--nav-padding));
53634
54074
  --x-nav-link-border-radius: var(--link-border-radius, var(--x-nav-child-border-radius));
53635
54075
 
53636
- &:first-child {
54076
+ &:nth-child(1 of .navi_link) {
53637
54077
  border-top-left-radius: var(--x-nav-link-border-radius);
53638
54078
  border-bottom-left-radius: var(--x-nav-link-border-radius);
53639
54079
  }
53640
54080
 
53641
- &:last-child {
54081
+ &:nth-last-child(1 of .navi_link) {
53642
54082
  border-top-right-radius: var(--x-nav-link-border-radius);
53643
54083
  border-bottom-right-radius: var(--x-nav-link-border-radius);
53644
54084
  }
@@ -53741,7 +54181,7 @@ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
53741
54181
  };
53742
54182
 
53743
54183
  /**
53744
- * @type {import("ignore:preact").FunctionComponent<{
54184
+ * @type {import("preact").FunctionComponent<{
53745
54185
  * currentIndicator?: boolean|"top"|"bottom"|"left"|"right",
53746
54186
  * currentIndicatorSlides?: boolean,
53747
54187
  * slideContainer?: string,
@@ -54891,7 +55331,7 @@ const TABS_ALIGN_TO_JUSTIFY_CONTENT = {
54891
55331
  };
54892
55332
 
54893
55333
  /**
54894
- * @type {import("ignore:preact").FunctionComponent<{
55334
+ * @type {import("preact").FunctionComponent<{
54895
55335
  * value?: any,
54896
55336
  * defaultValue?: any,
54897
55337
  * onChange?: (value: any) => void,
@@ -54935,7 +55375,7 @@ const TABS_ALIGN_TO_JUSTIFY_CONTENT = {
54935
55375
  * grows the binder instead of closing in on the text.
54936
55376
  * @param {string} [props.tabBorderColor] - The inactive tabs alone; the rest of
54937
55377
  * the outline follows `borderColor`.
54938
- * @param {import("ignore:preact").ComponentChildren} props.children - `Binder.Item`s.
55378
+ * @param {import("preact").ComponentChildren} props.children - `Binder.Item`s.
54939
55379
  */
54940
55380
  const Binder = ({
54941
55381
  children,
@@ -55076,13 +55516,13 @@ const Binder = ({
55076
55516
  /**
55077
55517
  * @param {any} [props.value] - What identifies this item; defaults to its
55078
55518
  * position among its siblings.
55079
- * @param {import("ignore:preact").ComponentChildren} [props.label] - What the tab
55519
+ * @param {import("preact").ComponentChildren} [props.label] - What the tab
55080
55520
  * shows.
55081
55521
  * @param {object} [props.route] - Makes the tab a `Link`
55082
55522
  * to that route, and the binder follows the url: the open item is the one
55083
55523
  * whose route matches, no `value`/`onChange` needed.
55084
55524
  * @param {string} [props.href] - Same, for a plain url.
55085
- * @param {import("ignore:preact").ComponentChildren} [props.children] - The page.
55525
+ * @param {import("preact").ComponentChildren} [props.children] - The page.
55086
55526
  *
55087
55527
  * Every other prop goes to the tab (a `Box`, or a `Link` when routed): an item
55088
55528
  * styles and configures the control that opens it.
@@ -55492,7 +55932,7 @@ const FixedBarStyleCSSVars = {
55492
55932
  };
55493
55933
 
55494
55934
  /**
55495
- * @type {import("ignore:preact").FunctionComponent<{
55935
+ * @type {import("preact").FunctionComponent<{
55496
55936
  * area?: "top"|"bottom"|"left"|"right",
55497
55937
  * width?: string|number,
55498
55938
  * height?: string|number,
@@ -55883,7 +56323,7 @@ const css$U = /* css */`.navi_summary_marker {
55883
56323
  `;
55884
56324
 
55885
56325
  /**
55886
- * @type {import("ignore:preact").FunctionComponent<{
56326
+ * @type {import("preact").FunctionComponent<{
55887
56327
  * open?: boolean,
55888
56328
  * loading?: boolean,
55889
56329
  * openDirection?: "down" | "up" | "left",
@@ -56303,8 +56743,8 @@ const useExpandableContext = partName => {
56303
56743
  };
56304
56744
 
56305
56745
  /**
56306
- * @type {import("ignore:preact").FunctionComponent<{
56307
- * ui?: import("ignore:preact").ComponentChildren | ((state: { open: boolean }) => import("ignore:preact").ComponentChildren),
56746
+ * @type {import("preact").FunctionComponent<{
56747
+ * ui?: import("preact").ComponentChildren | ((state: { open: boolean }) => import("preact").ComponentChildren),
56308
56748
  * open?: boolean,
56309
56749
  * defaultOpen?: boolean,
56310
56750
  * signal?: import("@preact/signals").Signal,
@@ -56944,9 +57384,9 @@ const Expandable = props => {
56944
57384
  * Controls inside it keep their own behavior and do not toggle. A Box, so it
56945
57385
  * takes the layout and style props every Box takes.
56946
57386
  *
56947
- * @type {import("ignore:preact").FunctionComponent<{
56948
- * marker?: false | import("ignore:preact").ComponentChildren,
56949
- * children?: import("ignore:preact").ComponentChildren | ((state: { open: boolean }) => import("ignore:preact").ComponentChildren),
57387
+ * @type {import("preact").FunctionComponent<{
57388
+ * marker?: false | import("preact").ComponentChildren,
57389
+ * children?: import("preact").ComponentChildren | ((state: { open: boolean }) => import("preact").ComponentChildren),
56950
57390
  * }>}
56951
57391
  * @param marker - The chevron drawn before the label. `false` removes it (a UI
56952
57392
  * part that is already an icon has no room for one); any node replaces it.
@@ -57021,7 +57461,7 @@ const ExpandableUI = ({
57021
57461
  * Expandable, children may be a function `(data) => ui` or a branches object —
57022
57462
  * see ActionRenderer.
57023
57463
  *
57024
- * @type {import("ignore:preact").FunctionComponent<{}>}
57464
+ * @type {import("preact").FunctionComponent<{}>}
57025
57465
  */
57026
57466
  const ExpandableContent = ({
57027
57467
  children,
@@ -57177,6 +57617,40 @@ const createToggleEvent = open => {
57177
57617
  return toggleEvent;
57178
57618
  };
57179
57619
 
57620
+ /**
57621
+ * Creates a renderComponent function that passes props through a chain of resolvers.
57622
+ * Each resolver is a component function rendered in sequence (hooks are allowed).
57623
+ * To pass through to the next resolver, call useNextResolver() and render the
57624
+ * returned Next component with the desired props.
57625
+ * To terminate the chain early (e.g. render a specialized component), render
57626
+ * directly without calling Next.
57627
+ *
57628
+ * The last entry in the array is the final/target component — it receives null
57629
+ * from useNextResolver() indicating it is terminal.
57630
+ *
57631
+ * Usage:
57632
+ * const renderButton = createComponentResolver([ResolverA, ResolverB, ButtonTarget]);
57633
+ * // Then inside a component render:
57634
+ * renderButton(props)
57635
+ *
57636
+ * A chain step is one component: a runner, defined once per position, whose
57637
+ * render CALLS its resolver's body rather than rendering it as a child. The
57638
+ * body runs synchronously inside the runner's render — its hooks are the
57639
+ * runner's, at the same tree position on every render — and for exactly that
57640
+ * long, `useNextResolver()` answers with the runner after this one (null for
57641
+ * the last). Nothing is handed down through a context: a chain rendered by the
57642
+ * hundred (a list's rows) pays one component per step, not a provider and a
57643
+ * wrapper around each.
57644
+ *
57645
+ * A resolver that renders ANOTHER resolver — a type resolver picking the
57646
+ * component for `type="date"` — cannot render it as a JSX child: that child
57647
+ * would render later, outside any body, with no Next to read. It renders it
57648
+ * with `renderResolver(Resolver, props)`, which captures the Next of the body
57649
+ * it is called from and hands it to a runner made for that resolver — the
57650
+ * child continues the chain where its parent stood. Called anywhere else, or
57651
+ * a `useNextResolver()` reached outside a body, throws: the chain has no
57652
+ * silent answer to give there.
57653
+ */
57180
57654
  const createComponentResolver = (resolvers, {
57181
57655
  pure
57182
57656
  } = {}) => {
@@ -57386,10 +57860,21 @@ const useAccentColorAttributes = (
57386
57860
  };
57387
57861
 
57388
57862
  installImportMetaCssBuild(import.meta);
57863
+ /**
57864
+ * We need the content to visually shrink (scale down) but the button interactive area MUST remain intact
57865
+ * Otherwise a click on the edges of the button cannot not trigger the click event (mouseup occurs outside the button)
57866
+ **/
57867
+
57868
+ /**
57869
+ * We have to re-define the CSS of button because getComputedStyle(button).borderColor returns
57870
+ * rgb(0, 0, 0) while being visually grey in chrome
57871
+ * So we redefine chrome styles so that loader can keep up with the actual color visible to the user
57872
+ */
57389
57873
  const css$R = /* css */`@layer navi {
57390
57874
  .navi_button {
57391
57875
  --button-border-radius: var(--navi-control-border-radius);
57392
57876
  --button-border-width: var(--navi-control-border-width);
57877
+ --border-width: var(--button-border-width);
57393
57878
  --button-cta-background-color: var(--navi-accent-color);
57394
57879
  --button-outline-width: var(--navi-focus-outline-width);
57395
57880
  --button-outline-offset: calc(-.5 * var(--button-outline-width));
@@ -57398,7 +57883,7 @@ const css$R = /* css */`@layer navi {
57398
57883
  --button-padding-y-default: var(--navi-button-padding-y-default);
57399
57884
  --button-loader-color: var(--navi-loader-color);
57400
57885
  --button-border-color: var(--navi-control-border-color);
57401
- --button-background-color: var(--button-background, var(--navi-button-background-color));
57886
+ --button-background-color: var(--navi-button-background-color);
57402
57887
  --button-color: currentColor;
57403
57888
  --button-cursor: pointer;
57404
57889
  --button-font-size: var(--navi-control-font-size);
@@ -57431,6 +57916,92 @@ const css$R = /* css */`@layer navi {
57431
57916
  line-height: var(--navi-control-line-height);
57432
57917
  display: inline-block;
57433
57918
  }
57919
+
57920
+ .navi_button[data-variant="discrete"] {
57921
+ --button-border-width: 0;
57922
+ --button-border-color: transparent;
57923
+ --button-border-color-hover: var(--button-border-color);
57924
+ --button-border-color-current: var(--button-border-color);
57925
+ --button-border-color-readonly: var(--button-border-color);
57926
+ --button-border-color-disabled: var(--button-border-color);
57927
+ --button-background-color: transparent;
57928
+ --button-background-color-hover: color-mix(in srgb,
57929
+ currentColor 8%,
57930
+ var(--button-background-color));
57931
+ --button-background-color-readonly: var(--button-background-color);
57932
+ --button-background-color-disabled: var(--button-background-color);
57933
+ }
57934
+
57935
+ .navi_button[data-variant="bare"] {
57936
+ --button-padding-x-default: 0;
57937
+ --button-padding-y-default: 0;
57938
+ --button-border-width: 0;
57939
+ --button-border-color: transparent;
57940
+ --button-border-color-hover: var(--button-border-color);
57941
+ --button-border-color-current: var(--button-border-color);
57942
+ --button-border-color-readonly: var(--button-border-color);
57943
+ --button-border-color-disabled: var(--button-border-color);
57944
+ --button-background-color: transparent;
57945
+ --button-background-color-hover: var(--button-background-color);
57946
+ --button-background-color-readonly: var(--button-background-color);
57947
+ --button-background-color-disabled: var(--button-background-color);
57948
+ font-size: inherit;
57949
+ font-family: inherit;
57950
+ line-height: inherit;
57951
+ }
57952
+
57953
+ .navi_button[data-variant="discrete-border"] {
57954
+ --button-background-color: transparent;
57955
+ --button-background-color-hover: var(--button-background-color);
57956
+ --button-background-color-readonly: var(--button-background-color);
57957
+ --button-background-color-disabled: var(--button-background-color);
57958
+ }
57959
+
57960
+ .navi_button[data-variant="border"] {
57961
+ --button-background-color: transparent;
57962
+ --button-background-color-hover: color-mix(in srgb,
57963
+ currentColor 8%,
57964
+ var(--button-background-color));
57965
+ --button-background-color-readonly: var(--button-background-color);
57966
+ --button-background-color-disabled: var(--button-background-color);
57967
+ }
57968
+
57969
+ .navi_button[data-icon] {
57970
+ --button-padding: 0;
57971
+ }
57972
+
57973
+ .navi_button[data-cta] {
57974
+ --button-background-color: var(--button-cta-background-color);
57975
+ --button-border-color: var(--button-cta-background-color);
57976
+ --button-color: white;
57977
+ --button-background-color-hover: color-mix(in srgb,
57978
+ var(--button-background-color) 85%,
57979
+ white);
57980
+ --button-border-color-hover: color-mix(in srgb,
57981
+ var(--button-border-color) 85%,
57982
+ white);
57983
+ --button-background-color-readonly: color-mix(in srgb,
57984
+ var(--button-background-color) 50%,
57985
+ white);
57986
+ --button-border-color-readonly: color-mix(in srgb,
57987
+ var(--button-border-color) 50%,
57988
+ white);
57989
+ --button-background-color-disabled: color-mix(in srgb,
57990
+ var(--button-background-color) 40%,
57991
+ white);
57992
+ --button-border-color-disabled: color-mix(in srgb,
57993
+ var(--button-border-color) 40%,
57994
+ white);
57995
+ --button-color-disabled: color-mix(in srgb,
57996
+ var(--button-color) 60%,
57997
+ transparent);
57998
+ }
57999
+
58000
+ .navi_button[data-content-display="contents"] {
58001
+ background: none;
58002
+ border: none;
58003
+ padding: 0;
58004
+ }
57434
58005
  }
57435
58006
 
57436
58007
  a.navi_button {
@@ -57447,7 +58018,6 @@ a.navi_button {
57447
58018
  --x-button-color: var(--button-color);
57448
58019
  --x-button-cursor: var(--button-cursor);
57449
58020
  box-sizing: border-box;
57450
- aspect-ratio: inherit;
57451
58021
  color: var(--x-button-color);
57452
58022
  border-top-left-radius: var(--x-corner-top-left-radius, var(--button-border-radius));
57453
58023
  border-top-right-radius: var(--x-corner-top-right-radius, var(--button-border-radius));
@@ -57456,12 +58026,15 @@ a.navi_button {
57456
58026
  cursor: var(--x-button-cursor);
57457
58027
  touch-action: manipulation;
57458
58028
  user-select: none;
57459
- background: none;
57460
- border: none;
57461
58029
  outline: none;
57462
- padding: 0;
57463
58030
  position: relative;
57464
58031
 
58032
+ &:where(:not([data-content-display="contents"])) {
58033
+ background: none;
58034
+ border: none;
58035
+ padding: 0;
58036
+ }
58037
+
57465
58038
  & .navi_button_content {
57466
58039
  --x-corner-top-left-radius: initial;
57467
58040
  --x-corner-top-right-radius: initial;
@@ -57483,8 +58056,7 @@ a.navi_button {
57483
58056
  gap: inherit;
57484
58057
  color: inherit;
57485
58058
  vertical-align: inherit;
57486
- background: var(--x-button-background);
57487
- background-color: var(--x-button-background-color, var(--x-button-background));
58059
+ background: var(--x-button-background, var(--x-button-background-color));
57488
58060
  border: var(--x-button-border);
57489
58061
  border-radius: inherit;
57490
58062
  outline-width: var(--button-outline-width);
@@ -57507,6 +58079,10 @@ a.navi_button {
57507
58079
  }
57508
58080
  }
57509
58081
 
58082
+ &[data-background-color] .navi_button_content {
58083
+ background-color: var(--x-button-background-color);
58084
+ }
58085
+
57510
58086
  &[data-href-current] {
57511
58087
  --x-button-border-color: var(--button-border-color-current);
57512
58088
  --x-button-background-color: var(--button-background-color-current);
@@ -57551,7 +58127,7 @@ a.navi_button {
57551
58127
  }
57552
58128
 
57553
58129
  &[data-content-display="contents"] {
57554
- & [data-focus-visible] {
58130
+ &[data-focus-visible] {
57555
58131
  outline: var(--button-outline-width) solid var(--button-outline-color);
57556
58132
  outline-offset: var(--button-outline-offset);
57557
58133
  }
@@ -57577,19 +58153,6 @@ a.navi_button {
57577
58153
  }
57578
58154
 
57579
58155
  &[data-variant="discrete"] {
57580
- --button-border-width: 0;
57581
- --button-border-color: transparent;
57582
- --button-border-color-hover: var(--button-border-color);
57583
- --button-border-color-current: var(--button-border-color);
57584
- --button-border-color-readonly: var(--button-border-color);
57585
- --button-border-color-disabled: var(--button-border-color);
57586
- --button-background-color: var(--button-background, transparent);
57587
- --button-background-color-hover: color-mix(in srgb,
57588
- currentColor 8%,
57589
- var(--button-background-color));
57590
- --button-background-color-readonly: var(--button-background-color);
57591
- --button-background-color-disabled: var(--button-background-color);
57592
-
57593
58156
  @media (hover: hover) {
57594
58157
  &[data-pressed] {
57595
58158
  & .navi_button_content {
@@ -57600,17 +58163,6 @@ a.navi_button {
57600
58163
  }
57601
58164
 
57602
58165
  &[data-variant="bare"] {
57603
- --button-border-width: 0;
57604
- --button-border-color: transparent;
57605
- --button-border-color-hover: var(--button-border-color);
57606
- --button-border-color-current: var(--button-border-color);
57607
- --button-border-color-readonly: var(--button-border-color);
57608
- --button-border-color-disabled: var(--button-border-color);
57609
- --button-background-color: var(--button-background, transparent);
57610
- --button-background-color-hover: var(--button-background-color);
57611
- --button-background-color-readonly: var(--button-background-color);
57612
- --button-background-color-disabled: var(--button-background-color);
57613
-
57614
58166
  &[data-pressed] {
57615
58167
  & .navi_button_content {
57616
58168
  transform: none;
@@ -57619,10 +58171,6 @@ a.navi_button {
57619
58171
  }
57620
58172
 
57621
58173
  &[data-variant="discrete-border"] {
57622
- --button-background-color: var(--button-background, transparent);
57623
- --button-background-color-hover: var(--button-background-color);
57624
- --button-background-color-readonly: var(--button-background-color);
57625
- --button-background-color-disabled: var(--button-background-color);
57626
58174
  --x-button-border-color: transparent;
57627
58175
 
57628
58176
  &[data-hover] {
@@ -57634,15 +58182,6 @@ a.navi_button {
57634
58182
  }
57635
58183
  }
57636
58184
 
57637
- &[data-variant="border"] {
57638
- --button-background-color: var(--button-background, transparent);
57639
- --button-background-color-hover: color-mix(in srgb,
57640
- currentColor 8%,
57641
- var(--button-background-color));
57642
- --button-background-color-readonly: var(--button-background-color);
57643
- --button-background-color-disabled: var(--button-background-color);
57644
- }
57645
-
57646
58185
  &[data-press-effect="scale"][data-pressed] {
57647
58186
  & .navi_button_content {
57648
58187
  transform: scale(.9);
@@ -57656,37 +58195,9 @@ a.navi_button {
57656
58195
  }
57657
58196
 
57658
58197
  &[data-icon] {
57659
- --button-padding: 0;
57660
58198
  justify-content: center;
57661
58199
  align-items: center;
57662
58200
  }
57663
-
57664
- &[data-cta] {
57665
- --button-background-color: var(--button-background, var(--button-cta-background-color));
57666
- --button-border-color: var(--button-cta-background-color);
57667
- --button-color: white;
57668
- --button-background-color-hover: color-mix(in srgb,
57669
- var(--button-background-color) 85%,
57670
- white);
57671
- --button-border-color-hover: color-mix(in srgb,
57672
- var(--button-border-color) 85%,
57673
- white);
57674
- --button-background-color-readonly: color-mix(in srgb,
57675
- var(--button-background-color) 50%,
57676
- white);
57677
- --button-border-color-readonly: color-mix(in srgb,
57678
- var(--button-border-color) 50%,
57679
- white);
57680
- --button-background-color-disabled: color-mix(in srgb,
57681
- var(--button-background-color) 40%,
57682
- white);
57683
- --button-border-color-disabled: color-mix(in srgb,
57684
- var(--button-border-color) 40%,
57685
- white);
57686
- --button-color-disabled: color-mix(in srgb,
57687
- var(--button-color) 60%,
57688
- transparent);
57689
- }
57690
58201
  }
57691
58202
  `;
57692
58203
  const ButtonUI = props => {
@@ -57815,6 +58326,7 @@ const ButtonUI = props => {
57815
58326
  "data-press-effect": pressEffect,
57816
58327
  "data-icon": icon ? "" : undefined,
57817
58328
  "data-cta": cta ? "" : undefined,
58329
+ "data-background-color": props.backgroundColor === undefined ? undefined : "",
57818
58330
  "data-callout-arrow-x": "center"
57819
58331
  // style management
57820
58332
  ,
@@ -58127,7 +58639,7 @@ const ButtonWithHref = props => {
58127
58639
  } = getHrefTargetInfo(href, documentUrlSignal.peek());
58128
58640
  return jsx(Next, {
58129
58641
  ...props,
58130
- target: target === undefined ? isSameSite ? undefined : "_blank" : target,
58642
+ target: target === undefined ? getDefaultHrefTarget(href, documentUrlSignal.peek()) : target,
58131
58643
  rel: rel === undefined ? isSameSite ? undefined : "noopener noreferrer" : rel
58132
58644
  // Under whatever the caller already says: a route knows better whether
58133
58645
  // it is the page one is on (see button_route.jsx), and a demo can force
@@ -58287,7 +58799,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
58287
58799
  };
58288
58800
 
58289
58801
  /**
58290
- * @type {import("ignore:preact").FunctionComponent<{
58802
+ * @type {import("preact").FunctionComponent<{
58291
58803
  * selfInteractions?: string,
58292
58804
  * whenSelfInteractionsBlocked?: "hide" | "refuse" | "ignore",
58293
58805
  * replace?: boolean,
@@ -58369,7 +58881,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
58369
58881
  * more: the work may already have happened on the other side (see
58370
58882
  * docs/actions.md#aborting-saves-resources-it-does-not-undo), so say it only
58371
58883
  * where the screen can be re-opened on what is actually there.
58372
- * @param {boolean|import("ignore:preact").ComponentChildren} [confirm] Ask before
58884
+ * @param {boolean|import("preact").ComponentChildren} [confirm] Ask before
58373
58885
  * doing it, in the place the button stands: the first press replaces the
58374
58886
  * button with this question and a "Confirmer"/"Annuler" pair, the second one
58375
58887
  * does what the button was for. `true` asks navi's default question. The
@@ -58380,9 +58892,9 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
58380
58892
  * question up, its message on the button that asked. Use `<Picker
58381
58893
  * type="confirm">` instead when the question is long enough to want a popup,
58382
58894
  * or when the row has no space for it.
58383
- * @param {import("ignore:preact").ComponentChildren} [confirmLabel] Label of the
58895
+ * @param {import("preact").ComponentChildren} [confirmLabel] Label of the
58384
58896
  * second press.
58385
- * @param {import("ignore:preact").ComponentChildren} [cancelLabel] Label of the way
58897
+ * @param {import("preact").ComponentChildren} [cancelLabel] Label of the way
58386
58898
  * out of the question.
58387
58899
  * @param {string} [confirmTestId] `data-testid` of the second press.
58388
58900
  * @param {string} [cancelTestId] `data-testid` of the way out.
@@ -58481,13 +58993,13 @@ const css$P = /* css */`.navi_control_swap {
58481
58993
  `;
58482
58994
 
58483
58995
  /**
58484
- * @type {import("ignore:preact").FunctionComponent<{
58996
+ * @type {import("preact").FunctionComponent<{
58485
58997
  * value?: string,
58486
58998
  * defaultValue?: string,
58487
58999
  * signal?: import("@preact/signals").Signal<string>,
58488
59000
  * onChange?: (name: string, event: Event) => void,
58489
59001
  * animation?: boolean,
58490
- * children?: import("ignore:preact").ComponentChildren,
59002
+ * children?: import("preact").ComponentChildren,
58491
59003
  * }>}
58492
59004
  * @param value - The name of the side holding the floor, driven from outside:
58493
59005
  * the row swaps to match every change of it, and a press on a cap can still
@@ -58634,13 +59146,13 @@ const ControlSwap = props => {
58634
59146
  * reads these and draws the caps at its ends, the controls between them. The
58635
59147
  * props below say what the side IS; every other prop describes its cap.
58636
59148
  *
58637
- * @type {import("ignore:preact").FunctionComponent<{
59149
+ * @type {import("preact").FunctionComponent<{
58638
59150
  * name?: string,
58639
- * icon: import("ignore:preact").ComponentChildren,
59151
+ * icon: import("preact").ComponentChildren,
58640
59152
  * label: string,
58641
- * badge?: boolean | import("ignore:preact").ComponentChildren,
59153
+ * badge?: boolean | import("preact").ComponentChildren,
58642
59154
  * autoFocus?: boolean,
58643
- * children?: import("ignore:preact").ComponentChildren,
59155
+ * children?: import("preact").ComponentChildren,
58644
59156
  * [key: string]: any,
58645
59157
  * }>}
58646
59158
  * @param name - How `value`/`signal`/`onChange` name this side. Its position
@@ -58848,6 +59360,51 @@ const createAvailableConstraint = (
58848
59360
  };
58849
59361
  };
58850
59362
 
59363
+ /**
59364
+ * ControlGroup — a named sub-group of form controls whose combined state is
59365
+ * aggregated into a single key/value object, exactly like a Form but without
59366
+ * the <form> element semantics.
59367
+ *
59368
+ * Use it when you need a self-contained cluster of inputs that:
59369
+ * - produce one composite value (an object) as their joint state
59370
+ * - can carry their own action / uiAction / command
59371
+ * - may live inside or outside a <Form>
59372
+ *
59373
+ * Props:
59374
+ * name — the key under which the composite value is registered in a
59375
+ * parent Form (optional when used standalone)
59376
+ * action — called with the aggregated object value when any child changes
59377
+ * command — navi command string (e.g. "--navi-send")
59378
+ * disabled — propagates to all children
59379
+ * readOnly — propagates to all children
59380
+ * as — HTML element to render (default "div")
59381
+ * children — the controls that belong to this group
59382
+ *
59383
+ * aggregateChildStates / distributeChildUIState
59384
+ * — what the group is worth, in both directions, when that is not
59385
+ * simply "one key per child": two wheels that add up to a number
59386
+ * of minutes, three fields that make one date. With them the
59387
+ * group takes and hands back a single value, so it can be driven
59388
+ * by one `value`/`signal` like any other control. Same mechanism
59389
+ * InputDuration uses for its own hour/minute/second fields (see
59390
+ * input_duration.jsx). `name` is unaffected: it still says under
59391
+ * which key the group's value — whatever shape it takes —
59392
+ * lands in the form around it.
59393
+ *
59394
+ * distributeChildStates
59395
+ * — the same way down, asked ONCE for all the children at a time:
59396
+ * `(groupValue, children) => Map<child, state>`. The plural of
59397
+ * `distributeChildUIState`, and the mirror of
59398
+ * `aggregateChildStates`, which already sees every child. For a
59399
+ * group whose children cannot be placed one at a time — four
59400
+ * seats where who sits down decides who moves, a row where the
59401
+ * first answer changes what the others may show. A child the Map
59402
+ * does not name is left where it is. Given both, this one wins.
59403
+ * It also closes the loop: the group holds ONE answer its
59404
+ * children are views of, so when one of them speaks the others
59405
+ * are placed again (the one that acted is left where the user
59406
+ * put it).
59407
+ */
58851
59408
  const ControlGroup = props => {
58852
59409
  const defaultRef = useRef();
58853
59410
  props.ref = props.ref || defaultRef;
@@ -59040,6 +59597,41 @@ const css$N = /* css */`@layer navi {
59040
59597
  var(--button-background-color) 95%,
59041
59598
  black);
59042
59599
  }
59600
+
59601
+ .navi_checkbox[data-variant="switch"] {
59602
+ --margin: var(--switch-margin);
59603
+ --width: var(--switch-outer-width);
59604
+ --height: unset;
59605
+ --border-radius: var(--switch-border-radius);
59606
+ --background-color: var(--switch-background-color);
59607
+ --background-color-hover: var(--switch-background-color-hover);
59608
+ --background-color-readonly: var(--switch-background-color-readonly);
59609
+ --background-color-disabled: var(--switch-background-color-disabled);
59610
+ --background-color-checked: var(--switch-background-color-checked);
59611
+ --background-color-hover-checked: var(--switch-background-color-hover-checked);
59612
+ --background-color-readonly-checked: var(--switch-background-color-readonly-checked);
59613
+ --background-color-disabled-checked: var(--switch-background-color-disabled-checked);
59614
+ }
59615
+
59616
+ .navi_checkbox[data-variant="icon"] {
59617
+ --margin: 0;
59618
+ --width: auto;
59619
+ --height: auto;
59620
+ }
59621
+
59622
+ .navi_checkbox[data-variant="button"] {
59623
+ --margin: 0;
59624
+ --width: auto;
59625
+ --height: auto;
59626
+ --border-color: var(--button-border-color);
59627
+ --border-color-hover: var(--button-border-color-hover);
59628
+ --background-color: var(--button-background-color);
59629
+ --background-color-hover: var(--button-background-color-hover);
59630
+ --background-color-readonly: var(--button-background-color-readonly);
59631
+ --background-color-disabled: var(--button-background-color-disabled);
59632
+ --border-color-checked: var(--button-border-color);
59633
+ --background-color-checked: var(--button-background-color);
59634
+ }
59043
59635
  }
59044
59636
 
59045
59637
  .navi_checkbox {
@@ -59180,18 +59772,6 @@ const css$N = /* css */`@layer navi {
59180
59772
 
59181
59773
  &[data-variant="switch"] {
59182
59774
  --switch-outer-width: calc(var(--switch-width) + var(--switch-padding));
59183
- --margin: var(--switch-margin);
59184
- --width: var(--switch-outer-width);
59185
- --height: unset;
59186
- --border-radius: var(--switch-border-radius);
59187
- --background-color: var(--switch-background-color);
59188
- --background-color-hover: var(--switch-background-color-hover);
59189
- --background-color-readonly: var(--switch-background-color-readonly);
59190
- --background-color-disabled: var(--switch-background-color-disabled);
59191
- --background-color-checked: var(--switch-background-color-checked);
59192
- --background-color-hover-checked: var(--switch-background-color-hover-checked);
59193
- --background-color-readonly-checked: var(--switch-background-color-readonly-checked);
59194
- --background-color-disabled-checked: var(--switch-background-color-disabled-checked);
59195
59775
  min-width: var(--switch-outer-width);
59196
59776
  padding: var(--switch-padding);
59197
59777
  background-color: var(--x-background-color);
@@ -59201,25 +59781,11 @@ const css$N = /* css */`@layer navi {
59201
59781
  }
59202
59782
 
59203
59783
  &[data-variant="icon"] {
59204
- --margin: 0;
59205
- --width: auto;
59206
- --height: auto;
59207
59784
  background: none;
59208
59785
  border: none;
59209
59786
  }
59210
59787
 
59211
59788
  &[data-variant="button"] {
59212
- --margin: 0;
59213
- --width: auto;
59214
- --height: auto;
59215
- --border-color: var(--button-border-color);
59216
- --border-color-hover: var(--button-border-color-hover);
59217
- --background-color: var(--button-background-color);
59218
- --background-color-hover: var(--button-background-color-hover);
59219
- --background-color-readonly: var(--button-background-color-readonly);
59220
- --background-color-disabled: var(--button-background-color-disabled);
59221
- --border-color-checked: var(--button-border-color);
59222
- --background-color-checked: var(--button-background-color);
59223
59789
  padding-top: var(--button-padding-top, var(--button-padding-y, var(--button-padding, var(--button-padding-y-default))));
59224
59790
  padding-right: var(--button-padding-right, var(--button-padding-x, var(--button-padding, var(--button-padding-x-default))));
59225
59791
  padding-bottom: var(--button-padding-bottom, var(--button-padding-y, var(--button-padding, var(--button-padding-y-default))));
@@ -59416,7 +59982,7 @@ const css$M = /* css */`@layer navi {
59416
59982
  [navi-field] {
59417
59983
  --spacing-with-control: var(--navi-xs);
59418
59984
 
59419
- & .navi_checkbox, & .navi_radio {
59985
+ & .navi_checkbox:not([data-variant="switch"]), & .navi_radio {
59420
59986
  --margin: 0;
59421
59987
  }
59422
59988
  }
@@ -59771,6 +60337,28 @@ const css$L = /* css */`@layer navi {
59771
60337
  --button-border-color-disabled: var(--border-color-readonly);
59772
60338
  --button-background-color-disabled: var(--background-color-readonly);
59773
60339
  }
60340
+
60341
+ .navi_radio[data-variant="icon"] {
60342
+ --width: auto;
60343
+ --height: auto;
60344
+ --outline-offset: 2px;
60345
+ --outline-width: 2px;
60346
+ }
60347
+
60348
+ .navi_radio[data-variant="button"] {
60349
+ --margin: 0;
60350
+ --outline-offset: 0px;
60351
+ --width: auto;
60352
+ --height: auto;
60353
+ --border-color: var(--button-border-color);
60354
+ --border-color-hover: var(--button-border-color-hover);
60355
+ --background-color: var(--button-background-color);
60356
+ --background-color-hover: var(--button-background-color-hover);
60357
+ --background-color-readonly: var(--button-background-color-readonly);
60358
+ --background-color-disabled: var(--button-background-color-disabled);
60359
+ --border-color-checked: var(--button-border-color);
60360
+ --background-color-checked: var(--button-background-color);
60361
+ }
59774
60362
  }
59775
60363
 
59776
60364
  .navi_radio {
@@ -59926,26 +60514,7 @@ const css$L = /* css */`@layer navi {
59926
60514
  }
59927
60515
  }
59928
60516
 
59929
- &[data-variant="icon"] {
59930
- --width: auto;
59931
- --height: auto;
59932
- --outline-offset: 2px;
59933
- --outline-width: 2px;
59934
- }
59935
-
59936
60517
  &[data-variant="button"] {
59937
- --margin: 0;
59938
- --outline-offset: 0px;
59939
- --width: auto;
59940
- --height: auto;
59941
- --border-color: var(--button-border-color);
59942
- --border-color-hover: var(--button-border-color-hover);
59943
- --background-color: var(--button-background-color);
59944
- --background-color-hover: var(--button-background-color-hover);
59945
- --background-color-readonly: var(--button-background-color-readonly);
59946
- --background-color-disabled: var(--button-background-color-disabled);
59947
- --border-color-checked: var(--button-border-color);
59948
- --background-color-checked: var(--button-background-color);
59949
60518
  padding-top: var(--button-padding-top, var(--button-padding-y, var(--button-padding, var(--button-padding-y-default))));
59950
60519
  padding-right: var(--button-padding-right, var(--button-padding-x, var(--button-padding, var(--button-padding-x-default))));
59951
60520
  padding-bottom: var(--button-padding-bottom, var(--button-padding-y, var(--button-padding, var(--button-padding-y-default))));
@@ -60169,6 +60738,7 @@ const css$K = /* css */`@layer navi {
60169
60738
  --outline-width: var(--navi-focus-outline-width);
60170
60739
  --outline-offset: var(--outline-width);
60171
60740
  --outline-color: var(--navi-focus-outline-color);
60741
+ --margin: 2px;
60172
60742
  --height: 8px;
60173
60743
  --thumb-size: 16px;
60174
60744
  --thumb-width: var(--thumb-size);
@@ -60244,6 +60814,7 @@ const css$K = /* css */`@layer navi {
60244
60814
  box-sizing: border-box;
60245
60815
  width: fit-content;
60246
60816
  height: var(--height);
60817
+ margin: var(--margin);
60247
60818
  font-size: var(--font-size);
60248
60819
  font-family: var(--font-family);
60249
60820
  border-top-left-radius: var(--x-corner-top-left-radius, var(--outline-border-radius));
@@ -60256,7 +60827,6 @@ const css$K = /* css */`@layer navi {
60256
60827
  outline-offset: var(--outline-offset);
60257
60828
  flex-direction: row;
60258
60829
  align-items: center;
60259
- margin: 2px;
60260
60830
  position: relative;
60261
60831
 
60262
60832
  & .navi_control_input {
@@ -60553,6 +61123,45 @@ const inputCss = /* css */ `@layer navi {
60553
61123
  grey);
60554
61124
  --color-disabled: var(--color-dimmed);
60555
61125
  }
61126
+
61127
+ .navi_input[data-background-transparent] {
61128
+ --background-color-hover: var(--background-color);
61129
+ --background-color-focus: var(--navi-surface-color);
61130
+ }
61131
+
61132
+ .navi_input[data-variant="discrete"], .navi_input[data-variant="discrete-border"] {
61133
+ --background-color: transparent;
61134
+ --background-color-hover: var(--background-color);
61135
+ --background-color-focus: var(--navi-surface-color);
61136
+
61137
+ &[data-background] {
61138
+ --background-color-focus: transparent;
61139
+ }
61140
+ }
61141
+
61142
+ .navi_input[data-variant="discrete"] {
61143
+ --border-color: transparent;
61144
+ }
61145
+
61146
+ .navi_input[data-variant="text"] {
61147
+ --background-color: transparent;
61148
+ --background-color-hover: var(--background-color);
61149
+ --background-color-focus: var(--background-color);
61150
+ --background-color-readonly: var(--background-color);
61151
+ --background-color-disabled: var(--background-color);
61152
+ --border-color: transparent;
61153
+ --border-color-hover: var(--border-color);
61154
+ --border-color-readonly: var(--border-color);
61155
+ --border-color-disabled: var(--border-color);
61156
+ }
61157
+
61158
+ .navi_input[data-variant="underline"] {
61159
+ --background-color: transparent;
61160
+ --background-color-hover: var(--background-color);
61161
+ --background-color-focus: var(--background-color);
61162
+ --background-color-readonly: var(--background-color);
61163
+ --background-color-disabled: var(--background-color);
61164
+ }
60556
61165
  }
60557
61166
 
60558
61167
  .navi_input {
@@ -60704,19 +61313,12 @@ const inputCss = /* css */ `@layer navi {
60704
61313
  }
60705
61314
 
60706
61315
  &[data-background-transparent] {
60707
- --background-color-hover: var(--background-color);
60708
- --background-color-focus: var(--navi-surface-color);
60709
-
60710
61316
  &[data-focus] {
60711
61317
  --x-background-color: var(--background-color-focus);
60712
61318
  }
60713
61319
  }
60714
61320
 
60715
61321
  &[data-variant="discrete"], &[data-variant="discrete-border"] {
60716
- --background-color: transparent;
60717
- --background-color-hover: var(--background-color);
60718
- --background-color-focus: var(--navi-surface-color);
60719
-
60720
61322
  &[data-focus] {
60721
61323
  --x-background-color: var(--background-color-focus);
60722
61324
  }
@@ -60724,15 +61326,9 @@ const inputCss = /* css */ `@layer navi {
60724
61326
  &[data-readonly], &[data-disabled] {
60725
61327
  --x-background-color: var(--background-color);
60726
61328
  }
60727
-
60728
- &[data-background] {
60729
- --background-color-focus: transparent;
60730
- }
60731
61329
  }
60732
61330
 
60733
61331
  &[data-variant="discrete"] {
60734
- --border-color: transparent;
60735
-
60736
61332
  &[data-focus] {
60737
61333
  --x-border-color: color-mix(in srgb,
60738
61334
  var(--border-color) 55%,
@@ -60741,24 +61337,10 @@ const inputCss = /* css */ `@layer navi {
60741
61337
  }
60742
61338
 
60743
61339
  &[data-variant="text"] {
60744
- --background-color: transparent;
60745
- --background-color-hover: var(--background-color);
60746
- --background-color-focus: var(--background-color);
60747
- --background-color-readonly: var(--background-color);
60748
- --background-color-disabled: var(--background-color);
60749
- --border-color: transparent;
60750
- --border-color-hover: var(--border-color);
60751
- --border-color-readonly: var(--border-color);
60752
- --border-color-disabled: var(--border-color);
60753
61340
  cursor: inherit;
60754
61341
  }
60755
61342
 
60756
61343
  &[data-variant="underline"] {
60757
- --background-color: transparent;
60758
- --background-color-hover: var(--background-color);
60759
- --background-color-focus: var(--background-color);
60760
- --background-color-readonly: var(--background-color);
60761
- --background-color-disabled: var(--background-color);
60762
61344
  border: none;
60763
61345
  border-radius: 0;
60764
61346
  padding-left: 0;
@@ -61954,6 +62536,38 @@ const InputStyleCSSVars = {
61954
62536
  const InputPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading", ":-navi-has-value"];
61955
62537
  const InputPseudoElements = ["::-navi-loader"];
61956
62538
 
62539
+ /**
62540
+ * @type {import("preact").FunctionComponent<{
62541
+ * type?: string,
62542
+ * icon?: import("preact").ComponentChildren | null,
62543
+ * clearable?: boolean,
62544
+ * value?: any,
62545
+ * boolean?: boolean,
62546
+ * selectOnPress?: boolean,
62547
+ * [key: string]: any,
62548
+ * }>}
62549
+ * @param {any} [value] On a checkbox, what it is worth checked (`"on"` when
62550
+ * left out); unchecked it is `undefined`. See control_value.md, "What a
62551
+ * checkbox is worth".
62552
+ * @param {boolean} [boolean] Makes a checkbox a yes/no: `true` checked, `false`
62553
+ * unchecked, everywhere it hands its value. Takes no `value`.
62554
+ * @param {import("preact").ComponentChildren|null} [icon] What the textual
62555
+ * types that draw an icon (`search`, `email`, `tel`) put in their slot: left
62556
+ * out, the type's own glyph; `null`, nothing — for a field an application
62557
+ * draws its own magnifier beside, where a second one two centimetres away
62558
+ * would be one too many; anything else is drawn instead. A `search` field
62559
+ * swaps that slot for the clear cross as soon as it holds a value, whatever
62560
+ * this says.
62561
+ * @param {boolean} [clearable] Gives any other type the same slot: the icon
62562
+ * while empty (none, unless `icon` says otherwise), the clear cross once
62563
+ * filled.
62564
+ * @param {boolean} [selectOnPress] For a textual field whose value is usually
62565
+ * replaced rather than edited, like a browser's address bar: the first press
62566
+ * after the field took the focus selects its whole text once released, with a
62567
+ * mouse or a finger, and a second press places the caret. A focus that came
62568
+ * without a press (a dialog opening, Tab, code) selects nothing; the first key
62569
+ * that types selects first, so it replaces the text.
62570
+ */
61957
62571
  const Input = props => {
61958
62572
  resolveInputProps(props);
61959
62573
  const {
@@ -62043,7 +62657,7 @@ const TextareaStyleCSSVars = {
62043
62657
  };
62044
62658
 
62045
62659
  /**
62046
- * @type {import("ignore:preact").FunctionComponent<{
62660
+ * @type {import("preact").FunctionComponent<{
62047
62661
  * value?: string,
62048
62662
  * defaultValue?: string,
62049
62663
  * signal?: import("@preact/signals").Signal<string>,
@@ -62150,7 +62764,7 @@ const Textarea = ({
62150
62764
  * draws: put it under the box, in a form footer, next to a label, and feed it
62151
62765
  * the same value or signal as the textarea.
62152
62766
  *
62153
- * @type {import("ignore:preact").FunctionComponent<{
62767
+ * @type {import("preact").FunctionComponent<{
62154
62768
  * value?: string,
62155
62769
  * signal?: import("@preact/signals").Signal<string>,
62156
62770
  * maxLength?: number,
@@ -62383,8 +62997,8 @@ const useEditionController = () => {
62383
62997
  * it in CSS (the `[data-editing] input` rules in `control/table/table_css.js`),
62384
62998
  * the demo `control/demos/action/8_editable_demo.html` does it with props.
62385
62999
  *
62386
- * @type {import("ignore:preact").FunctionComponent<{
62387
- * children?: import("ignore:preact").ComponentChildren,
63000
+ * @type {import("preact").FunctionComponent<{
63001
+ * children?: import("preact").ComponentChildren,
62388
63002
  * action: Function | object,
62389
63003
  * editing?: { event?: Event } | null,
62390
63004
  * onEditEnd?: (detail: { success?: boolean, cancelled?: boolean, event: Event }) => void,
@@ -62747,6 +63361,77 @@ const commitSubtree = (controller, e) => {
62747
63361
  * <form> element (FormNested).
62748
63362
  */
62749
63363
 
63364
+ /**
63365
+ * @param {object} props
63366
+ * @param {boolean} [props.standalone] - Its value is its own: the form does not
63367
+ * register with the control group around it (a Picker, a ControlGroup), so
63368
+ * what is typed in it never becomes part of that group's value, and nothing
63369
+ * that group does — distributing a value, resetting, cascading validation —
63370
+ * reaches it. For a form that lives INSIDE something else while answering a
63371
+ * different question — "create the thing I am about to pick" inside a picker,
63372
+ * say. Every control takes the same prop, for the same reason. A form inside
63373
+ * another form is that by construction and does not need the prop: the group
63374
+ * it would register into is a form, which never owns another form's fields.
63375
+ * @param {boolean} [props.canSendWhileUnchanged] - Send even when nothing changed. By
63376
+ * default a form only acts on an answer that is actually new: submitting a
63377
+ * form nobody touched — one just rendered, one reopened and left alone — runs
63378
+ * no action (a field still showing its `defaultValue` is an answer: confirming
63379
+ * a suggestion), and after the first submission it is that value the next
63380
+ * one is compared against. Everything around the action still happens either
63381
+ * way: the constraints are checked, and what follows the send still follows
63382
+ * it (the slide moves on, the popup closes) — the user is done regardless of
63383
+ * whether there was anything to send. Set this for a form where sending the
63384
+ * same thing twice is the point — a single button that fires off a
63385
+ * notification, an action whose duplicates are fine. See also
63386
+ * `readOnlyWhileFormUnchanged` on `Button`, for a submit that should say it
63387
+ * is waiting rather than accept a press that sends nothing.
63388
+ * @param {boolean} [props.actionAbortable] - The person waiting may give up on
63389
+ * the send: closing the popup this form holds calls the run off and goes
63390
+ * through, instead of being refused. For a send whose answer may never come —
63391
+ * a request over a network that stops answering — where a sheet with no way
63392
+ * out is worse than an answer lost. Aborting frees the client and nothing
63393
+ * more: the write may already have landed on the other side (see
63394
+ * docs/actions.md#aborting-saves-resources-it-does-not-undo), so say it only
63395
+ * where the screen can be re-opened on what is actually there. Every control
63396
+ * inside the form inherits it along with the wait itself.
63397
+ * @param {boolean} [props.optimistic] - The send is taken as done on the form's
63398
+ * own word: no busy state, and what follows it (`command="--navi-close"`, a
63399
+ * slide moving on) runs at once, the run going on behind. A failure then has
63400
+ * no sheet to be read in: `resetOnError` puts the form back and the error
63401
+ * callout is drawn on what surrounds the closed sheet. For a write not worth being
63402
+ * waited for — a score, a note — never for one whose refusal changes what the
63403
+ * person does next (see
63404
+ * docs/popup_open.md#closing-when-a-button-also-runs-an-action).
63405
+ * @param {any} [props.pristineKey] - What the form is measured against, taken
63406
+ * again every time this changes. A form knows what it holds as soon as its
63407
+ * fields have registered, which is the right moment for a form whose values
63408
+ * are there on the first render — and never the right one for a screen that
63409
+ * modifies something: the resource arrives a request later and fills the
63410
+ * fields, and a form comparing against what it held BEFORE that opens already
63411
+ * changed. Pass whatever says the filling is done (a boolean, the resource
63412
+ * itself, a count of what has loaded) and what the fields carry at that
63413
+ * moment becomes the reference — including the fields that settle in a render
63414
+ * of their own, so there is no tick to wait for on the caller's side. Change
63415
+ * it once, when the screen is ready: taken again after someone started
63416
+ * typing, it would call what they wrote the reference.
63417
+ * @param {string} [props.command] - What follows a submission that went
63418
+ * through: the form has answered its question, and this says what the screen
63419
+ * does about it. Nothing runs when the submission is refused — the form then
63420
+ * stays in front of the user, showing what it is waiting for.
63421
+ *
63422
+ * `"--navi-close"` dismisses the popup the form is in,
63423
+ * `"--navi-left"`/`"--navi-right"`/`"--navi-up"`/`"--navi-down"` move on the
63424
+ * slide map it is in, `"--navi-nav-to:/the/url"` takes the user to a page,
63425
+ * `"--navi-void"` stays put — the one for a form inside a popup that must
63426
+ * stay open once it is sent (a side panel one keeps editing in).
63427
+ * Any navi command, really: it is triggered from the form, so it finds its
63428
+ * target the way that command always does.
63429
+ *
63430
+ * Left out, the surface the form sits in decides (see resolveAfterSend in
63431
+ * commands.js): a popup closes, a slide goes on to the next one — or back to
63432
+ * the one it came from when there is no next — and a form on a page does
63433
+ * nothing.
63434
+ */
62750
63435
  const Form = props => {
62751
63436
  const defaultRef = useRef();
62752
63437
  props.ref = props.ref || defaultRef;
@@ -63083,6 +63768,7 @@ const useRequestSubmitThroughGates = ref => {
63083
63768
  installImportMetaCssBuild(import.meta);
63084
63769
  const css$H = /* css */`.navi_group {
63085
63770
  --group-border-width: var(--navi-control-border-width);
63771
+ --border-width: initial;
63086
63772
 
63087
63773
  & > :not([navi-out-of-flow]) {
63088
63774
  &:hover, &[data-hover] {
@@ -66342,6 +67028,122 @@ SlideContainer.Down = SlideDown;
66342
67028
  SlideContainer.First = SlideFirst;
66343
67029
  SlideContainer.Last = SlideLast;
66344
67030
 
67031
+ /**
67032
+ * Displays a date/time value in a human-readable format using the current locale.
67033
+ *
67034
+ * @param {Date|number|string} children
67035
+ * The date to display. Accepts:
67036
+ * - a `Date` instance
67037
+ * - a Unix timestamp (number, in ms)
67038
+ * - a date string `"YYYY-MM-DD"` or any string parseable by `Date`
67039
+ * If the value cannot be parsed, it is rendered as-is.
67040
+ * If undefined/null, renders a placeholder shaped like the type: `"–"` for
67041
+ * `"relative"`, `"--:--"` for `"time"`, `"--"` for the durations, the
67042
+ * locale's field pattern for `"date"`/`"month"`/`"week"`/`"datetime"`.
67043
+ *
67044
+ * @param {"date"|"month"|"week"|"datetime"|"time"|"hour"|"minute"|"second"|"duration"|"relative"} [type="relative"]
67045
+ * Controls the display format:
67046
+ * - `"date"` → "lundi 11 mai" (long by default), "samedi 1 janvier 2050" outside the current year (see `year`);
67047
+ * `format="short"` → "lun. 11 mai"; `format="numeric"` → "11/05/2026";
67048
+ * `format={{ weekday: "long", month: "short" }}` spells the two apart → "mercredi 2 sept.";
67049
+ * a part set to `false` is dropped — `format={{ day: false, month: false }}` → "mercredi"
67050
+ * - `"month"` → "juin 2026"
67051
+ * - `"week"` → the ISO week as given ("2026-W38"), in a `<time datetime>`
67052
+ * - `"datetime"` → "lun. 11 mai, 14:30" (long); `format="short"` → "11 mai, 14:30"; `format="narrow"` → "11/05, 14:30"
67053
+ * - `"time"` → time-of-day as duration by default (e.g. "14:30" → "14 heures 30");
67054
+ * `format="timestring"` → clock "14 h 30". Midnight (00:xx) is
67055
+ * special-cased regardless of `format` — "0 heures 5 minutes"
67056
+ * would otherwise collapse to "5 minutes", indistinguishable
67057
+ * from an actual 5-minute duration: `format="long"` (default)
67058
+ * says "minuit et 5 minutes" (`format="short"/"narrow"/"compact"`
67059
+ * keep the zero hour instead — "0 h et 5 min"/"0h 5min"/"00h05").
67060
+ * `format="compact"` also zero-pads any single-digit hour, not
67061
+ * just midnight (e.g. "5h30" → "05h30"), closer to "05:30";
67062
+ * `pad={false}` writes it the way one says it instead ("8h30",
67063
+ * "8h"). See `<TimeRange>` for a span between two of them.
67064
+ * - `"hour"` → hours as duration (e.g. 1.5 → "1 heure 30 minutes")
67065
+ * - `"minute"` → minutes as duration (e.g. 90 → "1 heure 30 minutes")
67066
+ * - `"second"` → seconds as duration (e.g. 90 → "1 minute 30 secondes")
67067
+ * These three promote to the largest fitting unit, days included (36 hours
67068
+ * → "1 jour et 12 heures"); see `forceUnit` to stay in the given unit.
67069
+ * - `"duration"` → duration string/object/number (seconds); `format="iso"` → ISO 8601
67070
+ * - `"relative"` → "dans 1 heure 30" / "En cours" / "il y a 2 heures"
67071
+ * Handles past, present, and future.
67072
+ * `format="short"` → "dans 1 h et 30 min"; `format="narrow"`
67073
+ * → "+3 h"; `format="compact"` → "dans 1h30".
67074
+ * `eventDuration` defaults to 0 (instantaneous: no "En cours" window).
67075
+ *
67076
+ * @param {number|string} [eventDuration=0]
67077
+ * Duration of the event in milliseconds, or a duration string (`"PT1H30M"`,
67078
+ * `"1hour30minute"`). Only used with `type="relative"`.
67079
+ * When omitted, the event is instantaneous (point in time, no "En cours" window).
67080
+ * @param {boolean} [bare]
67081
+ * When true, strips the past-tense literal ("il y a", "ago") and returns only integer + unit.
67082
+ * Only applies to the past state of `type="relative"`.
67083
+ * @param {"long"|"short"|"narrow"|"compact"|"numeric"|"timestring"|"iso"|{ weekday?: "long"|"short"|"narrow"|false, day?: boolean, month?: "long"|"short"|"narrow"|"numeric"|false }} [format="long"]
67084
+ * Controls the verbosity of the output. Defaults to `"long"` for all types.
67085
+ * `"long"`, `"short"`, `"narrow"` and `"compact"` are verbosities every `type`
67086
+ * writes. The three under them name a shape only some types have; a `type`
67087
+ * without that shape writes its `"long"` form and names the dropped word in
67088
+ * the console.
67089
+ * - `"short"` → Intl short (e.g. "2 h et 15 min", short month for dates/datetimes, no weekday for datetime)
67090
+ * - `"narrow"` → Intl narrow (e.g. "2h 15min", numeric month for datetime)
67091
+ * - `"compact"` → custom compact notation (e.g. "2h15", no minute symbol
67092
+ * when hours present). A date has nothing tighter than its
67093
+ * digits, so `type="date"`/`"month"`/`"datetime"` write the
67094
+ * all-digit spelling ("17/09/2026", "17/09 14:30")
67095
+ * - `"numeric"` → numeric date, for `type="date"` and `type="month"` (e.g. "11/09/2026", "09/2026")
67096
+ * - `"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)
67097
+ * - `"iso"` → ISO 8601 string, only for `type="duration"` (e.g. "PT2H15M")
67098
+ * - an object → `type="date"` only, one verbosity per part
67099
+ * (`{ weekday: "long", month: "short" }`): a narrow card
67100
+ * keeps the weekday whole, since that is what the eye
67101
+ * lands on, and abbreviates the month, since that is
67102
+ * where the characters are. `false` drops a part:
67103
+ * `{ day: false, month: false }` is the weekday alone
67104
+ * ("mardi"), `{ month: false }` weekday + day-of-month
67105
+ * ("mardi 18").
67106
+ * @param {boolean} [forceUnit=false]
67107
+ * Keeps the value in the unit named by `type` however big it gets, for
67108
+ * `type="hour"`, `"minute"` and `"second"`: 36 hours reads "36 heures"
67109
+ * instead of the default "1 jour et 12 heures". The default promotes to the
67110
+ * largest fitting unit because it reads better; force the unit when the unit
67111
+ * itself is the information (a quota, a counter).
67112
+ * @param {number} [decimals]
67113
+ * `type="duration"` and `type="second"` only — how many decimals a fraction
67114
+ * of a second keeps, rounded: with `decimals={1}`, 1.234 reads "1,2 seconde"
67115
+ * and 1.96 "2 secondes"; a whole value is never padded ("12 secondes").
67116
+ * Without it the fraction is written to the millisecond ("1,234 seconde"). A
67117
+ * duration measured in milliseconds rarely comes out whole, and its last
67118
+ * digits are usually noise.
67119
+ * @param {boolean} [pad=true]
67120
+ * `type="time"` + `format="compact"` only — whether the clock is written at a
67121
+ * fixed width. `true` (default) zero-pads the hour and always writes the
67122
+ * minutes ("08h00", "08h30"), so a column of times lines up. `false` writes
67123
+ * the shape one says out loud instead: bare hour, minutes only when there are
67124
+ * any ("8h", "8h30"). The spelled-out formats put their units in words and
67125
+ * need neither, so they ignore it.
67126
+ * @param {"hour"|"minute"} [precision]
67127
+ * `type="time"` + `format="compact"` only, and rarely set by hand — whether a
67128
+ * zero minute is written ("8h00") or dropped ("8h"). Defaults to whatever
67129
+ * `pad` implies; the one reason to force `"minute"` on an unpadded clock is
67130
+ * to agree with a partner that has minutes of its own, which is what
67131
+ * `<TimeRange>` does for you.
67132
+ * @param {boolean|"auto"} [year]
67133
+ * `type="date"` only — whether the year is written. `"auto"` writes it only
67134
+ * when the date is not in the current year, the way a calendar does
67135
+ * ("lundi 11 mai", but "samedi 1 janvier 2050"); `false` drops it ("30/07",
67136
+ * the day/month order still following the locale). Defaults to `"auto"`
67137
+ * for the spelled formats and to `true` for `format="numeric"`. A format
67138
+ * without the month (`{ month: false }`) never writes it.
67139
+ * @param {boolean} [dayLabel]
67140
+ * When true and `type="date"`, appends the locale-aware relative label
67141
+ * ("hier", "aujourd'hui", "demain") when the date is yesterday, today, or tomorrow.
67142
+ * @param {string} [lang]
67143
+ * BCP 47 locale tag (e.g. `"fr"`, `"en-US"`).
67144
+ * Defaults to `languagesSignal.value`: the browser's languages, as
67145
+ * `setPreferredLanguage`/`setSupportedLanguages` narrow them.
67146
+ */
66345
67147
  const Time = props => {
66346
67148
  const {
66347
67149
  type
@@ -66606,6 +67408,20 @@ const TimeMinute = ({
66606
67408
  children: String(children)
66607
67409
  });
66608
67410
  }
67411
+ if (!Number.isInteger(minutes)) {
67412
+ // a fraction of a minute is read in seconds, the way formatMinuteDuration
67413
+ // writes it ("1 minute et 30 secondes")
67414
+ const clock = formatClock(Math.round(minutes * 60));
67415
+ return jsx(TimeText, {
67416
+ dateTime: clock,
67417
+ ...props,
67418
+ children: format === "timestring" ? clock : formatMinuteDuration(minutes, {
67419
+ lang,
67420
+ format,
67421
+ forceUnit
67422
+ })
67423
+ });
67424
+ }
66609
67425
  const totalHours = Math.floor(minutes / 60);
66610
67426
  const remainingMinutes = minutes % 60;
66611
67427
  const hh = String(totalHours).padStart(2, "0");
@@ -66635,6 +67451,7 @@ const TimeSecond = ({
66635
67451
  lang = languagesSignal.value,
66636
67452
  format = "long",
66637
67453
  forceUnit = false,
67454
+ decimals,
66638
67455
  ...props
66639
67456
  }) => {
66640
67457
  if (children === undefined || children === null) {
@@ -66650,19 +67467,17 @@ const TimeSecond = ({
66650
67467
  children: String(children)
66651
67468
  });
66652
67469
  }
66653
- const h = Math.floor(seconds / 3600);
66654
- const m = Math.floor(seconds % 3600 / 60);
66655
- const s = seconds % 60;
66656
- const dateTime = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
67470
+ const dateTime = formatClock(seconds);
66657
67471
  let text;
66658
67472
  if (format === "timestring") {
66659
67473
  // Always HH:MM:SS to avoid ambiguity with HH:MM time-of-day format
66660
- text = dateTime;
67474
+ text = formatClock(seconds, decimals);
66661
67475
  } else {
66662
67476
  text = formatSecondDuration(seconds, {
66663
67477
  lang,
66664
67478
  format,
66665
- forceUnit
67479
+ forceUnit,
67480
+ decimals
66666
67481
  });
66667
67482
  }
66668
67483
  return jsx(TimeText, {
@@ -66715,6 +67530,7 @@ const TimeDuration = ({
66715
67530
  children,
66716
67531
  lang = languagesSignal.value,
66717
67532
  format = "long",
67533
+ decimals,
66718
67534
  ...props
66719
67535
  }) => {
66720
67536
  if (children === undefined || children === null) {
@@ -66762,7 +67578,8 @@ const TimeDuration = ({
66762
67578
  ...props,
66763
67579
  children: formatDuration(duration, {
66764
67580
  lang,
66765
- format
67581
+ format,
67582
+ decimals
66766
67583
  })
66767
67584
  });
66768
67585
  }
@@ -66774,7 +67591,8 @@ const TimeDuration = ({
66774
67591
  }
66775
67592
  const text = formatDuration(duration, {
66776
67593
  lang,
66777
- format
67594
+ format,
67595
+ decimals
66778
67596
  });
66779
67597
  return jsx(TimeText, {
66780
67598
  dateTime: isoString,
@@ -66914,6 +67732,24 @@ const TimeRange = ({
66914
67732
  });
66915
67733
  };
66916
67734
 
67735
+ // "HH:MM:SS", the fraction of a second after a period ("00:00:01.234"): the
67736
+ // shape a `datetime` attribute reads, which takes up to 3 decimals.
67737
+ const formatClock = (seconds, decimals = 3) => {
67738
+ const factor = 10 ** decimals;
67739
+ const total = Math.round(seconds * factor);
67740
+ const fraction = total % factor;
67741
+ const totalSeconds = (total - fraction) / factor;
67742
+ const h = Math.floor(totalSeconds / 3600);
67743
+ const m = Math.floor(totalSeconds % 3600 / 60);
67744
+ const s = totalSeconds % 60;
67745
+ const clock = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
67746
+ if (fraction === 0) {
67747
+ return clock;
67748
+ }
67749
+ const digits = String(fraction).padStart(decimals, "0").replace(/0+$/, "");
67750
+ return `${clock}.${digits}`;
67751
+ };
67752
+
66917
67753
  /**
66918
67754
  * Wraps multiple inputs together and handles keyboard navigation and paste
66919
67755
  * distribution between them.
@@ -67167,6 +68003,11 @@ const renderSafe = (value) => {
67167
68003
  };
67168
68004
 
67169
68005
  installImportMetaCssBuild(import.meta);
68006
+ /**
68007
+ * The group a popup opens in, or null: what a Popover or a Dialog reads to know
68008
+ * that its wall is the group's. Every popup provides null again to what it
68009
+ * holds, so a popup opened from inside a popup is in front of it, not a peer.
68010
+ */
67170
68011
  const PopupGroupContext = createContext(null);
67171
68012
 
67172
68013
  /**
@@ -67333,13 +68174,13 @@ const css$F = /* css */`@property --x-popup-group-top {
67333
68174
  * `navi-out-of-flow` like everything navi adds to a tree. See
67334
68175
  * docs/popup_backdrop.md.
67335
68176
  *
67336
- * @type {import("ignore:preact").FunctionComponent<{
68177
+ * @type {import("preact").FunctionComponent<{
67337
68178
  * backdropVariant?: "auto" | "lift" | "discrete" | "invisible",
67338
68179
  * backdropColor?: string,
67339
68180
  * backdropFilter?: string,
67340
68181
  * themeColor?: string | false,
67341
68182
  * role?: string,
67342
- * children?: import("ignore:preact").ComponentChildren,
68183
+ * children?: import("preact").ComponentChildren,
67343
68184
  * [key: string]: any,
67344
68185
  * }>}
67345
68186
  * @param {"auto"|"lift"|"discrete"|"invisible"} [props.backdropVariant="auto"] -
@@ -69386,6 +70227,35 @@ const clipOf = (side, distance) => {
69386
70227
  return `inset(${UNCUT} ${UNCUT} ${UNCUT} ${cut})`;
69387
70228
  };
69388
70229
 
70230
+ /**
70231
+ * The cross that closes the popup it sits in — reached as `<Popup.Close />`,
70232
+ * `<Dialog.Close />` or `<Popover.Close />`. A discrete icon button sending
70233
+ * `--navi-close`, which the nearest popup above it answers; the caller only
70234
+ * decides where it goes (the right end of a header, most of the time).
70235
+ *
70236
+ * It is the way out of a popup, so it is the one affordance in there that the
70237
+ * surrounding control's read-only and disabled do not reach: a read-only picker
70238
+ * still opens (reading changes nothing) and hands its popup that read-only, and
70239
+ * a cross wearing it would trap the user in a popup they were invited to read.
70240
+ * A close button written by hand inherits it and refuses the press — which is
70241
+ * the reason to use this one rather than a `<Button command="--navi-close" />`.
70242
+ *
70243
+ * Exempt from the state around it, never from the popup's own answer: the cross
70244
+ * ASKS, and the popup decides — a dialog holding a form mid-send keeps the
70245
+ * close, a Picker holding a value that does not validate keeps it too.
70246
+ *
70247
+ * @type {import("preact").FunctionComponent<{
70248
+ * label?: string,
70249
+ * iconSize?: number | string,
70250
+ * } & Record<string, any>>}
70251
+ * @param {string} [props.label] - What assistive tech reads for the cross
70252
+ * (navi's own "Close" text by default, in the active language).
70253
+ * @param {number|string} [props.iconSize] - How big the cross is drawn, apart
70254
+ * from the box around it. Unset, the glyph follows the button's font size
70255
+ * like a character, and `size` moves glyph and padding together. Set it when
70256
+ * the cross stands in for an affordance the app draws elsewhere and has to
70257
+ * land on the same pixels: `padding="s" iconSize="m"`.
70258
+ */
69389
70259
  const PopupClose = ({
69390
70260
  label,
69391
70261
  iconSize,
@@ -69634,6 +70504,9 @@ const TitlePseudoClasses = [":hover"];
69634
70504
  * step of a form — so only the title knows when it is there.
69635
70505
  */
69636
70506
 
70507
+ // The popup a title names: the nearest one around it. A surface that is no
70508
+ // popup of this kind but holds content of its own (a picker's callout) sets it
70509
+ // back to null, so a title in there names nothing rather than the popup around.
69637
70510
  const PopupTitleContext = createContext(null);
69638
70511
 
69639
70512
  /**
@@ -69643,10 +70516,10 @@ const PopupTitleContext = createContext(null);
69643
70516
  * announces the popup by it, and `getByRole("dialog", { name })` finds it.
69644
70517
  * An `aria-label`/`aria-labelledby` written on the popup itself wins over it.
69645
70518
  *
69646
- * @type {import("ignore:preact").FunctionComponent<{
70519
+ * @type {import("preact").FunctionComponent<{
69647
70520
  * as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6",
69648
70521
  * id?: string,
69649
- * children?: import("ignore:preact").ComponentChildren,
70522
+ * children?: import("preact").ComponentChildren,
69650
70523
  * } & Record<string, any>>}
69651
70524
  */
69652
70525
  const PopupTitle = props => {
@@ -69707,6 +70580,12 @@ const usePopupName = ({
69707
70580
  const NO_TITLE = [];
69708
70581
 
69709
70582
  installImportMetaCssBuild(import.meta);
70583
+ // Same need popover.jsx has for its own local renderer (see its
70584
+ // openLocalPopoverCount): a layer="local" dialog is a plain positioned element
70585
+ // in normal flow, so it gets none of the top layer's free "last shown wins" and
70586
+ // would paint under anything declared after it. A live count, not an
70587
+ // ever-growing counter, so the order stays small and resets once nothing is
70588
+ // open.
69710
70589
  let openLocalDialogCount = 0;
69711
70590
  const css$C = /* css */`
69712
70591
  /* jsenv-css-opaque: popupCss overlaps these rules, own sheet = cascade
@@ -70781,7 +71660,7 @@ const css$C = /* css */`
70781
71660
  * the dialog: its accessible name when neither `aria-label`/`aria-labelledby`
70782
71661
  * nor a `<Dialog.Title>` names it. A Picker passes its trigger's.
70783
71662
  * @param {string} [props.openerAriaLabel] - The same, as a string.
70784
- * @param {import("ignore:preact").ComponentChildren} props.children
71663
+ * @param {import("preact").ComponentChildren} props.children
70785
71664
  */
70786
71665
  const Dialog = props => {
70787
71666
  import.meta.css = [css$C, "@jsenv/navi/src/layout/dialog.jsx"];
@@ -71644,8 +72523,10 @@ const useDialogProps = props => {
71644
72523
  // gap — the same thing its CSS width/height say (see [data-expand-x],
71645
72524
  // [data-expand-y]) — so the placement derives it instead of measuring
71646
72525
  // it: a measurement here lays out the dialog's whole content, rows and
71647
- // all, for a number already known.
71648
- const fill = expandX && expandY;
72526
+ // all, for a number already known. A cap (maxWidth/maxHeight, a Picker's
72527
+ // anchor ceiling) stretches it to less than that, and only a measured
72528
+ // box can be centred.
72529
+ const fill = expandX && expandY && !isSizeCapped(dialogEl);
71649
72530
  const pickOptions = {
71650
72531
  positionArea,
71651
72532
  container: positionedAncestor,
@@ -72221,6 +73102,14 @@ const switchTransitionsOn = dialogEl => {
72221
73102
  dialogEl.style.transitionProperty = "";
72222
73103
  dialogEl.removeAttribute("navi-transitions-off");
72223
73104
  };
73105
+
73106
+ // Reads style, not layout: an unset cap is the guaranteed-invalid value its
73107
+ // @property gives it, and reads back as "". Catches a cap written inline by
73108
+ // the prop as well as one set from a stylesheet.
73109
+ const isSizeCapped = dialogEl => {
73110
+ const computedStyle = getComputedStyle(dialogEl);
73111
+ return computedStyle.getPropertyValue("--dialog-max-width") !== "" || computedStyle.getPropertyValue("--dialog-max-height") !== "";
73112
+ };
72224
73113
  const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":focus-within"];
72225
73114
 
72226
73115
  // Lets consumers pass animationDuration="0.5s" as a regular prop; Box maps
@@ -72246,6 +73135,11 @@ Dialog.Close = PopupClose;
72246
73135
  Dialog.Title = PopupTitle;
72247
73136
 
72248
73137
  installImportMetaCssBuild(import.meta);
73138
+ // Custom renderer only (layer="local") — count of currently-open instances,
73139
+ // see --popover-stack-order's own CSS comment for why a plain "last shown"
73140
+ // default doesn't happen for free the way it does for the top-layer
73141
+ // renderer. A live count (not an ever-growing counter) so the assigned
73142
+ // order stays small and resets to 0 once nothing is open.
72249
73143
  let openLocalPopoverCount = 0;
72250
73144
  const css$B = /* css */`
72251
73145
  /* jsenv-css-opaque: popupCss overlaps these rules, own sheet = cascade
@@ -72853,7 +73747,7 @@ const css$B = /* css */`
72853
73747
  * something names — these, `aria-label`/`aria-labelledby`, a
72854
73748
  * `<Popover.Title>` — is a `dialog`, unless given another `role`: a name on
72855
73749
  * an element with no role is read by nobody.
72856
- * @param {import("ignore:preact").ComponentChildren} props.children
73750
+ * @param {import("preact").ComponentChildren} props.children
72857
73751
  */
72858
73752
  const Popover = props => {
72859
73753
  import.meta.css = [css$B, "@jsenv/navi/src/layout/popover.jsx"];
@@ -74223,7 +75117,7 @@ const css$A = /* css */`@layer navi {
74223
75117
  * content whose fresh state is its initial state: an edit left unsent at
74224
75118
  * the close does not come back at the next opening (an untouched field
74225
75119
  * needs none of this to show a new `defaultValue` — it follows one).
74226
- * @param {import("ignore:preact").ComponentChildren} props.children
75120
+ * @param {import("preact").ComponentChildren} props.children
74227
75121
  */
74228
75122
  const Popup = props => {
74229
75123
  import.meta.css = [css$A, "@jsenv/navi/src/layout/popup.jsx"];
@@ -75914,6 +76808,18 @@ const PickerConfirmBody = ({
75914
76808
  });
75915
76809
  };
75916
76810
 
76811
+ /**
76812
+ * PickerNaviMinute — a custom minute picker that opens a number input.
76813
+ *
76814
+ * Renders a popover containing an `<input type="number">` pre-filled with the
76815
+ * current value, plus Confirm / Clear / Cancel actions. The picker holds a
76816
+ * number of minutes: its own field is a text one told
76817
+ * `navi-input-type="minute"`, so the value is read back as a number and a
76818
+ * min/max/step message speaks of minutes.
76819
+ *
76820
+ * Usage:
76821
+ * <Picker type="navi_minute" min={15} max={300} step={15} value={90} />
76822
+ */
75917
76823
  const PickerNaviMinute = props => {
75918
76824
  const Next = useNextResolver();
75919
76825
  const {
@@ -76297,6 +77203,16 @@ const LoadingDotsSvg = () => {
76297
77203
  });
76298
77204
  };
76299
77205
 
77206
+ /**
77207
+ * A loading indicator sized to match the surrounding text (1em × 1em).
77208
+ * Inherits color from the current text color by default.
77209
+ *
77210
+ * @param {"circle"|"dots"} [props.variant="circle"] - Visual style of the indicator
77211
+ * @param {string} [props.color="currentColor"] - Color of the indicator; inherits from CSS `color` by default
77212
+ *
77213
+ * Every other prop goes to the `Icon` it is drawn in: `size` is a font size
77214
+ * (the indicator being 1em), `width`/`height` size it as a block.
77215
+ */
76300
77216
  const LoadingIndicator = ({
76301
77217
  variant = "circle",
76302
77218
  ...props
@@ -76359,7 +77275,7 @@ const css$x = /* css */`@layer navi {
76359
77275
  * Everything about how it looks goes through a CSS var, so it can be set per
76360
77276
  * separator as a prop or once for a whole surface in CSS.
76361
77277
  *
76362
- * @type {import("ignore:preact").FunctionComponent<{
77278
+ * @type {import("preact").FunctionComponent<{
76363
77279
  * vertical?: boolean,
76364
77280
  * size?: string|number,
76365
77281
  * color?: string,
@@ -77962,6 +78878,9 @@ const applySearchHighlight = (el, highlight) => {
77962
78878
  };
77963
78879
 
77964
78880
  installImportMetaCssBuild(import.meta);
78881
+ // Everything the list knows about its items — see list_items.js. Filled in by
78882
+ // the children as they render, read by everything that must reserve room for
78883
+ // what is not rendered, count what is, or know what stands above it.
77965
78884
  const ListItemsContext = createContext(null);
77966
78885
  // The group an item is declared in, by id (see ListItemGroup): what its count
77967
78886
  // and its separator are scoped to.
@@ -82716,7 +83635,7 @@ const LIST_ITEM_STYLE_CSS_VARS = {
82716
83635
  * of it. Props not listed here are forwarded to the rendered <li> (Box layout
82717
83636
  * props included).
82718
83637
  *
82719
- * @type {import("ignore:preact").FunctionComponent<{
83638
+ * @type {import("preact").FunctionComponent<{
82720
83639
  * id?: string,
82721
83640
  * selectable?: boolean,
82722
83641
  * value?: any,
@@ -82725,14 +83644,14 @@ const LIST_ITEM_STYLE_CSS_VARS = {
82725
83644
  * pointed?: boolean,
82726
83645
  * selectableArea?: "all" | "manual",
82727
83646
  * skeleton?: boolean,
82728
- * error?: boolean | import("ignore:preact").ComponentChildren,
83647
+ * error?: boolean | import("preact").ComponentChildren,
82729
83648
  * onErrorDismiss?: (event: Event) => void,
82730
83649
  * loading?: boolean | "adding" | "removing" | "updating",
82731
83650
  * readOnly?: boolean,
82732
83651
  * filtered?: boolean,
82733
83652
  * hidden?: boolean,
82734
83653
  * matchInfo?: {match: boolean, matchScore?: number, matchRanges?: Array<[number, number]>},
82735
- * children?: import("ignore:preact").ComponentChildren,
83654
+ * children?: import("preact").ComponentChildren,
82736
83655
  * [key: string]: any,
82737
83656
  * }>}
82738
83657
  * @param {string} [props.id]
@@ -82767,7 +83686,7 @@ const LIST_ITEM_STYLE_CSS_VARS = {
82767
83686
  * real item. This is what a `renderSkeleton` returns for an item on its way;
82768
83687
  * Box layout props (padding…) pass through so the placeholder can match the
82769
83688
  * real items' metrics.
82770
- * @param {boolean|import("ignore:preact").ComponentChildren} [props.error]
83689
+ * @param {boolean|import("preact").ComponentChildren} [props.error]
82771
83690
  * What this item stood for failed: the message replaces its content, styled
82772
83691
  * like the list's own error. `true` shows a generic sentence. When
82773
83692
  * `onErrorDismiss` is given, a dismiss button is drawn next to the message
@@ -82907,21 +83826,21 @@ const VISIBILITY_HIDDEN_STYLE = {
82907
83826
  * Several runs can live in one list, next to plain `<List.Item>` children and
82908
83827
  * inside `<List.Group>`s; each takes its place in declaration order.
82909
83828
  *
82910
- * @type {import("ignore:preact").FunctionComponent<{
82911
- * renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("ignore:preact").ComponentChildren,
83829
+ * @type {import("preact").FunctionComponent<{
83830
+ * renderItem: (item: any, index: number, state: {refreshing: boolean}) => import("preact").ComponentChildren,
82912
83831
  * findText?: (item: any, index: number) => string,
82913
83832
  * items?: any[],
82914
83833
  * itemsAction?: (range: {start: number, end: number, limit: number, before?: string, after?: string, around?: string, count?: number, signal: AbortSignal}) => any,
82915
83834
  * debounce?: number,
82916
83835
  * count?: number,
82917
83836
  * groupBy?: (item: any, index: number) => any,
82918
- * renderGroupLabel?: (item: any, index: number) => import("ignore:preact").ComponentChildren,
83837
+ * renderGroupLabel?: (item: any, index: number) => import("preact").ComponentChildren,
82919
83838
  * groupLabelProps?: (item: any, index: number) => object,
82920
83839
  * pageSize?: number,
82921
83840
  * memoryBudget?: number,
82922
- * renderSkeleton?: false | ((index: number) => import("ignore:preact").ComponentChildren),
83841
+ * renderSkeleton?: false | ((index: number) => import("preact").ComponentChildren),
82923
83842
  * itemSize?: (index: number) => number,
82924
- * renderError?: (failure: {error: any, retry: () => void, start: number, end: number}) => import("ignore:preact").ComponentChildren,
83843
+ * renderError?: (failure: {error: any, retry: () => void, start: number, end: number}) => import("preact").ComponentChildren,
82925
83844
  * onRequestStateChange?: (state: {busy: boolean, refreshing: boolean, range: {start: number, end: number}|null}) => void,
82926
83845
  * onCountChange?: (count: number|undefined) => void,
82927
83846
  * }>}
@@ -84525,7 +85444,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
84525
85444
  * List — generic virtualized scroll container.
84526
85445
  * Items must use <List.Item> to participate in tracking.
84527
85446
  *
84528
- * @type {import("ignore:preact").FunctionComponent<{
85447
+ * @type {import("preact").FunctionComponent<{
84529
85448
  * selectable?: boolean,
84530
85449
  * multiple?: boolean,
84531
85450
  * deselectable?: boolean,
@@ -84546,16 +85465,16 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
84546
85465
  * scroller?: "self" | "parent" | "document" | Element | {current: Element},
84547
85466
  * hoverWhileScrolling?: boolean,
84548
85467
  * scrollResetOnNavigation?: boolean,
84549
- * fallback?: import("ignore:preact").ComponentChildren,
84550
- * searchFallback?: import("ignore:preact").ComponentChildren,
85468
+ * fallback?: import("preact").ComponentChildren,
85469
+ * searchFallback?: import("preact").ComponentChildren,
84551
85470
  * searchText?: string,
84552
85471
  * searchNoMatchMode?: "remove" | "invisible_and_inert" | "muted",
84553
85472
  * loading?: boolean,
84554
- * loadingFallback?: "skeleton" | "loader" | import("ignore:preact").ComponentChildren,
85473
+ * loadingFallback?: "skeleton" | "loader" | import("preact").ComponentChildren,
84555
85474
  * loadingSkeletonCount?: number,
84556
- * renderSkeleton?: false | ((index: number) => import("ignore:preact").ComponentChildren),
84557
- * error?: boolean | import("ignore:preact").ComponentChildren,
84558
- * separator?: boolean | import("ignore:preact").ComponentChildren,
85475
+ * renderSkeleton?: false | ((index: number) => import("preact").ComponentChildren),
85476
+ * error?: boolean | import("preact").ComponentChildren,
85477
+ * separator?: boolean | import("preact").ComponentChildren,
84559
85478
  * itemTransition?: boolean,
84560
85479
  * lockSize?: boolean,
84561
85480
  * horizontal?: boolean,
@@ -84569,7 +85488,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
84569
85488
  * expandX?: boolean,
84570
85489
  * expandY?: boolean,
84571
85490
  * expand?: boolean,
84572
- * children?: import("ignore:preact").ComponentChildren,
85491
+ * children?: import("preact").ComponentChildren,
84573
85492
  * [key: string]: any,
84574
85493
  * }>}
84575
85494
  * @param {string} [props.columns]
@@ -84629,7 +85548,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
84629
85548
  * have, so nothing moves when they arrive. Used for the items a `<List.Items>`
84630
85549
  * stands for and does not hold yet, and for the placeholder items drawn while
84631
85550
  * the whole list is `loading`. Defaults to a bare `<List.Item skeleton>`.
84632
- * @param {"skeleton"|"loader"|import("ignore:preact").ComponentChildren} [props.loadingFallback="skeleton"]
85551
+ * @param {"skeleton"|"loader"|import("preact").ComponentChildren} [props.loadingFallback="skeleton"]
84633
85552
  * What to display in place of the items while `loading` — that is, while
84634
85553
  * there is nothing to show at all: `"skeleton"` renders
84635
85554
  * `loadingSkeletonCount` placeholder items (look:
@@ -84813,6 +85732,17 @@ const List = /*#__PURE__*/Object.assign(ListResolved, {
84813
85732
  Group: ListItemGroup
84814
85733
  });
84815
85734
 
85735
+ /**
85736
+ * PickerNaviTime — a fully custom time picker driven by min/max/step.
85737
+ *
85738
+ * Unlike the native `<input type="time">`, this picker only exposes the exact
85739
+ * slots generated from min/max/step, so the user can never pick an out-of-range
85740
+ * or out-of-step value. The popover content is a selectable `List` of the
85741
+ * available slots; picking one closes the popover and submits the value.
85742
+ *
85743
+ * Usage:
85744
+ * <Picker type="navi_time" min="08:00" max="18:00" step="00:30" />
85745
+ */
84816
85746
  const PickerNaviTime = props => {
84817
85747
  const Next = useNextResolver();
84818
85748
  const {
@@ -84920,18 +85850,20 @@ const createBadgeRegistry = () => {
84920
85850
  };
84921
85851
 
84922
85852
  installImportMetaCssBuild(import.meta);
84923
- const css$u = /* css */`@layer navi;
85853
+ const css$u = /* css */`@layer navi {
85854
+ .navi_badge {
85855
+ --font-size: .7em;
85856
+ --badge-padding-x-default: .8em;
85857
+ --badge-padding-y-default: .4em;
85858
+ }
85859
+ }
84924
85860
 
84925
85861
  .navi_badge {
84926
- --font-size: .7em;
84927
- --badge-padding-x-default: .8em;
84928
- --badge-padding-y-default: .4em;
84929
85862
  --x-badge-padding-top: var(--badge-padding-top, var(--badge-padding-y, var(--badge-padding, var(--badge-padding-y-default))));
84930
85863
  --x-badge-padding-right: var(--badge-padding-right, var(--badge-padding-x, var(--badge-padding, var(--badge-padding-x-default))));
84931
85864
  --x-badge-padding-bottom: var(--badge-padding-bottom, var(--badge-padding-y, var(--badge-padding, var(--badge-padding-y-default))));
84932
85865
  --x-badge-padding-left: var(--badge-padding-left, var(--badge-padding-x, var(--badge-padding, var(--badge-padding-x-default))));
84933
85866
  --x-background: var(--background, light-dark(#e0e0e0, #3a3a3a));
84934
- --x-background-color: var(--background-color, var(--x-background));
84935
85867
  --x-color: var(--color, white);
84936
85868
  max-width: 200px;
84937
85869
  padding-top: var(--x-badge-padding-top);
@@ -84942,7 +85874,6 @@ const css$u = /* css */`@layer navi;
84942
85874
  font-size: var(--font-size);
84943
85875
  text-box: trim-both cap alphabetic;
84944
85876
  background: var(--x-background);
84945
- background-color: var(--x-background-color);
84946
85877
  border-radius: 1em;
84947
85878
  display: inline;
84948
85879
  position: relative;
@@ -85068,7 +85999,6 @@ const BadgeStyleCSSVars = {
85068
85999
  paddingRight: "--badge-padding-right",
85069
86000
  paddingBottom: "--badge-padding-bottom",
85070
86001
  paddingLeft: "--badge-padding-left",
85071
- backgroundColor: "--background-color",
85072
86002
  background: "--background",
85073
86003
  borderColor: "--border-color",
85074
86004
  color: "--color",
@@ -85242,7 +86172,7 @@ const useBadgeRegistry = (children, enabled) => {
85242
86172
  * shrinkWrap on its own builds the measurement ghost: under maxLines the list
85243
86173
  * is measured in place, once the rows are known.
85244
86174
  *
85245
- * @param {import("ignore:preact").ComponentChildren} [fallback]
86175
+ * @param {import("preact").ComponentChildren} [fallback]
85246
86176
  * Rendered in place of the badges when there is none. Without it an empty
85247
86177
  * list renders nothing at all. In a <Picker> this is the only placeholder
85248
86178
  * the user gets — a picker given a `ui` does not draw its own — so pass the
@@ -85876,10 +86806,10 @@ const PickerArrayUI = /*#__PURE__*/asPickerOwnUI(() => {
85876
86806
  * — a row being read still says what was picked, it just no longer offers to
85877
86807
  * unpick it.
85878
86808
  *
85879
- * @type {import("ignore:preact").FunctionComponent<{
86809
+ * @type {import("preact").FunctionComponent<{
85880
86810
  * value: any,
85881
86811
  * commandFor?: string,
85882
- * children?: import("ignore:preact").ComponentChildren,
86812
+ * children?: import("preact").ComponentChildren,
85883
86813
  * [key: string]: any,
85884
86814
  * }>}
85885
86815
  * @param {any} value The value this chip stands for — one entry of what the
@@ -86145,6 +87075,7 @@ const css$r = /* css */`@layer navi {
86145
87075
  .navi_picker {
86146
87076
  --picker-border-radius: var(--navi-control-border-radius);
86147
87077
  --picker-border-width: var(--navi-control-border-width);
87078
+ --border-width: var(--picker-border-width);
86148
87079
  --picker-outline-width: var(--navi-focus-outline-width);
86149
87080
  --picker-outline-offset: calc(-.5 * var(--picker-outline-width));
86150
87081
  --picker-outline-color: var(--navi-focus-outline-color);
@@ -86184,6 +87115,111 @@ const css$r = /* css */`@layer navi {
86184
87115
  --picker-align-x-default: flex-start;
86185
87116
  --picker-align-y-default: center;
86186
87117
  --picker-text-align-default: initial;
87118
+ font-size: var(--picker-font-size);
87119
+ font-family: var(--picker-font-family);
87120
+ line-height: var(--navi-control-line-height);
87121
+ }
87122
+
87123
+ .navi_picker[data-variant="icon"] {
87124
+ --picker-border-width: 0px;
87125
+ --picker-border-color: transparent;
87126
+ --picker-border-color-hover: var(--picker-border-color);
87127
+ --picker-border-color-readonly: var(--picker-border-color);
87128
+ --picker-border-color-disabled: var(--picker-border-color);
87129
+ --picker-background-color: transparent;
87130
+ --picker-background-color-hover: var(--picker-background-color);
87131
+ --picker-background-color-readonly: var(--picker-background-color);
87132
+ --picker-background-color-disabled: var(--picker-background-color);
87133
+ --picker-icon-color: currentColor;
87134
+ }
87135
+
87136
+ .navi_picker[data-variant="discrete"] {
87137
+ --picker-border-width: 0px;
87138
+ --picker-border-color: transparent;
87139
+ --picker-border-color-hover: var(--picker-border-color);
87140
+ --picker-border-color-readonly: var(--picker-border-color);
87141
+ --picker-border-color-disabled: var(--picker-border-color);
87142
+ --picker-background-color: transparent;
87143
+ --picker-background-color-hover: color-mix(in srgb,
87144
+ currentColor 8%,
87145
+ var(--picker-background-color));
87146
+ --picker-background-color-readonly: var(--picker-background-color);
87147
+ --picker-background-color-disabled: var(--picker-background-color);
87148
+ }
87149
+
87150
+ .navi_picker[data-variant="headless"] {
87151
+ --picker-padding-x-default: 0;
87152
+ --picker-padding-y-default: 0;
87153
+ --picker-border-width: 0px;
87154
+ --picker-border-color: transparent;
87155
+ --picker-outline-width: 0px;
87156
+ --picker-border-color-hover: var(--picker-border-color);
87157
+ --picker-border-color-readonly: var(--picker-border-color);
87158
+ --picker-border-color-disabled: var(--picker-border-color);
87159
+ --picker-background-color: transparent;
87160
+ --picker-background-color-hover: var(--picker-background-color);
87161
+ --picker-background-color-readonly: var(--picker-background-color);
87162
+ --picker-background-color-disabled: var(--picker-background-color);
87163
+ --picker-icon-color: currentColor;
87164
+ }
87165
+
87166
+ .navi_picker[data-variant="bare"] {
87167
+ --picker-padding-x-default: 0;
87168
+ --picker-padding-y-default: 0;
87169
+ --picker-align-y-default: stretch;
87170
+ --picker-border-width: 0px;
87171
+ --picker-border-color: transparent;
87172
+ --picker-border-color-hover: var(--picker-border-color);
87173
+ --picker-border-color-readonly: var(--picker-border-color);
87174
+ --picker-border-color-disabled: var(--picker-border-color);
87175
+ --picker-background-color: transparent;
87176
+ --picker-background-color-hover: var(--picker-background-color);
87177
+ --picker-background-color-readonly: var(--picker-background-color);
87178
+ --picker-background-color-disabled: var(--picker-background-color);
87179
+ --picker-icon-color: currentColor;
87180
+ font-size: inherit;
87181
+ font-family: inherit;
87182
+ line-height: inherit;
87183
+ }
87184
+
87185
+ .navi_picker[data-variant="button"] {
87186
+ --picker-padding-x-default: var(--navi-button-padding-x-default);
87187
+ --picker-padding-y-default: var(--navi-button-padding-y-default);
87188
+ --picker-align-x-default: center;
87189
+ --picker-background-color: var(--navi-button-background-color);
87190
+ --picker-border-color-readonly: color-mix(in srgb,
87191
+ var(--picker-border-color) 30%,
87192
+ var(--navi-surface-color));
87193
+ --picker-background-color-readonly: var(--picker-background-color);
87194
+ --picker-color-readonly: color-mix(in srgb,
87195
+ var(--picker-color) 30%,
87196
+ transparent);
87197
+ --picker-border-color-disabled: var(--picker-border-color-readonly);
87198
+ --picker-background-color-disabled: var(--picker-background-color-readonly);
87199
+ --picker-color-disabled: var(--picker-color-readonly);
87200
+ --picker-text-align-default: center;
87201
+ }
87202
+
87203
+ .navi_picker[data-variant="text"] {
87204
+ --picker-padding-x-default: 0;
87205
+ --picker-padding-y-default: 0;
87206
+ --picker-border-width: 0px;
87207
+ --picker-border-color: transparent;
87208
+ --picker-border-color-hover: var(--picker-border-color);
87209
+ --picker-border-color-readonly: var(--picker-border-color);
87210
+ --picker-border-color-disabled: var(--picker-border-color);
87211
+ --picker-background-color: transparent;
87212
+ --picker-background-color-hover: var(--picker-background-color);
87213
+ --picker-background-color-readonly: var(--picker-background-color);
87214
+ --picker-background-color-disabled: var(--picker-background-color);
87215
+ font-size: inherit;
87216
+ font-family: inherit;
87217
+ }
87218
+
87219
+ .navi_picker[data-icon] {
87220
+ --picker-padding-x-default: 0;
87221
+ --picker-padding-y-default: 0;
87222
+ --picker-align-x-default: center;
86187
87223
  }
86188
87224
  }
86189
87225
 
@@ -86206,10 +87242,7 @@ const css$r = /* css */`@layer navi {
86206
87242
  box-sizing: border-box;
86207
87243
  max-width: 100%;
86208
87244
  color: var(--x-picker-color);
86209
- font-size: var(--picker-font-size);
86210
- font-family: var(--picker-font-family);
86211
87245
  text-align: inherit;
86212
- line-height: var(--navi-control-line-height);
86213
87246
  border-top-left-radius: var(--x-corner-top-left-radius, var(--picker-border-radius));
86214
87247
  border-top-right-radius: var(--x-corner-top-right-radius, var(--picker-border-radius));
86215
87248
  border-bottom-right-radius: var(--x-corner-bottom-right-radius, var(--picker-border-radius));
@@ -86451,48 +87484,7 @@ const css$r = /* css */`@layer navi {
86451
87484
  --x-picker-border-color: var(--callout-color);
86452
87485
  }
86453
87486
 
86454
- &[data-variant="icon"] {
86455
- --picker-border-width: 0px;
86456
- --picker-border-color: transparent;
86457
- --picker-border-color-hover: var(--picker-border-color);
86458
- --picker-border-color-readonly: var(--picker-border-color);
86459
- --picker-border-color-disabled: var(--picker-border-color);
86460
- --picker-background-color: transparent;
86461
- --picker-background-color-hover: var(--picker-background-color);
86462
- --picker-background-color-readonly: var(--picker-background-color);
86463
- --picker-background-color-disabled: var(--picker-background-color);
86464
- --picker-icon-color: currentColor;
86465
- }
86466
-
86467
- &[data-variant="discrete"] {
86468
- --picker-border-width: 0px;
86469
- --picker-border-color: transparent;
86470
- --picker-border-color-hover: var(--picker-border-color);
86471
- --picker-border-color-readonly: var(--picker-border-color);
86472
- --picker-border-color-disabled: var(--picker-border-color);
86473
- --picker-background-color: transparent;
86474
- --picker-background-color-hover: color-mix(in srgb,
86475
- currentColor 8%,
86476
- var(--picker-background-color));
86477
- --picker-background-color-readonly: var(--picker-background-color);
86478
- --picker-background-color-disabled: var(--picker-background-color);
86479
- }
86480
-
86481
87487
  &[data-variant="headless"] {
86482
- --picker-padding-x-default: 0;
86483
- --picker-padding-y-default: 0;
86484
- --picker-border-width: 0px;
86485
- --picker-border-color: transparent;
86486
- --picker-outline-width: 0px;
86487
- --picker-border-color-hover: var(--picker-border-color);
86488
- --picker-border-color-readonly: var(--picker-border-color);
86489
- --picker-border-color-disabled: var(--picker-border-color);
86490
- --picker-background-color: transparent;
86491
- --picker-background-color-hover: var(--picker-background-color);
86492
- --picker-background-color-readonly: var(--picker-background-color);
86493
- --picker-background-color-disabled: var(--picker-background-color);
86494
- --picker-icon-color: currentColor;
86495
-
86496
87488
  & > .navi_picker_box {
86497
87489
  z-index: -1;
86498
87490
  position: absolute;
@@ -86501,23 +87493,6 @@ const css$r = /* css */`@layer navi {
86501
87493
  }
86502
87494
 
86503
87495
  &[data-variant="bare"] {
86504
- --picker-padding-x-default: 0;
86505
- --picker-padding-y-default: 0;
86506
- --picker-align-y-default: stretch;
86507
- --picker-border-width: 0px;
86508
- --picker-border-color: transparent;
86509
- --picker-border-color-hover: var(--picker-border-color);
86510
- --picker-border-color-readonly: var(--picker-border-color);
86511
- --picker-border-color-disabled: var(--picker-border-color);
86512
- --picker-background-color: transparent;
86513
- --picker-background-color-hover: var(--picker-background-color);
86514
- --picker-background-color-readonly: var(--picker-background-color);
86515
- --picker-background-color-disabled: var(--picker-background-color);
86516
- --picker-icon-color: currentColor;
86517
- font-size: inherit;
86518
- font-family: inherit;
86519
- line-height: inherit;
86520
-
86521
87496
  & > .navi_picker_box {
86522
87497
  min-height: 0;
86523
87498
  }
@@ -86552,39 +87527,8 @@ const css$r = /* css */`@layer navi {
86552
87527
  }
86553
87528
  }
86554
87529
 
86555
- &[data-variant="button"] {
86556
- --picker-padding-x-default: var(--navi-button-padding-x-default);
86557
- --picker-padding-y-default: var(--navi-button-padding-y-default);
86558
- --picker-align-x-default: center;
86559
- --picker-background-color: var(--navi-button-background-color);
86560
- --picker-border-color-readonly: color-mix(in srgb,
86561
- var(--picker-border-color) 30%,
86562
- var(--navi-surface-color));
86563
- --picker-background-color-readonly: var(--picker-background-color);
86564
- --picker-color-readonly: color-mix(in srgb,
86565
- var(--picker-color) 30%,
86566
- transparent);
86567
- --picker-border-color-disabled: var(--picker-border-color-readonly);
86568
- --picker-background-color-disabled: var(--picker-background-color-readonly);
86569
- --picker-color-disabled: var(--picker-color-readonly);
86570
- --picker-text-align-default: center;
86571
- }
86572
-
86573
87530
  &[data-variant="text"] {
86574
- --picker-padding-x-default: 0;
86575
- --picker-padding-y-default: 0;
86576
- --picker-border-width: 0px;
86577
- --picker-border-color: transparent;
86578
- --picker-border-color-hover: var(--picker-border-color);
86579
- --picker-border-color-readonly: var(--picker-border-color);
86580
- --picker-border-color-disabled: var(--picker-border-color);
86581
- --picker-background-color: transparent;
86582
- --picker-background-color-hover: var(--picker-background-color);
86583
- --picker-background-color-readonly: var(--picker-background-color);
86584
- --picker-background-color-disabled: var(--picker-background-color);
86585
- font-size: inherit;
86586
87531
  text-underline-offset: .2em;
86587
- font-family: inherit;
86588
87532
  text-decoration: underline dotted;
86589
87533
 
86590
87534
  &[data-hover] {
@@ -86593,10 +87537,6 @@ const css$r = /* css */`@layer navi {
86593
87537
  }
86594
87538
 
86595
87539
  &[data-icon] {
86596
- --picker-padding-x-default: 0;
86597
- --picker-padding-y-default: 0;
86598
- --picker-align-x-default: center;
86599
-
86600
87540
  & > .navi_picker_box {
86601
87541
  min-height: 0;
86602
87542
  }
@@ -87035,13 +87975,13 @@ const PickerButton = props => {
87035
87975
  * It goes when there is nothing to take out and when nothing can be changed, so
87036
87976
  * a caller places it once and never asks whether to draw it.
87037
87977
  *
87038
- * @type {import("ignore:preact").FunctionComponent<{
87978
+ * @type {import("preact").FunctionComponent<{
87039
87979
  * size?: number | string,
87040
- * children?: import("ignore:preact").ComponentChildren,
87980
+ * children?: import("preact").ComponentChildren,
87041
87981
  * } & Record<string, any>>}
87042
87982
  * @param {number|string} [size="inherit"] How big the cross is drawn, as
87043
87983
  * `<Icon size>` reads it — "inherit" takes the size of the text around it.
87044
- * @param {import("ignore:preact").ComponentChildren} [children] The icon, in place of
87984
+ * @param {import("preact").ComponentChildren} [children] The icon, in place of
87045
87985
  * the cross. Anything else goes to the `<Button>` underneath (`variant`,
87046
87986
  * `paddingX`, `aria-label`, …).
87047
87987
  */
@@ -87361,6 +88301,115 @@ const PickerFirstResolver = props => {
87361
88301
  });
87362
88302
  };
87363
88303
 
88304
+ // Every picker type is resolved into one of these before it gets here (see
88305
+ // PickerTypeResolver and resolveInputProps): a native input type, or navi_js
88306
+ // for the ones whose value is a whole object. Anything else is a type nobody
88307
+ // implements — the picker then behaves as a plain text one, silently, and an
88308
+ // object value reaches the DOM as "[object Object]".
88309
+ const PICKER_RESOLVED_TYPE_SET = new Set(["text", "search", "tel", "url", "email", "password", "number", "range", "date", "month", "week", "time", "datetime-local", "color", "file", "checkbox", "radio", "hidden", "navi_js"]);
88310
+ const pickerTypeWarnedSet = new Set();
88311
+ // A headless picker draws nothing at all — no value, no slot — so anything a
88312
+ // caller wrote for either is silently dropped. Said out loud, because a prop
88313
+ // that renders nowhere looks exactly like a prop that does not work.
88314
+ let uiDrawnByNobodyWarned = false;
88315
+ const warnOnUIDrawnByNobody = props => {
88316
+ if (uiDrawnByNobodyWarned) {
88317
+ return;
88318
+ }
88319
+ const {
88320
+ ui,
88321
+ variant
88322
+ } = props;
88323
+ if (variant !== "headless" || ui === undefined || ui === "default") {
88324
+ return;
88325
+ }
88326
+ if (pickerUIIsNaviOwn(ui)) {
88327
+ // What a `type` resolver puts there for a picker that did not name a
88328
+ // drawing (see picker_types.jsx). Nobody wrote it, so there is nobody to
88329
+ // tell that it is not drawn.
88330
+ return;
88331
+ }
88332
+ uiDrawnByNobodyWarned = true;
88333
+ console.warn(`[navi] <Picker variant="headless" ui={…}> — a headless picker draws nothing, so this "ui" is never rendered. ` + `For a trigger that is one icon of your own: <Picker variant="icon" ui={<YourSvg />}>.`);
88334
+ };
88335
+
88336
+ // `clearable` is "put the cross in the right slot", and a picker without one is
88337
+ // handed the prop and drops it. Said out loud, because a caller asking for a
88338
+ // cross and getting none has no way to tell "ignored here" from "broken".
88339
+ const clearableWithoutSlotWarnedSet = new Set();
88340
+ const warnOnClearableWithoutSlot = (props, hasRightSlot) => {
88341
+ const {
88342
+ clearable,
88343
+ variant,
88344
+ icon,
88345
+ rightSlot,
88346
+ picksNothing
88347
+ } = props;
88348
+ if (!clearable || hasRightSlot) {
88349
+ return;
88350
+ }
88351
+ // What took the slot away: what the message names, and what makes two
88352
+ // pickers the same case.
88353
+ const cause = picksNothing ? "picks-nothing" : rightSlot === null ? "rightSlot={null}" : icon ? "icon" : `variant="${variant}"`;
88354
+ if (clearableWithoutSlotWarnedSet.has(cause)) {
88355
+ return;
88356
+ }
88357
+ clearableWithoutSlotWarnedSet.add(cause);
88358
+ if (picksNothing) {
88359
+ console.warn(`[navi] <Picker type="confirm" clearable> — a picker that picks nothing holds nothing to clear, so no cross is drawn.`);
88360
+ return;
88361
+ }
88362
+ console.warn(`[navi] <Picker ${cause} clearable> — that leaves no right slot, so no cross is drawn. ` + `Draw it where it belongs instead: <Picker ${cause} ui={<YourDrawing>… <Picker.Clear /></YourDrawing>}>. ` + `It is the same cross, and it takes itself away when there is nothing left to clear.`);
88363
+ };
88364
+ let chevronOffBareWarned = false;
88365
+ const warnOnChevronOffBare = (props, isBare) => {
88366
+ const {
88367
+ chevron,
88368
+ variant
88369
+ } = props;
88370
+ if (chevron === undefined || isBare || chevronOffBareWarned) {
88371
+ return;
88372
+ }
88373
+ chevronOffBareWarned = true;
88374
+ console.warn(`[navi] <Picker variant="${variant}" chevron="${chevron}"> — \`chevron\` places the chevron of a bare picker, whose drawing has no slot beside it. ` + `This variant draws its chevron in its right slot already (rightSlotIcon, rightSlot to change it); \`chevron\` is ignored.`);
88375
+ };
88376
+ const warnOnUnknownPickerType = props => {
88377
+ const {
88378
+ type
88379
+ } = props;
88380
+ if (type === undefined || PICKER_RESOLVED_TYPE_SET.has(type)) {
88381
+ return;
88382
+ }
88383
+ if (pickerTypeWarnedSet.has(type)) {
88384
+ return;
88385
+ }
88386
+ pickerTypeWarnedSet.add(type);
88387
+ console.warn(`[navi] <Picker type="${type}"> — "${type}" is not a picker type. ` + `The picker holds a single text value instead, so an object given to it is written as "[object Object]". ` + `Types are: date, month, week, time, datetime, duration, color, file, text, array, object, confirm — ` + `"object" being the one for a popup holding several named controls.`);
88388
+ };
88389
+
88390
+ // PickerFirstResolver comes before the presets so a preset reads the
88391
+ // min/max/step a bound signal fills in (resolveInputProps); a preset type
88392
+ // therefore has no row in NAVI_TYPE_DEFAULTS (resolve_input_props.js).
88393
+ const PickerResolved = /*#__PURE__*/createComponentResolver([PickerFirstResolver, PickerPresetResolver, PickerConfirmResolver, PickerCustomResolver, PickerTypeResolver, PickerButton]);
88394
+ const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
88395
+ Date: PickerDateUI,
88396
+ Time: PickerTimeUI,
88397
+ Duration: PickerDurationUI,
88398
+ Week: PickerWeekUI,
88399
+ Datetime: PickerDatetimeUI,
88400
+ File: PickerFileUI,
88401
+ Color: PickerColorUI,
88402
+ Object: PickerObjectUI,
88403
+ Multiple: PickerArrayUI,
88404
+ PencilSvg,
88405
+ ChevronDownSvg: ChevronDownSvg$1,
88406
+ ClockSvg,
88407
+ DurationSvg,
88408
+ CalendarSvg,
88409
+ FileSvg,
88410
+ ColorSvg
88411
+ });
88412
+
87364
88413
  /**
87365
88414
  * Button-like trigger that opens a picker (native or custom popup) when clicked.
87366
88415
  *
@@ -87379,13 +88428,13 @@ const PickerFirstResolver = props => {
87379
88428
  * picker_confirm.jsx, and `message`, `confirmLabel`, `cancelLabel`,
87380
88429
  * `focusOnOpen` below.
87381
88430
  *
87382
- * @type {import("ignore:preact").FunctionComponent<{
88431
+ * @type {import("preact").FunctionComponent<{
87383
88432
  * type?: "date" | "month" | "week" | "time" | "datetime" | "duration" | "color" | "file" | "text" | "object" | "array" | "confirm" | "navi_time" | "navi_minute" | "navi_number" | "navi_percentage",
87384
88433
  * value?: any,
87385
88434
  * defaultValue?: any,
87386
88435
  * name?: string,
87387
- * placeholder?: import("ignore:preact").ComponentChildren,
87388
- * ui?: import("ignore:preact").ComponentChildren | "default" | import("ignore:preact").ComponentType<{ value: any, loading: boolean, interactive: boolean }>,
88436
+ * placeholder?: import("preact").ComponentChildren,
88437
+ * ui?: import("preact").ComponentChildren | "default" | import("preact").ComponentType<{ value: any, loading: boolean, interactive: boolean }>,
87389
88438
  * required?: boolean,
87390
88439
  * min?: Date | string | number,
87391
88440
  * max?: Date | string | number,
@@ -87395,8 +88444,8 @@ const PickerFirstResolver = props => {
87395
88444
  * openWhileReadOnly?: boolean,
87396
88445
  * error?: boolean | string,
87397
88446
  * uiAction?: (value: any, event: Event) => void,
87398
- * action?: (value: any, event: Event) => void,
87399
- * children?: import("ignore:preact").ComponentChildren,
88447
+ * action?: (value: any, { event, signal }: { event: Event, signal: AbortSignal }) => void,
88448
+ * children?: import("preact").ComponentChildren,
87400
88449
  * mode?: "popover" | "dialog" | "callout",
87401
88450
  * openOn?: "press" | "longpress" | "contextmenu" | string | string[],
87402
88451
  * animation?: boolean | "auto" | "fading" | "scaling" | "sliding" | "lifting" | `slide-from-${string}` | { open: boolean | "auto" | "fading" | "scaling" | "sliding" | `slide-from-${string}`, close: "lifting" },
@@ -87416,14 +88465,14 @@ const PickerFirstResolver = props => {
87416
88465
  * icon?: boolean,
87417
88466
  * alignX?: "start" | "center" | "end",
87418
88467
  * alignY?: "start" | "center" | "end" | "stretch",
87419
- * rightSlotIcon?: import("ignore:preact").ComponentChildren,
88468
+ * rightSlotIcon?: import("preact").ComponentChildren,
87420
88469
  * rightSlotIconSize?: number | string,
87421
- * rightSlot?: import("ignore:preact").ComponentChildren,
88470
+ * rightSlot?: import("preact").ComponentChildren,
87422
88471
  * chevron?: "end" | "start" | "hang-end",
87423
- * clearConfirm?: string | import("ignore:preact").ComponentChildren,
87424
- * message?: import("ignore:preact").ComponentChildren,
87425
- * confirmLabel?: import("ignore:preact").ComponentChildren,
87426
- * cancelLabel?: import("ignore:preact").ComponentChildren,
88472
+ * clearConfirm?: string | import("preact").ComponentChildren,
88473
+ * message?: import("preact").ComponentChildren,
88474
+ * confirmLabel?: import("preact").ComponentChildren,
88475
+ * cancelLabel?: import("preact").ComponentChildren,
87427
88476
  * focusOnOpen?: "confirm" | "cancel" | "none",
87428
88477
  * onConfirm?: (confirmEvent: CustomEvent) => void,
87429
88478
  * onOpen?: (openEvent: CustomEvent) => void,
@@ -87456,14 +88505,14 @@ const PickerFirstResolver = props => {
87456
88505
  * dialogSizeFromAnchor?: boolean,
87457
88506
  * dockedOnSmallTouchScreen?: boolean | "top" | "bottom",
87458
88507
  * marginWithContainer?: number | string,
87459
- * anchor?: import("ignore:preact").RefObject<HTMLElement> | HTMLElement,
88508
+ * anchor?: import("preact").RefObject<HTMLElement> | HTMLElement,
87460
88509
  * escapeEffect?: "cancel" | "close",
87461
88510
  * pressOutside?: "close" | "cancel" | "capture",
87462
88511
  * backdrop?: boolean,
87463
88512
  * backdropVariant?: "auto" | "lift" | "discrete" | "invisible",
87464
88513
  * backdropColor?: string,
87465
88514
  * backdropFilter?: string,
87466
- * ref?: import("ignore:preact").RefObject<HTMLElement>,
88515
+ * ref?: import("preact").RefObject<HTMLElement>,
87467
88516
  * [key: string]: any,
87468
88517
  * }>}
87469
88518
  * @param {string} [popupBackgroundColor] The popup's surface. Left out it is
@@ -87502,7 +88551,7 @@ const PickerFirstResolver = props => {
87502
88551
  * max_lines_context.js). `variant="text"` and `variant="bare"` are not
87503
88552
  * clamped: their drawing is the caller's own, a `Text` in the `ui` cuts
87504
88553
  * itself with its own `maxLines`. See docs/typography.md.
87505
- * @param {import("ignore:preact").ComponentChildren | "default" | import("ignore:preact").ComponentType} [ui] What the
88554
+ * @param {import("preact").ComponentChildren | "default" | import("preact").ComponentType} [ui] What the
87506
88555
  * trigger draws in place of the value's default rendering (a date, a list
87507
88556
  * joined by commas…) — and then all it draws: `placeholder` is not shown
87508
88557
  * next to it, and an empty value is not greyed as a placeholder either: what
@@ -87619,7 +88668,7 @@ const PickerFirstResolver = props => {
87619
88668
  * popover narrower than the trigger stays narrow. Popover mode only — a
87620
88669
  * dialog never takes the trigger's width to begin with (see
87621
88670
  * `dialogMinWidth`).
87622
- * @param {import("ignore:preact").ComponentChildren} [rightSlotIcon] What the right
88671
+ * @param {import("preact").ComponentChildren} [rightSlotIcon] What the right
87623
88672
  * slot draws in place of the chevron. It is the whole slot, not an addition
87624
88673
  * to it: the picker then no longer says on its own that it opens, so pass
87625
88674
  * something that does. Decoration only — it is wrapped in an `<Icon>`, which
@@ -87651,7 +88700,7 @@ const PickerFirstResolver = props => {
87651
88700
  * any `pressPaddingRight`. The chevron is drawn in `--picker-icon-color`
87652
88701
  * (`currentColor` here) and `--picker-icon-color-hover`; a dimmed one is
87653
88702
  * `--picker-icon-color: color-mix(in srgb, currentColor 55%, transparent)`.
87654
- * @param {import("ignore:preact").ComponentChildren} [rightSlot] Same place, rendered
88703
+ * @param {import("preact").ComponentChildren} [rightSlot] Same place, rendered
87655
88704
  * as-is: no `<Icon>` around it, nothing `aria-hidden`. This is where an
87656
88705
  * interactive right slot goes. `null` takes the slot away entirely — the
87657
88706
  * gutter it carries with it, so the trigger measures its drawing and centers
@@ -87669,7 +88718,7 @@ const PickerFirstResolver = props => {
87669
88718
  * opening it, the anchor being itself, and one popup per instance without
87670
88719
  * an id to wire. See "The trigger wears the wait" in docs/popup_open.md for
87671
88720
  * when that beats a `Button` opening a shared `Dialog`.
87672
- * @param {string|import("ignore:preact").ComponentChildren} [clearConfirm] The
88721
+ * @param {string|import("preact").ComponentChildren} [clearConfirm] The
87673
88722
  * question asked before the clear cross clears anything — the `message` of
87674
88723
  * the `<Picker type="confirm">` the cross then is, plain text or JSX. Asked
87675
88724
  * BEFORE the ui state is emptied, so answering no leaves the field exactly
@@ -87726,15 +88775,15 @@ const PickerFirstResolver = props => {
87726
88775
  * buttons. A shape, not a paint: the variant still decides what is drawn
87727
88776
  * around it, so `variant="discrete" icon` is the discrete hover wash on an
87728
88777
  * icon-sized square. `variant="icon"` is this shape with no surface at all.
87729
- * @param {import("ignore:preact").ComponentChildren} [message] `type="confirm"`: the
88778
+ * @param {import("preact").ComponentChildren} [message] `type="confirm"`: the
87730
88779
  * question, in the popup's default body — text, or JSX when it needs an
87731
88780
  * emphasis, a link. Left out, a generic "are you sure?" in the current
87732
88781
  * language. To draw the whole popup instead, pass `children`: yes is then
87733
88782
  * whatever carries `--navi-confirm`, no whatever closes (`--navi-cancel`).
87734
- * @param {import("ignore:preact").ComponentChildren} [confirmLabel] `type="confirm"`,
88783
+ * @param {import("preact").ComponentChildren} [confirmLabel] `type="confirm"`,
87735
88784
  * default body: what the yes button reads. Defaults to the translated
87736
88785
  * "Confirm".
87737
- * @param {import("ignore:preact").ComponentChildren} [cancelLabel] Same, for the no
88786
+ * @param {import("preact").ComponentChildren} [cancelLabel] Same, for the no
87738
88787
  * button. Defaults to the translated "Cancel".
87739
88788
  * @param {"confirm"|"cancel"|"none"} [focusOnOpen="confirm"] `type="confirm"`,
87740
88789
  * default body: which button the keyboard lands on when the popup opens.
@@ -87851,7 +88900,7 @@ const PickerFirstResolver = props => {
87851
88900
  * should go on meaning cancel, and
87852
88901
  * docs/popup_open.md#what-cancel-actually-undoes for what a cancel puts
87853
88902
  * back.
87854
- * @param {import("ignore:preact").RefObject<HTMLElement>|HTMLElement} [anchor] What
88903
+ * @param {import("preact").RefObject<HTMLElement>|HTMLElement} [anchor] What
87855
88904
  * the popup hangs off, when that is not the picker itself: a picker whose
87856
88905
  * trigger is one piece of a bigger control (the chevron half of a split
87857
88906
  * button) points at the whole control, so the popup lines up with it and is
@@ -87935,115 +88984,6 @@ const PickerFirstResolver = props => {
87935
88984
  * `type="confirm"`) never builds them early: its popup holds no value, there
87936
88985
  * is nothing to read.
87937
88986
  */
87938
- // Every picker type is resolved into one of these before it gets here (see
87939
- // PickerTypeResolver and resolveInputProps): a native input type, or navi_js
87940
- // for the ones whose value is a whole object. Anything else is a type nobody
87941
- // implements — the picker then behaves as a plain text one, silently, and an
87942
- // object value reaches the DOM as "[object Object]".
87943
- const PICKER_RESOLVED_TYPE_SET = new Set(["text", "search", "tel", "url", "email", "password", "number", "range", "date", "month", "week", "time", "datetime-local", "color", "file", "checkbox", "radio", "hidden", "navi_js"]);
87944
- const pickerTypeWarnedSet = new Set();
87945
- // A headless picker draws nothing at all — no value, no slot — so anything a
87946
- // caller wrote for either is silently dropped. Said out loud, because a prop
87947
- // that renders nowhere looks exactly like a prop that does not work.
87948
- let uiDrawnByNobodyWarned = false;
87949
- const warnOnUIDrawnByNobody = props => {
87950
- if (uiDrawnByNobodyWarned) {
87951
- return;
87952
- }
87953
- const {
87954
- ui,
87955
- variant
87956
- } = props;
87957
- if (variant !== "headless" || ui === undefined || ui === "default") {
87958
- return;
87959
- }
87960
- if (pickerUIIsNaviOwn(ui)) {
87961
- // What a `type` resolver puts there for a picker that did not name a
87962
- // drawing (see picker_types.jsx). Nobody wrote it, so there is nobody to
87963
- // tell that it is not drawn.
87964
- return;
87965
- }
87966
- uiDrawnByNobodyWarned = true;
87967
- console.warn(`[navi] <Picker variant="headless" ui={…}> — a headless picker draws nothing, so this "ui" is never rendered. ` + `For a trigger that is one icon of your own: <Picker variant="icon" ui={<YourSvg />}>.`);
87968
- };
87969
-
87970
- // `clearable` is "put the cross in the right slot", and a picker without one is
87971
- // handed the prop and drops it. Said out loud, because a caller asking for a
87972
- // cross and getting none has no way to tell "ignored here" from "broken".
87973
- const clearableWithoutSlotWarnedSet = new Set();
87974
- const warnOnClearableWithoutSlot = (props, hasRightSlot) => {
87975
- const {
87976
- clearable,
87977
- variant,
87978
- icon,
87979
- rightSlot,
87980
- picksNothing
87981
- } = props;
87982
- if (!clearable || hasRightSlot) {
87983
- return;
87984
- }
87985
- // What took the slot away: what the message names, and what makes two
87986
- // pickers the same case.
87987
- const cause = picksNothing ? "picks-nothing" : rightSlot === null ? "rightSlot={null}" : icon ? "icon" : `variant="${variant}"`;
87988
- if (clearableWithoutSlotWarnedSet.has(cause)) {
87989
- return;
87990
- }
87991
- clearableWithoutSlotWarnedSet.add(cause);
87992
- if (picksNothing) {
87993
- console.warn(`[navi] <Picker type="confirm" clearable> — a picker that picks nothing holds nothing to clear, so no cross is drawn.`);
87994
- return;
87995
- }
87996
- console.warn(`[navi] <Picker ${cause} clearable> — that leaves no right slot, so no cross is drawn. ` + `Draw it where it belongs instead: <Picker ${cause} ui={<YourDrawing>… <Picker.Clear /></YourDrawing>}>. ` + `It is the same cross, and it takes itself away when there is nothing left to clear.`);
87997
- };
87998
- let chevronOffBareWarned = false;
87999
- const warnOnChevronOffBare = (props, isBare) => {
88000
- const {
88001
- chevron,
88002
- variant
88003
- } = props;
88004
- if (chevron === undefined || isBare || chevronOffBareWarned) {
88005
- return;
88006
- }
88007
- chevronOffBareWarned = true;
88008
- console.warn(`[navi] <Picker variant="${variant}" chevron="${chevron}"> — \`chevron\` places the chevron of a bare picker, whose drawing has no slot beside it. ` + `This variant draws its chevron in its right slot already (rightSlotIcon, rightSlot to change it); \`chevron\` is ignored.`);
88009
- };
88010
- const warnOnUnknownPickerType = props => {
88011
- const {
88012
- type
88013
- } = props;
88014
- if (type === undefined || PICKER_RESOLVED_TYPE_SET.has(type)) {
88015
- return;
88016
- }
88017
- if (pickerTypeWarnedSet.has(type)) {
88018
- return;
88019
- }
88020
- pickerTypeWarnedSet.add(type);
88021
- console.warn(`[navi] <Picker type="${type}"> — "${type}" is not a picker type. ` + `The picker holds a single text value instead, so an object given to it is written as "[object Object]". ` + `Types are: date, month, week, time, datetime, duration, color, file, text, array, object, confirm — ` + `"object" being the one for a popup holding several named controls.`);
88022
- };
88023
-
88024
- // PickerFirstResolver comes before the presets so a preset reads the
88025
- // min/max/step a bound signal fills in (resolveInputProps); a preset type
88026
- // therefore has no row in NAVI_TYPE_DEFAULTS (resolve_input_props.js).
88027
- const PickerResolved = /*#__PURE__*/createComponentResolver([PickerFirstResolver, PickerPresetResolver, PickerConfirmResolver, PickerCustomResolver, PickerTypeResolver, PickerButton]);
88028
- const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
88029
- Date: PickerDateUI,
88030
- Time: PickerTimeUI,
88031
- Duration: PickerDurationUI,
88032
- Week: PickerWeekUI,
88033
- Datetime: PickerDatetimeUI,
88034
- File: PickerFileUI,
88035
- Color: PickerColorUI,
88036
- Object: PickerObjectUI,
88037
- Multiple: PickerArrayUI,
88038
- PencilSvg,
88039
- ChevronDownSvg: ChevronDownSvg$1,
88040
- ClockSvg,
88041
- DurationSvg,
88042
- CalendarSvg,
88043
- FileSvg,
88044
- ColorSvg
88045
- });
88046
-
88047
88987
  // Assembled in pure expressions rather than written onto Picker afterwards
88048
88988
  // (`Picker.Chip = PickerChip`): what createComponentResolver returns is an object
88049
88989
  // a bundler cannot see through, so a property assigned to it later is a side
@@ -88300,7 +89240,7 @@ const css$q = /* css */`@layer navi {
88300
89240
  const TRAVELS_NONE = [];
88301
89241
 
88302
89242
  /**
88303
- * @type {import("ignore:preact").FunctionComponent<{
89243
+ * @type {import("preact").FunctionComponent<{
88304
89244
  * value?: any,
88305
89245
  * defaultValue?: any,
88306
89246
  * signal?: import("@preact/signals").Signal<any>,
@@ -88316,7 +89256,7 @@ const TRAVELS_NONE = [];
88316
89256
  * fallbackValue?: any,
88317
89257
  * valueAtStep: (value: any, count: number) => any,
88318
89258
  * compareValues?: (a: any, b: any) => number,
88319
- * renderValue?: (value: any, context: { maxLines?: number }) => import("ignore:preact").ComponentChildren,
89259
+ * renderValue?: (value: any, context: { maxLines?: number }) => import("preact").ComponentChildren,
88320
89260
  * controlProps?: object,
88321
89261
  * duration?: number,
88322
89262
  * vertical?: boolean,
@@ -89103,14 +90043,14 @@ const renderValueDefault = value => String(value ?? "");
89103
90043
  * single `value`/`signal` like any other control. `TimeSpin` is that, for a
89104
90044
  * time of day.
89105
90045
  *
89106
- * @type {import("ignore:preact").FunctionComponent<{
90046
+ * @type {import("preact").FunctionComponent<{
89107
90047
  * name?: string,
89108
90048
  * value?: any,
89109
90049
  * defaultValue?: any,
89110
90050
  * signal?: import("@preact/signals").Signal<any>,
89111
90051
  * aggregateChildStates?: (childUIStateControllers: any[]) => any,
89112
90052
  * distributeChildUIState?: (groupState: any, childUIStateController: any) => any,
89113
- * children?: import("ignore:preact").ComponentChildren,
90053
+ * children?: import("preact").ComponentChildren,
89114
90054
  * [key: string]: any,
89115
90055
  * }>}
89116
90056
  * Everything a box takes is taken here too — `width`, `borderWidth`,
@@ -89213,7 +90153,7 @@ SpinGroup.Separator = SpinGroupSeparator;
89213
90153
  * above and below it by default — sideways they would be where the caret
89214
90154
  * moves — with the bigger number up top.
89215
90155
  *
89216
- * @type {import("ignore:preact").FunctionComponent<{
90156
+ * @type {import("preact").FunctionComponent<{
89217
90157
  * value?: number|string,
89218
90158
  * defaultValue?: number|string,
89219
90159
  * min?: number,
@@ -89318,7 +90258,7 @@ const numberAtStep = (value, count, {
89318
90258
  * A day one steps through: the date plus what it is to today when there is a
89319
90259
  * word for it, and a calendar behind it for the day that is far from here.
89320
90260
  *
89321
- * @type {import("ignore:preact").FunctionComponent<{
90261
+ * @type {import("preact").FunctionComponent<{
89322
90262
  * value?: string,
89323
90263
  * defaultValue?: string,
89324
90264
  * signal?: import("@preact/signals").Signal<string>,
@@ -89329,7 +90269,7 @@ const numberAtStep = (value, count, {
89329
90269
  * maxMessage?: any,
89330
90270
  * step?: number,
89331
90271
  * lang?: string,
89332
- * renderDay?: (day: string) => import("ignore:preact").ComponentChildren,
90272
+ * renderDay?: (day: string) => import("preact").ComponentChildren,
89333
90273
  * [key: string]: any,
89334
90274
  * }>}
89335
90275
  * @param {string} [value] The day shown, as "YYYY-MM-DD".
@@ -89340,7 +90280,7 @@ const numberAtStep = (value, count, {
89340
90280
  * @param {"long"|"short"|"numeric"} [format="long"] How the date is written.
89341
90281
  * Long by default, since this is a control one reads rather than a column
89342
90282
  * one scans; `short` where the room is not there.
89343
- * @param {(day: string) => import("ignore:preact").ComponentChildren} [renderDay] What
90283
+ * @param {(day: string) => import("preact").ComponentChildren} [renderDay] What
89344
90284
  * to write for a day. Defaults to the date plus what it is to today when
89345
90285
  * there is a word for it ("samedi 8 août (demain)"), since a day near now is
89346
90286
  * read as a distance from now before it is read as a date.
@@ -89457,7 +90397,7 @@ const HOUR_MAX = 23;
89457
90397
  const MINUTE_MAX = 59;
89458
90398
 
89459
90399
  /**
89460
- * @type {import("ignore:preact").FunctionComponent<{
90400
+ * @type {import("preact").FunctionComponent<{
89461
90401
  * name?: string,
89462
90402
  * value?: string,
89463
90403
  * defaultValue?: string,
@@ -89465,7 +90405,7 @@ const MINUTE_MAX = 59;
89465
90405
  * minuteStep?: number,
89466
90406
  * pad?: number,
89467
90407
  * loop?: boolean,
89468
- * separator?: import("ignore:preact").ComponentChildren,
90408
+ * separator?: import("preact").ComponentChildren,
89469
90409
  * hourLabel?: string,
89470
90410
  * minuteLabel?: string,
89471
90411
  * [key: string]: any,
@@ -89478,7 +90418,7 @@ const MINUTE_MAX = 59;
89478
90418
  * of a day to stop at. Say `loop={false}` for two ends one cannot step past.
89479
90419
  * @param {number} [pad=2] How many digits an hour and a minute are written on:
89480
90420
  * a clock says "07:00", never "7:0". Say `pad={0}` for the bare numbers.
89481
- * @param {import("ignore:preact").ComponentChildren} [separator] What is written
90421
+ * @param {import("preact").ComponentChildren} [separator] What is written
89482
90422
  * between the hours and the minutes. "h" in French, ":" elsewhere.
89483
90423
  * Everything a box takes is taken here too — `width`, `borderRadius`, `size`.
89484
90424
  */
@@ -89544,7 +90484,7 @@ const distributeTime$1 = (groupState, childUIStateController) => {
89544
90484
  };
89545
90485
 
89546
90486
  /**
89547
- * @type {import("ignore:preact").FunctionComponent<{
90487
+ * @type {import("preact").FunctionComponent<{
89548
90488
  * name?: string,
89549
90489
  * value?: { start?: string, end?: string },
89550
90490
  * defaultValue?: { start?: string, end?: string },
@@ -89554,13 +90494,13 @@ const distributeTime$1 = (groupState, childUIStateController) => {
89554
90494
  * timeProps?: object,
89555
90495
  * loop?: boolean,
89556
90496
  * size?: string,
89557
- * startLabel?: import("ignore:preact").ComponentChildren,
89558
- * endLabel?: import("ignore:preact").ComponentChildren,
90497
+ * startLabel?: import("preact").ComponentChildren,
90498
+ * endLabel?: import("preact").ComponentChildren,
89559
90499
  * [key: string]: any,
89560
90500
  * }>}
89561
90501
  * @param {{ start?: string, end?: string }} [value] The span shown, as two
89562
90502
  * "HH:MM".
89563
- * @param {import("ignore:preact").ComponentChildren} [startLabel] What is written
90503
+ * @param {import("preact").ComponentChildren} [startLabel] What is written
89564
90504
  * before the first time ("De"), and `endLabel` between the two ("à"). Say
89565
90505
  * `null` for neither.
89566
90506
  * @param {number} [minuteStep=1] How many minutes a press on a minute chevron
@@ -89657,7 +90597,7 @@ const css$p = /* css */`.navi_checkbox_group {
89657
90597
  *
89658
90598
  * See `<RadioGroup>` for the same shape holding a single value.
89659
90599
  *
89660
- * @type {import("ignore:preact").FunctionComponent<{
90600
+ * @type {import("preact").FunctionComponent<{
89661
90601
  * maxLength?: number,
89662
90602
  * maxLengthGuard?: number,
89663
90603
  * [key: string]: any,
@@ -89718,6 +90658,19 @@ const CheckboxGroupInterface = props => {
89718
90658
  });
89719
90659
  };
89720
90660
 
90661
+ /**
90662
+ * @type {import("preact").FunctionComponent<{
90663
+ * unit: string,
90664
+ * plural?: boolean,
90665
+ * format?: "long" | "short" | "narrow",
90666
+ * lang?: string,
90667
+ * label?: string,
90668
+ * size?: string,
90669
+ * sizeRatio?: number,
90670
+ * style?: import("preact").JSX.CSSProperties,
90671
+ * [key: string]: any,
90672
+ * }>}
90673
+ */
89721
90674
  const Unit = ({
89722
90675
  unit,
89723
90676
  plural,
@@ -90475,7 +91428,7 @@ const css$m = /* css */`.navi_input.navi_select {
90475
91428
  `;
90476
91429
 
90477
91430
  /**
90478
- * @type {import("ignore:preact").FunctionComponent<{
91431
+ * @type {import("preact").FunctionComponent<{
90479
91432
  * value?: string,
90480
91433
  * defaultValue?: string,
90481
91434
  * signal?: import("@preact/signals").Signal<string>,
@@ -90557,30 +91510,40 @@ const css$l = /* css */`.navi_split_button {
90557
91510
 
90558
91511
  & > .navi_group {
90559
91512
  flex: auto;
91513
+
91514
+ & > .navi_button {
91515
+ flex: auto;
91516
+ }
90560
91517
  }
90561
91518
  }
90562
91519
 
90563
91520
  .navi_split_button_menu {
91521
+ --border-width: 0px;
90564
91522
  display: flex;
90565
91523
  position: relative;
91524
+
91525
+ & > .navi_button {
91526
+ margin-left: calc(var(--border-width) * -1);
91527
+ }
90566
91528
  }
90567
91529
  `;
90568
91530
 
90569
91531
  /**
90570
- * @type {import("ignore:preact").FunctionComponent<{
90571
- * options: Array<{value: any, label: import("ignore:preact").ComponentChildren, [key: string]: any}>,
91532
+ * @type {import("preact").FunctionComponent<{
91533
+ * options: Array<{value: any, label: import("preact").ComponentChildren, [key: string]: any}>,
90572
91534
  * value?: any,
90573
91535
  * defaultValue?: any,
90574
- * label?: import("ignore:preact").ComponentChildren,
90575
- * action?: (value: any, event: Event) => void | Promise<void>,
91536
+ * label?: import("preact").ComponentChildren,
91537
+ * action?: (value: any, { event, signal }: { event: Event, signal: AbortSignal }) => void | Promise<void>,
90576
91538
  * command?: string,
90577
91539
  * commandFor?: string,
90578
91540
  * prefetch?: boolean,
90579
- * onValueChange?: (value: any, event: Event) => void,
91541
+ * onValueChange?: (value: any, { event }: { event: Event }) => void,
90580
91542
  * chooseEffect?: "select" | "run",
90581
91543
  * menuLabel?: string,
90582
- * menuIcon?: import("ignore:preact").ComponentChildren,
91544
+ * menuIcon?: import("preact").ComponentChildren,
90583
91545
  * menuIconSize?: number | string,
91546
+ * menuTestId?: string,
90584
91547
  * loading?: boolean,
90585
91548
  * readOnly?: boolean,
90586
91549
  * disabled?: boolean,
@@ -90616,19 +91579,21 @@ const css$l = /* css */`.navi_split_button {
90616
91579
  * popupTestId?: string,
90617
91580
  * [key: string]: any,
90618
91581
  * }>}
90619
- * @param {Array<{value: any, label: import("ignore:preact").ComponentChildren}>} options
91582
+ * @param {Array<{value: any, label: import("preact").ComponentChildren}>} options
90620
91583
  * The ways of doing the thing, in the order the menu lists them. Anything
90621
91584
  * else on an entry (`disabled`, `icon`, `padding`…) reaches its row.
90622
91585
  * @param {any} [value] Which entry the button stands for — the one it runs, and
90623
91586
  * the one it is named after. `defaultValue` is the uncontrolled form; without
90624
91587
  * either, the first entry.
90625
- * @param {import("ignore:preact").ComponentChildren} [label] What the button says, when
91588
+ * @param {import("preact").ComponentChildren} [label] What the button says, when
90626
91589
  * that is not the name of the entry it stands for — a button that keeps one
90627
91590
  * name whatever the menu does.
90628
- * @param {(value: any, event: Event) => void} [action] Do the thing, for the
90629
- * entry given. Pressed on the button it is the entry the button stands for;
90630
- * with `chooseEffect="run"` it is also the entry just chosen. Awaited: the
90631
- * whole split button is busy until it settles.
91591
+ * @param {(value: any, { event, signal }) => void} [action] Do the thing, for
91592
+ * the entry given. Pressed on the button it is the entry the button stands
91593
+ * for; with `chooseEffect="run"` it is also the entry just chosen. Run as a
91594
+ * navi action: the second argument is the run's (see docs/actions.md), the
91595
+ * press being its `event`. Awaited: the whole split button is busy until it
91596
+ * settles.
90632
91597
  * @param {string} [command] What a press on the button does once its `action`
90633
91598
  * is done, as on a `<Button>` (with `commandFor`, and `command-value` when
90634
91599
  * what the command is about is not the entry the button stands for). A
@@ -90642,12 +91607,14 @@ const css$l = /* css */`.navi_split_button {
90642
91607
  * menu of one-offs around a primary action.
90643
91608
  * @param {string} [menuLabel] What the chevron half is called, for a screen
90644
91609
  * reader and for the tooltip a long press raises.
90645
- * @param {import("ignore:preact").ComponentChildren} [menuIcon] What the chevron half
91610
+ * @param {import("preact").ComponentChildren} [menuIcon] What the chevron half
90646
91611
  * draws, in place of the chevron. It is the whole of what that half says, so
90647
91612
  * pass something that still reads as "there is more here" — and say what,
90648
91613
  * through `menuLabel`.
90649
91614
  * @param {number|string} [menuIconSize] How big that icon is drawn. Defaults to
90650
91615
  * the button's own font size.
91616
+ * @param {string} [menuTestId] The `data-testid` of the chevron half, which
91617
+ * navi builds and the caller has no other way to name.
90651
91618
  * @param {"popover"|"dialog"} [mode="popover"] What the menu is drawn as. A
90652
91619
  * picker left to itself turns into a dialog on a small screen; a split
90653
91620
  * button's menu stays hung off the button there, so this says popover unless
@@ -90668,8 +91635,12 @@ const css$l = /* css */`.navi_split_button {
90668
91635
  * `popupBorderRadius`, `animation`. See picker.jsx for what each one says.
90669
91636
  * An `aria-*` attribute describes the button and reaches the main half —
90670
91637
  * `aria-pressed` for a split button whose main half toggles a mode;
90671
- * `aria-hidden` excepted, which hides the whole control. Anything else lands
90672
- * on the split button's own box.
91638
+ * `aria-hidden` excepted, which hides the whole control. How the words are
91639
+ * written (`size`, `bold`, `lineHeight`, `noWrap`…) reaches both halves, and so
91640
+ * does vertical padding (`paddingY`, `paddingTop`, `paddingBottom`); the rest
91641
+ * of the padding is the main half's — the chevron half keeps its own, which
91642
+ * gives it its width. Anything else (`className`, `width`, margins, `data-*`)
91643
+ * lands on the split button's own box.
90673
91644
  */
90674
91645
  const SplitButton = props => {
90675
91646
  import.meta.css = [css$l, "@jsenv/navi/src/control/input/split_button.jsx"];
@@ -90688,6 +91659,7 @@ const SplitButton = props => {
90688
91659
  menuLabel = buttonI18n("button.more_actions"),
90689
91660
  menuIcon = jsx(ChevronDownSvg$1, {}),
90690
91661
  menuIconSize,
91662
+ menuTestId,
90691
91663
  loading,
90692
91664
  readOnly,
90693
91665
  disabled,
@@ -90707,11 +91679,13 @@ const SplitButton = props => {
90707
91679
  ...rest
90708
91680
  } = props;
90709
91681
  // Everything the popup answers to travels to the Picker; an aria-* describes
90710
- // the button a screen reader meets, which is the main half; everything else
90711
- // is the split button's own box (margins, width, data-*). Sorted by name
90712
- // rather than named one by one so a Picker popup prop is forwarded by adding
90713
- // it to that list, not by threading it through here.
90714
- const [popupProps, buttonAriaProps, boxProps] = sortRestProps(rest);
91682
+ // the button a screen reader meets, which is the main half; how the words are
91683
+ // written and how far they sit from the edge belong to the halves, since a
91684
+ // <button> inherits neither from the box around it; everything else is the
91685
+ // split button's own box (margins, width, data-*). Sorted by name rather
91686
+ // than named one by one so a Picker popup prop is forwarded by adding it to
91687
+ // that list, not by threading it through here.
91688
+ const [popupProps, buttonAriaProps, halfStyleProps, mainPaddingProps, boxProps] = sortRestProps(rest);
90715
91689
  // A split button is a control on the page, not a place one goes: its menu
90716
91690
  // hangs off it even on a phone, where a picker left to itself would decide a
90717
91691
  // small screen means a dialog. Passing mode="dialog" asks for that back.
@@ -90756,6 +91730,7 @@ const SplitButton = props => {
90756
91730
  borderColor,
90757
91731
  borderWidth,
90758
91732
  borderRadius,
91733
+ ...halfStyleProps,
90759
91734
  pressEffect,
90760
91735
  loading: loadingResolved,
90761
91736
  readOnly,
@@ -90765,7 +91740,7 @@ const SplitButton = props => {
90765
91740
  };
90766
91741
  return jsxs(Box, {
90767
91742
  ref: rootRef,
90768
- className: "navi_split_button",
91743
+ baseClassName: "navi_split_button",
90769
91744
  borderRadius: borderRadius,
90770
91745
  ...boxProps,
90771
91746
  children: [jsx(LoadingOutline, {
@@ -90782,6 +91757,7 @@ const SplitButton = props => {
90782
91757
  "command-value": commandValue,
90783
91758
  prefetch: prefetch,
90784
91759
  ...buttonAriaProps,
91760
+ ...mainPaddingProps,
90785
91761
  ...halfProps,
90786
91762
  children: label === undefined ? optionShown?.label : label
90787
91763
  }), jsxs("div", {
@@ -90791,6 +91767,7 @@ const SplitButton = props => {
90791
91767
  paddingX: "s",
90792
91768
  expandY: true,
90793
91769
  "aria-label": menuLabel,
91770
+ "data-testid": menuTestId,
90794
91771
  command: "--navi-open",
90795
91772
  commandFor: menuId,
90796
91773
  ...halfProps,
@@ -90870,9 +91847,16 @@ const SplitButton = props => {
90870
91847
  // a caller reaches all of them through the split button (see picker.jsx's JSDoc
90871
91848
  // for what each one says).
90872
91849
  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"]);
91850
+ // Vertical padding is said about both halves so they stay the same height; the
91851
+ // horizontal one is the main half's alone, the chevron half's own (paddingX="s")
91852
+ // being what gives it its width.
91853
+ const HALF_PADDING_PROP_SET = new Set(["paddingY", "paddingTop", "paddingBottom"]);
91854
+ const MAIN_PADDING_PROP_SET = new Set(["padding", "paddingX", "paddingLeft", "paddingRight"]);
90873
91855
  const sortRestProps = props => {
90874
91856
  const popupProps = {};
90875
91857
  const buttonAriaProps = {};
91858
+ const halfStyleProps = {};
91859
+ const mainPaddingProps = {};
90876
91860
  const boxProps = {};
90877
91861
  for (const key of Object.keys(props)) {
90878
91862
  if (POPUP_PROP_SET.has(key)) {
@@ -90881,11 +91865,15 @@ const sortRestProps = props => {
90881
91865
  // aria-hidden hides a subtree rather than describing a button: on the
90882
91866
  // main half alone it would leave the chevron half exposed.
90883
91867
  buttonAriaProps[key] = props[key];
91868
+ } else if (isTypoProp(key) || HALF_PADDING_PROP_SET.has(key)) {
91869
+ halfStyleProps[key] = props[key];
91870
+ } else if (MAIN_PADDING_PROP_SET.has(key)) {
91871
+ mainPaddingProps[key] = props[key];
90884
91872
  } else {
90885
91873
  boxProps[key] = props[key];
90886
91874
  }
90887
91875
  }
90888
- return [popupProps, buttonAriaProps, boxProps];
91876
+ return [popupProps, buttonAriaProps, halfStyleProps, mainPaddingProps, boxProps];
90889
91877
  };
90890
91878
 
90891
91879
  /**
@@ -92087,7 +93075,7 @@ const WheelGroupContext = createContext(null);
92087
93075
  * value, the value lives in one hidden input for the form. The visible rows are
92088
93076
  * presentational.
92089
93077
  *
92090
- * @type {import("ignore:preact").FunctionComponent<{
93078
+ * @type {import("preact").FunctionComponent<{
92091
93079
  * value?: any,
92092
93080
  * defaultValue?: any,
92093
93081
  * action?: (value: any) => void,
@@ -92105,7 +93093,7 @@ const WheelGroupContext = createContext(null);
92105
93093
  * readOnly?: boolean,
92106
93094
  * disabled?: boolean,
92107
93095
  * loading?: boolean,
92108
- * children?: import("ignore:preact").ComponentChildren,
93096
+ * children?: import("preact").ComponentChildren,
92109
93097
  * [key: string]: any,
92110
93098
  * }>}
92111
93099
  * @param {number} [props.visibleCount=3] - Odd number of rows visible in the viewport (the center one is the selection).
@@ -92734,8 +93722,9 @@ function WheelUI(props) {
92734
93722
  }, {
92735
93723
  controlType: "input",
92736
93724
  // The host is the hidden input carrying the value: the container is
92737
- // what is focused and spun.
92738
- listenersOn: "root"
93725
+ // what is focused and spun, and the spinbutton its name has to name.
93726
+ listenersOn: "root",
93727
+ nameOn: "root"
92739
93728
  });
92740
93729
  const uiStateController = getUIStateControllerById(controlHostProps.id);
92741
93730
  const {
@@ -93592,9 +94581,9 @@ const WheelWindow = ({
93592
94581
  * of rows and fills each from this tracked data — so the value list is the source
93593
94582
  * of truth, not a per-item DOM node. Selection lives on the wheel's value.
93594
94583
  *
93595
- * @type {import("ignore:preact").FunctionComponent<{
94584
+ * @type {import("preact").FunctionComponent<{
93596
94585
  * value: any,
93597
- * children?: import("ignore:preact").ComponentChildren,
94586
+ * children?: import("preact").ComponentChildren,
93598
94587
  * [key: string]: any,
93599
94588
  * }>}
93600
94589
  */
@@ -93629,9 +94618,9 @@ Wheel.Item = WheelItem;
93629
94618
  * each Wheel.Item (it's a Box: paddingX="0.5ch") so the gap stays scrollable.
93630
94619
  * `spacing` is just the Box flex gap between children (a layout gap, not padding).
93631
94620
  *
93632
- * @type {import("ignore:preact").FunctionComponent<{
94621
+ * @type {import("preact").FunctionComponent<{
93633
94622
  * horizontal?: boolean,
93634
- * children?: import("ignore:preact").ComponentChildren,
94623
+ * children?: import("preact").ComponentChildren,
93635
94624
  * [key: string]: any,
93636
94625
  * }>}
93637
94626
  * @param {boolean} [props.horizontal] - Stack the (horizontal) wheels vertically instead of in a row.
@@ -93828,14 +94817,14 @@ const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
93828
94817
  const END_OF_DAY = END_OF_DAY_HOUR * MINUTES_PER_HOUR;
93829
94818
 
93830
94819
  /**
93831
- * @type {import("ignore:preact").FunctionComponent<{
94820
+ * @type {import("preact").FunctionComponent<{
93832
94821
  * name?: string,
93833
94822
  * value?: string,
93834
94823
  * defaultValue?: string,
93835
94824
  * signal?: import("@preact/signals").Signal<string>,
93836
94825
  * minuteStep?: number,
93837
94826
  * loop?: boolean,
93838
- * separator?: import("ignore:preact").ComponentChildren,
94827
+ * separator?: import("preact").ComponentChildren,
93839
94828
  * hourLabel?: string,
93840
94829
  * minuteLabel?: string,
93841
94830
  * size?: string,
@@ -93856,7 +94845,7 @@ const END_OF_DAY = END_OF_DAY_HOUR * MINUTES_PER_HOUR;
93856
94845
  * @param {boolean} [loop=true] The wheels go round: 23h then 0h, 59 minutes
93857
94846
  * then 0. What a clock does. Say `loop={false}` for two ends one cannot turn
93858
94847
  * past.
93859
- * @param {import("ignore:preact").ComponentChildren} [separator] What is written
94848
+ * @param {import("preact").ComponentChildren} [separator] What is written
93860
94849
  * between the hours and the minutes. "h" in French, ":" elsewhere. In a time
93861
94850
  * of whole hours it follows each hour on its row instead ("8h"), and nothing
93862
94851
  * is written by default outside French.
@@ -93975,7 +94964,7 @@ const TimeWheel = ({
93975
94964
  };
93976
94965
 
93977
94966
  /**
93978
- * @type {import("ignore:preact").FunctionComponent<{
94967
+ * @type {import("preact").FunctionComponent<{
93979
94968
  * name?: string,
93980
94969
  * value?: { start?: string, end?: string },
93981
94970
  * defaultValue?: { start?: string, end?: string },
@@ -93984,15 +94973,15 @@ const TimeWheel = ({
93984
94973
  * minDuration?: number,
93985
94974
  * loop?: boolean,
93986
94975
  * size?: string,
93987
- * startLabel?: import("ignore:preact").ComponentChildren,
93988
- * endLabel?: import("ignore:preact").ComponentChildren,
94976
+ * startLabel?: import("preact").ComponentChildren,
94977
+ * endLabel?: import("preact").ComponentChildren,
93989
94978
  * labelPosition?: "before" | "above",
93990
94979
  * timeProps?: object,
93991
94980
  * [key: string]: any,
93992
94981
  * }>}
93993
94982
  * @param {{ start?: string, end?: string }} [value] The span shown, as two
93994
94983
  * "HH:MM".
93995
- * @param {import("ignore:preact").ComponentChildren} [startLabel] What is written
94984
+ * @param {import("preact").ComponentChildren} [startLabel] What is written
93996
94985
  * before the first time ("De"), and `endLabel` between the two ("à"). Say
93997
94986
  * `null` for neither.
93998
94987
  * @param {"before"|"above"} [labelPosition="before"] Where each label stands.
@@ -97977,17 +98966,25 @@ const Address = ({
97977
98966
  };
97978
98967
 
97979
98968
  installImportMetaCssBuild(import.meta);
97980
- const css$a = /* css */`@layer navi;
98969
+ const css$a = /* css */`@layer navi {
98970
+ .navi_text.navi_badge_count {
98971
+ --font-size: .7em;
98972
+ --badge-count-padding-x-default: .5em;
98973
+ --badge-count-padding-y-default: .3em;
98974
+
98975
+ &[data-ellipse][data-single-char], &[data-circle] {
98976
+ --badge-count-padding-x-default: 0;
98977
+ --badge-count-padding-y-default: 0;
98978
+ }
98979
+ }
98980
+ }
97981
98981
 
97982
98982
  .navi_text.navi_badge_count {
97983
98983
  white-space: nowrap;
97984
- --font-size: .7em;
97985
98984
  --x-background: var(--background);
97986
- --x-background-color: var(--background-color, var(--x-background));
98985
+ --x-background-color: var(--background-color);
97987
98986
  --x-color-contrasting: var(--navi-color-white);
97988
98987
  --x-color: var(--color, var(--x-color-contrasting));
97989
- --badge-count-padding-x-default: .5em;
97990
- --badge-count-padding-y-default: .3em;
97991
98988
  --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))));
97992
98989
  --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))));
97993
98990
  --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))));
@@ -98019,12 +99016,9 @@ const css$a = /* css */`@layer navi;
98019
99016
  padding-bottom: var(--x-badge-count-padding-bottom);
98020
99017
  padding-left: var(--x-badge-count-padding-left);
98021
99018
  background: var(--x-background);
98022
- background-color: var(--x-background-color);
98023
99019
  border-radius: 1em;
98024
99020
 
98025
99021
  &[data-single-char] {
98026
- --badge-count-padding-x-default: 0;
98027
- --badge-count-padding-y-default: 0;
98028
99022
  box-sizing: content-box;
98029
99023
  text-align: center;
98030
99024
  border-radius: 100em;
@@ -98037,8 +99031,6 @@ const css$a = /* css */`@layer navi;
98037
99031
 
98038
99032
  &[data-circle] {
98039
99033
  --x-number-font-size: var(--font-size);
98040
- --badge-count-padding-x-default: 0;
98041
- --badge-count-padding-y-default: 0;
98042
99034
  box-sizing: content-box;
98043
99035
  aspect-ratio: 1;
98044
99036
  width: var(--x-radius);
@@ -98048,7 +99040,6 @@ const css$a = /* css */`@layer navi;
98048
99040
  padding-bottom: var(--x-badge-count-padding-bottom);
98049
99041
  padding-left: var(--x-badge-count-padding-left);
98050
99042
  background: var(--x-background);
98051
- background-color: var(--x-background-color);
98052
99043
  border-radius: 100em;
98053
99044
  justify-content: center;
98054
99045
  align-items: center;
@@ -98073,6 +99064,10 @@ const css$a = /* css */`@layer navi;
98073
99064
  font-size: var(--x-number-font-size);
98074
99065
  }
98075
99066
  }
99067
+
99068
+ &[data-background-color] {
99069
+ background-color: var(--x-background-color);
99070
+ }
98076
99071
  }
98077
99072
  `;
98078
99073
  const BadgeCountOverflow = () => jsx("span", {
@@ -98209,6 +99204,7 @@ const BadgeCountEllipse = ({
98209
99204
  "data-ellipse": "",
98210
99205
  "data-value-overflow": hasOverflow ? "" : undefined,
98211
99206
  "data-loading": loading ? "" : undefined,
99207
+ "data-background-color": props.backgroundColor === undefined ? undefined : "",
98212
99208
  "data-single-char": charCount === 1 ? "" : undefined,
98213
99209
  ...props,
98214
99210
  styleCSSVars: BadgeCountStyleCSSVars,
@@ -98231,6 +99227,7 @@ const BadgeCountCircle = ({
98231
99227
  "data-circle": "",
98232
99228
  bold: true,
98233
99229
  "data-loading": loading ? "" : undefined,
99230
+ "data-background-color": props.backgroundColor === undefined ? undefined : "",
98234
99231
  "data-single-char": charCount === 1 ? "" : undefined,
98235
99232
  "data-two-chars": charCount === 2 ? "" : undefined,
98236
99233
  "data-three-chars": charCount === 3 ? "" : undefined,
@@ -98723,7 +99720,7 @@ const css$8 = /* css */`@layer navi;
98723
99720
  `;
98724
99721
 
98725
99722
  /**
98726
- * @type {import("ignore:preact").FunctionComponent<{
99723
+ * @type {import("preact").FunctionComponent<{
98727
99724
  * children?: number | string,
98728
99725
  * unit?: string | [string, string],
98729
99726
  * unitPosition?: "right" | "bottom",
@@ -99315,8 +100312,8 @@ const css$4 = /* css */`.svg_mask_content * {
99315
100312
  *
99316
100313
  * @param {Object} props - Component properties
99317
100314
  * @param {string} props.viewBox - The main viewBox for the composition (required as soon as there is an overlay)
99318
- * @param {import("ignore:preact").ComponentChildren} props.children - SVG elements (first is base, rest are overlays)
99319
- * @returns {import("ignore:preact").VNode} A composed SVG with all elements properly masked
100315
+ * @param {import("preact").ComponentChildren} props.children - SVG elements (first is base, rest are overlays)
100316
+ * @returns {import("preact").VNode} A composed SVG with all elements properly masked
99320
100317
  */
99321
100318
  const SVGMaskOverlay = ({
99322
100319
  viewBox,
@@ -99406,7 +100403,6 @@ const css$3 = /* css */`@layer navi {
99406
100403
  padding-bottom: var(--layout-padding-bottom, var(--layout-padding-y, var(--layout-padding)));
99407
100404
  padding-left: var(--layout-padding-left, var(--layout-padding-x, var(--layout-padding)));
99408
100405
  background: var(--layout-background);
99409
- background-color: var(--layout-background-color, var(--layout-background));
99410
100406
  border-width: var(--layout-border-width);
99411
100407
  border-style: solid;
99412
100408
  border-color: var(--layout-border-color);
@@ -99423,7 +100419,6 @@ const CardLayoutStyleCSSVars = {
99423
100419
  borderWidth: "--layout-border-width",
99424
100420
  borderColor: "--layout-border-color",
99425
100421
  background: "--layout-background",
99426
- backgroundColor: "--layout-background-color",
99427
100422
  padding: "--layout-padding",
99428
100423
  paddingTop: "--layout-padding-top",
99429
100424
  paddingBottom: "--layout-padding-bottom",
@@ -99438,7 +100433,7 @@ const CardLayoutStyleCSSVars = {
99438
100433
  * what holds it, so how tall that area is is up to the page around it, as for
99439
100434
  * `ViewportLayout`.
99440
100435
  *
99441
- * @type {import("ignore:preact").FunctionComponent<{
100436
+ * @type {import("preact").FunctionComponent<{
99442
100437
  * alignX?: string,
99443
100438
  * alignY?: string,
99444
100439
  * margin?: string|number,
@@ -99463,6 +100458,7 @@ const CardLayout = ({
99463
100458
  children,
99464
100459
  alignX = "center",
99465
100460
  alignY = "center",
100461
+ backgroundColor,
99466
100462
  ...props
99467
100463
  }) => {
99468
100464
  import.meta.css = [css$3, "@jsenv/navi/src/layout/card_layout.jsx"];
@@ -99481,6 +100477,7 @@ const CardLayout = ({
99481
100477
  className: "navi_card",
99482
100478
  alignX: alignX,
99483
100479
  alignY: alignY,
100480
+ backgroundColor: backgroundColor,
99484
100481
  children: children
99485
100482
  })
99486
100483
  });
@@ -99654,7 +100651,7 @@ const LINE_GAP = 5;
99654
100651
  const StepListContext = createContext(null);
99655
100652
 
99656
100653
  /**
99657
- * @type {import("ignore:preact").FunctionComponent<{
100654
+ * @type {import("preact").FunctionComponent<{
99658
100655
  * current?: string,
99659
100656
  * slideContainer?: string,
99660
100657
  * travelByClick?: boolean,
@@ -100011,7 +101008,7 @@ const sameShallow = (previousObject, nextObject) => {
100011
101008
  * It is both StepList.Item and an export of its own, the way Slide is to
100012
101009
  * SlideContainer.
100013
101010
  *
100014
- * @type {import("ignore:preact").FunctionComponent<{
101011
+ * @type {import("preact").FunctionComponent<{
100015
101012
  * value: string,
100016
101013
  * done?: boolean,
100017
101014
  * [key: string]: any,
@@ -100073,7 +101070,7 @@ const ViewportLayoutStyleCSSVars = {
100073
101070
  * same inside a route, a preview pane or a demo box, and it is up to the page
100074
101071
  * around it to be full-height.
100075
101072
  *
100076
- * @type {import("ignore:preact").FunctionComponent<{
101073
+ * @type {import("preact").FunctionComponent<{
100077
101074
  * padding?: string|number,
100078
101075
  * paddingTop?: string|number,
100079
101076
  * paddingBottom?: string|number,
@@ -100298,7 +101295,7 @@ const css = /* css */`.navi_side_panel {
100298
101295
  * `closeByPressOutside`/`pressOutside` only say what a
100299
101296
  * press on the wall does, not whether there is one (see `dialog.jsx`'s own
100300
101297
  * doc).
100301
- * @param {import("ignore:preact").ComponentChildren} props.children - No built-in
101298
+ * @param {import("preact").ComponentChildren} props.children - No built-in
100302
101299
  * close button — add one wherever it makes sense for the layout (e.g. a
100303
101300
  * plain `<Button command="--navi-close">` in `SidePanel.Head`), or rely on
100304
101301
  * `closeByPressOutside`/Escape instead.
@@ -100397,7 +101394,7 @@ const toCssLength = (value, propertyName) => value === undefined || value === nu
100397
101394
  * @param {object} props
100398
101395
  * @param {string} [props.className] - Merged with the shared
100399
101396
  * `"navi_side_panel_head"` class this file's own CSS targets.
100400
- * @param {import("ignore:preact").ComponentChildren} props.children
101397
+ * @param {import("preact").ComponentChildren} props.children
100401
101398
  */
100402
101399
  const SidePanelHead = props => jsx(Box, {
100403
101400
  baseClassName: "navi_side_panel_head",
@@ -100410,7 +101407,7 @@ const SidePanelHead = props => jsx(Box, {
100410
101407
  * @param {object} props
100411
101408
  * @param {string} [props.className] - Merged with the shared
100412
101409
  * `"navi_side_panel_foot"` class this file's own CSS targets.
100413
- * @param {import("ignore:preact").ComponentChildren} props.children
101410
+ * @param {import("preact").ComponentChildren} props.children
100414
101411
  */
100415
101412
  const SidePanelFoot = props => jsx(Box, {
100416
101413
  className: "navi_side_panel_foot",
@@ -100419,6 +101416,58 @@ const SidePanelFoot = props => jsx(Box, {
100419
101416
  SidePanel.Head = SidePanelHead;
100420
101417
  SidePanel.Foot = SidePanelFoot;
100421
101418
 
101419
+ /**
101420
+ * Creates a linked `[Slot, SlotFill]` pair so content rendered anywhere in
101421
+ * the tree (`SlotFill`) appears at a fixed location elsewhere (`Slot`) — a
101422
+ * lightweight, signal-based alternative to a DOM portal: `SlotFill` itself
101423
+ * renders nothing (no extra DOM node at its own call site), it just writes
101424
+ * its props into a shared signal that `Slot` reads reactively.
101425
+ *
101426
+ * Holds at most one filler at a time — there is no stacking/queueing. The
101427
+ * filler is the last `SlotFill` (from this same `createSlot()` call) to
101428
+ * mount, or the first to render into an empty slot; later renders of a
101429
+ * `SlotFill` that is not the filler leave the slot alone. A `SlotFill`
101430
+ * unmounting empties the slot only while it is still that one, and so does one
101431
+ * in a page a route transition keeps while leaving (see keepLeavingPages in
101432
+ * nav/route.jsx): still mounted, hidden, it must neither stay in the slot nor
101433
+ * take it back from the page arriving.
101434
+ *
101435
+ * **One `SlotFill`, rendered where the choice is made.** `isFilled` follows
101436
+ * the fills as they mount and unmount, in the order the tree walks them. A
101437
+ * slot fed from several places — a `SlotFill` in each card of a board, each
101438
+ * card deciding on its own whether it is the open one — reads as empty
101439
+ * between the leaver and the newcomer whenever they render separately, and
101440
+ * anything acting on `isFilled` acts on that empty frame (a `SidePanel` with
101441
+ * `open={isFilled}` closes for good: its `onClose` clears the state the
101442
+ * newcomer was waiting for). The slot cannot tell that gap from a real
101443
+ * emptying, and does not try to: render one `SlotFill` at the level that
101444
+ * holds the choice, with the chosen content as its children, and bind a
101445
+ * state that must not blink (a panel's `open`) to the app's own signal
101446
+ * rather than to `isFilled` — see docs/popup_open.md, "One panel, fed by a
101447
+ * slot".
101448
+ *
101449
+ * `Slot` keeps `SlotRenderer` mounted permanently — even while unfilled it
101450
+ * still renders it, with no props and `isFilled={false}` — instead of
101451
+ * unmounting it. This lets `SlotRenderer` be a persistent wrapper (a
101452
+ * `SidePanel` that opens and closes with an animation instead of being
101453
+ * mounted/unmounted alongside the filler — see 7_slot_demo.html's "Two
101454
+ * side-panel slots" section). A `SlotRenderer` that wants the
101455
+ * render-nothing-when-unfilled behavior can opt in with its own
101456
+ * `if (!isFilled) return null;` (the default `Box` renderer doesn't do
101457
+ * this — an unfilled default slot just renders an empty `<Box/>`).
101458
+ *
101459
+ * @param {import("preact").ComponentType} [SlotRenderer=Box] - Rendered by
101460
+ * `Slot`, always, with whatever props `SlotFill` last set (spread
101461
+ * directly) plus `isFilled`. Swap this out to reuse the same slot
101462
+ * mechanism for something other than a plain `Box` (e.g. a specific
101463
+ * component expecting its own particular props, or a persistent wrapper
101464
+ * like `SidePanel`).
101465
+ * @returns {[import("preact").ComponentType, import("preact").ComponentType<object>]}
101466
+ * `[Slot, SlotFill]` — `Slot` takes no props, render it once wherever the
101467
+ * content should appear. `SlotFill` takes whatever props `SlotRenderer`
101468
+ * expects, render it anywhere else in the tree to supply/update that
101469
+ * content.
101470
+ */
100422
101471
  const createSlot = (SlotRenderer = Box) => {
100423
101472
  const slotPropsSignal = signal();
100424
101473
  let filler = null;