@rogieking/figui3 2.25.0 → 2.27.0

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/components.css CHANGED
@@ -1152,7 +1152,7 @@ fig-chit {
1152
1152
  background: var(--chit-background);
1153
1153
  background-size: var(--chit-bg-size);
1154
1154
  background-position: var(--chit-bg-position);
1155
- background-repeat: repeat;
1155
+ background-repeat: no-repeat;
1156
1156
  border-radius: 0.125rem;
1157
1157
  box-shadow: inset 0 0 0 1px var(--figma-color-bordertranslucent);
1158
1158
  }
@@ -1199,16 +1199,21 @@ fig-chit {
1199
1199
  /* Fig Image */
1200
1200
  fig-image {
1201
1201
  --image-size: 2rem;
1202
+ --fit: cover;
1203
+ --image-width: var(--image-size);
1204
+ --image-height: var(--image-size);
1205
+ --aspect-ratio: 1 / 1;
1202
1206
  display: inline-grid;
1203
1207
  place-items: center;
1204
- width: var(--image-size);
1205
- height: var(--image-size);
1206
- aspect-ratio: 1 / 1;
1208
+ width: var(--image-width);
1209
+ height: var(--image-height);
1210
+ aspect-ratio: var(--aspect-ratio);
1207
1211
  > * {
1208
1212
  grid-area: 1/1;
1209
1213
  }
1210
1214
  fig-chit {
1211
- --size: var(--image-size) !important;
1215
+ --size: 100% !important;
1216
+ --chit-bg-size: var(--fit);
1212
1217
  &[disabled] {
1213
1218
  opacity: 1;
1214
1219
  }
@@ -1224,10 +1229,16 @@ fig-image {
1224
1229
  &[size="large"] {
1225
1230
  --image-size: 8rem;
1226
1231
  }
1227
- &[aspect-ratio="auto"] {
1228
- aspect-ratio: var(--aspect-ratio);
1232
+ &[size="auto"] {
1233
+ --image-size: 6rem;
1234
+ &[full]:not([full="false"]) {
1235
+ --image-width: 100%;
1236
+ }
1237
+ }
1238
+ &[aspect-ratio] {
1229
1239
  height: auto;
1230
1240
  fig-chit {
1241
+ --size: 100% !important;
1231
1242
  height: auto !important;
1232
1243
  aspect-ratio: var(--aspect-ratio) !important;
1233
1244
  }
@@ -1253,6 +1264,84 @@ fig-image {
1253
1264
  }
1254
1265
  }
1255
1266
 
1267
+ /* Easing Curve */
1268
+ fig-easing-curve {
1269
+ display: flex;
1270
+ flex-direction: column;
1271
+ gap: var(--spacer-2);
1272
+ --stroke-width: 1.25;
1273
+
1274
+ width: 100%;
1275
+ .fig-easing-curve-svg {
1276
+ width: 100%;
1277
+ aspect-ratio: 1 / 1;
1278
+ overflow: visible;
1279
+ }
1280
+ .fig-easing-curve-svg-container {
1281
+ border-radius: var(--radius-medium);
1282
+ background: var(--figma-color-bg-secondary);
1283
+ padding: var(--spacer-2);
1284
+ }
1285
+ .fig-easing-curve-diagonal,
1286
+ .fig-easing-curve-bounds,
1287
+ .fig-easing-curve-arm,
1288
+ .fig-easing-curve-path,
1289
+ .fig-easing-curve-target {
1290
+ pointer-events: none;
1291
+ }
1292
+ .fig-easing-curve-diagonal {
1293
+ stroke: var(--figma-color-bordertranslucent);
1294
+ stroke-width: var(--stroke-width);
1295
+ stroke-linejoin: round;
1296
+ stroke-linecap: round;
1297
+ }
1298
+ .fig-easing-curve-bounds {
1299
+ fill: transparent;
1300
+ }
1301
+ .fig-easing-curve-arm {
1302
+ stroke: var(--figma-color-border-strong);
1303
+ stroke-width: var(--stroke-width);
1304
+ stroke-linejoin: round;
1305
+ stroke-linecap: round;
1306
+ }
1307
+ .fig-easing-curve-path {
1308
+ fill: none;
1309
+ stroke: var(--figma-color-text);
1310
+ stroke-width: var(--stroke-width);
1311
+ stroke-linejoin: round;
1312
+ stroke-linecap: round;
1313
+ }
1314
+ .fig-easing-curve-target {
1315
+ stroke: var(--figma-color-bordertranslucent);
1316
+ stroke-width: var(--stroke-width);
1317
+ stroke-linejoin: round;
1318
+ stroke-linecap: round;
1319
+ }
1320
+ .fig-easing-curve-handle {
1321
+ fill: var(--figma-color-border-strong);
1322
+ cursor: grab;
1323
+ pointer-events: all;
1324
+ stroke: var(--figma-color-bg-secondary);
1325
+ stroke-width: var(--stroke-width);
1326
+ &:active {
1327
+ cursor: grabbing;
1328
+ fill: var(--figma-color-bg-brand);
1329
+ }
1330
+ }
1331
+ .fig-easing-curve-duration-bar {
1332
+ fill: var(--figma-color-border-strong);
1333
+ stroke: var(--figma-color-bg-secondary);
1334
+ stroke-width: var(--stroke-width);
1335
+ cursor: ew-resize;
1336
+ pointer-events: all;
1337
+ }
1338
+ .fig-easing-curve-dropdown {
1339
+ option svg {
1340
+ vertical-align: middle;
1341
+ }
1342
+ }
1343
+ }
1344
+
1256
1345
  /* Combo input */
1257
1346
  .input-combo {
1258
1347
  display: inline-flex;
@@ -2497,7 +2586,7 @@ fig-input-fill {
2497
2586
 
2498
2587
  fig-input-number {
2499
2588
  flex: 0;
2500
- flex-basis: 3.25rem;
2589
+ flex-basis: 3rem;
2501
2590
  }
2502
2591
 
2503
2592
  &[disabled]:not([disabled="false"]) {
package/fig.js CHANGED
@@ -365,6 +365,9 @@ customElements.define("fig-dropdown", FigDropdown);
365
365
  * @attr {string} offset - Comma-separated offset values: left,top,right,bottom
366
366
  */
367
367
  class FigTooltip extends HTMLElement {
368
+ static #lastShownAt = 0;
369
+ static #warmupWindow = 500;
370
+
368
371
  #boundHideOnChromeOpen;
369
372
  #boundHidePopupOutsideClick;
370
373
  #touchTimeout;
@@ -532,7 +535,9 @@ class FigTooltip extends HTMLElement {
532
535
  showDelayedPopup() {
533
536
  this.render();
534
537
  clearTimeout(this.timeout);
535
- this.timeout = setTimeout(this.showPopup.bind(this), this.delay);
538
+ const warm = Date.now() - FigTooltip.#lastShownAt < FigTooltip.#warmupWindow;
539
+ const effectiveDelay = warm ? 0 : this.delay;
540
+ this.timeout = setTimeout(this.showPopup.bind(this), effectiveDelay);
536
541
  }
537
542
 
538
543
  showPopup() {
@@ -544,6 +549,7 @@ class FigTooltip extends HTMLElement {
544
549
  this.popup.style.zIndex = figGetHighestZIndex() + 1;
545
550
 
546
551
  this.isOpen = true;
552
+ FigTooltip.#lastShownAt = Date.now();
547
553
  this.#startObserving();
548
554
  }
549
555
 
@@ -594,6 +600,7 @@ class FigTooltip extends HTMLElement {
594
600
  }
595
601
 
596
602
  this.isOpen = false;
603
+ FigTooltip.#lastShownAt = Date.now();
597
604
  }
598
605
 
599
606
  #startObserving() {
@@ -1078,7 +1085,9 @@ class FigPopup extends HTMLDialogElement {
1078
1085
  super();
1079
1086
  this.#boundReposition = this.#queueReposition.bind(this);
1080
1087
  this.#boundScroll = (e) => {
1081
- if (this.open && !this.contains(e.target)) this.#positionPopup();
1088
+ if (this.open && !this.contains(e.target) && this.#shouldAutoReposition()) {
1089
+ this.#positionPopup();
1090
+ }
1082
1091
  };
1083
1092
  this.#boundOutsidePointerDown = this.#handleOutsidePointerDown.bind(this);
1084
1093
  this.#boundPointerDown = this.#handlePointerDown.bind(this);
@@ -1215,6 +1224,7 @@ class FigPopup extends HTMLDialogElement {
1215
1224
 
1216
1225
  this.#setupObservers();
1217
1226
  document.addEventListener("pointerdown", this.#boundOutsidePointerDown, true);
1227
+ this.#wasDragged = false;
1218
1228
  this.#queueReposition();
1219
1229
  this.#isPopupActive = true;
1220
1230
 
@@ -1227,6 +1237,7 @@ class FigPopup extends HTMLDialogElement {
1227
1237
  if (anchor) anchor.classList.remove("has-popup-open");
1228
1238
 
1229
1239
  this.#isPopupActive = false;
1240
+ this.#wasDragged = false;
1230
1241
  this.#teardownObservers();
1231
1242
  document.removeEventListener(
1232
1243
  "pointerdown",
@@ -1397,6 +1408,7 @@ class FigPopup extends HTMLDialogElement {
1397
1408
  if (dx > this.#dragThreshold || dy > this.#dragThreshold) {
1398
1409
  this.#isDragging = true;
1399
1410
  this.#dragPending = false;
1411
+ this.#wasDragged = true;
1400
1412
  this.setPointerCapture(e.pointerId);
1401
1413
  this.style.cursor = "grabbing";
1402
1414
 
@@ -1801,7 +1813,7 @@ class FigPopup extends HTMLDialogElement {
1801
1813
  }
1802
1814
 
1803
1815
  #queueReposition() {
1804
- if (!this.open) return;
1816
+ if (!this.open || !this.#shouldAutoReposition()) return;
1805
1817
  if (this.#rafId !== null) return;
1806
1818
 
1807
1819
  this.#rafId = requestAnimationFrame(() => {
@@ -1809,6 +1821,11 @@ class FigPopup extends HTMLDialogElement {
1809
1821
  this.#positionPopup();
1810
1822
  });
1811
1823
  }
1824
+
1825
+ #shouldAutoReposition() {
1826
+ if (!(this.drag && this.#wasDragged)) return true;
1827
+ return !this.#resolveAnchor();
1828
+ }
1812
1829
  }
1813
1830
  customElements.define("fig-popup", FigPopup, { extends: "dialog" });
1814
1831
 
@@ -3830,7 +3847,7 @@ class FigInputFill extends HTMLElement {
3830
3847
  }
3831
3848
 
3832
3849
  static get observedAttributes() {
3833
- return ["value", "disabled", "mode", "experimental"];
3850
+ return ["value", "disabled", "mode", "experimental", "alpha"];
3834
3851
  }
3835
3852
 
3836
3853
  connectedCallback() {
@@ -3890,6 +3907,8 @@ class FigInputFill extends HTMLElement {
3890
3907
  if (mode) attrs["mode"] = mode;
3891
3908
  const experimental = this.getAttribute("experimental");
3892
3909
  if (experimental) attrs["experimental"] = experimental;
3910
+ const alpha = this.getAttribute("alpha");
3911
+ if (alpha) attrs["alpha"] = alpha;
3893
3912
  // picker-* overrides (except anchor, handled programmatically)
3894
3913
  for (const { name, value } of this.attributes) {
3895
3914
  if (name.startsWith("picker-") && name !== "picker-anchor") {
@@ -3905,6 +3924,22 @@ class FigInputFill extends HTMLElement {
3905
3924
  #render() {
3906
3925
  const disabled = this.hasAttribute("disabled");
3907
3926
  const fillPickerValue = JSON.stringify(this.value);
3927
+ const showAlpha = this.getAttribute("alpha") !== "false";
3928
+
3929
+ const opacityHtml = (value) =>
3930
+ showAlpha
3931
+ ? `<fig-tooltip text="Opacity">
3932
+ <fig-input-number
3933
+ class="fig-input-fill-opacity"
3934
+ placeholder="##"
3935
+ min="0"
3936
+ max="100"
3937
+ value="${value}"
3938
+ units="%"
3939
+ ${disabled ? "disabled" : ""}>
3940
+ </fig-input-number>
3941
+ </fig-tooltip>`
3942
+ : "";
3908
3943
 
3909
3944
  let controlsHtml = "";
3910
3945
 
@@ -3918,17 +3953,7 @@ class FigInputFill extends HTMLElement {
3918
3953
  value="${this.#solid.color.slice(1).toUpperCase()}"
3919
3954
  ${disabled ? "disabled" : ""}>
3920
3955
  </fig-input-text>
3921
- <fig-tooltip text="Opacity">
3922
- <fig-input-number
3923
- class="fig-input-fill-opacity"
3924
- placeholder="##"
3925
- min="0"
3926
- max="100"
3927
- value="${Math.round(this.#solid.alpha * 100)}"
3928
- units="%"
3929
- ${disabled ? "disabled" : ""}>
3930
- </fig-input-number>
3931
- </fig-tooltip>`;
3956
+ ${opacityHtml(Math.round(this.#solid.alpha * 100))}`;
3932
3957
  break;
3933
3958
 
3934
3959
  case "gradient":
@@ -3937,65 +3962,25 @@ class FigInputFill extends HTMLElement {
3937
3962
  this.#gradient.type.slice(1);
3938
3963
  controlsHtml = `
3939
3964
  <label class="fig-input-fill-label">${gradientLabel}</label>
3940
- <fig-tooltip text="Opacity">
3941
- <fig-input-number
3942
- class="fig-input-fill-opacity"
3943
- placeholder="##"
3944
- min="0"
3945
- max="100"
3946
- value="${this.#gradient.stops[0]?.opacity ?? 100}"
3947
- units="%"
3948
- ${disabled ? "disabled" : ""}>
3949
- </fig-input-number>
3950
- </fig-tooltip>`;
3965
+ ${opacityHtml(this.#gradient.stops[0]?.opacity ?? 100)}`;
3951
3966
  break;
3952
3967
 
3953
3968
  case "image":
3954
3969
  controlsHtml = `
3955
3970
  <label class="fig-input-fill-label">Image</label>
3956
- <fig-tooltip text="Opacity">
3957
- <fig-input-number
3958
- class="fig-input-fill-opacity"
3959
- placeholder="##"
3960
- min="0"
3961
- max="100"
3962
- value="${Math.round((this.#image.opacity ?? 1) * 100)}"
3963
- units="%"
3964
- ${disabled ? "disabled" : ""}>
3965
- </fig-input-number>
3966
- </fig-tooltip>`;
3971
+ ${opacityHtml(Math.round((this.#image.opacity ?? 1) * 100))}`;
3967
3972
  break;
3968
3973
 
3969
3974
  case "video":
3970
3975
  controlsHtml = `
3971
3976
  <label class="fig-input-fill-label">Video</label>
3972
- <fig-tooltip text="Opacity">
3973
- <fig-input-number
3974
- class="fig-input-fill-opacity"
3975
- placeholder="##"
3976
- min="0"
3977
- max="100"
3978
- value="${Math.round((this.#video.opacity ?? 1) * 100)}"
3979
- units="%"
3980
- ${disabled ? "disabled" : ""}>
3981
- </fig-input-number>
3982
- </fig-tooltip>`;
3977
+ ${opacityHtml(Math.round((this.#video.opacity ?? 1) * 100))}`;
3983
3978
  break;
3984
3979
 
3985
3980
  case "webcam":
3986
3981
  controlsHtml = `
3987
3982
  <label class="fig-input-fill-label">Webcam</label>
3988
- <fig-tooltip text="Opacity">
3989
- <fig-input-number
3990
- class="fig-input-fill-opacity"
3991
- placeholder="##"
3992
- min="0"
3993
- max="100"
3994
- value="${Math.round((this.#webcam.opacity ?? 1) * 100)}"
3995
- units="%"
3996
- ${disabled ? "disabled" : ""}>
3997
- </fig-input-number>
3998
- </fig-tooltip>`;
3983
+ ${opacityHtml(Math.round((this.#webcam.opacity ?? 1) * 100))}`;
3999
3984
  break;
4000
3985
  }
4001
3986
 
@@ -5154,6 +5139,14 @@ class FigImage extends HTMLElement {
5154
5139
  this.size = this.getAttribute("size") || "small";
5155
5140
  this.innerHTML = this.#getInnerHTML();
5156
5141
  this.#updateRefs();
5142
+ const ar = this.getAttribute("aspect-ratio");
5143
+ if (ar && ar !== "auto") {
5144
+ this.style.setProperty("--aspect-ratio", ar);
5145
+ }
5146
+ const fit = this.getAttribute("fit");
5147
+ if (fit) {
5148
+ this.style.setProperty("--fit", fit);
5149
+ }
5157
5150
  }
5158
5151
  disconnectedCallback() {
5159
5152
  this.fileInput?.removeEventListener("change", this.#boundHandleFileInput);
@@ -5190,10 +5183,13 @@ class FigImage extends HTMLElement {
5190
5183
  this.image.crossOrigin = "Anonymous";
5191
5184
  this.image.onload = async () => {
5192
5185
  this.aspectRatio = this.image.width / this.image.height;
5193
- this.style.setProperty(
5194
- "--aspect-ratio",
5195
- `${this.image.width}/${this.image.height}`
5196
- );
5186
+ const ar = this.getAttribute("aspect-ratio");
5187
+ if (!ar || ar === "auto") {
5188
+ this.style.setProperty(
5189
+ "--aspect-ratio",
5190
+ `${this.image.width}/${this.image.height}`
5191
+ );
5192
+ }
5197
5193
  this.dispatchEvent(
5198
5194
  new CustomEvent("loaded", {
5199
5195
  bubbles: true,
@@ -5265,7 +5261,7 @@ class FigImage extends HTMLElement {
5265
5261
  this.setAttribute("src", this.blob);
5266
5262
  }
5267
5263
  static get observedAttributes() {
5268
- return ["src", "upload"];
5264
+ return ["src", "upload", "aspect-ratio", "fit"];
5269
5265
  }
5270
5266
  get src() {
5271
5267
  return this.#src;
@@ -5297,10 +5293,665 @@ class FigImage extends HTMLElement {
5297
5293
  if (name === "size") {
5298
5294
  this.size = newValue;
5299
5295
  }
5296
+ if (name === "aspect-ratio") {
5297
+ if (newValue && newValue !== "auto") {
5298
+ this.style.setProperty("--aspect-ratio", newValue);
5299
+ } else if (!newValue) {
5300
+ this.style.removeProperty("--aspect-ratio");
5301
+ }
5302
+ }
5303
+ if (name === "fit") {
5304
+ if (newValue) {
5305
+ this.style.setProperty("--fit", newValue);
5306
+ } else {
5307
+ this.style.removeProperty("--fit");
5308
+ }
5309
+ }
5300
5310
  }
5301
5311
  }
5302
5312
  customElements.define("fig-image", FigImage);
5303
5313
 
5314
+ /**
5315
+ * A bezier / spring easing curve editor with draggable control points.
5316
+ * @attr {string} value - Bezier: "0.42, 0, 0.58, 1" or Spring: "spring(200, 15, 1)"
5317
+ * @attr {number} precision - Decimal places for output values (default 2)
5318
+ * @attr {boolean} dropdown - Show a preset dropdown selector
5319
+ */
5320
+ class FigEasingCurve extends HTMLElement {
5321
+ #cp1 = { x: 0.42, y: 0 };
5322
+ #cp2 = { x: 0.58, y: 1 };
5323
+ #spring = { stiffness: 200, damping: 15, mass: 1 };
5324
+ #mode = "bezier";
5325
+ #precision = 2;
5326
+ #isDragging = null;
5327
+ #svg = null;
5328
+ #curve = null;
5329
+ #line1 = null;
5330
+ #line2 = null;
5331
+ #handle1 = null;
5332
+ #handle2 = null;
5333
+ #dropdown = null;
5334
+ #presetName = null;
5335
+ #targetLine = null;
5336
+ #springDuration = 0.8;
5337
+ #drawWidth = 200;
5338
+ #drawHeight = 200;
5339
+ #bounds = null;
5340
+ #diagonal = null;
5341
+ #resizeObserver = null;
5342
+
5343
+ static PRESETS = [
5344
+ { group: null, name: "Linear", type: "bezier", value: [0, 0, 1, 1] },
5345
+ { group: "Bezier", name: "Ease in", type: "bezier", value: [0.42, 0, 1, 1] },
5346
+ { group: "Bezier", name: "Ease out", type: "bezier", value: [0, 0, 0.58, 1] },
5347
+ { group: "Bezier", name: "Ease in and out", type: "bezier", value: [0.42, 0, 0.58, 1] },
5348
+ { group: "Bezier", name: "Ease in back", type: "bezier", value: [0.6, -0.28, 0.735, 0.045] },
5349
+ { group: "Bezier", name: "Ease out back", type: "bezier", value: [0.175, 0.885, 0.32, 1.275] },
5350
+ { group: "Bezier", name: "Ease in and out back", type: "bezier", value: [0.68, -0.55, 0.265, 1.55] },
5351
+ { group: "Bezier", name: "Custom bezier", type: "bezier", value: null },
5352
+ { group: "Spring", name: "Gentle", type: "spring", spring: { stiffness: 120, damping: 14, mass: 1 } },
5353
+ { group: "Spring", name: "Quick", type: "spring", spring: { stiffness: 380, damping: 20, mass: 1 } },
5354
+ { group: "Spring", name: "Bouncy", type: "spring", spring: { stiffness: 250, damping: 8, mass: 1 } },
5355
+ { group: "Spring", name: "Slow", type: "spring", spring: { stiffness: 60, damping: 11, mass: 1 } },
5356
+ { group: "Spring", name: "Custom spring", type: "spring", spring: null },
5357
+ ];
5358
+
5359
+ static get observedAttributes() {
5360
+ return ["value", "precision"];
5361
+ }
5362
+
5363
+ connectedCallback() {
5364
+ this.#precision = parseInt(this.getAttribute("precision") || "2");
5365
+ const val = this.getAttribute("value");
5366
+ if (val) this.#parseValue(val);
5367
+ this.#presetName = this.#matchPreset();
5368
+ this.#render();
5369
+ this.#setupResizeObserver();
5370
+ }
5371
+
5372
+ disconnectedCallback() {
5373
+ this.#isDragging = null;
5374
+ if (this.#resizeObserver) {
5375
+ this.#resizeObserver.disconnect();
5376
+ this.#resizeObserver = null;
5377
+ }
5378
+ }
5379
+
5380
+ attributeChangedCallback(name, oldValue, newValue) {
5381
+ if (!this.#svg) return;
5382
+ if (name === "value" && newValue) {
5383
+ const prevMode = this.#mode;
5384
+ this.#parseValue(newValue);
5385
+ this.#presetName = this.#matchPreset();
5386
+ if (prevMode !== this.#mode) {
5387
+ this.#render();
5388
+ } else {
5389
+ this.#updatePaths();
5390
+ this.#syncDropdown();
5391
+ }
5392
+ }
5393
+ if (name === "precision") {
5394
+ this.#precision = parseInt(newValue || "2");
5395
+ }
5396
+ }
5397
+
5398
+ get value() {
5399
+ if (this.#mode === "spring") {
5400
+ const { stiffness, damping, mass } = this.#spring;
5401
+ return `spring(${stiffness}, ${damping}, ${mass})`;
5402
+ }
5403
+ const p = this.#precision;
5404
+ return `${this.#cp1.x.toFixed(p)}, ${this.#cp1.y.toFixed(p)}, ${this.#cp2.x.toFixed(p)}, ${this.#cp2.y.toFixed(p)}`;
5405
+ }
5406
+
5407
+ get cssValue() {
5408
+ if (this.#mode === "spring") {
5409
+ const points = this.#simulateSpring();
5410
+ const samples = 20;
5411
+ const step = Math.max(1, Math.floor(points.length / samples));
5412
+ const vals = [];
5413
+ for (let i = 0; i < points.length; i += step) {
5414
+ vals.push(points[i].value.toFixed(3));
5415
+ }
5416
+ if (points.length > 0) vals.push(points[points.length - 1].value.toFixed(3));
5417
+ return `linear(${vals.join(", ")})`;
5418
+ }
5419
+ const p = this.#precision;
5420
+ return `cubic-bezier(${this.#cp1.x.toFixed(p)}, ${this.#cp1.y.toFixed(p)}, ${this.#cp2.x.toFixed(p)}, ${this.#cp2.y.toFixed(p)})`;
5421
+ }
5422
+
5423
+ get preset() {
5424
+ return this.#presetName;
5425
+ }
5426
+
5427
+ set value(v) {
5428
+ this.setAttribute("value", v);
5429
+ }
5430
+
5431
+ #parseValue(str) {
5432
+ const springMatch = str.match(/^spring\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/);
5433
+ if (springMatch) {
5434
+ this.#mode = "spring";
5435
+ this.#spring.stiffness = parseFloat(springMatch[1]);
5436
+ this.#spring.damping = parseFloat(springMatch[2]);
5437
+ this.#spring.mass = parseFloat(springMatch[3]);
5438
+ return;
5439
+ }
5440
+ const parts = str.split(",").map((s) => parseFloat(s.trim()));
5441
+ if (parts.length >= 4 && parts.every((n) => !isNaN(n))) {
5442
+ this.#mode = "bezier";
5443
+ this.#cp1.x = parts[0];
5444
+ this.#cp1.y = parts[1];
5445
+ this.#cp2.x = parts[2];
5446
+ this.#cp2.y = parts[3];
5447
+ }
5448
+ }
5449
+
5450
+ #matchPreset() {
5451
+ const ep = 0.001;
5452
+ if (this.#mode === "bezier") {
5453
+ for (const p of FigEasingCurve.PRESETS) {
5454
+ if (p.type !== "bezier" || !p.value) continue;
5455
+ if (
5456
+ Math.abs(this.#cp1.x - p.value[0]) < ep &&
5457
+ Math.abs(this.#cp1.y - p.value[1]) < ep &&
5458
+ Math.abs(this.#cp2.x - p.value[2]) < ep &&
5459
+ Math.abs(this.#cp2.y - p.value[3]) < ep
5460
+ ) return p.name;
5461
+ }
5462
+ return "Custom bezier";
5463
+ }
5464
+ for (const p of FigEasingCurve.PRESETS) {
5465
+ if (p.type !== "spring" || !p.spring) continue;
5466
+ if (
5467
+ Math.abs(this.#spring.stiffness - p.spring.stiffness) < ep &&
5468
+ Math.abs(this.#spring.damping - p.spring.damping) < ep &&
5469
+ Math.abs(this.#spring.mass - p.spring.mass) < ep
5470
+ ) return p.name;
5471
+ }
5472
+ return "Custom spring";
5473
+ }
5474
+
5475
+ // --- Spring simulation ---
5476
+
5477
+ #simulateSpring() {
5478
+ const { stiffness, damping, mass } = this.#spring;
5479
+ const dt = 0.004;
5480
+ const maxTime = 5;
5481
+ const points = [];
5482
+ let pos = 0, vel = 0;
5483
+ for (let t = 0; t <= maxTime; t += dt) {
5484
+ const force = -stiffness * (pos - 1) - damping * vel;
5485
+ vel += (force / mass) * dt;
5486
+ pos += vel * dt;
5487
+ points.push({ t, value: pos });
5488
+ if (t > 0.1 && Math.abs(pos - 1) < 0.0005 && Math.abs(vel) < 0.0005) break;
5489
+ }
5490
+ return points;
5491
+ }
5492
+
5493
+ static #springIcon(spring, size = 24) {
5494
+ const { stiffness, damping, mass } = spring;
5495
+ const dt = 0.004;
5496
+ const maxTime = 5;
5497
+ const pts = [];
5498
+ let pos = 0, vel = 0;
5499
+ for (let t = 0; t <= maxTime; t += dt) {
5500
+ const force = -stiffness * (pos - 1) - damping * vel;
5501
+ vel += (force / mass) * dt;
5502
+ pos += vel * dt;
5503
+ pts.push({ t, value: pos });
5504
+ if (t > 0.1 && Math.abs(pos - 1) < 0.001 && Math.abs(vel) < 0.001) break;
5505
+ }
5506
+ const totalTime = pts[pts.length - 1].t || 1;
5507
+ let maxVal = 1;
5508
+ for (const p of pts) if (p.value > maxVal) maxVal = p.value;
5509
+ let minVal = 0;
5510
+ for (const p of pts) if (p.value < minVal) minVal = p.value;
5511
+ const range = Math.max(maxVal - minVal, 1);
5512
+ const pad = 6;
5513
+ const s = size - pad * 2;
5514
+ const step = Math.max(1, Math.floor(pts.length / 30));
5515
+ let d = "";
5516
+ for (let i = 0; i < pts.length; i += step) {
5517
+ const x = pad + (pts[i].t / totalTime) * s;
5518
+ const y = pad + (1 - (pts[i].value - minVal) / range) * s;
5519
+ d += (i === 0 ? "M" : "L") + x.toFixed(1) + "," + y.toFixed(1);
5520
+ }
5521
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" fill="none"><path d="${d}" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/></svg>`;
5522
+ }
5523
+
5524
+ static curveIcon(cp1x, cp1y, cp2x, cp2y, size = 24) {
5525
+ const pad = 6;
5526
+ const s = size - pad * 2;
5527
+ const x = (n) => pad + n * s;
5528
+ const y = (n) => pad + (1 - n) * s;
5529
+ const d = `M${x(0)},${y(0)} C${x(cp1x)},${y(cp1y)} ${x(cp2x)},${y(cp2y)} ${x(1)},${y(1)}`;
5530
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" fill="none"><path d="${d}" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`;
5531
+ }
5532
+
5533
+ // --- Rendering ---
5534
+
5535
+ #render() {
5536
+ this.innerHTML = this.#getInnerHTML();
5537
+ this.#cacheRefs();
5538
+ this.#syncViewportSize();
5539
+ this.#updatePaths();
5540
+ this.#setupEvents();
5541
+ }
5542
+
5543
+ #getDropdownHTML() {
5544
+ if (this.getAttribute("dropdown") !== "true") return "";
5545
+ let optionsHTML = "";
5546
+ let currentGroup = undefined;
5547
+ for (const p of FigEasingCurve.PRESETS) {
5548
+ if (p.group !== currentGroup) {
5549
+ if (currentGroup !== undefined) optionsHTML += `</optgroup>`;
5550
+ if (p.group) optionsHTML += `<optgroup label="${p.group}">`;
5551
+ currentGroup = p.group;
5552
+ }
5553
+ let icon;
5554
+ if (p.type === "spring") {
5555
+ const sp = p.spring || this.#spring;
5556
+ icon = FigEasingCurve.#springIcon(sp);
5557
+ } else {
5558
+ const v = p.value || [this.#cp1.x, this.#cp1.y, this.#cp2.x, this.#cp2.y];
5559
+ icon = FigEasingCurve.curveIcon(...v);
5560
+ }
5561
+ const selected = p.name === this.#presetName ? " selected" : "";
5562
+ optionsHTML += `<option value="${p.name}"${selected}>${icon} ${p.name}</option>`;
5563
+ }
5564
+ if (currentGroup) optionsHTML += `</optgroup>`;
5565
+ return `<fig-dropdown class="fig-easing-curve-dropdown" full experimental="modern">${optionsHTML}</fig-dropdown>`;
5566
+ }
5567
+
5568
+ #getInnerHTML() {
5569
+ const size = 200;
5570
+ const dropdown = this.#getDropdownHTML();
5571
+
5572
+ if (this.#mode === "spring") {
5573
+ const targetY = 40;
5574
+ const startY = 180;
5575
+ return `${dropdown}<div class="fig-easing-curve-svg-container"><svg viewBox="0 0 ${size} ${size}" class="fig-easing-curve-svg">
5576
+ <rect class="fig-easing-curve-bounds" x="0" y="0" width="${size}" height="${size}"/>
5577
+ <line class="fig-easing-curve-target" x1="0" y1="${targetY}" x2="${size}" y2="${targetY}"/>
5578
+ <line class="fig-easing-curve-diagonal" x1="0" y1="${startY}" x2="0" y2="${startY}"/>
5579
+ <path class="fig-easing-curve-path"/>
5580
+ <circle class="fig-easing-curve-handle" data-handle="bounce" r="5.25"/>
5581
+ <rect class="fig-easing-curve-duration-bar" data-handle="duration" width="6" height="16" rx="3" ry="3"/>
5582
+ </svg></div>`;
5583
+ }
5584
+
5585
+ return `${dropdown}<div class="fig-easing-curve-svg-container"><svg viewBox="0 0 ${size} ${size}" class="fig-easing-curve-svg">
5586
+ <rect class="fig-easing-curve-bounds" x="0" y="0" width="${size}" height="${size}"/>
5587
+ <line class="fig-easing-curve-diagonal" x1="0" y1="${size}" x2="${size}" y2="0"/>
5588
+ <line class="fig-easing-curve-arm" data-arm="1"/>
5589
+ <line class="fig-easing-curve-arm" data-arm="2"/>
5590
+ <path class="fig-easing-curve-path"/>
5591
+ <circle class="fig-easing-curve-handle" data-handle="1" r="5.25"/>
5592
+ <circle class="fig-easing-curve-handle" data-handle="2" r="5.25"/>
5593
+ </svg></div>`;
5594
+ }
5595
+
5596
+ #cacheRefs() {
5597
+ this.#svg = this.querySelector(".fig-easing-curve-svg");
5598
+ this.#curve = this.querySelector(".fig-easing-curve-path");
5599
+ this.#line1 = this.querySelector('[data-arm="1"]');
5600
+ this.#line2 = this.querySelector('[data-arm="2"]');
5601
+ this.#handle1 = this.querySelector('[data-handle="1"]') || this.querySelector('[data-handle="bounce"]');
5602
+ this.#handle2 = this.querySelector('[data-handle="2"]') || this.querySelector('[data-handle="duration"]');
5603
+ this.#dropdown = this.querySelector(".fig-easing-curve-dropdown");
5604
+ this.#targetLine = this.querySelector(".fig-easing-curve-target");
5605
+ this.#bounds = this.querySelector(".fig-easing-curve-bounds");
5606
+ this.#diagonal = this.querySelector(".fig-easing-curve-diagonal");
5607
+ }
5608
+
5609
+ #setupResizeObserver() {
5610
+ if (this.#resizeObserver || !window.ResizeObserver) return;
5611
+ this.#resizeObserver = new ResizeObserver(() => {
5612
+ if (this.#syncViewportSize()) {
5613
+ this.#updatePaths();
5614
+ }
5615
+ });
5616
+ this.#resizeObserver.observe(this);
5617
+ }
5618
+
5619
+ #syncViewportSize() {
5620
+ if (!this.#svg) return false;
5621
+ const rect = this.#svg.getBoundingClientRect();
5622
+ const width = Math.max(1, Math.round(rect.width || 200));
5623
+ const height = Math.max(1, Math.round(rect.height || 200));
5624
+ const changed = width !== this.#drawWidth || height !== this.#drawHeight;
5625
+ this.#drawWidth = width;
5626
+ this.#drawHeight = height;
5627
+ this.#svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
5628
+ return changed;
5629
+ }
5630
+
5631
+ // --- Coordinate helpers ---
5632
+
5633
+ #toSVG(nx, ny) {
5634
+ return { x: nx * this.#drawWidth, y: (1 - ny) * this.#drawHeight };
5635
+ }
5636
+
5637
+ #fromSVG(sx, sy) {
5638
+ return { x: sx / this.#drawWidth, y: 1 - sy / this.#drawHeight };
5639
+ }
5640
+
5641
+ #springScale = { minVal: 0, maxVal: 1.2, totalTime: 1 };
5642
+
5643
+ #springToSVG(nt, nv) {
5644
+ const pad = 20;
5645
+ const draw = this.#drawHeight - pad * 2;
5646
+ const { minVal, maxVal } = this.#springScale;
5647
+ const range = maxVal - minVal || 1;
5648
+ return {
5649
+ x: nt * this.#drawWidth,
5650
+ y: pad + (1 - (nv - minVal) / range) * draw,
5651
+ };
5652
+ }
5653
+
5654
+ // --- Path updates ---
5655
+
5656
+ #updatePaths() {
5657
+ this.#syncViewportSize();
5658
+ if (this.#mode === "spring") {
5659
+ this.#updateSpringPaths();
5660
+ } else {
5661
+ this.#updateBezierPaths();
5662
+ }
5663
+ }
5664
+
5665
+ #updateBezierPaths() {
5666
+ if (this.#bounds) {
5667
+ this.#bounds.setAttribute("x", "0");
5668
+ this.#bounds.setAttribute("y", "0");
5669
+ this.#bounds.setAttribute("width", this.#drawWidth);
5670
+ this.#bounds.setAttribute("height", this.#drawHeight);
5671
+ }
5672
+ if (this.#diagonal) {
5673
+ this.#diagonal.setAttribute("x1", "0");
5674
+ this.#diagonal.setAttribute("y1", this.#drawHeight);
5675
+ this.#diagonal.setAttribute("x2", this.#drawWidth);
5676
+ this.#diagonal.setAttribute("y2", "0");
5677
+ }
5678
+
5679
+ const p0 = this.#toSVG(0, 0);
5680
+ const p1 = this.#toSVG(this.#cp1.x, this.#cp1.y);
5681
+ const p2 = this.#toSVG(this.#cp2.x, this.#cp2.y);
5682
+ const p3 = this.#toSVG(1, 1);
5683
+
5684
+ this.#curve.setAttribute("d", `M${p0.x},${p0.y} C${p1.x},${p1.y} ${p2.x},${p2.y} ${p3.x},${p3.y}`);
5685
+ this.#line1.setAttribute("x1", p0.x);
5686
+ this.#line1.setAttribute("y1", p0.y);
5687
+ this.#line1.setAttribute("x2", p1.x);
5688
+ this.#line1.setAttribute("y2", p1.y);
5689
+ this.#line2.setAttribute("x1", p3.x);
5690
+ this.#line2.setAttribute("y1", p3.y);
5691
+ this.#line2.setAttribute("x2", p2.x);
5692
+ this.#line2.setAttribute("y2", p2.y);
5693
+ this.#handle1.setAttribute("cx", p1.x);
5694
+ this.#handle1.setAttribute("cy", p1.y);
5695
+ this.#handle2.setAttribute("cx", p2.x);
5696
+ this.#handle2.setAttribute("cy", p2.y);
5697
+ }
5698
+
5699
+ #updateSpringPaths() {
5700
+ if (this.#bounds) {
5701
+ this.#bounds.setAttribute("x", "0");
5702
+ this.#bounds.setAttribute("y", "0");
5703
+ this.#bounds.setAttribute("width", this.#drawWidth);
5704
+ this.#bounds.setAttribute("height", this.#drawHeight);
5705
+ }
5706
+
5707
+ const points = this.#simulateSpring();
5708
+ if (!points.length) return;
5709
+ const totalTime = points[points.length - 1].t || 1;
5710
+
5711
+ let minVal = 0, maxVal = 1;
5712
+ for (const p of points) {
5713
+ if (p.value < minVal) minVal = p.value;
5714
+ if (p.value > maxVal) maxVal = p.value;
5715
+ }
5716
+ const maxDistFromCenter = Math.max(Math.abs(minVal - 1), Math.abs(maxVal - 1), 0.01);
5717
+ const valPad = 0;
5718
+ this.#springScale = {
5719
+ minVal: 1 - maxDistFromCenter - valPad,
5720
+ maxVal: 1 + maxDistFromCenter + valPad,
5721
+ totalTime,
5722
+ };
5723
+
5724
+ const durationNorm = Math.max(0.05, Math.min(0.95, this.#springDuration));
5725
+ let d = "";
5726
+ for (let i = 0; i < points.length; i++) {
5727
+ const nt = (points[i].t / totalTime) * durationNorm;
5728
+ const pt = this.#springToSVG(nt, points[i].value);
5729
+ d += (i === 0 ? "M" : "L") + pt.x.toFixed(1) + "," + pt.y.toFixed(1);
5730
+ }
5731
+ const flatStart = this.#springToSVG(durationNorm, 1);
5732
+ const flatEnd = this.#springToSVG(1, 1);
5733
+ d += `L${flatStart.x.toFixed(1)},${flatStart.y.toFixed(1)} L${flatEnd.x.toFixed(1)},${flatEnd.y.toFixed(1)}`;
5734
+ this.#curve.setAttribute("d", d);
5735
+
5736
+ // Update target line position at value=1
5737
+ if (this.#targetLine) {
5738
+ const tl = this.#springToSVG(0, 1);
5739
+ const tr = this.#springToSVG(1, 1);
5740
+ this.#targetLine.setAttribute("x1", tl.x);
5741
+ this.#targetLine.setAttribute("y1", tl.y);
5742
+ this.#targetLine.setAttribute("x2", tr.x);
5743
+ this.#targetLine.setAttribute("y2", tr.y);
5744
+ }
5745
+
5746
+ // Bounce handle: at first overshoot peak
5747
+ const peak = this.#findPeakOvershoot(points);
5748
+ const peakNorm = (peak.t / totalTime) * durationNorm;
5749
+ const peakPt = this.#springToSVG(peakNorm, peak.value);
5750
+ this.#handle1.setAttribute("cx", peakPt.x);
5751
+ this.#handle1.setAttribute("cy", peakPt.y);
5752
+
5753
+ // Duration handle: on the target line
5754
+ const targetPt = this.#springToSVG(durationNorm, 1);
5755
+ this.#handle2.setAttribute("x", targetPt.x - 3);
5756
+ this.#handle2.setAttribute("y", targetPt.y - 8);
5757
+
5758
+ }
5759
+
5760
+ #findPeakOvershoot(points) {
5761
+ let peak = { t: 0, value: 1 };
5762
+ let passedTarget = false;
5763
+ for (const p of points) {
5764
+ if (p.value >= 0.99) passedTarget = true;
5765
+ if (passedTarget && p.value > peak.value) {
5766
+ peak = { t: p.t, value: p.value };
5767
+ }
5768
+ }
5769
+ return peak;
5770
+ }
5771
+
5772
+ // --- Dropdown ---
5773
+
5774
+ #syncDropdown() {
5775
+ if (!this.#dropdown) return;
5776
+ this.#dropdown.value = this.#presetName;
5777
+ this.#refreshCustomPresetIcons();
5778
+ }
5779
+
5780
+ #setOptionIconByValue(root, optionValue, icon) {
5781
+ if (!root) return;
5782
+ for (const option of root.querySelectorAll("option")) {
5783
+ if (option.value === optionValue) {
5784
+ option.innerHTML = `${icon} ${optionValue}`;
5785
+ }
5786
+ }
5787
+ }
5788
+
5789
+ #refreshCustomPresetIcons() {
5790
+ if (!this.#dropdown) return;
5791
+ const bezierIcon = FigEasingCurve.curveIcon(
5792
+ this.#cp1.x,
5793
+ this.#cp1.y,
5794
+ this.#cp2.x,
5795
+ this.#cp2.y
5796
+ );
5797
+ const springIcon = FigEasingCurve.#springIcon(this.#spring);
5798
+
5799
+ // Update both slotted options and the cloned native select options.
5800
+ this.#setOptionIconByValue(this.#dropdown, "Custom bezier", bezierIcon);
5801
+ this.#setOptionIconByValue(this.#dropdown, "Custom spring", springIcon);
5802
+ this.#setOptionIconByValue(this.#dropdown.select, "Custom bezier", bezierIcon);
5803
+ this.#setOptionIconByValue(this.#dropdown.select, "Custom spring", springIcon);
5804
+ }
5805
+
5806
+ // --- Events ---
5807
+
5808
+ #emit(type) {
5809
+ this.dispatchEvent(new CustomEvent(type, {
5810
+ bubbles: true,
5811
+ detail: {
5812
+ mode: this.#mode,
5813
+ value: this.value,
5814
+ cssValue: this.cssValue,
5815
+ preset: this.#presetName,
5816
+ },
5817
+ }));
5818
+ }
5819
+
5820
+ #setupEvents() {
5821
+ if (this.#mode === "bezier") {
5822
+ this.#handle1.addEventListener("pointerdown", (e) => this.#startBezierDrag(e, 1));
5823
+ this.#handle2.addEventListener("pointerdown", (e) => this.#startBezierDrag(e, 2));
5824
+ } else {
5825
+ this.#handle1.addEventListener("pointerdown", (e) => this.#startSpringDrag(e, "bounce"));
5826
+ this.#handle2.addEventListener("pointerdown", (e) => this.#startSpringDrag(e, "duration"));
5827
+ }
5828
+
5829
+ if (this.#dropdown) {
5830
+ this.#dropdown.addEventListener("change", (e) => {
5831
+ const name = e.detail;
5832
+ const preset = FigEasingCurve.PRESETS.find((p) => p.name === name);
5833
+ if (!preset) return;
5834
+
5835
+ if (preset.type === "bezier") {
5836
+ if (preset.value) {
5837
+ this.#cp1.x = preset.value[0];
5838
+ this.#cp1.y = preset.value[1];
5839
+ this.#cp2.x = preset.value[2];
5840
+ this.#cp2.y = preset.value[3];
5841
+ }
5842
+ this.#presetName = name;
5843
+ if (this.#mode !== "bezier") {
5844
+ this.#mode = "bezier";
5845
+ this.#render();
5846
+ } else {
5847
+ this.#updatePaths();
5848
+ }
5849
+ } else if (preset.type === "spring") {
5850
+ if (preset.spring) {
5851
+ this.#spring = { ...preset.spring };
5852
+ }
5853
+ this.#presetName = name;
5854
+ if (this.#mode !== "spring") {
5855
+ this.#mode = "spring";
5856
+ this.#render();
5857
+ } else {
5858
+ this.#updatePaths();
5859
+ }
5860
+ }
5861
+ this.#emit("input");
5862
+ this.#emit("change");
5863
+ });
5864
+ }
5865
+ }
5866
+
5867
+ #clientToSVG(e) {
5868
+ const ctm = this.#svg.getScreenCTM();
5869
+ if (!ctm) return { x: 0, y: 0 };
5870
+ const inv = ctm.inverse();
5871
+ return {
5872
+ x: inv.a * e.clientX + inv.c * e.clientY + inv.e,
5873
+ y: inv.b * e.clientX + inv.d * e.clientY + inv.f,
5874
+ };
5875
+ }
5876
+
5877
+ #startBezierDrag(e, handle) {
5878
+ e.preventDefault();
5879
+ this.#isDragging = handle;
5880
+
5881
+ const onMove = (e) => {
5882
+ if (!this.#isDragging) return;
5883
+ const svgPt = this.#clientToSVG(e);
5884
+ const norm = this.#fromSVG(svgPt.x, svgPt.y);
5885
+
5886
+ norm.x = Math.round(norm.x * 100) / 100;
5887
+ norm.y = Math.round(norm.y * 100) / 100;
5888
+ norm.x = Math.max(0, Math.min(1, norm.x));
5889
+
5890
+ if (this.#isDragging === 1) {
5891
+ this.#cp1.x = norm.x;
5892
+ this.#cp1.y = norm.y;
5893
+ } else {
5894
+ this.#cp2.x = norm.x;
5895
+ this.#cp2.y = norm.y;
5896
+ }
5897
+ this.#updatePaths();
5898
+ this.#presetName = this.#matchPreset();
5899
+ this.#syncDropdown();
5900
+ this.#emit("input");
5901
+ };
5902
+
5903
+ const onUp = () => {
5904
+ this.#isDragging = null;
5905
+ document.removeEventListener("pointermove", onMove);
5906
+ document.removeEventListener("pointerup", onUp);
5907
+ this.#emit("change");
5908
+ };
5909
+
5910
+ document.addEventListener("pointermove", onMove);
5911
+ document.addEventListener("pointerup", onUp);
5912
+ }
5913
+
5914
+ #startSpringDrag(e, handleType) {
5915
+ e.preventDefault();
5916
+ this.#isDragging = handleType;
5917
+
5918
+ const startDamping = this.#spring.damping;
5919
+ const startStiffness = this.#spring.stiffness;
5920
+ const startDuration = this.#springDuration;
5921
+ const startY = e.clientY;
5922
+ const startX = e.clientX;
5923
+
5924
+ const onMove = (e) => {
5925
+ if (!this.#isDragging) return;
5926
+
5927
+ if (handleType === "bounce") {
5928
+ const dy = e.clientY - startY;
5929
+ this.#spring.damping = Math.max(1, Math.round(startDamping + dy * 0.15));
5930
+ } else {
5931
+ const dx = e.clientX - startX;
5932
+ this.#springDuration = Math.max(0.05, Math.min(0.95, startDuration + dx / 200));
5933
+ this.#spring.stiffness = Math.max(10, Math.round(startStiffness - dx * 1.5));
5934
+ }
5935
+
5936
+ this.#updatePaths();
5937
+ this.#presetName = this.#matchPreset();
5938
+ this.#syncDropdown();
5939
+ this.#emit("input");
5940
+ };
5941
+
5942
+ const onUp = () => {
5943
+ this.#isDragging = null;
5944
+ document.removeEventListener("pointermove", onMove);
5945
+ document.removeEventListener("pointerup", onUp);
5946
+ this.#emit("change");
5947
+ };
5948
+
5949
+ document.addEventListener("pointermove", onMove);
5950
+ document.addEventListener("pointerup", onUp);
5951
+ }
5952
+ }
5953
+ customElements.define("fig-easing-curve", FigEasingCurve);
5954
+
5304
5955
  /**
5305
5956
  * A custom joystick input element.
5306
5957
  * @attr {string} value - The current position of the joystick (e.g., "0.5,0.5").
package/index.html CHANGED
@@ -4339,6 +4339,32 @@
4339
4339
  <fig-button>No delay</fig-button>
4340
4340
  </fig-tooltip>
4341
4341
 
4342
+ <h3>Warmup (Rapid Hover)</h3>
4343
+ <p class="description">After the first tooltip appears, moving quickly between these buttons shows
4344
+ subsequent tooltips instantly — no repeated delay.</p>
4345
+ <hstack>
4346
+ <fig-tooltip text="Add" delay="500">
4347
+ <fig-button icon variant="ghost">
4348
+ <span class="fig-mask-icon" style="--icon: var(--icon-add)"></span>
4349
+ </fig-button>
4350
+ </fig-tooltip>
4351
+ <fig-tooltip text="Remove" delay="500">
4352
+ <fig-button icon variant="ghost">
4353
+ <span class="fig-mask-icon" style="--icon: var(--icon-minus)"></span>
4354
+ </fig-button>
4355
+ </fig-tooltip>
4356
+ <fig-tooltip text="Rotate" delay="500">
4357
+ <fig-button icon variant="ghost">
4358
+ <span class="fig-mask-icon" style="--icon: var(--icon-rotate)"></span>
4359
+ </fig-button>
4360
+ </fig-tooltip>
4361
+ <fig-tooltip text="Swap" delay="500">
4362
+ <fig-button icon variant="ghost">
4363
+ <span class="fig-mask-icon" style="--icon: var(--icon-swap)"></span>
4364
+ </fig-button>
4365
+ </fig-tooltip>
4366
+ </hstack>
4367
+
4342
4368
  <h3>Long Text</h3>
4343
4369
  <fig-tooltip text="This is a much longer tooltip that contains more detailed information about the element">
4344
4370
  <fig-button variant="secondary">Long tooltip</fig-button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "2.25.0",
3
+ "version": "2.27.0",
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": "MIT",