@jsenv/navi 0.29.103 → 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",
@@ -2335,17 +2335,37 @@ const ActionRequesterContext = createContext();
2335
2335
  * one wrong finds out by opening a popup it meant to keep shut. `ownTarget` is
2336
2336
  * that knowledge, said once.
2337
2337
  *
2338
- * The other half is interactivity: the affordance is a control, so it already
2339
- * refuses on its own terms once the zone around it holds it read-only — but a
2340
- * caller's `onClick` is plain DOM and fires before any gate. So an own target
2341
- * withholds the caller's handler until its own gate has allowed it, and by
2342
- * default goes rather than greys: a remove cross that still removes is worse
2343
- * than no cross, and one that refuses politely still says "there is something to
2344
- * remove here" on a row that is only being read.
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.
2345
2356
  */
2346
2357
 
2347
2358
 
2348
- const OWN_TARGET_ATTRIBUTE = "data-navi-own-target";
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";
2349
2369
 
2350
2370
  /**
2351
2371
  * Whether `event` was aimed at an own target sitting below this control — in
@@ -2354,7 +2374,7 @@ const OWN_TARGET_ATTRIBUTE = "data-navi-own-target";
2354
2374
  * Read from the event's target rather than from a mark left by a handler: the
2355
2375
  * question is "who is this press for", and the DOM between the pointer and the
2356
2376
  * control is the whole answer. Nothing is asked of the own target itself, which
2357
- * is what lets it be anything — a button, a link, a field.
2377
+ * is what lets it be anything — a button, a link, a field, a bare element.
2358
2378
  */
