@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 +4 -0
- package/dist/jsenv_navi.js +243 -87
- package/dist/jsenv_navi.js.map +11 -11
- package/docs/AI_INSTRUCTIONS.md +10 -0
- package/docs/i18n.md +183 -0
- package/docs/navigation.md +41 -0
- package/docs/resource.md +11 -11
- package/package.json +2 -2
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.
|
package/dist/jsenv_navi.js
CHANGED
|
@@ -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
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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
|
-
*
|
|
180
|
-
*
|
|
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
|
|
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
|
|
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
|
|
385
|
-
* only other languages need
|
|
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!"
|
|
391
|
-
* i18n("Hello [name]!", { name: "Alice" }); // "Bonjour Alice !"
|
|
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
|
-
*
|
|
395
|
-
*
|
|
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!"
|
|
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
|
-
* ##
|
|
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
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
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
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
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
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
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
|
-
* //
|
|
657
|
-
* naviI18n.addLangKeys("
|
|
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 (
|
|
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
|
-
*
|
|
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.
|
|
30556
|
+
* source: `itemsAction={GAME.GET_RANGE.bindParams({ radar })}`.
|
|
30420
30557
|
*/
|
|
30421
30558
|
|
|
30422
30559
|
|
|
30423
|
-
const
|
|
30560
|
+
const createRangeReader = (
|
|
30424
30561
|
actionName,
|
|
30425
30562
|
callback,
|
|
30426
30563
|
{ store, params: boundParams },
|
|
30427
30564
|
) => {
|
|
30428
|
-
const
|
|
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
|
|
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(
|
|
30454
|
-
|
|
30455
|
-
|
|
30456
|
-
return
|
|
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
|
|
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
|
|
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
|
-
* -
|
|
30660
|
+
* - GET_RANGE → `{ items, start, count }`, one slice of the collection
|
|
30524
30661
|
*
|
|
30525
|
-
* `
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
31707
|
-
// A
|
|
31708
|
-
// (see
|
|
31709
|
-
stateFacade.
|
|
31710
|
-
`${name}.
|
|
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
|
|
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
|
|
54243
|
-
* `itemsAction={GAME.
|
|
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
|
|
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
|
|
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
|
-
*
|
|
65869
|
-
* Returns a plain string when all replacements are
|
|
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="
|
|
65876
|
-
*
|
|
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
|