@jsenv/navi 0.29.409 → 0.29.411
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/dev/jsenv_navi.js +138 -68
- package/dist/dev/jsenv_navi.js.map +8 -8
- package/dist/jsenv_navi.js +138 -68
- package/dist/jsenv_navi.js.map +8 -8
- package/docs/AI_INSTRUCTIONS.md +2 -1
- package/docs/scroll.md +43 -0
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -13437,7 +13437,9 @@ const createControlValidation = (
|
|
|
13437
13437
|
|
|
13438
13438
|
let newConstraintValidityState = { valid: true };
|
|
13439
13439
|
const constraintSet = getConstraintSet();
|
|
13440
|
-
|
|
13440
|
+
// Only for the debug lines of a failure or a change: computed when one of
|
|
13441
|
+
// them is written, not at every check (every notch of a wheel).
|
|
13442
|
+
const elementSig = () => getElementSignature(controller.ref.current);
|
|
13441
13443
|
// Not logged: every control checks its constraints on every interaction and
|
|
13442
13444
|
// almost always passes, so this line alone was most of the debug output —
|
|
13443
13445
|
// and with devtools open, formatting it costs a frame. What matters is a
|
|
@@ -13463,7 +13465,7 @@ const createControlValidation = (
|
|
|
13463
13465
|
constraintValidityInfo.message,
|
|
13464
13466
|
);
|
|
13465
13467
|
debugUIState(
|
|
13466
|
-
`${elementSig} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
|
|
13468
|
+
`${elementSig()} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
|
|
13467
13469
|
);
|
|
13468
13470
|
const thisConstraintFailureInfo = {
|
|
13469
13471
|
name: constraint.name,
|
|
@@ -13535,7 +13537,7 @@ const createControlValidation = (
|
|
|
13535
13537
|
if (element) {
|
|
13536
13538
|
debugUIState(
|
|
13537
13539
|
event,
|
|
13538
|
-
`${elementSig} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
|
|
13540
|
+
`${elementSig()} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
|
|
13539
13541
|
);
|
|
13540
13542
|
dispatchPublicCustomEvent(element, NAVI_VALIDITY_CHANGE_CUSTOM_EVENT);
|
|
13541
13543
|
}
|
|
@@ -13581,7 +13583,7 @@ const createControlValidation = (
|
|
|
13581
13583
|
event,
|
|
13582
13584
|
{ report = false, fromRequestAction = false } = {},
|
|
13583
13585
|
) => {
|
|
13584
|
-
const elementSig = getElementSignature(controller.ref.current);
|
|
13586
|
+
const elementSig = () => getElementSignature(controller.ref.current);
|
|
13585
13587
|
const isValid = checkValidity({ event, fromRequestAction });
|
|
13586
13588
|
if (failingManagedControlValidity) {
|
|
13587
13589
|
// Group/form case: find the actual failing leaf and report on it.
|
|
@@ -13603,7 +13605,7 @@ const createControlValidation = (
|
|
|
13603
13605
|
if (report) {
|
|
13604
13606
|
debugUIState(
|
|
13605
13607
|
event,
|
|
13606
|
-
`syncValidity ${elementSig}: has failing constraint and report=true -> reportValidity`,
|
|
13608
|
+
`syncValidity ${elementSig()}: has failing constraint and report=true -> reportValidity`,
|
|
13607
13609
|
);
|
|
13608
13610
|
reportValidity({ event });
|
|
13609
13611
|
} else if (failedConstraintInfo.status === "error") {
|
|
@@ -13613,12 +13615,12 @@ const createControlValidation = (
|
|
|
13613
13615
|
// on the next action attempt via autoResetOnAction.
|
|
13614
13616
|
debugUIState(
|
|
13615
13617
|
event,
|
|
13616
|
-
`syncValidity ${elementSig}: has error constraint and report=false -> keep callout open`,
|
|
13618
|
+
`syncValidity ${elementSig()}: has error constraint and report=false -> keep callout open`,
|
|
13617
13619
|
);
|
|
13618
13620
|
} else {
|
|
13619
13621
|
debugUIState(
|
|
13620
13622
|
event,
|
|
13621
|
-
`syncValidity ${elementSig}: has failing constraint but report=false -> close callout if any`,
|
|
13623
|
+
`syncValidity ${elementSig()}: has failing constraint but report=false -> close callout if any`,
|
|
13622
13624
|
);
|
|
13623
13625
|
callout.removeOpenToken(VALIDATION_TOKEN, event);
|
|
13624
13626
|
}
|
|
@@ -27352,6 +27354,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
27352
27354
|
|
|
27353
27355
|
// Store previous route states to detect changes
|
|
27354
27356
|
const routePreviousStateMap = new WeakMap();
|
|
27357
|
+
let previousPath = null;
|
|
27355
27358
|
const updateRoutes = (
|
|
27356
27359
|
url,
|
|
27357
27360
|
{
|
|
@@ -27360,6 +27363,14 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
27360
27363
|
} = {},
|
|
27361
27364
|
) => {
|
|
27362
27365
|
currentUrl = url;
|
|
27366
|
+
// Whether a route matches is decided by the path alone: the search string
|
|
27367
|
+
// only fills the params (see matchUrl). An address that keeps its path — a
|
|
27368
|
+
// search param written, at every notch of a wheel bound to it — cannot make
|
|
27369
|
+
// a route start or stop matching, so only the routes already matching are
|
|
27370
|
+
// read again.
|
|
27371
|
+
const path = urlWithoutSearchAndHash(url);
|
|
27372
|
+
const pathKept = path === previousPath;
|
|
27373
|
+
previousPath = path;
|
|
27363
27374
|
const returnValue = {};
|
|
27364
27375
|
const routeMatchInfoSet = new Set();
|
|
27365
27376
|
for (const route of routeSet) {
|
|
@@ -27375,7 +27386,8 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
27375
27386
|
const oldParams = previousState.params;
|
|
27376
27387
|
const oldActionParams = previousState.actionParams;
|
|
27377
27388
|
|
|
27378
|
-
let extractedParams =
|
|
27389
|
+
let extractedParams =
|
|
27390
|
+
pathKept && !oldMatching ? null : routePattern.applyOn(url);
|
|
27379
27391
|
let newMatching = Boolean(extractedParams);
|
|
27380
27392
|
let newParams;
|
|
27381
27393
|
if (extractedParams) {
|
|
@@ -27444,6 +27456,25 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
27444
27456
|
}
|
|
27445
27457
|
}
|
|
27446
27458
|
|
|
27459
|
+
// What the matching routes say, read once for every connection of every
|
|
27460
|
+
// route that does not match (below), rather than by walking all the
|
|
27461
|
+
// routes again for each of those connections.
|
|
27462
|
+
const matchingRawParamsArray = [];
|
|
27463
|
+
const paramNameDeclaredByMatchingRouteSet = new Set();
|
|
27464
|
+
const matchingFamilyRootSet = new Set();
|
|
27465
|
+
for (const matchingRoute of matchingRouteSet) {
|
|
27466
|
+
matchingRawParamsArray.push(matchingRoute.rawParamsSignal.value);
|
|
27467
|
+
const matchingPattern =
|
|
27468
|
+
getRoutePrivateProperties(matchingRoute).routePattern;
|
|
27469
|
+
for (const paramName of matchingPattern.queryConnectionMap.keys()) {
|
|
27470
|
+
paramNameDeclaredByMatchingRouteSet.add(paramName);
|
|
27471
|
+
}
|
|
27472
|
+
for (const paramName of matchingPattern.pathConnectionMap.keys()) {
|
|
27473
|
+
paramNameDeclaredByMatchingRouteSet.add(paramName);
|
|
27474
|
+
}
|
|
27475
|
+
matchingFamilyRootSet.add(matchingPattern.familyRoot);
|
|
27476
|
+
}
|
|
27477
|
+
|
|
27447
27478
|
for (const {
|
|
27448
27479
|
route,
|
|
27449
27480
|
routePrivateProperties,
|
|
@@ -27459,40 +27490,21 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
27459
27490
|
const urlParamValue = rawParams[paramName];
|
|
27460
27491
|
|
|
27461
27492
|
if (!newMatching) {
|
|
27462
|
-
//
|
|
27463
|
-
|
|
27464
|
-
|
|
27465
|
-
|
|
27466
|
-
|
|
27467
|
-
|
|
27468
|
-
|
|
27469
|
-
|
|
27470
|
-
|
|
27471
|
-
|
|
27472
|
-
|
|
27473
|
-
|
|
27474
|
-
|
|
27475
|
-
|
|
27476
|
-
|
|
27477
|
-
|
|
27478
|
-
// Same param, on the page one is arriving at: what it is worth
|
|
27479
|
-
// there is that page's business, url or no url — it may open on
|
|
27480
|
-
// a default of its own (see searchParams' `{ signal, default }`).
|
|
27481
|
-
const otherPattern =
|
|
27482
|
-
getRoutePrivateProperties(otherRoute).routePattern;
|
|
27483
|
-
if (
|
|
27484
|
-
otherPattern.queryConnectionMap.has(paramName) ||
|
|
27485
|
-
otherPattern.pathConnectionMap.has(paramName)
|
|
27486
|
-
) {
|
|
27487
|
-
parameterDeclaredByMatchingRoute = true;
|
|
27488
|
-
}
|
|
27489
|
-
|
|
27490
|
-
// Same family = same topmost ancestor
|
|
27491
|
-
// (familyRoot, computed in setupRoutePatterns)
|
|
27492
|
-
if (otherPattern.familyRoot === routePattern.familyRoot) {
|
|
27493
|
-
matchingRouteInSameFamily = true;
|
|
27494
|
-
}
|
|
27495
|
-
}
|
|
27493
|
+
// Whether a matching route extracts this parameter from the url
|
|
27494
|
+
const parameterExtractedByMatchingRoute =
|
|
27495
|
+
matchingRawParamsArray.some(
|
|
27496
|
+
(matchingRawParams) => paramName in matchingRawParams,
|
|
27497
|
+
);
|
|
27498
|
+
// Same param, on the page one is arriving at: what it is worth
|
|
27499
|
+
// there is that page's business, url or no url — it may open on
|
|
27500
|
+
// a default of its own (see searchParams' `{ signal, default }`).
|
|
27501
|
+
const parameterDeclaredByMatchingRoute =
|
|
27502
|
+
paramNameDeclaredByMatchingRouteSet.has(paramName);
|
|
27503
|
+
// Same family = same topmost ancestor
|
|
27504
|
+
// (familyRoot, computed in setupRoutePatterns)
|
|
27505
|
+
const matchingRouteInSameFamily = matchingFamilyRootSet.has(
|
|
27506
|
+
routePattern.familyRoot,
|
|
27507
|
+
);
|
|
27496
27508
|
|
|
27497
27509
|
// A weak param qualifies one visit: leaving the route ends it,
|
|
27498
27510
|
// whatever the family and whatever the default. Coming back by a
|
|
@@ -27725,6 +27737,11 @@ const getParamsCacheKey = (params) => {
|
|
|
27725
27737
|
return key;
|
|
27726
27738
|
};
|
|
27727
27739
|
|
|
27740
|
+
const urlWithoutSearchAndHash = (url) => {
|
|
27741
|
+
const urlObject = new URL(url);
|
|
27742
|
+
return `${urlObject.origin}${urlObject.pathname}`;
|
|
27743
|
+
};
|
|
27744
|
+
|
|
27728
27745
|
/**
|
|
27729
27746
|
* Is there an entry of THIS document behind the current one? And ahead of it?
|
|
27730
27747
|
*
|
|
@@ -28168,7 +28185,13 @@ const forgetScrollerUnlessPageLeft = (name) => {
|
|
|
28168
28185
|
}
|
|
28169
28186
|
};
|
|
28170
28187
|
|
|
28171
|
-
|
|
28188
|
+
// `currentUrlString`: the address the href is compared with — the document's by
|
|
28189
|
+
// default. A caller subscribed to documentUrlSignal passes the value it read,
|
|
28190
|
+
// which is ahead of window.location while a debounced write is pending.
|
|
28191
|
+
const getHrefTargetInfo = (
|
|
28192
|
+
href,
|
|
28193
|
+
currentUrlString = window.location.href,
|
|
28194
|
+
) => {
|
|
28172
28195
|
href = String(href);
|
|
28173
28196
|
|
|
28174
28197
|
if (!href || href.trim() === "") {
|
|
@@ -28181,8 +28204,8 @@ const getHrefTargetInfo = (href) => {
|
|
|
28181
28204
|
};
|
|
28182
28205
|
}
|
|
28183
28206
|
|
|
28184
|
-
const currentUrl = new URL(
|
|
28185
|
-
const targetUrl = new URL(href,
|
|
28207
|
+
const currentUrl = new URL(currentUrlString);
|
|
28208
|
+
const targetUrl = new URL(href, currentUrlString);
|
|
28186
28209
|
|
|
28187
28210
|
let isCurrent = false;
|
|
28188
28211
|
{
|
|
@@ -28224,6 +28247,21 @@ const getHrefTargetInfo = (href) => {
|
|
|
28224
28247
|
};
|
|
28225
28248
|
};
|
|
28226
28249
|
|
|
28250
|
+
// A field of getHrefTargetInfo that depends on the page one is on (isCurrent,
|
|
28251
|
+
// isAnchor), read through a computed of that one boolean: the caller re-renders
|
|
28252
|
+
// when it flips, not at every address write — a search param bound to a wheel
|
|
28253
|
+
// writes the address at every notch.
|
|
28254
|
+
const useHrefTargetFlag = (href, flagName) => {
|
|
28255
|
+
const flagSignal = useMemo(
|
|
28256
|
+
() =>
|
|
28257
|
+
computed(
|
|
28258
|
+
() => getHrefTargetInfo(href, documentUrlSignal.value)[flagName],
|
|
28259
|
+
),
|
|
28260
|
+
[href, flagName],
|
|
28261
|
+
);
|
|
28262
|
+
return flagSignal.value;
|
|
28263
|
+
};
|
|
28264
|
+
|
|
28227
28265
|
/*
|
|
28228
28266
|
* An app can ship more than one document — an admin panel, a status page, an
|
|
28229
28267
|
* entry point of its own — and a link to one of them is not a route, it is a
|
|
@@ -46729,19 +46767,28 @@ const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
|
|
|
46729
46767
|
|
|
46730
46768
|
/**
|
|
46731
46769
|
* Hook that reactively checks if a URL is visited.
|
|
46732
|
-
* Re-renders when
|
|
46770
|
+
* Re-renders when that answer changes — not at every url added to the visited
|
|
46771
|
+
* set, which grows at every new address (a search param bound to a wheel
|
|
46772
|
+
* writes one at every notch).
|
|
46733
46773
|
*
|
|
46734
46774
|
* @param {string} url - The URL to check
|
|
46735
46775
|
* @returns {boolean} Whether the URL has been visited
|
|
46736
46776
|
*/
|
|
46737
46777
|
const useIsVisited = (url) => {
|
|
46738
|
-
|
|
46739
|
-
|
|
46740
|
-
|
|
46741
|
-
|
|
46742
|
-
|
|
46743
|
-
|
|
46744
|
-
|
|
46778
|
+
const visitedSignal = useMemo(
|
|
46779
|
+
() =>
|
|
46780
|
+
computed(() => {
|
|
46781
|
+
// Both are what the answer depends on: the set, and the address a
|
|
46782
|
+
// relative url is resolved against (see isVisited).
|
|
46783
|
+
// eslint-disable-next-line no-unused-expressions
|
|
46784
|
+
visitedUrlsSignal.value;
|
|
46785
|
+
// eslint-disable-next-line no-unused-expressions
|
|
46786
|
+
documentUrlSignal.value;
|
|
46787
|
+
return isVisited(url);
|
|
46788
|
+
}),
|
|
46789
|
+
[url],
|
|
46790
|
+
);
|
|
46791
|
+
return visitedSignal.value;
|
|
46745
46792
|
};
|
|
46746
46793
|
|
|
46747
46794
|
/**
|
|
@@ -47401,13 +47448,14 @@ const LinkPlain = props => {
|
|
|
47401
47448
|
const loading = basePseudoState[":-navi-loading"];
|
|
47402
47449
|
const shouldDimColor = readOnly || disabled;
|
|
47403
47450
|
useDimColorWhen(props.ref, shouldDimColor);
|
|
47404
|
-
//
|
|
47405
|
-
|
|
47451
|
+
// Re-rendered when one of these flips, not at every address write (see
|
|
47452
|
+
// useHrefTargetFlag). Whether the href leaves the site does not depend on
|
|
47453
|
+
// the address.
|
|
47454
|
+
const isCurrent = useHrefTargetFlag(href, "isCurrent");
|
|
47455
|
+
const isAnchor = useHrefTargetFlag(href, "isAnchor");
|
|
47406
47456
|
const {
|
|
47407
|
-
isSameSite
|
|
47408
|
-
|
|
47409
|
-
isCurrent
|
|
47410
|
-
} = getHrefTargetInfo(href);
|
|
47457
|
+
isSameSite
|
|
47458
|
+
} = getHrefTargetInfo(href, documentUrlSignal.peek());
|
|
47411
47459
|
// A tab that is a SLIDE is current when the container is on it — which the
|
|
47412
47460
|
// <Nav> around reads off that container, so nothing here has to be told.
|
|
47413
47461
|
const innerCurrent = current || (slide ? nav?.currentSlideArea === slide : isCurrent);
|
|
@@ -52373,10 +52421,9 @@ const ButtonHrefResolver = props => {
|
|
|
52373
52421
|
});
|
|
52374
52422
|
};
|
|
52375
52423
|
|
|
52376
|
-
//
|
|
52377
|
-
//
|
|
52378
|
-
//
|
|
52379
|
-
// would otherwise pay for each write.
|
|
52424
|
+
// Whether the href is the page one is on depends on the address, and re-renders
|
|
52425
|
+
// the button only when it flips (see useHrefTargetFlag). Whether it leaves the
|
|
52426
|
+
// site does not depend on the address.
|
|
52380
52427
|
const ButtonWithHref = props => {
|
|
52381
52428
|
const Next = useNextResolver();
|
|
52382
52429
|
const {
|
|
@@ -52385,12 +52432,10 @@ const ButtonWithHref = props => {
|
|
|
52385
52432
|
rel,
|
|
52386
52433
|
pseudoState
|
|
52387
52434
|
} = props;
|
|
52388
|
-
|
|
52389
|
-
useDocumentUrl();
|
|
52435
|
+
const isCurrent = useHrefTargetFlag(href, "isCurrent");
|
|
52390
52436
|
const {
|
|
52391
|
-
isSameSite
|
|
52392
|
-
|
|
52393
|
-
} = getHrefTargetInfo(href);
|
|
52437
|
+
isSameSite
|
|
52438
|
+
} = getHrefTargetInfo(href, documentUrlSignal.peek());
|
|
52394
52439
|
return jsx(Next, {
|
|
52395
52440
|
...props,
|
|
52396
52441
|
target: target === undefined ? isSameSite ? undefined : "_blank" : target,
|
|
@@ -69770,6 +69815,11 @@ const css$x = /* css */`@layer navi {
|
|
|
69770
69815
|
display: flex;
|
|
69771
69816
|
overflow: hidden;
|
|
69772
69817
|
|
|
69818
|
+
&[data-borderless] {
|
|
69819
|
+
--x-list-border-radius: 0px;
|
|
69820
|
+
overflow: visible;
|
|
69821
|
+
}
|
|
69822
|
+
|
|
69773
69823
|
&[data-overflow-visible="both"] {
|
|
69774
69824
|
overflow: visible;
|
|
69775
69825
|
}
|
|
@@ -70454,6 +70504,7 @@ const ListUI = props => {
|
|
|
70454
70504
|
"data-item-transition": itemTransition ? "" : undefined,
|
|
70455
70505
|
popover: popover,
|
|
70456
70506
|
"data-horizontal": horizontal ? "" : undefined,
|
|
70507
|
+
"data-borderless": isBorderless(rest) ? "" : undefined,
|
|
70457
70508
|
"data-scroller": getScrollerAttribute(scroller),
|
|
70458
70509
|
"data-overflow-visible": getOverflowVisibleAttribute(overflow, overflowX, overflowY),
|
|
70459
70510
|
"navi-hover-while-scrolling": hoverWhileScrolling ? "" : undefined,
|
|
@@ -71824,6 +71875,25 @@ const getScrollerAttribute = scroller => {
|
|
|
71824
71875
|
return "parent";
|
|
71825
71876
|
};
|
|
71826
71877
|
|
|
71878
|
+
// A radius asked for explicitly is kept: a borderless list painting a
|
|
71879
|
+
// background of its own may want that surface rounded, and clipped to it.
|
|
71880
|
+
const isBorderless = ({
|
|
71881
|
+
border,
|
|
71882
|
+
borderWidth,
|
|
71883
|
+
borderRadius
|
|
71884
|
+
}) => {
|
|
71885
|
+
if (borderRadius !== undefined) {
|
|
71886
|
+
return false;
|
|
71887
|
+
}
|
|
71888
|
+
if (borderWidth !== undefined) {
|
|
71889
|
+
return parseFloat(borderWidth) === 0;
|
|
71890
|
+
}
|
|
71891
|
+
if (border === undefined) {
|
|
71892
|
+
return false;
|
|
71893
|
+
}
|
|
71894
|
+
return border === "none" || parseFloat(border) === 0;
|
|
71895
|
+
};
|
|
71896
|
+
|
|
71827
71897
|
// overflow lands as an inline style on the inner scroll element, which is
|
|
71828
71898
|
// enough for every value but "visible": that one only means anything once the
|
|
71829
71899
|
// frame around it stops clipping too. Reading the per-axis props over the
|