@jsenv/navi 0.29.9 → 0.29.11
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 +389 -65
- package/dist/jsenv_navi.js.map +29 -21
- package/docs/MOBILE_LAYOUT_PITFALLS.md +69 -14
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -1663,6 +1663,11 @@ const generateSignalId = () => {
|
|
|
1663
1663
|
* @param {"string" | "number" | "boolean" | "object"} [options.type="string"] - Type for localStorage serialization/deserialization
|
|
1664
1664
|
* @param {number} [options.step] - For number type: step size for precision. Values will be rounded to nearest multiple of step.
|
|
1665
1665
|
* @param {Array} [options.oneOf] - Array of valid values for validation. Signal will be marked invalid if value is not in this array
|
|
1666
|
+
* @param {boolean} [options.weak=false] - The param qualifies one visit, not the screen: it is written into a
|
|
1667
|
+
* url only when explicitly named (`routeParams={{ edit: id }}`), never inherited from the signal's current
|
|
1668
|
+
* value, and it goes back to the default value when the route stops matching. Use it for params like an
|
|
1669
|
+
* "edit this one" id, where every other link to the screen must lead to the plain screen.
|
|
1670
|
+
* Incompatible with `persists` (throws).
|
|
1666
1671
|
* @param {boolean} [options.debug=false] - Enable debug logging for this signal's operations
|
|
1667
1672
|
* @returns {import("@preact/signals").Signal} A signal that can be synchronized with a source signal and/or persisted in localStorage. The signal includes a `validity` property for validation state.
|
|
1668
1673
|
*
|
|
@@ -1735,8 +1740,15 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
1735
1740
|
default: staticFallback,
|
|
1736
1741
|
ignoreArrayOrder,
|
|
1737
1742
|
autoFix,
|
|
1743
|
+
weak = false,
|
|
1738
1744
|
} = options;
|
|
1739
1745
|
|
|
1746
|
+
if (weak && persists) {
|
|
1747
|
+
throw new TypeError(
|
|
1748
|
+
`stateSignal "${id}": weak and persists are contradictory — a weak param qualifies one visit, it cannot be restored from a previous session.`,
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1740
1752
|
// Check if defaultValue is a signal (dynamic default) or static value
|
|
1741
1753
|
const isDynamicDefault =
|
|
1742
1754
|
defaultValue &&
|
|
@@ -2099,6 +2111,23 @@ setBaseUrl(
|
|
|
2099
2111
|
: window.location.origin,
|
|
2100
2112
|
);
|
|
2101
2113
|
|
|
2114
|
+
/**
|
|
2115
|
+
* The single place where "this param was not provided, take the signal value"
|
|
2116
|
+
* happens while building a url. A weak param qualifies one visit, not the
|
|
2117
|
+
* screen: it enters a url only when the caller names it (or when it is already
|
|
2118
|
+
* in the url being preserved), so here it always reads as absent.
|
|
2119
|
+
* Not reading the signal also keeps the built url from depending on it.
|
|
2120
|
+
*/
|
|
2121
|
+
const readSignalForUrlBuild = (connection) => {
|
|
2122
|
+
if (!connection || !connection.signal) {
|
|
2123
|
+
return undefined;
|
|
2124
|
+
}
|
|
2125
|
+
if (connection.weak) {
|
|
2126
|
+
return undefined;
|
|
2127
|
+
}
|
|
2128
|
+
return connection.signal.value;
|
|
2129
|
+
};
|
|
2130
|
+
|
|
2102
2131
|
/**
|
|
2103
2132
|
* Creates a custom route pattern matcher
|
|
2104
2133
|
*/
|
|
@@ -2173,7 +2202,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2173
2202
|
// Parameter was explicitly provided - always respect explicit parameters
|
|
2174
2203
|
continue;
|
|
2175
2204
|
}
|
|
2176
|
-
const signalValue = connection
|
|
2205
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
2177
2206
|
if (signalValue !== undefined) {
|
|
2178
2207
|
// Parameter was not provided, check signal value
|
|
2179
2208
|
resolvedParams[paramName] = signalValue;
|
|
@@ -2186,7 +2215,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2186
2215
|
// Parameter was explicitly provided - always respect explicit parameters
|
|
2187
2216
|
continue;
|
|
2188
2217
|
}
|
|
2189
|
-
const signalValue = connection
|
|
2218
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
2190
2219
|
if (signalValue !== undefined) {
|
|
2191
2220
|
// Parameter was not provided, check signal value
|
|
2192
2221
|
resolvedParams[paramName] = signalValue;
|
|
@@ -2229,7 +2258,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2229
2258
|
continue;
|
|
2230
2259
|
}
|
|
2231
2260
|
|
|
2232
|
-
const ancestorSignalValue = ancestorConnection
|
|
2261
|
+
const ancestorSignalValue = readSignalForUrlBuild(ancestorConnection);
|
|
2233
2262
|
if (
|
|
2234
2263
|
ancestorSignalValue !== undefined &&
|
|
2235
2264
|
ancestorSignalValue !== ancestorConnection.getDefaultValue()
|
|
@@ -2307,7 +2336,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2307
2336
|
if (childParam in resolvedParams) {
|
|
2308
2337
|
continue;
|
|
2309
2338
|
}
|
|
2310
|
-
const childSignalValue = childConnection
|
|
2339
|
+
const childSignalValue = readSignalForUrlBuild(childConnection);
|
|
2311
2340
|
// Only include if not already resolved and is non-default
|
|
2312
2341
|
if (
|
|
2313
2342
|
childSignalValue !== undefined &&
|
|
@@ -2349,7 +2378,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2349
2378
|
}
|
|
2350
2379
|
} else {
|
|
2351
2380
|
// Parameter not provided but signal has a value
|
|
2352
|
-
const signalValue = connection
|
|
2381
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
2353
2382
|
if (connection.isCustomValue(signalValue)) {
|
|
2354
2383
|
// Only include custom values
|
|
2355
2384
|
filtered[paramName] = signalValue;
|
|
@@ -2368,7 +2397,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2368
2397
|
}
|
|
2369
2398
|
} else {
|
|
2370
2399
|
// Parameter not provided but signal has a value
|
|
2371
|
-
const signalValue = connection
|
|
2400
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
2372
2401
|
if (connection.isCustomValue(signalValue)) {
|
|
2373
2402
|
// Only include custom values
|
|
2374
2403
|
filtered[paramName] = signalValue;
|
|
@@ -2385,7 +2414,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2385
2414
|
const canReachLiteralValue = (literalValue, params, literalPosition) => {
|
|
2386
2415
|
// Check parent's own parameters (signals and user params)
|
|
2387
2416
|
const parentCanProvide = connections.some((conn) => {
|
|
2388
|
-
const signalValue = conn
|
|
2417
|
+
const signalValue = readSignalForUrlBuild(conn);
|
|
2389
2418
|
const userValue = params[conn.paramName];
|
|
2390
2419
|
const effectiveValue = userValue !== undefined ? userValue : signalValue;
|
|
2391
2420
|
return (
|
|
@@ -2414,7 +2443,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2414
2443
|
return false;
|
|
2415
2444
|
}
|
|
2416
2445
|
return connsAtPosition.some((conn) => {
|
|
2417
|
-
const signalValue = conn
|
|
2446
|
+
const signalValue = readSignalForUrlBuild(conn);
|
|
2418
2447
|
return signalValue === literalValue && conn.isCustomValue(signalValue);
|
|
2419
2448
|
});
|
|
2420
2449
|
};
|
|
@@ -2469,7 +2498,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2469
2498
|
pathConnectionMap.get(paramName) ||
|
|
2470
2499
|
queryConnectionMap.get(paramName);
|
|
2471
2500
|
if (parentConnection) {
|
|
2472
|
-
parentParamValue = parentConnection
|
|
2501
|
+
parentParamValue = readSignalForUrlBuild(parentConnection);
|
|
2473
2502
|
}
|
|
2474
2503
|
}
|
|
2475
2504
|
|
|
@@ -2554,7 +2583,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2554
2583
|
paramValue = item.userValue;
|
|
2555
2584
|
} else {
|
|
2556
2585
|
paramName = item.paramName;
|
|
2557
|
-
paramValue = item
|
|
2586
|
+
paramValue = readSignalForUrlBuild(item);
|
|
2558
2587
|
// Only include custom parent signal values (not using defaults)
|
|
2559
2588
|
if (paramValue === undefined || !item.isCustomValue(paramValue)) {
|
|
2560
2589
|
return { isCompatible: true, shouldInclude: false };
|
|
@@ -2661,7 +2690,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2661
2690
|
if (!siblingConnection) {
|
|
2662
2691
|
continue;
|
|
2663
2692
|
}
|
|
2664
|
-
const siblingSignalValue = siblingConnection
|
|
2693
|
+
const siblingSignalValue = readSignalForUrlBuild(siblingConnection);
|
|
2665
2694
|
if (siblingSignalValue === undefined) {
|
|
2666
2695
|
continue;
|
|
2667
2696
|
}
|
|
@@ -2705,7 +2734,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2705
2734
|
const explicitValue = params[paramName];
|
|
2706
2735
|
const connection =
|
|
2707
2736
|
pathConnectionMap.get(paramName) || queryConnectionMap.get(paramName);
|
|
2708
|
-
const signalValue = connection
|
|
2737
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
2709
2738
|
|
|
2710
2739
|
// Check if the parameter has the required value
|
|
2711
2740
|
if (
|
|
@@ -2759,7 +2788,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2759
2788
|
hasActiveParams = true;
|
|
2760
2789
|
}
|
|
2761
2790
|
} else {
|
|
2762
|
-
const signalValue = connection
|
|
2791
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
2763
2792
|
if (signalValue !== undefined) {
|
|
2764
2793
|
// No explicit override - use signal value
|
|
2765
2794
|
childParams[paramName] = signalValue;
|
|
@@ -2976,7 +3005,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
2976
3005
|
}
|
|
2977
3006
|
// If explicitly undefined, don't include it (which means don't use child route)
|
|
2978
3007
|
} else {
|
|
2979
|
-
const signalValue = connection
|
|
3008
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
2980
3009
|
if (
|
|
2981
3010
|
signalValue !== undefined &&
|
|
2982
3011
|
connection.isCustomValue(signalValue)
|
|
@@ -3016,7 +3045,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3016
3045
|
continue; // Already have this parameter
|
|
3017
3046
|
}
|
|
3018
3047
|
|
|
3019
|
-
const signalValue = connection
|
|
3048
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
3020
3049
|
// Only include custom signal values (not using defaults)
|
|
3021
3050
|
if (
|
|
3022
3051
|
signalValue !== undefined &&
|
|
@@ -3198,7 +3227,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3198
3227
|
paramName,
|
|
3199
3228
|
connection,
|
|
3200
3229
|
] of childPatternObj.pathConnectionMap) {
|
|
3201
|
-
const signalValue = connection
|
|
3230
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
3202
3231
|
if (
|
|
3203
3232
|
signalValue !== undefined &&
|
|
3204
3233
|
connection.isCustomValue(signalValue)
|
|
@@ -3220,7 +3249,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3220
3249
|
paramName,
|
|
3221
3250
|
connection,
|
|
3222
3251
|
] of childPatternObj.queryConnectionMap) {
|
|
3223
|
-
const signalValue = connection
|
|
3252
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
3224
3253
|
if (
|
|
3225
3254
|
signalValue !== undefined &&
|
|
3226
3255
|
connection.isCustomValue(signalValue)
|
|
@@ -3840,7 +3869,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3840
3869
|
);
|
|
3841
3870
|
for (const conn of targetAncestor.connections) {
|
|
3842
3871
|
console.debug(
|
|
3843
|
-
`[${pattern}] tryDirectOptimization: Target connection ${conn.paramName}: value=${conn
|
|
3872
|
+
`[${pattern}] tryDirectOptimization: Target connection ${conn.paramName}: value=${readSignalForUrlBuild(conn)}, isCustom=${conn.isCustomValue(readSignalForUrlBuild(conn))}`,
|
|
3844
3873
|
);
|
|
3845
3874
|
}
|
|
3846
3875
|
}
|
|
@@ -3857,7 +3886,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3857
3886
|
}
|
|
3858
3887
|
|
|
3859
3888
|
// Only include if not already processed and has custom value (not default)
|
|
3860
|
-
const signalValue = connection
|
|
3889
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
3861
3890
|
if (signalValue !== undefined) {
|
|
3862
3891
|
// Don't include path parameters that correspond to literal segments we're optimizing away
|
|
3863
3892
|
const targetParam = targetParams.find((p) => p.name === paramName);
|
|
@@ -3903,7 +3932,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3903
3932
|
}
|
|
3904
3933
|
|
|
3905
3934
|
// Only inherit custom values (not defaults) that we don't already have
|
|
3906
|
-
const signalValue = connection
|
|
3935
|
+
const signalValue = readSignalForUrlBuild(connection);
|
|
3907
3936
|
if (
|
|
3908
3937
|
signalValue !== undefined &&
|
|
3909
3938
|
connection.isCustomValue(signalValue)
|
|
@@ -3989,7 +4018,7 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
3989
4018
|
}
|
|
3990
4019
|
|
|
3991
4020
|
// Only inherit if we don't have this param and parent has custom value (not default)
|
|
3992
|
-
const parentSignalValue = parentConnection
|
|
4021
|
+
const parentSignalValue = readSignalForUrlBuild(parentConnection);
|
|
3993
4022
|
if (
|
|
3994
4023
|
parentSignalValue !== undefined &&
|
|
3995
4024
|
parentConnection.isCustomValue(parentSignalValue)
|
|
@@ -4075,7 +4104,36 @@ const createRoutePattern = (pattern, { searchParams = {} } = {}) => {
|
|
|
4075
4104
|
// search params are updated. When buildMostPreciseUrl performs an ancestor
|
|
4076
4105
|
// optimisation (e.g. "/map/isochrone/compare" → "/map/isochrone") it is trusted
|
|
4077
4106
|
// as-is because the built pathname will differ from the route's own base pathname.
|
|
4107
|
+
// Weak params are never inherited from their signal, but a url that already
|
|
4108
|
+
// carries one keeps it: staying on the same screen while another param
|
|
4109
|
+
// changes must not end the visit this param qualifies.
|
|
4110
|
+
const carryOverWeakParams = (currentUrl, params) => {
|
|
4111
|
+
let paramsWithWeak = params;
|
|
4112
|
+
for (const [paramName, connection] of [
|
|
4113
|
+
...pathConnectionMap,
|
|
4114
|
+
...queryConnectionMap,
|
|
4115
|
+
]) {
|
|
4116
|
+
if (!connection.weak || paramName in paramsWithWeak) {
|
|
4117
|
+
continue;
|
|
4118
|
+
}
|
|
4119
|
+
const currentValue = connection.signal.peek();
|
|
4120
|
+
if (currentValue === undefined) {
|
|
4121
|
+
continue;
|
|
4122
|
+
}
|
|
4123
|
+
const currentParams = applyOn(currentUrl);
|
|
4124
|
+
if (!currentParams || currentParams[paramName] === undefined) {
|
|
4125
|
+
continue;
|
|
4126
|
+
}
|
|
4127
|
+
if (paramsWithWeak === params) {
|
|
4128
|
+
paramsWithWeak = { ...params };
|
|
4129
|
+
}
|
|
4130
|
+
paramsWithWeak[paramName] = currentParams[paramName];
|
|
4131
|
+
}
|
|
4132
|
+
return paramsWithWeak;
|
|
4133
|
+
};
|
|
4134
|
+
|
|
4078
4135
|
const buildUrlPreservingPath = (currentUrl, params = {}) => {
|
|
4136
|
+
params = carryOverWeakParams(currentUrl, params);
|
|
4079
4137
|
const relativeBuiltUrl = buildMostPreciseUrl(params);
|
|
4080
4138
|
if (!currentUrl) {
|
|
4081
4139
|
return resolveRouteUrl(relativeBuiltUrl);
|
|
@@ -4332,7 +4390,7 @@ const parsePattern = (pattern, { pathConnectionMap, queryConnectionMap }) => {
|
|
|
4332
4390
|
if (
|
|
4333
4391
|
connection &&
|
|
4334
4392
|
connection.signal &&
|
|
4335
|
-
connection
|
|
4393
|
+
readSignalForUrlBuild(connection) === undefined &&
|
|
4336
4394
|
!hasDefault
|
|
4337
4395
|
) {
|
|
4338
4396
|
isOptional = true;
|
|
@@ -5516,7 +5574,10 @@ const route = (pattern, { searchParams } = {}) => {
|
|
|
5516
5574
|
});
|
|
5517
5575
|
};
|
|
5518
5576
|
route.matchesParams = (providedParams) => {
|
|
5519
|
-
|
|
5577
|
+
// paramsSignal (not route.params) so a component calling this during render
|
|
5578
|
+
// subscribes to param changes: a navigation from /games/me/a to /games/me/b
|
|
5579
|
+
// leaves matchingSignal untouched, only the params change.
|
|
5580
|
+
const currentParams = route.paramsSignal.value;
|
|
5520
5581
|
const resolvedParams = routePattern.resolveParams({
|
|
5521
5582
|
...currentParams,
|
|
5522
5583
|
...providedParams,
|
|
@@ -5883,6 +5944,22 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
5883
5944
|
}
|
|
5884
5945
|
}
|
|
5885
5946
|
|
|
5947
|
+
// A weak param qualifies one visit: leaving the route ends it,
|
|
5948
|
+
// whatever the family and whatever the default. Coming back by a
|
|
5949
|
+
// url that does not carry the param comes back to a blank screen.
|
|
5950
|
+
if (connection.weak) {
|
|
5951
|
+
if (!parameterExtractedByMatchingRoute) {
|
|
5952
|
+
const defaultValue = connection.getDefaultValue();
|
|
5953
|
+
if (debug) {
|
|
5954
|
+
console.debug(
|
|
5955
|
+
`[route] weak param ${paramName}: route no longer matching, back to ${defaultValue}`,
|
|
5956
|
+
);
|
|
5957
|
+
}
|
|
5958
|
+
paramSignal.value = defaultValue;
|
|
5959
|
+
}
|
|
5960
|
+
continue;
|
|
5961
|
+
}
|
|
5962
|
+
|
|
5886
5963
|
// Only reset signal if:
|
|
5887
5964
|
// 1. We're navigating within the same route family (not to completely unrelated routes)
|
|
5888
5965
|
// 2. AND no matching route extracts this parameter from URL
|
|
@@ -9665,7 +9742,7 @@ const setupNetworkMonitoring = () => {
|
|
|
9665
9742
|
};
|
|
9666
9743
|
setupNetworkMonitoring();
|
|
9667
9744
|
|
|
9668
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
9745
|
+
installImportMetaCssBuild(import.meta);const css$Y = /* css */`
|
|
9669
9746
|
.navi_loading_indicator_fluid_container {
|
|
9670
9747
|
position: relative;
|
|
9671
9748
|
display: flex;
|
|
@@ -9697,7 +9774,7 @@ const LoadingIndicatorFluid = ({
|
|
|
9697
9774
|
visuallyHidden,
|
|
9698
9775
|
...rest
|
|
9699
9776
|
}) => {
|
|
9700
|
-
import.meta.css = [css$
|
|
9777
|
+
import.meta.css = [css$Y, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
|
|
9701
9778
|
const ref = useRef(null);
|
|
9702
9779
|
// The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
|
|
9703
9780
|
const [containerWidth, setContainerWidth] = useState(0);
|
|
@@ -9902,7 +9979,7 @@ const LoadingRectangleSvg = ({
|
|
|
9902
9979
|
});
|
|
9903
9980
|
};
|
|
9904
9981
|
|
|
9905
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
9982
|
+
installImportMetaCssBuild(import.meta);const css$X = /* css */`
|
|
9906
9983
|
.navi_loading_outline_wrapper {
|
|
9907
9984
|
position: absolute;
|
|
9908
9985
|
/* Controls place the outline slightly outside their box, right on top of
|
|
@@ -9939,7 +10016,7 @@ installImportMetaCssBuild(import.meta);const css$W = /* css */`
|
|
|
9939
10016
|
}
|
|
9940
10017
|
`;
|
|
9941
10018
|
const LoadingOutline = props => {
|
|
9942
|
-
import.meta.css = [css$
|
|
10019
|
+
import.meta.css = [css$X, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
|
|
9943
10020
|
if (props.containerRef) {
|
|
9944
10021
|
const container = props.containerRef.current;
|
|
9945
10022
|
if (!container) {
|
|
@@ -10264,7 +10341,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
|
|
|
10264
10341
|
};
|
|
10265
10342
|
|
|
10266
10343
|
installImportMetaCssBuild(import.meta);// https://jsfiddle.net/v5xzJ/4/
|
|
10267
|
-
const css$
|
|
10344
|
+
const css$W = /* css */`
|
|
10268
10345
|
@layer navi {
|
|
10269
10346
|
.navi_text {
|
|
10270
10347
|
&[data-skeleton] {
|
|
@@ -10770,7 +10847,7 @@ const TextShrinkWrap = props => {
|
|
|
10770
10847
|
});
|
|
10771
10848
|
};
|
|
10772
10849
|
const TextUI = props => {
|
|
10773
|
-
import.meta.css = [css$
|
|
10850
|
+
import.meta.css = [css$W, "@jsenv/navi/src/text/text.jsx"];
|
|
10774
10851
|
let {
|
|
10775
10852
|
ref,
|
|
10776
10853
|
spacing,
|
|
@@ -13484,7 +13561,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
13484
13561
|
* - Arrow automatically shows when pointing at a valid anchor element
|
|
13485
13562
|
* - Centers in viewport when no anchor element provided or anchor is too big
|
|
13486
13563
|
*/
|
|
13487
|
-
const css$
|
|
13564
|
+
const css$V = /* css */`
|
|
13488
13565
|
@layer navi {
|
|
13489
13566
|
.navi_callout {
|
|
13490
13567
|
/* A callout is parented to what it explains, so it inherits from it — and
|
|
@@ -13704,7 +13781,7 @@ const openCallout = (message, {
|
|
|
13704
13781
|
skipFocus = false,
|
|
13705
13782
|
debug = () => {}
|
|
13706
13783
|
} = {}) => {
|
|
13707
|
-
import.meta.css = [css$
|
|
13784
|
+
import.meta.css = [css$V, "@jsenv/navi/src/control/rules/callout/callout.js"];
|
|
13708
13785
|
if (debug === true) {
|
|
13709
13786
|
debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
|
|
13710
13787
|
}
|
|
@@ -22782,7 +22859,7 @@ const getAssociatedLabels = element => {
|
|
|
22782
22859
|
return [];
|
|
22783
22860
|
};
|
|
22784
22861
|
|
|
22785
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
22862
|
+
installImportMetaCssBuild(import.meta);const css$U = /* css */`
|
|
22786
22863
|
@layer navi {
|
|
22787
22864
|
.navi_button {
|
|
22788
22865
|
--button-border-radius: var(--navi-control-border-radius);
|
|
@@ -23175,7 +23252,7 @@ installImportMetaCssBuild(import.meta);const css$T = /* css */`
|
|
|
23175
23252
|
}
|
|
23176
23253
|
`;
|
|
23177
23254
|
const ButtonUI = props => {
|
|
23178
|
-
import.meta.css = [css$
|
|
23255
|
+
import.meta.css = [css$U, "@jsenv/navi/src/control/input/button_ui.jsx"];
|
|
23179
23256
|
const {
|
|
23180
23257
|
ref,
|
|
23181
23258
|
// href/link
|
|
@@ -25303,7 +25380,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
25303
25380
|
* reaches the real container.
|
|
25304
25381
|
*/
|
|
25305
25382
|
let openLocalDialogCount = 0;
|
|
25306
|
-
const css$
|
|
25383
|
+
const css$T = /* css */`
|
|
25307
25384
|
@layer navi {
|
|
25308
25385
|
.navi_dialog {
|
|
25309
25386
|
/* Min gap between the dialog and the edges of its container. Written
|
|
@@ -25713,7 +25790,7 @@ const css$S = /* css */`
|
|
|
25713
25790
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
25714
25791
|
*/
|
|
25715
25792
|
const Dialog = props => {
|
|
25716
|
-
import.meta.css = [css$
|
|
25793
|
+
import.meta.css = [css$T, "@jsenv/navi/src/layout/dialog.jsx"];
|
|
25717
25794
|
if (props.openController) {
|
|
25718
25795
|
return jsx(ControlledDialog, {
|
|
25719
25796
|
...props
|
|
@@ -26531,7 +26608,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
26531
26608
|
* and applied.
|
|
26532
26609
|
*/
|
|
26533
26610
|
let openLocalPopoverCount = 0;
|
|
26534
|
-
const css$
|
|
26611
|
+
const css$S = /* css */`
|
|
26535
26612
|
@layer navi {
|
|
26536
26613
|
.navi_popover {
|
|
26537
26614
|
/* soft: user-configurable preferred max-height. Kept as a *default*
|
|
@@ -26901,7 +26978,7 @@ const css$R = /* css */`
|
|
|
26901
26978
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
26902
26979
|
*/
|
|
26903
26980
|
const Popover = props => {
|
|
26904
|
-
import.meta.css = [css$
|
|
26981
|
+
import.meta.css = [css$S, "@jsenv/navi/src/layout/popover.jsx"];
|
|
26905
26982
|
if (props.openController) {
|
|
26906
26983
|
return jsx(ControlledPopover, {
|
|
26907
26984
|
...props
|
|
@@ -27865,7 +27942,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
27865
27942
|
* event, and a caller replacing the body entirely then has one protocol to
|
|
27866
27943
|
* follow — `--navi-confirm` for yes, anything that closes for no.
|
|
27867
27944
|
*/
|
|
27868
|
-
const css$
|
|
27945
|
+
const css$R = /* css */`
|
|
27869
27946
|
/* The width lives on the body rather than on the popup, so that custom
|
|
27870
27947
|
content (which replaces this body entirely) sizes itself instead of
|
|
27871
27948
|
inheriting a ceiling meant for a sentence-long question. */
|
|
@@ -28002,7 +28079,7 @@ const ConfirmPopup = ({
|
|
|
28002
28079
|
onAnswer,
|
|
28003
28080
|
onClosed
|
|
28004
28081
|
}) => {
|
|
28005
|
-
import.meta.css = [css$
|
|
28082
|
+
import.meta.css = [css$R, "@jsenv/navi/src/action/confirm_popup.jsx"];
|
|
28006
28083
|
const {
|
|
28007
28084
|
mode,
|
|
28008
28085
|
confirmLabel,
|
|
@@ -28086,7 +28163,7 @@ const defaultBody = (message, {
|
|
|
28086
28163
|
});
|
|
28087
28164
|
};
|
|
28088
28165
|
|
|
28089
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
28166
|
+
installImportMetaCssBuild(import.meta);const css$Q = /* css */`
|
|
28090
28167
|
.action_error {
|
|
28091
28168
|
margin-top: 0;
|
|
28092
28169
|
margin-bottom: 20px;
|
|
@@ -28111,7 +28188,7 @@ const ActionRenderer = ({
|
|
|
28111
28188
|
children,
|
|
28112
28189
|
disabled
|
|
28113
28190
|
}) => {
|
|
28114
|
-
import.meta.css = [css$
|
|
28191
|
+
import.meta.css = [css$Q, "@jsenv/navi/src/action/action_renderer.jsx"];
|
|
28115
28192
|
if (action === undefined) {
|
|
28116
28193
|
throw new Error("ActionRenderer requires an action to render, but none was provided.");
|
|
28117
28194
|
}
|
|
@@ -31360,6 +31437,15 @@ const useActionAsyncData = (action, {
|
|
|
31360
31437
|
return [staleData, true, undefined];
|
|
31361
31438
|
}
|
|
31362
31439
|
|
|
31440
|
+
// An action without params has nothing to run and no one to start it: this is
|
|
31441
|
+
// where a route action lands when its params getter returns false. It is not a
|
|
31442
|
+
// load in progress, so there is nothing to wait for — suspending here would
|
|
31443
|
+
// throw a promise that never settles, and the whole <Loading> subtree would
|
|
31444
|
+
// stay hidden for good, silently.
|
|
31445
|
+
if (runningState !== RUNNING && action.paramsSignal.peek() === undefined) {
|
|
31446
|
+
return [action.dataSignal.peek(), false, undefined];
|
|
31447
|
+
}
|
|
31448
|
+
|
|
31363
31449
|
// IDLE or RUNNING with loadingEffect: "delegate" — suspend
|
|
31364
31450
|
const reason = runningState === RUNNING ? "loading" : "idle";
|
|
31365
31451
|
loadingRef.current = {
|
|
@@ -33534,11 +33620,12 @@ const collectBranches = children => {
|
|
|
33534
33620
|
type: "container",
|
|
33535
33621
|
node: child
|
|
33536
33622
|
};
|
|
33623
|
+
const guardMatching = route ? route.matchingSignal.value : false;
|
|
33537
33624
|
if (!matchingBranch) {
|
|
33538
33625
|
if (matchingChild) {
|
|
33539
33626
|
// Real leaf match inside — always select this container
|
|
33540
33627
|
matchingBranch = branch;
|
|
33541
|
-
} else if (
|
|
33628
|
+
} else if (guardMatching) {
|
|
33542
33629
|
// No leaf match but an explicit route guard matches — select this
|
|
33543
33630
|
// container so it can render its own fallback inside its layout
|
|
33544
33631
|
matchingBranch = branch;
|
|
@@ -33556,7 +33643,12 @@ const collectBranches = children => {
|
|
|
33556
33643
|
type: "leaf",
|
|
33557
33644
|
node: child
|
|
33558
33645
|
};
|
|
33559
|
-
|
|
33646
|
+
// every signal is read even once a match is found: reading is what
|
|
33647
|
+
// subscribes the container to it, and a branch that is skipped today is
|
|
33648
|
+
// the one that must wake the container up tomorrow
|
|
33649
|
+
const matching = route.matchingSignal.value;
|
|
33650
|
+
const paramsMatching = routeParams ? route.matchesParams(routeParams) : true;
|
|
33651
|
+
if (!matchingBranch && matching && paramsMatching) {
|
|
33560
33652
|
matchingBranch = branch;
|
|
33561
33653
|
}
|
|
33562
33654
|
}
|
|
@@ -35142,7 +35234,7 @@ const PhoneSvg = () => {
|
|
|
35142
35234
|
};
|
|
35143
35235
|
|
|
35144
35236
|
installImportMetaCssBuild(import.meta);// # TextAnchor — how it works
|
|
35145
|
-
const css$
|
|
35237
|
+
const css$P = /* css */`
|
|
35146
35238
|
.navi_text_anchor {
|
|
35147
35239
|
vertical-align: baseline;
|
|
35148
35240
|
user-select: none;
|
|
@@ -35177,7 +35269,7 @@ const TextAnchor = ({
|
|
|
35177
35269
|
textSize,
|
|
35178
35270
|
lineLayout
|
|
35179
35271
|
}) => {
|
|
35180
|
-
import.meta.css = [css$
|
|
35272
|
+
import.meta.css = [css$P, "@jsenv/navi/src/text/text_anchor.jsx"];
|
|
35181
35273
|
const anchorRef = useRef();
|
|
35182
35274
|
|
|
35183
35275
|
// Plain useLayoutEffect would also fire while an ancestor dialog/popover
|
|
@@ -35292,7 +35384,7 @@ const computeTopOffset = ({
|
|
|
35292
35384
|
};
|
|
35293
35385
|
const charTopCanvas = document.createElement("canvas");
|
|
35294
35386
|
|
|
35295
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
35387
|
+
installImportMetaCssBuild(import.meta);const css$O = /* css */`
|
|
35296
35388
|
@layer navi {
|
|
35297
35389
|
/* Ensure data attributes from box.jsx can win to update display */
|
|
35298
35390
|
.navi_icon {
|
|
@@ -35450,7 +35542,7 @@ const Icon = ({
|
|
|
35450
35542
|
fillLine,
|
|
35451
35543
|
...props
|
|
35452
35544
|
}) => {
|
|
35453
|
-
import.meta.css = [css$
|
|
35545
|
+
import.meta.css = [css$O, "@jsenv/navi/src/text/icon.jsx"];
|
|
35454
35546
|
const innerChildren = href ? jsx("svg", {
|
|
35455
35547
|
width: "100%",
|
|
35456
35548
|
height: "100%",
|
|
@@ -35603,7 +35695,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
|
|
|
35603
35695
|
});
|
|
35604
35696
|
};
|
|
35605
35697
|
|
|
35606
|
-
installImportMetaCssBuild(import.meta);const css$
|
|
35698
|
+
installImportMetaCssBuild(import.meta);const css$N = /* css */`
|
|
35607
35699
|
@layer navi {
|
|
35608
35700
|
.navi_link {
|
|
35609
35701
|
--link-border-radius: unset;
|
|
@@ -36042,7 +36134,7 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
36042
36134
|
* @param {boolean} [props.readOnly]
|
|
36043
36135
|
*/
|
|
36044
36136
|
const Link = props => {
|
|
36045
|
-
import.meta.css = [css$
|
|
36137
|
+
import.meta.css = [css$N, "@jsenv/navi/src/nav/link/link.jsx"];
|
|
36046
36138
|
if (props.route) {
|
|
36047
36139
|
return jsx(LinkWithRoute, {
|
|
36048
36140
|
...props
|
|
@@ -36277,7 +36369,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
36277
36369
|
* TabList component with support for horizontal and vertical layouts
|
|
36278
36370
|
* https://dribbble.com/search/tabs
|
|
36279
36371
|
*/
|
|
36280
|
-
const css$
|
|
36372
|
+
const css$M = /* css */`
|
|
36281
36373
|
@layer navi {
|
|
36282
36374
|
.navi_nav {
|
|
36283
36375
|
--nav-border: none;
|
|
@@ -36452,7 +36544,7 @@ const Nav = ({
|
|
|
36452
36544
|
// "before" or "after": which side the panel sits on, turning the nav into folder tabs
|
|
36453
36545
|
...props
|
|
36454
36546
|
}) => {
|
|
36455
|
-
import.meta.css = [css$
|
|
36547
|
+
import.meta.css = [css$M, "@jsenv/navi/src/nav/link/nav.jsx"];
|
|
36456
36548
|
children = toChildArray(children);
|
|
36457
36549
|
return jsx(Box, {
|
|
36458
36550
|
as: "nav",
|
|
@@ -36844,7 +36936,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
36844
36936
|
* Border width participates in layout (it is added to the tab and page
|
|
36845
36937
|
* padding): a thick border grows the binder rather than eating into the text.
|
|
36846
36938
|
*/
|
|
36847
|
-
const css$
|
|
36939
|
+
const css$L = /* css */`
|
|
36848
36940
|
@layer navi {
|
|
36849
36941
|
.navi_binder {
|
|
36850
36942
|
--binder-border-width: var(--navi-control-border-width);
|
|
@@ -37157,7 +37249,7 @@ const Binder = ({
|
|
|
37157
37249
|
pagePadding,
|
|
37158
37250
|
...props
|
|
37159
37251
|
}) => {
|
|
37160
|
-
import.meta.css = [css$
|
|
37252
|
+
import.meta.css = [css$L, "@jsenv/navi/src/nav/binder/binder.jsx"];
|
|
37161
37253
|
const items = toChildArray(children).map((child, index) => {
|
|
37162
37254
|
const {
|
|
37163
37255
|
value: itemValue,
|
|
@@ -37579,7 +37671,7 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
37579
37671
|
* into the size; a box-shadow draws the identical line and stays out of
|
|
37580
37672
|
* layout.
|
|
37581
37673
|
*/
|
|
37582
|
-
const css$
|
|
37674
|
+
const css$K = /* css */`
|
|
37583
37675
|
@layer navi {
|
|
37584
37676
|
:root {
|
|
37585
37677
|
--navi-fixed-bar-width: 56px;
|
|
@@ -37717,7 +37809,7 @@ const FixedBar = ({
|
|
|
37717
37809
|
border = true,
|
|
37718
37810
|
...props
|
|
37719
37811
|
}) => {
|
|
37720
|
-
import.meta.css = [css$
|
|
37812
|
+
import.meta.css = [css$K, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
|
|
37721
37813
|
const defaultRef = useRef();
|
|
37722
37814
|
props.ref = props.ref || defaultRef;
|
|
37723
37815
|
// Whichever of width/height crosses the edge the bar sits on is what the
|
|
@@ -37754,6 +37846,178 @@ const FixedBar = ({
|
|
|
37754
37846
|
});
|
|
37755
37847
|
};
|
|
37756
37848
|
|
|
37849
|
+
/**
|
|
37850
|
+
* Finds the element that is too wide for the page and says which one it is.
|
|
37851
|
+
*
|
|
37852
|
+
* A page that overflows horizontally is a bug wherever it happens, and on
|
|
37853
|
+
* Chrome Android it is a catastrophic one: the layout viewport inflates to the
|
|
37854
|
+
* content and `position: fixed` centering goes with it (see
|
|
37855
|
+
* docs/MOBILE_LAYOUT_PITFALLS.md). The remedy there is a wrapper in
|
|
37856
|
+
* `overflow-x: clip`, and it works — at the price of making the cause
|
|
37857
|
+
* invisible: nothing sticks out anymore, so nothing says a fixed width, a
|
|
37858
|
+
* `min-width` or an unbreakable string is still oversized. This puts the
|
|
37859
|
+
* signal back, without giving up the net.
|
|
37860
|
+
*
|
|
37861
|
+
* What counts as "too wide" is measured against the box that clips, not
|
|
37862
|
+
* against the document: with `clip` there is no scrollable overflow to read
|
|
37863
|
+
* (`scrollWidth` reports none), so the rectangles of the descendants are what
|
|
37864
|
+
* tells.
|
|
37865
|
+
*
|
|
37866
|
+
* Two things are deliberately not reported, because they cannot reach the
|
|
37867
|
+
* document:
|
|
37868
|
+
* - anything inside a box that clips or scrolls on its own — a wide table in
|
|
37869
|
+
* its `overflow-x: auto` container is doing exactly what it should;
|
|
37870
|
+
* - anything out of flow in the viewport's own coordinates (`position: fixed`,
|
|
37871
|
+
* the top layer), which contributes nothing to the document's overflow.
|
|
37872
|
+
*/
|
|
37873
|
+
|
|
37874
|
+
|
|
37875
|
+
// Subpixel layout rounds rectangles up on boxes that fit exactly.
|
|
37876
|
+
const OVERFLOW_TOLERANCE = 1;
|
|
37877
|
+
|
|
37878
|
+
const css$J = /* css */ `
|
|
37879
|
+
[data-navi-overflow-x] {
|
|
37880
|
+
outline: 2px dashed #e74c3c;
|
|
37881
|
+
outline-offset: -2px;
|
|
37882
|
+
}
|
|
37883
|
+
`;
|
|
37884
|
+
|
|
37885
|
+
/**
|
|
37886
|
+
* @param {object} [options]
|
|
37887
|
+
* @param {Element} [options.root=document.body] The box the content must fit
|
|
37888
|
+
* in — the wrapper carrying `overflow-x: clip`, when there is one.
|
|
37889
|
+
* @param {boolean} [options.highlight=true] Outline the culprits on screen.
|
|
37890
|
+
* @param {(overflows: Array<{element: Element, overflow: number, side: "left"|"right"}>) => void} [options.onDetect]
|
|
37891
|
+
* Replaces the default console warning.
|
|
37892
|
+
* @returns {() => void} Stops watching.
|
|
37893
|
+
*/
|
|
37894
|
+
const detectHorizontalOverflow = ({
|
|
37895
|
+
root = document.body,
|
|
37896
|
+
highlight = true,
|
|
37897
|
+
onDetect = warnOverflows,
|
|
37898
|
+
} = {}) => {
|
|
37899
|
+
let styleEl = null;
|
|
37900
|
+
if (highlight) {
|
|
37901
|
+
styleEl = document.createElement("style");
|
|
37902
|
+
styleEl.textContent = css$J;
|
|
37903
|
+
document.head.appendChild(styleEl);
|
|
37904
|
+
}
|
|
37905
|
+
|
|
37906
|
+
let highlightedSet = new Set();
|
|
37907
|
+
const detect = () => {
|
|
37908
|
+
const overflows = findOverflows(root);
|
|
37909
|
+
if (highlight) {
|
|
37910
|
+
const nextHighlightedSet = new Set();
|
|
37911
|
+
for (const { element } of overflows) {
|
|
37912
|
+
element.setAttribute("data-navi-overflow-x", "");
|
|
37913
|
+
nextHighlightedSet.add(element);
|
|
37914
|
+
}
|
|
37915
|
+
for (const element of highlightedSet) {
|
|
37916
|
+
if (!nextHighlightedSet.has(element)) {
|
|
37917
|
+
element.removeAttribute("data-navi-overflow-x");
|
|
37918
|
+
}
|
|
37919
|
+
}
|
|
37920
|
+
highlightedSet = nextHighlightedSet;
|
|
37921
|
+
}
|
|
37922
|
+
if (overflows.length) {
|
|
37923
|
+
onDetect(overflows);
|
|
37924
|
+
}
|
|
37925
|
+
};
|
|
37926
|
+
|
|
37927
|
+
// Measuring inside a resize callback is what makes the loop; wait for the
|
|
37928
|
+
// frame that resize produced.
|
|
37929
|
+
let frame = null;
|
|
37930
|
+
const requestDetect = () => {
|
|
37931
|
+
if (frame !== null) {
|
|
37932
|
+
return;
|
|
37933
|
+
}
|
|
37934
|
+
frame = requestAnimationFrame(() => {
|
|
37935
|
+
frame = null;
|
|
37936
|
+
detect();
|
|
37937
|
+
});
|
|
37938
|
+
};
|
|
37939
|
+
|
|
37940
|
+
// Resize catches the window narrowing and the content growing (the root gets
|
|
37941
|
+
// taller); mutations catch a wide element arriving without changing the
|
|
37942
|
+
// root's size.
|
|
37943
|
+
const resizeObserver = new ResizeObserver(requestDetect);
|
|
37944
|
+
resizeObserver.observe(root);
|
|
37945
|
+
const mutationObserver = new MutationObserver(requestDetect);
|
|
37946
|
+
mutationObserver.observe(root, { subtree: true, childList: true });
|
|
37947
|
+
requestDetect();
|
|
37948
|
+
|
|
37949
|
+
return () => {
|
|
37950
|
+
resizeObserver.disconnect();
|
|
37951
|
+
mutationObserver.disconnect();
|
|
37952
|
+
if (frame !== null) {
|
|
37953
|
+
cancelAnimationFrame(frame);
|
|
37954
|
+
frame = null;
|
|
37955
|
+
}
|
|
37956
|
+
for (const element of highlightedSet) {
|
|
37957
|
+
element.removeAttribute("data-navi-overflow-x");
|
|
37958
|
+
}
|
|
37959
|
+
highlightedSet.clear();
|
|
37960
|
+
if (styleEl) {
|
|
37961
|
+
styleEl.remove();
|
|
37962
|
+
}
|
|
37963
|
+
};
|
|
37964
|
+
};
|
|
37965
|
+
|
|
37966
|
+
const warnOverflows = (overflows) => {
|
|
37967
|
+
for (const { element, overflow, side } of overflows) {
|
|
37968
|
+
console.warn(
|
|
37969
|
+
`${getElementSignature(element)} overflows the page by ${Math.round(
|
|
37970
|
+
overflow,
|
|
37971
|
+
)}px on the ${side}. Look for a width in px, a min-width, or an unbreakable string; if it is meant to be wider than the screen, give it its own "overflow-x: auto".`,
|
|
37972
|
+
element,
|
|
37973
|
+
);
|
|
37974
|
+
}
|
|
37975
|
+
};
|
|
37976
|
+
|
|
37977
|
+
const findOverflows = (root) => {
|
|
37978
|
+
const rootRect = root.getBoundingClientRect();
|
|
37979
|
+
const rootStyle = getComputedStyle(root);
|
|
37980
|
+
// Only the end side is watched: what sticks out there is what the document
|
|
37981
|
+
// grows to hold, and what inflates the layout viewport. Past the start edge
|
|
37982
|
+
// the content is simply unreachable, and that is where the offscreen
|
|
37983
|
+
// patterns (a label parked at `left: -9999px`) live.
|
|
37984
|
+
const side = rootStyle.direction === "rtl" ? "left" : "right";
|
|
37985
|
+
const overflows = [];
|
|
37986
|
+
const collect = (parentEl) => {
|
|
37987
|
+
for (const el of parentEl.children) {
|
|
37988
|
+
const style = getComputedStyle(el);
|
|
37989
|
+
if (style.display === "none" || style.position === "fixed") {
|
|
37990
|
+
continue;
|
|
37991
|
+
}
|
|
37992
|
+
if (el.hasAttribute("popover") || el.tagName === "DIALOG") {
|
|
37993
|
+
continue;
|
|
37994
|
+
}
|
|
37995
|
+
const rect = el.getBoundingClientRect();
|
|
37996
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
37997
|
+
continue;
|
|
37998
|
+
}
|
|
37999
|
+
const overflow =
|
|
38000
|
+
side === "right"
|
|
38001
|
+
? rect.right - rootRect.right
|
|
38002
|
+
: rootRect.left - rect.left;
|
|
38003
|
+
if (overflow > OVERFLOW_TOLERANCE) {
|
|
38004
|
+
// The outermost box that sticks out is the one to fix; its children
|
|
38005
|
+
// stick out because it does.
|
|
38006
|
+
overflows.push({ element: el, overflow, side });
|
|
38007
|
+
continue;
|
|
38008
|
+
}
|
|
38009
|
+
// A box holding its own horizontal overflow cannot leak into the page,
|
|
38010
|
+
// whatever it holds — a wide table in its own scroll box is right.
|
|
38011
|
+
if (style.overflowX !== "visible") {
|
|
38012
|
+
continue;
|
|
38013
|
+
}
|
|
38014
|
+
collect(el);
|
|
38015
|
+
}
|
|
38016
|
+
};
|
|
38017
|
+
collect(root);
|
|
38018
|
+
return overflows;
|
|
38019
|
+
};
|
|
38020
|
+
|
|
37757
38021
|
const useFocusGroup = (
|
|
37758
38022
|
elementRef,
|
|
37759
38023
|
{
|
|
@@ -46956,7 +47220,7 @@ const css$u = /* css */`
|
|
|
46956
47220
|
around it does. Its own scroll box must then be transparent to
|
|
46957
47221
|
layout — otherwise it would cap the list at a height of its own and
|
|
46958
47222
|
start a second, nested scroll inside the page's. */
|
|
46959
|
-
&[data-scroller
|
|
47223
|
+
&[data-scroller] {
|
|
46960
47224
|
max-height: none;
|
|
46961
47225
|
overflow: visible;
|
|
46962
47226
|
|
|
@@ -46966,6 +47230,14 @@ const css$u = /* css */`
|
|
|
46966
47230
|
}
|
|
46967
47231
|
}
|
|
46968
47232
|
|
|
47233
|
+
/* Scrolling with the page means sticking to the viewport, and a FixedBar
|
|
47234
|
+
is in front of that viewport: without the offset a sticky label lands
|
|
47235
|
+
behind the bar. The bar publishes the room it takes (see
|
|
47236
|
+
fixed_bar_space.js) and it is 0px when there is no bar. */
|
|
47237
|
+
&[data-scroller="document"] {
|
|
47238
|
+
--x-list-group-label-top: var(--navi-fixed-bar-space-top, 0px);
|
|
47239
|
+
}
|
|
47240
|
+
|
|
46969
47241
|
&[data-expand-x] {
|
|
46970
47242
|
width: 100%;
|
|
46971
47243
|
}
|
|
@@ -47324,7 +47596,7 @@ const css$u = /* css */`
|
|
|
47324
47596
|
|
|
47325
47597
|
.navi_list_item_group_label {
|
|
47326
47598
|
position: sticky;
|
|
47327
|
-
top:
|
|
47599
|
+
top: var(--list-group-label-top, var(--x-list-group-label-top, 0px));
|
|
47328
47600
|
z-index: 1;
|
|
47329
47601
|
display: block;
|
|
47330
47602
|
background-color: var(--list-group-label-background-color);
|
|
@@ -47567,7 +47839,7 @@ const ListUI = props => {
|
|
|
47567
47839
|
baseClassName: "navi_list_container",
|
|
47568
47840
|
popover: popover,
|
|
47569
47841
|
"data-horizontal": horizontal ? "" : undefined,
|
|
47570
|
-
"data-scroller": scroller
|
|
47842
|
+
"data-scroller": getScrollerAttribute(scroller),
|
|
47571
47843
|
"data-expand-x": expandX || expand ? "" : undefined,
|
|
47572
47844
|
"data-expand-y": expandY || expand ? "" : undefined,
|
|
47573
47845
|
expandX: expandX,
|
|
@@ -47826,6 +48098,7 @@ const useListScrollSync = ({
|
|
|
47826
48098
|
setScrollerElResolved(current => current === scrollerElNow ? current : scrollerElNow);
|
|
47827
48099
|
};
|
|
47828
48100
|
useLayoutEffect(resolveScroller);
|
|
48101
|
+
useStickyScrollportWarning();
|
|
47829
48102
|
|
|
47830
48103
|
// The row the scroll holds onto across a change of geometry, and where it
|
|
47831
48104
|
// sat when that change was decided. Captured at the two moments the list
|
|
@@ -48603,6 +48876,28 @@ const getScrollerViewportRect = scrollerEl => {
|
|
|
48603
48876
|
}
|
|
48604
48877
|
return scrollerEl.getBoundingClientRect();
|
|
48605
48878
|
};
|
|
48879
|
+
const useStickyScrollportWarning = (ref, scroller) => {
|
|
48880
|
+
useRef(false);
|
|
48881
|
+
useLayoutEffect(() => {
|
|
48882
|
+
{
|
|
48883
|
+
return;
|
|
48884
|
+
}
|
|
48885
|
+
});
|
|
48886
|
+
};
|
|
48887
|
+
|
|
48888
|
+
// The CSS needs to tell "the page scrolls me" from "some box around me
|
|
48889
|
+
// scrolls me": only the first one sticks to the viewport, where the fixed bars
|
|
48890
|
+
// are.
|
|
48891
|
+
const getScrollerAttribute = scroller => {
|
|
48892
|
+
if (scroller === "self") {
|
|
48893
|
+
return undefined;
|
|
48894
|
+
}
|
|
48895
|
+
if (scroller === "document") {
|
|
48896
|
+
return "document";
|
|
48897
|
+
}
|
|
48898
|
+
return "parent";
|
|
48899
|
+
};
|
|
48900
|
+
|
|
48606
48901
|
// scroller="parent": the list virtualizes against the scroll box it lives in
|
|
48607
48902
|
// instead of one of its own. Which box that is can only be measured, and a
|
|
48608
48903
|
// measurement holds for the geometry it was taken on — see resolveScroller in
|
|
@@ -52047,6 +52342,10 @@ const css$p = /* css */`
|
|
|
52047
52342
|
keyboard here (see navi-focus-delegate below). */
|
|
52048
52343
|
outline-color: var(--navi-focus-outline-color);
|
|
52049
52344
|
outline-offset: 0px;
|
|
52345
|
+
/* No grey flash under a finger: what a press does is said by the chevron's
|
|
52346
|
+
own background, and the browser's rectangle is drawn square over corners
|
|
52347
|
+
that are round. Inherited, so the three pieces inside get it too. */
|
|
52348
|
+
-webkit-tap-highlight-color: var(--navi-control-tap-highlight-color);
|
|
52050
52349
|
}
|
|
52051
52350
|
/* The middle holds the keyboard, and this box wears its ring: whatever is in
|
|
52052
52351
|
there fills it, so a ring of its own would be drawn a pixel inside this
|
|
@@ -52173,7 +52472,12 @@ const css$p = /* css */`
|
|
|
52173
52472
|
border-radius: 0;
|
|
52174
52473
|
cursor: pointer;
|
|
52175
52474
|
}
|
|
52176
|
-
|
|
52475
|
+
/* Said as data-hover rather than :hover: a touch browser synthesizes the
|
|
52476
|
+
enter and never the leave, so a CSS :hover would stay grey under the last
|
|
52477
|
+
chevron pressed until something else was touched. What tracks it (see
|
|
52478
|
+
pseudo_styles.js) knows there is no hover on such a device and simply does
|
|
52479
|
+
not set the attribute. */
|
|
52480
|
+
.navi_picker_spin > .navi_picker_spin_way_out[data-hover] {
|
|
52177
52481
|
background: color-mix(in srgb, currentColor 8%, transparent);
|
|
52178
52482
|
}
|
|
52179
52483
|
/* Nothing that way: still there, still pressable — pressing it is how one
|
|
@@ -52199,22 +52503,28 @@ const css$p = /* css */`
|
|
|
52199
52503
|
of a rounded spin is rounded there too, and nowhere else — the two corners
|
|
52200
52504
|
it does not own stay at the 0 above. Said with inherit rather than clipped
|
|
52201
52505
|
away with overflow, which would cut the focus ring of the very button it
|
|
52202
|
-
rounds.
|
|
52506
|
+
rounds.
|
|
52507
|
+
Which chevron is which is asked of the chevron itself (data-way-out) rather
|
|
52508
|
+
than of its place among its siblings: the loading outline is a <span> too
|
|
52509
|
+
and it is written first, so :first-of-type named IT and the chevron at the
|
|
52510
|
+
start went unrounded. */
|
|
52203
52511
|
.navi_picker_spin:not([data-vertical])
|
|
52204
|
-
> .navi_picker_spin_way_out
|
|
52512
|
+
> .navi_picker_spin_way_out[data-way-out="start"] {
|
|
52205
52513
|
border-start-start-radius: inherit;
|
|
52206
52514
|
border-end-start-radius: inherit;
|
|
52207
52515
|
}
|
|
52208
52516
|
.navi_picker_spin:not([data-vertical])
|
|
52209
|
-
> .navi_picker_spin_way_out
|
|
52517
|
+
> .navi_picker_spin_way_out[data-way-out="end"] {
|
|
52210
52518
|
border-start-end-radius: inherit;
|
|
52211
52519
|
border-end-end-radius: inherit;
|
|
52212
52520
|
}
|
|
52213
|
-
.navi_picker_spin[data-vertical]
|
|
52521
|
+
.navi_picker_spin[data-vertical]
|
|
52522
|
+
> .navi_picker_spin_way_out[data-way-out="start"] {
|
|
52214
52523
|
border-start-start-radius: inherit;
|
|
52215
52524
|
border-start-end-radius: inherit;
|
|
52216
52525
|
}
|
|
52217
|
-
.navi_picker_spin[data-vertical]
|
|
52526
|
+
.navi_picker_spin[data-vertical]
|
|
52527
|
+
> .navi_picker_spin_way_out[data-way-out="end"] {
|
|
52218
52528
|
border-end-end-radius: inherit;
|
|
52219
52529
|
border-end-start-radius: inherit;
|
|
52220
52530
|
}
|
|
@@ -52455,6 +52765,7 @@ const Spin = ({
|
|
|
52455
52765
|
const wayOut = atStart => {
|
|
52456
52766
|
const isNext = atStart ? startIsNext : !startIsNext;
|
|
52457
52767
|
return jsx(WayOut, {
|
|
52768
|
+
atStart: atStart,
|
|
52458
52769
|
unavailableMessage: wayOutMessage(atStart ? startAllowed : endAllowed, isNext ? "spin.nothing_after" : "spin.nothing_before"),
|
|
52459
52770
|
label: isNext ? nextLabel ?? naviI18n("spin.next") : previousLabel ?? naviI18n("spin.previous"),
|
|
52460
52771
|
onPress: e => {
|
|
@@ -52632,6 +52943,7 @@ const Spin = ({
|
|
|
52632
52943
|
// the container's own tabIndex, or the field's), where the arrows already mean
|
|
52633
52944
|
// this.
|
|
52634
52945
|
const WayOut = ({
|
|
52946
|
+
atStart,
|
|
52635
52947
|
commandFor,
|
|
52636
52948
|
unavailableMessage,
|
|
52637
52949
|
label,
|
|
@@ -52640,6 +52952,17 @@ const WayOut = ({
|
|
|
52640
52952
|
}) => jsx(Box, {
|
|
52641
52953
|
as: "span",
|
|
52642
52954
|
baseClassName: "navi_picker_spin_way_out"
|
|
52955
|
+
// Which end of the box it sits in, said by the chevron rather than read
|
|
52956
|
+
// from its place among its siblings: that is what the corners it is
|
|
52957
|
+
// rounded by are keyed on (see the CSS above).
|
|
52958
|
+
,
|
|
52959
|
+
|
|
52960
|
+
"data-way-out": atStart ? "start" : "end"
|
|
52961
|
+
// Tracked rather than left to CSS :hover, which stays on after a tap on a
|
|
52962
|
+
// touch device (see the CSS above).
|
|
52963
|
+
,
|
|
52964
|
+
|
|
52965
|
+
pseudoClasses: WAY_OUT_PSEUDO_CLASSES
|
|
52643
52966
|
// Announced as a button because that is what it is to whoever cannot see
|
|
52644
52967
|
// the chevron — and marked unavailable rather than removed when there is
|
|
52645
52968
|
// nothing that way, so it keeps its place.
|
|
@@ -52691,6 +53014,7 @@ const WayOut = ({
|
|
|
52691
53014
|
})
|
|
52692
53015
|
});
|
|
52693
53016
|
const PICKER_SPIN_PSEUDO_CLASSES = [":hover", ":focus-visible"];
|
|
53017
|
+
const WAY_OUT_PSEUDO_CLASSES = [":hover"];
|
|
52694
53018
|
|
|
52695
53019
|
// A padding written on this box would sit between its border and the chevrons,
|
|
52696
53020
|
// which are meant to reach the corners they are rounded by — so each padding
|
|
@@ -62662,5 +62986,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
62662
62986
|
})
|
|
62663
62987
|
});
|
|
62664
62988
|
|
|
62665
|
-
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, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, 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, 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, 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 };
|
|
62989
|
+
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, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, 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, 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 };
|
|
62666
62990
|
//# sourceMappingURL=jsenv_navi.js.map
|