@rogieking/figui3 8.9.41 → 8.9.43

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/fig-lab.css CHANGED
@@ -1060,6 +1060,12 @@ propskit-slider {
1060
1060
  var(--propskit-slider-fig-slider-height) - var(--spacer-3)
1061
1061
  );
1062
1062
  --propskit-slider-number-right: calc(2px + var(--spacer-1));
1063
+ --propskit-slider-handle-fade-start-threshold: var(--spacer-3);
1064
+ --propskit-slider-handle-fade-end-threshold: var(--spacer-2);
1065
+ --propskit-slider-handle-proximity: 1;
1066
+ --propskit-slider-handle-reveal: 0;
1067
+ --propskit-slider-handle-scale: var(--propskit-slider-handle-proximity);
1068
+ --propskit-slider-handle-color: light-dark(#222, var(--figma-color-text));
1063
1069
 
1064
1070
  display: block;
1065
1071
  position: relative;
@@ -1075,6 +1081,11 @@ propskit-slider {
1075
1081
  padding-left: var(--propskit-slider-horizontal-padding);
1076
1082
  padding-right: var(--propskit-slider-horizontal-padding);
1077
1083
 
1084
+ &:hover,
1085
+ &:focus-within {
1086
+ --propskit-slider-handle-reveal: 1;
1087
+ }
1088
+
1078
1089
  &[size="small"] {
1079
1090
  --propskit-slider-height: 1.5rem;
1080
1091
  --propskit-slider-fig-slider-thumb-height: calc(
@@ -1149,10 +1160,7 @@ propskit-slider {
1149
1160
  }
1150
1161
  --slider-field-height: var(--propskit-slider-height);
1151
1162
  --slider-thumb-width: var(--spacer-1);
1152
- --slider-thumb-color: light-dark(
1153
- var(--figma-color-text-secondary),
1154
- var(--figma-color-text)
1155
- );
1163
+ --slider-thumb-color: var(--propskit-slider-handle-color);
1156
1164
  /*--slider-thumb-height: calc(
1157
1165
  var(--spacer-2) + (var(--spacer-3) - var(--spacer-2)) *
1158
1166
  min(var(--slider-complete), calc(1 - var(--slider-complete))) * 2
@@ -1176,14 +1184,11 @@ propskit-slider {
1176
1184
  var(--propskit-slider-thumb-inset)
1177
1185
  );
1178
1186
  --slider-handle-shadow:
1179
- inset 0 0 0 var(--slider-thumb-height) var(--slider-thumb-color),
1180
- var(--handle-shadow) !important;
1181
- /* Fade handle toward track ends (0 at edges, 1 at center). Squared = sharper falloff. */
1182
- --slider-thumb-edge-fade: calc(
1183
- min(var(--slider-complete), calc(1 - var(--slider-complete))) * 2
1184
- );
1187
+ inset 0 0 0 var(--slider-thumb-height) var(--slider-thumb-color) !important;
1185
1188
  --slider-thumb-opacity: calc(
1186
- var(--slider-thumb-edge-fade) * var(--slider-thumb-edge-fade)
1189
+ var(--propskit-slider-handle-proximity) *
1190
+ var(--propskit-slider-handle-proximity) *
1191
+ var(--propskit-slider-handle-reveal)
1187
1192
  );
1188
1193
  --slider-tick-opacity: 0.5;
1189
1194
 
@@ -1481,6 +1486,16 @@ propskit-slider input[type="range"]:active::-moz-range-thumb {
1481
1486
  cursor: ew-resize;
1482
1487
  }
1483
1488
 
1489
+ propskit-slider input[type="range"]::-webkit-slider-thumb {
1490
+ transform: scale(var(--propskit-slider-handle-scale));
1491
+ transform-origin: center;
1492
+ }
1493
+
1494
+ propskit-slider input[type="range"]::-moz-range-thumb {
1495
+ transform: scale(var(--propskit-slider-handle-scale));
1496
+ transform-origin: center;
1497
+ }
1498
+
1484
1499
  fig-input-wheel {
1485
1500
  --fig-input-wheel-handle-drag-max: 0.5rem;
1486
1501
  --fig-input-wheel-handle-drag-offset: 0px;
@@ -1568,8 +1583,7 @@ fig-input-wheel {
1568
1583
  transition: transform var(--fig-input-wheel-handle-spring);
1569
1584
  box-shadow:
1570
1585
  inset 0 0 0 var(--fig-input-wheel-handle-height)
1571
- var(--fig-input-wheel-handle-color),
1572
- var(--handle-shadow);
1586
+ var(--fig-input-wheel-handle-color);
1573
1587
  }
1574
1588
 
1575
1589
  &[data-fig-input-wheel-active] .fig-input-wheel-handle {
package/fig-lab.js CHANGED
@@ -4631,6 +4631,7 @@ figLabDefineElement("propskit-group", PropskitGroup);
4631
4631
  class PropskitSlider extends HTMLElement {
4632
4632
  static #DRAG_THRESHOLD_PX = 4;
4633
4633
  static #DRAGGING_BODY_CLASS = "propskit-slider-dragging";
4634
+ static #TEXT_MEASURE_CANVAS = null;
4634
4635
 
4635
4636
  #field = null;
4636
4637
  #label = null;
@@ -4640,6 +4641,12 @@ class PropskitSlider extends HTMLElement {
4640
4641
  #managedSliderAttrs = new Set();
4641
4642
  #steppersSyncFrame = 0;
4642
4643
  #focusSyncFrame = 0;
4644
+ #handleProximityFrame = 0;
4645
+ #handleProximityObserver = null;
4646
+ #handleProximityMetricsDirty = true;
4647
+ #handleRadius = 0;
4648
+ #handleFadeStartThreshold = 1;
4649
+ #handleFadeEndThreshold = 0;
4643
4650
  #rangeInput = null;
4644
4651
  #contextMenu = null;
4645
4652
  #pendingClickTimer = 0;
@@ -4698,6 +4705,8 @@ class PropskitSlider extends HTMLElement {
4698
4705
  this.#syncSliderAttributes();
4699
4706
  this.#bindSliderEvents();
4700
4707
  this.#queueFocusDelegationSync();
4708
+ this.#connectHandleProximityObserver();
4709
+ this.#queueHandleProximitySync();
4701
4710
  this.removeEventListener("pointerdown", this.#boundHandleElasticPointerDown, {
4702
4711
  capture: true,
4703
4712
  });
@@ -4777,6 +4786,11 @@ class PropskitSlider extends HTMLElement {
4777
4786
  cancelAnimationFrame(this.#focusSyncFrame);
4778
4787
  this.#focusSyncFrame = 0;
4779
4788
  }
4789
+ if (this.#handleProximityFrame) {
4790
+ cancelAnimationFrame(this.#handleProximityFrame);
4791
+ this.#handleProximityFrame = 0;
4792
+ }
4793
+ this.#handleProximityObserver?.disconnect();
4780
4794
  this.#clearPendingClick();
4781
4795
  this.#stopNumberTracking();
4782
4796
  clearTimeout(this.#numberClickResetTimer);
@@ -4881,6 +4895,7 @@ class PropskitSlider extends HTMLElement {
4881
4895
  "direction",
4882
4896
  this.getAttribute("direction") || "horizontal",
4883
4897
  );
4898
+ this.#queueHandleProximitySync();
4884
4899
  }
4885
4900
 
4886
4901
  #syncSliderAttributes() {
@@ -4941,6 +4956,9 @@ class PropskitSlider extends HTMLElement {
4941
4956
  this.#managedSliderAttrs = nextManaged;
4942
4957
  this.#pushExternalValueToSlider();
4943
4958
  this.#queueSteppersSync();
4959
+ this.#handleProximityMetricsDirty = true;
4960
+ this.#connectHandleProximityObserver();
4961
+ this.#queueHandleProximitySync();
4944
4962
  }
4945
4963
 
4946
4964
  #syncNumberTheme(sliderType) {
@@ -5006,6 +5024,207 @@ class PropskitSlider extends HTMLElement {
5006
5024
  : 0;
5007
5025
  this.style.setProperty("--propskit-slider-complete", String(complete));
5008
5026
  this.#syncNumberTheme((this.getAttribute("type") || "range").toLowerCase());
5027
+ this.#queueHandleProximitySync();
5028
+ }
5029
+
5030
+ #connectHandleProximityObserver() {
5031
+ if (!globalThis.ResizeObserver) return;
5032
+ if (!this.#handleProximityObserver) {
5033
+ this.#handleProximityObserver = new ResizeObserver(() => {
5034
+ this.#handleProximityMetricsDirty = true;
5035
+ this.#queueHandleProximitySync();
5036
+ });
5037
+ }
5038
+ this.#handleProximityObserver.disconnect();
5039
+ for (const element of [
5040
+ this,
5041
+ this.#label,
5042
+ this.#slider?.querySelector("fig-input-number"),
5043
+ this.#slider?.querySelector("fig-input-number input"),
5044
+ this.#slider?.querySelector('input[type="range"]'),
5045
+ ]) {
5046
+ if (element) this.#handleProximityObserver.observe(element);
5047
+ }
5048
+ }
5049
+
5050
+ #queueHandleProximitySync() {
5051
+ if (this.#handleProximityFrame) {
5052
+ cancelAnimationFrame(this.#handleProximityFrame);
5053
+ }
5054
+ this.#handleProximityFrame = requestAnimationFrame(() => {
5055
+ this.#handleProximityFrame = 0;
5056
+ this.#syncHandleProximity();
5057
+ });
5058
+ }
5059
+
5060
+ #syncHandleProximity() {
5061
+ const rangeInput = this.#slider?.querySelector('input[type="range"]');
5062
+ if (!rangeInput || !this.#slider) return;
5063
+ const rangeRect = rangeInput.getBoundingClientRect();
5064
+ if (!rangeRect.width) return;
5065
+
5066
+ const min = Number(rangeInput.min || 0);
5067
+ const max = Number(rangeInput.max || 100);
5068
+ const value = Number(rangeInput.value);
5069
+ const complete =
5070
+ Number.isFinite(min) &&
5071
+ Number.isFinite(max) &&
5072
+ max > min &&
5073
+ Number.isFinite(value)
5074
+ ? Math.max(0, Math.min(1, (value - min) / (max - min)))
5075
+ : 0;
5076
+ const handleCenter = rangeRect.left + complete * rangeRect.width;
5077
+ if (this.#handleProximityMetricsDirty) {
5078
+ this.#handleRadius =
5079
+ this.#readCssLength("--slider-thumb-width", this.#slider) / 2;
5080
+ this.#handleFadeStartThreshold = Math.max(
5081
+ 1,
5082
+ this.#readCssLength(
5083
+ "--propskit-slider-handle-fade-start-threshold",
5084
+ ),
5085
+ );
5086
+ this.#handleFadeEndThreshold = Math.min(
5087
+ this.#handleFadeStartThreshold,
5088
+ this.#readCssLength(
5089
+ "--propskit-slider-handle-fade-end-threshold",
5090
+ ),
5091
+ );
5092
+ this.#handleProximityMetricsDirty = false;
5093
+ }
5094
+ const handleRadius = this.#handleRadius;
5095
+ const distances = [];
5096
+ const labelRect =
5097
+ this.#label?.parentElement === this.#field
5098
+ ? this.#contentRect(this.#label)
5099
+ : null;
5100
+ const numberInput = this.#slider.querySelector("fig-input-number input");
5101
+ const numberRect = numberInput ? this.#inputTextRect(numberInput) : null;
5102
+
5103
+ if (
5104
+ labelRect?.width &&
5105
+ labelRect.bottom > rangeRect.top &&
5106
+ labelRect.top < rangeRect.bottom
5107
+ ) {
5108
+ distances.push(handleCenter - handleRadius - labelRect.right);
5109
+ }
5110
+ if (
5111
+ numberRect?.width &&
5112
+ numberRect.bottom > rangeRect.top &&
5113
+ numberRect.top < rangeRect.bottom
5114
+ ) {
5115
+ distances.push(numberRect.left - handleCenter - handleRadius);
5116
+ }
5117
+
5118
+ const fadeStartThreshold = this.#handleFadeStartThreshold;
5119
+ const fadeEndThreshold = this.#handleFadeEndThreshold;
5120
+ const nearestDistance = distances.length
5121
+ ? Math.min(...distances)
5122
+ : fadeStartThreshold;
5123
+ const fadeRange = Math.max(
5124
+ 0.001,
5125
+ fadeStartThreshold - fadeEndThreshold,
5126
+ );
5127
+ const proximity = Math.max(
5128
+ 0,
5129
+ Math.min(1, (nearestDistance - fadeEndThreshold) / fadeRange),
5130
+ );
5131
+ this.style.setProperty(
5132
+ "--propskit-slider-handle-proximity",
5133
+ String(proximity),
5134
+ );
5135
+ }
5136
+
5137
+ #contentRect(element) {
5138
+ const range = document.createRange();
5139
+ range.selectNodeContents(element);
5140
+ const rects = [...range.getClientRects()].filter(
5141
+ (rect) => rect.width > 0 && rect.height > 0,
5142
+ );
5143
+ range.detach?.();
5144
+ if (!rects.length) return null;
5145
+ const left = Math.min(...rects.map((rect) => rect.left));
5146
+ const right = Math.max(...rects.map((rect) => rect.right));
5147
+ const top = Math.min(...rects.map((rect) => rect.top));
5148
+ const bottom = Math.max(...rects.map((rect) => rect.bottom));
5149
+ return {
5150
+ left,
5151
+ right,
5152
+ top,
5153
+ bottom,
5154
+ width: right - left,
5155
+ height: bottom - top,
5156
+ };
5157
+ }
5158
+
5159
+ #inputTextRect(input) {
5160
+ const text = input.value || input.placeholder || "";
5161
+ if (!text) return null;
5162
+ const inputRect = input.getBoundingClientRect();
5163
+ const style = getComputedStyle(input);
5164
+ const textWidth = this.#measureTextWidth(text, style);
5165
+ const contentLeft =
5166
+ inputRect.left +
5167
+ (Number.parseFloat(style.borderLeftWidth) || 0) +
5168
+ (Number.parseFloat(style.paddingLeft) || 0);
5169
+ const contentRight =
5170
+ inputRect.right -
5171
+ (Number.parseFloat(style.borderRightWidth) || 0) -
5172
+ (Number.parseFloat(style.paddingRight) || 0);
5173
+ const contentWidth = Math.max(0, contentRight - contentLeft);
5174
+ const width = Math.min(contentWidth, textWidth);
5175
+ const alignment = style.textAlign;
5176
+ let left = contentLeft;
5177
+ if (alignment === "right" || alignment === "end") {
5178
+ left = contentRight - width;
5179
+ } else if (alignment === "center") {
5180
+ left = contentLeft + (contentWidth - width) / 2;
5181
+ }
5182
+ return {
5183
+ left,
5184
+ right: left + width,
5185
+ top: inputRect.top,
5186
+ bottom: inputRect.bottom,
5187
+ width,
5188
+ height: inputRect.height,
5189
+ };
5190
+ }
5191
+
5192
+ #measureTextWidth(text, style) {
5193
+ if (!PropskitSlider.#TEXT_MEASURE_CANVAS) {
5194
+ PropskitSlider.#TEXT_MEASURE_CANVAS = document.createElement("canvas");
5195
+ }
5196
+ const context = PropskitSlider.#TEXT_MEASURE_CANVAS.getContext("2d");
5197
+ if (!context) return 0;
5198
+ context.font =
5199
+ style.font ||
5200
+ `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
5201
+ if ("fontKerning" in context) context.fontKerning = style.fontKerning;
5202
+ if ("fontStretch" in context) context.fontStretch = style.fontStretch;
5203
+ if ("fontVariantCaps" in context) {
5204
+ context.fontVariantCaps = style.fontVariantCaps;
5205
+ }
5206
+ if ("letterSpacing" in context) {
5207
+ context.letterSpacing = style.letterSpacing;
5208
+ }
5209
+ return context.measureText(text).width;
5210
+ }
5211
+
5212
+ #readCssLength(property, context = this) {
5213
+ const probe = document.createElement("div");
5214
+ probe.style.setProperty(
5215
+ property,
5216
+ getComputedStyle(context).getPropertyValue(property),
5217
+ );
5218
+ Object.assign(probe.style, {
5219
+ position: "absolute",
5220
+ visibility: "hidden",
5221
+ pointerEvents: "none",
5222
+ width: `var(${property})`,
5223
+ });
5224
+ this.appendChild(probe);
5225
+ const value = Number.parseFloat(getComputedStyle(probe).width);
5226
+ probe.remove();
5227
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
5009
5228
  }
5010
5229
 
5011
5230
  #pushExternalValueToSlider() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "8.9.41",
3
+ "version": "8.9.43",
4
4
  "description": "A lightweight web components library for building Figma plugin and widget UIs with native look and feel",
5
5
  "author": "Rogie King",
6
6
  "license": "SEE LICENSE IN LICENSE",