@rogieking/figui3 3.15.0 → 3.17.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
@@ -75,24 +75,31 @@ function figUniqueId() {
75
75
  return Date.now().toString(36) + Math.random().toString(36).substring(2);
76
76
  }
77
77
 
78
- /**
79
- * Gets the highest z-index currently in use on the page
80
- * @returns {number} The highest z-index found, minimum of 1000
81
- */
78
+ let _figZCounter = 10000;
82
79
  function figGetHighestZIndex() {
83
- let highest = 1000; // Baseline minimum
80
+ return _figZCounter++;
81
+ }
84
82
 
85
- // Check all elements with inline z-index or computed z-index
86
- const elements = document.querySelectorAll("*");
87
- for (const el of elements) {
88
- const zIndex = parseInt(getComputedStyle(el).zIndex, 10);
89
- if (!isNaN(zIndex) && zIndex > highest) {
90
- highest = zIndex;
91
- }
83
+ function figSyncCssVar(el, prop, value) {
84
+ if (value && value.trim()) {
85
+ el.style.setProperty(prop, value.trim());
86
+ } else {
87
+ el.style.removeProperty(prop);
92
88
  }
89
+ }
93
90
 
94
- return highest;
91
+ let _figSharedCanvas = null;
92
+ let _figSharedCtx = null;
93
+ function figGetSharedCanvas(width = 1, height = 1) {
94
+ if (!_figSharedCanvas) {
95
+ _figSharedCanvas = document.createElement("canvas");
96
+ _figSharedCtx = _figSharedCanvas.getContext("2d");
97
+ }
98
+ if (_figSharedCanvas.width !== width) _figSharedCanvas.width = width;
99
+ if (_figSharedCanvas.height !== height) _figSharedCanvas.height = height;
100
+ return { canvas: _figSharedCanvas, ctx: _figSharedCtx };
95
101
  }
102
+
96
103
  /**
97
104
  * Checks if the browser supports the native popover API
98
105
  * @returns {boolean} True if popover is supported
@@ -253,6 +260,7 @@ class FigDropdown extends HTMLElement {
253
260
  set label(value) {
254
261
  this.#label = value;
255
262
  }
263
+ #boundSlotChange;
256
264
  constructor() {
257
265
  super();
258
266
  this.select = document.createElement("select");
@@ -260,6 +268,7 @@ class FigDropdown extends HTMLElement {
260
268
  this.attachShadow({ mode: "open" });
261
269
  this.#boundHandleSelectInput = this.#handleSelectInput.bind(this);
262
270
  this.#boundHandleSelectChange = this.#handleSelectChange.bind(this);
271
+ this.#boundSlotChange = this.slotChange.bind(this);
263
272
  }
264
273
 
265
274
  #supportsSelectedContent() {
@@ -342,7 +351,7 @@ class FigDropdown extends HTMLElement {
342
351
  this.appendChild(this.select);
343
352
  this.shadowRoot.appendChild(this.optionsSlot);
344
353
 
345
- this.optionsSlot.addEventListener("slotchange", this.slotChange.bind(this));
354
+ this.optionsSlot.addEventListener("slotchange", this.#boundSlotChange);
346
355
 
347
356
  this.#addEventListeners();
348
357
  }
@@ -478,6 +487,12 @@ class FigDropdown extends HTMLElement {
478
487
  this.select.setAttribute("aria-label", this.#label);
479
488
  }
480
489
  }
490
+
491
+ disconnectedCallback() {
492
+ this.optionsSlot.removeEventListener("slotchange", this.#boundSlotChange);
493
+ this.select.removeEventListener("input", this.#boundHandleSelectInput);
494
+ this.select.removeEventListener("change", this.#boundHandleSelectChange);
495
+ }
481
496
  }
482
497
 
483
498
  customElements.define("fig-dropdown", FigDropdown);
@@ -496,6 +511,12 @@ class FigTooltip extends HTMLElement {
496
511
 
497
512
  #boundHideOnChromeOpen;
498
513
  #boundHidePopupOutsideClick;
514
+ #boundShowDelayedPopup;
515
+ #boundHandlePointerLeave;
516
+ #boundHandleTouchStart;
517
+ #boundHandleTouchMove;
518
+ #boundHandleTouchEnd;
519
+ #boundHandleTouchCancel;
499
520
  #touchTimeout;
500
521
  #isTouching = false;
501
522
  #observer = null;
@@ -506,9 +527,14 @@ class FigTooltip extends HTMLElement {
506
527
  let delay = parseInt(this.getAttribute("delay"));
507
528
  this.delay = !isNaN(delay) ? delay : 500;
508
529
 
509
- // Bind methods that will be used as event listeners
510
530
  this.#boundHideOnChromeOpen = this.#hideOnChromeOpen.bind(this);
511
531
  this.#boundHidePopupOutsideClick = this.hidePopupOutsideClick.bind(this);
532
+ this.#boundShowDelayedPopup = this.showDelayedPopup.bind(this);
533
+ this.#boundHandlePointerLeave = this.#handlePointerLeave.bind(this);
534
+ this.#boundHandleTouchStart = this.#handleTouchStart.bind(this);
535
+ this.#boundHandleTouchMove = this.#handleTouchMove.bind(this);
536
+ this.#boundHandleTouchEnd = this.#handleTouchEnd.bind(this);
537
+ this.#boundHandleTouchCancel = this.#handleTouchCancel.bind(this);
512
538
  }
513
539
  connectedCallback() {
514
540
  this.setup();
@@ -517,16 +543,13 @@ class FigTooltip extends HTMLElement {
517
543
 
518
544
  disconnectedCallback() {
519
545
  this.destroy();
520
- // Remove global listeners
521
546
  document.removeEventListener(
522
547
  "mousedown",
523
548
  this.#boundHideOnChromeOpen,
524
549
  true,
525
550
  );
526
- // Disconnect mutation observer
527
551
  this.#stopObserving();
528
552
 
529
- // Remove click outside listener for click action
530
553
  if (this.action === "click") {
531
554
  document.body.removeEventListener(
532
555
  "click",
@@ -534,15 +557,17 @@ class FigTooltip extends HTMLElement {
534
557
  );
535
558
  }
536
559
 
537
- // Clean up touch-related timers and listeners
538
560
  clearTimeout(this.#touchTimeout);
539
561
  if (this.action === "hover") {
540
- this.removeEventListener("touchstart", this.#handleTouchStart);
541
- this.removeEventListener("touchmove", this.#handleTouchMove);
542
- this.removeEventListener("touchend", this.#handleTouchEnd);
543
- this.removeEventListener("touchcancel", this.#handleTouchCancel);
562
+ this.removeEventListener("pointerenter", this.#boundShowDelayedPopup);
563
+ this.removeEventListener("pointerleave", this.#boundHandlePointerLeave);
564
+ this.removeEventListener("touchstart", this.#boundHandleTouchStart);
565
+ this.removeEventListener("touchmove", this.#boundHandleTouchMove);
566
+ this.removeEventListener("touchend", this.#boundHandleTouchEnd);
567
+ this.removeEventListener("touchcancel", this.#boundHandleTouchCancel);
544
568
  } else if (this.action === "click") {
545
- this.removeEventListener("touchstart", this.showDelayedPopup);
569
+ this.removeEventListener("click", this.#boundShowDelayedPopup);
570
+ this.removeEventListener("touchstart", this.#boundShowDelayedPopup);
546
571
  }
547
572
  }
548
573
 
@@ -613,36 +638,29 @@ class FigTooltip extends HTMLElement {
613
638
  if (this.action === "manual") return;
614
639
  if (this.action === "hover") {
615
640
  if (!this.isTouchDevice()) {
616
- this.addEventListener("pointerenter", this.showDelayedPopup.bind(this));
617
- this.addEventListener(
618
- "pointerleave",
619
- this.#handlePointerLeave.bind(this),
620
- );
641
+ this.addEventListener("pointerenter", this.#boundShowDelayedPopup);
642
+ this.addEventListener("pointerleave", this.#boundHandlePointerLeave);
621
643
  }
622
- // Touch support for mobile hover simulation
623
- this.addEventListener("touchstart", this.#handleTouchStart.bind(this), {
644
+ this.addEventListener("touchstart", this.#boundHandleTouchStart, {
624
645
  passive: true,
625
646
  });
626
- this.addEventListener("touchmove", this.#handleTouchMove.bind(this), {
647
+ this.addEventListener("touchmove", this.#boundHandleTouchMove, {
627
648
  passive: true,
628
649
  });
629
- this.addEventListener("touchend", this.#handleTouchEnd.bind(this), {
650
+ this.addEventListener("touchend", this.#boundHandleTouchEnd, {
630
651
  passive: true,
631
652
  });
632
- this.addEventListener("touchcancel", this.#handleTouchCancel.bind(this), {
653
+ this.addEventListener("touchcancel", this.#boundHandleTouchCancel, {
633
654
  passive: true,
634
655
  });
635
656
  } else if (this.action === "click") {
636
- this.addEventListener("click", this.showDelayedPopup.bind(this));
657
+ this.addEventListener("click", this.#boundShowDelayedPopup);
637
658
  document.body.addEventListener("click", this.#boundHidePopupOutsideClick);
638
-
639
- // Touch support for better mobile responsiveness
640
- this.addEventListener("touchstart", this.showDelayedPopup.bind(this), {
659
+ this.addEventListener("touchstart", this.#boundShowDelayedPopup, {
641
660
  passive: true,
642
661
  });
643
662
  }
644
663
 
645
- // Add listener for chrome interactions
646
664
  document.addEventListener("mousedown", this.#boundHideOnChromeOpen, true);
647
665
  }
648
666
 
@@ -934,6 +952,7 @@ class FigDialog extends HTMLDialogElement {
934
952
  #boundPointerDown;
935
953
  #boundPointerMove;
936
954
  #boundPointerUp;
955
+ #boundClose;
937
956
  #offset = 16; // 1rem in pixels
938
957
  #positionInitialized = false;
939
958
  #dragThreshold = 3; // pixels before drag starts
@@ -943,6 +962,7 @@ class FigDialog extends HTMLDialogElement {
943
962
  this.#boundPointerDown = this.#handlePointerDown.bind(this);
944
963
  this.#boundPointerMove = this.#handlePointerMove.bind(this);
945
964
  this.#boundPointerUp = this.#handlePointerUp.bind(this);
965
+ this.#boundClose = this.close.bind(this);
946
966
  }
947
967
 
948
968
  connectedCallback() {
@@ -962,12 +982,15 @@ class FigDialog extends HTMLDialogElement {
962
982
 
963
983
  disconnectedCallback() {
964
984
  this.#removeDragListeners();
985
+ this.querySelectorAll("fig-button[close-dialog]").forEach((button) => {
986
+ button.removeEventListener("click", this.#boundClose);
987
+ });
965
988
  }
966
989
 
967
990
  #addCloseListeners() {
968
991
  this.querySelectorAll("fig-button[close-dialog]").forEach((button) => {
969
- button.removeEventListener("click", this.close);
970
- button.addEventListener("click", this.close.bind(this));
992
+ button.removeEventListener("click", this.#boundClose);
993
+ button.addEventListener("click", this.#boundClose);
971
994
  });
972
995
  }
973
996
 
@@ -2242,16 +2265,18 @@ figDefineCustomizedBuiltIn("fig-popup", FigPopup, { extends: "dialog" });
2242
2265
  */
2243
2266
  class FigTab extends HTMLElement {
2244
2267
  #selected;
2268
+ #boundHandleClick;
2245
2269
  constructor() {
2246
2270
  super();
2247
2271
  this.content = null;
2248
2272
  this.#selected = false;
2273
+ this.#boundHandleClick = this.handleClick.bind(this);
2249
2274
  }
2250
2275
  connectedCallback() {
2251
2276
  this.setAttribute("label", this.innerText);
2252
2277
  this.setAttribute("role", "tab");
2253
2278
  this.setAttribute("tabindex", "0");
2254
- this.addEventListener("click", this.handleClick.bind(this));
2279
+ this.addEventListener("click", this.#boundHandleClick);
2255
2280
 
2256
2281
  requestAnimationFrame(() => {
2257
2282
  if (typeof this.getAttribute("content") === "string") {
@@ -2276,7 +2301,7 @@ class FigTab extends HTMLElement {
2276
2301
  this.setAttribute("selected", value ? "true" : "false");
2277
2302
  }
2278
2303
  disconnectedCallback() {
2279
- this.removeEventListener("click", this.handleClick);
2304
+ this.removeEventListener("click", this.#boundHandleClick);
2280
2305
  }
2281
2306
  handleClick() {
2282
2307
  this.selected = true;
@@ -2305,8 +2330,12 @@ customElements.define("fig-tab", FigTab);
2305
2330
  * @attr {string} name - Identifier for the tabs group
2306
2331
  */
2307
2332
  class FigTabs extends HTMLElement {
2333
+ #boundHandleClick;
2334
+ #boundHandleKeyDown;
2308
2335
  constructor() {
2309
2336
  super();
2337
+ this.#boundHandleClick = this.handleClick.bind(this);
2338
+ this.#boundHandleKeyDown = this.#handleKeyDown.bind(this);
2310
2339
  }
2311
2340
 
2312
2341
  static get observedAttributes() {
@@ -2316,8 +2345,8 @@ class FigTabs extends HTMLElement {
2316
2345
  connectedCallback() {
2317
2346
  this.name = this.getAttribute("name") || "tabs";
2318
2347
  this.setAttribute("role", "tablist");
2319
- this.addEventListener("click", this.handleClick.bind(this));
2320
- this.addEventListener("keydown", this.#handleKeyDown.bind(this));
2348
+ this.addEventListener("click", this.#boundHandleClick);
2349
+ this.addEventListener("keydown", this.#boundHandleKeyDown);
2321
2350
  requestAnimationFrame(() => {
2322
2351
  const value = this.getAttribute("value");
2323
2352
  if (value) {
@@ -2345,8 +2374,8 @@ class FigTabs extends HTMLElement {
2345
2374
  }
2346
2375
 
2347
2376
  disconnectedCallback() {
2348
- this.removeEventListener("click", this.handleClick);
2349
- this.removeEventListener("keydown", this.#handleKeyDown);
2377
+ this.removeEventListener("click", this.#boundHandleClick);
2378
+ this.removeEventListener("keydown", this.#boundHandleKeyDown);
2350
2379
  }
2351
2380
 
2352
2381
  #handleKeyDown(event) {
@@ -2446,14 +2475,16 @@ customElements.define("fig-tabs", FigTabs);
2446
2475
  class FigSegment extends HTMLElement {
2447
2476
  #value;
2448
2477
  #selected;
2478
+ #boundHandleClick;
2449
2479
  constructor() {
2450
2480
  super();
2481
+ this.#boundHandleClick = this.handleClick.bind(this);
2451
2482
  }
2452
2483
  connectedCallback() {
2453
- this.addEventListener("click", this.handleClick.bind(this));
2484
+ this.addEventListener("click", this.#boundHandleClick);
2454
2485
  }
2455
2486
  disconnectedCallback() {
2456
- this.removeEventListener("click", this.handleClick);
2487
+ this.removeEventListener("click", this.#boundHandleClick);
2457
2488
  }
2458
2489
  handleClick() {
2459
2490
  const parentControl = this.closest("fig-segmented-control");
@@ -4519,6 +4550,8 @@ class FigInputColor extends HTMLElement {
4519
4550
  const hidePicker = this.picker === "false";
4520
4551
  const showAlpha = this.getAttribute("alpha") === "true";
4521
4552
  const fpAttrs = this.#buildFillPickerAttrs();
4553
+ const disabled = this.#disabled;
4554
+ const disabledAttr = disabled ? " disabled" : "";
4522
4555
 
4523
4556
  let html = ``;
4524
4557
  const showText = this.getAttribute("text") === "true";
@@ -4526,7 +4559,7 @@ class FigInputColor extends HTMLElement {
4526
4559
  let label = `<fig-input-text
4527
4560
  type="text"
4528
4561
  placeholder="000000"
4529
- value="${this.hexOpaque.slice(1).toUpperCase()}">
4562
+ value="${this.hexOpaque.slice(1).toUpperCase()}"${disabledAttr}>
4530
4563
  </fig-input-text>`;
4531
4564
  if (showAlpha) {
4532
4565
  label += `<fig-tooltip text="Opacity">
@@ -4535,7 +4568,7 @@ class FigInputColor extends HTMLElement {
4535
4568
  min="0"
4536
4569
  max="100"
4537
4570
  value="${this.#alphaPercent}"
4538
- units="%">
4571
+ units="%"${disabledAttr}>
4539
4572
  </fig-input-number>
4540
4573
  </fig-tooltip>`;
4541
4574
  }
@@ -4547,8 +4580,8 @@ class FigInputColor extends HTMLElement {
4547
4580
  showAlpha ? "" : 'alpha="false"'
4548
4581
  } value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
4549
4582
  this.#alphaPercent
4550
- }}'></fig-fill-picker>`
4551
- : `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"></fig-chit>`;
4583
+ }}'${disabledAttr}></fig-fill-picker>`
4584
+ : `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"${disabledAttr}></fig-chit>`;
4552
4585
  }
4553
4586
 
4554
4587
  html = `<div class="input-combo">
@@ -4564,8 +4597,8 @@ class FigInputColor extends HTMLElement {
4564
4597
  showAlpha ? "" : 'alpha="false"'
4565
4598
  } value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
4566
4599
  this.#alphaPercent
4567
- }}'></fig-fill-picker>`
4568
- : `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"></fig-chit>`;
4600
+ }}'${disabledAttr}></fig-fill-picker>`
4601
+ : `<fig-chit background="${this.hexOpaque}" alpha="${this.rgba.a}"${disabledAttr}></fig-chit>`;
4569
4602
  }
4570
4603
  }
4571
4604
  this.innerHTML = html;
@@ -4759,7 +4792,7 @@ class FigInputColor extends HTMLElement {
4759
4792
  }
4760
4793
 
4761
4794
  static get observedAttributes() {
4762
- return ["value", "style", "mode", "picker", "experimental", "alpha", "text"];
4795
+ return ["value", "style", "mode", "picker", "experimental", "alpha", "text", "disabled"];
4763
4796
  }
4764
4797
 
4765
4798
  get mode() {
@@ -4813,6 +4846,26 @@ class FigInputColor extends HTMLElement {
4813
4846
  case "text":
4814
4847
  if (this.isConnected) this.#buildUI();
4815
4848
  break;
4849
+ case "disabled":
4850
+ this.#syncDisabled();
4851
+ break;
4852
+ }
4853
+ }
4854
+
4855
+ get #disabled() {
4856
+ return this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
4857
+ }
4858
+
4859
+ #syncDisabled() {
4860
+ const disabled = this.#disabled;
4861
+ for (const child of [this.#swatch, this.#textInput, this.#alphaInput]) {
4862
+ if (!child) continue;
4863
+ if (disabled) child.setAttribute("disabled", "");
4864
+ else child.removeAttribute("disabled");
4865
+ }
4866
+ if (this.#fillPicker) {
4867
+ if (disabled) this.#fillPicker.setAttribute("disabled", "");
4868
+ else this.#fillPicker.removeAttribute("disabled");
4816
4869
  }
4817
4870
  }
4818
4871
 
@@ -5062,6 +5115,12 @@ class FigInputFill extends HTMLElement {
5062
5115
  this.#render();
5063
5116
  }
5064
5117
 
5118
+ disconnectedCallback() {
5119
+ this.#fillPicker = null;
5120
+ this.#opacityInput = null;
5121
+ this.#hexInput = null;
5122
+ }
5123
+
5065
5124
  #parseValue() {
5066
5125
  const valueAttr = this.getAttribute("value");
5067
5126
  if (!valueAttr) return;
@@ -5132,6 +5191,15 @@ class FigInputFill extends HTMLElement {
5132
5191
  .join(" ");
5133
5192
  }
5134
5193
 
5194
+ #syncDisabled() {
5195
+ const disabled = this.hasAttribute("disabled");
5196
+ for (const child of [this.#fillPicker, this.#opacityInput, this.#hexInput]) {
5197
+ if (!child) continue;
5198
+ if (disabled) child.setAttribute("disabled", "");
5199
+ else child.removeAttribute("disabled");
5200
+ }
5201
+ }
5202
+
5135
5203
  #render() {
5136
5204
  const disabled = this.hasAttribute("disabled");
5137
5205
  const fillPickerValue = JSON.stringify(this.value);
@@ -5664,10 +5732,7 @@ class FigInputFill extends HTMLElement {
5664
5732
  }
5665
5733
  break;
5666
5734
  case "disabled":
5667
- // Re-render to update disabled state
5668
- if (this.#fillPicker) {
5669
- this.#render();
5670
- }
5735
+ this.#syncDisabled();
5671
5736
  break;
5672
5737
  case "mode":
5673
5738
  case "experimental":
@@ -5698,17 +5763,22 @@ customElements.define("fig-input-fill", FigInputFill);
5698
5763
  */
5699
5764
  class FigInputPalette extends HTMLElement {
5700
5765
  #colors = [];
5701
- #pickers = [];
5766
+ #inlinePickers = [];
5767
+ #expandedPickers = [];
5702
5768
  #renderRAF = null;
5703
5769
 
5704
5770
  static get observedAttributes() {
5705
- return ["value", "disabled", "min", "max", "expanded"];
5771
+ return ["value", "disabled", "min", "max", "expanded", "add"];
5706
5772
  }
5707
5773
 
5708
5774
  get #expanded() {
5709
5775
  return this.hasAttribute("expanded") && this.getAttribute("expanded") !== "false";
5710
5776
  }
5711
5777
 
5778
+ get #showAdd() {
5779
+ return !this.hasAttribute("add") || this.getAttribute("add") !== "false";
5780
+ }
5781
+
5712
5782
  get #min() {
5713
5783
  const v = parseInt(this.getAttribute("min"));
5714
5784
  return isNaN(v) ? 2 : v;
@@ -5733,7 +5803,8 @@ class FigInputPalette extends HTMLElement {
5733
5803
  cancelAnimationFrame(this.#renderRAF);
5734
5804
  this.#renderRAF = null;
5735
5805
  }
5736
- this.#pickers = [];
5806
+ this.#inlinePickers = [];
5807
+ this.#expandedPickers = [];
5737
5808
  }
5738
5809
 
5739
5810
  attributeChangedCallback(name, oldValue, newValue) {
@@ -5750,6 +5821,7 @@ class FigInputPalette extends HTMLElement {
5750
5821
  case "min":
5751
5822
  case "max":
5752
5823
  case "expanded":
5824
+ case "add":
5753
5825
  this.#render();
5754
5826
  break;
5755
5827
  }
@@ -5826,32 +5898,50 @@ class FigInputPalette extends HTMLElement {
5826
5898
  const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
5827
5899
 
5828
5900
  this.innerHTML = "";
5829
- this.#pickers = [];
5901
+ this.#inlinePickers = [];
5902
+ this.#expandedPickers = [];
5903
+
5904
+ const inlineWrap = document.createElement("div");
5905
+ inlineWrap.className = "palette-colors-inline";
5830
5906
 
5831
5907
  const wrap = document.createElement("div");
5832
5908
  wrap.className = "palette-colors";
5833
5909
  this.#colors.forEach((entry, i) => {
5834
- wrap.appendChild(this.#createPicker(entry, i, disabled));
5910
+ wrap.appendChild(this.#createPicker(entry, i, disabled, { inline: true }));
5835
5911
  });
5836
- this.appendChild(wrap);
5912
+ inlineWrap.appendChild(wrap);
5913
+
5914
+ if (this.#showAdd) this.#createAddButton(disabled, inlineWrap);
5915
+ this.appendChild(inlineWrap);
5837
5916
 
5838
- this.#createAddButton(disabled);
5917
+ const expandedWrap = document.createElement("div");
5918
+ expandedWrap.className = "palette-colors-expanded";
5919
+ this.#colors.forEach((entry, i) => {
5920
+ expandedWrap.appendChild(this.#createPicker(entry, i, disabled));
5921
+ });
5922
+ this.appendChild(expandedWrap);
5839
5923
  }
5840
5924
 
5841
- #createPicker(entry, index, disabled) {
5925
+ #createPicker(entry, index, disabled, { inline = false } = {}) {
5842
5926
  const hexAlpha = entry.alpha < 1
5843
5927
  ? entry.color + Math.round(entry.alpha * 255).toString(16).padStart(2, "0")
5844
5928
  : entry.color;
5845
- const expanded = this.#expanded;
5846
5929
  const ic = document.createElement("fig-input-color");
5847
5930
  ic.setAttribute("value", hexAlpha);
5848
- ic.setAttribute("text", expanded ? "true" : "false");
5849
5931
  ic.setAttribute("picker", "figma");
5850
- ic.setAttribute("alpha", "true");
5851
5932
  ic.setAttribute("picker-anchor", "self");
5852
- if (expanded) ic.setAttribute("full", "");
5933
+ if (inline) {
5934
+ ic.setAttribute("text", "false");
5935
+ ic.setAttribute("alpha", "true");
5936
+ } else {
5937
+ ic.setAttribute("text", "true");
5938
+ ic.setAttribute("alpha", "true");
5939
+ ic.setAttribute("full", "");
5940
+ }
5853
5941
  if (disabled) ic.setAttribute("disabled", "");
5854
5942
 
5943
+ const siblingList = inline ? this.#expandedPickers : this.#inlinePickers;
5944
+
5855
5945
  const updateFromPicker = (e) => {
5856
5946
  e.stopPropagation();
5857
5947
  const el = e.currentTarget;
@@ -5859,6 +5949,14 @@ class FigInputPalette extends HTMLElement {
5859
5949
  color: el.hexOpaque || this.#colors[index].color,
5860
5950
  alpha: el.rgba ? el.rgba.a : this.#colors[index].alpha,
5861
5951
  };
5952
+ const sibling = siblingList[index];
5953
+ if (sibling) {
5954
+ const entry = this.#colors[index];
5955
+ const hex = entry.alpha < 1
5956
+ ? entry.color + Math.round(entry.alpha * 255).toString(16).padStart(2, "0")
5957
+ : entry.color;
5958
+ sibling.setAttribute("value", hex);
5959
+ }
5862
5960
  };
5863
5961
 
5864
5962
  ic.addEventListener("input", (e) => {
@@ -5871,11 +5969,12 @@ class FigInputPalette extends HTMLElement {
5871
5969
  this.#emitChange();
5872
5970
  });
5873
5971
 
5874
- this.#pickers.push(ic);
5972
+ if (inline) this.#inlinePickers.push(ic);
5973
+ else this.#expandedPickers.push(ic);
5875
5974
  return ic;
5876
5975
  }
5877
5976
 
5878
- #createAddButton(disabled) {
5977
+ #createAddButton(disabled, parent = this) {
5879
5978
  const atMax = this.#colors.length >= this.#max;
5880
5979
  const addBtn = document.createElement("fig-button");
5881
5980
  addBtn.setAttribute("variant", "ghost");
@@ -5892,19 +5991,21 @@ class FigInputPalette extends HTMLElement {
5892
5991
  const tooltip = document.createElement("fig-tooltip");
5893
5992
  tooltip.setAttribute("text", "Add color");
5894
5993
  tooltip.appendChild(addBtn);
5895
- this.appendChild(tooltip);
5994
+ parent.appendChild(tooltip);
5896
5995
  }
5897
5996
 
5898
5997
  #addColor(entry) {
5899
5998
  this.#colors.push(entry);
5900
5999
  const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
5901
- const ic = this.#createPicker(entry, this.#colors.length - 1, disabled);
6000
+ const index = this.#colors.length - 1;
6001
+
6002
+ const inlineIc = this.#createPicker(entry, index, disabled, { inline: true });
5902
6003
  const wrap = this.querySelector(".palette-colors");
5903
- if (wrap) {
5904
- wrap.appendChild(ic);
5905
- } else {
5906
- this.appendChild(ic);
5907
- }
6004
+ if (wrap) wrap.appendChild(inlineIc);
6005
+
6006
+ const expandedIc = this.#createPicker(entry, index, disabled);
6007
+ const expandedWrap = this.querySelector(".palette-colors-expanded");
6008
+ if (expandedWrap) expandedWrap.appendChild(expandedIc);
5908
6009
 
5909
6010
  if (this.#colors.length >= this.#max) {
5910
6011
  const addBtn = this.querySelector(".palette-add-btn");
@@ -5914,17 +6015,19 @@ class FigInputPalette extends HTMLElement {
5914
6015
  }
5915
6016
 
5916
6017
  #updateChit(index) {
5917
- const ic = this.#pickers[index];
5918
- if (!ic) return;
5919
6018
  const entry = this.#colors[index];
6019
+ if (!entry) return;
5920
6020
  const hexAlpha = entry.alpha < 1
5921
6021
  ? entry.color + Math.round(entry.alpha * 255).toString(16).padStart(2, "0")
5922
6022
  : entry.color;
5923
- ic.setAttribute("value", hexAlpha);
6023
+ const inl = this.#inlinePickers[index];
6024
+ if (inl) inl.setAttribute("value", hexAlpha);
6025
+ const exp = this.#expandedPickers[index];
6026
+ if (exp) exp.setAttribute("value", hexAlpha);
5924
6027
  }
5925
6028
 
5926
6029
  #syncPickers() {
5927
- if (this.#pickers.length !== this.#colors.length) {
6030
+ if (this.#inlinePickers.length !== this.#colors.length) {
5928
6031
  this.#render();
5929
6032
  return;
5930
6033
  }
@@ -5935,7 +6038,7 @@ class FigInputPalette extends HTMLElement {
5935
6038
 
5936
6039
  #syncDisabled() {
5937
6040
  const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
5938
- this.#pickers.forEach((fp) => {
6041
+ [...this.#inlinePickers, ...this.#expandedPickers].forEach((fp) => {
5939
6042
  if (disabled) fp.setAttribute("disabled", "");
5940
6043
  else fp.removeAttribute("disabled");
5941
6044
  });
@@ -5975,6 +6078,7 @@ customElements.define("fig-input-palette", FigInputPalette);
5975
6078
  * @fires change - When the gradient value is committed
5976
6079
  */
5977
6080
  class FigInputGradient extends HTMLElement {
6081
+ static SHIFT_SNAP = 5;
5978
6082
  #chit;
5979
6083
  #track;
5980
6084
  #handleDragging = false;
@@ -6007,6 +6111,16 @@ class FigInputGradient extends HTMLElement {
6007
6111
 
6008
6112
  disconnectedCallback() {
6009
6113
  document.removeEventListener("keydown", this.#onKeyDown);
6114
+ if (this.#colorObserver) {
6115
+ this.#colorObserver.disconnect();
6116
+ this.#colorObserver = null;
6117
+ }
6118
+ clearTimeout(this.#arrowTooltipTimer);
6119
+ this.removeEventListener("pointerenter", this.#onTrackEnter);
6120
+ this.removeEventListener("pointermove", this.#onTrackMove);
6121
+ this.removeEventListener("pointerleave", this.#onTrackLeave);
6122
+ this.removeEventListener("click", this.#onTrackClick);
6123
+ this.removeEventListener("dblclick", this.#onTrackDblClick);
6010
6124
  }
6011
6125
 
6012
6126
  #onKeyDown = (e) => {
@@ -6046,7 +6160,7 @@ class FigInputGradient extends HTMLElement {
6046
6160
  const idx = parseInt(selected.dataset.stopIndex, 10);
6047
6161
  if (isNaN(idx) || !this.#gradient.stops[idx]) return;
6048
6162
  e.preventDefault();
6049
- const delta = (e.key === "ArrowRight" ? 1 : -1) * (e.shiftKey ? 10 : 1);
6163
+ const delta = (e.key === "ArrowRight" ? 1 : -1) * (e.shiftKey ? FigInputGradient.SHIFT_SNAP : 1);
6050
6164
  const stop = this.#gradient.stops[idx];
6051
6165
  stop.position = Math.max(0, Math.min(100, stop.position + delta));
6052
6166
  selected.setAttribute("value", `${stop.position}% 50%`);
@@ -6141,10 +6255,8 @@ class FigInputGradient extends HTMLElement {
6141
6255
  }
6142
6256
 
6143
6257
  #sampleGradientColor(position) {
6144
- const canvas = document.createElement("canvas");
6145
- canvas.width = 256;
6146
- canvas.height = 1;
6147
- const ctx = canvas.getContext("2d");
6258
+ const { ctx } = figGetSharedCanvas(256, 1);
6259
+ ctx.clearRect(0, 0, 256, 1);
6148
6260
  const grad = ctx.createLinearGradient(0, 0, 256, 0);
6149
6261
  for (const stop of this.#gradient.stops) {
6150
6262
  try {
@@ -6242,15 +6354,15 @@ class FigInputGradient extends HTMLElement {
6242
6354
  this.#syncChit();
6243
6355
  this.#emitInput();
6244
6356
  this.#emitChange();
6245
- this.#track?.querySelectorAll("fig-handle[selected]").forEach((h) => {
6246
- h.removeAttribute("selected");
6247
- });
6248
6357
  }
6249
6358
 
6250
6359
  #onTrackDblClick = (e) => {
6251
6360
  if (!this.#track) return;
6252
6361
  if (!e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) return;
6253
6362
  this.#distributeStops();
6363
+ this.#track.querySelectorAll("fig-handle[selected]").forEach((h) => {
6364
+ h.removeAttribute("selected");
6365
+ });
6254
6366
  };
6255
6367
 
6256
6368
  #onTrackClick = (e) => {
@@ -6258,7 +6370,15 @@ class FigInputGradient extends HTMLElement {
6258
6370
  if (this.#handleDragging) return;
6259
6371
  if (e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) {
6260
6372
  if (e.shiftKey) {
6373
+ const clickedHandle = e.target.closest("fig-handle");
6374
+ const stopIdx = parseInt(clickedHandle?.dataset.stopIndex, 10);
6261
6375
  this.#distributeStops();
6376
+ if (!isNaN(stopIdx)) {
6377
+ this.#track.querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)").forEach((h) => {
6378
+ if (parseInt(h.dataset.stopIndex, 10) === stopIdx) h.select();
6379
+ else h.deselect();
6380
+ });
6381
+ }
6262
6382
  e.stopPropagation();
6263
6383
  }
6264
6384
  return;
@@ -6307,6 +6427,7 @@ class FigInputGradient extends HTMLElement {
6307
6427
  for (let i = 0; i < stops.length; i++) {
6308
6428
  const h = handles[i];
6309
6429
  const stop = stops[i];
6430
+ h.dataset.stopIndex = i;
6310
6431
  h.setAttribute("value", `${stop.position}% 50%`);
6311
6432
  h.setAttribute("color", stop.color);
6312
6433
  const tip = h.closest("fig-tooltip");
@@ -6374,6 +6495,44 @@ class FigInputGradient extends HTMLElement {
6374
6495
  #setupEventListeners() {
6375
6496
  if (!this.#track) return;
6376
6497
 
6498
+ this.#track.addEventListener("pointerdown", (e) => {
6499
+ if (this.hasAttribute("disabled")) return;
6500
+ if (e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) return;
6501
+ if (e.button !== 0) return;
6502
+ e.preventDefault();
6503
+
6504
+ const trackRect = this.#track.getBoundingClientRect();
6505
+ const pct = Math.max(0, Math.min(1, (e.clientX - trackRect.left) / trackRect.width));
6506
+ const position = Math.round(pct * 100);
6507
+ const color = this.#sampleGradientColor(pct);
6508
+ this.#gradient.stops.push({ position, color, opacity: 100 });
6509
+ this.#gradient.stops.sort((a, b) => a.position - b.position);
6510
+ const newIndex = this.#gradient.stops.findIndex(
6511
+ (s) => s.position === position && s.color === color,
6512
+ );
6513
+ this.#syncHandles();
6514
+ this.#syncChit();
6515
+ this.#emitInput();
6516
+ this.#hideGhost();
6517
+
6518
+ const handles = this.#track.querySelectorAll(
6519
+ "fig-handle:not(.fig-input-gradient-ghost)",
6520
+ );
6521
+ const newHandle = handles[newIndex];
6522
+ if (newHandle) {
6523
+ newHandle.select();
6524
+ newHandle.dispatchEvent(new PointerEvent("pointerdown", {
6525
+ bubbles: true,
6526
+ clientX: e.clientX,
6527
+ clientY: e.clientY,
6528
+ pointerId: e.pointerId,
6529
+ pointerType: e.pointerType,
6530
+ button: e.button,
6531
+ buttons: e.buttons,
6532
+ }));
6533
+ }
6534
+ });
6535
+
6377
6536
  this.#track.addEventListener("input", (e) => {
6378
6537
  const handle = e.target.closest("fig-handle");
6379
6538
  if (!handle) return;
@@ -6387,7 +6546,7 @@ class FigInputGradient extends HTMLElement {
6387
6546
  let position = rawPosition;
6388
6547
  const trackW = this.#track.getBoundingClientRect().width;
6389
6548
  if (e.detail?.shiftKey) {
6390
- position = Math.round(position / 10) * 10;
6549
+ position = Math.round(position / FigInputGradient.SHIFT_SNAP) * FigInputGradient.SHIFT_SNAP;
6391
6550
  } else {
6392
6551
  const snapPct = trackW > 0 ? (5 / trackW) * 100 : 0;
6393
6552
  for (let i = 0; i < this.#gradient.stops.length; i++) {
@@ -6515,10 +6674,24 @@ class FigInputGradient extends HTMLElement {
6515
6674
  this.#syncHandles();
6516
6675
  break;
6517
6676
  case "disabled":
6518
- this.#render();
6677
+ this.#syncDisabled();
6519
6678
  break;
6520
6679
  }
6521
6680
  }
6681
+
6682
+ #syncDisabled() {
6683
+ const disabled = this.hasAttribute("disabled");
6684
+ if (this.#chit) {
6685
+ if (disabled) this.#chit.setAttribute("disabled", "");
6686
+ else this.#chit.removeAttribute("disabled");
6687
+ }
6688
+ if (this.#track) {
6689
+ for (const handle of this.#track.querySelectorAll("fig-handle")) {
6690
+ if (disabled) handle.setAttribute("disabled", "");
6691
+ else handle.removeAttribute("disabled");
6692
+ }
6693
+ }
6694
+ }
6522
6695
  }
6523
6696
  customElements.define("fig-input-gradient", FigInputGradient);
6524
6697
 
@@ -6761,9 +6934,11 @@ customElements.define("fig-switch", FigSwitch);
6761
6934
  class FigToast extends HTMLDialogElement {
6762
6935
  _defaultOffset = 16; // 1rem in pixels
6763
6936
  _autoCloseTimer = null;
6937
+ #boundHandleClose;
6764
6938
 
6765
6939
  constructor() {
6766
6940
  super();
6941
+ this.#boundHandleClose = this.handleClose.bind(this);
6767
6942
  }
6768
6943
 
6769
6944
  getOffset() {
@@ -6813,8 +6988,8 @@ class FigToast extends HTMLDialogElement {
6813
6988
 
6814
6989
  addCloseListeners() {
6815
6990
  this.querySelectorAll("[close-toast]").forEach((button) => {
6816
- button.removeEventListener("click", this.handleClose);
6817
- button.addEventListener("click", this.handleClose.bind(this));
6991
+ button.removeEventListener("click", this.#boundHandleClose);
6992
+ button.addEventListener("click", this.#boundHandleClose);
6818
6993
  });
6819
6994
  }
6820
6995
 
@@ -7101,12 +7276,10 @@ class FigChit extends HTMLElement {
7101
7276
  }
7102
7277
 
7103
7278
  #toHex(color) {
7104
- // Convert color to hex for the native input
7105
7279
  if (!color) return "#D9D9D9";
7106
7280
  if (color.startsWith("#")) return color.slice(0, 7);
7107
- // Use canvas to convert rgba/named colors to hex
7108
7281
  try {
7109
- const ctx = document.createElement("canvas").getContext("2d");
7282
+ const { ctx } = figGetSharedCanvas(1, 1);
7110
7283
  ctx.fillStyle = color;
7111
7284
  return ctx.fillStyle;
7112
7285
  } catch {
@@ -7497,11 +7670,10 @@ class FigEasingCurve extends HTMLElement {
7497
7670
  #bounds = null;
7498
7671
  #diagonal = null;
7499
7672
  #resizeObserver = null;
7500
- #bezierHandleRadius = 3.625;
7673
+ #bezierHandleRadius = 5;
7501
7674
  #bezierEndpointRadius = 2;
7502
- #springHandleRadius = 3.625;
7503
- #durationBarWidth = 6;
7504
- #durationBarHeight = 16;
7675
+ #durationBarWidth = 10;
7676
+ #durationBarHeight = 10;
7505
7677
  #durationBarRadius = 3;
7506
7678
 
7507
7679
  static PRESETS = [
@@ -7576,7 +7748,7 @@ class FigEasingCurve extends HTMLElement {
7576
7748
 
7577
7749
  connectedCallback() {
7578
7750
  this.#precision = parseInt(this.getAttribute("precision") || "2");
7579
- this.#syncAspectRatioVar(this.getAttribute("aspect-ratio"));
7751
+ figSyncCssVar(this, "--aspect-ratio", this.getAttribute("aspect-ratio"));
7580
7752
  const val = this.getAttribute("value");
7581
7753
  if (val) this.#parseValue(val);
7582
7754
  this.#presetName = this.#matchPreset();
@@ -7592,17 +7764,9 @@ class FigEasingCurve extends HTMLElement {
7592
7764
  }
7593
7765
  }
7594
7766
 
7595
- #syncAspectRatioVar(value) {
7596
- if (value && value.trim()) {
7597
- this.style.setProperty("--aspect-ratio", value.trim());
7598
- } else {
7599
- this.style.removeProperty("--aspect-ratio");
7600
- }
7601
- }
7602
-
7603
7767
  attributeChangedCallback(name, oldValue, newValue) {
7604
7768
  if (name === "aspect-ratio") {
7605
- this.#syncAspectRatioVar(newValue);
7769
+ figSyncCssVar(this, "--aspect-ratio", newValue);
7606
7770
  if (this.#svg) {
7607
7771
  this.#syncViewportSize();
7608
7772
  this.#updatePaths();
@@ -7818,6 +7982,7 @@ class FigEasingCurve extends HTMLElement {
7818
7982
  this.#syncMetricsFromCSS();
7819
7983
  this.innerHTML = this.#getInnerHTML();
7820
7984
  this.#cacheRefs();
7985
+ this.#syncHandleSizes();
7821
7986
  this.#syncViewportSize();
7822
7987
  this.#updatePaths();
7823
7988
  this.#setupEvents();
@@ -7857,8 +8022,6 @@ class FigEasingCurve extends HTMLElement {
7857
8022
  const size = 200;
7858
8023
  const dropdown = this.#getDropdownHTML();
7859
8024
 
7860
- const hs = this.#bezierHandleRadius * 2;
7861
-
7862
8025
  if (this.#mode === "spring") {
7863
8026
  const targetY = 40;
7864
8027
  const startY = 180;
@@ -7867,8 +8030,8 @@ class FigEasingCurve extends HTMLElement {
7867
8030
  <line class="fig-easing-curve-target" x1="0" y1="${targetY}" x2="${size}" y2="${targetY}"/>
7868
8031
  <line class="fig-easing-curve-diagonal" x1="0" y1="${startY}" x2="0" y2="${startY}"/>
7869
8032
  <path class="fig-easing-curve-path"/>
7870
- <foreignObject class="fig-easing-curve-handle" data-handle="bounce" width="${hs}" height="${hs}"><fig-handle></fig-handle></foreignObject>
7871
- <foreignObject class="fig-easing-curve-handle fig-easing-curve-duration-bar" data-handle="duration" width="${this.#durationBarWidth}" height="${this.#durationBarHeight}"><fig-handle></fig-handle></foreignObject>
8033
+ <foreignObject class="fig-easing-curve-handle" data-handle="bounce" width="20" height="20"><fig-handle size="small"></fig-handle></foreignObject>
8034
+ <foreignObject class="fig-easing-curve-handle fig-easing-curve-duration-bar" data-handle="duration" width="20" height="20"><fig-handle size="small"></fig-handle></foreignObject>
7872
8035
  </svg></div>`;
7873
8036
  }
7874
8037
 
@@ -7880,8 +8043,8 @@ class FigEasingCurve extends HTMLElement {
7880
8043
  <path class="fig-easing-curve-path"/>
7881
8044
  <circle class="fig-easing-curve-endpoint" data-endpoint="start" r="${this.#bezierEndpointRadius}"/>
7882
8045
  <circle class="fig-easing-curve-endpoint" data-endpoint="end" r="${this.#bezierEndpointRadius}"/>
7883
- <foreignObject class="fig-easing-curve-handle" data-handle="1" width="${hs}" height="${hs}"><fig-handle></fig-handle></foreignObject>
7884
- <foreignObject class="fig-easing-curve-handle" data-handle="2" width="${hs}" height="${hs}"><fig-handle></fig-handle></foreignObject>
8046
+ <foreignObject class="fig-easing-curve-handle" data-handle="1" width="20" height="20"><fig-handle size="small"></fig-handle></foreignObject>
8047
+ <foreignObject class="fig-easing-curve-handle" data-handle="2" width="20" height="20"><fig-handle size="small"></fig-handle></foreignObject>
7885
8048
  </svg></div>`;
7886
8049
  }
7887
8050
 
@@ -7893,26 +8056,10 @@ class FigEasingCurve extends HTMLElement {
7893
8056
  }
7894
8057
 
7895
8058
  #syncMetricsFromCSS() {
7896
- this.#bezierHandleRadius = this.#readCssNumber(
7897
- "--easing-bezier-handle-radius",
7898
- this.#bezierHandleRadius,
7899
- );
7900
8059
  this.#bezierEndpointRadius = this.#readCssNumber(
7901
8060
  "--easing-bezier-endpoint-radius",
7902
8061
  this.#bezierEndpointRadius,
7903
8062
  );
7904
- this.#springHandleRadius = this.#readCssNumber(
7905
- "--easing-spring-handle-radius",
7906
- this.#springHandleRadius,
7907
- );
7908
- this.#durationBarWidth = this.#readCssNumber(
7909
- "--easing-duration-bar-width",
7910
- this.#durationBarWidth,
7911
- );
7912
- this.#durationBarHeight = this.#readCssNumber(
7913
- "--easing-duration-bar-height",
7914
- this.#durationBarHeight,
7915
- );
7916
8063
  this.#durationBarRadius = this.#readCssNumber(
7917
8064
  "--easing-duration-bar-radius",
7918
8065
  this.#durationBarRadius,
@@ -7938,6 +8085,28 @@ class FigEasingCurve extends HTMLElement {
7938
8085
  this.#diagonal = this.querySelector(".fig-easing-curve-diagonal");
7939
8086
  }
7940
8087
 
8088
+ #syncHandleSizes() {
8089
+ const h1El = this.#handle1?.querySelector("fig-handle");
8090
+ const h2El = this.#handle2?.querySelector("fig-handle");
8091
+ if (h1El) {
8092
+ const w = h1El.offsetWidth || this.#bezierHandleRadius * 2;
8093
+ const h = h1El.offsetHeight || this.#bezierHandleRadius * 2;
8094
+ this.#bezierHandleRadius = Math.max(w, h) / 2;
8095
+ this.#handle1.setAttribute("width", w);
8096
+ this.#handle1.setAttribute("height", h);
8097
+ }
8098
+ if (h2El) {
8099
+ const w = h2El.offsetWidth || this.#durationBarWidth;
8100
+ const h = h2El.offsetHeight || this.#durationBarHeight;
8101
+ if (this.#mode === "spring") {
8102
+ this.#durationBarWidth = w;
8103
+ this.#durationBarHeight = h;
8104
+ }
8105
+ this.#handle2.setAttribute("width", w);
8106
+ this.#handle2.setAttribute("height", h);
8107
+ }
8108
+ }
8109
+
7941
8110
  #setupResizeObserver() {
7942
8111
  if (this.#resizeObserver || !window.ResizeObserver) return;
7943
8112
  this.#resizeObserver = new ResizeObserver(() => {
@@ -8392,12 +8561,9 @@ class Fig3DRotate extends HTMLElement {
8392
8561
 
8393
8562
  connectedCallback() {
8394
8563
  this.#precision = parseInt(this.getAttribute("precision") || "1");
8395
- this.#syncAspectRatioVar(this.getAttribute("aspect-ratio"));
8396
- this.#syncPerspectiveVar(this.getAttribute("perspective"));
8397
- this.#syncCSSVar(
8398
- "--perspective-origin",
8399
- this.getAttribute("perspective-origin"),
8400
- );
8564
+ figSyncCssVar(this, "--aspect-ratio", this.getAttribute("aspect-ratio"));
8565
+ figSyncCssVar(this, "--perspective", this.getAttribute("perspective"));
8566
+ figSyncCssVar(this, "--perspective-origin", this.getAttribute("perspective-origin"));
8401
8567
  this.#syncTransformOrigin(this.getAttribute("transform-origin"));
8402
8568
  this.#parseFields(this.getAttribute("fields"));
8403
8569
  const val = this.getAttribute("value");
@@ -8415,29 +8581,7 @@ class Fig3DRotate extends HTMLElement {
8415
8581
  }
8416
8582
  }
8417
8583
 
8418
- #syncAspectRatioVar(value) {
8419
- if (value && value.trim()) {
8420
- this.style.setProperty("--aspect-ratio", value.trim());
8421
- } else {
8422
- this.style.removeProperty("--aspect-ratio");
8423
- }
8424
- }
8425
-
8426
- #syncPerspectiveVar(value) {
8427
- if (value && value.trim()) {
8428
- this.style.setProperty("--perspective", value.trim());
8429
- } else {
8430
- this.style.removeProperty("--perspective");
8431
- }
8432
- }
8433
-
8434
- #syncCSSVar(prop, value) {
8435
- if (value && value.trim()) {
8436
- this.style.setProperty(prop, value.trim());
8437
- } else {
8438
- this.style.removeProperty(prop);
8439
- }
8440
- }
8584
+
8441
8585
 
8442
8586
  #syncTransformOrigin(value) {
8443
8587
  if (!value || !value.trim()) {
@@ -8486,15 +8630,15 @@ class Fig3DRotate extends HTMLElement {
8486
8630
 
8487
8631
  attributeChangedCallback(name, oldValue, newValue) {
8488
8632
  if (name === "aspect-ratio") {
8489
- this.#syncAspectRatioVar(newValue);
8633
+ figSyncCssVar(this, "--aspect-ratio", newValue);
8490
8634
  return;
8491
8635
  }
8492
8636
  if (name === "perspective") {
8493
- this.#syncPerspectiveVar(newValue);
8637
+ figSyncCssVar(this, "--perspective", newValue);
8494
8638
  return;
8495
8639
  }
8496
8640
  if (name === "perspective-origin") {
8497
- this.#syncCSSVar("--perspective-origin", newValue);
8641
+ figSyncCssVar(this, "--perspective-origin", newValue);
8498
8642
  return;
8499
8643
  }
8500
8644
  if (name === "transform-origin") {
@@ -8744,7 +8888,7 @@ class FigOriginGrid extends HTMLElement {
8744
8888
 
8745
8889
  connectedCallback() {
8746
8890
  this.#precision = parseInt(this.getAttribute("precision") || "0");
8747
- this.#syncAspectRatioVar(this.getAttribute("aspect-ratio"));
8891
+ figSyncCssVar(this, "--aspect-ratio", this.getAttribute("aspect-ratio"));
8748
8892
  this.#applyIncomingValue(this.getAttribute("value"));
8749
8893
 
8750
8894
  this.#render();
@@ -8768,7 +8912,7 @@ class FigOriginGrid extends HTMLElement {
8768
8912
 
8769
8913
  attributeChangedCallback(name, oldValue, newValue) {
8770
8914
  if (name === "aspect-ratio") {
8771
- this.#syncAspectRatioVar(newValue);
8915
+ figSyncCssVar(this, "--aspect-ratio", newValue);
8772
8916
  return;
8773
8917
  }
8774
8918
  if (name === "drag") {
@@ -8807,14 +8951,6 @@ class FigOriginGrid extends HTMLElement {
8807
8951
  return attr.toLowerCase() !== "false";
8808
8952
  }
8809
8953
 
8810
- #syncAspectRatioVar(value) {
8811
- if (value && value.trim()) {
8812
- this.style.setProperty("--aspect-ratio", value.trim());
8813
- } else {
8814
- this.style.removeProperty("--aspect-ratio");
8815
- }
8816
- }
8817
-
8818
8954
  #syncDragState() {
8819
8955
  if (!this.#grid) return;
8820
8956
  this.#grid.classList.toggle("drag-disabled", !this.#dragEnabled);
@@ -9198,6 +9334,7 @@ class FigInputJoystick extends HTMLElement {
9198
9334
  #boundYFocusOut = null;
9199
9335
  #isSyncingValueAttr = false;
9200
9336
  #defaultPosition = { x: 0.5, y: 0.5 };
9337
+ #initialized = false;
9201
9338
 
9202
9339
  constructor() {
9203
9340
  super();
@@ -9209,7 +9346,6 @@ class FigInputJoystick extends HTMLElement {
9209
9346
  this.xInput = null;
9210
9347
  this.yInput = null;
9211
9348
  this.coordinates = "screen";
9212
- this.#initialized = false;
9213
9349
  this.#boundPlanePointerDown = (e) => this.#handlePlanePointerDown(e);
9214
9350
  this.#boundHandlePointerDown = () => {
9215
9351
  this.isDragging = true;
@@ -9223,8 +9359,6 @@ class FigInputJoystick extends HTMLElement {
9223
9359
  this.#boundYFocusOut = () => this.#handleFieldFocusOut();
9224
9360
  }
9225
9361
 
9226
- #initialized = false;
9227
-
9228
9362
  connectedCallback() {
9229
9363
  // Initialize position
9230
9364
  requestAnimationFrame(() => {
@@ -9233,7 +9367,7 @@ class FigInputJoystick extends HTMLElement {
9233
9367
  this.transform = this.getAttribute("transform") || 1;
9234
9368
  this.transform = Number(this.transform);
9235
9369
  this.coordinates = this.getAttribute("coordinates") || "screen";
9236
- this.#syncAspectRatioVar(this.getAttribute("aspect-ratio"));
9370
+ figSyncCssVar(this, "--aspect-ratio", this.getAttribute("aspect-ratio"));
9237
9371
  if (!this.hasAttribute("value")) {
9238
9372
  this.setAttribute("value", "50% 50%");
9239
9373
  }
@@ -9252,14 +9386,6 @@ class FigInputJoystick extends HTMLElement {
9252
9386
  return this.coordinates === "math" ? 1 - y : y;
9253
9387
  }
9254
9388
 
9255
- #syncAspectRatioVar(value) {
9256
- if (value && value.trim()) {
9257
- this.style.setProperty("--aspect-ratio", value.trim());
9258
- } else {
9259
- this.style.removeProperty("--aspect-ratio");
9260
- }
9261
- }
9262
-
9263
9389
  disconnectedCallback() {
9264
9390
  this.#cleanupListeners();
9265
9391
  }
@@ -9561,7 +9687,7 @@ class FigInputJoystick extends HTMLElement {
9561
9687
  }
9562
9688
  attributeChangedCallback(name, oldValue, newValue) {
9563
9689
  if (name === "aspect-ratio") {
9564
- this.#syncAspectRatioVar(newValue);
9690
+ figSyncCssVar(this, "--aspect-ratio", newValue);
9565
9691
  return;
9566
9692
  }
9567
9693
  if (name === "value") {
@@ -9611,6 +9737,11 @@ class FigInputAngle extends HTMLElement {
9611
9737
  #opposite;
9612
9738
  #prevRawAngle = null;
9613
9739
  #boundHandleRawChange;
9740
+ #boundHandleMouseDown;
9741
+ #boundHandleTouchStart;
9742
+ #boundHandleKeyDown;
9743
+ #boundHandleKeyUp;
9744
+ #boundHandleAngleInput;
9614
9745
 
9615
9746
  constructor() {
9616
9747
  super();
@@ -9631,6 +9762,11 @@ class FigInputAngle extends HTMLElement {
9631
9762
  this.rotationSpan = null;
9632
9763
 
9633
9764
  this.#boundHandleRawChange = this.#handleRawChange.bind(this);
9765
+ this.#boundHandleMouseDown = this.#handleMouseDown.bind(this);
9766
+ this.#boundHandleTouchStart = this.#handleTouchStart.bind(this);
9767
+ this.#boundHandleKeyDown = this.#handleKeyDown.bind(this);
9768
+ this.#boundHandleKeyUp = this.#handleKeyUp.bind(this);
9769
+ this.#boundHandleAngleInput = this.#handleAngleInput.bind(this);
9634
9770
  }
9635
9771
 
9636
9772
  connectedCallback() {
@@ -9805,30 +9941,23 @@ class FigInputAngle extends HTMLElement {
9805
9941
  this.angleInput = this.querySelector("fig-input-number[name='angle']");
9806
9942
  this.rotationSpan = this.querySelector(".fig-input-angle-rotations");
9807
9943
  this.#updateRotationDisplay();
9808
- this.plane?.addEventListener("mousedown", this.#handleMouseDown.bind(this));
9809
- this.plane?.addEventListener(
9810
- "touchstart",
9811
- this.#handleTouchStart.bind(this),
9812
- );
9813
- window.addEventListener("keydown", this.#handleKeyDown.bind(this));
9814
- window.addEventListener("keyup", this.#handleKeyUp.bind(this));
9944
+ this.plane?.addEventListener("mousedown", this.#boundHandleMouseDown);
9945
+ this.plane?.addEventListener("touchstart", this.#boundHandleTouchStart);
9946
+ window.addEventListener("keydown", this.#boundHandleKeyDown);
9947
+ window.addEventListener("keyup", this.#boundHandleKeyUp);
9815
9948
  if (this.text && this.angleInput) {
9816
- this.angleInput.addEventListener(
9817
- "input",
9818
- this.#handleAngleInput.bind(this),
9819
- );
9949
+ this.angleInput.addEventListener("input", this.#boundHandleAngleInput);
9820
9950
  }
9821
- // Capture-phase listener for unit suffix parsing
9822
9951
  this.addEventListener("change", this.#boundHandleRawChange, true);
9823
9952
  }
9824
9953
 
9825
9954
  #cleanupListeners() {
9826
- this.plane?.removeEventListener("mousedown", this.#handleMouseDown);
9827
- this.plane?.removeEventListener("touchstart", this.#handleTouchStart);
9828
- window.removeEventListener("keydown", this.#handleKeyDown);
9829
- window.removeEventListener("keyup", this.#handleKeyUp);
9955
+ this.plane?.removeEventListener("mousedown", this.#boundHandleMouseDown);
9956
+ this.plane?.removeEventListener("touchstart", this.#boundHandleTouchStart);
9957
+ window.removeEventListener("keydown", this.#boundHandleKeyDown);
9958
+ window.removeEventListener("keyup", this.#boundHandleKeyUp);
9830
9959
  if (this.text && this.angleInput) {
9831
- this.angleInput.removeEventListener("input", this.#handleAngleInput);
9960
+ this.angleInput.removeEventListener("input", this.#boundHandleAngleInput);
9832
9961
  }
9833
9962
  this.removeEventListener("change", this.#boundHandleRawChange, true);
9834
9963
  }
@@ -10333,6 +10462,8 @@ class FigFillPicker extends HTMLElement {
10333
10462
  #opacitySlider = null;
10334
10463
  #isDraggingColor = false;
10335
10464
  #teardownColorAreaEvents = null;
10465
+ #dialogOpenObserver = null;
10466
+ #webcamTabObserver = null;
10336
10467
 
10337
10468
  constructor() {
10338
10469
  super();
@@ -10358,10 +10489,26 @@ class FigFillPicker extends HTMLElement {
10358
10489
  this.#teardownColorAreaEvents();
10359
10490
  this.#teardownColorAreaEvents = null;
10360
10491
  }
10492
+ if (this.#dialogOpenObserver) {
10493
+ this.#dialogOpenObserver.disconnect();
10494
+ this.#dialogOpenObserver = null;
10495
+ }
10496
+ if (this.#webcamTabObserver) {
10497
+ this.#webcamTabObserver.disconnect();
10498
+ this.#webcamTabObserver = null;
10499
+ }
10500
+ if (this.#webcam.stream) {
10501
+ this.#webcam.stream.getTracks().forEach((track) => track.stop());
10502
+ this.#webcam.stream = null;
10503
+ }
10504
+ if (this.#video.url && this.#video.url.startsWith("blob:")) {
10505
+ URL.revokeObjectURL(this.#video.url);
10506
+ }
10361
10507
  if (this.#chit) this.#chit.removeAttribute("selected");
10362
10508
  if (this.#dialog) {
10363
10509
  this.#dialog.close();
10364
10510
  this.#dialog.remove();
10511
+ this.#dialog = null;
10365
10512
  }
10366
10513
  }
10367
10514
 
@@ -10698,11 +10845,11 @@ class FigFillPicker extends HTMLElement {
10698
10845
  };
10699
10846
  this.#dialog.addEventListener("close", onDialogClose);
10700
10847
 
10701
- const observer = new MutationObserver(() => {
10848
+ this.#dialogOpenObserver = new MutationObserver(() => {
10702
10849
  const isOpen = this.#dialog.hasAttribute("open") && this.#dialog.getAttribute("open") !== "false";
10703
10850
  if (!isOpen) onDialogClose();
10704
10851
  });
10705
- observer.observe(this.#dialog, { attributes: true, attributeFilter: ["open"] });
10852
+ this.#dialogOpenObserver.observe(this.#dialog, { attributes: true, attributeFilter: ["open"] });
10706
10853
 
10707
10854
  // Initialize built-in tabs (skip any overridden by custom slots)
10708
10855
  const builtinInits = {
@@ -11624,10 +11771,15 @@ class FigFillPicker extends HTMLElement {
11624
11771
  }
11625
11772
  }
11626
11773
 
11774
+ static #gradientSupportCache = new Map();
11627
11775
  #testGradientSupport(css) {
11776
+ const cached = FigFillPicker.#gradientSupportCache.get(css);
11777
+ if (cached !== undefined) return cached;
11628
11778
  const el = document.createElement("div");
11629
11779
  el.style.background = css;
11630
- return !!el.style.background;
11780
+ const result = !!el.style.background;
11781
+ FigFillPicker.#gradientSupportCache.set(css, result);
11782
+ return result;
11631
11783
  }
11632
11784
 
11633
11785
  #getGradientCSS() {
@@ -11972,13 +12124,12 @@ class FigFillPicker extends HTMLElement {
11972
12124
  }
11973
12125
  };
11974
12126
 
11975
- // Start webcam when tab is shown
11976
- const observer = new MutationObserver(() => {
12127
+ this.#webcamTabObserver = new MutationObserver(() => {
11977
12128
  if (container.style.display !== "none" && !this.#webcam.stream) {
11978
12129
  startWebcam();
11979
12130
  }
11980
12131
  });
11981
- observer.observe(container, {
12132
+ this.#webcamTabObserver.observe(container, {
11982
12133
  attributes: true,
11983
12134
  attributeFilter: ["style"],
11984
12135
  });
@@ -12497,8 +12648,8 @@ class FigColorTip extends HTMLElement {
12497
12648
  }
12498
12649
 
12499
12650
  try {
12500
- const ctx = document.createElement("canvas").getContext("2d");
12501
- if (!ctx) return "#D9D9D9";
12651
+ const { ctx } = figGetSharedCanvas(1, 1);
12652
+ ctx.fillStyle = "#000000";
12502
12653
  ctx.fillStyle = value;
12503
12654
  const resolved = ctx.fillStyle;
12504
12655
  if (resolved.startsWith("#")) {
@@ -12703,7 +12854,7 @@ class FigChooser extends HTMLElement {
12703
12854
  }
12704
12855
 
12705
12856
  static get observedAttributes() {
12706
- return ["value", "disabled", "choice-element", "drag", "overflow", "loop"];
12857
+ return ["value", "disabled", "choice-element", "drag", "overflow", "loop", "padding"];
12707
12858
  }
12708
12859
 
12709
12860
  get #overflowMode() {
@@ -13339,8 +13490,8 @@ class FigHandle extends HTMLElement {
13339
13490
  };
13340
13491
 
13341
13492
  const axes = this.#axes;
13342
- if (axes.x) this.style.left = `${resolve(xToken, rect.width, hw)}px`;
13343
- if (axes.y) this.style.top = `${resolve(yToken, rect.height, hh)}px`;
13493
+ if (axes.x) this.style.left = `${Math.round(resolve(xToken, rect.width, hw))}px`;
13494
+ if (axes.y) this.style.top = `${Math.round(resolve(yToken, rect.height, hh))}px`;
13344
13495
  }
13345
13496
 
13346
13497
  #syncValueAttribute() {
@@ -13370,7 +13521,7 @@ class FigHandle extends HTMLElement {
13370
13521
  select() {
13371
13522
  if (this.hasAttribute("disabled")) return;
13372
13523
  this.setAttribute("selected", "");
13373
- if (this.getAttribute("type") === "color") this.#showColorTip();
13524
+ if (this.getAttribute("type") === "color" && !this.#isDragging) this.#showColorTip();
13374
13525
  }
13375
13526
 
13376
13527
  deselect() {
@@ -13410,6 +13561,9 @@ class FigHandle extends HTMLElement {
13410
13561
  } else {
13411
13562
  this.style.setProperty("--fill", value);
13412
13563
  }
13564
+ if (this.#colorTip && value) {
13565
+ this.#colorTip.setAttribute("value", value);
13566
+ }
13413
13567
  }
13414
13568
  if (name === "drag") this.#syncDrag();
13415
13569
  if (name === "value" && !this.#applyingValue && !this.#isDragging) {
@@ -13450,12 +13604,13 @@ class FigHandle extends HTMLElement {
13450
13604
 
13451
13605
  this.#isDragging = true;
13452
13606
  const axes = this.#axes;
13453
- const containerRect = container.getBoundingClientRect();
13454
13607
  const handleW = this.offsetWidth;
13455
13608
  const handleH = this.offsetHeight;
13609
+ let lastRect = null;
13456
13610
 
13457
13611
  const clampAndApply = (clientX, clientY, shiftKey = false) => {
13458
13612
  const rect = container.getBoundingClientRect();
13613
+ lastRect = rect;
13459
13614
  const currentLeft = parseFloat(this.style.left) || 0;
13460
13615
  const currentTop = parseFloat(this.style.top) || 0;
13461
13616
  const rawX = clientX - rect.left - handleW / 2;
@@ -13491,11 +13646,11 @@ class FigHandle extends HTMLElement {
13491
13646
 
13492
13647
  if (axes.x) {
13493
13648
  const left = centerX * rect.width - handleW / 2;
13494
- this.style.left = `${Math.max(-handleW / 2, Math.min(rect.width - handleW / 2, left))}px`;
13649
+ this.style.left = `${Math.round(Math.max(-handleW / 2, Math.min(rect.width - handleW / 2, left)))}px`;
13495
13650
  }
13496
13651
  if (axes.y) {
13497
13652
  const top = centerY * rect.height - handleH / 2;
13498
- this.style.top = `${Math.max(-handleH / 2, Math.min(rect.height - handleH / 2, top))}px`;
13653
+ this.style.top = `${Math.round(Math.max(-handleH / 2, Math.min(rect.height - handleH / 2, top)))}px`;
13499
13654
  }
13500
13655
  };
13501
13656
 
@@ -13512,7 +13667,7 @@ class FigHandle extends HTMLElement {
13512
13667
  new CustomEvent("input", {
13513
13668
  bubbles: true,
13514
13669
  detail: {
13515
- ...this.#positionDetail(container.getBoundingClientRect()),
13670
+ ...this.#positionDetail(lastRect),
13516
13671
  shiftKey: e.shiftKey,
13517
13672
  },
13518
13673
  }),
@@ -13531,7 +13686,7 @@ class FigHandle extends HTMLElement {
13531
13686
  this.dispatchEvent(
13532
13687
  new CustomEvent("change", {
13533
13688
  bubbles: true,
13534
- detail: this.#positionDetail(container.getBoundingClientRect()),
13689
+ detail: this.#positionDetail(lastRect),
13535
13690
  }),
13536
13691
  );
13537
13692
  const swallowClick = (evt) => {