@rogieking/figui3 3.21.2 → 3.23.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
@@ -947,10 +947,161 @@ class FigTooltip extends HTMLElement {
947
947
  this.hidePopup();
948
948
  }
949
949
  }
950
+
951
+ static #programmatic = new WeakMap();
952
+
953
+ static show(anchor, text, options = {}) {
954
+ FigTooltip.hide(anchor);
955
+ const delay = options.delay ?? 500;
956
+ const warm =
957
+ Date.now() - FigTooltip.#lastShownAt < FigTooltip.#warmupWindow;
958
+ const effectiveDelay = warm ? 0 : delay;
959
+
960
+ const state = { timeout: null, popup: null };
961
+ FigTooltip.#programmatic.set(anchor, state);
962
+
963
+ state.timeout = setTimeout(() => {
964
+ const popup = document.createElement("span");
965
+ popup.setAttribute("class", "fig-tooltip");
966
+ popup.setAttribute("role", "tooltip");
967
+ popup.style.position = "fixed";
968
+ popup.style.pointerEvents = "none";
969
+ const content = document.createElement("span");
970
+ content.innerText = text;
971
+ popup.append(content);
972
+
973
+ const parentDialog = anchor.closest("dialog");
974
+ if (parentDialog && parentDialog.open) {
975
+ parentDialog.append(popup);
976
+ } else {
977
+ document.body.append(popup);
978
+ }
979
+
980
+ const rect = anchor.getBoundingClientRect();
981
+ const popupRect = popup.getBoundingClientRect();
982
+ const container = popup.parentElement;
983
+ const containerRect =
984
+ container && container !== document.body
985
+ ? container.getBoundingClientRect()
986
+ : { left: 0, top: 0 };
987
+
988
+ let top = rect.top - popupRect.height - 4 - containerRect.top;
989
+ let left =
990
+ rect.left + (rect.width - popupRect.width) / 2 - containerRect.left;
991
+ popup.setAttribute("position", "top");
992
+
993
+ if (top + containerRect.top < 0) {
994
+ popup.setAttribute("position", "bottom");
995
+ top = rect.bottom + 4 - containerRect.top;
996
+ }
997
+ if (left + containerRect.left < 8) {
998
+ left = 8 - containerRect.left;
999
+ }
1000
+ if (left + popupRect.width + containerRect.left > window.innerWidth - 8) {
1001
+ left = window.innerWidth - popupRect.width - 8 - containerRect.left;
1002
+ }
1003
+
1004
+ const targetCenter = rect.left - containerRect.left + rect.width / 2;
1005
+ popup.style.setProperty("--beak-offset", `${targetCenter - left}px`);
1006
+ popup.style.top = `${top}px`;
1007
+ popup.style.left = `${left}px`;
1008
+ popup.style.zIndex = figGetHighestZIndex() + 1;
1009
+
1010
+ state.popup = popup;
1011
+ FigTooltip.#lastShownAt = Date.now();
1012
+ }, effectiveDelay);
1013
+ }
1014
+
1015
+ static hide(anchor) {
1016
+ const state = FigTooltip.#programmatic.get(anchor);
1017
+ if (!state) return;
1018
+ clearTimeout(state.timeout);
1019
+ if (state.popup) state.popup.remove();
1020
+ FigTooltip.#programmatic.delete(anchor);
1021
+ }
950
1022
  }
951
1023
 
952
1024
  customElements.define("fig-tooltip", FigTooltip);
953
1025
 
