@jsenv/navi 0.29.103 → 0.29.105
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 +1268 -863
- package/dist/jsenv_navi.js.map +184 -155
- package/docs/AI_INSTRUCTIONS.md +7 -2
- package/docs/actions.md +33 -0
- package/docs/badge_list.md +3 -2
- package/docs/interactions.md +57 -11
- package/docs/popup_open.md +74 -0
- 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,
|
|
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, 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);
|
|
@@ -2404,9 +2430,13 @@ const useOwnTargetHidden = (props) => {
|
|
|
2404
2430
|
*
|
|
2405
2431
|
* Beyond recursive object/array comparison it covers the edge cases `===` gets
|
|
2406
2432
|
* "wrong" for equality purposes: NaN equals NaN, Date compared by time value,
|
|
2407
|
-
* cycles don't loop (a
|
|
2408
|
-
*
|
|
2409
|
-
*
|
|
2433
|
+
* cycles don't loop (a set of the pairs being compared guards circular refs),
|
|
2434
|
+
* and same-type is required
|
|
2435
|
+
* before descending. Functions, and objects with nothing enumerable to compare
|
|
2436
|
+
* (a Set, a Map, an element, a URL), are equal by reference only — nothing in
|
|
2437
|
+
* them says whether two are "the same". Cheap paths run first: reference
|
|
2438
|
+
* equality, then the identity short-circuit below, then array length before
|
|
2439
|
+
* element-by-element.
|
|
2410
2440
|
*
|
|
2411
2441
|
* SYMBOL_IDENTITY. Two *different* object instances can be declared "conceptually
|
|
2412
2442
|
* the same" by sharing a SYMBOL_IDENTITY value; the comparison then treats them as
|
|
@@ -2468,6 +2498,11 @@ const compareTwoJsValues = (
|
|
|
2468
2498
|
if (aType !== bType) {
|
|
2469
2499
|
return false;
|
|
2470
2500
|
}
|
|
2501
|
+
if (aType === "function") {
|
|
2502
|
+
// Not the same function (reference equality came first), and nothing in
|
|
2503
|
+
// a function says whether two of them do the same thing.
|
|
2504
|
+
return false;
|
|
2505
|
+
}
|
|
2471
2506
|
const aIsPrimitive =
|
|
2472
2507
|
a === null || (aType !== "object" && aType !== "function");
|
|
2473
2508
|
const bIsPrimitive =
|
|
@@ -2478,14 +2513,22 @@ const compareTwoJsValues = (
|
|
|
2478
2513
|
if (aIsPrimitive && bIsPrimitive) {
|
|
2479
2514
|
return a === b;
|
|
2480
2515
|
}
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
if (seenSet.has(b)) {
|
|
2516
|
+
// Back on something still being compared: a cycle. No loop, and no answer
|
|
2517
|
+
// either — equal by a route that never ends is not equal.
|
|
2518
|
+
if (seenSet.has(a) || seenSet.has(b)) {
|
|
2485
2519
|
return false;
|
|
2486
2520
|
}
|
|
2521
|
+
// Held only while a and b are being compared, not for the rest of the
|
|
2522
|
+
// walk: the same object is rightly met again elsewhere — in an unordered
|
|
2523
|
+
// array every element of a is tried against every element of b.
|
|
2487
2524
|
seenSet.add(a);
|
|
2488
2525
|
seenSet.add(b);
|
|
2526
|
+
const result = compareComposite(a, b);
|
|
2527
|
+
seenSet.delete(a);
|
|
2528
|
+
seenSet.delete(b);
|
|
2529
|
+
return result;
|
|
2530
|
+
};
|
|
2531
|
+
const compareComposite = (a, b) => {
|
|
2489
2532
|
const aIsArray = Array.isArray(a);
|
|
2490
2533
|
const bIsArray = Array.isArray(b);
|
|
2491
2534
|
if (aIsArray !== bIsArray) {
|
|
@@ -2543,25 +2586,25 @@ const compareTwoJsValues = (
|
|
|
2543
2586
|
return true;
|
|
2544
2587
|
}
|
|
2545
2588
|
// Date objects must be compared by time value, not by enumerable keys (which are empty)
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
const aTime = a.getTime();
|
|
2554
|
-
const bTime = b.getTime();
|
|
2555
|
-
if (aTime !== bTime) {
|
|
2556
|
-
return false;
|
|
2557
|
-
}
|
|
2558
|
-
}
|
|
2589
|
+
const aIsDate = a instanceof Date;
|
|
2590
|
+
const bIsDate = b instanceof Date;
|
|
2591
|
+
if (aIsDate !== bIsDate) {
|
|
2592
|
+
return false;
|
|
2593
|
+
}
|
|
2594
|
+
if (aIsDate) {
|
|
2595
|
+
return a.getTime() === b.getTime();
|
|
2559
2596
|
}
|
|
2560
2597
|
const aKeys = Object.keys(a);
|
|
2561
2598
|
const bKeys = Object.keys(b);
|
|
2562
2599
|
if (aKeys.length !== bKeys.length) {
|
|
2563
2600
|
return false;
|
|
2564
2601
|
}
|
|
2602
|
+
if (aKeys.length === 0 && (!isPlainObject$2(a) || !isPlainObject$2(b))) {
|
|
2603
|
+
// A Set, a Map, an element, a URL: nothing enumerable to tell two of them
|
|
2604
|
+
// apart, so they are the same one or they are not — and they are not,
|
|
2605
|
+
// reference equality came first.
|
|
2606
|
+
return false;
|
|
2607
|
+
}
|
|
2565
2608
|
if (lightKeySet) {
|
|
2566
2609
|
// compare light keys first, then remaining keys
|
|
2567
2610
|
// (optimization for cases where some keys are more likely to differ and/or faster to compare)
|
|
@@ -2599,6 +2642,11 @@ const compareTwoJsValues = (
|
|
|
2599
2642
|
return compare(rootA, rootB);
|
|
2600
2643
|
};
|
|
2601
2644
|
|
|
2645
|
+
const isPlainObject$2 = (value) => {
|
|
2646
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2647
|
+
return prototype === Object.prototype || prototype === null;
|
|
2648
|
+
};
|
|
2649
|
+
|
|
2602
2650
|
// Global signal registry for route template detection
|
|
2603
2651
|
const globalSignalRegistry = new Map();
|
|
2604
2652
|
let signalIdCounter = 0;
|
|
@@ -6861,6 +6909,10 @@ const findControlProxy = (el) => {
|
|
|
6861
6909
|
return firstProxy;
|
|
6862
6910
|
};
|
|
6863
6911
|
|
|
6912
|
+
let renderMessageText = text => text;
|
|
6913
|
+
const setCalloutMessageTextRenderer = renderer => {
|
|
6914
|
+
renderMessageText = renderer;
|
|
6915
|
+
};
|
|
6864
6916
|
const CalloutRequestCloseContext = createContext();
|
|
6865
6917
|
const useCalloutRequestClose = () => {
|
|
6866
6918
|
return useContext(CalloutRequestCloseContext);
|
|
@@ -6875,6 +6927,48 @@ const renderIntoCallout = (jsx$1, calloutMessageElement, {
|
|
|
6875
6927
|
render(calloutJsx, calloutMessageElement);
|
|
6876
6928
|
};
|
|
6877
6929
|
|
|
6930
|
+
// An HTML message is rendered through preact rather than innerHTML so that its
|
|
6931
|
+
// text can go through renderMessageText: an emoji in a validation message must
|
|
6932
|
+
// not make the first line taller than the icon and close button beside it.
|
|
6933
|
+
const renderHtmlIntoCallout = (html, calloutMessageElement, {
|
|
6934
|
+
requestClose
|
|
6935
|
+
}) => {
|
|
6936
|
+
const template = document.createElement("template");
|
|
6937
|
+
template.innerHTML = html;
|
|
6938
|
+
renderIntoCallout(domToVNodes(template.content), calloutMessageElement, {
|
|
6939
|
+
requestClose
|
|
6940
|
+
});
|
|
6941
|
+
};
|
|
6942
|
+
|
|
6943
|
+
// Unmounts whatever preact rendered before the element is filled by hand
|
|
6944
|
+
// (a DOM node, an iframe); innerHTML alone would leave preact believing its
|
|
6945
|
+
// tree is still there.
|
|
6946
|
+
const clearCalloutMessage = calloutMessageElement => {
|
|
6947
|
+
render(null, calloutMessageElement);
|
|
6948
|
+
calloutMessageElement.innerHTML = "";
|
|
6949
|
+
};
|
|
6950
|
+
const domToVNodes = node => {
|
|
6951
|
+
const vnodes = [];
|
|
6952
|
+
for (const child of node.childNodes) {
|
|
6953
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
6954
|
+
vnodes.push(renderMessageText(child.data));
|
|
6955
|
+
continue;
|
|
6956
|
+
}
|
|
6957
|
+
if (child.nodeType !== Node.ELEMENT_NODE) {
|
|
6958
|
+
continue;
|
|
6959
|
+
}
|
|
6960
|
+
const props = {};
|
|
6961
|
+
for (const {
|
|
6962
|
+
name,
|
|
6963
|
+
value
|
|
6964
|
+
} of child.attributes) {
|
|
6965
|
+
props[name] = value;
|
|
6966
|
+
}
|
|
6967
|
+
vnodes.push(h(child.localName, props, ...domToVNodes(child)));
|
|
6968
|
+
}
|
|
6969
|
+
return vnodes;
|
|
6970
|
+
};
|
|
6971
|
+
|
|
6878
6972
|
installImportMetaCssBuild(import.meta);/**
|
|
6879
6973
|
* A callout component that mimics native browser validation messages.
|
|
6880
6974
|
* Features:
|
|
@@ -6884,7 +6978,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
6884
6978
|
* - Arrow automatically shows when pointing at a valid anchor element
|
|
6885
6979
|
* - Centers in viewport when no anchor element provided or anchor is too big
|
|
6886
6980
|
*/
|
|
6887
|
-
const css$
|
|
6981
|
+
const css$12 = /* css */`
|
|
6888
6982
|
@layer navi {
|
|
6889
6983
|
.navi_callout {
|
|
6890
6984
|
/* A callout is parented to what it explains, so it inherits from it — and
|
|
@@ -6986,7 +7080,10 @@ const css$13 = /* css */`
|
|
|
6986
7080
|
}
|
|
6987
7081
|
|
|
6988
7082
|
.navi_callout_body {
|
|
6989
|
-
|
|
7083
|
+
/* The first line of the message: the icon and the close button sit
|
|
7084
|
+
in columns of that height, pinned to the top, so both stay level
|
|
7085
|
+
with it however many lines the message takes. */
|
|
7086
|
+
--callout-icon-height: round(1lh, 1px);
|
|
6990
7087
|
|
|
6991
7088
|
position: relative;
|
|
6992
7089
|
display: flex;
|
|
@@ -7042,10 +7139,15 @@ const css$13 = /* css */`
|
|
|
7042
7139
|
align-self: flex-start;
|
|
7043
7140
|
|
|
7044
7141
|
.navi_callout_close_button {
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7142
|
+
/* A square filling the column, so the whole first line is the target;
|
|
7143
|
+
the padding keeps the cross itself at glyph size. */
|
|
7144
|
+
display: inline-flex;
|
|
7145
|
+
box-sizing: border-box;
|
|
7146
|
+
aspect-ratio: 1 / 1;
|
|
7147
|
+
height: 100%;
|
|
7148
|
+
padding: 0.2em;
|
|
7149
|
+
align-items: center;
|
|
7150
|
+
justify-content: center;
|
|
7049
7151
|
color: currentColor;
|
|
7050
7152
|
font-size: inherit;
|
|
7051
7153
|
background: none;
|
|
@@ -7127,7 +7229,7 @@ const openCallout = (message, {
|
|
|
7127
7229
|
skipFocus = false,
|
|
7128
7230
|
debug = () => {}
|
|
7129
7231
|
} = {}) => {
|
|
7130
|
-
import.meta.css = [css$
|
|
7232
|
+
import.meta.css = [css$12, "@jsenv/navi/src/control/rules/callout/callout.js"];
|
|
7131
7233
|
if (debug === true) {
|
|
7132
7234
|
debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
|
|
7133
7235
|
}
|
|
@@ -7309,11 +7411,11 @@ const openCallout = (message, {
|
|
|
7309
7411
|
} else if (newMessage instanceof Node) {
|
|
7310
7412
|
// Handle DOM node (cloned from CSS selector)
|
|
7311
7413
|
debug(`callout update message (node)`);
|
|
7312
|
-
calloutMessageElement
|
|
7414
|
+
clearCalloutMessage(calloutMessageElement);
|
|
7313
7415
|
calloutMessageElement.appendChild(newMessage);
|
|
7314
7416
|
} else if (typeof newMessage === "function") {
|
|
7315
7417
|
debug(`callout update message (function)`);
|
|
7316
|
-
calloutMessageElement
|
|
7418
|
+
clearCalloutMessage(calloutMessageElement);
|
|
7317
7419
|
newMessage({
|
|
7318
7420
|
renderIntoCallout: jsx => renderIntoCallout(jsx, calloutMessageElement, {
|
|
7319
7421
|
requestClose
|
|
@@ -7338,12 +7440,13 @@ const openCallout = (message, {
|
|
|
7338
7440
|
iframe.style.backgroundColor = "white";
|
|
7339
7441
|
iframe.srcdoc = newMessage;
|
|
7340
7442
|
debug(`callout update message (html document iframe)`);
|
|
7341
|
-
|
|
7342
|
-
calloutMessageElement.innerHTML = "";
|
|
7443
|
+
clearCalloutMessage(calloutMessageElement);
|
|
7343
7444
|
calloutMessageElement.appendChild(iframe);
|
|
7344
7445
|
} else {
|
|
7345
7446
|
debug(`callout update message: ${typeof newMessage === "string" ? newMessage.slice(0, 80) : String(newMessage)}`);
|
|
7346
|
-
calloutMessageElement
|
|
7447
|
+
renderHtmlIntoCallout(String(newMessage), calloutMessageElement, {
|
|
7448
|
+
requestClose
|
|
7449
|
+
});
|
|
7347
7450
|
}
|
|
7348
7451
|
}
|
|
7349
7452
|
// After updating content the callout size likely changed — re-position immediately
|
|
@@ -16627,16 +16730,10 @@ const getHowToHandleStyleProp = (name) => {
|
|
|
16627
16730
|
}
|
|
16628
16731
|
return getStyle;
|
|
16629
16732
|
};
|
|
16630
|
-
const prepareStyleValue = (
|
|
16631
|
-
existingValue,
|
|
16632
|
-
value,
|
|
16633
|
-
name,
|
|
16634
|
-
styleContext,
|
|
16635
|
-
context,
|
|
16636
|
-
) => {
|
|
16733
|
+
const prepareStyleValue = (existingValue, value, name, styleContext) => {
|
|
16637
16734
|
const stringifier = getStringifier(name);
|
|
16638
|
-
const cssValue = stringifier(value, name, styleContext
|
|
16639
|
-
const mergedValue = mergeOneStyle(existingValue, cssValue, name,
|
|
16735
|
+
const cssValue = stringifier(value, name, styleContext);
|
|
16736
|
+
const mergedValue = mergeOneStyle(existingValue, cssValue, name, "css");
|
|
16640
16737
|
return mergedValue;
|
|
16641
16738
|
};
|
|
16642
16739
|
|
|
@@ -17725,30 +17822,61 @@ const isKeyboardModality = () => keyboardNavigationUsed;
|
|
|
17725
17822
|
return false;
|
|
17726
17823
|
};
|
|
17727
17824
|
|
|
17728
|
-
//
|
|
17729
|
-
//
|
|
17730
|
-
//
|
|
17731
|
-
//
|
|
17732
|
-
|
|
17733
|
-
|
|
17734
|
-
|
|
17735
|
-
|
|
17736
|
-
|
|
17737
|
-
|
|
17738
|
-
|
|
17739
|
-
|
|
17740
|
-
|
|
17741
|
-
|
|
17742
|
-
|
|
17743
|
-
|
|
17744
|
-
|
|
17745
|
-
|
|
17746
|
-
|
|
17747
|
-
|
|
17748
|
-
|
|
17749
|
-
|
|
17825
|
+
// One registration per element for everything focus-related, shared by
|
|
17826
|
+
// :focus, :focus-visible and :focus-within: they all react to the same
|
|
17827
|
+
// focusin/focusout, and they all re-check through the same callback (see
|
|
17828
|
+
// initPseudoStyles), which the Set turns into one call.
|
|
17829
|
+
const focusTrackingWeakMap = new WeakMap();
|
|
17830
|
+
const trackFocus = (el, callback) => {
|
|
17831
|
+
let tracking = focusTrackingWeakMap.get(el);
|
|
17832
|
+
if (!tracking) {
|
|
17833
|
+
const callbackSet = new Set();
|
|
17834
|
+
const onFocusChange = (e) => {
|
|
17835
|
+
for (const trackedCallback of callbackSet) {
|
|
17836
|
+
trackedCallback();
|
|
17837
|
+
}
|
|
17838
|
+
notifyAriaControlled(el, e);
|
|
17839
|
+
};
|
|
17840
|
+
el.addEventListener("focusin", onFocusChange);
|
|
17841
|
+
el.addEventListener("focusout", onFocusChange);
|
|
17842
|
+
tracking = {
|
|
17843
|
+
callbackSet,
|
|
17844
|
+
teardown: () => {
|
|
17845
|
+
el.removeEventListener("focusin", onFocusChange);
|
|
17846
|
+
el.removeEventListener("focusout", onFocusChange);
|
|
17847
|
+
focusTrackingWeakMap.delete(el);
|
|
17848
|
+
},
|
|
17849
|
+
};
|
|
17850
|
+
focusTrackingWeakMap.set(el, tracking);
|
|
17851
|
+
observeAriaControls();
|
|
17852
|
+
}
|
|
17853
|
+
tracking.callbackSet.add(callback);
|
|
17854
|
+
return () => {
|
|
17855
|
+
tracking.callbackSet.delete(callback);
|
|
17856
|
+
if (tracking.callbackSet.size === 0) {
|
|
17857
|
+
tracking.teardown();
|
|
17750
17858
|
}
|
|
17859
|
+
};
|
|
17860
|
+
};
|
|
17861
|
+
// When aria-controls changes on a focused element, what it used to control
|
|
17862
|
+
// and what it controls now both re-check their inherited focus. One observer
|
|
17863
|
+
// on the document rather than one per tracked element: the attribute changes
|
|
17864
|
+
// rarely, on few elements, while thousands are tracked — and it can be set
|
|
17865
|
+
// after the element was, so "has it at setup" would miss it.
|
|
17866
|
+
let ariaControlsObserver = null;
|
|
17867
|
+
const observeAriaControls = () => {
|
|
17868
|
+
if (ariaControlsObserver) {
|
|
17869
|
+
return;
|
|
17870
|
+
}
|
|
17871
|
+
ariaControlsObserver = new MutationObserver((mutations) => {
|
|
17751
17872
|
for (const mutation of mutations) {
|
|
17873
|
+
const el = mutation.target;
|
|
17874
|
+
if (!focusTrackingWeakMap.has(el)) {
|
|
17875
|
+
continue;
|
|
17876
|
+
}
|
|
17877
|
+
if (!el.matches(":focus-within")) {
|
|
17878
|
+
continue;
|
|
17879
|
+
}
|
|
17752
17880
|
const oldIds = (mutation.oldValue || "").split(" ").filter(Boolean);
|
|
17753
17881
|
for (const id of oldIds) {
|
|
17754
17882
|
const controlled = document.getElementById(id);
|
|
@@ -17756,30 +17884,20 @@ const isKeyboardModality = () => keyboardNavigationUsed;
|
|
|
17756
17884
|
requestPseudoStateCheck(controlled, {});
|
|
17757
17885
|
}
|
|
17758
17886
|
}
|
|
17887
|
+
notifyAriaControlled(el, {});
|
|
17759
17888
|
}
|
|
17760
|
-
notifyAriaControlled(el, {});
|
|
17761
17889
|
});
|
|
17762
|
-
|
|
17890
|
+
ariaControlsObserver.observe(document.documentElement, {
|
|
17891
|
+
subtree: true,
|
|
17763
17892
|
attributes: true,
|
|
17764
17893
|
attributeFilter: ["aria-controls"],
|
|
17765
17894
|
attributeOldValue: true,
|
|
17766
17895
|
});
|
|
17767
|
-
// }
|
|
17768
|
-
return () => {
|
|
17769
|
-
el.removeEventListener("focusin", onFocusChange);
|
|
17770
|
-
el.removeEventListener("focusout", onFocusChange);
|
|
17771
|
-
observer?.disconnect();
|
|
17772
|
-
};
|
|
17773
17896
|
};
|
|
17774
17897
|
|
|
17775
17898
|
definePseudoClass(":focus", {
|
|
17776
17899
|
attribute: "data-focus",
|
|
17777
|
-
setup:
|
|
17778
|
-
const cleanup = setupFocus(el, callback);
|
|
17779
|
-
return () => {
|
|
17780
|
-
cleanup();
|
|
17781
|
-
};
|
|
17782
|
-
},
|
|
17900
|
+
setup: trackFocus,
|
|
17783
17901
|
test: (el) => {
|
|
17784
17902
|
if (el.matches(":focus")) {
|
|
17785
17903
|
return true;
|
|
@@ -17795,9 +17913,7 @@ const isKeyboardModality = () => keyboardNavigationUsed;
|
|
|
17795
17913
|
// No per-element keydown/keyup listener: the shared recheckFocusChainOnKey
|
|
17796
17914
|
// handler re-checks the focused element (the only one a keystroke can turn
|
|
17797
17915
|
// focus-visible) so a keypress stays O(1), not O(number-of-boxes).
|
|
17798
|
-
setup:
|
|
17799
|
-
return setupFocus(el, callback);
|
|
17800
|
-
},
|
|
17916
|
+
setup: trackFocus,
|
|
17801
17917
|
test: (el) => {
|
|
17802
17918
|
if (isMatchingFocusVisible(el)) {
|
|
17803
17919
|
return true;
|
|
@@ -17810,18 +17926,7 @@ const isKeyboardModality = () => keyboardNavigationUsed;
|
|
|
17810
17926
|
});
|
|
17811
17927
|
definePseudoClass(":focus-within", {
|
|
17812
17928
|
attribute: "data-focus-within",
|
|
17813
|
-
setup:
|
|
17814
|
-
const onFocusChange = (e) => {
|
|
17815
|
-
callback();
|
|
17816
|
-
notifyAriaControlled(el, e);
|
|
17817
|
-
};
|
|
17818
|
-
el.addEventListener("focusin", onFocusChange);
|
|
17819
|
-
el.addEventListener("focusout", onFocusChange);
|
|
17820
|
-
return () => {
|
|
17821
|
-
el.removeEventListener("focusin", onFocusChange);
|
|
17822
|
-
el.removeEventListener("focusout", onFocusChange);
|
|
17823
|
-
};
|
|
17824
|
-
},
|
|
17929
|
+
setup: trackFocus,
|
|
17825
17930
|
test: (el) => {
|
|
17826
17931
|
if (el.matches(":focus-within")) {
|
|
17827
17932
|
return true;
|
|
@@ -18116,8 +18221,18 @@ const initPseudoStyles = (
|
|
|
18116
18221
|
onStateChange(state, oldPseudoState);
|
|
18117
18222
|
}),
|
|
18118
18223
|
);
|
|
18119
|
-
|
|
18224
|
+
// One function for every way a re-check can be asked, so that setups
|
|
18225
|
+
// registering it side by side (the focus tracking, for one) hold the same
|
|
18226
|
+
// callback and call it once.
|
|
18227
|
+
const requestCheck = () => {
|
|
18120
18228
|
checkPseudoClasses();
|
|
18229
|
+
};
|
|
18230
|
+
element.addEventListener("navi_pseudo_state_request_check", requestCheck);
|
|
18231
|
+
addTeardown(() => {
|
|
18232
|
+
element.removeEventListener(
|
|
18233
|
+
"navi_pseudo_state_request_check",
|
|
18234
|
+
requestCheck,
|
|
18235
|
+
);
|
|
18121
18236
|
});
|
|
18122
18237
|
|
|
18123
18238
|
for (const pseudoClass of pseudoClasses) {
|
|
@@ -18128,9 +18243,7 @@ const initPseudoStyles = (
|
|
|
18128
18243
|
}
|
|
18129
18244
|
const { setup } = pseudoClassDefinition;
|
|
18130
18245
|
if (setup) {
|
|
18131
|
-
const cleanup = setup(element,
|
|
18132
|
-
checkPseudoClasses();
|
|
18133
|
-
});
|
|
18246
|
+
const cleanup = setup(element, requestCheck);
|
|
18134
18247
|
addTeardown(cleanup);
|
|
18135
18248
|
}
|
|
18136
18249
|
}
|
|
@@ -18150,6 +18263,11 @@ const applyStyle = (
|
|
|
18150
18263
|
return;
|
|
18151
18264
|
}
|
|
18152
18265
|
const styleToApply = getStyleToApply(style, pseudoState, pseudoNamedStyles);
|
|
18266
|
+
// The same object comes back for every state change of a box whose inline
|
|
18267
|
+
// style has no pseudo entry: nothing to write.
|
|
18268
|
+
if (appliedStyleWeakMap.get(element) === styleToApply) {
|
|
18269
|
+
return;
|
|
18270
|
+
}
|
|
18153
18271
|
updateStyle(element, styleToApply, preventInitialTransition);
|
|
18154
18272
|
};
|
|
18155
18273
|
|
|
@@ -18164,52 +18282,74 @@ const getStyleToApply = (styles, pseudoState, pseudoNamedStyles) => {
|
|
|
18164
18282
|
) {
|
|
18165
18283
|
return styles;
|
|
18166
18284
|
}
|
|
18167
|
-
|
|
18168
|
-
const
|
|
18169
|
-
|
|
18170
|
-
|
|
18171
|
-
|
|
18172
|
-
return true;
|
|
18173
|
-
}
|
|
18174
|
-
// Handle pseudo-elements with states like "::-navi-loader:checked:disabled"
|
|
18175
|
-
const pseudoStatesString = pseudoKey.slice(nextColonIndex);
|
|
18176
|
-
return isMatching(pseudoStatesString);
|
|
18285
|
+
let style = styles;
|
|
18286
|
+
for (const pseudoKey of Object.keys(pseudoNamedStyles)) {
|
|
18287
|
+
const requiredStates = getPseudoKeyRequiredStates(pseudoKey);
|
|
18288
|
+
if (!requiredStates.every((state) => pseudoState[state])) {
|
|
18289
|
+
continue;
|
|
18177
18290
|
}
|
|
18178
|
-
|
|
18179
|
-
|
|
18180
|
-
return pseudoState[pseudoKey];
|
|
18291
|
+
if (style === styles) {
|
|
18292
|
+
style = { ...styles };
|
|
18181
18293
|
}
|
|
18182
|
-
//
|
|
18183
|
-
|
|
18184
|
-
|
|
18185
|
-
|
|
18186
|
-
|
|
18187
|
-
|
|
18188
|
-
|
|
18189
|
-
|
|
18190
|
-
|
|
18191
|
-
|
|
18192
|
-
|
|
18193
|
-
|
|
18294
|
+
// Both sides are already normalized for CSS by the box; only the
|
|
18295
|
+
// properties that compose (a press scale on top of a translate) go through
|
|
18296
|
+
// a merge, the rest is a plain override.
|
|
18297
|
+
const styleToAdd = pseudoNamedStyles[pseudoKey];
|
|
18298
|
+
for (const key of Object.keys(styleToAdd)) {
|
|
18299
|
+
const value = styleToAdd[key];
|
|
18300
|
+
if (value === undefined) {
|
|
18301
|
+
continue;
|
|
18302
|
+
}
|
|
18303
|
+
if (key === "transform" || key === "willChange") {
|
|
18304
|
+
style[key] = mergeOneStyle(style[key], value, key, "css");
|
|
18305
|
+
} else {
|
|
18306
|
+
style[key] = value;
|
|
18307
|
+
}
|
|
18194
18308
|
}
|
|
18195
18309
|
}
|
|
18196
|
-
|
|
18197
|
-
|
|
18310
|
+
return style;
|
|
18311
|
+
};
|
|
18312
|
+
|
|
18313
|
+
// The state names a pseudo key asks for, parsed once: the same few keys come
|
|
18314
|
+
// back on every state change of every box that has them. "::x" alone always
|
|
18315
|
+
// matches; "::x:a:b" and ":a:b" ask for ":a" and ":b" — the state keys as
|
|
18316
|
+
// checkPseudoClasses writes them, colon included.
|
|
18317
|
+
const pseudoKeyRequiredStatesMap = new Map();
|
|
18318
|
+
const getPseudoKeyRequiredStates = (pseudoKey) => {
|
|
18319
|
+
const cached = pseudoKeyRequiredStatesMap.get(pseudoKey);
|
|
18320
|
+
if (cached) {
|
|
18321
|
+
return cached;
|
|
18198
18322
|
}
|
|
18199
|
-
let
|
|
18200
|
-
|
|
18201
|
-
|
|
18323
|
+
let requiredStates;
|
|
18324
|
+
if (pseudoKey.startsWith("::")) {
|
|
18325
|
+
const nextColonIndex = pseudoKey.indexOf(":", 2);
|
|
18326
|
+
requiredStates =
|
|
18327
|
+
nextColonIndex === -1
|
|
18328
|
+
? []
|
|
18329
|
+
: getPseudoKeyRequiredStates(pseudoKey.slice(nextColonIndex));
|
|
18330
|
+
} else {
|
|
18331
|
+
const nextColonIndex = pseudoKey.indexOf(":", 1);
|
|
18332
|
+
requiredStates =
|
|
18333
|
+
nextColonIndex === -1
|
|
18334
|
+
? [pseudoKey]
|
|
18335
|
+
: pseudoKey
|
|
18336
|
+
.slice(1)
|
|
18337
|
+
.split(":")
|
|
18338
|
+
.map((state) => `:${state}`);
|
|
18202
18339
|
}
|
|
18203
|
-
|
|
18340
|
+
pseudoKeyRequiredStatesMap.set(pseudoKey, requiredStates);
|
|
18341
|
+
return requiredStates;
|
|
18204
18342
|
};
|
|
18205
18343
|
|
|
18206
|
-
|
|
18344
|
+
// element → the style object last written to it, so the next one is written
|
|
18345
|
+
// as a difference: the values that changed, the keys it no longer has.
|
|
18346
|
+
const appliedStyleWeakMap = new WeakMap();
|
|
18207
18347
|
const elementTransitionWeakMap = new WeakMap();
|
|
18208
18348
|
const elementRenderedWeakSet = new WeakSet();
|
|
18209
|
-
const
|
|
18349
|
+
const NO_STYLE = {};
|
|
18210
18350
|
const updateStyle = (element, style, preventInitialTransition) => {
|
|
18211
|
-
const
|
|
18212
|
-
const
|
|
18351
|
+
const styleToApply = style || NO_STYLE;
|
|
18352
|
+
const styleApplied = appliedStyleWeakMap.get(element) || NO_STYLE;
|
|
18213
18353
|
// TRANSITION ANTI-FLICKER STRATEGY:
|
|
18214
18354
|
// Problem: When setting both transition and styled properties simultaneously
|
|
18215
18355
|
// (e.g., el.style.transition = "border-radius 0.3s ease"; el.style.borderRadius = "20px"),
|
|
@@ -18219,32 +18359,32 @@ const updateStyle = (element, style, preventInitialTransition) => {
|
|
|
18219
18359
|
// transition to "none", then restore the intended transition after the frame completes.
|
|
18220
18360
|
// We handle multiple updateStyle calls in the same frame gracefully - only one
|
|
18221
18361
|
// requestAnimationFrame is scheduled per element, and the final transition value wins.
|
|
18222
|
-
let
|
|
18362
|
+
let skipTransition = false;
|
|
18223
18363
|
if (!elementRenderedWeakSet.has(element)) {
|
|
18224
|
-
const hasTransition =
|
|
18364
|
+
const hasTransition = Object.hasOwn(styleToApply, "transition");
|
|
18225
18365
|
if (hasTransition || preventInitialTransition) {
|
|
18226
|
-
if (elementTransitionWeakMap.has(element)) {
|
|
18227
|
-
elementTransitionWeakMap.set(element, style?.transition);
|
|
18228
|
-
} else {
|
|
18366
|
+
if (!elementTransitionWeakMap.has(element)) {
|
|
18229
18367
|
element.style.transition = "none";
|
|
18230
|
-
elementTransitionWeakMap.set(element, style?.transition);
|
|
18231
18368
|
}
|
|
18232
|
-
|
|
18233
|
-
|
|
18234
|
-
|
|
18369
|
+
elementTransitionWeakMap.set(element, styleToApply.transition);
|
|
18370
|
+
// Stays "none" until the first frame puts the intended value back
|
|
18371
|
+
skipTransition = true;
|
|
18235
18372
|
}
|
|
18236
18373
|
afterFirstFrame(element);
|
|
18237
18374
|
}
|
|
18238
18375
|
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
for (const key of styleKeySetToApply) {
|
|
18242
|
-
const value = style[key];
|
|
18376
|
+
for (const key of Object.keys(styleToApply)) {
|
|
18377
|
+
const value = styleToApply[key];
|
|
18243
18378
|
if (value === undefined || value === null) {
|
|
18244
|
-
//
|
|
18379
|
+
// a removal: handled below with the keys this style no longer has
|
|
18380
|
+
continue;
|
|
18381
|
+
}
|
|
18382
|
+
if (skipTransition && key === "transition") {
|
|
18383
|
+
continue;
|
|
18384
|
+
}
|
|
18385
|
+
if (styleApplied[key] === value) {
|
|
18245
18386
|
continue;
|
|
18246
18387
|
}
|
|
18247
|
-
keysToDelete.delete(key);
|
|
18248
18388
|
if (key.startsWith("--")) {
|
|
18249
18389
|
element.style.setProperty(key, value);
|
|
18250
18390
|
} else {
|
|
@@ -18252,8 +18392,15 @@ const updateStyle = (element, style, preventInitialTransition) => {
|
|
|
18252
18392
|
}
|
|
18253
18393
|
}
|
|
18254
18394
|
|
|
18255
|
-
|
|
18256
|
-
|
|
18395
|
+
for (const key of Object.keys(styleApplied)) {
|
|
18396
|
+
const previousValue = styleApplied[key];
|
|
18397
|
+
if (previousValue === undefined || previousValue === null) {
|
|
18398
|
+
continue;
|
|
18399
|
+
}
|
|
18400
|
+
const value = styleToApply[key];
|
|
18401
|
+
if (value !== undefined && value !== null) {
|
|
18402
|
+
continue;
|
|
18403
|
+
}
|
|
18257
18404
|
if (key.startsWith("--")) {
|
|
18258
18405
|
element.style.removeProperty(key);
|
|
18259
18406
|
} else {
|
|
@@ -18261,7 +18408,7 @@ const updateStyle = (element, style, preventInitialTransition) => {
|
|
|
18261
18408
|
}
|
|
18262
18409
|
}
|
|
18263
18410
|
|
|
18264
|
-
|
|
18411
|
+
appliedStyleWeakMap.set(element, styleToApply);
|
|
18265
18412
|
};
|
|
18266
18413
|
|
|
18267
18414
|
// One frame for every element waiting for its first one, not one frame each.
|
|
@@ -18313,8 +18460,10 @@ const useComposeElementRef = (syncElement, externalRef) => {
|
|
|
18313
18460
|
const cleanupRef = useRef(null);
|
|
18314
18461
|
const elRef = useRef(null);
|
|
18315
18462
|
const prevSyncElementRef = useRef(undefined);
|
|
18316
|
-
const
|
|
18463
|
+
const stableRef = useRef(null);
|
|
18317
18464
|
const externalRefRef = useRef(externalRef);
|
|
18465
|
+
const syncElementRef = useRef(syncElement);
|
|
18466
|
+
syncElementRef.current = syncElement;
|
|
18318
18467
|
// Detect external ref identity change between renders. The refCallback is
|
|
18319
18468
|
// stable across renders, so when the parent passes a new ref object (or
|
|
18320
18469
|
// switches from null to a ref), Preact does NOT re-fire the callback while
|
|
@@ -18338,24 +18487,24 @@ const useComposeElementRef = (syncElement, externalRef) => {
|
|
|
18338
18487
|
}
|
|
18339
18488
|
externalRefRef.current = externalRef;
|
|
18340
18489
|
|
|
18341
|
-
|
|
18342
|
-
|
|
18343
|
-
|
|
18344
|
-
|
|
18345
|
-
|
|
18346
|
-
|
|
18347
|
-
const
|
|
18348
|
-
|
|
18349
|
-
|
|
18350
|
-
|
|
18351
|
-
|
|
18352
|
-
|
|
18353
|
-
|
|
18354
|
-
|
|
18355
|
-
|
|
18356
|
-
|
|
18357
|
-
|
|
18358
|
-
|
|
18490
|
+
if (!stableRef.current) {
|
|
18491
|
+
// Created once, like the ref callback that calls it, and reading the sync
|
|
18492
|
+
// function through a ref for that reason: the element can be replaced long
|
|
18493
|
+
// after the first render — a tag that changes, a box hidden then shown —
|
|
18494
|
+
// and what the new element gets must be the current render's sync, not
|
|
18495
|
+
// the one the first render closed over.
|
|
18496
|
+
const runSync = (el) => {
|
|
18497
|
+
if (cleanupRef.current) {
|
|
18498
|
+
cleanupRef.current();
|
|
18499
|
+
cleanupRef.current = null;
|
|
18500
|
+
}
|
|
18501
|
+
const syncElementNow = syncElementRef.current;
|
|
18502
|
+
prevSyncElementRef.current = syncElementNow;
|
|
18503
|
+
const cleanup = syncElementNow(el);
|
|
18504
|
+
if (typeof cleanup === "function") {
|
|
18505
|
+
cleanupRef.current = cleanup;
|
|
18506
|
+
}
|
|
18507
|
+
};
|
|
18359
18508
|
const refCallback = (el) => {
|
|
18360
18509
|
elRef.current = el;
|
|
18361
18510
|
// Keep .current in sync immediately so useEffect callbacks that read
|
|
@@ -18379,10 +18528,15 @@ const useComposeElementRef = (syncElement, externalRef) => {
|
|
|
18379
18528
|
prevSyncElementRef.current = undefined;
|
|
18380
18529
|
}
|
|
18381
18530
|
};
|
|
18382
|
-
|
|
18531
|
+
stableRef.current = { refCallback, runSync };
|
|
18532
|
+
}
|
|
18533
|
+
const { refCallback, runSync } = stableRef.current;
|
|
18534
|
+
|
|
18535
|
+
// If element already mounted, re-sync when syncElement reference changed.
|
|
18536
|
+
if (elRef.current && syncElement !== prevSyncElementRef.current) {
|
|
18537
|
+
runSync(elRef.current);
|
|
18383
18538
|
}
|
|
18384
18539
|
|
|
18385
|
-
const refCallback = refCallbackRef.current;
|
|
18386
18540
|
refCallback.current = elRef.current;
|
|
18387
18541
|
return refCallback;
|
|
18388
18542
|
};
|
|
@@ -18576,12 +18730,6 @@ import.meta.css = [/* css */`
|
|
|
18576
18730
|
told it may, and without that the body grows instead of scrolling */
|
|
18577
18731
|
min-height: 0;
|
|
18578
18732
|
flex: 0 1 auto;
|
|
18579
|
-
/* Overflow makes it focusable via tab: apply the outline styles */
|
|
18580
|
-
outline-width: var(--navi-focus-outline-width);
|
|
18581
|
-
/* Outline must appear ON the body, not outside */
|
|
18582
|
-
/* Because for instance when body is within dialog or slide with overflow: hidden it would not be visible */
|
|
18583
|
-
outline-offset: calc(-1 * var(--navi-focus-outline-width));
|
|
18584
|
-
overflow: auto;
|
|
18585
18733
|
|
|
18586
18734
|
/* The same reading as the header's corners above, on all four: a body
|
|
18587
18735
|
follows the corners of the box it is drawn in — which is also what
|
|
@@ -18590,6 +18738,12 @@ import.meta.css = [/* css */`
|
|
|
18590
18738
|
border-top-right-radius: inherit;
|
|
18591
18739
|
border-bottom-right-radius: inherit;
|
|
18592
18740
|
border-bottom-left-radius: inherit;
|
|
18741
|
+
/* Overflow makes it focusable via tab: apply the outline styles */
|
|
18742
|
+
outline-width: var(--navi-focus-outline-width);
|
|
18743
|
+
/* Outline must appear ON the body, not outside */
|
|
18744
|
+
/* Because for instance when body is within dialog or slide with overflow: hidden it would not be visible */
|
|
18745
|
+
outline-offset: calc(-1 * var(--navi-focus-outline-width));
|
|
18746
|
+
overflow: auto;
|
|
18593
18747
|
|
|
18594
18748
|
&:focus-visible {
|
|
18595
18749
|
outline-style: solid;
|
|
@@ -18736,6 +18890,7 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
|
|
|
18736
18890
|
* childPropSet?: Set<string>,
|
|
18737
18891
|
* preventInitialTransition?: boolean,
|
|
18738
18892
|
* separator?: import("ignore:preact").ComponentChildren | ((index: number) => import("ignore:preact").ComponentChildren),
|
|
18893
|
+
* ownTarget?: boolean | "refuse" | "always",
|
|
18739
18894
|
* children?: import("ignore:preact").ComponentChildren,
|
|
18740
18895
|
* [key: string]: any,
|
|
18741
18896
|
* }>}
|
|
@@ -18743,10 +18898,94 @@ const PSEUDO_STATE_CHILD_PROP_SET = new Set(["tabIndex", "tabindex"]);
|
|
|
18743
18898
|
const Box = props => {
|
|
18744
18899
|
const {
|
|
18745
18900
|
ref,
|
|
18901
|
+
children,
|
|
18902
|
+
separator,
|
|
18903
|
+
interactions,
|
|
18904
|
+
...computeProps
|
|
18905
|
+
} = props;
|
|
18906
|
+
const parentBoxFlow = useContext(BoxFlowContext);
|
|
18907
|
+
// Which interactions this box answers, and with what. Read here rather than
|
|
18908
|
+
// on the control, so a swipe or a hold can be declared on anything — a row, a
|
|
18909
|
+
// card, a block of text — and reach the control it belongs to (which is what
|
|
18910
|
+
// carries the action, and what knows it is disabled) by looking for it.
|
|
18911
|
+
// Read through a ref by the effect below: what an interaction DOES is this
|
|
18912
|
+
// render, while WHEN it happens is wired once, at mount (see
|
|
18913
|
+
// useInteractionsEffect).
|
|
18914
|
+
const interactionsRef = useRef(null);
|
|
18915
|
+
interactionsRef.current = resolveInteractions(interactions);
|
|
18916
|
+
|
|
18917
|
+
// What the props say about this box is worked out once per distinct set of
|
|
18918
|
+
// them: a parent re-rendering hands every box below it a new props object,
|
|
18919
|
+
// and most of the time nothing in it has changed. Handlers are the
|
|
18920
|
+
// exception — a closure is new on every render — so they are compared by
|
|
18921
|
+
// name only and put back fresh, see withCurrentHandlers.
|
|
18922
|
+
const renderMemoRef = useRef(null);
|
|
18923
|
+
const renderMemo = renderMemoRef.current;
|
|
18924
|
+
let computed;
|
|
18925
|
+
if (renderMemo && renderMemo.parentBoxFlow === parentBoxFlow && arePropsEquivalent(renderMemo.props, computeProps)) {
|
|
18926
|
+
computed = withCurrentHandlers(renderMemo.computed, computeProps);
|
|
18927
|
+
} else {
|
|
18928
|
+
computed = computeBox(computeProps, parentBoxFlow);
|
|
18929
|
+
}
|
|
18930
|
+
renderMemoRef.current = {
|
|
18931
|
+
props: computeProps,
|
|
18932
|
+
parentBoxFlow,
|
|
18933
|
+
computed
|
|
18934
|
+
};
|
|
18935
|
+
const {
|
|
18936
|
+
TagName,
|
|
18937
|
+
boxFlow,
|
|
18938
|
+
boxFlowIsDefault,
|
|
18939
|
+
row,
|
|
18940
|
+
column,
|
|
18941
|
+
aspectRatio,
|
|
18942
|
+
visualSelector,
|
|
18943
|
+
innerClassName,
|
|
18944
|
+
selfForwardedProps,
|
|
18945
|
+
childForwardedProps,
|
|
18946
|
+
styleDeps
|
|
18947
|
+
} = computed;
|
|
18948
|
+
const syncBox = useCallback(computed.syncBox, styleDeps);
|
|
18949
|
+
const finalRef = useComposeElementRef(syncBox, ref);
|
|
18950
|
+
useInteractionsEffect(finalRef, interactionsRef);
|
|
18951
|
+
let innerChildren = children;
|
|
18952
|
+
if (separator) {
|
|
18953
|
+
// Flatten nested arrays (e.g., from .map()) to treat each element as individual child
|
|
18954
|
+
innerChildren = applySeparatorOnChildren(innerChildren, separator);
|
|
18955
|
+
}
|
|
18956
|
+
|
|
18957
|
+
// When hasChildUsingForwardedProps is used it means
|
|
18958
|
+
// Some/all the children needs to access remainingProps
|
|
18959
|
+
// to render and will provide a function to do so.
|
|
18960
|
+
if (props.hasChildUsingForwardedProps) {
|
|
18961
|
+
innerChildren = jsx(BoxForwardedPropsContext.Provider, {
|
|
18962
|
+
value: childForwardedProps,
|
|
18963
|
+
children: innerChildren
|
|
18964
|
+
});
|
|
18965
|
+
}
|
|
18966
|
+
return jsx(TagName, {
|
|
18967
|
+
ref: finalRef,
|
|
18968
|
+
className: innerClassName,
|
|
18969
|
+
"navi-box-flow": boxFlowIsDefault ? undefined : boxFlow,
|
|
18970
|
+
"navi-box-flow-row": row ? "" : undefined,
|
|
18971
|
+
"navi-box-flow-column": column ? "" : undefined,
|
|
18972
|
+
"navi-aspect-ratio": aspectRatio ? aspectRatio : undefined,
|
|
18973
|
+
"data-visual-selector": visualSelector,
|
|
18974
|
+
...selfForwardedProps,
|
|
18975
|
+
children: jsx(BoxFlowContext.Provider, {
|
|
18976
|
+
value: boxFlow,
|
|
18977
|
+
children: innerChildren
|
|
18978
|
+
})
|
|
18979
|
+
});
|
|
18980
|
+
};
|
|
18981
|
+
|
|
18982
|
+
// Everything the props decide, for the JSX and for the DOM sync. Pure, which
|
|
18983
|
+
// is what lets Box keep the previous result when the props are equivalent.
|
|
18984
|
+
const computeBox = (props, parentBoxFlow) => {
|
|
18985
|
+
const {
|
|
18746
18986
|
as: asProp = "div",
|
|
18747
18987
|
baseClassName,
|
|
18748
18988
|
className,
|
|
18749
|
-
baseStyle,
|
|
18750
18989
|
// style management
|
|
18751
18990
|
style,
|
|
18752
18991
|
styleCSSVars = STYLE_CSS_VARS_DEFAULT,
|
|
@@ -18772,8 +19011,6 @@ const Box = props => {
|
|
|
18772
19011
|
// (when transition is set via props, this is done automatically)
|
|
18773
19012
|
// so this prop is useful only when transition is enabled from "outside" (via CSS)
|
|
18774
19013
|
preventInitialTransition,
|
|
18775
|
-
children,
|
|
18776
|
-
separator,
|
|
18777
19014
|
// Layout roles inside a scrolling container (a Dialog, a Popover): the
|
|
18778
19015
|
// header stays at the top and the footer at the bottom while the rest
|
|
18779
19016
|
// scrolls, or — when a body is present — the body is what scrolls and the
|
|
@@ -18783,22 +19020,24 @@ const Box = props => {
|
|
|
18783
19020
|
header,
|
|
18784
19021
|
footer,
|
|
18785
19022
|
body,
|
|
18786
|
-
//
|
|
18787
|
-
//
|
|
18788
|
-
//
|
|
18789
|
-
//
|
|
18790
|
-
|
|
19023
|
+
// A press landing here is aimed AT this box, not at whatever it sits in — a
|
|
19024
|
+
// cross an application draws in a card's corner, a badge on a row that
|
|
19025
|
+
// travels. Writing the attribute is the whole of it here: it is read off
|
|
19026
|
+
// the DOM by the controls above and by the gesture readers, and what an
|
|
19027
|
+
// affordance makes of the read-only around it is a question only a control
|
|
19028
|
+
// can answer (see own_target.js).
|
|
19029
|
+
ownTarget,
|
|
18791
19030
|
...rest
|
|
18792
19031
|
} = props;
|
|
19032
|
+
if (ownTarget) {
|
|
19033
|
+
rest[OWN_TARGET_ATTRIBUTE] = typeof ownTarget === "string" ? ownTarget : "";
|
|
19034
|
+
}
|
|
18793
19035
|
let as = asProp;
|
|
18794
19036
|
|
|
18795
19037
|
// A box that scrolls is what gives header/footer/body their meaning, and
|
|
18796
19038
|
// saying overflow="auto" is already saying it — no second prop for the same
|
|
18797
19039
|
// fact. Dialog and Popover get it the same way, by asking for that overflow.
|
|
18798
|
-
const scrolls =
|
|
18799
|
-
const value = rest[name];
|
|
18800
|
-
return value === "auto" || value === "scroll";
|
|
18801
|
-
});
|
|
19040
|
+
const scrolls = isScrollingOverflow(rest.overflow) || isScrollingOverflow(rest.overflowX) || isScrollingOverflow(rest.overflowY);
|
|
18802
19041
|
// <header>/<footer> rather than a div: the role is exactly what those tags
|
|
18803
19042
|
// mean, and a screen reader gets it for free. The body stays a div — <main>
|
|
18804
19043
|
// means "the main content of the document", which a popup's body is not.
|
|
@@ -18834,8 +19073,6 @@ const Box = props => {
|
|
|
18834
19073
|
}
|
|
18835
19074
|
}
|
|
18836
19075
|
const defaultDisplay = getDefaultDisplay(TagName);
|
|
18837
|
-
// Read the parent flow early so we can use it when display="inherit" is requested.
|
|
18838
|
-
const parentBoxFlow = useContext(BoxFlowContext);
|
|
18839
19076
|
let {
|
|
18840
19077
|
inline,
|
|
18841
19078
|
block,
|
|
@@ -18911,12 +19148,6 @@ const Box = props => {
|
|
|
18911
19148
|
}
|
|
18912
19149
|
const boxFlowIsDefault = boxFlow === defaultDisplay;
|
|
18913
19150
|
|
|
18914
|
-
// Read through a ref by the effect below: what an interaction DOES is this
|
|
18915
|
-
// render, while WHEN it happens is wired once, at mount (see
|
|
18916
|
-
// useInteractionsEffect).
|
|
18917
|
-
const interactionsRef = useRef(null);
|
|
18918
|
-
interactionsRef.current = resolveInteractions(interactions);
|
|
18919
|
-
const remainingPropKeySet = new Set(Object.keys(rest));
|
|
18920
19151
|
// The box is only a frame and one of its descendants IS the component (a Button
|
|
18921
19152
|
// and its content): everything, event handlers included, belongs to that
|
|
18922
19153
|
// descendant.
|
|
@@ -18924,15 +19155,20 @@ const Box = props => {
|
|
|
18924
19155
|
const innerClassName = withPropsClassName(baseClassName, className);
|
|
18925
19156
|
const selfForwardedProps = {};
|
|
18926
19157
|
const childForwardedProps = {};
|
|
18927
|
-
let
|
|
19158
|
+
let styleDeps;
|
|
19159
|
+
let syncBox;
|
|
18928
19160
|
{
|
|
18929
|
-
|
|
19161
|
+
styleDeps = [
|
|
18930
19162
|
// Layout and alignment props
|
|
18931
19163
|
parentBoxFlow, boxFlow,
|
|
18932
19164
|
// Style context dependencies
|
|
18933
19165
|
styleCSSVars, pseudoClasses, pseudoElements,
|
|
18934
19166
|
// Selectors
|
|
18935
19167
|
visualSelector, pseudoStateSelector, preventInitialTransition];
|
|
19168
|
+
// The pseudo state goes into the deps as key/value entries and never as the
|
|
19169
|
+
// object itself: a control builds that object on every render, and its
|
|
19170
|
+
// identity in the deps would re-run the sync — listeners, observers, style
|
|
19171
|
+
// writes — on each of them, for a state that has not changed.
|
|
18936
19172
|
let innerPseudoState;
|
|
18937
19173
|
if (basePseudoState && pseudoState) {
|
|
18938
19174
|
innerPseudoState = {};
|
|
@@ -18942,30 +19178,28 @@ const Box = props => {
|
|
|
18942
19178
|
if (pseudoStateKeySet.has(key)) {
|
|
18943
19179
|
pseudoStateKeySet.delete(key);
|
|
18944
19180
|
const value = pseudoState[key];
|
|
18945
|
-
styleDeps.push(value);
|
|
19181
|
+
styleDeps.push(key, value);
|
|
18946
19182
|
innerPseudoState[key] = value;
|
|
18947
19183
|
} else {
|
|
18948
19184
|
const value = basePseudoState[key];
|
|
18949
|
-
styleDeps.push(value);
|
|
19185
|
+
styleDeps.push(key, value);
|
|
18950
19186
|
innerPseudoState[key] = value;
|
|
18951
19187
|
}
|
|
18952
19188
|
}
|
|
18953
19189
|
for (const key of pseudoStateKeySet) {
|
|
18954
19190
|
const value = pseudoState[key];
|
|
18955
|
-
styleDeps.push(value);
|
|
19191
|
+
styleDeps.push(key, value);
|
|
18956
19192
|
innerPseudoState[key] = value;
|
|
18957
19193
|
}
|
|
18958
19194
|
} else if (basePseudoState) {
|
|
18959
19195
|
innerPseudoState = basePseudoState;
|
|
18960
19196
|
for (const key of Object.keys(basePseudoState)) {
|
|
18961
|
-
|
|
18962
|
-
styleDeps.push(value);
|
|
19197
|
+
styleDeps.push(key, basePseudoState[key]);
|
|
18963
19198
|
}
|
|
18964
19199
|
} else if (pseudoState) {
|
|
18965
19200
|
innerPseudoState = pseudoState;
|
|
18966
19201
|
for (const key of Object.keys(pseudoState)) {
|
|
18967
|
-
|
|
18968
|
-
styleDeps.push(value);
|
|
19202
|
+
styleDeps.push(key, pseudoState[key]);
|
|
18969
19203
|
}
|
|
18970
19204
|
} else {
|
|
18971
19205
|
innerPseudoState = PSEUDO_STATE_DEFAULT;
|
|
@@ -18983,8 +19217,8 @@ const Box = props => {
|
|
|
18983
19217
|
};
|
|
18984
19218
|
let boxPseudoNamedStyles = PSEUDO_NAMED_STYLES_DEFAULT;
|
|
18985
19219
|
const canForwardToChild = hasChildUsingForwardedProps;
|
|
18986
|
-
const addStyle = (value, name, styleContext, stylesTarget
|
|
18987
|
-
const mergedValue = prepareStyleValue(stylesTarget[name], value, name, styleContext
|
|
19220
|
+
const addStyle = (value, name, styleContext, stylesTarget) => {
|
|
19221
|
+
const mergedValue = prepareStyleValue(stylesTarget[name], value, name, styleContext);
|
|
18988
19222
|
const cssVar = styleContext.styleCSSVars[name];
|
|
18989
19223
|
if (cssVar) {
|
|
18990
19224
|
addCSSVar(mergedValue, cssVar, stylesTarget);
|
|
@@ -19006,20 +19240,20 @@ const Box = props => {
|
|
|
19006
19240
|
styleDeps.push(name, value); // impact box style -> add to deps
|
|
19007
19241
|
stylesTarget[name] = value;
|
|
19008
19242
|
};
|
|
19009
|
-
const addStyleMaybeForwarding = (value, name, styleContext, stylesTarget,
|
|
19243
|
+
const addStyleMaybeForwarding = (value, name, styleContext, stylesTarget, visualChildPropStrategy) => {
|
|
19010
19244
|
if (!visualChildPropStrategy) {
|
|
19011
|
-
addStyle(value, name, styleContext, stylesTarget
|
|
19245
|
+
addStyle(value, name, styleContext, stylesTarget);
|
|
19012
19246
|
return false;
|
|
19013
19247
|
}
|
|
19014
19248
|
const cssVar = styleCSSVars[name];
|
|
19015
19249
|
if (cssVar) {
|
|
19016
19250
|
// css var wins over visual child handling
|
|
19017
|
-
addStyle(value, name, styleContext, stylesTarget
|
|
19251
|
+
addStyle(value, name, styleContext, stylesTarget);
|
|
19018
19252
|
return false;
|
|
19019
19253
|
}
|
|
19020
19254
|
if (visualChildPropStrategy === "copy") {
|
|
19021
19255
|
// we stylyze ourself + forward prop to the child
|
|
19022
|
-
addStyle(value, name, styleContext, stylesTarget
|
|
19256
|
+
addStyle(value, name, styleContext, stylesTarget);
|
|
19023
19257
|
}
|
|
19024
19258
|
if (!canForwardToChild) {
|
|
19025
19259
|
return false;
|
|
@@ -19032,13 +19266,15 @@ const Box = props => {
|
|
|
19032
19266
|
// style={{ ":hover": { backgroundColor: "red" } }}
|
|
19033
19267
|
// then we'll track ":hover" state changes even for basic elements like <div>
|
|
19034
19268
|
const pseudoClassesFromStyleSet = new Set();
|
|
19035
|
-
boxPseudoNamedStyles = {};
|
|
19036
19269
|
const visitProp = (value, name, styleContext, boxStylesTarget, styleOrigin) => {
|
|
19037
19270
|
const isPseudoElement = name.startsWith("::");
|
|
19038
19271
|
const isPseudoClass = name.startsWith(":");
|
|
19039
19272
|
if (isPseudoElement || isPseudoClass) {
|
|
19040
19273
|
styleDeps.push(name);
|
|
19041
19274
|
pseudoClassesFromStyleSet.add(name);
|
|
19275
|
+
if (boxPseudoNamedStyles === PSEUDO_NAMED_STYLES_DEFAULT) {
|
|
19276
|
+
boxPseudoNamedStyles = {};
|
|
19277
|
+
}
|
|
19042
19278
|
const pseudoStyleContext = {
|
|
19043
19279
|
...styleContext,
|
|
19044
19280
|
styleCSSVars: {
|
|
@@ -19059,18 +19295,16 @@ const Box = props => {
|
|
|
19059
19295
|
const pseudoClassStyles = {};
|
|
19060
19296
|
for (const key of pseudoStyleKeys) {
|
|
19061
19297
|
visitProp(value[key], key, pseudoStyleContext, pseudoClassStyles, "pseudo_style");
|
|
19062
|
-
boxPseudoNamedStyles[name] = pseudoClassStyles;
|
|
19063
19298
|
}
|
|
19299
|
+
boxPseudoNamedStyles[name] = pseudoClassStyles;
|
|
19064
19300
|
return;
|
|
19065
19301
|
}
|
|
19066
|
-
|
|
19067
|
-
|
|
19068
|
-
if (isCss) {
|
|
19069
|
-
addStyle(value, name, styleContext, boxStylesTarget, context);
|
|
19302
|
+
if (styleOrigin === "style") {
|
|
19303
|
+
addStyle(value, name, styleContext, boxStylesTarget);
|
|
19070
19304
|
return;
|
|
19071
19305
|
}
|
|
19072
19306
|
if (name.startsWith("--")) {
|
|
19073
|
-
addStyle(value, name, styleContext, boxStylesTarget
|
|
19307
|
+
addStyle(value, name, styleContext, boxStylesTarget);
|
|
19074
19308
|
return;
|
|
19075
19309
|
}
|
|
19076
19310
|
const isPseudoStyle = styleOrigin === "pseudo_style";
|
|
@@ -19082,7 +19316,7 @@ const Box = props => {
|
|
|
19082
19316
|
if (
|
|
19083
19317
|
// prop name === css style name
|
|
19084
19318
|
!getStyle) {
|
|
19085
|
-
const needForwarding = addStyleMaybeForwarding(value, name, styleContext, boxStylesTarget,
|
|
19319
|
+
const needForwarding = addStyleMaybeForwarding(value, name, styleContext, boxStylesTarget, visualChildPropStrategy);
|
|
19086
19320
|
if (needForwarding) {
|
|
19087
19321
|
if (isPseudoStyle) ; else {
|
|
19088
19322
|
childForwardedProps[name] = value;
|
|
@@ -19097,7 +19331,7 @@ const Box = props => {
|
|
|
19097
19331
|
let needForwarding = false;
|
|
19098
19332
|
for (const styleName of Object.keys(cssValues)) {
|
|
19099
19333
|
const cssValue = cssValues[styleName];
|
|
19100
|
-
needForwarding = addStyleMaybeForwarding(cssValue, styleName, styleContext, boxStylesTarget,
|
|
19334
|
+
needForwarding = addStyleMaybeForwarding(cssValue, styleName, styleContext, boxStylesTarget, visualChildPropStrategy);
|
|
19101
19335
|
}
|
|
19102
19336
|
if (needForwarding) {
|
|
19103
19337
|
if (isPseudoStyle) ; else {
|
|
@@ -19133,13 +19367,7 @@ const Box = props => {
|
|
|
19133
19367
|
}
|
|
19134
19368
|
return;
|
|
19135
19369
|
};
|
|
19136
|
-
|
|
19137
|
-
for (const key of baseStyle) {
|
|
19138
|
-
const value = baseStyle[key];
|
|
19139
|
-
visitProp(value, key, styleContext, boxStyles, "baseStyle");
|
|
19140
|
-
}
|
|
19141
|
-
}
|
|
19142
|
-
for (const propName of remainingPropKeySet) {
|
|
19370
|
+
for (const propName of Object.keys(rest)) {
|
|
19143
19371
|
const propValue = rest[propName];
|
|
19144
19372
|
if (baseChildPropSet?.has(propName) || childPropSet?.has(propName)) {
|
|
19145
19373
|
if (canForwardToChild) {
|
|
@@ -19149,9 +19377,6 @@ const Box = props => {
|
|
|
19149
19377
|
}
|
|
19150
19378
|
continue;
|
|
19151
19379
|
}
|
|
19152
|
-
if (canForwardToChild && toCopySet.has(propName)) {
|
|
19153
|
-
childForwardedProps[propName] = propValue;
|
|
19154
|
-
}
|
|
19155
19380
|
const isDataAttribute = propName.startsWith("data-");
|
|
19156
19381
|
if (isDataAttribute) {
|
|
19157
19382
|
selfForwardedProps[propName] = propValue;
|
|
@@ -19193,7 +19418,6 @@ const Box = props => {
|
|
|
19193
19418
|
visitProp(styleValue, styleName, styleContext, boxStyles, "style");
|
|
19194
19419
|
}
|
|
19195
19420
|
}
|
|
19196
|
-
styleDeps.push(pseudoStateSelector, innerPseudoState);
|
|
19197
19421
|
let innerPseudoClasses;
|
|
19198
19422
|
if (pseudoClassesFromStyleSet.size) {
|
|
19199
19423
|
innerPseudoClasses = [...pseudoClasses];
|
|
@@ -19210,7 +19434,7 @@ const Box = props => {
|
|
|
19210
19434
|
styleDeps.push(...pseudoClasses);
|
|
19211
19435
|
}
|
|
19212
19436
|
}
|
|
19213
|
-
|
|
19437
|
+
syncBox = boxEl => {
|
|
19214
19438
|
const pseudoStateEl = pseudoStateSelector ? boxEl.querySelector(pseudoStateSelector) : boxEl;
|
|
19215
19439
|
if (!pseudoStateEl) {
|
|
19216
19440
|
console.error(`pseudoStateSelector "${pseudoStateSelector}" did not match any element inside the box`, boxEl);
|
|
@@ -19225,42 +19449,24 @@ const Box = props => {
|
|
|
19225
19449
|
elementToImpact: boxEl,
|
|
19226
19450
|
elementListeningPseudoState: visualEl === pseudoStateEl ? null : visualEl
|
|
19227
19451
|
});
|
|
19228
|
-
}
|
|
19229
|
-
finalRef = useComposeElementRef(syncBox, ref);
|
|
19230
|
-
}
|
|
19231
|
-
useInteractionsEffect(finalRef, interactionsRef);
|
|
19232
|
-
let innerChildren = children;
|
|
19233
|
-
if (separator) {
|
|
19234
|
-
// Flatten nested arrays (e.g., from .map()) to treat each element as individual child
|
|
19235
|
-
innerChildren = applySeparatorOnChildren(innerChildren, separator);
|
|
19236
|
-
}
|
|
19237
|
-
|
|
19238
|
-
// When hasChildUsingForwardedProps is used it means
|
|
19239
|
-
// Some/all the children needs to access remainingProps
|
|
19240
|
-
// to render and will provide a function to do so.
|
|
19241
|
-
if (hasChildUsingForwardedProps) {
|
|
19242
|
-
innerChildren = jsx(BoxForwardedPropsContext.Provider, {
|
|
19243
|
-
value: childForwardedProps,
|
|
19244
|
-
children: innerChildren
|
|
19245
|
-
});
|
|
19452
|
+
};
|
|
19246
19453
|
}
|
|
19247
19454
|
const aspectRatio = rest.square || rest.circle ? "1/1" : rest.aspectRatio;
|
|
19248
|
-
return
|
|
19249
|
-
|
|
19250
|
-
|
|
19251
|
-
|
|
19252
|
-
|
|
19253
|
-
|
|
19254
|
-
|
|
19255
|
-
|
|
19256
|
-
|
|
19257
|
-
|
|
19258
|
-
|
|
19259
|
-
|
|
19260
|
-
|
|
19261
|
-
}
|
|
19455
|
+
return {
|
|
19456
|
+
TagName,
|
|
19457
|
+
boxFlow,
|
|
19458
|
+
boxFlowIsDefault,
|
|
19459
|
+
row,
|
|
19460
|
+
column,
|
|
19461
|
+
aspectRatio,
|
|
19462
|
+
visualSelector,
|
|
19463
|
+
innerClassName,
|
|
19464
|
+
selfForwardedProps,
|
|
19465
|
+
childForwardedProps,
|
|
19466
|
+
styleDeps,
|
|
19467
|
+
syncBox
|
|
19468
|
+
};
|
|
19262
19469
|
};
|
|
19263
|
-
const toCopySet = new Set([]);
|
|
19264
19470
|
const applySeparatorOnChildren = (children, separator) => {
|
|
19265
19471
|
const flattenedChildren = toChildArray(children);
|
|
19266
19472
|
if (flattenedChildren.length <= 1) {
|
|
@@ -19306,6 +19512,62 @@ const isNonZeroSpacing = value => {
|
|
|
19306
19512
|
}
|
|
19307
19513
|
return true;
|
|
19308
19514
|
};
|
|
19515
|
+
const isScrollingOverflow = value => value === "auto" || value === "scroll";
|
|
19516
|
+
|
|
19517
|
+
// Same props as far as computeBox is concerned. Handlers count by name only,
|
|
19518
|
+
// see withCurrentHandlers; no style or state key starts with "on".
|
|
19519
|
+
const arePropsEquivalent = (previousProps, props) => compareTwoJsValues(previousProps, props, {
|
|
19520
|
+
keyComparator: comparePropAt
|
|
19521
|
+
});
|
|
19522
|
+
const comparePropAt = (a, b, key, recurse) => {
|
|
19523
|
+
if (typeof key === "string" && key.startsWith("on")) {
|
|
19524
|
+
return true;
|
|
19525
|
+
}
|
|
19526
|
+
return recurse(a, b);
|
|
19527
|
+
};
|
|
19528
|
+
// The previous computation with this render's handlers: a handler goes where
|
|
19529
|
+
// its name went the previous time, self or child, the split being decided by
|
|
19530
|
+
// props that are the same. The forwarded objects keep their identity when no
|
|
19531
|
+
// handler changed, so what reads them through context sees nothing new.
|
|
19532
|
+
const withCurrentHandlers = (computed, props) => {
|
|
19533
|
+
let {
|
|
19534
|
+
selfForwardedProps,
|
|
19535
|
+
childForwardedProps
|
|
19536
|
+
} = computed;
|
|
19537
|
+
for (const key of Object.keys(props)) {
|
|
19538
|
+
if (!key.startsWith("on")) {
|
|
19539
|
+
continue;
|
|
19540
|
+
}
|
|
19541
|
+
const value = props[key];
|
|
19542
|
+
if (Object.hasOwn(childForwardedProps, key)) {
|
|
19543
|
+
if (childForwardedProps[key] !== value) {
|
|
19544
|
+
if (childForwardedProps === computed.childForwardedProps) {
|
|
19545
|
+
childForwardedProps = {
|
|
19546
|
+
...childForwardedProps
|
|
19547
|
+
};
|
|
19548
|
+
}
|
|
19549
|
+
childForwardedProps[key] = value;
|
|
19550
|
+
}
|
|
19551
|
+
} else if (Object.hasOwn(selfForwardedProps, key)) {
|
|
19552
|
+
if (selfForwardedProps[key] !== value) {
|
|
19553
|
+
if (selfForwardedProps === computed.selfForwardedProps) {
|
|
19554
|
+
selfForwardedProps = {
|
|
19555
|
+
...selfForwardedProps
|
|
19556
|
+
};
|
|
19557
|
+
}
|
|
19558
|
+
selfForwardedProps[key] = value;
|
|
19559
|
+
}
|
|
19560
|
+
}
|
|
19561
|
+
}
|
|
19562
|
+
if (selfForwardedProps === computed.selfForwardedProps && childForwardedProps === computed.childForwardedProps) {
|
|
19563
|
+
return computed;
|
|
19564
|
+
}
|
|
19565
|
+
return {
|
|
19566
|
+
...computed,
|
|
19567
|
+
selfForwardedProps,
|
|
19568
|
+
childForwardedProps
|
|
19569
|
+
};
|
|
19570
|
+
};
|
|
19309
19571
|
|
|
19310
19572
|
const useDebounceTrue = (value, delay = 300) => {
|
|
19311
19573
|
const [debouncedTrue, setDebouncedTrue] = useState(false);
|
|
@@ -19419,7 +19681,7 @@ const setupNetworkMonitoring = () => {
|
|
|
19419
19681
|
};
|
|
19420
19682
|
setupNetworkMonitoring();
|
|
19421
19683
|
|
|
19422
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
19684
|
+
installImportMetaCssBuild(import.meta);const css$11 = /* css */`
|
|
19423
19685
|
.navi_loading_indicator_fluid_container {
|
|
19424
19686
|
position: relative;
|
|
19425
19687
|
display: flex;
|
|
@@ -19451,7 +19713,7 @@ const LoadingIndicatorFluid = ({
|
|
|
19451
19713
|
visuallyHidden,
|
|
19452
19714
|
...rest
|
|
19453
19715
|
}) => {
|
|
19454
|
-
import.meta.css = [css$
|
|
19716
|
+
import.meta.css = [css$11, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
|
|
19455
19717
|
const ref = useRef(null);
|
|
19456
19718
|
// The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
|
|
19457
19719
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
@@ -19656,7 +19918,7 @@ const LoadingRectangleSvg = ({
|
|
|
19656
19918
|
});
|
|
19657
19919
|
};
|
|
19658
19920
|
|
|
19659
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
19921
|
+
installImportMetaCssBuild(import.meta);const css$10 = /* css */`
|
|
19660
19922
|
.navi_loading_outline_wrapper {
|
|
19661
19923
|
position: absolute;
|
|
19662
19924
|
/* Controls place the outline slightly outside their box, right on top of
|
|
@@ -19693,7 +19955,7 @@ installImportMetaCssBuild(import.meta);const css$11 = /* css */`
|
|
|
19693
19955
|
}
|
|
19694
19956
|
`;
|
|
19695
19957
|
const LoadingOutline = props => {
|
|
19696
|
-
import.meta.css = [css$
|
|
19958
|
+
import.meta.css = [css$10, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
|
|
19697
19959
|
if (props.containerRef) {
|
|
19698
19960
|
const container = props.containerRef.current;
|
|
19699
19961
|
if (!container) {
|
|
@@ -19883,6 +20145,256 @@ const getHrefTargetInfo = (href) => {
|
|
|
19883
20145
|
};
|
|
19884
20146
|
};
|
|
19885
20147
|
|
|
20148
|
+
/**
|
|
20149
|
+
* A variant of useLayoutEffect that accounts for ancestor <dialog>/<details>
|
|
20150
|
+
* or popover visibility.
|
|
20151
|
+
*
|
|
20152
|
+
* Motivation: some effects (auto-scroll, measurement, focus) only make sense
|
|
20153
|
+
* when the element is actually presented on screen. A plain useLayoutEffect
|
|
20154
|
+
* fires on mount even when the component is inside a closed <dialog>, a
|
|
20155
|
+
* collapsed <details>, or a hidden popover, where scroll and layout operations
|
|
20156
|
+
* are no-ops.
|
|
20157
|
+
*
|
|
20158
|
+
* Behavior:
|
|
20159
|
+
* - No <dialog>/<details>/[popover] ancestor → runs like a normal
|
|
20160
|
+
* useLayoutEffect with the provided deps.
|
|
20161
|
+
* - Inside a closed/hidden ancestor → skips the initial run; instead runs
|
|
20162
|
+
* the callback once the ancestor opens — see @jsenv/dom's own
|
|
20163
|
+
* observeAncestorOpenState for exactly how that's detected, and why it
|
|
20164
|
+
* matters that it happens before the browser paints.
|
|
20165
|
+
* - Inside an open ancestor → runs on mount AND every subsequent open.
|
|
20166
|
+
*
|
|
20167
|
+
* The callback's second argument is always a `navi_displayed` CustomEvent,
|
|
20168
|
+
* with `detail: { ancestor, ancestorType, becauseAncestorOpened }`:
|
|
20169
|
+
* - No <dialog>/<details>/[popover]/[aria-expanded] ancestor at all →
|
|
20170
|
+
* `{ ancestor: document, ancestorType: "document" }`.
|
|
20171
|
+
* - Otherwise → `{ ancestor: <the matched element>, ancestorType: "dialog"
|
|
20172
|
+
* | "popover" | "details" | "aria-expanded" }`.
|
|
20173
|
+
* `becauseAncestorOpened` distinguishes the two ways of coming on screen:
|
|
20174
|
+
* - true — the element was already mounted and the ancestor just opened,
|
|
20175
|
+
* revealing it along with everything else it holds. The opening has an
|
|
20176
|
+
* owner (the ancestor's own transferFocus/openEffect), and what it reveals
|
|
20177
|
+
* should defer to it — see use_auto_focus.js.
|
|
20178
|
+
* - false — the element was mounted just now, into a surface already on
|
|
20179
|
+
* screen (or into the plain document). Nothing else owns this appearance:
|
|
20180
|
+
* what the element says about itself (an autofocus, a measurement) is the
|
|
20181
|
+
* only word there is.
|
|
20182
|
+
*
|
|
20183
|
+
* Usage:
|
|
20184
|
+
* useDisplayedLayoutEffect(ref, () => {
|
|
20185
|
+
* scrollToSelected();
|
|
20186
|
+
* }, []);
|
|
20187
|
+
*/
|
|
20188
|
+
const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
20189
|
+
if (typeof callback !== "function") {
|
|
20190
|
+
throw new TypeError("useDisplayedLayoutEffect: callback is not a function");
|
|
20191
|
+
}
|
|
20192
|
+
|
|
20193
|
+
// Keep a stable ref so the open listener always calls the latest callback
|
|
20194
|
+
// without needing to be re-registered when deps change.
|
|
20195
|
+
const callbackRef = useRef(callback);
|
|
20196
|
+
callbackRef.current = callback;
|
|
20197
|
+
|
|
20198
|
+
// Run on mount (or when deps change) — but only if the element is visible.
|
|
20199
|
+
useLayoutEffect(() => {
|
|
20200
|
+
const el = ref.current;
|
|
20201
|
+
if (!el) {
|
|
20202
|
+
return;
|
|
20203
|
+
}
|
|
20204
|
+
const ancestor = closestOpenableAncestor(el);
|
|
20205
|
+
if (!ancestor) {
|
|
20206
|
+
callbackRef.current(el, createDisplayedEvent(document, false));
|
|
20207
|
+
return;
|
|
20208
|
+
}
|
|
20209
|
+
if (!isAncestorOpen(ancestor)) {
|
|
20210
|
+
// Ancestor is closed — skip now; the observeAncestorOpenState call
|
|
20211
|
+
// below will fire once it opens.
|
|
20212
|
+
return;
|
|
20213
|
+
}
|
|
20214
|
+
callbackRef.current(el, createDisplayedEvent(ancestor, false));
|
|
20215
|
+
}, deps);
|
|
20216
|
+
|
|
20217
|
+
// Re-run every time the ancestor opens.
|
|
20218
|
+
useLayoutEffect(() => {
|
|
20219
|
+
const el = ref.current;
|
|
20220
|
+
if (!el) {
|
|
20221
|
+
return undefined;
|
|
20222
|
+
}
|
|
20223
|
+
const ancestor = closestOpenableAncestor(el);
|
|
20224
|
+
if (!ancestor) {
|
|
20225
|
+
return undefined;
|
|
20226
|
+
}
|
|
20227
|
+
return observeAncestorOpenState(ancestor, ({ isOpen }) => {
|
|
20228
|
+
if (!isOpen) {
|
|
20229
|
+
return;
|
|
20230
|
+
}
|
|
20231
|
+
const lastEl = ref.current;
|
|
20232
|
+
callbackRef.current(lastEl, createDisplayedEvent(ancestor, true));
|
|
20233
|
+
});
|
|
20234
|
+
}, []);
|
|
20235
|
+
};
|
|
20236
|
+
|
|
20237
|
+
const createDisplayedEvent = (ancestor, becauseAncestorOpened) => {
|
|
20238
|
+
return new CustomEvent("navi_displayed", {
|
|
20239
|
+
detail: {
|
|
20240
|
+
ancestor,
|
|
20241
|
+
ancestorType: getAncestorOpenType(ancestor),
|
|
20242
|
+
becauseAncestorOpened,
|
|
20243
|
+
},
|
|
20244
|
+
});
|
|
20245
|
+
};
|
|
20246
|
+
|
|
20247
|
+
installImportMetaCssBuild(import.meta);// # TextAnchor — how it works
|
|
20248
|
+
const css$$ = /* css */`
|
|
20249
|
+
.navi_text_anchor {
|
|
20250
|
+
vertical-align: baseline;
|
|
20251
|
+
user-select: none;
|
|
20252
|
+
overflow: hidden;
|
|
20253
|
+
}
|
|
20254
|
+
`;
|
|
20255
|
+
|
|
20256
|
+
/**
|
|
20257
|
+
* Positions children vertically relative to the surrounding text, correcting for font-size differences.
|
|
20258
|
+
*
|
|
20259
|
+
* Place this component around any inline element whose font-size differs from the surrounding text.
|
|
20260
|
+
* It renders an invisible anchor that inherits the surrounding text's font metrics, then shifts
|
|
20261
|
+
* the child so that its visual position matches the requested `textAnchor` value — regardless of
|
|
20262
|
+
* font-size, display type (inline, inline-block, inline-flex…), or the active `vertical-align`.
|
|
20263
|
+
*
|
|
20264
|
+
* @param {"line-top"|"char-top"|"center"|"char-bottom"|"line-bottom"} [textAnchor="char-bottom"]
|
|
20265
|
+
* - `"line-top"` — child top aligns with the top of the surrounding line box
|
|
20266
|
+
* - `"char-top"` — child top aligns with the top of visible characters (ink ascent)
|
|
20267
|
+
* - `"center"` — child is vertically centered on the surrounding line box
|
|
20268
|
+
* - `"char-bottom"` — child bottom aligns to the text baseline (no correction, browser default)
|
|
20269
|
+
* - `"line-bottom"` — child bottom aligns with the bottom of the surrounding line box
|
|
20270
|
+
* @param {{ size?: number, verticalAlign?: string }} [lineLayout]
|
|
20271
|
+
* Describes the surrounding line context. Used as layout-effect dependencies so the correction
|
|
20272
|
+
* reruns when the surrounding text's font-size or vertical-align changes.
|
|
20273
|
+
* @param {import("ignore:preact").RefObject} childRef — ref on the child element to reposition
|
|
20274
|
+
*/
|
|
20275
|
+
const TextAnchor = ({
|
|
20276
|
+
childRef,
|
|
20277
|
+
children,
|
|
20278
|
+
textAnchor = "char-bottom",
|
|
20279
|
+
textKey,
|
|
20280
|
+
textSize,
|
|
20281
|
+
lineLayout
|
|
20282
|
+
}) => {
|
|
20283
|
+
import.meta.css = [css$$, "@jsenv/navi/src/text/text_anchor.jsx"];
|
|
20284
|
+
const anchorRef = useRef();
|
|
20285
|
+
|
|
20286
|
+
// Plain useLayoutEffect would also fire while an ancestor dialog/popover
|
|
20287
|
+
// (e.g. a closed SidePanel) is still display:none — every rect involved
|
|
20288
|
+
// reads 0×0 at that point, so the math trivially (and wrongly) resolves
|
|
20289
|
+
// to topOffset 0: not a real "no correction needed" result, just a
|
|
20290
|
+
// zero-by-zero coincidence that happens to look fine only because it
|
|
20291
|
+
// leaves the browser's own default alignment untouched. The real
|
|
20292
|
+
// correction then only gets applied later, on whatever unrelated
|
|
20293
|
+
// re-render next happens to change one of this effect's own deps —
|
|
20294
|
+
// which reads as the child "jumping" even though nothing about its own
|
|
20295
|
+
// geometry changed. useDisplayedLayoutEffect skips the initial run in
|
|
20296
|
+
// that case and reruns once the ancestor actually opens instead.
|
|
20297
|
+
useDisplayedLayoutEffect(anchorRef, anchorEl => {
|
|
20298
|
+
const childEl = childRef.current;
|
|
20299
|
+
if (!anchorEl || !childEl) {
|
|
20300
|
+
return;
|
|
20301
|
+
}
|
|
20302
|
+
// Only correct when the anchor lives in an inline formatting context.
|
|
20303
|
+
// If the parent is a flex/grid container, inline layout rules don't apply
|
|
20304
|
+
// and our font-metrics model is invalid.
|
|
20305
|
+
const parentDisplay = getComputedStyle(anchorEl.parentElement).display;
|
|
20306
|
+
if (parentDisplay !== "inline" && parentDisplay !== "inline-block" && parentDisplay !== "block") {
|
|
20307
|
+
// we must hide the anchor otherwise it would affect layout without providing any benefit (would trigger flex gap for instance)
|
|
20308
|
+
anchorEl.setAttribute("hidden", "");
|
|
20309
|
+
setTopOffset(childEl, 0);
|
|
20310
|
+
return;
|
|
20311
|
+
}
|
|
20312
|
+
anchorEl.removeAttribute("hidden");
|
|
20313
|
+
const topOffset = computeTopOffset({
|
|
20314
|
+
anchorEl,
|
|
20315
|
+
childEl,
|
|
20316
|
+
textAnchor
|
|
20317
|
+
});
|
|
20318
|
+
setTopOffset(childEl, topOffset);
|
|
20319
|
+
}, [textAnchor, textKey, textSize, lineLayout?.size, lineLayout?.verticalAlign]);
|
|
20320
|
+
return jsxs(Fragment$1, {
|
|
20321
|
+
children: [children, jsx("span", {
|
|
20322
|
+
ref: anchorRef,
|
|
20323
|
+
className: "navi_text_anchor",
|
|
20324
|
+
"aria-hidden": "true",
|
|
20325
|
+
children: "\u200B"
|
|
20326
|
+
})]
|
|
20327
|
+
});
|
|
20328
|
+
};
|
|
20329
|
+
const setTopOffset = (childEl, topOffset) => {
|
|
20330
|
+
// position:relative + top shifts the element visually.
|
|
20331
|
+
// marginTop: -topOffset makes the layout box follow the visual position, so any container
|
|
20332
|
+
// (button, link, box…) computes its own padding/border/height based on the real final position
|
|
20333
|
+
// rather than the original unshifted one. This means a badge inside a button will symmetrically
|
|
20334
|
+
// expand the button height instead of overflowing or being clipped.
|
|
20335
|
+
// marginBottom: topOffset compensates the marginTop so the line height stays unchanged —
|
|
20336
|
+
// the shift is purely a repositioning, not an inflation of the line.
|
|
20337
|
+
if (!topOffset) {
|
|
20338
|
+
childEl.style.position = "";
|
|
20339
|
+
childEl.style.top = "";
|
|
20340
|
+
childEl.style.marginTop = "";
|
|
20341
|
+
childEl.style.marginBottom = "";
|
|
20342
|
+
return;
|
|
20343
|
+
}
|
|
20344
|
+
childEl.style.position = "relative";
|
|
20345
|
+
childEl.style.top = `${topOffset}px`;
|
|
20346
|
+
childEl.style.marginTop = `${-topOffset}px`;
|
|
20347
|
+
childEl.style.marginBottom = `${topOffset}px`;
|
|
20348
|
+
};
|
|
20349
|
+
const computeTopOffset = ({
|
|
20350
|
+
anchorEl,
|
|
20351
|
+
childEl,
|
|
20352
|
+
textAnchor
|
|
20353
|
+
}) => {
|
|
20354
|
+
if (textAnchor === "char-bottom") {
|
|
20355
|
+
// Align child's bottom with the char's bottom = the baseline.
|
|
20356
|
+
// The CSS spec says an inline-block with no text content has its baseline at its bottom margin edge.
|
|
20357
|
+
// So the browser's default placement already puts the child's bottom at the line's baseline.
|
|
20358
|
+
// No correction needed.
|
|
20359
|
+
return 0;
|
|
20360
|
+
}
|
|
20361
|
+
// The anchor's rendered rect corresponds to the surrounding text's line box:
|
|
20362
|
+
// top and bottom are the visual bounds of the line (including line-height).
|
|
20363
|
+
const anchorRect = anchorEl.getBoundingClientRect();
|
|
20364
|
+
|
|
20365
|
+
// Measure the child's current rect, then subtract any previously applied top correction
|
|
20366
|
+
// to recover its natural position — avoiding a style reset + reflow.
|
|
20367
|
+
const childRect = childEl.getBoundingClientRect();
|
|
20368
|
+
const childH = childRect.height;
|
|
20369
|
+
const previousTop = parseFloat(childEl.style.top) || 0;
|
|
20370
|
+
const childNaturalTop = childRect.top - previousTop;
|
|
20371
|
+
|
|
20372
|
+
// Compute desired child top Y based on textAnchor intention.
|
|
20373
|
+
let desiredChildTopY = 0;
|
|
20374
|
+
if (textAnchor === "line-top") {
|
|
20375
|
+
desiredChildTopY = anchorRect.top;
|
|
20376
|
+
} else if (textAnchor === "char-top") {
|
|
20377
|
+
const anchorStyle = getComputedStyle(anchorEl);
|
|
20378
|
+
const ctx = charTopCanvas.getContext("2d");
|
|
20379
|
+
ctx.font = `${anchorStyle.fontWeight} ${anchorStyle.fontSize} ${anchorStyle.fontFamily}`;
|
|
20380
|
+
const m = ctx.measureText("M");
|
|
20381
|
+
const baselineY = anchorRect.bottom - m.fontBoundingBoxDescent;
|
|
20382
|
+
desiredChildTopY = baselineY - m.actualBoundingBoxAscent;
|
|
20383
|
+
} else if (textAnchor === "center") {
|
|
20384
|
+
const anchorCenterY = (anchorRect.top + anchorRect.bottom) / 2;
|
|
20385
|
+
desiredChildTopY = anchorCenterY - childH / 2;
|
|
20386
|
+
} else if (textAnchor === "char-bottom") {
|
|
20387
|
+
// Already handled above (early return 0), but guard here for completeness.
|
|
20388
|
+
return 0;
|
|
20389
|
+
} else if (textAnchor === "line-bottom") {
|
|
20390
|
+
desiredChildTopY = anchorRect.bottom - childH;
|
|
20391
|
+
} else {
|
|
20392
|
+
return 0; // unknown textAnchor, no correction
|
|
20393
|
+
}
|
|
20394
|
+
return desiredChildTopY - childNaturalTop;
|
|
20395
|
+
};
|
|
20396
|
+
const charTopCanvas = document.createElement("canvas");
|
|
20397
|
+
|
|
19886
20398
|
const useInitialTextSelection = (ref, textSelection) => {
|
|
19887
20399
|
const deps = [];
|
|
19888
20400
|
if (Array.isArray(textSelection)) {
|
|
@@ -19983,7 +20495,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
|
|
|
19983
20495
|
};
|
|
19984
20496
|
|
|
19985
20497
|
installImportMetaCssBuild(import.meta);// https://jsfiddle.net/v5xzJ/4/
|
|
19986
|
-
const css$
|
|
20498
|
+
const css$_ = /* css */`
|
|
19987
20499
|
@layer navi {
|
|
19988
20500
|
.navi_text {
|
|
19989
20501
|
&[data-skeleton] {
|
|
@@ -20167,6 +20679,91 @@ const css$10 = /* css */`
|
|
|
20167
20679
|
opacity: 1;
|
|
20168
20680
|
}
|
|
20169
20681
|
}
|
|
20682
|
+
|
|
20683
|
+
/* ── Icon ── */
|
|
20684
|
+
|
|
20685
|
+
@layer navi {
|
|
20686
|
+
/* Ensure data attributes from box.jsx can win to update display */
|
|
20687
|
+
.navi_icon {
|
|
20688
|
+
display: inline-flex;
|
|
20689
|
+
box-sizing: border-box;
|
|
20690
|
+
max-width: 100%;
|
|
20691
|
+
/* An icon never grows past the box it sits in, so a glyph can never make
|
|
20692
|
+
a line of text taller than the text itself. lineOverflow="allow" opts
|
|
20693
|
+
out, for an icon that is an affordance rather than a character — a
|
|
20694
|
+
control's chevron or clear button, sized to be touched, not read. */
|
|
20695
|
+
max-height: 100%;
|
|
20696
|
+
|
|
20697
|
+
&[data-line-overflow="allow"] {
|
|
20698
|
+
max-height: none;
|
|
20699
|
+
}
|
|
20700
|
+
}
|
|
20701
|
+
}
|
|
20702
|
+
|
|
20703
|
+
.navi_icon {
|
|
20704
|
+
white-space: nowrap;
|
|
20705
|
+
vertical-align: inherit;
|
|
20706
|
+
|
|
20707
|
+
&[data-icon-char] {
|
|
20708
|
+
aspect-ratio: 1/1;
|
|
20709
|
+
min-width: 0;
|
|
20710
|
+
height: round(1em, 1px);
|
|
20711
|
+
max-height: round(1em, 1px);
|
|
20712
|
+
flex-grow: 0 !important;
|
|
20713
|
+
align-items: center;
|
|
20714
|
+
justify-content: center;
|
|
20715
|
+
|
|
20716
|
+
/* fillLine: measured on the line box (1lh) instead of the character box
|
|
20717
|
+
(1em). The icon still stays inside the line — it just uses all of it,
|
|
20718
|
+
which is what an icon standing on its own in a control's slot wants,
|
|
20719
|
+
where a glyph sitting among letters wants to match their size. */
|
|
20720
|
+
&[data-fill-line] {
|
|
20721
|
+
height: round(1lh, 1px);
|
|
20722
|
+
max-height: round(1lh, 1px);
|
|
20723
|
+
}
|
|
20724
|
+
|
|
20725
|
+
svg,
|
|
20726
|
+
img {
|
|
20727
|
+
width: 100%;
|
|
20728
|
+
height: 100%;
|
|
20729
|
+
}
|
|
20730
|
+
svg {
|
|
20731
|
+
overflow: visible;
|
|
20732
|
+
}
|
|
20733
|
+
}
|
|
20734
|
+
&[data-flow-inline] {
|
|
20735
|
+
width: 1em;
|
|
20736
|
+
height: 1em;
|
|
20737
|
+
}
|
|
20738
|
+
&[data-interactive] {
|
|
20739
|
+
cursor: pointer;
|
|
20740
|
+
}
|
|
20741
|
+
&[data-icon-text] {
|
|
20742
|
+
-webkit-font-smoothing: antialiased;
|
|
20743
|
+
text-rendering: optimizeLegibility;
|
|
20744
|
+
}
|
|
20745
|
+
}
|
|
20746
|
+
|
|
20747
|
+
.navi_icon > svg,
|
|
20748
|
+
.navi_icon > img {
|
|
20749
|
+
width: 100%;
|
|
20750
|
+
height: 100%;
|
|
20751
|
+
}
|
|
20752
|
+
.navi_icon[data-width-fixed] > svg,
|
|
20753
|
+
.navi_icon[data-width-fixed] > img {
|
|
20754
|
+
width: 100%;
|
|
20755
|
+
height: auto;
|
|
20756
|
+
}
|
|
20757
|
+
.navi_icon[data-height-fixed] > svg,
|
|
20758
|
+
.navi_icon[data-height-fixed] > img {
|
|
20759
|
+
width: auto;
|
|
20760
|
+
height: 100%;
|
|
20761
|
+
}
|
|
20762
|
+
.navi_icon[data-width-fixed][data-height-fixed] > svg,
|
|
20763
|
+
.navi_icon[data-width-fixed][data-height-fixed] > img {
|
|
20764
|
+
width: 100%;
|
|
20765
|
+
height: 100%;
|
|
20766
|
+
}
|
|
20170
20767
|
`;
|
|
20171
20768
|
const REGULAR_SPACE = jsx("span", {
|
|
20172
20769
|
"data-navi-space": "",
|
|
@@ -20349,6 +20946,7 @@ const shouldInjectSpacingBetween = (left, right) => {
|
|
|
20349
20946
|
* loading?: boolean,
|
|
20350
20947
|
* skeleton?: boolean,
|
|
20351
20948
|
* attachLastChild?: boolean,
|
|
20949
|
+
* emojiAsIcon?: boolean,
|
|
20352
20950
|
* preventSpaceUnderlines?: boolean,
|
|
20353
20951
|
* holdSpaceForStyle?: import("ignore:preact").JSX.CSSProperties,
|
|
20354
20952
|
* boldStable?: boolean,
|
|
@@ -20388,6 +20986,12 @@ const shouldInjectSpacingBetween = (left, right) => {
|
|
|
20388
20986
|
* truncation belongs outside the `Text` instead (see `docs/typography.md`).
|
|
20389
20987
|
* `Link` sets it on its own whenever it renders an end icon.
|
|
20390
20988
|
*
|
|
20989
|
+
* @param {boolean} [emojiAsIcon]
|
|
20990
|
+
* Renders every emoji found in the string children as an `Icon`, so it sits
|
|
20991
|
+
* in the line like a character and never makes the line taller than the
|
|
20992
|
+
* text. For free text a user typed (a message, a description) — the only
|
|
20993
|
+
* place an emoji is expected; see `docs/typography.md`.
|
|
20994
|
+
*
|
|
20391
20995
|
* @param {boolean} [preventSpaceUnderlines]
|
|
20392
20996
|
* Replaces real space characters between children with padding-based spaces.
|
|
20393
20997
|
* Useful inside `<a>` elements where browsers draw an underline under spaces.
|
|
@@ -20421,14 +21025,6 @@ const shouldInjectSpacingBetween = (left, right) => {
|
|
|
20421
21025
|
* internally for overlays such as the skeleton container.
|
|
20422
21026
|
*/
|
|
20423
21027
|
const Text = props => {
|
|
20424
|
-
const defaultRef = useRef();
|
|
20425
|
-
const ref = props.ref || defaultRef;
|
|
20426
|
-
return jsx(TextDispatcher, {
|
|
20427
|
-
...props,
|
|
20428
|
-
ref: ref
|
|
20429
|
-
});
|
|
20430
|
-
};
|
|
20431
|
-
const TextDispatcher = props => {
|
|
20432
21028
|
if (props.loading || props.skeleton) {
|
|
20433
21029
|
return jsx(TextSkeleton, {
|
|
20434
21030
|
...props
|
|
@@ -20454,9 +21050,8 @@ const TextDispatcher = props => {
|
|
|
20454
21050
|
});
|
|
20455
21051
|
};
|
|
20456
21052
|
const TextShrinkWrap = props => {
|
|
20457
|
-
const
|
|
20458
|
-
|
|
20459
|
-
} = props;
|
|
21053
|
+
const defaultRef = useRef();
|
|
21054
|
+
const ref = props.ref || defaultRef;
|
|
20460
21055
|
const applyWidth = () => {
|
|
20461
21056
|
const text = ref.current;
|
|
20462
21057
|
// Reset any previously forced width so we measure the natural size
|
|
@@ -20497,19 +21092,21 @@ const TextShrinkWrap = props => {
|
|
|
20497
21092
|
window.removeEventListener("resize", applyWidth);
|
|
20498
21093
|
};
|
|
20499
21094
|
}, []);
|
|
20500
|
-
return jsx(
|
|
21095
|
+
return jsx(Text, {
|
|
20501
21096
|
...props,
|
|
21097
|
+
ref: ref,
|
|
20502
21098
|
"data-shrinkwrap": "",
|
|
20503
21099
|
shrinkWrap: undefined
|
|
20504
21100
|
});
|
|
20505
21101
|
};
|
|
20506
21102
|
const TextUI = props => {
|
|
20507
|
-
import.meta.css = [css$
|
|
21103
|
+
import.meta.css = [css$_, "@jsenv/navi/src/text/text.jsx"];
|
|
20508
21104
|
let {
|
|
20509
21105
|
ref,
|
|
20510
21106
|
spacing,
|
|
20511
21107
|
preventSpaceUnderlines = false,
|
|
20512
21108
|
attachLastChild = false,
|
|
21109
|
+
emojiAsIcon = false,
|
|
20513
21110
|
boldStable,
|
|
20514
21111
|
holdSpaceForStyle,
|
|
20515
21112
|
capitalize,
|
|
@@ -20540,6 +21137,11 @@ const TextUI = props => {
|
|
|
20540
21137
|
} else {
|
|
20541
21138
|
children = applySpacingOnTextChildren(children, resolvedSpacing, defaultSpace);
|
|
20542
21139
|
}
|
|
21140
|
+
if (emojiAsIcon) {
|
|
21141
|
+
// After the spacing pass: an emoji glued to a word ("hello👋") must not
|
|
21142
|
+
// get a separator injected as if it were a child element.
|
|
21143
|
+
children = renderEmojiAsIcon(children);
|
|
21144
|
+
}
|
|
20543
21145
|
if (boldStable) {
|
|
20544
21146
|
const {
|
|
20545
21147
|
bold
|
|
@@ -20608,7 +21210,7 @@ const TextSkeleton = ({
|
|
|
20608
21210
|
"aria-hidden": "true",
|
|
20609
21211
|
children: "W"
|
|
20610
21212
|
});
|
|
20611
|
-
return jsx(
|
|
21213
|
+
return jsx(Text, {
|
|
20612
21214
|
"data-skeleton": "",
|
|
20613
21215
|
"data-loading": loading ? "" : undefined,
|
|
20614
21216
|
...props,
|
|
@@ -20624,7 +21226,7 @@ const TextOverflow = ({
|
|
|
20624
21226
|
children,
|
|
20625
21227
|
...rest
|
|
20626
21228
|
}) => {
|
|
20627
|
-
return jsx(
|
|
21229
|
+
return jsx(Text, {
|
|
20628
21230
|
block: true,
|
|
20629
21231
|
as: "div",
|
|
20630
21232
|
pre: noWrap === undefined ? true : undefined
|
|
@@ -20646,14 +21248,254 @@ const TextWithSelectRange = ({
|
|
|
20646
21248
|
selectRange,
|
|
20647
21249
|
...props
|
|
20648
21250
|
}) => {
|
|
20649
|
-
|
|
20650
|
-
|
|
21251
|
+
const defaultRef = useRef();
|
|
21252
|
+
const innerRef = ref || defaultRef;
|
|
21253
|
+
useInitialTextSelection(innerRef, selectRange);
|
|
21254
|
+
return jsx(Text, {
|
|
20651
21255
|
...props,
|
|
20652
|
-
ref:
|
|
21256
|
+
ref: innerRef,
|
|
20653
21257
|
selectRange: undefined
|
|
20654
21258
|
});
|
|
20655
21259
|
};
|
|
20656
21260
|
|
|
21261
|
+
/**
|
|
21262
|
+
* Renders an icon — an inline SVG/emoji/text glyph that inherits the
|
|
21263
|
+
* surrounding text's `currentColor` and (by default) its font size, so it sits
|
|
21264
|
+
* on the text baseline like a character.
|
|
21265
|
+
*
|
|
21266
|
+
* Content comes from either `href` (references an external/sprite symbol via
|
|
21267
|
+
* `<use>`) or `children` (an inline `<svg>` element, or a string for a
|
|
21268
|
+
* text/emoji icon). All extra props are spread onto the underlying `Box`/`Text`
|
|
21269
|
+
* (sizing, spacing, color, className, data-attributes, …).
|
|
21270
|
+
*
|
|
21271
|
+
* Render mode is chosen automatically:
|
|
21272
|
+
* - `children` is a **string** → a text icon (`<Text data-icon-text>`).
|
|
21273
|
+
* - **sized** (an explicit `width`/`height`, or `flex`/`grid`) → a block icon
|
|
21274
|
+
* (`<Box square>`), laid out as its own box rather than inline.
|
|
21275
|
+
* - otherwise → an **inline char-like** icon that flows on the text baseline
|
|
21276
|
+
* (`data-icon-char`), aligned via `textAnchor`.
|
|
21277
|
+
*
|
|
21278
|
+
* Accessibility: an icon is treated as decorative (`aria-hidden`) by default
|
|
21279
|
+
* whenever it has no explicit size and no `onClick`; give it an explicit
|
|
21280
|
+
* `decorative={false}` (or make it interactive) when it conveys meaning.
|
|
21281
|
+
*
|
|
21282
|
+
* @param {object} props
|
|
21283
|
+
* @param {string} [props.href] - URL/id of an external SVG symbol, rendered via
|
|
21284
|
+
* `<svg><use href></svg>`. Mutually exclusive with meaningful `children`.
|
|
21285
|
+
* @param {import("ignore:preact").ComponentChildren} [props.children] - Inline icon
|
|
21286
|
+
* content: an `<svg>` element, or a string (renders as a text/emoji icon).
|
|
21287
|
+
* @param {boolean} [props.decorative] - Marks the icon `aria-hidden`. Defaults
|
|
21288
|
+
* to `true` for an unsized, non-interactive icon; pass `false` for a
|
|
21289
|
+
* meaning-bearing icon that needs to be exposed to assistive tech.
|
|
21290
|
+
* @param {(event: MouseEvent) => void} [props.onClick] - Makes the icon
|
|
21291
|
+
* interactive (`data-interactive`, pointer cursor) and non-decorative.
|
|
21292
|
+
* @param {"line-top"|"char-top"|"center"|"char-bottom"|"line-bottom"} [props.textAnchor="center"]
|
|
21293
|
+
* - Vertical alignment within the surrounding text line for the inline
|
|
21294
|
+
* char-like mode, forwarded to `TextAnchor`: `"line-top"`/`"line-bottom"`
|
|
21295
|
+
* align to the line box edges, `"char-top"` to the ink ascent, `"center"`
|
|
21296
|
+
* centers on the line box, `"char-bottom"` sits on the baseline. See
|
|
21297
|
+
* `text_anchor.jsx`.
|
|
21298
|
+
* @param {{ size?: number, verticalAlign?: string }} [props.lineLayout] -
|
|
21299
|
+
* Describes the surrounding line context (font size / vertical-align),
|
|
21300
|
+
* forwarded to `TextAnchor` so it recomputes the vertical correction when
|
|
21301
|
+
* that context changes.
|
|
21302
|
+
* @param {string|number} [props.width] - Explicit width; `"auto"` clears it.
|
|
21303
|
+
* Any explicit size switches the icon to block (sized) mode.
|
|
21304
|
+
* @param {string|number} [props.height] - Explicit height; `"auto"` clears it.
|
|
21305
|
+
* @param {"allow"} [props.lineOverflow] - `"allow"` lets the icon be taller
|
|
21306
|
+
* than the box it sits in (a line of text, a control's slot) instead of being
|
|
21307
|
+
* capped by it. For an icon that is an affordance sized for the finger rather
|
|
21308
|
+
* than a character sized for reading.
|
|
21309
|
+
* @param {boolean} [props.fillLine] - Sizes the icon on the line box (1lh)
|
|
21310
|
+
* rather than on the character box (1em), so it uses the full height of the
|
|
21311
|
+
* line without leaving it. Unlike `lineOverflow`, the icon still never
|
|
21312
|
+
* exceeds the line.
|
|
21313
|
+
* @param {boolean} [props.square] - Keep a 1:1 box; combined with one explicit
|
|
21314
|
+
* dimension it fixes the other too.
|
|
21315
|
+
* @param {boolean} [props.circle] - Like `square`, plus a circular shape.
|
|
21316
|
+
* @param {string|number} [props.aspectRatio] - Fixes the second dimension from
|
|
21317
|
+
* the one explicit dimension.
|
|
21318
|
+
* @param {"x"|"y"|boolean} [props.flex] - Forces block/flex layout; auto-set to
|
|
21319
|
+
* `"x"` when the icon is sized.
|
|
21320
|
+
* @param {boolean} [props.grid] - Forces block/grid layout.
|
|
21321
|
+
* @param {string} [props.className] - Merged with the base `"navi_icon"` class.
|
|
21322
|
+
*/
|
|
21323
|
+
const Icon = ({
|
|
21324
|
+
href,
|
|
21325
|
+
children,
|
|
21326
|
+
decorative,
|
|
21327
|
+
onClick,
|
|
21328
|
+
textAnchor = "center",
|
|
21329
|
+
lineLayout,
|
|
21330
|
+
lineOverflow,
|
|
21331
|
+
fillLine,
|
|
21332
|
+
...props
|
|
21333
|
+
}) => {
|
|
21334
|
+
import.meta.css = [css$_, "@jsenv/navi/src/text/text.jsx"];
|
|
21335
|
+
const innerChildren = href ? jsx("svg", {
|
|
21336
|
+
width: "100%",
|
|
21337
|
+
height: "100%",
|
|
21338
|
+
children: jsx("use", {
|
|
21339
|
+
href: href
|
|
21340
|
+
})
|
|
21341
|
+
}) : children;
|
|
21342
|
+
let {
|
|
21343
|
+
flex,
|
|
21344
|
+
grid,
|
|
21345
|
+
width,
|
|
21346
|
+
height
|
|
21347
|
+
} = props;
|
|
21348
|
+
if (width === "auto") {
|
|
21349
|
+
width = undefined;
|
|
21350
|
+
}
|
|
21351
|
+
if (height === "auto") {
|
|
21352
|
+
height = undefined;
|
|
21353
|
+
}
|
|
21354
|
+
const hasExplicitWidth = width !== undefined;
|
|
21355
|
+
const hasExplicitHeight = height !== undefined;
|
|
21356
|
+
const widthFixed = hasExplicitWidth || hasExplicitHeight && (props.square || props.circle || props.aspectRatio);
|
|
21357
|
+
const heightFixed = hasExplicitHeight || hasExplicitWidth && (props.square || props.circle || props.aspectRatio);
|
|
21358
|
+
if (widthFixed || heightFixed) {
|
|
21359
|
+
if (flex === undefined) {
|
|
21360
|
+
flex = "x";
|
|
21361
|
+
}
|
|
21362
|
+
} else if (decorative === undefined && !onClick) {
|
|
21363
|
+
decorative = true;
|
|
21364
|
+
}
|
|
21365
|
+
const ariaProps = decorative ? {
|
|
21366
|
+
"aria-hidden": "true"
|
|
21367
|
+
} : {};
|
|
21368
|
+
const textRef = useRef();
|
|
21369
|
+
if (typeof children === "string") {
|
|
21370
|
+
return jsx(Text, {
|
|
21371
|
+
...props,
|
|
21372
|
+
...ariaProps,
|
|
21373
|
+
"data-icon-text": "",
|
|
21374
|
+
"data-line-overflow": lineOverflow,
|
|
21375
|
+
"data-fill-line": fillLine ? "" : undefined,
|
|
21376
|
+
children: children
|
|
21377
|
+
});
|
|
21378
|
+
}
|
|
21379
|
+
if (flex || grid) {
|
|
21380
|
+
return jsx(Box, {
|
|
21381
|
+
square: true,
|
|
21382
|
+
...props,
|
|
21383
|
+
...ariaProps,
|
|
21384
|
+
flex: flex,
|
|
21385
|
+
baseClassName: "navi_icon",
|
|
21386
|
+
"data-width-fixed": widthFixed ? "" : undefined,
|
|
21387
|
+
"data-height-fixed": heightFixed ? "" : undefined,
|
|
21388
|
+
"data-interactive": onClick ? "" : undefined,
|
|
21389
|
+
"data-line-overflow": lineOverflow,
|
|
21390
|
+
"data-fill-line": fillLine ? "" : undefined,
|
|
21391
|
+
onClick: onClick,
|
|
21392
|
+
children: innerChildren
|
|
21393
|
+
});
|
|
21394
|
+
}
|
|
21395
|
+
return jsx(TextAnchor, {
|
|
21396
|
+
childRef: textRef,
|
|
21397
|
+
textAnchor: textAnchor,
|
|
21398
|
+
textSize: props.size,
|
|
21399
|
+
lineLayout: lineLayout,
|
|
21400
|
+
children: jsxs(Text, {
|
|
21401
|
+
...props,
|
|
21402
|
+
...ariaProps,
|
|
21403
|
+
className: withPropsClassName("navi_icon", props.className),
|
|
21404
|
+
spacing: "pre",
|
|
21405
|
+
"data-icon-char": "",
|
|
21406
|
+
"data-line-overflow": lineOverflow,
|
|
21407
|
+
"data-fill-line": fillLine ? "" : undefined,
|
|
21408
|
+
"data-width-fixed": widthFixed ? "" : undefined,
|
|
21409
|
+
"data-height-fixed": heightFixed ? "" : undefined,
|
|
21410
|
+
"data-interactive": onClick ? "" : undefined,
|
|
21411
|
+
onClick: onClick,
|
|
21412
|
+
ref: textRef,
|
|
21413
|
+
children: [jsx("span", {
|
|
21414
|
+
style: "user-select:none",
|
|
21415
|
+
children: "\u200B"
|
|
21416
|
+
}), innerChildren]
|
|
21417
|
+
})
|
|
21418
|
+
});
|
|
21419
|
+
};
|
|
21420
|
+
|
|
21421
|
+
// An emoji-presentation character (🌸), or a pictogram forced into emoji
|
|
21422
|
+
// presentation by VS16 (❤️), followed by its skin-tone modifiers and ZWJ
|
|
21423
|
+
// sequence. A flag is two regional indicators that must stay together.
|
|
21424
|
+
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;
|
|
21425
|
+
|
|
21426
|
+
/**
|
|
21427
|
+
* What `emojiAsIcon` does, for something that renders text without going
|
|
21428
|
+
* through `Text` (a callout's message): every emoji in the string children
|
|
21429
|
+
* comes back wrapped in an `Icon`. The system emoji fonts have a taller
|
|
21430
|
+
* ascent/descent than text fonts, so a raw emoji glyph makes its line taller
|
|
21431
|
+
* than the lines around it; inside an Icon it is capped at 1em and centered on
|
|
21432
|
+
* the line like any glyph icon.
|
|
21433
|
+
*
|
|
21434
|
+
* Children come back in the shape they arrived in: a string stays a string
|
|
21435
|
+
* when it holds no emoji, and the array is only built once a child actually
|
|
21436
|
+
* needs rewriting.
|
|
21437
|
+
*/
|
|
21438
|
+
const renderEmojiAsIcon = children => {
|
|
21439
|
+
if (typeof children === "string") {
|
|
21440
|
+
return renderEmojiInString(children);
|
|
21441
|
+
}
|
|
21442
|
+
const childArray = toChildArray(children);
|
|
21443
|
+
let result = null;
|
|
21444
|
+
let index = 0;
|
|
21445
|
+
for (const child of childArray) {
|
|
21446
|
+
if (typeof child === "string") {
|
|
21447
|
+
const rendered = renderEmojiInString(child);
|
|
21448
|
+
if (rendered !== child) {
|
|
21449
|
+
if (result === null) {
|
|
21450
|
+
result = childArray.slice(0, index);
|
|
21451
|
+
}
|
|
21452
|
+
for (const part of rendered) {
|
|
21453
|
+
result.push(part);
|
|
21454
|
+
}
|
|
21455
|
+
index++;
|
|
21456
|
+
continue;
|
|
21457
|
+
}
|
|
21458
|
+
}
|
|
21459
|
+
if (result !== null) {
|
|
21460
|
+
result.push(child);
|
|
21461
|
+
}
|
|
21462
|
+
index++;
|
|
21463
|
+
}
|
|
21464
|
+
if (result === null) {
|
|
21465
|
+
return children;
|
|
21466
|
+
}
|
|
21467
|
+
return result;
|
|
21468
|
+
};
|
|
21469
|
+
const renderEmojiInString = string => {
|
|
21470
|
+
let parts = null;
|
|
21471
|
+
let lastIndex = 0;
|
|
21472
|
+
for (const match of string.matchAll(EMOJI_REGEX)) {
|
|
21473
|
+
if (parts === null) {
|
|
21474
|
+
parts = [];
|
|
21475
|
+
}
|
|
21476
|
+
if (match.index > lastIndex) {
|
|
21477
|
+
parts.push(string.slice(lastIndex, match.index));
|
|
21478
|
+
}
|
|
21479
|
+
parts.push(jsx(Icon, {
|
|
21480
|
+
decorative: false,
|
|
21481
|
+
children: jsx("span", {
|
|
21482
|
+
children: match[0]
|
|
21483
|
+
})
|
|
21484
|
+
}));
|
|
21485
|
+
lastIndex = match.index + match[0].length;
|
|
21486
|
+
}
|
|
21487
|
+
if (parts === null) {
|
|
21488
|
+
return string;
|
|
21489
|
+
}
|
|
21490
|
+
if (lastIndex < string.length) {
|
|
21491
|
+
parts.push(string.slice(lastIndex));
|
|
21492
|
+
}
|
|
21493
|
+
return parts;
|
|
21494
|
+
};
|
|
21495
|
+
// A callout message is free text like any other; see callout.jsx for why it
|
|
21496
|
+
// cannot import this itself.
|
|
21497
|
+
setCalloutMessageTextRenderer(renderEmojiAsIcon);
|
|
21498
|
+
|
|
20657
21499
|
const LIGHT_ACCENT_ATTRIBUTE = "data-accent-light";
|
|
20658
21500
|
const VERY_LIGHT_ACCENT_ATTRIBUTE = "data-accent-very-light";
|
|
20659
21501
|
const DARK_CONTRAST_ATTRIBUTE = "data-accent-needs-dark-fg";
|
|
@@ -20797,105 +21639,6 @@ const useActionStatus = (action) => {
|
|
|
20797
21639
|
};
|
|
20798
21640
|
};
|
|
20799
21641
|
|
|
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
21642
|
/**
|
|
20900
21643
|
* Decides which element receives focus when a container (popover, dialog, …)
|
|
20901
21644
|
* opens, and gives it back to where it came from when the container closes.
|
|
@@ -27435,13 +28178,11 @@ const useInteractiveProps = (props, {
|
|
|
27435
28178
|
const [controlRootProps, controlHostProps] = splitControlProps(props);
|
|
27436
28179
|
controlRootProps["navi-control"] = controlInfo.controlType;
|
|
27437
28180
|
if (props.ownTarget) {
|
|
27438
|
-
//
|
|
27439
|
-
//
|
|
27440
|
-
//
|
|
27441
|
-
//
|
|
27442
|
-
controlRootProps[OWN_TARGET_ATTRIBUTE] = "";
|
|
27443
|
-
controlRootProps["data-no-drag-travel"] = "";
|
|
27444
|
-
controlRootProps["data-drag-ignore"] = "";
|
|
28181
|
+
// One attribute, in the DOM, because that is where the claim is read from —
|
|
28182
|
+
// by the controls above and by the gesture readers below (see
|
|
28183
|
+
// own_target.js). The prop is the ergonomic form of it and nothing more: an
|
|
28184
|
+
// element an application draws itself writes the same attribute by hand.
|
|
28185
|
+
controlRootProps[OWN_TARGET_ATTRIBUTE] = typeof props.ownTarget === "string" ? props.ownTarget : "";
|
|
27445
28186
|
}
|
|
27446
28187
|
const {
|
|
27447
28188
|
"navi-control-proxy-for": naviProxyFor
|
|
@@ -27477,10 +28218,10 @@ const useInteractiveProps = (props, {
|
|
|
27477
28218
|
});
|
|
27478
28219
|
}
|
|
27479
28220
|
{
|
|
27480
|
-
const
|
|
27481
|
-
const
|
|
28221
|
+
const controlDisabledFromAbove = useContext(DisabledContext);
|
|
28222
|
+
const controlReadOnlyFromAbove = useContext(ReadOnlyContext);
|
|
27482
28223
|
const controlRequired = useContext(RequiredContext);
|
|
27483
|
-
const
|
|
28224
|
+
const controlLoadingFromAbove = useContext(LoadingContext$1);
|
|
27484
28225
|
const parentActionRequester = useContext(ActionRequesterContext);
|
|
27485
28226
|
const actionStatus = useActionStatus(boundAction);
|
|
27486
28227
|
const {
|
|
@@ -27490,6 +28231,16 @@ const useInteractiveProps = (props, {
|
|
|
27490
28231
|
loading,
|
|
27491
28232
|
optimistic
|
|
27492
28233
|
} = props;
|
|
28234
|
+
|
|
28235
|
+
// `ownTarget="always"`: an affordance that writes nothing to the control it
|
|
28236
|
+
// sits in has no business inheriting that control's state — a diskette
|
|
28237
|
+
// saving a row into the reader's own address book stays pressable on a game
|
|
28238
|
+
// nobody may edit. Its own props still hold; only what came from above is
|
|
28239
|
+
// dropped (see own_target.js for the three modes).
|
|
28240
|
+
const zoneStateApplies = !ownTargetIgnoresZoneState(props.ownTarget);
|
|
28241
|
+
const controlDisabled = zoneStateApplies && controlDisabledFromAbove;
|
|
28242
|
+
const controlReadOnly = zoneStateApplies && controlReadOnlyFromAbove;
|
|
28243
|
+
const controlLoading = zoneStateApplies && controlLoadingFromAbove;
|
|
27493
28244
|
const disabledResolved = disabled || controlDisabled;
|
|
27494
28245
|
const requiredResolved = required || controlRequired;
|
|
27495
28246
|
// Busy because the group above is running the action THIS control asked
|
|
@@ -27501,7 +28252,7 @@ const useInteractiveProps = (props, {
|
|
|
27501
28252
|
// (`maxLengthGuard`) and this one would make it longer: it can be pointed
|
|
27502
28253
|
// at, focused and pressed — and answers why (see readonly_constraint.js) —
|
|
27503
28254
|
// but cannot be taken.
|
|
27504
|
-
const readOnlyFromParentMaxLengthGuard = Boolean(uiStateController.parentUIStateController?.isChildBlockedByMaxLengthGuard?.(uiStateController));
|
|
28255
|
+
const readOnlyFromParentMaxLengthGuard = Boolean(zoneStateApplies && uiStateController.parentUIStateController?.isChildBlockedByMaxLengthGuard?.(uiStateController));
|
|
27505
28256
|
const readOnlyBase = readOnly || controlReadOnly || loadingBase || readOnlyFromParentMaxLengthGuard || controlInfo.readOnlyUncontrolled;
|
|
27506
28257
|
// An optimistic control trusts its action to succeed: the state the user
|
|
27507
28258
|
// just set stays visible and interactive while the action runs — no
|
|
@@ -27935,7 +28686,7 @@ const getAssociatedLabels = element => {
|
|
|
27935
28686
|
return Array.from(element.labels);
|
|
27936
28687
|
};
|
|
27937
28688
|
|
|
27938
|
-
installImportMetaCssBuild(import.meta);const css
|
|
28689
|
+
installImportMetaCssBuild(import.meta);const css$Z = /* css */`
|
|
27939
28690
|
@layer navi {
|
|
27940
28691
|
.navi_button {
|
|
27941
28692
|
--button-border-radius: var(--navi-control-border-radius);
|
|
@@ -28366,7 +29117,7 @@ installImportMetaCssBuild(import.meta);const css$$ = /* css */`
|
|
|
28366
29117
|
}
|
|
28367
29118
|
`;
|
|
28368
29119
|
const ButtonUI = props => {
|
|
28369
|
-
import.meta.css = [css
|
|
29120
|
+
import.meta.css = [css$Z, "@jsenv/navi/src/control/input/button_ui.jsx"];
|
|
28370
29121
|
const {
|
|
28371
29122
|
ref,
|
|
28372
29123
|
// href/link
|
|
@@ -28379,6 +29130,7 @@ const ButtonUI = props => {
|
|
|
28379
29130
|
icon,
|
|
28380
29131
|
cta,
|
|
28381
29132
|
spacing,
|
|
29133
|
+
emojiAsIcon = true,
|
|
28382
29134
|
// Whether the button draws the loading outline itself. A button that is
|
|
28383
29135
|
// one half of a bigger control says no: what is busy is the control, and
|
|
28384
29136
|
// the outline belongs around the whole of it (see split_button.jsx).
|
|
@@ -28436,6 +29188,7 @@ const ButtonUI = props => {
|
|
|
28436
29188
|
|
|
28437
29189
|
type: "button",
|
|
28438
29190
|
spacing: undefined,
|
|
29191
|
+
emojiAsIcon: undefined,
|
|
28439
29192
|
cta: undefined,
|
|
28440
29193
|
pressEffect: undefined,
|
|
28441
29194
|
loadingOutline: undefined,
|
|
@@ -28489,6 +29242,7 @@ const ButtonUI = props => {
|
|
|
28489
29242
|
...controlChildrenWrapperProps,
|
|
28490
29243
|
children: jsx(ButtonContent, {
|
|
28491
29244
|
spacing: spacing,
|
|
29245
|
+
emojiAsIcon: emojiAsIcon,
|
|
28492
29246
|
children: children
|
|
28493
29247
|
})
|
|
28494
29248
|
})]
|
|
@@ -28496,6 +29250,7 @@ const ButtonUI = props => {
|
|
|
28496
29250
|
};
|
|
28497
29251
|
const ButtonContent = ({
|
|
28498
29252
|
spacing,
|
|
29253
|
+
emojiAsIcon,
|
|
28499
29254
|
children
|
|
28500
29255
|
}) => {
|
|
28501
29256
|
const boxForwardedProps = useContext(BoxForwardedPropsContext);
|
|
@@ -28503,6 +29258,7 @@ const ButtonContent = ({
|
|
|
28503
29258
|
...boxForwardedProps,
|
|
28504
29259
|
display: "inherit",
|
|
28505
29260
|
spacing: spacing,
|
|
29261
|
+
emojiAsIcon: emojiAsIcon,
|
|
28506
29262
|
className: "navi_button_content",
|
|
28507
29263
|
children: [children, jsx(ButtonShadow, {})]
|
|
28508
29264
|
});
|
|
@@ -28665,17 +29421,22 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
|
|
|
28665
29421
|
|
|
28666
29422
|
/**
|
|
28667
29423
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
28668
|
-
* ownTarget?: boolean | "refuse",
|
|
29424
|
+
* ownTarget?: boolean | "refuse" | "always",
|
|
29425
|
+
* emojiAsIcon?: boolean,
|
|
28669
29426
|
* [key: string]: any,
|
|
28670
29427
|
* }>}
|
|
28671
|
-
* @param {boolean
|
|
28672
|
-
*
|
|
28673
|
-
*
|
|
28674
|
-
*
|
|
28675
|
-
*
|
|
28676
|
-
*
|
|
28677
|
-
*
|
|
28678
|
-
*
|
|
29428
|
+
* @param {boolean} [emojiAsIcon=true] Renders the emoji of the label as icons
|
|
29429
|
+
* so the button keeps the height of its text — `Text`'s prop, on by default
|
|
29430
|
+
* here. Pass `false` to let an emoji draw at its natural size.
|
|
29431
|
+
* @param {boolean|"refuse"|"always"} [ownTarget] A real target inside a zone
|
|
29432
|
+
* that belongs to another control — a chip's cross on a picker's façade, an
|
|
29433
|
+
* eye on a pressable row, a diskette inside a slide that travels. The press is
|
|
29434
|
+
* this button's alone (no travel starts, no popup opens, no navi control above
|
|
29435
|
+
* answers) and its `onClick` waits for its own interaction gate instead of
|
|
29436
|
+
* firing from the DOM. What it does where the zone is read-only, disabled or
|
|
29437
|
+
* busy depends on whether it WRITES to the control it sits in: it goes by
|
|
29438
|
+
* default, `"refuse"` keeps it and refuses with a callout, `"always"` ignores
|
|
29439
|
+
* the zone's state entirely — for a gesture that never touched that control.
|
|
28679
29440
|
*/
|
|
28680
29441
|
const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
|
|
28681
29442
|
|
|
@@ -30096,7 +30857,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
30096
30857
|
* reaches the real container.
|
|
30097
30858
|
*/
|
|
30098
30859
|
let openLocalDialogCount = 0;
|
|
30099
|
-
const css$
|
|
30860
|
+
const css$Y = /* css */`
|
|
30100
30861
|
@layer navi {
|
|
30101
30862
|
.navi_dialog {
|
|
30102
30863
|
/* Min gap between the dialog and the edges of its container. Written
|
|
@@ -30684,7 +31445,7 @@ const css$_ = /* css */`
|
|
|
30684
31445
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
30685
31446
|
*/
|
|
30686
31447
|
const Dialog = props => {
|
|
30687
|
-
import.meta.css = [css$
|
|
31448
|
+
import.meta.css = [css$Y, "@jsenv/navi/src/layout/dialog.jsx"];
|
|
30688
31449
|
if (props.openController) {
|
|
30689
31450
|
return jsx(ControlledDialog, {
|
|
30690
31451
|
...props
|
|
@@ -31676,7 +32437,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
31676
32437
|
* and applied.
|
|
31677
32438
|
*/
|
|
31678
32439
|
let openLocalPopoverCount = 0;
|
|
31679
|
-
const css$
|
|
32440
|
+
const css$X = /* css */`
|
|
31680
32441
|
@layer navi {
|
|
31681
32442
|
.navi_popover {
|
|
31682
32443
|
/* soft: user-configurable preferred max-height. Kept as a *default*
|
|
@@ -32136,7 +32897,7 @@ const css$Z = /* css */`
|
|
|
32136
32897
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
32137
32898
|
*/
|
|
32138
32899
|
const Popover = props => {
|
|
32139
|
-
import.meta.css = [css$
|
|
32900
|
+
import.meta.css = [css$X, "@jsenv/navi/src/layout/popover.jsx"];
|
|
32140
32901
|
if (props.openController) {
|
|
32141
32902
|
return jsx(ControlledPopover, {
|
|
32142
32903
|
...props
|
|
@@ -33164,7 +33925,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
33164
33925
|
* event, and a caller replacing the body entirely then has one protocol to
|
|
33165
33926
|
* follow — `--navi-confirm` for yes, anything that closes for no.
|
|
33166
33927
|
*/
|
|
33167
|
-
const css$
|
|
33928
|
+
const css$W = /* css */`
|
|
33168
33929
|
/* The width lives on the body rather than on the popup, so that custom
|
|
33169
33930
|
content (which replaces this body entirely) sizes itself instead of
|
|
33170
33931
|
inheriting a ceiling meant for a sentence-long question. */
|
|
@@ -33301,7 +34062,7 @@ const ConfirmPopup = ({
|
|
|
33301
34062
|
onAnswer,
|
|
33302
34063
|
onClosed
|
|
33303
34064
|
}) => {
|
|
33304
|
-
import.meta.css = [css$
|
|
34065
|
+
import.meta.css = [css$W, "@jsenv/navi/src/action/confirm_popup.jsx"];
|
|
33305
34066
|
const {
|
|
33306
34067
|
mode,
|
|
33307
34068
|
confirmLabel,
|
|
@@ -33385,7 +34146,7 @@ const defaultBody = (message, {
|
|
|
33385
34146
|
});
|
|
33386
34147
|
};
|
|
33387
34148
|
|
|
33388
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
34149
|
+
installImportMetaCssBuild(import.meta);const css$V = /* css */`
|
|
33389
34150
|
.action_error {
|
|
33390
34151
|
margin-top: 0;
|
|
33391
34152
|
margin-bottom: 20px;
|
|
@@ -33410,7 +34171,7 @@ const ActionRenderer = ({
|
|
|
33410
34171
|
children,
|
|
33411
34172
|
disabled
|
|
33412
34173
|
}) => {
|
|
33413
|
-
import.meta.css = [css$
|
|
34174
|
+
import.meta.css = [css$V, "@jsenv/navi/src/action/action_renderer.jsx"];
|
|
33414
34175
|
if (action === undefined) {
|
|
33415
34176
|
throw new Error("ActionRenderer requires an action to render, but none was provided.");
|
|
33416
34177
|
}
|
|
@@ -39035,7 +39796,7 @@ const ROUTE_TRAVEL_ATTRIBUTE = "data-navi-route-travel";
|
|
|
39035
39796
|
// the root pictures must NOT move (they carry the whole viewport, blank bands
|
|
39036
39797
|
// included).
|
|
39037
39798
|
|
|
39038
|
-
const css$
|
|
39799
|
+
const css$U = /* css */`
|
|
39039
39800
|
/* The marked region is a picture of its own for the length of a transition of
|
|
39040
39801
|
OURS, and only then — the name is what makes the pages a picture the
|
|
39041
39802
|
movement below can carry.
|
|
@@ -39464,7 +40225,7 @@ const RouteTransitionArea = ({
|
|
|
39464
40225
|
children,
|
|
39465
40226
|
...rest
|
|
39466
40227
|
}) => {
|
|
39467
|
-
import.meta.css = [css$
|
|
40228
|
+
import.meta.css = [css$U, "@jsenv/navi/src/nav/route_transition.jsx"];
|
|
39468
40229
|
const props = {
|
|
39469
40230
|
...rest,
|
|
39470
40231
|
[TRANSITION_AREA_ATTRIBUTE]: ""
|
|
@@ -39521,7 +40282,7 @@ const RouteTransitionArea = ({
|
|
|
39521
40282
|
* @returns {() => void} remove this relation.
|
|
39522
40283
|
*/
|
|
39523
40284
|
const defineRouteTransition = (from, to, transition) => {
|
|
39524
|
-
import.meta.css = [css$
|
|
40285
|
+
import.meta.css = [css$U, "@jsenv/navi/src/nav/route_transition.jsx"];
|
|
39525
40286
|
const {
|
|
39526
40287
|
type,
|
|
39527
40288
|
duration
|
|
@@ -39557,7 +40318,7 @@ const defineRouteTransition = (from, to, transition) => {
|
|
|
39557
40318
|
* @returns {() => void} remove this default.
|
|
39558
40319
|
*/
|
|
39559
40320
|
const defineRouteDefaultTransition = transition => {
|
|
39560
|
-
import.meta.css = [css$
|
|
40321
|
+
import.meta.css = [css$U, "@jsenv/navi/src/nav/route_transition.jsx"];
|
|
39561
40322
|
const value = normalizeTransition(transition);
|
|
39562
40323
|
defaultTransition = value;
|
|
39563
40324
|
return () => {
|
|
@@ -40158,7 +40919,7 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
|
|
|
40158
40919
|
const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
|
|
40159
40920
|
// The name the box wears while it travels, and only then (see nameForTravel).
|
|
40160
40921
|
const TRAVEL_NAME = "navi-route-travel";
|
|
40161
|
-
const css$
|
|
40922
|
+
const css$T = /* css */`
|
|
40162
40923
|
/* The name that makes the page inside this box a picture of its own during a
|
|
40163
40924
|
transition — rather than part of the one big picture the document takes, so
|
|
40164
40925
|
the two pages can move past each other while everything else stays where it
|
|
@@ -40548,7 +41309,7 @@ const RouteTravel = ({
|
|
|
40548
41309
|
children,
|
|
40549
41310
|
...rest
|
|
40550
41311
|
}) => {
|
|
40551
|
-
import.meta.css = [css$
|
|
41312
|
+
import.meta.css = [css$T, "@jsenv/navi/src/nav/route_travel.jsx"];
|
|
40552
41313
|
const elementRef = useRef();
|
|
40553
41314
|
const gestureRef = useRef(null);
|
|
40554
41315
|
// The travel in hand: the transition keeping the picture of the page being
|
|
@@ -43109,402 +43870,6 @@ const PhoneSvg = () => {
|
|
|
43109
43870
|
});
|
|
43110
43871
|
};
|
|
43111
43872
|
|
|
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
43873
|
/**
|
|
43509
43874
|
* Hook that reactively checks if a URL is visited.
|
|
43510
43875
|
* Re-renders when the visited URL set changes.
|
|
@@ -63066,7 +63431,8 @@ const useBadgeRegistry = (children, enabled) => {
|
|
|
63066
63431
|
* not the same in each. Nothing is set up for a case that cannot happen: a
|
|
63067
63432
|
* plain list is one element holding its children as-is — no registry, no
|
|
63068
63433
|
* effect —, a capped one collects its badges but measures nothing, and only
|
|
63069
|
-
* shrinkWrap
|
|
63434
|
+
* shrinkWrap on its own builds the measurement ghost: under maxLines the list
|
|
63435
|
+
* is measured in place, once the rows are known.
|
|
63070
63436
|
*
|
|
63071
63437
|
* @param {import("ignore:preact").ComponentChildren} [fallback]
|
|
63072
63438
|
* Rendered in place of the badges when there is none. Without it an empty
|
|
@@ -63081,8 +63447,9 @@ const useBadgeRegistry = (children, enabled) => {
|
|
|
63081
63447
|
* Narrows the list down to its widest row so the last row isn't ragged.
|
|
63082
63448
|
* Defaults to true inside a <Picker> — the trigger draws a border around the
|
|
63083
63449
|
* list, so the ragged edge shows — and false elsewhere, where the work would
|
|
63084
|
-
* often go unseen: opt in where an edge is visible.
|
|
63085
|
-
*
|
|
63450
|
+
* often go unseen: opt in where an edge is visible. Composes with maxLines:
|
|
63451
|
+
* the rows are read at the full width first, and the list is narrowed once
|
|
63452
|
+
* the surplus is gone.
|
|
63086
63453
|
* @param {number} [max]
|
|
63087
63454
|
* Caps how many badges are rendered; the surplus becomes a "+N" badge, which
|
|
63088
63455
|
* takes one of the max slots.
|
|
@@ -63106,11 +63473,10 @@ const BadgeList = props => {
|
|
|
63106
63473
|
shrinkWrap = maxLinesFromAbove !== undefined
|
|
63107
63474
|
} = props;
|
|
63108
63475
|
if (maxLinesResolved !== undefined) {
|
|
63109
|
-
// shrinkWrap is dropped on purpose: it narrows the list down to its widest
|
|
63110
|
-
// row, which would re-wrap the badges under the cap just measured.
|
|
63111
63476
|
return jsx(BadgeListMaxLines, {
|
|
63112
63477
|
...props,
|
|
63113
|
-
maxLines: maxLinesResolved
|
|
63478
|
+
maxLines: maxLinesResolved,
|
|
63479
|
+
shrinkWrap: shrinkWrap
|
|
63114
63480
|
});
|
|
63115
63481
|
}
|
|
63116
63482
|
if (shrinkWrap) {
|
|
@@ -63251,6 +63617,7 @@ const BadgeListMaxLines = ({
|
|
|
63251
63617
|
children,
|
|
63252
63618
|
max,
|
|
63253
63619
|
maxLines,
|
|
63620
|
+
shrinkWrap,
|
|
63254
63621
|
...boxProps
|
|
63255
63622
|
}) => {
|
|
63256
63623
|
const registry = useBadgeRegistry(children, true);
|
|
@@ -63272,8 +63639,29 @@ const BadgeListMaxLines = ({
|
|
|
63272
63639
|
// out of the cap and nothing has to be watched.
|
|
63273
63640
|
const watchesResize = fit !== null && fit.count > 1;
|
|
63274
63641
|
|
|
63642
|
+
// The list's own width. While the rows are being read the list takes all the
|
|
63643
|
+
// room it is given — a width kept from an earlier shrink wrap would fold the
|
|
63644
|
+
// badges under it. Once the surplus is gone, shrinkWrap narrows the list to
|
|
63645
|
+
// its widest row, measured where the badges stand: nothing is painted before
|
|
63646
|
+
// the effect below has run, so no clone is needed.
|
|
63647
|
+
useLayoutEffect(() => {
|
|
63648
|
+
const visibleEl = visibleRef.current;
|
|
63649
|
+
if (!visibleEl) {
|
|
63650
|
+
return;
|
|
63651
|
+
}
|
|
63652
|
+
visibleEl.style.width = "";
|
|
63653
|
+
if (fit === null || !shrinkWrap) {
|
|
63654
|
+
return;
|
|
63655
|
+
}
|
|
63656
|
+
const widestRowWidth = measureWidestChildRow(visibleEl);
|
|
63657
|
+
if (widestRowWidth !== null) {
|
|
63658
|
+
visibleEl.style.width = `${Math.ceil(widestRowWidth)}px`;
|
|
63659
|
+
}
|
|
63660
|
+
}, [fit, shrinkWrap]);
|
|
63661
|
+
|
|
63275
63662
|
// Runs after every render, which is when the badges have registered and the
|
|
63276
|
-
// DOM holds whatever this render asked for
|
|
63663
|
+
// DOM holds whatever this render asked for, at the width the effect above
|
|
63664
|
+
// gave it.
|
|
63277
63665
|
useLayoutEffect(() => {
|
|
63278
63666
|
const visibleEl = visibleRef.current;
|
|
63279
63667
|
if (!visibleEl) {
|
|
@@ -63308,7 +63696,8 @@ const BadgeListMaxLines = ({
|
|
|
63308
63696
|
// And not every width change either. Nothing guarantees an ancestor whose
|
|
63309
63697
|
// width does not follow its content (a column with align-items: start
|
|
63310
63698
|
// sizes every row to what is inside it), so rendering every badge widens
|
|
63311
|
-
// what is being watched and dropping the surplus
|
|
63699
|
+
// what is being watched and dropping the surplus — then shrink wrapping
|
|
63700
|
+
// what is left — narrows it right back.
|
|
63312
63701
|
// Those two widths are this list talking to itself; measuring again on
|
|
63313
63702
|
// them never ends. Any other width is the room around it changing.
|
|
63314
63703
|
const width = outerParent.getBoundingClientRect().width;
|
|
@@ -64177,7 +64566,7 @@ installImportMetaCssBuild(import.meta);const css$u = /* css */`
|
|
|
64177
64566
|
means "open the picker". An own target is the exception, the same way
|
|
64178
64567
|
the clear cross is one in the slot below: it says the press is aimed at
|
|
64179
64568
|
IT, so it has to be reachable at all. */
|
|
64180
|
-
[data-
|
|
64569
|
+
[data-own-target] {
|
|
64181
64570
|
pointer-events: auto;
|
|
64182
64571
|
}
|
|
64183
64572
|
}
|
|
@@ -75686,6 +76075,9 @@ const MessageBox = ({
|
|
|
75686
76075
|
padding = "sm",
|
|
75687
76076
|
icon,
|
|
75688
76077
|
leftStripe,
|
|
76078
|
+
// A message is free text: an emoji is expected in it, and must not push
|
|
76079
|
+
// the first line down next to the icon and the close button.
|
|
76080
|
+
emojiAsIcon = true,
|
|
75689
76081
|
children,
|
|
75690
76082
|
onClose,
|
|
75691
76083
|
...rest
|
|
@@ -75723,26 +76115,39 @@ const MessageBox = ({
|
|
|
75723
76115
|
value: setHasTitleChild,
|
|
75724
76116
|
children: [icon && jsx(Icon, {
|
|
75725
76117
|
color: "var(--x-message-color)",
|
|
75726
|
-
height: "
|
|
76118
|
+
height: "1lh",
|
|
75727
76119
|
maxHeight: "auto",
|
|
75728
76120
|
selfAlignY: "start",
|
|
75729
76121
|
aspectRatio: "auto",
|
|
75730
76122
|
children: icon
|
|
75731
76123
|
}), jsx(Text, {
|
|
76124
|
+
emojiAsIcon: emojiAsIcon,
|
|
75732
76125
|
children: children
|
|
75733
|
-
}), onClose &&
|
|
75734
|
-
|
|
75735
|
-
|
|
75736
|
-
|
|
75737
|
-
|
|
76126
|
+
}), onClose &&
|
|
76127
|
+
// A column as tall as the first line of the message, pinned to the
|
|
76128
|
+
// top, the button centered in it: the close button stays level
|
|
76129
|
+
// with the first line however many lines the message takes (same
|
|
76130
|
+
// layout as Callout).
|
|
76131
|
+
jsx(Box, {
|
|
76132
|
+
flex: true,
|
|
75738
76133
|
alignY: "center",
|
|
75739
|
-
|
|
75740
|
-
|
|
75741
|
-
|
|
75742
|
-
|
|
75743
|
-
|
|
75744
|
-
|
|
75745
|
-
|
|
76134
|
+
height: "1lh",
|
|
76135
|
+
selfAlignY: "start",
|
|
76136
|
+
shrink: false,
|
|
76137
|
+
children: jsx(Button, {
|
|
76138
|
+
action: onClose,
|
|
76139
|
+
icon: true,
|
|
76140
|
+
border: "none",
|
|
76141
|
+
alignX: "center",
|
|
76142
|
+
alignY: "center",
|
|
76143
|
+
style: {
|
|
76144
|
+
":hover": {
|
|
76145
|
+
backgroundColor: "rgba(0, 0, 0, 0.1)"
|
|
76146
|
+
}
|
|
76147
|
+
},
|
|
76148
|
+
children: jsx(Icon, {
|
|
76149
|
+
children: jsx(CloseSvg, {})
|
|
76150
|
+
})
|
|
75746
76151
|
})
|
|
75747
76152
|
})]
|
|
75748
76153
|
})
|
|
@@ -77830,5 +78235,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
77830
78235
|
})
|
|
77831
78236
|
});
|
|
77832
78237
|
|
|
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 };
|
|
78238
|
+
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
78239
|
//# sourceMappingURL=jsenv_navi.js.map
|