@jsenv/navi 0.28.10 → 0.29.0

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.
@@ -5356,7 +5356,7 @@ const stateSignal = (defaultValue, options = {}) => {
5356
5356
  const existingEntry = globalSignalRegistry.get(signalIdString);
5357
5357
  {
5358
5358
  throw new Error(
5359
- `Signal ID conflict: A signal with ID "${signalIdString}" already exists (existing default: ${existingEntry.options.getDefaultValue()})`,
5359
+ `Signal ID conflict: A signal with ID "${signalIdString}" already exists (existing default: ${existingEntry.options.getDefaultValue()}). If this is the same stateSignal() call site running twice, the module was evaluated twice — check the network tab for the same file requested both bare and with "?hot=".`,
5360
5360
  );
5361
5361
  }
5362
5362
  }
@@ -33946,10 +33946,13 @@ installImportMetaCssBuild(import.meta);const css$E = /* css */`
33946
33946
  black
33947
33947
  );
33948
33948
  /* Readonly */
33949
+ /* Fading toward the surface, not toward white: "washed out" means
33950
+ closer to the paper behind the button, whatever color that paper is
33951
+ (white here IS the surface only in the light theme). */
33949
33952
  --button-border-color-readonly: color-mix(
33950
33953
  in srgb,
33951
33954
  var(--button-border-color) 30%,
33952
- white
33955
+ var(--navi-surface-color)
33953
33956
  );
33954
33957
  --button-background-color-readonly: var(--button-background-color);
33955
33958
  --button-color-readonly: color-mix(
@@ -35085,6 +35088,9 @@ installImportMetaCssBuild(import.meta);/**
35085
35088
  * a solid surface. variant="discrete-border" does the same but keeps the border.
35086
35089
  * - variant="discrete" + backgroundColor: the color applies at rest and hover,
35087
35090
  * and the field goes transparent while focused.
35091
+ *
35092
+ * variant="text" is the odd one: it renders no <input> at all, just the value
35093
+ * as text — see InputTextualAsText below for what it is for and what it drops.
35088
35094
  */
35089
35095
  const inputCss = /* css */`
35090
35096
  @layer navi {
@@ -35181,7 +35187,12 @@ const inputCss = /* css */`
35181
35187
  cursor: inherit;
35182
35188
  pointer-events: auto;
35183
35189
 
35184
- .navi_control_input {
35190
+ /* The text of variant="text" is measured with the real thing rather than
35191
+ beside it: same padding, same margins, same room — so the two are the
35192
+ same box and a field's style can move without one of them staying
35193
+ behind. */
35194
+ .navi_control_input,
35195
+ .navi_input_text {
35185
35196
  box-sizing: content-box;
35186
35197
  min-width: 1ch;
35187
35198
  margin-top: calc(-1 * var(--x-padding-top));
@@ -35193,6 +35204,10 @@ const inputCss = /* css */`
35193
35204
  flex-grow: 1;
35194
35205
  color: inherit;
35195
35206
  font-size: inherit;
35207
+ /* A form control does not inherit the font on its own — the browser has
35208
+ one of its own for it, monospace for a <textarea> — so the box's font
35209
+ (--navi-control-font-family) is handed down by hand. */
35210
+ font-family: inherit;
35196
35211
  text-align: inherit;
35197
35212
  background: none;
35198
35213
  border: none;
@@ -35225,6 +35240,34 @@ const inputCss = /* css */`
35225
35240
  }
35226
35241
  }
35227
35242
 
35243
+ .navi_input_text {
35244
+ /* Nothing to read yet is still a line: the box may not collapse just
35245
+ because the value is empty. */
35246
+ min-height: 1lh;
35247
+ /* A form control keeps a line of its own whatever line-height the page
35248
+ is written in; the text that stands in for one has to say the same
35249
+ number, or the box it is meant to match changes height. */
35250
+ line-height: normal;
35251
+ }
35252
+ /* The value is cut by a box of its own, and that is the whole reason it
35253
+ exists: a field ends its text at the content edge, while an overflow set
35254
+ on the padded box would let it run through the padding — the same value
35255
+ would then lose a character on one side and not on the other. This box
35256
+ IS the content edge, so both stop on the same letter.
35257
+ Cut rather than wrapped (a second line would be taller than the field),
35258
+ and cut rather than ellipsed (an ellipsis costs a character the field
35259
+ does not lose). */
35260
+ .navi_input_text_value {
35261
+ display: block;
35262
+ /* The whole content box, short value or not: a field's text is laid out
35263
+ in the full width whatever it holds, so text-align lands in the same
35264
+ place on both. */
35265
+ width: 100%;
35266
+ text-overflow: clip;
35267
+ white-space: nowrap;
35268
+ overflow: hidden;
35269
+ }
35270
+
35228
35271
  .navi_input_slot {
35229
35272
  margin-right: var(--slot-spacing, calc(2px + 0.1em));
35230
35273
  margin-left: var(--slot-spacing, calc(2px + 0.1em));
@@ -35334,6 +35377,16 @@ const inputCss = /* css */`
35334
35377
  }
