@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.
- package/dist/jsenv_navi.js +89 -40
- package/dist/jsenv_navi.js.map +5 -5
- package/docs/AI_INSTRUCTIONS.md +31 -11
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
* AI reading this file:
|
|
3
|
-
*
|
|
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",
|
|
19227
|
-
* @param {boolean} [options.
|
|
19228
|
-
*
|
|
19229
|
-
*
|
|
19230
|
-
*
|
|
19231
|
-
*
|
|
19232
|
-
*
|
|
19233
|
-
*
|
|
19234
|
-
*
|
|
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",
|
|
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",
|
|
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
|
-
...(
|
|
19268
|
+
...(clockStyle ? { hoursDisplay: "always" } : {}),
|
|
19254
19269
|
});
|
|
19255
|
-
if (h === 0 && !
|
|
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
|
-
|
|
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 `${
|
|
19288
|
+
return `${hStr}${hSym}`;
|
|
19271
19289
|
}
|
|
19272
|
-
return `${
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
39988
|
-
//
|
|
39989
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
40058
|
+
clockStyle: true
|
|
40010
40059
|
});
|
|
40011
40060
|
} else {
|
|
40012
40061
|
// Swap just the "0 heure(s)" part of the Intl-generated duration
|