@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.
@@ -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. Re-resolves live as the pointer
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
- const isDocked = dockedOnSmallTouchScreen && smallTouchScreenSignal.value;
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 nothing. It runs the callback with
33900
- * the range the list asks for, writes what comes back into the store, and hands
33901
- * the rows back as store items — never a copy of the JSON, so the relations a
33902
- * row reads are the shared ones and a request sent from a row is read back on
33903
- * it. A row following its own fields through a write reads them from the store
33904
- * (`RESOURCE.useById(id)`): an update replaces the item object, and the one the
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
- * Keeping nothing does not mean hearing nothing: a mutation that decides who
33911
- * belongs to the collection (a POST, a DELETE, whatever `rerunOn.GET_RANGE`
33912
- * says) bumps `invalidationSignal`, and whoever reads slices through it goes
33913
- * and asks again — the counterpart, for a reader, of what a rerun is for an
33914
- * action. Every reader made by `bindParams` shares the signal of the one it
33915
- * comes from: the params say which slices are read, not which collection.
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
- { store, params: boundParams, invalidationSignal = signal(0) },
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
- return createRangeReader(actionName, callback, {
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
- pagesRef.current = {
58696
- byIndex: new Map(),
58697
- count: undefined
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(false);
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 || pages.byIndex.size <= 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
- for (const index of pages.byIndex.keys()) {
58769
- if (index < keepFrom || index > keepTo) {
58770
- pages.byIndex.delete(index);
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
  };