@rogieking/figui3 8.9.42 → 8.9.44

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
@@ -250,6 +250,7 @@ propskit-group {
250
250
  & > fig-header {
251
251
  cursor: default;
252
252
  padding-left: var(--spacer-3);
253
+ gap: 0;
253
254
 
254
255
  &:focus-visible {
255
256
  --figma-focus-outline-radius: var(--radius-medium);
@@ -258,19 +259,28 @@ propskit-group {
258
259
  border-radius: var(--figma-focus-outline-radius);
259
260
  }
260
261
 
261
- h3 {
262
+ & > h3 {
263
+ flex: 1 1 auto;
264
+ min-width: 0;
265
+ display: block;
262
266
  margin: 0;
267
+ padding-left: 0;
268
+ text-overflow: ellipsis;
269
+ overflow: hidden;
270
+ white-space: nowrap;
271
+ max-width: 100%;
263
272
  user-select: none;
273
+ }
264
274
 
265
- .propskit-group-chevron {
266
- --icon: var(--icon-16-chevron);
267
- --size: 1rem;
268
- color: var(--figma-color-text-tertiary);
269
- transform: rotate(-90deg);
270
- transition: transform var(--transition-duration)
271
- var(--transition-timing-function);
272
- margin-left: calc(var(--spacer-3) * -1);
273
- }
275
+ & > .propskit-group-chevron {
276
+ --icon: var(--icon-16-chevron);
277
+ --size: 1rem;
278
+ flex: 0 0 auto;
279
+ color: var(--figma-color-text-tertiary);
280
+ transform: rotate(-90deg);
281
+ transition: transform var(--transition-duration)
282
+ var(--transition-timing-function);
283
+ margin-left: calc(var(--spacer-2) * -1);
274
284
  }
275
285
  }
276
286
 
@@ -1060,6 +1070,12 @@ propskit-slider {
1060
1070
  var(--propskit-slider-fig-slider-height) - var(--spacer-3)
1061
1071
  );
1062
1072
  --propskit-slider-number-right: calc(2px + var(--spacer-1));
1073
+ --propskit-slider-handle-fade-start-threshold: var(--spacer-3);
1074
+ --propskit-slider-handle-fade-end-threshold: var(--spacer-2);
1075
+ --propskit-slider-handle-proximity: 1;
1076
+ --propskit-slider-handle-reveal: 0;
1077
+ --propskit-slider-handle-scale: var(--propskit-slider-handle-proximity);
1078
+ --propskit-slider-handle-color: light-dark(#222, var(--figma-color-text));
1063
1079
 
1064
1080
  display: block;
1065
1081
  position: relative;
@@ -1075,6 +1091,11 @@ propskit-slider {
1075
1091
  padding-left: var(--propskit-slider-horizontal-padding);
1076
1092
  padding-right: var(--propskit-slider-horizontal-padding);
1077
1093
 
1094
+ &:hover,
1095
+ &:focus-within {
1096
+ --propskit-slider-handle-reveal: 1;
1097
+ }
1098
+
1078
1099
  &[size="small"] {
1079
1100
  --propskit-slider-height: 1.5rem;
1080
1101
  --propskit-slider-fig-slider-thumb-height: calc(
@@ -1149,10 +1170,7 @@ propskit-slider {
1149
1170
  }
1150
1171
  --slider-field-height: var(--propskit-slider-height);
1151
1172
  --slider-thumb-width: var(--spacer-1);
1152
- --slider-thumb-color: light-dark(
1153
- var(--figma-color-text-secondary),
1154
- var(--figma-color-text)
1155
- );
1173
+ --slider-thumb-color: var(--propskit-slider-handle-color);
1156
1174
  /*--slider-thumb-height: calc(
1157
1175
  var(--spacer-2) + (var(--spacer-3) - var(--spacer-2)) *
1158
1176
  min(var(--slider-complete), calc(1 - var(--slider-complete))) * 2
@@ -1177,12 +1195,10 @@ propskit-slider {
1177
1195
  );
1178
1196
  --slider-handle-shadow:
1179
1197
  inset 0 0 0 var(--slider-thumb-height) var(--slider-thumb-color) !important;
1180
- /* Fade handle toward track ends (0 at edges, 1 at center). Squared = sharper falloff. */
1181
- --slider-thumb-edge-fade: calc(
1182
- min(var(--slider-complete), calc(1 - var(--slider-complete))) * 2
1183
- );
1184
1198
  --slider-thumb-opacity: calc(
1185
- var(--slider-thumb-edge-fade) * var(--slider-thumb-edge-fade)
1199
+ var(--propskit-slider-handle-proximity) *
1200
+ var(--propskit-slider-handle-proximity) *
1201
+ var(--propskit-slider-handle-reveal)
1186
1202
  );
1187
1203
  --slider-tick-opacity: 0.5;
1188
1204
 
@@ -1480,6 +1496,16 @@ propskit-slider input[type="range"]:active::-moz-range-thumb {
1480
1496
  cursor: ew-resize;
1481
1497
  }
1482
1498
 
1499
+ propskit-slider input[type="range"]::-webkit-slider-thumb {
1500
+ transform: scale(var(--propskit-slider-handle-scale));
1501
+ transform-origin: center;
1502
+ }
1503
+
1504
+ propskit-slider input[type="range"]::-moz-range-thumb {
1505
+ transform: scale(var(--propskit-slider-handle-scale));
1506
+ transform-origin: center;
1507
+ }
1508
+
1483
1509
  fig-input-wheel {
1484
1510
  --fig-input-wheel-handle-drag-max: 0.5rem;
1485
1511
  --fig-input-wheel-handle-drag-offset: 0px;
package/fig-lab.js CHANGED
@@ -4589,10 +4589,7 @@ class PropskitGroup extends HTMLElement {
4589
4589
 
4590
4590
  if (!h3.id) h3.id = figLabUniqueId("propskit-group");
4591
4591
  if (this.#header.dataset.generated) {
4592
- // Preserve chevron while updating the title text node.
4593
- const chevron = h3.querySelector(".propskit-group-chevron");
4594
4592
  h3.textContent = label;
4595
- if (chevron) h3.prepend(chevron);
4596
4593
  }
4597
4594
  if (!this.hasAttribute("role")) this.setAttribute("role", "group");
4598
4595
  if (
@@ -4602,14 +4599,17 @@ class PropskitGroup extends HTMLElement {
4602
4599
  this.setAttribute("aria-labelledby", h3.id);
4603
4600
  }
4604
4601
 
4605
- if (!h3.querySelector(".propskit-group-chevron")) {
4606
- const chevron = document.createElement("fig-icon");
4602
+ let chevron =
4603
+ this.#header.querySelector(":scope > .propskit-group-chevron") ||
4604
+ h3.querySelector(":scope > .propskit-group-chevron");
4605
+ if (!chevron) {
4606
+ chevron = document.createElement("fig-icon");
4607
4607
  chevron.setAttribute("name", "chevron");
4608
4608
  chevron.setAttribute("size", "small");
4609
4609
  chevron.className = "propskit-group-chevron";
4610
- h3.prepend(chevron);
4611
4610
  }
4612
- this.#chevron = h3.querySelector(".propskit-group-chevron");
4611
+ this.#header.insertBefore(chevron, h3);
4612
+ this.#chevron = chevron;
4613
4613
  this.#syncResetButton();
4614
4614
  this.#header.setAttribute("role", "button");
4615
4615
  this.#syncDisabled();
@@ -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/fig.js CHANGED
@@ -16987,14 +16987,15 @@ class FigGroup extends HTMLElement {
16987
16987
  }
16988
16988
 
16989
16989
  if (isCollapsible) {
16990
- if (!h3.querySelector(".fig-group-chevron")) {
16991
- const chevron = createFigIcon("chevron", {
16990
+ let chevron = this.#header.querySelector(".fig-group-chevron");
16991
+ if (!chevron) {
16992
+ chevron = createFigIcon("chevron", {
16992
16993
  size: "small",
16993
16994
  className: "fig-group-chevron",
16994
16995
  });
16995
- h3.prepend(chevron);
16996
16996
  }
16997
- this.#chevron = h3.querySelector(".fig-group-chevron");
16997
+ this.#header.insertBefore(chevron, h3);
16998
+ this.#chevron = chevron;
16998
16999
  h3.removeEventListener("click", this.#handleToggle);
16999
17000
  this.#header.removeEventListener("click", this.#handleToggle);
17000
17001
  this.#header.addEventListener("click", this.#handleToggle);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "8.9.42",
3
+ "version": "8.9.44",
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",