@jsenv/navi 0.29.55 → 0.29.57
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 +157 -29
- package/dist/jsenv_navi.js.map +10 -6
- package/docs/AI_INSTRUCTIONS.md +2 -1
- package/docs/list_refresh.md +39 -7
- package/docs/resource.md +8 -2
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -6,7 +6,7 @@ import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visua
|
|
|
6
6
|
export { coarsePointerSignal } from "./jsenv_navi_side_effects.js";
|
|
7
7
|
import { elementIsFocusable, createPubSub, dispatchInternalCustomEvent, dispatchCustomEvent, getElementSignature, findEvent, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createIterableWeakSet, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, mergeTwoStyles, normalizeStyles, resolveCSSSize, hasCSSSizeUnit, resolveOklchLightness, contrastColor, closestOpenableAncestor, isAncestorOpen, observeAncestorOpenState, getAncestorOpenType, scrollRoomTowards, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, findBefore, findAfter, initFocusGroup, scrollIntoViewScoped, getScrollContainer, canScroll, measureWidestChildRow, performTabNavigation, wheelGestureIsTakenFrom, releaseWheelGesture, claimWheelGesture, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, stringifyStyle as stringifyStyle$1 } from "@jsenv/dom";
|
|
8
8
|
export { contrastColor, findEvent, startDragTo } from "@jsenv/dom";
|
|
9
|
-
import { signal, computed, effect, batch, useSignal } from "@preact/signals";
|
|
9
|
+
import { signal, computed, effect, batch, untracked, useSignal } from "@preact/signals";
|
|
10
10
|
import { createContext, isValidElement, h, Fragment, render, toChildArray, options, cloneElement } from "preact";
|
|
11
11
|
import { useContext, useLayoutEffect, useCallback, useRef, useState, useEffect, useMemo, useId, useErrorBoundary } from "preact/hooks";
|
|
12
12
|
import { jsx, jsxs, Fragment as Fragment$1 } from "preact/jsx-runtime";
|
|
@@ -29642,7 +29642,10 @@ const css$W = /* css */`
|
|
|
29642
29642
|
* from where the finger just tapped, and size alone would dock a narrow
|
|
29643
29643
|
* desktop window, which is still a mouse. It supplies defaults for
|
|
29644
29644
|
* `positionArea`, `marginWithContainer`, `expandX` and `scrollCapture`, so
|
|
29645
|
-
* any of them can still be pinned explicitly.
|
|
29645
|
+
* any of them can still be pinned explicitly. Ignored entirely when `expandY`
|
|
29646
|
+
* (or `expand`) is set: a dialog already filling the height is on the bottom
|
|
29647
|
+
* edge docking would bring it to, so docking could only take away the shape
|
|
29648
|
+
* the caller asked for. Re-resolves live as the pointer
|
|
29646
29649
|
* type or the window size changes. A sheet resting on the bottom edge is also
|
|
29647
29650
|
* pushed back down to close it, held by its header (a direct child `Box` with
|
|
29648
29651
|
* the `header` prop) — or from anywhere when it has none. See `swipe_to_close.js`.
|
|
@@ -29661,7 +29664,7 @@ const css$W = /* css */`
|
|
|
29661
29664
|
* container allows (`--dialog-maxmax-width`). Set by
|
|
29662
29665
|
* `dockedOnSmallTouchScreen` on a small touch screen.
|
|
29663
29666
|
* @param {boolean} [props.expandY] - Same, vertically
|
|
29664
|
-
* (`--dialog-maxmax-height`).
|
|
29667
|
+
* (`--dialog-maxmax-height`). Cancels `dockedOnSmallTouchScreen`.
|
|
29665
29668
|
* @param {string|number} [props.marginWithContainer="3appw"] - Minimum gap kept
|
|
29666
29669
|
* between the dialog and the edges of its container, whatever its
|
|
29667
29670
|
* `positionArea`: it both caps the dialog's own size (via
|
|
@@ -29993,10 +29996,15 @@ const useDialogProps = props => {
|
|
|
29993
29996
|
});
|
|
29994
29997
|
const isModal = layer === "top";
|
|
29995
29998
|
const ref = props.ref;
|
|
29999
|
+
const expandY = Boolean(expand) || Boolean(expandYProp);
|
|
29996
30000
|
// Only a small touch screen changes anything: on a mouse — and on a touch
|
|
29997
30001
|
// screen too big to reach the bottom edge of — a dialog already wants to be
|
|
29998
30002
|
// the centered box it is by default, so there is nothing to resolve.
|
|
29999
|
-
|
|
30003
|
+
// expandY cancels the docking outright: docking exists to bring the dialog
|
|
30004
|
+
// down to the edge the thumb is on, and a dialog already filling the height
|
|
30005
|
+
// is on that edge — all docking could still do is take away the shape the
|
|
30006
|
+
// caller asked for (and arm a swipe-down on something that never rose).
|
|
30007
|
+
const isDocked = dockedOnSmallTouchScreen && smallTouchScreenSignal.value && !expandY;
|
|
30000
30008
|
const positionArea = positionAreaProp ?? (isDocked ? DOCKED.positionArea : "center");
|
|
30001
30009
|
const marginWithContainer = marginWithContainerProp ?? (isDocked ? DOCKED.marginWithContainer :
|
|
30002
30010
|
// A share of whatever holds the dialog: the app's own screen for a
|
|
@@ -30009,7 +30017,6 @@ const useDialogProps = props => {
|
|
|
30009
30017
|
// handing them over — the docked default only applies when neither was said
|
|
30010
30018
|
const expandXUnset = expand === undefined && expandXProp === undefined;
|
|
30011
30019
|
const expandX = expandXUnset ? isDocked && DOCKED.expandX : Boolean(expand) || Boolean(expandXProp);
|
|
30012
|
-
const expandY = Boolean(expand) || Boolean(expandYProp);
|
|
30013
30020
|
const scrollCapture = scrollCaptureProp ?? (isDocked ? DOCKED.scrollCapture : false);
|
|
30014
30021
|
const backdropRef = useRef();
|
|
30015
30022
|
// Disarms a still-pending backdrop hide from a previous close (see
|
|
@@ -33896,31 +33903,61 @@ const getParamScope = (params) => {
|
|
|
33896
33903
|
* replays it, and takes a place in the rerun graph — two things a slice must
|
|
33897
33904
|
* not do: the list already holds the slices it received and glues them back
|
|
33898
33905
|
* together, and a mutation would otherwise send every slice ever loaded back to
|
|
33899
|
-
* the network at once. So the reader keeps
|
|
33900
|
-
* the range the list asks for, writes what comes back into the store, and
|
|
33901
|
-
* the rows back as store items — never a copy of the JSON, so the
|
|
33902
|
-
* row reads are the shared ones and a request sent from a row is
|
|
33903
|
-
* it. A row following its own fields through a write reads them
|
|
33904
|
-
* (`RESOURCE.useById(id)`): an update replaces the item object,
|
|
33905
|
-
* list is holding is the one it was given.
|
|
33906
|
+
* the network at once. So the reader keeps no response. It runs the callback
|
|
33907
|
+
* with the range the list asks for, writes what comes back into the store, and
|
|
33908
|
+
* hands the rows back as store items — never a copy of the JSON, so the
|
|
33909
|
+
* relations a row reads are the shared ones and a request sent from a row is
|
|
33910
|
+
* read back on it. A row following its own fields through a write reads them
|
|
33911
|
+
* from the store (`RESOURCE.useById(id)`): an update replaces the item object,
|
|
33912
|
+
* and the one the list is holding is the one it was given.
|
|
33913
|
+
*
|
|
33914
|
+
* What the reader does keep is the collection's composition: which rank holds
|
|
33915
|
+
* which id, and how many ranks there are, per resolved bound params. The rows
|
|
33916
|
+
* themselves are in the store already; the composition is the one thing about a
|
|
33917
|
+
* paginated collection the store cannot model, and without it a list that left
|
|
33918
|
+
* the screen comes back to skeletons and a request for rows it had a second
|
|
33919
|
+
* before. Ids and a count, so nothing here can hold a stale copy of a row, and
|
|
33920
|
+
* a row dropped from the store simply stops resolving. A list that comes back
|
|
33921
|
+
* draws the composition it left and asks again for the window it draws — the
|
|
33922
|
+
* revalidation an invalidation goes through, from a fresh mount.
|
|
33906
33923
|
*
|
|
33907
33924
|
* The reader is a function, so a list feeds on it the way it feeds on any other
|
|
33908
33925
|
* source: `itemsAction={GAME.GET_RANGE.bindParams({ radar })}`.
|
|
33909
33926
|
*
|
|
33910
|
-
*
|
|
33911
|
-
*
|
|
33912
|
-
*
|
|
33913
|
-
* and asks again — the counterpart, for a reader,
|
|
33914
|
-
* action. Every reader made by `bindParams` shares
|
|
33915
|
-
*
|
|
33927
|
+
* A mutation that decides who belongs to the collection (a POST, a DELETE,
|
|
33928
|
+
* whatever `rerunOn.GET_RANGE` says) bumps `invalidationSignal` and drops the
|
|
33929
|
+
* compositions: they stand for an order that is gone. Whoever reads slices
|
|
33930
|
+
* through the reader then goes and asks again — the counterpart, for a reader,
|
|
33931
|
+
* of what a rerun is for an action. Every reader made by `bindParams` shares
|
|
33932
|
+
* the signal and the compositions of the one it comes from: the params say
|
|
33933
|
+
* which slices are read, not which collection.
|
|
33916
33934
|
*/
|
|
33917
33935
|
|
|
33918
33936
|
|
|
33919
33937
|
const createRangeReader = (
|
|
33920
33938
|
actionName,
|
|
33921
33939
|
callback,
|
|
33922
|
-
{
|
|
33940
|
+
{
|
|
33941
|
+
store,
|
|
33942
|
+
params: boundParams,
|
|
33943
|
+
invalidationSignal = signal(0),
|
|
33944
|
+
compositionSet = new Set(),
|
|
33945
|
+
},
|
|
33923
33946
|
) => {
|
|
33947
|
+
// Which composition this reader is about: the values its params hold, not its
|
|
33948
|
+
// own identity, so two `bindParams({ scope: "thread" })` made in two places
|
|
33949
|
+
// read and write the same one. There are as many compositions as there are
|
|
33950
|
+
// collections read through this reader — a handful, walked with the deep
|
|
33951
|
+
// comparison the rest of the codebase memoizes on.
|
|
33952
|
+
const currentParams = () => untracked(() => resolveParams(boundParams));
|
|
33953
|
+
const findComposition = (params) => {
|
|
33954
|
+
for (const composition of compositionSet) {
|
|
33955
|
+
if (compareTwoJsValues(composition.params, params)) {
|
|
33956
|
+
return composition;
|
|
33957
|
+
}
|
|
33958
|
+
}
|
|
33959
|
+
return null;
|
|
33960
|
+
};
|
|
33924
33961
|
const readRange = async (range = {}) => {
|
|
33925
33962
|
const { signal, ...rangeParams } = range;
|
|
33926
33963
|
const paramsResolved = { ...resolveParams(boundParams), ...rangeParams };
|
|
@@ -33952,14 +33989,80 @@ const createRangeReader = (
|
|
|
33952
33989
|
// stand for a composition that is gone.
|
|
33953
33990
|
readRange.invalidationSignal = invalidationSignal;
|
|
33954
33991
|
readRange.invalidate = () => {
|
|
33992
|
+
compositionSet.clear();
|
|
33955
33993
|
invalidationSignal.value = invalidationSignal.peek() + 1;
|
|
33956
33994
|
};
|
|
33995
|
+
// The rows of a composition, drawn from the store: a rank whose row is gone
|
|
33996
|
+
// from the store resolves to nothing and is asked for again.
|
|
33997
|
+
readRange.readComposition = () => {
|
|
33998
|
+
const composition = findComposition(currentParams());
|
|
33999
|
+
if (!composition) {
|
|
34000
|
+
return null;
|
|
34001
|
+
}
|
|
34002
|
+
const byIndex = new Map();
|
|
34003
|
+
untracked(() => {
|
|
34004
|
+
for (const [index, id] of composition.idByIndex) {
|
|
34005
|
+
const item = store.select(id);
|
|
34006
|
+
if (item) {
|
|
34007
|
+
byIndex.set(index, item);
|
|
34008
|
+
}
|
|
34009
|
+
}
|
|
34010
|
+
});
|
|
34011
|
+
return { byIndex, count: composition.count };
|
|
34012
|
+
};
|
|
34013
|
+
// `replace` is a revalidation: the ranks that are not in what just came back
|
|
34014
|
+
// stood for a composition that has moved on. Otherwise the ranks are merged,
|
|
34015
|
+
// so two lists reading the same collection through their own windows add up
|
|
34016
|
+
// to one composition instead of taking turns erasing each other.
|
|
34017
|
+
readRange.writeComposition = ({ byIndex, count, replace }) => {
|
|
34018
|
+
const params = currentParams();
|
|
34019
|
+
let composition = findComposition(params);
|
|
34020
|
+
if (!composition) {
|
|
34021
|
+
composition = { params, idByIndex: new Map(), count };
|
|
34022
|
+
compositionSet.add(composition);
|
|
34023
|
+
} else if (replace) {
|
|
34024
|
+
composition.idByIndex = new Map();
|
|
34025
|
+
}
|
|
34026
|
+
composition.count = count;
|
|
34027
|
+
for (const [index, item] of byIndex) {
|
|
34028
|
+
const id = item ? item[store.idKey] : undefined;
|
|
34029
|
+
if (id !== undefined) {
|
|
34030
|
+
composition.idByIndex.set(index, id);
|
|
34031
|
+
}
|
|
34032
|
+
}
|
|
34033
|
+
};
|
|
34034
|
+
// The same trade a list makes with the rows it holds, applied to what is kept
|
|
34035
|
+
// for the next mount. Two lists on one collection each trim by their own
|
|
34036
|
+
// window; the rows a list is drawing stay on its screen regardless — a rank
|
|
34037
|
+
// dropped here is one that gets asked for again after a remount.
|
|
34038
|
+
readRange.trimComposition = (keepFrom, keepTo, budget) => {
|
|
34039
|
+
const composition = findComposition(currentParams());
|
|
34040
|
+
if (!composition || !budget || composition.idByIndex.size <= budget) {
|
|
34041
|
+
return;
|
|
34042
|
+
}
|
|
34043
|
+
for (const index of composition.idByIndex.keys()) {
|
|
34044
|
+
if (index < keepFrom || index > keepTo) {
|
|
34045
|
+
composition.idByIndex.delete(index);
|
|
34046
|
+
}
|
|
34047
|
+
}
|
|
34048
|
+
};
|
|
34049
|
+
// Memoized for the reasons an action's bindParams is (see actions.js): params
|
|
34050
|
+
// and the reader they make have synchronized lifetimes, and params equal in
|
|
34051
|
+
// value give back the reader that already exists instead of a second one.
|
|
34052
|
+
const readerByParams = createJsValueWeakMap();
|
|
33957
34053
|
readRange.bindParams = (paramsToBind) => {
|
|
33958
|
-
|
|
34054
|
+
const existing = readerByParams.get(paramsToBind);
|
|
34055
|
+
if (existing) {
|
|
34056
|
+
return existing;
|
|
34057
|
+
}
|
|
34058
|
+
const reader = createRangeReader(actionName, callback, {
|
|
33959
34059
|
store,
|
|
33960
34060
|
params: boundParams ? { ...boundParams, ...paramsToBind } : paramsToBind,
|
|
33961
34061
|
invalidationSignal,
|
|
34062
|
+
compositionSet,
|
|
33962
34063
|
});
|
|
34064
|
+
readerByParams.set(paramsToBind, reader);
|
|
34065
|
+
return reader;
|
|
33963
34066
|
};
|
|
33964
34067
|
return readRange;
|
|
33965
34068
|
};
|
|
@@ -58690,19 +58793,30 @@ const useItemStore = ({
|
|
|
58690
58793
|
itemsAction,
|
|
58691
58794
|
memoryBudget
|
|
58692
58795
|
}) => {
|
|
58796
|
+
// What the source kept of the collection when the screen it was on went away
|
|
58797
|
+
// (a range reader keeps the composition: see resource_range_reader.js). The
|
|
58798
|
+
// rows are drawn from it right away and the window is asked for again — the
|
|
58799
|
+
// revalidation below, entered from a fresh mount rather than from a write.
|
|
58693
58800
|
const pagesRef = useRef(null);
|
|
58801
|
+
let restored = false;
|
|
58694
58802
|
if (!pagesRef.current) {
|
|
58695
|
-
|
|
58696
|
-
|
|
58697
|
-
|
|
58698
|
-
|
|
58803
|
+
const composition = typeof itemsAction === "function" && itemsAction.readComposition ? itemsAction.readComposition() : null;
|
|
58804
|
+
if (composition && composition.count !== undefined) {
|
|
58805
|
+
pagesRef.current = composition;
|
|
58806
|
+
restored = true;
|
|
58807
|
+
} else {
|
|
58808
|
+
pagesRef.current = {
|
|
58809
|
+
byIndex: new Map(),
|
|
58810
|
+
count: undefined
|
|
58811
|
+
};
|
|
58812
|
+
}
|
|
58699
58813
|
}
|
|
58700
58814
|
const pages = pagesRef.current;
|
|
58701
58815
|
const [, setPageVersion] = useState(0);
|
|
58702
58816
|
// The rows held are out of date and the run has not asked for the new ones
|
|
58703
58817
|
// yet. They stay on screen until the answer comes: what is drawn is from
|
|
58704
58818
|
// before, which is not the same thing as nothing to draw.
|
|
58705
|
-
const staleRef = useRef(
|
|
58819
|
+
const staleRef = useRef(restored);
|
|
58706
58820
|
const [refreshing, setRefreshing] = useState(false);
|
|
58707
58821
|
// A source that says when what it reads has moved (a resource range reader
|
|
58708
58822
|
// does: see rerunOn.GET_RANGE) is heard here — a write deciding who belongs
|
|
@@ -58760,16 +58874,21 @@ const useItemStore = ({
|
|
|
58760
58874
|
// trade the render window makes, one order of magnitude further out.
|
|
58761
58875
|
forget: (windowFrom, windowTo) => {
|
|
58762
58876
|
const budget = memoryBudget === undefined ? ITEM_STORE_MAX_DEFAULT : memoryBudget;
|
|
58763
|
-
if (!budget
|
|
58877
|
+
if (!budget) {
|
|
58764
58878
|
return;
|
|
58765
58879
|
}
|
|
58766
58880
|
const keepFrom = windowFrom - ITEM_STORE_KEEP_AROUND;
|
|
58767
58881
|
const keepTo = windowTo + ITEM_STORE_KEEP_AROUND;
|
|
58768
|
-
|
|
58769
|
-
|
|
58770
|
-
|
|
58882
|
+
if (pages.byIndex.size > budget) {
|
|
58883
|
+
for (const index of pages.byIndex.keys()) {
|
|
58884
|
+
if (index < keepFrom || index > keepTo) {
|
|
58885
|
+
pages.byIndex.delete(index);
|
|
58886
|
+
}
|
|
58771
58887
|
}
|
|
58772
58888
|
}
|
|
58889
|
+
if (typeof itemsAction === "function" && itemsAction.trimComposition) {
|
|
58890
|
+
itemsAction.trimComposition(keepFrom, keepTo, budget);
|
|
58891
|
+
}
|
|
58773
58892
|
},
|
|
58774
58893
|
retry: () => {
|
|
58775
58894
|
const request = requestRef.current;
|
|
@@ -58935,6 +59054,15 @@ const useItemStore = ({
|
|
|
58935
59054
|
i++;
|
|
58936
59055
|
}
|
|
58937
59056
|
pages.count = pageCount;
|
|
59057
|
+
// Which rank holds which id, kept by the source so a list drawing
|
|
59058
|
+
// this collection again finds it drawn (see readComposition above).
|
|
59059
|
+
if (itemsAction.writeComposition) {
|
|
59060
|
+
itemsAction.writeComposition({
|
|
59061
|
+
byIndex: pages.byIndex,
|
|
59062
|
+
count: pageCount,
|
|
59063
|
+
replace: revalidating
|
|
59064
|
+
});
|
|
59065
|
+
}
|
|
58938
59066
|
virtual.pagesSignal.value = virtual.pagesSignal.peek() + 1;
|
|
58939
59067
|
setPageVersion(version => version + 1);
|
|
58940
59068
|
};
|