@jsenv/navi 0.30.25 → 0.30.27
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/dev/jsenv_navi.js +269 -30
- package/dist/dev/jsenv_navi.js.map +10 -8
- package/dist/dev/jsenv_navi_side_effects.js +20 -11
- package/dist/dev/jsenv_navi_side_effects.js.map +3 -3
- package/dist/jsenv_navi.js +266 -30
- package/dist/jsenv_navi.js.map +10 -8
- package/dist/jsenv_navi_side_effects.js +20 -11
- package/dist/jsenv_navi_side_effects.js.map +3 -3
- package/docs/AI_INSTRUCTIONS.md +3 -2
- package/docs/actions.md +4 -0
- package/docs/dynamic_import.md +18 -0
- package/docs/safe_area.md +25 -0
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* AI reading this file: read ../docs/AI_INSTRUCTIONS.md for context on
|
|
3
3
|
* using @jsenv/navi as intended.
|
|
4
4
|
*/
|
|
5
|
-
import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
|
|
5
|
+
import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal, getSafeAreaInsets } from "./jsenv_navi_side_effects.js";
|
|
6
6
|
import { createPubSub, createIterableWeakSet, elementIsFocusable, getScrollContainer, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, trapScrollInside, scrollIntoViewScoped, isPressDisputedByDrag, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, scrollRoomTowards, overflowGoesToViewport, getVirtualKeyboardOverlayHeight, subscribeVisualViewportResizeSettled, subscribeVirtualKeyboardGeometryChange, parsePositionArea, snapToPixel, trapFocusInside, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent } from "@jsenv/dom";
|
|
7
7
|
export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
|
|
8
8
|
import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
|
|
@@ -783,6 +783,91 @@ const stringifyForDisplay = (
|
|
|
783
783
|
return String(value);
|
|
784
784
|
};
|
|
785
785
|
|
|
786
|
+
/*
|
|
787
|
+
* Who asks for an action instance right now, and what a run called off for
|
|
788
|
+
* want of askers owes to whoever comes next.
|
|
789
|
+
*
|
|
790
|
+
* Instances are shared by params: two route actions reading the same thing
|
|
791
|
+
* with the same params, or a route action and a component reading it with
|
|
792
|
+
* `run: true`, ask ONE instance. So a binding whose params turn falsy (its
|
|
793
|
+
* route left) does not own the run in flight: it calls it off only when
|
|
794
|
+
* nothing else asks for that instance at that moment. What asks is an asker:
|
|
795
|
+
* a binding of actionRunEffect while its params are truthy, a mounted
|
|
796
|
+
* `useAsyncData` reader with `run: true`. Each says which instance it asks for
|
|
797
|
+
* when the question is put, rather than registering one: a reader of a
|
|
798
|
+
* binding follows the binding's target the moment it moves, not at its next
|
|
799
|
+
* render.
|
|
800
|
+
*
|
|
801
|
+
* Calling a run off is a guess about who is coming: a reader mounting after
|
|
802
|
+
* it (the page arriving), or suspended before its first commit, finds an
|
|
803
|
+
* instance nobody will run again. So the instance is marked, and a `run: true`
|
|
804
|
+
* reader finding it marked asks again, as an arriving binding does when its
|
|
805
|
+
* run() finds the instance aborted. An abort decided by anyone else (abort(),
|
|
806
|
+
* a give-up on a busy popup) carries no mark: aborting does not undo, and the
|
|
807
|
+
* reader leaves it be.
|
|
808
|
+
*/
|
|
809
|
+
|
|
810
|
+
const askerSet = createIterableWeakSet("action_asker");
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* `readAskedInstance()` returns what is asked for now: an instance, a binding
|
|
814
|
+
* (its current target is what counts), or null. The asker is held weakly:
|
|
815
|
+
* whoever adds it keeps it, and removes it once it asks for nothing for good.
|
|
816
|
+
*/
|
|
817
|
+
const addActionAsker = (readAskedInstance) => {
|
|
818
|
+
const asker = { readAskedInstance };
|
|
819
|
+
askerSet.add(asker);
|
|
820
|
+
return asker;
|
|
821
|
+
};
|
|
822
|
+
const removeActionAsker = (asker) => {
|
|
823
|
+
askerSet.delete(asker);
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
const calledOffWeakSet = new WeakSet();
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Aborts the instance's run unless an asker asks for it, and marks a run
|
|
830
|
+
* called off that way (see isCalledOffForWantOfAskers).
|
|
831
|
+
*/
|
|
832
|
+
const callOffUnlessAsked = (action, reason) => {
|
|
833
|
+
const instance = resolveInstance(action);
|
|
834
|
+
for (const asker of askerSet) {
|
|
835
|
+
if (resolveInstance(asker.readAskedInstance()) === instance) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (!instance.abort(reason)) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
// A callback that ignores its abort signal completes all the same.
|
|
843
|
+
if (instance.runningStateSignal.peek() !== ABORTED) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
calledOffWeakSet.add(instance);
|
|
847
|
+
// The mark is for this abort only: once the instance moves, an abort that
|
|
848
|
+
// comes later is somebody else's.
|
|
849
|
+
const unsubscribe = instance.runningStateSignal.subscribe((state) => {
|
|
850
|
+
if (state !== ABORTED) {
|
|
851
|
+
calledOffWeakSet.delete(instance);
|
|
852
|
+
unsubscribe();
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
const isCalledOffForWantOfAskers = (action) => {
|
|
858
|
+
return calledOffWeakSet.has(resolveInstance(action));
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
// A binding counts as the instance it targets now. Read as it stands, never
|
|
862
|
+
// through getCurrentAction(), which would flush a debounced binding.
|
|
863
|
+
const resolveInstance = (action) => {
|
|
864
|
+
let instance = action;
|
|
865
|
+
while (instance && instance.isProxy) {
|
|
866
|
+
instance = getActionPrivateProperties(instance).currentAction;
|
|
867
|
+
}
|
|
868
|
+
return instance;
|
|
869
|
+
};
|
|
870
|
+
|
|
786
871
|
const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
|
|
787
872
|
|
|
788
873
|
/*
|
|
@@ -3535,6 +3620,7 @@ const updateActions = ({
|
|
|
3535
3620
|
|
|
3536
3621
|
const actionToRunPrivateProperties =
|
|
3537
3622
|
getActionPrivateProperties(actionToPrerunOrRun);
|
|
3623
|
+
actionToRunPrivateProperties.answeredAsync = false;
|
|
3538
3624
|
let performRunResult;
|
|
3539
3625
|
try {
|
|
3540
3626
|
performRunResult = actionToRunPrivateProperties.performRun({
|
|
@@ -3559,6 +3645,7 @@ const updateActions = ({
|
|
|
3559
3645
|
activationWeakSet.add(actionToPrerunOrRun);
|
|
3560
3646
|
|
|
3561
3647
|
if (performRunResult && typeof performRunResult.then === "function") {
|
|
3648
|
+
actionToRunPrivateProperties.answeredAsync = true;
|
|
3562
3649
|
actionPromiseMap.set(actionToPrerunOrRun, performRunResult);
|
|
3563
3650
|
allThenableArray.push(performRunResult);
|
|
3564
3651
|
hasAsync = true;
|
|
@@ -3588,7 +3675,9 @@ const updateActions = ({
|
|
|
3588
3675
|
|
|
3589
3676
|
// Execute promotions (prerun -> run requested)
|
|
3590
3677
|
for (const actionToPromote of willPromoteSet) {
|
|
3591
|
-
|
|
3678
|
+
writeActionState(() => {
|
|
3679
|
+
actionToPromote.isPrerunSignal.value = false;
|
|
3680
|
+
});
|
|
3592
3681
|
}
|
|
3593
3682
|
}
|
|
3594
3683
|
|
|
@@ -3597,13 +3686,21 @@ const updateActions = ({
|
|
|
3597
3686
|
// that IS carrying it. `await action.run()` then means the same thing to
|
|
3598
3687
|
// every caller: the second one waits for the first one's request and reads
|
|
3599
3688
|
// the same data, instead of being handed `undefined` before the network has
|
|
3600
|
-
// said anything.
|
|
3689
|
+
// said anything. It answers in the tense of that run: the answer of a run
|
|
3690
|
+
// that answered with a promise comes back as one, so `run().catch()` holds
|
|
3691
|
+
// whatever state the instance is in. See the run() JSDoc.
|
|
3601
3692
|
{
|
|
3602
3693
|
for (const actionToJoin of willJoinSet) {
|
|
3603
3694
|
const runningPromise = actionPromiseMap.get(actionToJoin);
|
|
3604
3695
|
if (runningPromise) {
|
|
3605
3696
|
hasAsync = true;
|
|
3606
3697
|
resultArray.push({ type: "async", promise: runningPromise });
|
|
3698
|
+
} else if (getActionPrivateProperties(actionToJoin).answeredAsync) {
|
|
3699
|
+
hasAsync = true;
|
|
3700
|
+
resultArray.push({
|
|
3701
|
+
type: "async",
|
|
3702
|
+
promise: Promise.resolve(actionToJoin.dataSignal.peek()),
|
|
3703
|
+
});
|
|
3607
3704
|
} else {
|
|
3608
3705
|
resultArray.push({
|
|
3609
3706
|
type: "sync",
|
|
@@ -3809,6 +3906,10 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
3809
3906
|
* the one that asked first or the one that arrived while the request was
|
|
3810
3907
|
* out. Use `rerun()` to force a fresh run ("refresh", "check now", any
|
|
3811
3908
|
* explicit user intent to go back to the network).
|
|
3909
|
+
*
|
|
3910
|
+
* An asynchronous callback answers with a promise, joined or not:
|
|
3911
|
+
* `ACTION.run().catch(…)` holds in every state. A synchronous one answers
|
|
3912
|
+
* synchronously, and throws its failure.
|
|
3812
3913
|
*/
|
|
3813
3914
|
const run = (options) => {
|
|
3814
3915
|
action.debug(`${action}.run(${stringifyForDisplay(options)})`);
|
|
@@ -4267,7 +4368,9 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4267
4368
|
// another run's, or idle.
|
|
4268
4369
|
return;
|
|
4269
4370
|
}
|
|
4270
|
-
|
|
4371
|
+
writeActionState(() => {
|
|
4372
|
+
runningStateSignal.value = ABORTED;
|
|
4373
|
+
});
|
|
4271
4374
|
internalAbortController.abort(abortReason);
|
|
4272
4375
|
actionAbortMap.delete(action);
|
|
4273
4376
|
if (isPrerun && (globalAbortSignal.aborted || abortSignal.aborted)) {
|
|
@@ -4291,7 +4394,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4291
4394
|
|
|
4292
4395
|
actionAbortMap.set(action, abort);
|
|
4293
4396
|
|
|
4294
|
-
|
|
4397
|
+
writeActionState(() => {
|
|
4295
4398
|
if (valueSignal.peek() === undefined) {
|
|
4296
4399
|
// An action saying nothing of its own may still have a value the
|
|
4297
4400
|
// page kept from the previous document (see kept_values.js).
|
|
@@ -4370,7 +4473,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4370
4473
|
* before the UI attempts to render the now-missing resource.
|
|
4371
4474
|
*/
|
|
4372
4475
|
|
|
4373
|
-
|
|
4476
|
+
writeActionState(() => {
|
|
4374
4477
|
const value = resultToValue
|
|
4375
4478
|
? resultToValue(runResult, action)
|
|
4376
4479
|
: runResult;
|
|
@@ -4399,7 +4502,9 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4399
4502
|
error === internalAbortSignal.reason) ||
|
|
4400
4503
|
error.name === "AbortError";
|
|
4401
4504
|
if (isAbort) {
|
|
4402
|
-
|
|
4505
|
+
writeActionState(() => {
|
|
4506
|
+
runningStateSignal.value = ABORTED;
|
|
4507
|
+
});
|
|
4403
4508
|
if (isPrerun && abortSignal.aborted) {
|
|
4404
4509
|
prerunProtectionRegistry.unprotect(action);
|
|
4405
4510
|
}
|
|
@@ -4410,7 +4515,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4410
4515
|
if (isPrerun) {
|
|
4411
4516
|
markErrorAsPrerunFailure(error);
|
|
4412
4517
|
}
|
|
4413
|
-
|
|
4518
|
+
writeActionState(() => {
|
|
4414
4519
|
errorSignal.value = error;
|
|
4415
4520
|
runningStateSignal.value = FAILED;
|
|
4416
4521
|
onError?.(error, { event, action, args });
|
|
@@ -4502,7 +4607,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4502
4607
|
}
|
|
4503
4608
|
|
|
4504
4609
|
actionPromiseMap.delete(action);
|
|
4505
|
-
|
|
4610
|
+
writeActionState(() => {
|
|
4506
4611
|
if (!willRunOrPrerun) {
|
|
4507
4612
|
errorSignal.value = undefined;
|
|
4508
4613
|
valueSignal.value = valueInitial;
|
|
@@ -4520,6 +4625,9 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4520
4625
|
provisionalValue,
|
|
4521
4626
|
inheritData,
|
|
4522
4627
|
keepsAnswers: keptAnswerRegistry !== null,
|
|
4628
|
+
// Whether the last run answered with a promise, written by
|
|
4629
|
+
// updateActions: the tense a run joining this instance answers in.
|
|
4630
|
+
answeredAsync: false,
|
|
4523
4631
|
|
|
4524
4632
|
performRun,
|
|
4525
4633
|
performReset,
|
|
@@ -5003,6 +5111,37 @@ const isPlainObject$2 = (obj) => {
|
|
|
5003
5111
|
);
|
|
5004
5112
|
};
|
|
5005
5113
|
|
|
5114
|
+
// Every state write of a run (start, end, failure, abort, reset), batched. At
|
|
5115
|
+
// the end of the batch preact runs the effects those writes woke up, anywhere
|
|
5116
|
+
// in the app, and rethrows the first one that threw: from there it would fail
|
|
5117
|
+
// this run, or leave it RUNNING, or stop the update half way. That throw is
|
|
5118
|
+
// the effect's, so it is reported on its own, as action_error_report.js
|
|
5119
|
+
// reports an error (thrown from a timer), and the action carries on. What
|
|
5120
|
+
// `write` throws (resultToValue, onComplete…) is the run's, and goes on.
|
|
5121
|
+
const writeActionState = (write) => {
|
|
5122
|
+
let writeThrew = false;
|
|
5123
|
+
let writeError;
|
|
5124
|
+
try {
|
|
5125
|
+
batch(() => {
|
|
5126
|
+
try {
|
|
5127
|
+
write();
|
|
5128
|
+
} catch (e) {
|
|
5129
|
+
writeThrew = true;
|
|
5130
|
+
writeError = e;
|
|
5131
|
+
}
|
|
5132
|
+
});
|
|
5133
|
+
} catch (effectError) {
|
|
5134
|
+
setTimeout(() => {
|
|
5135
|
+
throw effectError;
|
|
5136
|
+
});
|
|
5137
|
+
}
|
|
5138
|
+
if (writeThrew) {
|
|
5139
|
+
throw writeError;
|
|
5140
|
+
}
|
|
5141
|
+
};
|
|
5142
|
+
|
|
5143
|
+
const askerByBindingWeakMap = new WeakMap();
|
|
5144
|
+
|
|
5006
5145
|
/**
|
|
5007
5146
|
* Reactively runs an action whenever the params derived from signals change.
|
|
5008
5147
|
*
|
|
@@ -5099,8 +5238,9 @@ const actionRunEffect = (
|
|
|
5099
5238
|
if (actionTargetPrevious && actionTarget) {
|
|
5100
5239
|
// params changed
|
|
5101
5240
|
if (!asking) {
|
|
5102
|
-
// falsy params, don't run
|
|
5103
|
-
|
|
5241
|
+
// falsy params, don't run; the instance is shared by params, and
|
|
5242
|
+
// something else may still ask for it (see action_askers.js)
|
|
5243
|
+
callOffUnlessAsked(actionTargetPrevious, "abortOnFalsyParams");
|
|
5104
5244
|
return;
|
|
5105
5245
|
}
|
|
5106
5246
|
if (!askingBefore) {
|
|
@@ -5126,6 +5266,11 @@ const actionRunEffect = (
|
|
|
5126
5266
|
},
|
|
5127
5267
|
...options,
|
|
5128
5268
|
});
|
|
5269
|
+
// Held by the binding, as long as it lives: askers are held weakly.
|
|
5270
|
+
askerByBindingWeakMap.set(
|
|
5271
|
+
actionRunnedByThisEffect,
|
|
5272
|
+
addActionAsker(() => (asking ? actionRunnedByThisEffect : null)),
|
|
5273
|
+
);
|
|
5129
5274
|
if (asking) {
|
|
5130
5275
|
runUnwatched(() =>
|
|
5131
5276
|
actionRunnedByThisEffect.run({ reason: "initial truthy params" }),
|
|
@@ -9857,8 +10002,10 @@ const activeRouteReadsSignal = computed(() => {
|
|
|
9857
10002
|
/**
|
|
9858
10003
|
* Binds an action to a route: it runs when the route matches, with the params
|
|
9859
10004
|
* the effect reads off the address, runs again on a `reload()`, and is aborted
|
|
9860
|
-
* when the route is left
|
|
9861
|
-
*
|
|
10005
|
+
* when the route is left, unless something else still asks for the same read
|
|
10006
|
+
* with the same params: another route action, a mounted `useAsyncData` with
|
|
10007
|
+
* `run: true`. Its data, its wait and its failure are read by the page through
|
|
10008
|
+
* `useAsyncData`.
|
|
9862
10009
|
*
|
|
9863
10010
|
* A page's CODE is declared the same way — the import is one more thing the
|
|
9864
10011
|
* address asks for, started with the data rather than after it:
|
|
@@ -31925,6 +32072,11 @@ const useForceRender = () => {
|
|
|
31925
32072
|
* failure a prerun left (a `prerun: true` head start, a press that preran
|
|
31926
32073
|
* it): nothing on screen asked for that run, so this asks again, once.
|
|
31927
32074
|
*
|
|
32075
|
+
* While mounted, it asks for that instance as a route action does: a route
|
|
32076
|
+
* action reading the same thing with the same params does not call the run
|
|
32077
|
+
* off under it when its route is left. A run called off that way before
|
|
32078
|
+
* this component was there to ask is asked again.
|
|
32079
|
+
*
|
|
31928
32080
|
* Prefer a `routeAction` wherever the parameter is one the address holds (a
|
|
31929
32081
|
* path param, a search param bound to a `stateSignal`), popup included: it is
|
|
31930
32082
|
* asked for when the address changes, with everything else that address
|
|
@@ -32074,6 +32226,20 @@ const useActionAsyncData = (action, {
|
|
|
32074
32226
|
}
|
|
32075
32227
|
useOnLoad(action, onLoad);
|
|
32076
32228
|
usePoll(action, poll);
|
|
32229
|
+
const routePage = useContext(RoutePageContext);
|
|
32230
|
+
|
|
32231
|
+
// `run: true` asks for the instance as long as this reader is mounted: a
|
|
32232
|
+
// binding sharing it whose route is left does not call the run off under it
|
|
32233
|
+
// (see action_askers.js). Asked through the binding itself, whose target
|
|
32234
|
+
// moves before this renders again.
|
|
32235
|
+
const askedRef = useRef(null);
|
|
32236
|
+
askedRef.current = run ? action : null;
|
|
32237
|
+
useLayoutEffect(() => {
|
|
32238
|
+
const asker = addActionAsker(() => askedRef.current);
|
|
32239
|
+
return () => {
|
|
32240
|
+
removeActionAsker(asker);
|
|
32241
|
+
};
|
|
32242
|
+
}, []);
|
|
32077
32243
|
|
|
32078
32244
|
// The instance the binding points at, remembered from one render to the
|
|
32079
32245
|
// next: a binding that comes back to params whose instance failed — a search
|
|
@@ -32086,6 +32252,11 @@ const useActionAsyncData = (action, {
|
|
|
32086
32252
|
const instanceRef = useRef(instance);
|
|
32087
32253
|
const backOnFailure = instanceRef.current !== instance && action.runningStateSignal.peek() === FAILED;
|
|
32088
32254
|
instanceRef.current = instance;
|
|
32255
|
+
// A run called off because nothing asked for it any more, before this reader
|
|
32256
|
+
// was there to ask (mounting after, or suspended before its first commit):
|
|
32257
|
+
// nobody else will run it again. Not from a page the address has left, whose
|
|
32258
|
+
// own route action is what called it off.
|
|
32259
|
+
const calledOffBeforeThisReader = isCalledOffForWantOfAskers(action) && !(routePage && !routePage.isShown());
|
|
32089
32260
|
// `run: true` — this component owns the request, so it is what starts it, and
|
|
32090
32261
|
// it starts it from the render rather than from an effect: a component that
|
|
32091
32262
|
// suspends has no effects, so a run written in one would be waiting for
|
|
@@ -32093,7 +32264,7 @@ const useActionAsyncData = (action, {
|
|
|
32093
32264
|
// no-op — which is what makes starting it while rendering sound. From there
|
|
32094
32265
|
// the wait is the ordinary one: suspended into <Loading>, or drawn by the
|
|
32095
32266
|
// component under `loading: true`, exactly as for data someone else ran.
|
|
32096
|
-
if (run && action.paramsSignal.peek() !== undefined && (runIsOwed(action) || backOnFailure)) {
|
|
32267
|
+
if (run && action.paramsSignal.peek() !== undefined && (runIsOwed(action) || backOnFailure || calledOffBeforeThisReader)) {
|
|
32097
32268
|
// Nothing waits on this run: the failure is held by the action, and this
|
|
32098
32269
|
// hook is what reads it back.
|
|
32099
32270
|
runUnwatched(() => action.run({
|
|
@@ -32116,7 +32287,6 @@ const useActionAsyncData = (action, {
|
|
|
32116
32287
|
// has already processed the settlement and the detached DOM is discarded.
|
|
32117
32288
|
const runningState = action.runningStateSignal.peek();
|
|
32118
32289
|
const [, setTick] = useState(0);
|
|
32119
|
-
const routePage = useContext(RoutePageContext);
|
|
32120
32290
|
// What this render read of the action, for the subscriptions below to tell
|
|
32121
32291
|
// a change that came between the render and them from none at all.
|
|
32122
32292
|
const readRef = useRef(null);
|
|
@@ -32324,6 +32494,13 @@ const useActionAsyncData = (action, {
|
|
|
32324
32494
|
actionPendingPromiseWeakMap.delete(action);
|
|
32325
32495
|
unsubscribe();
|
|
32326
32496
|
resolve();
|
|
32497
|
+
} else if (reason === "loading" && state === ABORTED) {
|
|
32498
|
+
// The run waited on was called off: drawn again, this reader asks
|
|
32499
|
+
// for it again when nothing else will (calledOffBeforeThisReader),
|
|
32500
|
+
// or waits for the next run as idle.
|
|
32501
|
+
actionPendingPromiseWeakMap.delete(action);
|
|
32502
|
+
unsubscribe();
|
|
32503
|
+
resolve();
|
|
32327
32504
|
}
|
|
32328
32505
|
});
|
|
32329
32506
|
});
|
|
@@ -59513,6 +59690,7 @@ const measureDrawing = ({
|
|
|
59513
59690
|
* never seen under a bar, and after every read of the commit that queued them.
|
|
59514
59691
|
*/
|
|
59515
59692
|
|
|
59693
|
+
|
|
59516
59694
|
// Several bars can share an edge — during a page transition the outgoing and
|
|
59517
59695
|
// the incoming one are both mounted. They are all pinned to that same edge, so
|
|
59518
59696
|
// they overlap: the room to give back is the largest of them, not their sum,
|
|
@@ -59530,6 +59708,12 @@ const pendingSizeMapByArea = new Map();
|
|
|
59530
59708
|
let flushMicrotaskQueued = false;
|
|
59531
59709
|
let flushFrame = null;
|
|
59532
59710
|
|
|
59711
|
+
// The safe area moves with these writes and no resize comes with them: what is
|
|
59712
|
+
// placed in it (a Popover with container="safe-area") is told here.
|
|
59713
|
+
const [publishFixedBarSpaceChange, subscribeFixedBarSpaceChange] =
|
|
59714
|
+
createPubSub();
|
|
59715
|
+
const onFixedBarSpaceChange = subscribeFixedBarSpaceChange;
|
|
59716
|
+
|
|
59533
59717
|
// A size a render produced: a prop, the children or a theme variable changed
|
|
59534
59718
|
// the bar, and the room it takes is given back before that commit is painted.
|
|
59535
59719
|
/**
|
|
@@ -59580,13 +59764,19 @@ const queueSize = (area, barElement, size) => {
|
|
|
59580
59764
|
};
|
|
59581
59765
|
|
|
59582
59766
|
const flushPendingSizes = () => {
|
|
59767
|
+
let written = false;
|
|
59583
59768
|
for (const [area, pendingSizeMap] of pendingSizeMapByArea) {
|
|
59584
59769
|
for (const [barElement, size] of pendingSizeMap) {
|
|
59585
59770
|
storeSize(area, barElement, size);
|
|
59586
59771
|
}
|
|
59587
|
-
writeSpace(area)
|
|
59772
|
+
if (writeSpace(area)) {
|
|
59773
|
+
written = true;
|
|
59774
|
+
}
|
|
59588
59775
|
}
|
|
59589
59776
|
pendingSizeMapByArea.clear();
|
|
59777
|
+
if (written) {
|
|
59778
|
+
publishFixedBarSpaceChange();
|
|
59779
|
+
}
|
|
59590
59780
|
};
|
|
59591
59781
|
|
|
59592
59782
|
const storeSize = (area, barElement, size) => {
|
|
@@ -59613,18 +59803,20 @@ const writeSpace = (area) => {
|
|
|
59613
59803
|
const property = `--navi-fixed-bar-space-${area}`;
|
|
59614
59804
|
const { style } = document.documentElement;
|
|
59615
59805
|
if (sizeMap.size === 0) {
|
|
59616
|
-
if (writtenValueByArea.has(area)) {
|
|
59617
|
-
|
|
59618
|
-
style.removeProperty(property);
|
|
59806
|
+
if (!writtenValueByArea.has(area)) {
|
|
59807
|
+
return false;
|
|
59619
59808
|
}
|
|
59620
|
-
|
|
59809
|
+
writtenValueByArea.delete(area);
|
|
59810
|
+
style.removeProperty(property);
|
|
59811
|
+
return true;
|
|
59621
59812
|
}
|
|
59622
59813
|
const value = `${largestSize}px`;
|
|
59623
59814
|
if (writtenValueByArea.get(area) === value) {
|
|
59624
|
-
return;
|
|
59815
|
+
return false;
|
|
59625
59816
|
}
|
|
59626
59817
|
writtenValueByArea.set(area, value);
|
|
59627
59818
|
style.setProperty(property, value);
|
|
59819
|
+
return true;
|
|
59628
59820
|
};
|
|
59629
59821
|
|
|
59630
59822
|
installImportMetaCssBuild(import.meta);
|
|
@@ -74415,8 +74607,9 @@ const needsPopupFromMount = (props) => {
|
|
|
74415
74607
|
* (measured by wematch, 2026-09-06: no panel at all for 120ms, then a panel
|
|
74416
74608
|
* with its head missing, then the head arriving from below). So the cut is
|
|
74417
74609
|
* taken only where it hides something the screen would otherwise show — the
|
|
74418
|
-
* band of glass beside an app narrowed with --navi-app-max-width
|
|
74419
|
-
*
|
|
74610
|
+
* band of glass beside an app narrowed with --navi-app-max-width, the fixed
|
|
74611
|
+
* bars beside a popover placed in the safe area — and is a constant
|
|
74612
|
+
* everywhere else: at layer="top" the viewport's own edge cuts the
|
|
74420
74613
|
* travel on the compositor for free, and at layer="local" the clip wrapper
|
|
74421
74614
|
* (a static ancestor, overflow: hidden) does. Where a band exists the cut
|
|
74422
74615
|
* still animates and still can drift under load; the full answer there is a
|
|
@@ -74548,22 +74741,34 @@ const popupCss = /* css */ `
|
|
|
74548
74741
|
0 for a 0 band and 100vmax for any other, a step written without a
|
|
74549
74742
|
branch. See this file's top comment for why an animated cut has to be
|
|
74550
74743
|
that rare. */
|
|
74744
|
+
--x-popup-area-inset-top: var(--navi-app-inset-top, 0px);
|
|
74745
|
+
--x-popup-area-inset-right: var(--navi-app-inset-right, 0px);
|
|
74746
|
+
--x-popup-area-inset-bottom: var(--navi-app-inset-bottom, 0px);
|
|
74747
|
+
--x-popup-area-inset-left: var(--navi-app-inset-left, 0px);
|
|
74551
74748
|
--x-popup-cut-top-at-area: calc(
|
|
74552
74749
|
-1 * var(--container-position-room-top, 100vmax) - 100vmax +
|
|
74553
|
-
min(100vmax, var(--
|
|
74750
|
+
min(100vmax, var(--x-popup-area-inset-top) * 1000000)
|
|
74554
74751
|
);
|
|
74555
74752
|
--x-popup-cut-right-at-area: calc(
|
|
74556
74753
|
-1 * var(--container-position-room-right, 100vmax) - 100vmax +
|
|
74557
|
-
min(100vmax, var(--
|
|
74754
|
+
min(100vmax, var(--x-popup-area-inset-right) * 1000000)
|
|
74558
74755
|
);
|
|
74559
74756
|
--x-popup-cut-bottom-at-area: calc(
|
|
74560
74757
|
-1 * var(--container-position-room-bottom, 100vmax) - 100vmax +
|
|
74561
|
-
min(100vmax, var(--
|
|
74758
|
+
min(100vmax, var(--x-popup-area-inset-bottom) * 1000000)
|
|
74562
74759
|
);
|
|
74563
74760
|
--x-popup-cut-left-at-area: calc(
|
|
74564
74761
|
-1 * var(--container-position-room-left, 100vmax) - 100vmax +
|
|
74565
|
-
min(100vmax, var(--
|
|
74762
|
+
min(100vmax, var(--x-popup-area-inset-left) * 1000000)
|
|
74566
74763
|
);
|
|
74764
|
+
/* Placed in the safe area (Popover's container="safe-area"): past its edge
|
|
74765
|
+
lie the fixed bars, and a popup sliding in comes out from behind them. */
|
|
74766
|
+
&[data-container="safe-area"] {
|
|
74767
|
+
--x-popup-area-inset-top: var(--navi-safe-area-inset-top);
|
|
74768
|
+
--x-popup-area-inset-right: var(--navi-safe-area-inset-right);
|
|
74769
|
+
--x-popup-area-inset-bottom: var(--navi-safe-area-inset-bottom);
|
|
74770
|
+
--x-popup-area-inset-left: var(--navi-safe-area-inset-left);
|
|
74771
|
+
}
|
|
74567
74772
|
/* The translate property is deliberately absent from this list — it
|
|
74568
74773
|
carries where the popup stands, and applyNewPosition (visible_rect.js)
|
|
74569
74774
|
owns it and drives its own transition through the Web Animations API
|
|
@@ -78360,6 +78565,14 @@ const css$C = /* css */`
|
|
|
78360
78565
|
* (`"3appw"`) or a container length (`"3cqw"`).
|
|
78361
78566
|
* @param {string|number} [props.marginWithAnchor=0] - Extra spacing kept
|
|
78362
78567
|
* between the popover and the edges of its anchor, written the same way.
|
|
78568
|
+
* @param {"app"|"safe-area"} [props.container="app"] - The rectangle a
|
|
78569
|
+
* `layer="top"` popover is placed in: docked against with no anchor,
|
|
78570
|
+
* flipped and clamped within with one. `"app"`: the app's own screen, its
|
|
78571
|
+
* fixed bars included — the popover covers them. `"safe-area"`: that screen
|
|
78572
|
+
* less the bars and the device's own bands (`--navi-safe-area-inset-*`), for
|
|
78573
|
+
* something docked over the page that must leave the bars uncovered, a
|
|
78574
|
+
* toast. Followed live as a bar comes or goes. Still in the top layer, so
|
|
78575
|
+
* above what the page holds. See docs/safe_area.md.
|
|
78363
78576
|
* @param {boolean} [props.backdrop=true] - Whether anything is laid between
|
|
78364
78577
|
* the popover and the page at all — asked before what an outside press does
|
|
78365
78578
|
* and before how the backdrop is painted. `false` leaves the page
|
|
@@ -78789,6 +79002,7 @@ const usePopoverProps = props => {
|
|
|
78789
79002
|
// "center" default.
|
|
78790
79003
|
positionAreaWhenAnchorIsInvalid,
|
|
78791
79004
|
marginWithContainer = 0,
|
|
79005
|
+
container = "app",
|
|
78792
79006
|
// "close" → pointer press outside closes (a plain close = commit)
|
|
78793
79007
|
// "cancel" → pointer press outside closes AS A CANCEL (revert)
|
|
78794
79008
|
// "capture"→ absorb the press, stay open
|
|
@@ -78880,6 +79094,7 @@ const usePopoverProps = props => {
|
|
|
78880
79094
|
})
|
|
78881
79095
|
}) : null;
|
|
78882
79096
|
const isTopLayer = layer === "top";
|
|
79097
|
+
const placesInSafeArea = isTopLayer && container === "safe-area";
|
|
78883
79098
|
const ref = props.ref;
|
|
78884
79099
|
const backdropRef = useRef();
|
|
78885
79100
|
// Disarms a still-pending backdrop hide from a previous close (see
|
|
@@ -78928,14 +79143,15 @@ const usePopoverProps = props => {
|
|
|
78928
79143
|
positionAreaFixed,
|
|
78929
79144
|
positionAreaWhenAnchorIsInvalid,
|
|
78930
79145
|
marginWithAnchor,
|
|
78931
|
-
marginWithContainer
|
|
79146
|
+
marginWithContainer,
|
|
79147
|
+
placesInSafeArea
|
|
78932
79148
|
};
|
|
78933
79149
|
const repositionRef = useRef(null);
|
|
78934
79150
|
useEffect(() => {
|
|
78935
79151
|
repositionRef.current?.(new CustomEvent("position_props_change", {
|
|
78936
79152
|
detail: {}
|
|
78937
79153
|
}));
|
|
78938
|
-
}, [positionArea, positionAreaFixed, positionAreaWhenAnchorIsInvalid, marginWithAnchor, marginWithContainer]);
|
|
79154
|
+
}, [positionArea, positionAreaFixed, positionAreaWhenAnchorIsInvalid, marginWithAnchor, marginWithContainer, placesInSafeArea]);
|
|
78939
79155
|
// The freeze is taken where the value changes, not only at open time: a
|
|
78940
79156
|
// popover showing skeletons first says sizing="auto" until its content is
|
|
78941
79157
|
// there, and would otherwise be held at the size of the waiting state.
|
|
@@ -79185,8 +79401,11 @@ const usePopoverProps = props => {
|
|
|
79185
79401
|
positionAreaFixed,
|
|
79186
79402
|
positionAreaWhenAnchorIsInvalid,
|
|
79187
79403
|
marginWithAnchor,
|
|
79188
|
-
marginWithContainer
|
|
79404
|
+
marginWithContainer,
|
|
79405
|
+
placesInSafeArea
|
|
79189
79406
|
} = positionPropsRef.current;
|
|
79407
|
+
// undefined keeps the app's own screen (setPlacementViewportInsets).
|
|
79408
|
+
const viewportInsets = placesInSafeArea ? getSafeAreaInsets() : undefined;
|
|
79190
79409
|
let position;
|
|
79191
79410
|
if (hasAnchorElement) {
|
|
79192
79411
|
const {
|
|
@@ -79225,6 +79444,7 @@ const usePopoverProps = props => {
|
|
|
79225
79444
|
// renderer, same as omitting `container` entirely would resolve
|
|
79226
79445
|
// to on its own.
|
|
79227
79446
|
container: positionedAncestor,
|
|
79447
|
+
viewportInsets,
|
|
79228
79448
|
minLeft,
|
|
79229
79449
|
event: positionEvent
|
|
79230
79450
|
};
|
|
@@ -79271,6 +79491,7 @@ const usePopoverProps = props => {
|
|
|
79271
79491
|
const pickOptions = {
|
|
79272
79492
|
positionArea,
|
|
79273
79493
|
container: positionedAncestor,
|
|
79494
|
+
viewportInsets,
|
|
79274
79495
|
marginWithContainer: resolveMarginInPixels(marginWithContainer, "marginWithContainer"),
|
|
79275
79496
|
event: positionEvent
|
|
79276
79497
|
};
|
|
@@ -79392,6 +79613,20 @@ const usePopoverProps = props => {
|
|
|
79392
79613
|
// while open (e.g. an expand/collapse toggle inside it) — not just when
|
|
79393
79614
|
// the anchor itself moves/resizes/re-anchors.
|
|
79394
79615
|
rectEffect.observeSize(popoverEl);
|
|
79616
|
+
// A bar mounting or leaving moves the safe area and leaves the viewport as
|
|
79617
|
+
// it was, so rectEffect.check() would find nothing changed: placed
|
|
79618
|
+
// directly. "resize", so the popover glides to its new place.
|
|
79619
|
+
if (isTopLayer) {
|
|
79620
|
+
addCleanup(onFixedBarSpaceChange(() => {
|
|
79621
|
+
if (!positionPropsRef.current.placesInSafeArea) {
|
|
79622
|
+
return;
|
|
79623
|
+
}
|
|
79624
|
+
if (deferPlacementWhileCovered(popoverEl)) {
|
|
79625
|
+
return;
|
|
79626
|
+
}
|
|
79627
|
+
placeUnlessAnchorOutOfView(new CustomEvent("resize"));
|
|
79628
|
+
}));
|
|
79629
|
+
}
|
|
79395
79630
|
// Whatever its own backdrop: a modal dialog above leaves all of the page
|
|
79396
79631
|
// inert, a popover without a wall included. layer="local" is left out, its
|
|
79397
79632
|
// ceiling answers to its container.
|
|
@@ -79732,6 +79967,7 @@ const usePopoverProps = props => {
|
|
|
79732
79967
|
id,
|
|
79733
79968
|
tabIndex,
|
|
79734
79969
|
"data-layer": layer,
|
|
79970
|
+
"data-container": placesInSafeArea ? "safe-area" : undefined,
|
|
79735
79971
|
"data-expand-x": expand || expandX ? "" : undefined,
|
|
79736
79972
|
"data-expand-y": expand || expandY ? "" : undefined,
|
|
79737
79973
|
"navi-animation": isAutoAnimation ? undefined : animation,
|