@jsenv/navi 0.27.81 → 0.27.83

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.
@@ -1,8 +1,6 @@
1
- /*!
2
- * AI reading this file: this is a generated build artifact (bundled,
3
- * flattened, no source comments). Before using or debugging @jsenv/navi,
4
- * read ../docs/AI_INSTRUCTIONS.md — it explains where the real docs and
5
- * source are, and the key concepts needed to use the API correctly.
1
+ /*
2
+ * AI reading this file: read ../docs/AI_INSTRUCTIONS.md for context on
3
+ * using @jsenv/navi as intended.
6
4
  */
7
5
  import { installImportMetaCssBuild, windowWidthSignal } from "./jsenv_navi_side_effects.js";
8
6
  import { isValidElement, createContext, h, toChildArray, render, Fragment, cloneElement } from "preact";
@@ -18970,6 +18968,17 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-single-space");
18970
18968
  */
18971
18969
 
18972
18970
 
18971
+ // Our own compact/custom duration notation interpolates raw numbers
18972
+ // directly (unlike Intl.DurationFormat, which groups thousands on its own,
18973
+ // e.g. "5 400 secondes") — this keeps that consistent without reimplementing
18974
+ // locale-aware grouping. Falls back to the raw value as-is for a
18975
+ // non-numeric mid-edit value (e.g. "2a"), which Intl.NumberFormat can't
18976
+ // format anyway.
18977
+ const formatCompactNumber = (value, lang) => {
18978
+ const n = Number(value);
18979
+ return Number.isFinite(n) ? new Intl.NumberFormat(lang).format(n) : value;
18980
+ };
18981
+
18973
18982
  /**
18974
18983
  * Formats a date as a human-readable day string.
18975
18984
  *
@@ -19223,15 +19232,20 @@ const formatTime = (date, lang) => {
19223
19232
  * "compact" uses our own notation that omits the minute symbol when hours are present.
19224
19233
  *
19225
19234
  * @param {number} minutes
19226
- * @param {{ lang?: string, format?: "long"|"short"|"narrow"|"compact", alwaysShowHours?: boolean }} [options]
19227
- * @param {boolean} [options.alwaysShowHours=false] - Normally a zero-hours
19228
- * component is dropped entirely (a real 5-minute duration should print as
19229
- * "5 minutes", not "0 hours 5 minutes") — set this to keep it (e.g. "0 h
19230
- * et 5 min"/"0h 5min"/"0h05") instead. Only meaningful when `minutes` is
19231
- * itself less than 60, i.e. the hours component would otherwise be zero;
19232
- * used by `<Time type="time">` for a time-of-day at midnight, where
19233
- * dropping the hour would make it indistinguishable from an actual
19234
- * duration (see time.jsx's own TimeTime).
19235
+ * @param {{ lang?: string, format?: "long"|"short"|"narrow"|"compact", clockStyle?: boolean }} [options]
19236
+ * @param {boolean} [options.clockStyle=false] - Set this when `minutes`
19237
+ * represents a time-of-day rather than a real duration (used by
19238
+ * `<Time type="time">`, see time.jsx's own TimeTime) — affects two
19239
+ * things at once, both consequences of a clock's "0" being a meaningful
19240
+ * hour rather than "no hours":
19241
+ * - a zero-hours component is normally dropped entirely (a real 5-minute
19242
+ * duration should print as "5 minutes", not "0 hours 5 minutes"); this
19243
+ * keeps it instead (e.g. "0 h et 5 min"/"0h 5min"/"00h05") so midnight
19244
+ * doesn't collapse to something indistinguishable from an actual
19245
+ * 5-minute duration.
19246
+ * - `format: "compact"` also zero-pads a single-digit hour to 2 digits
19247
+ * (e.g. "5h30" → "05h30"), so it reads closer to a "05:30" clock.
19248
+ * Must not be set for plain duration formatting.
19235
19249
  *
19236
19250
  * @example
19237
19251
  * formatMinuteDuration(90, { lang: "fr" }) // "1 heure 30 minutes" (long, default)
@@ -19239,20 +19253,21 @@ const formatTime = (date, lang) => {
19239
19253
  * formatMinuteDuration(90, { lang: "fr", format: "narrow" }) // "1h 30min" (Intl narrow)
19240
19254
  * formatMinuteDuration(90, { lang: "fr", format: "compact" }) // "1h30" (custom, no minute symbol)
19241
19255
  * formatMinuteDuration(45, { lang: "en", format: "compact" }) // "45min"
19242
- * formatMinuteDuration(5, { lang: "fr", format: "narrow", alwaysShowHours: true }) // "0h 5min"
19256
+ * formatMinuteDuration(5, { lang: "fr", format: "narrow", clockStyle: true }) // "0h 5min"
19257
+ * formatMinuteDuration(330, { lang: "fr", format: "compact", clockStyle: true }) // "05h30"
19243
19258
  */