35335
35378
  }
35336
35379
 
35380
+ /* The box of a field, without a field in it: the border is kept and made
35381
+ invisible rather than dropped, and the background goes with it, so what
35382
+ is left is text sitting exactly where the value would be — swap one for
35383
+ the other and nothing on the page moves. */
35384
+ &[data-variant="text"] {
35385
+ --x-background-color: transparent;
35386
+ --x-border-color: transparent;
35387
+ cursor: inherit;
35388
+ }
35389
+
35337
35390
  &[data-variant="underline"] {
35338
35391
  border: none;
35339
35392
  border-radius: 0;
@@ -35459,12 +35512,9 @@ const InputTextualUI = props => {
35459
35512
  // when remainingProps contains expandX which would try to set width to 100%
35460
35513
  delete inputControlRootProps.width;
35461
35514
  if (width === "maxLength") {
35462
- const {
35463
- maxLength
35464
- } = inputControlHostProps;
35465
- if (maxLength !== undefined) {
35466
- const isNumeric = props.inputMode === "numeric";
35467
- inputControlHostProps.width = isNumeric ? `${maxLength}ch` : `calc(${maxLength} * 1.5ch)`;
35515
+ const widthFromMaxLength = resolveWidthFromMaxLength(inputControlHostProps.maxLength, props.inputMode);
35516
+ if (widthFromMaxLength !== undefined) {
35517
+ inputControlHostProps.width = widthFromMaxLength;
35468
35518
  }
35469
35519
  } else if (width === "content") {
35470
35520
  inputControlHostProps.fieldSizing = "content";
@@ -35507,6 +35557,96 @@ const InputTextualUI = props => {
35507
35557
  }), childrenWithContext]
35508
35558
  });
35509
35559
  };
