@jsenv/navi 0.29.96 → 0.29.97

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.
@@ -24717,6 +24717,16 @@ const useUIGroupStateController = (
24717
24717
  // between them, from here on.
24718
24718
  controller.stateGivenFromAbove = false;
24719
24719
  if (resolvedDistributeChildStates) {
24720
+ if (compareTwoJsValues(groupUIState, controller.uiState)) {
24721
+ // The aggregate answered what the group already holds, which is
24722
+ // how such a group says "not yet, ask me again". Some gestures
24723
+ // cannot avoid an in-between — two people swapping seats means one
24724
+ // leaves before the other arrives — and the aggregate is where
24725
+ // that half-state is recognised. Placing the children from it
24726
+ // would undo the half of the gesture that has already landed, and
24727
+ // publishing it would hand a half-answer to the row above.
24728
+ return;
24729
+ }
24720
24730
  // A group answering for all its children at once holds ONE answer
24721
24731
  // its children are views OF, rather than a value that IS what they
24722
24732
  // said: a list saying who plays and four seats saying who sits
@@ -75271,19 +75281,42 @@ installImportMetaCssBuild(import.meta);/**
75271
75281
  * — read off the vnodes (toChildArray, so a .map() or a fragment is fine; a
75272
75282
  * component of your own wrapping an Item is not seen). The Item renders its
75273
75283
  * label; everything positional is this component's business.
75284
+ *
75285
+ * `slideContainer` connects the list to a <SlideContainer> by id, both ways:
75286
+ * pressing a step travels there (--navi-go-to-slide), and the position is
75287
+ * READ off the container rather than said by a prop — including mid-travel:
75288
+ * the container paints --slide-travel-progress on this element (it is a
75289
+ * follower, same mechanism as <Nav slideContainer>), so the halo rides the
75290
+ * drag under the finger, in CSS alone. The path then follows the position
75291
+ * too, clamped between `reached` (never retracts) and `reachable` (never
75292
+ * ahead of the answers): dragging towards a step whose way is earned fills
75293
+ * the line under the finger, dragging past the answers does not.
75274
75294
  */
75275
75295
  const css$2 = /* css */`
75276
75296
  .navi_step_list {
75277
- /* The knobs, in one place: accent is the path, muted is what the path
75278
- has not reached, on-accent writes on filled dots. --step-list-path-line
75297
+ /* The knobs: accent is the path, muted is what the path has not
75298
+ reached, on-accent writes on filled dots, and --step-list-path-line
75279
75299
  exists apart from the accent for a dark band where the walked line
75280
- reads better plain white. */
75281
- --step-list-accent: #4f8ef7;
75282
- --step-list-on-accent: white;
75283
- --step-list-muted: light-dark(#8a93a8, #8b99b8);
75284
- --step-list-line: light-dark(#c9d0dd, rgba(255, 255, 255, 0.35));
75285
- --step-list-current-color: light-dark(#1c2433, white);
75286
- --step-list-path-line: var(--step-list-accent);
75300
+ reads better plain white. Said from OUTSIDE (any ancestor — a dark
75301
+ band, a themed app) on the plain names; resolved here through an
75302
+ indirection (--x-…, the way Button does), because a default written
75303
+ on the plain name on this very element would beat anything an
75304
+ ancestor says. */
75305
+ --x-step-list-accent: var(--step-list-accent, #4f8ef7);
75306
+ --x-step-list-on-accent: var(--step-list-on-accent, white);
75307
+ --x-step-list-muted: var(--step-list-muted, light-dark(#8a93a8, #8b99b8));
75308
+ --x-step-list-line: var(
75309
+ --step-list-line,
75310
+ light-dark(#c9d0dd, rgba(255, 255, 255, 0.35))
75311
+ );
75312
+ --x-step-list-current-color: var(
75313
+ --step-list-current-color,
75314
+ light-dark(#1c2433, white)
75315
+ );
75316
+ --x-step-list-path-line: var(
75317
+ --step-list-path-line,
75318
+ var(--x-step-list-accent)
75319
+ );
75287
75320
 
75288
75321
  position: relative;
75289
75322
  display: block;
@@ -75297,19 +75330,29 @@ const css$2 = /* css */`
75297
75330
  }
75298
75331
  /* The road not walked yet. */
75299
75332
  .navi_step_list_rail line {
75300
- stroke: var(--step-list-line);
75333
+ stroke: var(--x-step-list-line);
75301
75334
  stroke-width: 2;
75302
75335
  stroke-dasharray: 4 5;
75303
75336
  }
75304
75337
  .navi_step_list_rail circle {
75305
75338
  fill: none;
75306
- stroke: var(--step-list-muted);
75339
+ stroke: var(--x-step-list-muted);
75307
75340
  stroke-width: 1.5;
75308
75341
  }
75309
75342
  .navi_step_list_rail text {
75310
75343
  font-weight: 600;
75311
75344
  font-size: 12px;
75312
- fill: var(--step-list-muted);
75345
+ fill: var(--x-step-list-muted);
75346
+ }
75347
+ /* The current dot says so in the drawing itself, not only by its halo: on
75348
+ a dark band a muted number under a faint ring reads as nothing. Said in
75349
+ the base layer only (see renderRail) — a current dot the path has
75350
+ covered keeps the filled colors. */
75351
+ .navi_step_list_rail g[data-current] circle {
75352
+ stroke: var(--x-step-list-current-color);
75353
+ }
75354
+ .navi_step_list_rail g[data-current] text {
75355
+ fill: var(--x-step-list-current-color);
75313
75356
  }
75314
75357
  /* The path: same drawing, filled, revealed up to the step it has come to.
75315
75358
  The clip is set inline (a width in px); transitioning it is what makes
@@ -75318,15 +75361,15 @@ const css$2 = /* css */`
75318
75361
  transition: clip-path 300ms ease;
75319
75362
  }
75320
75363
  .navi_step_list_rail_filled line {
75321
- stroke: var(--step-list-path-line);
75364
+ stroke: var(--x-step-list-path-line);
75322
75365
  stroke-dasharray: none;
75323
75366
  }
75324
75367
  .navi_step_list_rail_filled circle {
75325
- fill: var(--step-list-accent);
75326
- stroke: var(--step-list-accent);
75368
+ fill: var(--x-step-list-accent);
75369
+ stroke: var(--x-step-list-accent);
75327
75370
  }
75328
75371
  .navi_step_list_rail_filled text {
75329
- fill: var(--step-list-on-accent);
75372
+ fill: var(--x-step-list-on-accent);
75330
75373
  }
75331
75374
  /* The position: a halo around the dot being looked at. It slides from dot
75332
75375
  to dot (transform, transitioned) — the g moves, the circle inside is
@@ -75336,9 +75379,53 @@ const css$2 = /* css */`
75336
75379
  }
