@jsenv/navi 0.27.49 → 0.27.51

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.
@@ -8869,11 +8869,11 @@ import.meta.css = [/* css */`
8869
8869
  }
8870
8870
  }
8871
8871
  /*
8872
-
8873
- It's very common to declare a display on component as follow
8872
+ To set display on component, code usually do something like:
8874
8873
  .component_class { display: component_display; }
8875
8874
 
8876
- This kill the default behavior of [hidden] attribute and we need to explicitly handle it with:
8875
+ It overrides the default behavior of [hidden] attribute!
8876
+ This needs to be explicitly handled with:
8877
8877
  .component_class[hidden] { display: none; }
8878
8878
 
8879
8879
  To avoid this extra work and potential mistakes we force the default behavior of [hidden] attribute.
@@ -19687,11 +19687,6 @@ const MIN_CONSTRAINT = {
19687
19687
  name: "min",
19688
19688
  messageAttribute: "data-min-message",
19689
19689
  check: (field) => {
19690
- // Sub-inputs inside a duration_group: min/max is validated at the group level, not per-field.
19691
- // Per-field min/max attributes are kept for arrow-key navigation bounds only.
19692
- if (field.parentUIStateController?.controlType === "duration_group") {
19693
- return null;
19694
- }
19695
19690
  if (field.controlType === "duration_group") {
19696
19691
  const min = field.controlHostProps.min;
19697
19692
  if (min === undefined || min === null) {
@@ -19795,11 +19790,6 @@ const MAX_CONSTRAINT = {
19795
19790
  name: "max",
19796
19791
  messageAttribute: "data-max-message",
19797
19792
  check: (field) => {
19798
- // Sub-inputs inside a duration_group: min/max is validated at the group level, not per-field.
19799
- // Per-field min/max attributes are kept for arrow-key navigation bounds only.
19800
- if (field.parentUIStateController?.controlType === "duration_group") {
19801
- return null;
19802
- }
19803
19793
  if (field.controlType === "duration_group") {
19804
19794
  const max = field.controlHostProps.max;
19805
19795
  if (max === undefined || max === null) {
@@ -20171,21 +20161,21 @@ const NAVI_VALIDITY_CHANGE_CUSTOM_EVENT = "navi_validity_change";
20171
20161
 
20172
20162
  const VALIDATION_TOKEN = createOpenToken();
20173
20163
 
20164
+ // the order matters here, the first constraint is picked first when multiple constraints fail
20165
+ // so it's better to keep the most complex constraints at the end of the list
20166
+ // so the more basic ones shows up first
20174
20167
  const STANDARD_CONSTRAINT_SET = new Set([
20175
20168
  REQUIRED_CONSTRAINT,
20176
20169
  PATTERN_CONSTRAINT,
20177
20170
  TYPE_EMAIL_CONSTRAINT,
20178
20171
  TYPE_NUMBER_CONSTRAINT,
20179
- MIN_LENGTH_CONSTRAINT,
20180
- MAX_LENGTH_CONSTRAINT,
20181
20172
  MIN_CONSTRAINT,
20182
20173
  MAX_CONSTRAINT,
20183
20174
  STEP_CONSTRAINT,
20175
+ MIN_LENGTH_CONSTRAINT,
20176
+ MAX_LENGTH_CONSTRAINT,
20184
20177
  ]);
20185
20178
  const NAVI_CONSTRAINT_SET = new Set([
20186
- // the order matters here, the last constraint is picked first when multiple constraints fail
20187
- // so it's better to keep the most complex constraints at the beginning of the list
20188
- // so the more basic ones shows up first
20189
20179
  MIN_SPECIAL_CHAR_CONSTRAINT,
20190
20180
  SINGLE_SPACE_CONSTRAINT,
20191
20181
  MIN_DIGIT_CONSTRAINT,
@@ -20483,10 +20473,10 @@ const createControlValidation = (
20483
20473
  const pickConstraintFailureInfo = (a, b) => {
20484
20474
  const aPrio = getConstraintFailureInfoPriority(a);
20485
20475
  const bPrio = getConstraintFailureInfoPriority(b);
20486
- if (aPrio > bPrio) {
20487
- return a;
20476
+ if (bPrio > aPrio) {
20477
+ return b;
20488
20478
  }
20489
- return b;
20479
+ return a;
20490
20480
  };
20491
20481
  const getConstraintFailureInfoPriority = (failureInfo) => {
20492
20482
  if (failureInfo.status === "error") {
@@ -21972,36 +21962,30 @@ const FormContext = createContext();
21972
21962
  * Each value is a regex character class (including the [ ] delimiters).
21973
21963
  */
21974
21964
  const CHAR_CLASS_PRESETS = {
21975
- numeric: "[0-9]", // digits only
21976
- alpha: "[A-Za-z]", // letters only
21977
- alphanumeric: "[0-9A-Za-z]", // letters and digits
21978
- uppercase: "[A-Z]", // uppercase letters only
21979
- tel: "[-0-9+() ]", // phone: digits, +, -, parens, space
21980
- card: "[0-9 ]", // credit card: digits and spaces
21981
- hex: "[0-9A-Fa-f]", // hexadecimal digits
21982
- pin: "[0-9]", // numeric PIN
21983
- postal: "[0-9A-Za-z -]", // postal code (FR, UK, US)
21984
- iban: "[0-9A-Z]", // IBAN: uppercase and digits
21985
- slug: "[a-z0-9-]", // URL slug
21986
- };
21987
-
21988
- // Presets that imply a specific mobile keyboard layout
21989
- const INPUT_MODE_FROM_PRESET = {
21990
- numeric: "numeric",
21991
- pin: "numeric",
21992
- card: "numeric",
21993
- tel: "tel",
21965
+ numeric: "[0-9]", // digits only
21966
+ alpha: "[A-Za-z]", // letters only
21967
+ alphanumeric: "[0-9A-Za-z]", // letters and digits
21968
+ decimal: "[-0-9.,]", // digits, minus, dot, comma
21969
+ uppercase: "[A-Z]", // uppercase letters only
21970
+ tel: "[-0-9+() ]", // phone: digits, +, -, parens, space
21971
+ email: "[a-zA-Z0-9._%+@-]", // email characters
21972
+ card: "[0-9 ]", // credit card: digits and spaces
21973
+ hex: "[0-9A-Fa-f]", // hexadecimal digits
21974
+ pin: "[0-9]", // numeric PIN
21975
+ postal: "[0-9A-Za-z -]", // postal code (FR, UK, US)
21976
+ iban: "[0-9A-Z]", // IBAN: uppercase and digits
21977
+ slug: "[a-z0-9-]", // URL slug
21994
21978
  };
21995
21979
 
21996
21980
  // Specific i18n keys per preset — more informative than the generic fallback
21997
21981
  const MESSAGE_KEY_FROM_PRESET = {
21998
- numeric: "constraint.guard.number",
21999
- pin: "constraint.guard.number",
22000
- alpha: "constraint.guard.alpha",
21982
+ numeric: "constraint.guard.number",
21983
+ pin: "constraint.guard.number",
21984
+ alpha: "constraint.guard.alpha",
22001
21985
  alphanumeric: "constraint.guard.alphanumeric",
22002
- uppercase: "constraint.guard.uppercase",
22003
- hex: "constraint.guard.hex",
22004
- slug: "constraint.guard.slug",
21986
+ uppercase: "constraint.guard.uppercase",
21987
+ hex: "constraint.guard.hex",
21988
+ slug: "constraint.guard.slug",
22005
21989
  // tel, card, postal, iban, custom → generic fallback
22006
21990
  };
22007
21991
 
@@ -22014,13 +21998,6 @@ const resolveCharClass = (value) => {
22014
21998
  return CHAR_CLASS_PRESETS[value] ?? value;
22015
21999
  };
22016
22000
 
22017
- /**
22018
- * Returns the inputMode to auto-apply for the given preset, or null.
22019
- */
22020
- const resolveInputModeFromAllowedChars = (value) => {
22021
- return INPUT_MODE_FROM_PRESET[value] ?? null;
22022
- };
22023
-
22024
22001
  /**
22025
22002
  * Returns the i18n key for the char guard rejection message.
22026
22003
  * Falls back to the generic "constraint.guard.chars" for custom classes and
@@ -32213,20 +32190,99 @@ const resolveInputProps = (props) => {
32213
32190
  if (currentTypeStepFormatter) {
32214
32191
  props.step = currentTypeStepFormatter(props.step);
32215
32192
  }
32193
+
32194
+ // For navi_number: choose inputMode based on whether step/min/max suggest decimals.
32195
+ // inputMode="numeric" (integer keyboard) vs "decimal" (keyboard with decimal separator).
32196
+ if (currentType === "navi_number") {
32197
+ if (props.inputMode === undefined) {
32198
+ props.inputMode =
32199
+ hasDecimalPlaces(props.step) ||
32200
+ hasDecimalPlaces(props.min) ||
32201
+ hasDecimalPlaces(props.max)
32202
+ ? "decimal"
32203
+ : "numeric";
32204
+ }
32205
+ }
32206
+
32207
+ const { charGuard } = props;
32208
+ if (charGuard) {
32209
+ if (charGuard === true || charGuard === "auto") {
32210
+ // Auto-resolve charGuard from context.
32211
+ let charGuardResolved;
32212
+ const inputMode = props.inputMode;
32213
+ if (inputMode === "numeric") {
32214
+ charGuardResolved = "numeric";
32215
+ } else if (inputMode === "decimal") {
32216
+ charGuardResolved = "decimal";
32217
+ } else if (currentType === "tel") {
32218
+ charGuardResolved = "tel";
32219
+ } else if (currentType === "email") {
32220
+ charGuardResolved = "email";
32221
+ }
32222
+ if (charGuardResolved !== undefined) {
32223
+ props.charGuard = charGuardResolved;
32224
+ }
32225
+ }
32226
+ // charGuard is now resolved: derive inputMode from it if not already set.
32227
+ if (props.inputMode === undefined && props.charGuard) {
32228
+ const autoMode = INPUT_MODE_FROM_CHAR_GUARD[props.charGuard];
32229
+ if (autoMode) {
32230
+ props.inputMode = autoMode;
32231
+ }
32232
+ }
32233
+ // Build pattern from the resolved charGuard (preset name → class, or raw class passthrough).
32234
+ if (props.pattern === undefined && props.charGuard) {
32235
+ const charClass = CHAR_CLASS_PRESETS[props.charGuard] ?? props.charGuard;
32236
+ props.pattern = `${charClass}*`;
32237
+ }
32238
+ }
32239
+
32240
+ // Compute maxLength from max when inputMode is numeric/decimal.
32241
+ // Done here (after inputMode is set) so controller.props has the resolved value.
32242
+ if (props.maxLength === undefined) {
32243
+ if (props.inputMode === "numeric") {
32244
+ const { min, max } = props;
32245
+ if (max === undefined) ; else {
32246
+ const canBeNegative = min === undefined ? max < 0 : min < 0;
32247
+ const signCharCount = canBeNegative ? 1 : 0;
32248
+ const integerDigitCount = String(Math.floor(Math.abs(max))).length;
32249
+ props.maxLength = signCharCount + integerDigitCount;
32250
+ }
32251
+ } else if (props.inputMode === "decimal") {
32252
+ const { min, max, step } = props;
32253
+ if (max === undefined) ; else if (step === undefined) ; else {
32254
+ const canBeNegative = min === undefined ? max < 0 : min < 0;
32255
+ const signCharCount = canBeNegative ? 1 : 0;
32256
+ const integerDigitCount = String(Math.floor(Math.abs(max))).length;
32257
+ const stepStr = String(step);
32258
+ const dotIndex = stepStr.indexOf(".");
32259
+ // integer step + decimal inputMode is an unusual combo, but we stay consistent:
32260
+ // no decimal part in maxLength since valid values are whole numbers anyway
32261
+ const isIntegerStep = dotIndex === -1;
32262
+ const decimalSignCharCount = isIntegerStep ? 0 : 1;
32263
+ const decimalDigitCount = isIntegerStep
32264
+ ? 0
32265
+ : stepStr.length - dotIndex - 1;
32266
+ props.maxLength =
32267
+ signCharCount +
32268
+ integerDigitCount +
32269
+ decimalSignCharCount +
32270
+ decimalDigitCount;
32271
+ }
32272
+ }
32273
+ }
32274
+
32275
+ // Resolve maxLengthGuard boolean/auto → the computed maxLength number.
32276
+ if (props.maxLengthGuard === true || props.maxLengthGuard === "auto") {
32277
+ props.maxLengthGuard =
32278
+ typeof props.maxLength === "number" ? props.maxLength : undefined;
32279
+ }
32280
+
32216
32281
  const currentTypeDefaults = NAVI_TYPE_DEFAULTS[currentType];
32217
32282
  if (!currentTypeDefaults) {
32218
32283
  return;
32219
32284
  }
32220
- // For navi_number: choose inputMode based on whether step/min/max suggest decimals.
32221
- // inputMode="numeric" (integer keyboard) vs "decimal" (keyboard with decimal separator).
32222
- if (currentType === "navi_number" && props.inputMode === undefined) {
32223
- props.inputMode =
32224
- hasDecimalPlaces(props.step) ||
32225
- hasDecimalPlaces(props.min) ||
32226
- hasDecimalPlaces(props.max)
32227
- ? "decimal"
32228
- : "numeric";
32229
- }
32285
+
32230
32286
  for (const key of Object.keys(currentTypeDefaults)) {
32231
32287
  if (props[key] === undefined) {
32232
32288
  props[key] = currentTypeDefaults[key];
@@ -32237,6 +32293,15 @@ const resolveInputProps = (props) => {
32237
32293
  resolveInputProps(props);
32238
32294
  };
32239
32295
 
32296
+ // Presets that imply a specific mobile keyboard inputMode.
32297
+ const INPUT_MODE_FROM_CHAR_GUARD = {
32298
+ numeric: "numeric",
32299
+ pin: "numeric",
32300
+ card: "numeric",
32301
+ tel: "tel",
32302
+ decimal: "decimal",
32303
+ };
32304
+
32240
32305
  const normalizeToDate = (value) => {
32241
32306
  if (value === undefined || value === null) {
32242
32307
  return null;
@@ -32702,7 +32767,7 @@ const RangePseudoElements = ["::-navi-loader"];
32702
32767
  const InputModeResolver = props => {
32703
32768
  const Next = useNextResolver();
32704
32769
  if (props.inputMode === "numeric" || props.inputMode === "decimal") {
32705
- return jsx(InputModeNumeric, {
32770
+ return jsx(InputModeNumericOrDecimal, {
32706
32771
  ...props
32707
32772
  });
32708
32773
  }
@@ -32710,37 +32775,26 @@ const InputModeResolver = props => {
32710
32775
  ...props
32711
32776
  });
32712
32777
  };
32713
- const InputModeNumeric = props => {
32778
+ const InputModeNumericOrDecimal = props => {
32714
32779
  const Next = useNextResolver();
32715
- const {
32716
- min,
32717
- max,
32718
- step = 1
32719
- } = props;
32720
- let maxLength;
32721
- if (max !== undefined) {
32722
- const integerDigits = String(Math.floor(max)).length;
32723
- // If step has decimal places, the value can contain a separator + those digits
32724
- const stepStr = String(step);
32725
- const dotIndex = stepStr.indexOf(".");
32726
- const decimalDigits = dotIndex === -1 ? 0 : stepStr.length - dotIndex - 1;
32727
- // If min is negative (or unknown and max itself is negative), a "-" sign can appear
32728
- const canBeNegative = min !== undefined ? min < 0 : max < 0;
32729
- const signChar = canBeNegative ? 1 : 0;
32730
- maxLength = signChar + integerDigits + (decimalDigits > 0 ? 1 + decimalDigits : 0);
32731
- }
32732
32780
  return jsx(Next, {
32733
- maxLength: maxLength,
32734
32781
  ...props,
32735
32782
  onInput: e => {
32736
32783
  props.onInput?.(e);
32737
32784
  if (e.defaultPrevented) {
32738
32785
  return;
32739
32786
  }
32740
- if (maxLength === undefined) {
32741
- return;
32742
- }
32743
32787
  const input = e.currentTarget;
32788
+ let maxLength;
32789
+ const maxLengthProp = input.maxLength;
32790
+ if (maxLengthProp === -1) {
32791
+ const naviMaxLengthAttr = input.getAttribute("navi-max-length");
32792
+ if (naviMaxLengthAttr === null) {
32793
+ // no max length
32794
+ return;
32795
+ }
32796
+ maxLength = Number(naviMaxLengthAttr);
32797
+ }
32744
32798
  if (input.value.length < maxLength) {
32745
32799
  return;
32746
32800
  }
@@ -33705,18 +33759,6 @@ const InputTextualFirstResolver = props => {
33705
33759
  const defaultRef = useRef(null);
33706
33760
  props.ref = props.ref || defaultRef;
33707
33761
  resolveInputProps(props);
33708
-
33709
- // charGuard → auto inputMode + auto pattern for mobile keyboard hints
33710
- if (props.charGuard) {
33711
- if (props.inputMode === undefined) {
33712
- const autoMode = resolveInputModeFromAllowedChars(props.charGuard);
33713
- if (autoMode) props.inputMode = autoMode;
33714
- }
33715
- if (props.pattern === undefined) {
33716
- const charClass = resolveCharClass(props.charGuard);
33717
- props.pattern = `${charClass}*`;
33718
- }
33719
- }
33720
33762
  return jsx(Next, {
33721
33763
  ...props
33722
33764
  });
@@ -33732,6 +33774,10 @@ const RealInput = props => {
33732
33774
  // Never set native maxLength — our guard handles it. maxLength stays in
33733
33775
  // inputControlHostProps so form validation constraints still read it.
33734
33776
  maxLength: undefined
33777
+ // But do expose it (needed by navi_input_full event)
33778
+ ,
33779
+
33780
+ "navi-max-length": props.maxLength
33735
33781
  });
33736
33782
  };
33737
33783
  const InputStyleCSSVars = {
@@ -34671,7 +34717,9 @@ const InputDuration = props => {
34671
34717
  uiAction,
34672
34718
  action,
34673
34719
  unitHour,
34674
- textAlign = "auto"
34720
+ textAlign = "auto",
34721
+ maxLengthGuard,
34722
+ charGuard
34675
34723
  } = props;
34676
34724
  const minDuration = parseDuration(props.min);
34677
34725
  const maxDuration = parseDuration(props.max);
@@ -34799,6 +34847,8 @@ const InputDuration = props => {
34799
34847
  className: "navi_input_duration",
34800
34848
  "data-callout-arrow-x": "center",
34801
34849
  width: "fit-content",
34850
+ flex: true,
34851
+ spacing: "xxs",
34802
34852
  ...groupRootProps,
34803
34853
  unit: undefined,
34804
34854
  unitHour: undefined,
@@ -34818,37 +34868,34 @@ const InputDuration = props => {
34818
34868
  } : {
34819
34869
  defaultValue: initialIsoString
34820
34870
  })
34821
- }), jsx(Box, {
34822
- flex: true,
34823
- spacing: "xxs",
34824
- width: "fit-content",
34825
- children: jsx(ControlgroupChildrenWrapper, {
34826
- ...childrenWrapperProps,
34827
- name: undefined,
34828
- children: jsx(InputDurationFields, {
34829
- showHours: showHours,
34830
- showMinutes: showMinutes,
34831
- showSeconds: showSeconds,
34832
- showMilliseconds: showMilliseconds,
34833
- minutesReadOnly: minutesReadOnly,
34834
- secondsReadOnly: secondsReadOnly,
34835
- millisecondsReadOnly: millisecondsReadOnly,
34836
- stepHasMilliseconds: stepHasMilliseconds,
34837
- controlled: hasValue,
34838
- hourValue: hourValue,
34839
- minuteValue: minuteValue,
34840
- secondValue: secondValue,
34841
- millisecondValue: millisecondValue,
34842
- minSeconds: minSeconds,
34843
- maxSeconds: maxSeconds,
34844
- stepSeconds: stepSeconds,
34845
- unitHour: unitHour,
34846
- textAlign: textAlign,
34847
- required: required,
34848
- readOnly: readOnly,
34849
- disabled: disabled,
34850
- basePseudoState: basePseudoState
34851
- })
34871
+ }), jsx(ControlgroupChildrenWrapper, {
34872
+ ...childrenWrapperProps,
34873
+ name: undefined,
34874
+ children: jsx(InputDurationFields, {
34875
+ showHours: showHours,
34876
+ showMinutes: showMinutes,
34877
+ showSeconds: showSeconds,
34878
+ showMilliseconds: showMilliseconds,
34879
+ minutesReadOnly: minutesReadOnly,
34880
+ secondsReadOnly: secondsReadOnly,
34881
+ millisecondsReadOnly: millisecondsReadOnly,
34882
+ stepHasMilliseconds: stepHasMilliseconds,
34883
+ controlled: hasValue,
34884
+ hourValue: hourValue,
34885
+ minuteValue: minuteValue,
34886
+ secondValue: secondValue,
34887
+ millisecondValue: millisecondValue,
34888
+ minSeconds: minSeconds,
34889
+ maxSeconds: maxSeconds,
34890
+ stepSeconds: stepSeconds,
34891
+ unitHour: unitHour,
34892
+ textAlign: textAlign,
34893
+ maxLengthGuard: maxLengthGuard,
34894
+ charGuard: charGuard,
34895
+ required: required,
34896
+ readOnly: readOnly,
34897
+ disabled: disabled,
34898
+ basePseudoState: basePseudoState
34852
34899
  })
34853
34900
  })]
34854
34901
  });
@@ -34908,7 +34955,20 @@ const useClipboardProps = groupRef => {
34908
34955
  });
34909
34956
  e.preventDefault();
34910
34957
  };
34958
+ const isFromDurationField = e => {
34959
+ const target = e.target;
34960
+ if (!target.matches) {
34961
+ return false;
34962
+ }
34963
+ if (!target.matches(".navi_duration_part")) {
34964
+ return false;
34965
+ }
34966
+ return true;
34967
+ };
34911
34968
  const onCopy = e => {
34969
+ if (!isFromDurationField(e)) {
34970
+ return;
34971
+ }
34912
34972
  const payload = getClipboardPayload();
34913
34973
  if (!payload) {
34914
34974
  return;
@@ -34918,6 +34978,9 @@ const useClipboardProps = groupRef => {
34918
34978
  e.preventDefault();
34919
34979
  };
34920
34980
  const onCut = e => {
34981
+ if (!isFromDurationField(e)) {
34982
+ return;
34983
+ }
34921
34984
  const payload = getClipboardPayload();
34922
34985
  if (!payload) {
34923
34986
  return;
@@ -34927,6 +34990,9 @@ const useClipboardProps = groupRef => {
34927
34990
  applyToGroup("", e);
34928
34991
  };
34929
34992
  const onPaste = e => {
34993
+ if (!isFromDurationField(e)) {
34994
+ return;
34995
+ }
34930
34996
  const naviData = e.clipboardData.getData("application/x-navi");
34931
34997
  const textData = e.clipboardData.getData("text/plain");
34932
34998
  let isoValue = null;
@@ -35119,10 +35185,12 @@ const InputDurationPart = ({
35119
35185
  return jsxs(Label, {
35120
35186
  flex: "y",
35121
35187
  "data-separator": separator || undefined,
35122
- children: [jsx(Input
35123
- // When autofocused this field should be selected
35124
- // this help to modify the value on mobile
35125
- , {
35188
+ children: [jsx(Input, {
35189
+ className: "navi_duration_part"
35190
+ // When autofocused this field should be selected
35191
+ // this help to modify the value on mobile
35192
+ ,
35193
+
35126
35194
  autoFocusSelect: true,
35127
35195
  type: "navi_number",
35128
35196
  "navi-input-type": unit,
@@ -35396,6 +35464,7 @@ const Dialog = props => {
35396
35464
  const topOffset = vv.offsetTop;
35397
35465
  const marginTop = availableHeight > dialogHeight ? topOffset + (availableHeight - dialogHeight) / 2 : topOffset;
35398
35466
  dialogEl.style.setProperty("--dialog-top-inset", `${snapToPixel(marginTop)}px`);
35467
+ dispatchCustomEvent(dialogEl, "navi_position_change");
35399
35468
  };
35400
35469
  const onScroll = () => {
35401
35470
  updatePosition();
@@ -37188,7 +37257,7 @@ const createItemTracker = (onChange) => {
37188
37257
  const visibleItemsSignal = signal([]);
37189
37258
  const countSignal = signal(0);
37190
37259
  const visibleCountSignal = signal(0);
37191
- const matchCountSignal = signal(0);
37260
+ const noMatchCountSignal = signal(0);
37192
37261
 
37193
37262
  let notifyScheduled = false;
37194
37263
  const runNotify = () => {
@@ -37210,7 +37279,7 @@ const createItemTracker = (onChange) => {
37210
37279
  let visibleItemsChanged = false;
37211
37280
  const allItems = [];
37212
37281
  const visibleItems = [];
37213
- let newMatchCount = 0;
37282
+ let newNoMatchCount = 0;
37214
37283
  for (let i = 0; i < allOrderedKeys.length; i++) {
37215
37284
  const key = allOrderedKeys[i];
37216
37285
  const item = allRegistrations.get(key);
@@ -37219,12 +37288,12 @@ const createItemTracker = (onChange) => {
37219
37288
  if (!allItemsChanged && item !== prevAllItems[i]) {
37220
37289
  allItemsChanged = true;
37221
37290
  }
37222
- if (!item.filtered && !item.hidden) {
37291
+ if (item.match === false) {
37292
+ newNoMatchCount++;
37293
+ }
37294
+ if (!item.hidden) {
37223
37295
  const visibleIdx = visibleItems.length;
37224
37296
  visibleItems.push(item);
37225
- if (item.matchScore > 0) {
37226
- newMatchCount++;
37227
- }
37228
37297
  if (!visibleItemsChanged && item !== prevVisibleItems[visibleIdx]) {
37229
37298
  visibleItemsChanged = true;
37230
37299
  }
@@ -37246,9 +37315,9 @@ const createItemTracker = (onChange) => {
37246
37315
  visibleItemsSignal.value = visibleItems;
37247
37316
  someChange = true;
37248
37317
  }
37249
- const matchCountModified = matchCountSignal.peek() !== newMatchCount;
37250
- if (matchCountModified) {
37251
- matchCountSignal.value = newMatchCount;
37318
+ const noMatchCountModified = noMatchCountSignal.peek() !== newNoMatchCount;
37319
+ if (noMatchCountModified) {
37320
+ noMatchCountSignal.value = newNoMatchCount;
37252
37321
  someChange = true;
37253
37322
  }
37254
37323
  if (someChange) {
@@ -37447,7 +37516,7 @@ const createItemTracker = (onChange) => {
37447
37516
  visibleItemsSignal,
37448
37517
  countSignal,
37449
37518
  visibleCountSignal,
37450
- matchCountSignal,
37519
+ noMatchCountSignal,
37451
37520
  _flushSync,
37452
37521
  };
37453
37522
  };
@@ -37514,8 +37583,8 @@ installImportMetaCssBuild(import.meta);const css$n = /* css */`
37514
37583
  --list-item-outline-color: var(--navi-focus-outline-color);
37515
37584
  /* Focus outline end */
37516
37585
  --list-item-border-color: var(--navi-control-border-color);
37517
- --list-item-padding-x-default: var(--navi-control-padding-x-default);
37518
- --list-item-padding-y-default: var(--navi-control-padding-y-default);
37586
+ --list-item-padding-x-default: var(--navi-list-item-padding-x-default);
37587
+ --list-item-padding-y-default: var(--navi-list-item-padding-y-default);
37519
37588
 
37520
37589
  /* Hover (mouse) */
37521
37590
  --list-item-background-color-hover: light-dark(#f5f5f5, #2a2a2a);
@@ -37564,6 +37633,14 @@ installImportMetaCssBuild(import.meta);const css$n = /* css */`
37564
37633
  }
37565
37634
  }
37566
37635
 
37636
+ .navi_list_container[navi-selectable] {
37637
+ .navi_list_fallback,
37638
+ .navi_list_no_match_fallback {
37639
+ --list-item-padding-x-default: inherit;
37640
+ --list-item-padding-y-default: inherit;
37641
+ }
37642
+ }
37643
+
37567
37644
  .navi_list_item[navi-selectable] {
37568
37645
  --list-item-padding-x-default: inherit;
37569
37646
  --list-item-padding-y-default: inherit;
@@ -37573,6 +37650,13 @@ installImportMetaCssBuild(import.meta);const css$n = /* css */`
37573
37650
  outline-offset: var(--list-item-outline-offset);
37574
37651
  cursor: var(--x-list-item-cursor);
37575
37652
 
37653
+ .navi_checkbox {
37654
+ --margin: 0;
37655
+ }
37656
+ .navi_radio {
37657
+ --margin: 0;
37658
+ }
37659
+
37576
37660
  &[navi-selectable] {
37577
37661
  user-select: none;
37578
37662
  }
@@ -38184,6 +38268,11 @@ const applySearchHighlight = (el, highlight) => {
38184
38268
  installImportMetaCssBuild(import.meta);const ListItemTrackerContext = createContext(null);
38185
38269
  const GroupItemTrackerContext = createContext(null);
38186
38270
  const PendingScrollRefContext = createContext(null);
38271
+ // Controls how List.Item behaves when match=false (set via List searchNoMatchMode prop):
38272
+ // "remove" — remove from DOM (default)
38273
+ // "invisible_and_inert" — keep in DOM, invisible and non-interactive (preserves layout, no content visible)
38274
+ // "muted" — keep in DOM, visible but opacified and still interactive
38275
+ const SearchNoMatchModeContext = createContext("remove");
38187
38276
 
38188
38277
  // When total rendered items exceeds renderBudget, a render window [start, end)
38189
38278
  // is activated to cap the number of DOM nodes. Items outside the window return
@@ -38385,6 +38474,14 @@ const css$m = /* css */`
38385
38474
  scroll-margin-right: var(--x-list-scroll-spacing-right);
38386
38475
  scroll-margin-bottom: var(--x-list-scroll-spacing-bottom);
38387
38476
  scroll-margin-left: var(--x-list-scroll-spacing-left);
38477
+
38478
+ &[aria-hidden="true"] {
38479
+ opacity: 0;
38480
+ }
38481
+
38482
+ &[navi-muted] {
38483
+ opacity: 0.35;
38484
+ }
38388
38485
  }
38389
38486
 
38390
38487
  /* Virtual scroll fillers — must remain invisible.
@@ -38410,10 +38507,10 @@ const css$m = /* css */`
38410
38507
  Using order ensures fallbacks always appear after items regardless of DOM order.
38411
38508
  matchFallback intentionally shares the same order as fallback so it appears
38412
38509
  at the same visual position — after an input if present but before any items
38413
- still displayed (non-matching items remain in DOM with hidden prop):
38510
+ still displayed (non-matching items remain in DOM, invisible_and_inert or muted):
38414
38511
  1. Input (sticky header, order: -2)
38415
- 2. matchFallback (order: -1)
38416
- 3. hidden items (regular order, after DOM flow)
38512
+ 2. searchFallback (order: -1)
38513
+ 3. invisible/dim items (regular order, after DOM flow)
38417
38514
  4. HOT FIX OF THE DEAD for bottom filler + preact issue: order: 1
38418
38515
  5. sticky footer (order: 2)
38419
38516
  */
@@ -38427,12 +38524,39 @@ const css$m = /* css */`
38427
38524
  order: -2;
38428
38525
  }
38429
38526
  .navi_list_fallback,
38430
- .navi_list_no_match_fallback {
38527
+ .navi_list_search_fallback {
38431
38528
  order: -1;
38432
38529
  color: light-dark(#888, #aaa);
38433
38530
  &[navi-default] {
38434
38531
  display: inline;
38435
- padding: var(--list-item-padding);
38532
+ padding-top: var(
38533
+ --list-item-padding-top,
38534
+ var(
38535
+ --list-item-padding-y,
38536
+ var(--list-item-padding, var(--list-item-padding-y-default))
38537
+ )
38538
+ );
38539
+ padding-right: var(
38540
+ --list-item-padding-right,
38541
+ var(
38542
+ --list-item-padding-x,
38543
+ var(--list-item-padding, var(--list-item-padding-x-default))
38544
+ )
38545
+ );
38546
+ padding-bottom: var(
38547
+ --list-item-padding-bottom,
38548
+ var(
38549
+ --list-item-padding-y,
38550
+ var(--list-item-padding, var(--list-item-padding-y-default))
38551
+ )
38552
+ );
38553
+ padding-left: var(
38554
+ --list-item-padding-left,
38555
+ var(
38556
+ --list-item-padding-x,
38557
+ var(--list-item-padding, var(--list-item-padding-x-default))
38558
+ )
38559
+ );
38436
38560
  text-align: center;
38437
38561
  user-select: none;
38438
38562
  }
@@ -38506,37 +38630,6 @@ const css$m = /* css */`
38506
38630
  }
38507
38631
  }
38508
38632
  `;
38509
-
38510
- /**
38511
- * List — generic virtualized scroll container.
38512
- *
38513
- * Renders children inside a scrollable container with an optional render budget
38514
- * for virtual scrolling. Items must use <ListItem> to participate in tracking.
38515
- *
38516
- * Props:
38517
- * keyboardInteractions — when true, attaches arrow/enter/escape keyboard shortcuts
38518
- * that dispatch navi_list_nav / navi_list_confirm / navi_list_clear
38519
- * to the list container. Pair with uiAction for a full keyboard-
38520
- * navigable list.
38521
- * uiAction — called with the selected value on confirm. When provided
38522
- * the list becomes interactive: tracks hover and keyboard-
38523
- * pointed state, handles navi_list_nav / navi_list_clear /
38524
- * navi_list_confirm custom events via ListInteractionContext.
38525
- * popover — when true, renders as a managed popover positioned near
38526
- * an anchor element via navi_list_open / navi_list_close events.
38527
- * renderBudget — max items in DOM at once (default 100, virtual scroll when exceeded)
38528
- * virtualItemSize — fixed px size per item (width if horizontal, height otherwise) when all items have the same size.
38529
- * Enables precise virtual-scroll filler sizing without a DOM
38530
- * measurement pass. Required when renderBudget is active and
38531
- * item height is known up-front.
38532
- * fallback — content shown when no items exist at all
38533
- * matchFallback — content shown when items exist but all are hidden (e.g. no search match)
38534
- * separator — element or function(index, { previousItem, currentItem }) inserted between visible items
38535
- * lockSize — when true, captures the container's dimensions on first render
38536
- * (always in unfiltered state). Those values become min-width/
38537
- * min-height so filtering cannot collapse the layout.
38538
- * ...rest — forwarded to the outer scroll container <Box>
38539
- */
38540
38633
  const ListUI = props => {
38541
38634
  import.meta.css = [css$m, "@jsenv/navi/src/control/list/list.jsx"];
38542
38635
  const {
@@ -38545,7 +38638,7 @@ const ListUI = props => {
38545
38638
  renderBudgetSkipCheck,
38546
38639
  role,
38547
38640
  fallback,
38548
- noMatchFallback,
38641
+ searchFallback,
38549
38642
  separator,
38550
38643
  children,
38551
38644
  popover,
@@ -38556,6 +38649,7 @@ const ListUI = props => {
38556
38649
  virtualItemSize,
38557
38650
  lockSize,
38558
38651
  searchText,
38652
+ searchNoMatchMode = "remove",
38559
38653
  horizontal,
38560
38654
  spacing,
38561
38655
  ...rest
@@ -38645,8 +38739,8 @@ const ListUI = props => {
38645
38739
  children: jsx(ListContent, {
38646
38740
  role: role,
38647
38741
  fallback: fallback,
38648
- noMatchFallback: noMatchFallback,
38649
- searchText: searchText,
38742
+ searchFallback: searchFallback,
38743
+ searchNoMatchMode: searchNoMatchMode,
38650
38744
  separator: separator,
38651
38745
  expandX: expandX,
38652
38746
  expand: expand,
@@ -38670,12 +38764,48 @@ const ListFirstResolver = props => {
38670
38764
  ...props
38671
38765
  });
38672
38766
  };
38767
+ /**
38768
+ * List — generic virtualized scroll container.
38769
+ *
38770
+ * Renders children inside a scrollable container with an optional render budget
38771
+ * for virtual scrolling. Items must use <List.Item> to participate in tracking.
38772
+ *
38773
+ * Props:
38774
+ * selectable — enables selection: items get aria roles, action/uiAction callbacks,
38775
+ * and keyboard navigation (arrow keys, enter, escape).
38776
+ * action — called with the selected value when the user confirms a selection.
38777
+ * uiAction — called on every interaction (hover, keyboard navigation, confirm).
38778
+ * When provided the list tracks hovered/pointed state via ListInteractionContext.
38779
+ * popover — when true, renders as a managed popover positioned near
38780
+ * an anchor element via navi_list_open / navi_list_close events.
38781
+ * renderBudget — max items in DOM at once (default 100, virtual scroll when exceeded).
38782
+ * virtualItemSize — fixed px size per item (width if horizontal, height otherwise).
38783
+ * Enables precise virtual-scroll filler sizing without a DOM
38784
+ * measurement pass. Useful when renderBudget is active and
38785
+ * all items have the same known height.
38786
+ * fallback — content shown when the list has no items at all.
38787
+ * searchFallback — content shown when every matchable item (those with a match prop)
38788
+ * has match=false. Defaults to a "no results" message.
38789
+ * Pass false/null/'' to disable.
38790
+ * searchText — current search string, used to trigger scroll-to-top when
38791
+ * search becomes active and to drive search highlight.
38792
+ * searchNoMatchMode — controls how List.Item behaves when match=false (default "remove"):
38793
+ * "remove" — remove from DOM
38794
+ * "invisible_and_inert" — keep in DOM, invisible and non-interactive (preserves layout)
38795
+ * "muted" — keep in DOM, visible but opacified and still interactive
38796
+ * separator — element or function(index, { previousItem, currentItem }) inserted between visible items.
38797
+ * lockSize — captures the container's dimensions on first render so filtering
38798
+ * cannot collapse the layout (sets min-width/min-height).
38799
+ * horizontal — lays items out in a row instead of a column.
38800
+ * spacing — gap between items (forwarded to Box spacing prop).
38801
+ * ...rest — forwarded to the outer container <Box>.
38802
+ */
38673
38803
  const List = createComponentResolver([ListFirstResolver, ListSelectableResolver, ListUI]);
38674
38804
  const ListContent = ({
38675
38805
  role,
38676
38806
  fallback,
38677
- noMatchFallback,
38678
- searchText,
38807
+ searchFallback,
38808
+ searchNoMatchMode,
38679
38809
  separator,
38680
38810
  expandX,
38681
38811
  expand,
@@ -38693,8 +38823,8 @@ const ListContent = ({
38693
38823
  children: jsx(UnorderedList, {
38694
38824
  role: role,
38695
38825
  fallback: fallback,
38696
- noMatchFallback: noMatchFallback,
38697
- searchText: searchText,
38826
+ searchFallback: searchFallback,
38827
+ searchNoMatchMode: searchNoMatchMode,
38698
38828
  separator: separator === true ? jsx(Separator, {
38699
38829
  margin: "0"
38700
38830
  }) : separator,
@@ -39121,8 +39251,8 @@ const UnorderedList = ({
39121
39251
  renderWindow,
39122
39252
  virtualItemSizeSignal,
39123
39253
  fallback,
39124
- noMatchFallback,
39125
- searchText,
39254
+ searchFallback,
39255
+ searchNoMatchMode,
39126
39256
  separator,
39127
39257
  horizontal,
39128
39258
  spacing,
@@ -39139,20 +39269,22 @@ const UnorderedList = ({
39139
39269
  children: [jsx(BeforeFiller, {
39140
39270
  virtualItemSizeSignal: virtualItemSizeSignal,
39141
39271
  renderWindowStart: renderWindow.start
39142
- }), jsx(NoMatchFallback, {
39143
- noMatchFallback: noMatchFallback,
39144
- tracker: tracker,
39145
- searchText: searchText
39272
+ }), jsx(SearchFallback, {
39273
+ searchFallback: searchFallback,
39274
+ tracker: tracker
39146
39275
  }), jsx(Fallback, {
39147
39276
  fallback: fallback,
39148
39277
  tracker: tracker
39149
- }), jsx(RenderWindowContext.Provider, {
39150
- value: renderWindow,
39151
- children: jsx(SeparatorContext.Provider, {
39152
- value: separator ?? null,
39153
- children: jsx(ListItemTrackerContext.Provider, {
39154
- value: tracker,
39155
- children: children
39278
+ }), jsx(SearchNoMatchModeContext.Provider, {
39279
+ value: searchNoMatchMode,
39280
+ children: jsx(RenderWindowContext.Provider, {
39281
+ value: renderWindow,
39282
+ children: jsx(SeparatorContext.Provider, {
39283
+ value: separator ?? null,
39284
+ children: jsx(ListItemTrackerContext.Provider, {
39285
+ value: tracker,
39286
+ children: children
39287
+ })
39156
39288
  })
39157
39289
  })
39158
39290
  }), jsx(AfterFiller, {
@@ -39162,31 +39294,33 @@ const UnorderedList = ({
39162
39294
  })]
39163
39295
  });
39164
39296
  };
39165
- const NoMatchFallback = ({
39297
+
39298
+ // Show when all matchable items (those with a match prop) are non-matching.
39299
+ // The match prop on List.Item signals participation in a matching system
39300
+ // (search, filter, etc.). searchFallback appears when every such item has match=false.
39301
+ const SearchFallback = ({
39166
39302
  tracker,
39167
- noMatchFallback,
39168
- searchText
39303
+ searchFallback
39169
39304
  }) => {
39170
39305
  const itemCount = tracker.countSignal.value;
39171
- const visibleItemCount = tracker.visibleCountSignal.value;
39172
- const matchCount = tracker.matchCountSignal.value;
39173
- // Show when all items are filtered out (filtered prop), or when search is
39174
- // active but no visible item has a positive match score.
39175
- const allHidden = itemCount > 0 && visibleItemCount === 0;
39176
- const noneMatch = searchText && visibleItemCount > 0 && matchCount === 0;
39177
- const showMatchFallback = allHidden || noneMatch;
39178
- if (noMatchFallback === undefined) {
39179
- noMatchFallback = allHidden ? naviI18n("list.no_match") : naviI18n("list.no_match_rest_shown");
39306
+ const noMatchCount = tracker.noMatchCountSignal.value;
39307
+ const showMatchFallback = noMatchCount > 0 && noMatchCount === itemCount;
39308
+ if (searchFallback === undefined) {
39309
+ searchFallback = naviI18n("list.no_match");
39310
+ }
39311
+ if (!searchFallback) {
39312
+ // explicitely disabled by user (<List searchFallback={false|null|''}>)
39313
+ return null;
39180
39314
  }
39181
39315
  if (!showMatchFallback) {
39182
39316
  return null;
39183
39317
  }
39184
39318
  return jsx(ListItem, {
39185
39319
  role: "presentation",
39186
- className: "navi_list_item navi_list_no_match_fallback",
39320
+ className: "navi_list_item navi_list_search_fallback",
39187
39321
  hidden: !showMatchFallback,
39188
- "navi-default": typeof noMatchFallback === "string" ? "" : undefined,
39189
- children: noMatchFallback
39322
+ "navi-default": typeof searchFallback === "string" ? "" : undefined,
39323
+ children: searchFallback
39190
39324
  });
39191
39325
  };
39192
39326
  const Fallback = ({
@@ -39291,6 +39425,18 @@ const ListItemUI = props => {
39291
39425
  props.id = props.id || idDefault;
39292
39426
  const renderWindow = useContext(RenderWindowContext);
39293
39427
  const tracker = useContext(ListItemTrackerContext);
39428
+ const searchNoMatchMode = useContext(SearchNoMatchModeContext);
39429
+ // Derive filtered/hidden/nonMatching from the `match` prop + searchNoMatchMode context.
39430
+ // The `match` prop replaces the older `filtered`/`hidden` per-item props.
39431
+ if (props.match === false) {
39432
+ if (searchNoMatchMode === "remove") {
39433
+ props.filtered = true;
39434
+ } else if (searchNoMatchMode === "invisible_and_inert") {
39435
+ props.hidden = true;
39436
+ } else if (searchNoMatchMode === "muted") {
39437
+ props.muted = true;
39438
+ }
39439
+ }
39294
39440
  const item = props;
39295
39441
  const visibleIndex = tracker.useTrackItem(item);
39296
39442
  const groupTracker = useContext(GroupItemTrackerContext);
@@ -39350,6 +39496,7 @@ const ListItemReal = props => {
39350
39496
  ref,
39351
39497
  id,
39352
39498
  hidden,
39499
+ muted,
39353
39500
  highlight,
39354
39501
  children,
39355
39502
  ...rest
@@ -39382,7 +39529,15 @@ const ListItemReal = props => {
39382
39529
  index: undefined,
39383
39530
  selected: undefined,
39384
39531
  matchScore: undefined,
39385
- hidden: hidden,
39532
+ match: undefined
39533
+ // We use aria-hidden and not hidden because hidden would be forced to
39534
+ // display: none while here we want to keep it in the DOM to avoid layout shift
39535
+ // but visually hidden
39536
+ ,
39537
+
39538
+ "aria-hidden": hidden,
39539
+ inert: hidden ? true : undefined,
39540
+ "navi-muted": muted ? "" : undefined,
39386
39541
  ref: ref,
39387
39542
  children: children
39388
39543
  });
@@ -40454,6 +40609,7 @@ installImportMetaCssBuild(import.meta);const css$i = /* css */`
40454
40609
  outline: none;
40455
40610
  cursor: inherit;
40456
40611
  pointer-events: auto;
40612
+ user-select: all;
40457
40613
 
40458
40614
  &::-webkit-calendar-picker-indicator {
40459
40615
  cursor: inherit;