@jsenv/navi 0.29.102 → 0.29.104

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.
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
6
6
  export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
7
- import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, clickIsSuppressed, isTouchDrivenEvent, scrollIntoViewScoped, scrollRoomTowards, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, findBefore, findAfter, initFocusGroup, stringifyStyle as stringifyStyle$1, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
7
+ import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, hasCSSSizeUnit, resolveOklchLightness, contrastColor, clickIsSuppressed, isTouchDrivenEvent, scrollIntoViewScoped, scrollRoomTowards, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, findBefore, findAfter, initFocusGroup, stringifyStyle as stringifyStyle$1, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
8
8
  export { clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
9
9
  import { signal, computed, effect, batch, untracked, useSignal } from "@preact/signals";
10
10
  import { createContext, isValidElement, h, Fragment, render, toChildArray, options, cloneElement } from "preact";
@@ -38,7 +38,7 @@ installImportMetaCssBuild(import.meta);/**
38
38
  * any of these, and a number is the last resort, not the first tool.
39
39
  */
40
40
 
41
- const css$15 = /* css */`
41
+ const css$14 = /* css */`
42
42
  @layer navi {
43
43
  :root {
44
44
  /* A control that overlaps its neighbours (the members of a Group share
@@ -102,7 +102,7 @@ const css$15 = /* css */`
102
102
  }
103
103
  }
104
104
  `;
105
- import.meta.css = [css$15, "@jsenv/navi/src/navi_z_indexes.js"];
105
+ import.meta.css = [css$14, "@jsenv/navi/src/navi_z_indexes.js"];
106
106
 
107
107
  const addIntoArray = (array, ...valuesToAdd) => {
108
108
  if (valuesToAdd.length === 1) {
@@ -371,7 +371,7 @@ installImportMetaCssBuild(import.meta);/**
371
371
  * the very first render and the browser does everything on its own.
372
372
  */
373
373
  const URL_TARGET_ATTRIBUTE = "data-url-target";
374
- const css$14 = /* css */`
374
+ const css$13 = /* css */`
375
375
  @layer navi {
376
376
  [${URL_TARGET_ATTRIBUTE}] {
377
377
  animation: navi_url_target var(--navi-url-target-duration, 2000ms)
@@ -389,7 +389,7 @@ const css$14 = /* css */`
389
389
  }
390
390
  }
391
391
  `;
392
- import.meta.css = [css$14, "@jsenv/navi/src/nav/url_target/url_target.js"];
392
+ import.meta.css = [css$13, "@jsenv/navi/src/nav/url_target/url_target.js"];
393
393
  let urlTargetOptions = {
394
394
  block: "start",
395
395
  behavior: "instant",
@@ -1339,6 +1339,10 @@ naviI18n.addAll({
1339
1339
  en: "More actions",
1340
1340
  fr: "Autres actions",
1341
1341
  },
1342
+ "button.remove": {
1343
+ en: "Remove",
1344
+ fr: "Retirer",
1345
+ },
1342
1346
  });
1343
1347
 
1344
1348
  // Default built-in translations — apps can override any key via add()
@@ -1638,6 +1642,14 @@ naviI18n.addAll({
1638
1642
  fr: "Cette option n'est pas disponible.",
1639
1643
  en: "This option is not available.",
1640
1644
  },
1645
+ "constraint.readonly.selection": {
1646
+ fr: "La sélection ne peut plus être modifiée.",
1647
+ en: "This selection cannot be changed.",
1648
+ },
1649
+ "constraint.readonly.choice": {
1650
+ fr: "Ce choix ne peut plus être changé.",
1651
+ en: "This choice cannot be changed.",
1652
+ },
1641
1653
  "constraint.readonly.item": {
1642
1654
  fr: "Cet élément n'est pas disponible.",
1643
1655
  en: "This item is not available.",
@@ -2114,6 +2126,302 @@ naviI18n.addAll({
2114
2126
 
2115
2127
  const FormContext = createContext();
2116
2128
 
2129
+ const CONSTRAINT_ATTRIBUTE_SET = new Set();
2130
+
2131
+ const CONSTRAINT_NAME_TO_PROP = {
2132
+ disabled: "disabledMessage",
2133
+ required: "requiredMessage",
2134
+ pattern: "patternMessage",
2135
+ type_email: "typeMessage",
2136
+ type_number: "typeMessage",
2137
+ min_length: "minLengthMessage",
2138
+ max_length: "maxLengthMessage",
2139
+ min: "minMessage",
2140
+ max: "maxMessage",
2141
+ single_space: "singleSpaceMessage",
2142
+ same_as: "sameAsMessage",
2143
+ min_lower_letter: "minLowerLetterMessage",
2144
+ min_upper_letter: "minUpperLetterMessage",
2145
+ min_digit: "minDigitMessage",
2146
+ min_special_char: "minSpecialCharMessage",
2147
+ one_of: "oneOfMessage",
2148
+ readonly: "readOnlyMessage",
2149
+ available: "availableMessage",
2150
+ };
2151
+
2152
+ const CONSTRAINT_MESSAGE_PROP_NAME_SET = new Set(
2153
+ Object.values(CONSTRAINT_NAME_TO_PROP),
2154
+ );
2155
+
2156
+ const extractMessageAndRemainingProps = (props) => {
2157
+ const ownMessages = {};
2158
+ const remaining = {};
2159
+ const keyToVisit = new Set(Object.keys(props));
2160
+ for (const key of keyToVisit) {
2161
+ if (CONSTRAINT_MESSAGE_PROP_NAME_SET.has(key)) {
2162
+ ownMessages[key] = props[key];
2163
+ } else {
2164
+ remaining[key] = props[key];
2165
+ }
2166
+ }
2167
+ return [ownMessages, remaining];
2168
+ };
2169
+
2170
+ const getConstraintMessage = (
2171
+ controller,
2172
+ constraint,
2173
+ generatedMessage,
2174
+ { requester },
2175
+ ) => {
2176
+ const { name: constraintName } = constraint;
2177
+ const propName = CONSTRAINT_NAME_TO_PROP[constraintName];
2178
+
2179
+ // 1. Search first on the requester (e.g. the <li> that was clicked),
2180
+ // then fall back to element (e.g. the hidden <input>).
2181
+ if (requester) {
2182
+ const requesterController = requester.__uiStateController__;
2183
+ if (requesterController && requesterController !== controller) {
2184
+ const requesterControllerMessage = requesterController.props[propName];
2185
+ if (requesterControllerMessage) {
2186
+ return {
2187
+ message: requesterControllerMessage,
2188
+ origin: "requester controller",
2189
+ };
2190
+ }
2191
+ }
2192
+ }
2193
+
2194
+ const controllerMessage = controller.props[propName];
2195
+ if (controllerMessage) {
2196
+ return {
2197
+ message: controllerMessage,
2198
+ origin: "controller",
2199
+ };
2200
+ }
2201
+
2202
+ return {
2203
+ message: generatedMessage,
2204
+ origin: "generated message",
2205
+ };
2206
+ };
2207
+
2208
+ // prop that we'll set on the control
2209
+ const CONTROL_ATTRIBUTE_SET = new Set([
2210
+ ...CONSTRAINT_ATTRIBUTE_SET,
2211
+
2212
+ "ref",
2213
+ "children",
2214
+ "id",
2215
+ "name",
2216
+ "type",
2217
+ "value",
2218
+ "checked",
2219
+ "placeholder",
2220
+ "inputMode",
2221
+ "autoComplete",
2222
+ "spellcheck",
2223
+ "autoCorrect",
2224
+ "aria-controls",
2225
+ "tabIndex",
2226
+ "command",
2227
+ "commandFor",
2228
+ "command-value", // not standard but make sense, allow to give param to the command in question
2229
+ "list",
2230
+
2231
+ // "ui-action-target",
2232
+ "navi-input-type",
2233
+ "navi-value-pad",
2234
+ "navi-control-proxy-for",
2235
+ "navi-command-proxy-for",
2236
+ "navi-command-target",
2237
+ "onnavi_command",
2238
+ "onnavi_request_open",
2239
+ "onnavi_request_close",
2240
+
2241
+ "data-callout-arrow-x",
2242
+ "data-callout-point-to-border-box",
2243
+ "data-callout-point-to-content-box",
2244
+ "data-callout-viewport-spacing",
2245
+ "data-callout-position",
2246
+ "data-callout-position-fixed",
2247
+
2248
+ "data-testid", // playwright, cypress
2249
+ "data-separator", // used by InputGroup paste-to-fill
2250
+ ]);
2251
+ // prop concerning control but that won't end up in the DOM if not inside CONTROL_ATTRIBUTE_SET
2252
+ const CONTROL_PROP_SET = new Set([
2253
+ ...CONTROL_ATTRIBUTE_SET,
2254
+ ...CONSTRAINT_MESSAGE_PROP_NAME_SET,
2255
+
2256
+ "action",
2257
+ "confirm",
2258
+ "confirmPopupContent",
2259
+ "actionEvent",
2260
+ "actionAfterChange",
2261
+ "actionOnMouseDown",
2262
+ "actionDebounce",
2263
+ // A signal bound two-way to the control: its value seeds the control's state and
2264
+ // is written back on every uiAction. Precludes value/checked (see createControlInfo).
2265
+ "signal",
2266
+ "defaultValue",
2267
+ "defaultChecked",
2268
+ "readOnly", // will depend wether readOnly is supported
2269
+
2270
+ "loading",
2271
+ "basePseudoState",
2272
+ "constraints",
2273
+
2274
+ // A real target inside a zone that belongs to another control — see
2275
+ // own_target.js.
2276
+ "ownTarget",
2277
+
2278
+ "autoFocus",
2279
+ "autoFocusVisible",
2280
+ "autoFocusSelect",
2281
+
2282
+ "onMouseDown",
2283
+ "onClick",
2284
+ "onKeyDown",
2285
+ "onPaste",
2286
+ "onInput",
2287
+ "eventReactionDefinitions",
2288
+
2289
+ "onCancel",
2290
+ "cancelOnBlurInvalid",
2291
+ "cancelOnEscape",
2292
+ "onActionPrevented",
2293
+ "onActionStart",
2294
+ "onActionAborted",
2295
+ "onActionError",
2296
+ "actionErrorEffect",
2297
+ "errorMapping",
2298
+ "onActionEnd",
2299
+
2300
+ "resetOnCancel",
2301
+ "resetOnAbort",
2302
+ "resetOnError",
2303
+ "optimistic",
2304
+
2305
+ "charGuard",
2306
+ "maxLengthGuard",
2307
+ ]);
2308
+
2309
+ const MessagePropsRefContext = createContext();
2310
+
2311
+ const ControlIdContext = createContext();
2312
+ const ControlNameContext = createContext();
2313
+ const DisabledContext = createContext();
2314
+ const ReadOnlyContext = createContext();
2315
+ const RequiredContext = createContext();
2316
+ const LoadingContext$1 = createContext();
2317
+ createContext();
2318
+
2319
+ const ActionContext = createContext();
2320
+ const ActionRequesterContext = createContext();
2321
+
2322
+ /**
2323
+ * A target of its own inside a zone that belongs to another control.
2324
+ *
2325
+ * A pressable row, a picker's façade, a slide that travels under the finger:
2326
+ * each of them answers a press that lands anywhere in its box. An affordance an
2327
+ * application draws in there — a chip's cross, an eye that opens a profile, a
2328
+ * diskette that saves a guest — is aimed AT, not merely inside, and the press
2329
+ * belongs to it alone.
2330
+ *
2331
+ * Saying that by hand takes three guards, one per moment of the same press: the
2332
+ * pointerdown where gestures are arbitrated, the mousedown where a picker opens,
2333
+ * the click where it opens too when a gesture disputed the press. All three are
2334
+ * navi's own knowledge of navi's own event flow, and an application that gets
2335
+ * one wrong finds out by opening a popup it meant to keep shut. `ownTarget` is
2336
+ * that knowledge, said once.
2337
+ *
2338
+ * The other half is interactivity, and the question that settles it is: **does
2339
+ * this affordance write to the control it sits in?**
2340
+ *
2341
+ * - it does, and once the value cannot be changed it has nothing left to offer:
2342
+ * it GOES. A remove cross that still removes is worse than no cross, and one
2343
+ * that refuses politely still says "there is something to remove here" on a
2344
+ * row that is only being read. That is the default.
2345
+ * - it does, but its presence is information in itself: `"refuse"` keeps it and
2346
+ * refuses in its own words, like every other navi control.
2347
+ * - it does NOT — a diskette saving a row into the reader's own address book, a
2348
+ * badge explaining why a placement is odd. The read-only around it is about a
2349
+ * value it never touches, so `"always"` ignores it: the affordance stays lit
2350
+ * and stays pressable. It is on the caller to use it only for a gesture that
2351
+ * genuinely writes nothing to the control around it.
2352
+ *
2353
+ * Whichever mode, the affordance's own handler runs from inside its own gate
2354
+ * rather than from the DOM: a caller's `onClick` fires before any of this, and
2355
+ * would go off from a button drawn greyed.
2356
+ */
2357
+
2358
+
2359
+ /**
2360
+ * The whole claim, in the DOM, on any element — a `<button>` an application
2361
+ * draws itself as much as a navi control. It is read from the outside and
2362
+ * nowhere else: by the controls above (below), and by the gesture readers
2363
+ * (@jsenv/dom's DRAG_EXCLUDED_SELECTOR and DRAG_IGNORED_SELECTOR), which is why
2364
+ * one attribute is enough and the `ownTarget` prop only writes it.
2365
+ *
2366
+ * Its value is the mode, when there is one to say: `data-own-target="always"`.
2367
+ */
2368
+ const OWN_TARGET_ATTRIBUTE = "data-own-target";
2369
+
2370
+ /**
2371
+ * Whether `event` was aimed at an own target sitting below this control — in
2372
+ * which case the control is not what the press was for and must not answer it.
2373
+ *
2374
+ * Read from the event's target rather than from a mark left by a handler: the
2375
+ * question is "who is this press for", and the DOM between the pointer and the
2376
+ * control is the whole answer. Nothing is asked of the own target itself, which
2377
+ * is what lets it be anything — a button, a link, a field, a bare element.
2378
+ */
2379
+ const isAimedAtOwnTargetBelow = (event, controlHost) => {
2380
+ const target = event?.target;
2381
+ if (!target || typeof target.closest !== "function") {
2382
+ return false;
2383
+ }
2384
+ const ownTarget = target.closest(`[${OWN_TARGET_ATTRIBUTE}]`);
2385
+ if (!ownTarget) {
2386
+ return false;
2387
+ }
2388
+ // The claim is against what is ABOVE it: the control that IS the own target,
2389
+ // and any control living inside it, are being aimed at like anything else.
2390
+ if (ownTarget === controlHost || ownTarget.contains(controlHost)) {
2391
+ return false;
2392
+ }
2393
+ // From the root rather than the host: a layered control (a picker holding an
2394
+ // input) has its gate on the input, and what the application drew sits beside
2395
+ // it, not in it.
2396
+ const controlRoot = controlHost.closest("[navi-control]") || controlHost;
2397
+ return controlRoot.contains(ownTarget);
2398
+ };
2399
+
2400
+ /**
2401
+ * Whether the read-only, disabled and busy of the zone around this own target
2402
+ * are about it at all — see the modes at the top of this file.
2403
+ */
2404
+ const ownTargetIgnoresZoneState = (ownTarget) => ownTarget === "always";
2405
+
2406
+ /**
2407
+ * Whether an own target has nothing to offer where it sits: it writes to the
2408
+ * control around it, and that control cannot be changed, so the affordance goes.
2409
+ */
2410
+ const useOwnTargetHidden = (props) => {
2411
+ const disabled = useContext(DisabledContext);
2412
+ const readOnly = useContext(ReadOnlyContext);
2413
+ const loading = useContext(LoadingContext$1);
2414
+ const { ownTarget } = props;
2415
+ if (
2416
+ !ownTarget ||
2417
+ ownTarget === "refuse" ||
2418
+ ownTargetIgnoresZoneState(ownTarget)
2419
+ ) {
2420
+ return false;
2421
+ }
2422
+ return Boolean(disabled || readOnly || loading);
2423
+ };
2424
+
2117
2425
  /*
2118
2426
  * Deep structural equality for arbitrary JS values — what `===` can't do but this
2119
2427
  * codebase constantly needs: memoization cache keys ({ id: 1 } equal to { id: 1 }),
@@ -6490,83 +6798,6 @@ const isInertOnClick = (element) => {
6490
6798
  return true;
6491
6799
  };
6492
6800
 
6493
- const CONSTRAINT_NAME_TO_PROP = {
6494
- disabled: "disabledMessage",
6495
- required: "requiredMessage",
6496
- pattern: "patternMessage",
6497
- type_email: "typeMessage",
6498
- type_number: "typeMessage",
6499
- min_length: "minLengthMessage",
6500
- max_length: "maxLengthMessage",
6501
- min: "minMessage",
6502
- max: "maxMessage",
6503
- single_space: "singleSpaceMessage",
6504
- same_as: "sameAsMessage",
6505
- min_lower_letter: "minLowerLetterMessage",
6506
- min_upper_letter: "minUpperLetterMessage",
6507
- min_digit: "minDigitMessage",
6508
- min_special_char: "minSpecialCharMessage",
6509
- one_of: "oneOfMessage",
6510
- readonly: "readOnlyMessage",
6511
- available: "availableMessage",
6512
- };
6513
-
6514
- const CONSTRAINT_MESSAGE_PROP_NAME_SET = new Set(
6515
- Object.values(CONSTRAINT_NAME_TO_PROP),
6516
- );
6517
-
6518
- const extractMessageAndRemainingProps = (props) => {
6519
- const ownMessages = {};
6520
- const remaining = {};
6521
- const keyToVisit = new Set(Object.keys(props));
6522
- for (const key of keyToVisit) {
6523
- if (CONSTRAINT_MESSAGE_PROP_NAME_SET.has(key)) {
6524
- ownMessages[key] = props[key];
6525
- } else {
6526
- remaining[key] = props[key];
6527
- }
6528
- }
6529
- return [ownMessages, remaining];
6530
- };
6531
-
6532
- const getConstraintMessage = (
6533
- controller,
6534
- constraint,
6535
- generatedMessage,
6536
- { requester },
6537
- ) => {
6538
- const { name: constraintName } = constraint;
6539
- const propName = CONSTRAINT_NAME_TO_PROP[constraintName];
6540
-
6541
- // 1. Search first on the requester (e.g. the <li> that was clicked),
6542
- // then fall back to element (e.g. the hidden <input>).
6543
- if (requester) {
6544
- const requesterController = requester.__uiStateController__;
6545
- if (requesterController && requesterController !== controller) {
6546
- const requesterControllerMessage = requesterController.props[propName];
6547
- if (requesterControllerMessage) {
6548
- return {
6549
- message: requesterControllerMessage,
6550
- origin: "requester controller",
6551
- };
6552
- }
6553
- }
6554
- }
6555
-
6556
- const controllerMessage = controller.props[propName];
6557
- if (controllerMessage) {
6558
- return {
6559
- message: controllerMessage,
6560
- origin: "controller",
6561
- };
6562
- }
6563
-
6564
- return {
6565
- message: generatedMessage,
6566
- origin: "generated message",
6567
- };
6568
- };
6569
-
6570
6801
  /**
6571
6802
  * DOM utilities for the proxy control pattern.
6572
6803
  *
@@ -6656,6 +6887,10 @@ const findControlProxy = (el) => {
6656
6887
  return firstProxy;
6657
6888
  };
6658
6889
 
6890
+ let renderMessageText = text => text;
6891
+ const setCalloutMessageTextRenderer = renderer => {
6892
+ renderMessageText = renderer;
6893
+ };
6659
6894
  const CalloutRequestCloseContext = createContext();
6660
6895
  const useCalloutRequestClose = () => {
6661
6896
  return useContext(CalloutRequestCloseContext);
@@ -6670,6 +6905,48 @@ const renderIntoCallout = (jsx$1, calloutMessageElement, {
6670
6905
  render(calloutJsx, calloutMessageElement);
6671
6906
  };
6672
6907
 
6908
+ // An HTML message is rendered through preact rather than innerHTML so that its
6909
+ // text can go through renderMessageText: an emoji in a validation message must
6910
+ // not make the first line taller than the icon and close button beside it.
6911
+ const renderHtmlIntoCallout = (html, calloutMessageElement, {
6912
+ requestClose
6913
+ }) => {
6914
+ const template = document.createElement("template");
6915
+ template.innerHTML = html;
6916
+ renderIntoCallout(domToVNodes(template.content), calloutMessageElement, {
6917
+ requestClose
6918
+ });
6919
+ };
6920
+
6921
+ // Unmounts whatever preact rendered before the element is filled by hand
6922
+ // (a DOM node, an iframe); innerHTML alone would leave preact believing its
6923
+ // tree is still there.
6924
+ const clearCalloutMessage = calloutMessageElement => {
6925
+ render(null, calloutMessageElement);
6926
+ calloutMessageElement.innerHTML = "";
6927
+ };
6928
+ const domToVNodes = node => {
6929
+ const vnodes = [];
6930
+ for (const child of node.childNodes) {
6931
+ if (child.nodeType === Node.TEXT_NODE) {
6932
+ vnodes.push(renderMessageText(child.data));
6933
+ continue;
6934
+ }
6935
+ if (child.nodeType !== Node.ELEMENT_NODE) {
6936
+ continue;
6937
+ }
6938
+ const props = {};
6939
+ for (const {
6940
+ name,
6941
+ value
6942
+ } of child.attributes) {
6943
+ props[name] = value;
6944
+ }
6945
+ vnodes.push(h(child.localName, props, ...domToVNodes(child)));
6946
+ }
6947
+ return vnodes;
6948
+ };
6949
+
6673
6950
  installImportMetaCssBuild(import.meta);/**
6674
6951
  * A callout component that mimics native browser validation messages.
6675
6952
  * Features:
@@ -6679,7 +6956,7 @@ installImportMetaCssBuild(import.meta);/**
6679
6956
  * - Arrow automatically shows when pointing at a valid anchor element
6680
6957
  * - Centers in viewport when no anchor element provided or anchor is too big
6681
6958
  */
6682
- const css$13 = /* css */`
6959
+ const css$12 = /* css */`
6683
6960
  @layer navi {
6684
6961
  .navi_callout {
6685
6962
  /* A callout is parented to what it explains, so it inherits from it — and
@@ -6781,7 +7058,10 @@ const css$13 = /* css */`
6781
7058
  }
6782
7059
 
6783
7060
  .navi_callout_body {
6784
- --callout-icon-height: round(1.5em, 1px);
7061
+ /* The first line of the message: the icon and the close button sit
7062
+ in columns of that height, pinned to the top, so both stay level
7063
+ with it however many lines the message takes. */
7064
+ --callout-icon-height: round(1lh, 1px);
6785
7065
 
6786
7066
  position: relative;
6787
7067
  display: flex;
@@ -6837,10 +7117,15 @@ const css$13 = /* css */`
6837
7117
  align-self: flex-start;
6838
7118
 
6839
7119
  .navi_callout_close_button {
6840
- width: 1em;
6841
- height: 1em;
6842
- padding: 0;
6843
- align-self: center;
7120
+ /* A square filling the column, so the whole first line is the target;
7121
+ the padding keeps the cross itself at glyph size. */
7122
+ display: inline-flex;
7123
+ box-sizing: border-box;
7124
+ aspect-ratio: 1 / 1;
7125
+ height: 100%;
7126
+ padding: 0.2em;
7127
+ align-items: center;
7128
+ justify-content: center;
6844
7129
  color: currentColor;
6845
7130
  font-size: inherit;
6846
7131
  background: none;
@@ -6922,7 +7207,7 @@ const openCallout = (message, {
6922
7207
  skipFocus = false,
6923
7208
  debug = () => {}
6924
7209
  } = {}) => {
6925
- import.meta.css = [css$13, "@jsenv/navi/src/control/rules/callout/callout.js"];
7210
+ import.meta.css = [css$12, "@jsenv/navi/src/control/rules/callout/callout.js"];
6926
7211
  if (debug === true) {
6927
7212
  debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
6928
7213
  }
@@ -7104,11 +7389,11 @@ const openCallout = (message, {
7104
7389
  } else if (newMessage instanceof Node) {
7105
7390
  // Handle DOM node (cloned from CSS selector)
7106
7391
  debug(`callout update message (node)`);
7107
- calloutMessageElement.innerHTML = "";
7392
+ clearCalloutMessage(calloutMessageElement);
7108
7393
  calloutMessageElement.appendChild(newMessage);
7109
7394
  } else if (typeof newMessage === "function") {
7110
7395
  debug(`callout update message (function)`);
7111
- calloutMessageElement.innerHTML = "";
7396
+ clearCalloutMessage(calloutMessageElement);
7112
7397
  newMessage({
7113
7398
  renderIntoCallout: jsx => renderIntoCallout(jsx, calloutMessageElement, {
7114
7399
  requestClose
@@ -7133,12 +7418,13 @@ const openCallout = (message, {
7133
7418
  iframe.style.backgroundColor = "white";
7134
7419
  iframe.srcdoc = newMessage;
7135
7420
  debug(`callout update message (html document iframe)`);
7136
- // Clear existing content and add iframe
7137
- calloutMessageElement.innerHTML = "";
7421
+ clearCalloutMessage(calloutMessageElement);
7138
7422
  calloutMessageElement.appendChild(iframe);
7139
7423
  } else {
7140
7424
  debug(`callout update message: ${typeof newMessage === "string" ? newMessage.slice(0, 80) : String(newMessage)}`);
7141
- calloutMessageElement.innerHTML = newMessage;
7425
+ renderHtmlIntoCallout(String(newMessage), calloutMessageElement, {
7426
+ requestClose
7427
+ });
7142
7428
  }
7143
7429
  }
7144
7430
  // After updating content the callout size likely changed — re-position immediately
@@ -8241,8 +8527,6 @@ const ABORTED = { id: "aborted" };
8241
8527
  const FAILED = { id: "failed" };
8242
8528
  const COMPLETED = { id: "completed" };
8243
8529
 
8244
- const CONSTRAINT_ATTRIBUTE_SET = new Set();
8245
-
8246
8530
  const BUSY_CONSTRAINT = {
8247
8531
  name: "busy",
8248
8532
  messageAttribute: "data-busy-message",
@@ -10295,6 +10579,20 @@ const onRequestInteraction = (
10295
10579
 
10296
10580
  const currentTarget = requestInteractionCustomEvent.currentTarget;
10297
10581
  const controlHost = findControlHost(currentTarget) || currentTarget;
10582
+
10583
+ // Aimed at something else that lives in this control's box: a chip's cross, an
10584
+ // eye, a diskette. The press is that affordance's alone (see own_target.js),
10585
+ // and stepping back here — rather than stopping the propagation over there —
10586
+ // is what leaves the event whole for everything that is not a navi
10587
+ // interaction. Stepping back, not refusing: the reaction never happened, so
10588
+ // its `prevented`/`always` (an `e.preventDefault()`, for most of them) have
10589
+ // nothing to undo and would take the press from the affordance itself.
10590
+ if (isAimedAtOwnTargetBelow(event, controlHost)) {
10591
+ debugInteraction(event, `"${name}" is for an own target below`);
10592
+ requestInteractionCustomEvent.preventDefault();
10593
+ return false;
10594
+ }
10595
+
10298
10596
  const controller = controlHost.__uiStateController__;
10299
10597
 
10300
10598
  if (controller && !bypassInteractivity) {
@@ -16085,6 +16383,26 @@ const POSITION_PROPS = {
16085
16383
  return { transform: `skew(${value})` };
16086
16384
  },
16087
16385
  };
16386
+ const singleLineEllipsisStyles = () => {
16387
+ return {
16388
+ overflow: "hidden",
16389
+ textOverflow: "ellipsis",
16390
+ overflowWrap: "normal",
16391
+ };
16392
+ };
16393
+ // The lines beyond the clamp are still laid out, so the clip edge decides
16394
+ // whether the top of the first hidden one is visible: "overflow: hidden" clips
16395
+ // at the padding box and lets it show inside the block-end padding. Clipping at
16396
+ // the content box instead ends the element right after its last visible line.
16397
+ const lineClampStyles = (value) => {
16398
+ return {
16399
+ "overflow": "clip",
16400
+ "overflowClipMargin": "content-box",
16401
+ "display": "-webkit-box",
16402
+ "-webkit-box-orient": "vertical",
16403
+ "-webkit-line-clamp": value,
16404
+ };
16405
+ };
16088
16406
  const TYPO_PROPS = {
16089
16407
  font: applyOnCSSProp("fontFamily"),
16090
16408
  fontFamily: PASS_THROUGH,
@@ -16122,39 +16440,21 @@ const TYPO_PROPS = {
16122
16440
  return null;
16123
16441
  }
16124
16442
  if (value === 1 || value === "1") {
16125
- return {
16126
- overflow: "hidden",
16127
- textOverflow: "ellipsis",
16128
- overflowWrap: "normal",
16129
- };
16443
+ return singleLineEllipsisStyles();
16130
16444
  }
16131
- return {
16132
- "overflow": "hidden",
16133
- "display": "-webkit-box",
16134
- "-webkit-box-orient": "vertical",
16135
- "-webkit-line-clamp": value,
16136
- };
16445
+ return lineClampStyles(value);
16137
16446
  },
16138
16447
  overflowEllipsis: (value) => {
16139
16448
  if (!value) {
16140
16449
  return null;
16141
16450
  }
16142
- return {
16143
- overflow: "hidden",
16144
- textOverflow: "ellipsis",
16145
- overflowWrap: "normal",
16146
- };
16451
+ return singleLineEllipsisStyles();
16147
16452
  },
16148
16453
  lineClamp: (value) => {
16149
16454
  if (!value) {
16150
16455
  return null;
16151
16456
  }
16152
- return {
16153
- "overflow": "hidden",
16154
- "display": "-webkit-box",
16155
- "-webkit-box-orient": "vertical",
16156
- "-webkit-line-clamp": value,
16157
- };
16457
+ return lineClampStyles(value);
16158
16458
  },
16159
16459
  textAlign: PASS_THROUGH,
16160
16460
  textBox: PASS_THROUGH,
@@ -16200,6 +16500,7 @@ const VISUAL_PROPS = {
16200
16500
  overflow: PASS_THROUGH,
16201
16501
  overflowX: PASS_THROUGH,
16202
16502
  overflowY: PASS_THROUGH,
16503
+ overflowClipMargin: PASS_THROUGH,
16203
16504
  objectFit: PASS_THROUGH,
16204
16505
  accentColor: PASS_THROUGH,
16205
16506
  scrollbarWidth: PASS_THROUGH,
@@ -19199,7 +19500,7 @@ const setupNetworkMonitoring = () => {
19199
19500
  };
19200
19501
  setupNetworkMonitoring();
19201
19502
 
19202
- installImportMetaCssBuild(import.meta);const css$12 = /* css */`
19503
+ installImportMetaCssBuild(import.meta);const css$11 = /* css */`
19203
19504
  .navi_loading_indicator_fluid_container {
19204
19505
  position: relative;
19205
19506
  display: flex;
@@ -19231,7 +19532,7 @@ const LoadingIndicatorFluid = ({
19231
19532
  visuallyHidden,
19232
19533
  ...rest
19233
19534
  }) => {
19234
- import.meta.css = [css$12, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
19535
+ import.meta.css = [css$11, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
19235
19536
  const ref = useRef(null);
19236
19537
  // The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
19237
19538
  const [containerWidth, setContainerWidth] = useState(0);
@@ -19436,7 +19737,7 @@ const LoadingRectangleSvg = ({
19436
19737
  });
19437
19738
  };
19438
19739
 
19439
- installImportMetaCssBuild(import.meta);const css$11 = /* css */`
19740
+ installImportMetaCssBuild(import.meta);const css$10 = /* css */`
19440
19741
  .navi_loading_outline_wrapper {
19441
19742
  position: absolute;
19442
19743
  /* Controls place the outline slightly outside their box, right on top of
@@ -19473,7 +19774,7 @@ installImportMetaCssBuild(import.meta);const css$11 = /* css */`
19473
19774
  }
19474
19775
  `;
19475
19776
  const LoadingOutline = props => {
19476
- import.meta.css = [css$11, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
19777
+ import.meta.css = [css$10, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
19477
19778
  if (props.containerRef) {
19478
19779
  const container = props.containerRef.current;
19479
19780
  if (!container) {
@@ -19663,6 +19964,256 @@ const getHrefTargetInfo = (href) => {
19663
19964
  };
19664
19965
  };
19665
19966
 
19967
+ /**
19968
+ * A variant of useLayoutEffect that accounts for ancestor <dialog>/<details>
19969
+ * or popover visibility.
19970
+ *
19971
+ * Motivation: some effects (auto-scroll, measurement, focus) only make sense
19972
+ * when the element is actually presented on screen. A plain useLayoutEffect
19973
+ * fires on mount even when the component is inside a closed <dialog>, a
19974
+ * collapsed <details>, or a hidden popover, where scroll and layout operations
19975
+ * are no-ops.
19976
+ *
19977
+ * Behavior:
19978
+ * - No <dialog>/<details>/[popover] ancestor → runs like a normal
19979
+ * useLayoutEffect with the provided deps.
19980
+ * - Inside a closed/hidden ancestor → skips the initial run; instead runs
19981
+ * the callback once the ancestor opens — see @jsenv/dom's own
19982
+ * observeAncestorOpenState for exactly how that's detected, and why it
19983
+ * matters that it happens before the browser paints.
19984
+ * - Inside an open ancestor → runs on mount AND every subsequent open.
19985
+ *
19986
+ * The callback's second argument is always a `navi_displayed` CustomEvent,
19987
+ * with `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
19988
+ * - No <dialog>/<details>/[popover]/[aria-expanded] ancestor at all →
19989
+ * `{ ancestor: document, ancestorType: "document" }`.
19990
+ * - Otherwise → `{ ancestor: <the matched element>, ancestorType: "dialog"
19991
+ * | "popover" | "details" | "aria-expanded" }`.
19992
+ * `becauseAncestorOpened` distinguishes the two ways of coming on screen:
19993
+ * - true — the element was already mounted and the ancestor just opened,
19994
+ * revealing it along with everything else it holds. The opening has an
19995
+ * owner (the ancestor's own transferFocus/openEffect), and what it reveals
19996
+ * should defer to it — see use_auto_focus.js.
19997
+ * - false — the element was mounted just now, into a surface already on
19998
+ * screen (or into the plain document). Nothing else owns this appearance:
19999
+ * what the element says about itself (an autofocus, a measurement) is the
20000
+ * only word there is.
20001
+ *
20002
+ * Usage:
20003
+ * useDisplayedLayoutEffect(ref, () => {
20004
+ * scrollToSelected();
20005
+ * }, []);
20006
+ */
20007
+ const useDisplayedLayoutEffect = (ref, callback, deps) => {
20008
+ if (typeof callback !== "function") {
20009
+ throw new TypeError("useDisplayedLayoutEffect: callback is not a function");
20010
+ }
20011
+
20012
+ // Keep a stable ref so the open listener always calls the latest callback
20013
+ // without needing to be re-registered when deps change.
20014
+ const callbackRef = useRef(callback);
20015
+ callbackRef.current = callback;
20016
+
20017
+ // Run on mount (or when deps change) — but only if the element is visible.
20018
+ useLayoutEffect(() => {
20019
+ const el = ref.current;
20020
+ if (!el) {
20021
+ return;
20022
+ }
20023
+ const ancestor = closestOpenableAncestor(el);
20024
+ if (!ancestor) {
20025
+ callbackRef.current(el, createDisplayedEvent(document, false));
20026
+ return;
20027
+ }
20028
+ if (!isAncestorOpen(ancestor)) {
20029
+ // Ancestor is closed — skip now; the observeAncestorOpenState call
20030
+ // below will fire once it opens.
20031
+ return;
20032
+ }
20033
+ callbackRef.current(el, createDisplayedEvent(ancestor, false));
20034
+ }, deps);
20035
+
20036
+ // Re-run every time the ancestor opens.
20037
+ useLayoutEffect(() => {
20038
+ const el = ref.current;
20039
+ if (!el) {
20040
+ return undefined;
20041
+ }
20042
+ const ancestor = closestOpenableAncestor(el);
20043
+ if (!ancestor) {
20044
+ return undefined;
20045
+ }
20046
+ return observeAncestorOpenState(ancestor, ({ isOpen }) => {
20047
+ if (!isOpen) {
20048
+ return;
20049
+ }
20050
+ const lastEl = ref.current;
20051
+ callbackRef.current(lastEl, createDisplayedEvent(ancestor, true));
20052
+ });
20053
+ }, []);
20054
+ };
20055
+
20056
+ const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
20057
+ return new CustomEvent("navi_displayed", {
20058
+ detail: {
20059
+ ancestor,
20060
+ ancestorType: getAncestorOpenType(ancestor),
20061
+ becauseAncestorOpened,
20062
+ },
20063
+ });
20064
+ };
20065
+
20066
+ installImportMetaCssBuild(import.meta);// # TextAnchor — how it works
20067
+ const css$$ = /* css */`
20068
+ .navi_text_anchor {
20069
+ vertical-align: baseline;
20070
+ user-select: none;
20071
+ overflow: hidden;
20072
+ }
20073
+ `;
20074
+
20075
+ /**
20076
+ * Positions children vertically relative to the surrounding text, correcting for font-size differences.
20077
+ *
20078
+ * Place this component around any inline element whose font-size differs from the surrounding text.
20079
+ * It renders an invisible anchor that inherits the surrounding text's font metrics, then shifts
20080
+ * the child so that its visual position matches the requested `textAnchor` value — regardless of
20081
+ * font-size, display type (inline, inline-block, inline-flex…), or the active `vertical-align`.
20082
+ *
20083
+ * @param {"line-top"|"char-top"|"center"|"char-bottom"|"line-bottom"} [textAnchor="char-bottom"]
20084
+ * - `"line-top"` — child top aligns with the top of the surrounding line box
20085
+ * - `"char-top"` — child top aligns with the top of visible characters (ink ascent)
20086
+ * - `"center"` — child is vertically centered on the surrounding line box
20087
+ * - `"char-bottom"` — child bottom aligns to the text baseline (no correction, browser default)
20088
+ * - `"line-bottom"` — child bottom aligns with the bottom of the surrounding line box
20089
+ * @param {{ size?: number, verticalAlign?: string }} [lineLayout]
20090
+ * Describes the surrounding line context. Used as layout-effect dependencies so the correction
20091
+ * reruns when the surrounding text's font-size or vertical-align changes.
20092
+ * @param {import("ignore:preact").RefObject} childRef — ref on the child element to reposition
20093
+ */
20094
+ const TextAnchor = ({
20095
+ childRef,
20096
+ children,
20097
+ textAnchor = "char-bottom",
20098
+ textKey,
20099
+ textSize,
20100
+ lineLayout
20101
+ }) => {
20102
+ import.meta.css = [css$$, "@jsenv/navi/src/text/text_anchor.jsx"];
20103
+ const anchorRef = useRef();
20104
+
20105
+ // Plain useLayoutEffect would also fire while an ancestor dialog/popover
20106
+ // (e.g. a closed SidePanel) is still display:none — every rect involved
20107
+ // reads 0×0 at that point, so the math trivially (and wrongly) resolves
20108
+ // to topOffset 0: not a real "no correction needed" result, just a
20109
+ // zero-by-zero coincidence that happens to look fine only because it
20110
+ // leaves the browser's own default alignment untouched. The real
20111
+ // correction then only gets applied later, on whatever unrelated
20112
+ // re-render next happens to change one of this effect's own deps —
20113
+ // which reads as the child "jumping" even though nothing about its own
20114
+ // geometry changed. useDisplayedLayoutEffect skips the initial run in
20115
+ // that case and reruns once the ancestor actually opens instead.
20116
+ useDisplayedLayoutEffect(anchorRef, anchorEl => {
20117
+ const childEl = childRef.current;
20118
+ if (!anchorEl || !childEl) {
20119
+ return;
20120
+ }
20121
+ // Only correct when the anchor lives in an inline formatting context.
20122
+ // If the parent is a flex/grid container, inline layout rules don't apply
20123
+ // and our font-metrics model is invalid.
20124
+ const parentDisplay = getComputedStyle(anchorEl.parentElement).display;
20125
+ if (parentDisplay !== "inline" && parentDisplay !== "inline-block" && parentDisplay !== "block") {
20126
+ // we must hide the anchor otherwise it would affect layout without providing any benefit (would trigger flex gap for instance)
20127
+ anchorEl.setAttribute("hidden", "");
20128
+ setTopOffset(childEl, 0);
20129
+ return;
20130
+ }
20131
+ anchorEl.removeAttribute("hidden");
20132
+ const topOffset = computeTopOffset({
20133
+ anchorEl,
20134
+ childEl,
20135
+ textAnchor
20136
+ });
20137
+ setTopOffset(childEl, topOffset);
20138
+ }, [textAnchor, textKey, textSize, lineLayout?.size, lineLayout?.verticalAlign]);
20139
+ return jsxs(Fragment$1, {
20140
+ children: [children, jsx("span", {
20141
+ ref: anchorRef,
20142
+ className: "navi_text_anchor",
20143
+ "aria-hidden": "true",
20144
+ children: "\u200B"
20145
+ })]
20146
+ });
20147
+ };
20148
+ const setTopOffset = (childEl, topOffset) => {
20149
+ // position:relative + top shifts the element visually.
20150
+ // marginTop: -topOffset makes the layout box follow the visual position, so any container
20151
+ // (button, link, box…) computes its own padding/border/height based on the real final position
20152
+ // rather than the original unshifted one. This means a badge inside a button will symmetrically
20153
+ // expand the button height instead of overflowing or being clipped.
20154
+ // marginBottom: topOffset compensates the marginTop so the line height stays unchanged —
20155
+ // the shift is purely a repositioning, not an inflation of the line.
20156
+ if (!topOffset) {
20157
+ childEl.style.position = "";
20158
+ childEl.style.top = "";
20159
+ childEl.style.marginTop = "";
20160
+ childEl.style.marginBottom = "";
20161
+ return;
20162
+ }
20163
+ childEl.style.position = "relative";
20164
+ childEl.style.top = `${topOffset}px`;
20165
+ childEl.style.marginTop = `${-topOffset}px`;
20166
+ childEl.style.marginBottom = `${topOffset}px`;
20167
+ };
20168
+ const computeTopOffset = ({
20169
+ anchorEl,
20170
+ childEl,
20171
+ textAnchor
20172
+ }) => {
20173
+ if (textAnchor === "char-bottom") {
20174
+ // Align child's bottom with the char's bottom = the baseline.
20175
+ // The CSS spec says an inline-block with no text content has its baseline at its bottom margin edge.
20176
+ // So the browser's default placement already puts the child's bottom at the line's baseline.
20177
+ // No correction needed.
20178
+ return 0;
20179
+ }
20180
+ // The anchor's rendered rect corresponds to the surrounding text's line box:
20181
+ // top and bottom are the visual bounds of the line (including line-height).
20182
+ const anchorRect = anchorEl.getBoundingClientRect();
20183
+
20184
+ // Measure the child's current rect, then subtract any previously applied top correction
20185
+ // to recover its natural position — avoiding a style reset + reflow.
20186
+ const childRect = childEl.getBoundingClientRect();
20187
+ const childH = childRect.height;
20188
+ const previousTop = parseFloat(childEl.style.top) || 0;
20189
+ const childNaturalTop = childRect.top - previousTop;
20190
+
20191
+ // Compute desired child top Y based on textAnchor intention.
20192
+ let desiredChildTopY = 0;
20193
+ if (textAnchor === "line-top") {
20194
+ desiredChildTopY = anchorRect.top;
20195
+ } else if (textAnchor === "char-top") {
20196
+ const anchorStyle = getComputedStyle(anchorEl);
20197
+ const ctx = charTopCanvas.getContext("2d");
20198
+ ctx.font = `${anchorStyle.fontWeight} ${anchorStyle.fontSize} ${anchorStyle.fontFamily}`;
20199
+ const m = ctx.measureText("M");
20200
+ const baselineY = anchorRect.bottom - m.fontBoundingBoxDescent;
20201
+ desiredChildTopY = baselineY - m.actualBoundingBoxAscent;
20202
+ } else if (textAnchor === "center") {
20203
+ const anchorCenterY = (anchorRect.top + anchorRect.bottom) / 2;
20204
+ desiredChildTopY = anchorCenterY - childH / 2;
20205
+ } else if (textAnchor === "char-bottom") {
20206
+ // Already handled above (early return 0), but guard here for completeness.
20207
+ return 0;
20208
+ } else if (textAnchor === "line-bottom") {
20209
+ desiredChildTopY = anchorRect.bottom - childH;
20210
+ } else {
20211
+ return 0; // unknown textAnchor, no correction
20212
+ }
20213
+ return desiredChildTopY - childNaturalTop;
20214
+ };
20215
+ const charTopCanvas = document.createElement("canvas");
20216
+
19666
20217
  const useInitialTextSelection = (ref, textSelection) => {
19667
20218
  const deps = [];
19668
20219
  if (Array.isArray(textSelection)) {
@@ -19763,7 +20314,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
19763
20314
  };
19764
20315
 
19765
20316
  installImportMetaCssBuild(import.meta);// https://jsfiddle.net/v5xzJ/4/
19766
- const css$10 = /* css */`
20317
+ const css$_ = /* css */`
19767
20318
  @layer navi {
19768
20319
  .navi_text {
19769
20320
  &[data-skeleton] {
@@ -19947,6 +20498,91 @@ const css$10 = /* css */`
19947
20498
  opacity: 1;
19948
20499
  }
19949
20500
  }
20501
+
20502
+ /* ── Icon ── */
20503
+
20504
+ @layer navi {
20505
+ /* Ensure data attributes from box.jsx can win to update display */
20506
+ .navi_icon {
20507
+ display: inline-flex;
20508
+ box-sizing: border-box;
20509
+ max-width: 100%;
20510
+ /* An icon never grows past the box it sits in, so a glyph can never make
20511
+ a line of text taller than the text itself. lineOverflow="allow" opts
20512
+ out, for an icon that is an affordance rather than a character — a
20513
+ control's chevron or clear button, sized to be touched, not read. */
20514
+ max-height: 100%;
20515
+
20516
+ &[data-line-overflow="allow"] {
20517
+ max-height: none;
20518
+ }
20519
+ }
20520
+ }
20521
+
20522
+ .navi_icon {
20523
+ white-space: nowrap;
20524
+ vertical-align: inherit;
20525
+
20526
+ &[data-icon-char] {
20527
+ aspect-ratio: 1/1;
20528
+ min-width: 0;
20529
+ height: round(1em, 1px);
20530
+ max-height: round(1em, 1px);
20531
+ flex-grow: 0 !important;
20532
+ align-items: center;
20533
+ justify-content: center;
20534
+
20535
+ /* fillLine: measured on the line box (1lh) instead of the character box
20536
+ (1em). The icon still stays inside the line — it just uses all of it,
20537
+ which is what an icon standing on its own in a control's slot wants,
20538
+ where a glyph sitting among letters wants to match their size. */
20539
+ &[data-fill-line] {
20540
+ height: round(1lh, 1px);
20541
+ max-height: round(1lh, 1px);
20542
+ }
20543
+
20544
+ svg,
20545
+ img {
20546
+ width: 100%;
20547
+ height: 100%;
20548
+ }
20549
+ svg {
20550
+ overflow: visible;
20551
+ }
20552
+ }
20553
+ &[data-flow-inline] {
20554
+ width: 1em;
20555
+ height: 1em;
20556
+ }
20557
+ &[data-interactive] {
20558
+ cursor: pointer;
20559
+ }
20560
+ &[data-icon-text] {
20561
+ -webkit-font-smoothing: antialiased;
20562
+ text-rendering: optimizeLegibility;
20563
+ }
20564
+ }
20565
+
20566
+ .navi_icon > svg,
20567
+ .navi_icon > img {
20568
+ width: 100%;
20569
+ height: 100%;
20570
+ }
20571
+ .navi_icon[data-width-fixed] > svg,
20572
+ .navi_icon[data-width-fixed] > img {
20573
+ width: 100%;
20574
+ height: auto;
20575
+ }
20576
+ .navi_icon[data-height-fixed] > svg,
20577
+ .navi_icon[data-height-fixed] > img {
20578
+ width: auto;
20579
+ height: 100%;
20580
+ }
20581
+ .navi_icon[data-width-fixed][data-height-fixed] > svg,
20582
+ .navi_icon[data-width-fixed][data-height-fixed] > img {
20583
+ width: 100%;
20584
+ height: 100%;
20585
+ }
19950
20586
  `;
19951
20587
  const REGULAR_SPACE = jsx("span", {
19952
20588
  "data-navi-space": "",
@@ -20129,6 +20765,7 @@ const shouldInjectSpacingBetween = (left, right) => {
20129
20765
  * loading?: boolean,
20130
20766
  * skeleton?: boolean,
20131
20767
  * attachLastChild?: boolean,
20768
+ * emojiAsIcon?: boolean,
20132
20769
  * preventSpaceUnderlines?: boolean,
20133
20770
  * holdSpaceForStyle?: import("ignore:preact").JSX.CSSProperties,
20134
20771
  * boldStable?: boolean,
@@ -20168,6 +20805,12 @@ const shouldInjectSpacingBetween = (left, right) => {
20168
20805
  * truncation belongs outside the `Text` instead (see `docs/typography.md`).
20169
20806
  * `Link` sets it on its own whenever it renders an end icon.
20170
20807
  *
20808
+ * @param {boolean} [emojiAsIcon]
20809
+ * Renders every emoji found in the string children as an `Icon`, so it sits
20810
+ * in the line like a character and never makes the line taller than the
20811
+ * text. For free text a user typed (a message, a description) — the only
20812
+ * place an emoji is expected; see `docs/typography.md`.
20813
+ *
20171
20814
  * @param {boolean} [preventSpaceUnderlines]
20172
20815
  * Replaces real space characters between children with padding-based spaces.
20173
20816
  * Useful inside `<a>` elements where browsers draw an underline under spaces.
@@ -20284,12 +20927,13 @@ const TextShrinkWrap = props => {
20284
20927
  });
20285
20928
  };
20286
20929
  const TextUI = props => {
20287
- import.meta.css = [css$10, "@jsenv/navi/src/text/text.jsx"];
20930
+ import.meta.css = [css$_, "@jsenv/navi/src/text/text.jsx"];
20288
20931
  let {
20289
20932
  ref,
20290
20933
  spacing,
20291
20934
  preventSpaceUnderlines = false,
20292
20935
  attachLastChild = false,
20936
+ emojiAsIcon = false,
20293
20937
  boldStable,
20294
20938
  holdSpaceForStyle,
20295
20939
  capitalize,
@@ -20320,6 +20964,11 @@ const TextUI = props => {
20320
20964
  } else {
20321
20965
  children = applySpacingOnTextChildren(children, resolvedSpacing, defaultSpace);
20322
20966
  }
20967
+ if (emojiAsIcon) {
20968
+ // After the spacing pass: an emoji glued to a word ("hello👋") must not
20969
+ // get a separator injected as if it were a child element.
20970
+ children = renderEmojiAsIcon(children);
20971
+ }
20323
20972
  if (boldStable) {
20324
20973
  const {
20325
20974
  bold
@@ -20434,6 +21083,244 @@ const TextWithSelectRange = ({
20434
21083
  });
20435
21084
  };
20436
21085
 
21086
+ /**
21087
+ * Renders an icon — an inline SVG/emoji/text glyph that inherits the
21088
+ * surrounding text's `currentColor` and (by default) its font size, so it sits
21089
+ * on the text baseline like a character.
21090
+ *
21091
+ * Content comes from either `href` (references an external/sprite symbol via
21092
+ * `<use>`) or `children` (an inline `<svg>` element, or a string for a
21093
+ * text/emoji icon). All extra props are spread onto the underlying `Box`/`Text`
21094
+ * (sizing, spacing, color, className, data-attributes, …).
21095
+ *
21096
+ * Render mode is chosen automatically:
21097
+ * - `children` is a **string** → a text icon (`<Text data-icon-text>`).
21098
+ * - **sized** (an explicit `width`/`height`, or `flex`/`grid`) → a block icon
21099
+ * (`<Box square>`), laid out as its own box rather than inline.
21100
+ * - otherwise → an **inline char-like** icon that flows on the text baseline
21101
+ * (`data-icon-char`), aligned via `textAnchor`.
21102
+ *
21103
+ * Accessibility: an icon is treated as decorative (`aria-hidden`) by default
21104
+ * whenever it has no explicit size and no `onClick`; give it an explicit
21105
+ * `decorative={false}` (or make it interactive) when it conveys meaning.
21106
+ *
21107
+ * @param {object} props
21108
+ * @param {string} [props.href] - URL/id of an external SVG symbol, rendered via
21109
+ * `<svg><use href></svg>`. Mutually exclusive with meaningful `children`.
21110
+ * @param {import("ignore:preact").ComponentChildren} [props.children] - Inline icon
21111
+ * content: an `<svg>` element, or a string (renders as a text/emoji icon).
21112
+ * @param {boolean} [props.decorative] - Marks the icon `aria-hidden`. Defaults
21113
+ * to `true` for an unsized, non-interactive icon; pass `false` for a
21114
+ * meaning-bearing icon that needs to be exposed to assistive tech.
21115
+ * @param {(event: MouseEvent) => void} [props.onClick] - Makes the icon
21116
+ * interactive (`data-interactive`, pointer cursor) and non-decorative.
21117
+ * @param {"line-top"|"char-top"|"center"|"char-bottom"|"line-bottom"} [props.textAnchor="center"]
21118
+ * - Vertical alignment within the surrounding text line for the inline
21119
+ * char-like mode, forwarded to `TextAnchor`: `"line-top"`/`"line-bottom"`
21120
+ * align to the line box edges, `"char-top"` to the ink ascent, `"center"`
21121
+ * centers on the line box, `"char-bottom"` sits on the baseline. See
21122
+ * `text_anchor.jsx`.
21123
+ * @param {{ size?: number, verticalAlign?: string }} [props.lineLayout] -
21124
+ * Describes the surrounding line context (font size / vertical-align),
21125
+ * forwarded to `TextAnchor` so it recomputes the vertical correction when
21126
+ * that context changes.
21127
+ * @param {string|number} [props.width] - Explicit width; `"auto"` clears it.
21128
+ * Any explicit size switches the icon to block (sized) mode.
21129
+ * @param {string|number} [props.height] - Explicit height; `"auto"` clears it.
21130
+ * @param {"allow"} [props.lineOverflow] - `"allow"` lets the icon be taller
21131
+ * than the box it sits in (a line of text, a control's slot) instead of being
21132
+ * capped by it. For an icon that is an affordance sized for the finger rather
21133
+ * than a character sized for reading.
21134
+ * @param {boolean} [props.fillLine] - Sizes the icon on the line box (1lh)
21135
+ * rather than on the character box (1em), so it uses the full height of the
21136
+ * line without leaving it. Unlike `lineOverflow`, the icon still never
21137
+ * exceeds the line.
21138
+ * @param {boolean} [props.square] - Keep a 1:1 box; combined with one explicit
21139
+ * dimension it fixes the other too.
21140
+ * @param {boolean} [props.circle] - Like `square`, plus a circular shape.
21141
+ * @param {string|number} [props.aspectRatio] - Fixes the second dimension from
21142
+ * the one explicit dimension.
21143
+ * @param {"x"|"y"|boolean} [props.flex] - Forces block/flex layout; auto-set to
21144
+ * `"x"` when the icon is sized.
21145
+ * @param {boolean} [props.grid] - Forces block/grid layout.
21146
+ * @param {string} [props.className] - Merged with the base `"navi_icon"` class.
21147
+ */
21148
+ const Icon = ({
21149
+ href,
21150
+ children,
21151
+ decorative,
21152
+ onClick,
21153
+ textAnchor = "center",
21154
+ lineLayout,
21155
+ lineOverflow,
21156
+ fillLine,
21157
+ ...props
21158
+ }) => {
21159
+ import.meta.css = [css$_, "@jsenv/navi/src/text/text.jsx"];
21160
+ const innerChildren = href ? jsx("svg", {
21161
+ width: "100%",
21162
+ height: "100%",
21163
+ children: jsx("use", {
21164
+ href: href
21165
+ })
21166
+ }) : children;
21167
+ let {
21168
+ flex,
21169
+ grid,
21170
+ width,
21171
+ height
21172
+ } = props;
21173
+ if (width === "auto") {
21174
+ width = undefined;
21175
+ }
21176
+ if (height === "auto") {
21177
+ height = undefined;
21178
+ }
21179
+ const hasExplicitWidth = width !== undefined;
21180
+ const hasExplicitHeight = height !== undefined;
21181
+ const widthFixed = hasExplicitWidth || hasExplicitHeight && (props.square || props.circle || props.aspectRatio);
21182
+ const heightFixed = hasExplicitHeight || hasExplicitWidth && (props.square || props.circle || props.aspectRatio);
21183
+ if (widthFixed || heightFixed) {
21184
+ if (flex === undefined) {
21185
+ flex = "x";
21186
+ }
21187
+ } else if (decorative === undefined && !onClick) {
21188
+ decorative = true;
21189
+ }
21190
+ const ariaProps = decorative ? {
21191
+ "aria-hidden": "true"
21192
+ } : {};
21193
+ const textRef = useRef();
21194
+ if (typeof children === "string") {
21195
+ return jsx(Text, {
21196
+ ...props,
21197
+ ...ariaProps,
21198
+ "data-icon-text": "",
21199
+ "data-line-overflow": lineOverflow,
21200
+ "data-fill-line": fillLine ? "" : undefined,
21201
+ children: children
21202
+ });
21203
+ }
21204
+ if (flex || grid) {
21205
+ return jsx(Box, {
21206
+ square: true,
21207
+ ...props,
21208
+ ...ariaProps,
21209
+ flex: flex,
21210
+ baseClassName: "navi_icon",
21211
+ "data-width-fixed": widthFixed ? "" : undefined,
21212
+ "data-height-fixed": heightFixed ? "" : undefined,
21213
+ "data-interactive": onClick ? "" : undefined,
21214
+ "data-line-overflow": lineOverflow,
21215
+ "data-fill-line": fillLine ? "" : undefined,
21216
+ onClick: onClick,
21217
+ children: innerChildren
21218
+ });
21219
+ }
21220
+ return jsx(TextAnchor, {
21221
+ childRef: textRef,
21222
+ textAnchor: textAnchor,
21223
+ textSize: props.size,
21224
+ lineLayout: lineLayout,
21225
+ children: jsxs(Text, {
21226
+ ...props,
21227
+ ...ariaProps,
21228
+ className: withPropsClassName("navi_icon", props.className),
21229
+ spacing: "pre",
21230
+ "data-icon-char": "",
21231
+ "data-line-overflow": lineOverflow,
21232
+ "data-fill-line": fillLine ? "" : undefined,
21233
+ "data-width-fixed": widthFixed ? "" : undefined,
21234
+ "data-height-fixed": heightFixed ? "" : undefined,
21235
+ "data-interactive": onClick ? "" : undefined,
21236
+ onClick: onClick,
21237
+ ref: textRef,
21238
+ children: [jsx("span", {
21239
+ style: "user-select:none",
21240
+ children: "\u200B"
21241
+ }), innerChildren]
21242
+ })
21243
+ });
21244
+ };
21245
+
21246
+ // An emoji-presentation character (🌸), or a pictogram forced into emoji
21247
+ // presentation by VS16 (❤️), followed by its skin-tone modifiers and ZWJ
21248
+ // sequence. A flag is two regional indicators that must stay together.
21249
+ const EMOJI_REGEX = /\p{Regional_Indicator}{2}|(?:\p{Emoji_Presentation}|[\p{Extended_Pictographic}--\p{Emoji_Presentation}]\uFE0F)(?:[\p{Emoji_Modifier}\uFE0F]|\u200D\p{Extended_Pictographic}\uFE0F?)*/gv;
21250
+
21251
+ /**
21252
+ * What `emojiAsIcon` does, for something that renders text without going
21253
+ * through `Text` (a callout's message): every emoji in the string children
21254
+ * comes back wrapped in an `Icon`. The system emoji fonts have a taller
21255
+ * ascent/descent than text fonts, so a raw emoji glyph makes its line taller
21256
+ * than the lines around it; inside an Icon it is capped at 1em and centered on
21257
+ * the line like any glyph icon.
21258
+ *
21259
+ * Children come back in the shape they arrived in: a string stays a string
21260
+ * when it holds no emoji, and the array is only built once a child actually
21261
+ * needs rewriting.
21262
+ */
21263
+ const renderEmojiAsIcon = children => {
21264
+ if (typeof children === "string") {
21265
+ return renderEmojiInString(children);
21266
+ }
21267
+ const childArray = toChildArray(children);
21268
+ let result = null;
21269
+ let index = 0;
21270
+ for (const child of childArray) {
21271
+ if (typeof child === "string") {
21272
+ const rendered = renderEmojiInString(child);
21273
+ if (rendered !== child) {
21274
+ if (result === null) {
21275
+ result = childArray.slice(0, index);
21276
+ }
21277
+ for (const part of rendered) {
21278
+ result.push(part);
21279
+ }
21280
+ index++;
21281
+ continue;
21282
+ }
21283
+ }
21284
+ if (result !== null) {
21285
+ result.push(child);
21286
+ }
21287
+ index++;
21288
+ }
21289
+ if (result === null) {
21290
+ return children;
21291
+ }
21292
+ return result;
21293
+ };
21294
+ const renderEmojiInString = string => {
21295
+ let parts = null;
21296
+ let lastIndex = 0;
21297
+ for (const match of string.matchAll(EMOJI_REGEX)) {
21298
+ if (parts === null) {
21299
+ parts = [];
21300
+ }
21301
+ if (match.index > lastIndex) {
21302
+ parts.push(string.slice(lastIndex, match.index));
21303
+ }
21304
+ parts.push(jsx(Icon, {
21305
+ decorative: false,
21306
+ children: jsx("span", {
21307
+ children: match[0]
21308
+ })
21309
+ }));
21310
+ lastIndex = match.index + match[0].length;
21311
+ }
21312
+ if (parts === null) {
21313
+ return string;
21314
+ }
21315
+ if (lastIndex < string.length) {
21316
+ parts.push(string.slice(lastIndex));
21317
+ }
21318
+ return parts;
21319
+ };
21320
+ // A callout message is free text like any other; see callout.jsx for why it
21321
+ // cannot import this itself.
21322
+ setCalloutMessageTextRenderer(renderEmojiAsIcon);
21323
+
20437
21324
  const LIGHT_ACCENT_ATTRIBUTE = "data-accent-light";
20438
21325
  const VERY_LIGHT_ACCENT_ATTRIBUTE = "data-accent-very-light";
20439
21326
  const DARK_CONTRAST_ATTRIBUTE = "data-accent-needs-dark-fg";
@@ -20577,105 +21464,6 @@ const useActionStatus = (action) => {
20577
21464
  };
20578
21465
  };
20579
21466
 
20580
- /**
20581
- * A variant of useLayoutEffect that accounts for ancestor <dialog>/<details>
20582
- * or popover visibility.
20583
- *
20584
- * Motivation: some effects (auto-scroll, measurement, focus) only make sense
20585
- * when the element is actually presented on screen. A plain useLayoutEffect
20586
- * fires on mount even when the component is inside a closed <dialog>, a
20587
- * collapsed <details>, or a hidden popover, where scroll and layout operations
20588
- * are no-ops.
20589
- *
20590
- * Behavior:
20591
- * - No <dialog>/<details>/[popover] ancestor → runs like a normal
20592
- * useLayoutEffect with the provided deps.
20593
- * - Inside a closed/hidden ancestor → skips the initial run; instead runs
20594
- * the callback once the ancestor opens — see @jsenv/dom's own
20595
- * observeAncestorOpenState for exactly how that's detected, and why it
20596
- * matters that it happens before the browser paints.
20597
- * - Inside an open ancestor → runs on mount AND every subsequent open.
20598
- *
20599
- * The callback's second argument is always a `navi_displayed` CustomEvent,
20600
- * with `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
20601
- * - No <dialog>/<details>/[popover]/[aria-expanded] ancestor at all →
20602
- * `{ ancestor: document, ancestorType: "document" }`.
20603
- * - Otherwise → `{ ancestor: <the matched element>, ancestorType: "dialog"
20604
- * | "popover" | "details" | "aria-expanded" }`.
20605
- * `becauseAncestorOpened` distinguishes the two ways of coming on screen:
20606
- * - true — the element was already mounted and the ancestor just opened,
20607
- * revealing it along with everything else it holds. The opening has an
20608
- * owner (the ancestor's own transferFocus/openEffect), and what it reveals
20609
- * should defer to it — see use_auto_focus.js.
20610
- * - false — the element was mounted just now, into a surface already on
20611
- * screen (or into the plain document). Nothing else owns this appearance:
20612
- * what the element says about itself (an autofocus, a measurement) is the
20613
- * only word there is.
20614
- *
20615
- * Usage:
20616
- * useDisplayedLayoutEffect(ref, () => {
20617
- * scrollToSelected();
20618
- * }, []);
20619
- */
20620
- const useDisplayedLayoutEffect = (ref, callback, deps) => {
20621
- if (typeof callback !== "function") {
20622
- throw new TypeError("useDisplayedLayoutEffect: callback is not a function");
20623
- }
20624
-
20625
- // Keep a stable ref so the open listener always calls the latest callback
20626
- // without needing to be re-registered when deps change.
20627
- const callbackRef = useRef(callback);
20628
- callbackRef.current = callback;
20629
-
20630
- // Run on mount (or when deps change) — but only if the element is visible.
20631
- useLayoutEffect(() => {
20632
- const el = ref.current;
20633
- if (!el) {
20634
- return;
20635
- }
20636
- const ancestor = closestOpenableAncestor(el);
20637
- if (!ancestor) {
20638
- callbackRef.current(el, createDisplayedEvent(document, false));
20639
- return;
20640
- }
20641
- if (!isAncestorOpen(ancestor)) {
20642
- // Ancestor is closed — skip now; the observeAncestorOpenState call
20643
- // below will fire once it opens.
20644
- return;
20645
- }
20646
- callbackRef.current(el, createDisplayedEvent(ancestor, false));
20647
- }, deps);
20648
-
20649
- // Re-run every time the ancestor opens.
20650
- useLayoutEffect(() => {
20651
- const el = ref.current;
20652
- if (!el) {
20653
- return undefined;
20654
- }
20655
- const ancestor = closestOpenableAncestor(el);
20656
- if (!ancestor) {
20657
- return undefined;
20658
- }
20659
- return observeAncestorOpenState(ancestor, ({ isOpen }) => {
20660
- if (!isOpen) {
20661
- return;
20662
- }
20663
- const lastEl = ref.current;
20664
- callbackRef.current(lastEl, createDisplayedEvent(ancestor, true));
20665
- });
20666
- }, []);
20667
- };
20668
-
20669
- const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
20670
- return new CustomEvent("navi_displayed", {
20671
- detail: {
20672
- ancestor,
20673
- ancestorType: getAncestorOpenType(ancestor),
20674
- becauseAncestorOpened,
20675
- },
20676
- });
20677
- };
20678
-
20679
21467
  /**
20680
21468
  * Decides which element receives focus when a container (popover, dialog, …)
20681
21469
  * opens, and gives it back to where it came from when the container closes.
@@ -23467,116 +24255,6 @@ registerNaviCommand("--navi-unselect", (source, event) => {
23467
24255
  };
23468
24256
  });
23469
24257
 
23470
- // prop that we'll set on the control
23471
- const CONTROL_ATTRIBUTE_SET = new Set([
23472
- ...CONSTRAINT_ATTRIBUTE_SET,
23473
-
23474
- "ref",
23475
- "children",
23476
- "id",
23477
- "name",
23478
- "type",
23479
- "value",
23480
- "checked",
23481
- "placeholder",
23482
- "inputMode",
23483
- "autoComplete",
23484
- "spellcheck",
23485
- "autoCorrect",
23486
- "aria-controls",
23487
- "tabIndex",
23488
- "command",
23489
- "commandFor",
23490
- "command-value", // not standard but make sense, allow to give param to the command in question
23491
- "list",
23492
-
23493
- // "ui-action-target",
23494
- "navi-input-type",
23495
- "navi-value-pad",
23496
- "navi-control-proxy-for",
23497
- "navi-command-proxy-for",
23498
- "navi-command-target",
23499
- "onnavi_command",
23500
- "onnavi_request_open",
23501
- "onnavi_request_close",
23502
-
23503
- "data-callout-arrow-x",
23504
- "data-callout-point-to-border-box",
23505
- "data-callout-point-to-content-box",
23506
- "data-callout-viewport-spacing",
23507
- "data-callout-position",
23508
- "data-callout-position-fixed",
23509
-
23510
- "data-testid", // playwright, cypress
23511
- "data-separator", // used by InputGroup paste-to-fill
23512
- ]);
23513
- // prop concerning control but that won't end up in the DOM if not inside CONTROL_ATTRIBUTE_SET
23514
- const CONTROL_PROP_SET = new Set([
23515
- ...CONTROL_ATTRIBUTE_SET,
23516
- ...CONSTRAINT_MESSAGE_PROP_NAME_SET,
23517
-
23518
- "action",
23519
- "confirm",
23520
- "confirmPopupContent",
23521
- "actionEvent",
23522
- "actionAfterChange",
23523
- "actionOnMouseDown",
23524
- "actionDebounce",
23525
- // A signal bound two-way to the control: its value seeds the control's state and
23526
- // is written back on every uiAction. Precludes value/checked (see createControlInfo).
23527
- "signal",
23528
- "defaultValue",
23529
- "defaultChecked",
23530
- "readOnly", // will depend wether readOnly is supported
23531
-
23532
- "loading",
23533
- "basePseudoState",
23534
- "constraints",
23535
-
23536
- "autoFocus",
23537
- "autoFocusVisible",
23538
- "autoFocusSelect",
23539
-
23540
- "onMouseDown",
23541
- "onClick",
23542
- "onKeyDown",
23543
- "onPaste",
23544
- "onInput",
23545
- "eventReactionDefinitions",
23546
-
23547
- "onCancel",
23548
- "cancelOnBlurInvalid",
23549
- "cancelOnEscape",
23550
- "onActionPrevented",
23551
- "onActionStart",
23552
- "onActionAborted",
23553
- "onActionError",
23554
- "actionErrorEffect",
23555
- "errorMapping",
23556
- "onActionEnd",
23557
-
23558
- "resetOnCancel",
23559
- "resetOnAbort",
23560
- "resetOnError",
23561
- "optimistic",
23562
-
23563
- "charGuard",
23564
- "maxLengthGuard",
23565
- ]);
23566
-
23567
- const MessagePropsRefContext = createContext();
23568
-
23569
- const ControlIdContext = createContext();
23570
- const ControlNameContext = createContext();
23571
- const DisabledContext = createContext();
23572
- const ReadOnlyContext = createContext();
23573
- const RequiredContext = createContext();
23574
- const LoadingContext$1 = createContext();
23575
- createContext();
23576
-
23577
- const ActionContext = createContext();
23578
- const ActionRequesterContext = createContext();
23579
-
23580
24258
  /**
23581
24259
  * How a control tells the labels pointing at it what it is (disabled, readOnly,
23582
24260
  * required) and when it goes away.
@@ -25798,18 +26476,18 @@ const useUIFacadeStateController = (props, realUIStateController) => {
25798
26476
  if (child !== firstChildControllerRef.current) {
25799
26477
  return;
25800
26478
  }
25801
- if (
25802
- silent &&
25803
- uiStateHoldsNothing(child.uiState) &&
25804
- !uiStateHoldsNothing(s.realUIStateController.uiState)
25805
- ) {
26479
+ if (silent && uiStateHoldsNothing(child.uiState)) {
25806
26480
  // A silent sync means the child's own structure changed (children
25807
26481
  // mounted/unmounted), not that the user acted. A child that ends up
25808
26482
  // with no value there is one that currently *cannot* express one —
25809
26483
  // a <List loading> holds no items yet, a popup whose items are gone
25810
26484
  // aggregates to the empty array its stateType falls back to — which
25811
26485
  // must not read as the user clearing the picker, nor fire its
25812
- // uiAction.
26486
+ // uiAction. And when the picker holds nothing either, there is
26487
+ // still nothing to adopt: the sync would only respell one nothing
26488
+ // as another (an array picker's [] becoming undefined, say) and
26489
+ // hand that to uiAction — an empty array picker opened its popup
26490
+ // and told its owner the value changed.
25813
26491
  return;
25814
26492
  }
25815
26493
  updatingRef.current = true;
@@ -25822,7 +26500,18 @@ const useUIFacadeStateController = (props, realUIStateController) => {
25822
26500
  detail: {},
25823
26501
  });
25824
26502
  chainEvent(propagateUpEvent, e);
25825
- s.realUIStateController.setUIState(child.uiState, propagateUpEvent);
26503
+ // What the popup aggregates arrives in the popup's terms, where an
26504
+ // empty multiple list is `undefined`. The picker answers a question of
26505
+ // its own shape (see resolveEmptyUIState): a <Picker type="array">
26506
+ // whose last item was unselected holds [], the same thing clearing it
26507
+ // leaves — not a value that changes type on its owner the moment it
26508
+ // empties.
26509
+ const { emptyUIState } = s.realUIStateController;
26510
+ const uiStateToAdopt =
26511
+ child.uiState === undefined && emptyUIState !== undefined
26512
+ ? emptyUIState
26513
+ : child.uiState;
26514
+ s.realUIStateController.setUIState(uiStateToAdopt, propagateUpEvent);
25826
26515
  updatingRef.current = false;
25827
26516
  },
25828
26517
  };
@@ -26732,11 +27421,29 @@ const useControlProps = (props, {
26732
27421
  }
26733
27422
  return dispatched;
26734
27423
  };
26735
- const applyEventReaction = (eventName, e) => {
27424
+
27425
+ // What the caller wrote runs from inside the gate when this control is an
27426
+ // own target: a plain `onClick` is DOM, and it would fire from a cross drawn
27427
+ // greyed by the read-only control the affordance sits in (see own_target.js).
27428
+ const callerHandlerIsGated = Boolean(props.ownTarget);
27429
+ const gateCallerHandler = (handler, e) => {
27430
+ if (!handler) {
27431
+ return undefined;
27432
+ }
27433
+ if (callerHandlerIsGated) {
27434
+ return handler;
27435
+ }
27436
+ handler(e);
27437
+ return undefined;
27438
+ };
27439
+ const applyEventReaction = (eventName, e, callerHandler) => {
26736
27440
  const defaultEventReactionDefinition = defaultEventReactionDefinitions?.[eventName];
26737
27441
  const customEventReactionDefinition = eventReactionDefinitions?.[eventName];
26738
27442
  const reaction = customEventReactionDefinition?.(e) ?? defaultEventReactionDefinition?.(e);
26739
27443
  if (!reaction) {
27444
+ // No reaction means no gate: there is nothing here that could refuse the
27445
+ // caller's handler, so withholding it would only lose it.
27446
+ callerHandler?.(e);
26740
27447
  return false;
26741
27448
  }
26742
27449
  const {
@@ -26761,19 +27468,18 @@ const useControlProps = (props, {
26761
27468
  prevented?.();
26762
27469
  },
26763
27470
  allowed: () => {
27471
+ callerHandler?.(e);
26764
27472
  allowed?.();
26765
27473
  },
26766
27474
  always
26767
27475
  });
26768
27476
  };
26769
27477
  const onMouseDown = e => {
26770
- props.onMouseDown?.(e);
26771
- applyEventReaction("mouseDown", e);
27478
+ applyEventReaction("mouseDown", e, gateCallerHandler(props.onMouseDown, e));
26772
27479
  transferFocusToTarget(e);
26773
27480
  };
26774
27481
  const onClick = e => {
26775
- props.onClick?.(e);
26776
- applyEventReaction("click", e);
27482
+ applyEventReaction("click", e, gateCallerHandler(props.onClick, e));
26777
27483
  transferFocusToTarget(e);
26778
27484
  };
26779
27485
  const onKeyDown = e => {
@@ -27296,6 +28002,13 @@ const useInteractiveProps = (props, {
27296
28002
  } = props;
27297
28003
  const [controlRootProps, controlHostProps] = splitControlProps(props);
27298
28004
  controlRootProps["navi-control"] = controlInfo.controlType;
28005
+ if (props.ownTarget) {
28006
+ // One attribute, in the DOM, because that is where the claim is read from —
28007
+ // by the controls above and by the gesture readers below (see
28008
+ // own_target.js). The prop is the ergonomic form of it and nothing more: an
28009
+ // element an application draws itself writes the same attribute by hand.
28010
+ controlRootProps[OWN_TARGET_ATTRIBUTE] = typeof props.ownTarget === "string" ? props.ownTarget : "";
28011
+ }
27299
28012
  const {
27300
28013
  "navi-control-proxy-for": naviProxyFor
27301
28014
  } = props;
@@ -27330,10 +28043,10 @@ const useInteractiveProps = (props, {
27330
28043
  });
27331
28044
  }
27332
28045
  {
27333
- const controlDisabled = useContext(DisabledContext);
27334
- const controlReadOnly = useContext(ReadOnlyContext);
28046
+ const controlDisabledFromAbove = useContext(DisabledContext);
28047
+ const controlReadOnlyFromAbove = useContext(ReadOnlyContext);
27335
28048
  const controlRequired = useContext(RequiredContext);
27336
- const controlLoading = useContext(LoadingContext$1);
28049
+ const controlLoadingFromAbove = useContext(LoadingContext$1);
27337
28050
  const parentActionRequester = useContext(ActionRequesterContext);
27338
28051
  const actionStatus = useActionStatus(boundAction);
27339
28052
  const {
@@ -27343,6 +28056,16 @@ const useInteractiveProps = (props, {
27343
28056
  loading,
27344
28057
  optimistic
27345
28058
  } = props;
28059
+
28060
+ // `ownTarget="always"`: an affordance that writes nothing to the control it
28061
+ // sits in has no business inheriting that control's state — a diskette
28062
+ // saving a row into the reader's own address book stays pressable on a game
28063
+ // nobody may edit. Its own props still hold; only what came from above is
28064
+ // dropped (see own_target.js for the three modes).
28065
+ const zoneStateApplies = !ownTargetIgnoresZoneState(props.ownTarget);
28066
+ const controlDisabled = zoneStateApplies && controlDisabledFromAbove;
28067
+ const controlReadOnly = zoneStateApplies && controlReadOnlyFromAbove;
28068
+ const controlLoading = zoneStateApplies && controlLoadingFromAbove;
27346
28069
  const disabledResolved = disabled || controlDisabled;
27347
28070
  const requiredResolved = required || controlRequired;
27348
28071
  // Busy because the group above is running the action THIS control asked
@@ -27354,7 +28077,7 @@ const useInteractiveProps = (props, {
27354
28077
  // (`maxLengthGuard`) and this one would make it longer: it can be pointed
27355
28078
  // at, focused and pressed — and answers why (see readonly_constraint.js) —
27356
28079
  // but cannot be taken.
27357
- const readOnlyFromParentMaxLengthGuard = Boolean(uiStateController.parentUIStateController?.isChildBlockedByMaxLengthGuard?.(uiStateController));
28080
+ const readOnlyFromParentMaxLengthGuard = Boolean(zoneStateApplies && uiStateController.parentUIStateController?.isChildBlockedByMaxLengthGuard?.(uiStateController));
27358
28081
  const readOnlyBase = readOnly || controlReadOnly || loadingBase || readOnlyFromParentMaxLengthGuard || controlInfo.readOnlyUncontrolled;
27359
28082
  // An optimistic control trusts its action to succeed: the state the user
27360
28083
  // just set stays visible and interactive while the action runs — no
@@ -27788,7 +28511,7 @@ const getAssociatedLabels = element => {
27788
28511
  return Array.from(element.labels);
27789
28512
  };
27790
28513
 
27791
- installImportMetaCssBuild(import.meta);const css$$ = /* css */`
28514
+ installImportMetaCssBuild(import.meta);const css$Z = /* css */`
27792
28515
  @layer navi {
27793
28516
  .navi_button {
27794
28517
  --button-border-radius: var(--navi-control-border-radius);
@@ -28219,7 +28942,7 @@ installImportMetaCssBuild(import.meta);const css$$ = /* css */`
28219
28942
  }
28220
28943
  `;
28221
28944
  const ButtonUI = props => {
28222
- import.meta.css = [css$$, "@jsenv/navi/src/control/input/button_ui.jsx"];
28945
+ import.meta.css = [css$Z, "@jsenv/navi/src/control/input/button_ui.jsx"];
28223
28946
  const {
28224
28947
  ref,
28225
28948
  // href/link
@@ -28232,6 +28955,7 @@ const ButtonUI = props => {
28232
28955
  icon,
28233
28956
  cta,
28234
28957
  spacing,
28958
+ emojiAsIcon = true,
28235
28959
  // Whether the button draws the loading outline itself. A button that is
28236
28960
  // one half of a bigger control says no: what is busy is the control, and
28237
28961
  // the outline belongs around the whole of it (see split_button.jsx).
@@ -28289,6 +29013,7 @@ const ButtonUI = props => {
28289
29013
 
28290
29014
  type: "button",
28291
29015
  spacing: undefined,
29016
+ emojiAsIcon: undefined,
28292
29017
  cta: undefined,
28293
29018
  pressEffect: undefined,
28294
29019
  loadingOutline: undefined,
@@ -28342,6 +29067,7 @@ const ButtonUI = props => {
28342
29067
  ...controlChildrenWrapperProps,
28343
29068
  children: jsx(ButtonContent, {
28344
29069
  spacing: spacing,
29070
+ emojiAsIcon: emojiAsIcon,
28345
29071
  children: children
28346
29072
  })
28347
29073
  })]
@@ -28349,6 +29075,7 @@ const ButtonUI = props => {
28349
29075
  };
28350
29076
  const ButtonContent = ({
28351
29077
  spacing,
29078
+ emojiAsIcon,
28352
29079
  children
28353
29080
  }) => {
28354
29081
  const boxForwardedProps = useContext(BoxForwardedPropsContext);
@@ -28356,6 +29083,7 @@ const ButtonContent = ({
28356
29083
  ...boxForwardedProps,
28357
29084
  display: "inherit",
28358
29085
  spacing: spacing,
29086
+ emojiAsIcon: emojiAsIcon,
28359
29087
  className: "navi_button_content",
28360
29088
  children: [children, jsx(ButtonShadow, {})]
28361
29089
  });
@@ -28412,6 +29140,7 @@ const ButtonFirstResolver = props => {
28412
29140
  const Next = useNextResolver();
28413
29141
  const defaultRef = useRef(null);
28414
29142
  props.ref = props.ref || defaultRef;
29143
+ const ownTargetHidden = useOwnTargetHidden(props);
28415
29144
 
28416
29145
  // Attached to the element rather than kept as a prop: the action a button
28417
29146
  // requests is not always run by the button (a submit button hands the send to
@@ -28421,6 +29150,9 @@ const ButtonFirstResolver = props => {
28421
29150
  message: props.confirm,
28422
29151
  content: props.confirmPopupContent
28423
29152
  });
29153
+ if (ownTargetHidden) {
29154
+ return null;
29155
+ }
28424
29156
  return jsx(Next, {
28425
29157
  ...props
28426
29158
  });
@@ -28511,6 +29243,26 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
28511
29243
  children: naviI18n("button.open")
28512
29244
  })
28513
29245
  };
29246
+
29247
+ /**
29248
+ * @type {import("ignore:preact").FunctionComponent<{
29249
+ * ownTarget?: boolean | "refuse" | "always",
29250
+ * emojiAsIcon?: boolean,
29251
+ * [key: string]: any,
29252
+ * }>}
29253
+ * @param {boolean} [emojiAsIcon=true] Renders the emoji of the label as icons
29254
+ * so the button keeps the height of its text — `Text`'s prop, on by default
29255
+ * here. Pass `false` to let an emoji draw at its natural size.
29256
+ * @param {boolean|"refuse"|"always"} [ownTarget] A real target inside a zone
29257
+ * that belongs to another control — a chip's cross on a picker's façade, an
29258
+ * eye on a pressable row, a diskette inside a slide that travels. The press is
29259
+ * this button's alone (no travel starts, no popup opens, no navi control above
29260
+ * answers) and its `onClick` waits for its own interaction gate instead of
29261
+ * firing from the DOM. What it does where the zone is read-only, disabled or
29262
+ * busy depends on whether it WRITES to the control it sits in: it goes by
29263
+ * default, `"refuse"` keeps it and refuses with a callout, `"always"` ignores
29264
+ * the zone's state entirely — for a gesture that never touched that control.
29265
+ */
28514
29266
  const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
28515
29267
 
28516
29268
  // How long a popup waits before handing the focus to a field, when giving it
@@ -29930,7 +30682,7 @@ installImportMetaCssBuild(import.meta);/**
29930
30682
  * reaches the real container.
29931
30683
  */
29932
30684
  let openLocalDialogCount = 0;
29933
- const css$_ = /* css */`
30685
+ const css$Y = /* css */`
29934
30686
  @layer navi {
29935
30687
  .navi_dialog {
29936
30688
  /* Min gap between the dialog and the edges of its container. Written
@@ -30071,6 +30823,10 @@ const css$_ = /* css */`
30071
30823
  padding: 0;
30072
30824
  flex-direction: column;
30073
30825
 
30826
+ /* A new surface writes in its own ink, not in its container's — see
30827
+ --navi-popup-color. Declared here rather than left to the UA's own
30828
+ dialog { color: CanvasText } so the ink is themed along with the paper. */
30829
+ color: var(--navi-popup-color);
30074
30830
  background-color: var(--dialog-background-color);
30075
30831
  border-width: var(--dialog-border-width);
30076
30832
  border-style: solid;
@@ -30514,7 +31270,7 @@ const css$_ = /* css */`
30514
31270
  * @param {import("ignore:preact").ComponentChildren} props.children
30515
31271
  */
30516
31272
  const Dialog = props => {
30517
- import.meta.css = [css$_, "@jsenv/navi/src/layout/dialog.jsx"];
31273
+ import.meta.css = [css$Y, "@jsenv/navi/src/layout/dialog.jsx"];
30518
31274
  if (props.openController) {
30519
31275
  return jsx(ControlledDialog, {
30520
31276
  ...props
@@ -31506,7 +32262,7 @@ installImportMetaCssBuild(import.meta);/**
31506
32262
  * and applied.
31507
32263
  */
31508
32264
  let openLocalPopoverCount = 0;
31509
- const css$Z = /* css */`
32265
+ const css$X = /* css */`
31510
32266
  @layer navi {
31511
32267
  .navi_popover {
31512
32268
  /* soft: user-configurable preferred max-height. Kept as a *default*
@@ -31600,6 +32356,10 @@ const css$Z = /* css */`
31600
32356
  var(--x-popover-max-height)
31601
32357
  );
31602
32358
  max-height: var(--x-popover-max-height);
32359
+ /* A new surface writes in its own ink, not in its container's — see
32360
+ --navi-popup-color. The UA only resets color on a [popover] element,
32361
+ and the local renderer is a plain div. */
32362
+ color: var(--navi-popup-color);
31603
32363
  background-color: var(--popover-background-color);
31604
32364
  border-width: var(--popover-border-width);
31605
32365
  border-style: solid;
@@ -31962,7 +32722,7 @@ const css$Z = /* css */`
31962
32722
  * @param {import("ignore:preact").ComponentChildren} props.children
31963
32723
  */
31964
32724
  const Popover = props => {
31965
- import.meta.css = [css$Z, "@jsenv/navi/src/layout/popover.jsx"];
32725
+ import.meta.css = [css$X, "@jsenv/navi/src/layout/popover.jsx"];
31966
32726
  if (props.openController) {
31967
32727
  return jsx(ControlledPopover, {
31968
32728
  ...props
@@ -32990,7 +33750,7 @@ installImportMetaCssBuild(import.meta);/**
32990
33750
  * event, and a caller replacing the body entirely then has one protocol to
32991
33751
  * follow — `--navi-confirm` for yes, anything that closes for no.
32992
33752
  */
32993
- const css$Y = /* css */`
33753
+ const css$W = /* css */`
32994
33754
  /* The width lives on the body rather than on the popup, so that custom
32995
33755
  content (which replaces this body entirely) sizes itself instead of
32996
33756
  inheriting a ceiling meant for a sentence-long question. */
@@ -33127,7 +33887,7 @@ const ConfirmPopup = ({
33127
33887
  onAnswer,
33128
33888
  onClosed
33129
33889
  }) => {
33130
- import.meta.css = [css$Y, "@jsenv/navi/src/action/confirm_popup.jsx"];
33890
+ import.meta.css = [css$W, "@jsenv/navi/src/action/confirm_popup.jsx"];
33131
33891
  const {
33132
33892
  mode,
33133
33893
  confirmLabel,
@@ -33211,7 +33971,7 @@ const defaultBody = (message, {
33211
33971
  });
33212
33972
  };
33213
33973
 
33214
- installImportMetaCssBuild(import.meta);const css$X = /* css */`
33974
+ installImportMetaCssBuild(import.meta);const css$V = /* css */`
33215
33975
  .action_error {
33216
33976
  margin-top: 0;
33217
33977
  margin-bottom: 20px;
@@ -33236,7 +33996,7 @@ const ActionRenderer = ({
33236
33996
  children,
33237
33997
  disabled
33238
33998
  }) => {
33239
- import.meta.css = [css$X, "@jsenv/navi/src/action/action_renderer.jsx"];
33999
+ import.meta.css = [css$V, "@jsenv/navi/src/action/action_renderer.jsx"];
33240
34000
  if (action === undefined) {
33241
34001
  throw new Error("ActionRenderer requires an action to render, but none was provided.");
33242
34002
  }
@@ -38861,7 +39621,7 @@ const ROUTE_TRAVEL_ATTRIBUTE = "data-navi-route-travel";
38861
39621
  // the root pictures must NOT move (they carry the whole viewport, blank bands
38862
39622
  // included).
38863
39623
 
38864
- const css$W = /* css */`
39624
+ const css$U = /* css */`
38865
39625
  /* The marked region is a picture of its own for the length of a transition of
38866
39626
  OURS, and only then — the name is what makes the pages a picture the
38867
39627
  movement below can carry.
@@ -39290,7 +40050,7 @@ const RouteTransitionArea = ({
39290
40050
  children,
39291
40051
  ...rest
39292
40052
  }) => {
39293
- import.meta.css = [css$W, "@jsenv/navi/src/nav/route_transition.jsx"];
40053
+ import.meta.css = [css$U, "@jsenv/navi/src/nav/route_transition.jsx"];
39294
40054
  const props = {
39295
40055
  ...rest,
39296
40056
  [TRANSITION_AREA_ATTRIBUTE]: ""
@@ -39347,7 +40107,7 @@ const RouteTransitionArea = ({
39347
40107
  * @returns {() => void} remove this relation.
39348
40108
  */
39349
40109
  const defineRouteTransition = (from, to, transition) => {
39350
- import.meta.css = [css$W, "@jsenv/navi/src/nav/route_transition.jsx"];
40110
+ import.meta.css = [css$U, "@jsenv/navi/src/nav/route_transition.jsx"];
39351
40111
  const {
39352
40112
  type,
39353
40113
  duration
@@ -39383,7 +40143,7 @@ const defineRouteTransition = (from, to, transition) => {
39383
40143
  * @returns {() => void} remove this default.
39384
40144
  */
39385
40145
  const defineRouteDefaultTransition = transition => {
39386
- import.meta.css = [css$W, "@jsenv/navi/src/nav/route_transition.jsx"];
40146
+ import.meta.css = [css$U, "@jsenv/navi/src/nav/route_transition.jsx"];
39387
40147
  const value = normalizeTransition(transition);
39388
40148
  defaultTransition = value;
39389
40149
  return () => {
@@ -39984,7 +40744,7 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
39984
40744
  const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
39985
40745
  // The name the box wears while it travels, and only then (see nameForTravel).
39986
40746
  const TRAVEL_NAME = "navi-route-travel";
39987
- const css$V = /* css */`
40747
+ const css$T = /* css */`
39988
40748
  /* The name that makes the page inside this box a picture of its own during a
39989
40749
  transition — rather than part of the one big picture the document takes, so
39990
40750
  the two pages can move past each other while everything else stays where it
@@ -40374,7 +41134,7 @@ const RouteTravel = ({
40374
41134
  children,
40375
41135
  ...rest
40376
41136
  }) => {
40377
- import.meta.css = [css$V, "@jsenv/navi/src/nav/route_travel.jsx"];
41137
+ import.meta.css = [css$T, "@jsenv/navi/src/nav/route_travel.jsx"];
40378
41138
  const elementRef = useRef();
40379
41139
  const gestureRef = useRef(null);
40380
41140
  // The travel in hand: the transition keeping the picture of the page being
@@ -42935,402 +43695,6 @@ const PhoneSvg = () => {
42935
43695
  });
42936
43696
  };
42937
43697
 
42938
- installImportMetaCssBuild(import.meta);// # TextAnchor — how it works
42939
- const css$U = /* css */`
42940
- .navi_text_anchor {
42941
- vertical-align: baseline;
42942
- user-select: none;
42943
- overflow: hidden;
42944
- }
42945
- `;
42946
-
42947
- /**
42948
- * Positions children vertically relative to the surrounding text, correcting for font-size differences.
42949
- *
42950
- * Place this component around any inline element whose font-size differs from the surrounding text.
42951
- * It renders an invisible anchor that inherits the surrounding text's font metrics, then shifts
42952
- * the child so that its visual position matches the requested `textAnchor` value — regardless of
42953
- * font-size, display type (inline, inline-block, inline-flex…), or the active `vertical-align`.
42954
- *
42955
- * @param {"line-top"|"char-top"|"center"|"char-bottom"|"line-bottom"} [textAnchor="char-bottom"]
42956
- * - `"line-top"` — child top aligns with the top of the surrounding line box
42957
- * - `"char-top"` — child top aligns with the top of visible characters (ink ascent)
42958
- * - `"center"` — child is vertically centered on the surrounding line box
42959
- * - `"char-bottom"` — child bottom aligns to the text baseline (no correction, browser default)
42960
- * - `"line-bottom"` — child bottom aligns with the bottom of the surrounding line box
42961
- * @param {{ size?: number, verticalAlign?: string }} [lineLayout]
42962
- * Describes the surrounding line context. Used as layout-effect dependencies so the correction
42963
- * reruns when the surrounding text's font-size or vertical-align changes.
42964
- * @param {import("ignore:preact").RefObject} childRef — ref on the child element to reposition
42965
- */
42966
- const TextAnchor = ({
42967
- childRef,
42968
- children,
42969
- textAnchor = "char-bottom",
42970
- textKey,
42971
- textSize,
42972
- lineLayout
42973
- }) => {
42974
- import.meta.css = [css$U, "@jsenv/navi/src/text/text_anchor.jsx"];
42975
- const anchorRef = useRef();
42976
-
42977
- // Plain useLayoutEffect would also fire while an ancestor dialog/popover
42978
- // (e.g. a closed SidePanel) is still display:none — every rect involved
42979
- // reads 0×0 at that point, so the math trivially (and wrongly) resolves
42980
- // to topOffset 0: not a real "no correction needed" result, just a
42981
- // zero-by-zero coincidence that happens to look fine only because it
42982
- // leaves the browser's own default alignment untouched. The real
42983
- // correction then only gets applied later, on whatever unrelated
42984
- // re-render next happens to change one of this effect's own deps —
42985
- // which reads as the child "jumping" even though nothing about its own
42986
- // geometry changed. useDisplayedLayoutEffect skips the initial run in
42987
- // that case and reruns once the ancestor actually opens instead.
42988
- useDisplayedLayoutEffect(anchorRef, anchorEl => {
42989
- const childEl = childRef.current;
42990
- if (!anchorEl || !childEl) {
42991
- return;
42992
- }
42993
- // Only correct when the anchor lives in an inline formatting context.
42994
- // If the parent is a flex/grid container, inline layout rules don't apply
42995
- // and our font-metrics model is invalid.
42996
- const parentDisplay = getComputedStyle(anchorEl.parentElement).display;
42997
- if (parentDisplay !== "inline" && parentDisplay !== "inline-block" && parentDisplay !== "block") {
42998
- // we must hide the anchor otherwise it would affect layout without providing any benefit (would trigger flex gap for instance)
42999
- anchorEl.setAttribute("hidden", "");
43000
- setTopOffset(childEl, 0);
43001
- return;
43002
- }
43003
- anchorEl.removeAttribute("hidden");
43004
- const topOffset = computeTopOffset({
43005
- anchorEl,
43006
- childEl,
43007
- textAnchor
43008
- });
43009
- setTopOffset(childEl, topOffset);
43010
- }, [textAnchor, textKey, textSize, lineLayout?.size, lineLayout?.verticalAlign]);
43011
- return jsxs(Fragment$1, {
43012
- children: [children, jsx("span", {
43013
- ref: anchorRef,
43014
- className: "navi_text_anchor",
43015
- "aria-hidden": "true",
43016
- children: "\u200B"
43017
- })]
43018
- });
43019
- };
43020
- const setTopOffset = (childEl, topOffset) => {
43021
- // position:relative + top shifts the element visually.
43022
- // marginTop: -topOffset makes the layout box follow the visual position, so any container
43023
- // (button, link, box…) computes its own padding/border/height based on the real final position
43024
- // rather than the original unshifted one. This means a badge inside a button will symmetrically
43025
- // expand the button height instead of overflowing or being clipped.
43026
- // marginBottom: topOffset compensates the marginTop so the line height stays unchanged —
43027
- // the shift is purely a repositioning, not an inflation of the line.
43028
- if (!topOffset) {
43029
- childEl.style.position = "";
43030
- childEl.style.top = "";
43031
- childEl.style.marginTop = "";
43032
- childEl.style.marginBottom = "";
43033
- return;
43034
- }
43035
- childEl.style.position = "relative";
43036
- childEl.style.top = `${topOffset}px`;
43037
- childEl.style.marginTop = `${-topOffset}px`;
43038
- childEl.style.marginBottom = `${topOffset}px`;
43039
- };
43040
- const computeTopOffset = ({
43041
- anchorEl,
43042
- childEl,
43043
- textAnchor
43044
- }) => {
43045
- if (textAnchor === "char-bottom") {
43046
- // Align child's bottom with the char's bottom = the baseline.
43047
- // The CSS spec says an inline-block with no text content has its baseline at its bottom margin edge.
43048
- // So the browser's default placement already puts the child's bottom at the line's baseline.
43049
- // No correction needed.
43050
- return 0;
43051
- }
43052
- // The anchor's rendered rect corresponds to the surrounding text's line box:
43053
- // top and bottom are the visual bounds of the line (including line-height).
43054
- const anchorRect = anchorEl.getBoundingClientRect();
43055
-
43056
- // Measure the child's current rect, then subtract any previously applied top correction
43057
- // to recover its natural position — avoiding a style reset + reflow.
43058
- const childRect = childEl.getBoundingClientRect();
43059
- const childH = childRect.height;
43060
- const previousTop = parseFloat(childEl.style.top) || 0;
43061
- const childNaturalTop = childRect.top - previousTop;
43062
-
43063
- // Compute desired child top Y based on textAnchor intention.
43064
- let desiredChildTopY = 0;
43065
- if (textAnchor === "line-top") {
43066
- desiredChildTopY = anchorRect.top;
43067
- } else if (textAnchor === "char-top") {
43068
- const anchorStyle = getComputedStyle(anchorEl);
43069
- const ctx = charTopCanvas.getContext("2d");
43070
- ctx.font = `${anchorStyle.fontWeight} ${anchorStyle.fontSize} ${anchorStyle.fontFamily}`;
43071
- const m = ctx.measureText("M");
43072
- const baselineY = anchorRect.bottom - m.fontBoundingBoxDescent;
43073
- desiredChildTopY = baselineY - m.actualBoundingBoxAscent;
43074
- } else if (textAnchor === "center") {
43075
- const anchorCenterY = (anchorRect.top + anchorRect.bottom) / 2;
43076
- desiredChildTopY = anchorCenterY - childH / 2;
43077
- } else if (textAnchor === "char-bottom") {
43078
- // Already handled above (early return 0), but guard here for completeness.
43079
- return 0;
43080
- } else if (textAnchor === "line-bottom") {
43081
- desiredChildTopY = anchorRect.bottom - childH;
43082
- } else {
43083
- return 0; // unknown textAnchor, no correction
43084
- }
43085
- return desiredChildTopY - childNaturalTop;
43086
- };
43087
- const charTopCanvas = document.createElement("canvas");
43088
-
43089
- installImportMetaCssBuild(import.meta);const css$T = /* css */`
43090
- @layer navi {
43091
- /* Ensure data attributes from box.jsx can win to update display */
43092
- .navi_icon {
43093
- display: inline-flex;
43094
- box-sizing: border-box;
43095
- max-width: 100%;
43096
- /* An icon never grows past the box it sits in, so a glyph can never make
43097
- a line of text taller than the text itself. lineOverflow="allow" opts
43098
- out, for an icon that is an affordance rather than a character — a
43099
- control's chevron or clear button, sized to be touched, not read. */
43100
- max-height: 100%;
43101
-
43102
- &[data-line-overflow="allow"] {
43103
- max-height: none;
43104
- }
43105
- }
43106
- }
43107
-
43108
- .navi_icon {
43109
- white-space: nowrap;
43110
- vertical-align: inherit;
43111
-
43112
- &[data-icon-char] {
43113
- aspect-ratio: 1/1;
43114
- min-width: 0;
43115
- height: round(1em, 1px);
43116
- max-height: round(1em, 1px);
43117
- flex-grow: 0 !important;
43118
- align-items: center;
43119
- justify-content: center;
43120
-
43121
- /* fillLine: measured on the line box (1lh) instead of the character box
43122
- (1em). The icon still stays inside the line — it just uses all of it,
43123
- which is what an icon standing on its own in a control's slot wants,
43124
- where a glyph sitting among letters wants to match their size. */
43125
- &[data-fill-line] {
43126
- height: round(1lh, 1px);
43127
- max-height: round(1lh, 1px);
43128
- }
43129
-
43130
- svg,
43131
- img {
43132
- width: 100%;
43133
- height: 100%;
43134
- }
43135
- svg {
43136
- overflow: visible;
43137
- }
43138
- }
43139
- &[data-flow-inline] {
43140
- width: 1em;
43141
- height: 1em;
43142
- }
43143
- &[data-interactive] {
43144
- cursor: pointer;
43145
- }
43146
- &[data-icon-text] {
43147
- -webkit-font-smoothing: antialiased;
43148
- text-rendering: optimizeLegibility;
43149
- }
43150
- }
43151
-
43152
- .navi_icon > svg,
43153
- .navi_icon > img {
43154
- width: 100%;
43155
- height: 100%;
43156
- }
43157
- .navi_icon[data-width-fixed] > svg,
43158
- .navi_icon[data-width-fixed] > img {
43159
- width: 100%;
43160
- height: auto;
43161
- }
43162
- .navi_icon[data-height-fixed] > svg,
43163
- .navi_icon[data-height-fixed] > img {
43164
- width: auto;
43165
- height: 100%;
43166
- }
43167
- .navi_icon[data-width-fixed][data-height-fixed] > svg,
43168
- .navi_icon[data-width-fixed][data-height-fixed] > img {
43169
- width: 100%;
43170
- height: 100%;
43171
- }
43172
- `;
43173
-
43174
- /**
43175
- * Renders an icon — an inline SVG/emoji/text glyph that inherits the
43176
- * surrounding text's `currentColor` and (by default) its font size, so it sits
43177
- * on the text baseline like a character.
43178
- *
43179
- * Content comes from either `href` (references an external/sprite symbol via
43180
- * `<use>`) or `children` (an inline `<svg>` element, or a string for a
43181
- * text/emoji icon). All extra props are spread onto the underlying `Box`/`Text`
43182
- * (sizing, spacing, color, className, data-attributes, …).
43183
- *
43184
- * Render mode is chosen automatically:
43185
- * - `children` is a **string** → a text icon (`<Text data-icon-text>`).
43186
- * - **sized** (an explicit `width`/`height`, or `flex`/`grid`) → a block icon
43187
- * (`<Box square>`), laid out as its own box rather than inline.
43188
- * - otherwise → an **inline char-like** icon that flows on the text baseline
43189
- * (`data-icon-char`), aligned via `textAnchor`.
43190
- *
43191
- * Accessibility: an icon is treated as decorative (`aria-hidden`) by default
43192
- * whenever it has no explicit size and no `onClick`; give it an explicit
43193
- * `decorative={false}` (or make it interactive) when it conveys meaning.
43194
- *
43195
- * @param {object} props
43196
- * @param {string} [props.href] - URL/id of an external SVG symbol, rendered via
43197
- * `<svg><use href></svg>`. Mutually exclusive with meaningful `children`.
43198
- * @param {import("ignore:preact").ComponentChildren} [props.children] - Inline icon
43199
- * content: an `<svg>` element, or a string (renders as a text/emoji icon).
43200
- * @param {boolean} [props.decorative] - Marks the icon `aria-hidden`. Defaults
43201
- * to `true` for an unsized, non-interactive icon; pass `false` for a
43202
- * meaning-bearing icon that needs to be exposed to assistive tech.
43203
- * @param {(event: MouseEvent) => void} [props.onClick] - Makes the icon
43204
- * interactive (`data-interactive`, pointer cursor) and non-decorative.
43205
- * @param {"line-top"|"char-top"|"center"|"char-bottom"|"line-bottom"} [props.textAnchor="center"]
43206
- * - Vertical alignment within the surrounding text line for the inline
43207
- * char-like mode, forwarded to `TextAnchor`: `"line-top"`/`"line-bottom"`
43208
- * align to the line box edges, `"char-top"` to the ink ascent, `"center"`
43209
- * centers on the line box, `"char-bottom"` sits on the baseline. See
43210
- * `text_anchor.jsx`.
43211
- * @param {{ size?: number, verticalAlign?: string }} [props.lineLayout] -
43212
- * Describes the surrounding line context (font size / vertical-align),
43213
- * forwarded to `TextAnchor` so it recomputes the vertical correction when
43214
- * that context changes.
43215
- * @param {string|number} [props.width] - Explicit width; `"auto"` clears it.
43216
- * Any explicit size switches the icon to block (sized) mode.
43217
- * @param {string|number} [props.height] - Explicit height; `"auto"` clears it.
43218
- * @param {"allow"} [props.lineOverflow] - `"allow"` lets the icon be taller
43219
- * than the box it sits in (a line of text, a control's slot) instead of being
43220
- * capped by it. For an icon that is an affordance sized for the finger rather
43221
- * than a character sized for reading.
43222
- * @param {boolean} [props.fillLine] - Sizes the icon on the line box (1lh)
43223
- * rather than on the character box (1em), so it uses the full height of the
43224
- * line without leaving it. Unlike `lineOverflow`, the icon still never
43225
- * exceeds the line.
43226
- * @param {boolean} [props.square] - Keep a 1:1 box; combined with one explicit
43227
- * dimension it fixes the other too.
43228
- * @param {boolean} [props.circle] - Like `square`, plus a circular shape.
43229
- * @param {string|number} [props.aspectRatio] - Fixes the second dimension from
43230
- * the one explicit dimension.
43231
- * @param {"x"|"y"|boolean} [props.flex] - Forces block/flex layout; auto-set to
43232
- * `"x"` when the icon is sized.
43233
- * @param {boolean} [props.grid] - Forces block/grid layout.
43234
- * @param {string} [props.className] - Merged with the base `"navi_icon"` class.
43235
- */
43236
- const Icon = ({
43237
- href,
43238
- children,
43239
- decorative,
43240
- onClick,
43241
- textAnchor = "center",
43242
- lineLayout,
43243
- lineOverflow,
43244
- fillLine,
43245
- ...props
43246
- }) => {
43247
- import.meta.css = [css$T, "@jsenv/navi/src/text/icon.jsx"];
43248
- const innerChildren = href ? jsx("svg", {
43249
- width: "100%",
43250
- height: "100%",
43251
- children: jsx("use", {
43252
- href: href
43253
- })
43254
- }) : children;
43255
- let {
43256
- flex,
43257
- grid,
43258
- width,
43259
- height
43260
- } = props;
43261
- if (width === "auto") {
43262
- width = undefined;
43263
- }
43264
- if (height === "auto") {
43265
- height = undefined;
43266
- }
43267
- const hasExplicitWidth = width !== undefined;
43268
- const hasExplicitHeight = height !== undefined;
43269
- const widthFixed = hasExplicitWidth || hasExplicitHeight && (props.square || props.circle || props.aspectRatio);
43270
- const heightFixed = hasExplicitHeight || hasExplicitWidth && (props.square || props.circle || props.aspectRatio);
43271
- if (widthFixed || heightFixed) {
43272
- if (flex === undefined) {
43273
- flex = "x";
43274
- }
43275
- } else if (decorative === undefined && !onClick) {
43276
- decorative = true;
43277
- }
43278
- const ariaProps = decorative ? {
43279
- "aria-hidden": "true"
43280
- } : {};
43281
- const textRef = useRef();
43282
- if (typeof children === "string") {
43283
- return jsx(Text, {
43284
- ...props,
43285
- ...ariaProps,
43286
- "data-icon-text": "",
43287
- "data-line-overflow": lineOverflow,
43288
- "data-fill-line": fillLine ? "" : undefined,
43289
- children: children
43290
- });
43291
- }
43292
- if (flex || grid) {
43293
- return jsx(Box, {
43294
- square: true,
43295
- ...props,
43296
- ...ariaProps,
43297
- flex: flex,
43298
- baseClassName: "navi_icon",
43299
- "data-width-fixed": widthFixed ? "" : undefined,
43300
- "data-height-fixed": heightFixed ? "" : undefined,
43301
- "data-interactive": onClick ? "" : undefined,
43302
- "data-line-overflow": lineOverflow,
43303
- "data-fill-line": fillLine ? "" : undefined,
43304
- onClick: onClick,
43305
- children: innerChildren
43306
- });
43307
- }
43308
- return jsx(TextAnchor, {
43309
- childRef: textRef,
43310
- textAnchor: textAnchor,
43311
- textSize: props.size,
43312
- lineLayout: lineLayout,
43313
- children: jsxs(Text, {
43314
- ...props,
43315
- ...ariaProps,
43316
- className: withPropsClassName("navi_icon", props.className),
43317
- spacing: "pre",
43318
- "data-icon-char": "",
43319
- "data-line-overflow": lineOverflow,
43320
- "data-fill-line": fillLine ? "" : undefined,
43321
- "data-width-fixed": widthFixed ? "" : undefined,
43322
- "data-height-fixed": heightFixed ? "" : undefined,
43323
- "data-interactive": onClick ? "" : undefined,
43324
- onClick: onClick,
43325
- ref: textRef,
43326
- children: [jsx("span", {
43327
- style: "user-select:none",
43328
- children: "\u200B"
43329
- }), innerChildren]
43330
- })
43331
- });
43332
- };
43333
-
43334
43698
  /**
43335
43699
  * Hook that reactively checks if a URL is visited.
43336
43700
  * Re-renders when the visited URL set changes.
@@ -55463,6 +55827,16 @@ const isTextInputElement = (el) => {
55463
55827
  );
55464
55828
  };
55465
55829
 
55830
+ // How many lines the thing around a component gives it, when that thing caps
55831
+ // its own height and cannot cap what it holds.
55832
+ //
55833
+ // A <Picker> is the case it exists for: its value is clamped with maxLines,
55834
+ // which is CSS line-clamp — it counts line boxes of inline text and has no idea
55835
+ // what a wrapped flex row is. So a <BadgeList> rendered as a picker's ui reads
55836
+ // the number from here and caps its own rows to it, and the picker turns its
55837
+ // own clamp off (see .navi_picker_value:has(.navi_badge_list) in picker.jsx).
55838
+ const MaxLinesContext = createContext(undefined);
55839
+
55466
55840
  // When a component render a prop that can be anything (js value of preact element)
55467
55841
  // make sure it cannot throw during render by converting it to a string if it's not a valid preact element or a primitive value
55468
55842
  const renderSafe = (value) => {
@@ -57861,12 +58235,24 @@ const ListItemSelectable = props => {
57861
58235
  ...rest
57862
58236
  } = props;
57863
58237
  const multiple = useContext(SelectableListMultipleContext);
58238
+ // Whose reason it is that this row cannot be taken. Read-only reaching it
58239
+ // from above is the LIST's, and what is settled is then the whole answer —
58240
+ // said as the selection where several things are taken, as the choice where
58241
+ // one thing is. Said of each row in turn, "this option is not available"
58242
+ // describes something else entirely: a list where each row happens to be
58243
+ // unavailable for its own reasons, which goes on being said that way. Busy
58244
+ // is not settled either: a list waiting on something says nothing about what
58245
+ // will be possible once it is done, so the row keeps its own words.
58246
+ const readOnlyFromAbove = useContext(ReadOnlyContext);
58247
+ const loadingFromAbove = useContext(LoadingContext$1);
58248
+ const answerIsSettled = Boolean(readOnlyFromAbove) && !loadingFromAbove;
58249
+ const readOnlyMessageKey = answerIsSettled ? multiple ? `constraint.readonly.selection` : `constraint.readonly.choice` : `constraint.readonly.option`;
57864
58250
  const inputRef = useRef();
57865
58251
  const inputType = multiple ? "checkbox" : "radio";
57866
58252
  const inputId = `${id}_input`;
57867
58253
  inputRef.nullCanHappen = true; // virtualization
57868
58254
  const [checkableRootProps, checkableProps, controlChildrenWrapperProps] = useCheckableProps({
57869
- readOnlyMessage: naviI18n(`constraint.readonly.option`, props),
58255
+ readOnlyMessage: naviI18n(readOnlyMessageKey, props),
57870
58256
  ...rest,
57871
58257
  ref: inputRef,
57872
58258
  id: inputId,
@@ -62511,6 +62897,51 @@ const PickerPresetResolver = props => {
62511
62897
  });
62512
62898
  };
62513
62899
 
62900
+ // Put around its children by BadgeList so a Badge below knows it is inside one.
62901
+ // A badge then renders nothing at all: it hands its props to the list and the
62902
+ // list renders it. Badges register in tree order, so by the time the list gets
62903
+ // to its own content it holds them all, in source order, and knows how many
62904
+ // there are before deciding what to show — without ever walking children
62905
+ // vnodes, and without rendering a badge it then has to take back.
62906
+ const BadgeListContext = createContext(null);
62907
+
62908
+ const createBadgeRegistry = () => {
62909
+ let pass = 0;
62910
+ let passCount = 0;
62911
+ let entries = [];
62912
+ let childrenAreNew = true;
62913
+
62914
+ return {
62915
+ // Called by BadgeList at the top of every render. childrenAreNew says
62916
+ // whether it was handed fresh children vnodes: when it re-renders on its
62917
+ // own (its own state changed) Preact hands the untouched children straight
62918
+ // back and skips them, so none of the badges registers again. That empty
62919
+ // pass means "unchanged", not "no badge left" — see getEntries.
62920
+ startPass: (areNew) => {
62921
+ pass++;
62922
+ passCount = 0;
62923
+ childrenAreNew = areNew;
62924
+ },
62925
+ // entryState is the badge's own memory. A badge that re-renders on its own
62926
+ // must update its entry, not append a second one.
62927
+ register: (entryState, props) => {
62928
+ if (entryState.pass !== pass) {
62929
+ entryState.pass = pass;
62930
+ entryState.index = passCount;
62931
+ passCount++;
62932
+ }
62933
+ entries[entryState.index] = props;
62934
+ },
62935
+ getEntries: () => {
62936
+ if (passCount === 0 && !childrenAreNew) {
62937
+ return entries;
62938
+ }
62939
+ entries.length = passCount;
62940
+ return entries;
62941
+ },
62942
+ };
62943
+ };
62944
+
62514
62945
  installImportMetaCssBuild(import.meta);const css$x = /* css */`
62515
62946
  @layer navi {
62516
62947
  }
@@ -62569,6 +63000,11 @@ installImportMetaCssBuild(import.meta);const css$x = /* css */`
62569
63000
  align-items: stretch;
62570
63001
  color: var(--x-color);
62571
63002
  font-size: var(--font-size);
63003
+ /* Cuts the font's half-leading above the first line and below the last one,
63004
+ down to cap-height/baseline: the padding becomes the only vertical space
63005
+ and the text is exactly centered. Space between wrapped lines is left
63006
+ untouched, unlike a line-height tweak. */
63007
+ text-box: trim-both cap alphabetic;
62572
63008
  background: var(--x-background);
62573
63009
  background-color: var(--x-background-color);
62574
63010
  border-radius: 1em;
@@ -62618,7 +63054,22 @@ installImportMetaCssBuild(import.meta);const css$x = /* css */`
62618
63054
  }
62619
63055
  }
62620
63056
  `;
62621
- const Badge = ({
63057
+ const Badge = props => {
63058
+ const badgeList = useContext(BadgeListContext);
63059
+ const entryStateRef = useRef();
63060
+ if (badgeList) {
63061
+ // Inside a BadgeList the badge renders nothing: it hands itself over and
63062
+ // the list renders it, which is how the list gets to see them all before
63063
+ // deciding how many it shows. See badge_list_context.js.
63064
+ const entryState = entryStateRef.current || (entryStateRef.current = {});
63065
+ badgeList.register(entryState, props);
63066
+ return null;
63067
+ }
63068
+ return jsx(BadgeUI, {
63069
+ ...props
63070
+ });
63071
+ };
63072
+ const BadgeUI = ({
62622
63073
  children,
62623
63074
  className,
62624
63075
  ...props
@@ -62633,7 +63084,14 @@ const Badge = ({
62633
63084
  return jsx(Text, {
62634
63085
  className: withPropsClassName("navi_badge", className),
62635
63086
  bold: true,
62636
- maxLines: 1,
63087
+ maxLines: 1
63088
+ // The text-box trim ends the content box at the baseline, and a clamped
63089
+ // badge is clipped there: descenders of the last visible line would be
63090
+ // cut. Halfway to the next line's cap top keeps them, still above any
63091
+ // ink from the line the clamp hides.
63092
+ ,
63093
+
63094
+ overflowClipMargin: "content-box calc((1lh - 1cap) / 2)",
62637
63095
  ...props,
62638
63096
  styleCSSVars: BadgeStyleCSSVars,
62639
63097
  spacing: jsx("span", {}),
@@ -62657,6 +63115,15 @@ const BadgeStyleCSSVars = {
62657
63115
  fontSize: "--font-size"
62658
63116
  };
62659
63117
  const BadgeButton = props => {
63118
+ const ownTargetHidden = useOwnTargetHidden(props);
63119
+ if (ownTargetHidden) {
63120
+ return null;
63121
+ }
63122
+ return jsx(BadgeButtonUI, {
63123
+ ...props
63124
+ });
63125
+ };
63126
+ const BadgeButtonUI = props => {
62660
63127
  const defaultRef = useRef();
62661
63128
  props.ref = props.ref || defaultRef;
62662
63129
  const [buttonRootProps, buttonHostProps] = useControlProps(props, {
@@ -62687,20 +63154,208 @@ installImportMetaCssBuild(import.meta);const css$w = /* css */`
62687
63154
  visibility: hidden;
62688
63155
  pointer-events: none;
62689
63156
  }
63157
+
63158
+ /* maxLines renders every badge for one layout, reads where the rows fell,
63159
+ then renders again with only what fits. The in-between is hidden rather
63160
+ than clipped: the badges that don't make it must leave the DOM, not sit
63161
+ there cut in half. Both renders land in the same frame (the second one
63162
+ is queued from a layout effect), so nothing shows up half measured. */
63163
+ &[navi-badge-list-measuring] {
63164
+ visibility: hidden;
63165
+ }
62690
63166
  }
62691
63167
 
62692
63168
  .navi_badge.navi_badge_more {
62693
63169
  white-space: nowrap;
62694
63170
  }
62695
63171
  `;
62696
- const BadgeList = ({
63172
+
63173
+ // Groups badges by the row they wrapped onto.
63174
+ // The signal we look for is horizontal: inside a row each badge starts further
63175
+ // right than the previous one, at a wrap the next one starts back at the row
63176
+ // start. Vertical positions can't be used because "align-items: center" gives
63177
+ // badges of different heights different tops within a single row.
63178
+ const groupRectsByRow = elements => {
63179
+ const rows = [];
63180
+ let previousLeft = -Infinity;
63181
+ for (const element of elements) {
63182
+ const rect = element.getBoundingClientRect();
63183
+ if (rect.width === 0 && rect.height === 0) {
63184
+ continue;
63185
+ }
63186
+ if (rows.length === 0 || rect.left <= previousLeft) {
63187
+ rows.push([]);
63188
+ }
63189
+ rows[rows.length - 1].push(rect);
63190
+ previousLeft = rect.left;
63191
+ }
63192
+ return rows;
63193
+ };
63194
+
63195
+ // Reads a list that currently holds every badge plus the "+N" badge and tells
63196
+ // how many badges fit in maxLines rows.
63197
+ const measureRowFit = (listEl, maxLines) => {
63198
+ const elements = Array.from(listEl.children);
63199
+ if (elements.length < 2) {
63200
+ return elements.length;
63201
+ }
63202
+ // The "+N" badge is rendered last. It sits after every badge so it moves none
63203
+ // of them, which is what lets it be measured in the same layout it is left
63204
+ // out of.
63205
+ const moreRect = elements[elements.length - 1].getBoundingClientRect();
63206
+ const badgeElements = elements.slice(0, -1);
63207
+ const rows = groupRectsByRow(badgeElements);
63208
+ if (rows.length <= maxLines) {
63209
+ return badgeElements.length;
63210
+ }
63211
+ const styles = getComputedStyle(listEl);
63212
+ const gap = parseFloat(styles.columnGap) || 0;
63213
+ const contentRight = listEl.getBoundingClientRect().right - (parseFloat(styles.paddingRight) || 0) - (parseFloat(styles.borderRightWidth) || 0);
63214
+
63215
+ // The "+N" badge lands right after the last kept badge, so it eats into the
63216
+ // last visible row: drop badges from that row until it fits. Emptying that
63217
+ // row entirely is a valid outcome — the badge then wraps onto it and takes it
63218
+ // for itself, which is still within maxLines.
63219
+ const rowsKept = rows.slice(0, maxLines);
63220
+ const lastRow = rowsKept[rowsKept.length - 1];
63221
+ let lastRowCount = lastRow.length;
63222
+ while (lastRowCount > 0 && lastRow[lastRowCount - 1].right + gap + moreRect.width > contentRight + 0.5) {
63223
+ lastRowCount--;
63224
+ }
63225
+ return rowsKept.slice(0, -1).reduce((count, row) => count + row.length, 0) + lastRowCount;
63226
+ };
63227
+ const BADGE_LIST_PROPS = {
63228
+ inline: true,
63229
+ flex: "x",
63230
+ alignY: "center",
63231
+ spacing: "xs"
63232
+ };
63233
+
63234
+ // The badges hand themselves over as they render instead of being read upfront
63235
+ // from the children vnodes: see badge_list_context.js. Only worth it when the
63236
+ // list has something to decide — how many to show, or whether there is none at
63237
+ // all. Otherwise the badges render themselves and nothing has to be collected.
63238
+ const useBadgeRegistry = (children, enabled) => {
63239
+ const registryRef = useRef();
63240
+ const previousChildrenRef = useRef();
63241
+ if (!enabled) {
63242
+ return null;
63243
+ }
63244
+ const registry = registryRef.current || (registryRef.current = createBadgeRegistry());
63245
+ // Whether Preact is about to render the badges again or hand back the ones it
63246
+ // already has, which it does when the list re-renders on its own.
63247
+ registry.startPass(previousChildrenRef.current !== children);
63248
+ previousChildrenRef.current = children;
63249
+ return registry;
63250
+ };
63251
+
63252
+ /**
63253
+ * A row of badges that wraps.
63254
+ *
63255
+ * Four shapes behind one name, because what the list has to do at runtime is
63256
+ * not the same in each. Nothing is set up for a case that cannot happen: a
63257
+ * plain list is one element holding its children as-is — no registry, no
63258
+ * effect —, a capped one collects its badges but measures nothing, and only
63259
+ * shrinkWrap ever builds the measurement ghost.
63260
+ *
63261
+ * @param {import("ignore:preact").ComponentChildren} [fallback]
63262
+ * Rendered in place of the badges when there is none. Without it an empty
63263
+ * list renders nothing at all. In a <Picker> this is the only placeholder
63264
+ * the user gets — a picker given a `ui` does not draw its own — so pass the
63265
+ * placeholder text: `fallback="Select skills…"`. Plain text is the right
63266
+ * choice there: it reads at the picker's size, in the placeholder color the
63267
+ * picker gives its empty value slot, and the box stays the same height. A
63268
+ * transparent <Badge> matches the slot to the pixel instead, at the price of
63269
+ * badge-sized text. See docs/badge_list.md.
63270
+ * @param {boolean} [shrinkWrap]
63271
+ * Narrows the list down to its widest row so the last row isn't ragged.
63272
+ * Defaults to true inside a <Picker> — the trigger draws a border around the
63273
+ * list, so the ragged edge shows — and false elsewhere, where the work would
63274
+ * often go unseen: opt in where an edge is visible. Ignored when maxLines is
63275
+ * in play, which needs the full width to know where the rows fall.
63276
+ * @param {number} [max]
63277
+ * Caps how many badges are rendered; the surplus becomes a "+N" badge, which
63278
+ * takes one of the max slots.
63279
+ * @param {number} [maxLines]
63280
+ * Caps how many rows are shown, measured. Falls back to what the surrounding
63281
+ * component grants (a <Picker>, see max_lines_context.js).
63282
+ */
63283
+ const BadgeList = props => {
63284
+ import.meta.css = [css$w, "@jsenv/navi/src/text/badge_list.jsx"];
63285
+ const {
63286
+ maxLines,
63287
+ max,
63288
+ fallback
63289
+ } = props;
63290
+ const maxLinesFromAbove = useContext(MaxLinesContext);
63291
+ const maxLinesResolved = maxLines === undefined ? maxLinesFromAbove : maxLines;
63292
+ // Granted lines mean a clamping container — a <Picker> — whose border makes
63293
+ // the ragged last row show; nothing of the sort around us, and shrink
63294
+ // wrapping is work nobody sees unless asked for.
63295
+ const {
63296
+ shrinkWrap = maxLinesFromAbove !== undefined
63297
+ } = props;
63298
+ if (maxLinesResolved !== undefined) {
63299
+ // shrinkWrap is dropped on purpose: it narrows the list down to its widest
63300
+ // row, which would re-wrap the badges under the cap just measured.
63301
+ return jsx(BadgeListMaxLines, {
63302
+ ...props,
63303
+ maxLines: maxLinesResolved
63304
+ });
63305
+ }
63306
+ if (shrinkWrap) {
63307
+ return jsx(BadgeListShrinkWrap, {
63308
+ ...props
63309
+ });
63310
+ }
63311
+ if (max !== undefined || fallback !== undefined) {
63312
+ return jsx(BadgeListCounted, {
63313
+ ...props
63314
+ });
63315
+ }
63316
+ return jsx(BadgeListPlain, {
63317
+ ...props
63318
+ });
63319
+ };
63320
+
63321
+ // Nothing to measure, cap, or count: the badges render themselves — no
63322
+ // registry, no context, no effect, one element.
63323
+ const BadgeListPlain = props => {
63324
+ return jsx(Box, {
63325
+ baseClassName: "navi_badge_list",
63326
+ ...BADGE_LIST_PROPS,
63327
+ ...props
63328
+ });
63329
+ };
63330
+
63331
+ // max/fallback need the badge count, so the badges are collected — but nothing
63332
+ // is measured and no DOM is watched.
63333
+ const BadgeListCounted = ({
62697
63334
  fallback,
62698
63335
  children,
62699
- shrinkWrap = true,
62700
63336
  max,
62701
- ...props
63337
+ ...boxProps
62702
63338
  }) => {
62703
- import.meta.css = [css$w, "@jsenv/navi/src/text/badge_list.jsx"];
63339
+ const registry = useBadgeRegistry(children, true);
63340
+ return jsx(Box, {
63341
+ baseClassName: "navi_badge_list",
63342
+ ...BADGE_LIST_PROPS,
63343
+ ...boxProps,
63344
+ children: jsx(BadgeListChildren, {
63345
+ registry: registry,
63346
+ max: max,
63347
+ fallback: fallback,
63348
+ children: children
63349
+ })
63350
+ });
63351
+ };
63352
+ const BadgeListShrinkWrap = ({
63353
+ fallback,
63354
+ children,
63355
+ max,
63356
+ ...restProps
63357
+ }) => {
63358
+ const registry = useBadgeRegistry(children, max !== undefined || fallback !== undefined);
62704
63359
  const measureRef = useRef();
62705
63360
  const visibleRef = useRef();
62706
63361
  useLayoutEffect(() => {
@@ -62713,18 +63368,25 @@ const BadgeList = ({
62713
63368
  let rafId;
62714
63369
  const measure = () => {
62715
63370
  visibleEl.style.width = "";
62716
- if (shrinkWrap) {
62717
- // Clone the already-rendered DOM nodes instead of letting React/Preact
62718
- // render the children a second time into the ghost: re-rendering would
62719
- // instantiate Badge/Badge.Button a second time, double-registering
62720
- // their controllers (and any other mount side effect) under the same id.
62721
- measureEl.replaceChildren(...Array.from(visibleEl.children, child => child.cloneNode(true)));
62722
- const optimalWidth = measureWidestChildRow(measureEl);
62723
- if (optimalWidth !== null) {
62724
- visibleEl.style.width = `${Math.ceil(optimalWidth)}px`;
62725
- }
63371
+ // Clone the already-rendered DOM nodes instead of letting React/Preact
63372
+ // render the children a second time into the ghost: re-rendering would
63373
+ // instantiate Badge/Badge.Button a second time, double-registering their
63374
+ // controllers (and any other mount side effect) under the same id.
63375
+ measureEl.replaceChildren(...Array.from(visibleEl.children, child => child.cloneNode(true)));
63376
+ const optimalWidth = measureWidestChildRow(measureEl);
63377
+ if (optimalWidth !== null) {
63378
+ visibleEl.style.width = `${Math.ceil(optimalWidth)}px`;
62726
63379
  }
62727
63380
  };
63381
+
63382
+ // A single badge is a single row, and a single row is already the widest
63383
+ // one: there is nothing to narrow, and no width the list could be given
63384
+ // that would change that.
63385
+ if (visibleEl.children.length < 2) {
63386
+ visibleEl.style.width = "";
63387
+ measureEl.replaceChildren();
63388
+ return undefined;
63389
+ }
62728
63390
  measure();
62729
63391
  const onResize = () => {
62730
63392
  cancelAnimationFrame(rafId);
@@ -62741,36 +63403,201 @@ const BadgeList = ({
62741
63403
  observer?.disconnect();
62742
63404
  window.removeEventListener("resize", onResize);
62743
63405
  };
62744
- }, [shrinkWrap, children]);
62745
- const childArray = toChildArray(children);
62746
- const hasMax = max !== undefined && childArray.length > max;
62747
- const visibleChildren = hasMax ? childArray.slice(0, max - 1) : childArray;
62748
- const hiddenCount = hasMax ? childArray.length - (max - 1) : 0;
62749
- const sharedProps = {
62750
- inline: true,
62751
- flex: "x",
62752
- alignY: "center",
62753
- spacing: "xs",
62754
- ...props
63406
+ }, [children]);
63407
+ const boxProps = {
63408
+ ...BADGE_LIST_PROPS,
63409
+ ...restProps
62755
63410
  };
62756
- return jsxs(Box, {
62757
- relative: true,
62758
- children: [jsx(Box, {
62759
- baseClassName: "navi_badge_list",
62760
- ...sharedProps,
62761
- ref: measureRef,
62762
- "aria-hidden": "true",
62763
- "navi-badge-list-clone": ""
62764
- }), jsxs(Box, {
62765
- baseClassName: "navi_badge_list",
62766
- ...sharedProps,
62767
- ref: visibleRef,
62768
- children: [visibleChildren.length ? visibleChildren : fallback, hiddenCount > 0 && jsx(Badge, {
62769
- className: "navi_badge_more",
62770
- children: naviI18n("badge_list.more", {
62771
- count: hiddenCount
63411
+ return (
63412
+ // inline flex, not a plain block: the wrapper must sit on the line the way
63413
+ // the list itself would, otherwise the list lands a pixel low in some
63414
+ // containers.
63415
+ jsxs(Box, {
63416
+ relative: true,
63417
+ inline: true,
63418
+ flex: "x",
63419
+ children: [jsx(Box, {
63420
+ baseClassName: "navi_badge_list",
63421
+ ...boxProps,
63422
+ ref: measureRef,
63423
+ "aria-hidden": "true",
63424
+ "navi-badge-list-clone": ""
63425
+ }), jsx(Box, {
63426
+ baseClassName: "navi_badge_list",
63427
+ ...boxProps,
63428
+ ref: visibleRef,
63429
+ children: jsx(BadgeListChildren, {
63430
+ registry: registry,
63431
+ max: max,
63432
+ fallback: fallback,
63433
+ children: children
62772
63434
  })
62773
63435
  })]
63436
+ })
63437
+ );
63438
+ };
63439
+ const BadgeListMaxLines = ({
63440
+ fallback,
63441
+ children,
63442
+ max,
63443
+ maxLines,
63444
+ ...boxProps
63445
+ }) => {
63446
+ const registry = useBadgeRegistry(children, true);
63447
+ const visibleRef = useRef();
63448
+
63449
+ // What the measure found: how many badges there were and how many of them fit
63450
+ // in maxLines rows. null means "not measured yet": the list then renders them
63451
+ // all, hidden, for the layout effect to read.
63452
+ const [fit, setFit] = useState(null);
63453
+ // The two widths this list gives whatever is around it: the one it takes with
63454
+ // every badge rendered, and the one it settles on once the surplus is gone.
63455
+ // Neither is a reason to measure again — see the resize watch below.
63456
+ const selfWidthsRef = useRef({
63457
+ full: -1,
63458
+ settled: -1
63459
+ });
63460
+ const measuring = fit === null;
63461
+ // A single badge is a single row whatever the width, so nothing can move it
63462
+ // out of the cap and nothing has to be watched.
63463
+ const watchesResize = fit !== null && fit.count > 1;
63464
+
63465
+ // Runs after every render, which is when the badges have registered and the
63466
+ // DOM holds whatever this render asked for.
63467
+ useLayoutEffect(() => {
63468
+ const visibleEl = visibleRef.current;
63469
+ if (!visibleEl) {
63470
+ return;
63471
+ }
63472
+ const count = registry.getEntries().length;
63473
+ const width = visibleEl.parentElement?.getBoundingClientRect().width ?? -1;
63474
+ if (fit === null) {
63475
+ selfWidthsRef.current.full = width;
63476
+ setFit({
63477
+ count,
63478
+ shown: count < 2 ? count : measureRowFit(visibleEl, maxLines)
63479
+ });
63480
+ return;
63481
+ }
63482
+ selfWidthsRef.current.settled = width;
63483
+ if (fit.count !== count) {
63484
+ // Badges came or went: what was measured no longer describes them.
63485
+ setFit(null);
63486
+ }
63487
+ });
63488
+ useLayoutEffect(() => {
63489
+ const outerParent = visibleRef.current?.parentElement;
63490
+ if (!watchesResize || !outerParent) {
63491
+ return undefined;
63492
+ }
63493
+ let rafId;
63494
+ const remeasure = () => {
63495
+ // Only a width change can move the rows — a height change is this list
63496
+ // growing or shrinking, never the room it was given.
63497
+ //
63498
+ // And not every width change either. Nothing guarantees an ancestor whose
63499
+ // width does not follow its content (a column with align-items: start
63500
+ // sizes every row to what is inside it), so rendering every badge widens
63501
+ // what is being watched and dropping the surplus narrows it right back.
63502
+ // Those two widths are this list talking to itself; measuring again on
63503
+ // them never ends. Any other width is the room around it changing.
63504
+ const width = outerParent.getBoundingClientRect().width;
63505
+ const {
63506
+ full,
63507
+ settled
63508
+ } = selfWidthsRef.current;
63509
+ if (Math.abs(width - full) < 0.5 || Math.abs(width - settled) < 0.5) {
63510
+ return;
63511
+ }
63512
+ cancelAnimationFrame(rafId);
63513
+ rafId = requestAnimationFrame(() => setFit(null));
63514
+ };
63515
+ const observer = new ResizeObserver(remeasure);
63516
+ observer.observe(outerParent);
63517
+ window.addEventListener("resize", remeasure);
63518
+ return () => {
63519
+ cancelAnimationFrame(rafId);
63520
+ observer.disconnect();
63521
+ window.removeEventListener("resize", remeasure);
63522
+ };
63523
+ }, [watchesResize, maxLines]);
63524
+ return jsx(Box, {
63525
+ baseClassName: "navi_badge_list",
63526
+ ...BADGE_LIST_PROPS,
63527
+ ...boxProps,
63528
+ ref: visibleRef,
63529
+ "navi-badge-list-measuring": measuring ? "" : undefined,
63530
+ children: jsx(BadgeListChildren, {
63531
+ registry: registry,
63532
+ max: max,
63533
+ fallback: fallback,
63534
+ measuring: measuring,
63535
+ rowFit: fit?.shown ?? null,
63536
+ children: children
63537
+ })
63538
+ });
63539
+ };
63540
+ const BadgeListChildren = ({
63541
+ registry,
63542
+ children,
63543
+ fallback,
63544
+ max,
63545
+ measuring,
63546
+ rowFit
63547
+ }) => {
63548
+ if (!registry) {
63549
+ // The badges are on their own: nothing here decides which of them render.
63550
+ return children;
63551
+ }
63552
+ return jsxs(Fragment$1, {
63553
+ children: [jsx(BadgeListContext.Provider, {
63554
+ value: registry,
63555
+ children: children
63556
+ }), jsx(BadgeListContent, {
63557
+ registry: registry,
63558
+ fallback: fallback,
63559
+ max: max,
63560
+ measuring: measuring,
63561
+ rowFit: rowFit
63562
+ })]
63563
+ });
63564
+ };
63565
+ const BadgeListContent = ({
63566
+ registry,
63567
+ fallback,
63568
+ max,
63569
+ measuring,
63570
+ rowFit
63571
+ }) => {
63572
+ const entries = registry.getEntries();
63573
+ const count = entries.length;
63574
+ if (count === 0) {
63575
+ return fallback;
63576
+ }
63577
+
63578
+ // The "+N" badge stands among the badges, so it takes one of the max slots
63579
+ // when there is a surplus to name. A list of exactly `max` badges has nothing
63580
+ // to name and keeps all of them.
63581
+ let shownCount = max !== undefined && count > max ? max - 1 : count;
63582
+ if (!measuring && rowFit !== null && rowFit !== undefined && rowFit < shownCount) {
63583
+ shownCount = rowFit;
63584
+ }
63585
+ // While measuring, everything above is on screen (hidden) along with the "+N"
63586
+ // badge, so the layout effect can see where the rows fall and how much room
63587
+ // that badge asks for. Its label then reads the worst case — every badge
63588
+ // hidden — so the room reserved is never short.
63589
+ const hasMore = measuring || shownCount < count;
63590
+ return jsxs(Fragment$1, {
63591
+ children: [entries.slice(0, shownCount).map((badgeProps, index) =>
63592
+ // Keyed by position: a badge's own key went to the registering vnode
63593
+ // above and doesn't reach here, and badges keep no state worth moving.
63594
+ jsx(BadgeUI, {
63595
+ ...badgeProps
63596
+ }, index)), hasMore && jsx(BadgeUI, {
63597
+ className: "navi_badge_more",
63598
+ children: naviI18n("badge_list.more", {
63599
+ count: measuring ? count : count - shownCount
63600
+ })
62774
63601
  })]
62775
63602
  });
62776
63603
  };
@@ -63001,7 +63828,6 @@ const PickerObjectUI = () => {
63001
63828
  const PickerArray = props => {
63002
63829
  const Next = useNextResolver();
63003
63830
  return jsx(Next, {
63004
- maxLines: "3",
63005
63831
  ui: jsx(PickerArrayUI, {}),
63006
63832
  ...props,
63007
63833
  type: "navi_js",
@@ -63031,6 +63857,57 @@ const PickerArrayUI = () => {
63031
63857
  })
63032
63858
  });
63033
63859
  };
63860
+
63861
+ /**
63862
+ * One value the picker holds, drawn as a chip with a cross that takes it back
63863
+ * out. Sits wherever the application draws what was picked — on the picker's
63864
+ * façade (`ui`) or inside its popup — and both behave the same.
63865
+ *
63866
+ * The cross asks with `--navi-unselect` rather than writing a new list, and it
63867
+ * asks the picker — which holds what was picked, and hands it down to whatever
63868
+ * draws it in the popup. Nothing to name: the picker is the nearest control
63869
+ * around the chip. `commandFor` is for a chip that stands outside the picker it
63870
+ * speaks for.
63871
+ *
63872
+ * The cross is an own target (see own_target.js), so the press belongs to it
63873
+ * and not to the picker underneath, and it goes when the picker turns read-only
63874
+ * — a row being read still says what was picked, it just no longer offers to
63875
+ * unpick it.
63876
+ *
63877
+ * @type {import("ignore:preact").FunctionComponent<{
63878
+ * value: any,
63879
+ * commandFor?: string,
63880
+ * children?: import("ignore:preact").ComponentChildren,
63881
+ * [key: string]: any,
63882
+ * }>}
63883
+ * @param {any} value The value this chip stands for — one entry of what the
63884
+ * picker holds, and what `--navi-unselect` carries.
63885
+ * @param {string} [commandFor] The id of the picker to take the value out of,
63886
+ * when the chip does not sit inside it.
63887
+ */
63888
+ const PickerChip = ({
63889
+ value,
63890
+ commandFor,
63891
+ children,
63892
+ ...rest
63893
+ }) => {
63894
+ return jsxs(Badge, {
63895
+ inline: true,
63896
+ flex: true,
63897
+ ...rest,
63898
+ children: [children, jsx(Badge.Button, {
63899
+ ownTarget: true,
63900
+ command: "--navi-unselect",
63901
+ commandFor: commandFor,
63902
+ value: value,
63903
+ "aria-label": naviI18n("button.remove"),
63904
+ children: jsx(Icon, {
63905
+ lineOverflow: "allow",
63906
+ children: jsx(CloseSvg, {})
63907
+ })
63908
+ })]
63909
+ });
63910
+ };
63034
63911
  const PickerColor = props => {
63035
63912
  const Next = useNextResolver();
63036
63913
  return jsx(Next, {
@@ -63469,6 +64346,30 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
63469
64346
  &[navi-placeholder] {
63470
64347
  color: var(--picker-placeholder-color);
63471
64348
  }
64349
+
64350
+ /* A <BadgeList> caps its own rows: it renders the badges that fit and a
64351
+ "+N" badge for the rest, reading the number from MaxLinesContext right
64352
+ below. The picker must then not clamp on top of it — line-clamp turns
64353
+ the value into a -webkit-box and single-line truncation into a
64354
+ nowrap block, either of which takes the badge list out of the
64355
+ inline-flex layout it needs. maxLines writes those as inline styles on
64356
+ the element, hence !important. */
64357
+ &:has(.navi_badge_list) {
64358
+ display: inline-flex !important;
64359
+ -webkit-line-clamp: none !important;
64360
+ overflow: visible !important;
64361
+ -webkit-box-orient: horizontal !important;
64362
+ text-overflow: clip !important;
64363
+ white-space: normal !important;
64364
+ }
64365
+
64366
+ /* The façade is transparent to the pointer — a press on what it draws
64367
+ means "open the picker". An own target is the exception, the same way
64368
+ the clear cross is one in the slot below: it says the press is aimed at
64369
+ IT, so it has to be reachable at all. */
64370
+ [data-own-target] {
64371
+ pointer-events: auto;
64372
+ }
63472
64373
  }
63473
64374
  .navi_picker_right_slot {
63474
64375
  display: inline-flex;
@@ -63757,214 +64658,284 @@ const PickerButton = props => {
63757
64658
  // clearing being a modification like any other.
63758
64659
  const interactive = !basePseudoState[":disabled"] && !basePseudoState[":read-only"] && !loading;
63759
64660
  usePickerErrorCallout(uiStateController, error);
63760
- return jsxs(Box, {
63761
- as: "div",
63762
- ref: ref
63763
- // The flow this element really has (.navi_picker is display:inline-flex).
63764
- // Left unsaid, Box reads a <div> as block and resolves alignX into a
63765
- // text-align which is a different intention entirely (that one is the
63766
- // textAlign prop, placing the text INSIDE the value slot).
63767
- ,
64661
+ return (
64662
+ /* Read-only crosses into everything the picker is made of: what it really
64663
+ holds is drawn by controls of their own — in the popup, and on the façade
64664
+ where an application puts its own affordances and each of them refuses
64665
+ in its own words once told. Said from the read-only state alone, never
64666
+ from the busy one an action running for a moment is not the same thing
64667
+ as a value nobody may change. */
64668
+ jsx(ReadOnlyContext.Provider, {
64669
+ value: readOnlyResolved,
64670
+ children: jsxs(Box, {
64671
+ as: "div",
64672
+ ref: ref
64673
+ // The flow this element really has (.navi_picker is display:inline-flex).
64674
+ // Left unsaid, Box reads a <div> as block and resolves alignX into a
64675
+ // text-align — which is a different intention entirely (that one is the
64676
+ // textAlign prop, placing the text INSIDE the value slot).
64677
+ ,
63768
64678
 
63769
- inline: true,
63770
- flex: "x",
63771
- baseClassName: "navi_picker",
63772
- pseudoClasses: PICKER_BUTTON_PSEUDO_CLASSES,
63773
- "data-variant": variant,
63774
- "navi-picker": "",
63775
- "navi-single-line": isSingleLine ? "" : undefined,
63776
- "navi-ui-custom": ui === "default" ? undefined : "",
63777
- "data-readonly-opens": readOnlyOpens ? "" : undefined,
63778
- "data-popup-width-fit-content": popupWidthFitContent ? "" : undefined,
63779
- ...pickerRemainingProps,
63780
- basePseudoState: basePseudoState,
63781
- styleCSSVars: PickerStyleCSSVars,
63782
- variant: undefined,
63783
- rightSlotIcon: undefined,
63784
- rightSlotIconSize: undefined,
63785
- rightSlot: undefined,
63786
- clearConfirm: undefined,
63787
- clearConfirmPopupContent: undefined,
63788
- openWhileReadOnly: undefined,
63789
- ui: undefined,
63790
- maxLines: undefined,
63791
- popupWidthFitContent: undefined,
63792
- error: undefined,
63793
- dayLabel: undefined
63794
- // This wrapper will receive keyboard event bubbling from the picker popup content
63795
- // we re-dispatch on the input (to get escape to close for instance)
63796
- ,
64679
+ inline: true,
64680
+ flex: "x",
64681
+ baseClassName: "navi_picker",
64682
+ pseudoClasses: PICKER_BUTTON_PSEUDO_CLASSES,
64683
+ "data-variant": variant,
64684
+ "navi-picker": "",
64685
+ "navi-single-line": isSingleLine ? "" : undefined,
64686
+ "navi-ui-custom": ui === "default" ? undefined : "",
64687
+ "data-readonly-opens": readOnlyOpens ? "" : undefined,
64688
+ "data-popup-width-fit-content": popupWidthFitContent ? "" : undefined,
64689
+ ...pickerRemainingProps,
64690
+ basePseudoState: basePseudoState,
64691
+ styleCSSVars: PickerStyleCSSVars,
64692
+ variant: undefined,
64693
+ rightSlotIcon: undefined,
64694
+ rightSlotIconSize: undefined,
64695
+ rightSlot: undefined,
64696
+ clearConfirm: undefined,
64697
+ clearConfirmPopupContent: undefined,
64698
+ openWhileReadOnly: undefined,
64699
+ ui: undefined,
64700
+ maxLines: undefined,
64701
+ popupWidthFitContent: undefined,
64702
+ error: undefined,
64703
+ dayLabel: undefined
64704
+ // This wrapper will receive keyboard event bubbling from the picker popup content
64705
+ // we re-dispatch on the input (to get escape to close for instance)
64706
+ ,
63797
64707
 
63798
- onKeyDown: inputProps.onKeyDown
63799
- // in case request open/close are dispatched on the control root ->
63800
- // redispatch them to the host
63801
- ,
64708
+ onKeyDown: inputProps.onKeyDown
64709
+ // in case request open/close are dispatched on the control root ->
64710
+ // redispatch them to the host
64711
+ ,
63802
64712
 
63803
- onnavi_request_open: inputProps.onnavi_request_open,
63804
- onnavi_request_close: inputProps.onnavi_request_close,
63805
- children: [jsxs("span", {
63806
- className: "navi_picker_box",
63807
- children: [variant === "headless" ? null : jsx(LoadingOutline, {
63808
- loading: loading,
63809
- color: "var(--picker-loader-color)",
63810
- inset: -2
63811
- }), jsx(PickerInput, {
63812
- tabIndex: variant === "headless" ? -1 : undefined,
63813
- "aria-hidden": variant === "headless" ? "true" : undefined,
63814
- ...inputProps,
63815
- // eslint-disable-next-line react/no-children-prop
63816
- children: undefined // we will render children into the div
64713
+ onnavi_request_open: inputProps.onnavi_request_open,
64714
+ onnavi_request_close: inputProps.onnavi_request_close
64715
+ // `--navi-select`/`--navi-unselect` about one entry of the list the
64716
+ // picker holds — a chip on the façade, a suggestion beside the field.
64717
+ // Answered here rather than by the control drawing that list in the
64718
+ // popup, even though rows are what such a control owns: a picker given
64719
+ // its own value builds its popup only on first open (see
64720
+ // popup_content_mount.js), so before that there is no such control at
64721
+ // all — and building the whole popup to have someone to talk to, for a
64722
+ // cross, is the wrong price. The picker holds the value in the first
64723
+ // place and hands it down whenever the popup is built.
63817
64724
  ,
63818
64725
 
63819
- ui: ui,
63820
- onFocus: e => {
63821
- inputProps.onFocus?.(e);
63822
- e.target.select();
64726
+ onnavi_request_select: e => {
64727
+ requestPickerListEntry(ref.current, inputRef.current, e, "select");
63823
64728
  },
63824
- onCopy: e => {
63825
- const pickerEl = ref.current;
63826
- if (isWithinPickerContent(e.target, pickerEl)) {
63827
- return;
63828
- }
63829
- const uiState = uiStateController.uiState;
63830
- if (uiState === undefined) {
63831
- return;
63832
- }
63833
- e.preventDefault();
63834
- const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
63835
- e.clipboardData.setData("text/plain", displayText);
63836
- e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
64729
+ onnavi_request_unselect: e => {
64730
+ requestPickerListEntry(ref.current, inputRef.current, e, "unselect");
63837
64731
  },
63838
- onCut: e => {
63839
- const pickerEl = ref.current;
63840
- if (isWithinPickerContent(e.target, pickerEl)) {
63841
- return;
63842
- }
63843
- const uiState = uiStateController.uiState;
63844
- if (uiState === undefined) {
63845
- return;
63846
- }
63847
- // the copy part don't need control to be interactable
63848
- const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
63849
- e.clipboardData.setData("text/plain", displayText);
63850
- e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
63851
- // the clear ui state part need control to be interactable
63852
- dispatchRequestInteraction(pickerEl, {
63853
- event: e,
63854
- name: "cut",
63855
- allowed: () => {
63856
- dispatchRequestClearUIState(inputRef.current, e);
63857
- }
63858
- });
63859
- e.preventDefault();
63860
- },
63861
- onPaste: e => {
63862
- const pickerEl = ref.current;
63863
- if (isWithinPickerContent(e.target, pickerEl)) {
63864
- // Don't intercept inside the picker popup content.
63865
- return;
63866
- }
63867
- const naviData = e.clipboardData.getData("application/x-navi");
63868
- let pasteValue;
63869
- if (naviData) {
63870
- try {
63871
- pasteValue = JSON.parse(naviData);
63872
- } catch {
63873
- pasteValue = naviData;
63874
- }
63875
- } else {
63876
- pasteValue = e.clipboardData.getData("text/plain");
63877
- }
63878
- dispatchRequestInteraction(pickerEl, {
63879
- event: e,
63880
- name: "paste",
63881
- allowed: () => {
63882
- dispatchRequestSetUIState(inputRef.current, pasteValue, {
63883
- event: e
64732
+ children: [jsxs("span", {
64733
+ className: "navi_picker_box",
64734
+ children: [variant === "headless" ? null : jsx(LoadingOutline, {
64735
+ loading: loading,
64736
+ color: "var(--picker-loader-color)",
64737
+ inset: -2
64738
+ }), jsx(PickerInput, {
64739
+ tabIndex: variant === "headless" ? -1 : undefined,
64740
+ "aria-hidden": variant === "headless" ? "true" : undefined,
64741
+ ...inputProps,
64742
+ // eslint-disable-next-line react/no-children-prop
64743
+ children: undefined // we will render children into the div
64744
+ ,
64745
+
64746
+ ui: ui,
64747
+ onFocus: e => {
64748
+ inputProps.onFocus?.(e);
64749
+ e.target.select();
64750
+ },
64751
+ onCopy: e => {
64752
+ const pickerEl = ref.current;
64753
+ if (isWithinPickerContent(e.target, pickerEl)) {
64754
+ return;
64755
+ }
64756
+ const uiState = uiStateController.uiState;
64757
+ if (uiState === undefined) {
64758
+ return;
64759
+ }
64760
+ e.preventDefault();
64761
+ const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
64762
+ e.clipboardData.setData("text/plain", displayText);
64763
+ e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
64764
+ },
64765
+ onCut: e => {
64766
+ const pickerEl = ref.current;
64767
+ if (isWithinPickerContent(e.target, pickerEl)) {
64768
+ return;
64769
+ }
64770
+ const uiState = uiStateController.uiState;
64771
+ if (uiState === undefined) {
64772
+ return;
64773
+ }
64774
+ // the copy part don't need control to be interactable
64775
+ const displayText = pickerEl.querySelector(".navi_picker_value")?.textContent ?? String(uiState);
64776
+ e.clipboardData.setData("text/plain", displayText);
64777
+ e.clipboardData.setData("application/x-navi", JSON.stringify(uiState));
64778
+ // the clear ui state part need control to be interactable
64779
+ dispatchRequestInteraction(pickerEl, {
64780
+ event: e,
64781
+ name: "cut",
64782
+ allowed: () => {
64783
+ dispatchRequestClearUIState(inputRef.current, e);
64784
+ }
63884
64785
  });
64786
+ e.preventDefault();
64787
+ },
64788
+ onPaste: e => {
64789
+ const pickerEl = ref.current;
64790
+ if (isWithinPickerContent(e.target, pickerEl)) {
64791
+ // Don't intercept inside the picker popup content.
64792
+ return;
64793
+ }
64794
+ const naviData = e.clipboardData.getData("application/x-navi");
64795
+ let pasteValue;
64796
+ if (naviData) {
64797
+ try {
64798
+ pasteValue = JSON.parse(naviData);
64799
+ } catch {
64800
+ pasteValue = naviData;
64801
+ }
64802
+ } else {
64803
+ pasteValue = e.clipboardData.getData("text/plain");
64804
+ }
64805
+ dispatchRequestInteraction(pickerEl, {
64806
+ event: e,
64807
+ name: "paste",
64808
+ allowed: () => {
64809
+ dispatchRequestSetUIState(inputRef.current, pasteValue, {
64810
+ event: e
64811
+ });
64812
+ }
64813
+ });
64814
+ e.preventDefault();
63885
64815
  }
63886
- });
63887
- e.preventDefault();
63888
- }
63889
- }), variant === "icon" || variant === "headless" || ui === "default" ? null : jsx(Text, {
63890
- className: "navi_picker_value",
63891
- "navi-placeholder": value === undefined || value === "" ? "" : undefined,
63892
- maxLines: maxLines,
63893
- children: jsx(PickerContext.Provider, {
63894
- value: {
63895
- value,
63896
- placeholder,
63897
- maxLines
63898
- },
63899
- children: ui === undefined ? jsx(PickerDefaultUI, {}) : ui
63900
- })
63901
- }), variant === "headless" || ui === "default" ? null : jsx("span", {
63902
- className: "navi_picker_right_slot",
63903
- children: jsx(ControlIdContext.Provider, {
63904
- value: undefined,
63905
- children: jsx(ControlNameContext.Provider, {
63906
- value: undefined,
63907
- children: clearable && interactive && value !== undefined && value !== "" ? jsx(Button, {
63908
- command: "--navi-clear",
63909
- commandFor: inputProps.id
63910
- // The question, asked before the clear rather than by the
63911
- // action the clear sends — see the --navi-clear command.
63912
- ,
63913
-
63914
- confirm: clearConfirm,
63915
- confirmPopupContent: clearConfirmPopupContent,
63916
- tabIndex: "-1"
63917
- // No navi-focus-delegate, unlike the identical button inside an
63918
- // input: handing focus back to the picker's own input is what
63919
- // opens the popup, and clearing is the opposite intention.
63920
- ,
63921
-
63922
- icon: true,
63923
- variant: "discrete"
63924
- // What is busy once the clear is sent is the picker — the value
63925
- // being removed is the whole field's, and the picker already
63926
- // draws the wait around all of it. Two outlines for one wait is
63927
- // one too many.
63928
- ,
63929
-
63930
- loadingOutline: false
63931
- // preventDefault, not just tabIndex="-1": a mousedown focuses
63932
- // its target before any click happens, and this button should
63933
- // never hold focus at all — the field keeps it.
63934
- ,
63935
-
63936
- onMouseDown: e => {
63937
- e.preventDefault();
63938
- },
63939
- flex: true,
63940
- align: "center",
63941
- children: jsx(Icon, {
64816
+ }), variant === "icon" || variant === "headless" || ui === "default" ? null : jsx(Text, {
64817
+ className: "navi_picker_value",
64818
+ "navi-placeholder": uiStateHoldsNothing(value) ? "" : undefined,
64819
+ maxLines: maxLines,
64820
+ children: jsx(PickerOwnContent, {
64821
+ children: jsx(PickerContext.Provider, {
64822
+ value: {
64823
+ value,
64824
+ placeholder,
64825
+ maxLines
64826
+ },
64827
+ children: jsx(MaxLinesContext.Provider, {
64828
+ value: maxLines,
64829
+ children: ui === undefined ? jsx(PickerDefaultUI, {}) : ui
64830
+ })
64831
+ })
64832
+ })
64833
+ }), variant === "headless" || ui === "default" ? null : jsx("span", {
64834
+ className: "navi_picker_right_slot",
64835
+ children: jsx(PickerOwnContent, {
64836
+ children: clearable && interactive && value !== undefined && value !== "" ? jsx(Button, {
64837
+ command: "--navi-clear",
64838
+ commandFor: inputProps.id
64839
+ // The question, asked before the clear rather than by the
64840
+ // action the clear sends see the --navi-clear command.
64841
+ ,
64842
+
64843
+ confirm: clearConfirm,
64844
+ confirmPopupContent: clearConfirmPopupContent,
64845
+ tabIndex: "-1"
64846
+ // No navi-focus-delegate, unlike the identical button inside an
64847
+ // input: handing focus back to the picker's own input is what
64848
+ // opens the popup, and clearing is the opposite intention.
64849
+ ,
64850
+
64851
+ icon: true,
64852
+ variant: "discrete"
64853
+ // What is busy once the clear is sent is the picker — the value
64854
+ // being removed is the whole field's, and the picker already
64855
+ // draws the wait around all of it. Two outlines for one wait is
64856
+ // one too many.
64857
+ ,
64858
+
64859
+ loadingOutline: false
64860
+ // preventDefault, not just tabIndex="-1": a mousedown focuses
64861
+ // its target before any click happens, and this button should
64862
+ // never hold focus at all the field keeps it.
64863
+ ,
64864
+
64865
+ onMouseDown: e => {
64866
+ e.preventDefault();
64867
+ },
64868
+ flex: true,
64869
+ align: "center",
64870
+ children: jsx(Icon, {
64871
+ size: rightSlotIconSize,
64872
+ lineOverflow: "allow",
64873
+ children: jsx(CloseSvg, {})
64874
+ })
64875
+ }) : rightSlot === undefined ?
64876
+ // lineOverflow: what sits in the slot is an affordance, not a
64877
+ // character — a caller asking for a bigger one wants it bigger,
64878
+ // not capped at the height of the line it sits on
64879
+ jsx(Icon, {
63942
64880
  size: rightSlotIconSize,
63943
64881
  lineOverflow: "allow",
63944
- children: jsx(CloseSvg, {})
63945
- })
63946
- }) : rightSlot === undefined ?
63947
- // lineOverflow: what sits in the slot is an affordance, not a
63948
- // character — a caller asking for a bigger one wants it bigger,
63949
- // not capped at the height of the line it sits on
63950
- jsx(Icon, {
63951
- size: rightSlotIconSize,
63952
- lineOverflow: "allow",
63953
- children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
63954
- }) : rightSlot
64882
+ children: rightSlotIcon === undefined ? jsx(ChevronDownSvg$1, {}) : rightSlotIcon
64883
+ }) : rightSlot
64884
+ })
64885
+ })]
64886
+ }), jsx(ControlFacadeChildrenWrapper, {
64887
+ ...facadeChildrenProps,
64888
+ children: jsx("div", {
64889
+ className: "navi_picker_content",
64890
+ children: children
63955
64891
  })
63956
- })
63957
- })]
63958
- }), jsx(ControlFacadeChildrenWrapper, {
63959
- ...facadeChildrenProps,
63960
- children: jsx(ReadOnlyContext.Provider, {
63961
- value: readOnlyResolved,
63962
- children: jsx("div", {
63963
- className: "navi_picker_content",
63964
- children: children
63965
- })
64892
+ })]
63966
64893
  })
63967
- })]
64894
+ })
64895
+ );
64896
+ };
64897
+ // What the picker draws itself — the value it shows, the furniture in its slot,
64898
+ // and whatever a caller puts in either — is not another control of the field
64899
+ // around it: none of it may take the id (nor the name) a <Field> hands down,
64900
+ // which is the picker's. Two controls under one id is one registry entry, and
64901
+ // the one that unmounts first — the clear cross the moment the field it emptied
64902
+ // is empty, a chip the moment its value is taken out — takes the picker's own
64903
+ // entry with it, leaving the picker looking for a controller that is gone.
64904
+ const PickerOwnContent = ({
64905
+ children
64906
+ }) => jsx(ControlIdContext.Provider, {
64907
+ value: undefined,
64908
+ children: jsx(ControlNameContext.Provider, {
64909
+ value: undefined,
64910
+ children: children
64911
+ })
64912
+ });
64913
+
64914
+ // `id` is what --navi-select/--navi-unselect carry (a list addresses its rows by
64915
+ // id); asked of a picker, what they carry is one entry of the list the picker
64916
+ // holds — the same thing a `<Picker.Chip value>` stands for.
64917
+ const requestPickerListEntry = (pickerEl, pickerInputEl, e, goal) => {
64918
+ const uiState = getUIStateFromElement(pickerInputEl);
64919
+ if (!Array.isArray(uiState)) {
64920
+ return;
64921
+ }
64922
+ const {
64923
+ id: entry
64924
+ } = e.detail;
64925
+ const isThere = uiState.some(item => compareTwoJsValues(item, entry));
64926
+ if (goal === "select" ? isThere : !isThere) {
64927
+ return;
64928
+ }
64929
+ const uiStateNext = goal === "select" ? [...uiState, entry] : uiState.filter(item => !compareTwoJsValues(item, entry));
64930
+ dispatchRequestInteraction(pickerEl, {
64931
+ event: e,
64932
+ name: goal,
64933
+ prevented: () => e.preventDefault(),
64934
+ allowed: () => {
64935
+ dispatchRequestSetUIState(pickerInputEl, uiStateNext, {
64936
+ event: e
64937
+ });
64938
+ }
63968
64939
  });
63969
64940
  };
63970
64941
  const isWithinPickerContent = (el, pickerEl) => {
@@ -64109,6 +65080,7 @@ const PickerFirstResolver = props => {
64109
65080
  });
64110
65081
  };
64111
65082
  const Picker = createComponentResolver([PickerFirstResolver, PickerPresetResolver, PickerCustomResolver, PickerTypeResolver, PickerButton]);
65083
+ Picker.Chip = PickerChip;
64112
65084
  Picker.UI = PickerDefaultUI;
64113
65085
  Picker.UI.Date = PickerDateUI;
64114
65086
  Picker.UI.Time = PickerTimeUI;
@@ -74904,6 +75876,9 @@ const MessageBox = ({
74904
75876
  padding = "sm",
74905
75877
  icon,
74906
75878
  leftStripe,
75879
+ // A message is free text: an emoji is expected in it, and must not push
75880
+ // the first line down next to the icon and the close button.
75881
+ emojiAsIcon = true,
74907
75882
  children,
74908
75883
  onClose,
74909
75884
  ...rest
@@ -74941,26 +75916,39 @@ const MessageBox = ({
74941
75916
  value: setHasTitleChild,
74942
75917
  children: [icon && jsx(Icon, {
74943
75918
  color: "var(--x-message-color)",
74944
- height: "1.5em",
75919
+ height: "1lh",
74945
75920
  maxHeight: "auto",
74946
75921
  selfAlignY: "start",
74947
75922
  aspectRatio: "auto",
74948
75923
  children: icon
74949
75924
  }), jsx(Text, {
75925
+ emojiAsIcon: emojiAsIcon,
74950
75926
  children: children
74951
- }), onClose && jsx(Button, {
74952
- action: onClose,
74953
- icon: true,
74954
- border: "none",
74955
- alignX: "center",
75927
+ }), onClose &&
75928
+ // A column as tall as the first line of the message, pinned to the
75929
+ // top, the button centered in it: the close button stays level
75930
+ // with the first line however many lines the message takes (same
75931
+ // layout as Callout).
75932
+ jsx(Box, {
75933
+ flex: true,
74956
75934
  alignY: "center",
74957
- style: {
74958
- ":hover": {
74959
- backgroundColor: "rgba(0, 0, 0, 0.1)"
74960
- }
74961
- },
74962
- children: jsx(Icon, {
74963
- children: jsx(CloseSvg, {})
75935
+ height: "1lh",
75936
+ selfAlignY: "start",
75937
+ shrink: false,
75938
+ children: jsx(Button, {
75939
+ action: onClose,
75940
+ icon: true,
75941
+ border: "none",
75942
+ alignX: "center",
75943
+ alignY: "center",
75944
+ style: {
75945
+ ":hover": {
75946
+ backgroundColor: "rgba(0, 0, 0, 0.1)"
75947
+ }
75948
+ },
75949
+ children: jsx(Icon, {
75950
+ children: jsx(CloseSvg, {})
75951
+ })
74964
75952
  })
74965
75953
  })]
74966
75954
  })
@@ -77048,5 +78036,5 @@ const UserSvg = () => jsx("svg", {
77048
78036
  })
77049
78037
  });
77050
78038
 
77051
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineNaviConfirmPopupOptions, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
78039
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineNaviConfirmPopupOptions, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, renderEmojiAsIcon, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
77052
78040
  //# sourceMappingURL=jsenv_navi.js.map