75337
75380
  .navi_step_list_marker circle {
75338
75381
  fill: none;
75339
- stroke: color-mix(in srgb, var(--step-list-accent) 65%, transparent);
75382
+ stroke: color-mix(in srgb, var(--x-step-list-accent) 65%, transparent);
75340
75383
  stroke-width: 1.5;
75341
75384
  }
75385
+
75386
+ /* Connected to slides: the movement is not this component's anymore. The
75387
+ container paints --slide-travel-progress here (this element follows it,
75388
+ see data-slide-container-follows) — an asked-for travel animates it, a
75389
+ finger drags it — and everything below is a calc() of that number, so
75390
+ the halo and the path move per frame in CSS alone. The transitions are
75391
+ off: they would chase a finger that is already the pace.
75392
+ Position, in dots-x px: where the picture is right now. */
75393
+ .navi_step_list[data-slide-container-follows] {
75394
+ --step-list-position: calc(
75395
+ var(--step-list-pos-x, 0) + var(--slide-travel-progress) *
75396
+ var(--step-list-pos-dx, 0)
75397
+ );
75398
+ }
75399
+ .navi_step_list[data-slide-container-follows] .navi_step_list_marker {
75400
+ transform: translateX(calc(var(--step-list-position) * 1px));
75401
+ transition: none;
75402
+ }
75403
+ /* The path follows the position, clamped: never back below what was
75404
+ earned (--step-list-reached-x), never ahead of what the answers allow
75405
+ (--step-list-reachable-x). The +14 covers the dot it stands on (radius
75406
+ plus stroke). */
75407
+ .navi_step_list[data-slide-container-follows] .navi_step_list_rail_filled {
75408
+ clip-path: inset(
75409
+ 0
75410
+ calc(
75411
+ (
75412
+ var(--step-list-w, 0) -
75413
+ (
75414
+ clamp(
75415
+ var(--step-list-reached-x, -9999),
75416
+ var(--step-list-position),
75417
+ var(--step-list-reachable-x, -9999)
75418
+ ) +
75419
+ 14
75420
+ )
75421
+ ) *
75422
+ 1px
75423
+ )
75424
+ 0 0
75425
+ );
75426
+ transition: none;
75427
+ }
75428
+
75342
75429
  /* One press target per step, covering the dot AND the label under it. The
75343
75430
  feedback is NOT the whole surface: a rectangle would say the whole band
75344
75431
  is a button, when the affordance is the dot — so hover and focus land on
@@ -75352,9 +75439,9 @@ const css$2 = /* css */`
75352
75439
  box-sizing: border-box;
75353
75440
  height: 100%;
75354
75441
  }
75355
- /* Doubled selector: the discrete variant declares its own hover background
75356
- var, and navi's stylesheet is injected after this one specificity is
75357
- what makes these values the ones read. */
75442
+ /* Doubled selector: the button's own state formulas (a readonly color
75443
+ mixed at the variant level) are declared in navi's stylesheet, injected
75444
+ after this one — specificity is what makes these values the ones read. */
75358
75445
  .navi_step_list .navi_step_list_step {
75359
75446
  position: relative;
75360
75447
  display: block;
@@ -75363,11 +75450,13 @@ const css$2 = /* css */`
75363
75450
  padding: 0;