19244
19259
  const formatMinuteDuration = (
19245
19260
  minutes,
19246
- { lang = languagesSignal.value, format = "long", alwaysShowHours = false } = {},
19261
+ { lang = languagesSignal.value, format = "long", clockStyle = false } = {},
19247
19262
  ) => {
19248
19263
  const h = Math.floor(minutes / 60);
19249
19264
  const m = minutes % 60;
19250
19265
  if (format !== "compact" && typeof Intl.DurationFormat !== "undefined") {
19251
19266
  const fmt = new Intl.DurationFormat(lang, {
19252
19267
  style: format, // "long", "short", or "narrow"
19253
- ...(alwaysShowHours ? { hoursDisplay: "always" } : {}),
19268
+ ...(clockStyle ? { hoursDisplay: "always" } : {}),
19254
19269
  });
19255
- if (h === 0 && !alwaysShowHours) {
19270
+ if (h === 0 && !clockStyle) {
19256
19271
  return fmt.format({ minutes: m });
19257
19272
  }
19258
19273
  if (m === 0) {
@@ -19263,13 +19278,16 @@ const formatMinuteDuration = (
19263
19278
  // format="compact": "1h30", "45min", "2h" — no minute symbol when hours are present
19264
19279
  const hSym = naviI18n("time.duration.hour_symbol", undefined, { lang });
19265
19280
  const mSym = naviI18n("time.duration.minute_symbol", undefined, { lang });
19266
- if (h === 0 && !alwaysShowHours) {
19281
+ const hStr = clockStyle
19282
+ ? String(h).padStart(2, "0")
19283
+ : formatCompactNumber(h, lang);
19284
+ if (h === 0 && !clockStyle) {
19267
19285
  return `${m}${mSym}`;
19268
19286
  }
19269
19287
  if (m === 0) {
19270
- return `${h}${hSym}`;
19288
+ return `${hStr}${hSym}`;
19271
19289
  }
19272
- return `${h}${hSym}${String(m).padStart(2, "0")}`;
19290
+ return `${hStr}${hSym}${String(m).padStart(2, "0")}`;
19273
19291
  };
19274
19292
 
19275
19293
  /**
@@ -19324,7 +19342,9 @@ const formatSecondDuration = (
19324
19342
  const mSym = naviI18n("time.duration.minute_symbol", undefined, { lang });
19325
19343
  const sSym = naviI18n("time.duration.second_symbol", undefined, { lang });
19326
19344
  const parts = [];
19327
- if (h > 0) parts.push(`${h}${hSym}`);
19345
+ // m/s are always 0-59 by construction (never need grouping); h can be
19346
+ // arbitrarily large for a long duration.
19347
+ if (h > 0) parts.push(`${formatCompactNumber(h, lang)}${hSym}`);
19328
19348
  if (m > 0) parts.push(`${m}${mSym}`);
19329
19349
  if (s > 0 || parts.length === 0) parts.push(`${s}${sSym}`);
19330
19350
  return parts.join("");
@@ -19425,36 +19445,40 @@ const formatDuration = (
19425
19445
  const parts = [];
19426
19446
 
19427
19447
  if (hasNonZero("years")) {
19428
- parts.push(`${duration.years}${sym("year")}`);
19448
+ parts.push(`${formatCompactNumber(duration.years, lang)}${sym("year")}`);
19429
19449
  }
19430
19450
  if (hasNonZero("months")) {
19431
- parts.push(`${duration.months}${sym("month")}`);
19451
+ parts.push(`${formatCompactNumber(duration.months, lang)}${sym("month")}`);
19432
19452
  }
19433
19453
  if (hasNonZero("weeks")) {
19434
- parts.push(`${duration.weeks}${sym("week")}`);
19454
+ parts.push(`${formatCompactNumber(duration.weeks, lang)}${sym("week")}`);
19435
19455
  }
19436
19456
  if (hasNonZero("days")) {
19437
- parts.push(`${duration.days}${sym("day")}`);
19457
+ parts.push(`${formatCompactNumber(duration.days, lang)}${sym("day")}`);
19438
19458
  }
19439
19459
 
19440
- // Hours + minutes: when both present, pad minutes to 2 digits after the h symbol
19460
+ // Hours + minutes: when both present, pad minutes to 2 digits after the h
19461
+ // symbol — minutes stays a plain 2-digit pad (it's always 0-59 by
19462
+ // convention), only hours goes through grouping.
19441
19463
  const hSym = sym("hour");
19442
19464
  const mSym = sym("minute");
19443
19465
  if (hasNonZero("hours") && hasNonZero("minutes")) {
19444
19466
  parts.push(
19445
- `${duration.hours}${hSym}${String(duration.minutes).padStart(2, "0")}`,
19467
+ `${formatCompactNumber(duration.hours, lang)}${hSym}${String(duration.minutes).padStart(2, "0")}`,
19446
19468
  );
19447
19469
  } else if (hasNonZero("hours")) {
19448
- parts.push(`${duration.hours}${hSym}`);
19470
+ parts.push(`${formatCompactNumber(duration.hours, lang)}${hSym}`);
19449
19471
  } else if (hasNonZero("minutes")) {
19450
- parts.push(`${duration.minutes}${mSym}`);
19472
+ parts.push(`${formatCompactNumber(duration.minutes, lang)}${mSym}`);
19451
19473
  }
19452
19474
 
19453
19475
  if (hasNonZero("seconds")) {
19454
- parts.push(`${duration.seconds}${sym("second")}`);
19476
+ parts.push(`${formatCompactNumber(duration.seconds, lang)}${sym("second")}`);
19455
19477
  }
19456
19478
  if (hasNonZero("milliseconds")) {
19457
- parts.push(`${duration.milliseconds}${sym("millisecond")}`);
19479
+ parts.push(
19480
+ `${formatCompactNumber(duration.milliseconds, lang)}${sym("millisecond")}`,
19481
+ );
19458
19482
  }
19459
19483
  return parts.join("") || "0";
19460
19484
  };
@@ -33399,7 +33423,21 @@ const InputTextualWithSuggestions = props => {
33399
33423
  // Nice side effect is that input_group.jsx will see all input is selected
33400
33424
  // and arrow left/right will always nav between inputs.
33401
33425
  // (Otherwise we would prevent left/right + show calllout about readonly)
33426
+ //
33427
+ // Not on touch: .select() there triggers the native mobile text-selection UI
33428
+ // (handles + magnifier), which makes no sense for a picker (opens a
33429
+ // popover/dialog on tap, not meant for text selection) and isn't wanted on a
33430
+ // plain readonly text input either. onPointerDown tracks the pointer type
33431
+ // that initiated the interaction (same convention as button_ui.jsx's own
33432
+ // `e.pointerType !== "touch"` check) so the focus handler — which fires
33433
+ // right after, but as a plain FocusEvent with no pointerType of its own —
33434
+ // can also skip select() for that same interaction.
33402
33435
  const useAutoSelectReadOnly = (props) => {
33436
+ const lastPointerTypeRef = useRef(null);
33437
+ const onPointerDown = (e) => {
33438
+ props.onPointerDown?.(e);
33439
+ lastPointerTypeRef.current = e.pointerType;
33440
+ };
33403
33441
  const onFocus = (e) => {
33404
33442
  props.onFocus(e);
33405
33443
  if (e.defaultPrevented) {
@@ -33408,6 +33446,9 @@ const useAutoSelectReadOnly = (props) => {
33408
33446
  if (!e.target.readOnly) {
33409
33447
  return;
33410
33448
  }
33449
+ if (lastPointerTypeRef.current === "touch") {
33450
+ return;
33451
+ }
33411
33452
  e.preventDefault();
33412
33453
  e.target.select();
33413
33454
  };
@@ -33419,11 +33460,14 @@ const useAutoSelectReadOnly = (props) => {
33419
33460
  if (!e.target.readOnly) {
33420
33461
  return;
33421
33462
  }
33463
+ if (lastPointerTypeRef.current === "touch") {
33464
+ return;
33465
+ }
33422
33466
  e.preventDefault();
33423
33467
  e.target.select();
33424
33468
  };
33425
33469
 
33426
- return { onFocus, onMouseDown };
33470
+ return { onFocus, onMouseDown, onPointerDown };
33427
33471
  };
33428
33472
 
33429
33473
  installImportMetaCssBuild(import.meta);/**
@@ -39976,22 +40020,27 @@ const TimeTime = ({
39976
40020
  // other hour keeps at least its own "N hour(s)" wording as a hint that
39977
40021
  // this is a time-of-day, not a duration — only hour 0 loses that hint
39978
40022
  // entirely.
40023
+ // clockStyle: this is always a time-of-day here, never a duration — keeps
40024
+ // a zero hour instead of dropping it (midnight would otherwise be
40025
+ // indistinguishable from an actual 5-minute duration), and in
40026
+ // format="compact" also zero-pads a single-digit hour so "5h30"/"0h05"
40027
+ // read as "05h30"/"00h05", closer to a "HH:MM" clock.
39979
40028
  let text;
39980
40029
  if (date.getHours() !== 0) {
39981
40030
  text = formatMinuteDuration(totalMinutes, {
39982
40031
  lang,
39983
- format
40032
+ format,
40033
+ clockStyle: true
39984
40034
  });
39985
40035
  } else if (format !== "long") {
39986
40036
  // short/narrow/compact: keep the "0 h"/"0h" hour part instead of
39987
- // dropping it (formatMinuteDuration's own alwaysShowHours) — e.g.
39988
- // "0 h et 5 min"/"0h 5min"/"0h05" rather than substituting a
39989
- // translated "midnight" word, which would look out of place squeezed
39990
- // into these otherwise terse, symbol-based formats.
40037
+ // dropping it — e.g. "0 h et 5 min"/"0h 5min"/"00h05" — rather than
40038
+ // substituting a translated "midnight" word, which would look out of
40039
+ // place squeezed into these otherwise terse, symbol-based formats.
39991
40040
  text = formatMinuteDuration(totalMinutes, {
39992
40041
  lang,
39993
40042
  format,
39994
- alwaysShowHours: true
40043
+ clockStyle: true
39995
40044
  });
39996
40045
  } else {
39997
40046
  const midnightWord = naviI18n("time.midnight", undefined, {
@@ -40006,7 +40055,7 @@ const TimeTime = ({
40006
40055
  text = formatMinuteDuration(totalMinutes, {
40007
40056
  lang,
40008
40057
  format,
40009
- alwaysShowHours: true
40058
+ clockStyle: true
40010
40059
  });
40011
40060
  } else {
40012
40061
  // Swap just the "0 heure(s)" part of the Intl-generated duration