35560
+ // How wide a field is when its width is left to what it accepts: a value that
35561
+ // cannot exceed maxLength characters needs no more room than that. Shared with
35562
+ // the text variant, which must land on the same number or the two would not be
35563
+ // the same box.
35564
+ const resolveWidthFromMaxLength = (maxLength, inputMode) => {
35565
+ if (maxLength === undefined) {
35566
+ return undefined;
35567
+ }
35568
+ if (inputMode === "numeric") {
35569
+ return `${maxLength}ch`;
35570
+ }
35571
+ return `calc(${maxLength} * 1.5ch)`;
35572
+ };
35573
+
35574
+ /**
35575
+ * variant="text" — the value, written where the field would be and taking
35576
+ * exactly its room: same paddings, same font, same line, and the border kept
35577
+ * but made invisible, so a value one only reads and the same value being
35578
+ * edited are one box. What it is for: an information that is sometimes known
35579
+ * (a name already on the profile) and sometimes asked for. Swapping the field
35580
+ * for its text must move nothing under it.
35581
+ *
35582
+ * It is text, and nothing else: no <input>, so nothing to focus, nothing in
35583
+ * the tab order, and nothing sent when the form is submitted — a value the
35584
+ * form must carry goes in an <Input type="hidden"> beside this one. Not a
35585
+ * disabled control either: `disabled`/`aria-disabled` would announce a field
35586
+ * one cannot use, where there is no field at all.
35587
+ *
35588
+ * The field's own props are dropped rather than half-honoured (placeholder,
35589
+ * the guards, the slots): they all describe an edition that does not happen
35590
+ * here. What is kept is what decides the box.
35591
+ */
35592
+ // Everything a field takes that a text does not: what the value is said with,
35593
+ // what the box is measured from (read below, then dropped too), and all the
35594
+ // rest — the guards, the slots, the constraints — which describe an edition
35595
+ // that does not happen here. What survives is what a Box understands: width,
35596
+ // spacing, colors, className, style.
35597
+ const INPUT_ONLY_PROPS = ["value", "defaultValue", "signal", "id", "maxLength", "inputMode", "width", "variant", "type", "name", "placeholder", "required", "readOnly", "disabled", "loading", "error", "min", "max", "step", "pattern", "autoComplete", "autoCorrect", "spellcheck", "charGuard", "maxLengthGuard", "list", "suggestions", "headless", "fieldSizing", "action", "uiAction", "ui", "children"];
35598
+ const InputTextualAsText = props => {
35599
+ import.meta.css = [inputCss, "@jsenv/navi/src/control/input/input_textual.jsx"];
35600
+ // The id a Field handed down, which is what its Label points at.
35601
+ const controlId = useContext(ControlIdContext);
35602
+ const {
35603
+ value,
35604
+ defaultValue,
35605
+ signal,
35606
+ id,
35607
+ maxLength,
35608
+ inputMode,
35609
+ width = "maxLength"
35610
+ } = props;
35611
+ const valueShown = signal ? signal.value : value ?? defaultValue;
35612
+ const textWidth = width === "maxLength" ? resolveWidthFromMaxLength(maxLength, inputMode) : width === "content" ? undefined : width;
35613
+ const boxProps = {
35614
+ ...props
35615
+ };
35616
+ for (const inputOnlyProp of INPUT_ONLY_PROPS) {
35617
+ delete boxProps[inputOnlyProp];
35618
+ }
35619
+ return jsx(Box, {
35620
+ as: "span",
35621
+ inline: true,
35622
+ flex: true,
35623
+ baseClassName: "navi_input",
35624
+ "data-variant": "text",
35625
+ styleCSSVars: InputStyleCSSVars,
35626
+ ...boxProps,
35627
+ children: jsx(Box, {
35628
+ as: "span",
35629
+ baseClassName: "navi_input_text",
35630
+ id: id || controlId,
35631
+ width: textWidth,
35632
+ children: jsx("span", {
35633
+ className: "navi_input_text_value",
35634
+ children: valueShown
35635
+ })
35636
+ })
35637
+ });
35638
+ };
35639
+ const InputTextualAsTextResolver = props => {
35640
+ const Next = useNextResolver();
35641
+ if (props.variant === "text") {
35642
+ return jsx(InputTextualAsText, {
35643
+ ...props
35644
+ });
35645
+ }
35646
+ return jsx(Next, {
35647
+ ...props
35648
+ });
35649
+ };
35510
35650
  const InputTextualFirstResolver = props => {
35511
35651
  const Next = useNextResolver();
35512
35652
  const defaultRef = useRef(null);
@@ -35515,7 +35655,7 @@ const InputTextualFirstResolver = props => {
35515
35655
  ...props
35516
35656
  });
35517
35657
  };
