@rogieking/figui3 2.27.0 → 2.29.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/fig.js CHANGED
@@ -175,6 +175,8 @@ class FigDropdown extends HTMLElement {
175
175
  #selectedValue = null; // Stores last selected value for dropdown type
176
176
  #boundHandleSelectInput;
177
177
  #boundHandleSelectChange;
178
+ #selectedContentEnabled = false;
179
+ #selectedContentEl = null;
178
180
 
179
181
  get label() {
180
182
  return this.#label;
@@ -191,6 +193,52 @@ class FigDropdown extends HTMLElement {
191
193
  this.#boundHandleSelectChange = this.#handleSelectChange.bind(this);
192
194
  }
193
195
 
196
+ #supportsSelectedContent() {
197
+ if (typeof CSS === "undefined" || typeof CSS.supports !== "function")
198
+ return false;
199
+ try {
200
+ return (
201
+ CSS.supports("appearance: base-select") &&
202
+ CSS.supports("selector(::picker(select))")
203
+ );
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+
209
+ #enableSelectedContentIfNeeded() {
210
+ const experimental = this.getAttribute("experimental") || "";
211
+ const wantsModern = experimental
212
+ .split(/\s+/)
213
+ .filter(Boolean)
214
+ .includes("modern");
215
+
216
+ if (!wantsModern || !this.#supportsSelectedContent()) {
217
+ this.#selectedContentEnabled = false;
218
+ return;
219
+ }
220
+
221
+ const button = document.createElement("button");
222
+ button.setAttribute("type", "button");
223
+ button.setAttribute("aria-hidden", "true");
224
+ const selected = document.createElement("selectedcontent");
225
+ button.appendChild(selected);
226
+ this.select.appendChild(button);
227
+ this.#selectedContentEnabled = true;
228
+ this.#selectedContentEl = selected;
229
+ }
230
+
231
+ #syncSelectedContent() {
232
+ if (!this.#selectedContentEl) return;
233
+ const selectedOption = this.select.selectedOptions?.[0];
234
+ if (!selectedOption) {
235
+ this.#selectedContentEl.textContent = "";
236
+ return;
237
+ }
238
+ // Fallback mirror for browsers that don't auto-project selectedcontent reliably.
239
+ this.#selectedContentEl.innerHTML = selectedOption.innerHTML;
240
+ }
241
+
194
242
  #addEventListeners() {
195
243
  this.select.addEventListener("input", this.#boundHandleSelectInput);
196
244
  this.select.addEventListener("change", this.#boundHandleSelectChange);
@@ -199,7 +247,7 @@ class FigDropdown extends HTMLElement {
199
247
  #hasPersistentControl(optionEl) {
200
248
  if (!optionEl || !(optionEl instanceof Element)) return false;
201
249
  return !!optionEl.querySelector(
202
- 'fig-checkbox, fig-switch, input[type="checkbox"]'
250
+ 'fig-checkbox, fig-switch, input[type="checkbox"]',
203
251
  );
204
252
  }
205
253
 
@@ -235,6 +283,8 @@ class FigDropdown extends HTMLElement {
235
283
  this.select.firstChild.remove();
236
284
  }
237
285
 
286
+ this.#enableSelectedContentIfNeeded();
287
+
238
288
  if (this.type === "dropdown") {
239
289
  const hiddenOption = document.createElement("option");
240
290
  hiddenOption.setAttribute("hidden", "true");
@@ -248,6 +298,7 @@ class FigDropdown extends HTMLElement {
248
298
  }
249
299
  });
250
300
  this.#syncSelectedValue(this.value);
301
+ this.#syncSelectedContent();
251
302
  if (this.type === "dropdown") {
252
303
  this.select.selectedIndex = -1;
253
304
  }
@@ -269,12 +320,13 @@ class FigDropdown extends HTMLElement {
269
320
  this.#selectedValue = selectedValue;
270
321
  }
271
322
  this.setAttribute("value", selectedValue);
323
+ this.#syncSelectedContent();
272
324
  this.dispatchEvent(
273
325
  new CustomEvent("input", {
274
326
  detail: selectedValue,
275
327
  bubbles: true,
276
328
  composed: true,
277
- })
329
+ }),
278
330
  );
279
331
  }
280
332
 
@@ -295,12 +347,13 @@ class FigDropdown extends HTMLElement {
295
347
  if (this.type === "dropdown") {
296
348
  this.select.selectedIndex = -1;
297
349
  }
350
+ this.#syncSelectedContent();
298
351
  this.dispatchEvent(
299
352
  new CustomEvent("change", {
300
353
  detail: selectedValue,
301
354
  bubbles: true,
302
355
  composed: true,
303
- })
356
+ }),
304
357
  );
305
358
  }
306
359
 
@@ -325,7 +378,7 @@ class FigDropdown extends HTMLElement {
325
378
  this.setAttribute("value", value);
326
379
  }
327
380
  static get observedAttributes() {
328
- return ["value", "type"];
381
+ return ["value", "type", "experimental"];
329
382
  }
330
383
  #syncSelectedValue(value) {
331
384
  // For dropdown type, don't sync the visual selection - it should always show the hidden placeholder
@@ -339,6 +392,7 @@ class FigDropdown extends HTMLElement {
339
392
  }
340
393
  });
341
394
  }
395
+ this.#syncSelectedContent();
342
396
  }