75364
75451
  font-size: 12px;
75365
75452
  outline: none;
75366
- --button-color: var(--step-list-muted);
75367
- --button-color-readonly: var(--step-list-muted);
75368
- --button-background-color: transparent;
75369
- --button-background-color-hover: transparent;
75370
- --button-background-color-readonly: transparent;
75453
+ --button-color: var(--x-step-list-muted);
75454
+ --button-color-readonly: var(--x-step-list-muted);
75455
+ /* The button's own focus ring, silenced: it would outline the whole
75456
+ press surface, and the ring this list draws is the one around the dot
75457
+ (see the ::before rules below) — two rings read as a mistake. Width
75458
+ rather than style, because the ::before sets its own style in full. */
75459
+ --button-outline-width: 0px;
75371
75460
  }
75372
75461
  /* Centered on the dot: same vertical middle as the rail (top 0, height 34,
75373
75462
  cy 17). */
@@ -75383,11 +75472,11 @@ const css$2 = /* css */`
75383
75472
  }
75384
75473
  .navi_step_list_step:hover::before,
75385
75474
  .navi_step_list_step[data-hover]::before {
75386
- background: color-mix(in srgb, var(--step-list-accent) 15%, transparent);
75475
+ background: color-mix(in srgb, var(--x-step-list-accent) 15%, transparent);
75387
75476
  }
75388
75477
  .navi_step_list .navi_step_list_step:hover,
75389
75478
  .navi_step_list .navi_step_list_step[data-hover] {
75390
- --button-color: var(--step-list-current-color);
75479
+ --button-color: var(--x-step-list-current-color);
75391
75480
  }
75392
75481
  .navi_step_list_step:focus-visible::before,
75393
75482
  .navi_step_list_step[data-focus-visible]::before {
@@ -75405,8 +75494,8 @@ const css$2 = /* css */`
75405
75494
  }
75406
75495
  .navi_step_list .navi_step_list_step[data-current] {
75407
75496
  font-weight: 600;
75408
- --button-color: var(--step-list-current-color);
75409
- --button-color-readonly: var(--step-list-current-color);
75497
+ --button-color: var(--x-step-list-current-color);
75498
+ --button-color-readonly: var(--x-step-list-current-color);
75410
75499
  }
