@jsenv/navi 0.29.115 → 0.29.117
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 +115 -35
- package/dist/jsenv_navi.js.map +15 -6
- package/docs/AI_INSTRUCTIONS.md +3 -3
- package/docs/control_value.md +43 -0
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -29628,7 +29628,11 @@ registerNaviCommand("--navi-void", (source) => {
|
|
|
29628
29628
|
};
|
|
29629
29629
|
});
|
|
29630
29630
|
|
|
29631
|
-
|
|
29631
|
+
// "--navi-update:smooth" asks the control to be SEEN moving to the value — a
|
|
29632
|
+
// wheel scrolls to it rather than swapping its digits — so a shortcut under
|
|
29633
|
+
// two wheels shows which one it changed, and by how much. The value itself is
|
|
29634
|
+
// set at once either way; only the drawing is concerned.
|
|
29635
|
+
registerNaviCommand("--navi-update", (source, event, { argument }) => {
|
|
29632
29636
|
const target =
|
|
29633
29637
|
resolveExplicitTarget(source) || resolveFirstParentControl(source);
|
|
29634
29638
|
if (!target) {
|
|
@@ -29645,6 +29649,7 @@ registerNaviCommand("--navi-update", (source, event) => {
|
|
|
29645
29649
|
const commandValue = resolveCommandValue(source, event);
|
|
29646
29650
|
dispatchRequestSetUIState(target, commandValue, {
|
|
29647
29651
|
event,
|
|
29652
|
+
behavior: argument,
|
|
29648
29653
|
});
|
|
29649
29654
|
},
|
|
29650
29655
|
});
|
|
@@ -50058,8 +50063,12 @@ const SlideContainer = ({
|
|
|
50058
50063
|
// after it — a step that has been answered is not a step to stay on.
|
|
50059
50064
|
const done = (area, event) => {
|
|
50060
50065
|
markAnswered(area);
|
|
50061
|
-
if (!moveNext(event
|
|
50062
|
-
|
|
50066
|
+
if (!moveNext(event, {
|
|
50067
|
+
released: true
|
|
50068
|
+
})) {
|
|
50069
|
+
movePrevious(event, {
|
|
50070
|
+
released: true
|
|
50071
|
+
});
|
|
50063
50072
|
}
|
|
50064
50073
|
};
|
|
50065
50074
|
|
|
@@ -50377,6 +50386,7 @@ const SlideContainer = ({
|
|
|
50377
50386
|
forward,
|
|
50378
50387
|
event,
|
|
50379
50388
|
value,
|
|
50389
|
+
released,
|
|
50380
50390
|
dx = 0,
|
|
50381
50391
|
dy = 0
|
|
50382
50392
|
} = {}) => {
|
|
@@ -50391,6 +50401,7 @@ const SlideContainer = ({
|
|
|
50391
50401
|
forward,
|
|
50392
50402
|
event,
|
|
50393
50403
|
value,
|
|
50404
|
+
released,
|
|
50394
50405
|
dx,
|
|
50395
50406
|
dy
|
|
50396
50407
|
});
|
|
@@ -50427,7 +50438,11 @@ const SlideContainer = ({
|
|
|
50427
50438
|
// event dispatched by hand: a slide that holds on to the user holds them
|
|
50428
50439
|
// whatever they press. Read off the slide being LEFT, because that is what
|
|
50429
50440
|
// has a reason to keep them (an answer still missing, a step not taken).
|
|
50430
|
-
|
|
50441
|
+
// `released` is the slide letting go itself (--navi-done, see Slide): the
|
|
50442
|
+
// one departure the forward hold does not apply to. Said here rather than
|
|
50443
|
+
// by dropping the attribute, which stays the render's to write — a hold
|
|
50444
|
+
// meant to stay (an explicit preventNavNext) is still there on the way back.
|
|
50445
|
+
if (forward ? !released && currentElement?.hasAttribute("data-prevent-nav-next") : currentElement?.hasAttribute("data-prevent-nav-previous")) {
|
|
50431
50446
|
return false;
|
|
50432
50447
|
}
|
|
50433
50448
|
// The focus moves here, while the event that asked for it is still in hand:
|
|
@@ -50576,15 +50591,20 @@ const SlideContainer = ({
|
|
|
50576
50591
|
dx,
|
|
50577
50592
|
dy,
|
|
50578
50593
|
event,
|
|
50579
|
-
value
|
|
50594
|
+
value,
|
|
50595
|
+
released
|
|
50580
50596
|
} = pendingRollsRef.current.shift();
|
|
50581
50597
|
if (dx || dy) {
|
|
50582
|
-
move(dx, dy, event,
|
|
50598
|
+
move(dx, dy, event, {
|
|
50599
|
+
value,
|
|
50600
|
+
released
|
|
50601
|
+
});
|
|
50583
50602
|
return;
|
|
50584
50603
|
}
|
|
50585
50604
|
goToArea(area, {
|
|
50586
50605
|
event,
|
|
50587
|
-
value
|
|
50606
|
+
value,
|
|
50607
|
+
released
|
|
50588
50608
|
});
|
|
50589
50609
|
}, [noTravel]);
|
|
50590
50610
|
|
|
@@ -50703,10 +50723,14 @@ const SlideContainer = ({
|
|
|
50703
50723
|
return area;
|
|
50704
50724
|
}
|
|
50705
50725
|
};
|
|
50706
|
-
const move = (dx, dy, event,
|
|
50726
|
+
const move = (dx, dy, event, {
|
|
50727
|
+
value,
|
|
50728
|
+
released
|
|
50729
|
+
} = {}) => goToArea(areaTowards(dx, dy), {
|
|
50707
50730
|
forward: dx > 0 || dy > 0,
|
|
50708
50731
|
event,
|
|
50709
50732
|
value,
|
|
50733
|
+
released,
|
|
50710
50734
|
dx,
|
|
50711
50735
|
dy
|
|
50712
50736
|
});
|
|
@@ -50716,8 +50740,8 @@ const SlideContainer = ({
|
|
|
50716
50740
|
// slides are. On a map they mean the same thing, and fall back to the other
|
|
50717
50741
|
// axis when there is nothing that way — a step onwards, however the screens
|
|
50718
50742
|
// happen to be arranged.
|
|
50719
|
-
const moveNext = event => vertical ? move(0, 1, event) || move(1, 0, event) : move(1, 0, event) || move(0, 1, event);
|
|
50720
|
-
const movePrevious = event => vertical ? move(0, -1, event) || move(-1, 0, event) : move(-1, 0, event) || move(0, -1, event);
|
|
50743
|
+
const moveNext = (event, options) => vertical ? move(0, 1, event, options) || move(1, 0, event, options) : move(1, 0, event, options) || move(0, 1, event, options);
|
|
50744
|
+
const movePrevious = (event, options) => vertical ? move(0, -1, event, options) || move(-1, 0, event, options) : move(-1, 0, event, options) || move(0, -1, event, options);
|
|
50721
50745
|
|
|
50722
50746
|
// Where the track is right now, as the gesture left it: the resting place of
|
|
50723
50747
|
// the slide being dragged, plus what the pointer has pulled since.
|
|
@@ -51364,7 +51388,9 @@ const SlideContainer = ({
|
|
|
51364
51388
|
// (a --navi-right command carries the click that ran it, that click its
|
|
51365
51389
|
// own mousedown), and the focus transfer reads the whole chain to know
|
|
51366
51390
|
// where the interaction started.
|
|
51367
|
-
move(dx, dy, e,
|
|
51391
|
+
move(dx, dy, e, {
|
|
51392
|
+
value
|
|
51393
|
+
});
|
|
51368
51394
|
}
|
|
51369
51395
|
// By name rather than by direction (--navi-go-to-slide): the caller says
|
|
51370
51396
|
// where, the map says nothing about it.
|
|
@@ -51524,11 +51550,6 @@ const Slide = ({
|
|
|
51524
51550
|
,
|
|
51525
51551
|
|
|
51526
51552
|
onnavi_done: e => {
|
|
51527
|
-
// Dropped imperatively rather than left to the re-render this
|
|
51528
|
-
// schedules: moving on happens in this same handler, and the gate it
|
|
51529
|
-
// goes through reads this attribute off the DOM (see goToArea) — a
|
|
51530
|
-
// render is a microtask away, the move is not.
|
|
51531
|
-
e.currentTarget.removeAttribute("data-prevent-nav-next");
|
|
51532
51553
|
container?.done(slideArea);
|
|
51533
51554
|
rest.onnavi_done?.(e);
|
|
51534
51555
|
},
|
|
@@ -56908,6 +56929,15 @@ const css$C = /* css */`
|
|
|
56908
56929
|
--popup-border-width: 1px;
|
|
56909
56930
|
--popup-border-color: var(--navi-popup-border-color);
|
|
56910
56931
|
|
|
56932
|
+
/* A popup is a page of its own, opened from a control: it is written on
|
|
56933
|
+
the page's line, as the number, so each text it holds keeps a line
|
|
56934
|
+
relative to its own size. Its element sits under the control that
|
|
56935
|
+
opens it (a Picker holds its popup children inside its root), and
|
|
56936
|
+
line-height inherits as computed: the control's line is a length
|
|
56937
|
+
(--navi-control-line-height), and inherited it would arrive as that
|
|
56938
|
+
control's pixels — a 12px caption on an 18px picker's 23px rows. */
|
|
56939
|
+
line-height: var(--navi-line-height);
|
|
56940
|
+
|
|
56911
56941
|
&.navi_popover {
|
|
56912
56942
|
--popover-border-radius: var(--popup-border-radius);
|
|
56913
56943
|
--popover-border-width: var(--popup-border-width);
|
|
@@ -57173,7 +57203,12 @@ installImportMetaCssBuild(import.meta);const css$B = /* css */`
|
|
|
57173
57203
|
--picker-popup-border-radius,
|
|
57174
57204
|
var(--picker-border-radius)
|
|
57175
57205
|
);
|
|
57176
|
-
|
|
57206
|
+
/* Explicit fallback, for the same reason as --popover-background-color
|
|
57207
|
+
above: the dialog paints border-width from this var with no fallback
|
|
57208
|
+
of its own, so an unset dialogBorderWidth would leave the var
|
|
57209
|
+
guaranteed-invalid and border-width at its initial "medium" (3px).
|
|
57210
|
+
0px is the dialog's own default. */
|
|
57211
|
+
--dialog-border-width: var(--picker-dialog-border-width, 0px);
|
|
57177
57212
|
--dialog-border-color: var(--x-picker-border-color);
|
|
57178
57213
|
/* The picker's own surface is not this one — see the popover branch,
|
|
57179
57214
|
including why the fallback is spelled out. */
|
|
@@ -70549,7 +70584,7 @@ const WheelGroupContext = createContext(null);
|
|
|
70549
70584
|
* @param {boolean} [props.glass] - Frost the neighbouring rows so the center reads as a clear "window" (iOS-picker style). Inherited from a WheelGroup.
|
|
70550
70585
|
* @param {boolean} [props.frameBorder] - Line the center-window edges with a faint frame (off by default; independent of glass). Tune via --wheel-frame-color.
|
|
70551
70586
|
* @param {boolean|number} [props.zoom] - Make the centered value stand out by size: the neighbours are drawn smaller, so a value grows as it slides into the window and shrinks as it leaves (the centered one keeps its natural size). `true` uses the default ratio (1.3); a number is the ratio itself (1.8 = the center reads 80% bigger than its neighbours). Inherited from a WheelGroup.
|
|
70552
|
-
* @param {number} [props.glideSpeed=0.16] - Speed (px/ms) of the programmatic glide used by arrow keys, taps and the navi_scroll "smooth" behavior. Lower = slower, more visible transitions; ≈0.16 covers one 32px row in 200ms.
|
|
70587
|
+
* @param {number} [props.glideSpeed=0.16] - Speed (px/ms) of the programmatic glide used by arrow keys, taps, a value set with `--navi-update:smooth` and the navi_scroll "smooth" behavior. Lower = slower, more visible transitions; ≈0.16 covers one 32px row in 200ms.
|
|
70553
70588
|
* @param {string} [props.type] - Informative value kind (e.g. "integer", "day"). Used only for rendering hints, like tabular figures for "integer".
|
|
70554
70589
|
*/
|
|
70555
70590
|
const Wheel = props => {
|
|
@@ -71271,6 +71306,23 @@ function WheelUI(props) {
|
|
|
71271
71306
|
// faster, so a second press mid-glide reads as accelerating, not restarting.
|
|
71272
71307
|
const targetPosRef = useRef(null);
|
|
71273
71308
|
const glideRef = useRef(null);
|
|
71309
|
+
// Who aimed the running glide. A value set from outside (`--navi-update:smooth`)
|
|
71310
|
+
// is already committed by whoever set it: the wheel only catches up with it,
|
|
71311
|
+
// and its arrival must not be reported as a settle — no navi_wheel_settle, no
|
|
71312
|
+
// action. A user input is the opposite: its arrival IS the settle. Written at
|
|
71313
|
+
// every motion start (settle, glideTo).
|
|
71314
|
+
const glideFromOutsideRef = useRef(false);
|
|
71315
|
+
// How the value that just arrived asked to be shown. A set request can say
|
|
71316
|
+
// `behavior: "smooth"` (what `--navi-update:smooth` puts on it); the wheel
|
|
71317
|
+
// then scrolls to the value instead of swapping the digits. Read off the
|
|
71318
|
+
// navi_ui_state_change the controller dispatches — the request may have
|
|
71319
|
+
// reached this wheel through a group distributing to its children, so it is
|
|
71320
|
+
// looked up along the event chain. Consumed by the sync effect below.
|
|
71321
|
+
const pendingBehaviorRef = useRef(null);
|
|
71322
|
+
const onUIStateChange = e => {
|
|
71323
|
+
const setRequest = findEvent(e, "navi_set_ui_state");
|
|
71324
|
+
pendingBehaviorRef.current = setRequest ? setRequest.detail.behavior : null;
|
|
71325
|
+
};
|
|
71274
71326
|
// The glide loop is bound once (mount effect) but the speed can change live
|
|
71275
71327
|
// (e.g. a demo control) — read it through a ref so it uses the latest.
|
|
71276
71328
|
const glideSpeedRef = useRef(glideSpeed);
|
|
@@ -71461,11 +71513,11 @@ function WheelUI(props) {
|
|
|
71461
71513
|
posRef.current = index * size;
|
|
71462
71514
|
renderPos(vp);
|
|
71463
71515
|
}
|
|
71464
|
-
|
|
71516
|
+
centeredIndexRef.current = index;
|
|
71517
|
+
if (!interactive || glideFromOutsideRef.current) {
|
|
71465
71518
|
return;
|
|
71466
71519
|
}
|
|
71467
71520
|
const settleEvent = new CustomEvent("navi_wheel_settle");
|
|
71468
|
-
centeredIndexRef.current = index;
|
|
71469
71521
|
debugScroll(`settle: committed → ${trackedItemsRef.current[index].value}`);
|
|
71470
71522
|
requestSelectValue(trackedItemsRef.current[index].value, settleEvent);
|
|
71471
71523
|
// The value is now stable → commit the action. The wheel runs actionEvent
|
|
@@ -71548,13 +71600,21 @@ function WheelUI(props) {
|
|
|
71548
71600
|
// Spring stiffness (fraction of remaining distance per ~frame). Scales with the
|
|
71549
71601
|
// glide speed so slower = gentler chase; clamped so it never crawls or snaps.
|
|
71550
71602
|
const glideSpringFactor = () => clampNumber(glideSpeedRef.current * 1.4, 0.06, 0.45);
|
|
71551
|
-
const glideTo = (vp, target
|
|
71603
|
+
const glideTo = (vp, target, {
|
|
71604
|
+
fromOutside = false
|
|
71605
|
+
} = {}) => {
|
|
71552
71606
|
// A discrete glide overrides any fling momentum.
|
|
71553
71607
|
if (momentumRef.current !== null) {
|
|
71554
71608
|
cancelAnimationFrame(momentumRef.current);
|
|
71555
71609
|
momentumRef.current = null;
|
|
71556
71610
|
}
|
|
71557
71611
|
targetPosRef.current = target;
|
|
71612
|
+
// A user input anywhere in the movement makes its arrival a user settle;
|
|
71613
|
+
// an outside value re-aiming a glide the user started never takes that
|
|
71614
|
+
// away from them (their tap still has to be answered with a settle).
|
|
71615
|
+
if (glideRef.current === null || !fromOutside) {
|
|
71616
|
+
glideFromOutsideRef.current = fromOutside;
|
|
71617
|
+
}
|
|
71558
71618
|
if (glideRef.current === null) {
|
|
71559
71619
|
debugScroll("glide: start");
|
|
71560
71620
|
glideRef.current = requestAnimationFrame(() => glideStep(vp, performance.now()));
|
|
@@ -71570,6 +71630,7 @@ function WheelUI(props) {
|
|
|
71570
71630
|
// overshoots only a handful of rows (a picker isn't a free-scrolling list).
|
|
71571
71631
|
const settle = (vp, velocity) => {
|
|
71572
71632
|
cancelAnim();
|
|
71633
|
+
glideFromOutsideRef.current = false;
|
|
71573
71634
|
debugScroll(`settle: momentum start (v=${velocity}px/ms)`);
|
|
71574
71635
|
// A drag fling: allow the full swipe velocity (see WHEEL_FLING_MAX_VELOCITY)
|
|
71575
71636
|
// so a hard swipe carries across the list instead of being clipped to a few
|
|
@@ -71634,13 +71695,19 @@ function WheelUI(props) {
|
|
|
71634
71695
|
glideTo(vp, target);
|
|
71635
71696
|
};
|
|
71636
71697
|
|
|
71637
|
-
// Center value `index`
|
|
71638
|
-
//
|
|
71698
|
+
// Center value `index` for a value the wheel did not choose itself (first
|
|
71699
|
+
// display, a value set from outside). "smooth" glides to the nearest copy
|
|
71700
|
+
// (glideTargetFor) so a wrap goes the short way, and arrives silently (see
|
|
71701
|
+
// glideFromOutsideRef); "auto" puts the row in place at once, stopping
|
|
71702
|
+
// whatever was moving so no glide lands on top of it a frame later.
|
|
71639
71703
|
const centerOnIndex = (vp, index, behavior) => {
|
|
71640
71704
|
const target = glideTargetFor(vp, index);
|
|
71641
71705
|
if (behavior === "smooth") {
|
|
71642
|
-
glideTo(vp, target
|
|
71706
|
+
glideTo(vp, target, {
|
|
71707
|
+
fromOutside: true
|
|
71708
|
+
});
|
|
71643
71709
|
} else {
|
|
71710
|
+
cancelAnim();
|
|
71644
71711
|
setPos(vp, target);
|
|
71645
71712
|
}
|
|
71646
71713
|
};
|
|
@@ -71652,7 +71719,7 @@ function WheelUI(props) {
|
|
|
71652
71719
|
const glideToIndex = (vp, index, event) => {
|
|
71653
71720
|
centeredIndexRef.current = index;
|
|
71654
71721
|
requestSelectValue(trackedItemsRef.current[index].value, event);
|
|
71655
|
-
|
|
71722
|
+
glideTo(vp, glideTargetFor(vp, index));
|
|
71656
71723
|
};
|
|
71657
71724
|
// Index we are heading to (the target, not the mid-glide visual center).
|
|
71658
71725
|
const currentTargetIndex = vp => {
|
|
@@ -71681,14 +71748,12 @@ function WheelUI(props) {
|
|
|
71681
71748
|
// Sync the center with the current value — used on first display and whenever
|
|
71682
71749
|
// the controlled value changes from outside.
|
|
71683
71750
|
const syncCenterToSelection = (viewportEl, behavior) => {
|
|
71684
|
-
// A
|
|
71685
|
-
//
|
|
71686
|
-
//
|
|
71687
|
-
//
|
|
71688
|
-
//
|
|
71689
|
-
|
|
71690
|
-
// is honoured once it settles (centeredIndexRef then differs from selection).
|
|
71691
|
-
if (glideRef.current !== null || momentumRef.current !== null) {
|
|
71751
|
+
// A fling in flight reports its own value at every row crossing and commits
|
|
71752
|
+
// on settle: the finger that threw it owns the wheel until then, and a value
|
|
71753
|
+
// arriving meanwhile is re-stated by the next crossing anyway. A glide is
|
|
71754
|
+
// different: its target is re-aimed below (glideTo never restarts the
|
|
71755
|
+
// loop), so a value set from outside mid-glide is where the wheel ends up.
|
|
71756
|
+
if (momentumRef.current !== null) {
|
|
71692
71757
|
return;
|
|
71693
71758
|
}
|
|
71694
71759
|
if (trackedItemsRef.current.length === 0) {
|
|
@@ -71730,13 +71795,27 @@ function WheelUI(props) {
|
|
|
71730
71795
|
};
|
|
71731
71796
|
}, []);
|
|
71732
71797
|
|
|
71733
|
-
// React to controlled value changes coming from outside.
|
|
71798
|
+
// React to controlled value changes coming from outside. The row is put in
|
|
71799
|
+
// place at once, unless the request asked for "smooth": the value is already
|
|
71800
|
+
// set for whoever reads it, and the wheel scrolls to it so the eye can follow
|
|
71801
|
+
// the change (a shortcut button under two wheels: which one moved, and by how
|
|
71802
|
+
// much). Never on the first centering (no target yet → nothing to glide from,
|
|
71803
|
+
// the displayed effect above handles it), nor under reduced motion.
|
|
71734
71804
|
useLayoutEffect(() => {
|
|
71805
|
+
const requestedBehavior = pendingBehaviorRef.current;
|
|
71806
|
+
pendingBehaviorRef.current = null;
|
|
71735
71807
|
const viewportEl = getViewport();
|
|
71736
71808
|
if (!viewportEl || viewportEl.offsetParent === null) {
|
|
71737
71809
|
return;
|
|
71738
71810
|
}
|
|
71739
|
-
|
|
71811
|
+
let behavior = "auto";
|
|
71812
|
+
if (requestedBehavior === "smooth" && centeredIndexRef.current !== null) {
|
|
71813
|
+
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
71814
|
+
if (!prefersReducedMotion) {
|
|
71815
|
+
behavior = "smooth";
|
|
71816
|
+
}
|
|
71817
|
+
}
|
|
71818
|
+
syncCenterToSelection(viewportEl, behavior);
|
|
71740
71819
|
});
|
|
71741
71820
|
useWheelInteractions({
|
|
71742
71821
|
ref,
|
|
@@ -71804,6 +71883,7 @@ function WheelUI(props) {
|
|
|
71804
71883
|
children: [jsx(Box, {
|
|
71805
71884
|
as: "input",
|
|
71806
71885
|
...controlHostProps,
|
|
71886
|
+
onnavi_ui_state_change: onUIStateChange,
|
|
71807
71887
|
tabindex: -1,
|
|
71808
71888
|
"aria-hidden": "true",
|
|
71809
71889
|
className: "navi_wheel_input"
|