@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/dev/jsenv_navi.js
CHANGED
|
@@ -13920,7 +13920,9 @@ const createControlValidation = (
|
|
|
13920
13920
|
|
|
13921
13921
|
let newConstraintValidityState = { valid: true };
|
|
13922
13922
|
const constraintSet = getConstraintSet();
|
|
13923
|
-
|
|
13923
|
+
// Only for the debug lines of a failure or a change: computed when one of
|
|
13924
|
+
// them is written, not at every check (every notch of a wheel).
|
|
13925
|
+
const elementSig = () => getElementSignature(controller.ref.current);
|
|
13924
13926
|
// Not logged: every control checks its constraints on every interaction and
|
|
13925
13927
|
// almost always passes, so this line alone was most of the debug output —
|
|
13926
13928
|
// and with devtools open, formatting it costs a frame. What matters is a
|
|
@@ -13946,7 +13948,7 @@ const createControlValidation = (
|
|
|
13946
13948
|
constraintValidityInfo.message,
|
|
13947
13949
|
);
|
|
13948
13950
|
debugUIState(
|
|
13949
|
-
`${elementSig} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
|
|
13951
|
+
`${elementSig()} constraint "${constraint.name}" failed -> ${constraintValidityInfo.message}`,
|
|
13950
13952
|
);
|
|
13951
13953
|
const thisConstraintFailureInfo = {
|
|
13952
13954
|
name: constraint.name,
|
|
@@ -14018,7 +14020,7 @@ const createControlValidation = (
|
|
|
14018
14020
|
if (element) {
|
|
14019
14021
|
debugUIState(
|
|
14020
14022
|
event,
|
|
14021
|
-
`${elementSig} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
|
|
14023
|
+
`${elementSig()} constraint validity changed -> dispatch ${NAVI_VALIDITY_CHANGE_CUSTOM_EVENT}`,
|
|
14022
14024
|
);
|
|
14023
14025
|
dispatchPublicCustomEvent(element, NAVI_VALIDITY_CHANGE_CUSTOM_EVENT);
|
|
14024
14026
|
}
|
|
@@ -14064,7 +14066,7 @@ const createControlValidation = (
|
|
|
14064
14066
|
event,
|
|
14065
14067
|
{ report = false, fromRequestAction = false } = {},
|
|
14066
14068
|
) => {
|
|
14067
|
-
const elementSig = getElementSignature(controller.ref.current);
|
|
14069
|
+
const elementSig = () => getElementSignature(controller.ref.current);
|
|
14068
14070
|
const isValid = checkValidity({ event, fromRequestAction });
|
|
14069
14071
|
if (failingManagedControlValidity) {
|
|
14070
14072
|
// Group/form case: find the actual failing leaf and report on it.
|
|
@@ -14086,7 +14088,7 @@ const createControlValidation = (
|
|
|
14086
14088
|
if (report) {
|
|
14087
14089
|
debugUIState(
|
|
14088
14090
|
event,
|
|
14089
|
-
`syncValidity ${elementSig}: has failing constraint and report=true -> reportValidity`,
|
|
14091
|
+
`syncValidity ${elementSig()}: has failing constraint and report=true -> reportValidity`,
|
|
14090
14092
|
);
|
|
14091
14093
|
reportValidity({ event });
|
|
14092
14094
|
} else if (failedConstraintInfo.status === "error") {
|
|
@@ -14096,12 +14098,12 @@ const createControlValidation = (
|
|
|
14096
14098
|
// on the next action attempt via autoResetOnAction.
|
|
14097
14099
|
debugUIState(
|
|
14098
14100
|
event,
|
|
14099
|
-
`syncValidity ${elementSig}: has error constraint and report=false -> keep callout open`,
|
|
14101
|
+
`syncValidity ${elementSig()}: has error constraint and report=false -> keep callout open`,
|
|
14100
14102
|
);
|
|
14101
14103
|
} else {
|
|
14102
14104
|
debugUIState(
|
|
14103
14105
|
event,
|
|
14104
|
-
`syncValidity ${elementSig}: has failing constraint but report=false -> close callout if any`,
|
|
14106
|
+
`syncValidity ${elementSig()}: has failing constraint but report=false -> close callout if any`,
|
|
14105
14107
|
);
|
|
14106
14108
|
callout.removeOpenToken(VALIDATION_TOKEN, event);
|
|
14107
14109
|
}
|
|
@@ -28088,6 +28090,7 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
28088
28090
|
|
|
28089
28091
|
// Store previous route states to detect changes
|
|
28090
28092
|
const routePreviousStateMap = new WeakMap();
|
|
28093
|
+
let previousPath = null;
|
|
28091
28094
|
const updateRoutes = (
|
|
28092
28095
|
url,
|
|
28093
28096
|
{
|
|
@@ -28096,6 +28099,14 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
28096
28099
|
} = {},
|
|
28097
28100
|
) => {
|
|
28098
28101
|
currentUrl = url;
|
|
28102
|
+
// Whether a route matches is decided by the path alone: the search string
|
|
28103
|
+
// only fills the params (see matchUrl). An address that keeps its path — a
|
|
28104
|
+
// search param written, at every notch of a wheel bound to it — cannot make
|
|
28105
|
+
// a route start or stop matching, so only the routes already matching are
|
|
28106
|
+
// read again.
|
|
28107
|
+
const path = urlWithoutSearchAndHash(url);
|
|
28108
|
+
const pathKept = path === previousPath;
|
|
28109
|
+
previousPath = path;
|
|
28099
28110
|
const returnValue = {};
|
|
28100
28111
|
const routeMatchInfoSet = new Set();
|
|
28101
28112
|
for (const route of routeSet) {
|
|
@@ -28111,7 +28122,8 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
28111
28122
|
const oldParams = previousState.params;
|
|
28112
28123
|
const oldActionParams = previousState.actionParams;
|
|
28113
28124
|
|
|
28114
|
-
let extractedParams =
|
|
28125
|
+
let extractedParams =
|
|
28126
|
+
pathKept && !oldMatching ? null : routePattern.applyOn(url);
|
|
28115
28127
|
let newMatching = Boolean(extractedParams);
|
|
28116
28128
|
let newParams;
|
|
28117
28129
|
if (extractedParams) {
|
|
@@ -28180,6 +28192,25 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
28180
28192
|
}
|
|
28181
28193
|
}
|
|
28182
28194
|
|
|
28195
|
+
// What the matching routes say, read once for every connection of every
|
|
28196
|
+
// route that does not match (below), rather than by walking all the
|
|
28197
|
+
// routes again for each of those connections.
|
|
28198
|
+
const matchingRawParamsArray = [];
|
|
28199
|
+
const paramNameDeclaredByMatchingRouteSet = new Set();
|
|
28200
|
+
const matchingFamilyRootSet = new Set();
|
|
28201
|
+
for (const matchingRoute of matchingRouteSet) {
|
|
28202
|
+
matchingRawParamsArray.push(matchingRoute.rawParamsSignal.value);
|
|
28203
|
+
const matchingPattern =
|
|
28204
|
+
getRoutePrivateProperties(matchingRoute).routePattern;
|
|
28205
|
+
for (const paramName of matchingPattern.queryConnectionMap.keys()) {
|
|
28206
|
+
paramNameDeclaredByMatchingRouteSet.add(paramName);
|
|
28207
|
+
}
|
|
28208
|
+
for (const paramName of matchingPattern.pathConnectionMap.keys()) {
|
|
28209
|
+
paramNameDeclaredByMatchingRouteSet.add(paramName);
|
|
28210
|
+
}
|
|
28211
|
+
matchingFamilyRootSet.add(matchingPattern.familyRoot);
|
|
28212
|
+
}
|
|
28213
|
+
|
|
28183
28214
|
for (const {
|
|
28184
28215
|
route,
|
|
28185
28216
|
routePrivateProperties,
|
|
@@ -28195,40 +28226,21 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
28195
28226
|
const urlParamValue = rawParams[paramName];
|
|
28196
28227
|
|
|
28197
28228
|
if (!newMatching) {
|
|
28198
|
-
//
|
|
28199
|
-
|
|
28200
|
-
|
|
28201
|
-
|
|
28202
|
-
|
|
28203
|
-
|
|
28204
|
-
|
|
28205
|
-
|
|
28206
|
-
|
|
28207
|
-
|
|
28208
|
-
|
|
28209
|
-
|
|
28210
|
-
|
|
28211
|
-
|
|
28212
|
-
|
|
28213
|
-
|
|
28214
|
-
// Same param, on the page one is arriving at: what it is worth
|
|
28215
|
-
// there is that page's business, url or no url — it may open on
|
|
28216
|
-
// a default of its own (see searchParams' `{ signal, default }`).
|
|
28217
|
-
const otherPattern =
|
|
28218
|
-
getRoutePrivateProperties(otherRoute).routePattern;
|
|
28219
|
-
if (
|
|
28220
|
-
otherPattern.queryConnectionMap.has(paramName) ||
|
|
28221
|
-
otherPattern.pathConnectionMap.has(paramName)
|
|
28222
|
-
) {
|
|
28223
|
-
parameterDeclaredByMatchingRoute = true;
|
|
28224
|
-
}
|
|
28225
|
-
|
|
28226
|
-
// Same family = same topmost ancestor
|
|
28227
|
-
// (familyRoot, computed in setupRoutePatterns)
|
|
28228
|
-
if (otherPattern.familyRoot === routePattern.familyRoot) {
|
|
28229
|
-
matchingRouteInSameFamily = true;
|
|
28230
|
-
}
|
|
28231
|
-
}
|
|
28229
|
+
// Whether a matching route extracts this parameter from the url
|
|
28230
|
+
const parameterExtractedByMatchingRoute =
|
|
28231
|
+
matchingRawParamsArray.some(
|
|
28232
|
+
(matchingRawParams) => paramName in matchingRawParams,
|
|
28233
|
+
);
|
|
28234
|
+
// Same param, on the page one is arriving at: what it is worth
|
|
28235
|
+
// there is that page's business, url or no url — it may open on
|
|
28236
|
+
// a default of its own (see searchParams' `{ signal, default }`).
|
|
28237
|
+
const parameterDeclaredByMatchingRoute =
|
|
28238
|
+
paramNameDeclaredByMatchingRouteSet.has(paramName);
|
|
28239
|
+
// Same family = same topmost ancestor
|
|
28240
|
+
// (familyRoot, computed in setupRoutePatterns)
|
|
28241
|
+
const matchingRouteInSameFamily = matchingFamilyRootSet.has(
|
|
28242
|
+
routePattern.familyRoot,
|
|
28243
|
+
);
|
|
28232
28244
|
|
|
28233
28245
|
// A weak param qualifies one visit: leaving the route ends it,
|
|
28234
28246
|
// whatever the family and whatever the default. Coming back by a
|
|
@@ -28480,6 +28492,11 @@ const getParamsCacheKey = (params) => {
|
|
|
28480
28492
|
return key;
|
|
28481
28493
|
};
|
|
28482
28494
|
|
|
28495
|
+
const urlWithoutSearchAndHash = (url) => {
|
|
28496
|
+
const urlObject = new URL(url);
|
|
28497
|
+
return `${urlObject.origin}${urlObject.pathname}`;
|
|
28498
|
+
};
|
|
28499
|
+
|
|
28483
28500
|
/**
|
|
28484
28501
|
* Is there an entry of THIS document behind the current one? And ahead of it?
|
|
28485
28502
|
*
|
|
@@ -28923,7 +28940,13 @@ const forgetScrollerUnlessPageLeft = (name) => {
|
|
|
28923
28940
|
}
|
|
28924
28941
|
};
|
|
28925
28942
|
|
|
28926
|
-
|
|
28943
|
+
// `currentUrlString`: the address the href is compared with — the document's by
|
|
28944
|
+
// default. A caller subscribed to documentUrlSignal passes the value it read,
|
|
28945
|
+
// which is ahead of window.location while a debounced write is pending.
|
|
28946
|
+
const getHrefTargetInfo = (
|
|
28947
|
+
href,
|
|
28948
|
+
currentUrlString = window.location.href,
|
|
28949
|
+
) => {
|
|
28927
28950
|
href = String(href);
|
|
28928
28951
|
|
|
28929
28952
|
if (!href || href.trim() === "") {
|
|
@@ -28936,8 +28959,8 @@ const getHrefTargetInfo = (href) => {
|
|
|
28936
28959
|
};
|
|
28937
28960
|
}
|
|
28938
28961
|
|
|
28939
|
-
const currentUrl = new URL(
|
|
28940
|
-
const targetUrl = new URL(href,
|
|
28962
|
+
const currentUrl = new URL(currentUrlString);
|
|
28963
|
+
const targetUrl = new URL(href, currentUrlString);
|
|
28941
28964
|
|
|
28942
28965
|
let isCurrent = false;
|
|
28943
28966
|
{
|
|
@@ -28979,6 +29002,21 @@ const getHrefTargetInfo = (href) => {
|
|
|
28979
29002
|
};
|
|
28980
29003
|
};
|
|
28981
29004
|
|
|
29005
|
+
// A field of getHrefTargetInfo that depends on the page one is on (isCurrent,
|
|
29006
|
+
// isAnchor), read through a computed of that one boolean: the caller re-renders
|
|
29007
|
+
// when it flips, not at every address write — a search param bound to a wheel
|
|
29008
|
+
// writes the address at every notch.
|
|
29009
|
+
const useHrefTargetFlag = (href, flagName) => {
|
|
29010
|
+
const flagSignal = useMemo(
|
|
29011
|
+
() =>
|
|
29012
|
+
computed(
|
|
29013
|
+
() => getHrefTargetInfo(href, documentUrlSignal.value)[flagName],
|
|
29014
|
+
),
|
|
29015
|
+
[href, flagName],
|
|
29016
|
+
);
|
|
29017
|
+
return flagSignal.value;
|
|
29018
|
+
};
|
|
29019
|
+
|
|
28982
29020
|
/*
|
|
28983
29021
|
* An app can ship more than one document — an admin panel, a status page, an
|
|
28984
29022
|
* entry point of its own — and a link to one of them is not a route, it is a
|
|
@@ -47870,19 +47908,28 @@ const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
|
|
|
47870
47908
|
|
|
47871
47909
|
/**
|
|
47872
47910
|
* Hook that reactively checks if a URL is visited.
|
|
47873
|
-
* Re-renders when
|
|
47911
|
+
* Re-renders when that answer changes — not at every url added to the visited
|
|
47912
|
+
* set, which grows at every new address (a search param bound to a wheel
|
|
47913
|
+
* writes one at every notch).
|
|
47874
47914
|
*
|
|
47875
47915
|
* @param {string} url - The URL to check
|
|
47876
47916
|
* @returns {boolean} Whether the URL has been visited
|
|
47877
47917
|
*/
|
|
47878
47918
|
const useIsVisited = (url) => {
|
|
47879
|
-
|
|
47880
|
-
|
|
47881
|
-
|
|
47882
|
-
|
|
47883
|
-
|
|
47884
|
-
|
|
47885
|
-
|
|
47919
|
+
const visitedSignal = useMemo(
|
|
47920
|
+
() =>
|
|
47921
|
+
computed(() => {
|
|
47922
|
+
// Both are what the answer depends on: the set, and the address a
|
|
47923
|
+
// relative url is resolved against (see isVisited).
|
|
47924
|
+
// eslint-disable-next-line no-unused-expressions
|
|
47925
|
+
visitedUrlsSignal.value;
|
|
47926
|
+
// eslint-disable-next-line no-unused-expressions
|
|
47927
|
+
documentUrlSignal.value;
|
|
47928
|
+
return isVisited(url);
|
|
47929
|
+
}),
|
|
47930
|
+
[url],
|
|
47931
|
+
);
|
|
47932
|
+
return visitedSignal.value;
|
|
47886
47933
|
};
|
|
47887
47934
|
|
|
47888
47935
|
/**
|
|
@@ -48551,13 +48598,14 @@ const LinkPlain = props => {
|
|
|
48551
48598
|
const loading = basePseudoState[":-navi-loading"];
|
|
48552
48599
|
const shouldDimColor = readOnly || disabled;
|
|
48553
48600
|
useDimColorWhen(props.ref, shouldDimColor);
|
|
48554
|
-
//
|
|
48555
|
-
|
|
48601
|
+
// Re-rendered when one of these flips, not at every address write (see
|
|
48602
|
+
// useHrefTargetFlag). Whether the href leaves the site does not depend on
|
|
48603
|
+
// the address.
|
|
48604
|
+
const isCurrent = useHrefTargetFlag(href, "isCurrent");
|
|
48605
|
+
const isAnchor = useHrefTargetFlag(href, "isAnchor");
|
|
48556
48606
|
const {
|
|
48557
|
-
isSameSite
|
|
48558
|
-
|
|
48559
|
-
isCurrent
|
|
48560
|
-
} = getHrefTargetInfo(href);
|
|
48607
|
+
isSameSite
|
|
48608
|
+
} = getHrefTargetInfo(href, documentUrlSignal.peek());
|
|
48561
48609
|
// A tab that is a SLIDE is current when the container is on it — which the
|
|
48562
48610
|
// <Nav> around reads off that container, so nothing here has to be told.
|
|
48563
48611
|
const innerCurrent = current || (slide ? nav?.currentSlideArea === slide : isCurrent);
|
|
@@ -53523,10 +53571,9 @@ const ButtonHrefResolver = props => {
|
|
|
53523
53571
|
});
|
|
53524
53572
|
};
|
|
53525
53573
|
|
|
53526
|
-
//
|
|
53527
|
-
//
|
|
53528
|
-
//
|
|
53529
|
-
// would otherwise pay for each write.
|
|
53574
|
+
// Whether the href is the page one is on depends on the address, and re-renders
|
|
53575
|
+
// the button only when it flips (see useHrefTargetFlag). Whether it leaves the
|
|
53576
|
+
// site does not depend on the address.
|
|
53530
53577
|
const ButtonWithHref = props => {
|
|
53531
53578
|
const Next = useNextResolver();
|
|
53532
53579
|
const {
|
|
@@ -53535,12 +53582,10 @@ const ButtonWithHref = props => {
|
|
|
53535
53582
|
rel,
|
|
53536
53583
|
pseudoState
|
|
53537
53584
|
} = props;
|
|
53538
|
-
|
|
53539
|
-
useDocumentUrl();
|
|
53585
|
+
const isCurrent = useHrefTargetFlag(href, "isCurrent");
|
|
53540
53586
|
const {
|
|
53541
|
-
isSameSite
|
|
53542
|
-
|
|
53543
|
-
} = getHrefTargetInfo(href);
|
|
53587
|
+
isSameSite
|
|
53588
|
+
} = getHrefTargetInfo(href, documentUrlSignal.peek());
|
|
53544
53589
|
return jsx(Next, {
|
|
53545
53590
|
...props,
|
|
53546
53591
|
target: target === undefined ? isSameSite ? undefined : "_blank" : target,
|
|
@@ -71143,6 +71188,11 @@ const css$x = /* css */`@layer navi {
|
|
|
71143
71188
|
display: flex;
|
|
71144
71189
|
overflow: hidden;
|
|
71145
71190
|
|
|
71191
|
+
&[data-borderless] {
|
|
71192
|
+
--x-list-border-radius: 0px;
|
|
71193
|
+
overflow: visible;
|
|
71194
|
+
}
|
|
71195
|
+
|
|
71146
71196
|
&[data-overflow-visible="both"] {
|
|
71147
71197
|
overflow: visible;
|
|
71148
71198
|
}
|
|
@@ -71827,6 +71877,7 @@ const ListUI = props => {
|
|
|
71827
71877
|
"data-item-transition": itemTransition ? "" : undefined,
|
|
71828
71878
|
popover: popover,
|
|
71829
71879
|
"data-horizontal": horizontal ? "" : undefined,
|
|
71880
|
+
"data-borderless": isBorderless(rest) ? "" : undefined,
|
|
71830
71881
|
"data-scroller": getScrollerAttribute(scroller),
|
|
71831
71882
|
"data-overflow-visible": getOverflowVisibleAttribute(overflow, overflowX, overflowY),
|
|
71832
71883
|
"navi-hover-while-scrolling": hoverWhileScrolling ? "" : undefined,
|
|
@@ -73296,6 +73347,25 @@ const getScrollerAttribute = scroller => {
|
|
|
73296
73347
|
return "parent";
|
|
73297
73348
|
};
|
|
73298
73349
|
|
|
73350
|
+
// A radius asked for explicitly is kept: a borderless list painting a
|
|
73351
|
+
// background of its own may want that surface rounded, and clipped to it.
|
|
73352
|
+
const isBorderless = ({
|
|
73353
|
+
border,
|
|
73354
|
+
borderWidth,
|
|
73355
|
+
borderRadius
|
|
73356
|
+
}) => {
|
|
73357
|
+
if (borderRadius !== undefined) {
|
|
73358
|
+
return false;
|
|
73359
|
+
}
|
|
73360
|
+
if (borderWidth !== undefined) {
|
|
73361
|
+
return parseFloat(borderWidth) === 0;
|
|
73362
|
+
}
|
|
73363
|
+
if (border === undefined) {
|
|
73364
|
+
return false;
|
|
73365
|
+
}
|
|
73366
|
+
return border === "none" || parseFloat(border) === 0;
|
|
73367
|
+
};
|
|
73368
|
+
|
|
73299
73369
|
// overflow lands as an inline style on the inner scroll element, which is
|
|
73300
73370
|
// enough for every value but "visible": that one only means anything once the
|
|
73301
73371
|
// frame around it stops clipping too. Reading the per-axis props over the
|