75411
75500
  `;
75412
75501
  const RAIL_H = 34;
@@ -75422,22 +75511,48 @@ const LINE_GAP = 5;
75422
75511
  * @type {import("ignore:preact").FunctionComponent<{
75423
75512
  * current?: string,
75424
75513
  * reached?: string,
75425
- * onStepPress?: (value: string, event: Event) => void,
75514
+ * reachable?: string,
75515
+ * slideContainer?: string,
75516
+ * travelByClick?: boolean,
75517
+ * travelByKeyboard?: boolean,
75426
75518
  * [key: string]: any,
75427
75519
  * }>}
75428
75520
  * @param {string} [current] - the step being looked at: its dot gets the
75429
75521
  * halo, its label the emphasis. Omit for "nowhere" — a confirmation
75430
- * screen after the walk, say.
75522
+ * screen after the walk, say. With `slideContainer` the position is read
75523
+ * off the container instead, and this prop is ignored.
75431
75524
  * @param {string} [reached] - the step the path has come to: the line is
75432
75525
  * solid and the dots filled up to it, dashed past it. Omit for a path
75433
75526
  * that has not started.
75434
- * @param {(value: string, event: Event) => void} [onStepPress] - a step was
75435
- * pressed. Without it the steps are read-only shown, not offered.
75527
+ * @param {string} [reachable] - how far the path MAY go (with
75528
+ * `slideContainer` only): between `reached` and this step the path
75529
+ * follows the position — a drag towards a step whose way is earned fills
75530
+ * the line under the finger. Defaults to `reached`: the path then never
75531
+ * moves with the position at all.
75532
+ * @param {string} [slideContainer] - id of a <SlideContainer> these steps
75533
+ * are the slides of. Pressing a step travels there
75534
+ * (--navi-go-to-slide), the halo follows the container — drags included —
75535
+ * and this element becomes a follower of the container
75536
+ * (data-slide-container-follows), which is also what keeps the arrow keys
75537
+ * working from here.
75538
+ * @param {boolean} [travelByClick=true] - whether pressing a step goes
75539
+ * there. Off, the steps are read-only — shown, not offered. To DO
75540
+ * something on a press, say `onClick` on the Item itself: like every other
75541
+ * prop an Item carries, it lands on that step's button.
75542
+ * @param {boolean} [travelByKeyboard=true] - whether the arrow keys walk
75543
+ * from one step to the other (the focus moves, Enter presses). Only when
75544
+ * the list stands alone: connected to slides the arrows belong to the
75545
+ * CONTAINER — this element is a follower, so a press here already walks
75546
+ * the slides, and the container's own `travelByKeyboard` is the one that
75547
+ * says so. One owner per mode, or one arrow would do both.
75436
75548
  */
75437
75549
  const StepList = ({
75438
75550
  current,
75439
75551
  reached,
75440
- onStepPress,
75552
+ reachable,
75553
+ slideContainer,
75554
+ travelByClick = true,
75555
+ travelByKeyboard = true,
75441
75556
  children,
75442
75557
  ...rest
75443
75558
  }) => {
@@ -75460,6 +75575,15 @@ const StepList = ({
75460
75575
  };
75461
75576
  }, []);
75462
75577
 
75578
+ // The arrows walk the steps — standing alone only: connected, this element
75579
+ // follows the container, whose own keydown listener already walks the
75580
+ // slides from here (and whose travelByKeyboard says whether to). A focus
75581
+ // group on top of that would make one arrow do both.
75582
+ useFocusGroup(rootRef, {
75583
+ enabled: Boolean(travelByKeyboard) && !slideContainer,
75584
+ direction: "x"
75585
+ });
75586
+
75463
75587
  // The steps, read off the children: each <StepList.Item> vnode says which
75464
75588
  // step it is (value) and is rendered as the label under its dot.
75465
75589
  const stepVNodes = toChildArray(children).filter(child => child && child.props);
@@ -75475,8 +75599,68 @@ const StepList = ({
75475
75599
  }
75476
75600
  }
75477
75601
  const indexOf = value => stepVNodes.findIndex((vnode, index) => valueOf(vnode, index) === value);
75478
- const currentIndex = current === undefined ? -1 : indexOf(current);
75602
+
75603
+ // Where the slides are, read off the container: which slide is current,
75604
+ // and — while a travel or a drag is playing — which one the picture leans
75605
+ // towards. The current area re-renders this component (the emphasized
75606
+ // label, the data-current dot); the in-between positions never do: they
75607
+ // are written as numbers on this element and interpolated by the CSS
75608
+ // above, at the pace of --slide-travel-progress.
75609
+ const [containerCurrent, setContainerCurrent] = useState(undefined);
75610
+ useLayoutEffect(() => {
75611
+ if (!slideContainer || dotXs.length === 0) {
75612
+ return undefined;
75613
+ }
75614
+ const containerElement = document.getElementById(slideContainer);
75615
+ if (!containerElement) {
75616
+ console.warn(`<StepList slideContainer="${slideContainer}"> but no element with that id found`);
75617
+ return undefined;
75618
+ }
75619
+ const rootElement = rootRef.current;
75620
+ const read = () => {
75621
+ const currentArea = containerElement.getAttribute("data-slide-current");
75622
+ const towardArea = containerElement.getAttribute("data-slide-travel-toward");
75623
+ setContainerCurrent(currentArea ?? undefined);
75624
+ const currentIdx = currentArea === null ? -1 : indexOf(currentArea);
75625
+ if (currentIdx === -1) {
75626
+ // A slide no step names (a confirmation screen): the halo is not
75627
+ // rendered, and the last position is left standing for the path.
75628
+ return;
75629
+ }
75630
+ const x = dotXs[currentIdx];
75631
+ let dx = 0;
75632
+ if (towardArea && towardArea !== currentArea) {
75633
+ const towardIdx = indexOf(towardArea);
75634
+ if (towardIdx !== -1 && towardIdx !== currentIdx) {
75635
+ // The container counts +1 when the picture leans on a slide BEFORE
75636
+ // the current one, -1 after: the delta is signed the same way, so
75637
+ // progress × delta lands exactly on the other dot.
75638
+ const sign = towardIdx > currentIdx ? -1 : 1;
75639
+ dx = (dotXs[towardIdx] - x) * sign;
75640
+ }
75641
+ }
75642
+ rootElement.style.setProperty("--step-list-pos-x", x);
75643
+ rootElement.style.setProperty("--step-list-pos-dx", dx);
75644
+ };
75645
+ read();
75646
+ const observer = new MutationObserver(read);
75647
+ observer.observe(containerElement, {
75648
+ attributes: true,
75649
+ attributeFilter: ["data-slide-current", "data-slide-travel-toward"]
75650
+ });
75651
+ return () => {
75652
+ observer.disconnect();
75653
+ };
75654
+ // width: the dots move when the room does, and the written positions are
75655
+ // pixels of those dots.
75656
+ }, [slideContainer, width, stepCount]);
75657
+ const resolvedCurrent = slideContainer ? containerCurrent : current;
75658
+ const currentIndex = resolvedCurrent === undefined ? -1 : indexOf(resolvedCurrent);
75479
75659
  const reachedIndex = reached === undefined ? -1 : indexOf(reached);
75660
+ let reachableIndex = reachable === undefined ? -1 : indexOf(reachable);
75661
+ if (reachableIndex < reachedIndex) {
75662
+ reachableIndex = reachedIndex;
75663
+ }
75480
75664
  // Covers the reached dot entirely (radius plus stroke), and nothing when
75481
75665
  // the path has not started.
75482
75666
  const fillX = reachedIndex === -1 ? 0 : dotXs[reachedIndex] + DOT_R + 3;
@@ -75484,7 +75668,7 @@ const StepList = ({
75484
75668
  const slotWidth = dotXs.length > 1 ? dotXs[1] - dotXs[0] : width;
75485
75669
  const renderRail = filled => jsx("svg", {
75486
75670
  className: filled ? "navi_step_list_rail navi_step_list_rail_filled" : "navi_step_list_rail",
75487
- style: filled ? {
75671
+ style: filled && !slideContainer ? {
75488
75672
  clipPath: `inset(0 ${width - fillX}px 0 0)`
75489
75673
  } : undefined,
75490
75674
  width: width,
@@ -75492,6 +75676,9 @@ const StepList = ({
75492
75676
  viewBox: `0 0 ${width} ${RAIL_H}`,
75493
75677
  "aria-hidden": "true",
75494
75678
  children: dotXs.map((x, index) => jsxs("g", {
75679
+ // Base layer only: a current dot the path covers keeps the filled
75680
+ // colors (see the css).
75681
+ "data-current": !filled && index === currentIndex ? "" : undefined,
75495
75682
  children: [index > 0 ? jsx("line", {
75496
75683
  x1: dotXs[index - 1] + DOT_R + LINE_GAP,
75497
75684
  y1: cy,
@@ -75514,7 +75701,21 @@ const StepList = ({
75514
75701
  ...rest,
75515
75702
  ref: rootRef,
75516
75703
  baseClassName: "navi_step_list",
75517
- "data-step-list": "",
75704
+ "data-step-list": ""
75705
+ // A follower of the container: the travel's progress is painted here
75706
+ // for the CSS to draw with, and the arrow keys keep walking the slides
75707
+ // from this element.
75708
+ ,
75709
+
75710
+ "data-slide-container-follows": slideContainer,
75711
+ style: {
75712
+ ...rest.style,
75713
+ ...(slideContainer ? {
75714
+ "--step-list-w": width,
75715
+ "--step-list-reached-x": reachedIndex === -1 ? -9999 : dotXs[reachedIndex],
75716
+ "--step-list-reachable-x": reachableIndex === -1 ? -9999 : dotXs[reachableIndex]
75717
+ } : undefined)
75718
+ },
75518
75719
  children: width > 0 && stepCount > 0 ? jsxs(Fragment$1, {
75519
75720
  children: [renderRail(false), renderRail(true), currentIndex !== -1 && dotXs[currentIndex] !== undefined ? jsx("svg", {
75520
75721
  className: "navi_step_list_rail",
@@ -75523,8 +75724,12 @@ const StepList = ({
75523
75724
  viewBox: `0 0 ${width} ${RAIL_H}`,
75524
75725
  "aria-hidden": "true",
75525
75726
  children: jsx("g", {
75526
- className: "navi_step_list_marker",
75527
- style: {
75727
+ className: "navi_step_list_marker"
75728
+ // Connected to slides, the position comes from the CSS calc
75729
+ // above — an inline transform would override it.
75730
+ ,
75731
+
75732
+ style: slideContainer ? undefined : {
75528
75733
  transform: `translateX(${dotXs[currentIndex]}px)`
75529
75734
  },
75530
75735
  children: jsx("circle", {
@@ -75559,14 +75764,23 @@ const StepList = ({
75559
75764
  },
75560
75765
  children: jsx(Button, {
75561
75766
  ...itemRest,
75562
- variant: "discrete",
75767
+ // bare, not discrete: what is drawn IS the dot and its
75768
+ // label — the hover wash a discrete button paints over its
75769
+ // whole surface is exactly what must not appear here (the
75770
+ // feedback is the circle over the dot, see the css).
75771
+ variant: "bare",
75563
75772
  className: "navi_step_list_step",
75564
75773
  "aria-current": index === currentIndex ? "step" : undefined,
75565
75774
  "data-current": index === currentIndex ? "" : undefined,
75566
- readOnly: !onStepPress,
75567
- onClick: onStepPress ? e => {
75568
- onStepPress(value, e);
75569
- } : undefined,
75775
+ readOnly: !travelByClick
75776
+ // Towards the slides when connected, by name: the command
75777
+ // reaches the container wherever this list sits on the
75778
+ // page. What else a press should do is the Item's own
75779
+ // onClick, which arrived through itemRest.
75780
+ ,
75781
+
75782
+ command: slideContainer && travelByClick ? `--navi-go-to-slide:${value}` : undefined,
75783
+ commandFor: slideContainer,
75570
75784
  children: jsx("span", {
75571
75785
  className: "navi_step_list_label",
75572
75786
  children: stepVNode