@jsenv/navi 0.29.26 → 0.29.28

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/README.md CHANGED
@@ -29,6 +29,10 @@ between related actions. Parent/child relations are first-class — `.one`,
29
29
 
30
30
  **`Text`** and related components (`Title`, `Paragraph`, `Code`, `Caption`) handle typography consistently across the app.
31
31
 
32
+ ## Texts & i18n
33
+
34
+ `interpolateText`/`Interpolate` keep a sentence containing values readable as one string, and `createI18n` gives the app a single place holding its wording — worth it even with one language. `naviI18n` is where navi's own texts (validation messages, button labels, relative time) can be overridden or translated. See [docs/i18n.md](./docs/i18n.md).
35
+
32
36
  ## Icons
33
37
 
34
38
  Icons are a piece that is often missing or painful in web projects. The `Icon` component makes icons behave like text — they scale with font size, inherit color, and align naturally in any layout. No sizing hacks, no SVG wrangling.
@@ -167,17 +167,39 @@ const requestConfirmation = async ({ message, content, anchor }) => {
167
167
  };
168
168
 
169
169
  /**
170
- * Interpolates a template string, replacing [key] placeholders with values.
171
- * Values can be strings or JSX elements (when allowJsx is true).
172
- * Returns a plain string when all replacements are strings, or a Preact
173
- * fragment when JSX values are present and allowJsx is enabled.
170
+ * Interpolates a template string, replacing `[key]` placeholders with values.
171
+ *
172
+ * Usable on its own no i18n instance required whenever a sentence should
173
+ * stay readable as one string instead of being cut into JSX expressions or
174
+ * concatenations. `<Interpolate>` is the JSX form of this function, and
175
+ * `createI18n` runs every translation through it. See `docs/i18n.md`.
174
176
  *
175
177
  * `[]` was chosen as the placeholder delimiter (rather than `{}` or `{{}}`)
176
178
  * because it does not conflict with JSX syntax, JavaScript template literals,
177
179
  * or common punctuation in translated strings.
178
180
  *
179
- * Pass `allowJsx: true` to enable VNode replacements (used by <Interpolate>).
180
- * Without it, all values are coerced to strings.
181
+ * @param {string} template
182
+ * e.g. `"Hello [name], you have [count] messages"`. A non-string is returned
183
+ * untouched, as is any template when `replacements` is missing.
184
+ * @param {object} [replacements]
185
+ * Values keyed by placeholder name. A key can be:
186
+ * - a direct name — `[name]` ← `{ name: "Alice" }`
187
+ * - a dot-path — `[item.label]` ← `{ item: { label: "Book" } }` (a literal
188
+ * `"item.label"` key wins over the path)
189
+ *
190
+ * A value that is a function is called at that point, so an expensive or
191
+ * lazily-known replacement is only computed when the placeholder is actually
192
+ * present in this language's template.
193
+ *
194
+ * A placeholder with no matching value is left in the output as-is
195
+ * (`"[name]"`), making the gap visible rather than silently empty.
196
+ * @param {object} [options]
197
+ * @param {boolean} [options.allowJsx=false]
198
+ * Allow VNode replacements (what `<Interpolate>` passes). Without it, a VNode
199
+ * value warns and is coerced to a string.
200
+ * @returns {string|import("preact").VNode}
201
+ * A plain string when every replacement is a string, a Preact fragment when
202
+ * at least one VNode was interpolated with `allowJsx`.
181
203
  */
