@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.
- package/dist/jsenv_navi.js +338 -162
- package/dist/jsenv_navi.js.map +27 -20
- package/docs/AI_INSTRUCTIONS.md +8 -3
- package/docs/actions.md +22 -0
- package/docs/resource.md +61 -1
- package/docs/route_transitions.md +6 -2
- package/docs/testid.md +22 -4
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -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
|
|
902
|
-
* (a Set, a Map, an element, a URL), are equal by reference only —
|
|
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
|
|
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
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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 (
|
|
1246
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
|
28109
|
-
*
|
|
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).
|