1026
+ /* Text Truncation */
1027
+ class FigTruncate extends HTMLElement {
1028
+ static observedAttributes = ["position", "tail"];
1029
+
1030
+ #originalText = null;
1031
+ #boundEnter = null;
1032
+ #boundLeave = null;
1033
+
1034
+ connectedCallback() {
1035
+ this.#originalText = this.textContent;
1036
+ requestAnimationFrame(() => {
1037
+ this.#render();
1038
+ this.#setupTooltip();
1039
+ });
1040
+ }
1041
+
1042
+ disconnectedCallback() {
1043
+ this.#teardownTooltip();
1044
+ }
1045
+
1046
+ attributeChangedCallback(name, oldValue, newValue) {
1047
+ if (oldValue === newValue) return;
1048
+ if (this.#originalText === null) return;
1049
+ this.#render();
1050
+ }
1051
+
1052
+ #render() {
1053
+ const position = this.getAttribute("position") || "right";
1054
+ const text = this.#originalText || "";
1055
+ if (position === "middle") {
1056
+ const tail = this.getAttribute("tail");
1057
+ let splitIndex;
1058
+ if (tail) {
1059
+ const idx = text.lastIndexOf(tail);
1060
+ splitIndex = idx > 0 ? idx : Math.ceil(text.length / 2);
1061
+ } else {
1062
+ splitIndex = Math.ceil(text.length / 2);
1063
+ }
1064
+ this.innerHTML = "";
1065
+ const startSpan = document.createElement("span");
1066
+ startSpan.className = "start";
1067
+ startSpan.textContent = text.slice(0, splitIndex);
1068
+ const endSpan = document.createElement("span");
1069
+ endSpan.className = "end";
1070
+ endSpan.textContent = text.slice(splitIndex);
1071
+ this.appendChild(startSpan);
1072
+ this.appendChild(endSpan);
1073
+ } else {
1074
+ this.textContent = text;
1075
+ }
1076
+ }
1077
+
1078
+ #setupTooltip() {
1079
+ if (
1080
+ !this.hasAttribute("tooltip") ||
1081
+ this.getAttribute("tooltip") === "false"
1082
+ )
1083
+ return;
1084
+ this.#boundEnter = () => {
1085
+ if (this.scrollWidth <= this.clientWidth) return;
1086
+ FigTooltip.show(this, this.#originalText);
1087
+ };
1088
+ this.#boundLeave = () => {
1089
+ FigTooltip.hide(this);
1090
+ };
1091
+ this.addEventListener("pointerenter", this.#boundEnter);
1092
+ this.addEventListener("pointerleave", this.#boundLeave);
1093
+ }
1094
+
1095
+ #teardownTooltip() {
1096
+ if (this.#boundEnter)
1097
+ this.removeEventListener("pointerenter", this.#boundEnter);
1098
+ if (this.#boundLeave)
1099
+ this.removeEventListener("pointerleave", this.#boundLeave);
1100
+ FigTooltip.hide(this);
1101
+ }
1102
+ }
1103
+ customElements.define("fig-truncate", FigTruncate);
1104
+
954
1105
  /* Dialog */
955
1106
  /**
956
1107
  * A custom dialog element for modal and non-modal dialogs.
@@ -3533,8 +3684,12 @@ class FigInputText extends HTMLElement {
3533
3684
  valueTransformed = this.#formatNumber(valueTransformed);
3534
3685
  this.value = value;
3535
3686
  this.input.value = valueTransformed;
3536
- this.dispatchEvent(new CustomEvent("input", { detail: this.value, bubbles: true }));
3537
- this.dispatchEvent(new CustomEvent("change", { detail: this.value, bubbles: true }));
3687
+ this.dispatchEvent(
3688
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
3689
+ );
3690
+ this.dispatchEvent(
3691
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
3692
+ );
3538
3693
  }
3539
3694
  #handleMouseDown(e) {
3540
3695
  if (this.type !== "number") return;
@@ -4023,8 +4178,12 @@ class FigInputNumber extends HTMLElement {
4023
4178
  value = this.#sanitizeInput(value, false);
4024
4179
  this.value = value;
4025
4180
  this.input.value = this.#formatWithUnit(this.value);
4026
- this.dispatchEvent(new CustomEvent("input", { detail: this.value, bubbles: true }));
4027
- this.dispatchEvent(new CustomEvent("change", { detail: this.value, bubbles: true }));
4181
+ this.dispatchEvent(
4182
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
4183
+ );
4184
+ this.dispatchEvent(
4185
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
4186
+ );
4028
4187
  }
4029
4188
 
4030
4189
  #handleMouseDown(e) {
@@ -4209,6 +4368,10 @@ customElements.define("fig-avatar", FigAvatar);
4209
4368
 
4210
4369
  /* Form Field */
4211
4370
  class FigField extends HTMLElement {
4371
+ #toggleable = false;
4372
+ #chevron = null;
4373
+ #boundToggle = null;
4374
+
4212
4375
  constructor() {
4213
4376
  super();
4214
4377
  }
@@ -4223,15 +4386,65 @@ class FigField extends HTMLElement {
4223
4386
  this.input = Array.from(this.childNodes).find((node) =>
4224
4387
  node.nodeName.toLowerCase().startsWith("fig-"),
4225
4388
  );
4226
- if (this.input && this.label) {
4389
+
4390
+ this.#toggleable = !!(this.input && "open" in this.input);
4391
+
4392
+ if (this.#toggleable && this.label) {
4393
+ this.#chevron = document.createElement("span");
4394
+ this.#chevron.className = "fig-mask-icon fig-field-chevron";
4395
+ this.insertBefore(this.#chevron, this.label);
4396
+
4397
+ this.#boundToggle = (e) => {
4398
+ e.preventDefault();
4399
+ e.stopPropagation();
4400
+ if (this.input && typeof this.input.open !== "undefined") {
4401
+ this.input.open = !this.input.open;
4402
+ }
4403
+ };
4404
+ this.#chevron.addEventListener("click", this.#boundToggle);
4405
+ this.label.addEventListener("click", this.#boundToggle);
4406
+ } else if (this.input && this.label) {
4227
4407
  this.label.addEventListener("click", this.focus.bind(this));
4408
+ }
4409
+
4410
+ if (this.input && this.label && !this.#toggleable) {
4228
4411
  let inputId = this.input.getAttribute("id") || figUniqueId();
4229
4412
  this.input.setAttribute("id", inputId);
4230
4413
  this.label.setAttribute("for", inputId);
4231
4414
  }
4415
+
4416
+ if (this.label) {
4417
+ this.label.addEventListener(
4418
+ "pointerenter",
4419
+ this.#onLabelEnter.bind(this),
4420
+ );
4421
+ this.label.addEventListener(
4422
+ "pointerleave",
4423
+ this.#onLabelLeave.bind(this),
4424
+ );
4425
+ }
4232
4426
  });
4233
4427
  }
4234
4428
 
4429
+ disconnectedCallback() {
4430
+ if (this.label) FigTooltip.hide(this.label);
4431
+ if (this.label && this.#boundToggle) {
4432
+ this.label.removeEventListener("click", this.#boundToggle);
4433
+ }
4434
+ if (this.#chevron && this.#boundToggle) {
4435
+ this.#chevron.removeEventListener("click", this.#boundToggle);
4436
+ }
4437
+ }
4438
+
4439
+ #onLabelEnter() {
4440
+ if (!this.label || this.label.scrollWidth <= this.label.clientWidth) return;
4441
+ FigTooltip.show(this.label, this.label.textContent.trim());
4442
+ }
4443
+
4444
+ #onLabelLeave() {
4445
+ if (this.label) FigTooltip.hide(this.label);
4446
+ }
4447
+
4235
4448
  attributeChangedCallback(name, oldValue, newValue) {
4236
4449
  switch (name) {
4237
4450
  case "label":
@@ -4243,7 +4456,18 @@ class FigField extends HTMLElement {
4243
4456
  }
4244
4457
 
4245
4458
  focus() {
4246
- this.input.focus();
4459
+ if (!this.input) return;
4460
+ if (this.input.contains(document.activeElement)) return;
4461
+ const nativeInputs = this.input.querySelectorAll("input, select, textarea");
4462
+ if (nativeInputs.length === 1) {
4463
+ nativeInputs[0].focus();
4464
+ nativeInputs[0].click();
4465
+ } else {
4466
+ this.input.focus();
4467
+ if (nativeInputs.length === 0) {
4468
+ this.input.click();
4469
+ }
4470
+ }
4247
4471
  }
4248
4472
  }
4249
4473
  customElements.define("fig-field", FigField);
@@ -4578,7 +4802,7 @@ class FigInputColor extends HTMLElement {
4578
4802
  const disabledAttr = disabled ? " disabled" : "";
4579
4803
 
4580
4804
  let html = ``;
4581
- const showText = this.getAttribute("text") === "true";
4805
+ const showText = this.getAttribute("text") !== "false";
4582
4806
  if (showText) {
4583
4807
  let label = `<fig-input-text
4584
4808
  type="text"
@@ -4801,22 +5025,35 @@ class FigInputColor extends HTMLElement {
4801
5025
  }
4802
5026
 
4803
5027
  #emitInputEvent() {
4804
- this.dispatchEvent(new CustomEvent("input", {
4805
- bubbles: true,
4806
- cancelable: true,
4807
- detail: { value: this.value, hex: this.hex, rgba: this.rgba },
4808
- }));
5028
+ this.dispatchEvent(
5029
+ new CustomEvent("input", {
5030
+ bubbles: true,
5031
+ cancelable: true,
5032
+ detail: { value: this.value, hex: this.hex, rgba: this.rgba },
5033
+ }),
5034
+ );
4809
5035
  }
4810
5036
  #emitChangeEvent() {
4811
- this.dispatchEvent(new CustomEvent("change", {
4812
- bubbles: true,
4813
- cancelable: true,
4814
- detail: { value: this.value, hex: this.hex, rgba: this.rgba },
4815
- }));
5037
+ this.dispatchEvent(
5038
+ new CustomEvent("change", {
5039
+ bubbles: true,
5040
+ cancelable: true,
5041
+ detail: { value: this.value, hex: this.hex, rgba: this.rgba },
5042
+ }),
5043
+ );
4816
5044
  }
4817
5045
 
4818
5046
  static get observedAttributes() {
4819
- return ["value", "style", "mode", "picker", "experimental", "alpha", "text", "disabled"];
5047
+ return [
5048
+ "value",
5049
+ "style",
5050
+ "mode",
5051
+ "picker",
5052
+ "experimental",
5053
+ "alpha",
5054
+ "text",
5055
+ "disabled",
5056
+ ];
4820
5057
  }
4821
5058
 
4822
5059
  get mode() {
@@ -4877,7 +5114,9 @@ class FigInputColor extends HTMLElement {
4877
5114
  }
4878
5115
 
4879
5116
  get #disabled() {
4880
- return this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
5117
+ return (
5118
+ this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false"
5119
+ );
4881
5120
  }
4882
5121
 
4883
5122
  #syncDisabled() {
@@ -5076,9 +5315,15 @@ function figRGBToOklab(r, g, b) {
5076
5315
  const lr = figRGBToLinear(r);
5077
5316
  const lg = figRGBToLinear(g);
5078
5317
  const lb = figRGBToLinear(b);
5079
- const l_ = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
5080
- const m_ = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
5081
- const s_ = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
5318
+ const l_ = Math.cbrt(
5319
+ 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb,
5320
+ );
5321
+ const m_ = Math.cbrt(
5322
+ 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb,
5323
+ );
5324
+ const s_ = Math.cbrt(
5325
+ 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb,
5326
+ );
5082
5327
  return {
5083
5328
  l: 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
5084
5329
  a: 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
@@ -5101,7 +5346,11 @@ function figOklabToRGB(L, a, b) {
5101
5346
  }
5102
5347
 
5103
5348
  function figOklabToOklch(L, a, b) {
5104
- return { l: L, c: Math.sqrt(a * a + b * b), h: (Math.atan2(b, a) * 180) / Math.PI };
5349
+ return {
5350
+ l: L,
5351
+ c: Math.sqrt(a * a + b * b),
5352
+ h: (Math.atan2(b, a) * 180) / Math.PI,
5353
+ };
5105
5354
  }
5106
5355
 
5107
5356
  function figOklchToOklab(l, c, h) {
@@ -5130,15 +5379,21 @@ function figInterpolateHue(h1, h2, t, mode) {
5130
5379
  else if (diff < -180) diff += 360;
5131
5380
  break;
5132
5381
  }
5133
- return ((a + diff * t) % 360 + 360) % 360;
5382
+ return (((a + diff * t) % 360) + 360) % 360;
5134
5383
  }
5135
5384
 
5136
- function figSampleGradientAt(stops, position, interpolationSpace, hueInterpolation) {
5385
+ function figSampleGradientAt(
5386
+ stops,
5387
+ position,
5388
+ interpolationSpace,
5389
+ hueInterpolation,
5390
+ ) {
5137
5391
  const sorted = [...stops].sort((a, b) => a.position - b.position);
5138
5392
  const pos = position * 100;
5139
5393
  if (sorted.length === 0) return "#888888";
5140
5394
  if (pos <= sorted[0].position) return sorted[0].color;
5141
- if (pos >= sorted[sorted.length - 1].position) return sorted[sorted.length - 1].color;
5395
+ if (pos >= sorted[sorted.length - 1].position)
5396
+ return sorted[sorted.length - 1].color;
5142
5397
 
5143
5398
  let i = 0;
5144
5399
  while (i < sorted.length - 1 && sorted[i + 1].position < pos) i++;
@@ -5154,8 +5409,12 @@ function figSampleGradientAt(stops, position, interpolationSpace, hueInterpolati
5154
5409
  const space = interpolationSpace || "oklab";
5155
5410
 
5156
5411
  if (space === "srgb-linear") {
5157
- const lr1 = figRGBToLinear(c1.r), lg1 = figRGBToLinear(c1.g), lb1 = figRGBToLinear(c1.b);
5158
- const lr2 = figRGBToLinear(c2.r), lg2 = figRGBToLinear(c2.g), lb2 = figRGBToLinear(c2.b);
5412
+ const lr1 = figRGBToLinear(c1.r),
5413
+ lg1 = figRGBToLinear(c1.g),
5414
+ lb1 = figRGBToLinear(c1.b);
5415
+ const lr2 = figRGBToLinear(c2.r),
5416
+ lg2 = figRGBToLinear(c2.g),
5417
+ lb2 = figRGBToLinear(c2.b);
5159
5418
  r = figLinearToSRGB(lr1 + (lr2 - lr1) * t);
5160
5419
  g = figLinearToSRGB(lg1 + (lg2 - lg1) * t);
5161
5420
  b = figLinearToSRGB(lb1 + (lb2 - lb1) * t);
@@ -5166,10 +5425,17 @@ function figSampleGradientAt(stops, position, interpolationSpace, hueInterpolati
5166
5425
  const lch2 = figOklabToOklch(lab2.l, lab2.a, lab2.b);
5167
5426
  const L = lch1.l + (lch2.l - lch1.l) * t;
5168
5427
  const C = lch1.c + (lch2.c - lch1.c) * t;
5169
- const H = figInterpolateHue(lch1.h, lch2.h, t, hueInterpolation || "shorter");
5428
+ const H = figInterpolateHue(
5429
+ lch1.h,
5430
+ lch2.h,
5431
+ t,
5432
+ hueInterpolation || "shorter",
5433
+ );
5170
5434
  const lab = figOklchToOklab(L, C, H);
5171
5435
  const rgb = figOklabToRGB(lab.l, lab.a, lab.b);
5172
- r = rgb.r; g = rgb.g; b = rgb.b;
5436
+ r = rgb.r;
5437
+ g = rgb.g;
5438
+ b = rgb.b;
5173
5439
  } else {
5174
5440
  const lab1 = figRGBToOklab(c1.r, c1.g, c1.b);
5175
5441
  const lab2 = figRGBToOklab(c2.r, c2.g, c2.b);
@@ -5177,7 +5443,9 @@ function figSampleGradientAt(stops, position, interpolationSpace, hueInterpolati
5177
5443
  const a = lab1.a + (lab2.a - lab1.a) * t;
5178
5444
  const bv = lab1.b + (lab2.b - lab1.b) * t;
5179
5445
  const rgb = figOklabToRGB(L, a, bv);
5180
- r = rgb.r; g = rgb.g; b = rgb.b;
5446
+ r = rgb.r;
5447
+ g = rgb.g;
5448
+ b = rgb.b;
5181
5449
  }
5182
5450
 
5183
5451
  return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`.toUpperCase();
@@ -5347,7 +5615,11 @@ class FigInputFill extends HTMLElement {
5347
5615
 
5348
5616
  #syncDisabled() {
5349
5617
  const disabled = this.hasAttribute("disabled");
5350
- for (const child of [this.#fillPicker, this.#opacityInput, this.#hexInput]) {
5618
+ for (const child of [
5619
+ this.#fillPicker,
5620
+ this.#opacityInput,
5621
+ this.#hexInput,
5622
+ ]) {
5351
5623
  if (!child) continue;
5352
5624
  if (disabled) child.setAttribute("disabled", "");
5353
5625
  else child.removeAttribute("disabled");
@@ -5911,11 +6183,28 @@ class FigInputPalette extends HTMLElement {
5911
6183
  #renderRAF = null;
5912
6184
 
5913
6185
  static get observedAttributes() {
5914
- return ["value", "disabled", "min", "max", "expanded", "add"];
6186
+ return ["value", "disabled", "min", "max", "open", "add"];
6187
+ }
6188
+
6189
+ get open() {
6190
+ return this.hasAttribute("open") && this.getAttribute("open") !== "false";
5915
6191
  }
5916
6192
 
5917
- get #expanded() {
5918
- return this.hasAttribute("expanded") && this.getAttribute("expanded") !== "false";
6193
+ set open(value) {
6194
+ const was = this.open;
6195
+ if (value) {
6196
+ this.setAttribute("open", "");
6197
+ } else {
6198
+ this.removeAttribute("open");
6199
+ }
6200
+ if (was !== !!value) {
6201
+ this.dispatchEvent(
6202
+ new CustomEvent("openchange", {
6203
+ detail: { open: !!value },
6204
+ bubbles: true,
6205
+ }),
6206
+ );
6207
+ }
5919
6208
  }
5920
6209
 
5921
6210
  get #showAdd() {
@@ -5933,6 +6222,7 @@ class FigInputPalette extends HTMLElement {
5933
6222
  }
5934
6223
 
5935
6224
  connectedCallback() {
6225
+ if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "0");
5936
6226
  if (this.#renderRAF) cancelAnimationFrame(this.#renderRAF);
5937
6227
  this.#renderRAF = requestAnimationFrame(() => {
5938
6228
  this.#renderRAF = null;
@@ -5963,7 +6253,7 @@ class FigInputPalette extends HTMLElement {
5963
6253
  break;
5964
6254
  case "min":
5965
6255
  case "max":
5966
- case "expanded":
6256
+ case "open":
5967
6257
  case "add":
5968
6258
  this.#render();
5969
6259
  break;
@@ -5985,12 +6275,21 @@ class FigInputPalette extends HTMLElement {
5985
6275
  if (Array.isArray(parsed)) {
5986
6276
  this.#colors = parsed.map((entry) => {
5987
6277
  if (typeof entry === "string") {
5988
- return { color: entry.slice(0, 7), alpha: entry.length > 7 ? parseInt(entry.slice(7, 9), 16) / 255 : 1 };
6278
+ return {
6279
+ color: entry.slice(0, 7),
6280
+ alpha:
6281
+ entry.length > 7 ? parseInt(entry.slice(7, 9), 16) / 255 : 1,
6282
+ };
5989
6283
  }
5990
6284
  if (entry && typeof entry === "object") {
5991
6285
  return {
5992
6286
  color: entry.color || "#D9D9D9",
5993
- alpha: entry.alpha !== undefined ? entry.alpha : (entry.opacity !== undefined ? entry.opacity / 100 : 1),
6287
+ alpha:
6288
+ entry.alpha !== undefined
6289
+ ? entry.alpha
6290
+ : entry.opacity !== undefined
6291
+ ? entry.opacity / 100
6292
+ : 1,
5994
6293
  };
5995
6294
  }
5996
6295
  return { color: "#D9D9D9", alpha: 1 };
@@ -6015,10 +6314,13 @@ class FigInputPalette extends HTMLElement {
6015
6314
 
6016
6315
  // Single hex
6017
6316
  if (trimmed.startsWith("#")) {
6018
- this.#colors = [{
6019
- color: trimmed.slice(0, 7),
6020
- alpha: trimmed.length > 7 ? parseInt(trimmed.slice(7, 9), 16) / 255 : 1,
6021
- }];
6317
+ this.#colors = [
6318
+ {
6319
+ color: trimmed.slice(0, 7),
6320
+ alpha:
6321
+ trimmed.length > 7 ? parseInt(trimmed.slice(7, 9), 16) / 255 : 1,
6322
+ },
6323
+ ];
6022
6324
  return;
6023
6325
  }
6024
6326
 
@@ -6038,7 +6340,9 @@ class FigInputPalette extends HTMLElement {
6038
6340
  }
6039
6341
 
6040
6342
  #render() {
6041
- const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
6343
+ const disabled =
6344
+ this.hasAttribute("disabled") &&
6345
+ this.getAttribute("disabled") !== "false";
6042
6346
 
6043
6347
  this.innerHTML = "";
6044
6348
  this.#inlinePickers = [];
@@ -6050,7 +6354,9 @@ class FigInputPalette extends HTMLElement {
6050
6354
  const wrap = document.createElement("div");
6051
6355
  wrap.className = "palette-colors";
6052
6356
  this.#colors.forEach((entry, i) => {
6053
- wrap.appendChild(this.#createPicker(entry, i, disabled, { inline: true }));
6357
+ wrap.appendChild(
6358
+ this.#createPicker(entry, i, disabled, { inline: true }),
6359
+ );
6054
6360
  });
6055
6361
  inlineWrap.appendChild(wrap);
6056
6362
 
@@ -6066,9 +6372,13 @@ class FigInputPalette extends HTMLElement {
6066
6372
  }
6067
6373
 
6068
6374
  #createPicker(entry, index, disabled, { inline = false } = {}) {
6069
- const hexAlpha = entry.alpha < 1
6070
- ? entry.color + Math.round(entry.alpha * 255).toString(16).padStart(2, "0")
6071
- : entry.color;
6375
+ const hexAlpha =
6376
+ entry.alpha < 1
6377
+ ? entry.color +
6378
+ Math.round(entry.alpha * 255)
6379
+ .toString(16)
6380
+ .padStart(2, "0")
6381
+ : entry.color;
6072
6382
  const ic = document.createElement("fig-input-color");
6073
6383
  ic.setAttribute("value", hexAlpha);
6074
6384
  ic.setAttribute("picker", "figma");
@@ -6095,9 +6405,13 @@ class FigInputPalette extends HTMLElement {
6095
6405
  const sibling = siblingList[index];
6096
6406
  if (sibling) {
6097
6407
  const entry = this.#colors[index];
6098
- const hex = entry.alpha < 1
6099
- ? entry.color + Math.round(entry.alpha * 255).toString(16).padStart(2, "0")
6100
- : entry.color;
6408
+ const hex =
6409
+ entry.alpha < 1
6410
+ ? entry.color +
6411
+ Math.round(entry.alpha * 255)
6412
+ .toString(16)
6413
+ .padStart(2, "0")
6414
+ : entry.color;
6101
6415
  sibling.setAttribute("value", hex);
6102
6416
  }
6103
6417
  };
@@ -6120,14 +6434,18 @@ class FigInputPalette extends HTMLElement {
6120
6434
  #createAddButton(disabled, parent = this) {
6121
6435
  const atMax = this.#colors.length >= this.#max;
6122
6436
  const addBtn = document.createElement("fig-button");
6123
- addBtn.setAttribute("variant", "ghost");
6437
+ addBtn.setAttribute("variant", "input");
6124
6438
  addBtn.setAttribute("icon", "true");
6125
6439
  addBtn.setAttribute("aria-label", "Add color");
6126
6440
  addBtn.className = "palette-add-btn";
6127
6441
  if (disabled || atMax) addBtn.setAttribute("disabled", "");
6128
6442
  addBtn.innerHTML = `<span class="fig-mask-icon" style="--icon: var(--icon-add)"></span>`;
6129
6443
  addBtn.addEventListener("click", () => {
6130
- if (this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") return;
6444
+ if (
6445
+ this.hasAttribute("disabled") &&
6446
+ this.getAttribute("disabled") !== "false"
6447
+ )
6448
+ return;
6131
6449
  if (this.#colors.length >= this.#max) return;
6132
6450
  this.#addColor({ color: "#D9D9D9", alpha: 1 });
6133
6451
  });
@@ -6139,10 +6457,14 @@ class FigInputPalette extends HTMLElement {
6139
6457
 
6140
6458
  #addColor(entry) {
6141
6459
  this.#colors.push(entry);
6142
- const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
6460
+ const disabled =
6461
+ this.hasAttribute("disabled") &&
6462
+ this.getAttribute("disabled") !== "false";
6143
6463
  const index = this.#colors.length - 1;
6144
6464
 
6145
- const inlineIc = this.#createPicker(entry, index, disabled, { inline: true });
6465
+ const inlineIc = this.#createPicker(entry, index, disabled, {
6466
+ inline: true,
6467
+ });
6146
6468
  const wrap = this.querySelector(".palette-colors");
6147
6469
  if (wrap) wrap.appendChild(inlineIc);
6148
6470
 
@@ -6160,9 +6482,13 @@ class FigInputPalette extends HTMLElement {
6160
6482
  #updateChit(index) {
6161
6483
  const entry = this.#colors[index];
6162
6484
  if (!entry) return;
6163
- const hexAlpha = entry.alpha < 1
6164
- ? entry.color + Math.round(entry.alpha * 255).toString(16).padStart(2, "0")
6165
- : entry.color;
6485
+ const hexAlpha =
6486
+ entry.alpha < 1
6487
+ ? entry.color +
6488
+ Math.round(entry.alpha * 255)
6489
+ .toString(16)
6490
+ .padStart(2, "0")
6491
+ : entry.color;
6166
6492
  const inl = this.#inlinePickers[index];
6167
6493
  if (inl) inl.setAttribute("value", hexAlpha);
6168
6494
  const exp = this.#expandedPickers[index];
@@ -6180,7 +6506,9 @@ class FigInputPalette extends HTMLElement {
6180
6506
  }
6181
6507
 
6182
6508
  #syncDisabled() {
6183
- const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
6509
+ const disabled =
6510
+ this.hasAttribute("disabled") &&
6511
+ this.getAttribute("disabled") !== "false";
6184
6512
  [...this.#inlinePickers, ...this.#expandedPickers].forEach((fp) => {
6185
6513
  if (disabled) fp.setAttribute("disabled", "");
6186
6514
  else fp.removeAttribute("disabled");
@@ -6314,7 +6642,9 @@ class FigInputGradient extends HTMLElement {
6314
6642
  const idx = parseInt(selected.dataset.stopIndex, 10);
6315
6643
  if (isNaN(idx) || !this.#gradient.stops[idx]) return;
6316
6644
  e.preventDefault();
6317
- const delta = (e.key === "ArrowRight" ? 1 : -1) * (e.shiftKey ? FigInputGradient.SHIFT_SNAP : 1);
6645
+ const delta =
6646
+ (e.key === "ArrowRight" ? 1 : -1) *
6647
+ (e.shiftKey ? FigInputGradient.SHIFT_SNAP : 1);
6318
6648
  const stop = this.#gradient.stops[idx];
6319
6649
  stop.position = Math.max(0, Math.min(100, stop.position + delta));
6320
6650
  selected.setAttribute("value", `${stop.position}% 50%`);
@@ -6582,10 +6912,12 @@ class FigInputGradient extends HTMLElement {
6582
6912
  const stopIdx = parseInt(clickedHandle?.dataset.stopIndex, 10);
6583
6913
  this.#distributeStops();
6584
6914
  if (!isNaN(stopIdx)) {
6585
- this.#track.querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)").forEach((h) => {
6586
- if (parseInt(h.dataset.stopIndex, 10) === stopIdx) h.select();
6587
- else h.deselect();
6588
- });
6915
+ this.#track
6916
+ .querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)")
6917
+ .forEach((h) => {
6918
+ if (parseInt(h.dataset.stopIndex, 10) === stopIdx) h.select();
6919
+ else h.deselect();
6920
+ });
6589
6921
  }
6590
6922
  e.stopPropagation();
6591
6923
  }
@@ -6724,7 +7056,10 @@ class FigInputGradient extends HTMLElement {
6724
7056
  e.stopPropagation();
6725
7057
 
6726
7058
  const trackRect = this.#track.getBoundingClientRect();
6727
- const pct = Math.max(0, Math.min(1, (e.clientX - trackRect.left) / trackRect.width));
7059
+ const pct = Math.max(
7060
+ 0,
7061
+ Math.min(1, (e.clientX - trackRect.left) / trackRect.width),
7062
+ );
6728
7063
  const position = Math.round(pct * 100);
6729
7064
  const color = this.#sampleGradientColor(pct);
6730
7065
  this.#gradient.stops.push({ position, color, opacity: 100 });
@@ -6743,19 +7078,23 @@ class FigInputGradient extends HTMLElement {
6743
7078
  );
6744
7079
  const newHandle = handles[newIndex];
6745
7080
  if (newHandle) {
6746
- this.#track.querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)").forEach((h) => {
6747
- if (h !== newHandle) h.deselect();
6748
- });
7081
+ this.#track
7082
+ .querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)")
7083
+ .forEach((h) => {
7084
+ if (h !== newHandle) h.deselect();
7085
+ });
6749
7086
  newHandle.select();
6750
- newHandle.dispatchEvent(new PointerEvent("pointerdown", {
6751
- bubbles: true,
6752
- clientX: e.clientX,
6753
- clientY: e.clientY,
6754
- pointerId: e.pointerId,
6755
- pointerType: e.pointerType,
6756
- button: e.button,
6757
- buttons: e.buttons,
6758
- }));
7087
+ newHandle.dispatchEvent(
7088
+ new PointerEvent("pointerdown", {
7089
+ bubbles: true,
7090
+ clientX: e.clientX,
7091
+ clientY: e.clientY,
7092
+ pointerId: e.pointerId,
7093
+ pointerType: e.pointerType,
7094
+ button: e.button,
7095
+ buttons: e.buttons,
7096
+ }),
7097
+ );
6759
7098
  }
6760
7099
  });
6761
7100
 
@@ -6770,7 +7109,10 @@ class FigInputGradient extends HTMLElement {
6770
7109
  if (e.detail.opacity !== undefined) {
6771
7110
  this.#gradient.stops[idx].opacity = e.detail.opacity;
6772
7111
  }
6773
- handle.setAttribute("color", this.#stopColorCSS(this.#gradient.stops[idx]));
7112
+ handle.setAttribute(
7113
+ "color",
7114
+ this.#stopColorCSS(this.#gradient.stops[idx]),
7115
+ );
6774
7116
  this.#syncChit();
6775
7117
  this.#emitInput();
6776
7118
  }
@@ -6785,7 +7127,9 @@ class FigInputGradient extends HTMLElement {
6785
7127
  let position = rawPosition;
6786
7128
  const trackW = this.#track.getBoundingClientRect().width;
6787
7129
  if (e.detail?.shiftKey) {
6788
- position = Math.round(position / FigInputGradient.SHIFT_SNAP) * FigInputGradient.SHIFT_SNAP;
7130
+ position =
7131
+ Math.round(position / FigInputGradient.SHIFT_SNAP) *
7132
+ FigInputGradient.SHIFT_SNAP;
6789
7133
  } else {
6790
7134
  const snapPct = trackW > 0 ? (5 / trackW) * 100 : 0;
6791
7135
  for (let i = 0; i < this.#gradient.stops.length; i++) {
@@ -6825,7 +7169,10 @@ class FigInputGradient extends HTMLElement {
6825
7169
  if (e.detail.opacity !== undefined) {
6826
7170
  this.#gradient.stops[idx].opacity = e.detail.opacity;
6827
7171
  }
6828
- handle.setAttribute("color", this.#stopColorCSS(this.#gradient.stops[idx]));
7172
+ handle.setAttribute(
7173
+ "color",
7174
+ this.#stopColorCSS(this.#gradient.stops[idx]),
7175
+ );
6829
7176
  this.#syncChit();
6830
7177
  this.#emitChange();
6831
7178
  }
@@ -7578,13 +7925,13 @@ class FigChit extends HTMLElement {
7578
7925
 
7579
7926
  if (this.#type === "color") {
7580
7927
  const hex = this.#toHex(bg);
7581
- this.innerHTML = `<input type="color" value="${hex}" />`;
7928
+ this.innerHTML = `<div></div><input type="color" value="${hex}" />`;
7582
7929
  this.input = this.querySelector("input");
7583
7930
  if (!isVar) {
7584
7931
  this.input.addEventListener("input", this.#boundHandleInput);
7585
7932
  }
7586
7933
  } else {
7587
- this.innerHTML = "";
7934
+ this.innerHTML = "<div></div>";
7588
7935
  this.input = null;
7589
7936
  }
7590
7937
  } else if (this.#type === "color" && this.input) {
@@ -7594,8 +7941,14 @@ class FigChit extends HTMLElement {
7594
7941
  }
7595
7942
  }
7596
7943
 
7597
- const isImage = /^(linear-gradient|radial-gradient|conic-gradient|repeating-|url)\s*\(/i.test(rawBg);
7598
- this.style.setProperty("--chit-background", isImage ? rawBg : `linear-gradient(${rawBg}, ${rawBg})`);
7944
+ const isImage =
7945
+ /^(linear-gradient|radial-gradient|conic-gradient|repeating-|url)\s*\(/i.test(
7946
+ rawBg,
7947
+ );
7948
+ this.style.setProperty(
7949
+ "--chit-background",
7950
+ isImage ? rawBg : `linear-gradient(${rawBg}, ${rawBg})`,
7951
+ );
7599
7952
  }
7600
7953
 
7601
7954
  #handleInput(e) {
@@ -7622,8 +7975,14 @@ class FigChit extends HTMLElement {
7622
7975
  if (oldValue === newValue) return;
7623
7976
  if (name === "background") {
7624
7977
  if (this.#internalUpdate) {
7625
- const isImg = /^(linear-gradient|radial-gradient|conic-gradient|repeating-|url)\s*\(/i.test(newValue);
7626
- this.style.setProperty("--chit-background", isImg ? newValue : `linear-gradient(${newValue}, ${newValue})`);
7978
+ const isImg =
7979
+ /^(linear-gradient|radial-gradient|conic-gradient|repeating-|url)\s*\(/i.test(
7980
+ newValue,
7981
+ );
7982
+ this.style.setProperty(
7983
+ "--chit-background",
7984
+ isImg ? newValue : `linear-gradient(${newValue}, ${newValue})`,
7985
+ );
7627
7986
  return;
7628
7987
  }
7629
7988
  this.#render();
@@ -7645,9 +8004,7 @@ class FigChit extends HTMLElement {
7645
8004
  }
7646
8005
  }
7647
8006
  customElements.define("fig-chit", FigChit);
7648
- class FigSwatch extends FigChit{
7649
-
7650
- }
8007
+ class FigSwatch extends FigChit {}
7651
8008
  customElements.define("fig-swatch", FigSwatch);
7652
8009
 
7653
8010
  /* Upload */
@@ -7905,6 +8262,220 @@ class FigImage extends HTMLElement {
7905
8262
  }
7906
8263
  customElements.define("fig-image", FigImage);
7907
8264
 
8265
+ /* File Upload Input */
8266
+ class FigInputFile extends HTMLElement {
8267
+ static observedAttributes = ["accepts", "label", "disabled", "multiple"];
8268
+
8269
+ #fileInput = null;
8270
+ #filenameEl = null;
8271
+ #clearBtn = null;
8272
+ #tooltipEl = null;
8273
+ #uploadBtn = null;
8274
+ #files = null;
8275
+
8276
+ get files() {
8277
+ return this.#files;
8278
+ }
8279
+
8280
+ get value() {
8281
+ if (!this.#files || this.#files.length === 0) return "";
8282
+ if (this.#files.length === 1) return this.#files[0].name;
8283
+ return `${this.#files.length} files`;
8284
+ }
8285
+
8286
+ connectedCallback() {
8287
+ this.#render();
8288
+ this.addEventListener("dragover", this.#onDragOver);
8289
+ this.addEventListener("dragleave", this.#onDragLeave);
8290
+ this.addEventListener("drop", this.#onDrop);
8291
+ }
8292
+
8293
+ disconnectedCallback() {
8294
+ this.#fileInput?.removeEventListener("change", this.#onFileChange);
8295
+ this.#clearBtn?.removeEventListener("click", this.#onClear);
8296
+ this.removeEventListener("dragover", this.#onDragOver);
8297
+ this.removeEventListener("dragleave", this.#onDragLeave);
8298
+ this.removeEventListener("drop", this.#onDrop);
8299
+ }
8300
+
8301
+ attributeChangedCallback(name, oldValue, newValue) {
8302
+ if (oldValue === newValue) return;
8303
+ this.#render();
8304
+ }
8305
+
8306
+ clear() {
8307
+ this.#files = null;
8308
+ if (this.#fileInput) this.#fileInput.value = "";
8309
+ this.#render();
8310
+ this.#emitEvents();
8311
+ }
8312
+
8313
+ #emitEvents() {
8314
+ const detail = { files: this.#files };
8315
+ this.dispatchEvent(new CustomEvent("input", { detail, bubbles: true }));
8316
+ this.dispatchEvent(new CustomEvent("change", { detail, bubbles: true }));
8317
+ }
8318
+
8319
+ #onFileChange = () => {
8320
+ if (this.#fileInput.files.length > 0) {
8321
+ this.#files = this.#fileInput.files;
8322
+ this.#render();
8323
+ this.#emitEvents();
8324
+ }
8325
+ };
8326
+
8327
+ #onClear = (e) => {
8328
+ e.stopPropagation();
8329
+ this.clear();
8330
+ };
8331
+
8332
+ #onDragOver = (e) => {
8333
+ e.preventDefault();
8334
+ this.setAttribute("dragover", "");
8335
+ };
8336
+
8337
+ #onDragLeave = () => {
8338
+ this.removeAttribute("dragover");
8339
+ };
8340
+
8341
+ #onDrop = (e) => {
8342
+ e.preventDefault();
8343
+ this.removeAttribute("dragover");
8344
+ if (
8345
+ this.hasAttribute("disabled") &&
8346
+ this.getAttribute("disabled") !== "false"
8347
+ )
8348
+ return;
8349
+
8350
+ const accepts = this.getAttribute("accepts");
8351
+ let dropped = Array.from(e.dataTransfer.files);
8352
+ if (accepts) {
8353
+ const allowed = accepts.split(",").map((s) => s.trim().toLowerCase());
8354
+ dropped = dropped.filter((file) => {
8355
+ const ext = "." + file.name.split(".").pop().toLowerCase();
8356
+ const mime = file.type.toLowerCase();
8357
+ return allowed.some(
8358
+ (a) =>
8359
+ a === ext ||
8360
+ a === mime ||
8361
+ (a.endsWith("/*") && mime.startsWith(a.slice(0, -1))),
8362
+ );
8363
+ });
8364
+ }
8365
+ if (!this.hasAttribute("multiple")) {
8366
+ dropped = dropped.slice(0, 1);
8367
+ }
8368
+ if (dropped.length === 0) return;
8369
+
8370
+ const dt = new DataTransfer();
8371
+ dropped.forEach((f) => dt.items.add(f));
8372
+ this.#files = dt.files;
8373
+ if (this.#fileInput) {
8374
+ this.#fileInput.files = dt.files;
8375
+ }
8376
+ this.#render();
8377
+ this.#emitEvents();
8378
+ };
8379
+
8380
+ #render() {
8381
+ const accepts = this.getAttribute("accepts") || "";
8382
+ const label = this.getAttribute("label") || "Upload";
8383
+ const disabled =
8384
+ this.hasAttribute("disabled") &&
8385
+ this.getAttribute("disabled") !== "false";
8386
+ const multiple = this.hasAttribute("multiple");
8387
+ const hasFile = this.#files && this.#files.length > 0;
8388
+
8389
+ this.innerHTML = "";
8390
+
8391
+ if (hasFile) {
8392
+ const tooltipText = accepts
8393
+ ? `Accepts ${accepts
8394
+ .split(",")
8395
+ .map((s) => s.trim())
8396
+ .join(", ")}`
8397
+ : "";
8398
+
8399
+ this.#uploadBtn = document.createElement("fig-button");
8400
+ this.#uploadBtn.setAttribute("variant", "input");
8401
+ this.#uploadBtn.setAttribute("type", "upload");
8402
+ this.#uploadBtn.className = "fig-input-file-filename";
8403
+ if (disabled) this.#uploadBtn.setAttribute("disabled", "");
8404
+ const truncEl = document.createElement("fig-truncate");
8405
+ truncEl.setAttribute("position", "middle");
8406
+ truncEl.setAttribute("tooltip", "");
8407
+ const filename = this.value;
8408
+ const dotIdx = filename.lastIndexOf(".");
8409
+ if (dotIdx > 0) truncEl.setAttribute("tail", filename.slice(dotIdx));
8410
+ truncEl.textContent = filename;
8411
+ this.#uploadBtn.appendChild(truncEl);
8412
+
8413
+ this.#fileInput = document.createElement("input");
8414
+ this.#fileInput.type = "file";
8415
+ this.#fileInput.title = "";
8416
+ if (accepts) this.#fileInput.setAttribute("accept", accepts);
8417
+ if (multiple) this.#fileInput.setAttribute("multiple", "");
8418
+ this.#fileInput.addEventListener("change", this.#onFileChange);
8419
+ this.#uploadBtn.appendChild(this.#fileInput);
8420
+
8421
+ if (tooltipText) {
8422
+ this.#tooltipEl = document.createElement("fig-tooltip");
8423
+ this.#tooltipEl.setAttribute("text", tooltipText);
8424
+ this.#tooltipEl.appendChild(this.#uploadBtn);
8425
+ this.appendChild(this.#tooltipEl);
8426
+ } else {
8427
+ this.appendChild(this.#uploadBtn);
8428
+ }
8429
+
8430
+ const clearTooltip = document.createElement("fig-tooltip");
8431
+ clearTooltip.setAttribute("text", "Remove");
8432
+ this.#clearBtn = document.createElement("fig-button");
8433
+ this.#clearBtn.setAttribute("variant", "ghost");
8434
+ this.#clearBtn.setAttribute("icon", "true");
8435
+ this.#clearBtn.className = "fig-input-file-clear";
8436
+ if (disabled) this.#clearBtn.setAttribute("disabled", "");
8437
+ this.#clearBtn.innerHTML = `<span class="fig-mask-icon" style="--icon: var(--icon-minus);"></span>`;
8438
+ this.#clearBtn.addEventListener("click", this.#onClear);
8439
+ clearTooltip.appendChild(this.#clearBtn);
8440
+ this.appendChild(clearTooltip);
8441
+ } else {
8442
+ const tooltipText = accepts
8443
+ ? `Accepts ${accepts
8444
+ .split(",")
8445
+ .map((s) => s.trim())
8446
+ .join(", ")}`
8447
+ : "";
8448
+
8449
+ if (tooltipText) {
8450
+ this.#tooltipEl = document.createElement("fig-tooltip");
8451
+ this.#tooltipEl.setAttribute("text", tooltipText);
8452
+ }
8453
+
8454
+ this.#uploadBtn = document.createElement("fig-button");
8455
+ this.#uploadBtn.setAttribute("variant", "input");
8456
+ this.#uploadBtn.setAttribute("type", "upload");
8457
+ this.#uploadBtn.textContent = label;
8458
+ if (disabled) this.#uploadBtn.setAttribute("disabled", "");
8459
+
8460
+ this.#fileInput = document.createElement("input");
8461
+ this.#fileInput.type = "file";
8462
+ this.#fileInput.title = "";
8463
+ if (accepts) this.#fileInput.setAttribute("accept", accepts);
8464
+ if (multiple) this.#fileInput.setAttribute("multiple", "");
8465
+ this.#fileInput.addEventListener("change", this.#onFileChange);
8466
+ this.#uploadBtn.appendChild(this.#fileInput);
8467
+
8468
+ if (this.#tooltipEl) {
8469
+ this.#tooltipEl.appendChild(this.#uploadBtn);
8470
+ this.appendChild(this.#tooltipEl);
8471
+ } else {
8472
+ this.appendChild(this.#uploadBtn);
8473
+ }
8474
+ }
8475
+ }
8476
+ }
8477
+ customElements.define("fig-input-file", FigInputFile);
8478
+
7908
8479
  /**
7909
8480
  * A bezier / spring easing curve editor with draggable control points.
7910
8481
  * @attr {string} value - Bezier: "0.42, 0, 0.58, 1" or Spring: "spring(200, 15, 1)"
@@ -8828,7 +9399,11 @@ class Fig3DRotate extends HTMLElement {
8828
9399
  this.#precision = parseInt(this.getAttribute("precision") || "1");
8829
9400
  figSyncCssVar(this, "--aspect-ratio", this.getAttribute("aspect-ratio"));
8830
9401
  figSyncCssVar(this, "--perspective", this.getAttribute("perspective"));
8831
- figSyncCssVar(this, "--perspective-origin", this.getAttribute("perspective-origin"));
9402
+ figSyncCssVar(
9403
+ this,
9404
+ "--perspective-origin",
9405
+ this.getAttribute("perspective-origin"),
9406
+ );
8832
9407
  this.#syncTransformOrigin(this.getAttribute("transform-origin"));
8833
9408
  this.#parseFields(this.getAttribute("fields"));
8834
9409
  const val = this.getAttribute("value");
@@ -8846,8 +9421,6 @@ class Fig3DRotate extends HTMLElement {
8846
9421
  }
8847
9422
  }
8848
9423
 
8849
-
8850
-
8851
9424
  #syncTransformOrigin(value) {
8852
9425
  if (!value || !value.trim()) {
8853
9426
  this.style.removeProperty("--transform-origin");
@@ -10682,6 +11255,117 @@ class FigLayer extends HTMLElement {
10682
11255
  }
10683
11256
  customElements.define("fig-layer", FigLayer);
10684
11257
 
11258
+ // FigGroup
11259
+ class FigGroup extends HTMLElement {
11260
+ static observedAttributes = ["name", "collapsible"];
11261
+
11262
+ #header = null;
11263
+ #chevron = null;
11264
+
11265
+ connectedCallback() {
11266
+ requestAnimationFrame(() => this.#render());
11267
+ }
11268
+
11269
+ disconnectedCallback() {
11270
+ if (this.#chevron) {
11271
+ this.#chevron.removeEventListener("click", this.#handleToggle);
11272
+ }
11273
+ if (this.#header) {
11274
+ this.#header.removeEventListener("click", this.#handleToggle);
11275
+ }
11276
+ }
11277
+
11278
+ attributeChangedCallback(name, oldValue, newValue) {
11279
+ if (oldValue === newValue) return;
11280
+ this.#render();
11281
+ }
11282
+
11283
+ get open() {
11284
+ const attr = this.getAttribute("open");
11285
+ return attr !== null && attr !== "false";
11286
+ }
11287
+
11288
+ set open(value) {
11289
+ const was = this.open;
11290
+ if (value) {
11291
+ this.setAttribute("open", "true");
11292
+ } else {
11293
+ this.setAttribute("open", "false");
11294
+ }
11295
+ if (was !== !!value) {
11296
+ this.dispatchEvent(
11297
+ new CustomEvent("openchange", {
11298
+ detail: { open: !!value },
11299
+ bubbles: true,
11300
+ }),
11301
+ );
11302
+ }
11303
+ }
11304
+
11305
+ #handleToggle = (e) => {
11306
+ e.stopPropagation();
11307
+ this.open = !this.open;
11308
+ };
11309
+
11310
+ #render() {
11311
+ const isCollapsible = this.hasAttribute("collapsible");
11312
+ const nameAttr = this.getAttribute("name");
11313
+ const label = nameAttr || (isCollapsible ? "Group" : null);
11314
+
11315
+ // Check if user supplied their own fig-header
11316
+ const userHeader = this.querySelector(":scope > fig-header");
11317
+
11318
+ if (!label && !isCollapsible && !userHeader) {
11319
+ if (this.#header && this.#header.dataset.generated) {
11320
+ this.#header.remove();
11321
+ this.#header = null;
11322
+ this.#chevron = null;
11323
+ }
11324
+ return;
11325
+ }
11326
+
11327
+ if (userHeader) {
11328
+ this.#header = userHeader;
11329
+ } else if (!this.#header || !this.#header.dataset.generated) {
11330
+ this.#header = document.createElement("fig-header");
11331
+ this.#header.setAttribute("borderless", "");
11332
+ this.#header.dataset.generated = "true";
11333
+ this.prepend(this.#header);
11334
+ }
11335
+
11336
+ // Ensure h3 exists inside header
11337
+ let h3 = this.#header.querySelector("h3");
11338
+ if (!h3) {
11339
+ h3 = document.createElement("h3");
11340
+ this.#header.prepend(h3);
11341
+ }
11342
+ if (this.#header.dataset.generated) {
11343
+ h3.textContent = label;
11344
+ }
11345
+
11346
+ if (isCollapsible) {
11347
+ if (!h3.querySelector(".fig-group-chevron")) {
11348
+ const chevron = document.createElement("span");
11349
+ chevron.className = "fig-mask-icon fig-group-chevron";
11350
+ h3.prepend(chevron);
11351
+ }
11352
+ this.#chevron = h3.querySelector(".fig-group-chevron");
11353
+ h3.addEventListener("click", this.#handleToggle);
11354
+
11355
+ if (!this.hasAttribute("open")) {
11356
+ this.setAttribute("open", "false");
11357
+ }
11358
+ } else {
11359
+ if (this.#chevron) {
11360
+ this.#chevron.remove();
11361
+ this.#chevron = null;
11362
+ }
11363
+ this.removeAttribute("open");
11364
+ }
11365
+ }
11366
+ }
11367
+ customElements.define("fig-group", FigGroup);
11368
+
10685
11369
  // FigFillPicker
10686
11370
  /**
10687
11371
  * A comprehensive fill picker component supporting solid colors, gradients, images, video, and webcam.
@@ -11114,10 +11798,15 @@ class FigFillPicker extends HTMLElement {
11114
11798
  this.#dialog.addEventListener("close", onDialogClose);
11115
11799
 
11116
11800
  this.#dialogOpenObserver = new MutationObserver(() => {
11117
- const isOpen = this.#dialog.hasAttribute("open") && this.#dialog.getAttribute("open") !== "false";
11801
+ const isOpen =
11802
+ this.#dialog.hasAttribute("open") &&
11803
+ this.#dialog.getAttribute("open") !== "false";
11118
11804
  if (!isOpen) onDialogClose();
11119
11805
  });
11120
- this.#dialogOpenObserver.observe(this.#dialog, { attributes: true, attributeFilter: ["open"] });
11806
+ this.#dialogOpenObserver.observe(this.#dialog, {
11807
+ attributes: true,
11808
+ attributeFilter: ["open"],
11809
+ });
11121
11810
 
11122
11811
  // Initialize built-in tabs (skip any overridden by custom slots)
11123
11812
  const builtinInits = {
@@ -11213,6 +11902,8 @@ class FigFillPicker extends HTMLElement {
11213
11902
  <fig-preview class="fig-fill-picker-color-area">
11214
11903
  <canvas width="200" height="200"></canvas>
11215
11904
  <fig-handle
11905
+ type="color"
11906
+ color="${this.#hsvToHex({ ...this.#color, a: 1 })}"
11216
11907
  drag
11217
11908
  drag-surface=".fig-fill-picker-color-area"
11218
11909
  drag-axes="x,y"
@@ -11875,7 +12566,9 @@ class FigFillPicker extends HTMLElement {
11875
12566
  });
11876
12567
 
11877
12568
  // Embedded gradient bar input
11878
- const gradientBarInput = container.querySelector(".fig-fill-picker-gradient-bar-input");
12569
+ const gradientBarInput = container.querySelector(
12570
+ ".fig-fill-picker-gradient-bar-input",
12571
+ );
11879
12572
  if (gradientBarInput) {
11880
12573
  const syncFromBarInput = (e) => {
11881
12574
  e.stopPropagation();
@@ -11948,7 +12641,10 @@ class FigFillPicker extends HTMLElement {
11948
12641
  if (barInput) {
11949
12642
  barInput.setAttribute(
11950
12643
  "value",
11951
- JSON.stringify({ type: "gradient", gradient: gradientToValueShape(this.#gradient) }),
12644
+ JSON.stringify({
12645
+ type: "gradient",
12646
+ gradient: gradientToValueShape(this.#gradient),
12647
+ }),
11952
12648
  );
11953
12649
  }
11954
12650
 
@@ -11977,7 +12673,8 @@ class FigFillPicker extends HTMLElement {
11977
12673
  if (colorInput) colorInput.setAttribute("value", stop.color);
11978
12674
  const removeBtn = row.querySelector(".fig-fill-picker-stop-remove");
11979
12675
  if (removeBtn) {
11980
- if (this.#gradient.stops.length <= 2) removeBtn.setAttribute("disabled", "");
12676
+ if (this.#gradient.stops.length <= 2)
12677
+ removeBtn.setAttribute("disabled", "");
11981
12678
  else removeBtn.removeAttribute("disabled");
11982
12679
  }
11983
12680
  });
@@ -12910,9 +13607,14 @@ class FigColorTip extends HTMLElement {
12910
13607
  const color = this.#normalizeColor(rawValue);
12911
13608
  const alpha = this.#extractAlpha(rawValue);
12912
13609
  const alphaAttr = this.#alphaEnabled ? "" : 'alpha="false"';
12913
- const pickerValue = alpha < 1
12914
- ? JSON.stringify({ type: "solid", color, opacity: Math.round(alpha * 100) })
12915
- : JSON.stringify({ type: "solid", color });
13610
+ const pickerValue =
13611
+ alpha < 1
13612
+ ? JSON.stringify({
13613
+ type: "solid",
13614
+ color,
13615
+ opacity: Math.round(alpha * 100),
13616
+ })
13617
+ : JSON.stringify({ type: "solid", color });
12916
13618
  this.innerHTML = `
12917
13619
  <fig-fill-picker mode="solid" ${alphaAttr} value='${pickerValue}'>
12918
13620
  <fig-chit background="${color}"></fig-chit>
@@ -12955,7 +13657,9 @@ class FigColorTip extends HTMLElement {
12955
13657
  if (v.startsWith("#") && v.length === 9) {
12956
13658
  return parseInt(v.slice(7, 9), 16) / 255;
12957
13659
  }
12958
- const rgbaMatch = v.match(/rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*([\d.]+)\s*\)/i);
13660
+ const rgbaMatch = v.match(
13661
+ /rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*([\d.]+)\s*\)/i,
13662
+ );
12959
13663
  if (rgbaMatch) return parseFloat(rgbaMatch[1]);
12960
13664
  return 1;
12961
13665
  }
@@ -13012,9 +13716,10 @@ class FigColorTip extends HTMLElement {
13012
13716
  }
13013
13717
 
13014
13718
  if (this.#fillPicker) {
13015
- const pickerVal = alpha < 1
13016
- ? { type: "solid", color, opacity: Math.round(alpha * 100) }
13017
- : { type: "solid", color };
13719
+ const pickerVal =
13720
+ alpha < 1
13721
+ ? { type: "solid", color, opacity: Math.round(alpha * 100) }
13722
+ : { type: "solid", color };
13018
13723
  this.#fillPicker.setAttribute("value", JSON.stringify(pickerVal));
13019
13724
  if (this.#alphaEnabled) {
13020
13725
  this.#fillPicker.removeAttribute("alpha");
@@ -13192,7 +13897,15 @@ class FigChooser extends HTMLElement {
13192
13897
  }
13193
13898
 
13194
13899
  static get observedAttributes() {
13195
- return ["value", "disabled", "choice-element", "drag", "overflow", "loop", "padding"];
13900
+ return [
13901
+ "value",
13902
+ "disabled",
13903
+ "choice-element",
13904
+ "drag",
13905
+ "overflow",
13906
+ "loop",
13907
+ "padding",
13908
+ ];
13196
13909
  }
13197
13910
 
13198
13911
  get #overflowMode() {
@@ -13818,7 +14531,13 @@ class FigCanvasControl extends HTMLElement {
13818
14531
 
13819
14532
  attributeChangedCallback(name, oldVal, newVal) {
13820
14533
  if (oldVal === newVal) return;
13821
- if (name === "value" && !this.#isDragging && !this.#isSecondDragging && !this.#isRadiusDragging && !this.#isAngleDragging) {
14534
+ if (
14535
+ name === "value" &&
14536
+ !this.#isDragging &&
14537
+ !this.#isSecondDragging &&
14538
+ !this.#isRadiusDragging &&
14539
+ !this.#isAngleDragging
14540
+ ) {
13822
14541
  this.#parseValue();
13823
14542
  if (this.#pointHandle) this.#syncPositions();
13824
14543
  else this.#render();
@@ -13839,11 +14558,14 @@ class FigCanvasControl extends HTMLElement {
13839
14558
  }
13840
14559
  if (name === "snapping" && this.#pointHandle) {
13841
14560
  this.#pointHandle.setAttribute("drag-snapping", newVal || "false");
13842
- if (this.#secondHandle) this.#secondHandle.setAttribute("drag-snapping", newVal || "false");
14561
+ if (this.#secondHandle)
14562
+ this.#secondHandle.setAttribute("drag-snapping", newVal || "false");
13843
14563
  }
13844
14564
  if (name === "name") {
13845
- if (this.#pointTooltip) this.#pointTooltip.setAttribute("text", this.#pointTipText);
13846
- if (this.#secondTooltip) this.#secondTooltip.setAttribute("text", this.#secondTipText);
14565
+ if (this.#pointTooltip)
14566
+ this.#pointTooltip.setAttribute("text", this.#pointTipText);
14567
+ if (this.#secondTooltip)
14568
+ this.#secondTooltip.setAttribute("text", this.#secondTipText);
13847
14569
  }
13848
14570
  }
13849
14571
 
@@ -13868,13 +14590,16 @@ class FigCanvasControl extends HTMLElement {
13868
14590
  if (typeof v.angle === "number") this.#angle = v.angle;
13869
14591
  if (typeof v.x2 === "number") this.#x2 = v.x2;
13870
14592
  if (typeof v.y2 === "number") this.#y2 = v.y2;
13871
- } catch { /* ignore */ }
14593
+ } catch {
14594
+ /* ignore */
14595
+ }
13872
14596
  }
13873
14597
 
13874
14598
  get value() {
13875
14599
  const v = { x: this.#x, y: this.#y };
13876
14600
  if (this.#type === "color") {
13877
- const color = this.getAttribute("color") || this.#pointHandle?.getAttribute("color");
14601
+ const color =
14602
+ this.getAttribute("color") || this.#pointHandle?.getAttribute("color");
13878
14603
  if (color) v.color = color;
13879
14604
  }
13880
14605
  if (this.#hasRadius) {
@@ -14175,11 +14900,15 @@ class FigCanvasControl extends HTMLElement {
14175
14900
  }
14176
14901
 
14177
14902
  #emitInput() {
14178
- this.dispatchEvent(new CustomEvent("input", { bubbles: true, detail: this.value }));
14903
+ this.dispatchEvent(
14904
+ new CustomEvent("input", { bubbles: true, detail: this.value }),
14905
+ );
14179
14906
  }
14180
14907
 
14181
14908
  #emitChange() {
14182
- this.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: this.value }));
14909
+ this.dispatchEvent(
14910
+ new CustomEvent("change", { bubbles: true, detail: this.value }),
14911
+ );
14183
14912
  }
14184
14913
 
14185
14914
  #syncValueAttribute() {
@@ -14211,7 +14940,9 @@ class FigCanvasControl extends HTMLElement {
14211
14940
  }
14212
14941
  this.#syncPositions();
14213
14942
  if (this.#hasSecondPoint) {
14214
- document.body.style.cursor = this.#resizeCursorSvg(this.#pointPointLineDeg());
14943
+ document.body.style.cursor = this.#resizeCursorSvg(
14944
+ this.#pointPointLineDeg(),
14945
+ );
14215
14946
  }
14216
14947
  this.#emitInput();
14217
14948
  });
@@ -14227,14 +14958,17 @@ class FigCanvasControl extends HTMLElement {
14227
14958
  const py = e.detail?.py ?? this.#y / 100;
14228
14959
  this.#x = Math.round(Math.max(0, Math.min(100, px * 100)));
14229
14960
  this.#y = Math.round(Math.max(0, Math.min(100, py * 100)));
14230
- if (this.#pointTooltip && this.#type !== "color") this.#pointTooltip.removeAttribute("show");
14961
+ if (this.#pointTooltip && this.#type !== "color")
14962
+ this.#pointTooltip.removeAttribute("show");
14231
14963
  if (this.#hasSecondPoint) {
14232
14964
  document.body.style.cursor = this.#prevBodyCursor ?? "";
14233
14965
  }
14234
14966
  this.#syncPositions();
14235
14967
  this.#syncValueAttribute();
14236
14968
  this.#emitChange();
14237
- requestAnimationFrame(() => { this.#isDragging = false; });
14969
+ requestAnimationFrame(() => {
14970
+ this.#isDragging = false;
14971
+ });
14238
14972
  });
14239
14973
 
14240
14974
  if (this.#angleHandle) {
@@ -14251,8 +14985,8 @@ class FigCanvasControl extends HTMLElement {
14251
14985
  const hy = e.detail?.y ?? 0;
14252
14986
  const hw = this.#angleHandle.offsetWidth / 2;
14253
14987
  const hh = this.#angleHandle.offsetHeight / 2;
14254
- const dx = (hx + hw) - cx;
14255
- const dy = (hy + hh) - cy;
14988
+ const dx = hx + hw - cx;
14989
+ const dy = hy + hh - cy;
14256
14990
  let angle = (Math.atan2(dy, dx) * 180) / Math.PI;
14257
14991
  if (this.#shouldSnap(e.detail?.shiftKey)) {
14258
14992
  angle = Math.round(angle / 15) * 15;
@@ -14277,7 +15011,10 @@ class FigCanvasControl extends HTMLElement {
14277
15011
  }
14278
15012
 
14279
15013
  if (this.#angleTooltip) {
14280
- this.#angleTooltip.setAttribute("text", `Angle ${Math.round(this.#angle)}°`);
15014
+ this.#angleTooltip.setAttribute(
15015
+ "text",
15016
+ `Angle ${Math.round(this.#angle)}°`,
15017
+ );
14281
15018
  this.#angleTooltip.setAttribute("show", "true");
14282
15019
  this.#angleTooltip.showPopup?.();
14283
15020
  }
@@ -14292,7 +15029,9 @@ class FigCanvasControl extends HTMLElement {
14292
15029
  this.#syncPositions();
14293
15030
  this.#syncValueAttribute();
14294
15031
  this.#emitChange();
14295
- requestAnimationFrame(() => { this.#isAngleDragging = false; });
15032
+ requestAnimationFrame(() => {
15033
+ this.#isAngleDragging = false;
15034
+ });
14296
15035
  });
14297
15036
 
14298
15037
  this.#angleHandle.addEventListener("hitareadown", (e) => {
@@ -14325,7 +15064,11 @@ class FigCanvasControl extends HTMLElement {
14325
15064
  angle = Math.round(angle / 15) * 15;
14326
15065
  }
14327
15066
  this.#angle = angle;
14328
- if (this.#angleTooltip) this.#angleTooltip.setAttribute("text", `Angle ${Math.round(angle)}°`);
15067
+ if (this.#angleTooltip)
15068
+ this.#angleTooltip.setAttribute(
15069
+ "text",
15070
+ `Angle ${Math.round(angle)}°`,
15071
+ );
14329
15072
  this.#syncPositions();
14330
15073
  const curDeg = Math.round(angle);
14331
15074
  if (curDeg !== lastCursorDeg) {
@@ -14368,7 +15111,9 @@ class FigCanvasControl extends HTMLElement {
14368
15111
  this.#secondTooltip.showPopup?.();
14369
15112
  }
14370
15113
  this.#syncPositions();
14371
- document.body.style.cursor = this.#resizeCursorSvg(this.#pointPointLineDeg());
15114
+ document.body.style.cursor = this.#resizeCursorSvg(
15115
+ this.#pointPointLineDeg(),
15116
+ );
14372
15117
  this.#emitInput();
14373
15118
  });
14374
15119
 
@@ -14379,7 +15124,9 @@ class FigCanvasControl extends HTMLElement {
14379
15124
  this.#syncPositions();
14380
15125
  this.#syncValueAttribute();
14381
15126
  this.#emitChange();
14382
- requestAnimationFrame(() => { this.#isSecondDragging = false; });
15127
+ requestAnimationFrame(() => {
15128
+ this.#isSecondDragging = false;
15129
+ });
14383
15130
  });
14384
15131
 
14385
15132
  this.#setupPointPointHitArea(this.#pointHandle, true);
@@ -14443,7 +15190,9 @@ class FigCanvasControl extends HTMLElement {
14443
15190
  this.#y2 = newPctY;
14444
15191
  }
14445
15192
  this.#syncPositions();
14446
- const curDeg = Math.round(isFirst ? this.#pointPointLineDeg() + 180 : this.#pointPointLineDeg());
15193
+ const curDeg = Math.round(
15194
+ isFirst ? this.#pointPointLineDeg() + 180 : this.#pointPointLineDeg(),
15195
+ );
14447
15196
  if (curDeg !== lastCursorDeg) {
14448
15197
  lastCursorDeg = curDeg;
14449
15198
  document.body.style.cursor = this.#rotateCursorSvg(curDeg);
@@ -14475,7 +15224,10 @@ class FigCanvasControl extends HTMLElement {
14475
15224
  const rect = container.getBoundingClientRect();
14476
15225
  const cx = (this.#x / 100) * rect.width;
14477
15226
  const cy = (this.#y / 100) * rect.height;
14478
- const deg = (Math.atan2(e.clientY - rect.top - cy, e.clientX - rect.left - cx) * 180) / Math.PI;
15227
+ const deg =
15228
+ (Math.atan2(e.clientY - rect.top - cy, e.clientX - rect.left - cx) *
15229
+ 180) /
15230
+ Math.PI;
14479
15231
  circle.style.cursor = this.#resizeCursorSvg(deg);
14480
15232
  });
14481
15233
  const onDown = (e) => {
@@ -14497,7 +15249,10 @@ class FigCanvasControl extends HTMLElement {
14497
15249
  const rect0 = container.getBoundingClientRect();
14498
15250
  const cx0 = (this.#x / 100) * rect0.width;
14499
15251
  const cy0 = (this.#y / 100) * rect0.height;
14500
- const initDeg = (Math.atan2(e.clientY - rect0.top - cy0, e.clientX - rect0.left - cx0) * 180) / Math.PI;
15252
+ const initDeg =
15253
+ (Math.atan2(e.clientY - rect0.top - cy0, e.clientX - rect0.left - cx0) *
15254
+ 180) /
15255
+ Math.PI;
14501
15256
  let lastCursorDeg = Math.round(initDeg);
14502
15257
  document.body.style.cursor = this.#resizeCursorSvg(lastCursorDeg);
14503
15258
 
@@ -14528,7 +15283,8 @@ class FigCanvasControl extends HTMLElement {
14528
15283
  } else {
14529
15284
  this.#radius = Math.max(0, dist);
14530
15285
  }
14531
- if (this.#radiusTooltip) this.#radiusTooltip.setAttribute("text", this.#formatRadius());
15286
+ if (this.#radiusTooltip)
15287
+ this.#radiusTooltip.setAttribute("text", this.#formatRadius());
14532
15288
  this.#syncPositions();
14533
15289
  this.#emitInput();
14534
15290
  };
@@ -14549,7 +15305,8 @@ class FigCanvasControl extends HTMLElement {
14549
15305
  window.addEventListener("pointerup", onUp);
14550
15306
  };
14551
15307
  circle.addEventListener("pointerdown", onDown);
14552
- this._radiusDragCleanup = () => circle.removeEventListener("pointerdown", onDown);
15308
+ this._radiusDragCleanup = () =>
15309
+ circle.removeEventListener("pointerdown", onDown);
14553
15310
  }
14554
15311
 
14555
15312
  #teardownRadiusDrag() {
@@ -14711,8 +15468,10 @@ class FigHandle extends HTMLElement {
14711
15468
  };
14712
15469
 
14713
15470
  const axes = this.#axes;
14714
- if (axes.x) this.style.left = `${Math.round(resolve(xToken, rect.width, hw))}px`;
14715
- if (axes.y) this.style.top = `${Math.round(resolve(yToken, rect.height, hh))}px`;
15471
+ if (axes.x)
15472
+ this.style.left = `${Math.round(resolve(xToken, rect.width, hw))}px`;
15473
+ if (axes.y)
15474
+ this.style.top = `${Math.round(resolve(yToken, rect.height, hh))}px`;
14716
15475
  }
14717
15476
 
14718
15477
  #syncValueAttribute() {
@@ -14729,16 +15488,25 @@ class FigHandle extends HTMLElement {
14729
15488
  const raw = this.getAttribute("hit-area");
14730
15489
  if (!raw) return null;
14731
15490
  const tokens = raw.trim().split(/\s+/);
14732
- let vPad = 0, hPad = 0, circle = false;
15491
+ let vPad = 0,
15492
+ hPad = 0,
15493
+ circle = false;
14733
15494
  const nums = [];
14734
15495
  for (const t of tokens) {
14735
- if (t === "circle") { circle = true; continue; }
15496
+ if (t === "circle") {
15497
+ circle = true;
15498
+ continue;
15499
+ }
14736
15500
  const n = parseFloat(t);
14737
15501
  if (Number.isFinite(n)) nums.push(n);
14738
15502
  }
14739
- if (nums.length >= 2) { vPad = nums[0]; hPad = nums[1]; }
14740
- else if (nums.length === 1) { vPad = nums[0]; hPad = nums[0]; }
14741
- else return null;
15503
+ if (nums.length >= 2) {
15504
+ vPad = nums[0];
15505
+ hPad = nums[1];
15506
+ } else if (nums.length === 1) {
15507
+ vPad = nums[0];
15508
+ hPad = nums[0];
15509
+ } else return null;
14742
15510
  return { vPad, hPad, circle };
14743
15511
  }
14744
15512
 
@@ -14759,7 +15527,10 @@ class FigHandle extends HTMLElement {
14759
15527
  this.prepend(el);
14760
15528
  this.#hitAreaEl = el;
14761
15529
  }
14762
- this.style.setProperty("--fig-handle-hit-area-size", String(parsed.hPad * 2));
15530
+ this.style.setProperty(
15531
+ "--fig-handle-hit-area-size",
15532
+ String(parsed.hPad * 2),
15533
+ );
14763
15534
  if (parsed.circle) {
14764
15535
  this.#hitAreaEl.style.borderRadius = "50%";
14765
15536
  } else {
@@ -14773,10 +15544,12 @@ class FigHandle extends HTMLElement {
14773
15544
  if (this.#hitAreaMode === "delegate") {
14774
15545
  e.preventDefault();
14775
15546
  e.stopPropagation();
14776
- this.dispatchEvent(new CustomEvent("hitareadown", {
14777
- bubbles: true,
14778
- detail: { originalEvent: e },
14779
- }));
15547
+ this.dispatchEvent(
15548
+ new CustomEvent("hitareadown", {
15549
+ bubbles: true,
15550
+ detail: { originalEvent: e },
15551
+ }),
15552
+ );
14780
15553
  } else {
14781
15554
  this.#onPointerDown(e);
14782
15555
  }
@@ -14796,7 +15569,10 @@ class FigHandle extends HTMLElement {
14796
15569
  disconnectedCallback() {
14797
15570
  this.#teardownDrag();
14798
15571
  this.#hideColorTip();
14799
- if (this.#hitAreaEl) { this.#hitAreaEl.remove(); this.#hitAreaEl = null; }
15572
+ if (this.#hitAreaEl) {
15573
+ this.#hitAreaEl.remove();
15574
+ this.#hitAreaEl = null;
15575
+ }
14800
15576
  this.removeEventListener("click", this.#handleSelect);
14801
15577
  document.removeEventListener("pointerdown", this.#handleDeselect);
14802
15578
  document.removeEventListener("keydown", this.#handleKeyDown);
@@ -14805,7 +15581,8 @@ class FigHandle extends HTMLElement {
14805
15581
  select() {
14806
15582
  if (this.hasAttribute("disabled")) return;
14807
15583
  this.setAttribute("selected", "");
14808
- if (this.getAttribute("type") === "color" && !this.#isDragging) this.#showColorTip();
15584
+ if (this.getAttribute("type") === "color" && !this.#isDragging)
15585
+ this.#showColorTip();
14809
15586
  }
14810
15587
 
14811
15588
  deselect() {
@@ -14908,8 +15685,8 @@ class FigHandle extends HTMLElement {
14908
15685
  lastRect = rect;
14909
15686
  const currentLeft = parseFloat(this.style.left) || 0;
14910
15687
  const currentTop = parseFloat(this.style.top) || 0;
14911
- const rawX = (clientX - offsetX) - rect.left - handleW / 2;
14912
- const rawY = (clientY - offsetY) - rect.top - handleH / 2;
15688
+ const rawX = clientX - offsetX - rect.left - handleW / 2;
15689
+ const rawY = clientY - offsetY - rect.top - handleH / 2;
14913
15690
 
14914
15691
  const clampedX = Math.max(
14915
15692
  -handleW / 2,
@@ -15055,16 +15832,32 @@ class FigHandle extends HTMLElement {
15055
15832
  #handleColorTipInput = (e) => {
15056
15833
  e.stopPropagation();
15057
15834
  if (e.detail?.color) {
15058
- this.setAttribute("color", this.#colorWithOpacity(e.detail.color, e.detail.opacity));
15059
- this.dispatchEvent(new CustomEvent("input", { bubbles: true, detail: { color: e.detail.color, opacity: e.detail.opacity } }));
15835
+ this.setAttribute(
15836
+ "color",
15837
+ this.#colorWithOpacity(e.detail.color, e.detail.opacity),
15838
+ );
15839
+ this.dispatchEvent(
15840
+ new CustomEvent("input", {
15841
+ bubbles: true,
15842
+ detail: { color: e.detail.color, opacity: e.detail.opacity },
15843
+ }),
15844
+ );
15060
15845
  }
15061
15846
  };
15062
15847
 
15063
15848
  #handleColorTipChange = (e) => {
15064
15849
  e.stopPropagation();
15065
15850
  if (e.detail?.color) {
15066
- this.setAttribute("color", this.#colorWithOpacity(e.detail.color, e.detail.opacity));
15067
- this.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: { color: e.detail.color, opacity: e.detail.opacity } }));
15851
+ this.setAttribute(
15852
+ "color",
15853
+ this.#colorWithOpacity(e.detail.color, e.detail.opacity),
15854
+ );
15855
+ this.dispatchEvent(
15856
+ new CustomEvent("change", {
15857
+ bubbles: true,
15858
+ detail: { color: e.detail.color, opacity: e.detail.opacity },
15859
+ }),
15860
+ );
15068
15861
  }
15069
15862
  };
15070
15863