35518
- const InputTextual = createComponentResolver([InputTextualFirstResolver, InputWithListResolver, InputWithSuggestionsResolver, InputTypeResolver, InputModeResolver, InputHeadlessResolver, InputTextualUI]);
35658
+ const InputTextual = createComponentResolver([InputTextualAsTextResolver, InputTextualFirstResolver, InputWithListResolver, InputWithSuggestionsResolver, InputTypeResolver, InputModeResolver, InputHeadlessResolver, InputTextualUI]);
35519
35659
  const RealInput = ({
35520
35660
  maxLength,
35521
35661
  ...domProps
@@ -43668,6 +43808,10 @@ const lengthValue = value => typeof value === "number" ? `${value}px` : value;
43668
43808
  *
43669
43809
  * getTrackedItemByIndex(index): synchronous O(1) lookup of a visible item by
43670
43810
  * its visible rank. Returns undefined when index is out of range.
43811
+ *
43812
+ * peekItems(): the items as they stand right now, without waiting for the
43813
+ * deferred notification — what a sibling rendering after the items must read
43814
+ * to paint them in the same commit.
43671
43815
  */
43672
43816
 
43673
43817
  const useItemTracker = ({ onChange } = {}) => {
@@ -43770,7 +43914,8 @@ const createItemTracker = (onChange) => {
43770
43914
  visibleItemsSignal.value = visibleItems;
43771
43915
  someChange = true;
43772
43916
  }
43773
- const noMatchCountModified = noMatchCountSignal.peek() !== newNoMatchCount;
43917
+ const noMatchCountModified =
43918
+ noMatchCountSignal.peek() !== newNoMatchCount;
43774
43919
  if (noMatchCountModified) {
43775
43920
  noMatchCountSignal.value = newNoMatchCount;
43776
43921
  someChange = true;
@@ -43964,9 +44109,32 @@ const createItemTracker = (onChange) => {
43964
44109
  return registrations.get(key);
43965
44110
  };
43966
44111
 
44112
+ // The items as they stand right now, notification pending or not — same
44113
+ // content as itemsSignal, minus the wait.
44114
+ //
44115
+ // Items register during their own render, while the signal is only updated
44116
+ // on a deferred microtask (see notify): a sibling rendering after them would
44117
+ // otherwise paint from an empty list and correct itself a frame later. That
44118
+ // frame is visible whenever the painted size feeds a layout decision — a
44119
+ // dialog sizing itself on its content measures the empty version and shifts
44120
+ // once the real one lands. Reading this instead makes the first paint the
44121
+ // right one. Callers must still subscribe to itemsSignal to re-render on
44122
+ // LATER changes; this is the value to display, not the notification.
44123
+ const peekItems = () => {
44124
+ if (!notifyScheduled) {
44125
+ return itemsSignal.peek();
44126
+ }
44127
+ const items = [];
44128
+ for (const key of allOrderedKeys) {
44129
+ items.push(allRegistrations.get(key));
44130
+ }
44131
+ return items;
44132
+ };
44133
+
43967
44134
  return {
43968
44135
  useTrackItem,
43969
44136
  getTrackedItemByIndex,
44137
+ peekItems,
43970
44138
  itemsSignal,
43971
44139
  visibleItemsSignal,
43972
44140
  countSignal,
@@ -48325,7 +48493,49 @@ installImportMetaCssBuild(import.meta);/**
48325
48493
  * slide that is about to leave.
48326
48494
  */
48327
48495
  const css$p = /* css */`
48496
+ @layer navi {
48497
+ .navi_picker_spin {
48498
+ /* A picker one steps through is still a picker: what themes every picker
48499
+ padding themes this one too. */
48500
+ --picker-spin-padding-x-default: var(--navi-picker-padding-x-default);
48501
+ --picker-spin-padding-y-default: var(--navi-picker-padding-y-default);
48502
+ }
48503
+ }
48504
+
48328
48505
  .navi_picker_spin {
48506
+ /* The padding is written on what is inside the box rather than on the box
48507
+ — the day takes all four sides, the two chevrons only the vertical ones
48508
+ — so the four sides are resolved once here, in the side-then-axis-then
48509
+ -shorthand order a Box resolves them in. What writes them: the padding
48510
+ props, through PICKER_SPIN_STYLE_CSS_VARS below. */
48511
+ --x-picker-spin-padding-top: var(
48512
+ --picker-spin-padding-top,
48513
+ var(
48514
+ --picker-spin-padding-y,
48515
+ var(--picker-spin-padding, var(--picker-spin-padding-y-default))
48516
+ )
48517
+ );
48518
+ --x-picker-spin-padding-right: var(
48519
+ --picker-spin-padding-right,
48520
+ var(
48521
+ --picker-spin-padding-x,
48522
+ var(--picker-spin-padding, var(--picker-spin-padding-x-default))
48523
+ )
48524
+ );
48525
+ --x-picker-spin-padding-bottom: var(
48526
+ --picker-spin-padding-bottom,
48527
+ var(
48528
+ --picker-spin-padding-y,
48529
+ var(--picker-spin-padding, var(--picker-spin-padding-y-default))
48530
+ )
48531
+ );
48532
+ --x-picker-spin-padding-left: var(
48533
+ --picker-spin-padding-left,
48534
+ var(
48535
+ --picker-spin-padding-x,
48536
+ var(--picker-spin-padding, var(--picker-spin-padding-x-default))
48537
+ )
48538
+ );
48329
48539
  /* What the loading outline is drawn around. */
48330
48540
  position: relative;
48331
48541
  /* Written in the control font, like the picker it wraps: the day and its
@@ -48406,17 +48616,19 @@ const css$p = /* css */`
48406
48616
  width: min(100%, var(--picker-spin-picker-width, 12ch));
48407
48617
  translate: -50% 0;
48408
48618
  }
48409
- /* The value takes it as padding; the two chevrons take it as their own
48410
- --button-padding-y (a button's padding lives on its content, see
48411
- button_ui.jsx), which is why it is said as a variable rather than applied
48412
- here. Same number on all three: it is what makes them one line rather than
48413
- three boxes. */
48414
- .navi_picker_spin [data-slide] {
48415
- padding-block: var(--picker-spin-padding-y);
48416
- }
48417
- /* Centred, always: the middle holds three values of three different lengths,
48619
+ /* Where the padding lands: all four sides on the value, the two vertical
48620
+ ones on the chevrons below the same number above and below is what makes
48621
+ the three one line rather than three boxes, while sideways it is the room
48622
+ between the value and the chevron it would otherwise touch. Not on the box
48623
+ itself: that would push the chevrons off the corners they are rounded by.
48624
+
48625
+ Centred, always: the middle holds three values of three different lengths,
48418
48626
  and a value that starts where the last one ended reads as a jump. */
48419
48627
  .navi_picker_spin [data-slide] {
48628
+ padding-top: var(--x-picker-spin-padding-top);
48629
+ padding-right: var(--x-picker-spin-padding-right);
48630
+ padding-bottom: var(--x-picker-spin-padding-bottom);
48631
+ padding-left: var(--x-picker-spin-padding-left);
48420
48632
  text-align: center;
48421
48633
  overflow: hidden;
48422
48634
  }
@@ -48443,8 +48655,12 @@ const css$p = /* css */`
48443
48655
  around it, so "one line" means the same on both sides. */
48444
48656
  .navi_picker_spin > .navi_picker_spin_way_out {
48445
48657
  box-sizing: border-box;
48446
- height: calc(1lh + 2 * var(--picker-spin-padding-y));
48447
- padding-block: var(--picker-spin-padding-y);
48658
+ height: calc(
48659
+ 1lh + var(--x-picker-spin-padding-top) +
48660
+ var(--x-picker-spin-padding-bottom)
48661
+ );
48662
+ padding-top: var(--x-picker-spin-padding-top);
48663
+ padding-bottom: var(--x-picker-spin-padding-bottom);
48448
48664
  color: inherit;
48449
48665
  background: none;
48450
48666
  border: none;
@@ -48525,8 +48741,13 @@ const css$p = /* css */`
48525
48741
  * @param {"long"|"short"|"numeric"} [format="long"] How the date is written.
48526
48742
  * Long by default, since this is a control one reads rather than a column
48527
48743
  * one scans; `short` where the room is not there.
48528
- * @param {string} [paddingY="xs"] Above and below, on the day AND on the two
48529
- * chevrons: it is what makes the three the same height.
48744
+ * @param {string} [padding] The room around the value, `paddingX`/`paddingY`
48745
+ * and the four sides included. It goes on what is inside the box rather than
48746
+ * on the box: above and below it is taken by the day AND by the two chevrons,
48747
+ * which is what makes the three the same height; sideways it is the room
48748
+ * between the day and the chevron beside it. Left unsaid it is the padding
48749
+ * every picker takes (`--navi-picker-padding-x-default` and its `-y` twin),
48750
+ * so a theme that spaces its fields spaces this one with them.
48530
48751
  * @param {number} [maxLines] How many lines the day may take before it is cut
48531
48752
  * with an ellipsis — `maxLines={1}` keeps it on one line. Without it a day
48532
48753
  * too long for the box wraps, and the box grows.
@@ -48552,7 +48773,6 @@ const DaySpin = ({
48552
48773
  duration = 250,
48553
48774
  lang,
48554
48775
  format = "long",
48555
- paddingY = "xs",
48556
48776
  vertical,
48557
48777
  readOnly,
48558
48778
  disabled,
@@ -48696,13 +48916,10 @@ const DaySpin = ({
48696
48916
  ,
48697
48917
 
48698
48918
  pseudoClasses: PICKER_SPIN_PSEUDO_CLASSES,
48919
+ styleCSSVars: PICKER_SPIN_STYLE_CSS_VARS,
48699
48920
  "data-vertical": vertical ? "" : undefined,
48700
48921
  "data-readonly": readOnlyResolved ? "" : undefined,
48701
48922
  "data-disabled": disabledResolved ? "" : undefined,
48702
- style: {
48703
- "--picker-spin-padding-y": resolveSpacingSize(paddingY),
48704
- ...rest.style
48705
- },
48706
48923
  children: [jsx(LoadingOutline, {
48707
48924
  loading: loading,
48708
48925
  color: "var(--navi-loader-color)",
@@ -48884,6 +49101,20 @@ const WayOut = ({
48884
49101
  });
48885
49102
  const PICKER_SPIN_PSEUDO_CLASSES = [":hover", ":focus-visible"];
48886
49103
 
49104
+ // A padding written on this box would sit between its border and the chevrons,
49105
+ // which are meant to reach the corners they are rounded by — so each padding
49106
+ // prop becomes a variable instead, and the CSS above hands it to the day and to
49107
+ // the chevrons themselves. Same trick, and same var chain, as Picker's.
49108
+ const PICKER_SPIN_STYLE_CSS_VARS = {
49109
+ padding: "--picker-spin-padding",
49110
+ paddingX: "--picker-spin-padding-x",
49111
+ paddingY: "--picker-spin-padding-y",
49112
+ paddingTop: "--picker-spin-padding-top",
49113
+ paddingRight: "--picker-spin-padding-right",
49114
+ paddingBottom: "--picker-spin-padding-bottom",
49115
+ paddingLeft: "--picker-spin-padding-left"
49116
+ };
49117
+
48887
49118
  // The date, and what it is to today when that is something one has a word for:
48888
49119
  // "samedi 8 août (demain)" says both where one is and how far that is, and only
48889
49120
  // the second is read at a glance.
@@ -50733,12 +50964,16 @@ const css$k = /* css */`
50733
50964
  as rows scroll under the pointer. */
50734
50965
  user-select: none;
50735
50966
  -webkit-tap-highlight-color: transparent;
50736
- /* Rendering virtualization: only the rows within each wheel's own viewport
50737
- are painted; the rest (clipped by the viewport's overflow) are skipped.
50738
- The box is still laid out (fixed main-axis size below), so offsets, snap
50739
- and the wrap math are unaffected this cuts paint/compositing cost so a
50740
- wheel with many values (or a page full of wheels) scrolls smoothly. */
50741
- content-visibility: auto;
50967
+ /* NO content-visibility here, ever. content-visibility: auto was tried
50968
+ (to skip painting clipped rows) and it breaks the first opening of any
50969
+ popup holding a wheel: rows born inside a closed dialog are "skipped"
50970
+ with no last-remembered size, so they measure as empty (padding-only) on
50971
+ the open's own layout the dialog positions itself against that width
50972
+ and visibly slides once the real width lands. The un-skip only happens
50973
+ at the next update-the-rendering step: no forced reflow can read the
50974
+ true size any earlier, making the bug unfixable from the dialog side.
50975
+ There is also nothing left to win: the window renders only
50976
+ visibleCount + 2 recycled rows, all in or against the viewport. */
50742
50977
  }
50743
50978
 
50744
50979
  /* Orientation-specific sizing/layout. The mask worn by the base layer: the
@@ -51706,23 +51941,16 @@ function WheelUI(props) {
51706
51941
  const trackedItems = tracker.itemsSignal.value;
51707
51942
  const trackedItemsRef = useRef(trackedItems);
51708
51943
  trackedItemsRef.current = trackedItems;
51709
- const itemCount = trackedItems.length;
51710
51944
 
51711
51945
  // Virtualization: render only visibleCount + 2 rows (one buffer each side to
51712
51946
  // cover the partial rows a scroll reveals + a re-render's one-frame lag) and
51713
- // recycle them, filling each from trackedItems. `centerRowSignal` holds the row
51714
- // currently at the center (round(pos / itemSize)); it changes only when the
51715
- // wheel crosses a row (not every frame), so the window re-renders on demand, not
51716
- // per frame. WheelWindow derives its top slot (base) from that row and its own
51717
- // windowSize so a windowSize change (items still registering) recomputes base
51718
- // instead of leaving the top rows unrendered. The per-frame transform is
51719
- // imperative (--wheel-offset); renderedBaseRef mirrors the base the DOM shows so
51720
- // the transform stays consistent with it.
51721
- // visibleCount + 2, but never more rows than there are values.
51722
- let windowSize = visibleCount + 2;
51723
- if (windowSize > itemCount) {
51724
- windowSize = itemCount;
51725
- }
51947
+ // recycle them. `centerRowSignal` holds the row currently at the center
51948
+ // (round(pos / itemSize)); it changes only when the wheel crosses a row (not
51949
+ // every frame), so the window re-renders on demand, not per frame. WheelWindow
51950
+ // owns both its size and its top slot (base), derived from that row and from the
51951
+ // items it reads off the tracker itself see its own comment. The per-frame
51952
+ // transform is imperative (--wheel-offset); renderedBaseRef mirrors the base the
51953
+ // DOM shows so the transform stays consistent with it.
51726
51954
  const centerRowSignal = useSignal(0);
51727
51955
  const renderedBaseRef = useRef(0);
51728
51956
  // Row size in px, measured once from a rendered slot (rows are uniform).
@@ -52426,8 +52654,8 @@ function WheelUI(props) {
52426
52654
  className: "navi_wheel_list",
52427
52655
  children: jsx(WheelWindow, {
52428
52656
  centerRowSignal: centerRowSignal,
52429
- windowSize: windowSize,
52430
- trackedItems: trackedItems,
52657
+ visibleCount: visibleCount,
52658
+ tracker: tracker,
52431
52659
  isLoop: isLoop,
52432
52660
  onBaseCommit: commitRenderedBase
52433
52661
  })
@@ -52440,8 +52668,8 @@ function WheelUI(props) {
52440
52668
  className: "navi_wheel_list",
52441
52669
  children: jsx(WheelWindow, {
52442
52670
  centerRowSignal: centerRowSignal,
52443
- windowSize: windowSize,
52444
- trackedItems: trackedItems,
52671
+ visibleCount: visibleCount,
52672
+ tracker: tracker,
52445
52673
  isLoop: isLoop
52446
52674
  })
52447
52675
  })
@@ -52458,21 +52686,38 @@ const WHEEL_PSEUDO_CLASSES = [":focus-within", ":focus-visible", ":read-only", "
52458
52686
 
52459
52687
  // The recycled window: a fixed set of `windowSize` <li> slots (one per visible
52460
52688
  // row + 2 buffer), keyed by slot position so each DOM node is reused and only its
52461
- // content re-renders. `base` (the top slot's value index) is derived here from the
52462
- // center row + windowSize computing it here, not upstream, means a windowSize
52463
- // change (items still registering) recomputes base correctly. Each slot shows
52464
- // trackedItems[base + slot] (wrapped when looping). It reads centerRowSignal, so
52465
- // it re-renders ONLY when the wheel crosses a row, not every frame the per-frame
52466
- // motion is the imperative transform in applyOffset. On each new base it calls
52467
- // onBaseCommit so the transform re-syncs to the freshly rendered rows same-frame.
52689
+ // content re-renders. `base` (the top slot's value index) and windowSize are both
52690
+ // derived here rather than upstream, from the items read straight off the tracker:
52691
+ // this component renders right after the Wheel.Item children, which register
52692
+ // during their own render, so it can draw the real rows in the very first commit.
52693
+ // Taking them from the parent instead would mean drawing an empty wheel first (the
52694
+ // parent renders before its children, and the tracker's signal only lands on a
52695
+ // microtask) a wheel with no width, which is enough to misplace whatever sizes
52696
+ // itself on it, e.g. a dialog centered on its content.
52697
+ // Each slot shows items[base + slot] (wrapped when looping). It reads
52698
+ // centerRowSignal, so it re-renders ONLY when the wheel crosses a row, not every
52699
+ // frame — the per-frame motion is the imperative transform in applyOffset. On each
52700
+ // new base it calls onBaseCommit so the transform re-syncs to the freshly rendered
52701
+ // rows same-frame.
52468
52702
  const WheelWindow = ({
52469
52703
  centerRowSignal,
52470
- windowSize,
52471
- trackedItems,
52704
+ visibleCount,
52705
+ tracker,
52472
52706
  isLoop,
52473
52707
  onBaseCommit
52474
52708
  }) => {
52709
+ // Subscribing is what re-renders this window when items are added or removed
52710
+ // later; what to draw right now is read synchronously below.
52711
+ // eslint-disable-next-line no-unused-expressions
52712
+ tracker.itemsSignal.value;
52713
+ const trackedItems = tracker.peekItems();
52475
52714
  const count = trackedItems.length;
52715
+ // visibleCount + 2 (one buffer row each side), but never more rows than there
52716
+ // are values.
52717
+ let windowSize = visibleCount + 2;
52718
+ if (windowSize > count) {
52719
+ windowSize = count;
52720
+ }
52476
52721
  let base = centerRowSignal.value - Math.floor(windowSize / 2);
52477
52722
  // Non-loop: keep the window inside the value range so every slot maps to a real
52478
52723
  // value; the transform then leaves the blank runway above the first / below the
@@ -57972,9 +58217,9 @@ const css$2 = /* css */`
57972
58217
  .navi_card_layout {
57973
58218
  --layout-margin: 30px;
57974
58219
  --layout-padding: 20px;
57975
- --layout-background: white;
58220
+ --layout-background: var(--navi-surface-color);
57976
58221
  --layout-border-width: 2px;
57977
- --layout-border-color: lightgrey;
58222
+ --layout-border-color: var(--navi-popup-border-color);
57978
58223
  --layout-border-radius: 10px;
57979
58224
  --layout-min-width: 300px;
57980
58225
  --layout-min-height: auto;
@@ -58093,7 +58338,9 @@ installImportMetaCssBuild(import.meta);const css$1 = /* css */`
58093
58338
  @layer navi {
58094
58339
  .navi_viewport_layout {
58095
58340
  --layout-padding: 40px;
58096
- --layout-background: white;
58341
+ /* The page's own paper: the shared surface token, so a dark theme gets a
58342
+ dark page without every screen overriding it. */
58343
+ --layout-background: var(--navi-surface-color);
58097
58344
  }
58098
58345
  }
58099
58346
 
@@ -58300,14 +58547,17 @@ const css = /* css */`
58300
58547
  z-index: 1;
58301
58548
  background-color: var(--navi-popup-background-color);
58302
58549
  }
58550
+ /* The separator token, not the popup border: these lines split two
58551
+ regions of the panel's own surface (the head/foot from what scrolls
58552
+ under them), they are not the panel's edge. */
58303
58553
  .navi_side_panel_head {
58304
58554
  top: 0;
58305
- border-bottom: 1px solid var(--navi-popup-border-color);
58555
+ border-bottom: 1px solid var(--navi-separator-color-default);
58306
58556
  }
58307
58557
  .navi_side_panel_foot {
58308
58558
  bottom: 0;
58309
58559
  padding: 12px 16px;
58310
- border-top: 1px solid var(--navi-popup-border-color);
58560
+ border-top: 1px solid var(--navi-separator-color-default);
58311
58561
  }
58312
58562
  }
58313
58563
  `;