@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/dev/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, createEventGroupLogger, 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";
|
|
@@ -810,6 +810,91 @@ const stringifyForDisplay = (
|
|
|
810
810
|
return String(value);
|
|
811
811
|
};
|
|
812
812
|
|
|
813
|
+
/*
|
|
814
|
+
* Who asks for an action instance right now, and what a run called off for
|
|
815
|
+
* want of askers owes to whoever comes next.
|
|
816
|
+
*
|
|
817
|
+
* Instances are shared by params: two route actions reading the same thing
|
|
818
|
+
* with the same params, or a route action and a component reading it with
|
|
819
|
+
* `run: true`, ask ONE instance. So a binding whose params turn falsy (its
|
|
820
|
+
* route left) does not own the run in flight: it calls it off only when
|
|
821
|
+
* nothing else asks for that instance at that moment. What asks is an asker:
|
|
822
|
+
* a binding of actionRunEffect while its params are truthy, a mounted
|
|
823
|
+
* `useAsyncData` reader with `run: true`. Each says which instance it asks for
|
|
824
|
+
* when the question is put, rather than registering one: a reader of a
|
|
825
|
+
* binding follows the binding's target the moment it moves, not at its next
|
|
826
|
+
* render.
|
|
827
|
+
*
|
|
828
|
+
* Calling a run off is a guess about who is coming: a reader mounting after
|
|
829
|
+
* it (the page arriving), or suspended before its first commit, finds an
|
|
830
|
+
* instance nobody will run again. So the instance is marked, and a `run: true`
|
|
831
|
+
* reader finding it marked asks again, as an arriving binding does when its
|
|
832
|
+
* run() finds the instance aborted. An abort decided by anyone else (abort(),
|
|
833
|
+
* a give-up on a busy popup) carries no mark: aborting does not undo, and the
|
|
834
|
+
* reader leaves it be.
|
|
835
|
+
*/
|
|
836
|
+
|
|
837
|
+
const askerSet = createIterableWeakSet("action_asker");
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* `readAskedInstance()` returns what is asked for now: an instance, a binding
|
|
841
|
+
* (its current target is what counts), or null. The asker is held weakly:
|
|
842
|
+
* whoever adds it keeps it, and removes it once it asks for nothing for good.
|
|
843
|
+
*/
|
|
844
|
+
const addActionAsker = (readAskedInstance) => {
|
|
845
|
+
const asker = { readAskedInstance };
|
|
846
|
+
askerSet.add(asker);
|
|
847
|
+
return asker;
|
|
848
|
+
};
|
|
849
|
+
const removeActionAsker = (asker) => {
|
|
850
|
+
askerSet.delete(asker);
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
const calledOffWeakSet = new WeakSet();
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Aborts the instance's run unless an asker asks for it, and marks a run
|
|
857
|
+
* called off that way (see isCalledOffForWantOfAskers).
|
|
858
|
+
*/
|
|
859
|
+
const callOffUnlessAsked = (action, reason) => {
|
|
860
|
+
const instance = resolveInstance(action);
|
|
861
|
+
for (const asker of askerSet) {
|
|
862
|
+
if (resolveInstance(asker.readAskedInstance()) === instance) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (!instance.abort(reason)) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
// A callback that ignores its abort signal completes all the same.
|
|
870
|
+
if (instance.runningStateSignal.peek() !== ABORTED) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
calledOffWeakSet.add(instance);
|
|
874
|
+
// The mark is for this abort only: once the instance moves, an abort that
|
|
875
|
+
// comes later is somebody else's.
|
|
876
|
+
const unsubscribe = instance.runningStateSignal.subscribe((state) => {
|
|
877
|
+
if (state !== ABORTED) {
|
|
878
|
+
calledOffWeakSet.delete(instance);
|
|
879
|
+
unsubscribe();
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
const isCalledOffForWantOfAskers = (action) => {
|
|
885
|
+
return calledOffWeakSet.has(resolveInstance(action));
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
// A binding counts as the instance it targets now. Read as it stands, never
|
|
889
|
+
// through getCurrentAction(), which would flush a debounced binding.
|
|
890
|
+
const resolveInstance = (action) => {
|
|
891
|
+
let instance = action;
|
|
892
|
+
while (instance && instance.isProxy) {
|
|
893
|
+
instance = getActionPrivateProperties(instance).currentAction;
|
|
894
|
+
}
|
|
895
|
+
return instance;
|
|
896
|
+
};
|
|
897
|
+
|
|
813
898
|
const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
|
|
814
899
|
|
|
815
900
|
/*
|
|
@@ -3752,6 +3837,7 @@ ${lines.join("\n")}`);
|
|
|
3752
3837
|
|
|
3753
3838
|
const actionToRunPrivateProperties =
|
|
3754
3839
|
getActionPrivateProperties(actionToPrerunOrRun);
|
|
3840
|
+
actionToRunPrivateProperties.answeredAsync = false;
|
|
3755
3841
|
let performRunResult;
|
|
3756
3842
|
try {
|
|
3757
3843
|
performRunResult = actionToRunPrivateProperties.performRun({
|
|
@@ -3776,6 +3862,7 @@ ${lines.join("\n")}`);
|
|
|
3776
3862
|
activationWeakSet.add(actionToPrerunOrRun);
|
|
3777
3863
|
|
|
3778
3864
|
if (performRunResult && typeof performRunResult.then === "function") {
|
|
3865
|
+
actionToRunPrivateProperties.answeredAsync = true;
|
|
3779
3866
|
actionPromiseMap.set(actionToPrerunOrRun, performRunResult);
|
|
3780
3867
|
allThenableArray.push(performRunResult);
|
|
3781
3868
|
hasAsync = true;
|
|
@@ -3805,7 +3892,9 @@ ${lines.join("\n")}`);
|
|
|
3805
3892
|
|
|
3806
3893
|
// Execute promotions (prerun -> run requested)
|
|
3807
3894
|
for (const actionToPromote of willPromoteSet) {
|
|
3808
|
-
|
|
3895
|
+
writeActionState(() => {
|
|
3896
|
+
actionToPromote.isPrerunSignal.value = false;
|
|
3897
|
+
});
|
|
3809
3898
|
}
|
|
3810
3899
|
}
|
|
3811
3900
|
|
|
@@ -3814,13 +3903,21 @@ ${lines.join("\n")}`);
|
|
|
3814
3903
|
// that IS carrying it. `await action.run()` then means the same thing to
|
|
3815
3904
|
// every caller: the second one waits for the first one's request and reads
|
|
3816
3905
|
// the same data, instead of being handed `undefined` before the network has
|
|
3817
|
-
// said anything.
|
|
3906
|
+
// said anything. It answers in the tense of that run: the answer of a run
|
|
3907
|
+
// that answered with a promise comes back as one, so `run().catch()` holds
|
|
3908
|
+
// whatever state the instance is in. See the run() JSDoc.
|
|
3818
3909
|
{
|
|
3819
3910
|
for (const actionToJoin of willJoinSet) {
|
|
3820
3911
|
const runningPromise = actionPromiseMap.get(actionToJoin);
|
|
3821
3912
|
if (runningPromise) {
|
|
3822
3913
|
hasAsync = true;
|
|
3823
3914
|
resultArray.push({ type: "async", promise: runningPromise });
|
|
3915
|
+
} else if (getActionPrivateProperties(actionToJoin).answeredAsync) {
|
|
3916
|
+
hasAsync = true;
|
|
3917
|
+
resultArray.push({
|
|
3918
|
+
type: "async",
|
|
3919
|
+
promise: Promise.resolve(actionToJoin.dataSignal.peek()),
|
|
3920
|
+
});
|
|
3824
3921
|
} else {
|
|
3825
3922
|
resultArray.push({
|
|
3826
3923
|
type: "sync",
|
|
@@ -4065,6 +4162,10 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4065
4162
|
* the one that asked first or the one that arrived while the request was
|
|
4066
4163
|
* out. Use `rerun()` to force a fresh run ("refresh", "check now", any
|
|
4067
4164
|
* explicit user intent to go back to the network).
|
|
4165
|
+
*
|
|
4166
|
+
* An asynchronous callback answers with a promise, joined or not:
|
|
4167
|
+
* `ACTION.run().catch(…)` holds in every state. A synchronous one answers
|
|
4168
|
+
* synchronously, and throws its failure.
|
|
4068
4169
|
*/
|
|
4069
4170
|
const run = (options) => {
|
|
4070
4171
|
action.debug(`${action}.run(${stringifyForDisplay(options)})`);
|
|
@@ -4524,7 +4625,9 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4524
4625
|
// another run's, or idle.
|
|
4525
4626
|
return;
|
|
4526
4627
|
}
|
|
4527
|
-
|
|
4628
|
+
writeActionState(() => {
|
|
4629
|
+
runningStateSignal.value = ABORTED;
|
|
4630
|
+
});
|
|
4528
4631
|
internalAbortController.abort(abortReason);
|
|
4529
4632
|
actionAbortMap.delete(action);
|
|
4530
4633
|
if (isPrerun && (globalAbortSignal.aborted || abortSignal.aborted)) {
|
|
@@ -4551,7 +4654,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4551
4654
|
|
|
4552
4655
|
actionAbortMap.set(action, abort);
|
|
4553
4656
|
|
|
4554
|
-
|
|
4657
|
+
writeActionState(() => {
|
|
4555
4658
|
if (valueSignal.peek() === undefined) {
|
|
4556
4659
|
// An action saying nothing of its own may still have a value the
|
|
4557
4660
|
// page kept from the previous document (see kept_values.js).
|
|
@@ -4630,7 +4733,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4630
4733
|
* before the UI attempts to render the now-missing resource.
|
|
4631
4734
|
*/
|
|
4632
4735
|
|
|
4633
|
-
|
|
4736
|
+
writeActionState(() => {
|
|
4634
4737
|
const value = resultToValue
|
|
4635
4738
|
? resultToValue(runResult, action)
|
|
4636
4739
|
: runResult;
|
|
@@ -4662,7 +4765,9 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4662
4765
|
error === internalAbortSignal.reason) ||
|
|
4663
4766
|
error.name === "AbortError";
|
|
4664
4767
|
if (isAbort) {
|
|
4665
|
-
|
|
4768
|
+
writeActionState(() => {
|
|
4769
|
+
runningStateSignal.value = ABORTED;
|
|
4770
|
+
});
|
|
4666
4771
|
if (isPrerun && abortSignal.aborted) {
|
|
4667
4772
|
prerunProtectionRegistry.unprotect(action);
|
|
4668
4773
|
}
|
|
@@ -4676,7 +4781,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4676
4781
|
if (isPrerun) {
|
|
4677
4782
|
markErrorAsPrerunFailure(error);
|
|
4678
4783
|
}
|
|
4679
|
-
|
|
4784
|
+
writeActionState(() => {
|
|
4680
4785
|
errorSignal.value = error;
|
|
4681
4786
|
runningStateSignal.value = FAILED;
|
|
4682
4787
|
onError?.(error, { event, action, args });
|
|
@@ -4771,7 +4876,7 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4771
4876
|
}
|
|
4772
4877
|
|
|
4773
4878
|
actionPromiseMap.delete(action);
|
|
4774
|
-
|
|
4879
|
+
writeActionState(() => {
|
|
4775
4880
|
if (!willRunOrPrerun) {
|
|
4776
4881
|
errorSignal.value = undefined;
|
|
4777
4882
|
valueSignal.value = valueInitial;
|
|
@@ -4789,6 +4894,9 @@ const createAction = (callback, rootOptions = {}) => {
|
|
|
4789
4894
|
provisionalValue,
|
|
4790
4895
|
inheritData,
|
|
4791
4896
|
keepsAnswers: keptAnswerRegistry !== null,
|
|
4897
|
+
// Whether the last run answered with a promise, written by
|
|
4898
|
+
// updateActions: the tense a run joining this instance answers in.
|
|
4899
|
+
answeredAsync: false,
|
|
4792
4900
|
|
|
4793
4901
|
performRun,
|
|
4794
4902
|
performReset,
|
|
@@ -5272,6 +5380,37 @@ const isPlainObject$2 = (obj) => {
|
|
|
5272
5380
|
);
|
|
5273
5381
|
};
|
|
5274
5382
|
|
|
5383
|
+
// Every state write of a run (start, end, failure, abort, reset), batched. At
|
|
5384
|
+
// the end of the batch preact runs the effects those writes woke up, anywhere
|
|
5385
|
+
// in the app, and rethrows the first one that threw: from there it would fail
|
|
5386
|
+
// this run, or leave it RUNNING, or stop the update half way. That throw is
|
|
5387
|
+
// the effect's, so it is reported on its own, as action_error_report.js
|
|
5388
|
+
// reports an error (thrown from a timer), and the action carries on. What
|
|
5389
|
+
// `write` throws (resultToValue, onComplete…) is the run's, and goes on.
|
|
5390
|
+
const writeActionState = (write) => {
|
|
5391
|
+
let writeThrew = false;
|
|
5392
|
+
let writeError;
|
|
5393
|
+
try {
|
|
5394
|
+
batch(() => {
|
|
5395
|
+
try {
|
|
5396
|
+
write();
|
|
5397
|
+
} catch (e) {
|
|
5398
|
+
writeThrew = true;
|
|
5399
|
+
writeError = e;
|
|
5400
|
+
}
|
|
5401
|
+
});
|
|
5402
|
+
} catch (effectError) {
|
|
5403
|
+
setTimeout(() => {
|
|
5404
|
+
throw effectError;
|
|
5405
|
+
});
|
|
5406
|
+
}
|
|
5407
|
+
if (writeThrew) {
|
|
5408
|
+
throw writeError;
|
|
5409
|
+
}
|
|
5410
|
+
};
|
|
5411
|
+
|
|
5412
|
+
const askerByBindingWeakMap = new WeakMap();
|
|
5413
|
+
|
|
5275
5414
|
/**
|
|
5276
5415
|
* Reactively runs an action whenever the params derived from signals change.
|
|
5277
5416
|
*
|
|
@@ -5368,8 +5507,9 @@ const actionRunEffect = (
|
|
|
5368
5507
|
if (actionTargetPrevious && actionTarget) {
|
|
5369
5508
|
// params changed
|
|
5370
5509
|
if (!asking) {
|
|
5371
|
-
// falsy params, don't run
|
|
5372
|
-
|
|
5510
|
+
// falsy params, don't run; the instance is shared by params, and
|
|
5511
|
+
// something else may still ask for it (see action_askers.js)
|
|
5512
|
+
callOffUnlessAsked(actionTargetPrevious, "abortOnFalsyParams");
|
|
5373
5513
|
return;
|
|
5374
5514
|
}
|
|
5375
5515
|
if (!askingBefore) {
|
|
@@ -5395,6 +5535,11 @@ const actionRunEffect = (
|
|
|
5395
5535
|
},
|
|
5396
5536
|
...options,
|
|
5397
5537
|
});
|
|
5538
|
+
// Held by the binding, as long as it lives: askers are held weakly.
|
|
5539
|
+
askerByBindingWeakMap.set(
|
|
5540
|
+
actionRunnedByThisEffect,
|
|
5541
|
+
addActionAsker(() => (asking ? actionRunnedByThisEffect : null)),
|
|
5542
|
+
);
|
|
5398
5543
|
if (asking) {
|
|
5399
5544
|
runUnwatched(() =>
|
|
5400
5545
|
actionRunnedByThisEffect.run({ reason: "initial truthy params" }),
|
|
@@ -10267,8 +10412,10 @@ const ROUTE_ACTION_BINDING_OPTIONS = new Set([
|
|
|
10267
10412
|
/**
|
|
10268
10413
|
* Binds an action to a route: it runs when the route matches, with the params
|
|
10269
10414
|
* the effect reads off the address, runs again on a `reload()`, and is aborted
|
|
10270
|
-
* when the route is left
|
|
10271
|
-
*
|
|
10415
|
+
* when the route is left, unless something else still asks for the same read
|
|
10416
|
+
* with the same params: another route action, a mounted `useAsyncData` with
|
|
10417
|
+
* `run: true`. Its data, its wait and its failure are read by the page through
|
|
10418
|
+
* `useAsyncData`.
|
|
10272
10419
|
*
|
|
10273
10420
|
* A page's CODE is declared the same way — the import is one more thing the
|
|
10274
10421
|
* address asks for, started with the data rather than after it:
|
|
@@ -33055,6 +33202,11 @@ const useForceRender = () => {
|
|
|
33055
33202
|
* failure a prerun left (a `prerun: true` head start, a press that preran
|
|
33056
33203
|
* it): nothing on screen asked for that run, so this asks again, once.
|
|
33057
33204
|
*
|
|
33205
|
+
* While mounted, it asks for that instance as a route action does: a route
|
|
33206
|
+
* action reading the same thing with the same params does not call the run
|
|
33207
|
+
* off under it when its route is left. A run called off that way before
|
|
33208
|
+
* this component was there to ask is asked again.
|
|
33209
|
+
*
|
|
33058
33210
|
* Prefer a `routeAction` wherever the parameter is one the address holds (a
|
|
33059
33211
|
* path param, a search param bound to a `stateSignal`), popup included: it is
|
|
33060
33212
|
* asked for when the address changes, with everything else that address
|
|
@@ -33204,6 +33356,20 @@ const useActionAsyncData = (action, {
|
|
|
33204
33356
|
}
|
|
33205
33357
|
useOnLoad(action, onLoad);
|
|
33206
33358
|
usePoll(action, poll);
|
|
33359
|
+
const routePage = useContext(RoutePageContext);
|
|
33360
|
+
|
|
33361
|
+
// `run: true` asks for the instance as long as this reader is mounted: a
|
|
33362
|
+
// binding sharing it whose route is left does not call the run off under it
|
|
33363
|
+
// (see action_askers.js). Asked through the binding itself, whose target
|
|
33364
|
+
// moves before this renders again.
|
|
33365
|
+
const askedRef = useRef(null);
|
|
33366
|
+
askedRef.current = run ? action : null;
|
|
33367
|
+
useLayoutEffect(() => {
|
|
33368
|
+
const asker = addActionAsker(() => askedRef.current);
|
|
33369
|
+
return () => {
|
|
33370
|
+
removeActionAsker(asker);
|
|
33371
|
+
};
|
|
33372
|
+
}, []);
|
|
33207
33373
|
|
|
33208
33374
|
// The instance the binding points at, remembered from one render to the
|
|
33209
33375
|
// next: a binding that comes back to params whose instance failed — a search
|
|
@@ -33216,6 +33382,11 @@ const useActionAsyncData = (action, {
|
|
|
33216
33382
|
const instanceRef = useRef(instance);
|
|
33217
33383
|
const backOnFailure = instanceRef.current !== instance && action.runningStateSignal.peek() === FAILED;
|
|
33218
33384
|
instanceRef.current = instance;
|
|
33385
|
+
// A run called off because nothing asked for it any more, before this reader
|
|
33386
|
+
// was there to ask (mounting after, or suspended before its first commit):
|
|
33387
|
+
// nobody else will run it again. Not from a page the address has left, whose
|
|
33388
|
+
// own route action is what called it off.
|
|
33389
|
+
const calledOffBeforeThisReader = isCalledOffForWantOfAskers(action) && !(routePage && !routePage.isShown());
|
|
33219
33390
|
// `run: true` — this component owns the request, so it is what starts it, and
|
|
33220
33391
|
// it starts it from the render rather than from an effect: a component that
|
|
33221
33392
|
// suspends has no effects, so a run written in one would be waiting for
|
|
@@ -33223,7 +33394,7 @@ const useActionAsyncData = (action, {
|
|
|
33223
33394
|
// no-op — which is what makes starting it while rendering sound. From there
|
|
33224
33395
|
// the wait is the ordinary one: suspended into <Loading>, or drawn by the
|
|
33225
33396
|
// component under `loading: true`, exactly as for data someone else ran.
|
|
33226
|
-
if (run && action.paramsSignal.peek() !== undefined && (runIsOwed(action) || backOnFailure)) {
|
|
33397
|
+
if (run && action.paramsSignal.peek() !== undefined && (runIsOwed(action) || backOnFailure || calledOffBeforeThisReader)) {
|
|
33227
33398
|
// Nothing waits on this run: the failure is held by the action, and this
|
|
33228
33399
|
// hook is what reads it back.
|
|
33229
33400
|
runUnwatched(() => action.run({
|
|
@@ -33246,7 +33417,6 @@ const useActionAsyncData = (action, {
|
|
|
33246
33417
|
// has already processed the settlement and the detached DOM is discarded.
|
|
33247
33418
|
const runningState = action.runningStateSignal.peek();
|
|
33248
33419
|
const [, setTick] = useState(0);
|
|
33249
|
-
const routePage = useContext(RoutePageContext);
|
|
33250
33420
|
// What this render read of the action, for the subscriptions below to tell
|
|
33251
33421
|
// a change that came between the render and them from none at all.
|
|
33252
33422
|
const readRef = useRef(null);
|
|
@@ -33454,6 +33624,13 @@ const useActionAsyncData = (action, {
|
|
|
33454
33624
|
actionPendingPromiseWeakMap.delete(action);
|
|
33455
33625
|
unsubscribe();
|
|
33456
33626
|
resolve();
|
|
33627
|
+
} else if (reason === "loading" && state === ABORTED) {
|
|
33628
|
+
// The run waited on was called off: drawn again, this reader asks
|
|
33629
|
+
// for it again when nothing else will (calledOffBeforeThisReader),
|
|
33630
|
+
// or waits for the next run as idle.
|
|
33631
|
+
actionPendingPromiseWeakMap.delete(action);
|
|
33632
|
+
unsubscribe();
|
|
33633
|
+
resolve();
|
|
33457
33634
|
}
|
|
33458
33635
|
});
|
|
33459
33636
|
});
|
|
@@ -61068,6 +61245,7 @@ const measureDrawing = ({
|
|
|
61068
61245
|
* never seen under a bar, and after every read of the commit that queued them.
|
|
61069
61246
|
*/
|
|
61070
61247
|
|
|
61248
|
+
|
|
61071
61249
|
// Several bars can share an edge — during a page transition the outgoing and
|
|
61072
61250
|
// the incoming one are both mounted. They are all pinned to that same edge, so
|
|
61073
61251
|
// they overlap: the room to give back is the largest of them, not their sum,
|
|
@@ -61085,6 +61263,12 @@ const pendingSizeMapByArea = new Map();
|
|
|
61085
61263
|
let flushMicrotaskQueued = false;
|
|
61086
61264
|
let flushFrame = null;
|
|
61087
61265
|
|
|
61266
|
+
// The safe area moves with these writes and no resize comes with them: what is
|
|
61267
|
+
// placed in it (a Popover with container="safe-area") is told here.
|
|
61268
|
+
const [publishFixedBarSpaceChange, subscribeFixedBarSpaceChange] =
|
|
61269
|
+
createPubSub();
|
|
61270
|
+
const onFixedBarSpaceChange = subscribeFixedBarSpaceChange;
|
|
61271
|
+
|
|
61088
61272
|
// A size a render produced: a prop, the children or a theme variable changed
|
|
61089
61273
|
// the bar, and the room it takes is given back before that commit is painted.
|
|
61090
61274
|
/**
|
|
@@ -61135,13 +61319,19 @@ const queueSize = (area, barElement, size) => {
|
|
|
61135
61319
|
};
|
|
61136
61320
|
|
|
61137
61321
|
const flushPendingSizes = () => {
|
|
61322
|
+
let written = false;
|
|
61138
61323
|
for (const [area, pendingSizeMap] of pendingSizeMapByArea) {
|
|
61139
61324
|
for (const [barElement, size] of pendingSizeMap) {
|
|
61140
61325
|
storeSize(area, barElement, size);
|
|
61141
61326
|
}
|
|
61142
|
-
writeSpace(area)
|
|
61327
|
+
if (writeSpace(area)) {
|
|
61328
|
+
written = true;
|
|
61329
|
+
}
|
|
61143
61330
|
}
|
|
61144
61331
|
pendingSizeMapByArea.clear();
|
|
61332
|
+
if (written) {
|
|
61333
|
+
publishFixedBarSpaceChange();
|
|
61334
|
+
}
|
|
61145
61335
|
};
|
|
61146
61336
|
|
|
61147
61337
|
const storeSize = (area, barElement, size) => {
|
|
@@ -61168,18 +61358,20 @@ const writeSpace = (area) => {
|
|
|
61168
61358
|
const property = `--navi-fixed-bar-space-${area}`;
|
|
61169
61359
|
const { style } = document.documentElement;
|
|
61170
61360
|
if (sizeMap.size === 0) {
|
|
61171
|
-
if (writtenValueByArea.has(area)) {
|
|
61172
|
-
|
|
61173
|
-
style.removeProperty(property);
|
|
61361
|
+
if (!writtenValueByArea.has(area)) {
|
|
61362
|
+
return false;
|
|
61174
61363
|
}
|
|
61175
|
-
|
|
61364
|
+
writtenValueByArea.delete(area);
|
|
61365
|
+
style.removeProperty(property);
|
|
61366
|
+
return true;
|
|
61176
61367
|
}
|
|
61177
61368
|
const value = `${largestSize}px`;
|
|
61178
61369
|
if (writtenValueByArea.get(area) === value) {
|
|
61179
|
-
return;
|
|
61370
|
+
return false;
|
|
61180
61371
|
}
|
|
61181
61372
|
writtenValueByArea.set(area, value);
|
|
61182
61373
|
style.setProperty(property, value);
|
|
61374
|
+
return true;
|
|
61183
61375
|
};
|
|
61184
61376
|
|
|
61185
61377
|
installImportMetaCssBuild(import.meta);
|
|
@@ -76091,8 +76283,9 @@ const needsPopupFromMount = (props) => {
|
|
|
76091
76283
|
* (measured by wematch, 2026-09-06: no panel at all for 120ms, then a panel
|
|
76092
76284
|
* with its head missing, then the head arriving from below). So the cut is
|
|
76093
76285
|
* taken only where it hides something the screen would otherwise show — the
|
|
76094
|
-
* band of glass beside an app narrowed with --navi-app-max-width
|
|
76095
|
-
*
|
|
76286
|
+
* band of glass beside an app narrowed with --navi-app-max-width, the fixed
|
|
76287
|
+
* bars beside a popover placed in the safe area — and is a constant
|
|
76288
|
+
* everywhere else: at layer="top" the viewport's own edge cuts the
|
|
76096
76289
|
* travel on the compositor for free, and at layer="local" the clip wrapper
|
|
76097
76290
|
* (a static ancestor, overflow: hidden) does. Where a band exists the cut
|
|
76098
76291
|
* still animates and still can drift under load; the full answer there is a
|
|
@@ -76224,22 +76417,34 @@ const popupCss = /* css */ `
|
|
|
76224
76417
|
0 for a 0 band and 100vmax for any other, a step written without a
|
|
76225
76418
|
branch. See this file's top comment for why an animated cut has to be
|
|
76226
76419
|
that rare. */
|
|
76420
|
+
--x-popup-area-inset-top: var(--navi-app-inset-top, 0px);
|
|
76421
|
+
--x-popup-area-inset-right: var(--navi-app-inset-right, 0px);
|
|
76422
|
+
--x-popup-area-inset-bottom: var(--navi-app-inset-bottom, 0px);
|
|
76423
|
+
--x-popup-area-inset-left: var(--navi-app-inset-left, 0px);
|
|
76227
76424
|
--x-popup-cut-top-at-area: calc(
|
|
76228
76425
|
-1 * var(--container-position-room-top, 100vmax) - 100vmax +
|
|
76229
|
-
min(100vmax, var(--
|
|
76426
|
+
min(100vmax, var(--x-popup-area-inset-top) * 1000000)
|
|
76230
76427
|
);
|
|
76231
76428
|
--x-popup-cut-right-at-area: calc(
|
|
76232
76429
|
-1 * var(--container-position-room-right, 100vmax) - 100vmax +
|
|
76233
|
-
min(100vmax, var(--
|
|
76430
|
+
min(100vmax, var(--x-popup-area-inset-right) * 1000000)
|
|
76234
76431
|
);
|
|
76235
76432
|
--x-popup-cut-bottom-at-area: calc(
|
|
76236
76433
|
-1 * var(--container-position-room-bottom, 100vmax) - 100vmax +
|
|
76237
|
-
min(100vmax, var(--
|
|
76434
|
+
min(100vmax, var(--x-popup-area-inset-bottom) * 1000000)
|
|
76238
76435
|
);
|
|
76239
76436
|
--x-popup-cut-left-at-area: calc(
|
|
76240
76437
|
-1 * var(--container-position-room-left, 100vmax) - 100vmax +
|
|
76241
|
-
min(100vmax, var(--
|
|
76438
|
+
min(100vmax, var(--x-popup-area-inset-left) * 1000000)
|
|
76242
76439
|
);
|
|
76440
|
+
/* Placed in the safe area (Popover's container="safe-area"): past its edge
|
|
76441
|
+
lie the fixed bars, and a popup sliding in comes out from behind them. */
|
|
76442
|
+
&[data-container="safe-area"] {
|
|
76443
|
+
--x-popup-area-inset-top: var(--navi-safe-area-inset-top);
|
|
76444
|
+
--x-popup-area-inset-right: var(--navi-safe-area-inset-right);
|
|
76445
|
+
--x-popup-area-inset-bottom: var(--navi-safe-area-inset-bottom);
|
|
76446
|
+
--x-popup-area-inset-left: var(--navi-safe-area-inset-left);
|
|
76447
|
+
}
|
|
76243
76448
|
/* The translate property is deliberately absent from this list — it
|
|
76244
76449
|
carries where the popup stands, and applyNewPosition (visible_rect.js)
|
|
76245
76450
|
owns it and drives its own transition through the Web Animations API
|
|
@@ -80131,6 +80336,14 @@ const css$C = /* css */`
|
|
|
80131
80336
|
* (`"3appw"`) or a container length (`"3cqw"`).
|
|
80132
80337
|
* @param {string|number} [props.marginWithAnchor=0] - Extra spacing kept
|
|
80133
80338
|
* between the popover and the edges of its anchor, written the same way.
|
|
80339
|
+
* @param {"app"|"safe-area"} [props.container="app"] - The rectangle a
|
|
80340
|
+
* `layer="top"` popover is placed in: docked against with no anchor,
|
|
80341
|
+
* flipped and clamped within with one. `"app"`: the app's own screen, its
|
|
80342
|
+
* fixed bars included — the popover covers them. `"safe-area"`: that screen
|
|
80343
|
+
* less the bars and the device's own bands (`--navi-safe-area-inset-*`), for
|
|
80344
|
+
* something docked over the page that must leave the bars uncovered, a
|
|
80345
|
+
* toast. Followed live as a bar comes or goes. Still in the top layer, so
|
|
80346
|
+
* above what the page holds. See docs/safe_area.md.
|
|
80134
80347
|
* @param {boolean} [props.backdrop=true] - Whether anything is laid between
|
|
80135
80348
|
* the popover and the page at all — asked before what an outside press does
|
|
80136
80349
|
* and before how the backdrop is painted. `false` leaves the page
|
|
@@ -80562,6 +80775,7 @@ const usePopoverProps = props => {
|
|
|
80562
80775
|
// "center" default.
|
|
80563
80776
|
positionAreaWhenAnchorIsInvalid,
|
|
80564
80777
|
marginWithContainer = 0,
|
|
80778
|
+
container = "app",
|
|
80565
80779
|
// "close" → pointer press outside closes (a plain close = commit)
|
|
80566
80780
|
// "cancel" → pointer press outside closes AS A CANCEL (revert)
|
|
80567
80781
|
// "capture"→ absorb the press, stay open
|
|
@@ -80653,6 +80867,10 @@ const usePopoverProps = props => {
|
|
|
80653
80867
|
})
|
|
80654
80868
|
}) : null;
|
|
80655
80869
|
const isTopLayer = layer === "top";
|
|
80870
|
+
if (!isTopLayer && container !== "app") {
|
|
80871
|
+
console.warn(`Popover: container="${container}" is for layer="top". A layer="local" popover is placed in its positioned ancestor; put that ancestor where it should be.`);
|
|
80872
|
+
}
|
|
80873
|
+
const placesInSafeArea = isTopLayer && container === "safe-area";
|
|
80656
80874
|
const ref = props.ref;
|
|
80657
80875
|
const backdropRef = useRef();
|
|
80658
80876
|
// Disarms a still-pending backdrop hide from a previous close (see
|
|
@@ -80704,14 +80922,15 @@ const usePopoverProps = props => {
|
|
|
80704
80922
|
positionAreaFixed,
|
|
80705
80923
|
positionAreaWhenAnchorIsInvalid,
|
|
80706
80924
|
marginWithAnchor,
|
|
80707
|
-
marginWithContainer
|
|
80925
|
+
marginWithContainer,
|
|
80926
|
+
placesInSafeArea
|
|
80708
80927
|
};
|
|
80709
80928
|
const repositionRef = useRef(null);
|
|
80710
80929
|
useEffect(() => {
|
|
80711
80930
|
repositionRef.current?.(new CustomEvent("position_props_change", {
|
|
80712
80931
|
detail: {}
|
|
80713
80932
|
}));
|
|
80714
|
-
}, [positionArea, positionAreaFixed, positionAreaWhenAnchorIsInvalid, marginWithAnchor, marginWithContainer]);
|
|
80933
|
+
}, [positionArea, positionAreaFixed, positionAreaWhenAnchorIsInvalid, marginWithAnchor, marginWithContainer, placesInSafeArea]);
|
|
80715
80934
|
// The freeze is taken where the value changes, not only at open time: a
|
|
80716
80935
|
// popover showing skeletons first says sizing="auto" until its content is
|
|
80717
80936
|
// there, and would otherwise be held at the size of the waiting state.
|
|
@@ -80970,8 +81189,11 @@ const usePopoverProps = props => {
|
|
|
80970
81189
|
positionAreaFixed,
|
|
80971
81190
|
positionAreaWhenAnchorIsInvalid,
|
|
80972
81191
|
marginWithAnchor,
|
|
80973
|
-
marginWithContainer
|
|
81192
|
+
marginWithContainer,
|
|
81193
|
+
placesInSafeArea
|
|
80974
81194
|
} = positionPropsRef.current;
|
|
81195
|
+
// undefined keeps the app's own screen (setPlacementViewportInsets).
|
|
81196
|
+
const viewportInsets = placesInSafeArea ? getSafeAreaInsets() : undefined;
|
|
80975
81197
|
let position;
|
|
80976
81198
|
if (hasAnchorElement) {
|
|
80977
81199
|
const {
|
|
@@ -81010,6 +81232,7 @@ const usePopoverProps = props => {
|
|
|
81010
81232
|
// renderer, same as omitting `container` entirely would resolve
|
|
81011
81233
|
// to on its own.
|
|
81012
81234
|
container: positionedAncestor,
|
|
81235
|
+
viewportInsets,
|
|
81013
81236
|
minLeft,
|
|
81014
81237
|
event: positionEvent
|
|
81015
81238
|
};
|
|
@@ -81056,6 +81279,7 @@ const usePopoverProps = props => {
|
|
|
81056
81279
|
const pickOptions = {
|
|
81057
81280
|
positionArea,
|
|
81058
81281
|
container: positionedAncestor,
|
|
81282
|
+
viewportInsets,
|
|
81059
81283
|
marginWithContainer: resolveMarginInPixels(marginWithContainer, "marginWithContainer"),
|
|
81060
81284
|
event: positionEvent
|
|
81061
81285
|
};
|
|
@@ -81177,6 +81401,20 @@ const usePopoverProps = props => {
|
|
|
81177
81401
|
// while open (e.g. an expand/collapse toggle inside it) — not just when
|
|
81178
81402
|
// the anchor itself moves/resizes/re-anchors.
|
|
81179
81403
|
rectEffect.observeSize(popoverEl);
|
|
81404
|
+
// A bar mounting or leaving moves the safe area and leaves the viewport as
|
|
81405
|
+
// it was, so rectEffect.check() would find nothing changed: placed
|
|
81406
|
+
// directly. "resize", so the popover glides to its new place.
|
|
81407
|
+
if (isTopLayer) {
|
|
81408
|
+
addCleanup(onFixedBarSpaceChange(() => {
|
|
81409
|
+
if (!positionPropsRef.current.placesInSafeArea) {
|
|
81410
|
+
return;
|
|
81411
|
+
}
|
|
81412
|
+
if (deferPlacementWhileCovered(popoverEl)) {
|
|
81413
|
+
return;
|
|
81414
|
+
}
|
|
81415
|
+
placeUnlessAnchorOutOfView(new CustomEvent("resize"));
|
|
81416
|
+
}));
|
|
81417
|
+
}
|
|
81180
81418
|
// Whatever its own backdrop: a modal dialog above leaves all of the page
|
|
81181
81419
|
// inert, a popover without a wall included. layer="local" is left out, its
|
|
81182
81420
|
// ceiling answers to its container.
|
|
@@ -81518,6 +81756,7 @@ const usePopoverProps = props => {
|
|
|
81518
81756
|
id,
|
|
81519
81757
|
tabIndex,
|
|
81520
81758
|
"data-layer": layer,
|
|
81759
|
+
"data-container": placesInSafeArea ? "safe-area" : undefined,
|
|
81521
81760
|
"data-expand-x": expand || expandX ? "" : undefined,
|
|
81522
81761
|
"data-expand-y": expand || expandY ? "" : undefined,
|
|
81523
81762
|
"navi-animation": isAutoAnimation ? undefined : animation,
|