@jsenv/navi 0.29.156 → 0.29.157

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.
@@ -888,6 +888,157 @@ const isPlainObject$2 = obj => {
888
888
  return Object.getPrototypeOf(obj) === proto || Object.getPrototypeOf(obj) === null;
889
889
  };
890
890
 
891
+ const isSignal = (value) => {
892
+ return getSignalType(value) !== null;
893
+ };
894
+
895
+ const BRAND_SYMBOL = Symbol.for("preact-signals");
896
+ const getSignalType = (value) => {
897
+ if (!value || typeof value !== "object") {
898
+ return null;
899
+ }
900
+
901
+ if (value.brand !== BRAND_SYMBOL) {
902
+ return null;
903
+ }
904
+
905
+ if (typeof value._fn === "function") {
906
+ return "computed";
907
+ }
908
+
909
+ return "signal";
910
+ };
911
+
912
+ const MAX_ENTRIES = 5;
913
+
914
+ const stringifyForDisplay = (
915
+ value,
916
+ maxDepth = 2,
917
+ currentDepth = 0,
918
+ options = {},
919
+ ) => {
920
+ const { asFunctionArgs = false } = options;
921
+ const indent = " ".repeat(currentDepth);
922
+ const nextIndent = " ".repeat(currentDepth + 1);
923
+
924
+ if (currentDepth >= maxDepth) {
925
+ return typeof value === "object" && value !== null
926
+ ? "[Object]"
927
+ : String(value);
928
+ }
929
+
930
+ if (value === null) {
931
+ return "null";
932
+ }
933
+ if (value === undefined) {
934
+ return "undefined";
935
+ }
936
+ if (typeof value === "string") {
937
+ return `"${value}"`;
938
+ }
939
+ if (typeof value === "number" || typeof value === "boolean") {
940
+ return String(value);
941
+ }
942
+ if (typeof value === "function") {
943
+ return `[Function ${value.name || "anonymous"}]`;
944
+ }
945
+ if (value instanceof Date) {
946
+ return `Date(${value.toISOString()})`;
947
+ }
948
+ if (value instanceof RegExp) {
949
+ return value.toString();
950
+ }
951
+
952
+ if (Array.isArray(value)) {
953
+ const openBracket = asFunctionArgs ? "(" : "[";
954
+ const closeBracket = asFunctionArgs ? ")" : "]";
955
+
956
+ if (value.length === 0) return `${openBracket}${closeBracket}`;
957
+
958
+ // Display arrays with only one element on a single line
959
+ if (value.length === 1) {
960
+ const item = stringifyForDisplay(
961
+ value[0],
962
+ maxDepth,
963
+ currentDepth + 1,
964
+ // Remove asFunctionArgs for nested calls
965
+ { ...options, asFunctionArgs: false },
966
+ );
967
+ return `${openBracket}${item}${closeBracket}`;
968
+ }
969
+
970
+ if (value.length > MAX_ENTRIES) {
971
+ const preview = value
972
+ .slice(0, MAX_ENTRIES)
973
+ .map(
974
+ (v) =>
975
+ `${nextIndent}${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
976
+ );
977
+ return `${openBracket}\n${preview.join(",\n")},\n${nextIndent}...${value.length - MAX_ENTRIES} more\n${indent}${closeBracket}`;
978
+ }
979
+
980
+ const items = value.map(
981
+ (v) =>
982
+ `${nextIndent}${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
983
+ );
984
+ return `${openBracket}\n${items.join(",\n")}\n${indent}${closeBracket}`;
985
+ }
986
+
987
+ if (typeof value === "object") {
988
+ const signalType = getSignalType(value);
989
+ if (signalType) {
990
+ const signalValue = value.peek();
991
+ const prefix = signalType === "computed" ? "computed" : "signal";
992
+ return `${prefix}(${stringifyForDisplay(signalValue, maxDepth, currentDepth, { ...options, asFunctionArgs: false })})`;
993
+ }
994
+
995
+ const entries = Object.entries(value);
996
+ if (entries.length === 0) return "{}";
997
+
998
+ // ✅ Inclure les clés avec valeurs undefined/null
999
+ const allEntries = [];
1000
+ for (const [key, val] of entries) {
1001
+ allEntries.push([key, val]);
1002
+ }
1003
+
1004
+ // Ajouter les clés avec undefined (que Object.entries omet)
1005
+ const descriptor = Object.getOwnPropertyDescriptors(value);
1006
+ for (const [key, desc] of Object.entries(descriptor)) {
1007
+ if (desc.value === undefined && !entries.some(([k]) => k === key)) {
1008
+ allEntries.push([key, undefined]);
1009
+ }
1010
+ }
1011
+
1012
+ // Display objects with only one key on a single line
1013
+ if (allEntries.length === 1) {
1014
+ const [key, val] = allEntries[0];
1015
+ const valueStr = stringifyForDisplay(val, maxDepth, currentDepth + 1, {
1016
+ ...options,
1017
+ asFunctionArgs: false,
1018
+ });
1019
+ return `{ ${key}: ${valueStr} }`;
1020
+ }
1021
+
1022
+ if (allEntries.length > MAX_ENTRIES) {
1023
+ const preview = allEntries
1024
+ .slice(0, MAX_ENTRIES)
1025
+ .map(
1026
+ ([k, v]) =>
1027
+ `${nextIndent}${k}: ${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
1028
+ );
1029
+ return `{\n${preview.join(",\n")},\n${nextIndent}...${allEntries.length - MAX_ENTRIES} more\n${indent}}`;
1030
+ }
1031
+
1032
+ const pairs = allEntries.map(
1033
+ ([k, v]) =>
1034
+ `${nextIndent}${k}: ${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
1035
+ );
1036
+ return `{\n${pairs.join(",\n")}\n${indent}}`;
1037
+ }
1038
+
1039
+ return String(value);
1040
+ };
1041
+
891
1042
  /*
892
1043
  * Deep structural equality for arbitrary JS values — what `===` can't do but this
893
1044
  * codebase constantly needs: memoization cache keys ({ id: 1 } equal to { id: 1 }),
@@ -898,9 +1049,9 @@ const isPlainObject$2 = obj => {
898
1049
  * "wrong" for equality purposes: NaN equals NaN, Date compared by time value,
899
1050
  * cycles don't loop (a set of the pairs being compared guards circular refs),
900
1051
  * and same-type is required
901
- * before descending. Functions, and objects with nothing enumerable to compare
902
- * (a Set, a Map, an element, a URL), are equal by reference only — nothing in
903
- * them says whether two are "the same". Cheap paths run first: reference
1052
+ * before descending. Functions, signals, and objects with nothing enumerable to
1053
+ * compare (a Set, a Map, an element, a URL), are equal by reference only —
1054
+ * nothing in them says whether two are "the same". Cheap paths run first: reference
904
1055
  * equality, then the identity short-circuit below, then array length before
905
1056
  * element-by-element.
906
1057
  *
@@ -918,6 +1069,7 @@ const isPlainObject$2 = obj => {
918
1069
  * compareTwoJsValues(a, b); // true immediately, no property walk
919
1070
  */
920
1071
 
1072
+
921
1073
  // Marks objects with a conceptual identity that transcends reference equality —
922
1074
  // see the file comment. Symbol.for keeps it one shared symbol across modules.
923
1075
  const SYMBOL_IDENTITY = Symbol.for("navi_object_identity");
@@ -1060,6 +1212,15 @@ const compareTwoJsValues = (
1060
1212
  if (aIsDate) {
1061
1213
  return a.getTime() === b.getTime();
1062
1214
  }
1215
+ // A signal is a source of truth, not a value: two of them holding the same
1216
+ // thing right now say nothing about the next tick, and one may change while
1217
+ // the other does not. Their internals (the current value, a version
1218
+ // counter) are enumerable, so without this they would be walked and two
1219
+ // unrelated signals would pass for one — a params signal owned by one
1220
+ // screen would then be handed the action bound to another's.
1221
+ if (isSignal(a) || isSignal(b)) {
1222
+ return false;
1223
+ }
1063
1224
  const aKeys = Object.keys(a);
1064
1225
  const bKeys = Object.keys(b);
1065
1226
  if (aKeys.length !== bKeys.length) {
@@ -1142,159 +1303,37 @@ const debounceSignal = (
1142
1303
  clearTimeout(timeoutId);
1143
1304
  debouncedSignal.value = latestValue;
1144
1305
  };
1306
+ // Whoever reads the debounced value gets the previous one during the delay,
1307
+ // with nothing saying a newer one is on its way. This says it.
1308
+ debouncedSignal.settlingSignal = computed(() => {
1309
+ return !compareTwoJsValues(signalToDebounce.value, debouncedSignal.value);
1310
+ });
1145
1311
 
1146
1312
  return debouncedSignal;
1147
1313
  };
1148
1314
 
1149
- const isSignal = (value) => {
1150
- return getSignalType(value) !== null;
1151
- };
1152
-
1153
- const BRAND_SYMBOL = Symbol.for("preact-signals");
1154
- const getSignalType = (value) => {
1155
- if (!value || typeof value !== "object") {
1156
- return null;
1157
- }
1158
-
1159
- if (value.brand !== BRAND_SYMBOL) {
1160
- return null;
1161
- }
1162
-
1163
- if (typeof value._fn === "function") {
1164
- return "computed";
1165
- }
1166
-
1167
- return "signal";
1168
- };
1169
-
1170
- const MAX_ENTRIES = 5;
1171
-
1172
- const stringifyForDisplay = (
1173
- value,
1174
- maxDepth = 2,
1175
- currentDepth = 0,
1176
- options = {},
1177
- ) => {
1178
- const { asFunctionArgs = false } = options;
1179
- const indent = " ".repeat(currentDepth);
1180
- const nextIndent = " ".repeat(currentDepth + 1);
1181
-
1182
- if (currentDepth >= maxDepth) {
1183
- return typeof value === "object" && value !== null
1184
- ? "[Object]"
1185
- : String(value);
1186
- }
1187
-
1188
- if (value === null) {
1189
- return "null";
1190
- }
1191
- if (value === undefined) {
1192
- return "undefined";
1193
- }
1194
- if (typeof value === "string") {
1195
- return `"${value}"`;
1196
- }
1197
- if (typeof value === "number" || typeof value === "boolean") {
1198
- return String(value);
1199
- }
1200
- if (typeof value === "function") {
1201
- return `[Function ${value.name || "anonymous"}]`;
1202
- }
1203
- if (value instanceof Date) {
1204
- return `Date(${value.toISOString()})`;
1205
- }
1206
- if (value instanceof RegExp) {
1207
- return value.toString();
1208
- }
1315
+ const debouncedSignalCache = new WeakMap();
1209
1316
 
1210
- if (Array.isArray(value)) {
1211
- const openBracket = asFunctionArgs ? "(" : "[";
1212
- const closeBracket = asFunctionArgs ? ")" : "]";
1213
-
1214
- if (value.length === 0) return `${openBracket}${closeBracket}`;
1215
-
1216
- // Display arrays with only one element on a single line
1217
- if (value.length === 1) {
1218
- const item = stringifyForDisplay(
1219
- value[0],
1220
- maxDepth,
1221
- currentDepth + 1,
1222
- // Remove asFunctionArgs for nested calls
1223
- { ...options, asFunctionArgs: false },
1224
- );
1225
- return `${openBracket}${item}${closeBracket}`;
1226
- }
1227
-
1228
- if (value.length > MAX_ENTRIES) {
1229
- const preview = value
1230
- .slice(0, MAX_ENTRIES)
1231
- .map(
1232
- (v) =>
1233
- `${nextIndent}${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
1234
- );
1235
- return `${openBracket}\n${preview.join(",\n")},\n${nextIndent}...${value.length - MAX_ENTRIES} more\n${indent}${closeBracket}`;
1236
- }
1237
-
1238
- const items = value.map(
1239
- (v) =>
1240
- `${nextIndent}${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
1241
- );
1242
- return `${openBracket}\n${items.join(",\n")}\n${indent}${closeBracket}`;
1317
+ /**
1318
+ * The one debounced signal for a given source and delay.
1319
+ *
1320
+ * Two callers asking the same question of the same signal must wait on the same
1321
+ * answer: a debounced signal per caller is a timer per caller, and the actions
1322
+ * derived from them are then several instances of what is one request.
1323
+ */
1324
+ const getDebouncedSignal = (signalToDebounce, delay) => {
1325
+ let byDelay = debouncedSignalCache.get(signalToDebounce);
1326
+ if (!byDelay) {
1327
+ byDelay = new Map();
1328
+ debouncedSignalCache.set(signalToDebounce, byDelay);
1243
1329
  }
1244
-
1245
- if (typeof value === "object") {
1246
- const signalType = getSignalType(value);
1247
- if (signalType) {
1248
- const signalValue = value.peek();
1249
- const prefix = signalType === "computed" ? "computed" : "signal";
1250
- return `${prefix}(${stringifyForDisplay(signalValue, maxDepth, currentDepth, { ...options, asFunctionArgs: false })})`;
1251
- }
1252
-
1253
- const entries = Object.entries(value);
1254
- if (entries.length === 0) return "{}";
1255
-
1256
- // ✅ Inclure les clés avec valeurs undefined/null
1257
- const allEntries = [];
1258
- for (const [key, val] of entries) {
1259
- allEntries.push([key, val]);
1260
- }
1261
-
1262
- // Ajouter les clés avec undefined (que Object.entries omet)
1263
- const descriptor = Object.getOwnPropertyDescriptors(value);
1264
- for (const [key, desc] of Object.entries(descriptor)) {
1265
- if (desc.value === undefined && !entries.some(([k]) => k === key)) {
1266
- allEntries.push([key, undefined]);
1267
- }
1268
- }
1269
-
1270
- // Display objects with only one key on a single line
1271
- if (allEntries.length === 1) {
1272
- const [key, val] = allEntries[0];
1273
- const valueStr = stringifyForDisplay(val, maxDepth, currentDepth + 1, {
1274
- ...options,
1275
- asFunctionArgs: false,
1276
- });
1277
- return `{ ${key}: ${valueStr} }`;
1278
- }
1279
-
1280
- if (allEntries.length > MAX_ENTRIES) {
1281
- const preview = allEntries
1282
- .slice(0, MAX_ENTRIES)
1283
- .map(
1284
- ([k, v]) =>
1285
- `${nextIndent}${k}: ${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
1286
- );
1287
- return `{\n${preview.join(",\n")},\n${nextIndent}...${allEntries.length - MAX_ENTRIES} more\n${indent}}`;
1288
- }
1289
-
1290
- const pairs = allEntries.map(
1291
- ([k, v]) =>
1292
- `${nextIndent}${k}: ${stringifyForDisplay(v, maxDepth, currentDepth + 1, { ...options, asFunctionArgs: false })}`,
1293
- );
1294
- return `{\n${pairs.join(",\n")}\n${indent}}`;
1330
+ const existing = byDelay.get(delay);
1331
+ if (existing) {
1332
+ return existing;
1295
1333
  }
1296
-
1297
- return String(value);
1334
+ const debouncedSignal = debounceSignal(signalToDebounce, { delay });
1335
+ byDelay.set(delay, debouncedSignal);
1336
+ return debouncedSignal;
1298
1337
  };
1299
1338
 
1300
1339
  /**
@@ -3157,6 +3196,10 @@ const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
3157
3196
  * routing or lists.
3158
3197
  */
3159
3198
 
3199
+ // Params waiting to settle exist only under a debounced binding; every other
3200
+ // action answers "no" so nobody has to check whether it can be asked.
3201
+ const NOT_SETTLING_SIGNAL = signal(false);
3202
+
3160
3203
  let DEBUG$2 = false;
3161
3204
  const enableDebugActions = () => {
3162
3205
  DEBUG$2 = true;
@@ -3816,12 +3859,53 @@ const createAction = (callback, rootOptions = {}) => {
3816
3859
  ...options,
3817
3860
  });
3818
3861
  };
3862
+ /**
3863
+ * The action instance for these params.
3864
+ *
3865
+ * @param {any} newParamsOrSignal - params, or a signal holding them (the
3866
+ * result then retargets itself as the signal changes), or an object whose
3867
+ * values may be signals.
3868
+ * @param {object} [options]
3869
+ * @param {number} [options.debounce] - milliseconds the params must stay
3870
+ * stable before the instance follows them. What it buys is a screen that
3871
+ * owns its params — a search box, a picker — asking one question instead
3872
+ * of one per keystroke, without an effect: the binding IS the delay.
3873
+ * Only for a signal; params that cannot change have nothing to settle.
3874
+ * During the delay the instance is still the previous one, holding the
3875
+ * previous answer — `paramsSettlingSignal` is what says a newer one is
3876
+ * coming, and `useAsyncData(action, { loading: true })` reads it.
3877
+ */
3819
3878
  const bindParams = (newParamsOrSignal, options = {}) => {
3879
+ const { debounce, ...optionsWithoutDebounce } = options;
3880
+ if (debounce) {
3881
+ if (!isSignal(newParamsOrSignal)) {
3882
+ console.warn(
3883
+ `bindParams({ debounce }) expects a signal, received ${typeof newParamsOrSignal}: params that cannot change have nothing to settle.`,
3884
+ );
3885
+ } else {
3886
+ // The delay lives in a signal derived from theirs, one per (signal,
3887
+ // delay): the cache below then keys on it like on any other signal,
3888
+ // so two call sites asking the same question with the same delay get
3889
+ // the one instance, and two different delays get two.
3890
+ const debouncedParamsSignal = getDebouncedSignal(
3891
+ newParamsOrSignal,
3892
+ debounce,
3893
+ );
3894
+ return bindParams(debouncedParamsSignal, {
3895
+ ...optionsWithoutDebounce,
3896
+ syncParams: debouncedParamsSignal.flush,
3897
+ paramsSettlingSignal: debouncedParamsSignal.settlingSignal,
3898
+ });
3899
+ }
3900
+ }
3820
3901
  const existingChildAction = childActionWeakMap.get(newParamsOrSignal);
3821
3902
  if (existingChildAction) {
3822
3903
  return existingChildAction;
3823
3904
  }
3824
- const childAction = _bindParams(newParamsOrSignal, options);
3905
+ const childAction = _bindParams(
3906
+ newParamsOrSignal,
3907
+ optionsWithoutDebounce,
3908
+ );
3825
3909
  childActionWeakMap.set(newParamsOrSignal, childAction);
3826
3910
  if (childAction !== action) {
3827
3911
  // binding that added nothing resolves to the action itself; it must
@@ -3964,6 +4048,7 @@ const createAction = (callback, rootOptions = {}) => {
3964
4048
  },
3965
4049
 
3966
4050
  paramsSignal,
4051
+ paramsSettlingSignal: NOT_SETTLING_SIGNAL,
3967
4052
  runningStateSignal,
3968
4053
  isPrerunSignal,
3969
4054
  valueSignal,
@@ -4297,6 +4382,7 @@ const createActionProxyFromSignal = (
4297
4382
  inheritData = true,
4298
4383
  onChange,
4299
4384
  syncParams,
4385
+ paramsSettlingSignal = NOT_SETTLING_SIGNAL,
4300
4386
  } = {},
4301
4387
  ) => {
4302
4388
  const actionTargetChangeCallbackSet = new Set();
@@ -4473,6 +4559,7 @@ const createActionProxyFromSignal = (
4473
4559
  meta: action.meta,
4474
4560
 
4475
4561
  paramsSignal: proxyParamsSignal,
4562
+ paramsSettlingSignal,
4476
4563
  isPrerunSignal: proxySignal("isPrerunSignal", "isPrerun"),
4477
4564
  runningStateSignal: proxySignal("runningStateSignal", "runningState"),
4478
4565
  errorSignal: proxySignal("errorSignal", "error"),
@@ -4629,7 +4716,9 @@ const isPlainObject = (obj) => {
4629
4716
  * @param {number} [options.debounce] - When set, the action is only run once the derived params
4630
4717
  * have been stable for this many milliseconds. Useful to avoid firing a backend call on every
4631
4718
  * keystroke: set `debounce: 500` and the request is sent only after the user stops interacting
4632
- * with the filters for 500 ms.
4719
+ * with the filters for 500 ms. During that delay the action still holds the previous answer;
4720
+ * `action.paramsSettlingSignal` is what says a newer one is coming, and
4721
+ * `useAsyncData(action, { loading: true })` reads it.
4633
4722
  *
4634
4723
  * Example — auto-refresh a result list while the user tweaks filters:
4635
4724
  * ```js
@@ -4659,7 +4748,7 @@ const actionRunEffect = (
4659
4748
  if (typeof action === "function") {
4660
4749
  action = createAction(action);
4661
4750
  }
4662
- let actionParamsSignal = computed(() => {
4751
+ const actionParamsSignal = computed(() => {
4663
4752
  const params = deriveActionParamsFromSignals();
4664
4753
  action.debug(
4665
4754
  `Derived params for action "${action}": ${stringifyForDisplay(params)}`,
@@ -4677,14 +4766,8 @@ const actionRunEffect = (
4677
4766
  }
4678
4767
  return params;
4679
4768
  });
4680
- if (debounce) {
4681
- actionParamsSignal = debounceSignal(actionParamsSignal, {
4682
- delay: debounce,
4683
- });
4684
- }
4685
-
4686
4769
  const actionRunnedByThisEffect = action.bindParams(actionParamsSignal, {
4687
- syncParams: debounce ? actionParamsSignal.flush : undefined,
4770
+ debounce,
4688
4771
  onChange: (actionTarget, actionTargetPrevious, { explicitRunIntent }) => {
4689
4772
  if (explicitRunIntent) {
4690
4773
  // The caller already issued an explicit run/rerun/prerun/reset/abort —
@@ -15174,8 +15257,12 @@ const createResource = (
15174
15257
  idKey,
15175
15258
  uniqueKeys,
15176
15259
 
15260
+ // Reactive reads, not hooks: they subscribe the render that calls them to
15261
+ // the store's array signal. Nothing hook-shaped inside, so a loop or a
15262
+ // condition is fine. `useAllByIds` skips the ids the store does not hold.
15177
15263
  useArray: () => store.arraySignal.value,
15178
15264
  useById: (id) => store.select(idKey, id),
15265
+ useAllByIds: (idArray) => store.selectAll(idArray),
15179
15266
 
15180
15267
  withParams: undefined,
15181
15268
  one: undefined,
@@ -17374,14 +17461,34 @@ const useActionAsyncData = (action, {
17374
17461
  }
17375
17462
  setTick(n => n + 1);
17376
17463
  });
17464
+ // A debounced binding waits before it retargets, so nothing above changes
17465
+ // while the delay runs — but what is on screen is already out of date.
17466
+ let settlingNotificationIsInitial = true;
17467
+ const unsubscribeFromParamsSettling = action.paramsSettlingSignal.subscribe(() => {
17468
+ if (settlingNotificationIsInitial) {
17469
+ settlingNotificationIsInitial = false;
17470
+ return;
17471
+ }
17472
+ setTick(n => n + 1);
17473
+ });
17377
17474
  return () => {
17378
17475
  unsubscribeFromRunningState();
17379
17476
  unsubscribeFromData();
17477
+ unsubscribeFromParamsSettling();
17380
17478
  };
17381
17479
  // Bound to the action, not to the mount: params given as a plain object
17382
17480
  // make another action instance, and the component would otherwise stay
17383
17481
  // subscribed to the state of the one it no longer reads.
17384
17482
  }, [action]);
17483
+
17484
+ // The params moved and the run has not started: under a debounce the action
17485
+ // is still the previous one, COMPLETED with the previous answer. There is no
17486
+ // run to await yet, so nothing can be suspended on — but a component drawing
17487
+ // its own wait can say the answer on screen is on its way out, which is the
17488
+ // whole reason it asked for `loading`.
17489
+ if (loadingEffect === "use" && runningState !== FAILED && action.paramsSettlingSignal.peek()) {
17490
+ return [action.dataSignal.peek(), true, undefined];
17491
+ }
17385
17492
  if (runningState === COMPLETED) {
17386
17493
  return [action.dataSignal.peek(), false, undefined];
17387
17494
  }
@@ -28105,8 +28212,10 @@ const RouteTransitionArea = ({
28105
28212
  * @param {object} to - same forms. Going from `from` to `to` plays forward,
28106
28213
  * the reverse plays back — unless the reverse is written as a relation of
28107
28214
  * its own, which then owns that way (a movement of its own, or `"none"` for
28108
- * a plain cut). A change between two pages no relation was written for plays
28109
- * nothing.
28215
+ * a plain cut). Written with the SAME movement it owns that way all the
28216
+ * same, so both crossings play forward and the pair can never say "back":
28217
+ * that pair is warned about. A change between two pages no relation was
28218
+ * written for plays nothing.
28110
28219
  * @param {string|{type?: string, duration?: number|string}} [transition] -
28111
28220
  * what plays: a type name, or `{ type, duration }` to also say how long
28112
28221
  * (`--navi-route-transition-duration` says it for everyone otherwise).
@@ -28155,6 +28264,7 @@ const defineRouteTransition = (from, to, transition) => {
28155
28264
  type,
28156
28265
  duration
28157
28266
  };
28267
+ warnAboutBothWaysWritten(relation);
28158
28268
  relations.push(relation);
28159
28269
  rebuildWatcher();
28160
28270
  return () => {
@@ -28749,6 +28859,38 @@ const currentPageIndex$1 = pages => {
28749
28859
  }
28750
28860
  return currentIndex;
28751
28861
  };
28862
+
28863
+ // Both ways of a pair written with the SAME movement. The exact way travelled
28864
+ // is searched before any reverse (see findRelation), so each way then finds
28865
+ // its own relation and BOTH play forward: the pair loses the one thing a
28866
+ // direction is for. That is not a resolution to fix — it is what makes
28867
+ // reciprocity a default — it is a definition with no reading under which it is
28868
+ // what the author meant, so it is said here, where both relations are known.
28869
+ // Narrow on purpose: a different type is the asymmetry the reverse exists for,
28870
+ // "none" is silence said out loud, and a different duration is a way out taken
28871
+ // slower.
28872
+ const warnAboutBothWaysWritten = ({
28873
+ from,
28874
+ to,
28875
+ type,
28876
+ duration
28877
+ }) => {
28878
+ const reverse = relations.find(candidate => samePage$1(candidate.from, to) && samePage$1(candidate.to, from));
28879
+ if (!reverse) {
28880
+ return;
28881
+ }
28882
+ if (reverse.type !== type || reverse.duration !== duration) {
28883
+ return;
28884
+ }
28885
+ if (type === "none") {
28886
+ return;
28887
+ }
28888
+ // The one already written is named first: that is the order the application
28889
+ // wrote them in, and the order it will find them in to fix them.
28890
+ const written = `${describePage(reverse.from)} → ${describePage(reverse.to)}`;
28891
+ const added = `${describePage(from)} → ${describePage(to)}`;
28892
+ warnOnce(`both-ways-written:${written}|${added}`, `${written} and ${added} are both written with the same movement, so BOTH crossings play forward and this pair can never say "back" — the back button included. A relation written for the exact way travelled wins over being the reverse of another (see findRelation), which is what makes reciprocity the default: write the way back only to give it a DIFFERENT movement, or "none" to silence it. A single crossing that walks the map backwards says so on itself instead: <Link routeTransition={{ direction: "forward" }}>, or navTo(url, { routeTransition: { direction: "forward" } }).`);
28893
+ };
28752
28894
  const warnPagesBothCurrent = (pageKept, pageIgnored) => {
28753
28895
  const kept = describePage(pageKept);
28754
28896
  const ignored = describePage(pageIgnored);
@@ -58654,8 +58796,15 @@ const PickerCustomResolver = props => {
58654
58796
  props.variant = "icon";
58655
58797
  }
58656
58798
  // A door, never a field (see `allowNameless`): the form around it expects
58657
- // no value from it, and no name.
58799
+ // no value from it, and no name. So the trigger answers to "button" — it is
58800
+ // pressed to read something, never typed into — the same thing a confirm
58801
+ // picker says through `picksNothing` (see picker.jsx). Said as the role
58802
+ // here rather than through that prop: `picksNothing` also takes the right
58803
+ // slot away, and the status icon a callout trigger draws sits in it.
58658
58804
  props.allowNameless = true;
58805
+ if (props.role === undefined) {
58806
+ props.role = "button";
58807
+ }
58659
58808
  // A word in a sentence asks for a plain tooltip — no icon in the callout,
58660
58809
  // no status color; an icon one presses is the callout's own status icon,
58661
58810
  // and says "info" like the callout it opens.
@@ -67364,6 +67513,11 @@ const PickerButton = props => {
67364
67513
  // confirm resolver (see picker_confirm.jsx), which is the picker that is
67365
67514
  // one — a caller whose popup is a menu of actions says it for themselves.
67366
67515
  picksNothing,
67516
+ // Where the caller's own role goes: onto the host, beside the name (see
67517
+ // control_context.js) — a role and a name on two different elements name
67518
+ // nothing at all. Left unsaid, a picker that picks nothing takes "button"
67519
+ // (see PickerInput below).
67520
+ role,
67367
67521
  readOnly,
67368
67522
  error
67369
67523
  } = props;
@@ -67418,6 +67572,21 @@ const PickerButton = props => {
67418
67572
  // clearing being a modification like any other.
67419
67573
  const interactive = !basePseudoState[":disabled"] && !basePseudoState[":read-only"] && !loading;
67420
67574
  usePickerErrorCallout(uiStateController, error);
67575
+ // What the picker really is, said on the element that holds the name and
67576
+ // takes the focus (the host input below): a picker that picks nothing is a
67577
+ // door — its popup is a question (type="confirm") or something to read
67578
+ // (mode="callout", which says it for itself) — and a door is pressed. So it
67579
+ // answers to "button" rather than to the "textbox" its <input> would
67580
+ // otherwise be, which is both what a screen reader announces and what
67581
+ // getByRole("button", { name }) finds. A picker holding a value stays a
67582
+ // textbox: it does hold text.
67583
+ const hostRole = role ?? (picksNothing ? "button" : undefined);
67584
+ // And what that button is called, when the caller wrote it as a drawing
67585
+ // rather than as a word: a door draws no value, so what sits in its value
67586
+ // slot IS its label — the `ui` a caller gave it, or its placeholder. Skipped
67587
+ // the moment the caller names it themselves (aria-label/aria-labelledby),
67588
+ // which is the only way to name a door drawn as an icon.
67589
+ const hostLabelId = hostRole === "button" && inputProps["aria-label"] === undefined && inputProps["aria-labelledby"] === undefined && variant !== "headless" && ui !== "default" ? `${inputProps.id}_picker_label` : undefined;
67421
67590
  // What the picker knows about itself, for the pieces it does not place: the
67422
67591
  // drawings of its value (Picker.UI.*), and the affordances a caller may put
67423
67592
  // in their own `ui` (Picker.Clear) as much as the ones it puts in its slot.
@@ -67464,6 +67633,7 @@ const PickerButton = props => {
67464
67633
  basePseudoState: basePseudoState,
67465
67634
  styleCSSVars: PickerStyleCSSVars,
67466
67635
  variant: undefined,
67636
+ role: undefined,
67467
67637
  rightSlotIcon: undefined,
67468
67638
  rightSlotIconSize: undefined,
67469
67639
  rightSlot: undefined,
@@ -67512,6 +67682,8 @@ const PickerButton = props => {
67512
67682
  color: "var(--picker-loader-color)",
67513
67683
  inset: -2
67514
67684
  }), jsx(PickerInput, {
67685
+ role: hostRole,
67686
+ "aria-labelledby": hostLabelId,
67515
67687
  tabIndex: variant === "headless" ? -1 : undefined,
67516
67688
  "aria-hidden": variant === "headless" ? "true" : undefined,
67517
67689
  ...inputProps,
@@ -67587,6 +67759,10 @@ const PickerButton = props => {
67587
67759
  }
67588
67760
  }), variant === "headless" || ui === "default" ? null : jsx(Text, {
67589
67761
  className: "navi_picker_value"
67762
+ // Only when it names the trigger (see hostLabelId above).
67763
+ ,
67764
+
67765
+ id: hostLabelId
67590
67766
  // Tells the caller's own drawing of the control from the value
67591
67767
  // the picker draws itself, so each is written on its own line
67592
67768
  // (see .navi_picker_value in the CSS above).