@rogieking/figui3 2.28.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.
@@ -1086,7 +1121,11 @@ class FigPopup extends HTMLDialogElement {
1086
1121
  super();
1087
1122
  this.#boundReposition = this.#queueReposition.bind(this);
1088
1123
  this.#boundScroll = (e) => {
1089
- if (this.open && !this.contains(e.target) && this.#shouldAutoReposition()) {
1124
+ if (
1125
+ this.open &&
1126
+ !this.contains(e.target) &&
1127
+ this.#shouldAutoReposition()
1128
+ ) {
1090
1129
  this.#positionPopup();
1091
1130
  }
1092
1131
  };
@@ -1097,7 +1136,18 @@ class FigPopup extends HTMLDialogElement {
1097
1136
  }
1098
1137
 
1099
1138
  static get observedAttributes() {
1100
- 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
+ ];
1101
1151
  }
1102
1152
 
1103
1153
  get open() {
@@ -1168,7 +1218,7 @@ class FigPopup extends HTMLDialogElement {
1168
1218
  document.removeEventListener(
1169
1219
  "pointerdown",
1170
1220
  this.#boundOutsidePointerDown,
1171
- true
1221
+ true,
1172
1222
  );
1173
1223
  if (this.#rafId !== null) {
1174
1224
  cancelAnimationFrame(this.#rafId);
@@ -1224,7 +1274,11 @@ class FigPopup extends HTMLDialogElement {
1224
1274
  }
1225
1275
 
1226
1276
  this.#setupObservers();
1227
- document.addEventListener("pointerdown", this.#boundOutsidePointerDown, true);
1277
+ document.addEventListener(
1278
+ "pointerdown",
1279
+ this.#boundOutsidePointerDown,
1280
+ true,
1281
+ );
1228
1282
  this.#wasDragged = false;
1229
1283
  this.#queueReposition();
1230
1284
  this.#isPopupActive = true;
@@ -1243,7 +1297,7 @@ class FigPopup extends HTMLDialogElement {
1243
1297
  document.removeEventListener(
1244
1298
  "pointerdown",
1245
1299
  this.#boundOutsidePointerDown,
1246
- true
1300
+ true,
1247
1301
  );
1248
1302
 
1249
1303
  if (super.open) {
@@ -1284,7 +1338,10 @@ class FigPopup extends HTMLDialogElement {
1284
1338
  }
1285
1339
 
1286
1340
  window.addEventListener("resize", this.#boundReposition);
1287
- window.addEventListener("scroll", this.#boundScroll, { capture: true, passive: true });
1341
+ window.addEventListener("scroll", this.#boundScroll, {
1342
+ capture: true,
1343
+ passive: true,
1344
+ });
1288
1345
  }
1289
1346
 
1290
1347
  #teardownObservers() {
@@ -1301,7 +1358,10 @@ class FigPopup extends HTMLDialogElement {
1301
1358
  this.#mutationObserver = null;
1302
1359
  }
1303
1360
  window.removeEventListener("resize", this.#boundReposition);
1304
- window.removeEventListener("scroll", this.#boundScroll, { capture: true, passive: true });
1361
+ window.removeEventListener("scroll", this.#boundScroll, {
1362
+ capture: true,
1363
+ passive: true,
1364
+ });
1305
1365
  }
1306
1366
 
1307
1367
  #handleOutsidePointerDown(event) {
@@ -1352,15 +1412,31 @@ class FigPopup extends HTMLDialogElement {
1352
1412
 
1353
1413
  #isInteractiveElement(element) {
1354
1414
  const interactiveSelectors = [
1355
- "input", "button", "select", "textarea", "a",
1356
- "label", "details", "summary",
1357
- '[contenteditable="true"]', "[tabindex]",
1415
+ "input",
1416
+ "button",
1417
+ "select",
1418
+ "textarea",
1419
+ "a",
1420
+ "label",
1421
+ "details",
1422
+ "summary",
1423
+ '[contenteditable="true"]',
1424
+ "[tabindex]",
1358
1425
  ];
1359
1426
 
1360
1427
  const nonInteractiveFigElements = [
1361
- "FIG-HEADER", "FIG-DIALOG", "FIG-POPUP", "FIG-FIELD",
1362
- "FIG-TOOLTIP", "FIG-CONTENT", "FIG-TABS", "FIG-TAB",
1363
- "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",
1364
1440
  ];
1365
1441
 
1366
1442
  const isInteractive = (el) =>
@@ -1560,17 +1636,49 @@ class FigPopup extends HTMLDialogElement {
1560
1636
 
1561
1637
  #parseViewportMargins() {
1562
1638
  const raw = (this.getAttribute("viewport-margin") || "8").trim();
1563
- 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));
1564
1643
  const d = 8;
1565
1644
  if (tokens.length === 0) return { top: d, right: d, bottom: d, left: d };
1566
- if (tokens.length === 1) return { top: tokens[0], right: tokens[0], bottom: tokens[0], left: tokens[0] };
1567
- if (tokens.length === 2) return { top: tokens[0], right: tokens[1], bottom: tokens[0], left: tokens[1] };
1568
- if (tokens.length === 3) return { top: tokens[0], right: tokens[1], bottom: tokens[2], left: tokens[1] };
1569
- 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
+ };
1570
1672
  }
1571
1673
 
1572
1674
  #getPlacementCandidates(vertical, horizontal, shorthand) {
1573
- 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
+ };
1574
1682
 
1575
1683
  if (shorthand) {
1576
1684
  const isHorizontal = shorthand === "left" || shorthand === "right";
@@ -1613,21 +1721,30 @@ class FigPopup extends HTMLDialogElement {
1613
1721
  ];
1614
1722
  }
1615
1723
 
1616
- #computeCoords(anchorRect, popupRect, vertical, horizontal, offset, shorthand) {
1724
+ #computeCoords(
1725
+ anchorRect,
1726
+ popupRect,
1727
+ vertical,
1728
+ horizontal,
1729
+ offset,
1730
+ shorthand,
1731
+ ) {
1617
1732
  let top;
1618
1733
  let left;
1619
1734
 
1620
1735
  if (shorthand === "left" || shorthand === "right") {
1621
- left = shorthand === "left"
1622
- ? anchorRect.left - popupRect.width - offset.xPx
1623
- : anchorRect.right + offset.xPx;
1736
+ left =
1737
+ shorthand === "left"
1738
+ ? anchorRect.left - popupRect.width - offset.xPx
1739
+ : anchorRect.right + offset.xPx;
1624
1740
  top = anchorRect.top;
1625
1741
  return { top, left };
1626
1742
  }
1627
1743
  if (shorthand === "top" || shorthand === "bottom") {
1628
- top = shorthand === "top"
1629
- ? anchorRect.top - popupRect.height - offset.yPx
1630
- : anchorRect.bottom + offset.yPx;
1744
+ top =
1745
+ shorthand === "top"
1746
+ ? anchorRect.top - popupRect.height - offset.yPx
1747
+ : anchorRect.bottom + offset.yPx;
1631
1748
  left = anchorRect.left;
1632
1749
  return { top, left };
1633
1750
  }
@@ -1734,8 +1851,14 @@ class FigPopup extends HTMLDialogElement {
1734
1851
  #clamp(coords, popupRect, m) {
1735
1852
  const minLeft = m.left;
1736
1853
  const minTop = m.top;
1737
- const maxLeft = Math.max(m.left, window.innerWidth - popupRect.width - m.right);
1738
- 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
+ );
1739
1862
 
1740
1863
  return {
1741
1864
  left: Math.min(maxLeft, Math.max(minLeft, coords.left)),
@@ -1755,8 +1878,11 @@ class FigPopup extends HTMLDialogElement {
1755
1878
  if (!anchor) {
1756
1879
  this.#updatePopoverBeak(null, popupRect, 0, 0, "top");
1757
1880
  const centered = {
1758
- left: (m.left + (window.innerWidth - m.right - m.left - popupRect.width) / 2),
1759
- 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,
1760
1886
  };
1761
1887
  const clamped = this.#clamp(centered, popupRect, m);
1762
1888
  this.style.left = `${clamped.left}px`;
@@ -1765,24 +1891,44 @@ class FigPopup extends HTMLDialogElement {
1765
1891
  }
1766
1892
 
1767
1893
  const anchorRect = anchor.getBoundingClientRect();
1768
- const candidates = this.#getPlacementCandidates(vertical, horizontal, shorthand);
1894
+ const candidates = this.#getPlacementCandidates(
1895
+ vertical,
1896
+ horizontal,
1897
+ shorthand,
1898
+ );
1769
1899
  let best = null;
1770
1900
  let bestSide = "top";
1771
1901
  let bestScore = Number.POSITIVE_INFINITY;
1772
1902
 
1773
1903
  for (const { v, h, s } of candidates) {
1774
- 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
+ );
1775
1912
  const placementSide = this.#getPlacementSide(v, h, s);
1776
1913
 
1777
1914
  if (s) {
1778
1915
  const clamped = this.#clamp(coords, popupRect, m);
1779
- const primaryFits = (s === "left" || s === "right")
1780
- ? (coords.left >= m.left && coords.left + popupRect.width <= window.innerWidth - m.right)
1781
- : (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;
1782
1922
  if (primaryFits) {
1783
1923
  this.style.left = `${clamped.left}px`;
1784
1924
  this.style.top = `${clamped.top}px`;
1785
- 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
+ );
1786
1932
  return;
1787
1933
  }
1788
1934
  const score = this.#overflowScore(coords, popupRect, m);
@@ -1795,7 +1941,13 @@ class FigPopup extends HTMLDialogElement {
1795
1941
  if (this.#fits(coords, popupRect, m)) {
1796
1942
  this.style.left = `${coords.left}px`;
1797
1943
  this.style.top = `${coords.top}px`;
1798
- 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
+ );
1799
1951
  return;
1800
1952
  }
1801
1953
  const score = this.#overflowScore(coords, popupRect, m);
@@ -1810,7 +1962,13 @@ class FigPopup extends HTMLDialogElement {
1810
1962
  const clamped = this.#clamp(best || { left: 0, top: 0 }, popupRect, m);
1811
1963
  this.style.left = `${clamped.left}px`;
1812
1964
  this.style.top = `${clamped.top}px`;
1813
- 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
+ );
1814
1972
  }
1815
1973
 
1816
1974
  #queueReposition() {
@@ -1830,69 +1988,6 @@ class FigPopup extends HTMLDialogElement {
1830
1988
  }
1831
1989
  customElements.define("fig-popup", FigPopup, { extends: "dialog" });
1832
1990
 
1833
- /**
1834
- * A popover element using the native Popover API.
1835
- * @attr {string} trigger-action - The trigger action: "click" (default) or "hover"
1836
- * @attr {number} delay - Delay in ms before showing on hover (default: 0)
1837
- */
1838
- class FigPopover2 extends HTMLElement {
1839
- #popover;
1840
- #trigger;
1841
- #id;
1842
- #delay;
1843
- #timeout;
1844
- #action;
1845
-
1846
- constructor() {
1847
- super();
1848
- }
1849
- connectedCallback() {
1850
- this.#popover = this.querySelector("[popover]");
1851
- this.#trigger = this;
1852
- this.#delay = Number(this.getAttribute("delay")) || 0;
1853
- this.#action = this.getAttribute("trigger-action") || "click";
1854
- this.#id = `tooltip-${figUniqueId()}`;
1855
- if (this.#popover) {
1856
- this.#popover.setAttribute("id", this.#id);
1857
- this.#popover.setAttribute("role", "tooltip");
1858
- this.#popover.setAttribute("popover", "manual");
1859
- this.#popover.style["position-anchor"] = `--${this.#id}`;
1860
-
1861
- this.#trigger.setAttribute("popovertarget", this.#id);
1862
- this.#trigger.setAttribute("popovertargetaction", "toggle");
1863
- this.#trigger.style["anchor-name"] = `--${this.#id}`;
1864
-
1865
- if (this.#action === "hover") {
1866
- this.#trigger.addEventListener("mouseover", this.handleOpen.bind(this));
1867
- this.#trigger.addEventListener("mouseout", this.handleClose.bind(this));
1868
- } else {
1869
- this.#trigger.addEventListener("click", this.handleToggle.bind(this));
1870
- }
1871
-
1872
- document.body.append(this.#popover);
1873
- }
1874
- }
1875
-
1876
- handleClose() {
1877
- clearTimeout(this.#timeout);
1878
- this.#popover.hidePopover();
1879
- }
1880
- handleToggle() {
1881
- if (this.#popover.matches(":popover-open")) {
1882
- this.handleClose();
1883
- } else {
1884
- this.handleOpen();
1885
- }
1886
- }
1887
- handleOpen() {
1888
- clearTimeout(this.#timeout);
1889
- this.#timeout = setTimeout(() => {
1890
- this.#popover.showPopover();
1891
- }, this.#delay);
1892
- }
1893
- }
1894
- customElements.define("fig-popover-2", FigPopover2);
1895
-
1896
1991
  /* Tabs */
1897
1992
  /**
1898
1993
  * A custom tab element for use within FigTabs.
@@ -2173,14 +2268,15 @@ class FigSegmentedControl extends HTMLElement {
2173
2268
  this.name = this.getAttribute("name") || "segmented-control";
2174
2269
  this.addEventListener("click", this.handleClick.bind(this));
2175
2270
  this.#applyDisabled(
2176
- this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false"
2271
+ this.hasAttribute("disabled") &&
2272
+ this.getAttribute("disabled") !== "false",
2177
2273
  );
2178
2274
 
2179
2275
  // Ensure at least one segment is selected (default to first)
2180
2276
  requestAnimationFrame(() => {
2181
2277
  const segments = this.querySelectorAll("fig-segment");
2182
2278
  const hasSelected = Array.from(segments).some((s) =>
2183
- s.hasAttribute("selected")
2279
+ s.hasAttribute("selected"),
2184
2280
  );
2185
2281
  if (!hasSelected && segments.length > 0) {
2186
2282
  this.selectedSegment = segments[0];
@@ -2364,13 +2460,17 @@ class FigSlider extends HTMLElement {
2364
2460
  this.input.addEventListener("input", this.#boundHandleInput);
2365
2461
  this.input.removeEventListener("change", this.#boundHandleChange);
2366
2462
  this.input.addEventListener("change", this.#boundHandleChange);
2367
- this.input.addEventListener("pointerdown", () => { this.#isInteracting = true; });
2368
- 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
+ });
2369
2469
 
2370
2470
  if (this.default) {
2371
2471
  this.style.setProperty(
2372
2472
  "--default",
2373
- this.#calculateNormal(this.default)
2473
+ this.#calculateNormal(this.default),
2374
2474
  );
2375
2475
  }
2376
2476
 
@@ -2380,7 +2480,7 @@ class FigSlider extends HTMLElement {
2380
2480
  this.inputContainer.append(this.datalist);
2381
2481
  this.datalist.setAttribute(
2382
2482
  "id",
2383
- this.datalist.getAttribute("id") || figUniqueId()
2483
+ this.datalist.getAttribute("id") || figUniqueId(),
2384
2484
  );
2385
2485
  this.input.setAttribute("list", this.datalist.getAttribute("id"));
2386
2486
  } else if (this.type === "stepper") {
@@ -2405,7 +2505,7 @@ class FigSlider extends HTMLElement {
2405
2505
  }
2406
2506
  if (this.datalist) {
2407
2507
  let defaultOption = this.datalist.querySelector(
2408
- `option[value='${this.default}']`
2508
+ `option[value='${this.default}']`,
2409
2509
  );
2410
2510
  if (defaultOption) {
2411
2511
  defaultOption.setAttribute("default", "true");
@@ -2414,19 +2514,19 @@ class FigSlider extends HTMLElement {
2414
2514
  if (this.figInputNumber) {
2415
2515
  this.figInputNumber.removeEventListener(
2416
2516
  "input",
2417
- this.#boundHandleTextInput
2517
+ this.#boundHandleTextInput,
2418
2518
  );
2419
2519
  this.figInputNumber.addEventListener(
2420
2520
  "input",
2421
- this.#boundHandleTextInput
2521
+ this.#boundHandleTextInput,
2422
2522
  );
2423
2523
  this.figInputNumber.removeEventListener(
2424
2524
  "change",
2425
- this.#boundHandleTextChange
2525
+ this.#boundHandleTextChange,
2426
2526
  );
2427
2527
  this.figInputNumber.addEventListener(
2428
2528
  "change",
2429
- this.#boundHandleTextChange
2529
+ this.#boundHandleTextChange,
2430
2530
  );
2431
2531
  }
2432
2532
 
@@ -2446,11 +2546,11 @@ class FigSlider extends HTMLElement {
2446
2546
  if (this.figInputNumber) {
2447
2547
  this.figInputNumber.removeEventListener(
2448
2548
  "input",
2449
- this.#boundHandleTextInput
2549
+ this.#boundHandleTextInput,
2450
2550
  );
2451
2551
  this.figInputNumber.removeEventListener(
2452
2552
  "change",
2453
- this.#boundHandleTextChange
2553
+ this.#boundHandleTextChange,
2454
2554
  );
2455
2555
  }
2456
2556
  }
@@ -2460,7 +2560,7 @@ class FigSlider extends HTMLElement {
2460
2560
  this.value = this.input.value = this.figInputNumber.value;
2461
2561
  this.#syncProperties();
2462
2562
  this.dispatchEvent(
2463
- new CustomEvent("input", { detail: this.value, bubbles: true })
2563
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
2464
2564
  );
2465
2565
  }
2466
2566
  }
@@ -2490,7 +2590,7 @@ class FigSlider extends HTMLElement {
2490
2590
  #handleInput() {
2491
2591
  this.#syncValue();
2492
2592
  this.dispatchEvent(
2493
- new CustomEvent("input", { detail: this.value, bubbles: true })
2593
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
2494
2594
  );
2495
2595
  }
2496
2596
 
@@ -2498,7 +2598,7 @@ class FigSlider extends HTMLElement {
2498
2598
  this.#isInteracting = false;
2499
2599
  this.#syncValue();
2500
2600
  this.dispatchEvent(
2501
- new CustomEvent("change", { detail: this.value, bubbles: true })
2601
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
2502
2602
  );
2503
2603
  }
2504
2604
 
@@ -2507,7 +2607,7 @@ class FigSlider extends HTMLElement {
2507
2607
  this.value = this.input.value = this.figInputNumber.value;
2508
2608
  this.#syncProperties();
2509
2609
  this.dispatchEvent(
2510
- new CustomEvent("change", { detail: this.value, bubbles: true })
2610
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
2511
2611
  );
2512
2612
  }
2513
2613
  }
@@ -2727,10 +2827,10 @@ class FigInputText extends HTMLElement {
2727
2827
  this.value = value;
2728
2828
  this.input.value = valueTransformed;
2729
2829
  this.dispatchEvent(
2730
- new CustomEvent("input", { detail: this.value, bubbles: true })
2830
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
2731
2831
  );
2732
2832
  this.dispatchEvent(
2733
- new CustomEvent("change", { detail: this.value, bubbles: true })
2833
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
2734
2834
  );
2735
2835
  }
2736
2836
  #handleMouseMove(e) {
@@ -2778,13 +2878,13 @@ class FigInputText extends HTMLElement {
2778
2878
  if (typeof this.min === "number") {
2779
2879
  sanitized = Math.max(
2780
2880
  transform ? this.#transformNumber(this.min) : this.min,
2781
- sanitized
2881
+ sanitized,
2782
2882
  );
2783
2883
  }
2784
2884
  if (typeof this.max === "number") {
2785
2885
  sanitized = Math.min(
2786
2886
  transform ? this.#transformNumber(this.max) : this.max,
2787
- sanitized
2887
+ sanitized,
2788
2888
  );
2789
2889
  }
2790
2890
 
@@ -3104,7 +3204,7 @@ class FigInputNumber extends HTMLElement {
3104
3204
  e.target.value = "";
3105
3205
  }
3106
3206
  this.dispatchEvent(
3107
- new CustomEvent("change", { detail: this.value, bubbles: true })
3207
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
3108
3208
  );
3109
3209
  }
3110
3210
 
@@ -3130,10 +3230,10 @@ class FigInputNumber extends HTMLElement {
3130
3230
  this.input.value = this.#formatWithUnit(this.value);
3131
3231
 
3132
3232
  this.dispatchEvent(
3133
- new CustomEvent("input", { detail: this.value, bubbles: true })
3233
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
3134
3234
  );
3135
3235
  this.dispatchEvent(
3136
- new CustomEvent("change", { detail: this.value, bubbles: true })
3236
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
3137
3237
  );
3138
3238
  }
3139
3239
 
@@ -3145,7 +3245,7 @@ class FigInputNumber extends HTMLElement {
3145
3245
  this.value = "";
3146
3246
  }
3147
3247
  this.dispatchEvent(
3148
- new CustomEvent("input", { detail: this.value, bubbles: true })
3248
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
3149
3249
  );
3150
3250
  }
3151
3251
 
@@ -3162,10 +3262,10 @@ class FigInputNumber extends HTMLElement {
3162
3262
  e.target.value = "";
3163
3263
  }
3164
3264
  this.dispatchEvent(
3165
- new CustomEvent("input", { detail: this.value, bubbles: true })
3265
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
3166
3266
  );
3167
3267
  this.dispatchEvent(
3168
- new CustomEvent("change", { detail: this.value, bubbles: true })
3268
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
3169
3269
  );
3170
3270
  }
3171
3271
 
@@ -3224,7 +3324,9 @@ class FigInputNumber extends HTMLElement {
3224
3324
  const factor = Math.pow(10, precision);
3225
3325
  const rounded = Math.round(num * factor) / factor;
3226
3326
  // Only show decimals if needed and up to precision
3227
- return Number.isInteger(rounded) ? rounded : parseFloat(rounded.toFixed(precision));
3327
+ return Number.isInteger(rounded)
3328
+ ? rounded
3329
+ : parseFloat(rounded.toFixed(precision));
3228
3330
  }
3229
3331
 
3230
3332
  static get observedAttributes() {
@@ -3358,7 +3460,7 @@ class FigField extends HTMLElement {
3358
3460
  requestAnimationFrame(() => {
3359
3461
  this.label = this.querySelector(":scope>label");
3360
3462
  this.input = Array.from(this.childNodes).find((node) =>
3361
- node.nodeName.toLowerCase().startsWith("fig-")
3463
+ node.nodeName.toLowerCase().startsWith("fig-"),
3362
3464
  );
3363
3465
  if (this.input && this.label) {
3364
3466
  this.label.addEventListener("click", this.focus.bind(this));
@@ -3503,11 +3605,11 @@ class FigInputColor extends HTMLElement {
3503
3605
  }
3504
3606
  this.#fillPicker.addEventListener(
3505
3607
  "input",
3506
- this.#handleFillPickerInput.bind(this)
3608
+ this.#handleFillPickerInput.bind(this),
3507
3609
  );
3508
3610
  this.#fillPicker.addEventListener(
3509
3611
  "change",
3510
- this.#handleChange.bind(this)
3612
+ this.#handleChange.bind(this),
3511
3613
  );
3512
3614
  }
3513
3615
 
@@ -3520,22 +3622,22 @@ class FigInputColor extends HTMLElement {
3520
3622
  }
3521
3623
  this.#textInput.addEventListener(
3522
3624
  "input",
3523
- this.#handleTextInput.bind(this)
3625
+ this.#handleTextInput.bind(this),
3524
3626
  );
3525
3627
  this.#textInput.addEventListener(
3526
3628
  "change",
3527
- this.#handleChange.bind(this)
3629
+ this.#handleChange.bind(this),
3528
3630
  );
3529
3631
  }
3530
3632
 
3531
3633
  if (this.#alphaInput) {
3532
3634
  this.#alphaInput.addEventListener(
3533
3635
  "input",
3534
- this.#handleAlphaInput.bind(this)
3636
+ this.#handleAlphaInput.bind(this),
3535
3637
  );
3536
3638
  this.#alphaInput.addEventListener(
3537
3639
  "change",
3538
- this.#handleChange.bind(this)
3640
+ this.#handleChange.bind(this),
3539
3641
  );
3540
3642
  }
3541
3643
  });
@@ -3548,7 +3650,7 @@ class FigInputColor extends HTMLElement {
3548
3650
  g: isNaN(this.rgba.g) ? 0 : this.rgba.g,
3549
3651
  b: isNaN(this.rgba.b) ? 0 : this.rgba.b,
3550
3652
  },
3551
- this.rgba.a
3653
+ this.rgba.a,
3552
3654
  );
3553
3655
  this.hexWithAlpha = this.value.toUpperCase();
3554
3656
  this.hexOpaque = this.hexWithAlpha.slice(0, 7);
@@ -3591,7 +3693,7 @@ class FigInputColor extends HTMLElement {
3591
3693
  type: "solid",
3592
3694
  color: this.hexOpaque,
3593
3695
  opacity: this.alpha,
3594
- })
3696
+ }),
3595
3697
  );
3596
3698
  }
3597
3699
  this.#emitInputEvent();
@@ -3614,7 +3716,7 @@ class FigInputColor extends HTMLElement {
3614
3716
  // Display without # prefix
3615
3717
  this.#textInput.setAttribute(
3616
3718
  "value",
3617
- this.hexOpaque.slice(1).toUpperCase()
3719
+ this.hexOpaque.slice(1).toUpperCase(),
3618
3720
  );
3619
3721
  }
3620
3722
  this.#emitInputEvent();
@@ -3636,7 +3738,7 @@ class FigInputColor extends HTMLElement {
3636
3738
  if (this.#textInput) {
3637
3739
  this.#textInput.setAttribute(
3638
3740
  "value",
3639
- this.hexOpaque.slice(1).toUpperCase()
3741
+ this.hexOpaque.slice(1).toUpperCase(),
3640
3742
  );
3641
3743
  }
3642
3744
  if (this.#alphaInput && detail.alpha !== undefined) {
@@ -3694,7 +3796,7 @@ class FigInputColor extends HTMLElement {
3694
3796
  type: "solid",
3695
3797
  color: this.hexOpaque,
3696
3798
  opacity: this.alpha,
3697
- })
3799
+ }),
3698
3800
  );
3699
3801
  }
3700
3802
  if (this.#alphaInput) {
@@ -3761,7 +3863,7 @@ class FigInputColor extends HTMLElement {
3761
3863
  // Handle rgba colors
3762
3864
  else if (color.startsWith("rgba") || color.startsWith("rgb")) {
3763
3865
  let matches = color.match(
3764
- /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/
3866
+ /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/,
3765
3867
  );
3766
3868
  if (matches) {
3767
3869
  r = parseInt(matches[1]);
@@ -3773,7 +3875,7 @@ class FigInputColor extends HTMLElement {
3773
3875
  // Handle hsla colors
3774
3876
  else if (color.startsWith("hsla") || color.startsWith("hsl")) {
3775
3877
  let matches = color.match(
3776
- /hsla?\((\d+),\s*(\d+)%,\s*(\d+)%(?:,\s*(\d+(?:\.\d+)?))?\)/
3878
+ /hsla?\((\d+),\s*(\d+)%,\s*(\d+)%(?:,\s*(\d+(?:\.\d+)?))?\)/,
3777
3879
  );
3778
3880
  if (matches) {
3779
3881
  let h = parseInt(matches[1]) / 360;
@@ -3989,8 +4091,8 @@ class FigInputFill extends HTMLElement {
3989
4091
  this.innerHTML = `
3990
4092
  <div class="input-combo">
3991
4093
  <fig-fill-picker ${fpAttrs} value='${fillPickerValue}' ${
3992
- disabled ? "disabled" : ""
3993
- }></fig-fill-picker>
4094
+ disabled ? "disabled" : ""
4095
+ }></fig-fill-picker>
3994
4096
  ${controlsHtml}
3995
4097
  </div>`;
3996
4098
 
@@ -4127,13 +4229,13 @@ class FigInputFill extends HTMLElement {
4127
4229
  if (this.#hexInput) {
4128
4230
  this.#hexInput.setAttribute(
4129
4231
  "value",
4130
- this.#solid.color.slice(1).toUpperCase()
4232
+ this.#solid.color.slice(1).toUpperCase(),
4131
4233
  );
4132
4234
  }
4133
4235
  if (this.#opacityInput) {
4134
4236
  this.#opacityInput.setAttribute(
4135
4237
  "value",
4136
- Math.round(this.#solid.alpha * 100)
4238
+ Math.round(this.#solid.alpha * 100),
4137
4239
  );
4138
4240
  }
4139
4241
  break;
@@ -4141,7 +4243,7 @@ class FigInputFill extends HTMLElement {
4141
4243
  if (this.#opacityInput) {
4142
4244
  this.#opacityInput.setAttribute(
4143
4245
  "value",
4144
- this.#gradient.stops[0]?.opacity ?? 100
4246
+ this.#gradient.stops[0]?.opacity ?? 100,
4145
4247
  );
4146
4248
  }
4147
4249
  const label = this.querySelector(".fig-input-fill-label");
@@ -4157,7 +4259,7 @@ class FigInputFill extends HTMLElement {
4157
4259
  if (this.#opacityInput) {
4158
4260
  this.#opacityInput.setAttribute(
4159
4261
  "value",
4160
- Math.round((this.#image.opacity ?? 1) * 100)
4262
+ Math.round((this.#image.opacity ?? 1) * 100),
4161
4263
  );
4162
4264
  }
4163
4265
  break;
@@ -4165,7 +4267,7 @@ class FigInputFill extends HTMLElement {
4165
4267
  if (this.#opacityInput) {
4166
4268
  this.#opacityInput.setAttribute(
4167
4269
  "value",
4168
- Math.round((this.#video.opacity ?? 1) * 100)
4270
+ Math.round((this.#video.opacity ?? 1) * 100),
4169
4271
  );
4170
4272
  }
4171
4273
  break;
@@ -4173,7 +4275,7 @@ class FigInputFill extends HTMLElement {
4173
4275
  if (this.#opacityInput) {
4174
4276
  this.#opacityInput.setAttribute(
4175
4277
  "value",
4176
- Math.round((this.#webcam.opacity ?? 1) * 100)
4278
+ Math.round((this.#webcam.opacity ?? 1) * 100),
4177
4279
  );
4178
4280
  }
4179
4281
  break;
@@ -4377,7 +4479,7 @@ class FigInputFill extends HTMLElement {
4377
4479
  new CustomEvent("input", {
4378
4480
  bubbles: true,
4379
4481
  detail: this.value,
4380
- })
4482
+ }),
4381
4483
  );
4382
4484
  }
4383
4485
 
@@ -4386,7 +4488,7 @@ class FigInputFill extends HTMLElement {
4386
4488
  new CustomEvent("change", {
4387
4489
  bubbles: true,
4388
4490
  detail: this.value,
4389
- })
4491
+ }),
4390
4492
  );
4391
4493
  }
4392
4494
 
@@ -4626,14 +4728,14 @@ class FigCheckbox extends HTMLElement {
4626
4728
  bubbles: true,
4627
4729
  composed: true,
4628
4730
  detail: { checked: this.input.checked, value: this.input.value },
4629
- })
4731
+ }),
4630
4732
  );
4631
4733
  this.dispatchEvent(
4632
4734
  new CustomEvent("change", {
4633
4735
  bubbles: true,
4634
4736
  composed: true,
4635
4737
  detail: { checked: this.input.checked, value: this.input.value },
4636
- })
4738
+ }),
4637
4739
  );
4638
4740
  }
4639
4741
  }
@@ -4828,8 +4930,9 @@ class FigComboInput extends HTMLElement {
4828
4930
  }
4829
4931
  connectedCallback() {
4830
4932
  const customDropdown =
4831
- Array.from(this.children).find((child) => child.tagName === "FIG-DROPDOWN") ||
4832
- null;
4933
+ Array.from(this.children).find(
4934
+ (child) => child.tagName === "FIG-DROPDOWN",
4935
+ ) || null;
4833
4936
  this.#usesCustomDropdown = customDropdown !== null;
4834
4937
  if (customDropdown) {
4835
4938
  customDropdown.remove();
@@ -5151,7 +5254,10 @@ class FigImage extends HTMLElement {
5151
5254
  }
5152
5255
  disconnectedCallback() {
5153
5256
  this.fileInput?.removeEventListener("change", this.#boundHandleFileInput);
5154
- this.downloadButton?.removeEventListener("click", this.#boundHandleDownload);
5257
+ this.downloadButton?.removeEventListener(
5258
+ "click",
5259
+ this.#boundHandleDownload,
5260
+ );
5155
5261
  }
5156
5262
 
5157
5263
  #updateRefs() {
@@ -5160,13 +5266,22 @@ class FigImage extends HTMLElement {
5160
5266
  if (this.upload) {
5161
5267
  this.uploadButton = this.querySelector("fig-button[type='upload']");
5162
5268
  this.fileInput = this.uploadButton?.querySelector("input");
5163
- this.fileInput?.removeEventListener("change", this.#boundHandleFileInput);
5269
+ this.fileInput?.removeEventListener(
5270
+ "change",
5271
+ this.#boundHandleFileInput,
5272
+ );
5164
5273
  this.fileInput?.addEventListener("change", this.#boundHandleFileInput);
5165
5274
  }
5166
5275
  if (this.download) {
5167
5276
  this.downloadButton = this.querySelector("fig-button[type='download']");
5168
- this.downloadButton?.removeEventListener("click", this.#boundHandleDownload);
5169
- 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
+ );
5170
5285
  }
5171
5286
  });
5172
5287
  }
@@ -5188,7 +5303,7 @@ class FigImage extends HTMLElement {
5188
5303
  if (!ar || ar === "auto") {
5189
5304
  this.style.setProperty(
5190
5305
  "--aspect-ratio",
5191
- `${this.image.width}/${this.image.height}`
5306
+ `${this.image.width}/${this.image.height}`,
5192
5307
  );
5193
5308
  }
5194
5309
  this.dispatchEvent(
@@ -5199,7 +5314,7 @@ class FigImage extends HTMLElement {
5199
5314
  blob: this.blob,
5200
5315
  base64: this.base64,
5201
5316
  },
5202
- })
5317
+ }),
5203
5318
  );
5204
5319
  resolve();
5205
5320
 
@@ -5250,14 +5365,14 @@ class FigImage extends HTMLElement {
5250
5365
  blob: this.blob,
5251
5366
  base64: this.base64,
5252
5367
  },
5253
- })
5368
+ }),
5254
5369
  );
5255
5370
  //emit for change too
5256
5371
  this.dispatchEvent(
5257
5372
  new CustomEvent("change", {
5258
5373
  bubbles: true,
5259
5374
  cancelable: true,
5260
- })
5375
+ }),
5261
5376
  );
5262
5377
  this.setAttribute("src", this.blob);
5263
5378
  }
@@ -5278,7 +5393,7 @@ class FigImage extends HTMLElement {
5278
5393
  if (this.chit) {
5279
5394
  this.chit.setAttribute(
5280
5395
  "background",
5281
- this.#src ? `url(${this.#src})` : ""
5396
+ this.#src ? `url(${this.#src})` : "",
5282
5397
  );
5283
5398
  }
5284
5399
  if (this.#src) {
@@ -5331,6 +5446,8 @@ class FigEasingCurve extends HTMLElement {
5331
5446
  #line2 = null;
5332
5447
  #handle1 = null;
5333
5448
  #handle2 = null;
5449
+ #bezierEndpointStart = null;
5450
+ #bezierEndpointEnd = null;
5334
5451
  #dropdown = null;
5335
5452
  #presetName = null;
5336
5453
  #targetLine = null;
@@ -5340,29 +5457,86 @@ class FigEasingCurve extends HTMLElement {
5340
5457
  #bounds = null;
5341
5458
  #diagonal = null;
5342
5459
  #resizeObserver = null;
5460
+ #bezierHandleRadius = 3.625;
5461
+ #bezierEndpointRadius = 2;
5462
+ #springHandleRadius = 3.625;
5463
+ #durationBarWidth = 6;
5464
+ #durationBarHeight = 16;
5465
+ #durationBarRadius = 3;
5343
5466
 
5344
5467
  static PRESETS = [
5345
5468
  { group: null, name: "Linear", type: "bezier", value: [0, 0, 1, 1] },
5346
- { group: "Bezier", name: "Ease in", type: "bezier", value: [0.42, 0, 1, 1] },
5347
- { group: "Bezier", name: "Ease out", type: "bezier", value: [0, 0, 0.58, 1] },
5348
- { group: "Bezier", name: "Ease in and out", type: "bezier", value: [0.42, 0, 0.58, 1] },
5349
- { group: "Bezier", name: "Ease in back", type: "bezier", value: [0.6, -0.28, 0.735, 0.045] },
5350
- { group: "Bezier", name: "Ease out back", type: "bezier", value: [0.175, 0.885, 0.32, 1.275] },
5351
- { 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
+ },
5352
5505
  { group: "Bezier", name: "Custom bezier", type: "bezier", value: null },
5353
- { group: "Spring", name: "Gentle", type: "spring", spring: { stiffness: 120, damping: 14, mass: 1 } },
5354
- { group: "Spring", name: "Quick", type: "spring", spring: { stiffness: 380, damping: 20, mass: 1 } },
5355
- { group: "Spring", name: "Bouncy", type: "spring", spring: { stiffness: 250, damping: 8, mass: 1 } },
5356
- { 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
+ },
5357
5530
  { group: "Spring", name: "Custom spring", type: "spring", spring: null },
5358
5531
  ];
5359
5532
 
5360
5533
  static get observedAttributes() {
5361
- return ["value", "precision"];
5534
+ return ["value", "precision", "aspect-ratio"];
5362
5535
  }
5363
5536
 
5364
5537
  connectedCallback() {
5365
5538
  this.#precision = parseInt(this.getAttribute("precision") || "2");
5539
+ this.#syncAspectRatioVar(this.getAttribute("aspect-ratio"));
5366
5540
  const val = this.getAttribute("value");
5367
5541
  if (val) this.#parseValue(val);
5368
5542
  this.#presetName = this.#matchPreset();
@@ -5378,7 +5552,24 @@ class FigEasingCurve extends HTMLElement {
5378
5552
  }
5379
5553
  }
5380
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
+
5381
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
+
5382
5573
  if (!this.#svg) return;
5383
5574
  if (name === "value" && newValue) {
5384
5575
  const prevMode = this.#mode;
@@ -5414,7 +5605,8 @@ class FigEasingCurve extends HTMLElement {
5414
5605
  for (let i = 0; i < points.length; i += step) {
5415
5606
  vals.push(points[i].value.toFixed(3));
5416
5607
  }
5417
- 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));
5418
5610
  return `linear(${vals.join(", ")})`;
5419
5611
  }
5420
5612
  const p = this.#precision;
@@ -5430,7 +5622,9 @@ class FigEasingCurve extends HTMLElement {
5430
5622
  }
5431
5623
 
5432
5624
  #parseValue(str) {
5433
- 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
+ );
5434
5628
  if (springMatch) {
5435
5629
  this.#mode = "spring";
5436
5630
  this.#spring.stiffness = parseFloat(springMatch[1]);
@@ -5458,7 +5652,8 @@ class FigEasingCurve extends HTMLElement {
5458
5652
  Math.abs(this.#cp1.y - p.value[1]) < ep &&
5459
5653
  Math.abs(this.#cp2.x - p.value[2]) < ep &&
5460
5654
  Math.abs(this.#cp2.y - p.value[3]) < ep
5461
- ) return p.name;
5655
+ )
5656
+ return p.name;
5462
5657
  }
5463
5658
  return "Custom bezier";
5464
5659
  }
@@ -5468,7 +5663,8 @@ class FigEasingCurve extends HTMLElement {
5468
5663
  Math.abs(this.#spring.stiffness - p.spring.stiffness) < ep &&
5469
5664
  Math.abs(this.#spring.damping - p.spring.damping) < ep &&
5470
5665
  Math.abs(this.#spring.mass - p.spring.mass) < ep
5471
- ) return p.name;
5666
+ )
5667
+ return p.name;
5472
5668
  }
5473
5669
  return "Custom spring";
5474
5670
  }
@@ -5480,13 +5676,15 @@ class FigEasingCurve extends HTMLElement {
5480
5676
  const dt = 0.004;
5481
5677
  const maxTime = 5;
5482
5678
  const points = [];
5483
- let pos = 0, vel = 0;
5679
+ let pos = 0,
5680
+ vel = 0;
5484
5681
  for (let t = 0; t <= maxTime; t += dt) {
5485
5682
  const force = -stiffness * (pos - 1) - damping * vel;
5486
5683
  vel += (force / mass) * dt;
5487
5684
  pos += vel * dt;
5488
5685
  points.push({ t, value: pos });
5489
- 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;
5490
5688
  }
5491
5689
  return points;
5492
5690
  }
@@ -5496,7 +5694,8 @@ class FigEasingCurve extends HTMLElement {
5496
5694
  const dt = 0.004;
5497
5695
  const maxTime = 5;
5498
5696
  const pts = [];
5499
- let pos = 0, vel = 0;
5697
+ let pos = 0,
5698
+ vel = 0;
5500
5699
  for (let t = 0; t <= maxTime; t += dt) {
5501
5700
  const force = -stiffness * (pos - 1) - damping * vel;
5502
5701
  vel += (force / mass) * dt;
@@ -5519,21 +5718,64 @@ class FigEasingCurve extends HTMLElement {
5519
5718
  const y = pad + (1 - (pts[i].value - minVal) / range) * s;
5520
5719
  d += (i === 0 ? "M" : "L") + x.toFixed(1) + "," + y.toFixed(1);
5521
5720
  }
5522
- 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>`;
5523
5722
  }
5524
5723
 
5525
5724
  static curveIcon(cp1x, cp1y, cp2x, cp2y, size = 24) {
5526
- const pad = 6;
5527
- const s = size - pad * 2;
5528
- const x = (n) => pad + n * s;
5529
- const y = (n) => pad + (1 - n) * s;
5530
- const d = `M${x(0)},${y(0)} C${x(cp1x)},${y(cp1y)} ${x(cp2x)},${y(cp2y)} ${x(1)},${y(1)}`;
5531
- 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>`;
5532
5771
  }
5533
5772
 
5534
5773
  // --- Rendering ---
5535
5774
 
5536
5775
  #render() {
5776
+ this.classList.toggle("spring-mode", this.#mode === "spring");
5777
+ this.classList.toggle("bezier-mode", this.#mode !== "spring");
5778
+ this.#syncMetricsFromCSS();
5537
5779
  this.innerHTML = this.#getInnerHTML();
5538
5780
  this.#cacheRefs();
5539
5781
  this.#syncViewportSize();
@@ -5556,7 +5798,12 @@ class FigEasingCurve extends HTMLElement {
5556
5798
  const sp = p.spring || this.#spring;
5557
5799
  icon = FigEasingCurve.#springIcon(sp);
5558
5800
  } else {
5559
- 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
+ ];
5560
5807
  icon = FigEasingCurve.curveIcon(...v);
5561
5808
  }
5562
5809
  const selected = p.name === this.#presetName ? " selected" : "";
@@ -5578,8 +5825,8 @@ class FigEasingCurve extends HTMLElement {
5578
5825
  <line class="fig-easing-curve-target" x1="0" y1="${targetY}" x2="${size}" y2="${targetY}"/>
5579
5826
  <line class="fig-easing-curve-diagonal" x1="0" y1="${startY}" x2="0" y2="${startY}"/>
5580
5827
  <path class="fig-easing-curve-path"/>
5581
- <circle class="fig-easing-curve-handle" data-handle="bounce" r="5.25"/>
5582
- <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}"/>
5583
5830
  </svg></div>`;
5584
5831
  }
5585
5832
 
@@ -5589,18 +5836,60 @@ class FigEasingCurve extends HTMLElement {
5589
5836
  <line class="fig-easing-curve-arm" data-arm="1"/>
5590
5837
  <line class="fig-easing-curve-arm" data-arm="2"/>
5591
5838
  <path class="fig-easing-curve-path"/>
5592
- <circle class="fig-easing-curve-handle" data-handle="1" r="5.25"/>
5593
- <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}"/>
5594
5843
  </svg></div>`;
5595
5844
  }
5596
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
+
5597
5880
  #cacheRefs() {
5598
5881
  this.#svg = this.querySelector(".fig-easing-curve-svg");
5599
5882
  this.#curve = this.querySelector(".fig-easing-curve-path");
5600
5883
  this.#line1 = this.querySelector('[data-arm="1"]');
5601
5884
  this.#line2 = this.querySelector('[data-arm="2"]');
5602
- this.#handle1 = this.querySelector('[data-handle="1"]') || this.querySelector('[data-handle="bounce"]');
5603
- 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"]');
5604
5893
  this.#dropdown = this.querySelector(".fig-easing-curve-dropdown");
5605
5894
  this.#targetLine = this.querySelector(".fig-easing-curve-target");
5606
5895
  this.#bounds = this.querySelector(".fig-easing-curve-bounds");
@@ -5682,7 +5971,10 @@ class FigEasingCurve extends HTMLElement {
5682
5971
  const p2 = this.#toSVG(this.#cp2.x, this.#cp2.y);
5683
5972
  const p3 = this.#toSVG(1, 1);
5684
5973
 
5685
- 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
+ );
5686
5978
  this.#line1.setAttribute("x1", p0.x);
5687
5979
  this.#line1.setAttribute("y1", p0.y);
5688
5980
  this.#line1.setAttribute("x2", p1.x);
@@ -5695,6 +5987,14 @@ class FigEasingCurve extends HTMLElement {
5695
5987
  this.#handle1.setAttribute("cy", p1.y);
5696
5988
  this.#handle2.setAttribute("cx", p2.x);
5697
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
+ }
5698
5998
  }
5699
5999
 
5700
6000
  #updateSpringPaths() {
@@ -5709,12 +6009,17 @@ class FigEasingCurve extends HTMLElement {
5709
6009
  if (!points.length) return;
5710
6010
  const totalTime = points[points.length - 1].t || 1;
5711
6011
 
5712
- let minVal = 0, maxVal = 1;
6012
+ let minVal = 0,
6013
+ maxVal = 1;
5713
6014
  for (const p of points) {
5714
6015
  if (p.value < minVal) minVal = p.value;
5715
6016
  if (p.value > maxVal) maxVal = p.value;
5716
6017
  }
5717
- 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
+ );
5718
6023
  const valPad = 0;
5719
6024
  this.#springScale = {
5720
6025
  minVal: 1 - maxDistFromCenter - valPad,
@@ -5753,9 +6058,8 @@ class FigEasingCurve extends HTMLElement {
5753
6058
 
5754
6059
  // Duration handle: on the target line
5755
6060
  const targetPt = this.#springToSVG(durationNorm, 1);
5756
- this.#handle2.setAttribute("x", targetPt.x - 3);
5757
- this.#handle2.setAttribute("y", targetPt.y - 8);
5758
-
6061
+ this.#handle2.setAttribute("x", targetPt.x - this.#durationBarWidth / 2);
6062
+ this.#handle2.setAttribute("y", targetPt.y - this.#durationBarHeight / 2);
5759
6063
  }
5760
6064
 
5761
6065
  #findPeakOvershoot(points) {
@@ -5793,38 +6097,76 @@ class FigEasingCurve extends HTMLElement {
5793
6097
  this.#cp1.x,
5794
6098
  this.#cp1.y,
5795
6099
  this.#cp2.x,
5796
- this.#cp2.y
6100
+ this.#cp2.y,
5797
6101
  );
5798
6102
  const springIcon = FigEasingCurve.#springIcon(this.#spring);
5799
6103
 
5800
6104
  // Update both slotted options and the cloned native select options.
5801
6105
  this.#setOptionIconByValue(this.#dropdown, "Custom bezier", bezierIcon);
5802
6106
  this.#setOptionIconByValue(this.#dropdown, "Custom spring", springIcon);
5803
- this.#setOptionIconByValue(this.#dropdown.select, "Custom bezier", bezierIcon);
5804
- 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
+ );
5805
6117
  }
5806
6118
 
5807
6119
  // --- Events ---
5808
6120
 
5809
6121
  #emit(type) {
5810
- this.dispatchEvent(new CustomEvent(type, {
5811
- bubbles: true,
5812
- detail: {
5813
- mode: this.#mode,
5814
- value: this.value,
5815
- cssValue: this.cssValue,
5816
- preset: this.#presetName,
5817
- },
5818
- }));
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
+ );
5819
6133
  }
5820
6134
 
5821
6135
  #setupEvents() {
5822
6136
  if (this.#mode === "bezier") {
5823
- this.#handle1.addEventListener("pointerdown", (e) => this.#startBezierDrag(e, 1));
5824
- 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
+ }
5825
6152
  } else {
5826
- this.#handle1.addEventListener("pointerdown", (e) => this.#startSpringDrag(e, "bounce"));
5827
- 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
+ }
5828
6170
  }
5829
6171
 
5830
6172
  if (this.#dropdown) {
@@ -5875,6 +6217,11 @@ class FigEasingCurve extends HTMLElement {
5875
6217
  };
5876
6218
  }
5877
6219
 
6220
+ #bezierHandleForClientHalf(e) {
6221
+ const svgPt = this.#clientToSVG(e);
6222
+ return svgPt.x <= this.#drawWidth / 2 ? 1 : 2;
6223
+ }
6224
+
5878
6225
  #startBezierDrag(e, handle) {
5879
6226
  e.preventDefault();
5880
6227
  this.#isDragging = handle;
@@ -5927,11 +6274,20 @@ class FigEasingCurve extends HTMLElement {
5927
6274
 
5928
6275
  if (handleType === "bounce") {
5929
6276
  const dy = e.clientY - startY;
5930
- 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
+ );
5931
6281
  } else {
5932
6282
  const dx = e.clientX - startX;
5933
- this.#springDuration = Math.max(0.05, Math.min(0.95, startDuration + dx / 200));
5934
- 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
+ );
5935
6291
  }
5936
6292
 
5937
6293
  this.#updatePaths();
@@ -5953,6 +6309,250 @@ class FigEasingCurve extends HTMLElement {
5953
6309
  }
5954
6310
  customElements.define("fig-easing-curve", FigEasingCurve);
5955
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
+
5956
6556
  /**
5957
6557
  * A custom joystick input element.
5958
6558
  * @attr {string} value - The current position of the joystick (e.g., "0.5,0.5").
@@ -6046,7 +6646,7 @@ class FigInputJoystick extends HTMLElement {
6046
6646
  this.plane.addEventListener("mousedown", this.#handleMouseDown.bind(this));
6047
6647
  this.plane.addEventListener(
6048
6648
  "touchstart",
6049
- this.#handleTouchStart.bind(this)
6649
+ this.#handleTouchStart.bind(this),
6050
6650
  );
6051
6651
  window.addEventListener("keydown", this.#handleKeyDown.bind(this));
6052
6652
  window.addEventListener("keyup", this.#handleKeyUp.bind(this));
@@ -6106,7 +6706,7 @@ class FigInputJoystick extends HTMLElement {
6106
6706
  let x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
6107
6707
  let screenY = Math.max(
6108
6708
  0,
6109
- Math.min(1, (e.clientY - rect.top) / rect.height)
6709
+ Math.min(1, (e.clientY - rect.top) / rect.height),
6110
6710
  );
6111
6711
 
6112
6712
  // Convert screen Y to internal Y (flip for math coordinates)
@@ -6134,7 +6734,7 @@ class FigInputJoystick extends HTMLElement {
6134
6734
  new CustomEvent("input", {
6135
6735
  bubbles: true,
6136
6736
  cancelable: true,
6137
- })
6737
+ }),
6138
6738
  );
6139
6739
  }
6140
6740
 
@@ -6143,7 +6743,7 @@ class FigInputJoystick extends HTMLElement {
6143
6743
  new CustomEvent("change", {
6144
6744
  bubbles: true,
6145
6745
  cancelable: true,
6146
- })
6746
+ }),
6147
6747
  );
6148
6748
  }
6149
6749
 
@@ -6318,8 +6918,7 @@ class FigInputAngle extends HTMLElement {
6318
6918
  this.max = this.hasAttribute("max")
6319
6919
  ? Number(this.getAttribute("max"))
6320
6920
  : null;
6321
- this.showRotations =
6322
- this.getAttribute("show-rotations") === "true";
6921
+ this.showRotations = this.getAttribute("show-rotations") === "true";
6323
6922
 
6324
6923
  this.#render();
6325
6924
  this.#setupListeners();
@@ -6328,7 +6927,7 @@ class FigInputAngle extends HTMLElement {
6328
6927
  if (this.text && this.angleInput) {
6329
6928
  this.angleInput.setAttribute(
6330
6929
  "value",
6331
- this.angle.toFixed(this.precision)
6930
+ this.angle.toFixed(this.precision),
6332
6931
  );
6333
6932
  }
6334
6933
  });
@@ -6453,14 +7052,14 @@ class FigInputAngle extends HTMLElement {
6453
7052
  this.plane.addEventListener("mousedown", this.#handleMouseDown.bind(this));
6454
7053
  this.plane.addEventListener(
6455
7054
  "touchstart",
6456
- this.#handleTouchStart.bind(this)
7055
+ this.#handleTouchStart.bind(this),
6457
7056
  );
6458
7057
  window.addEventListener("keydown", this.#handleKeyDown.bind(this));
6459
7058
  window.addEventListener("keyup", this.#handleKeyUp.bind(this));
6460
7059
  if (this.text && this.angleInput) {
6461
7060
  this.angleInput.addEventListener(
6462
7061
  "input",
6463
- this.#handleAngleInput.bind(this)
7062
+ this.#handleAngleInput.bind(this),
6464
7063
  );
6465
7064
  }
6466
7065
  // Capture-phase listener for unit suffix parsing
@@ -6578,7 +7177,7 @@ class FigInputAngle extends HTMLElement {
6578
7177
  new CustomEvent("input", {
6579
7178
  bubbles: true,
6580
7179
  cancelable: true,
6581
- })
7180
+ }),
6582
7181
  );
6583
7182
  }
6584
7183
 
@@ -6587,7 +7186,7 @@ class FigInputAngle extends HTMLElement {
6587
7186
  new CustomEvent("change", {
6588
7187
  bubbles: true,
6589
7188
  cancelable: true,
6590
- })
7189
+ }),
6591
7190
  );
6592
7191
  }
6593
7192
 
@@ -6670,7 +7269,15 @@ class FigInputAngle extends HTMLElement {
6670
7269
  // --- Attributes ---
6671
7270
 
6672
7271
  static get observedAttributes() {
6673
- 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
+ ];
6674
7281
  }
6675
7282
 
6676
7283
  get value() {
@@ -6853,7 +7460,7 @@ class FigLayer extends HTMLElement {
6853
7460
  new CustomEvent("openchange", {
6854
7461
  detail: { open: value },
6855
7462
  bubbles: true,
6856
- })
7463
+ }),
6857
7464
  );
6858
7465
  }
6859
7466
  }
@@ -6875,7 +7482,7 @@ class FigLayer extends HTMLElement {
6875
7482
  new CustomEvent("visibilitychange", {
6876
7483
  detail: { visible: value },
6877
7484
  bubbles: true,
6878
- })
7485
+ }),
6879
7486
  );
6880
7487
  }
6881
7488
  }
@@ -6889,7 +7496,7 @@ class FigLayer extends HTMLElement {
6889
7496
  new CustomEvent("openchange", {
6890
7497
  detail: { open: isOpen },
6891
7498
  bubbles: true,
6892
- })
7499
+ }),
6893
7500
  );
6894
7501
  }
6895
7502
 
@@ -6899,7 +7506,7 @@ class FigLayer extends HTMLElement {
6899
7506
  new CustomEvent("visibilitychange", {
6900
7507
  detail: { visible: isVisible },
6901
7508
  bubbles: true,
6902
- })
7509
+ }),
6903
7510
  );
6904
7511
  }
6905
7512
  }
@@ -6979,7 +7586,7 @@ class FigFillPicker extends HTMLElement {
6979
7586
 
6980
7587
  #setupTrigger() {
6981
7588
  const child = Array.from(this.children).find(
6982
- (el) => !el.getAttribute("slot")?.startsWith("mode-")
7589
+ (el) => !el.getAttribute("slot")?.startsWith("mode-"),
6983
7590
  );
6984
7591
 
6985
7592
  if (!child) {
@@ -7078,7 +7685,7 @@ class FigFillPicker extends HTMLElement {
7078
7685
  bg = `url(${this.#image.url})`;
7079
7686
  const sizing = this.#getBackgroundSizing(
7080
7687
  this.#image.scaleMode,
7081
- this.#image.scale
7688
+ this.#image.scale,
7082
7689
  );
7083
7690
  bgSize = sizing.size;
7084
7691
  bgPosition = sizing.position;
@@ -7091,7 +7698,7 @@ class FigFillPicker extends HTMLElement {
7091
7698
  bg = `url(${this.#video.url})`;
7092
7699
  const sizing = this.#getBackgroundSizing(
7093
7700
  this.#video.scaleMode,
7094
- this.#video.scale
7701
+ this.#video.scale,
7095
7702
  );
7096
7703
  bgSize = sizing.size;
7097
7704
  bgPosition = sizing.position;
@@ -7186,7 +7793,7 @@ class FigFillPicker extends HTMLElement {
7186
7793
  if (mode) {
7187
7794
  const requested = mode.split(",").map((m) => m.trim().toLowerCase());
7188
7795
  allowedModes = requested.filter(
7189
- (m) => builtinModes.includes(m) || this.#customSlots[m]
7796
+ (m) => builtinModes.includes(m) || this.#customSlots[m],
7190
7797
  );
7191
7798
  if (allowedModes.length === 0) allowedModes = [...builtinModes];
7192
7799
  } else {
@@ -7240,9 +7847,7 @@ class FigFillPicker extends HTMLElement {
7240
7847
 
7241
7848
  // Populate custom tab containers and emit modeready
7242
7849
  for (const [modeName, { element }] of Object.entries(this.#customSlots)) {
7243
- const container = this.#dialog.querySelector(
7244
- `[data-tab="${modeName}"]`
7245
- );
7850
+ const container = this.#dialog.querySelector(`[data-tab="${modeName}"]`);
7246
7851
  if (!container) continue;
7247
7852
 
7248
7853
  // Move children (not the element itself) for vanilla HTML usage
@@ -7255,7 +7860,7 @@ class FigFillPicker extends HTMLElement {
7255
7860
  new CustomEvent("modeready", {
7256
7861
  bubbles: true,
7257
7862
  detail: { mode: modeName, container },
7258
- })
7863
+ }),
7259
7864
  );
7260
7865
  }
7261
7866
 
@@ -7292,9 +7897,7 @@ class FigFillPicker extends HTMLElement {
7292
7897
  // Listen for input/change from custom tab content
7293
7898
  for (const modeName of Object.keys(this.#customSlots)) {
7294
7899
  if (builtinModes.includes(modeName)) continue;
7295
- const container = this.#dialog.querySelector(
7296
- `[data-tab="${modeName}"]`
7297
- );
7900
+ const container = this.#dialog.querySelector(`[data-tab="${modeName}"]`);
7298
7901
  if (!container) continue;
7299
7902
  container.addEventListener("input", (e) => {
7300
7903
  if (e.target === this) return;
@@ -7314,7 +7917,7 @@ class FigFillPicker extends HTMLElement {
7314
7917
  #switchTab(tabName) {
7315
7918
  // Only allow switching to modes that have a tab container in the dialog
7316
7919
  const tab = this.#dialog?.querySelector(
7317
- `.fig-fill-picker-tab[data-tab="${tabName}"]`
7920
+ `.fig-fill-picker-tab[data-tab="${tabName}"]`,
7318
7921
  );
7319
7922
  if (!tab) return;
7320
7923
 
@@ -7380,7 +7983,7 @@ class FigFillPicker extends HTMLElement {
7380
7983
  <div class="fig-fill-picker-inputs">
7381
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>
7382
7985
  <fig-input-color class="fig-fill-picker-color-input" text="true" picker="false" value="${this.#hsvToHex(
7383
- this.#color
7986
+ this.#color,
7384
7987
  )}"></fig-input-color>
7385
7988
  </div>
7386
7989
  `;
@@ -7408,7 +8011,7 @@ class FigFillPicker extends HTMLElement {
7408
8011
  // Setup opacity slider
7409
8012
  if (showAlpha) {
7410
8013
  this.#opacitySlider = container.querySelector(
7411
- 'fig-slider[type="opacity"]'
8014
+ 'fig-slider[type="opacity"]',
7412
8015
  );
7413
8016
  this.#opacitySlider.addEventListener("input", (e) => {
7414
8017
  this.#color.a = parseFloat(e.target.value) / 100;
@@ -7501,13 +8104,13 @@ class FigFillPicker extends HTMLElement {
7501
8104
  if (!this.#colorAreaHandle || !this.#colorArea) return;
7502
8105
 
7503
8106
  const rect = this.#colorArea.getBoundingClientRect();
7504
-
8107
+
7505
8108
  // If the canvas isn't visible yet (0 dimensions), schedule a retry (max 5 attempts)
7506
8109
  if ((rect.width === 0 || rect.height === 0) && retryCount < 5) {
7507
8110
  requestAnimationFrame(() => this.#updateHandlePosition(retryCount + 1));
7508
8111
  return;
7509
8112
  }
7510
-
8113
+
7511
8114
  const x = (this.#color.s / 100) * rect.width;
7512
8115
  const y = ((100 - this.#color.v) / 100) * rect.height;
7513
8116
 
@@ -7515,7 +8118,7 @@ class FigFillPicker extends HTMLElement {
7515
8118
  this.#colorAreaHandle.style.top = `${y}px`;
7516
8119
  this.#colorAreaHandle.style.setProperty(
7517
8120
  "--picker-color",
7518
- this.#hsvToHex({ ...this.#color, a: 1 })
8121
+ this.#hsvToHex({ ...this.#color, a: 1 }),
7519
8122
  );
7520
8123
  }
7521
8124
 
@@ -7578,7 +8181,7 @@ class FigFillPicker extends HTMLElement {
7578
8181
  const hex = this.#hsvToHex(this.#color);
7579
8182
 
7580
8183
  const colorInput = this.#dialog.querySelector(
7581
- ".fig-fill-picker-color-input"
8184
+ ".fig-fill-picker-color-input",
7582
8185
  );
7583
8186
  if (colorInput) {
7584
8187
  colorInput.setAttribute("value", hex);
@@ -7647,7 +8250,7 @@ class FigFillPicker extends HTMLElement {
7647
8250
  #setupGradientEvents(container) {
7648
8251
  // Type dropdown
7649
8252
  const typeDropdown = container.querySelector(
7650
- ".fig-fill-picker-gradient-type"
8253
+ ".fig-fill-picker-gradient-type",
7651
8254
  );
7652
8255
  typeDropdown.addEventListener("change", (e) => {
7653
8256
  this.#gradient.type = e.target.value;
@@ -7658,7 +8261,7 @@ class FigFillPicker extends HTMLElement {
7658
8261
  // Angle input
7659
8262
  // Convert from fig-input-angle coordinates (0° = right) to CSS coordinates (0° = up)
7660
8263
  const angleInput = container.querySelector(
7661
- ".fig-fill-picker-gradient-angle"
8264
+ ".fig-fill-picker-gradient-angle",
7662
8265
  );
7663
8266
  angleInput.addEventListener("input", (e) => {
7664
8267
  const pickerAngle = parseFloat(e.target.value) || 0;
@@ -7717,10 +8320,10 @@ class FigFillPicker extends HTMLElement {
7717
8320
 
7718
8321
  // Show/hide angle vs center inputs
7719
8322
  const angleInput = container.querySelector(
7720
- ".fig-fill-picker-gradient-angle"
8323
+ ".fig-fill-picker-gradient-angle",
7721
8324
  );
7722
8325
  const centerInputs = container.querySelector(
7723
- ".fig-fill-picker-gradient-center"
8326
+ ".fig-fill-picker-gradient-center",
7724
8327
  );
7725
8328
 
7726
8329
  if (this.#gradient.type === "radial") {
@@ -7753,7 +8356,7 @@ class FigFillPicker extends HTMLElement {
7753
8356
  if (!this.#dialog) return;
7754
8357
 
7755
8358
  const list = this.#dialog.querySelector(
7756
- ".fig-fill-picker-gradient-stops-list"
8359
+ ".fig-fill-picker-gradient-stops-list",
7757
8360
  );
7758
8361
  if (!list) return;
7759
8362
 
@@ -7773,7 +8376,7 @@ class FigFillPicker extends HTMLElement {
7773
8376
  <span class="fig-mask-icon" style="--icon: var(--icon-minus)"></span>
7774
8377
  </fig-button>
7775
8378
  </div>
7776
- `
8379
+ `,
7777
8380
  )
7778
8381
  .join("");
7779
8382
 
@@ -7804,14 +8407,15 @@ class FigFillPicker extends HTMLElement {
7804
8407
  }
7805
8408
 
7806
8409
  stopColor.addEventListener("input", (e) => {
7807
- this.#gradient.stops[index].color =
7808
- e.target.hexOpaque || e.target.value;
7809
- const parsedAlpha = parseFloat(e.target.alpha);
7810
- this.#gradient.stops[index].opacity =
7811
- isNaN(parsedAlpha) ? 100 : parsedAlpha;
7812
- this.#updateGradientPreview();
7813
- this.#emitInput();
7814
- });
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
+ });
7815
8419
 
7816
8420
  row
7817
8421
  .querySelector(".fig-fill-picker-stop-remove")
@@ -7884,7 +8488,7 @@ class FigFillPicker extends HTMLElement {
7884
8488
 
7885
8489
  #setupImageEvents(container) {
7886
8490
  const scaleModeDropdown = container.querySelector(
7887
- ".fig-fill-picker-scale-mode"
8491
+ ".fig-fill-picker-scale-mode",
7888
8492
  );
7889
8493
  const scaleInput = container.querySelector(".fig-fill-picker-scale");
7890
8494
  const uploadBtn = container.querySelector(".fig-fill-picker-upload");
@@ -7926,7 +8530,7 @@ class FigFillPicker extends HTMLElement {
7926
8530
 
7927
8531
  // Drag and drop
7928
8532
  const previewArea = container.querySelector(
7929
- ".fig-fill-picker-media-preview"
8533
+ ".fig-fill-picker-media-preview",
7930
8534
  );
7931
8535
  previewArea.addEventListener("dragover", (e) => {
7932
8536
  e.preventDefault();
@@ -8034,7 +8638,7 @@ class FigFillPicker extends HTMLElement {
8034
8638
 
8035
8639
  #setupVideoEvents(container) {
8036
8640
  const scaleModeDropdown = container.querySelector(
8037
- ".fig-fill-picker-scale-mode"
8641
+ ".fig-fill-picker-scale-mode",
8038
8642
  );
8039
8643
  const uploadBtn = container.querySelector(".fig-fill-picker-upload");
8040
8644
  const fileInput = container.querySelector('input[type="file"]');
@@ -8053,7 +8657,7 @@ class FigFillPicker extends HTMLElement {
8053
8657
 
8054
8658
  // Drag and drop
8055
8659
  const previewArea = container.querySelector(
8056
- ".fig-fill-picker-media-preview"
8660
+ ".fig-fill-picker-media-preview",
8057
8661
  );
8058
8662
 
8059
8663
  fileInput.addEventListener("change", (e) => {
@@ -8124,10 +8728,10 @@ class FigFillPicker extends HTMLElement {
8124
8728
  const video = container.querySelector(".fig-fill-picker-webcam-video");
8125
8729
  const status = container.querySelector(".fig-fill-picker-webcam-status");
8126
8730
  const captureBtn = container.querySelector(
8127
- ".fig-fill-picker-webcam-capture"
8731
+ ".fig-fill-picker-webcam-capture",
8128
8732
  );
8129
8733
  const cameraSelect = container.querySelector(
8130
- ".fig-fill-picker-camera-select"
8734
+ ".fig-fill-picker-camera-select",
8131
8735
  );
8132
8736
 
8133
8737
  const startWebcam = async (deviceId = null) => {
@@ -8140,9 +8744,8 @@ class FigFillPicker extends HTMLElement {
8140
8744
  this.#webcam.stream.getTracks().forEach((track) => track.stop());
8141
8745
  }
8142
8746
 
8143
- this.#webcam.stream = await navigator.mediaDevices.getUserMedia(
8144
- constraints
8145
- );
8747
+ this.#webcam.stream =
8748
+ await navigator.mediaDevices.getUserMedia(constraints);
8146
8749
  video.srcObject = this.#webcam.stream;
8147
8750
  video.style.display = "block";
8148
8751
  status.style.display = "none";
@@ -8158,7 +8761,7 @@ class FigFillPicker extends HTMLElement {
8158
8761
  (cam, i) =>
8159
8762
  `<option value="${cam.deviceId}">${
8160
8763
  cam.label || `Camera ${i + 1}`
8161
- }</option>`
8764
+ }</option>`,
8162
8765
  )
8163
8766
  .join("");
8164
8767
  }
@@ -8450,7 +9053,7 @@ class FigFillPicker extends HTMLElement {
8450
9053
  new CustomEvent("input", {
8451
9054
  bubbles: true,
8452
9055
  detail: this.value,
8453
- })
9056
+ }),
8454
9057
  );
8455
9058
  }
8456
9059
 
@@ -8459,7 +9062,7 @@ class FigFillPicker extends HTMLElement {
8459
9062
  new CustomEvent("change", {
8460
9063
  bubbles: true,
8461
9064
  detail: this.value,
8462
- })
9065
+ }),
8463
9066
  );
8464
9067
  }
8465
9068
 
@@ -8531,7 +9134,7 @@ class FigFillPicker extends HTMLElement {
8531
9134
  this.#opacitySlider.setAttribute("value", this.#color.a * 100);
8532
9135
  this.#opacitySlider.setAttribute(
8533
9136
  "color",
8534
- this.#hsvToHex(this.#color)
9137
+ this.#hsvToHex(this.#color),
8535
9138
  );
8536
9139
  }
8537
9140
  }