@jsenv/navi 0.27.82 → 0.27.84

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.
@@ -6788,10 +6788,7 @@ const TYPO_PROPS = {
6788
6788
  // combo from MDN's own text-box example, most useful for compact things
6789
6789
  // like buttons/badges/labels. Use textBox/textBoxTrim/textBoxEdge
6790
6790
  // directly for any other combination.
6791
- textBoxCrop: applyToCssPropWhenTruthy(
6792
- "textBox",
6793
- "trim-both cap alphabetic",
6794
- ),
6791
+ textBoxCrop: applyToCssPropWhenTruthy("textBox", "trim-both cap alphabetic"),
6795
6792
  };
6796
6793
  const VISUAL_PROPS = {
6797
6794
  outline: PASS_THROUGH,
@@ -6827,6 +6824,8 @@ const VISUAL_PROPS = {
6827
6824
  overflowY: PASS_THROUGH,
6828
6825
  objectFit: PASS_THROUGH,
6829
6826
  accentColor: PASS_THROUGH,
6827
+ scrollbarWidth: PASS_THROUGH,
6828
+ scrollbarGutter: PASS_THROUGH,
6830
6829
  };
6831
6830
  const CONTENT_PROPS = {
6832
6831
  align: applyOnTwoProps("alignX", "alignY"),
@@ -18968,6 +18967,17 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-single-space");
18968
18967
  */
18969
18968
 
18970
18969
 
18970
+ // Our own compact/custom duration notation interpolates raw numbers
18971
+ // directly (unlike Intl.DurationFormat, which groups thousands on its own,
18972
+ // e.g. "5 400 secondes") — this keeps that consistent without reimplementing
18973
+ // locale-aware grouping. Falls back to the raw value as-is for a
18974
+ // non-numeric mid-edit value (e.g. "2a"), which Intl.NumberFormat can't
18975
+ // format anyway.
18976
+ const formatCompactNumber = (value, lang) => {
18977
+ const n = Number(value);
18978
+ return Number.isFinite(n) ? new Intl.NumberFormat(lang).format(n) : value;
18979
+ };
18980
+
18971
18981
  /**
18972
18982
  * Formats a date as a human-readable day string.
18973
18983
  *
@@ -19221,15 +19231,20 @@ const formatTime = (date, lang) => {
19221
19231
  * "compact" uses our own notation that omits the minute symbol when hours are present.
19222
19232
  *
19223
19233
  * @param {number} minutes
19224
- * @param {{ lang?: string, format?: "long"|"short"|"narrow"|"compact", alwaysShowHours?: boolean }} [options]
19225
- * @param {boolean} [options.alwaysShowHours=false] - Normally a zero-hours
19226
- * component is dropped entirely (a real 5-minute duration should print as
19227
- * "5 minutes", not "0 hours 5 minutes") — set this to keep it (e.g. "0 h
19228
- * et 5 min"/"0h 5min"/"0h05") instead. Only meaningful when `minutes` is
19229
- * itself less than 60, i.e. the hours component would otherwise be zero;
19230
- * used by `<Time type="time">` for a time-of-day at midnight, where
19231
- * dropping the hour would make it indistinguishable from an actual
19232
- * duration (see time.jsx's own TimeTime).
19234
+ * @param {{ lang?: string, format?: "long"|"short"|"narrow"|"compact", clockStyle?: boolean }} [options]
19235
+ * @param {boolean} [options.clockStyle=false] - Set this when `minutes`
19236
+ * represents a time-of-day rather than a real duration (used by
19237
+ * `<Time type="time">`, see time.jsx's own TimeTime) — affects two
19238
+ * things at once, both consequences of a clock's "0" being a meaningful
19239
+ * hour rather than "no hours":
19240
+ * - a zero-hours component is normally dropped entirely (a real 5-minute
19241
+ * duration should print as "5 minutes", not "0 hours 5 minutes"); this
19242
+ * keeps it instead (e.g. "0 h et 5 min"/"0h 5min"/"00h05") so midnight
19243
+ * doesn't collapse to something indistinguishable from an actual
19244
+ * 5-minute duration.
19245
+ * - `format: "compact"` also zero-pads a single-digit hour to 2 digits
19246
+ * (e.g. "5h30" → "05h30"), so it reads closer to a "05:30" clock.
19247
+ * Must not be set for plain duration formatting.
19233
19248
  *
19234
19249
  * @example
19235
19250
  * formatMinuteDuration(90, { lang: "fr" }) // "1 heure 30 minutes" (long, default)
@@ -19237,20 +19252,21 @@ const formatTime = (date, lang) => {
19237
19252
  * formatMinuteDuration(90, { lang: "fr", format: "narrow" }) // "1h 30min" (Intl narrow)
19238
19253
  * formatMinuteDuration(90, { lang: "fr", format: "compact" }) // "1h30" (custom, no minute symbol)
19239
19254
  * formatMinuteDuration(45, { lang: "en", format: "compact" }) // "45min"
19240
- * formatMinuteDuration(5, { lang: "fr", format: "narrow", alwaysShowHours: true }) // "0h 5min"
19255
+ * formatMinuteDuration(5, { lang: "fr", format: "narrow", clockStyle: true }) // "0h 5min"
19256
+ * formatMinuteDuration(330, { lang: "fr", format: "compact", clockStyle: true }) // "05h30"
19241
19257
  */
19242
19258
  const formatMinuteDuration = (
19243
19259
  minutes,
19244
- { lang = languagesSignal.value, format = "long", alwaysShowHours = false } = {},
19260
+ { lang = languagesSignal.value, format = "long", clockStyle = false } = {},
19245
19261
  ) => {
19246
19262
  const h = Math.floor(minutes / 60);
19247
19263
  const m = minutes % 60;
19248
19264
  if (format !== "compact" && typeof Intl.DurationFormat !== "undefined") {
19249
19265
  const fmt = new Intl.DurationFormat(lang, {
19250
19266
  style: format, // "long", "short", or "narrow"
19251
- ...(alwaysShowHours ? { hoursDisplay: "always" } : {}),
19267
+ ...(clockStyle ? { hoursDisplay: "always" } : {}),
19252
19268
  });
19253
- if (h === 0 && !alwaysShowHours) {
19269
+ if (h === 0 && !clockStyle) {
19254
19270
  return fmt.format({ minutes: m });
19255
19271
  }
19256
19272
  if (m === 0) {
@@ -19261,13 +19277,16 @@ const formatMinuteDuration = (
19261
19277
  // format="compact": "1h30", "45min", "2h" — no minute symbol when hours are present
19262
19278
  const hSym = naviI18n("time.duration.hour_symbol", undefined, { lang });
19263
19279
  const mSym = naviI18n("time.duration.minute_symbol", undefined, { lang });
19264
- if (h === 0 && !alwaysShowHours) {
19280
+ const hStr = clockStyle
19281
+ ? String(h).padStart(2, "0")
19282
+ : formatCompactNumber(h, lang);
19283
+ if (h === 0 && !clockStyle) {
19265
19284
  return `${m}${mSym}`;
19266
19285
  }
19267
19286
  if (m === 0) {
19268
- return `${h}${hSym}`;
19287
+ return `${hStr}${hSym}`;
19269
19288
  }
19270
- return `${h}${hSym}${String(m).padStart(2, "0")}`;
19289
+ return `${hStr}${hSym}${String(m).padStart(2, "0")}`;
19271
19290
  };
19272
19291
 
19273
19292
  /**
@@ -19322,7 +19341,9 @@ const formatSecondDuration = (
19322
19341
  const mSym = naviI18n("time.duration.minute_symbol", undefined, { lang });
19323
19342
  const sSym = naviI18n("time.duration.second_symbol", undefined, { lang });
19324
19343
  const parts = [];
19325
- if (h > 0) parts.push(`${h}${hSym}`);
19344
+ // m/s are always 0-59 by construction (never need grouping); h can be
19345
+ // arbitrarily large for a long duration.
19346
+ if (h > 0) parts.push(`${formatCompactNumber(h, lang)}${hSym}`);
19326
19347
  if (m > 0) parts.push(`${m}${mSym}`);
19327
19348
  if (s > 0 || parts.length === 0) parts.push(`${s}${sSym}`);
19328
19349
  return parts.join("");
@@ -19423,36 +19444,40 @@ const formatDuration = (
19423
19444
  const parts = [];
19424
19445
 
19425
19446
  if (hasNonZero("years")) {
19426
- parts.push(`${duration.years}${sym("year")}`);
19447
+ parts.push(`${formatCompactNumber(duration.years, lang)}${sym("year")}`);
19427
19448
  }
19428
19449
  if (hasNonZero("months")) {
19429
- parts.push(`${duration.months}${sym("month")}`);
19450
+ parts.push(`${formatCompactNumber(duration.months, lang)}${sym("month")}`);
19430
19451
  }
19431
19452
  if (hasNonZero("weeks")) {
19432
- parts.push(`${duration.weeks}${sym("week")}`);
19453
+ parts.push(`${formatCompactNumber(duration.weeks, lang)}${sym("week")}`);
19433
19454
  }
19434
19455
  if (hasNonZero("days")) {
19435
- parts.push(`${duration.days}${sym("day")}`);
19456
+ parts.push(`${formatCompactNumber(duration.days, lang)}${sym("day")}`);
19436
19457
  }
19437
19458
 
19438
- // Hours + minutes: when both present, pad minutes to 2 digits after the h symbol
19459
+ // Hours + minutes: when both present, pad minutes to 2 digits after the h
19460
+ // symbol — minutes stays a plain 2-digit pad (it's always 0-59 by
19461
+ // convention), only hours goes through grouping.
19439
19462
  const hSym = sym("hour");
19440
19463
  const mSym = sym("minute");
19441
19464
  if (hasNonZero("hours") && hasNonZero("minutes")) {
19442
19465
  parts.push(
19443
- `${duration.hours}${hSym}${String(duration.minutes).padStart(2, "0")}`,
19466
+ `${formatCompactNumber(duration.hours, lang)}${hSym}${String(duration.minutes).padStart(2, "0")}`,
19444
19467
  );
19445
19468
  } else if (hasNonZero("hours")) {
19446
- parts.push(`${duration.hours}${hSym}`);
19469
+ parts.push(`${formatCompactNumber(duration.hours, lang)}${hSym}`);
19447
19470
  } else if (hasNonZero("minutes")) {
19448
- parts.push(`${duration.minutes}${mSym}`);
19471
+ parts.push(`${formatCompactNumber(duration.minutes, lang)}${mSym}`);
19449
19472
  }
19450
19473
 
19451
19474
  if (hasNonZero("seconds")) {
19452
- parts.push(`${duration.seconds}${sym("second")}`);
19475
+ parts.push(`${formatCompactNumber(duration.seconds, lang)}${sym("second")}`);
19453
19476
  }
19454
19477
  if (hasNonZero("milliseconds")) {
19455
- parts.push(`${duration.milliseconds}${sym("millisecond")}`);
19478
+ parts.push(
19479
+ `${formatCompactNumber(duration.milliseconds, lang)}${sym("millisecond")}`,
19480
+ );
19456
19481
  }
19457
19482
  return parts.join("") || "0";
19458
19483
  };
@@ -33397,7 +33422,21 @@ const InputTextualWithSuggestions = props => {
33397
33422
  // Nice side effect is that input_group.jsx will see all input is selected
33398
33423
  // and arrow left/right will always nav between inputs.
33399
33424
  // (Otherwise we would prevent left/right + show calllout about readonly)
33425
+ //
33426
+ // Not on touch: .select() there triggers the native mobile text-selection UI
33427
+ // (handles + magnifier), which makes no sense for a picker (opens a
33428
+ // popover/dialog on tap, not meant for text selection) and isn't wanted on a
33429
+ // plain readonly text input either. onPointerDown tracks the pointer type
33430
+ // that initiated the interaction (same convention as button_ui.jsx's own
33431
+ // `e.pointerType !== "touch"` check) so the focus handler — which fires
33432
+ // right after, but as a plain FocusEvent with no pointerType of its own —
33433
+ // can also skip select() for that same interaction.
33400
33434
  const useAutoSelectReadOnly = (props) => {
33435
+ const lastPointerTypeRef = useRef(null);
33436
+ const onPointerDown = (e) => {
33437
+ props.onPointerDown?.(e);
33438
+ lastPointerTypeRef.current = e.pointerType;
33439
+ };
33401
33440
  const onFocus = (e) => {
33402
33441
  props.onFocus(e);
33403
33442
  if (e.defaultPrevented) {
@@ -33406,6 +33445,9 @@ const useAutoSelectReadOnly = (props) => {
33406
33445
  if (!e.target.readOnly) {
33407
33446
  return;
33408
33447
  }
33448
+ if (lastPointerTypeRef.current === "touch") {
33449
+ return;
33450
+ }
33409
33451
  e.preventDefault();
33410
33452
  e.target.select();
33411
33453
  };
@@ -33417,11 +33459,14 @@ const useAutoSelectReadOnly = (props) => {
33417
33459
  if (!e.target.readOnly) {
33418
33460
  return;
33419
33461
  }
33462
+ if (lastPointerTypeRef.current === "touch") {
33463
+ return;
33464
+ }
33420
33465
  e.preventDefault();
33421
33466
  e.target.select();
33422
33467
  };
33423
33468
 
33424
- return { onFocus, onMouseDown };
33469
+ return { onFocus, onMouseDown, onPointerDown };
33425
33470
  };
33426
33471
 
33427
33472
  installImportMetaCssBuild(import.meta);/**
@@ -39974,22 +40019,27 @@ const TimeTime = ({
39974
40019
  // other hour keeps at least its own "N hour(s)" wording as a hint that
39975
40020
  // this is a time-of-day, not a duration — only hour 0 loses that hint
39976
40021
  // entirely.
40022
+ // clockStyle: this is always a time-of-day here, never a duration — keeps
40023
+ // a zero hour instead of dropping it (midnight would otherwise be
40024
+ // indistinguishable from an actual 5-minute duration), and in
40025
+ // format="compact" also zero-pads a single-digit hour so "5h30"/"0h05"
40026
+ // read as "05h30"/"00h05", closer to a "HH:MM" clock.
39977
40027
  let text;
39978
40028
  if (date.getHours() !== 0) {
39979
40029
  text = formatMinuteDuration(totalMinutes, {
39980
40030
  lang,
39981
- format
40031
+ format,
40032
+ clockStyle: true
39982
40033
  });
39983
40034
  } else if (format !== "long") {
39984
40035
  // short/narrow/compact: keep the "0 h"/"0h" hour part instead of
39985
- // dropping it (formatMinuteDuration's own alwaysShowHours) — e.g.
39986
- // "0 h et 5 min"/"0h 5min"/"0h05" rather than substituting a
39987
- // translated "midnight" word, which would look out of place squeezed
39988
- // into these otherwise terse, symbol-based formats.
40036
+ // dropping it — e.g. "0 h et 5 min"/"0h 5min"/"00h05" — rather than
40037
+ // substituting a translated "midnight" word, which would look out of
40038
+ // place squeezed into these otherwise terse, symbol-based formats.
39989
40039
  text = formatMinuteDuration(totalMinutes, {
39990
40040
  lang,
39991
40041
  format,
39992
- alwaysShowHours: true
40042
+ clockStyle: true
39993
40043
  });
39994
40044
  } else {
39995
40045
  const midnightWord = naviI18n("time.midnight", undefined, {
@@ -40004,7 +40054,7 @@ const TimeTime = ({
40004
40054
  text = formatMinuteDuration(totalMinutes, {
40005
40055
  lang,
40006
40056
  format,
40007
- alwaysShowHours: true
40057
+ clockStyle: true
40008
40058
  });
40009
40059
  } else {
40010
40060
  // Swap just the "0 heure(s)" part of the Intl-generated duration
@@ -40475,7 +40525,11 @@ const createItemTracker = (onChange) => {
40475
40525
  }
40476
40526
 
40477
40527
  // Build allItems and visibleItems in a single pass over allOrderedKeys.
40478
- // Visible items are those without data.hidden — same relative order as orderedKeys.
40528
+ // Visible items are those without data.hidden or data.filtered — same
40529
+ // relative order as orderedKeys (syncItem already excludes both from
40530
+ // orderedKeys; this must match or consumers relying on visibleCountSignal
40531
+ // for virtual-scroll accounting, e.g. list.jsx's filler sizing, would
40532
+ // count filtered-out items as if they still took up space).
40479
40533
  const prevAllItems = itemsSignal.peek();
40480
40534
  const prevVisibleItems = visibleItemsSignal.peek();
40481
40535
  let allItemsChanged = prevAllItems.length !== allOrderedKeys.length;
@@ -40494,7 +40548,7 @@ const createItemTracker = (onChange) => {
40494
40548
  if (item.match === false) {
40495
40549
  newNoMatchCount++;
40496
40550
  }
40497
- if (!item.hidden) {
40551
+ if (!item.hidden && !item.filtered) {
40498
40552
  const visibleIdx = visibleItems.length;
40499
40553
  visibleItems.push(item);
40500
40554
  if (!visibleItemsChanged && item !== prevVisibleItems[visibleIdx]) {
@@ -41218,10 +41272,9 @@ const ListItemSelectable = props => {
41218
41272
  const {
41219
41273
  index,
41220
41274
  id = defaultId,
41221
- highlight,
41275
+ matchInfo,
41222
41276
  hidden,
41223
41277
  filtered,
41224
- matchScore,
41225
41278
  defaultSelected,
41226
41279
  selected,
41227
41280
  pointed,
@@ -41263,10 +41316,9 @@ const ListItemSelectable = props => {
41263
41316
  return jsxs(Next, {
41264
41317
  id: id,
41265
41318
  index: index,
41266
- highlight: highlight,
41319
+ matchInfo: matchInfo,
41267
41320
  filtered: filtered,
41268
41321
  hidden: hidden,
41269
- matchScore: matchScore,
41270
41322
  "aria-selected": checked,
41271
41323
  selected: checked,
41272
41324
  "navi-selectable": "",
@@ -41580,6 +41632,7 @@ const css$m = /* css */`
41580
41632
  flex-wrap: inherit;
41581
41633
  overflow: auto;
41582
41634
  overscroll-behavior: inherit; /* inherit select behavior */
41635
+ scrollbar-width: inherit;
41583
41636
  }
41584
41637
 
41585
41638
  &[data-expand-x] {
@@ -41871,7 +41924,7 @@ const ListUI = props => {
41871
41924
  import.meta.css = [css$m, "@jsenv/navi/src/control/list/list.jsx"];
41872
41925
  const {
41873
41926
  ref,
41874
- renderBudget = RENDER_BUDGET_DEFAULT,
41927
+ renderBudget: renderBudgetProp = RENDER_BUDGET_DEFAULT,
41875
41928
  renderBudgetSkipCheck,
41876
41929
  role,
41877
41930
  fallback,
@@ -41892,6 +41945,15 @@ const ListUI = props => {
41892
41945
  spacing,
41893
41946
  ...rest
41894
41947
  } = props;
41948
+ // Accept a string (e.g. from an HTML attribute: renderBudget="50") the
41949
+ // same way a bare number would work — arithmetic below (renderBudget / 2,
41950
+ // start + renderBudget, etc.) would silently misbehave on a raw string
41951
+ // ("+" concatenates instead of adding).
41952
+ let renderBudget = renderBudgetProp;
41953
+ if (typeof renderBudget === "string") {
41954
+ const parsed = Number(renderBudget);
41955
+ renderBudget = Number.isFinite(parsed) ? parsed : RENDER_BUDGET_DEFAULT;
41956
+ }
41895
41957
  if (renderBudget < 30 && !renderBudgetSkipCheck) {
41896
41958
  console.warn(`List: renderBudget=${renderBudget} is too low. A renderBudget below 30 is not supported: on large screens or when the list grows, items outside the window would appear as blank space instead of rendered content. Use a value of at least 30, or omit the prop to use the default (${RENDER_BUDGET_DEFAULT}).`);
41897
41959
  }
@@ -42021,7 +42083,7 @@ const ListFirstResolver = props => {
42021
42083
  * action?: (value: any) => void,
42022
42084
  * uiAction?: (value: any) => void,
42023
42085
  * popover?: boolean,
42024
- * renderBudget?: number,
42086
+ * renderBudget?: number | string,
42025
42087
  * virtualItemSize?: number,
42026
42088
  * fallback?: import("ignore:preact").ComponentChildren,
42027
42089
  * searchFallback?: import("ignore:preact").ComponentChildren,
@@ -42282,6 +42344,7 @@ const useListScrollSync = ({
42282
42344
  // (pour l'instant pas grave car on travaille pour le mode select qui fermera le dialog au select)
42283
42345
  const savedScrollRef = useRef(null);
42284
42346
  const topMatchScoresKeyRef = useRef("");
42347
+ const restoreScrollRafRef = useRef(null);
42285
42348
  useLayoutEffect(() => {
42286
42349
  const listScrollContainerEl = ref.current?.querySelector(`.navi_list_scroll_container`);
42287
42350
  if (!listScrollContainerEl) {
@@ -42298,7 +42361,20 @@ const useListScrollSync = ({
42298
42361
  savedScrollRef.current = null;
42299
42362
  debugScroll("Restoring scroll to", savedScroll);
42300
42363
  updateRenderWindow(savedScroll.renderWindow.start, savedScroll.renderWindow.end, "restore scroll window");
42301
- const raf = requestAnimationFrame(() => {
42364
+ // Tracked in a ref rather than cancelled via this effect's own cleanup:
42365
+ // updateRenderWindow above triggers a re-render, which re-runs this
42366
+ // effect (it has no dependency array — it needs to reactively poll
42367
+ // tracker state on every render) *before* the RAF below fires. That
42368
+ // second invocation sees savedScrollRef.current already nulled and
42369
+ // bails out early — if the RAF were tied to this invocation's cleanup,
42370
+ // it would get cancelled right there with nothing to replace it,
42371
+ // silently dropping the scroll restore (renderWindow ends up correct,
42372
+ // but scrollTop stays wherever it was, showing blank filler space).
42373
+ if (restoreScrollRafRef.current) {
42374
+ cancelAnimationFrame(restoreScrollRafRef.current);
42375
+ }
42376
+ restoreScrollRafRef.current = requestAnimationFrame(() => {
42377
+ restoreScrollRafRef.current = null;
42302
42378
  const left = savedScroll.left;
42303
42379
  const top = savedScroll.top;
42304
42380
  // use scrollTo to respect eventual css scroll-behavior: smooth;
@@ -42321,13 +42397,11 @@ const useListScrollSync = ({
42321
42397
  event: new CustomEvent("navi_scroll_restore")
42322
42398
  });
42323
42399
  });
42324
- return () => {
42325
- cancelAnimationFrame(raf);
42326
- };
42400
+ return undefined;
42327
42401
  }
42328
42402
  const visibleItems = tracker.visibleItemsSignal.peek();
42329
42403
  const topItems = visibleItems.slice(0, renderBudget);
42330
- const topMatchScoresKey = topItems.map(i => `${i.id}:${i.matchScore ?? ""}`).join(",");
42404
+ const topMatchScoresKey = topItems.map(i => `${i.id}:${i.matchInfo?.matchScore ?? ""}`).join(",");
42331
42405
  const currentTopMatchScore = topMatchScoresKeyRef.current;
42332
42406
  if (topMatchScoresKey === currentTopMatchScore) {
42333
42407
  // no changes in top matches -> no need to scroll
@@ -42742,9 +42816,13 @@ const ListItemUI = props => {
42742
42816
  const renderWindow = useContext(RenderWindowContext);
42743
42817
  const tracker = useContext(ListItemTrackerContext);
42744
42818
  const searchNoMatchMode = useContext(SearchNoMatchModeContext);
42745
- // Derive filtered/hidden/nonMatching from the `match` prop + searchNoMatchMode context.
42746
- // The `match` prop replaces the older `filtered`/`hidden` per-item props.
42747
- if (props.match === false) {
42819
+ // There is no standalone match/matchScore/highlight prop participation
42820
+ // in a matching system (search, filter…) only goes through `matchInfo`
42821
+ // (e.g. useSearchText's getItemMatchInfo(item): { match, matchScore,
42822
+ // matchRanges }), so there is exactly one way to wire it up.
42823
+ const matchInfo = props.matchInfo;
42824
+ // Derive filtered/hidden/muted from matchInfo.match + searchNoMatchMode context.
42825
+ if (matchInfo?.match === false) {
42748
42826
  if (searchNoMatchMode === "remove") {
42749
42827
  props.filtered = true;
42750
42828
  } else if (searchNoMatchMode === "invisible_and_inert") {
@@ -42813,7 +42891,7 @@ const ListItemReal = props => {
42813
42891
  id,
42814
42892
  hidden,
42815
42893
  muted,
42816
- highlight,
42894
+ matchInfo,
42817
42895
  children,
42818
42896
  ...rest
42819
42897
  } = props;
@@ -42831,8 +42909,9 @@ const ListItemReal = props => {
42831
42909
  pendingScroll.resolve(itemEl);
42832
42910
  }, [needScrollOnMount]);
42833
42911
 
42834
- // CSS Highlight API: mark matching text ranges when highlight prop is set.
42835
- useSearchHighlight(ref, highlight, [children, hidden]);
42912
+ // CSS Highlight API: mark matching text ranges from matchInfo.matchRanges,
42913
+ // if any (there is no standalone highlight prop — see ListItem's own doc).
42914
+ useSearchHighlight(ref, matchInfo?.matchRanges, [children, hidden]);
42836
42915
  const columnsOverrideProps = useListItemColumnsOverrideProps(rest.style);
42837
42916
  return jsx(Box, {
42838
42917
  as: "li",
@@ -42845,9 +42924,7 @@ const ListItemReal = props => {
42845
42924
  ...rest,
42846
42925
  ...columnsOverrideProps,
42847
42926
  index: undefined,
42848
- selected: undefined,
42849
- matchScore: undefined,
42850
- match: undefined
42927
+ selected: undefined
42851
42928
  // We use aria-hidden and not hidden because hidden would be forced to
42852
42929
  // display: none while here we want to keep it in the DOM to avoid layout shift
42853
42930
  // but visually hidden
@@ -42926,7 +43003,21 @@ const LIST_ITEM_PSEUDO_ELEMENTS = ["::highlight"];
42926
43003
  * filtered — when true, item is excluded from visible count and removed from DOM entirely
42927
43004
  * hidden — when true, item is excluded from visible count (no virtual scroll height)
42928
43005
  * but stays in DOM with the native HTML hidden attribute
42929
- * highlightarray of [start, end] ranges to highlight via CSS Highlight API
43006
+ * matchInfoparticipation in a matching system (search, filter…): the
43007
+ * object useSearchText's getItemMatchInfo(item) returns
43008
+ * (or any object shaped the same way):
43009
+ * <ListItem matchInfo={getItemMatchInfo(item)} />
43010
+ * There is no standalone match/matchScore/highlight prop —
43011
+ * matchInfo is the only way to wire this up:
43012
+ * match — false is interpreted per the List's own
43013
+ * searchNoMatchMode ("remove" -> filtered,
43014
+ * "invisible_and_inert" -> hidden,
43015
+ * "muted" -> muted).
43016
+ * matchScore — this item's search relevance score (higher =
43017
+ * more relevant). Only read for search-driven
43018
+ * scroll-to-top-match behavior.
43019
+ * matchRanges — array of [start, end] ranges to highlight via
43020
+ * CSS Highlight API.
42930
43021
  * ...rest — forwarded to the rendered <li> element
42931
43022
  */
42932
43023
  const ListItem = createComponentResolver([ListItemFirstResolver, ListItemSelectableResolver, ListItemHeaderOrFooterResolver, ListItemPresentationResolver, ListItemUI]);
@@ -44586,7 +44677,8 @@ const mergeRanges = (ranges) => {
44586
44677
  *
44587
44678
  * const [orderedItems, getItemMatchInfo] = useSearch(search, items, searchPerson);
44588
44679
  * // getItemMatchInfo(item).matchRanges is { ".name": [[start,end],…], ".address": [[start,end],…] }
44589
- * // Pass it to <ListItem highlight={matchRanges} /> — ListItem handles the object format.
44680
+ * // Pass the whole thing: <ListItem matchInfo={getItemMatchInfo(item)} />
44681
+ * // — ListItem handles the per-selector object format for matchRanges.
44590
44682
  * ```
44591
44683
  *
44592
44684
  * Each field config:
@@ -44626,7 +44718,9 @@ const createSearch = (fields) => {
44626
44718
  * followed by non-matched items in their natural order. No item is hidden.
44627
44719
  * Returns [orderedItems, getItemMatchInfo].
44628
44720
  * - orderedItems: all items, reordered
44629
- * - getItemMatchInfo(item): { match, score, ranges }
44721
+ * - getItemMatchInfo(item): { match, matchScore, matchRanges } — pass the
44722
+ * whole thing straight to <ListItem matchInfo={getItemMatchInfo(item)} />,
44723
+ * there is no need to destructure the three fields by hand.
44630
44724
  *
44631
44725
  * When searchText is empty, natural order is preserved and all items match with score 0.
44632
44726
  *
@@ -44679,8 +44773,8 @@ const buildMatchInfo = (searchText, items, matchFn) => {
44679
44773
  if (!result.match) {
44680
44774
  nonMatched.push({
44681
44775
  item,
44682
- score: result.matchScore,
44683
- ranges: result.matchRanges,
44776
+ matchScore: result.matchScore,
44777
+ matchRanges: result.matchRanges,
44684
44778
  });
44685
44779
  continue;
44686
44780
  }
@@ -44700,23 +44794,23 @@ const buildMatchInfo = (searchText, items, matchFn) => {
44700
44794
  }
44701
44795
  }
44702
44796
  if (lo < scoreEntries.length && scoreEntries[lo][0] === score) {
44703
- scoreEntries[lo][1].push({ item, ranges: result.matchRanges });
44797
+ scoreEntries[lo][1].push({ item, matchRanges: result.matchRanges });
44704
44798
  } else {
44705
44799
  scoreEntries.splice(lo, 0, [
44706
44800
  score,
44707
- [{ item, ranges: result.matchRanges }],
44801
+ [{ item, matchRanges: result.matchRanges }],
44708
44802
  ]);
44709
44803
  }
44710
44804
  }
44711
44805
 
44712
44806
  const matchInfoMap = new Map();
44713
44807
  for (const [score, bucket] of scoreEntries) {
44714
- for (const { item, ranges } of bucket) {
44715
- matchInfoMap.set(item, { match: true, score, ranges });
44808
+ for (const { item, matchRanges } of bucket) {
44809
+ matchInfoMap.set(item, { match: true, matchScore: score, matchRanges });
44716
44810
  }
44717
44811
  }
44718
- for (const { item, score, ranges } of nonMatched) {
44719
- matchInfoMap.set(item, { match: false, score, ranges });
44812
+ for (const { item, matchScore, matchRanges } of nonMatched) {
44813
+ matchInfoMap.set(item, { match: false, matchScore, matchRanges });
44720
44814
  }
44721
44815
 
44722
44816
  return { scoreEntries, nonMatched, matchInfoMap };