@jsenv/dom 0.8.8 → 0.9.1

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 +131 -73
  2. package/package.json +2 -2
package/dist/jsenv_dom.js CHANGED
@@ -104,29 +104,31 @@ const getElementSignature = (element) => {
104
104
  if (element.nodeType === Node.TEXT_NODE) {
105
105
  return `#text(${getElementSignature(element.nodeValue)})`;
106
106
  }
107
+ if (element instanceof HTMLElement) {
108
+ const tagName = element.tagName.toLowerCase();
109
+ const dataUIName = element.getAttribute("data-ui-name");
110
+ if (dataUIName) {
111
+ return `${tagName}[data-ui-name="${dataUIName}"]`;
112
+ }
113
+ if (element === document.body) {
114
+ return "<body>";
115
+ }
116
+ if (element === document.documentElement) {
117
+ return "<html>";
118
+ }
119
+ const elementId = element.id;
120
+ if (elementId) {
121
+ return `${tagName}#${elementId}`;
122
+ }
123
+ const className = element.className;
124
+ if (className) {
125
+ return `${tagName}.${className.split(" ").join(".")}`;
126
+ }
107
127
 
108
- const tagName = element.tagName.toLowerCase();
109
- const dataUIName = element.getAttribute("data-ui-name");
110
- if (dataUIName) {
111
- return `${tagName}[data-ui-name="${dataUIName}"]`;
128
+ const parentSignature = getElementSignature(element.parentElement);
129
+ return `${parentSignature} > ${tagName}`;
112
130
  }
113
- if (element === document.body) {
114
- return "<body>";
115
- }
116
- if (element === document.documentElement) {
117
- return "<html>";
118
- }
119
- const elementId = element.id;
120
- if (elementId) {
121
- return `${tagName}#${elementId}`;
122
- }
123
- const className = element.className;
124
- if (className) {
125
- return `${tagName}.${className.split(" ").join(".")}`;
126
- }
127
-
128
- const parentSignature = getElementSignature(element.parentElement);
129
- return `${parentSignature} > ${tagName}`;
131
+ return String(element);
130
132
  };
131
133
 
132
134
  const createIterableWeakSet = () => {
@@ -440,7 +442,6 @@ const parseCSSColor = (color, element) => {
440
442
 
441
443
  // Pass through CSS color functions we don't handle
442
444
  if (
443
- color.startsWith("color(") ||
444
445
  color.startsWith("lch(") ||
445
446
  color.startsWith("oklch(") ||
446
447
  color.startsWith("lab(") ||
@@ -451,6 +452,21 @@ const parseCSSColor = (color, element) => {
451
452
  return color;
452
453
  }
453
454
 
455
+ // color(srgb r g b) and color(srgb r g b / a)
456
+ if (color.startsWith("color(")) {
457
+ const srgbMatch = color.match(
458
+ /^color\(\s*srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+))?\s*\)$/,
459
+ );
460
+ if (srgbMatch) {
461
+ const r = Math.round(parseFloat(srgbMatch[1]) * 255);
462
+ const g = Math.round(parseFloat(srgbMatch[2]) * 255);
463
+ const b = Math.round(parseFloat(srgbMatch[3]) * 255);
464
+ const a = srgbMatch[4] !== undefined ? parseFloat(srgbMatch[4]) : 1;
465
+ return [r, g, b, a];
466
+ }
467
+ return color;
468
+ }
469
+
454
470
  // Pass through relative color syntax (CSS Color Module Level 5)
455
471
  if (color.includes(" from ")) {
456
472
  return color;
@@ -598,8 +614,9 @@ const convertColorToRgba = (color) => {
598
614
  }
599
615
 
600
616
  // Named colors (basic set)
601
- if (namedColors[color]) {
602
- return [...namedColors[color], 1];
617
+ const namedColorRgb = namedColors[color];
618
+ if (namedColorRgb) {
619
+ return [...namedColorRgb, 1];
603
620
  }
604
621
  return null;
605
622
  };
@@ -3322,46 +3339,60 @@ const createSetMany = (setter) => {
3322
3339
 
3323
3340
  const setAttributes = createSetMany(setAttribute);
3324
3341
 
3342
+ const isSameColor = (color1, color2) => {
3343
+ if (color1 === color2) {
3344
+ return true;
3345
+ }
3346
+ const color1String = String(parseCSSColor(color1));
3347
+ const color2String = String(parseCSSColor(color2));
3348
+ return color1String === color2String;
3349
+ };
3350
+
3325
3351
  /**
3326
- * Chooses between light and dark colors based on which provides better contrast against a background
3327
- * @param {string} backgroundColor - CSS color value (hex, rgb, hsl, CSS variable, etc.) to test against
3328
- * @param {string} [lightColor="white"] - Light color option (typically for dark backgrounds)
3329
- * @param {string} [darkColor="black"] - Dark color option (typically for light backgrounds)
3330
- * @param {Element} [element] - DOM element to resolve CSS variables against
3331
- * @returns {string} The color that provides better contrast (lightColor or darkColor)
3332
- * @example
3333
- * // Choose text color for a dark blue background
3334
- * pickLightOrDark("#1a202c") // returns "white"
3352
+ * Returns `"white"` or `"black"`, whichever provides better contrast against
3353
+ * the given background color mirroring the CSS `contrast-color()` function.
3354
+ *
3355
+ * `"white"` is preferred when both colors yield the same contrast ratio.
3335
3356
  *
3336
- * // Choose text color for a light background with CSS variable
3337
- * pickLightOrDark("var(--bg-color)", "white", "black", element) // returns "black" or "white"
3357
+ * @param {string} backgroundColor - CSS color value (hex, rgb, hsl, CSS variable, …)
3358
+ * @param {Element} [element] - DOM element used to resolve CSS variables / computed styles
3359
+ * @returns {"white"|"black"}
3360
+ * @example
3361
+ * contrastColor("#1a202c") // "white" (dark background)
3362
+ * contrastColor("#f5f5f5") // "black" (light background)
3363
+ * contrastColor("var(--bg)", el) // "white" or "black"
3338
3364
  */
3339
3365
 
3340
3366
 
3341
- const pickLightOrDark = (
3342
- backgroundColor,
3343
- lightColor = "white",
3344
- darkColor = "black",
3345
- element,
3346
- ) => {
3367
+ const contrastColor = (backgroundColor, element) => {
3347
3368
  const resolvedBgColor = parseCSSColor(backgroundColor, element);
3348
- const resolvedLightColor = parseCSSColor(lightColor, element);
3349
- const resolvedDarkColor = parseCSSColor(darkColor, element);
3350
-
3351
- if (!resolvedBgColor || !resolvedLightColor || !resolvedDarkColor) {
3352
- // Fallback to light color if parsing fails
3353
- return lightColor;
3369
+ if (!resolvedBgColor) {
3370
+ return "white";
3354
3371
  }
3355
3372
 
3356
- const contrastWithLight = getContrastRatio(
3357
- resolvedBgColor,
3358
- resolvedLightColor,
3359
- );
3360
- const contrastWithDark = getContrastRatio(resolvedBgColor, resolvedDarkColor);
3373
+ // Composite against white when the background has transparency so the
3374
+ // luminance reflects what the user actually sees.
3375
+ const [r, g, b] =
3376
+ resolvedBgColor[3] === 1
3377
+ ? resolvedBgColor
3378
+ : compositeColor(resolvedBgColor, WHITE_RGBA);
3379
+
3380
+ const bgLuminance = getLuminance(r, g, b);
3361
3381
 
3362
- return contrastWithLight > contrastWithDark ? lightColor : darkColor;
3382
+ // One luminance comparison replaces two full contrast-ratio computations.
3383
+ // White wins (or ties) when bgLuminance <= the crossover point where both
3384
+ // colors yield identical ratios:
3385
+ // contrastWithWhite = contrastWithBlack
3386
+ // 1.05 / (L + 0.05) = (L + 0.05) / 0.05
3387
+ // L = √(1.05 × 0.05) − 0.05 ≈ 0.179
3388
+ return bgLuminance <= EQUAL_CONTRAST_LUMINANCE ? "white" : "black";
3363
3389
  };
3364
3390
 
3391
+ // Luminance threshold at which white and black yield the same contrast ratio
3392
+ // against a background. Below → white wins or ties; above → black wins.
3393
+ const EQUAL_CONTRAST_LUMINANCE = Math.sqrt(1.05 * 0.05) - 0.05;
3394
+ const WHITE_RGBA = [255, 255, 255, 1];
3395
+
3365
3396
  /**
3366
3397
  * Resolves the luminance value of a CSS color
3367
3398
  * @param {string} color - CSS color value (hex, rgb, hsl, CSS variable, etc.)
@@ -9991,7 +10022,6 @@ const createTransition = ({
9991
10022
  `Debug breakpoint hit at ${(breakpoint * 100).toFixed(1)}% progress`,
9992
10023
  );
9993
10024
  const notifyDebuggerEnd = notifyDebuggerStart();
9994
- debugger;
9995
10025
  notifyDebuggerEnd();
9996
10026
  }
9997
10027
  if (effect === "pause") {
@@ -11615,7 +11645,7 @@ const initFlexDetailsSet = (
11615
11645
  }
11616
11646
  };
11617
11647
 
11618
- const applyAllocatedSpaces = (resizeDetails) => {
11648
+ const applyAllocatedSpaces = ({ reason, animated }) => {
11619
11649
  const changeSet = new Set();
11620
11650
  let maxChange = 0;
11621
11651
 
@@ -11623,9 +11653,8 @@ const initFlexDetailsSet = (
11623
11653
  const allocatedSpace = allocatedSpaceMap.get(child);
11624
11654
  const allocatedSize = spaceToSize(allocatedSpace, child);
11625
11655
  const space = spaceMap.get(child);
11626
- const size = spaceToSize(space, child);
11656
+ const size = spaceToSize(space === undefined ? 0 : space, child);
11627
11657
  const sizeChange = Math.abs(size - allocatedSize);
11628
-
11629
11658
  if (size === allocatedSize) {
11630
11659
  continue;
11631
11660
  }
@@ -11658,16 +11687,18 @@ const initFlexDetailsSet = (
11658
11687
  }
11659
11688
 
11660
11689
  // Don't animate if changes are too small (avoids imperceptible animations that hide scrollbars)
11661
- const shouldAnimate =
11662
- resizeDetails.animated && maxChange >= ANIMATION_THRESHOLD_PX;
11690
+ const shouldAnimate = animated && maxChange >= ANIMATION_THRESHOLD_PX;
11663
11691
 
11664
- if (debug && resizeDetails.animated && !shouldAnimate) {
11692
+ if (debug && animated && !shouldAnimate) {
11665
11693
  console.debug(
11666
11694
  `🚫 Skipping animation: max change ${maxChange.toFixed(2)}px < ${ANIMATION_THRESHOLD_PX}px threshold`,
11667
11695
  );
11668
11696
  }
11669
11697
 
11670
11698
  if (!shouldAnimate) {
11699
+ if (debug) {
11700
+ console.debug(`Applying size changes without animation`);
11701
+ }
11671
11702
  const sizeChangeEntries = [];
11672
11703
  for (const { element, target, sideEffect } of changeSet) {
11673
11704
  element.style.height = `${target}px`;
@@ -11677,19 +11708,26 @@ const initFlexDetailsSet = (
11677
11708
  }
11678
11709
  sizeChangeEntries.push({ element, value: target });
11679
11710
  }
11680
- onSizeChange?.(sizeChangeEntries, resizeDetails);
11711
+ onSizeChange?.(sizeChangeEntries, { reason, animated });
11681
11712
  return;
11682
11713
  }
11683
11714
 
11715
+ if (debug) {
11716
+ console.debug(`Start animating size changes`);
11717
+ }
11684
11718
  // Create height animations for each element in changeSet
11685
11719
  const transitions = Array.from(changeSet).map(({ element, target }) => {
11686
11720
  const transition = createHeightTransition(element, target, {
11687
11721
  duration: HEIGHT_TRANSITION_DURATION,
11722
+ // because we also set inline height when we don't want animation and it should win
11723
+ // we could also commit styles for animation or cancel any animation so that when we explicitely set height
11724
+ // sync the transition gets overriden
11725
+ styleSynchronizer: "inline_style",
11688
11726
  });
11689
11727
  return transition;
11690
11728
  });
11691
11729
 
11692
- const transition = transitionController.animate(transitions, {
11730
+ const transition = transitionController.update(transitions, {
11693
11731
  onChange: (changeEntries, isLast) => {
11694
11732
  // Apply side effects for each animated element
11695
11733
  for (const { transition, value } of changeEntries) {
@@ -11713,7 +11751,7 @@ const initFlexDetailsSet = (
11713
11751
  );
11714
11752
  onSizeChange(
11715
11753
  sizeChangeEntries,
11716
- isLast ? { ...resizeDetails, animated: false } : resizeDetails,
11754
+ isLast ? { reason, animated: false } : { reason, animated },
11717
11755
  );
11718
11756
  }
11719
11757
  },
@@ -11958,23 +11996,23 @@ const initFlexDetailsSet = (
11958
11996
  }
11959
11997
  };
11960
11998
 
11961
- const updateSpaceDistribution = (resizeDetails) => {
11999
+ const updateSpaceDistribution = ({ reason, animated }) => {
11962
12000
  if (debug) {
11963
- console.group(`updateSpaceDistribution: ${resizeDetails.reason}`);
12001
+ console.group(`updateSpaceDistribution: ${reason}`);
11964
12002
  }
11965
12003
  prepareSpaceDistribution();
11966
- distributeAvailableSpace(resizeDetails.reason);
12004
+ distributeAvailableSpace(reason);
11967
12005
  distributeRemainingSpace({
11968
12006
  childToGrow: openedDetailsArray[openedDetailsArray.length - 1],
11969
12007
  childToShrinkFrom: lastChild,
11970
12008
  });
11971
12009
  if (
11972
- resizeDetails.reason === "initial_space_distribution" ||
11973
- resizeDetails.reason === "content_change"
12010
+ reason === "initial_space_distribution" ||
12011
+ reason === "content_change"
11974
12012
  ) {
11975
12013
  spaceMap.clear(); // force to set size at start
11976
12014
  }
11977
- applyAllocatedSpaces(resizeDetails);
12015
+ applyAllocatedSpaces({ reason, animated });
11978
12016
  saveCurrentSizeAsRequestedSizes();
11979
12017
  if (debug) {
11980
12018
  console.groupEnd();
@@ -12015,6 +12053,19 @@ const initFlexDetailsSet = (
12015
12053
  }
12016
12054
  }
12017
12055
  if (someNew || someOld) {
12056
+ for (const child of container.children) {
12057
+ if (!child.dispatchEvent) {
12058
+ // ignore text nodes
12059
+ continue;
12060
+ }
12061
+ child.dispatchEvent(
12062
+ new CustomEvent("resizablechange", {
12063
+ detail: {
12064
+ resizable: resizableDetailsIdSet.has(child.id),
12065
+ },
12066
+ }),
12067
+ );
12068
+ }
12018
12069
  onResizableDetailsChange?.(resizableDetailsIdSet);
12019
12070
  }
12020
12071
  };
@@ -12231,11 +12282,18 @@ const initFlexDetailsSet = (
12231
12282
  if (currentAllocatedSpaceMap) {
12232
12283
  allocatedSpaceMap = currentAllocatedSpaceMap;
12233
12284
  saveCurrentSizeAsRequestedSizes({ replaceExistingAttributes: true });
12234
- if (onRequestedSizeChange) {
12235
- for (const [child, allocatedSpace] of allocatedSpaceMap) {
12236
- const size = spaceToSize(allocatedSpace, child);
12285
+ for (const [child, allocatedSpace] of allocatedSpaceMap) {
12286
+ const size = spaceToSize(allocatedSpace, child);
12287
+ if (onRequestedSizeChange) {
12237
12288
  onRequestedSizeChange(child, size);
12238
12289
  }
12290
+ child.dispatchEvent(
12291
+ new CustomEvent("resizeend", {
12292
+ detail: {
12293
+ size,
12294
+ },
12295
+ }),
12296
+ );
12239
12297
  }
12240
12298
  onMouseResizeEnd?.();
12241
12299
  }
@@ -12673,4 +12731,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
12673
12731
  };
12674
12732
  };
12675
12733
 
12676
- export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, canInterceptKeys, captureScrollState, 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, isScrollable, measureScrollbar, mergeOneStyle, mergeTwoStyles, normalizeStyles, parseStyle, pickLightOrDark, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, resolveCSSColor, resolveCSSSize, resolveColorLuminance, setAttribute, setAttributes, setStyles, startDragToResizeGesture, stickyAsRelativeCoords, stringifyStyle, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
12734
+ 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, 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.8.8",
3
+ "version": "0.9.1",
4
4
  "description": "DOM utilities for writing frontend code",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,7 +42,7 @@
42
42
  "@jsenv/core": "../../../",
43
43
  "@jsenv/navi": "../navi",
44
44
  "@jsenv/snapshot": "../../tooling/snapshot",
45
- "@preact/signals": "2.5.1",
45
+ "@preact/signals": "2.8.1",
46
46
  "preact": "11.0.0-beta.0"
47
47
  },
48
48
  "publishConfig": {