@jsenv/dom 0.10.1 → 0.10.2

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.
Files changed (2) hide show
  1. package/dist/jsenv_dom.js +90 -41
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -2084,6 +2084,9 @@ const normalizeStyle = (
2084
2084
  if (propertyName === "transform") {
2085
2085
  if (context === "js") {
2086
2086
  if (typeof value === "string") {
2087
+ if (isCSSKeyword(value)) {
2088
+ return value;
2089
+ }
2087
2090
  // For js context, prefer objects
2088
2091
  return parseCSSTransform(value, normalizeStyle);
2089
2092
  }
@@ -2121,6 +2124,9 @@ const normalizeStyle = (
2121
2124
  if (propertyName === "background") {
2122
2125
  if (context === "js") {
2123
2126
  if (typeof value === "string") {
2127
+ if (isCSSKeyword(value)) {
2128
+ return value;
2129
+ }
2124
2130
  // For js context, prefer objects
2125
2131
  return parseCSSBackground(value, {
2126
2132
  parseStyle,
@@ -2158,6 +2164,9 @@ const normalizeStyle = (
2158
2164
  if (propertyName === "border") {
2159
2165
  if (context === "js") {
2160
2166
  if (typeof value === "string") {
2167
+ if (isCSSKeyword(value)) {
2168
+ return value;
2169
+ }
2161
2170
  // For js context, prefer objects
2162
2171
  return parseCSSBorder(value, element);
2163
2172
  }
@@ -2285,10 +2294,20 @@ const normalizeStyle = (
2285
2294
  }
2286
2295
 
2287
2296
  if (colorPropertySet.has(propertyName)) {
2288
- if (typeof value === "string" && isCSSFunction(value)) {
2289
- return value;
2297
+ if (typeof value === "string") {
2298
+ if (isCSSKeyword(value)) {
2299
+ return value;
2300
+ }
2301
+ if (isCSSFunction(value)) {
2302
+ return value;
2303
+ }
2290
2304
  }
2291
2305
  const rgba = parseCSSColor(value, element);
2306
+ if (rgba === null) {
2307
+ // parseCSSColor could not parse the value (e.g. a CSS variable or unknown keyword)
2308
+ // return as-is so the original string reaches the DOM unchanged
2309
+ return value;
2310
+ }
2292
2311
  if (context === "js") {
2293
2312
  return rgba;
2294
2313
  }
@@ -2307,6 +2326,9 @@ const stringifyStyle = (value, propertyName, element) => {
2307
2326
  const isCSSFunction = (value) => {
2308
2327
  return /^[a-z-]+\(/.test(value);
2309
2328
  };
2329
+ const isCSSKeyword = (value) => {
2330
+ return globalCSSKeywordSet.has(value);
2331
+ };
2310
2332
  const normalizeNumber = (value, { unit, propertyName, preferedType }) => {
2311
2333
  if (typeof value === "string") {
2312
2334
  // CSS variables and CSS functions like calc() must be passed through as-is
@@ -3406,74 +3428,101 @@ const isSameColor = (color1, color2) => {
3406
3428
 
3407
3429
  /**
3408
3430
  * Returns `"white"` or `"black"`, whichever provides better contrast against
3409
- * the given background color mirroring the CSS `contrast-color()` function.
3431
+ * the given background color, using OKLCH lightness (perceptually uniform).
3410
3432
  *
3411
- * `"white"` is preferred when both colors yield the same contrast ratio.
3433
+ * Uses a threshold of 0.5 on the OKLCH L axis (0–1 scale).
3434
+ * Colors with L > threshold are considered light → return "black".
3435
+ * Colors with L ≤ threshold are considered dark → return "white".
3412
3436
  *
3413
3437
  * @param {string} backgroundColor - CSS color value (hex, rgb, hsl, CSS variable, …)
3414
3438
  * @param {Element} [element] - DOM element used to resolve CSS variables / computed styles
3439
+ * @param {number} [lightnessThreshold=0.5] - OKLCH L threshold (0–1). Below → "white", above → "black".
3415
3440
  * @returns {"white"|"black"}
3416
3441
  * @example
3417
- * contrastColor("#1a202c") // "white" (dark background)
3418
- * contrastColor("#f5f5f5") // "black" (light background)
3419
- * contrastColor("var(--bg)", el) // "white" or "black"
3442
+ * contrastColor("#1a202c") // "white" (dark background)
3443
+ * contrastColor("#f5f5f5") // "black" (light background)
3444
+ * contrastColor("#e91e8c") // "white" (vivid pink, perceptually dark)
3420
3445
  */
3421
-
3422
-
3423
- const contrastColor = (backgroundColor, element) => {
3446
+ const contrastColor = (
3447
+ backgroundColor,
3448
+ element,
3449
+ lightnessThreshold = 0.5,
3450
+ ) => {
3424
3451
  const resolvedBgColor = parseCSSColor(backgroundColor, element);
3425
3452
  if (!resolvedBgColor) {
3426
3453
  return "white";
3427
3454
  }
3428
-
3429
- // Composite against white when the background has transparency so the
3430
- // luminance reflects what the user actually sees.
3431
3455
  const [r, g, b] =
3432
3456
  resolvedBgColor[3] === 1
3433
3457
  ? resolvedBgColor
3434
3458
  : compositeColor(resolvedBgColor, WHITE_RGBA);
3435
-
3436
- const bgLuminance = getLuminance(r, g, b);
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";
3459
+ const L = rgbToOklchL(r, g, b);
3460
+ return L <= lightnessThreshold ? "white" : "black";
3445
3461
  };
3446
3462
 
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
3463
  /**
3453
- * Resolves the luminance value of a CSS color
3464
+ * Resolves the OKLCH lightness of a CSS color (perceptually uniform, 0–1 scale).
3465
+ *
3454
3466
  * @param {string} color - CSS color value (hex, rgb, hsl, CSS variable, etc.)
3455
3467
  * @param {Element} [element] - DOM element to resolve CSS variables against
3456
- * @returns {number|undefined} Relative luminance (0-1) according to WCAG formula, or undefined if color cannot be resolved
3468
+ * @returns {number|null} OKLCH L value (01), or null if color cannot be resolved
3457
3469
  * @example
3458
- * // Get luminance of a hex color
3459
- * resolveColorLuminance("#ff0000") // returns ~0.213 (red)
3460
- *
3461
- * // Get luminance of a CSS variable
3462
- * resolveColorLuminance("var(--primary-color)", element) // returns luminance value or undefined
3463
- *
3464
- * // Use for light/dark classification
3465
- * const luminance = resolveColorLuminance("#2ecc71");
3466
- * const isLight = luminance > 0.3; // true for light colors, false for dark
3470
+ * resolveOklchLightness("#e91e8c") // ~0.56 (vivid pink feels medium-bright)
3471
+ * resolveOklchLightness("#4476ff") // ~0.53 (blue)
3472
+ * resolveOklchLightness("#1a202c") // ~0.22 (dark background)
3473
+ */
3474
+ const resolveOklchLightness = (color, element) => {
3475
+ const rgba = parseCSSColor(color, element);
3476
+ if (!rgba) {
3477
+ return null;
3478
+ }
3479
+ const [r, g, b] = rgba;
3480
+ return rgbToOklchL(r, g, b);
3481
+ };
3482
+
3483
+ /**
3484
+ * Resolves the WCAG relative luminance of a CSS color (kept for backwards compatibility).
3485
+ * @deprecated Prefer resolveOklchLightness for perceptually uniform results.
3467
3486
  */
3468
3487
  const resolveColorLuminance = (color, element) => {
3469
3488
  const rgba = parseCSSColor(color, element);
3470
3489
  if (!rgba) {
3471
- return undefined;
3490
+ return null;
3472
3491
  }
3473
3492
  const [r, g, b] = rgba;
3474
3493
  return getLuminance(r, g, b);
3475
3494
  };
3476
3495
 
3496
+ const WHITE_RGBA = [255, 255, 255, 1];
3497
+
3498
+ /**
3499
+ * Converts sRGB (0–255 each) to OKLCH lightness L (0–1).
3500
+ * Implements the sRGB → Linear sRGB → XYZ D65 → OKLab → L pipeline.
3501
+ */
3502
+ const rgbToOklchL = (r, g, b) => {
3503
+ // sRGB → linear
3504
+ const toLinear = (c) => {
3505
+ c = c / 255;
3506
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
3507
+ };
3508
+ const lr = toLinear(r);
3509
+ const lg = toLinear(g);
3510
+ const lb = toLinear(b);
3511
+
3512
+ // Linear sRGB → LMS (Oklab M1 matrix)
3513
+ const l = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
3514
+ const m = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
3515
+ const s = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;
3516
+
3517
+ // Cube root
3518
+ const l_ = Math.cbrt(l);
3519
+ const m_ = Math.cbrt(m);
3520
+ const s_ = Math.cbrt(s);
3521
+
3522
+ // LMS → OKLab L
3523
+ return 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
3524
+ };
3525
+
3477
3526
  /**
3478
3527
  * Calculates the contrast ratio between two RGBA colors
3479
3528
  * Based on WCAG 2.1 specification
@@ -13184,4 +13233,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
13184
13233
  };
13185
13234
  };
13186
13235
 
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 };
13236
+ 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {