@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.
- package/dist/jsenv_navi.js +791 -585
- package/dist/jsenv_navi.js.map +124 -102
- package/docs/AI_INSTRUCTIONS.md +7 -2
- package/docs/interactions.md +48 -11
- package/docs/typography.md +43 -2
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -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,
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
2339
|
-
*
|
|
2340
|
-
*
|
|
2341
|
-
*
|
|
2342
|
-
*
|
|
2343
|
-
*
|
|
2344
|
-
*
|
|
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
|
-
|
|
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
|
|
2382
|
-
*
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
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 (
|
|
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$
|
|
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
|
-
|
|
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
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
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$
|
|
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
|
|
7392
|
+
clearCalloutMessage(calloutMessageElement);
|
|
7313
7393
|
calloutMessageElement.appendChild(newMessage);
|
|
7314
7394
|
} else if (typeof newMessage === "function") {
|
|
7315
7395
|
debug(`callout update message (function)`);
|
|
7316
|
-
calloutMessageElement
|
|
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
|
-
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
//
|
|
27439
|
-
//
|
|
27440
|
-
//
|
|
27441
|
-
//
|
|
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
|
|
27481
|
-
const
|
|
28046
|
+
const controlDisabledFromAbove = useContext(DisabledContext);
|
|
28047
|
+
const controlReadOnlyFromAbove = useContext(ReadOnlyContext);
|
|
27482
28048
|
const controlRequired = useContext(RequiredContext);
|
|
27483
|
-
const
|
|
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
|
|
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
|
|
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
|
|
28672
|
-
*
|
|
28673
|
-
*
|
|
28674
|
-
*
|
|
28675
|
-
*
|
|
28676
|
-
*
|
|
28677
|
-
*
|
|
28678
|
-
*
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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-
|
|
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: "
|
|
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 &&
|
|
75734
|
-
|
|
75735
|
-
|
|
75736
|
-
|
|
75737
|
-
|
|
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
|
-
|
|
75740
|
-
|
|
75741
|
-
|
|
75742
|
-
|
|
75743
|
-
|
|
75744
|
-
|
|
75745
|
-
|
|
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
|