182
204
  const interpolateText = (
183
205
  template,
@@ -288,8 +310,7 @@ if (typeof window !== "undefined") {
288
310
  * because that happens to be the browser's or the user's own preference.
289
311
  *
290
312
  * `null` (the default) means no restriction at all: every language the
291
- * browser/user prefers is allowed through, matching this module's previous,
292
- * unrestricted behavior.
313
+ * browser/user prefers is allowed through.
293
314
  */
294
315
  const supportedLanguagesSignal = signal(null);
295
316
 
@@ -376,30 +397,45 @@ const languagesSignal = computed(() => {
376
397
  });
377
398
 
378
399
  /**
379
- * Creates a lightweight i18n instance for translating text in the current locale.
400
+ * Creates a lightweight i18n instance: a central place where an app declares
401
+ * its texts once and reads them back translated into the active language.
402
+ *
403
+ * Worth using even in a single-language app — one registry beats strings
404
+ * scattered across components, and adding a second language later becomes a
405
+ * data change instead of a refactor. See `docs/i18n.md` for how to choose
406
+ * between the two key styles below and how this relates to `naviI18n`.
380
407
  *
381
408
  * @param {object} [options]
382
409
  * @param {string} [options.keyLang]
383
410
  * When set, each key also serves as its own translation for `keyLang`.
384
- * This allows writing keys directly in that language (typically English) so
385
- * only other languages need to be registered:
411
+ * This allows writing keys directly in that language (typically the language
412
+ * the app is written in) so only *other* languages need registering:
386
413
  *
387
414
  * ```js
388
415
  * const i18n = createI18n({ keyLang: "en" });
389
416
  * i18n.add("Hello [name]!", { fr: "Bonjour [name] !" });
390
- * i18n("Hello [name]!", { name: "Alice" }); // "Hello Alice!" (en — key is template)
391
- * i18n("Hello [name]!", { name: "Alice" }); // "Bonjour Alice !" (fr)
417
+ * i18n("Hello [name]!", { name: "Alice" }, { lang: "en" }); // "Hello Alice!"
418
+ * i18n("Hello [name]!", { name: "Alice" }, { lang: "fr" }); // "Bonjour Alice !"
392
419
  * ```
393
420
  *
394
- * Without `keyLang`, keys are opaque identifiers and all languages (including
395
- * the fallback) must be registered explicitly:
421
+ * `keyLang` only applies to keys passed to `add()`/`addAll()`; a key never
422
+ * registered stays opaque and comes back as-is.
423
+ *
424
+ * Without `keyLang`, keys are opaque identifiers and every language
425
+ * (including the one the app was written in) must be registered explicitly:
396
426
  *
397
427
  * ```js
398
428
  * const i18n = createI18n();
399
429
  * i18n.add("greeting", { en: "Hello [name]!", fr: "Bonjour [name] !" });
400
- * i18n("greeting", { name: "Alice" }); // "Hello Alice!" (en)
430
+ * i18n("greeting", { name: "Alice" }, { lang: "en" }); // "Hello Alice!"
401
431
  * ```
402
432
  *
433
+ * @param {string} [options.fallbackLang]
434
+ * Language consulted when the active language has no translation for a key
435
+ * — per key, not per language: a partially translated language falls through
436
+ * to `fallbackLang` only for the keys it is missing. Without it, a missing
437
+ * translation returns the key itself.
438
+ *
403
439
  * @param {string|string[]} [options.runtimeLang]
404
440
  * The active language (BCP 47 tag or ordered array of tags) — named
405
441
  * "runtime" rather than "system" because there is no actual access to the
@@ -413,7 +449,7 @@ const languagesSignal = computed(() => {
413
449
  *
414
450
  * ---
415
451
  *
416
- * ## Bulk registration
452
+ * ## Registration
417
453
  *
418
454
  * **`i18n.add(key, { lang: "translation" })`** — one key, multiple languages.
419
455
  *
@@ -422,18 +458,31 @@ const languagesSignal = computed(() => {
422
458
  * **`i18n.addLangKeys(lang, { key: "translation", ... })`** — full language pack
423
459
  * (useful when loading a JSON translation file).
424
460
  *
461
+ * All three accumulate: registering a key that already exists overwrites that
462
+ * one key and leaves the rest of the language untouched. This is what lets an
463
+ * app override a single built-in navi text without redeclaring the others.
464
+ *
425
465
  * A regional variant (e.g. `"fr-CA"`) automatically inherits all keys from its
426
466
  * parent (`"fr"`) that it does not explicitly override:
427
467
  * ```js
428
468
  * i18n.addLangKeys("fr", { hello: "Bonjour !" });
429
469
  * i18n.addLangKeys("fr-CA", { hello: "Allo !" }); // other "fr" keys inherited
430
470
  * ```
471
+ * Inheritance is resolved at registration time, so register the parent first.
431
472
  *
432
473
  * ---
433
474
  *
434
- * @returns {Function & { add, addAll, addLangKeys, format, languageMap }}
435
- * A callable function — `i18n(key, values?, { lang? })` — with the same
436
- * signature as `i18n.format()`. `format` is kept as an alias.
475
+ * ## Reading
476
+ *
477
+ * **`i18n(key, values?, { lang? })`** the translation for `key`, with
478
+ * `[placeholder]` occurrences replaced from `values` (see `interpolateText`).
479
+ * Returns `key` itself when nothing matches, so an untranslated string still
480
+ * renders something readable. `i18n.format` is an alias of this call.
481
+ *
482
+ * **`i18n.has(key, { lang? })`** — whether a translation genuinely exists,
483
+ * i.e. how to tell "no translation" apart from "translation equal to the key".
484
+ *
485
+ * @returns {Function & { add, addAll, addLangKeys, has, format, languageMap }}
437
486
  */
438
487
  const createI18n = ({ keyLang, fallbackLang, runtimeLang } = {}) => {
439
488
  const languageMap = new Map();
@@ -442,12 +491,10 @@ const createI18n = ({ keyLang, fallbackLang, runtimeLang } = {}) => {
442
491
  // resolve to, so it's what invalidates their own small caches.
443
492
  let languageMapVersion = 0;
444
493
 
445
- // Explicit runtimeLang stays fixed for this instance's lifetime (matches
446
- // the previous behavior exactly). Without one, re-read languagesSignal.value
447
- // fresh on every call instead of freezing it here via languagesSignal.peek()
448
- // once that would silently ignore setPreferredLanguage()/
449
- // setSupportedLanguages() (see lang_signal.js) for the rest of this
450
- // instance's life.
494
+ // Without an explicit runtimeLang, languagesSignal.value is re-read fresh on
495
+ // every call rather than frozen here via languagesSignal.peek() — freezing it
496
+ // would silently ignore setPreferredLanguage()/setSupportedLanguages() (see
497
+ // lang_signal.js) for the rest of this instance's life.
451
498
  const hasExplicitRuntimeLang = runtimeLang !== undefined;
452
499
 
453
500
  // matchBestLang does real work (a Map lookup per candidate, a possible
@@ -622,39 +669,49 @@ const matchBestLang = (lang, languageMap) => {
622
669
  };
623
670
 
624
671
  /**
625
- * The shared i18n instance for all @jsenv/navi components.
626
- *
627
- * Use `naviI18n.add(key, { lang: "translation" })` to register or override
628
- * any text used by navi components. The active language is read from
629
- * `languagesSignal` (see lang_signal.js combines the browser's own
630
- * `navigator.languages`, an optional `setPreferredLanguage()` user override,
631
- * and an optional `setSupportedLanguages()` app-wide allow-list), live on
632
- * every lookup.
633
- *
634
- * Built-in keys (can be overridden):
635
- * - `"time.less_than_minute"` e.g. "in less than a minute"
636
- * - `"time.ongoing"` — e.g. "Ongoing"
637
- * - `"time.tomorrow_at"` e.g. "[day] at [time]" ([day] and [time] are placeholders)
638
- * - `"time.midnight"` e.g. "midnight"
672
+ * The shared i18n instance holding every text @jsenv/navi components display
673
+ * on their own — validation messages, button labels, empty-list messages,
674
+ * relative time wording…
675
+ *
676
+ * It is navi's texts, not the application's: an app registers its own texts in
677
+ * its own `createI18n()` instance and reaches for `naviI18n` only to change
678
+ * what navi itself says, or to add a language navi does not ship. Keys here are
679
+ * opaque identifiers (`"list.empty"`), never the English sentence — the
680
+ * opposite of what an app is advised to do. `docs/i18n.md` explains why.
681
+ *
682
+ * The active language is read from `languagesSignal` (see lang_signal.js —
683
+ * combines the browser's own `navigator.languages`, an optional
684
+ * `setPreferredLanguage()` user override, and an optional
685
+ * `setSupportedLanguages()` app-wide allow-list), live on every lookup.
686
+ *
687
+ * Built-in key namespaces, all overridable — the registrations below are the
688
+ * exhaustive list, read them to find the exact key to override:
689
+ * - `"button.*"` — Clear, Reset, Send, Open, Close, Cancel, Confirm…
690
+ * - `"time.*"` — relative time wording, duration unit symbols, date field placeholders
691
+ * - `"spin.*"` — the ends of a steppable range
692
+ * - `"list.*"` — empty/no-match/failed-rows messages
693
+ * - `"badge_list.*"` — the "+[count] more" overflow badge
694
+ * - `"constraint.*"` — every field validation message
695
+ *
696
+ * Unit names get two derived keys, both optional: `<unit>__plural` and
697
+ * `<unit>__short`. `<Unit>`/`<Quantity>` fall back to the singular when the
698
+ * derived key is missing, and to `Intl.NumberFormat` when the unit itself is
699
+ * not registered at all — so only units Intl gets wrong need registering.
639
700
  *
640
701
  * @example
641
702
  * import { naviI18n } from "@jsenv/navi";
642
703
  *
643
- * // Register unit translations for Quantity:
644
- * naviI18n.add("minute", { en: "minute", fr: "minute" });
645
- * naviI18n.add("minute__plural", { en: "minutes", fr: "minutes" });
646
- *
647
- * // Register multiple keys at once:
648
- * naviI18n.addAll({
649
- * minute: { en: "minute", fr: "minute" },
650
- * minute__plural: { en: "minutes", fr: "minutes" },
651
- * });
652
- *
653
704
  * // Override a built-in text:
654
705
  * naviI18n.add("time.ongoing", { fr: "En cours…" });
655
706
  *
656
- * // Load a full language pack at once:
657
- * naviI18n.addLangKeys("fr", { minute: "minute", "minute__plural": "minutes" });
707
+ * // Teach navi a language it does not ship:
708
+ * naviI18n.addLangKeys("ja", { "list.empty": "項目がありません。" });
709
+ *
710
+ * // Register unit translations used by <Quantity>/<Unit>:
711
+ * naviI18n.addAll({
712
+ * ticket: { en: "ticket", fr: "billet" },
713
+ * ticket__plural: { en: "tickets", fr: "billets" },
714
+ * });
658
715
  */
659
716
  const naviI18n = createI18n();
660
717
 
@@ -2775,6 +2832,32 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2775
2832
  return false;
2776
2833
  }
2777
2834
 
2835
+ // Descending into a child path param means "this URL keeps the value you
2836
+ // are on" — right for a param that QUALIFIES a position (a tab, a mode),
2837
+ // wrong for one that NAMES a page. Two things must both hold for it to be
2838
+ // a name:
2839
+ //
2840
+ // - literal routes are declared for its other values ("/games/me/done").
2841
+ // Declaring them is the developer saying these values are places; a tab
2842
+ // nobody named a route after stays a qualifier;
2843
+ // - it has a default value, which makes THIS url the url of that default:
2844
+ // "/games/me" IS section=a-venir. Descending would leave the default
2845
+ // unaddressable — two states, one url. Without a default ("/map" is not
2846
+ // a panel, it is the absence of one) this url means nothing yet and
2847
+ // stays free to remember where you were.
2848
+ const thisUrlAlreadyMeansAParamValue = (connection) => {
2849
+ if (connection.paramType !== "path") {
2850
+ return false;
2851
+ }
2852
+ if (pathConnectionMap.has(connection.paramName)) {
2853
+ return false; // we carry that param ourselves, we are not its default
2854
+ }
2855
+ if (!connection.namedByLiteralRoutes) {
2856
+ return false;
2857
+ }
2858
+ return connection.getDefaultValue() !== undefined;
2859
+ };
2860
+
2778
2861
  // Check if child has active non-default signal values
2779
2862
  let hasActiveParams = false;
2780
2863
  const childParams = { ...compatibility.childParams };
@@ -2801,7 +2884,10 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
2801
2884
  if (signalValue !== undefined) {
2802
2885
  // No explicit override - use signal value
2803
2886
  childParams[paramName] = signalValue;
2804
- if (connection.isCustomValue(signalValue)) {
2887
+ if (
2888
+ connection.isCustomValue(signalValue) &&
2889
+ !thisUrlAlreadyMeansAParamValue(connection)
2890
+ ) {
2805
2891
  hasActiveParams = true;
2806
2892
  }
2807
2893
  }
@@ -5323,6 +5409,57 @@ const setupRoutePatterns = (routePatterns) => {
5323
5409
  collectDescendantPathSignals(routePattern);
5324
5410
  routePattern.descendantPathSignals = descendantPathSignalsByIndex;
5325
5411
  }
5412
+ // Phase 5b: Flag path params whose values are ALSO declared as literal routes
5413
+ // ("/games/me/done" next to "/games/me/:section"). That declaration is the
5414
+ // only reliable statement that the param names pages rather than qualifying
5415
+ // one — read by shouldUseChildRoute to decide whether an ancestor url may
5416
+ // descend into it.
5417
+ for (const routePattern of routePatternSet) {
5418
+ for (const connection of routePattern.connections) {
5419
+ if (connection.paramType !== "path") {
5420
+ continue;
5421
+ }
5422
+ const paramSegment = routePattern.pattern.segments.find(
5423
+ (seg) => seg.type === "param" && seg.name === connection.paramName,
5424
+ );
5425
+ if (!paramSegment) {
5426
+ continue;
5427
+ }
5428
+ const { index } = paramSegment;
5429
+ const sharesPathUpTo = (otherSegments) => {
5430
+ for (let i = 0; i < index; i++) {
5431
+ const seg = routePattern.pattern.segments[i];
5432
+ const otherSeg = otherSegments[i];
5433
+ if (!otherSeg) {
5434
+ return false;
5435
+ }
5436
+ if (seg.type === "literal" && otherSeg.type === "literal") {
5437
+ if (seg.value !== otherSeg.value) {
5438
+ return false;
5439
+ }
5440
+ } else if (seg.type !== otherSeg.type) {
5441
+ return false;
5442
+ }
5443
+ }
5444
+ return true;
5445
+ };
5446
+ for (const otherPattern of routePatternSet) {
5447
+ if (otherPattern === routePattern) {
5448
+ continue;
5449
+ }
5450
+ const otherSegments = otherPattern.pattern.segments;
5451
+ const otherSegment = otherSegments[index];
5452
+ if (!otherSegment || otherSegment.type !== "literal") {
5453
+ continue;
5454
+ }
5455
+ if (!sharesPathUpTo(otherSegments)) {
5456
+ continue;
5457
+ }
5458
+ connection.namedByLiteralRoutes = true;
5459
+ break;
5460
+ }
5461
+ }
5462
+ }
5326
5463
  // Phase 6: Calculate depths for all patterns
5327
5464
  for (const routePattern of routePatternSet) {
5328
5465
  calculatePatternDepth(routePattern);
@@ -30400,7 +30537,7 @@ const getParamScope = (params) => {
30400
30537
  };
30401
30538
 
30402
30539
  /*
30403
- * GET_PAGE: reading a resource one slice at a time, for a list that draws its
30540
+ * GET_RANGE: reading a resource one slice at a time, for a list that draws its
30404
30541
  * rows as it goes (`<List.Items itemsAction>`).
30405
30542
  *
30406
30543
  * It is on purpose not an action. An action keeps the one response it got and
@@ -30416,16 +30553,16 @@ const getParamScope = (params) => {
30416
30553
  * list is holding is the one it was given.
30417
30554
  *
30418
30555
  * The reader is a function, so a list feeds on it the way it feeds on any other
30419
- * source: `itemsAction={GAME.GET_PAGE.bindParams({ radar })}`.
30556
+ * source: `itemsAction={GAME.GET_RANGE.bindParams({ radar })}`.
30420
30557
  */
30421
30558
 
30422
30559
 
30423
- const createPageReader = (
30560
+ const createRangeReader = (
30424
30561
  actionName,
30425
30562
  callback,
30426
30563
  { store, params: boundParams },
30427
30564
  ) => {
30428
- const readPage = async (range = {}) => {
30565
+ const readRange = async (range = {}) => {
30429
30566
  const { signal, ...rangeParams } = range;
30430
30567
  const paramsResolved = { ...resolveParams(boundParams), ...rangeParams };
30431
30568
  const result = await callback(paramsResolved, { signal });
@@ -30440,7 +30577,7 @@ const createPageReader = (
30440
30577
  const startAsked = rangeParams.start;
30441
30578
  if (startAsked === undefined || startAsked < 0) {
30442
30579
  throw new TypeError(
30443
- `${actionName} must say where the page lands (start), it was asked for ${describeRangeAsked(rangeParams)}.`,
30580
+ `${actionName} must say where the range lands (start), it was asked for ${describeRangeAsked(rangeParams)}.`,
30444
30581
  );
30445
30582
  }
30446
30583
  start = startAsked;
@@ -30450,19 +30587,19 @@ const createPageReader = (
30450
30587
  }
30451
30588
  return { items, start, count };
30452
30589
  };
30453
- Object.defineProperty(readPage, "name", { value: actionName });
30454
- readPage.isPageReader = true;
30455
- readPage.bindParams = (paramsToBind) => {
30456
- return createPageReader(actionName, callback, {
30590
+ Object.defineProperty(readRange, "name", { value: actionName });
30591
+ readRange.isRangeReader = true;
30592
+ readRange.bindParams = (paramsToBind) => {
30593
+ return createRangeReader(actionName, callback, {
30457
30594
  store,
30458
30595
  params: boundParams ? { ...boundParams, ...paramsToBind } : paramsToBind,
30459
30596
  });
30460
30597
  };
30461
- return readPage;
30598
+ return readRange;
30462
30599
  };
30463
30600
 
30464
30601
  // Params bound to a reader may be signals (the radar currently on screen); the
30465
- // value they hold when the page is asked for is the one the page is about.
30602
+ // value they hold when the range is asked for is the one the range is about.
30466
30603
  const resolveParams = (params) => {
30467
30604
  if (!params) {
30468
30605
  return {};
@@ -30520,18 +30657,18 @@ const debug$2 = (args) => {
30520
30657
  * - GET / POST / PUT / PATCH → the full item object, e.g. `{ id, name }`
30521
30658
  * - DELETE → the id or `{ id }` of the removed item
30522
30659
  * - GET_MANY / POST_MANY / … → an array of item objects
30523
- * - GET_PAGE → `{ items, start, count }`, one slice of the collection
30660
+ * - GET_RANGE → `{ items, start, count }`, one slice of the collection
30524
30661
  *
30525
- * `GET_PAGE` is a reader rather than an action: it keeps no value and takes no place in
30662
+ * `GET_RANGE` is a reader rather than an action: it keeps no value and takes no place in
30526
30663
  * the rerun graph, so a `<List.Items>` can feed on it slice by slice
30527
- * (`itemsAction={USER.GET_PAGE.bindParams({ team })}`).
30664
+ * (`itemsAction={USER.GET_RANGE.bindParams({ team })}`).
30528
30665
  *
30529
30666
  * A sub-resource of the backend (`/games/:id/candidates`) must be modelled with a
30530
30667
  * relationship method, never as an `op`/`type` discriminator dispatched inside one
30531
30668
  * verb's callback.
30532
30669
  *
30533
30670
  * @param {string} name - resource name, used in action names and error messages
30534
- * @param {Object} restCallbacks - `{ idKey, uniqueKeys, rerunOn, dependencies, GET, GET_MANY, GET_PAGE, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
30671
+ * @param {Object} restCallbacks - `{ idKey, uniqueKeys, rerunOn, dependencies, GET, GET_MANY, GET_RANGE, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
30535
30672
  * @param {string} [restCallbacks.idKey] - primary key property, defaults to `"id"` (or the first `uniqueKeys` entry)
30536
30673
  * @param {string[]} [restCallbacks.uniqueKeys] - alternate keys the store can find an item by (e.g. `"username"`); a callback may return a different `id` to rename the item's primary key
30537
30674
  * @see docs/resource.md — relationships, callback return contracts, decision table
@@ -30555,7 +30692,7 @@ const resource = (
30555
30692
 
30556
30693
  GET,
30557
30694
  GET_MANY,
30558
- GET_PAGE,
30695
+ GET_RANGE,
30559
30696
  POST,
30560
30697
  POST_MANY,
30561
30698
  PUT,
@@ -30621,7 +30758,7 @@ const resource = (
30621
30758
  restCallbacks: {
30622
30759
  GET,
30623
30760
  GET_MANY,
30624
- GET_PAGE,
30761
+ GET_RANGE,
30625
30762
  POST,
30626
30763
  POST_MANY,
30627
30764
  PUT,
@@ -31703,11 +31840,11 @@ ${originalActionName} source location: ${locationInfo}`,
31703
31840
  if (restCallback === undefined) {
31704
31841
  continue;
31705
31842
  }
31706
- if (restCallbackKey === "GET_PAGE") {
31707
- // A page is read, never kept: no action, no place in the rerun graph
31708
- // (see resource_page_reader.js).
31709
- stateFacade.GET_PAGE = createPageReader(
31710
- `${name}.GET_PAGE`,
31843
+ if (restCallbackKey === "GET_RANGE") {
31844
+ // A range is read, never kept: no action, no place in the rerun graph
31845
+ // (see resource_range_reader.js).
31846
+ stateFacade.GET_RANGE = createRangeReader(
31847
+ `${name}.GET_RANGE`,
31711
31848
  restCallback,
31712
31849
  { store, params },
31713
31850
  );
@@ -54234,15 +54371,15 @@ const VISIBILITY_HIDDEN_STYLE = {
54234
54371
  * before it knows how many there are), `limit` (how many rows), and
54235
54372
  * `before`/`after`/`around` (the id of a row to count from, for a source
54236
54373
  * paginating by cursor). Answer with the rows (an array — that is all of
54237
- * them), or with a page the way a Content-Range does: `{ items, start, count }`
54374
+ * them), or with a range the way a Content-Range does: `{ items, start, count }`
54238
54375
  * — these rows, at this place, out of that many. May be async. The range also
54239
54376
  * carries a `signal`, aborted when the list stops wanting those rows (the
54240
54377
  * window has moved on) — pass it to fetch to call the request off.
54241
54378
  *
54242
- * A resource answers through its page reader:
54243
- * `itemsAction={GAME.GET_PAGE.bindParams({ radar })}` — the rows are upserted
54379
+ * A resource answers through its range reader:
54380
+ * `itemsAction={GAME.GET_RANGE.bindParams({ radar })}` — the rows are upserted
54244
54381
  * into the store on their way in, so the list draws store items rather than
54245
- * copies of the JSON. The list holds the pages, the store holds the objects
54382
+ * copies of the JSON. The list holds the slices, the store holds the objects
54246
54383
  * (see docs/resource.md).
54247
54384
  *
54248
54385
  * A collection held in memory answers synchronously: `itemsAction={() => rows}`.
@@ -54777,7 +54914,7 @@ const useItemStore = ({
54777
54914
  let result;
54778
54915
  try {
54779
54916
  if (typeof itemsAction !== "function") {
54780
- throw new TypeError(`itemsAction must be a function, received ${itemsAction}. A resource feeds a list through its page reader: itemsAction={RESOURCE.GET_PAGE.bindParams(...)} — its other actions keep one response and cannot answer a range.`);
54917
+ throw new TypeError(`itemsAction must be a function, received ${itemsAction}. A resource feeds a list through its range reader: itemsAction={RESOURCE.GET_RANGE.bindParams(...)} — its other actions keep one response and cannot answer a range.`);
54781
54918
  }
54782
54919
  result = itemsAction(range);
54783
54920
  } catch (e) {
@@ -65864,16 +66001,35 @@ const CodeBox = ({
65864
66001
  */
65865
66002
 
65866
66003
  /**
65867
- * Renders a template string with [key] placeholders replaced by props.
65868
- * Replacement values can be strings or JSX elements.
65869
- * Returns a plain string when all replacements are strings, a fragment otherwise.
66004
+ * Renders a template string with `[key]` placeholders replaced by props.
66005
+ * Every prop other than `children` is a replacement value; values can be
66006
+ * strings or JSX elements. Returns a plain string when all replacements are
66007
+ * strings, a fragment otherwise.
65870
66008
  *
65871
66009
  * Keeps the full sentence readable in one place and makes the string
65872
- * i18n-ready, since the template contains no JSX expressions.
66010
+ * i18n-ready, since the template contains no JSX expressions. `children` must
66011
+ * be a plain string for interpolation to happen.
66012
+ *
66013
+ * Placeholder resolution (dot-paths, function values, unmatched placeholders
66014
+ * left visible) is `interpolateText`'s — read its JSDoc for the details, and
66015
+ * `docs/i18n.md` for how this fits with `naviI18n`/`createI18n`.
65873
66016
  *
65874
66017
  * @example
65875
- * <Interpolate radiusKm={<Text bold>50 km</Text>} zoneName="votre zone">
65876
- * Données limitées à [radiusKm] autour de [zoneName].
66018
+ * <Interpolate radiusKm={<Text bold>50 km</Text>} zoneName="your area">
66019
+ * Data limited to [radiusKm] around [zoneName].
66020
+ * </Interpolate>
66021
+ *
66022
+ * @example
66023
+ * // translated template: the sentence comes from i18n, the JSX from here.
66024
+ * // With keyLang, the key is the sentence itself — same string as above, so
66025
+ * // moving a hardcoded <Interpolate> to i18n is wrapping it in a call.
66026
+ * const i18n = createI18n({ keyLang: "en" });
66027
+ * i18n.add("Data limited to [radiusKm] around [zoneName].", {
66028
+ * fr: "Données limitées à [radiusKm] autour de [zoneName].",
66029
+ * });
66030
+ *
66031
+ * <Interpolate radiusKm={<Text bold>50 km</Text>} zoneName="your area">
66032
+ * {i18n("Data limited to [radiusKm] around [zoneName].")}
65877
66033
  * </Interpolate>
65878
66034
  */
65879
66035
  const Interpolate = ({
@@ -67434,5 +67590,5 @@ const UserSvg = () => jsx("svg", {
67434
67590
  })
67435
67591
  });
67436
67592
 
67437
- export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineNaviConfirmPopupOptions, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
67593
+ export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, coarsePointerSignal, compareTwoJsValues, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineNaviConfirmPopupOptions, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, languagesSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setPreferredLanguage, setSupportedLanguages, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, usePopupMode, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
67438
67594
  //# sourceMappingURL=jsenv_navi.js.map