@jsenv/dom 0.10.1 → 0.10.3
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_dom.js +290 -150
- package/package.json +1 -1
package/dist/jsenv_dom.js
CHANGED
|
@@ -1908,6 +1908,8 @@ const pxPropertySet = new Set([
|
|
|
1908
1908
|
"paddingRight",
|
|
1909
1909
|
"paddingBottom",
|
|
1910
1910
|
"paddingLeft",
|
|
1911
|
+
"outlineWidth",
|
|
1912
|
+
"outlineOffset",
|
|
1911
1913
|
"borderWidth",
|
|
1912
1914
|
"borderTopWidth",
|
|
1913
1915
|
"borderRightWidth",
|
|
@@ -2084,6 +2086,9 @@ const normalizeStyle = (
|
|
|
2084
2086
|
if (propertyName === "transform") {
|
|
2085
2087
|
if (context === "js") {
|
|
2086
2088
|
if (typeof value === "string") {
|
|
2089
|
+
if (isCSSKeyword(value)) {
|
|
2090
|
+
return value;
|
|
2091
|
+
}
|
|
2087
2092
|
// For js context, prefer objects
|
|
2088
2093
|
return parseCSSTransform(value, normalizeStyle);
|
|
2089
2094
|
}
|
|
@@ -2121,6 +2126,9 @@ const normalizeStyle = (
|
|
|
2121
2126
|
if (propertyName === "background") {
|
|
2122
2127
|
if (context === "js") {
|
|
2123
2128
|
if (typeof value === "string") {
|
|
2129
|
+
if (isCSSKeyword(value)) {
|
|
2130
|
+
return value;
|
|
2131
|
+
}
|
|
2124
2132
|
// For js context, prefer objects
|
|
2125
2133
|
return parseCSSBackground(value, {
|
|
2126
2134
|
parseStyle,
|
|
@@ -2158,6 +2166,9 @@ const normalizeStyle = (
|
|
|
2158
2166
|
if (propertyName === "border") {
|
|
2159
2167
|
if (context === "js") {
|
|
2160
2168
|
if (typeof value === "string") {
|
|
2169
|
+
if (isCSSKeyword(value)) {
|
|
2170
|
+
return value;
|
|
2171
|
+
}
|
|
2161
2172
|
// For js context, prefer objects
|
|
2162
2173
|
return parseCSSBorder(value, element);
|
|
2163
2174
|
}
|
|
@@ -2285,10 +2296,20 @@ const normalizeStyle = (
|
|
|
2285
2296
|
}
|
|
2286
2297
|
|
|
2287
2298
|
if (colorPropertySet.has(propertyName)) {
|
|
2288
|
-
if (typeof value === "string"
|
|
2289
|
-
|
|
2299
|
+
if (typeof value === "string") {
|
|
2300
|
+
if (isCSSKeyword(value)) {
|
|
2301
|
+
return value;
|
|
2302
|
+
}
|
|
2303
|
+
if (isCSSFunction(value)) {
|
|
2304
|
+
return value;
|
|
2305
|
+
}
|
|
2290
2306
|
}
|
|
2291
2307
|
const rgba = parseCSSColor(value, element);
|
|
2308
|
+
if (rgba === null) {
|
|
2309
|
+
// parseCSSColor could not parse the value (e.g. a CSS variable or unknown keyword)
|
|
2310
|
+
// return as-is so the original string reaches the DOM unchanged
|
|
2311
|
+
return value;
|
|
2312
|
+
}
|
|
2292
2313
|
if (context === "js") {
|
|
2293
2314
|
return rgba;
|
|
2294
2315
|
}
|
|
@@ -2307,6 +2328,9 @@ const stringifyStyle = (value, propertyName, element) => {
|
|
|
2307
2328
|
const isCSSFunction = (value) => {
|
|
2308
2329
|
return /^[a-z-]+\(/.test(value);
|
|
2309
2330
|
};
|
|
2331
|
+
const isCSSKeyword = (value) => {
|
|
2332
|
+
return globalCSSKeywordSet.has(value);
|
|
2333
|
+
};
|
|
2310
2334
|
const normalizeNumber = (value, { unit, propertyName, preferedType }) => {
|
|
2311
2335
|
if (typeof value === "string") {
|
|
2312
2336
|
// CSS variables and CSS functions like calc() must be passed through as-is
|
|
@@ -3406,74 +3430,101 @@ const isSameColor = (color1, color2) => {
|
|
|
3406
3430
|
|
|
3407
3431
|
/**
|
|
3408
3432
|
* Returns `"white"` or `"black"`, whichever provides better contrast against
|
|
3409
|
-
* the given background color
|
|
3433
|
+
* the given background color, using OKLCH lightness (perceptually uniform).
|
|
3410
3434
|
*
|
|
3411
|
-
*
|
|
3435
|
+
* Uses a threshold of 0.5 on the OKLCH L axis (0–1 scale).
|
|
3436
|
+
* Colors with L > threshold are considered light → return "black".
|
|
3437
|
+
* Colors with L ≤ threshold are considered dark → return "white".
|
|
3412
3438
|
*
|
|
3413
3439
|
* @param {string} backgroundColor - CSS color value (hex, rgb, hsl, CSS variable, …)
|
|
3414
3440
|
* @param {Element} [element] - DOM element used to resolve CSS variables / computed styles
|
|
3441
|
+
* @param {number} [lightnessThreshold=0.5] - OKLCH L threshold (0–1). Below → "white", above → "black".
|
|
3415
3442
|
* @returns {"white"|"black"}
|
|
3416
3443
|
* @example
|
|
3417
|
-
* contrastColor("#1a202c")
|
|
3418
|
-
* contrastColor("#f5f5f5")
|
|
3419
|
-
* contrastColor("
|
|
3444
|
+
* contrastColor("#1a202c") // "white" (dark background)
|
|
3445
|
+
* contrastColor("#f5f5f5") // "black" (light background)
|
|
3446
|
+
* contrastColor("#e91e8c") // "white" (vivid pink, perceptually dark)
|
|
3420
3447
|
*/
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3448
|
+
const contrastColor = (
|
|
3449
|
+
backgroundColor,
|
|
3450
|
+
element,
|
|
3451
|
+
lightnessThreshold = 0.5,
|
|
3452
|
+
) => {
|
|
3424
3453
|
const resolvedBgColor = parseCSSColor(backgroundColor, element);
|
|
3425
3454
|
if (!resolvedBgColor) {
|
|
3426
3455
|
return "white";
|
|
3427
3456
|
}
|
|
3428
|
-
|
|
3429
|
-
// Composite against white when the background has transparency so the
|
|
3430
|
-
// luminance reflects what the user actually sees.
|
|
3431
3457
|
const [r, g, b] =
|
|
3432
3458
|
resolvedBgColor[3] === 1
|
|
3433
3459
|
? resolvedBgColor
|
|
3434
3460
|
: compositeColor(resolvedBgColor, WHITE_RGBA);
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
// One luminance comparison replaces two full contrast-ratio computations.
|
|
3439
|
-
// White wins (or ties) when bgLuminance <= the crossover point where both
|
|
3440
|
-
// colors yield identical ratios:
|
|
3441
|
-
// contrastWithWhite = contrastWithBlack
|
|
3442
|
-
// 1.05 / (L + 0.05) = (L + 0.05) / 0.05
|
|
3443
|
-
// L = √(1.05 × 0.05) − 0.05 ≈ 0.179
|
|
3444
|
-
return bgLuminance <= EQUAL_CONTRAST_LUMINANCE ? "white" : "black";
|
|
3461
|
+
const L = rgbToOklchL(r, g, b);
|
|
3462
|
+
return L <= lightnessThreshold ? "white" : "black";
|
|
3445
3463
|
};
|
|
3446
3464
|
|
|
3447
|
-
// Luminance threshold at which white and black yield the same contrast ratio
|
|
3448
|
-
// against a background. Below → white wins or ties; above → black wins.
|
|
3449
|
-
const EQUAL_CONTRAST_LUMINANCE = Math.sqrt(1.05 * 0.05) - 0.05;
|
|
3450
|
-
const WHITE_RGBA = [255, 255, 255, 1];
|
|
3451
|
-
|
|
3452
3465
|
/**
|
|
3453
|
-
* Resolves the
|
|
3466
|
+
* Resolves the OKLCH lightness of a CSS color (perceptually uniform, 0–1 scale).
|
|
3467
|
+
*
|
|
3454
3468
|
* @param {string} color - CSS color value (hex, rgb, hsl, CSS variable, etc.)
|
|
3455
3469
|
* @param {Element} [element] - DOM element to resolve CSS variables against
|
|
3456
|
-
* @returns {number|
|
|
3470
|
+
* @returns {number|null} OKLCH L value (0–1), or null if color cannot be resolved
|
|
3457
3471
|
* @example
|
|
3458
|
-
* //
|
|
3459
|
-
*
|
|
3460
|
-
*
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3472
|
+
* resolveOklchLightness("#e91e8c") // ~0.56 (vivid pink feels medium-bright)
|
|
3473
|
+
* resolveOklchLightness("#4476ff") // ~0.53 (blue)
|
|
3474
|
+
* resolveOklchLightness("#1a202c") // ~0.22 (dark background)
|
|
3475
|
+
*/
|
|
3476
|
+
const resolveOklchLightness = (color, element) => {
|
|
3477
|
+
const rgba = parseCSSColor(color, element);
|
|
3478
|
+
if (!rgba) {
|
|
3479
|
+
return null;
|
|
3480
|
+
}
|
|
3481
|
+
const [r, g, b] = rgba;
|
|
3482
|
+
return rgbToOklchL(r, g, b);
|
|
3483
|
+
};
|
|
3484
|
+
|
|
3485
|
+
/**
|
|
3486
|
+
* Resolves the WCAG relative luminance of a CSS color (kept for backwards compatibility).
|
|
3487
|
+
* @deprecated Prefer resolveOklchLightness for perceptually uniform results.
|
|
3467
3488
|
*/
|
|
3468
3489
|
const resolveColorLuminance = (color, element) => {
|
|
3469
3490
|
const rgba = parseCSSColor(color, element);
|
|
3470
3491
|
if (!rgba) {
|
|
3471
|
-
return
|
|
3492
|
+
return null;
|
|
3472
3493
|
}
|
|
3473
3494
|
const [r, g, b] = rgba;
|
|
3474
3495
|
return getLuminance(r, g, b);
|
|
3475
3496
|
};
|
|
3476
3497
|
|
|
3498
|
+
const WHITE_RGBA = [255, 255, 255, 1];
|
|
3499
|
+
|
|
3500
|
+
/**
|
|
3501
|
+
* Converts sRGB (0–255 each) to OKLCH lightness L (0–1).
|
|
3502
|
+
* Implements the sRGB → Linear sRGB → XYZ D65 → OKLab → L pipeline.
|
|
3503
|
+
*/
|
|
3504
|
+
const rgbToOklchL = (r, g, b) => {
|
|
3505
|
+
// sRGB → linear
|
|
3506
|
+
const toLinear = (c) => {
|
|
3507
|
+
c = c / 255;
|
|
3508
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
3509
|
+
};
|
|
3510
|
+
const lr = toLinear(r);
|
|
3511
|
+
const lg = toLinear(g);
|
|
3512
|
+
const lb = toLinear(b);
|
|
3513
|
+
|
|
3514
|
+
// Linear sRGB → LMS (Oklab M1 matrix)
|
|
3515
|
+
const l = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
|
|
3516
|
+
const m = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
|
|
3517
|
+
const s = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;
|
|
3518
|
+
|
|
3519
|
+
// Cube root
|
|
3520
|
+
const l_ = Math.cbrt(l);
|
|
3521
|
+
const m_ = Math.cbrt(m);
|
|
3522
|
+
const s_ = Math.cbrt(s);
|
|
3523
|
+
|
|
3524
|
+
// LMS → OKLab L
|
|
3525
|
+
return 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
|
|
3526
|
+
};
|
|
3527
|
+
|
|
3477
3528
|
/**
|
|
3478
3529
|
* Calculates the contrast ratio between two RGBA colors
|
|
3479
3530
|
* Based on WCAG 2.1 specification
|
|
@@ -6082,10 +6133,10 @@ const trapScrollInside = (element) => {
|
|
|
6082
6133
|
}
|
|
6083
6134
|
const [scrollbarWidth, scrollbarHeight] = measureScrollbar(el);
|
|
6084
6135
|
const paddingRight = parseInt(getStyle(el, "padding-right"), 0);
|
|
6085
|
-
const
|
|
6136
|
+
const paddingBottom = parseInt(getStyle(el, "padding-bottom"), 0);
|
|
6086
6137
|
const removeScrollLockStyles = setStyles(el, {
|
|
6087
6138
|
"padding-right": `${paddingRight + scrollbarWidth}px`,
|
|
6088
|
-
"padding-
|
|
6139
|
+
"padding-bottom": `${paddingBottom + scrollbarHeight}px`,
|
|
6089
6140
|
"overflow": "hidden",
|
|
6090
6141
|
});
|
|
6091
6142
|
cleanupCallbackSet.add(removeScrollLockStyles);
|
|
@@ -9526,6 +9577,10 @@ const stickyAsRelativeCoords = (
|
|
|
9526
9577
|
return [leftPosition, topPosition];
|
|
9527
9578
|
};
|
|
9528
9579
|
|
|
9580
|
+
// Minimum fraction of element width/height that must be visible on the preferred side
|
|
9581
|
+
// before flipping to the opposite side. Prevents flickering near the flip threshold.
|
|
9582
|
+
const MIN_CONTENT_VISIBILITY_RATIO = 0.6;
|
|
9583
|
+
|
|
9529
9584
|
/**
|
|
9530
9585
|
* Tracks how much of an element is visible within its scrollable parent and within the
|
|
9531
9586
|
* document viewport. Calls update() on initialization and whenever visibility changes
|
|
@@ -9827,47 +9882,52 @@ const visibleRectEffect = (element, update) => {
|
|
|
9827
9882
|
};
|
|
9828
9883
|
|
|
9829
9884
|
/**
|
|
9830
|
-
* Places element
|
|
9885
|
+
* Places element relative to anchor with independent control of horizontal and vertical axes.
|
|
9831
9886
|
*
|
|
9832
|
-
*
|
|
9833
|
-
*
|
|
9834
|
-
*
|
|
9835
|
-
*
|
|
9836
|
-
*
|
|
9837
|
-
*
|
|
9838
|
-
*
|
|
9887
|
+
* Horizontal axis — positionX / positionXFixed (left → right):
|
|
9888
|
+
* "to-the-left" element.right = anchor.left (sits entirely to the left of anchor)
|
|
9889
|
+
* "left-aligned" element.left = anchor.left (left edges aligned)
|
|
9890
|
+
* "center" element centered horizontally over anchor (default)
|
|
9891
|
+
* "right-aligned" element.right = anchor.right (right edges aligned)
|
|
9892
|
+
* "to-the-right" element.left = anchor.right (sits entirely to the right of anchor)
|
|
9893
|
+
*
|
|
9894
|
+
* Vertical axis — positionY / positionYFixed (top → bottom):
|
|
9895
|
+
* "above" element.bottom = anchor.top (sits above, no overlap)
|
|
9896
|
+
* "above-overlap" element.bottom = anchor.bottom (sits above, overlapping anchor)
|
|
9897
|
+
* "center" element centered vertically over anchor
|
|
9898
|
+
* "below-overlap" element.top = anchor.top (sits below, overlapping anchor)
|
|
9899
|
+
* "below" element.top = anchor.bottom (sits below, no overlap) (default)
|
|
9900
|
+
*
|
|
9901
|
+
* positionX / positionY attempt the requested placement and automatically flip to the
|
|
9902
|
+
* logical opposite when the element does not fit in the viewport:
|
|
9903
|
+
* above ↔ below, above-overlap ↔ below-overlap
|
|
9904
|
+
*
|
|
9905
|
+
* positionXFixed / positionYFixed skip the fit check entirely.
|
|
9839
9906
|
*
|
|
9840
|
-
*
|
|
9841
|
-
*
|
|
9842
|
-
*
|
|
9843
|
-
* - "left" → element.right = anchor.left, vertically centered
|
|
9844
|
-
* - "right" → element.left = anchor.right, vertically centered
|
|
9845
|
-
* - "top-left" → element.bottom = anchor.top, element.right = anchor.left
|
|
9846
|
-
* - "top-right" → element.bottom = anchor.top, element.left = anchor.right
|
|
9847
|
-
* - "bottom-left" → element.top = anchor.bottom, element.right = anchor.left
|
|
9848
|
-
* - "bottom-right" → element.top = anchor.bottom, element.left = anchor.right
|
|
9849
|
-
* - "center" → element centered on anchor (overlapping)
|
|
9907
|
+
* The resolved X and Y are persisted as data-position-x-current / data-position-y-current
|
|
9908
|
+
* on the element so subsequent calls start from the last resolved position (avoids
|
|
9909
|
+
* flickering when the element is near the flip threshold). Fixed axes are not persisted.
|
|
9850
9910
|
*
|
|
9851
9911
|
* @param {HTMLElement} element - The element to position (must be document-relative)
|
|
9852
9912
|
* @param {HTMLElement} anchor - The anchor element to position against
|
|
9853
9913
|
* @param {object} [options]
|
|
9854
|
-
* @param {string} [options.
|
|
9855
|
-
*
|
|
9856
|
-
*
|
|
9857
|
-
*
|
|
9858
|
-
* the last resolved position is persisted as data-position-current to avoid flickering.
|
|
9859
|
-
* @param {string} [options.position] - Force a specific position, skipping the fit-check.
|
|
9914
|
+
* @param {string} [options.positionX="center"] - Preferred X placement, with viewport fallback.
|
|
9915
|
+
* @param {string} [options.positionY="below"] - Preferred Y placement, with viewport fallback.
|
|
9916
|
+
* @param {string} [options.positionXFixed] - Force X placement, skipping the fit-check.
|
|
9917
|
+
* @param {string} [options.positionYFixed] - Force Y placement, skipping the fit-check.
|
|
9860
9918
|
* @param {number} [options.alignToViewportEdgeWhenAnchorNearEdge=0] - Snap to viewport left
|
|
9861
9919
|
* edge when anchor is within this many px of the left edge and element is wider than anchor.
|
|
9862
9920
|
* @param {number} [options.minLeft=0] - Minimum left coordinate (document-relative).
|
|
9863
|
-
* @returns {{
|
|
9921
|
+
* @returns {{ positionX, positionY, left, top, width, height, anchorLeft, anchorTop, anchorRight, anchorBottom, spaceLeft, spaceRight, spaceAbove, spaceBelow }}
|
|
9864
9922
|
*/
|
|
9865
9923
|
const pickPositionRelativeTo = (
|
|
9866
9924
|
element,
|
|
9867
9925
|
anchor,
|
|
9868
9926
|
{
|
|
9869
|
-
|
|
9870
|
-
|
|
9927
|
+
positionX = "center",
|
|
9928
|
+
positionY = "below",
|
|
9929
|
+
positionXFixed,
|
|
9930
|
+
positionYFixed,
|
|
9871
9931
|
alignToViewportEdgeWhenAnchorNearEdge = 0,
|
|
9872
9932
|
minLeft = 0,
|
|
9873
9933
|
} = {},
|
|
@@ -9895,61 +9955,151 @@ const pickPositionRelativeTo = (
|
|
|
9895
9955
|
const anchorWidth = anchorRight - anchorLeft;
|
|
9896
9956
|
const anchorHeight = anchorBottom - anchorTop;
|
|
9897
9957
|
|
|
9898
|
-
// Determine the active position: position wins, then data-position-current (last resolved),
|
|
9899
|
-
// then data-position-try attribute (user preference), then positionTry param
|
|
9900
|
-
let activePosition;
|
|
9901
|
-
if (position) {
|
|
9902
|
-
activePosition = position;
|
|
9903
|
-
} else {
|
|
9904
|
-
const positionCurrentFromAttribute = element.getAttribute(
|
|
9905
|
-
"data-position-current",
|
|
9906
|
-
);
|
|
9907
|
-
const positionTryFromAttribute = element.getAttribute("data-position-try");
|
|
9908
|
-
activePosition =
|
|
9909
|
-
positionCurrentFromAttribute || positionTryFromAttribute || positionTry;
|
|
9910
|
-
}
|
|
9911
|
-
|
|
9912
9958
|
const spaceAbove = anchorTop;
|
|
9913
9959
|
const spaceBelow = viewportHeight - anchorBottom;
|
|
9960
|
+
const spaceLeft = anchorLeft;
|
|
9961
|
+
const spaceRight = viewportWidth - anchorRight;
|
|
9962
|
+
|
|
9963
|
+
// Resolve active X and Y, and whether each is fixed (no flip fallback)
|
|
9964
|
+
let activeX;
|
|
9965
|
+
let activeY;
|
|
9966
|
+
const xIsFixed = Boolean(positionXFixed);
|
|
9967
|
+
const yIsFixed = Boolean(positionYFixed);
|
|
9968
|
+
const hasStoredY = Boolean(element.getAttribute("data-position-y-current"));
|
|
9969
|
+
const hasStoredX = Boolean(element.getAttribute("data-position-x-current"));
|
|
9970
|
+
if (xIsFixed) {
|
|
9971
|
+
activeX = positionXFixed;
|
|
9972
|
+
} else {
|
|
9973
|
+
const storedX = element.getAttribute("data-position-x-current");
|
|
9974
|
+
activeX = storedX ?? positionX;
|
|
9975
|
+
}
|
|
9976
|
+
if (yIsFixed) {
|
|
9977
|
+
activeY = positionYFixed;
|
|
9978
|
+
} else {
|
|
9979
|
+
const storedY = element.getAttribute("data-position-y-current");
|
|
9980
|
+
activeY = storedY ?? positionY;
|
|
9981
|
+
}
|
|
9914
9982
|
|
|
9915
|
-
// Resolve
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9983
|
+
// Resolve final Y
|
|
9984
|
+
let finalY;
|
|
9985
|
+
{
|
|
9986
|
+
const oppositeY = {
|
|
9987
|
+
"above": "below",
|
|
9988
|
+
"below": "above",
|
|
9989
|
+
"above-overlap": "below-overlap",
|
|
9990
|
+
"below-overlap": "above-overlap",
|
|
9991
|
+
};
|
|
9992
|
+
// Compute effective space for a given Y value
|
|
9993
|
+
const spaceFor = (y) => {
|
|
9994
|
+
if (y === "above") {
|
|
9995
|
+
return spaceAbove;
|
|
9996
|
+
}
|
|
9997
|
+
if (y === "above-overlap") {
|
|
9998
|
+
return spaceAbove + anchorHeight;
|
|
9999
|
+
}
|
|
10000
|
+
if (y === "below") {
|
|
10001
|
+
return spaceBelow;
|
|
10002
|
+
}
|
|
10003
|
+
if (y === "below-overlap") {
|
|
10004
|
+
return spaceBelow + anchorHeight;
|
|
10005
|
+
}
|
|
10006
|
+
return Infinity; // center
|
|
10007
|
+
};
|
|
10008
|
+
if (yIsFixed || activeY === "center") {
|
|
10009
|
+
finalY = activeY;
|
|
10010
|
+
} else if (!hasStoredY) {
|
|
10011
|
+
// Never positioned before — pick the best side from scratch.
|
|
10012
|
+
const preferred = positionY;
|
|
10013
|
+
const opposite = oppositeY[preferred];
|
|
10014
|
+
const preferredFits = spaceFor(preferred) >= elementHeight;
|
|
10015
|
+
const oppositeFits = spaceFor(opposite) >= elementHeight;
|
|
10016
|
+
if (preferredFits) {
|
|
10017
|
+
// Preferred fits completely — use it (even if opposite also fits)
|
|
10018
|
+
finalY = preferred;
|
|
10019
|
+
} else if (oppositeFits) {
|
|
10020
|
+
// Only opposite fits completely — flip
|
|
10021
|
+
finalY = opposite;
|
|
10022
|
+
} else {
|
|
10023
|
+
// Neither fits completely — use whichever meets the minimum ratio
|
|
10024
|
+
const preferredMeetsRatio =
|
|
10025
|
+
spaceFor(preferred) / elementHeight >= MIN_CONTENT_VISIBILITY_RATIO;
|
|
10026
|
+
finalY = preferredMeetsRatio ? preferred : opposite;
|
|
10027
|
+
}
|
|
9931
10028
|
} else {
|
|
9932
|
-
|
|
10029
|
+
// Previously positioned — stay as long as current side meets minimum ratio
|
|
10030
|
+
const currentFitsEnough =
|
|
10031
|
+
spaceFor(activeY) / elementHeight >= MIN_CONTENT_VISIBILITY_RATIO;
|
|
10032
|
+
if (currentFitsEnough) {
|
|
10033
|
+
finalY = activeY;
|
|
10034
|
+
} else {
|
|
10035
|
+
finalY = oppositeY[activeY];
|
|
10036
|
+
}
|
|
9933
10037
|
}
|
|
9934
|
-
}
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
10038
|
+
}
|
|
10039
|
+
|
|
10040
|
+
// Resolve final X
|
|
10041
|
+
let finalX;
|
|
10042
|
+
{
|
|
10043
|
+
const oppositeX = {
|
|
10044
|
+
"to-the-left": "to-the-right",
|
|
10045
|
+
"to-the-right": "to-the-left",
|
|
10046
|
+
"left-aligned": "right-aligned",
|
|
10047
|
+
"right-aligned": "left-aligned",
|
|
10048
|
+
};
|
|
10049
|
+
// Compute effective space for a given X value
|
|
10050
|
+
const spaceFor = (x) => {
|
|
10051
|
+
if (x === "to-the-left") {
|
|
10052
|
+
return spaceLeft;
|
|
10053
|
+
}
|
|
10054
|
+
if (x === "left-aligned") {
|
|
10055
|
+
return viewportWidth - anchorLeft;
|
|
10056
|
+
}
|
|
10057
|
+
if (x === "right-aligned") {
|
|
10058
|
+
return anchorRight;
|
|
10059
|
+
}
|
|
10060
|
+
if (x === "to-the-right") {
|
|
10061
|
+
return spaceRight;
|
|
10062
|
+
}
|
|
10063
|
+
return Infinity; // center
|
|
10064
|
+
};
|
|
10065
|
+
if (xIsFixed || activeX === "center") {
|
|
10066
|
+
finalX = activeX;
|
|
10067
|
+
} else if (!hasStoredX) {
|
|
10068
|
+
// Never positioned before — pick the best side from scratch.
|
|
10069
|
+
const preferred = positionX;
|
|
10070
|
+
const opposite = oppositeX[preferred];
|
|
10071
|
+
const preferredFits = spaceFor(preferred) >= elementWidth;
|
|
10072
|
+
const oppositeFits = spaceFor(opposite) >= elementWidth;
|
|
10073
|
+
if (preferredFits) {
|
|
10074
|
+
finalX = preferred;
|
|
10075
|
+
} else if (oppositeFits) {
|
|
10076
|
+
finalX = opposite;
|
|
10077
|
+
} else {
|
|
10078
|
+
const preferredMeetsRatio =
|
|
10079
|
+
spaceFor(preferred) / elementWidth >= MIN_CONTENT_VISIBILITY_RATIO;
|
|
10080
|
+
finalX = preferredMeetsRatio ? preferred : opposite;
|
|
10081
|
+
}
|
|
9939
10082
|
} else {
|
|
9940
|
-
|
|
10083
|
+
// Previously positioned — stay as long as current side meets minimum ratio
|
|
10084
|
+
const currentFitsEnough =
|
|
10085
|
+
spaceFor(activeX) / elementWidth >= MIN_CONTENT_VISIBILITY_RATIO;
|
|
10086
|
+
if (currentFitsEnough) {
|
|
10087
|
+
finalX = activeX;
|
|
10088
|
+
} else {
|
|
10089
|
+
finalX = oppositeX[activeX];
|
|
10090
|
+
}
|
|
9941
10091
|
}
|
|
9942
10092
|
}
|
|
9943
10093
|
|
|
9944
10094
|
// Calculate horizontal position (viewport-relative)
|
|
9945
10095
|
let elementPositionLeft;
|
|
9946
10096
|
{
|
|
9947
|
-
if (
|
|
10097
|
+
if (finalX === "to-the-left") {
|
|
9948
10098
|
elementPositionLeft = anchorLeft - elementWidth;
|
|
9949
|
-
} else if (
|
|
9950
|
-
elementPositionLeft =
|
|
9951
|
-
} else {
|
|
9952
|
-
//
|
|
10099
|
+
} else if (finalX === "left-aligned") {
|
|
10100
|
+
elementPositionLeft = anchorLeft;
|
|
10101
|
+
} else if (finalX === "center") {
|
|
10102
|
+
// Complex logic handles wide anchors and viewport-edge snapping
|
|
9953
10103
|
const anchorIsWiderThanViewport = anchorWidth > viewportWidth;
|
|
9954
10104
|
if (anchorIsWiderThanViewport) {
|
|
9955
10105
|
const anchorLeftIsVisible = anchorLeft >= 0;
|
|
@@ -9978,6 +10128,11 @@ const pickPositionRelativeTo = (
|
|
|
9978
10128
|
}
|
|
9979
10129
|
}
|
|
9980
10130
|
}
|
|
10131
|
+
} else if (finalX === "right-aligned") {
|
|
10132
|
+
elementPositionLeft = anchorRight - elementWidth;
|
|
10133
|
+
} else {
|
|
10134
|
+
// "to-the-right"
|
|
10135
|
+
elementPositionLeft = anchorRight;
|
|
9981
10136
|
}
|
|
9982
10137
|
// Constrain horizontal position to viewport boundaries
|
|
9983
10138
|
if (elementPositionLeft < 0) {
|
|
@@ -9990,40 +10145,33 @@ const pickPositionRelativeTo = (
|
|
|
9990
10145
|
// Calculate vertical position (viewport-relative)
|
|
9991
10146
|
let elementPositionTop;
|
|
9992
10147
|
{
|
|
9993
|
-
if (
|
|
10148
|
+
if (finalY === "above") {
|
|
10149
|
+
const idealTop = anchorTop - elementHeight;
|
|
10150
|
+
elementPositionTop = idealTop < 0 ? 0 : idealTop;
|
|
10151
|
+
} else if (finalY === "above-overlap") {
|
|
10152
|
+
const idealTop = anchorBottom - elementHeight;
|
|
10153
|
+
elementPositionTop = idealTop < 0 ? 0 : idealTop;
|
|
10154
|
+
} else if (finalY === "center") {
|
|
9994
10155
|
elementPositionTop = anchorTop + anchorHeight / 2 - elementHeight / 2;
|
|
9995
|
-
} else if (
|
|
9996
|
-
const idealTop =
|
|
10156
|
+
} else if (finalY === "below-overlap") {
|
|
10157
|
+
const idealTop = anchorTop;
|
|
9997
10158
|
elementPositionTop =
|
|
9998
10159
|
idealTop % 1 === 0 ? idealTop : Math.floor(idealTop) + 1;
|
|
9999
10160
|
} else {
|
|
10000
|
-
// "
|
|
10001
|
-
const idealTop =
|
|
10002
|
-
elementPositionTop =
|
|
10161
|
+
// "below"
|
|
10162
|
+
const idealTop = anchorBottom;
|
|
10163
|
+
elementPositionTop =
|
|
10164
|
+
idealTop % 1 === 0 ? idealTop : Math.floor(idealTop) + 1;
|
|
10003
10165
|
}
|
|
10004
10166
|
}
|
|
10005
10167
|
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
if (vertPart && horzPart) {
|
|
10011
|
-
finalPosition = `${vertPart}-${horzPart}`;
|
|
10012
|
-
} else if (vertPart) {
|
|
10013
|
-
finalPosition = vertPart;
|
|
10014
|
-
} else if (horzPart) {
|
|
10015
|
-
finalPosition = horzPart;
|
|
10016
|
-
} else {
|
|
10017
|
-
finalPosition = "center";
|
|
10018
|
-
}
|
|
10168
|
+
// Persist resolved X/Y so subsequent calls start from here (avoids flickering).
|
|
10169
|
+
// Fixed axes are not persisted.
|
|
10170
|
+
if (!xIsFixed) {
|
|
10171
|
+
element.setAttribute("data-position-x-current", finalX);
|
|
10019
10172
|
}
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
// (avoids flickering between positions when the element is near the threshold).
|
|
10023
|
-
// position is not persisted — it is always explicit.
|
|
10024
|
-
|
|
10025
|
-
if (!position) {
|
|
10026
|
-
element.setAttribute("data-position-current", finalPosition);
|
|
10173
|
+
if (!yIsFixed) {
|
|
10174
|
+
element.setAttribute("data-position-y-current", finalY);
|
|
10027
10175
|
}
|
|
10028
10176
|
|
|
10029
10177
|
// Get document scroll for final coordinate conversion
|
|
@@ -10036,7 +10184,8 @@ const pickPositionRelativeTo = (
|
|
|
10036
10184
|
const anchorDocumentBottom = anchorBottom + scrollTop;
|
|
10037
10185
|
|
|
10038
10186
|
return {
|
|
10039
|
-
|
|
10187
|
+
positionX: finalX,
|
|
10188
|
+
positionY: finalY,
|
|
10040
10189
|
left: elementDocumentLeft,
|
|
10041
10190
|
top: elementDocumentTop,
|
|
10042
10191
|
width: elementWidth,
|
|
@@ -10045,21 +10194,12 @@ const pickPositionRelativeTo = (
|
|
|
10045
10194
|
anchorTop: anchorDocumentTop,
|
|
10046
10195
|
anchorRight: anchorDocumentRight,
|
|
10047
10196
|
anchorBottom: anchorDocumentBottom,
|
|
10197
|
+
spaceLeft,
|
|
10198
|
+
spaceRight,
|
|
10048
10199
|
spaceAbove,
|
|
10049
10200
|
spaceBelow,
|
|
10050
10201
|
};
|
|
10051
10202
|
};
|
|
10052
|
-
// Decompose position flags
|
|
10053
|
-
const decomposePosition = (pos) => {
|
|
10054
|
-
return {
|
|
10055
|
-
isTop: pos === "top" || pos === "top-left" || pos === "top-right",
|
|
10056
|
-
isBottom:
|
|
10057
|
-
pos === "bottom" || pos === "bottom-left" || pos === "bottom-right",
|
|
10058
|
-
isLeft: pos === "left" || pos === "top-left" || pos === "bottom-left",
|
|
10059
|
-
isRight: pos === "right" || pos === "top-right" || pos === "bottom-right",
|
|
10060
|
-
isCenter: pos === "center",
|
|
10061
|
-
};
|
|
10062
|
-
};
|
|
10063
10203
|
|
|
10064
10204
|
const [publishDebugger, subscribeDebugger] = createPubSub();
|
|
10065
10205
|
|
|
@@ -13184,4 +13324,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
|
|
|
13184
13324
|
};
|
|
13185
13325
|
};
|
|
13186
13326
|
|
|
13187
|
-
export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, canInterceptKeys, captureScrollState, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isSameColor, isScrollable, measureScrollbar, mergeOneStyle, mergeTwoStyles, normalizeStyles, parseStyle, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, resolveCSSColor, resolveCSSSize, resolveColorLuminance, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyStyle, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
|
|
13327
|
+
export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, canInterceptKeys, captureScrollState, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findFocusable, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isSameColor, isScrollable, measureScrollbar, mergeOneStyle, mergeTwoStyles, normalizeStyles, parseStyle, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyStyle, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
|