@jsenv/navi 0.29.176 → 0.29.178

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.
@@ -34356,6 +34356,15 @@ const useUIStateController = (
34356
34356
  ) {
34357
34357
  continue;
34358
34358
  }
34359
+ // Only the sibling that IS checked has anything to let go of.
34360
+ // Asked anyway, an already-unchecked sibling still runs the
34361
+ // tail of its own setUIState (uiAction and all) — and a group
34362
+ // of N radios then pays N-1 of those on every selection, which
34363
+ // is what made every row of a selectable list re-render per
34364
+ // click.
34365
+ if (siblingController.uiState === undefined) {
34366
+ continue;
34367
+ }
34359
34368
  siblingController.setUIState(undefined, siblingUncheckEvent);
34360
34369
  }
34361
34370
  }
@@ -36368,7 +36377,7 @@ const ControlgroupChildrenWrapper = ({
36368
36377
  readOnly,
36369
36378
  loading,
36370
36379
  boundAction,
36371
- actionRequester
36380
+ actionRequesterSignal
36372
36381
  }) => jsx(MessagePropsRefContext.Provider, {
36373
36382
  value: undefined,
36374
36383
  children: jsx(ControlIdContext.Provider, {
@@ -36388,7 +36397,7 @@ const ControlgroupChildrenWrapper = ({
36388
36397
  children: jsx(ActionContext.Provider, {
36389
36398
  value: boundAction,
36390
36399
  children: jsx(ActionRequesterContext.Provider, {
36391
- value: actionRequester,
36400
+ value: actionRequesterSignal,
36392
36401
  children: children
36393
36402
  })
36394
36403
  })
@@ -37480,7 +37489,20 @@ const useControlgroupProps = (props, {
37480
37489
  if (implicitReadOnly && !props.readOnly) {
37481
37490
  props.readOnly = true;
37482
37491
  }
37483
- const [actionRequester, setActionRequester] = useState();
37492
+
37493
+ // Who pressed for the action the group is running, held in a SIGNAL and not
37494
+ // in render state: every control in the group reads it (to wear the group's
37495
+ // busy state when it is the one that asked), so as a plain context value a
37496
+ // new requester would force-re-render every one of them — a click on one row
37497
+ // of a selectable list re-rendering the whole list. The signal keeps the
37498
+ // context value stable; each control subscribes to its own "is it me"
37499
+ // computed (see loadingFromParent below), so a requester change re-renders
37500
+ // exactly the control it leaves and the one it lands on.
37501
+ const actionRequesterSignalRef = useRef(null);
37502
+ if (actionRequesterSignalRef.current === null) {
37503
+ actionRequesterSignalRef.current = signal(undefined);
37504
+ }
37505
+ const actionRequesterSignal = actionRequesterSignalRef.current;
37484
37506
  const [controlRootProps, controlgroupProps] = useInteractiveProps(props, {
37485
37507
  uiStateController: uiGroupStateController,
37486
37508
  boundAction,
@@ -37504,8 +37526,8 @@ const useControlgroupProps = (props, {
37504
37526
  readOnly,
37505
37527
  loading,
37506
37528
  boundAction,
37507
- actionRequester
37508
- }), [uiGroupStateController, controlgroupProps.name, controlgroupProps.required, disabled, readOnly, loading, boundAction, actionRequester]);
37529
+ actionRequesterSignal
37530
+ }), [uiGroupStateController, controlgroupProps.name, controlgroupProps.required, disabled, readOnly, loading, boundAction, actionRequesterSignal]);
37509
37531
  return [controlRootProps, {
37510
37532
  ...controlgroupProps,
37511
37533
  "name": undefined,
@@ -37517,7 +37539,7 @@ const useControlgroupProps = (props, {
37517
37539
  // any element wears: a <fieldset maxlength> means nothing.
37518
37540
  "maxLength": undefined,
37519
37541
  "onnavi_action_allowed": e => {
37520
- setActionRequester(e.detail.requester);
37542
+ actionRequesterSignal.value = e.detail.requester;
37521
37543
  controlgroupProps.onnavi_action_allowed(e);
37522
37544
  },
37523
37545
  "navi-control-group": ""
@@ -37680,7 +37702,13 @@ const useInteractiveProps = (props, {
37680
37702
  const controlReadOnlyFromAbove = useContext(ReadOnlyContext);
37681
37703
  const controlRequired = useContext(RequiredContext);
37682
37704
  const controlLoadingFromAbove = useContext(LoadingContext$1);
37683
- const parentActionRequester = useContext(ActionRequesterContext);
37705
+ // A SIGNAL holding the element whose press the group's action is wearing
37706
+ // (see useControlgroupProps). Subscribed to through a per-control computed
37707
+ // rather than read raw: `.value` read here directly would re-render every
37708
+ // control of the group on each new requester — the computed's boolean only
37709
+ // changes for the control the requester leaves and the one it lands on.
37710
+ const parentActionRequesterSignal = useContext(ActionRequesterContext);
37711
+ const isActionRequesterComputed = useMemo(() => parentActionRequesterSignal ? computed(() => parentActionRequesterSignal.value === ref.current) : null, [parentActionRequesterSignal]);
37684
37712
  const parallelGuard = useContext(ParallelGuardContext);
37685
37713
  const parentAction = useContext(ActionContext);
37686
37714
  const actionStatus = useActionStatus(boundAction);
@@ -37707,7 +37735,7 @@ const useInteractiveProps = (props, {
37707
37735
  // Busy because the group above is running the action THIS control asked
37708
37736
  // for (a submit button wearing its form's submission), as opposed to busy
37709
37737
  // on its own say-so.
37710
- const loadingFromParent = Boolean(controlLoading && parentActionRequester === ref.current);
37738
+ const loadingFromParent = Boolean(controlLoading && isActionRequesterComputed?.value);
37711
37739
  const loadingBase = loading || loadingFromParent;
37712
37740
  // Read-only because the selection above guards its length
37713
37741
  // (`maxLengthGuard`) and this one would make it longer: it can be pointed
@@ -42414,6 +42442,20 @@ const BETWEEN_TABS_ATTRIBUTE = "data-nav-between-tabs";
42414
42442
  // in pixels. A rough line, deliberately: nothing here needs to agree with a
42415
42443
  // paragraph, only to move the row about as far as a notch moves a page.
42416
42444
  const WHEEL_LINE_SIZE = 16;
42445
+
42446
+ // The animation a slide container puts on this row to bring the travel's
42447
+ // progress home (see animateTravelProgress in slide_container.jsx) — the cheap
42448
+ // source of that number while a travel plays (see syncTabScroll). Its starting
42449
+ // value is read off the keyframes here, once: the frame loop only ever asks
42450
+ // the animation how far along it is.
42451
+ const rememberProgressAnimation = (navElement, travel) => {
42452
+ const animation = navElement.getAnimations().find(candidate => {
42453
+ const keyframes = candidate.effect?.getKeyframes?.();
42454
+ return keyframes?.[0] && "--slide-travel-progress" in keyframes[0];
42455
+ });
42456
+ travel.progressAnimation = animation;
42457
+ travel.progressFrom = animation ? parseFloat(animation.effect.getKeyframes()[0]["--slide-travel-progress"]) || 0 : 0;
42458
+ };
42417
42459
  const css$W = /* css */`@layer navi {
42418
42460
  .navi_nav {
42419
42461
  --nav-border: none;
@@ -42740,8 +42782,27 @@ const Nav = ({
42740
42782
  if (currentIndex === -1) {
42741
42783
  // On a slide no tab in this row names: there is no tab to sit under.
42742
42784
  navElement.removeAttribute("data-nav-indicator-measured");
42785
+ indicatorPaintedForRef.current = null;
42743
42786
  return;
42744
42787
  }
42788
+ // Everything below is measured off the DOM (offsets force a layout, the
42789
+ // colours a style resolve), and what it depends on is exactly two facts:
42790
+ // which tab is current and which one the travel leans toward. A travel
42791
+ // announces its state several times between those two changing — painted
42792
+ // each time, the same measurement is paid again on frames the travel is
42793
+ // drawn on. The size observer clears the memo, since the same pair on a
42794
+ // resized row measures differently.
42795
+ const towardAreaNow = containerElement.getAttribute("data-slide-travel-toward");
42796
+ const paintedFor = indicatorPaintedForRef.current;
42797
+ if (paintedFor && paintedFor.currentTabElement === tabElements[currentIndex] && paintedFor.towardArea === towardAreaNow && paintedFor.tabCount === tabElements.length && paintedFor.vertical === vertical) {
42798
+ return;
42799
+ }
42800
+ indicatorPaintedForRef.current = {
42801
+ currentTabElement: tabElements[currentIndex],
42802
+ towardArea: towardAreaNow,
42803
+ tabCount: tabElements.length,
42804
+ vertical
42805
+ };
42745
42806
  const measure = tabElement => vertical ? {
42746
42807
  position: tabElement.offsetTop,
42747
42808
  length: tabElement.offsetHeight
@@ -42763,7 +42824,7 @@ const Nav = ({
42763
42824
  }
42764
42825
  };
42765
42826
  const currentMeasure = measure(tabElements[currentIndex]);
42766
- const towardArea = containerElement.getAttribute("data-slide-travel-toward");
42827
+ const towardArea = towardAreaNow;
42767
42828
  const towardIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === towardArea);
42768
42829
  let positionDelta = 0;
42769
42830
  let lengthDelta = 0;
@@ -42799,6 +42860,9 @@ const Nav = ({
42799
42860
  // about the row as it is now.
42800
42861
  const paintIndicatorGeometryRef = useRef(null);
42801
42862
  paintIndicatorGeometryRef.current = paintIndicatorGeometry;
42863
+ // What the last paint measured for — see the memo at the top of
42864
+ // paintIndicatorGeometry.
42865
+ const indicatorPaintedForRef = useRef(null);
42802
42866
 
42803
42867
  // A row of tabs wider than the box holding it scrolls (the caller asks for
42804
42868
  // that with overflowX) — and then saying which tab one is on is not enough,
@@ -42870,6 +42934,20 @@ const Nav = ({
42870
42934
  }
42871
42935
  let travel = travelScrollRef.current;
42872
42936
  if (!travel || travel.from !== currentTabElement || travel.toward !== towardTabElement) {
42937
+ // Everything that holds for the whole travel is read here and only here
42938
+ // — the frame loop below runs sixty times a second and every layout or
42939
+ // style read in it is paid on the very frames the travel is drawn on.
42940
+ // A row that cannot scroll has nowhere to ride the travel to: remembered
42941
+ // on the travel, and the loop is not even armed for it.
42942
+ const rideable = navElement.scrollWidth > navElement.clientWidth + 1 || navElement.scrollHeight > navElement.clientHeight + 1;
42943
+ if (!rideable) {
42944
+ travelScrollRef.current = {
42945
+ from: currentTabElement,
42946
+ toward: towardTabElement,
42947
+ rideable
42948
+ };
42949
+ return;
42950
+ }
42873
42951
  // The two ends, measured once for this travel: measuring them again on a
42874
42952
  // row that has since moved would be measuring from the answer.
42875
42953
  const fromOffsets = getScrollIntoViewScopedOffsets(currentTabElement, {
@@ -42885,14 +42963,46 @@ const Nav = ({
42885
42963
  travel = {
42886
42964
  from: currentTabElement,
42887
42965
  toward: towardTabElement,
42966
+ rideable,
42888
42967
  left: fromOffsets.left,
42889
42968
  top: fromOffsets.top,
42890
42969
  leftDelta: (towardOffsets.left - fromOffsets.left) * sign,
42891
42970
  topDelta: (towardOffsets.top - fromOffsets.top) * sign
42892
42971
  };
42972
+ rememberProgressAnimation(navElement, travel);
42893
42973
  travelScrollRef.current = travel;
42894
42974
  }
42895
- const progress = parseFloat(getComputedStyle(navElement).getPropertyValue("--slide-travel-progress")) || 0;
42975
+ if (!travel.rideable) {
42976
+ return;
42977
+ }
42978
+ // How far between the two slides the picture stands, read WITHOUT
42979
+ // getComputedStyle: computing the style of an element whose registered
42980
+ // property is animating is a full style resolve, and per frame it is what
42981
+ // ate the travel's frame budget on a throttled phone. The number has two
42982
+ // cheap sources, and the container guarantees one of them on this very
42983
+ // element (see paintTravelProgress / animateTravelProgress in
42984
+ // slide_container.jsx): a travel that plays on its own ANIMATES the
42985
+ // property here — its eased progress is on the animation itself
42986
+ // (getComputedTiming().progress is the transformed, post-easing progress)
42987
+ // — and a finger writes it as INLINE style per frame, readable as a plain
42988
+ // string.
42989
+ let progress;
42990
+ let eased = travel.progressAnimation ? travel.progressAnimation.effect.getComputedTiming().progress : null;
42991
+ if (eased === null) {
42992
+ // No animation, or one that is over: the travel may have been
42993
+ // re-animated since (a chained travel between the same two tabs), so
42994
+ // look again before falling back to the finger's inline value.
42995
+ rememberProgressAnimation(navElement, travel);
42996
+ eased = travel.progressAnimation ? travel.progressAnimation.effect.getComputedTiming().progress : null;
42997
+ }
42998
+ if (eased === null) {
42999
+ progress = parseFloat(navElement.style.getPropertyValue("--slide-travel-progress")) || 0;
43000
+ } else {
43001
+ // The animation walks the property from `progressFrom` to 0 (see
43002
+ // animateTravelProgress); its value at this instant is the same
43003
+ // interpolation, done here in arithmetic instead of in style.
43004
+ progress = travel.progressFrom * (1 - eased);
43005
+ }
42896
43006
  navElement.scrollTo({
42897
43007
  left: travel.left + progress * travel.leftDelta,
42898
43008
  top: travel.top + progress * travel.topDelta,
@@ -42956,6 +43066,8 @@ const Nav = ({
42956
43066
  // being pushed around by hand.
42957
43067
  useLayoutEffect(() => {
42958
43068
  const sizeObserver = new ResizeObserver(() => {
43069
+ // The same pair of tabs measures differently on a row of another size.
43070
+ indicatorPaintedForRef.current = null;
42959
43071
  paintIndicatorGeometryRef.current();
42960
43072
  // What was brought into sight was brought there for another row.
42961
43073
  tabInSightRef.current = null;
@@ -46984,7 +47096,9 @@ const useNextResolver = () => useContext(NextResolverContext);
46984
47096
  * ResolverIndexContext tracks which resolver is next so that when a resolver
46985
47097
  * re-renders and calls Next, the chain resumes from the correct position.
46986
47098
  */
46987
- const createComponentResolver = resolvers => {
47099
+ const createComponentResolver = (resolvers, {
47100
+ pure
47101
+ } = {}) => {
46988
47102
  const ResolverIndexContext = createContext(0);
46989
47103
  const ChainRunner = props => {
46990
47104
  const index = useContext(ResolverIndexContext);
@@ -47024,7 +47138,45 @@ const createComponentResolver = resolvers => {
47024
47138
  })
47025
47139
  });
47026
47140
  };
47027
- return renderComponent;
47141
+ if (!pure) {
47142
+ return renderComponent;
47143
+ }
47144
+ // The chain does not run again for props that say the same thing. The
47145
+ // signals integration already promises this for any component that READS a
47146
+ // signal ("props all === the previous ones → no render", relied on across
47147
+ // list.jsx) — but a chain head reads none, so without this a parent
47148
+ // re-rendering walks every resolver of every instance to conclude nothing
47149
+ // changed. For a component rendered by the hundred (a list's items), that
47150
+ // walk IS the cost of the parent's render. What still updates through the
47151
+ // bail: context (Preact re-renders subscribers directly) and signals (their
47152
+ // own subscription) — which is every way navi hands data down other than
47153
+ // props.
47154
+ // A FUNCTION component assigning shouldComponentUpdate on its own instance
47155
+ // (the pattern preact/compat's memo uses), and deliberately not a class: a
47156
+ // ref given to a class component is attached to the class INSTANCE, while a
47157
+ // function component receives it as a plain prop — and the chain forwards
47158
+ // it to the element the caller was reaching for.
47159
+ function PureRenderComponent(props) {
47160
+ this.shouldComponentUpdate = pureShouldComponentUpdate;
47161
+ return renderComponent(props);
47162
+ }
47163
+ return PureRenderComponent;
47164
+ };
47165
+ function pureShouldComponentUpdate(nextProps) {
47166
+ return shallowDiffers(this.props, nextProps);
47167
+ }
47168
+ const shallowDiffers = (a, b) => {
47169
+ for (const key in a) {
47170
+ if (!(key in b)) {
47171
+ return true;
47172
+ }
47173
+ }
47174
+ for (const key in b) {
47175
+ if (a[key] !== b[key]) {
47176
+ return true;
47177
+ }
47178
+ }
47179
+ return false;
47028
47180
  };
47029
47181
 
47030
47182
  const ButtonRouteResolver = props => {
@@ -53225,9 +53377,17 @@ const ratioOfOneTravel = (track, from, to, targetBefore) => {
53225
53377
  const trackOffsetPx = (track, containerElement) => {
53226
53378
  const trackRect = track.getBoundingClientRect();
53227
53379
  const containerRect = containerElement.getBoundingClientRect();
53380
+ // Against the box the track RESTS in — the content box, past the border
53381
+ // (clientLeft/clientTop are the border widths; the box never has a
53382
+ // scrollbar nor padding, see the top-of-file comment on padding). Measured
53383
+ // against the border box, a border reads as that many pixels of translate
53384
+ // nothing ever asked for — and it COMPOUNDS: every travel interrupted
53385
+ // mid-flight re-measures the track and carries the error into its "from",
53386
+ // so rapid back-and-forth gestures stack a 1px border into a visible
53387
+ // vertical drift of the whole track.
53228
53388
  return {
53229
- x: trackRect.x - containerRect.x,
53230
- y: trackRect.y - containerRect.y
53389
+ x: trackRect.x - containerRect.x - containerElement.clientLeft,
53390
+ y: trackRect.y - containerRect.y - containerElement.clientTop
53231
53391
  };
53232
53392
  };
53233
53393
  const offsetToPx = (offset, box) => {
@@ -66795,7 +66955,13 @@ const LIST_ITEM_STYLE_CSS_VARS = {
66795
66955
  * search-driven scroll-to-top-match behavior. `matchRanges` are [start, end]
66796
66956
  * ranges highlighted via the CSS Highlight API.
66797
66957
  */
66798
- const ListItem = createComponentResolver([ListItemFirstResolver, ListItemRowResolver, ListItemSkeletonResolver, ListItemSelectableResolver, ListItemHeaderOrFooterResolver, ListItemPresentationResolver, ListItemUI]);
66958
+ const ListItem = createComponentResolver([ListItemFirstResolver, ListItemRowResolver, ListItemSkeletonResolver, ListItemSelectableResolver, ListItemHeaderOrFooterResolver, ListItemPresentationResolver, ListItemUI],
66959
+ // Rendered by the hundred, and almost always with the same primitive props:
66960
+ // a selection change re-renders the list, and every row but two has nothing
66961
+ // to change.
66962
+ {
66963
+ pure: true
66964
+ });
66799
66965
  List.Item = ListItem;
66800
66966
 
66801
66967
  // Everything the list knows about the collection while its children are being
@@ -82085,18 +82251,18 @@ const css = /* css */`.navi_side_panel {
82085
82251
  height: var(--navi-side-panel-height, auto);
82086
82252
 
82087
82253
  &[data-layer="top"] {
82088
- --popover-max-height: var(--navi-vvh);
82089
- --popover-maxmax-height: var(--navi-vvh);
82090
- --popover-maxmax-width: var(--navi-vvw);
82091
- --dialog-maxmax-height: var(--navi-vvh);
82092
- --dialog-maxmax-width: var(--navi-vvw);
82254
+ --popover-max-height: var(--navi-app-height);
82255
+ --popover-maxmax-height: var(--navi-app-height);
82256
+ --popover-maxmax-width: var(--navi-app-width);
82257
+ --dialog-maxmax-height: var(--navi-app-height);
82258
+ --dialog-maxmax-width: var(--navi-app-width);
82093
82259
 
82094
82260
  &[navi-side="left"], &[navi-side="right"] {
82095
- height: var(--navi-side-panel-height, var(--navi-vvh));
82261
+ height: var(--navi-side-panel-height, var(--navi-app-height));
82096
82262
  }
82097
82263
 
82098
82264
  &[navi-side="top"], &[navi-side="bottom"] {
82099
- width: var(--navi-side-panel-width, var(--navi-vvw));
82265
+ width: var(--navi-side-panel-width, var(--navi-app-width));
82100
82266
  }
82101
82267
 
82102
82268
  &[navi-side="left"] {
@@ -82226,8 +82392,10 @@ const css = /* css */`.navi_side_panel {
82226
82392
  * @param {string|number} [props.minHeight] - Forwarded as-is to `Popup`'s
82227
82393
  * own `minHeight`.
82228
82394
  * @param {"top"|"local"} [props.layer="top"] - `"top"` (default): docks
82229
- * against the viewport (real top-layer rendering, matches a fixed,
82230
- * always-on-screen drawer). `"local"`: docks against the panel's own
82395
+ * against the app's own screen — the viewport, narrowed to the app's
82396
+ * rectangle when it declares `--navi-app-max-width` (real top-layer
82397
+ * rendering, matches a fixed, always-on-screen drawer). `"local"`: docks
82398
+ * against the panel's own
82231
82399
  * positioned DOM ancestor instead, confined to (and clipped by) it — for
82232
82400
  * a drawer that only takes over part of the page rather than the whole
82233
82401
  * viewport.