2359
2379
  const isAimedAtOwnTargetBelow = (event, controlHost) => {
2360
2380
  const target = event?.target;
@@ -2378,19 +2398,25 @@ const isAimedAtOwnTargetBelow = (event, controlHost) => {
2378
2398
  };
2379
2399
 
2380
2400
  /**
2381
- * Whether an own target has nothing to offer where it sits: the zone around it
2382
- * is read-only, disabled or busy, so the affordance goes.
2383
- *
2384
- * `ownTarget="refuse"` keeps it on screen instead, refusing with a callout like
2385
- * every other navi control — for an affordance whose presence is information in
2386
- * itself (an eye that opens a profile is worth seeing on a row being read).
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.
2387
2409
  */
2388
2410
  const useOwnTargetHidden = (props) => {
2389
2411
  const disabled = useContext(DisabledContext);
2390
2412
  const readOnly = useContext(ReadOnlyContext);
2391
2413
  const loading = useContext(LoadingContext$1);
2392
2414
  const { ownTarget } = props;
2393
- if (!ownTarget || ownTarget === "refuse") {
2415
+ if (
2416
+ !ownTarget ||
2417
+ ownTarget === "refuse" ||
2418
+ ownTargetIgnoresZoneState(ownTarget)
2419
+ ) {
2394
2420
  return false;
2395
2421
  }
2396
2422
  return Boolean(disabled || readOnly || loading);
@@ -6861,6 +6887,10 @@ const findControlProxy = (el) => {
6861
6887
  return firstProxy;
6862
6888
  };
6863
6889
 
6890
+ let renderMessageText = text => text;
6891
+ const setCalloutMessageTextRenderer = renderer => {
6892
+ renderMessageText = renderer;
6893
+ };
6864
6894
  const CalloutRequestCloseContext = createContext();
6865
6895
  const useCalloutRequestClose = () => {
6866
6896
  return useContext(CalloutRequestCloseContext);
@@ -6875,6 +6905,48 @@ const renderIntoCallout = (jsx$1, calloutMessageElement, {
6875
6905
  render(calloutJsx, calloutMessageElement);
6876
6906
  };
6877
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
+
6878
6950
  installImportMetaCssBuild(import.meta);/**
6879
6951
  * A callout component that mimics native browser validation messages.
6880
6952
  * Features:
@@ -6884,7 +6956,7 @@ installImportMetaCssBuild(import.meta);/**
6884
6956
  * - Arrow automatically shows when pointing at a valid anchor element
6885
6957
  * - Centers in viewport when no anchor element provided or anchor is too big
6886
6958
  */
6887
- const css$13 = /* css */`
6959
+ const css$12 = /* css */`
6888
6960
  @layer navi {
6889
6961
  .navi_callout {
6890
6962
  /* A callout is parented to what it explains, so it inherits from it — and
@@ -6986,7 +7058,10 @@ const css$13 = /* css */`
6986
7058
  }
6987
7059
 
6988
7060
  .navi_callout_body {
6989
- --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);
6990
7065
 
6991
7066
  position: relative;
6992
7067
  display: flex;
@@ -7042,10 +7117,15 @@ const css$13 = /* css */`
7042
7117
  align-self: flex-start;
7043
7118
 
7044
7119
  .navi_callout_close_button {
7045
- width: 1em;
7046
- height: 1em;
7047
- padding: 0;
7048
- 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;
7049
7129
  color: currentColor;
7050
7130
  font-size: inherit;
7051
7131
  background: none;
@@ -7127,7 +7207,7 @@ const openCallout = (message, {
7127
7207
  skipFocus = false,
7128
7208
  debug = () => {}
7129
7209
  } = {}) => {
7130
- 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"];
7131
7211
  if (debug === true) {
7132
7212
  debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
7133
7213
  }
@@ -7309,11 +7389,11 @@ const openCallout = (message, {
7309
7389
  } else if (newMessage instanceof Node) {
7310
7390
  // Handle DOM node (cloned from CSS selector)
7311
7391
  debug(`callout update message (node)`);
7312
- calloutMessageElement.innerHTML = "";
7392
+ clearCalloutMessage(calloutMessageElement);
7313
7393
  calloutMessageElement.appendChild(newMessage);
7314
7394
  } else if (typeof newMessage === "function") {
7315
7395
  debug(`callout update message (function)`);
7316
- calloutMessageElement.innerHTML = "";
7396
+ clearCalloutMessage(calloutMessageElement);
7317
7397
  newMessage({
7318
7398
  renderIntoCallout: jsx => renderIntoCallout(jsx, calloutMessageElement, {
7319
7399
  requestClose
@@ -7338,12 +7418,13 @@ const openCallout = (message, {
7338
7418
  iframe.style.backgroundColor = "white";
7339
7419
  iframe.srcdoc = newMessage;
7340
7420
  debug(`callout update message (html document iframe)`);
7341
- // Clear existing content and add iframe
7342
- calloutMessageElement.innerHTML = "";
7421
+ clearCalloutMessage(calloutMessageElement);
7343
7422
  calloutMessageElement.appendChild(iframe);
7344
7423
  } else {
7345
7424
  debug(`callout update message: ${typeof newMessage === "string" ? newMessage.slice(0, 80) : String(newMessage)}`);
7346
- calloutMessageElement.innerHTML = newMessage;
7425
+ renderHtmlIntoCallout(String(newMessage), calloutMessageElement, {
7426
+ requestClose
7427
+ });
7347
7428
  }
7348
7429
  }
7349
7430
  // After updating content the callout size likely changed — re-position immediately
@@ -19419,7 +19500,7 @@ const setupNetworkMonitoring = () => {
19419
19500
  };
19420
19501
  setupNetworkMonitoring();
19421
19502
 
19422
- installImportMetaCssBuild(import.meta);const css$12 = /* css */`
19503
+ installImportMetaCssBuild(import.meta);const css$11 = /* css */`
19423
19504
  .navi_loading_indicator_fluid_container {
19424
19505
  position: relative;
19425
19506
  display: flex;
@@ -19451,7 +19532,7 @@ const LoadingIndicatorFluid = ({
19451
19532
  visuallyHidden,
19452
19533
  ...rest
19453
19534
  }) => {
19454
- 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"];
19455
19536
  const ref = useRef(null);
19456
19537
  // The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
19457
19538
  const [containerWidth, setContainerWidth] = useState(0);
@@ -19656,7 +19737,7 @@ const LoadingRectangleSvg = ({
19656
19737
  });
19657
19738
  };
19658
19739
 
19659
- installImportMetaCssBuild(import.meta);const css$11 = /* css */`
19740
+ installImportMetaCssBuild(import.meta);const css$10 = /* css */`
19660
19741
  .navi_loading_outline_wrapper {
19661
19742
  position: absolute;
19662
19743
  /* Controls place the outline slightly outside their box, right on top of
@@ -19693,7 +19774,7 @@ installImportMetaCssBuild(import.meta);const css$11 = /* css */`
19693
19774
  }
19694
19775
  `;
19695
19776
  const LoadingOutline = props => {
19696
- 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"];
19697
19778
  if (props.containerRef) {
19698
19779
  const container = props.containerRef.current;
19699
19780
  if (!container) {
@@ -19883,6 +19964,256 @@ const getHrefTargetInfo = (href) => {
19883
19964
  };
19884
19965
  };
19885
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
+
19886
20217
  const useInitialTextSelection = (ref, textSelection) => {
19887
20218
  const deps = [];
19888
20219
  if (Array.isArray(textSelection)) {
@@ -19983,7 +20314,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
19983
20314
  };
19984
20315
 
19985
20316
  installImportMetaCssBuild(import.meta);// https://jsfiddle.net/v5xzJ/4/
19986
- const css$10 = /* css */`
20317
+ const css$_ = /* css */`
19987
20318
  @layer navi {
19988
20319
  .navi_text {
19989
20320
  &[data-skeleton] {
@@ -20167,6 +20498,91 @@ const css$10 = /* css */`
20167
20498
  opacity: 1;
20168
20499
  }
20169
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
+ }
20170
20586
  `;
20171
20587
  const REGULAR_SPACE = jsx("span", {
20172
20588
  "data-navi-space": "",
@@ -20349,6 +20765,7 @@ const shouldInjectSpacingBetween = (left, right) => {
20349
20765
  * loading?: boolean,
20350
20766
  * skeleton?: boolean,
20351
20767
  * attachLastChild?: boolean,
20768
+ * emojiAsIcon?: boolean,
20352
20769
  * preventSpaceUnderlines?: boolean,
20353
20770
  * holdSpaceForStyle?: import("ignore:preact").JSX.CSSProperties,
20354
20771
  * boldStable?: boolean,
@@ -20388,6 +20805,12 @@ const shouldInjectSpacingBetween = (left, right) => {
20388
20805
  * truncation belongs outside the `Text` instead (see `docs/typography.md`).
20389
20806
  * `Link` sets it on its own whenever it renders an end icon.
20390
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
+ *
20391
20814
  * @param {boolean} [preventSpaceUnderlines]
20392
20815
  * Replaces real space characters between children with padding-based spaces.
20393
20816
  * Useful inside `<a>` elements where browsers draw an underline under spaces.
@@ -20504,12 +20927,13 @@ const TextShrinkWrap = props => {
20504
20927
  });
20505
20928
  };
20506
20929
  const TextUI = props => {
20507
- import.meta.css = [css$10, "@jsenv/navi/src/text/text.jsx"];
20930
+ import.meta.css = [css$_, "@jsenv/navi/src/text/text.jsx"];
20508
20931
  let {
20509
20932
  ref,
20510
20933
  spacing,
20511
20934
  preventSpaceUnderlines = false,
20512
20935
  attachLastChild = false,
20936
+ emojiAsIcon = false,
20513
20937
  boldStable,
20514
20938
  holdSpaceForStyle,
20515
20939
  capitalize,
@@ -20540,6 +20964,11 @@ const TextUI = props => {
20540
20964
  } else {
20541
20965
  children = applySpacingOnTextChildren(children, resolvedSpacing, defaultSpace);
20542
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
+ }
20543
20972
  if (boldStable) {
20544
20973
  const {
20545
20974
  bold
@@ -20654,6 +21083,244 @@ const TextWithSelectRange = ({
20654
21083
  });
20655
21084
  };
20656
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
+
20657
21324
  const LIGHT_ACCENT_ATTRIBUTE = "data-accent-light";
20658
21325
  const VERY_LIGHT_ACCENT_ATTRIBUTE = "data-accent-very-light";
20659
21326
  const DARK_CONTRAST_ATTRIBUTE = "data-accent-needs-dark-fg";
@@ -20797,105 +21464,6 @@ const useActionStatus = (action) => {
20797
21464
  };
20798
21465
  };
20799
21466
 
20800
- /**
20801
- * A variant of useLayoutEffect that accounts for ancestor <dialog>/<details>
20802
- * or popover visibility.
20803
- *
20804
- * Motivation: some effects (auto-scroll, measurement, focus) only make sense
20805
- * when the element is actually presented on screen. A plain useLayoutEffect
20806
- * fires on mount even when the component is inside a closed <dialog>, a
20807
- * collapsed <details>, or a hidden popover, where scroll and layout operations
20808
- * are no-ops.
20809
- *
20810
- * Behavior:
20811
- * - No <dialog>/<details>/[popover] ancestor → runs like a normal
20812
- * useLayoutEffect with the provided deps.
20813
- * - Inside a closed/hidden ancestor → skips the initial run; instead runs
20814
- * the callback once the ancestor opens — see @jsenv/dom's own
20815
- * observeAncestorOpenState for exactly how that's detected, and why it
20816
- * matters that it happens before the browser paints.
20817
- * - Inside an open ancestor → runs on mount AND every subsequent open.
20818
- *
20819
- * The callback's second argument is always a `navi_displayed` CustomEvent,
20820
- * with `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
20821
- * - No <dialog>/<details>/[popover]/[aria-expanded] ancestor at all →
20822
- * `{ ancestor: document, ancestorType: "document" }`.
20823
- * - Otherwise → `{ ancestor: <the matched element>, ancestorType: "dialog"
20824
- * | "popover" | "details" | "aria-expanded" }`.
20825
- * `becauseAncestorOpened` distinguishes the two ways of coming on screen:
20826
- * - true — the element was already mounted and the ancestor just opened,
20827
- * revealing it along with everything else it holds. The opening has an
20828
- * owner (the ancestor's own transferFocus/openEffect), and what it reveals
20829
- * should defer to it — see use_auto_focus.js.
20830
- * - false — the element was mounted just now, into a surface already on
20831
- * screen (or into the plain document). Nothing else owns this appearance:
20832
- * what the element says about itself (an autofocus, a measurement) is the
20833
- * only word there is.
20834
- *
20835
- * Usage:
20836
- * useDisplayedLayoutEffect(ref, () => {
20837
- * scrollToSelected();
20838
- * }, []);
20839
- */
20840
- const useDisplayedLayoutEffect = (ref, callback, deps) => {
20841
- if (typeof callback !== "function") {
20842
- throw new TypeError("useDisplayedLayoutEffect: callback is not a function");
20843
- }
20844
-
20845
- // Keep a stable ref so the open listener always calls the latest callback
20846
- // without needing to be re-registered when deps change.
20847
- const callbackRef = useRef(callback);
20848
- callbackRef.current = callback;
20849
-
20850
- // Run on mount (or when deps change) — but only if the element is visible.
20851
- useLayoutEffect(() => {
20852
- const el = ref.current;
20853
- if (!el) {
20854
- return;
20855
- }
20856
- const ancestor = closestOpenableAncestor(el);
20857
- if (!ancestor) {
20858
- callbackRef.current(el, createDisplayedEvent(document, false));
20859
- return;
20860
- }
20861
- if (!isAncestorOpen(ancestor)) {
20862
- // Ancestor is closed — skip now; the observeAncestorOpenState call
20863
- // below will fire once it opens.
20864
- return;
20865
- }
20866
- callbackRef.current(el, createDisplayedEvent(ancestor, false));
20867
- }, deps);
20868
-
20869
- // Re-run every time the ancestor opens.
20870
- useLayoutEffect(() => {
20871
- const el = ref.current;
20872
- if (!el) {
20873
- return undefined;
20874
- }
20875
- const ancestor = closestOpenableAncestor(el);
20876
- if (!ancestor) {
20877
- return undefined;
20878
- }
20879
- return observeAncestorOpenState(ancestor, ({ isOpen }) => {
20880
- if (!isOpen) {
20881
- return;
20882
- }
20883
- const lastEl = ref.current;
20884
- callbackRef.current(lastEl, createDisplayedEvent(ancestor, true));
20885
- });
20886
- }, []);
20887
- };
20888
-
20889
- const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
20890
- return new CustomEvent("navi_displayed", {
20891
- detail: {
20892
- ancestor,
20893
- ancestorType: getAncestorOpenType(ancestor),
20894
- becauseAncestorOpened,
20895
- },
20896
- });
20897
- };
20898
-
20899
21467
  /**
20900
21468
  * Decides which element receives focus when a container (popover, dialog, …)
20901
21469
  * opens, and gives it back to where it came from when the container closes.
@@ -27435,13 +28003,11 @@ const useInteractiveProps = (props, {
27435
28003
  const [controlRootProps, controlHostProps] = splitControlProps(props);
27436
28004
  controlRootProps["navi-control"] = controlInfo.controlType;
27437
28005
  if (props.ownTarget) {
27438
- // "This press is mine" said in the DOM, because that is where it is read
27439
- // from the outside: by the controls above (see own_target.js), and by the
27440
- // two gesture readers below the one that travels a box and the one that
27441
- // carries a piece, each with its own way of being told to keep out.
27442
- controlRootProps[OWN_TARGET_ATTRIBUTE] = "";
27443
- controlRootProps["data-no-drag-travel"] = "";
27444
- controlRootProps["data-drag-ignore"] = "";
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 : "";
27445
28011
  }
27446
28012
  const {
27447
28013
  "navi-control-proxy-for": naviProxyFor
@@ -27477,10 +28043,10 @@ const useInteractiveProps = (props, {
27477
28043
  });
27478
28044
  }
27479
28045
  {
27480
- const controlDisabled = useContext(DisabledContext);
27481
- const controlReadOnly = useContext(ReadOnlyContext);
28046
+ const controlDisabledFromAbove = useContext(DisabledContext);
28047
+ const controlReadOnlyFromAbove = useContext(ReadOnlyContext);
27482
28048
  const controlRequired = useContext(RequiredContext);
27483
- const controlLoading = useContext(LoadingContext$1);
28049
+ const controlLoadingFromAbove = useContext(LoadingContext$1);
27484
28050
  const parentActionRequester = useContext(ActionRequesterContext);
27485
28051
  const actionStatus = useActionStatus(boundAction);
27486
28052
  const {
@@ -27490,6 +28056,16 @@ const useInteractiveProps = (props, {
27490
28056
  loading,
27491
28057
  optimistic
27492
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;
27493
28069
  const disabledResolved = disabled || controlDisabled;
27494
28070
  const requiredResolved = required || controlRequired;
27495
28071
  // Busy because the group above is running the action THIS control asked
@@ -27501,7 +28077,7 @@ const useInteractiveProps = (props, {
27501
28077
  // (`maxLengthGuard`) and this one would make it longer: it can be pointed
27502
28078
  // at, focused and pressed — and answers why (see readonly_constraint.js) —
27503
28079
  // but cannot be taken.
27504
- const readOnlyFromParentMaxLengthGuard = Boolean(uiStateController.parentUIStateController?.isChildBlockedByMaxLengthGuard?.(uiStateController));
28080
+ const readOnlyFromParentMaxLengthGuard = Boolean(zoneStateApplies && uiStateController.parentUIStateController?.isChildBlockedByMaxLengthGuard?.(uiStateController));
27505
28081
  const readOnlyBase = readOnly || controlReadOnly || loadingBase || readOnlyFromParentMaxLengthGuard || controlInfo.readOnlyUncontrolled;
27506
28082
  // An optimistic control trusts its action to succeed: the state the user
27507
28083
  // just set stays visible and interactive while the action runs — no
@@ -27935,7 +28511,7 @@ const getAssociatedLabels = element => {
27935
28511
  return Array.from(element.labels);
27936
28512
  };
27937
28513
 
27938
- installImportMetaCssBuild(import.meta);const css$$ = /* css */`
28514
+ installImportMetaCssBuild(import.meta);const css$Z = /* css */`
27939
28515
  @layer navi {
27940
28516
  .navi_button {
27941
28517
  --button-border-radius: var(--navi-control-border-radius);
@@ -28366,7 +28942,7 @@ installImportMetaCssBuild(import.meta);const css$$ = /* css */`
28366
28942
  }
28367
28943
  `;
28368
28944
  const ButtonUI = props => {
28369
- 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"];
28370
28946
  const {
28371
28947
  ref,
28372
28948
  // href/link
@@ -28379,6 +28955,7 @@ const ButtonUI = props => {
28379
28955
  icon,
28380
28956
  cta,
28381
28957
  spacing,
28958
+ emojiAsIcon = true,
28382
28959
  // Whether the button draws the loading outline itself. A button that is
28383
28960
  // one half of a bigger control says no: what is busy is the control, and
28384
28961
  // the outline belongs around the whole of it (see split_button.jsx).
@@ -28436,6 +29013,7 @@ const ButtonUI = props => {
28436
29013
 
28437
29014
  type: "button",
28438
29015
  spacing: undefined,
29016
+ emojiAsIcon: undefined,
28439
29017
  cta: undefined,
28440
29018
  pressEffect: undefined,
28441
29019
  loadingOutline: undefined,
@@ -28489,6 +29067,7 @@ const ButtonUI = props => {
28489
29067
  ...controlChildrenWrapperProps,
28490
29068
  children: jsx(ButtonContent, {
28491
29069
  spacing: spacing,
29070
+ emojiAsIcon: emojiAsIcon,
28492
29071
  children: children
28493
29072
  })
28494
29073
  })]
@@ -28496,6 +29075,7 @@ const ButtonUI = props => {
28496
29075
  };
28497
29076
  const ButtonContent = ({
28498
29077
  spacing,
29078
+ emojiAsIcon,
28499
29079
  children
28500
29080
  }) => {
28501
29081
  const boxForwardedProps = useContext(BoxForwardedPropsContext);
@@ -28503,6 +29083,7 @@ const ButtonContent = ({
28503
29083
  ...boxForwardedProps,
28504
29084
  display: "inherit",
28505
29085
  spacing: spacing,
29086
+ emojiAsIcon: emojiAsIcon,
28506
29087
  className: "navi_button_content",
28507
29088
  children: [children, jsx(ButtonShadow, {})]
28508
29089
  });
@@ -28665,17 +29246,22 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
28665
29246
 
28666
29247
  /**
28667
29248
  * @type {import("ignore:preact").FunctionComponent<{
28668
- * ownTarget?: boolean | "refuse",
29249
+ * ownTarget?: boolean | "refuse" | "always",
29250
+ * emojiAsIcon?: boolean,
28669
29251
  * [key: string]: any,
28670
29252
  * }>}
28671
- * @param {boolean|"refuse"} [ownTarget] A real target inside a zone that belongs
28672
- * to another control a chip's cross on a picker's façade, an eye on a
28673
- * pressable row, a diskette inside a slide that travels. The press is this
28674
- * button's alone (no travel starts, no popup opens, nothing above answers) and
28675
- * its `onClick` waits for its own interaction gate instead of firing from the
28676
- * DOM. Where the zone around it is read-only, disabled or busy the button
28677
- * goes; `"refuse"` keeps it on screen refusing with a callout, for an
28678
- * affordance whose presence is information in itself.
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.
28679
29265
  */
28680
29266
  const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
28681
29267
 
@@ -30096,7 +30682,7 @@ installImportMetaCssBuild(import.meta);/**
30096
30682
  * reaches the real container.
30097
30683
  */
30098
30684
  let openLocalDialogCount = 0;
30099
- const css$_ = /* css */`
30685
+ const css$Y = /* css */`
30100
30686
  @layer navi {
30101
30687
  .navi_dialog {
30102
30688
  /* Min gap between the dialog and the edges of its container. Written
@@ -30684,7 +31270,7 @@ const css$_ = /* css */`
30684
31270
  * @param {import("ignore:preact").ComponentChildren} props.children
30685
31271
  */
30686
31272
  const Dialog = props => {
30687
- import.meta.css = [css$_, "@jsenv/navi/src/layout/dialog.jsx"];
31273
+ import.meta.css = [css$Y, "@jsenv/navi/src/layout/dialog.jsx"];
30688
31274
  if (props.openController) {
30689
31275
  return jsx(ControlledDialog, {
30690
31276
  ...props
@@ -31676,7 +32262,7 @@ installImportMetaCssBuild(import.meta);/**
31676
32262
  * and applied.
31677
32263
  */
31678
32264
  let openLocalPopoverCount = 0;
31679
- const css$Z = /* css */`
32265
+ const css$X = /* css */`
31680
32266
  @layer navi {
31681
32267
  .navi_popover {
31682
32268
  /* soft: user-configurable preferred max-height. Kept as a *default*
@@ -32136,7 +32722,7 @@ const css$Z = /* css */`
32136
32722
  * @param {import("ignore:preact").ComponentChildren} props.children
32137
32723
  */
32138
32724
  const Popover = props => {
32139
- import.meta.css = [css$Z, "@jsenv/navi/src/layout/popover.jsx"];
32725
+ import.meta.css = [css$X, "@jsenv/navi/src/layout/popover.jsx"];
32140
32726
  if (props.openController) {
32141
32727
  return jsx(ControlledPopover, {
32142
32728
  ...props
@@ -33164,7 +33750,7 @@ installImportMetaCssBuild(import.meta);/**
33164
33750
  * event, and a caller replacing the body entirely then has one protocol to
33165
33751
  * follow — `--navi-confirm` for yes, anything that closes for no.
33166
33752
  */
33167
- const css$Y = /* css */`
33753
+ const css$W = /* css */`
33168
33754
  /* The width lives on the body rather than on the popup, so that custom
33169
33755
  content (which replaces this body entirely) sizes itself instead of
33170
33756
  inheriting a ceiling meant for a sentence-long question. */
@@ -33301,7 +33887,7 @@ const ConfirmPopup = ({
33301
33887
  onAnswer,
33302
33888
  onClosed
33303
33889
  }) => {
33304
- 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"];
33305
33891
  const {
33306
33892
  mode,
33307
33893
  confirmLabel,
@@ -33385,7 +33971,7 @@ const defaultBody = (message, {
33385
33971
  });
33386
33972
  };
33387
33973
 
33388
- installImportMetaCssBuild(import.meta);const css$X = /* css */`
33974
+ installImportMetaCssBuild(import.meta);const css$V = /* css */`
33389
33975
  .action_error {
33390
33976
  margin-top: 0;
33391
33977
  margin-bottom: 20px;
@@ -33410,7 +33996,7 @@ const ActionRenderer = ({
33410
33996
  children,
33411
33997
  disabled
33412
33998
  }) => {
33413
- 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"];
33414
34000
  if (action === undefined) {
33415
34001
  throw new Error("ActionRenderer requires an action to render, but none was provided.");
33416
34002
  }
@@ -39035,7 +39621,7 @@ const ROUTE_TRAVEL_ATTRIBUTE = "data-navi-route-travel";
39035
39621
  // the root pictures must NOT move (they carry the whole viewport, blank bands
39036
39622
  // included).
39037
39623
 
39038
- const css$W = /* css */`
39624
+ const css$U = /* css */`
39039
39625
  /* The marked region is a picture of its own for the length of a transition of
39040
39626
  OURS, and only then — the name is what makes the pages a picture the
39041
39627
  movement below can carry.
@@ -39464,7 +40050,7 @@ const RouteTransitionArea = ({
39464
40050
  children,
39465
40051
  ...rest
39466
40052
  }) => {
39467
- 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"];
39468
40054
  const props = {
39469
40055
  ...rest,
39470
40056
  [TRANSITION_AREA_ATTRIBUTE]: ""
@@ -39521,7 +40107,7 @@ const RouteTransitionArea = ({
39521
40107
  * @returns {() => void} remove this relation.
39522
40108
  */
39523
40109
  const defineRouteTransition = (from, to, transition) => {
39524
- 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"];
39525
40111
  const {
39526
40112
  type,
39527
40113
  duration
@@ -39557,7 +40143,7 @@ const defineRouteTransition = (from, to, transition) => {
39557
40143
  * @returns {() => void} remove this default.
39558
40144
  */
39559
40145
  const defineRouteDefaultTransition = transition => {
39560
- 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"];
39561
40147
  const value = normalizeTransition(transition);
39562
40148
  defaultTransition = value;
39563
40149
  return () => {
@@ -40158,7 +40744,7 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
40158
40744
  const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
40159
40745
  // The name the box wears while it travels, and only then (see nameForTravel).
40160
40746
  const TRAVEL_NAME = "navi-route-travel";
40161
- const css$V = /* css */`
40747
+ const css$T = /* css */`
40162
40748
  /* The name that makes the page inside this box a picture of its own during a
40163
40749
  transition — rather than part of the one big picture the document takes, so
40164
40750
  the two pages can move past each other while everything else stays where it
@@ -40548,7 +41134,7 @@ const RouteTravel = ({
40548
41134
  children,
40549
41135
  ...rest
40550
41136
  }) => {
40551
- 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"];
40552
41138
  const elementRef = useRef();
40553
41139
  const gestureRef = useRef(null);
40554
41140
  // The travel in hand: the transition keeping the picture of the page being
@@ -43109,402 +43695,6 @@ const PhoneSvg = () => {
43109
43695
  });
43110
43696
  };
43111
43697
 
43112
- installImportMetaCssBuild(import.meta);// # TextAnchor — how it works
43113
- const css$U = /* css */`
43114
- .navi_text_anchor {
43115
- vertical-align: baseline;
43116
- user-select: none;
43117
- overflow: hidden;
43118
- }
43119
- `;
43120
-
43121
- /**
43122
- * Positions children vertically relative to the surrounding text, correcting for font-size differences.
43123
- *
43124
- * Place this component around any inline element whose font-size differs from the surrounding text.
43125
- * It renders an invisible anchor that inherits the surrounding text's font metrics, then shifts
43126
- * the child so that its visual position matches the requested `textAnchor` value — regardless of
43127
- * font-size, display type (inline, inline-block, inline-flex…), or the active `vertical-align`.
43128
- *
43129
- * @param {"line-top"|"char-top"|"center"|"char-bottom"|"line-bottom"} [textAnchor="char-bottom"]
43130
- * - `"line-top"` — child top aligns with the top of the surrounding line box
43131
- * - `"char-top"` — child top aligns with the top of visible characters (ink ascent)
43132
- * - `"center"` — child is vertically centered on the surrounding line box
43133
- * - `"char-bottom"` — child bottom aligns to the text baseline (no correction, browser default)
43134
- * - `"line-bottom"` — child bottom aligns with the bottom of the surrounding line box
43135
- * @param {{ size?: number, verticalAlign?: string }} [lineLayout]
43136
- * Describes the surrounding line context. Used as layout-effect dependencies so the correction
43137
- * reruns when the surrounding text's font-size or vertical-align changes.
43138
- * @param {import("ignore:preact").RefObject} childRef — ref on the child element to reposition
43139
- */
43140
- const TextAnchor = ({
43141
- childRef,
43142
- children,
43143
- textAnchor = "char-bottom",
43144
- textKey,
43145
- textSize,
43146
- lineLayout
43147
- }) => {
43148
- import.meta.css = [css$U, "@jsenv/navi/src/text/text_anchor.jsx"];
43149
- const anchorRef = useRef();
43150
-
43151
- // Plain useLayoutEffect would also fire while an ancestor dialog/popover
43152
- // (e.g. a closed SidePanel) is still display:none — every rect involved
43153
- // reads 0×0 at that point, so the math trivially (and wrongly) resolves
43154
- // to topOffset 0: not a real "no correction needed" result, just a
43155
- // zero-by-zero coincidence that happens to look fine only because it
43156
- // leaves the browser's own default alignment untouched. The real
43157
- // correction then only gets applied later, on whatever unrelated
43158
- // re-render next happens to change one of this effect's own deps —
43159
- // which reads as the child "jumping" even though nothing about its own
43160
- // geometry changed. useDisplayedLayoutEffect skips the initial run in
43161
- // that case and reruns once the ancestor actually opens instead.
43162
- useDisplayedLayoutEffect(anchorRef, anchorEl => {
43163
- const childEl = childRef.current;
43164
- if (!anchorEl || !childEl) {
43165
- return;
43166
- }
43167
- // Only correct when the anchor lives in an inline formatting context.
43168
- // If the parent is a flex/grid container, inline layout rules don't apply
43169
- // and our font-metrics model is invalid.
43170
- const parentDisplay = getComputedStyle(anchorEl.parentElement).display;
43171
- if (parentDisplay !== "inline" && parentDisplay !== "inline-block" && parentDisplay !== "block") {
43172
- // we must hide the anchor otherwise it would affect layout without providing any benefit (would trigger flex gap for instance)
43173
- anchorEl.setAttribute("hidden", "");
43174
- setTopOffset(childEl, 0);
43175
- return;
43176
- }
43177
- anchorEl.removeAttribute("hidden");
43178
- const topOffset = computeTopOffset({
43179
- anchorEl,
43180
- childEl,
43181
- textAnchor
43182
- });
43183
- setTopOffset(childEl, topOffset);
43184
- }, [textAnchor, textKey, textSize, lineLayout?.size, lineLayout?.verticalAlign]);
43185
- return jsxs(Fragment$1, {
43186
- children: [children, jsx("span", {
43187
- ref: anchorRef,
43188
- className: "navi_text_anchor",
43189
- "aria-hidden": "true",
43190
- children: "\u200B"
43191
- })]
43192
- });
43193
- };
43194
- const setTopOffset = (childEl, topOffset) => {
43195
- // position:relative + top shifts the element visually.
43196
- // marginTop: -topOffset makes the layout box follow the visual position, so any container
43197
- // (button, link, box…) computes its own padding/border/height based on the real final position
43198
- // rather than the original unshifted one. This means a badge inside a button will symmetrically
43199
- // expand the button height instead of overflowing or being clipped.
43200
- // marginBottom: topOffset compensates the marginTop so the line height stays unchanged —
43201
- // the shift is purely a repositioning, not an inflation of the line.
43202
- if (!topOffset) {
43203
- childEl.style.position = "";
43204
- childEl.style.top = "";
43205
- childEl.style.marginTop = "";
43206
- childEl.style.marginBottom = "";
43207
- return;
43208
- }
43209
- childEl.style.position = "relative";
43210
- childEl.style.top = `${topOffset}px`;
43211
- childEl.style.marginTop = `${-topOffset}px`;
43212
- childEl.style.marginBottom = `${topOffset}px`;
43213
- };
43214
- const computeTopOffset = ({
43215
- anchorEl,
43216
- childEl,
43217
- textAnchor
43218
- }) => {
43219
- if (textAnchor === "char-bottom") {
43220
- // Align child's bottom with the char's bottom = the baseline.
43221
- // The CSS spec says an inline-block with no text content has its baseline at its bottom margin edge.
43222
- // So the browser's default placement already puts the child's bottom at the line's baseline.
43223
- // No correction needed.
43224
- return 0;
43225
- }
43226
- // The anchor's rendered rect corresponds to the surrounding text's line box:
43227
- // top and bottom are the visual bounds of the line (including line-height).
43228
- const anchorRect = anchorEl.getBoundingClientRect();
43229
-
43230
- // Measure the child's current rect, then subtract any previously applied top correction
43231
- // to recover its natural position — avoiding a style reset + reflow.
43232
- const childRect = childEl.getBoundingClientRect();
43233
- const childH = childRect.height;
43234
- const previousTop = parseFloat(childEl.style.top) || 0;
43235
- const childNaturalTop = childRect.top - previousTop;
43236
-
43237
- // Compute desired child top Y based on textAnchor intention.
43238
- let desiredChildTopY = 0;
43239
- if (textAnchor === "line-top") {
43240
- desiredChildTopY = anchorRect.top;
43241
- } else if (textAnchor === "char-top") {
43242
- const anchorStyle = getComputedStyle(anchorEl);
43243
- const ctx = charTopCanvas.getContext("2d");
43244
- ctx.font = `${anchorStyle.fontWeight} ${anchorStyle.fontSize} ${anchorStyle.fontFamily}`;
43245
- const m = ctx.measureText("M");
43246
- const baselineY = anchorRect.bottom - m.fontBoundingBoxDescent;
43247
- desiredChildTopY = baselineY - m.actualBoundingBoxAscent;
43248
- } else if (textAnchor === "center") {
43249
- const anchorCenterY = (anchorRect.top + anchorRect.bottom) / 2;
43250
- desiredChildTopY = anchorCenterY - childH / 2;
43251
- } else if (textAnchor === "char-bottom") {
43252
- // Already handled above (early return 0), but guard here for completeness.
43253
- return 0;
43254
- } else if (textAnchor === "line-bottom") {
43255
- desiredChildTopY = anchorRect.bottom - childH;
43256
- } else {
43257
- return 0; // unknown textAnchor, no correction
43258
- }
43259
- return desiredChildTopY - childNaturalTop;
43260
- };
43261
- const charTopCanvas = document.createElement("canvas");
43262
-
43263
- installImportMetaCssBuild(import.meta);const css$T = /* css */`
43264
- @layer navi {
43265
- /* Ensure data attributes from box.jsx can win to update display */
43266
- .navi_icon {
43267
- display: inline-flex;
43268
- box-sizing: border-box;
43269
- max-width: 100%;
43270
- /* An icon never grows past the box it sits in, so a glyph can never make
43271
- a line of text taller than the text itself. lineOverflow="allow" opts
43272
- out, for an icon that is an affordance rather than a character — a
43273
- control's chevron or clear button, sized to be touched, not read. */
43274
- max-height: 100%;
43275
-
43276
- &[data-line-overflow="allow"] {
43277
- max-height: none;
43278
- }
43279
- }
43280
- }
43281
-
43282
- .navi_icon {
43283
- white-space: nowrap;
43284
- vertical-align: inherit;
43285
-
43286
- &[data-icon-char] {
43287
- aspect-ratio: 1/1;
43288
- min-width: 0;
43289
- height: round(1em, 1px);
43290
- max-height: round(1em, 1px);
43291
- flex-grow: 0 !important;
43292
- align-items: center;
43293
- justify-content: center;
43294
-
43295
- /* fillLine: measured on the line box (1lh) instead of the character box
43296
- (1em). The icon still stays inside the line — it just uses all of it,
43297
- which is what an icon standing on its own in a control's slot wants,
43298
- where a glyph sitting among letters wants to match their size. */
43299
- &[data-fill-line] {
43300
- height: round(1lh, 1px);
43301
- max-height: round(1lh, 1px);
43302
- }
43303
-
43304
- svg,
43305
- img {
43306
- width: 100%;
43307
- height: 100%;
43308
- }
43309
- svg {
43310
- overflow: visible;
43311
- }
43312
- }
43313
- &[data-flow-inline] {
43314
- width: 1em;
43315
- height: 1em;
43316
- }
43317
- &[data-interactive] {
43318
- cursor: pointer;
43319
- }
43320
- &[data-icon-text] {
43321
- -webkit-font-smoothing: antialiased;
43322
- text-rendering: optimizeLegibility;
43323
- }
43324
- }
43325
-
43326
- .navi_icon > svg,
43327
- .navi_icon > img {
43328
- width: 100%;
43329
- height: 100%;
43330
- }
43331
- .navi_icon[data-width-fixed] > svg,
43332
- .navi_icon[data-width-fixed] > img {
43333
- width: 100%;
43334
- height: auto;
43335
- }
43336
- .navi_icon[data-height-fixed] > svg,
43337
- .navi_icon[data-height-fixed] > img {
43338
- width: auto;
43339
- height: 100%;
43340
- }
43341
- .navi_icon[data-width-fixed][data-height-fixed] > svg,
43342
- .navi_icon[data-width-fixed][data-height-fixed] > img {
43343
- width: 100%;
43344
- height: 100%;
43345
- }
43346
- `;
43347
-
43348
- /**
43349
- * Renders an icon — an inline SVG/emoji/text glyph that inherits the
43350
- * surrounding text's `currentColor` and (by default) its font size, so it sits
43351
- * on the text baseline like a character.
43352
- *
43353
- * Content comes from either `href` (references an external/sprite symbol via
43354
- * `<use>`) or `children` (an inline `<svg>` element, or a string for a
43355
- * text/emoji icon). All extra props are spread onto the underlying `Box`/`Text`
43356
- * (sizing, spacing, color, className, data-attributes, …).
43357
- *
43358
- * Render mode is chosen automatically:
43359
- * - `children` is a **string** → a text icon (`<Text data-icon-text>`).
43360
- * - **sized** (an explicit `width`/`height`, or `flex`/`grid`) → a block icon
43361
- * (`<Box square>`), laid out as its own box rather than inline.
43362
- * - otherwise → an **inline char-like** icon that flows on the text baseline
43363
- * (`data-icon-char`), aligned via `textAnchor`.
43364
- *
43365
- * Accessibility: an icon is treated as decorative (`aria-hidden`) by default
43366
- * whenever it has no explicit size and no `onClick`; give it an explicit
43367
- * `decorative={false}` (or make it interactive) when it conveys meaning.
43368
- *
43369
- * @param {object} props
43370
- * @param {string} [props.href] - URL/id of an external SVG symbol, rendered via
43371
- * `<svg><use href></svg>`. Mutually exclusive with meaningful `children`.
43372
- * @param {import("ignore:preact").ComponentChildren} [props.children] - Inline icon
43373
- * content: an `<svg>` element, or a string (renders as a text/emoji icon).
43374
- * @param {boolean} [props.decorative] - Marks the icon `aria-hidden`. Defaults
43375
- * to `true` for an unsized, non-interactive icon; pass `false` for a
43376
- * meaning-bearing icon that needs to be exposed to assistive tech.
43377
- * @param {(event: MouseEvent) => void} [props.onClick] - Makes the icon
43378
- * interactive (`data-interactive`, pointer cursor) and non-decorative.
43379
- * @param {"line-top"|"char-top"|"center"|"char-bottom"|"line-bottom"} [props.textAnchor="center"]
43380
- * - Vertical alignment within the surrounding text line for the inline
43381
- * char-like mode, forwarded to `TextAnchor`: `"line-top"`/`"line-bottom"`
43382
- * align to the line box edges, `"char-top"` to the ink ascent, `"center"`
43383
- * centers on the line box, `"char-bottom"` sits on the baseline. See
43384
- * `text_anchor.jsx`.
43385
- * @param {{ size?: number, verticalAlign?: string }} [props.lineLayout] -
43386
- * Describes the surrounding line context (font size / vertical-align),
43387
- * forwarded to `TextAnchor` so it recomputes the vertical correction when
43388
- * that context changes.
43389
- * @param {string|number} [props.width] - Explicit width; `"auto"` clears it.
43390
- * Any explicit size switches the icon to block (sized) mode.
43391
- * @param {string|number} [props.height] - Explicit height; `"auto"` clears it.
43392
- * @param {"allow"} [props.lineOverflow] - `"allow"` lets the icon be taller
43393
- * than the box it sits in (a line of text, a control's slot) instead of being
43394
- * capped by it. For an icon that is an affordance sized for the finger rather
43395
- * than a character sized for reading.
43396
- * @param {boolean} [props.fillLine] - Sizes the icon on the line box (1lh)
43397
- * rather than on the character box (1em), so it uses the full height of the
43398
- * line without leaving it. Unlike `lineOverflow`, the icon still never
43399
- * exceeds the line.
43400
- * @param {boolean} [props.square] - Keep a 1:1 box; combined with one explicit
43401
- * dimension it fixes the other too.
43402
- * @param {boolean} [props.circle] - Like `square`, plus a circular shape.
43403
- * @param {string|number} [props.aspectRatio] - Fixes the second dimension from
43404
- * the one explicit dimension.
43405
- * @param {"x"|"y"|boolean} [props.flex] - Forces block/flex layout; auto-set to
43406
- * `"x"` when the icon is sized.
43407
- * @param {boolean} [props.grid] - Forces block/grid layout.
43408
- * @param {string} [props.className] - Merged with the base `"navi_icon"` class.
43409
- */
43410
- const Icon = ({
43411
- href,
43412
- children,
43413
- decorative,
43414
- onClick,
43415
- textAnchor = "center",
43416
- lineLayout,
43417
- lineOverflow,
43418
- fillLine,
43419
- ...props
43420
- }) => {
43421
- import.meta.css = [css$T, "@jsenv/navi/src/text/icon.jsx"];
43422
- const innerChildren = href ? jsx("svg", {
43423
- width: "100%",
43424
- height: "100%",
43425
- children: jsx("use", {
43426
- href: href
43427
- })
43428
- }) : children;
43429
- let {
43430
- flex,
43431
- grid,
43432
- width,
43433
- height
43434
- } = props;
43435
- if (width === "auto") {
43436
- width = undefined;
43437
- }
43438
- if (height === "auto") {
43439
- height = undefined;
43440
- }
43441
- const hasExplicitWidth = width !== undefined;
43442
- const hasExplicitHeight = height !== undefined;
43443
- const widthFixed = hasExplicitWidth || hasExplicitHeight && (props.square || props.circle || props.aspectRatio);
43444
- const heightFixed = hasExplicitHeight || hasExplicitWidth && (props.square || props.circle || props.aspectRatio);
43445
- if (widthFixed || heightFixed) {
43446
- if (flex === undefined) {
43447
- flex = "x";
43448
- }
43449
- } else if (decorative === undefined && !onClick) {
43450
- decorative = true;
43451
- }
43452
- const ariaProps = decorative ? {
43453
- "aria-hidden": "true"
43454
- } : {};
43455
- const textRef = useRef();
43456
- if (typeof children === "string") {
43457
- return jsx(Text, {
43458
- ...props,
43459
- ...ariaProps,
43460
- "data-icon-text": "",
43461
- "data-line-overflow": lineOverflow,
43462
- "data-fill-line": fillLine ? "" : undefined,
43463
- children: children
43464
- });
43465
- }
43466
- if (flex || grid) {
43467
- return jsx(Box, {
43468
- square: true,
43469
- ...props,
43470
- ...ariaProps,
43471
- flex: flex,
43472
- baseClassName: "navi_icon",
43473
- "data-width-fixed": widthFixed ? "" : undefined,
43474
- "data-height-fixed": heightFixed ? "" : undefined,
43475
- "data-interactive": onClick ? "" : undefined,
43476
- "data-line-overflow": lineOverflow,
43477
- "data-fill-line": fillLine ? "" : undefined,
43478
- onClick: onClick,
43479
- children: innerChildren
43480
- });
43481
- }
43482
- return jsx(TextAnchor, {
43483
- childRef: textRef,
43484
- textAnchor: textAnchor,
43485
- textSize: props.size,
43486
- lineLayout: lineLayout,
43487
- children: jsxs(Text, {
43488
- ...props,
43489
- ...ariaProps,
43490
- className: withPropsClassName("navi_icon", props.className),
43491
- spacing: "pre",
43492
- "data-icon-char": "",
43493
- "data-line-overflow": lineOverflow,
43494
- "data-fill-line": fillLine ? "" : undefined,
43495
- "data-width-fixed": widthFixed ? "" : undefined,
43496
- "data-height-fixed": heightFixed ? "" : undefined,
43497
- "data-interactive": onClick ? "" : undefined,
43498
- onClick: onClick,
43499
- ref: textRef,
43500
- children: [jsx("span", {
43501
- style: "user-select:none",
43502
- children: "\u200B"
43503
- }), innerChildren]
43504
- })
43505
- });
43506
- };
43507
-
43508
43698
  /**
43509
43699
  * Hook that reactively checks if a URL is visited.
43510
43700
  * Re-renders when the visited URL set changes.
@@ -64177,7 +64367,7 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
64177
64367
  means "open the picker". An own target is the exception, the same way
64178
64368
  the clear cross is one in the slot below: it says the press is aimed at
64179
64369
  IT, so it has to be reachable at all. */
64180
- [data-navi-own-target] {
64370
+ [data-own-target] {
64181
64371
  pointer-events: auto;
64182
64372
  }
64183
64373
  }
@@ -75686,6 +75876,9 @@ const MessageBox = ({
75686
75876
  padding = "sm",
75687
75877
  icon,
75688
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,
75689
75882
  children,
75690
75883
  onClose,
75691
75884
  ...rest
@@ -75723,26 +75916,39 @@ const MessageBox = ({
75723
75916
  value: setHasTitleChild,
75724
75917
  children: [icon && jsx(Icon, {
75725
75918
  color: "var(--x-message-color)",
75726
- height: "1.5em",
75919
+ height: "1lh",
75727
75920
  maxHeight: "auto",
75728
75921
  selfAlignY: "start",
75729
75922
  aspectRatio: "auto",
75730
75923
  children: icon
75731
75924
  }), jsx(Text, {
75925
+ emojiAsIcon: emojiAsIcon,
75732
75926
  children: children
75733
- }), onClose && jsx(Button, {
75734
- action: onClose,
75735
- icon: true,
75736
- border: "none",
75737
- 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,
75738
75934
  alignY: "center",
75739
- style: {
75740
- ":hover": {
75741
- backgroundColor: "rgba(0, 0, 0, 0.1)"
75742
- }
75743
- },
75744
- children: jsx(Icon, {
75745
- 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
+ })
75746
75952
  })
75747
75953
  })]
75748
75954
  })
@@ -77830,5 +78036,5 @@ const UserSvg = () => jsx("svg", {
77830
78036
  })
77831
78037
  });
77832
78038
 
77833
- 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 };
77834
78040
  //# sourceMappingURL=jsenv_navi.js.map