343
397
  attributeChangedCallback(name, oldValue, newValue) {
344
398
  if (name === "value") {
@@ -347,6 +401,9 @@ class FigDropdown extends HTMLElement {
347
401
  if (name === "type") {
348
402
  this.type = newValue;
349
403
  }
404
+ if (name === "experimental") {
405
+ this.slotChange();
406
+ }
350
407
  if (name === "label") {
351
408
  this.#label = newValue;
352
409
  this.select.setAttribute("aria-label", this.#label);
@@ -395,7 +452,7 @@ class FigTooltip extends HTMLElement {
395
452
  document.removeEventListener(
396
453
  "mousedown",
397
454
  this.#boundHideOnChromeOpen,
398
- true
455
+ true,
399
456
  );
400
457
  // Disconnect mutation observer
401
458
  this.#stopObserving();
@@ -404,7 +461,7 @@ class FigTooltip extends HTMLElement {
404
461
  if (this.action === "click") {
405
462
  document.body.removeEventListener(
406
463
  "click",
407
- this.#boundHidePopupOutsideClick
464
+ this.#boundHidePopupOutsideClick,
408
465
  );
409
466
  }
410
467
 
@@ -470,7 +527,7 @@ class FigTooltip extends HTMLElement {
470
527
  if (this.action === "click") {
471
528
  document.body.removeEventListener(
472
529
  "click",
473
- this.#boundHidePopupOutsideClick
530
+ this.#boundHidePopupOutsideClick,
474
531
  );
475
532
  }
476
533
  }
@@ -488,7 +545,7 @@ class FigTooltip extends HTMLElement {
488
545
  this.addEventListener("pointerenter", this.showDelayedPopup.bind(this));
489
546
  this.addEventListener(
490
547
  "pointerleave",
491
- this.#handlePointerLeave.bind(this)
548
+ this.#handlePointerLeave.bind(this),
492
549
  );
493
550
  }
494
551
  // Touch support for mobile hover simulation
@@ -535,7 +592,8 @@ class FigTooltip extends HTMLElement {
535
592
  showDelayedPopup() {
536
593
  this.render();
537
594
  clearTimeout(this.timeout);
538
- const warm = Date.now() - FigTooltip.#lastShownAt < FigTooltip.#warmupWindow;
595
+ const warm =
596
+ Date.now() - FigTooltip.#lastShownAt < FigTooltip.#warmupWindow;
539
597
  const effectiveDelay = warm ? 0 : this.delay;
540
598
  this.timeout = setTimeout(this.showPopup.bind(this), effectiveDelay);
541
599
  }
@@ -740,29 +798,6 @@ class FigTooltip extends HTMLElement {
740
798
 
741
799
  customElements.define("fig-tooltip", FigTooltip);
742
800
 
743
- /* Popover */
744
- /**
745
- * A custom popover element extending FigTooltip.
746
- * @attr {string} action - The trigger action: "click" (default) or "hover"
747
- * @attr {string} size - The size of the popover
748
- */
749
- class FigPopover extends FigTooltip {
750
- constructor() {
751
- super();
752
- this.action = this.getAttribute("action") || "click";
753
- this.delay = parseInt(this.getAttribute("delay")) || 0;
754
- }
755
- render() {
756
- this.popup = this.popup || this.querySelector("[popover]");
757
- this.popup.setAttribute("class", "fig-popover");
758
- this.popup.style.position = "fixed";
759
- this.popup.style.visibility = "hidden";
760
- this.popup.style.display = "inline-flex";
761
- document.body.append(this.popup);
762
- }
763
- }
764
- customElements.define("fig-popover", FigPopover);
765
-
766
801
  /* Dialog */
767
802
  /**
768
803
  * A custom dialog element for modal and non-modal dialogs.
@@ -1080,12 +1115,17 @@ class FigPopup extends HTMLDialogElement {
1080
1115
  #boundPointerDown;
1081
1116
  #boundPointerMove;
1082
1117
  #boundPointerUp;
1118
+ #wasDragged = false;
1083
1119
 
1084
1120
  constructor() {
1085
1121
  super();
1086
1122
  this.#boundReposition = this.#queueReposition.bind(this);
1087
1123
  this.#boundScroll = (e) => {
1088
- if (this.open && !this.contains(e.target) && this.#shouldAutoReposition()) {
1124
+ if (
1125
+ this.open &&
1126
+ !this.contains(e.target) &&
1127
+ this.#shouldAutoReposition()
1128
+ ) {
1089
1129
  this.#positionPopup();
1090
1130
  }
1091
1131
  };
@@ -1096,7 +1136,18 @@ class FigPopup extends HTMLDialogElement {
1096
1136
  }
1097
1137
 
1098
1138
  static get observedAttributes() {
1099
- return ["open", "anchor", "position", "offset", "variant", "theme", "drag", "handle", "autoresize", "viewport-margin"];
1139
+ return [
1140
+ "open",
1141
+ "anchor",
1142
+ "position",
1143
+ "offset",
1144
+ "variant",
1145
+ "theme",
1146
+ "drag",
1147
+ "handle",
1148
+ "autoresize",
1149
+ "viewport-margin",
1150
+ ];
1100
1151
  }
1101
1152
 
1102
1153
  get open() {
@@ -1167,7 +1218,7 @@ class FigPopup extends HTMLDialogElement {
1167
1218
  document.removeEventListener(
1168
1219
  "pointerdown",
1169
1220
  this.#boundOutsidePointerDown,
1170
- true
1221
+ true,
1171
1222
  );
1172
1223
  if (this.#rafId !== null) {
1173
1224
  cancelAnimationFrame(this.#rafId);
@@ -1223,7 +1274,11 @@ class FigPopup extends HTMLDialogElement {
1223
1274
  }
1224
1275
 
1225
1276
  this.#setupObservers();
1226
- document.addEventListener("pointerdown", this.#boundOutsidePointerDown, true);
1277
+ document.addEventListener(
1278
+ "pointerdown",
1279
+ this.#boundOutsidePointerDown,
1280
+ true,
1281
+ );
1227
1282
  this.#wasDragged = false;
1228
1283
  this.#queueReposition();
1229
1284
  this.#isPopupActive = true;
@@ -1242,7 +1297,7 @@ class FigPopup extends HTMLDialogElement {
1242
1297
  document.removeEventListener(
1243
1298
  "pointerdown",
1244
1299
  this.#boundOutsidePointerDown,
1245
- true
1300
+ true,
1246
1301
  );
1247
1302
 
1248
1303
  if (super.open) {
@@ -1283,7 +1338,10 @@ class FigPopup extends HTMLDialogElement {
1283
1338
  }
1284
1339
 
1285
1340
  window.addEventListener("resize", this.#boundReposition);
1286
- window.addEventListener("scroll", this.#boundScroll, { capture: true, passive: true });
1341
+ window.addEventListener("scroll", this.#boundScroll, {
1342
+ capture: true,
1343
+ passive: true,
1344
+ });
1287
1345
  }
1288
1346
 
1289
1347
  #teardownObservers() {
@@ -1300,7 +1358,10 @@ class FigPopup extends HTMLDialogElement {
1300
1358
  this.#mutationObserver = null;
1301
1359
  }
1302
1360
  window.removeEventListener("resize", this.#boundReposition);
1303
- window.removeEventListener("scroll", this.#boundScroll, { capture: true, passive: true });
1361
+ window.removeEventListener("scroll", this.#boundScroll, {
1362
+ capture: true,
1363
+ passive: true,
1364
+ });
1304
1365
  }
1305
1366
 
1306
1367
  #handleOutsidePointerDown(event) {
@@ -1351,15 +1412,31 @@ class FigPopup extends HTMLDialogElement {
1351
1412
 
1352
1413
  #isInteractiveElement(element) {
1353
1414
  const interactiveSelectors = [
1354
- "input", "button", "select", "textarea", "a",
1355
- "label", "details", "summary",
1356
- '[contenteditable="true"]', "[tabindex]",
1415
+ "input",
1416
+ "button",
1417
+ "select",
1418
+ "textarea",
1419
+ "a",
1420
+ "label",
1421
+ "details",
1422
+ "summary",
1423
+ '[contenteditable="true"]',
1424
+ "[tabindex]",
1357
1425
  ];
1358
1426
 
1359
1427
  const nonInteractiveFigElements = [
1360
- "FIG-HEADER", "FIG-DIALOG", "FIG-POPUP", "FIG-FIELD",
1361
- "FIG-TOOLTIP", "FIG-CONTENT", "FIG-TABS", "FIG-TAB",
1362
- "FIG-POPOVER", "FIG-SHIMMER", "FIG-LAYER", "FIG-FILL-PICKER",
1428
+ "FIG-HEADER",
1429
+ "FIG-DIALOG",
1430
+ "FIG-POPUP",
1431
+ "FIG-FIELD",
1432
+ "FIG-TOOLTIP",
1433
+ "FIG-CONTENT",
1434
+ "FIG-TABS",
1435
+ "FIG-TAB",
1436
+ "FIG-POPOVER",
1437
+ "FIG-SHIMMER",
1438
+ "FIG-LAYER",
1439
+ "FIG-FILL-PICKER",
1363
1440
  ];
1364
1441
 
1365
1442
  const isInteractive = (el) =>
@@ -1559,17 +1636,49 @@ class FigPopup extends HTMLDialogElement {
1559
1636
 
1560
1637
  #parseViewportMargins() {
1561
1638
  const raw = (this.getAttribute("viewport-margin") || "8").trim();
1562
- const tokens = raw.split(/\s+/).map(Number).filter(n => !Number.isNaN(n));
1639
+ const tokens = raw
1640
+ .split(/\s+/)
1641
+ .map(Number)
1642
+ .filter((n) => !Number.isNaN(n));
1563
1643
  const d = 8;
1564
1644
  if (tokens.length === 0) return { top: d, right: d, bottom: d, left: d };
1565
- if (tokens.length === 1) return { top: tokens[0], right: tokens[0], bottom: tokens[0], left: tokens[0] };
1566
- if (tokens.length === 2) return { top: tokens[0], right: tokens[1], bottom: tokens[0], left: tokens[1] };
1567
- if (tokens.length === 3) return { top: tokens[0], right: tokens[1], bottom: tokens[2], left: tokens[1] };
1568
- return { top: tokens[0], right: tokens[1], bottom: tokens[2], left: tokens[3] };
1645
+ if (tokens.length === 1)
1646
+ return {
1647
+ top: tokens[0],
1648
+ right: tokens[0],
1649
+ bottom: tokens[0],
1650
+ left: tokens[0],
1651
+ };
1652
+ if (tokens.length === 2)
1653
+ return {
1654
+ top: tokens[0],
1655
+ right: tokens[1],
1656
+ bottom: tokens[0],
1657
+ left: tokens[1],
1658
+ };
1659
+ if (tokens.length === 3)
1660
+ return {
1661
+ top: tokens[0],
1662
+ right: tokens[1],
1663
+ bottom: tokens[2],
1664
+ left: tokens[1],
1665
+ };
1666
+ return {
1667
+ top: tokens[0],
1668
+ right: tokens[1],
1669
+ bottom: tokens[2],
1670
+ left: tokens[3],
1671
+ };
1569
1672
  }
1570
1673
 
1571
1674
  #getPlacementCandidates(vertical, horizontal, shorthand) {
1572
- const opp = { top: "bottom", bottom: "top", left: "right", right: "left", center: "center" };
1675
+ const opp = {
1676
+ top: "bottom",
1677
+ bottom: "top",
1678
+ left: "right",
1679
+ right: "left",
1680
+ center: "center",
1681
+ };
1573
1682
 
1574
1683
  if (shorthand) {
1575
1684
  const isHorizontal = shorthand === "left" || shorthand === "right";
@@ -1612,21 +1721,30 @@ class FigPopup extends HTMLDialogElement {
1612
1721
  ];
1613
1722
  }
1614
1723
 
1615
- #computeCoords(anchorRect, popupRect, vertical, horizontal, offset, shorthand) {
1724
+ #computeCoords(
1725
+ anchorRect,
1726
+ popupRect,
1727
+ vertical,
1728
+ horizontal,
1729
+ offset,
1730
+ shorthand,
1731
+ ) {
1616
1732
  let top;
1617
1733
  let left;
1618
1734
 
1619
1735
  if (shorthand === "left" || shorthand === "right") {
1620
- left = shorthand === "left"
1621
- ? anchorRect.left - popupRect.width - offset.xPx
1622
- : anchorRect.right + offset.xPx;
1736
+ left =
1737
+ shorthand === "left"
1738
+ ? anchorRect.left - popupRect.width - offset.xPx
1739
+ : anchorRect.right + offset.xPx;
1623
1740
  top = anchorRect.top;
1624
1741
  return { top, left };
1625
1742
  }
1626
1743
  if (shorthand === "top" || shorthand === "bottom") {
1627
- top = shorthand === "top"
1628
- ? anchorRect.top - popupRect.height - offset.yPx
1629
- : anchorRect.bottom + offset.yPx;
1744
+ top =
1745
+ shorthand === "top"
1746
+ ? anchorRect.top - popupRect.height - offset.yPx
1747
+ : anchorRect.bottom + offset.yPx;
1630
1748
  left = anchorRect.left;
1631
1749
  return { top, left };
1632
1750
  }
@@ -1733,8 +1851,14 @@ class FigPopup extends HTMLDialogElement {
1733
1851
  #clamp(coords, popupRect, m) {
1734
1852
  const minLeft = m.left;
1735
1853
  const minTop = m.top;
1736
- const maxLeft = Math.max(m.left, window.innerWidth - popupRect.width - m.right);
1737
- const maxTop = Math.max(m.top, window.innerHeight - popupRect.height - m.bottom);
1854
+ const maxLeft = Math.max(
1855
+ m.left,
1856
+ window.innerWidth - popupRect.width - m.right,
1857
+ );
1858
+ const maxTop = Math.max(
1859
+ m.top,
1860
+ window.innerHeight - popupRect.height - m.bottom,
1861
+ );
1738
1862
 
1739
1863
  return {
1740
1864
  left: Math.min(maxLeft, Math.max(minLeft, coords.left)),
@@ -1754,8 +1878,11 @@ class FigPopup extends HTMLDialogElement {
1754
1878
  if (!anchor) {
1755
1879
  this.#updatePopoverBeak(null, popupRect, 0, 0, "top");
1756
1880
  const centered = {
1757
- left: (m.left + (window.innerWidth - m.right - m.left - popupRect.width) / 2),
1758
- top: (m.top + (window.innerHeight - m.bottom - m.top - popupRect.height) / 2),
1881
+ left:
1882
+ m.left + (window.innerWidth - m.right - m.left - popupRect.width) / 2,
1883
+ top:
1884
+ m.top +
1885
+ (window.innerHeight - m.bottom - m.top - popupRect.height) / 2,
1759
1886
  };
1760
1887
  const clamped = this.#clamp(centered, popupRect, m);
1761
1888
  this.style.left = `${clamped.left}px`;
@@ -1764,24 +1891,44 @@ class FigPopup extends HTMLDialogElement {
1764
1891
  }
1765
1892
 
1766
1893
  const anchorRect = anchor.getBoundingClientRect();
1767
- const candidates = this.#getPlacementCandidates(vertical, horizontal, shorthand);
1894
+ const candidates = this.#getPlacementCandidates(
1895
+ vertical,
1896
+ horizontal,
1897
+ shorthand,
1898
+ );
1768
1899
  let best = null;
1769
1900
  let bestSide = "top";
1770
1901
  let bestScore = Number.POSITIVE_INFINITY;
1771
1902
 
1772
1903
  for (const { v, h, s } of candidates) {
1773
- const coords = this.#computeCoords(anchorRect, popupRect, v, h, offset, s);
1904
+ const coords = this.#computeCoords(
1905
+ anchorRect,
1906
+ popupRect,
1907
+ v,
1908
+ h,
1909
+ offset,
1910
+ s,
1911
+ );
1774
1912
  const placementSide = this.#getPlacementSide(v, h, s);
1775
1913
 
1776
1914
  if (s) {
1777
1915
  const clamped = this.#clamp(coords, popupRect, m);
1778
- const primaryFits = (s === "left" || s === "right")
1779
- ? (coords.left >= m.left && coords.left + popupRect.width <= window.innerWidth - m.right)
1780
- : (coords.top >= m.top && coords.top + popupRect.height <= window.innerHeight - m.bottom);
1916
+ const primaryFits =
1917
+ s === "left" || s === "right"
1918
+ ? coords.left >= m.left &&
1919
+ coords.left + popupRect.width <= window.innerWidth - m.right
1920
+ : coords.top >= m.top &&
1921
+ coords.top + popupRect.height <= window.innerHeight - m.bottom;
1781
1922
  if (primaryFits) {
1782
1923
  this.style.left = `${clamped.left}px`;
1783
1924
  this.style.top = `${clamped.top}px`;
1784
- this.#updatePopoverBeak(anchorRect, popupRect, clamped.left, clamped.top, placementSide);
1925
+ this.#updatePopoverBeak(
1926
+ anchorRect,
1927
+ popupRect,
1928
+ clamped.left,
1929
+ clamped.top,
1930
+ placementSide,
1931
+ );
1785
1932
  return;
1786
1933
  }
1787
1934
  const score = this.#overflowScore(coords, popupRect, m);
@@ -1794,7 +1941,13 @@ class FigPopup extends HTMLDialogElement {
1794
1941
  if (this.#fits(coords, popupRect, m)) {
1795
1942
  this.style.left = `${coords.left}px`;
1796
1943
  this.style.top = `${coords.top}px`;
1797
- this.#updatePopoverBeak(anchorRect, popupRect, coords.left, coords.top, placementSide);
1944
+ this.#updatePopoverBeak(
1945
+ anchorRect,
1946
+ popupRect,
1947
+ coords.left,
1948
+ coords.top,
1949
+ placementSide,
1950
+ );
1798
1951
  return;
1799
1952
  }
1800
1953
  const score = this.#overflowScore(coords, popupRect, m);
@@ -1809,7 +1962,13 @@ class FigPopup extends HTMLDialogElement {
1809
1962
  const clamped = this.#clamp(best || { left: 0, top: 0 }, popupRect, m);
1810
1963
  this.style.left = `${clamped.left}px`;
1811
1964
  this.style.top = `${clamped.top}px`;
1812
- this.#updatePopoverBeak(anchorRect, popupRect, clamped.left, clamped.top, bestSide);
1965
+ this.#updatePopoverBeak(
1966
+ anchorRect,
1967
+ popupRect,
1968
+ clamped.left,
1969
+ clamped.top,
1970
+ bestSide,
1971
+ );
1813
1972
  }
1814
1973
 
1815
1974
  #queueReposition() {
@@ -1829,69 +1988,6 @@ class FigPopup extends HTMLDialogElement {
1829
1988
  }
1830
1989
  customElements.define("fig-popup", FigPopup, { extends: "dialog" });
1831
1990
 
1832
- /**
1833
- * A popover element using the native Popover API.
1834
- * @attr {string} trigger-action - The trigger action: "click" (default) or "hover"
1835
- * @attr {number} delay - Delay in ms before showing on hover (default: 0)
1836
- */
1837
- class FigPopover2 extends HTMLElement {
1838
- #popover;
1839
- #trigger;
1840
- #id;
1841
- #delay;
1842
- #timeout;
1843
- #action;
1844
-
1845
- constructor() {
1846
- super();
1847
- }
1848
- connectedCallback() {
1849
- this.#popover = this.querySelector("[popover]");
1850
- this.#trigger = this;
1851
- this.#delay = Number(this.getAttribute("delay")) || 0;
1852
- this.#action = this.getAttribute("trigger-action") || "click";
1853
- this.#id = `tooltip-${figUniqueId()}`;
1854
- if (this.#popover) {
1855
- this.#popover.setAttribute("id", this.#id);
1856
- this.#popover.setAttribute("role", "tooltip");
1857
- this.#popover.setAttribute("popover", "manual");
1858
- this.#popover.style["position-anchor"] = `--${this.#id}`;
1859
-
1860
- this.#trigger.setAttribute("popovertarget", this.#id);
1861
- this.#trigger.setAttribute("popovertargetaction", "toggle");
1862
- this.#trigger.style["anchor-name"] = `--${this.#id}`;
1863
-
1864
- if (this.#action === "hover") {
1865
- this.#trigger.addEventListener("mouseover", this.handleOpen.bind(this));
1866
- this.#trigger.addEventListener("mouseout", this.handleClose.bind(this));
1867
- } else {
1868
- this.#trigger.addEventListener("click", this.handleToggle.bind(this));
1869
- }
1870
-
1871
- document.body.append(this.#popover);
1872
- }
1873
- }
1874
-
1875
- handleClose() {
1876
- clearTimeout(this.#timeout);
1877
- this.#popover.hidePopover();
1878
- }
1879
- handleToggle() {
1880
- if (this.#popover.matches(":popover-open")) {
1881
- this.handleClose();
1882
- } else {
1883
- this.handleOpen();
1884
- }
1885
- }
1886
- handleOpen() {
1887
- clearTimeout(this.#timeout);
1888
- this.#timeout = setTimeout(() => {
1889
- this.#popover.showPopover();
1890
- }, this.#delay);
1891
- }
1892
- }
1893
- customElements.define("fig-popover-2", FigPopover2);
1894
-
1895
1991
  /* Tabs */
1896
1992
  /**
1897
1993
  * A custom tab element for use within FigTabs.
@@ -2172,14 +2268,15 @@ class FigSegmentedControl extends HTMLElement {
2172
2268
  this.name = this.getAttribute("name") || "segmented-control";
2173
2269
  this.addEventListener("click", this.handleClick.bind(this));
2174
2270
  this.#applyDisabled(
2175
- this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false"
2271
+ this.hasAttribute("disabled") &&
2272
+ this.getAttribute("disabled") !== "false",
2176
2273
  );
2177
2274
 
2178
2275
  // Ensure at least one segment is selected (default to first)
2179
2276
  requestAnimationFrame(() => {
2180
2277
  const segments = this.querySelectorAll("fig-segment");
2181
2278
  const hasSelected = Array.from(segments).some((s) =>
2182
- s.hasAttribute("selected")
2279
+ s.hasAttribute("selected"),
2183
2280
  );
2184
2281
  if (!hasSelected && segments.length > 0) {
2185
2282
  this.selectedSegment = segments[0];
@@ -2363,13 +2460,17 @@ class FigSlider extends HTMLElement {
2363
2460
  this.input.addEventListener("input", this.#boundHandleInput);
2364
2461
  this.input.removeEventListener("change", this.#boundHandleChange);
2365
2462
  this.input.addEventListener("change", this.#boundHandleChange);
2366
- this.input.addEventListener("pointerdown", () => { this.#isInteracting = true; });
2367
- this.input.addEventListener("pointerup", () => { this.#isInteracting = false; });
2463
+ this.input.addEventListener("pointerdown", () => {
2464
+ this.#isInteracting = true;
2465
+ });
2466
+ this.input.addEventListener("pointerup", () => {
2467
+ this.#isInteracting = false;
2468
+ });
2368
2469
 
2369
2470
  if (this.default) {
2370
2471
  this.style.setProperty(
2371
2472
  "--default",
2372
- this.#calculateNormal(this.default)
2473
+ this.#calculateNormal(this.default),
2373
2474
  );
2374
2475
  }
2375
2476
 
@@ -2379,7 +2480,7 @@ class FigSlider extends HTMLElement {
2379
2480
  this.inputContainer.append(this.datalist);
2380
2481
  this.datalist.setAttribute(
2381
2482
  "id",
2382
- this.datalist.getAttribute("id") || figUniqueId()
2483
+ this.datalist.getAttribute("id") || figUniqueId(),
2383
2484
  );
2384
2485
  this.input.setAttribute("list", this.datalist.getAttribute("id"));
2385
2486
  } else if (this.type === "stepper") {
@@ -2404,7 +2505,7 @@ class FigSlider extends HTMLElement {
2404
2505
  }
2405
2506
  if (this.datalist) {
2406
2507
  let defaultOption = this.datalist.querySelector(
2407
- `option[value='${this.default}']`
2508
+ `option[value='${this.default}']`,
2408
2509
  );
2409
2510
  if (defaultOption) {
2410
2511
  defaultOption.setAttribute("default", "true");
@@ -2413,19 +2514,19 @@ class FigSlider extends HTMLElement {
2413
2514
  if (this.figInputNumber) {
2414
2515
  this.figInputNumber.removeEventListener(
2415
2516
  "input",
2416
- this.#boundHandleTextInput
2517
+ this.#boundHandleTextInput,
2417
2518
  );
2418
2519
  this.figInputNumber.addEventListener(
2419
2520
  "input",
2420
- this.#boundHandleTextInput
2521
+ this.#boundHandleTextInput,
2421
2522
  );
2422
2523
  this.figInputNumber.removeEventListener(
2423
2524
  "change",
2424
- this.#boundHandleTextChange
2525
+ this.#boundHandleTextChange,
2425
2526
  );
2426
2527
  this.figInputNumber.addEventListener(
2427
2528
  "change",
2428
- this.#boundHandleTextChange
2529
+ this.#boundHandleTextChange,
2429
2530
  );
2430
2531
  }
2431
2532
 
@@ -2445,11 +2546,11 @@ class FigSlider extends HTMLElement {
2445
2546
  if (this.figInputNumber) {
2446
2547
  this.figInputNumber.removeEventListener(
2447
2548
  "input",
2448
- this.#boundHandleTextInput
2549
+ this.#boundHandleTextInput,
2449
2550
  );
2450
2551
  this.figInputNumber.removeEventListener(
2451
2552
  "change",
2452
- this.#boundHandleTextChange
2553
+ this.#boundHandleTextChange,
2453
2554
  );
2454
2555
  }
2455
2556
  }
@@ -2459,7 +2560,7 @@ class FigSlider extends HTMLElement {
2459
2560
  this.value = this.input.value = this.figInputNumber.value;
2460
2561
  this.#syncProperties();
2461
2562
  this.dispatchEvent(
2462
- new CustomEvent("input", { detail: this.value, bubbles: true })
2563
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
2463
2564
  );
2464
2565
  }
2465
2566
  }
@@ -2489,7 +2590,7 @@ class FigSlider extends HTMLElement {
2489
2590
  #handleInput() {
2490
2591
  this.#syncValue();
2491
2592
  this.dispatchEvent(
2492
- new CustomEvent("input", { detail: this.value, bubbles: true })
2593
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
2493
2594
  );
2494
2595
  }
2495
2596
 
@@ -2497,7 +2598,7 @@ class FigSlider extends HTMLElement {
2497
2598
  this.#isInteracting = false;
2498
2599
  this.#syncValue();
2499
2600
  this.dispatchEvent(
2500
- new CustomEvent("change", { detail: this.value, bubbles: true })
2601
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
2501
2602
  );
2502
2603
  }
2503
2604
 
@@ -2506,7 +2607,7 @@ class FigSlider extends HTMLElement {
2506
2607
  this.value = this.input.value = this.figInputNumber.value;
2507
2608
  this.#syncProperties();
2508
2609
  this.dispatchEvent(
2509
- new CustomEvent("change", { detail: this.value, bubbles: true })
2610
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
2510
2611
  );
2511
2612
  }
2512
2613
  }
@@ -2726,10 +2827,10 @@ class FigInputText extends HTMLElement {
2726
2827
  this.value = value;
2727
2828
  this.input.value = valueTransformed;
2728
2829
  this.dispatchEvent(
2729
- new CustomEvent("input", { detail: this.value, bubbles: true })
2830
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
2730
2831
  );
2731
2832
  this.dispatchEvent(
2732
- new CustomEvent("change", { detail: this.value, bubbles: true })
2833
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
2733
2834
  );
2734
2835
  }
2735
2836
  #handleMouseMove(e) {
@@ -2777,13 +2878,13 @@ class FigInputText extends HTMLElement {
2777
2878
  if (typeof this.min === "number") {
2778
2879
  sanitized = Math.max(
2779
2880
  transform ? this.#transformNumber(this.min) : this.min,
2780
- sanitized
2881
+ sanitized,
2781
2882
  );
2782
2883
  }
2783
2884
  if (typeof this.max === "number") {
2784
2885
  sanitized = Math.min(
2785
2886
  transform ? this.#transformNumber(this.max) : this.max,
2786
- sanitized
2887
+ sanitized,
2787
2888
  );
2788
2889
  }
2789
2890
 
@@ -3103,7 +3204,7 @@ class FigInputNumber extends HTMLElement {
3103
3204
  e.target.value = "";
3104
3205
  }
3105
3206
  this.dispatchEvent(
3106
- new CustomEvent("change", { detail: this.value, bubbles: true })
3207
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
3107
3208
  );
3108
3209
  }
3109
3210
 
@@ -3129,10 +3230,10 @@ class FigInputNumber extends HTMLElement {
3129
3230
  this.input.value = this.#formatWithUnit(this.value);
3130
3231
 
3131
3232
  this.dispatchEvent(
3132
- new CustomEvent("input", { detail: this.value, bubbles: true })
3233
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
3133
3234
  );
3134
3235
  this.dispatchEvent(
3135
- new CustomEvent("change", { detail: this.value, bubbles: true })
3236
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
3136
3237
  );
3137
3238
  }
3138
3239
 
@@ -3144,7 +3245,7 @@ class FigInputNumber extends HTMLElement {
3144
3245
  this.value = "";
3145
3246
  }
3146
3247
  this.dispatchEvent(
3147
- new CustomEvent("input", { detail: this.value, bubbles: true })
3248
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
3148
3249
  );
3149
3250
  }
3150
3251
 
@@ -3161,10 +3262,10 @@ class FigInputNumber extends HTMLElement {
3161
3262
  e.target.value = "";
3162
3263
  }
3163
3264
  this.dispatchEvent(
3164
- new CustomEvent("input", { detail: this.value, bubbles: true })
3265
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
3165
3266
  );
3166
3267
  this.dispatchEvent(
3167
- new CustomEvent("change", { detail: this.value, bubbles: true })
3268
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
3168
3269
  );
3169
3270
  }
3170
3271
 
@@ -3223,7 +3324,9 @@ class FigInputNumber extends HTMLElement {
3223
3324
  const factor = Math.pow(10, precision);
3224
3325
  const rounded = Math.round(num * factor) / factor;
3225
3326
  // Only show decimals if needed and up to precision
3226
- return Number.isInteger(rounded) ? rounded : parseFloat(rounded.toFixed(precision));
3327
+ return Number.isInteger(rounded)
3328
+ ? rounded
3329
+ : parseFloat(rounded.toFixed(precision));
3227
3330
  }
3228
3331
 
3229
3332
  static get observedAttributes() {
@@ -3357,7 +3460,7 @@ class FigField extends HTMLElement {
3357
3460
  requestAnimationFrame(() => {
3358
3461
  this.label = this.querySelector(":scope>label");
3359
3462
  this.input = Array.from(this.childNodes).find((node) =>
3360
- node.nodeName.toLowerCase().startsWith("fig-")
3463
+ node.nodeName.toLowerCase().startsWith("fig-"),
3361
3464
  );
3362
3465
  if (this.input && this.label) {
3363
3466
  this.label.addEventListener("click", this.focus.bind(this));
@@ -3502,11 +3605,11 @@ class FigInputColor extends HTMLElement {
3502
3605
  }
3503
3606
  this.#fillPicker.addEventListener(
3504
3607
  "input",
3505
- this.#handleFillPickerInput.bind(this)
3608
+ this.#handleFillPickerInput.bind(this),
3506
3609
  );
3507
3610
  this.#fillPicker.addEventListener(
3508
3611
  "change",
3509
- this.#handleChange.bind(this)
3612
+ this.#handleChange.bind(this),
3510
3613
  );
3511
3614
  }
3512
3615
 
@@ -3519,22 +3622,22 @@ class FigInputColor extends HTMLElement {
3519
3622
  }
3520
3623
  this.#textInput.addEventListener(
3521
3624
  "input",
3522
- this.#handleTextInput.bind(this)
3625
+ this.#handleTextInput.bind(this),
3523
3626
  );
3524
3627
  this.#textInput.addEventListener(
3525
3628
  "change",
3526
- this.#handleChange.bind(this)
3629
+ this.#handleChange.bind(this),
3527
3630
  );
3528
3631
  }
3529
3632
 
3530
3633
  if (this.#alphaInput) {
3531
3634
  this.#alphaInput.addEventListener(
3532
3635
  "input",
3533
- this.#handleAlphaInput.bind(this)
3636
+ this.#handleAlphaInput.bind(this),
3534
3637
  );
3535
3638
  this.#alphaInput.addEventListener(
3536
3639
  "change",
3537
- this.#handleChange.bind(this)
3640
+ this.#handleChange.bind(this),
3538
3641
  );
3539
3642
  }
3540
3643
  });
@@ -3547,7 +3650,7 @@ class FigInputColor extends HTMLElement {
3547
3650
  g: isNaN(this.rgba.g) ? 0 : this.rgba.g,
3548
3651
  b: isNaN(this.rgba.b) ? 0 : this.rgba.b,
3549
3652
  },
3550
- this.rgba.a
3653
+ this.rgba.a,
3551
3654
  );
3552
3655
  this.hexWithAlpha = this.value.toUpperCase();
3553
3656
  this.hexOpaque = this.hexWithAlpha.slice(0, 7);
@@ -3590,7 +3693,7 @@ class FigInputColor extends HTMLElement {
3590
3693
  type: "solid",
3591
3694
  color: this.hexOpaque,
3592
3695
  opacity: this.alpha,
3593
- })
3696
+ }),
3594
3697
  );
3595
3698
  }
3596
3699
  this.#emitInputEvent();
@@ -3613,7 +3716,7 @@ class FigInputColor extends HTMLElement {
3613
3716
  // Display without # prefix
3614
3717
  this.#textInput.setAttribute(
3615
3718
  "value",
3616
- this.hexOpaque.slice(1).toUpperCase()
3719
+ this.hexOpaque.slice(1).toUpperCase(),
3617
3720
  );
3618
3721
  }
3619
3722
  this.#emitInputEvent();
@@ -3635,7 +3738,7 @@ class FigInputColor extends HTMLElement {
3635
3738
  if (this.#textInput) {
3636
3739
  this.#textInput.setAttribute(
3637
3740
  "value",
3638
- this.hexOpaque.slice(1).toUpperCase()
3741
+ this.hexOpaque.slice(1).toUpperCase(),
3639
3742
  );
3640
3743
  }
3641
3744
  if (this.#alphaInput && detail.alpha !== undefined) {
@@ -3693,7 +3796,7 @@ class FigInputColor extends HTMLElement {
3693
3796
  type: "solid",
3694
3797
  color: this.hexOpaque,
3695
3798
  opacity: this.alpha,
3696
- })
3799
+ }),
3697
3800
  );
3698
3801
  }
3699
3802
  if (this.#alphaInput) {
@@ -3760,7 +3863,7 @@ class FigInputColor extends HTMLElement {
3760
3863
  // Handle rgba colors
3761
3864
  else if (color.startsWith("rgba") || color.startsWith("rgb")) {
3762
3865
  let matches = color.match(
3763
- /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/
3866
+ /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/,
3764
3867
  );
3765
3868
  if (matches) {
3766
3869
  r = parseInt(matches[1]);
@@ -3772,7 +3875,7 @@ class FigInputColor extends HTMLElement {
3772
3875
  // Handle hsla colors
3773
3876
  else if (color.startsWith("hsla") || color.startsWith("hsl")) {
3774
3877
  let matches = color.match(
3775
- /hsla?\((\d+),\s*(\d+)%,\s*(\d+)%(?:,\s*(\d+(?:\.\d+)?))?\)/
3878
+ /hsla?\((\d+),\s*(\d+)%,\s*(\d+)%(?:,\s*(\d+(?:\.\d+)?))?\)/,
3776
3879
  );
3777
3880
  if (matches) {
3778
3881
  let h = parseInt(matches[1]) / 360;
@@ -3988,8 +4091,8 @@ class FigInputFill extends HTMLElement {
3988
4091
  this.innerHTML = `
3989
4092
  <div class="input-combo">
3990
4093
  <fig-fill-picker ${fpAttrs} value='${fillPickerValue}' ${
3991
- disabled ? "disabled" : ""
3992
- }></fig-fill-picker>
4094
+ disabled ? "disabled" : ""
4095
+ }></fig-fill-picker>
3993
4096
  ${controlsHtml}
3994
4097
  </div>`;
3995
4098
 
@@ -4126,13 +4229,13 @@ class FigInputFill extends HTMLElement {
4126
4229
  if (this.#hexInput) {
4127
4230
  this.#hexInput.setAttribute(
4128
4231
  "value",
4129
- this.#solid.color.slice(1).toUpperCase()
4232
+ this.#solid.color.slice(1).toUpperCase(),
4130
4233
  );
4131
4234
  }
4132
4235
  if (this.#opacityInput) {
4133
4236
  this.#opacityInput.setAttribute(
4134
4237
  "value",
4135
- Math.round(this.#solid.alpha * 100)
4238
+ Math.round(this.#solid.alpha * 100),
4136
4239
  );
4137
4240
  }
4138
4241
  break;
@@ -4140,7 +4243,7 @@ class FigInputFill extends HTMLElement {
4140
4243
  if (this.#opacityInput) {
4141
4244
  this.#opacityInput.setAttribute(
4142
4245
  "value",
4143
- this.#gradient.stops[0]?.opacity ?? 100
4246
+ this.#gradient.stops[0]?.opacity ?? 100,
4144
4247
  );
4145
4248
  }
4146
4249
  const label = this.querySelector(".fig-input-fill-label");
@@ -4156,7 +4259,7 @@ class FigInputFill extends HTMLElement {
4156
4259
  if (this.#opacityInput) {
4157
4260
  this.#opacityInput.setAttribute(
4158
4261
  "value",
4159
- Math.round((this.#image.opacity ?? 1) * 100)
4262
+ Math.round((this.#image.opacity ?? 1) * 100),
4160
4263
  );
4161
4264
  }
4162
4265
  break;
@@ -4164,7 +4267,7 @@ class FigInputFill extends HTMLElement {
4164
4267
  if (this.#opacityInput) {
4165
4268
  this.#opacityInput.setAttribute(
4166
4269
  "value",
4167
- Math.round((this.#video.opacity ?? 1) * 100)
4270
+ Math.round((this.#video.opacity ?? 1) * 100),
4168
4271
  );
4169
4272
  }
4170
4273
  break;
@@ -4172,7 +4275,7 @@ class FigInputFill extends HTMLElement {
4172
4275
  if (this.#opacityInput) {
4173
4276
  this.#opacityInput.setAttribute(
4174
4277
  "value",
4175
- Math.round((this.#webcam.opacity ?? 1) * 100)
4278
+ Math.round((this.#webcam.opacity ?? 1) * 100),
4176
4279
  );
4177
4280
  }
4178
4281
  break;
@@ -4376,7 +4479,7 @@ class FigInputFill extends HTMLElement {
4376
4479
  new CustomEvent("input", {
4377
4480
  bubbles: true,
4378
4481
  detail: this.value,
4379
- })
4482
+ }),
4380
4483
  );
4381
4484
  }
4382
4485
 
@@ -4385,7 +4488,7 @@ class FigInputFill extends HTMLElement {
4385
4488
  new CustomEvent("change", {
4386
4489
  bubbles: true,
4387
4490
  detail: this.value,
4388
- })
4491
+ }),
4389
4492
  );
4390
4493
  }
4391
4494
 
@@ -4625,14 +4728,14 @@ class FigCheckbox extends HTMLElement {
4625
4728
  bubbles: true,
4626
4729
  composed: true,
4627
4730
  detail: { checked: this.input.checked, value: this.input.value },
4628
- })
4731
+ }),
4629
4732
  );
4630
4733
  this.dispatchEvent(
4631
4734
  new CustomEvent("change", {
4632
4735
  bubbles: true,
4633
4736
  composed: true,
4634
4737
  detail: { checked: this.input.checked, value: this.input.value },
4635
- })
4738
+ }),
4636
4739
  );
4637
4740
  }
4638
4741
  }
@@ -4827,8 +4930,9 @@ class FigComboInput extends HTMLElement {
4827
4930
  }
4828
4931
  connectedCallback() {
4829
4932
  const customDropdown =
4830
- Array.from(this.children).find((child) => child.tagName === "FIG-DROPDOWN") ||
4831
- null;
4933
+ Array.from(this.children).find(
4934
+ (child) => child.tagName === "FIG-DROPDOWN",
4935
+ ) || null;
4832
4936
  this.#usesCustomDropdown = customDropdown !== null;
4833
4937
  if (customDropdown) {
4834
4938
  customDropdown.remove();
@@ -5150,7 +5254,10 @@ class FigImage extends HTMLElement {
5150
5254
  }
5151
5255
  disconnectedCallback() {
5152
5256
  this.fileInput?.removeEventListener("change", this.#boundHandleFileInput);
5153
- this.downloadButton?.removeEventListener("click", this.#boundHandleDownload);
5257
+ this.downloadButton?.removeEventListener(
5258
+ "click",
5259
+ this.#boundHandleDownload,
5260
+ );
5154
5261
  }
5155
5262
 
5156
5263
  #updateRefs() {
@@ -5159,13 +5266,22 @@ class FigImage extends HTMLElement {
5159
5266
  if (this.upload) {
5160
5267
  this.uploadButton = this.querySelector("fig-button[type='upload']");
5161
5268
  this.fileInput = this.uploadButton?.querySelector("input");
5162
- this.fileInput?.removeEventListener("change", this.#boundHandleFileInput);
5269
+ this.fileInput?.removeEventListener(
5270
+ "change",
5271
+ this.#boundHandleFileInput,
5272
+ );
5163
5273
  this.fileInput?.addEventListener("change", this.#boundHandleFileInput);
5164
5274
  }
5165
5275
  if (this.download) {
5166
5276
  this.downloadButton = this.querySelector("fig-button[type='download']");
5167
- this.downloadButton?.removeEventListener("click", this.#boundHandleDownload);
5168
- this.downloadButton?.addEventListener("click", this.#boundHandleDownload);
5277
+ this.downloadButton?.removeEventListener(
5278
+ "click",
5279
+ this.#boundHandleDownload,
5280
+ );
5281
+ this.downloadButton?.addEventListener(
5282
+ "click",
5283
+ this.#boundHandleDownload,
5284
+ );
5169
5285
  }
5170
5286
  });
5171
5287
  }
@@ -5187,7 +5303,7 @@ class FigImage extends HTMLElement {
5187
5303
  if (!ar || ar === "auto") {
5188
5304
  this.style.setProperty(
5189
5305
  "--aspect-ratio",
5190
- `${this.image.width}/${this.image.height}`
5306
+ `${this.image.width}/${this.image.height}`,
5191
5307
  );
5192
5308
  }
5193
5309
  this.dispatchEvent(
@@ -5198,7 +5314,7 @@ class FigImage extends HTMLElement {
5198
5314
  blob: this.blob,
5199
5315
  base64: this.base64,
5200
5316
  },
5201
- })
5317
+ }),
5202
5318
  );
5203
5319
  resolve();
5204
5320
 
@@ -5249,14 +5365,14 @@ class FigImage extends HTMLElement {
5249
5365
  blob: this.blob,
5250
5366
  base64: this.base64,
5251
5367
  },
5252
- })
5368
+ }),
5253
5369
  );
5254
5370
  //emit for change too
5255
5371
  this.dispatchEvent(
5256
5372
  new CustomEvent("change", {
5257
5373
  bubbles: true,
5258
5374
  cancelable: true,
5259
- })
5375
+ }),
5260
5376
  );
5261
5377
  this.setAttribute("src", this.blob);
5262
5378
  }
@@ -5277,7 +5393,7 @@ class FigImage extends HTMLElement {
5277
5393
  if (this.chit) {
5278
5394
  this.chit.setAttribute(
5279
5395
  "background",
5280
- this.#src ? `url(${this.#src})` : ""
5396
+ this.#src ? `url(${this.#src})` : "",
5281
5397
  );
5282
5398
  }
5283
5399
  if (this.#src) {
@@ -5330,6 +5446,8 @@ class FigEasingCurve extends HTMLElement {
5330
5446
  #line2 = null;
5331
5447
  #handle1 = null;
5332
5448
  #handle2 = null;
5449
+ #bezierEndpointStart = null;
5450
+ #bezierEndpointEnd = null;
5333
5451
  #dropdown = null;
5334
5452
  #presetName = null;
5335
5453
  #targetLine = null;
@@ -5339,29 +5457,86 @@ class FigEasingCurve extends HTMLElement {
5339
5457
  #bounds = null;
5340
5458
  #diagonal = null;
5341
5459
  #resizeObserver = null;
5460
+ #bezierHandleRadius = 3.625;
5461
+ #bezierEndpointRadius = 2;
5462
+ #springHandleRadius = 3.625;
5463
+ #durationBarWidth = 6;
5464
+ #durationBarHeight = 16;
5465
+ #durationBarRadius = 3;
5342
5466
 
5343
5467
  static PRESETS = [
5344
5468
  { 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] },
5469
+ {
5470
+ group: "Bezier",
5471
+ name: "Ease in",
5472
+ type: "bezier",
5473
+ value: [0.42, 0, 1, 1],
5474
+ },
5475
+ {
5476
+ group: "Bezier",
5477
+ name: "Ease out",
5478
+ type: "bezier",
5479
+ value: [0, 0, 0.58, 1],
5480
+ },
5481
+ {
5482
+ group: "Bezier",
5483
+ name: "Ease in and out",
5484
+ type: "bezier",
5485
+ value: [0.42, 0, 0.58, 1],
5486
+ },
5487
+ {
5488
+ group: "Bezier",
5489
+ name: "Ease in back",
5490
+ type: "bezier",
5491
+ value: [0.6, -0.28, 0.735, 0.045],
5492
+ },
5493
+ {
5494
+ group: "Bezier",
5495
+ name: "Ease out back",
5496
+ type: "bezier",
5497
+ value: [0.175, 0.885, 0.32, 1.275],
5498
+ },
5499
+ {
5500
+ group: "Bezier",
5501
+ name: "Ease in and out back",
5502
+ type: "bezier",
5503
+ value: [0.68, -0.55, 0.265, 1.55],
5504
+ },
5351
5505
  { 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 } },
5506
+ {
5507
+ group: "Spring",
5508
+ name: "Gentle",
5509
+ type: "spring",
5510
+ spring: { stiffness: 120, damping: 14, mass: 1 },
5511
+ },
5512
+ {
5513
+ group: "Spring",
5514
+ name: "Quick",
5515
+ type: "spring",
5516
+ spring: { stiffness: 380, damping: 20, mass: 1 },
5517
+ },
5518
+ {
5519
+ group: "Spring",
5520
+ name: "Bouncy",
5521
+ type: "spring",
5522
+ spring: { stiffness: 250, damping: 8, mass: 1 },
5523
+ },
5524
+ {
5525
+ group: "Spring",
5526
+ name: "Slow",
5527
+ type: "spring",
5528
+ spring: { stiffness: 60, damping: 11, mass: 1 },
5529
+ },
5356
5530
  { group: "Spring", name: "Custom spring", type: "spring", spring: null },
5357
5531
  ];
5358
5532
 
5359
5533
  static get observedAttributes() {
5360
- return ["value", "precision"];
5534
+ return ["value", "precision", "aspect-ratio"];
5361
5535
  }
5362
5536
 
5363
5537
  connectedCallback() {
5364
5538
  this.#precision = parseInt(this.getAttribute("precision") || "2");
5539
+ this.#syncAspectRatioVar(this.getAttribute("aspect-ratio"));
5365
5540
  const val = this.getAttribute("value");
5366
5541
  if (val) this.#parseValue(val);
5367
5542
  this.#presetName = this.#matchPreset();
@@ -5377,7 +5552,24 @@ class FigEasingCurve extends HTMLElement {
5377
5552
  }
5378
5553
  }
5379
5554
 
5555
+ #syncAspectRatioVar(value) {
5556
+ if (value && value.trim()) {
5557
+ this.style.setProperty("--aspect-ratio", value.trim());
5558
+ } else {
5559
+ this.style.removeProperty("--aspect-ratio");
5560
+ }
5561
+ }
5562
+
5380
5563
  attributeChangedCallback(name, oldValue, newValue) {
5564
+ if (name === "aspect-ratio") {
5565
+ this.#syncAspectRatioVar(newValue);
5566
+ if (this.#svg) {
5567
+ this.#syncViewportSize();
5568
+ this.#updatePaths();
5569
+ }
5570
+ return;
5571
+ }
5572
+
5381
5573
  if (!this.#svg) return;
5382
5574
  if (name === "value" && newValue) {
5383
5575
  const prevMode = this.#mode;
@@ -5413,7 +5605,8 @@ class FigEasingCurve extends HTMLElement {
5413
5605
  for (let i = 0; i < points.length; i += step) {
5414
5606
  vals.push(points[i].value.toFixed(3));
5415
5607
  }
5416
- if (points.length > 0) vals.push(points[points.length - 1].value.toFixed(3));
5608
+ if (points.length > 0)
5609
+ vals.push(points[points.length - 1].value.toFixed(3));
5417
5610
  return `linear(${vals.join(", ")})`;
5418
5611
  }
5419
5612
  const p = this.#precision;
@@ -5429,7 +5622,9 @@ class FigEasingCurve extends HTMLElement {
5429
5622
  }
5430
5623
 
5431
5624
  #parseValue(str) {
5432
- const springMatch = str.match(/^spring\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/);
5625
+ const springMatch = str.match(
5626
+ /^spring\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*\)$/,
5627
+ );
5433
5628
  if (springMatch) {
5434
5629
  this.#mode = "spring";
5435
5630
  this.#spring.stiffness = parseFloat(springMatch[1]);
@@ -5457,7 +5652,8 @@ class FigEasingCurve extends HTMLElement {
5457
5652
  Math.abs(this.#cp1.y - p.value[1]) < ep &&
5458
5653
  Math.abs(this.#cp2.x - p.value[2]) < ep &&
5459
5654
  Math.abs(this.#cp2.y - p.value[3]) < ep
5460
- ) return p.name;
5655
+ )
5656
+ return p.name;
5461
5657
  }
5462
5658
  return "Custom bezier";
5463
5659
  }
@@ -5467,7 +5663,8 @@ class FigEasingCurve extends HTMLElement {
5467
5663
  Math.abs(this.#spring.stiffness - p.spring.stiffness) < ep &&
5468
5664
  Math.abs(this.#spring.damping - p.spring.damping) < ep &&
5469
5665
  Math.abs(this.#spring.mass - p.spring.mass) < ep
5470
- ) return p.name;
5666
+ )
5667
+ return p.name;
5471
5668
  }
5472
5669
  return "Custom spring";
5473
5670
  }
@@ -5479,13 +5676,15 @@ class FigEasingCurve extends HTMLElement {
5479
5676
  const dt = 0.004;
5480
5677
  const maxTime = 5;
5481
5678
  const points = [];
5482
- let pos = 0, vel = 0;
5679
+ let pos = 0,
5680
+ vel = 0;
5483
5681
  for (let t = 0; t <= maxTime; t += dt) {
5484
5682
  const force = -stiffness * (pos - 1) - damping * vel;
5485
5683
  vel += (force / mass) * dt;
5486
5684
  pos += vel * dt;
5487
5685
  points.push({ t, value: pos });
5488
- if (t > 0.1 && Math.abs(pos - 1) < 0.0005 && Math.abs(vel) < 0.0005) break;
5686
+ if (t > 0.1 && Math.abs(pos - 1) < 0.0005 && Math.abs(vel) < 0.0005)
5687
+ break;
5489
5688
  }
5490
5689
  return points;
5491
5690
  }
@@ -5495,7 +5694,8 @@ class FigEasingCurve extends HTMLElement {
5495
5694
  const dt = 0.004;
5496
5695
  const maxTime = 5;
5497
5696
  const pts = [];
5498
- let pos = 0, vel = 0;
5697
+ let pos = 0,
5698
+ vel = 0;
5499
5699
  for (let t = 0; t <= maxTime; t += dt) {
5500
5700
  const force = -stiffness * (pos - 1) - damping * vel;
5501
5701
  vel += (force / mass) * dt;
@@ -5518,21 +5718,64 @@ class FigEasingCurve extends HTMLElement {
5518
5718
  const y = pad + (1 - (pts[i].value - minVal) / range) * s;
5519
5719
  d += (i === 0 ? "M" : "L") + x.toFixed(1) + "," + y.toFixed(1);
5520
5720
  }
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>`;
5721
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" fill="none"><path d="${d}" stroke="currentColor" stroke-width="1" stroke-linecap="round" fill="none"/></svg>`;
5522
5722
  }
5523
5723
 
5524
5724
  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>`;
5725
+ const draw = 12;
5726
+ const pad = (size - draw) / 2;
5727
+ const samples = 48;
5728
+ const points = [];
5729
+
5730
+ const cubic = (p0, p1, p2, p3, t) => {
5731
+ const mt = 1 - t;
5732
+ return (
5733
+ mt * mt * mt * p0 +
5734
+ 3 * mt * mt * t * p1 +
5735
+ 3 * mt * t * t * p2 +
5736
+ t * t * t * p3
5737
+ );
5738
+ };
5739
+
5740
+ for (let i = 0; i <= samples; i++) {
5741
+ const t = i / samples;
5742
+ points.push({
5743
+ x: cubic(0, cp1x, cp2x, 1, t),
5744
+ y: cubic(0, cp1y, cp2y, 1, t),
5745
+ });
5746
+ }
5747
+
5748
+ let minX = Infinity;
5749
+ let maxX = -Infinity;
5750
+ let minY = Infinity;
5751
+ let maxY = -Infinity;
5752
+ for (const p of points) {
5753
+ if (p.x < minX) minX = p.x;
5754
+ if (p.x > maxX) maxX = p.x;
5755
+ if (p.y < minY) minY = p.y;
5756
+ if (p.y > maxY) maxY = p.y;
5757
+ }
5758
+
5759
+ const rangeX = Math.max(maxX - minX, 1e-6);
5760
+ const rangeY = Math.max(maxY - minY, 1e-6);
5761
+ const toX = (x) => pad + ((x - minX) / rangeX) * draw;
5762
+ const toY = (y) => pad + (1 - (y - minY) / rangeY) * draw;
5763
+
5764
+ let d = "";
5765
+ for (let i = 0; i < points.length; i++) {
5766
+ const px = toX(points[i].x);
5767
+ const py = toY(points[i].y);
5768
+ d += `${i === 0 ? "M" : "L"}${px.toFixed(1)},${py.toFixed(1)}`;
5769
+ }
5770
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" fill="none"><path d="${d}" stroke="currentColor" stroke-width="1" stroke-linecap="round"/></svg>`;
5531
5771
  }
5532
5772
 
5533
5773
  // --- Rendering ---
5534
5774
 
5535
5775
  #render() {
5776
+ this.classList.toggle("spring-mode", this.#mode === "spring");
5777
+ this.classList.toggle("bezier-mode", this.#mode !== "spring");
5778
+ this.#syncMetricsFromCSS();
5536
5779
  this.innerHTML = this.#getInnerHTML();
5537
5780
  this.#cacheRefs();
5538
5781
  this.#syncViewportSize();
@@ -5555,7 +5798,12 @@ class FigEasingCurve extends HTMLElement {
5555
5798
  const sp = p.spring || this.#spring;
5556
5799
  icon = FigEasingCurve.#springIcon(sp);
5557
5800
  } else {
5558
- const v = p.value || [this.#cp1.x, this.#cp1.y, this.#cp2.x, this.#cp2.y];
5801
+ const v = p.value || [
5802
+ this.#cp1.x,
5803
+ this.#cp1.y,
5804
+ this.#cp2.x,
5805
+ this.#cp2.y,
5806
+ ];
5559
5807
  icon = FigEasingCurve.curveIcon(...v);
5560
5808
  }
5561
5809
  const selected = p.name === this.#presetName ? " selected" : "";
@@ -5577,8 +5825,8 @@ class FigEasingCurve extends HTMLElement {
5577
5825
  <line class="fig-easing-curve-target" x1="0" y1="${targetY}" x2="${size}" y2="${targetY}"/>
5578
5826
  <line class="fig-easing-curve-diagonal" x1="0" y1="${startY}" x2="0" y2="${startY}"/>
5579
5827
  <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"/>
5828
+ <circle class="fig-easing-curve-handle" data-handle="bounce" r="${this.#springHandleRadius}"/>
5829
+ <rect class="fig-easing-curve-duration-bar" data-handle="duration" width="${this.#durationBarWidth}" height="${this.#durationBarHeight}" rx="${this.#durationBarRadius}" ry="${this.#durationBarRadius}"/>
5582
5830
  </svg></div>`;
5583
5831
  }
5584
5832
 
@@ -5588,18 +5836,60 @@ class FigEasingCurve extends HTMLElement {
5588
5836
  <line class="fig-easing-curve-arm" data-arm="1"/>
5589
5837
  <line class="fig-easing-curve-arm" data-arm="2"/>
5590
5838
  <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"/>
5839
+ <circle class="fig-easing-curve-endpoint" data-endpoint="start" r="${this.#bezierEndpointRadius}"/>
5840
+ <circle class="fig-easing-curve-endpoint" data-endpoint="end" r="${this.#bezierEndpointRadius}"/>
5841
+ <circle class="fig-easing-curve-handle" data-handle="1" r="${this.#bezierHandleRadius}"/>
5842
+ <circle class="fig-easing-curve-handle" data-handle="2" r="${this.#bezierHandleRadius}"/>
5593
5843
  </svg></div>`;
5594
5844
  }
5595
5845
 
5846
+ #readCssNumber(name, fallback) {
5847
+ const raw = getComputedStyle(this).getPropertyValue(name).trim();
5848
+ if (!raw) return fallback;
5849
+ const value = Number.parseFloat(raw);
5850
+ return Number.isFinite(value) ? value : fallback;
5851
+ }
5852
+
5853
+ #syncMetricsFromCSS() {
5854
+ this.#bezierHandleRadius = this.#readCssNumber(
5855
+ "--easing-bezier-handle-radius",
5856
+ this.#bezierHandleRadius,
5857
+ );
5858
+ this.#bezierEndpointRadius = this.#readCssNumber(
5859
+ "--easing-bezier-endpoint-radius",
5860
+ this.#bezierEndpointRadius,
5861
+ );
5862
+ this.#springHandleRadius = this.#readCssNumber(
5863
+ "--easing-spring-handle-radius",
5864
+ this.#springHandleRadius,
5865
+ );
5866
+ this.#durationBarWidth = this.#readCssNumber(
5867
+ "--easing-duration-bar-width",
5868
+ this.#durationBarWidth,
5869
+ );
5870
+ this.#durationBarHeight = this.#readCssNumber(
5871
+ "--easing-duration-bar-height",
5872
+ this.#durationBarHeight,
5873
+ );
5874
+ this.#durationBarRadius = this.#readCssNumber(
5875
+ "--easing-duration-bar-radius",
5876
+ this.#durationBarRadius,
5877
+ );
5878
+ }
5879
+
5596
5880
  #cacheRefs() {
5597
5881
  this.#svg = this.querySelector(".fig-easing-curve-svg");
5598
5882
  this.#curve = this.querySelector(".fig-easing-curve-path");
5599
5883
  this.#line1 = this.querySelector('[data-arm="1"]');
5600
5884
  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"]');
5885
+ this.#handle1 =
5886
+ this.querySelector('[data-handle="1"]') ||
5887
+ this.querySelector('[data-handle="bounce"]');
5888
+ this.#handle2 =
5889
+ this.querySelector('[data-handle="2"]') ||
5890
+ this.querySelector('[data-handle="duration"]');
5891
+ this.#bezierEndpointStart = this.querySelector('[data-endpoint="start"]');
5892
+ this.#bezierEndpointEnd = this.querySelector('[data-endpoint="end"]');
5603
5893
  this.#dropdown = this.querySelector(".fig-easing-curve-dropdown");
5604
5894
  this.#targetLine = this.querySelector(".fig-easing-curve-target");
5605
5895
  this.#bounds = this.querySelector(".fig-easing-curve-bounds");
@@ -5681,7 +5971,10 @@ class FigEasingCurve extends HTMLElement {
5681
5971
  const p2 = this.#toSVG(this.#cp2.x, this.#cp2.y);
5682
5972
  const p3 = this.#toSVG(1, 1);
5683
5973
 
5684
- this.#curve.setAttribute("d", `M${p0.x},${p0.y} C${p1.x},${p1.y} ${p2.x},${p2.y} ${p3.x},${p3.y}`);
5974
+ this.#curve.setAttribute(
5975
+ "d",
5976
+ `M${p0.x},${p0.y} C${p1.x},${p1.y} ${p2.x},${p2.y} ${p3.x},${p3.y}`,
5977
+ );
5685
5978
  this.#line1.setAttribute("x1", p0.x);
5686
5979
  this.#line1.setAttribute("y1", p0.y);
5687
5980
  this.#line1.setAttribute("x2", p1.x);
@@ -5694,6 +5987,14 @@ class FigEasingCurve extends HTMLElement {
5694
5987
  this.#handle1.setAttribute("cy", p1.y);
5695
5988
  this.#handle2.setAttribute("cx", p2.x);
5696
5989
  this.#handle2.setAttribute("cy", p2.y);
5990
+ if (this.#bezierEndpointStart) {
5991
+ this.#bezierEndpointStart.setAttribute("cx", p0.x);
5992
+ this.#bezierEndpointStart.setAttribute("cy", p0.y);
5993
+ }
5994
+ if (this.#bezierEndpointEnd) {
5995
+ this.#bezierEndpointEnd.setAttribute("cx", p3.x);
5996
+ this.#bezierEndpointEnd.setAttribute("cy", p3.y);
5997
+ }
5697
5998
  }
5698
5999
 
5699
6000
  #updateSpringPaths() {
@@ -5708,12 +6009,17 @@ class FigEasingCurve extends HTMLElement {
5708
6009
  if (!points.length) return;
5709
6010
  const totalTime = points[points.length - 1].t || 1;
5710
6011
 
5711
- let minVal = 0, maxVal = 1;
6012
+ let minVal = 0,
6013
+ maxVal = 1;
5712
6014
  for (const p of points) {
5713
6015
  if (p.value < minVal) minVal = p.value;
5714
6016
  if (p.value > maxVal) maxVal = p.value;
5715
6017
  }
5716
- const maxDistFromCenter = Math.max(Math.abs(minVal - 1), Math.abs(maxVal - 1), 0.01);
6018
+ const maxDistFromCenter = Math.max(
6019
+ Math.abs(minVal - 1),
6020
+ Math.abs(maxVal - 1),
6021
+ 0.01,
6022
+ );
5717
6023
  const valPad = 0;
5718
6024
  this.#springScale = {
5719
6025
  minVal: 1 - maxDistFromCenter - valPad,
@@ -5752,9 +6058,8 @@ class FigEasingCurve extends HTMLElement {
5752
6058
 
5753
6059
  // Duration handle: on the target line
5754
6060
  const targetPt = this.#springToSVG(durationNorm, 1);
5755
- this.#handle2.setAttribute("x", targetPt.x - 3);
5756
- this.#handle2.setAttribute("y", targetPt.y - 8);
5757
-
6061
+ this.#handle2.setAttribute("x", targetPt.x - this.#durationBarWidth / 2);
6062
+ this.#handle2.setAttribute("y", targetPt.y - this.#durationBarHeight / 2);
5758
6063
  }
5759
6064
 
5760
6065
  #findPeakOvershoot(points) {
@@ -5792,38 +6097,76 @@ class FigEasingCurve extends HTMLElement {
5792
6097
  this.#cp1.x,
5793
6098
  this.#cp1.y,
5794
6099
  this.#cp2.x,
5795
- this.#cp2.y
6100
+ this.#cp2.y,
5796
6101
  );
5797
6102
  const springIcon = FigEasingCurve.#springIcon(this.#spring);
5798
6103
 
5799
6104
  // Update both slotted options and the cloned native select options.
5800
6105
  this.#setOptionIconByValue(this.#dropdown, "Custom bezier", bezierIcon);
5801
6106
  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);
6107
+ this.#setOptionIconByValue(
6108
+ this.#dropdown.select,
6109
+ "Custom bezier",
6110
+ bezierIcon,
6111
+ );
6112
+ this.#setOptionIconByValue(
6113
+ this.#dropdown.select,
6114
+ "Custom spring",
6115
+ springIcon,
6116
+ );
5804
6117
  }
5805
6118
 
5806
6119
  // --- Events ---
5807
6120
 
5808
6121
  #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
- }));
6122
+ this.dispatchEvent(
6123
+ new CustomEvent(type, {
6124
+ bubbles: true,
6125
+ detail: {
6126
+ mode: this.#mode,
6127
+ value: this.value,
6128
+ cssValue: this.cssValue,
6129
+ preset: this.#presetName,
6130
+ },
6131
+ }),
6132
+ );
5818
6133
  }
5819
6134
 
5820
6135
  #setupEvents() {
5821
6136
  if (this.#mode === "bezier") {
5822
- this.#handle1.addEventListener("pointerdown", (e) => this.#startBezierDrag(e, 1));
5823
- this.#handle2.addEventListener("pointerdown", (e) => this.#startBezierDrag(e, 2));
6137
+ this.#handle1.addEventListener("pointerdown", (e) =>
6138
+ this.#startBezierDrag(e, 1),
6139
+ );
6140
+ this.#handle2.addEventListener("pointerdown", (e) =>
6141
+ this.#startBezierDrag(e, 2),
6142
+ );
6143
+
6144
+ const bezierSurface = this.querySelector(".fig-easing-curve-svg-container");
6145
+ if (bezierSurface) {
6146
+ bezierSurface.addEventListener("pointerdown", (e) => {
6147
+ // Handles keep their own direct drag behavior.
6148
+ if (e.target?.closest?.(".fig-easing-curve-handle")) return;
6149
+ this.#startBezierDrag(e, this.#bezierHandleForClientHalf(e));
6150
+ });
6151
+ }
5824
6152
  } else {
5825
- this.#handle1.addEventListener("pointerdown", (e) => this.#startSpringDrag(e, "bounce"));
5826
- this.#handle2.addEventListener("pointerdown", (e) => this.#startSpringDrag(e, "duration"));
6153
+ this.#handle1.addEventListener("pointerdown", (e) => {
6154
+ e.stopPropagation();
6155
+ this.#startSpringDrag(e, "bounce");
6156
+ });
6157
+ this.#handle2.addEventListener("pointerdown", (e) => {
6158
+ e.stopPropagation();
6159
+ this.#startSpringDrag(e, "duration");
6160
+ });
6161
+
6162
+ const springSurface = this.querySelector(".fig-easing-curve-svg-container");
6163
+ if (springSurface) {
6164
+ springSurface.addEventListener("pointerdown", (e) => {
6165
+ // Bounce handle keeps its own drag mode/cursor.
6166
+ if (e.target?.closest?.(".fig-easing-curve-handle")) return;
6167
+ this.#startSpringDrag(e, "duration");
6168
+ });
6169
+ }
5827
6170
  }
5828
6171
 
5829
6172
  if (this.#dropdown) {
@@ -5874,6 +6217,11 @@ class FigEasingCurve extends HTMLElement {
5874
6217
  };
5875
6218
  }
5876
6219
 
6220
+ #bezierHandleForClientHalf(e) {
6221
+ const svgPt = this.#clientToSVG(e);
6222
+ return svgPt.x <= this.#drawWidth / 2 ? 1 : 2;
6223
+ }
6224
+
5877
6225
  #startBezierDrag(e, handle) {
5878
6226
  e.preventDefault();
5879
6227
  this.#isDragging = handle;
@@ -5926,11 +6274,20 @@ class FigEasingCurve extends HTMLElement {
5926
6274
 
5927
6275
  if (handleType === "bounce") {
5928
6276
  const dy = e.clientY - startY;
5929
- this.#spring.damping = Math.max(1, Math.round(startDamping + dy * 0.15));
6277
+ this.#spring.damping = Math.max(
6278
+ 1,
6279
+ Math.round(startDamping + dy * 0.15),
6280
+ );
5930
6281
  } else {
5931
6282
  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));
6283
+ this.#springDuration = Math.max(
6284
+ 0.05,
6285
+ Math.min(0.95, startDuration + dx / 200),
6286
+ );
6287
+ this.#spring.stiffness = Math.max(
6288
+ 10,
6289
+ Math.round(startStiffness - dx * 1.5),
6290
+ );
5934
6291
  }
5935
6292
 
5936
6293
  this.#updatePaths();
@@ -5952,6 +6309,250 @@ class FigEasingCurve extends HTMLElement {
5952
6309
  }
5953
6310
  customElements.define("fig-easing-curve", FigEasingCurve);
5954
6311
 
6312
+ /**
6313
+ * A 3D rotation control with an interactive cube preview.
6314
+ * @attr {string} value - CSS transform string, e.g. "rotateX(20deg) rotateY(-35deg) rotateZ(0deg)".
6315
+ * @attr {number} precision - Decimal places for angle output (default 1).
6316
+ */
6317
+ class Fig3DRotate extends HTMLElement {
6318
+ #rx = 0;
6319
+ #ry = 0;
6320
+ #rz = 0;
6321
+ #precision = 1;
6322
+ #isDragging = false;
6323
+ #isShiftHeld = false;
6324
+ #cube = null;
6325
+ #container = null;
6326
+ #boundKeyDown = null;
6327
+ #boundKeyUp = null;
6328
+ #fields = [];
6329
+ #fieldInputs = {};
6330
+
6331
+ static get observedAttributes() {
6332
+ return ["value", "precision", "aspect-ratio", "fields"];
6333
+ }
6334
+
6335
+ connectedCallback() {
6336
+ this.#precision = parseInt(this.getAttribute("precision") || "1");
6337
+ this.#syncAspectRatioVar(this.getAttribute("aspect-ratio"));
6338
+ this.#parseFields(this.getAttribute("fields"));
6339
+ const val = this.getAttribute("value");
6340
+ if (val) this.#parseValue(val);
6341
+ this.#render();
6342
+ }
6343
+
6344
+ disconnectedCallback() {
6345
+ this.#isDragging = false;
6346
+ if (this.#boundKeyDown) {
6347
+ window.removeEventListener("keydown", this.#boundKeyDown);
6348
+ window.removeEventListener("keyup", this.#boundKeyUp);
6349
+ }
6350
+ }
6351
+
6352
+ #syncAspectRatioVar(value) {
6353
+ if (value && value.trim()) {
6354
+ this.style.setProperty("--aspect-ratio", value.trim());
6355
+ } else {
6356
+ this.style.removeProperty("--aspect-ratio");
6357
+ }
6358
+ }
6359
+
6360
+ #parseFields(str) {
6361
+ if (!str || !str.trim()) {
6362
+ this.#fields = [];
6363
+ return;
6364
+ }
6365
+ const valid = ["rotateX", "rotateY", "rotateZ"];
6366
+ this.#fields = str
6367
+ .split(",")
6368
+ .map((s) => s.trim())
6369
+ .filter((s) => valid.includes(s));
6370
+ }
6371
+
6372
+ attributeChangedCallback(name, oldValue, newValue) {
6373
+ if (name === "aspect-ratio") {
6374
+ this.#syncAspectRatioVar(newValue);
6375
+ return;
6376
+ }
6377
+ if (name === "fields") {
6378
+ this.#parseFields(newValue);
6379
+ if (this.#cube) this.#render();
6380
+ return;
6381
+ }
6382
+ if (!this.#cube) return;
6383
+ if (name === "value" && newValue) {
6384
+ if (this.#isDragging) return;
6385
+ this.#parseValue(newValue);
6386
+ this.#updateCube();
6387
+ this.#syncFieldInputs();
6388
+ }
6389
+ if (name === "precision") {
6390
+ this.#precision = parseInt(newValue || "1");
6391
+ }
6392
+ }
6393
+
6394
+ get value() {
6395
+ const p = this.#precision;
6396
+ return `rotateX(${this.#rx.toFixed(p)}deg) rotateY(${this.#ry.toFixed(p)}deg) rotateZ(${this.#rz.toFixed(p)}deg)`;
6397
+ }
6398
+
6399
+ set value(v) {
6400
+ this.setAttribute("value", v);
6401
+ }
6402
+
6403
+ get rotateX() {
6404
+ return this.#rx;
6405
+ }
6406
+ get rotateY() {
6407
+ return this.#ry;
6408
+ }
6409
+ get rotateZ() {
6410
+ return this.#rz;
6411
+ }
6412
+
6413
+ #parseValue(str) {
6414
+ const rxMatch = str.match(/rotateX\(\s*(-?[\d.]+)\s*deg\s*\)/);
6415
+ const ryMatch = str.match(/rotateY\(\s*(-?[\d.]+)\s*deg\s*\)/);
6416
+ const rzMatch = str.match(/rotateZ\(\s*(-?[\d.]+)\s*deg\s*\)/);
6417
+ if (rxMatch) this.#rx = parseFloat(rxMatch[1]);
6418
+ if (ryMatch) this.#ry = parseFloat(ryMatch[1]);
6419
+ if (rzMatch) this.#rz = parseFloat(rzMatch[1]);
6420
+ }
6421
+
6422
+ #render() {
6423
+ const axisLabels = { rotateX: "X", rotateY: "Y", rotateZ: "Z" };
6424
+ const axisValues = { rotateX: this.#rx, rotateY: this.#ry, rotateZ: this.#rz };
6425
+ const fieldsHTML = this.#fields
6426
+ .map(
6427
+ (axis) =>
6428
+ `<fig-input-number
6429
+ name="${axis}"
6430
+ step="1"
6431
+ precision="1"
6432
+ value="${axisValues[axis]}"
6433
+ units="°">
6434
+ <span slot="prepend">${axisLabels[axis]}</span>
6435
+ </fig-input-number>`,
6436
+ )
6437
+ .join("");
6438
+
6439
+ this.innerHTML = `<div class="fig-3d-rotate-container" tabindex="0">
6440
+ <div class="fig-3d-rotate-scene">
6441
+ <div class="fig-3d-rotate-cube">
6442
+ <div class="fig-3d-rotate-face front"></div>
6443
+ <div class="fig-3d-rotate-face back"></div>
6444
+ <div class="fig-3d-rotate-face right"></div>
6445
+ <div class="fig-3d-rotate-face left"></div>
6446
+ <div class="fig-3d-rotate-face top"></div>
6447
+ <div class="fig-3d-rotate-face bottom"></div>
6448
+ </div>
6449
+ </div>
6450
+ </div>${fieldsHTML}`;
6451
+ this.#container = this.querySelector(".fig-3d-rotate-container");
6452
+ this.#cube = this.querySelector(".fig-3d-rotate-cube");
6453
+ this.#fieldInputs = {};
6454
+ for (const axis of this.#fields) {
6455
+ const input = this.querySelector(`fig-input-number[name="${axis}"]`);
6456
+ if (input) {
6457
+ this.#fieldInputs[axis] = input;
6458
+ const handleFieldValue = (e) => {
6459
+ e.stopPropagation();
6460
+ const val = parseFloat(e.target.value);
6461
+ if (isNaN(val)) return;
6462
+ if (axis === "rotateX") this.#rx = val;
6463
+ else if (axis === "rotateY") this.#ry = val;
6464
+ else if (axis === "rotateZ") this.#rz = val;
6465
+ this.#updateCube();
6466
+ this.#emit(e.type);
6467
+ };
6468
+ input.addEventListener("input", handleFieldValue);
6469
+ input.addEventListener("change", handleFieldValue);
6470
+ }
6471
+ }
6472
+ this.#updateCube();
6473
+ this.#setupEvents();
6474
+ }
6475
+
6476
+ #syncFieldInputs() {
6477
+ const axisValues = { rotateX: this.#rx, rotateY: this.#ry, rotateZ: this.#rz };
6478
+ for (const axis of this.#fields) {
6479
+ const input = this.#fieldInputs[axis];
6480
+ if (input) input.setAttribute("value", axisValues[axis].toFixed(this.#precision));
6481
+ }
6482
+ }
6483
+
6484
+ #updateCube() {
6485
+ if (!this.#cube) return;
6486
+ this.#cube.style.transform =
6487
+ `rotateX(${this.#rx}deg) rotateY(${this.#ry}deg) rotateZ(${this.#rz}deg)`;
6488
+ }
6489
+
6490
+ #emit(type) {
6491
+ this.dispatchEvent(
6492
+ new CustomEvent(type, {
6493
+ bubbles: true,
6494
+ detail: {
6495
+ value: this.value,
6496
+ rotateX: this.#rx,
6497
+ rotateY: this.#ry,
6498
+ rotateZ: this.#rz,
6499
+ },
6500
+ }),
6501
+ );
6502
+ }
6503
+
6504
+ #snapToIncrement(angle) {
6505
+ if (!this.#isShiftHeld) return angle;
6506
+ return Math.round(angle / 15) * 15;
6507
+ }
6508
+
6509
+ #setupEvents() {
6510
+ this.#container.addEventListener("pointerdown", (e) => this.#startDrag(e));
6511
+ this.#boundKeyDown = (e) => {
6512
+ if (e.key === "Shift") this.#isShiftHeld = true;
6513
+ };
6514
+ this.#boundKeyUp = (e) => {
6515
+ if (e.key === "Shift") this.#isShiftHeld = false;
6516
+ };
6517
+ window.addEventListener("keydown", this.#boundKeyDown);
6518
+ window.addEventListener("keyup", this.#boundKeyUp);
6519
+ }
6520
+
6521
+ #startDrag(e) {
6522
+ e.preventDefault();
6523
+ this.#isDragging = true;
6524
+ this.#container.classList.add("dragging");
6525
+
6526
+ const startX = e.clientX;
6527
+ const startY = e.clientY;
6528
+ const startRx = this.#rx;
6529
+ const startRy = this.#ry;
6530
+
6531
+ const onMove = (e) => {
6532
+ if (!this.#isDragging) return;
6533
+ const dx = e.clientX - startX;
6534
+ const dy = e.clientY - startY;
6535
+ this.#ry = this.#snapToIncrement(startRy + dx * 0.5);
6536
+ this.#rx = this.#snapToIncrement(startRx - dy * 0.5);
6537
+ this.#updateCube();
6538
+ this.#syncFieldInputs();
6539
+ this.#emit("input");
6540
+ };
6541
+
6542
+ const onUp = () => {
6543
+ this.#isDragging = false;
6544
+ this.#container.classList.remove("dragging");
6545
+ document.removeEventListener("pointermove", onMove);
6546
+ document.removeEventListener("pointerup", onUp);
6547
+ this.#emit("change");
6548
+ };
6549
+
6550
+ document.addEventListener("pointermove", onMove);
6551
+ document.addEventListener("pointerup", onUp);
6552
+ }
6553
+ }
6554
+ customElements.define("fig-3d-rotate", Fig3DRotate);
6555
+
5955
6556
  /**
5956
6557
  * A custom joystick input element.
5957
6558
  * @attr {string} value - The current position of the joystick (e.g., "0.5,0.5").
@@ -6045,7 +6646,7 @@ class FigInputJoystick extends HTMLElement {
6045
6646
  this.plane.addEventListener("mousedown", this.#handleMouseDown.bind(this));
6046
6647
  this.plane.addEventListener(
6047
6648
  "touchstart",
6048
- this.#handleTouchStart.bind(this)
6649
+ this.#handleTouchStart.bind(this),
6049
6650
  );
6050
6651
  window.addEventListener("keydown", this.#handleKeyDown.bind(this));
6051
6652
  window.addEventListener("keyup", this.#handleKeyUp.bind(this));
@@ -6105,7 +6706,7 @@ class FigInputJoystick extends HTMLElement {
6105
6706
  let x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
6106
6707
  let screenY = Math.max(
6107
6708
  0,
6108
- Math.min(1, (e.clientY - rect.top) / rect.height)
6709
+ Math.min(1, (e.clientY - rect.top) / rect.height),
6109
6710
  );
6110
6711
 
6111
6712
  // Convert screen Y to internal Y (flip for math coordinates)
@@ -6133,7 +6734,7 @@ class FigInputJoystick extends HTMLElement {
6133
6734
  new CustomEvent("input", {
6134
6735
  bubbles: true,
6135
6736
  cancelable: true,
6136
- })
6737
+ }),
6137
6738
  );
6138
6739
  }
6139
6740
 
@@ -6142,7 +6743,7 @@ class FigInputJoystick extends HTMLElement {
6142
6743
  new CustomEvent("change", {
6143
6744
  bubbles: true,
6144
6745
  cancelable: true,
6145
- })
6746
+ }),
6146
6747
  );
6147
6748
  }
6148
6749
 
@@ -6317,8 +6918,7 @@ class FigInputAngle extends HTMLElement {
6317
6918
  this.max = this.hasAttribute("max")
6318
6919
  ? Number(this.getAttribute("max"))
6319
6920
  : null;
6320
- this.showRotations =
6321
- this.getAttribute("show-rotations") === "true";
6921
+ this.showRotations = this.getAttribute("show-rotations") === "true";
6322
6922
 
6323
6923
  this.#render();
6324
6924
  this.#setupListeners();
@@ -6327,7 +6927,7 @@ class FigInputAngle extends HTMLElement {
6327
6927
  if (this.text && this.angleInput) {
6328
6928
  this.angleInput.setAttribute(
6329
6929
  "value",
6330
- this.angle.toFixed(this.precision)
6930
+ this.angle.toFixed(this.precision),
6331
6931
  );
6332
6932
  }
6333
6933
  });
@@ -6452,14 +7052,14 @@ class FigInputAngle extends HTMLElement {
6452
7052
  this.plane.addEventListener("mousedown", this.#handleMouseDown.bind(this));
6453
7053
  this.plane.addEventListener(
6454
7054
  "touchstart",
6455
- this.#handleTouchStart.bind(this)
7055
+ this.#handleTouchStart.bind(this),
6456
7056
  );
6457
7057
  window.addEventListener("keydown", this.#handleKeyDown.bind(this));
6458
7058
  window.addEventListener("keyup", this.#handleKeyUp.bind(this));
6459
7059
  if (this.text && this.angleInput) {
6460
7060
  this.angleInput.addEventListener(
6461
7061
  "input",
6462
- this.#handleAngleInput.bind(this)
7062
+ this.#handleAngleInput.bind(this),
6463
7063
  );
6464
7064
  }
6465
7065
  // Capture-phase listener for unit suffix parsing
@@ -6577,7 +7177,7 @@ class FigInputAngle extends HTMLElement {
6577
7177
  new CustomEvent("input", {
6578
7178
  bubbles: true,
6579
7179
  cancelable: true,
6580
- })
7180
+ }),
6581
7181
  );
6582
7182
  }
6583
7183
 
@@ -6586,7 +7186,7 @@ class FigInputAngle extends HTMLElement {
6586
7186
  new CustomEvent("change", {
6587
7187
  bubbles: true,
6588
7188
  cancelable: true,
6589
- })
7189
+ }),
6590
7190
  );
6591
7191
  }
6592
7192
 
@@ -6669,7 +7269,15 @@ class FigInputAngle extends HTMLElement {
6669
7269
  // --- Attributes ---
6670
7270
 
6671
7271
  static get observedAttributes() {
6672
- return ["value", "precision", "text", "min", "max", "units", "show-rotations"];
7272
+ return [
7273
+ "value",
7274
+ "precision",
7275
+ "text",
7276
+ "min",
7277
+ "max",
7278
+ "units",
7279
+ "show-rotations",
7280
+ ];
6673
7281
  }
6674
7282
 
6675
7283
  get value() {
@@ -6852,7 +7460,7 @@ class FigLayer extends HTMLElement {
6852
7460
  new CustomEvent("openchange", {
6853
7461
  detail: { open: value },
6854
7462
  bubbles: true,
6855
- })
7463
+ }),
6856
7464
  );
6857
7465
  }
6858
7466
  }
@@ -6874,7 +7482,7 @@ class FigLayer extends HTMLElement {
6874
7482
  new CustomEvent("visibilitychange", {
6875
7483
  detail: { visible: value },
6876
7484
  bubbles: true,
6877
- })
7485
+ }),
6878
7486
  );
6879
7487
  }
6880
7488
  }
@@ -6888,7 +7496,7 @@ class FigLayer extends HTMLElement {
6888
7496
  new CustomEvent("openchange", {
6889
7497
  detail: { open: isOpen },
6890
7498
  bubbles: true,
6891
- })
7499
+ }),
6892
7500
  );
6893
7501
  }
6894
7502
 
@@ -6898,7 +7506,7 @@ class FigLayer extends HTMLElement {
6898
7506
  new CustomEvent("visibilitychange", {
6899
7507
  detail: { visible: isVisible },
6900
7508
  bubbles: true,
6901
- })
7509
+ }),
6902
7510
  );
6903
7511
  }
6904
7512
  }
@@ -6978,7 +7586,7 @@ class FigFillPicker extends HTMLElement {
6978
7586
 
6979
7587
  #setupTrigger() {
6980
7588
  const child = Array.from(this.children).find(
6981
- (el) => !el.getAttribute("slot")?.startsWith("mode-")
7589
+ (el) => !el.getAttribute("slot")?.startsWith("mode-"),
6982
7590
  );
6983
7591
 
6984
7592
  if (!child) {
@@ -7077,7 +7685,7 @@ class FigFillPicker extends HTMLElement {
7077
7685
  bg = `url(${this.#image.url})`;
7078
7686
  const sizing = this.#getBackgroundSizing(
7079
7687
  this.#image.scaleMode,
7080
- this.#image.scale
7688
+ this.#image.scale,
7081
7689
  );
7082
7690
  bgSize = sizing.size;
7083
7691
  bgPosition = sizing.position;
@@ -7090,7 +7698,7 @@ class FigFillPicker extends HTMLElement {
7090
7698
  bg = `url(${this.#video.url})`;
7091
7699
  const sizing = this.#getBackgroundSizing(
7092
7700
  this.#video.scaleMode,
7093
- this.#video.scale
7701
+ this.#video.scale,
7094
7702
  );
7095
7703
  bgSize = sizing.size;
7096
7704
  bgPosition = sizing.position;
@@ -7185,7 +7793,7 @@ class FigFillPicker extends HTMLElement {
7185
7793
  if (mode) {
7186
7794
  const requested = mode.split(",").map((m) => m.trim().toLowerCase());
7187
7795
  allowedModes = requested.filter(
7188
- (m) => builtinModes.includes(m) || this.#customSlots[m]
7796
+ (m) => builtinModes.includes(m) || this.#customSlots[m],
7189
7797
  );
7190
7798
  if (allowedModes.length === 0) allowedModes = [...builtinModes];
7191
7799
  } else {
@@ -7239,9 +7847,7 @@ class FigFillPicker extends HTMLElement {
7239
7847
 
7240
7848
  // Populate custom tab containers and emit modeready
7241
7849
  for (const [modeName, { element }] of Object.entries(this.#customSlots)) {
7242
- const container = this.#dialog.querySelector(
7243
- `[data-tab="${modeName}"]`
7244
- );
7850
+ const container = this.#dialog.querySelector(`[data-tab="${modeName}"]`);
7245
7851
  if (!container) continue;
7246
7852
 
7247
7853
  // Move children (not the element itself) for vanilla HTML usage
@@ -7254,7 +7860,7 @@ class FigFillPicker extends HTMLElement {
7254
7860
  new CustomEvent("modeready", {
7255
7861
  bubbles: true,
7256
7862
  detail: { mode: modeName, container },
7257
- })
7863
+ }),
7258
7864
  );
7259
7865
  }
7260
7866
 
@@ -7291,9 +7897,7 @@ class FigFillPicker extends HTMLElement {
7291
7897
  // Listen for input/change from custom tab content
7292
7898
  for (const modeName of Object.keys(this.#customSlots)) {
7293
7899
  if (builtinModes.includes(modeName)) continue;
7294
- const container = this.#dialog.querySelector(
7295
- `[data-tab="${modeName}"]`
7296
- );
7900
+ const container = this.#dialog.querySelector(`[data-tab="${modeName}"]`);
7297
7901
  if (!container) continue;
7298
7902
  container.addEventListener("input", (e) => {
7299
7903
  if (e.target === this) return;
@@ -7313,7 +7917,7 @@ class FigFillPicker extends HTMLElement {
7313
7917
  #switchTab(tabName) {
7314
7918
  // Only allow switching to modes that have a tab container in the dialog
7315
7919
  const tab = this.#dialog?.querySelector(
7316
- `.fig-fill-picker-tab[data-tab="${tabName}"]`
7920
+ `.fig-fill-picker-tab[data-tab="${tabName}"]`,
7317
7921
  );
7318
7922
  if (!tab) return;
7319
7923
 
@@ -7379,7 +7983,7 @@ class FigFillPicker extends HTMLElement {
7379
7983
  <div class="fig-fill-picker-inputs">
7380
7984
  <fig-button icon variant="ghost" class="fig-fill-picker-eyedropper" title="Pick color from screen"><span class="fig-mask-icon" style="--icon: var(--icon-eyedropper)"></span></fig-button>
7381
7985
  <fig-input-color class="fig-fill-picker-color-input" text="true" picker="false" value="${this.#hsvToHex(
7382
- this.#color
7986
+ this.#color,
7383
7987
  )}"></fig-input-color>
7384
7988
  </div>
7385
7989
  `;
@@ -7407,7 +8011,7 @@ class FigFillPicker extends HTMLElement {
7407
8011
  // Setup opacity slider
7408
8012
  if (showAlpha) {
7409
8013
  this.#opacitySlider = container.querySelector(
7410
- 'fig-slider[type="opacity"]'
8014
+ 'fig-slider[type="opacity"]',
7411
8015
  );
7412
8016
  this.#opacitySlider.addEventListener("input", (e) => {
7413
8017
  this.#color.a = parseFloat(e.target.value) / 100;
@@ -7500,13 +8104,13 @@ class FigFillPicker extends HTMLElement {
7500
8104
  if (!this.#colorAreaHandle || !this.#colorArea) return;
7501
8105
 
7502
8106
  const rect = this.#colorArea.getBoundingClientRect();
7503
-
8107
+
7504
8108
  // If the canvas isn't visible yet (0 dimensions), schedule a retry (max 5 attempts)
7505
8109
  if ((rect.width === 0 || rect.height === 0) && retryCount < 5) {
7506
8110
  requestAnimationFrame(() => this.#updateHandlePosition(retryCount + 1));
7507
8111
  return;
7508
8112
  }
7509
-
8113
+
7510
8114
  const x = (this.#color.s / 100) * rect.width;
7511
8115
  const y = ((100 - this.#color.v) / 100) * rect.height;
7512
8116
 
@@ -7514,7 +8118,7 @@ class FigFillPicker extends HTMLElement {
7514
8118
  this.#colorAreaHandle.style.top = `${y}px`;
7515
8119
  this.#colorAreaHandle.style.setProperty(
7516
8120
  "--picker-color",
7517
- this.#hsvToHex({ ...this.#color, a: 1 })
8121
+ this.#hsvToHex({ ...this.#color, a: 1 }),
7518
8122
  );
7519
8123
  }
7520
8124
 
@@ -7577,7 +8181,7 @@ class FigFillPicker extends HTMLElement {
7577
8181
  const hex = this.#hsvToHex(this.#color);
7578
8182
 
7579
8183
  const colorInput = this.#dialog.querySelector(
7580
- ".fig-fill-picker-color-input"
8184
+ ".fig-fill-picker-color-input",
7581
8185
  );
7582
8186
  if (colorInput) {
7583
8187
  colorInput.setAttribute("value", hex);
@@ -7646,7 +8250,7 @@ class FigFillPicker extends HTMLElement {
7646
8250
  #setupGradientEvents(container) {
7647
8251
  // Type dropdown
7648
8252
  const typeDropdown = container.querySelector(
7649
- ".fig-fill-picker-gradient-type"
8253
+ ".fig-fill-picker-gradient-type",
7650
8254
  );
7651
8255
  typeDropdown.addEventListener("change", (e) => {
7652
8256
  this.#gradient.type = e.target.value;
@@ -7657,7 +8261,7 @@ class FigFillPicker extends HTMLElement {
7657
8261
  // Angle input
7658
8262
  // Convert from fig-input-angle coordinates (0° = right) to CSS coordinates (0° = up)
7659
8263
  const angleInput = container.querySelector(
7660
- ".fig-fill-picker-gradient-angle"
8264
+ ".fig-fill-picker-gradient-angle",
7661
8265
  );
7662
8266
  angleInput.addEventListener("input", (e) => {
7663
8267
  const pickerAngle = parseFloat(e.target.value) || 0;
@@ -7716,10 +8320,10 @@ class FigFillPicker extends HTMLElement {
7716
8320
 
7717
8321
  // Show/hide angle vs center inputs
7718
8322
  const angleInput = container.querySelector(
7719
- ".fig-fill-picker-gradient-angle"
8323
+ ".fig-fill-picker-gradient-angle",
7720
8324
  );
7721
8325
  const centerInputs = container.querySelector(
7722
- ".fig-fill-picker-gradient-center"
8326
+ ".fig-fill-picker-gradient-center",
7723
8327
  );
7724
8328
 
7725
8329
  if (this.#gradient.type === "radial") {
@@ -7752,7 +8356,7 @@ class FigFillPicker extends HTMLElement {
7752
8356
  if (!this.#dialog) return;
7753
8357
 
7754
8358
  const list = this.#dialog.querySelector(
7755
- ".fig-fill-picker-gradient-stops-list"
8359
+ ".fig-fill-picker-gradient-stops-list",
7756
8360
  );
7757
8361
  if (!list) return;
7758
8362
 
@@ -7772,7 +8376,7 @@ class FigFillPicker extends HTMLElement {
7772
8376
  <span class="fig-mask-icon" style="--icon: var(--icon-minus)"></span>
7773
8377
  </fig-button>
7774
8378
  </div>
7775
- `
8379
+ `,
7776
8380
  )
7777
8381
  .join("");
7778
8382
 
@@ -7803,14 +8407,15 @@ class FigFillPicker extends HTMLElement {
7803
8407
  }
7804
8408
 
7805
8409
  stopColor.addEventListener("input", (e) => {
7806
- this.#gradient.stops[index].color =
7807
- e.target.hexOpaque || e.target.value;
7808
- const parsedAlpha = parseFloat(e.target.alpha);
7809
- this.#gradient.stops[index].opacity =
7810
- isNaN(parsedAlpha) ? 100 : parsedAlpha;
7811
- this.#updateGradientPreview();
7812
- this.#emitInput();
7813
- });
8410
+ this.#gradient.stops[index].color =
8411
+ e.target.hexOpaque || e.target.value;
8412
+ const parsedAlpha = parseFloat(e.target.alpha);
8413
+ this.#gradient.stops[index].opacity = isNaN(parsedAlpha)
8414
+ ? 100
8415
+ : parsedAlpha;
8416
+ this.#updateGradientPreview();
8417
+ this.#emitInput();
8418
+ });
7814
8419
 
7815
8420
  row
7816
8421
  .querySelector(".fig-fill-picker-stop-remove")
@@ -7883,7 +8488,7 @@ class FigFillPicker extends HTMLElement {
7883
8488
 
7884
8489
  #setupImageEvents(container) {
7885
8490
  const scaleModeDropdown = container.querySelector(
7886
- ".fig-fill-picker-scale-mode"
8491
+ ".fig-fill-picker-scale-mode",
7887
8492
  );
7888
8493
  const scaleInput = container.querySelector(".fig-fill-picker-scale");
7889
8494
  const uploadBtn = container.querySelector(".fig-fill-picker-upload");
@@ -7925,7 +8530,7 @@ class FigFillPicker extends HTMLElement {
7925
8530
 
7926
8531
  // Drag and drop
7927
8532
  const previewArea = container.querySelector(
7928
- ".fig-fill-picker-media-preview"
8533
+ ".fig-fill-picker-media-preview",
7929
8534
  );
7930
8535
  previewArea.addEventListener("dragover", (e) => {
7931
8536
  e.preventDefault();
@@ -8033,7 +8638,7 @@ class FigFillPicker extends HTMLElement {
8033
8638
 
8034
8639
  #setupVideoEvents(container) {
8035
8640
  const scaleModeDropdown = container.querySelector(
8036
- ".fig-fill-picker-scale-mode"
8641
+ ".fig-fill-picker-scale-mode",
8037
8642
  );
8038
8643
  const uploadBtn = container.querySelector(".fig-fill-picker-upload");
8039
8644
  const fileInput = container.querySelector('input[type="file"]');
@@ -8052,7 +8657,7 @@ class FigFillPicker extends HTMLElement {
8052
8657
 
8053
8658
  // Drag and drop
8054
8659
  const previewArea = container.querySelector(
8055
- ".fig-fill-picker-media-preview"
8660
+ ".fig-fill-picker-media-preview",
8056
8661
  );
8057
8662
 
8058
8663
  fileInput.addEventListener("change", (e) => {
@@ -8123,10 +8728,10 @@ class FigFillPicker extends HTMLElement {
8123
8728
  const video = container.querySelector(".fig-fill-picker-webcam-video");
8124
8729
  const status = container.querySelector(".fig-fill-picker-webcam-status");
8125
8730
  const captureBtn = container.querySelector(
8126
- ".fig-fill-picker-webcam-capture"
8731
+ ".fig-fill-picker-webcam-capture",
8127
8732
  );
8128
8733
  const cameraSelect = container.querySelector(
8129
- ".fig-fill-picker-camera-select"
8734
+ ".fig-fill-picker-camera-select",
8130
8735
  );
8131
8736
 
8132
8737
  const startWebcam = async (deviceId = null) => {
@@ -8139,9 +8744,8 @@ class FigFillPicker extends HTMLElement {
8139
8744
  this.#webcam.stream.getTracks().forEach((track) => track.stop());
8140
8745
  }
8141
8746
 
8142
- this.#webcam.stream = await navigator.mediaDevices.getUserMedia(
8143
- constraints
8144
- );
8747
+ this.#webcam.stream =
8748
+ await navigator.mediaDevices.getUserMedia(constraints);
8145
8749
  video.srcObject = this.#webcam.stream;
8146
8750
  video.style.display = "block";
8147
8751
  status.style.display = "none";
@@ -8157,7 +8761,7 @@ class FigFillPicker extends HTMLElement {
8157
8761
  (cam, i) =>
8158
8762
  `<option value="${cam.deviceId}">${
8159
8763
  cam.label || `Camera ${i + 1}`
8160
- }</option>`
8764
+ }</option>`,
8161
8765
  )
8162
8766
  .join("");
8163
8767
  }
@@ -8449,7 +9053,7 @@ class FigFillPicker extends HTMLElement {
8449
9053
  new CustomEvent("input", {
8450
9054
  bubbles: true,
8451
9055
  detail: this.value,
8452
- })
9056
+ }),
8453
9057
  );
8454
9058
  }
8455
9059
 
@@ -8458,7 +9062,7 @@ class FigFillPicker extends HTMLElement {
8458
9062
  new CustomEvent("change", {
8459
9063
  bubbles: true,
8460
9064
  detail: this.value,
8461
- })
9065
+ }),
8462
9066
  );
8463
9067
  }
8464
9068
 
@@ -8530,7 +9134,7 @@ class FigFillPicker extends HTMLElement {
8530
9134
  this.#opacitySlider.setAttribute("value", this.#color.a * 100);
8531
9135
  this.#opacitySlider.setAttribute(
8532
9136
  "color",
8533
- this.#hsvToHex(this.#color)
9137
+ this.#hsvToHex(this.#color),
8534
9138
  );
8535
9139
  }
8536
9140
  }