@jsenv/navi 0.27.30 → 0.27.31

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.
@@ -6676,6 +6676,24 @@ const TYPO_PROPS = {
6676
6676
  uppercase: applyToCssPropWhenTruthy("textTransform", "uppercase", "none"),
6677
6677
  lowercase: applyToCssPropWhenTruthy("textTransform", "lowercase", "none"),
6678
6678
  letterSpacing: PASS_THROUGH,
6679
+ maxLines: (value) => {
6680
+ if (!value) {
6681
+ return null;
6682
+ }
6683
+ if (value === 1 || value === "1") {
6684
+ return {
6685
+ overflow: "hidden",
6686
+ textOverflow: "ellipsis",
6687
+ overflowWrap: "normal",
6688
+ };
6689
+ }
6690
+ return {
6691
+ "overflow": "hidden",
6692
+ "display": "-webkit-box",
6693
+ "-webkit-box-orient": "vertical",
6694
+ "-webkit-line-clamp": value,
6695
+ };
6696
+ },
6679
6697
  overflowEllipsis: (value) => {
6680
6698
  if (!value) {
6681
6699
  return null;
@@ -7606,9 +7624,19 @@ const definePseudoClass = (pseudoClass, definition) => {
7606
7624
  PSEUDO_CLASSES[pseudoClass] = definition;
7607
7625
  };
7608
7626
 
7627
+ // On touch devices (hover: none), browsers synthesize mouseenter/mouseleave
7628
+ // from touch events but never fire mouseleave when the finger lifts, leaving
7629
+ // el.matches(":hover") stuck at true. This causes hover styles (e.g. input
7630
+ // background highlight) to remain visible long after the user has stopped
7631
+ // touching the element. Checking (hover: hover) lets us skip hover tracking
7632
+ // entirely on touch-only devices where persistent hover makes no sense.
7633
+ const hoverSupported = window.matchMedia("(hover: hover)").matches;
7609
7634
  definePseudoClass(":hover", {
7610
7635
  attribute: "data-hover",
7611
7636
  setup: (el, callback) => {
7637
+ if (!hoverSupported) {
7638
+ return () => {};
7639
+ }
7612
7640
  const recheckProxy = (e) => {
7613
7641
  const proxy = findControlProxy(el);
7614
7642
  if (proxy) {
@@ -7672,6 +7700,9 @@ definePseudoClass(":hover", {
7672
7700
  };
7673
7701
  },
7674
7702
  test: (el) => {
7703
+ if (!hoverSupported) {
7704
+ return false;
7705
+ }
7675
7706
  if (el.matches(":hover")) {
7676
7707
  return true;
7677
7708
  }
@@ -12934,15 +12965,13 @@ const extractSearchParams = (urlObj, queryConnectionMap) => {
12934
12965
  decodedValue === "true" || decodedValue === "1" || decodedValue === "";
12935
12966
  } else if (signalType === "date") {
12936
12967
  const decodedValue = decodeURIComponent(rawValue);
12937
- // Accept both "YYYY-MM-DD" and full ISO string, always parse as UTC date
12938
- const datePart = decodedValue.slice(0, 10);
12939
- const [year, month, day] = datePart.split("-").map(Number);
12940
- const d = new Date(Date.UTC(year, month - 1, day));
12941
- params[key] = isNaN(d.getTime()) ? decodedValue : d;
12968
+ // Keep as "YYYY-MM-DD" string canonical date form, no timezone conversion
12969
+ params[key] = decodedValue.slice(0, 10);
12942
12970
  } else if (signalType === "datetime") {
12943
12971
  const decodedValue = decodeURIComponent(rawValue);
12972
+ // Normalize to ISO string — canonical datetime form
12944
12973
  const d = new Date(decodedValue);
12945
- params[key] = isNaN(d.getTime()) ? decodedValue : d;
12974
+ params[key] = isNaN(d.getTime()) ? decodedValue : d.toISOString();
12946
12975
  } else {
12947
12976
  params[key] = decodeURIComponent(rawValue);
12948
12977
  }
@@ -16709,6 +16738,7 @@ const css$O = /* css */`
16709
16738
  margin: 0;
16710
16739
  padding: 0;
16711
16740
  color: revert; /* Do no inherit element color, callout is inside the element it should use document color though */
16741
+ font-size: initial; /* Callout fells disconnected from the element, font size should be predictible and stable */
16712
16742
  background: transparent;
16713
16743
  border: none;
16714
16744
  opacity: 0;
@@ -19212,12 +19242,20 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-single-space");
19212
19242
  * formatDay(tomorrow, "fr") // "mardi 12 mai (demain)"
19213
19243
  * formatDay(nextWeek, "fr") // "lundi 18 mai"
19214
19244
  */
19215
- const formatDay = (date, locale, { long = false } = {}) => {
19216
- return new Intl.DateTimeFormat(locale, {
19245
+ const formatDay = (date, locale, { long = false, numeric = false } = {}) => {
19246
+ if (numeric) {
19247
+ return new Intl.DateTimeFormat(locale, {
19248
+ day: "2-digit",
19249
+ month: "2-digit",
19250
+ year: "numeric",
19251
+ }).format(date);
19252
+ }
19253
+ const result = new Intl.DateTimeFormat(locale, {
19217
19254
  weekday: long ? "long" : "short",
19218
19255
  day: "numeric",
19219
19256
  month: long ? "long" : "short",
19220
19257
  }).format(date);
19258
+ return result;
19221
19259
  };
19222
19260
 
19223
19261
  /**
@@ -19793,12 +19831,14 @@ const PATTERN_CONSTRAINT = {
19793
19831
  if (regex.test(value)) {
19794
19832
  return null;
19795
19833
  }
19796
- let message = naviI18n(`constraint.pattern.${fieldTypeSuffix(field)}`);
19797
- const title = field.title;
19798
- if (title) {
19799
- message += `<br />${title}`;
19834
+ const type = field.type;
19835
+ if (type === "email") {
19836
+ return naviI18n("constraint.pattern.email");
19837
+ }
19838
+ if (type === "password") {
19839
+ return naviI18n("constraint.pattern.password");
19800
19840
  }
19801
- return message;
19841
+ return naviI18n("constraint.pattern.default");
19802
19842
  },
19803
19843
  };
19804
19844
  CONSTRAINT_ATTRIBUTE_SET.add("pattern");
@@ -20144,20 +20184,20 @@ const STEP_CONSTRAINT = {
20144
20184
  const before = formatMsToTime(beforeMs, showSeconds);
20145
20185
  const after = formatMsToTime(afterMs, showSeconds);
20146
20186
  if (stepSeconds % 3600 === 0) {
20147
- return naviI18n("constraint.step.time.hours", {
20187
+ return naviI18n("constraint.step.time.hour", {
20148
20188
  step: String(stepSeconds / 3600),
20149
20189
  before,
20150
20190
  after,
20151
20191
  });
20152
20192
  }
20153
20193
  if (stepSeconds % 60 === 0) {
20154
- return naviI18n("constraint.step.time.minutes", {
20194
+ return naviI18n("constraint.step.time.minute", {
20155
20195
  step: String(stepSeconds / 60),
20156
20196
  before,
20157
20197
  after,
20158
20198
  });
20159
20199
  }
20160
- return naviI18n("constraint.step.time.seconds", {
20200
+ return naviI18n("constraint.step.time.second", {
20161
20201
  step: stepString,
20162
20202
  before,
20163
20203
  after,
@@ -20234,9 +20274,16 @@ const formatDateIso = (iso, inputType) => {
20234
20274
  const date = new Date(`${iso}-01T00:00:00`);
20235
20275
  return formatMonth(date, locale);
20236
20276
  }
20237
- // date, week, datetime-local: parse and use formatDay
20238
- const dateStr = iso.slice(0, 10);
20239
- const date = new Date(`${dateStr}T00:00:00`);
20277
+ // date, week, datetime-local: extract YYYY-MM-DD part and parse as local date
20278
+ const isoMatch = /^(\d{4}-\d{2}-\d{2})/.exec(iso);
20279
+ if (!isoMatch) {
20280
+ return iso;
20281
+ }
20282
+ const datePart = isoMatch[1];
20283
+ const date = new Date(`${datePart}T00:00:00`);
20284
+ if (isNaN(date.getTime())) {
20285
+ return iso;
20286
+ }
20240
20287
  return formatDay(date, locale, { long: true });
20241
20288
  };
20242
20289
 
@@ -20327,9 +20374,10 @@ const onRequestInteraction = (
20327
20374
  }
20328
20375
  }
20329
20376
  if (!skipConstraints) {
20330
- checkAndReportConstraints(requestStatus, INTERACTION_CONSTRAINTS, {
20377
+ checkAndReportConstraints(requestStatus, {
20331
20378
  event: requestInteractionCustomEvent,
20332
20379
  requester: event.target,
20380
+ fromRequestInteraction: true,
20333
20381
  debug: debugInteraction,
20334
20382
  });
20335
20383
  }
@@ -20417,11 +20465,11 @@ const onRequestAction = (
20417
20465
  checkEvent(requestStatus, event);
20418
20466
  }
20419
20467
  if (requestStatus.canProceed) {
20420
- checkAndReportConstraints(requestStatus, DEFAULT_CONSTRAINT_SET, {
20468
+ checkAndReportConstraints(requestStatus, {
20421
20469
  event: requestActionCustomEvent,
20422
20470
  requester,
20423
- debug: debugAction,
20424
20471
  fromRequestAction: true,
20472
+ debug: debugAction,
20425
20473
  });
20426
20474
  }
20427
20475
  if (requestStatus.canProceed) {
@@ -20496,8 +20544,7 @@ const checkEvent = (requestStatus, event) => {
20496
20544
 
20497
20545
  const checkAndReportConstraints = (
20498
20546
  requestStatus,
20499
- constraints,
20500
- { event, requester, debug, fromRequestAction } = {},
20547
+ { event, requester, fromRequestInteraction, fromRequestAction, debug } = {},
20501
20548
  ) => {
20502
20549
  const onInvalid = (failedValidationInterface) => {
20503
20550
  Object.assign(requestStatus, {
@@ -20526,9 +20573,9 @@ const checkAndReportConstraints = (
20526
20573
  }
20527
20574
  const isValid = validationInterface.checkValidity({
20528
20575
  event,
20529
- constraints,
20530
- fromRequestAction,
20531
20576
  requester,
20577
+ fromRequestInteraction,
20578
+ fromRequestAction,
20532
20579
  });
20533
20580
  if (!isValid) {
20534
20581
  // checkValidity delegates to the proxy target's VI when the element is a proxy.
@@ -20703,9 +20750,9 @@ const installCustomConstraintValidation = (
20703
20750
  addTeardown(resetValidity);
20704
20751
 
20705
20752
  const checkValidity = ({
20706
- constraints,
20707
20753
  event,
20708
20754
  requester = element,
20755
+ fromRequestInteraction,
20709
20756
  fromRequestAction,
20710
20757
  skipReadonly,
20711
20758
  } = {}) => {
@@ -20717,8 +20764,8 @@ const installCustomConstraintValidation = (
20717
20764
  targetVI = installCustomConstraintValidation(proxyTarget);
20718
20765
  }
20719
20766
  return targetVI.checkValidity({
20720
- constraints,
20721
20767
  event,
20768
+ fromRequestInteraction,
20722
20769
  fromRequestAction,
20723
20770
  requester,
20724
20771
  skipReadonly,
@@ -20735,9 +20782,9 @@ const installCustomConstraintValidation = (
20735
20782
  continue;
20736
20783
  }
20737
20784
  const managedIsValid = managedVI.checkValidity({
20738
- constraints,
20739
20785
  event,
20740
20786
  requester,
20787
+ fromRequestInteraction,
20741
20788
  fromRequestAction,
20742
20789
  skipReadonly:
20743
20790
  managedControl.tagName === "BUTTON" && managedControl !== requester,
@@ -20748,14 +20795,31 @@ const installCustomConstraintValidation = (
20748
20795
  }
20749
20796
  }
20750
20797
 
20798
+ // When checking a subset of constraints (e.g. INTERACTION_CONSTRAINTS), we must NOT
20799
+ // reset the full validity state — other constraints (like PATTERN) may still be failing
20800
+ // and we must preserve their state (failedConstraintInfo, callout, etc.).
20801
+ // We only do a scoped check and return its result without touching global state.
20802
+ if (fromRequestInteraction) {
20803
+ for (const constraint of INTERACTION_CONSTRAINTS) {
20804
+ const checkResult = constraint.check(element, {
20805
+ fromRequestAction,
20806
+ skipReadonly,
20807
+ skipRequired: element === requester,
20808
+ registerChange: () => {},
20809
+ });
20810
+ if (checkResult) {
20811
+ return false;
20812
+ }
20813
+ }
20814
+ return true;
20815
+ }
20751
20816
  let newConstraintValidityState = { valid: true };
20752
- // When constraints are explicitly provided (e.g. pointer interaction), use only those.
20753
- // Otherwise use default set merged with dynamic constraints.
20754
- const effectiveConstraints = constraints
20755
- ? constraints
20756
- : new Set([...DEFAULT_CONSTRAINT_SET, ...dynamicConstraintSet]);
20817
+ const constraintSet = new Set([
20818
+ ...DEFAULT_CONSTRAINT_SET,
20819
+ ...dynamicConstraintSet,
20820
+ ]);
20757
20821
  resetValidity({ fromRequestAction });
20758
- for (const constraint of effectiveConstraints) {
20822
+ for (const constraint of constraintSet) {
20759
20823
  const fieldForConstraint = element;
20760
20824
  const constraintCleanupSet = new Set();
20761
20825
  const registerChange = (register) => {
@@ -20829,9 +20893,11 @@ const installCustomConstraintValidation = (
20829
20893
  if (!hasTitleAttribute) {
20830
20894
  element.removeAttribute("title");
20831
20895
  }
20896
+ const checkValidityCallEvent =
20897
+ event || new CustomEvent("checkValidity called with no event");
20832
20898
  closeElementValidationMessage(
20833
- event || new CustomEvent("checkValidity called with no event"),
20834
- "becomes_valid",
20899
+ checkValidityCallEvent,
20900
+ `now_valid (after ${checkValidityCallEvent.type})`,
20835
20901
  );
20836
20902
  }
20837
20903
 
@@ -20847,7 +20913,7 @@ const installCustomConstraintValidation = (
20847
20913
  const [notifyCalloutOpen, onCalloutOpen] = createPubSub();
20848
20914
  const reportValidity = ({ event, requester, debug, skipFocus } = {}) => {
20849
20915
  if (!failedConstraintInfo) {
20850
- closeElementValidationMessage(event, "becomes_valid");
20916
+ closeElementValidationMessage(event, "is_valid");
20851
20917
  return;
20852
20918
  }
20853
20919
  if (failedConstraintInfo.silent) {
@@ -20995,6 +21061,7 @@ const installCustomConstraintValidation = (
20995
21061
  const resetOnInteraction = (e) => {
20996
21062
  customMessageMap.clear();
20997
21063
  closeElementValidationMessage(e, e.type);
21064
+ console.log("resetOnInteraction", e.type, e);
20998
21065
  checkValidity({ event: e });
20999
21066
  };
21000
21067
 
@@ -21003,35 +21070,57 @@ const installCustomConstraintValidation = (
21003
21070
  break close_and_check_on_input; // range inputs have a special behavior where "input" is triggered on pointer release, so we don't need to wait for it
21004
21071
  }
21005
21072
 
21006
- let waitPointerRelease;
21007
21073
  onCalloutOpen((openingEvent) => {
21008
- if (openingEvent && findEvent(openingEvent, "mousedown")) {
21009
- waitPointerRelease = true;
21074
+ const openedByMousedown = findEvent(openingEvent, "mousedown");
21075
+ const [cleanup, addCleanup] = createPubSub();
21076
+
21077
+ const setupResetOnInput = () => {
21078
+ const oninput = (e) => {
21079
+ resetOnInteraction(e);
21080
+ };
21081
+ element.addEventListener("input", oninput, { once: true });
21082
+ addCleanup(() => {
21083
+ element.removeEventListener("input", oninput, { once: true });
21084
+ });
21085
+ };
21086
+
21087
+ if (openedByMousedown) {
21010
21088
  const onMouseUp = () => {
21011
- setTimeout(() => {
21012
- waitPointerRelease = false;
21013
- });
21089
+ const timeout = setTimeout(setupResetOnInput);
21090
+ addCleanup(() => clearTimeout(timeout));
21014
21091
  };
21015
21092
  document.addEventListener("mouseup", onMouseUp, {
21016
21093
  once: true,
21017
21094
  capture: true,
21018
21095
  });
21019
- return () => {
21020
- document.removeEventListener("mouseup", onMouseUp, true);
21021
- };
21096
+ addCleanup(() => {
21097
+ document.removeEventListener("mouseup", onMouseUp, {
21098
+ once: true,
21099
+ capture: true,
21100
+ });
21101
+ });
21102
+ return cleanup;
21022
21103
  }
21023
- return undefined;
21024
- });
21025
21104
 
21026
- const oninput = (e) => {
21027
- if (waitPointerRelease) {
21028
- return;
21029
- }
21030
- resetOnInteraction(e);
21031
- };
21032
- element.addEventListener("input", oninput);
21033
- addTeardown(() => {
21034
- element.removeEventListener("input", oninput);
21105
+ // "change" can happen after an input looses focus
21106
+ // and if loose focus as the result of typing (navi_input_full going to next input)
21107
+ // the browser will fire an input event shortly after
21108
+ // causing the callout to immediatly close
21109
+ // an other way to express this could be that an "input" event should be allowed
21110
+ // to close callout only if at least event loop or 1ms occurs
21111
+ let closed = false;
21112
+ addCleanup(() => {
21113
+ closed = true;
21114
+ });
21115
+ queueMicrotask(() => {
21116
+ if (closed) {
21117
+ console.log("closed before");
21118
+ return;
21119
+ }
21120
+ console.log("listen input");
21121
+ setupResetOnInput();
21122
+ });
21123
+ return cleanup;
21035
21124
  });
21036
21125
  }
21037
21126
 
@@ -23160,11 +23249,13 @@ const OverflowPinnedContext = createContext(null);
23160
23249
  *
23161
23250
  * @param {object} props
23162
23251
  *
23163
- * @param {boolean} [props.overflowEllipsis]
23164
- * Truncates overflowing text with an ellipsis.
23252
+ * @param {number} [props.maxLines]
23253
+ * Truncates overflowing text with an ellipsis after at most N lines.
23254
+ * `maxLines={1}` truncates after one line (single-line ellipsis).
23255
+ * `maxLines={n}` (n > 1) truncates after n lines (multi-line clamp).
23165
23256
  *
23166
23257
  * @param {boolean} [props.overflowPinned]
23167
- * Must be used inside a `<Text overflowEllipsis>` parent.
23258
+ * Must be used inside a `<Text maxLines>` parent.
23168
23259
  * Pins this element outside the truncated text flow (e.g. a badge or icon).
23169
23260
  *
23170
23261
  * @param {string} [props.spacing]
@@ -23226,7 +23317,7 @@ const TextDispatcher = props => {
23226
23317
  ...props
23227
23318
  });
23228
23319
  }
23229
- if (props.overflowEllipsis) {
23320
+ if (props.maxLines === 1 || props.maxLines === "1") {
23230
23321
  return jsx(TextOverflow, {
23231
23322
  ...props
23232
23323
  });
@@ -23405,6 +23496,7 @@ const TextSkeleton = ({
23405
23496
  const TextOverflow = ({
23406
23497
  noWrap,
23407
23498
  spacing,
23499
+ capitalize,
23408
23500
  children,
23409
23501
  ...rest
23410
23502
  }) => {
@@ -23420,7 +23512,7 @@ const TextOverflow = ({
23420
23512
  preLine: rest.as === "p" ? true : undefined,
23421
23513
  noWrap: noWrap,
23422
23514
  ...rest,
23423
- overflowEllipsis: undefined,
23515
+ maxLines: undefined,
23424
23516
  "data-text-overflow": "",
23425
23517
  spacing: "pre",
23426
23518
  children: jsxs("span", {
@@ -23430,6 +23522,7 @@ const TextOverflow = ({
23430
23522
  children: jsx(Text, {
23431
23523
  className: "navi_text_overflow_text",
23432
23524
  spacing: spacing,
23525
+ capitalize: capitalize,
23433
23526
  children: children
23434
23527
  })
23435
23528
  }), overflowPinned && overflowPinned.position === "end" ? overflowPinned.vnode : null]
@@ -23447,7 +23540,7 @@ const TextOverflowPinned = props => {
23447
23540
  overflowPinned: undefined
23448
23541
  });
23449
23542
  if (!setOverflowPinned) {
23450
- console.warn("<Text overflowPinned> declared outside a <Text overflowEllipsis>");
23543
+ console.warn(`<Text overflowPinned> declared outside a <Text maxLines="1">`);
23451
23544
  return text;
23452
23545
  }
23453
23546
  if (overflowPinned) {
@@ -24259,11 +24352,11 @@ const CONTROL_ATTRIBUTE_SET = new Set([
24259
24352
  "autoComplete",
24260
24353
  "spellcheck",
24261
24354
  "autoCorrect",
24355
+ "aria-controls",
24356
+ "tabIndex",
24262
24357
 
24263
24358
  "navi-input-type",
24264
24359
  "navi-control-proxy-for",
24265
- "aria-controls",
24266
- "tabIndex",
24267
24360
 
24268
24361
  "data-callout-arrow-x",
24269
24362
  "data-callout-point-to-border-box",
@@ -24272,7 +24365,7 @@ const CONTROL_ATTRIBUTE_SET = new Set([
24272
24365
  "data-callout-position",
24273
24366
  "data-callout-position-fixed",
24274
24367
 
24275
- "data-testid",
24368
+ "data-testid", // playwright, cypress
24276
24369
  ]);
24277
24370
  // prop concerning control but that won't end up in the DOM if not inside CONTROL_ATTRIBUTE_SET
24278
24371
  const CONTROL_PROP_SET = new Set([
@@ -25736,7 +25829,13 @@ const ControlChildrenWrapper = ({
25736
25829
  value: undefined,
25737
25830
  children: jsx(ControlToInterfaceContext.Provider, {
25738
25831
  value: undefined,
25739
- children: children
25832
+ children: jsx(RequiredContext.Provider, {
25833
+ value: undefined,
25834
+ children: jsx(ControlNameContext.Provider, {
25835
+ value: undefined,
25836
+ children: children
25837
+ })
25838
+ })
25740
25839
  })
25741
25840
  })
25742
25841
  });
@@ -27551,7 +27650,7 @@ const LinkPlain = props => {
27551
27650
  endIcon,
27552
27651
  revealOnInteraction = Boolean(titleLevel),
27553
27652
  hrefFallback = !anchor,
27554
- overflowEllipsis,
27653
+ maxLines,
27555
27654
  children,
27556
27655
  constraints,
27557
27656
  ...remainingProps
@@ -27641,7 +27740,7 @@ const LinkPlain = props => {
27641
27740
  fontWeight: "bold"
27642
27741
  } : undefined,
27643
27742
  preventSpaceUnderlines: true,
27644
- overflowEllipsis: overflowEllipsis
27743
+ maxLines: maxLines
27645
27744
  // Visual
27646
27745
  ,
27647
27746
 
@@ -27694,7 +27793,7 @@ const LinkPlain = props => {
27694
27793
  color: "var(--link-loader-color)"
27695
27794
  }), currentIndicatorEl]
27696
27795
  }),
27697
- children: [startIconEl, innerChildren, endIconEl ? overflowEllipsis ? jsx(Text, {
27796
+ children: [startIconEl, innerChildren, endIconEl ? maxLines === 1 || maxLines === "1" ? jsx(Text, {
27698
27797
  overflowPinned: true,
27699
27798
  children: endIconEl
27700
27799
  }) : endIconEl : null]
@@ -28907,6 +29006,316 @@ const CheckboxPseudoClasses = [":hover", ":active", ":focus", ":focus-visible",
28907
29006
  const CheckboxPseudoElements = ["::-navi-loader", "::-navi-checkmark"];
28908
29007
 
28909
29008
  installImportMetaCssBuild(import.meta);const css$z = /* css */`
29009
+ @layer navi {
29010
+ [data-navi-field] {
29011
+ .navi_checkbox {
29012
+ --margin: 0;
29013
+ }
29014
+ .navi_radio {
29015
+ --margin: 0;
29016
+ }
29017
+ }
29018
+
29019
+ label[data-navi-field] {
29020
+ &[data-interactive] {
29021
+ cursor: pointer;
29022
+ user-select: none;
29023
+ }
29024
+ &[data-readonly],
29025
+ &[data-disabled] {
29026
+ color: rgba(0, 0, 0, 0.5);
29027
+ cursor: default;
29028
+ }
29029
+ }
29030
+
29031
+ [data-navi-field-container] {
29032
+ --field-spacing: var(--navi-xs);
29033
+
29034
+ > * + .navi_label {
29035
+ padding-left: var(--field-spacing);
29036
+ }
29037
+ > .navi_label:first-child {
29038
+ padding-right: var(--field-spacing);
29039
+ }
29040
+ &[data-vertical] > .navi_label:first-child {
29041
+ padding-bottom: var(--field-spacing);
29042
+ }
29043
+
29044
+ &[data-interactive] {
29045
+ .navi_label {
29046
+ cursor: pointer;
29047
+ /* When label is interactive ability to select text oftens conflicts with other click interactions */
29048
+ user-select: none;
29049
+ }
29050
+ }
29051
+ &[data-readonly],
29052
+ &[data-disabled] {
29053
+ .navi_label {
29054
+ color: rgba(0, 0, 0, 0.5);
29055
+ cursor: default;
29056
+ }
29057
+ }
29058
+ }
29059
+ }
29060
+ `;
29061
+
29062
+ /**
29063
+ * Field — a semantic wrapper that connects a label to a form control.
29064
+ *
29065
+ * It generates a stable `fieldId` (or accepts an explicit `id`) that is
29066
+ * automatically forwarded to the `Label` inside the field as `htmlFor` and to
29067
+ * any interactive control (Picker, Input, …) as its `id`, so clicking the
29068
+ * label focuses the control without requiring manual wiring.
29069
+ *
29070
+ * It also tracks the readOnly / disabled / interactive state reported by its
29071
+ * child control and reflects it on the `Label` (dimmed color, cursor change).
29072
+ *
29073
+ * Props:
29074
+ * id — optional explicit id used as the field id instead of the auto-generated one
29075
+ * vertical — shorthand for flex="y" + alignX="start"
29076
+ * children — any JSX; should contain a `Label` and a form control
29077
+ * ...rest — forwarded to the wrapping `<div>` (className, style, flex, spacing, …)
29078
+ *
29079
+ * @example
29080
+ * <Field flex spacing="s">
29081
+ * Date de début
29082
+ * <Input name="start_date" required />
29083
+ * </Field>
29084
+ */
29085
+ const Field = props => {
29086
+ import.meta.css = [css$z, "@jsenv/navi/src/control/field.jsx"];
29087
+ const refDefault = useRef();
29088
+ props.ref = props.ref || refDefault;
29089
+ const {
29090
+ as,
29091
+ vertical
29092
+ } = props;
29093
+ if (as === undefined && !vertical) {
29094
+ props.as = "label";
29095
+ }
29096
+ if (props.as === "label") {
29097
+ return jsx(FieldAsLabel, {
29098
+ ...props
29099
+ });
29100
+ }
29101
+ return jsx(FieldAsContainer, {
29102
+ ...props
29103
+ });
29104
+ };
29105
+ const FieldAsLabel = props => {
29106
+ return jsx(FieldUI, {
29107
+ ...props
29108
+ });
29109
+ };
29110
+ const FieldAsContainer = props => {
29111
+ const idDefault = useId();
29112
+ const fieldId = `field_${idDefault}`;
29113
+ props.fieldId = props.fieldId || props.id ? `${props.id}_field` : fieldId;
29114
+ return jsx(FieldUI, {
29115
+ ...props,
29116
+ "data-navi-field-container": "",
29117
+ styleCSSVars: FieldCSSVars
29118
+ });
29119
+ };
29120
+ const FieldCSSVars = {
29121
+ spacing: "--field-spacing"
29122
+ };
29123
+ const FieldUI = props => {
29124
+ import.meta.css = [css$z, "@jsenv/navi/src/control/field.jsx"];
29125
+ const {
29126
+ vertical
29127
+ } = props;
29128
+ const {
29129
+ fieldId,
29130
+ name,
29131
+ disabled,
29132
+ readOnly,
29133
+ required,
29134
+ loading,
29135
+ interactive,
29136
+ ...rest
29137
+ } = props;
29138
+ const [messageProps, remainingProps] = extractMessageAndRemainingProps(rest);
29139
+ const messagePropsRef = useRef();
29140
+ messagePropsRef.current = messageProps;
29141
+ const [disabledByChild, setDisabledByChild] = useState(false);
29142
+ const [readOnlyFromChild, setReadOnlyFromChild] = useState(false);
29143
+ const [interactiveFromChild, setInteractiveFromChild] = useState(false);
29144
+ const parentControlName = useContext(ControlNameContext);
29145
+ const parentControlDisabled = useContext(DisabledContext);
29146
+ const parentControlReadOnly = useContext(ReadOnlyContext);
29147
+ const parentControlRequired = useContext(RequiredContext);
29148
+ const parentControlLoading = useContext(LoadingContext);
29149
+ const nameResolved = name || parentControlName;
29150
+ const disabledResolved = disabled || parentControlDisabled;
29151
+ const readOnlyResolved = readOnly || parentControlReadOnly;
29152
+ const requiredResolved = required || parentControlRequired;
29153
+ const loadingResolved = loading || parentControlLoading;
29154
+ const controlToInterfaceContextValue = useMemo(() => ({
29155
+ id: fieldId,
29156
+ setReadOnly: setReadOnlyFromChild,
29157
+ setDisabled: setDisabledByChild,
29158
+ setInteractive: setInteractiveFromChild
29159
+ }), [fieldId]);
29160
+ let childrenWithContext;
29161
+ if (props.children === undefined) ; else {
29162
+ childrenWithContext = jsx(MessagePropsRefContext.Provider, {
29163
+ value: messagePropsRef,
29164
+ children: jsx(ControlToInterfaceContext.Provider, {
29165
+ value: controlToInterfaceContextValue,
29166
+ children: jsx(ControlNameContext.Provider, {
29167
+ value: nameResolved,
29168
+ children: jsx(DisabledContext.Provider, {
29169
+ value: disabledResolved,
29170
+ children: jsx(ReadOnlyContext.Provider, {
29171
+ value: readOnlyResolved,
29172
+ children: jsx(RequiredContext.Provider, {
29173
+ value: requiredResolved,
29174
+ children: jsx(LoadingContext.Provider, {
29175
+ value: loadingResolved,
29176
+ children: props.children
29177
+ })
29178
+ })
29179
+ })
29180
+ })
29181
+ })
29182
+ })
29183
+ });
29184
+ }
29185
+
29186
+ // a field inteface can make the field component
29187
+ // disabled/readonly when that field interface is disabled/readonly
29188
+ // this is the only bottom up communication there is
29189
+ // (apart from action requested by child which cause ancestor action to execute)
29190
+ const disabledOrByChild = disabledResolved || disabledByChild;
29191
+ const readOnlyOrByChild = readOnlyResolved || readOnlyFromChild;
29192
+ const interactiveOrByChild = interactive || interactiveFromChild;
29193
+ const fieldProps = {
29194
+ "data-navi-field": "",
29195
+ "data-interactive": interactiveOrByChild ? "" : undefined,
29196
+ ...remainingProps,
29197
+ "children": childrenWithContext,
29198
+ "pseudoClasses": FieldPseudoClasses,
29199
+ "basePseudoState": {
29200
+ ":disabled": disabledOrByChild,
29201
+ ":read-only": readOnlyOrByChild,
29202
+ ...remainingProps.basePseudoState
29203
+ }
29204
+ };
29205
+ return jsx(Box, {
29206
+ flex: vertical ? "y" : undefined,
29207
+ alignX: vertical ? "start" : undefined,
29208
+ "data-vertical": vertical ? "" : undefined,
29209
+ ...fieldProps
29210
+ });
29211
+ };
29212
+ const FieldPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
29213
+ const Label = props => {
29214
+ const {
29215
+ children,
29216
+ htmlFor,
29217
+ ...rest
29218
+ } = props;
29219
+ const controlToInterface = useContext(ControlToInterfaceContext);
29220
+ const fieldId = controlToInterface?.id;
29221
+ return jsx(Box, {
29222
+ as: "label",
29223
+ htmlFor: htmlFor || fieldId,
29224
+ baseClassName: "navi_label",
29225
+ pseudoClasses: LabelPseudoClasses,
29226
+ ...rest,
29227
+ children: children
29228
+ });
29229
+ };
29230
+ const LabelPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
29231
+
29232
+ const InputTextualContext = createContext(null);
29233
+
29234
+ const InputLeftSlot = props => {
29235
+ return jsx(InputSlot, {
29236
+ ...props,
29237
+ side: "left"
29238
+ });
29239
+ };
29240
+ const InputRightSlot = props => {
29241
+ return jsx(InputSlot, {
29242
+ ...props,
29243
+ side: "right"
29244
+ });
29245
+ };
29246
+ const InputIconSlot = ({
29247
+ children,
29248
+ side = "right",
29249
+ ...props
29250
+ }) => {
29251
+ return jsx(InputSlot, {
29252
+ side: side,
29253
+ children: jsx(Icon, {
29254
+ ...props,
29255
+ children: children
29256
+ })
29257
+ });
29258
+ };
29259
+ const InputUnitSlot = ({
29260
+ children,
29261
+ side = "right",
29262
+ ...props
29263
+ }) => {
29264
+ return jsx(InputSlot, {
29265
+ side: side,
29266
+ marginLeft: "xxs",
29267
+ noWrap: true,
29268
+ ...props,
29269
+ children: children
29270
+ });
29271
+ };
29272
+ const InputSlot = ({
29273
+ side,
29274
+ onClick,
29275
+ hideWhileEmpty,
29276
+ ...props
29277
+ }) => {
29278
+ const ctx = useContext(InputTextualContext);
29279
+ const {
29280
+ id,
29281
+ readOnly,
29282
+ disabled
29283
+ } = ctx || {};
29284
+ return jsx(Label, {
29285
+ htmlFor: id,
29286
+ className: "navi_input_slot",
29287
+ disabled: disabled,
29288
+ readOnly: readOnly,
29289
+ "data-readonly": readOnly,
29290
+ "data-disabled": disabled,
29291
+ "data-left": side === "left" ? "" : undefined,
29292
+ "data-right": side === "right" ? "" : undefined,
29293
+ "data-hide-while-empty": hideWhileEmpty ? "" : undefined,
29294
+ inline: true,
29295
+ flex: true,
29296
+ align: "center",
29297
+ onMouseDown: e => {
29298
+ // Only prevent focus from leaving when the input already has focus.
29299
+ // If the input is not focused, let the mousedown proceed normally so
29300
+ // the slot element (e.g. a clear button) can receive focus itself.
29301
+ const inputEl = document.getElementById(id);
29302
+ if (inputEl && inputEl === document.activeElement) {
29303
+ e.preventDefault();
29304
+ }
29305
+ },
29306
+ onClick: e => {
29307
+ onClick?.(e);
29308
+ const input = document.getElementById(id);
29309
+ const allowed = dispatchRequestInteraction(input, e);
29310
+ if (!allowed) {
29311
+ e.preventDefault();
29312
+ }
29313
+ },
29314
+ ...props
29315
+ });
29316
+ };
29317
+
29318
+ installImportMetaCssBuild(import.meta);const css$y = /* css */`
28910
29319
  @layer navi {
28911
29320
  .navi_radio {
28912
29321
  --margin: 3px 3px 0 5px;
@@ -29261,7 +29670,7 @@ const InputRadioHeadless = props => {
29261
29670
  };
29262
29671
  const APPEARANCE_SET = new Set(["icon", "button", "radio"]);
29263
29672
  const InputRadioFieldInterface = props => {
29264
- import.meta.css = [css$z, "@jsenv/navi/src/control/input/input_radio.jsx"];
29673
+ import.meta.css = [css$y, "@jsenv/navi/src/control/input/input_radio.jsx"];
29265
29674
  const [radioProps, remainingProps] = useCheckableProps(props);
29266
29675
  const {
29267
29676
  icon,
@@ -29549,7 +29958,7 @@ const NAVI_TYPE_DEFAULTS = {
29549
29958
  type: "text",
29550
29959
  inputMode: "numeric",
29551
29960
  autoCorrect: "off",
29552
- spellcheck: "false",
29961
+ spellcheck: false,
29553
29962
  autoComplete: "off",
29554
29963
  },
29555
29964
  };
@@ -29605,16 +30014,8 @@ const resolveInputProps = (props) => {
29605
30014
  }
29606
30015
 
29607
30016
  const currentType = props.type;
29608
- const currentTypeDefaults = NAVI_TYPE_DEFAULTS[currentType];
29609
- if (!currentTypeDefaults) {
29610
- return;
29611
- }
29612
- for (const key of Object.keys(currentTypeDefaults)) {
29613
- if (props[key] === undefined) {
29614
- props[key] = currentTypeDefaults[key];
29615
- }
29616
- }
29617
- // Apply formatters for the original navi type before remapping
30017
+ // Apply min/max/step formatters before anything else — this must run even for
30018
+ // standard HTML types (date, time, etc.) that have no NAVI_TYPE_DEFAULTS entry.
29618
30019
  const currentTypeMinMaxFormatter = MIN_MAX_FORMATTER_BY_TYPE[currentType];
29619
30020
  const currentTypeStepFormatter = STEP_FORMATTER_BY_TYPE[currentType];
29620
30021
  if (currentTypeMinMaxFormatter) {
@@ -29624,6 +30025,15 @@ const resolveInputProps = (props) => {
29624
30025
  if (currentTypeStepFormatter) {
29625
30026
  props.step = currentTypeStepFormatter(props.step);
29626
30027
  }
30028
+ const currentTypeDefaults = NAVI_TYPE_DEFAULTS[currentType];
30029
+ if (!currentTypeDefaults) {
30030
+ return;
30031
+ }
30032
+ for (const key of Object.keys(currentTypeDefaults)) {
30033
+ if (props[key] === undefined) {
30034
+ props[key] = currentTypeDefaults[key];
30035
+ }
30036
+ }
29627
30037
  const targetType = currentTypeDefaults.type;
29628
30038
  props.type = targetType;
29629
30039
  resolveInputProps(props);
@@ -29750,7 +30160,7 @@ const STEP_FORMATTER_BY_TYPE = {
29750
30160
  "datetime": parseStepToSeconds,
29751
30161
  };
29752
30162
 
29753
- installImportMetaCssBuild(import.meta);const css$y = /* css */`
30163
+ installImportMetaCssBuild(import.meta);const css$x = /* css */`
29754
30164
  @layer navi {
29755
30165
  .navi_input_range {
29756
30166
  --border-radius: 6px;
@@ -30004,7 +30414,7 @@ const InputRange = props => {
30004
30414
  });
30005
30415
  };
30006
30416
  const InputRangeFieldInterface = props => {
30007
- import.meta.css = [css$y, "@jsenv/navi/src/control/input/input_range.jsx"];
30417
+ import.meta.css = [css$x, "@jsenv/navi/src/control/input/input_range.jsx"];
30008
30418
  const {
30009
30419
  ref
30010
30420
  } = props;
@@ -30122,316 +30532,6 @@ const RangeStyleCSSVars = {
30122
30532
  const RangePseudoClasses = [":hover", ":active", ":-navi-pressed", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
30123
30533
  const RangePseudoElements = ["::-navi-loader"];
30124
30534
 
30125
- installImportMetaCssBuild(import.meta);const css$x = /* css */`
30126
- @layer navi {
30127
- [data-navi-field] {
30128
- .navi_checkbox {
30129
- --margin: 0;
30130
- }
30131
- .navi_radio {
30132
- --margin: 0;
30133
- }
30134
- }
30135
-
30136
- label[data-navi-field] {
30137
- &[data-interactive] {
30138
- cursor: pointer;
30139
- user-select: none;
30140
- }
30141
- &[data-readonly],
30142
- &[data-disabled] {
30143
- color: rgba(0, 0, 0, 0.5);
30144
- cursor: default;
30145
- }
30146
- }
30147
-
30148
- [data-navi-field-container] {
30149
- --field-spacing: var(--navi-xs);
30150
-
30151
- > * + .navi_label {
30152
- padding-left: var(--field-spacing);
30153
- }
30154
- > .navi_label:first-child {
30155
- padding-right: var(--field-spacing);
30156
- }
30157
- &[data-vertical] > .navi_label:first-child {
30158
- padding-bottom: var(--field-spacing);
30159
- }
30160
-
30161
- &[data-interactive] {
30162
- .navi_label {
30163
- cursor: pointer;
30164
- /* When label is interactive ability to select text oftens conflicts with other click interactions */
30165
- user-select: none;
30166
- }
30167
- }
30168
- &[data-readonly],
30169
- &[data-disabled] {
30170
- .navi_label {
30171
- color: rgba(0, 0, 0, 0.5);
30172
- cursor: default;
30173
- }
30174
- }
30175
- }
30176
- }
30177
- `;
30178
-
30179
- /**
30180
- * Field — a semantic wrapper that connects a label to a form control.
30181
- *
30182
- * It generates a stable `fieldId` (or accepts an explicit `id`) that is
30183
- * automatically forwarded to the `Label` inside the field as `htmlFor` and to
30184
- * any interactive control (Picker, Input, …) as its `id`, so clicking the
30185
- * label focuses the control without requiring manual wiring.
30186
- *
30187
- * It also tracks the readOnly / disabled / interactive state reported by its
30188
- * child control and reflects it on the `Label` (dimmed color, cursor change).
30189
- *
30190
- * Props:
30191
- * id — optional explicit id used as the field id instead of the auto-generated one
30192
- * vertical — shorthand for flex="y" + alignX="start"
30193
- * children — any JSX; should contain a `Label` and a form control
30194
- * ...rest — forwarded to the wrapping `<div>` (className, style, flex, spacing, …)
30195
- *
30196
- * @example
30197
- * <Field flex spacing="s">
30198
- * Date de début
30199
- * <Input name="start_date" required />
30200
- * </Field>
30201
- */
30202
- const Field = props => {
30203
- import.meta.css = [css$x, "@jsenv/navi/src/control/field.jsx"];
30204
- const refDefault = useRef();
30205
- props.ref = props.ref || refDefault;
30206
- const {
30207
- as,
30208
- vertical
30209
- } = props;
30210
- if (as === undefined && !vertical) {
30211
- props.as = "label";
30212
- }
30213
- if (props.as === "label") {
30214
- return jsx(FieldAsLabel, {
30215
- ...props
30216
- });
30217
- }
30218
- return jsx(FieldAsContainer, {
30219
- ...props
30220
- });
30221
- };
30222
- const FieldAsLabel = props => {
30223
- return jsx(FieldUI, {
30224
- ...props
30225
- });
30226
- };
30227
- const FieldAsContainer = props => {
30228
- const idDefault = useId();
30229
- const fieldId = `field_${idDefault}`;
30230
- props.fieldId = props.fieldId || props.id ? `${props.id}_field` : fieldId;
30231
- return jsx(FieldUI, {
30232
- ...props,
30233
- "data-navi-field-container": "",
30234
- styleCSSVars: FieldCSSVars
30235
- });
30236
- };
30237
- const FieldCSSVars = {
30238
- spacing: "--field-spacing"
30239
- };
30240
- const FieldUI = props => {
30241
- import.meta.css = [css$x, "@jsenv/navi/src/control/field.jsx"];
30242
- const {
30243
- vertical
30244
- } = props;
30245
- const {
30246
- fieldId,
30247
- name,
30248
- disabled,
30249
- readOnly,
30250
- required,
30251
- loading,
30252
- interactive,
30253
- ...rest
30254
- } = props;
30255
- const [messageProps, remainingProps] = extractMessageAndRemainingProps(rest);
30256
- const messagePropsRef = useRef();
30257
- messagePropsRef.current = messageProps;
30258
- const [disabledByChild, setDisabledByChild] = useState(false);
30259
- const [readOnlyFromChild, setReadOnlyFromChild] = useState(false);
30260
- const [interactiveFromChild, setInteractiveFromChild] = useState(false);
30261
- const parentControlName = useContext(ControlNameContext);
30262
- const parentControlDisabled = useContext(DisabledContext);
30263
- const parentControlReadOnly = useContext(ReadOnlyContext);
30264
- const parentControlRequired = useContext(RequiredContext);
30265
- const parentControlLoading = useContext(LoadingContext);
30266
- const nameResolved = name || parentControlName;
30267
- const disabledResolved = disabled || parentControlDisabled;
30268
- const readOnlyResolved = readOnly || parentControlReadOnly;
30269
- const requiredResolved = required || parentControlRequired;
30270
- const loadingResolved = loading || parentControlLoading;
30271
- const controlToInterfaceContextValue = useMemo(() => ({
30272
- id: fieldId,
30273
- setReadOnly: setReadOnlyFromChild,
30274
- setDisabled: setDisabledByChild,
30275
- setInteractive: setInteractiveFromChild
30276
- }), [fieldId]);
30277
- let childrenWithContext;
30278
- if (props.children === undefined) ; else {
30279
- childrenWithContext = jsx(MessagePropsRefContext.Provider, {
30280
- value: messagePropsRef,
30281
- children: jsx(ControlToInterfaceContext.Provider, {
30282
- value: controlToInterfaceContextValue,
30283
- children: jsx(ControlNameContext.Provider, {
30284
- value: nameResolved,
30285
- children: jsx(DisabledContext.Provider, {
30286
- value: disabledResolved,
30287
- children: jsx(ReadOnlyContext.Provider, {
30288
- value: readOnlyResolved,
30289
- children: jsx(RequiredContext.Provider, {
30290
- value: requiredResolved,
30291
- children: jsx(LoadingContext.Provider, {
30292
- value: loadingResolved,
30293
- children: props.children
30294
- })
30295
- })
30296
- })
30297
- })
30298
- })
30299
- })
30300
- });
30301
- }
30302
-
30303
- // a field inteface can make the field component
30304
- // disabled/readonly when that field interface is disabled/readonly
30305
- // this is the only bottom up communication there is
30306
- // (apart from action requested by child which cause ancestor action to execute)
30307
- const disabledOrByChild = disabledResolved || disabledByChild;
30308
- const readOnlyOrByChild = readOnlyResolved || readOnlyFromChild;
30309
- const interactiveOrByChild = interactive || interactiveFromChild;
30310
- const fieldProps = {
30311
- "data-navi-field": "",
30312
- "data-interactive": interactiveOrByChild ? "" : undefined,
30313
- ...remainingProps,
30314
- "children": childrenWithContext,
30315
- "pseudoClasses": FieldPseudoClasses,
30316
- "basePseudoState": {
30317
- ":disabled": disabledOrByChild,
30318
- ":read-only": readOnlyOrByChild,
30319
- ...remainingProps.basePseudoState
30320
- }
30321
- };
30322
- return jsx(Box, {
30323
- flex: vertical ? "y" : undefined,
30324
- alignX: vertical ? "start" : undefined,
30325
- "data-vertical": vertical ? "" : undefined,
30326
- ...fieldProps
30327
- });
30328
- };
30329
- const FieldPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
30330
- const Label = props => {
30331
- const {
30332
- children,
30333
- htmlFor,
30334
- ...rest
30335
- } = props;
30336
- const controlToInterface = useContext(ControlToInterfaceContext);
30337
- const fieldId = controlToInterface?.id;
30338
- return jsx(Box, {
30339
- as: "label",
30340
- htmlFor: htmlFor || fieldId,
30341
- baseClassName: "navi_label",
30342
- pseudoClasses: LabelPseudoClasses,
30343
- ...rest,
30344
- children: children
30345
- });
30346
- };
30347
- const LabelPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
30348
-
30349
- const InputTextualContext = createContext(null);
30350
-
30351
- const InputLeftSlot = props => {
30352
- return jsx(InputSlot, {
30353
- ...props,
30354
- side: "left"
30355
- });
30356
- };
30357
- const InputRightSlot = props => {
30358
- return jsx(InputSlot, {
30359
- ...props,
30360
- side: "right"
30361
- });
30362
- };
30363
- const InputIconSlot = ({
30364
- children,
30365
- side = "right",
30366
- ...props
30367
- }) => {
30368
- return jsx(InputSlot, {
30369
- side: side,
30370
- children: jsx(Icon, {
30371
- ...props,
30372
- children: children
30373
- })
30374
- });
30375
- };
30376
- const InputUnitSlot = ({
30377
- children,
30378
- side = "right",
30379
- ...props
30380
- }) => {
30381
- return jsx(InputSlot, {
30382
- side: side,
30383
- marginLeft: "xxs",
30384
- noWrap: true,
30385
- ...props,
30386
- children: children
30387
- });
30388
- };
30389
- const InputSlot = ({
30390
- side,
30391
- onClick,
30392
- hideWhileEmpty,
30393
- ...props
30394
- }) => {
30395
- const ctx = useContext(InputTextualContext);
30396
- const {
30397
- id,
30398
- readOnly,
30399
- disabled
30400
- } = ctx;
30401
- return jsx(Label, {
30402
- htmlFor: id,
30403
- className: "navi_input_slot",
30404
- disabled: disabled,
30405
- readOnly: readOnly,
30406
- "data-readonly": readOnly,
30407
- "data-disabled": disabled,
30408
- "data-left": side === "left" ? "" : undefined,
30409
- "data-right": side === "right" ? "" : undefined,
30410
- "data-hide-while-empty": hideWhileEmpty ? "" : undefined,
30411
- inline: true,
30412
- flex: true,
30413
- align: "center",
30414
- onMouseDown: e => {
30415
- // Only prevent focus from leaving when the input already has focus.
30416
- // If the input is not focused, let the mousedown proceed normally so
30417
- // the slot element (e.g. a clear button) can receive focus itself.
30418
- const inputEl = document.getElementById(id);
30419
- if (inputEl && inputEl === document.activeElement) {
30420
- e.preventDefault();
30421
- }
30422
- },
30423
- onClick: e => {
30424
- onClick?.(e);
30425
- const input = document.getElementById(id);
30426
- const allowed = dispatchRequestInteraction(input, e);
30427
- if (!allowed) {
30428
- e.preventDefault();
30429
- }
30430
- },
30431
- ...props
30432
- });
30433
- };
30434
-
30435
30535
  const InputNaviHourResolver = props => {
30436
30536
  const Next = useNextResolver();
30437
30537
  if (props["navi-input-type"] === "hour") {
@@ -30523,39 +30623,72 @@ const InputModeNumeric = props => {
30523
30623
  return jsx(Next, {
30524
30624
  maxLength: maxLength,
30525
30625
  ...props,
30526
- onKeyDown: e => {
30527
- props.onKeyDown?.(e);
30626
+ onInput: e => {
30627
+ props.onInput?.(e);
30528
30628
  if (e.defaultPrevented) {
30529
30629
  return;
30530
30630
  }
30531
- if (e.key !== "ArrowUp" && e.key !== "ArrowDown") {
30631
+ if (maxLength === undefined) {
30532
30632
  return;
30533
30633
  }
30534
- e.preventDefault();
30535
- const currentValue = Number(e.currentTarget.value);
30536
- if (Number.isNaN(currentValue)) {
30634
+ const input = e.currentTarget;
30635
+ if (input.value.length < maxLength) {
30537
30636
  return;
30538
30637
  }
30539
- const delta = e.key === "ArrowUp" ? step : -step;
30540
- // Snap to step grid relative to step base (min ?? 0), then move
30541
- const stepBase = min !== undefined ? min : 0;
30542
- const offset = currentValue - stepBase;
30543
- const currentStepIndex = Math.round(offset / step);
30544
- const snapped = stepBase + currentStepIndex * step;
30545
- let nextValue = snapped + delta;
30546
- if (min !== undefined && nextValue < min) {
30547
- nextValue = min;
30548
- }
30549
- if (max !== undefined && nextValue > max) {
30550
- nextValue = max;
30638
+ if (input.selectionStart !== maxLength) {
30639
+ return;
30551
30640
  }
30552
- triggerStringAction("update", nextValue, {
30553
- event: e,
30554
- actionTarget: e.currentTarget
30641
+ // Field is full and caret is at the end: notify listeners then
30642
+ // select all so the next keystroke starts a fresh value instead of
30643
+ // being silently blocked by maxlength.
30644
+ const allowed = dispatchPublicCustomEvent(input, "navi_input_full", {
30645
+ event: e
30555
30646
  });
30647
+ if (allowed) {
30648
+ input.select();
30649
+ }
30650
+ },
30651
+ onKeyDown: e => {
30652
+ props.onKeyDown?.(e);
30653
+ if (e.defaultPrevented) {
30654
+ return;
30655
+ }
30656
+ if (e.key === "ArrowUp" || e.key === "ArrowDown") {
30657
+ performArrowUpDown(e);
30658
+ return;
30659
+ }
30556
30660
  }
30557
30661
  });
30558
30662
  };
30663
+ const performArrowUpDown = e => {
30664
+ const input = e.currentTarget;
30665
+ const currentValue = Number(input.value);
30666
+ if (Number.isNaN(currentValue)) {
30667
+ e.preventDefault();
30668
+ return;
30669
+ }
30670
+ const min = input.min !== "" ? Number(input.min) : undefined;
30671
+ const max = input.max !== "" ? Number(input.max) : undefined;
30672
+ const step = input.step !== "" && input.step !== "any" ? Number(input.step) : 1;
30673
+ const delta = e.key === "ArrowUp" ? step : -step;
30674
+ // Snap to step grid relative to step base (min ?? 0), then move
30675
+ const stepBase = min !== undefined ? min : 0;
30676
+ const offset = currentValue - stepBase;
30677
+ const currentStepIndex = Math.round(offset / step);
30678
+ const snapped = stepBase + currentStepIndex * step;
30679
+ let nextValue = snapped + delta;
30680
+ if (min !== undefined && nextValue < min) {
30681
+ nextValue = min;
30682
+ }
30683
+ if (max !== undefined && nextValue > max) {
30684
+ nextValue = max;
30685
+ }
30686
+ triggerStringAction("update", nextValue, {
30687
+ event: e,
30688
+ actionTarget: e.currentTarget
30689
+ });
30690
+ e.preventDefault();
30691
+ };
30559
30692
 
30560
30693
  const SearchSvg = () => jsx("svg", {
30561
30694
  viewBox: "0 0 24 24",
@@ -31071,12 +31204,8 @@ const css$w = /* css */`
31071
31204
  var(--border-color) 45%,
31072
31205
  transparent
31073
31206
  );
31074
- --background-color-readonly: var(--background-color);
31075
- --color-readonly: color-mix(
31076
- in srgb,
31077
- var(--picker-border-color) 45%,
31078
- transparent
31079
- );
31207
+ --background-color-readonly: var(--background-color-hover);
31208
+ --color-readonly: color-mix(in srgb, var(--color) 65%, transparent);
31080
31209
  /* Disabled */
31081
31210
  --border-color-disabled: var(--border-color-readonly);
31082
31211
  --background-color-disabled: color-mix(
@@ -31241,6 +31370,49 @@ const css$w = /* css */`
31241
31370
  --x-background-color: transparent;
31242
31371
  }
31243
31372
  }
31373
+
31374
+ &[data-variant="underline"] {
31375
+ border: none;
31376
+ border-radius: 0;
31377
+ --x-background-color: transparent;
31378
+ padding-right: 0;
31379
+ padding-left: 0;
31380
+
31381
+ .navi_input_real_input_wrapper {
31382
+ position: relative;
31383
+ display: inline-flex;
31384
+ flex-grow: 1;
31385
+ }
31386
+
31387
+ .navi_input_underline {
31388
+ position: absolute;
31389
+ top: calc(100% - 1px);
31390
+ right: 0;
31391
+ left: 0;
31392
+ height: 1px;
31393
+ background-color: var(--x-border-color);
31394
+ pointer-events: none;
31395
+ }
31396
+
31397
+ &[data-hover] {
31398
+ --x-background-color: transparent;
31399
+ }
31400
+ &[data-focus-visible] {
31401
+ --x-background-color: transparent;
31402
+ outline-style: none;
31403
+
31404
+ .navi_input_underline {
31405
+ height: 2px;
31406
+ background-color: var(--outline-color);
31407
+ }
31408
+ }
31409
+ &[data-readonly] {
31410
+ --x-background-color: transparent;
31411
+ }
31412
+ &[data-disabled] {
31413
+ --x-background-color: transparent;
31414
+ }
31415
+ }
31244
31416
  }
31245
31417
 
31246
31418
  .navi_input .navi_control_input::placeholder {
@@ -31284,6 +31456,7 @@ const InputTextualUI = props => {
31284
31456
  const {
31285
31457
  ui,
31286
31458
  discrete,
31459
+ variant,
31287
31460
  width = "maxLength"
31288
31461
  } = props;
31289
31462
  const [inputProps, remainingProps] = useInputTextualProps(props);
@@ -31337,6 +31510,7 @@ const InputTextualUI = props => {
31337
31510
  discrete: undefined // handled via data attribute
31338
31511
  ,
31339
31512
 
31513
+ "data-variant": variant || undefined,
31340
31514
  styleCSSVars: InputStyleCSSVars,
31341
31515
  pseudoStateSelector: ".navi_control_input",
31342
31516
  pseudoClasses: InputPseudoClasses,
@@ -31345,7 +31519,14 @@ const InputTextualUI = props => {
31345
31519
  loading: loading,
31346
31520
  color: "var(--loader-color)",
31347
31521
  inset: -1
31348
- }), jsx(RealInput, {
31522
+ }), variant === "underline" ? jsxs("span", {
31523
+ className: "navi_input_real_input_wrapper",
31524
+ children: [jsx(RealInput, {
31525
+ ...inputProps
31526
+ }), jsx("span", {
31527
+ className: "navi_input_underline"
31528
+ })]
31529
+ }) : jsx(RealInput, {
31349
31530
  ...inputProps
31350
31531
  }), childrenWithContext]
31351
31532
  });
@@ -31437,7 +31618,9 @@ const Input = props => {
31437
31618
  };
31438
31619
  Input.UI = {
31439
31620
  LeftSlot: InputLeftSlot,
31440
- RightSlot: InputRightSlot
31621
+ RightSlot: InputRightSlot,
31622
+ IconSlot: InputIconSlot,
31623
+ UnitSlot: InputUnitSlot
31441
31624
  };
31442
31625
 
31443
31626
  installImportMetaCssBuild(import.meta);/**
@@ -31963,6 +32146,111 @@ const CheckboxGroupInterface = props => {
31963
32146
  });
31964
32147
  };
31965
32148
 
32149
+ const InputGroup = props => {
32150
+ const ref = useRef(null);
32151
+ useInputGroup(ref);
32152
+ return jsx(Box, {
32153
+ ref: ref,
32154
+ ...props
32155
+ });
32156
+ };
32157
+ const useInputGroup = ref => {
32158
+ const debugFocus = useDebugFocus();
32159
+ useEffect(() => {
32160
+ const el = ref.current;
32161
+ if (!el) {
32162
+ return () => {};
32163
+ }
32164
+ const getInputs = () => Array.from(el.querySelectorAll(".navi_control_input"));
32165
+ const focusInput = input => {
32166
+ input.focus();
32167
+ input.select();
32168
+ };
32169
+ const handleKeyDown = e => {
32170
+ if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") {
32171
+ return;
32172
+ }
32173
+ const active = document.activeElement;
32174
+ if (!isTextInputElement(active) || !el.contains(active)) {
32175
+ return;
32176
+ }
32177
+ if (e.key === "ArrowRight") {
32178
+ const atEnd = active.selectionStart === active.value.length && active.selectionEnd === active.value.length;
32179
+ if (!atEnd) {
32180
+ return;
32181
+ }
32182
+ const inputs = getInputs();
32183
+ const idx = inputs.indexOf(active);
32184
+ if (idx === -1) {
32185
+ debugFocus(e, "InputGroup ArrowRight on non group input → do nothing");
32186
+ return;
32187
+ }
32188
+ if (idx === inputs.length - 1) {
32189
+ debugFocus(e, "InputGroup ArrowRight at end of last input → do nothing");
32190
+ return;
32191
+ }
32192
+ debugFocus(e, "InputGroup ArrowRight at end of input[%d] → focus input[%d]", idx, idx + 1);
32193
+ e.preventDefault();
32194
+ focusInput(inputs[idx + 1]);
32195
+ return;
32196
+ }
32197
+ const atStart = active.selectionStart === 0 && active.selectionEnd === 0;
32198
+ if (!atStart) {
32199
+ return;
32200
+ }
32201
+ const inputs = getInputs();
32202
+ const idx = inputs.indexOf(active);
32203
+ if (idx === 0) {
32204
+ return;
32205
+ }
32206
+ debugFocus(e, "InputGroup ArrowLeft at start of input[%d] → focus input[%d]", idx, idx - 1);
32207
+ e.preventDefault();
32208
+ focusInput(inputs[idx - 1]);
32209
+ };
32210
+ const handleNaviInputFull = e => {
32211
+ const input = e.detail.event.currentTarget;
32212
+ if (!el.contains(input)) {
32213
+ return;
32214
+ }
32215
+ const inputs = getInputs();
32216
+ const idx = inputs.indexOf(input);
32217
+ if (idx === -1) {
32218
+ return;
32219
+ }
32220
+ if (idx === inputs.length - 1) {
32221
+ return;
32222
+ }
32223
+ const nextInput = inputs[idx + 1];
32224
+ debugFocus(e, "InputGroup navi_input_full on input -> move to next input", input, nextInput);
32225
+ e.preventDefault();
32226
+ focusInput(nextInput);
32227
+ };
32228
+ el.addEventListener("keydown", handleKeyDown, {
32229
+ capture: false
32230
+ });
32231
+ el.addEventListener("navi_input_full", handleNaviInputFull);
32232
+ return () => {
32233
+ el.removeEventListener("keydown", handleKeyDown, {
32234
+ capture: false
32235
+ });
32236
+ el.removeEventListener("navi_input_full", handleNaviInputFull);
32237
+ };
32238
+ }, [debugFocus]);
32239
+ };
32240
+ const isTextInputElement = el => {
32241
+ if (!el) {
32242
+ return false;
32243
+ }
32244
+ if (el.tagName === "TEXTAREA") {
32245
+ return true;
32246
+ }
32247
+ if (el.tagName !== "INPUT") {
32248
+ return false;
32249
+ }
32250
+ const type = el.type || "text";
32251
+ return type === "text" || type === "search" || type === "url" || type === "tel" || type === "email" || type === "password" || type === "number";
32252
+ };
32253
+
31966
32254
  installImportMetaCssBuild(import.meta);const css$s = /* css */`
31967
32255
  .navi_radio_group {
31968
32256
  border-style: solid;
@@ -33144,6 +33432,7 @@ const TimeDate = ({
33144
33432
  children,
33145
33433
  locale,
33146
33434
  long,
33435
+ numeric,
33147
33436
  dayLabel,
33148
33437
  now,
33149
33438
  ...props
@@ -33169,7 +33458,8 @@ const TimeDate = ({
33169
33458
  });
33170
33459
  }
33171
33460
  const base = formatDay(date, lang, {
33172
- long
33461
+ long,
33462
+ numeric
33173
33463
  });
33174
33464
  let text;
33175
33465
  if (dayLabel) {
@@ -33474,7 +33764,7 @@ const PickerText = props => {
33474
33764
  const PickerArray = props => {
33475
33765
  const Next = useNextResolver();
33476
33766
  return jsx(Next, {
33477
- maxRows: 3,
33767
+ maxLines: "3",
33478
33768
  ui: jsx(PickerArrayUI, {}),
33479
33769
  ...props,
33480
33770
  type: "navi_picker"
@@ -33484,7 +33774,7 @@ const PickerArrayUI = () => {
33484
33774
  const {
33485
33775
  value,
33486
33776
  placeholder,
33487
- maxRows
33777
+ maxLines
33488
33778
  } = useContext(PickerContext);
33489
33779
  if (!value || value.length === 0) {
33490
33780
  if (!placeholder) {
@@ -33495,8 +33785,7 @@ const PickerArrayUI = () => {
33495
33785
  return jsx(Text, {
33496
33786
  spacing: ", ",
33497
33787
  shrinkWrap: true,
33498
- lineClamp: maxRows > 1 ? maxRows : undefined,
33499
- overflowEllipsis: maxRows === 1 ? true : undefined,
33788
+ maxLines: maxLines,
33500
33789
  children: value.map(item => {
33501
33790
  return jsx("span", {
33502
33791
  children: item
@@ -33547,6 +33836,8 @@ const PickerDateUI = props => {
33547
33836
  return jsx(Time, {
33548
33837
  type: "date",
33549
33838
  color: "var(--picker-placeholder-color",
33839
+ capitalize: true,
33840
+ maxLines: "1",
33550
33841
  ...props
33551
33842
  });
33552
33843
  }
@@ -33555,6 +33846,7 @@ const PickerDateUI = props => {
33555
33846
  return jsx(Time, {
33556
33847
  type: "date",
33557
33848
  capitalize: true,
33849
+ maxLines: "1",
33558
33850
  ...props,
33559
33851
  children: value
33560
33852
  });
@@ -33568,7 +33860,7 @@ const PickerMonth = props => {
33568
33860
  type: "month"
33569
33861
  });
33570
33862
  };
33571
- const PickerMonthUI = () => {
33863
+ const PickerMonthUI = props => {
33572
33864
  const {
33573
33865
  value,
33574
33866
  placeholder
@@ -33577,14 +33869,18 @@ const PickerMonthUI = () => {
33577
33869
  if (!placeholder) {
33578
33870
  return jsx(Time, {
33579
33871
  type: "month",
33580
- color: "var(--picker-placeholder-color"
33872
+ color: "var(--picker-placeholder-color",
33873
+ maxLines: "1",
33874
+ ...props
33581
33875
  });
33582
33876
  }
33583
33877
  return placeholder;
33584
33878
  }
33585
33879
  return jsx(Time, {
33586
33880
  type: "month",
33881
+ maxLines: "1",
33587
33882
  capitalize: true,
33883
+ ...props,
33588
33884
  children: value
33589
33885
  });
33590
33886
  };
@@ -33597,7 +33893,7 @@ const PickerWeek = props => {
33597
33893
  type: "week"
33598
33894
  });
33599
33895
  };
33600
- const PickerWeekUI = () => {
33896
+ const PickerWeekUI = props => {
33601
33897
  const {
33602
33898
  value,
33603
33899
  placeholder
@@ -33606,7 +33902,9 @@ const PickerWeekUI = () => {
33606
33902
  if (!placeholder) {
33607
33903
  return jsx(Time, {
33608
33904
  type: "week",
33609
- color: "var(--picker-placeholder-color"
33905
+ color: "var(--picker-placeholder-color",
33906
+ maxLines: "1",
33907
+ ...props
33610
33908
  });
33611
33909
  }
33612
33910
  return placeholder;
@@ -33614,6 +33912,8 @@ const PickerWeekUI = () => {
33614
33912
  return jsx(Time, {
33615
33913
  type: "week",
33616
33914
  capitalize: true,
33915
+ maxLines: "1",
33916
+ ...props,
33617
33917
  children: value
33618
33918
  });
33619
33919
  };
@@ -33636,6 +33936,7 @@ const PickerTimeUI = props => {
33636
33936
  return jsx(Time, {
33637
33937
  type: "time",
33638
33938
  color: "var(--picker-placeholder-color",
33939
+ maxLines: "1",
33639
33940
  ...props
33640
33941
  });
33641
33942
  }
@@ -33643,6 +33944,7 @@ const PickerTimeUI = props => {
33643
33944
  }
33644
33945
  return jsx(Time, {
33645
33946
  type: "time",
33947
+ maxLines: "1",
33646
33948
  ...props,
33647
33949
  children: value
33648
33950
  });
@@ -33656,7 +33958,7 @@ const PickerDatetime = props => {
33656
33958
  type: "datetime-local"
33657
33959
  });
33658
33960
  };
33659
- const PickerDatetimeUI = () => {
33961
+ const PickerDatetimeUI = props => {
33660
33962
  const {
33661
33963
  value,
33662
33964
  placeholder
@@ -33665,13 +33967,16 @@ const PickerDatetimeUI = () => {
33665
33967
  if (!placeholder) {
33666
33968
  return jsx(Time, {
33667
33969
  type: "datetime",
33668
- color: "var(--picker-placeholder-color"
33970
+ color: "var(--picker-placeholder-color",
33971
+ maxLines: "1",
33972
+ ...props
33669
33973
  });
33670
33974
  }
33671
33975
  return placeholder;
33672
33976
  }
33673
33977
  return jsx(Time, {
33674
33978
  type: "datetime",
33979
+ maxLines: "1",
33675
33980
  children: value
33676
33981
  });
33677
33982
  };
@@ -34298,15 +34603,18 @@ installImportMetaCssBuild(import.meta);const css$m = /* css */`
34298
34603
  &[data-callout] {
34299
34604
  --x-list-border-color: var(--callout-color);
34300
34605
  }
34606
+
34607
+ .navi_list_item {
34608
+ --x-list-item-cursor: default;
34609
+ --x-list-item-border-color: var(--list-item-border-color);
34610
+
34611
+ position: relative;
34612
+ font-size: var(--navi-control-font-size);
34613
+ font-family: var(--navi-control-font-family);
34614
+ }
34301
34615
  }
34302
34616
 
34303
34617
  .navi_list_item[navi-selectable] {
34304
- --x-list-item-cursor: default;
34305
- --x-list-item-border-color: var(--list-item-border-color);
34306
-
34307
- position: relative;
34308
- font-size: var(--navi-control-font-size);
34309
- font-family: var(--navi-control-font-family);
34310
34618
  outline-width: var(--list-item-outline-width);
34311
34619
  outline-color: var(--list-item-outline-color);
34312
34620
  outline-offset: var(--list-item-outline-offset);
@@ -34380,7 +34688,7 @@ installImportMetaCssBuild(import.meta);const css$m = /* css */`
34380
34688
  }
34381
34689
 
34382
34690
  input,
34383
- .navi_picker {
34691
+ .navi_picker_content {
34384
34692
  color: revert;
34385
34693
  }
34386
34694
  }
@@ -35129,6 +35437,7 @@ const css$l = /* css */`
35129
35437
  background-color: var(--x-list-item-background-color);
35130
35438
  border: var(--x-list-item-border-width) solid
35131
35439
  var(--x-list-item-border-color);
35440
+ border-radius: var(--list-item-border-radius, 0px);
35132
35441
  /*
35133
35442
  CSS impossible d'obtenir un layout qui ferait en gros:
35134
35443
  width = max(min(max-content, 100%), unbreakable-content)
@@ -35138,7 +35447,7 @@ const css$l = /* css */`
35138
35447
  -> NOPE
35139
35448
  - Force overflow hidden + ellipsis
35140
35449
  - casse la lisibilité des mots insécables
35141
- - possible d'optin en utilisant overflowEllipsis sur le ListItem
35450
+ - possible d'optin en utilisant maxLines sur le ListItem
35142
35451
  -> Bien mais pas par défaut
35143
35452
  - Forcer le retour a la ligne des mot inécables
35144
35453
  - Aucun des inconvénient ci dessus
@@ -36147,6 +36456,8 @@ const ListItemReal = props => {
36147
36456
  });
36148
36457
  };
36149
36458
  const LIST_ITEM_STYLE_CSS_VARS = {
36459
+ "borderRadius": "--list-item-border-radius",
36460
+ "borderWidth": "--list-item-border-width",
36150
36461
  "padding": "--list-item-padding",
36151
36462
  "paddingX": "--list-item-padding-x",
36152
36463
  "paddingY": "--list-item-padding-y",
@@ -36157,7 +36468,6 @@ const LIST_ITEM_STYLE_CSS_VARS = {
36157
36468
  "color": "--list-item-color",
36158
36469
  "backgroundColor": "--list-item-background-color",
36159
36470
  "fontWeight": "--list-item-font-weight",
36160
- "borderWidth": "--list-item-border-width",
36161
36471
  "borderColor": "--list-item-border-color",
36162
36472
  ":-navi-pointed": {
36163
36473
  color: "--list-item-color-keyboard-pointed",
@@ -36537,7 +36847,6 @@ installImportMetaCssBuild(import.meta);const css$k = /* css */`
36537
36847
  align-self: flex-start;
36538
36848
  justify-content: center;
36539
36849
  color: var(--x-picker-icon-color);
36540
- transform: translateX(25%);
36541
36850
  }
36542
36851
  .navi_picker_input {
36543
36852
  position: absolute;
@@ -36554,13 +36863,17 @@ installImportMetaCssBuild(import.meta);const css$k = /* css */`
36554
36863
  pointer-events: none;
36555
36864
  }
36556
36865
 
36866
+ .navi_picker_content {
36867
+ display: contents;
36868
+ }
36869
+
36557
36870
  &[data-line-clamp] {
36558
36871
  overflow-wrap: anywhere;
36559
36872
  .navi_picker_value {
36560
36873
  display: -webkit-box;
36561
36874
  white-space: normal;
36562
36875
  -webkit-box-orient: vertical;
36563
- -webkit-line-clamp: var(--picker-max-rows);
36876
+ -webkit-line-clamp: var(--picker-max-lines);
36564
36877
  }
36565
36878
  }
36566
36879
 
@@ -36594,6 +36907,17 @@ installImportMetaCssBuild(import.meta);const css$k = /* css */`
36594
36907
  &[data-callout] {
36595
36908
  --x-picker-border-color: var(--callout-color);
36596
36909
  }
36910
+
36911
+ &[data-variant="icon"] {
36912
+ --x-picker-padding-top: 0;
36913
+ --x-picker-padding-right: 0;
36914
+ --x-picker-padding-bottom: 0;
36915
+ --x-picker-padding-left: 0;
36916
+ --picker-border-width: 0;
36917
+ --x-picker-border-color: transparent;
36918
+ --x-picker-background-color: transparent;
36919
+ --x-picker-icon-color: currentColor;
36920
+ }
36597
36921
  }
36598
36922
  `;
36599
36923
 
@@ -36630,12 +36954,16 @@ installImportMetaCssBuild(import.meta);const css$k = /* css */`
36630
36954
  */
36631
36955
  const PickerButton = props => {
36632
36956
  import.meta.css = [css$k, "@jsenv/navi/src/control/picker/picker.jsx"];
36957
+ if (typeof props.maxLines === "string") {
36958
+ props.maxLines = parseInt(props.maxLines);
36959
+ }
36633
36960
  const {
36634
36961
  ref,
36962
+ variant,
36635
36963
  icon,
36636
36964
  placeholder,
36637
36965
  ui,
36638
- maxRows
36966
+ maxLines
36639
36967
  } = props;
36640
36968
  const inputRef = useRef(null);
36641
36969
  const [inputProps, pickerRemainingProps] = useControlProps({
@@ -36657,7 +36985,7 @@ const PickerButton = props => {
36657
36985
  children
36658
36986
  } = inputProps;
36659
36987
  const loading = basePseudoState[":-navi-loading"];
36660
- const hasLineClamp = maxRows && maxRows > 1;
36988
+ const hasLineClamp = maxLines && maxLines > 1;
36661
36989
  return jsxs(Box, {
36662
36990
  as: "button",
36663
36991
  ref: ref,
@@ -36667,8 +36995,9 @@ const PickerButton = props => {
36667
36995
  pseudoClasses: PICKER_BUTTON_PSEUDO_CLASSES,
36668
36996
  disabled: disabled,
36669
36997
  "data-line-clamp": hasLineClamp ? "" : undefined,
36998
+ "data-variant": variant,
36670
36999
  style: {
36671
- "--picker-max-rows": maxRows || -1
37000
+ "--picker-max-lines": maxLines
36672
37001
  },
36673
37002
  ...pickerRemainingProps,
36674
37003
  basePseudoState: basePseudoState,
@@ -36678,9 +37007,10 @@ const PickerButton = props => {
36678
37007
  ,
36679
37008
 
36680
37009
  id: id,
37010
+ variant: undefined,
36681
37011
  icon: undefined,
36682
37012
  ui: undefined,
36683
- maxRows: undefined,
37013
+ maxLines: undefined,
36684
37014
  dayLabel: undefined
36685
37015
  // The button is handling the pointer interactions
36686
37016
  ,
@@ -36711,14 +37041,14 @@ const PickerButton = props => {
36711
37041
  onMouseDown: undefined,
36712
37042
  onClick: undefined,
36713
37043
  onKeyDown: undefined
36714
- }), jsx(Text, {
37044
+ }), variant === "icon" ? null : jsx(Text, {
36715
37045
  className: "navi_picker_value",
36716
37046
  "navi-placeholder": value === undefined || value === "" ? "" : undefined,
36717
37047
  children: jsx(PickerContext.Provider, {
36718
37048
  value: {
36719
37049
  value,
36720
37050
  placeholder,
36721
- maxRows
37051
+ maxLines
36722
37052
  },
36723
37053
  children: ui === undefined ? jsx(PickerDefaultUI, {}) : ui
36724
37054
  })
@@ -36729,7 +37059,10 @@ const PickerButton = props => {
36729
37059
  children: icon === undefined ? jsx(ChevronDownSvg, {}) : icon
36730
37060
  })
36731
37061
  }), jsx(ControlChildrenWrapper, {
36732
- children: children
37062
+ children: jsx("div", {
37063
+ className: "navi_picker_content",
37064
+ children: children
37065
+ })
36733
37066
  })]
36734
37067
  });
36735
37068
  };
@@ -36809,6 +37142,12 @@ Picker.UI.Datetime = PickerDatetimeUI;
36809
37142
  Picker.UI.File = PickerFileUI;
36810
37143
  Picker.UI.Color = PickerColorUI;
36811
37144
  Picker.UI.Multiple = PickerArrayUI;
37145
+ Picker.UI.PencilSvg = PencilSvg;
37146
+ Picker.UI.ChevronDownSvg = ChevronDownSvg;
37147
+ Picker.UI.ClockSvg = ClockSvg;
37148
+ Picker.UI.CalendarSvg = CalendarSvg;
37149
+ Picker.UI.FileSvg = FileSvg;
37150
+ Picker.UI.ColorSvg = ColorSvg;
36812
37151
 
36813
37152
  /**
36814
37153
  * applySearch — matches value against searchText.
@@ -41001,7 +41340,7 @@ const Badge = ({
41001
41340
  return jsx(Text, {
41002
41341
  className: withPropsClassName("navi_badge", className),
41003
41342
  bold: true,
41004
- overflowEllipsis: true,
41343
+ maxLines: 1,
41005
41344
  ...props,
41006
41345
  styleCSSVars: BadgeStyleCSSVars$1,
41007
41346
  spacing: jsx("span", {}),
@@ -41378,7 +41717,6 @@ const BadgeList = ({
41378
41717
  children,
41379
41718
  shrinkWrap = true,
41380
41719
  max,
41381
- maxRows,
41382
41720
  ...props
41383
41721
  }) => {
41384
41722
  import.meta.css = [css$9, "@jsenv/navi/src/text/badge_list.jsx"];
@@ -41442,7 +41780,6 @@ const BadgeList = ({
41442
41780
  baseClassName: "navi_badge_list",
41443
41781
  ...sharedProps,
41444
41782
  ref: visibleRef,
41445
- lineClamp: maxRows,
41446
41783
  children: [visibleChildren.length ? visibleChildren : fallback, hiddenCount > 0 && jsx(Badge, {
41447
41784
  className: "navi_badge_more",
41448
41785
  children: naviI18n("badge_list.more", {
@@ -41831,12 +42168,74 @@ const Interpolate = ({
41831
42168
  });
41832
42169
  };
41833
42170
 
42171
+ const Unit = ({
42172
+ unit,
42173
+ plural,
42174
+ lang,
42175
+ size = "smaller",
42176
+ sizeRatio,
42177
+ style,
42178
+ ...props
42179
+ }) => {
42180
+ let resolvedSize = size;
42181
+ let resolvedStyle = style;
42182
+ if (size === "smaller" || sizeRatio !== undefined) {
42183
+ resolvedSize = undefined;
42184
+ const ratio = sizeRatio !== undefined ? sizeRatio : 0.8;
42185
+ resolvedStyle = {
42186
+ fontSize: `calc(${ratio} * 1em)`,
42187
+ ...style
42188
+ };
42189
+ }
42190
+ const isPlural = Boolean(plural);
42191
+ let unitText = unit;
42192
+ const singularText = naviI18n(unit, undefined, {
42193
+ lang
42194
+ });
42195
+ if (singularText !== unit) {
42196
+ // unit is known to naviI18n
42197
+ if (isPlural) {
42198
+ const pluralKey = `${unit}__plural`;
42199
+ const pluralText = naviI18n(pluralKey, undefined, {
42200
+ lang
42201
+ });
42202
+ // fallback to singular if no plural key registered
42203
+ unitText = pluralText !== pluralKey ? pluralText : singularText;
42204
+ } else {
42205
+ unitText = singularText;
42206
+ }
42207
+ } else {
42208
+ // naviI18n has no translation — try Intl.NumberFormat with style:"unit"
42209
+ const intlText = formatIntlUnit(unit, isPlural, lang);
42210
+ if (intlText !== null) {
42211
+ unitText = intlText;
42212
+ }
42213
+ }
42214
+ return jsx(Text, {
42215
+ baseClassName: "navi_unit",
42216
+ size: resolvedSize,
42217
+ style: resolvedStyle,
42218
+ ...props,
42219
+ children: unitText
42220
+ });
42221
+ };
42222
+ const formatIntlUnit = (unit, plural, lang) => {
42223
+ try {
42224
+ const count = plural ? 2 : 1;
42225
+ const parts = new Intl.NumberFormat(lang, {
42226
+ style: "unit",
42227
+ unit,
42228
+ unitDisplay: "long"
42229
+ }).formatToParts(count);
42230
+ const unitPart = parts.find(p => p.type === "unit");
42231
+ return unitPart ? unitPart.value : null;
42232
+ } catch {
42233
+ return null;
42234
+ }
42235
+ };
42236
+
41834
42237
  installImportMetaCssBuild(import.meta);const css$7 = /* css */`
41835
42238
  @layer navi {
41836
- .navi_quantity {
41837
- --unit-color: color-mix(in srgb, currentColor 50%, white);
41838
- --unit-size-ratio: 0.7;
41839
- }
41840
42239
  }
41841
42240
 
41842
42241
  .navi_quantity {
@@ -41854,10 +42253,8 @@ installImportMetaCssBuild(import.meta);const css$7 = /* css */`
41854
42253
  letter-spacing: 0.06em;
41855
42254
  }
41856
42255
  .navi_quantity_body {
41857
- .navi_quantity_unit {
41858
- color: var(--unit-color);
42256
+ .navi_unit {
41859
42257
  font-weight: normal;
41860
- font-size: calc(var(--unit-size-ratio) * 1em);
41861
42258
  }
41862
42259
  }
41863
42260
 
@@ -41879,7 +42276,7 @@ installImportMetaCssBuild(import.meta);const css$7 = /* css */`
41879
42276
  text-align: center;
41880
42277
  }
41881
42278
  .navi_quantity_body {
41882
- .navi_quantity_unit {
42279
+ .navi_unit {
41883
42280
  display: inline-block;
41884
42281
  width: 100%;
41885
42282
  text-align: center;
@@ -41892,6 +42289,9 @@ const Quantity = ({
41892
42289
  children,
41893
42290
  unit,
41894
42291
  unitPosition = "right",
42292
+ unitSize = "smaller",
42293
+ unitSizeRatio,
42294
+ unitColor,
41895
42295
  label,
41896
42296
  size,
41897
42297
  lang,
@@ -41936,50 +42336,20 @@ const Quantity = ({
41936
42336
  children: jsx(LoadingDotsSvg, {})
41937
42337
  }) : valueFormatted
41938
42338
  }), unit && jsx(Unit, {
41939
- value: value,
41940
42339
  unit: unit,
41941
- lang: lang
42340
+ plural: typeof value === "number" ? value > 1 : false,
42341
+ lang: lang,
42342
+ size: unitSize,
42343
+ sizeRatio: unitSizeRatio,
42344
+ color: unitColor
41942
42345
  })]
41943
42346
  })]
41944
42347
  });
41945
42348
  };
41946
42349
  const QuantityPropsCSSVars = {
41947
- unitColor: "--unit-color",
41948
- unitSizeRatio: "--unit-size-ratio"
42350
+ unitColor: "--unit-color"
41949
42351
  };
41950
42352
  const QuantityPseudoClasses = [":hover", ":active", ":read-only", ":disabled", ":-navi-loading"];
41951
- const Unit = ({
41952
- value,
41953
- unit,
41954
- lang
41955
- }) => {
41956
- let unitText = unit;
41957
- if (Array.isArray(unit)) {
41958
- const [singular, plural] = unit;
41959
- unitText = value > 1 ? plural : singular;
41960
- } else {
41961
- const singularText = naviI18n(unit, undefined, {
41962
- lang
41963
- });
41964
- if (singularText !== unit) {
41965
- // unit is known to naviI18n
41966
- if (value > 1) {
41967
- const pluralKey = `${unit}__plural`;
41968
- const pluralText = naviI18n(pluralKey, undefined, {
41969
- lang
41970
- });
41971
- // fallback to singular if no plural key registered
41972
- unitText = pluralText !== pluralKey ? pluralText : singularText;
41973
- } else {
41974
- unitText = singularText;
41975
- }
41976
- }
41977
- }
41978
- return jsx("span", {
41979
- className: "navi_quantity_unit",
41980
- children: unitText
41981
- });
41982
- };
41983
42353
  const parseQuantityValue = children => {
41984
42354
  if (typeof children !== "string") {
41985
42355
  return children;
@@ -43027,5 +43397,5 @@ const UserSvg = () => jsx("svg", {
43027
43397
  })
43028
43398
  });
43029
43399
 
43030
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, Details, Dialog, DialogLayout, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, Paragraph, Picker, Popover, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Thead, Time, Title, Tr, UITransition, UserSvg, ViewportLayout, actionIntegratedVia, actionRunEffect, addCustomMessage, anyMatchingRouteSignal, applySearch, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, installCustomConstraintValidation, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, langSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navTo, naviI18n, openCallout, rawUrlPart, reload, removeCustomMessage, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSidePanelClose, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
43400
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, Details, Dialog, DialogLayout, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputGroup, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, Paragraph, Picker, Popover, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, actionIntegratedVia, actionRunEffect, addCustomMessage, anyMatchingRouteSignal, applySearch, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, installCustomConstraintValidation, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, langSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navTo, naviI18n, openCallout, rawUrlPart, reload, removeCustomMessage, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSidePanelClose, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
43031
43401
  //# sourceMappingURL=jsenv_navi.js.map