@rogieking/figui3 3.21.1 → 3.22.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,6 +947,78 @@ 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);
@@ -3533,8 +3605,12 @@ class FigInputText extends HTMLElement {
3533
3605
  valueTransformed = this.#formatNumber(valueTransformed);
3534
3606
  this.value = value;
3535
3607
  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 }));
3608
+ this.dispatchEvent(
3609
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
3610
+ );
3611
+ this.dispatchEvent(
3612
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
3613
+ );
3538
3614
  }
3539
3615
  #handleMouseDown(e) {
3540
3616
  if (this.type !== "number") return;
@@ -4023,8 +4099,12 @@ class FigInputNumber extends HTMLElement {
4023
4099
  value = this.#sanitizeInput(value, false);
4024
4100
  this.value = value;
4025
4101
  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 }));
4102
+ this.dispatchEvent(
4103
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
4104
+ );
4105
+ this.dispatchEvent(
4106
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
4107
+ );
4028
4108
  }
4029
4109
 
4030
4110
  #handleMouseDown(e) {
@@ -4209,6 +4289,10 @@ customElements.define("fig-avatar", FigAvatar);
4209
4289
 
4210
4290
  /* Form Field */
4211
4291
  class FigField extends HTMLElement {
4292
+ #toggleable = false;
4293
+ #chevron = null;
4294
+ #boundToggle = null;
4295
+
4212
4296
  constructor() {
4213
4297
  super();
4214
4298
  }
@@ -4223,15 +4307,65 @@ class FigField extends HTMLElement {
4223
4307
  this.input = Array.from(this.childNodes).find((node) =>
4224
4308
  node.nodeName.toLowerCase().startsWith("fig-"),
4225
4309
  );
4226
- if (this.input && this.label) {
4310
+
4311
+ this.#toggleable = !!(this.input && "open" in this.input);
4312
+
4313
+ if (this.#toggleable && this.label) {
4314
+ this.#chevron = document.createElement("span");
4315
+ this.#chevron.className = "fig-mask-icon fig-field-chevron";
4316
+ this.insertBefore(this.#chevron, this.label);
4317
+
4318
+ this.#boundToggle = (e) => {
4319
+ e.preventDefault();
4320
+ e.stopPropagation();
4321
+ if (this.input && typeof this.input.open !== "undefined") {
4322
+ this.input.open = !this.input.open;
4323
+ }
4324
+ };
4325
+ this.#chevron.addEventListener("click", this.#boundToggle);
4326
+ this.label.addEventListener("click", this.#boundToggle);
4327
+ } else if (this.input && this.label) {
4227
4328
  this.label.addEventListener("click", this.focus.bind(this));
4329
+ }
4330
+
4331
+ if (this.input && this.label && !this.#toggleable) {
4228
4332
  let inputId = this.input.getAttribute("id") || figUniqueId();
4229
4333
  this.input.setAttribute("id", inputId);
4230
4334
  this.label.setAttribute("for", inputId);
4231
4335
  }
4336
+
4337
+ if (this.label) {
4338
+ this.label.addEventListener(
4339
+ "pointerenter",
4340
+ this.#onLabelEnter.bind(this),
4341
+ );
4342
+ this.label.addEventListener(
4343
+ "pointerleave",
4344
+ this.#onLabelLeave.bind(this),
4345
+ );
4346
+ }
4232
4347
  });
4233
4348
  }
4234
4349
 
4350
+ disconnectedCallback() {
4351
+ if (this.label) FigTooltip.hide(this.label);
4352
+ if (this.label && this.#boundToggle) {
4353
+ this.label.removeEventListener("click", this.#boundToggle);
4354
+ }
4355
+ if (this.#chevron && this.#boundToggle) {
4356
+ this.#chevron.removeEventListener("click", this.#boundToggle);
4357
+ }
4358
+ }
4359
+
4360
+ #onLabelEnter() {
4361
+ if (!this.label || this.label.scrollWidth <= this.label.clientWidth) return;
4362
+ FigTooltip.show(this.label, this.label.textContent.trim());
4363
+ }
4364
+
4365
+ #onLabelLeave() {
4366
+ if (this.label) FigTooltip.hide(this.label);
4367
+ }
4368
+
4235
4369
  attributeChangedCallback(name, oldValue, newValue) {
4236
4370
  switch (name) {
4237
4371
  case "label":
@@ -4243,7 +4377,18 @@ class FigField extends HTMLElement {
4243
4377
  }
4244
4378
 
4245
4379
  focus() {
4246
- this.input.focus();
4380
+ if (!this.input) return;
4381
+ if (this.input.contains(document.activeElement)) return;
4382
+ const nativeInputs = this.input.querySelectorAll("input, select, textarea");
4383
+ if (nativeInputs.length === 1) {
4384
+ nativeInputs[0].focus();
4385
+ nativeInputs[0].click();
4386
+ } else {
4387
+ this.input.focus();
4388
+ if (nativeInputs.length === 0) {
4389
+ this.input.click();
4390
+ }
4391
+ }
4247
4392
  }
4248
4393
  }
4249
4394
  customElements.define("fig-field", FigField);
@@ -4578,7 +4723,7 @@ class FigInputColor extends HTMLElement {
4578
4723
  const disabledAttr = disabled ? " disabled" : "";
4579
4724
 
4580
4725
  let html = ``;
4581
- const showText = this.getAttribute("text") === "true";
4726
+ const showText = this.getAttribute("text") !== "false";
4582
4727
  if (showText) {
4583
4728
  let label = `<fig-input-text
4584
4729
  type="text"
@@ -4801,22 +4946,35 @@ class FigInputColor extends HTMLElement {
4801
4946
  }
4802
4947
 
4803
4948
  #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
- }));
4949
+ this.dispatchEvent(
4950
+ new CustomEvent("input", {
4951
+ bubbles: true,
4952
+ cancelable: true,
4953
+ detail: { value: this.value, hex: this.hex, rgba: this.rgba },
4954
+ }),
4955
+ );
4809
4956
  }
4810
4957
  #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
- }));
4958
+ this.dispatchEvent(
4959
+ new CustomEvent("change", {
4960
+ bubbles: true,
4961
+ cancelable: true,
4962
+ detail: { value: this.value, hex: this.hex, rgba: this.rgba },
4963
+ }),
4964
+ );
4816
4965
  }
4817
4966
 
4818
4967
  static get observedAttributes() {
4819
- return ["value", "style", "mode", "picker", "experimental", "alpha", "text", "disabled"];
4968
+ return [
4969
+ "value",
4970
+ "style",
4971
+ "mode",
4972
+ "picker",
4973
+ "experimental",
4974
+ "alpha",
4975
+ "text",
4976
+ "disabled",
4977
+ ];
4820
4978
  }
4821
4979
 
4822
4980
  get mode() {
@@ -4877,7 +5035,9 @@ class FigInputColor extends HTMLElement {
4877
5035
  }
4878
5036
 
4879
5037
  get #disabled() {
4880
- return this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
5038
+ return (
5039
+ this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false"
5040
+ );
4881
5041
  }
4882
5042
 
4883
5043
  #syncDisabled() {
@@ -5076,9 +5236,15 @@ function figRGBToOklab(r, g, b) {
5076
5236
  const lr = figRGBToLinear(r);
5077
5237
  const lg = figRGBToLinear(g);
5078
5238
  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);
5239
+ const l_ = Math.cbrt(
5240
+ 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb,
5241
+ );
5242
+ const m_ = Math.cbrt(
5243
+ 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb,
5244
+ );
5245
+ const s_ = Math.cbrt(
5246
+ 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb,
5247
+ );
5082
5248
  return {
5083
5249
  l: 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
5084
5250
  a: 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
@@ -5101,7 +5267,11 @@ function figOklabToRGB(L, a, b) {
5101
5267
  }
5102
5268
 
5103
5269
  function figOklabToOklch(L, a, b) {
5104
- return { l: L, c: Math.sqrt(a * a + b * b), h: (Math.atan2(b, a) * 180) / Math.PI };
5270
+ return {
5271
+ l: L,
5272
+ c: Math.sqrt(a * a + b * b),
5273
+ h: (Math.atan2(b, a) * 180) / Math.PI,
5274
+ };
5105
5275
  }
5106
5276
 
5107
5277
  function figOklchToOklab(l, c, h) {
@@ -5130,15 +5300,21 @@ function figInterpolateHue(h1, h2, t, mode) {
5130
5300
  else if (diff < -180) diff += 360;
5131
5301
  break;
5132
5302
  }
5133
- return ((a + diff * t) % 360 + 360) % 360;
5303
+ return (((a + diff * t) % 360) + 360) % 360;
5134
5304
  }
5135
5305
 
5136
- function figSampleGradientAt(stops, position, interpolationSpace, hueInterpolation) {
5306
+ function figSampleGradientAt(
5307
+ stops,
5308
+ position,
5309
+ interpolationSpace,
5310
+ hueInterpolation,
5311
+ ) {
5137
5312
  const sorted = [...stops].sort((a, b) => a.position - b.position);
5138
5313
  const pos = position * 100;
5139
5314
  if (sorted.length === 0) return "#888888";
5140
5315
  if (pos <= sorted[0].position) return sorted[0].color;
5141
- if (pos >= sorted[sorted.length - 1].position) return sorted[sorted.length - 1].color;
5316
+ if (pos >= sorted[sorted.length - 1].position)
5317
+ return sorted[sorted.length - 1].color;
5142
5318
 
5143
5319
  let i = 0;
5144
5320
  while (i < sorted.length - 1 && sorted[i + 1].position < pos) i++;
@@ -5154,8 +5330,12 @@ function figSampleGradientAt(stops, position, interpolationSpace, hueInterpolati
5154
5330
  const space = interpolationSpace || "oklab";
5155
5331
 
5156
5332
  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);
5333
+ const lr1 = figRGBToLinear(c1.r),
5334
+ lg1 = figRGBToLinear(c1.g),
5335
+ lb1 = figRGBToLinear(c1.b);
5336
+ const lr2 = figRGBToLinear(c2.r),
5337
+ lg2 = figRGBToLinear(c2.g),
5338
+ lb2 = figRGBToLinear(c2.b);
5159
5339
  r = figLinearToSRGB(lr1 + (lr2 - lr1) * t);
5160
5340
  g = figLinearToSRGB(lg1 + (lg2 - lg1) * t);
5161
5341
  b = figLinearToSRGB(lb1 + (lb2 - lb1) * t);
@@ -5166,10 +5346,17 @@ function figSampleGradientAt(stops, position, interpolationSpace, hueInterpolati
5166
5346
  const lch2 = figOklabToOklch(lab2.l, lab2.a, lab2.b);
5167
5347
  const L = lch1.l + (lch2.l - lch1.l) * t;
5168
5348
  const C = lch1.c + (lch2.c - lch1.c) * t;
5169
- const H = figInterpolateHue(lch1.h, lch2.h, t, hueInterpolation || "shorter");
5349
+ const H = figInterpolateHue(
5350
+ lch1.h,
5351
+ lch2.h,
5352
+ t,
5353
+ hueInterpolation || "shorter",
5354
+ );
5170
5355
  const lab = figOklchToOklab(L, C, H);
5171
5356
  const rgb = figOklabToRGB(lab.l, lab.a, lab.b);
5172
- r = rgb.r; g = rgb.g; b = rgb.b;
5357
+ r = rgb.r;
5358
+ g = rgb.g;
5359
+ b = rgb.b;
5173
5360
  } else {
5174
5361
  const lab1 = figRGBToOklab(c1.r, c1.g, c1.b);
5175
5362
  const lab2 = figRGBToOklab(c2.r, c2.g, c2.b);
@@ -5177,7 +5364,9 @@ function figSampleGradientAt(stops, position, interpolationSpace, hueInterpolati
5177
5364
  const a = lab1.a + (lab2.a - lab1.a) * t;
5178
5365
  const bv = lab1.b + (lab2.b - lab1.b) * t;
5179
5366
  const rgb = figOklabToRGB(L, a, bv);
5180
- r = rgb.r; g = rgb.g; b = rgb.b;
5367
+ r = rgb.r;
5368
+ g = rgb.g;
5369
+ b = rgb.b;
5181
5370
  }
5182
5371
 
5183
5372
  return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`.toUpperCase();
@@ -5347,7 +5536,11 @@ class FigInputFill extends HTMLElement {
5347
5536
 
5348
5537
  #syncDisabled() {
5349
5538
  const disabled = this.hasAttribute("disabled");
5350
- for (const child of [this.#fillPicker, this.#opacityInput, this.#hexInput]) {
5539
+ for (const child of [
5540
+ this.#fillPicker,
5541
+ this.#opacityInput,
5542
+ this.#hexInput,
5543
+ ]) {
5351
5544
  if (!child) continue;
5352
5545
  if (disabled) child.setAttribute("disabled", "");
5353
5546
  else child.removeAttribute("disabled");
@@ -5911,11 +6104,28 @@ class FigInputPalette extends HTMLElement {
5911
6104
  #renderRAF = null;
5912
6105
 
5913
6106
  static get observedAttributes() {
5914
- return ["value", "disabled", "min", "max", "expanded", "add"];
6107
+ return ["value", "disabled", "min", "max", "open", "add"];
5915
6108
  }
5916
6109
 
5917
- get #expanded() {
5918
- return this.hasAttribute("expanded") && this.getAttribute("expanded") !== "false";
6110
+ get open() {
6111
+ return this.hasAttribute("open") && this.getAttribute("open") !== "false";
6112
+ }
6113
+
6114
+ set open(value) {
6115
+ const was = this.open;
6116
+ if (value) {
6117
+ this.setAttribute("open", "");
6118
+ } else {
6119
+ this.removeAttribute("open");
6120
+ }
6121
+ if (was !== !!value) {
6122
+ this.dispatchEvent(
6123
+ new CustomEvent("openchange", {
6124
+ detail: { open: !!value },
6125
+ bubbles: true,
6126
+ }),
6127
+ );
6128
+ }
5919
6129
  }
5920
6130
 
5921
6131
  get #showAdd() {
@@ -5933,6 +6143,7 @@ class FigInputPalette extends HTMLElement {
5933
6143
  }
5934
6144
 
5935
6145
  connectedCallback() {
6146
+ if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "0");
5936
6147
  if (this.#renderRAF) cancelAnimationFrame(this.#renderRAF);
5937
6148
  this.#renderRAF = requestAnimationFrame(() => {
5938
6149
  this.#renderRAF = null;
@@ -5963,7 +6174,7 @@ class FigInputPalette extends HTMLElement {
5963
6174
  break;
5964
6175
  case "min":
5965
6176
  case "max":
5966
- case "expanded":
6177
+ case "open":
5967
6178
  case "add":
5968
6179
  this.#render();
5969
6180
  break;
@@ -5985,12 +6196,21 @@ class FigInputPalette extends HTMLElement {
5985
6196
  if (Array.isArray(parsed)) {
5986
6197
  this.#colors = parsed.map((entry) => {
5987
6198
  if (typeof entry === "string") {
5988
- return { color: entry.slice(0, 7), alpha: entry.length > 7 ? parseInt(entry.slice(7, 9), 16) / 255 : 1 };
6199
+ return {
6200
+ color: entry.slice(0, 7),
6201
+ alpha:
6202
+ entry.length > 7 ? parseInt(entry.slice(7, 9), 16) / 255 : 1,
6203
+ };
5989
6204
  }
5990
6205
  if (entry && typeof entry === "object") {
5991
6206
  return {
5992
6207
  color: entry.color || "#D9D9D9",
5993
- alpha: entry.alpha !== undefined ? entry.alpha : (entry.opacity !== undefined ? entry.opacity / 100 : 1),
6208
+ alpha:
6209
+ entry.alpha !== undefined
6210
+ ? entry.alpha
6211
+ : entry.opacity !== undefined
6212
+ ? entry.opacity / 100
6213
+ : 1,
5994
6214
  };
5995
6215
  }
5996
6216
  return { color: "#D9D9D9", alpha: 1 };
@@ -6015,10 +6235,13 @@ class FigInputPalette extends HTMLElement {
6015
6235
 
6016
6236
  // Single hex
6017
6237
  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
- }];
6238
+ this.#colors = [
6239
+ {
6240
+ color: trimmed.slice(0, 7),
6241
+ alpha:
6242
+ trimmed.length > 7 ? parseInt(trimmed.slice(7, 9), 16) / 255 : 1,
6243
+ },
6244
+ ];
6022
6245
  return;
6023
6246
  }
6024
6247
 
@@ -6038,7 +6261,9 @@ class FigInputPalette extends HTMLElement {
6038
6261
  }
6039
6262
 
6040
6263
  #render() {
6041
- const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
6264
+ const disabled =
6265
+ this.hasAttribute("disabled") &&
6266
+ this.getAttribute("disabled") !== "false";
6042
6267
 
6043
6268
  this.innerHTML = "";
6044
6269
  this.#inlinePickers = [];
@@ -6050,7 +6275,9 @@ class FigInputPalette extends HTMLElement {
6050
6275
  const wrap = document.createElement("div");
6051
6276
  wrap.className = "palette-colors";
6052
6277
  this.#colors.forEach((entry, i) => {
6053
- wrap.appendChild(this.#createPicker(entry, i, disabled, { inline: true }));
6278
+ wrap.appendChild(
6279
+ this.#createPicker(entry, i, disabled, { inline: true }),
6280
+ );
6054
6281
  });
6055
6282
  inlineWrap.appendChild(wrap);
6056
6283
 
@@ -6066,9 +6293,13 @@ class FigInputPalette extends HTMLElement {
6066
6293
  }
6067
6294
 
6068
6295
  #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;
6296
+ const hexAlpha =
6297
+ entry.alpha < 1
6298
+ ? entry.color +
6299
+ Math.round(entry.alpha * 255)
6300
+ .toString(16)
6301
+ .padStart(2, "0")
6302
+ : entry.color;
6072
6303
  const ic = document.createElement("fig-input-color");
6073
6304
  ic.setAttribute("value", hexAlpha);
6074
6305
  ic.setAttribute("picker", "figma");
@@ -6095,9 +6326,13 @@ class FigInputPalette extends HTMLElement {
6095
6326
  const sibling = siblingList[index];
6096
6327
  if (sibling) {
6097
6328
  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;
6329
+ const hex =
6330
+ entry.alpha < 1
6331
+ ? entry.color +
6332
+ Math.round(entry.alpha * 255)
6333
+ .toString(16)
6334
+ .padStart(2, "0")
6335
+ : entry.color;
6101
6336
  sibling.setAttribute("value", hex);
6102
6337
  }
6103
6338
  };
@@ -6120,14 +6355,18 @@ class FigInputPalette extends HTMLElement {
6120
6355
  #createAddButton(disabled, parent = this) {
6121
6356
  const atMax = this.#colors.length >= this.#max;
6122
6357
  const addBtn = document.createElement("fig-button");
6123
- addBtn.setAttribute("variant", "ghost");
6358
+ addBtn.setAttribute("variant", "input");
6124
6359
  addBtn.setAttribute("icon", "true");
6125
6360
  addBtn.setAttribute("aria-label", "Add color");
6126
6361
  addBtn.className = "palette-add-btn";
6127
6362
  if (disabled || atMax) addBtn.setAttribute("disabled", "");
6128
6363
  addBtn.innerHTML = `<span class="fig-mask-icon" style="--icon: var(--icon-add)"></span>`;
6129
6364
  addBtn.addEventListener("click", () => {
6130
- if (this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") return;
6365
+ if (
6366
+ this.hasAttribute("disabled") &&
6367
+ this.getAttribute("disabled") !== "false"
6368
+ )
6369
+ return;
6131
6370
  if (this.#colors.length >= this.#max) return;
6132
6371
  this.#addColor({ color: "#D9D9D9", alpha: 1 });
6133
6372
  });
@@ -6139,10 +6378,14 @@ class FigInputPalette extends HTMLElement {
6139
6378
 
6140
6379
  #addColor(entry) {
6141
6380
  this.#colors.push(entry);
6142
- const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
6381
+ const disabled =
6382
+ this.hasAttribute("disabled") &&
6383
+ this.getAttribute("disabled") !== "false";
6143
6384
  const index = this.#colors.length - 1;
6144
6385
 
6145
- const inlineIc = this.#createPicker(entry, index, disabled, { inline: true });
6386
+ const inlineIc = this.#createPicker(entry, index, disabled, {
6387
+ inline: true,
6388
+ });
6146
6389
  const wrap = this.querySelector(".palette-colors");
6147
6390
  if (wrap) wrap.appendChild(inlineIc);
6148
6391
 
@@ -6160,9 +6403,13 @@ class FigInputPalette extends HTMLElement {
6160
6403
  #updateChit(index) {
6161
6404
  const entry = this.#colors[index];
6162
6405
  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;
6406
+ const hexAlpha =
6407
+ entry.alpha < 1
6408
+ ? entry.color +
6409
+ Math.round(entry.alpha * 255)
6410
+ .toString(16)
6411
+ .padStart(2, "0")
6412
+ : entry.color;
6166
6413
  const inl = this.#inlinePickers[index];
6167
6414
  if (inl) inl.setAttribute("value", hexAlpha);
6168
6415
  const exp = this.#expandedPickers[index];
@@ -6180,7 +6427,9 @@ class FigInputPalette extends HTMLElement {
6180
6427
  }
6181
6428
 
6182
6429
  #syncDisabled() {
6183
- const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
6430
+ const disabled =
6431
+ this.hasAttribute("disabled") &&
6432
+ this.getAttribute("disabled") !== "false";
6184
6433
  [...this.#inlinePickers, ...this.#expandedPickers].forEach((fp) => {
6185
6434
  if (disabled) fp.setAttribute("disabled", "");
6186
6435
  else fp.removeAttribute("disabled");
@@ -6314,7 +6563,9 @@ class FigInputGradient extends HTMLElement {
6314
6563
  const idx = parseInt(selected.dataset.stopIndex, 10);
6315
6564
  if (isNaN(idx) || !this.#gradient.stops[idx]) return;
6316
6565
  e.preventDefault();
6317
- const delta = (e.key === "ArrowRight" ? 1 : -1) * (e.shiftKey ? FigInputGradient.SHIFT_SNAP : 1);
6566
+ const delta =
6567
+ (e.key === "ArrowRight" ? 1 : -1) *
6568
+ (e.shiftKey ? FigInputGradient.SHIFT_SNAP : 1);
6318
6569
  const stop = this.#gradient.stops[idx];
6319
6570
  stop.position = Math.max(0, Math.min(100, stop.position + delta));
6320
6571
  selected.setAttribute("value", `${stop.position}% 50%`);
@@ -6364,6 +6615,8 @@ class FigInputGradient extends HTMLElement {
6364
6615
  ...this.#gradient,
6365
6616
  ...parsed.gradient,
6366
6617
  });
6618
+ this.#gradient.type = "linear";
6619
+ this.#gradient.angle = 90;
6367
6620
  return;
6368
6621
  }
6369
6622
  if (parsed?.gradient) {
@@ -6371,6 +6624,8 @@ class FigInputGradient extends HTMLElement {
6371
6624
  ...this.#gradient,
6372
6625
  ...parsed.gradient,
6373
6626
  });
6627
+ this.#gradient.type = "linear";
6628
+ this.#gradient.angle = 90;
6374
6629
  }
6375
6630
  } catch (e) {
6376
6631
  // Ignore invalid JSON and keep current/default gradient.
@@ -6578,10 +6833,12 @@ class FigInputGradient extends HTMLElement {
6578
6833
  const stopIdx = parseInt(clickedHandle?.dataset.stopIndex, 10);
6579
6834
  this.#distributeStops();
6580
6835
  if (!isNaN(stopIdx)) {
6581
- this.#track.querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)").forEach((h) => {
6582
- if (parseInt(h.dataset.stopIndex, 10) === stopIdx) h.select();
6583
- else h.deselect();
6584
- });
6836
+ this.#track
6837
+ .querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)")
6838
+ .forEach((h) => {
6839
+ if (parseInt(h.dataset.stopIndex, 10) === stopIdx) h.select();
6840
+ else h.deselect();
6841
+ });
6585
6842
  }
6586
6843
  e.stopPropagation();
6587
6844
  }
@@ -6720,7 +6977,10 @@ class FigInputGradient extends HTMLElement {
6720
6977
  e.stopPropagation();
6721
6978
 
6722
6979
  const trackRect = this.#track.getBoundingClientRect();
6723
- const pct = Math.max(0, Math.min(1, (e.clientX - trackRect.left) / trackRect.width));
6980
+ const pct = Math.max(
6981
+ 0,
6982
+ Math.min(1, (e.clientX - trackRect.left) / trackRect.width),
6983
+ );
6724
6984
  const position = Math.round(pct * 100);
6725
6985
  const color = this.#sampleGradientColor(pct);
6726
6986
  this.#gradient.stops.push({ position, color, opacity: 100 });
@@ -6739,19 +6999,23 @@ class FigInputGradient extends HTMLElement {
6739
6999
  );
6740
7000
  const newHandle = handles[newIndex];
6741
7001
  if (newHandle) {
6742
- this.#track.querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)").forEach((h) => {
6743
- if (h !== newHandle) h.deselect();
6744
- });
7002
+ this.#track
7003
+ .querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)")
7004
+ .forEach((h) => {
7005
+ if (h !== newHandle) h.deselect();
7006
+ });
6745
7007
  newHandle.select();
6746
- newHandle.dispatchEvent(new PointerEvent("pointerdown", {
6747
- bubbles: true,
6748
- clientX: e.clientX,
6749
- clientY: e.clientY,
6750
- pointerId: e.pointerId,
6751
- pointerType: e.pointerType,
6752
- button: e.button,
6753
- buttons: e.buttons,
6754
- }));
7008
+ newHandle.dispatchEvent(
7009
+ new PointerEvent("pointerdown", {
7010
+ bubbles: true,
7011
+ clientX: e.clientX,
7012
+ clientY: e.clientY,
7013
+ pointerId: e.pointerId,
7014
+ pointerType: e.pointerType,
7015
+ button: e.button,
7016
+ buttons: e.buttons,
7017
+ }),
7018
+ );
6755
7019
  }
6756
7020
  });
6757
7021
 
@@ -6766,7 +7030,10 @@ class FigInputGradient extends HTMLElement {
6766
7030
  if (e.detail.opacity !== undefined) {
6767
7031
  this.#gradient.stops[idx].opacity = e.detail.opacity;
6768
7032
  }
6769
- handle.setAttribute("color", this.#stopColorCSS(this.#gradient.stops[idx]));
7033
+ handle.setAttribute(
7034
+ "color",
7035
+ this.#stopColorCSS(this.#gradient.stops[idx]),
7036
+ );
6770
7037
  this.#syncChit();
6771
7038
  this.#emitInput();
6772
7039
  }
@@ -6781,7 +7048,9 @@ class FigInputGradient extends HTMLElement {
6781
7048
  let position = rawPosition;
6782
7049
  const trackW = this.#track.getBoundingClientRect().width;
6783
7050
  if (e.detail?.shiftKey) {
6784
- position = Math.round(position / FigInputGradient.SHIFT_SNAP) * FigInputGradient.SHIFT_SNAP;
7051
+ position =
7052
+ Math.round(position / FigInputGradient.SHIFT_SNAP) *
7053
+ FigInputGradient.SHIFT_SNAP;
6785
7054
  } else {
6786
7055
  const snapPct = trackW > 0 ? (5 / trackW) * 100 : 0;
6787
7056
  for (let i = 0; i < this.#gradient.stops.length; i++) {
@@ -6821,7 +7090,10 @@ class FigInputGradient extends HTMLElement {
6821
7090
  if (e.detail.opacity !== undefined) {
6822
7091
  this.#gradient.stops[idx].opacity = e.detail.opacity;
6823
7092
  }
6824
- handle.setAttribute("color", this.#stopColorCSS(this.#gradient.stops[idx]));
7093
+ handle.setAttribute(
7094
+ "color",
7095
+ this.#stopColorCSS(this.#gradient.stops[idx]),
7096
+ );
6825
7097
  this.#syncChit();
6826
7098
  this.#emitChange();
6827
7099
  }
@@ -7574,13 +7846,13 @@ class FigChit extends HTMLElement {
7574
7846
 
7575
7847
  if (this.#type === "color") {
7576
7848
  const hex = this.#toHex(bg);
7577
- this.innerHTML = `<input type="color" value="${hex}" />`;
7849
+ this.innerHTML = `<div></div><input type="color" value="${hex}" />`;
7578
7850
  this.input = this.querySelector("input");
7579
7851
  if (!isVar) {
7580
7852
  this.input.addEventListener("input", this.#boundHandleInput);
7581
7853
  }
7582
7854
  } else {
7583
- this.innerHTML = "";
7855
+ this.innerHTML = "<div></div>";
7584
7856
  this.input = null;
7585
7857
  }
7586
7858
  } else if (this.#type === "color" && this.input) {
@@ -7590,8 +7862,14 @@ class FigChit extends HTMLElement {
7590
7862
  }
7591
7863
  }
7592
7864
 
7593
- const isImage = /^(linear-gradient|radial-gradient|conic-gradient|repeating-|url)\s*\(/i.test(rawBg);
7594
- this.style.setProperty("--chit-background", isImage ? rawBg : `linear-gradient(${rawBg}, ${rawBg})`);
7865
+ const isImage =
7866
+ /^(linear-gradient|radial-gradient|conic-gradient|repeating-|url)\s*\(/i.test(
7867
+ rawBg,
7868
+ );
7869
+ this.style.setProperty(
7870
+ "--chit-background",
7871
+ isImage ? rawBg : `linear-gradient(${rawBg}, ${rawBg})`,
7872
+ );
7595
7873
  }
7596
7874
 
7597
7875
  #handleInput(e) {
@@ -7618,8 +7896,14 @@ class FigChit extends HTMLElement {
7618
7896
  if (oldValue === newValue) return;
7619
7897
  if (name === "background") {
7620
7898
  if (this.#internalUpdate) {
7621
- const isImg = /^(linear-gradient|radial-gradient|conic-gradient|repeating-|url)\s*\(/i.test(newValue);
7622
- this.style.setProperty("--chit-background", isImg ? newValue : `linear-gradient(${newValue}, ${newValue})`);
7899
+ const isImg =
7900
+ /^(linear-gradient|radial-gradient|conic-gradient|repeating-|url)\s*\(/i.test(
7901
+ newValue,
7902
+ );
7903
+ this.style.setProperty(
7904
+ "--chit-background",
7905
+ isImg ? newValue : `linear-gradient(${newValue}, ${newValue})`,
7906
+ );
7623
7907
  return;
7624
7908
  }
7625
7909
  this.#render();
@@ -7641,9 +7925,7 @@ class FigChit extends HTMLElement {
7641
7925
  }
7642
7926
  }
7643
7927
  customElements.define("fig-chit", FigChit);
7644
- class FigSwatch extends FigChit{
7645
-
7646
- }
7928
+ class FigSwatch extends FigChit {}
7647
7929
  customElements.define("fig-swatch", FigSwatch);
7648
7930
 
7649
7931
  /* Upload */
@@ -8824,7 +9106,11 @@ class Fig3DRotate extends HTMLElement {
8824
9106
  this.#precision = parseInt(this.getAttribute("precision") || "1");
8825
9107
  figSyncCssVar(this, "--aspect-ratio", this.getAttribute("aspect-ratio"));
8826
9108
  figSyncCssVar(this, "--perspective", this.getAttribute("perspective"));
8827
- figSyncCssVar(this, "--perspective-origin", this.getAttribute("perspective-origin"));
9109
+ figSyncCssVar(
9110
+ this,
9111
+ "--perspective-origin",
9112
+ this.getAttribute("perspective-origin"),
9113
+ );
8828
9114
  this.#syncTransformOrigin(this.getAttribute("transform-origin"));
8829
9115
  this.#parseFields(this.getAttribute("fields"));
8830
9116
  const val = this.getAttribute("value");
@@ -8842,8 +9128,6 @@ class Fig3DRotate extends HTMLElement {
8842
9128
  }
8843
9129
  }
8844
9130
 
8845
-
8846
-
8847
9131
  #syncTransformOrigin(value) {
8848
9132
  if (!value || !value.trim()) {
8849
9133
  this.style.removeProperty("--transform-origin");
@@ -10678,6 +10962,117 @@ class FigLayer extends HTMLElement {
10678
10962
  }
10679
10963
  customElements.define("fig-layer", FigLayer);
10680
10964
 
10965
+ // FigGroup
10966
+ class FigGroup extends HTMLElement {
10967
+ static observedAttributes = ["name", "collapsible"];
10968
+
10969
+ #header = null;
10970
+ #chevron = null;
10971
+
10972
+ connectedCallback() {
10973
+ requestAnimationFrame(() => this.#render());
10974
+ }
10975
+
10976
+ disconnectedCallback() {
10977
+ if (this.#chevron) {
10978
+ this.#chevron.removeEventListener("click", this.#handleToggle);
10979
+ }
10980
+ if (this.#header) {
10981
+ this.#header.removeEventListener("click", this.#handleToggle);
10982
+ }
10983
+ }
10984
+
10985
+ attributeChangedCallback(name, oldValue, newValue) {
10986
+ if (oldValue === newValue) return;
10987
+ this.#render();
10988
+ }
10989
+
10990
+ get open() {
10991
+ const attr = this.getAttribute("open");
10992
+ return attr !== null && attr !== "false";
10993
+ }
10994
+
10995
+ set open(value) {
10996
+ const was = this.open;
10997
+ if (value) {
10998
+ this.setAttribute("open", "true");
10999
+ } else {
11000
+ this.setAttribute("open", "false");
11001
+ }
11002
+ if (was !== !!value) {
11003
+ this.dispatchEvent(
11004
+ new CustomEvent("openchange", {
11005
+ detail: { open: !!value },
11006
+ bubbles: true,
11007
+ }),
11008
+ );
11009
+ }
11010
+ }
11011
+
11012
+ #handleToggle = (e) => {
11013
+ e.stopPropagation();
11014
+ this.open = !this.open;
11015
+ };
11016
+
11017
+ #render() {
11018
+ const isCollapsible = this.hasAttribute("collapsible");
11019
+ const nameAttr = this.getAttribute("name");
11020
+ const label = nameAttr || (isCollapsible ? "Group" : null);
11021
+
11022
+ // Check if user supplied their own fig-header
11023
+ const userHeader = this.querySelector(":scope > fig-header");
11024
+
11025
+ if (!label && !isCollapsible && !userHeader) {
11026
+ if (this.#header && this.#header.dataset.generated) {
11027
+ this.#header.remove();
11028
+ this.#header = null;
11029
+ this.#chevron = null;
11030
+ }
11031
+ return;
11032
+ }
11033
+
11034
+ if (userHeader) {
11035
+ this.#header = userHeader;
11036
+ } else if (!this.#header || !this.#header.dataset.generated) {
11037
+ this.#header = document.createElement("fig-header");
11038
+ this.#header.setAttribute("borderless", "");
11039
+ this.#header.dataset.generated = "true";
11040
+ this.prepend(this.#header);
11041
+ }
11042
+
11043
+ // Ensure h3 exists inside header
11044
+ let h3 = this.#header.querySelector("h3");
11045
+ if (!h3) {
11046
+ h3 = document.createElement("h3");
11047
+ this.#header.prepend(h3);
11048
+ }
11049
+ if (this.#header.dataset.generated) {
11050
+ h3.textContent = label;
11051
+ }
11052
+
11053
+ if (isCollapsible) {
11054
+ if (!h3.querySelector(".fig-group-chevron")) {
11055
+ const chevron = document.createElement("span");
11056
+ chevron.className = "fig-mask-icon fig-group-chevron";
11057
+ h3.prepend(chevron);
11058
+ }
11059
+ this.#chevron = h3.querySelector(".fig-group-chevron");
11060
+ h3.addEventListener("click", this.#handleToggle);
11061
+
11062
+ if (!this.hasAttribute("open")) {
11063
+ this.setAttribute("open", "false");
11064
+ }
11065
+ } else {
11066
+ if (this.#chevron) {
11067
+ this.#chevron.remove();
11068
+ this.#chevron = null;
11069
+ }
11070
+ this.removeAttribute("open");
11071
+ }
11072
+ }
11073
+ }
11074
+ customElements.define("fig-group", FigGroup);
11075
+
10681
11076
  // FigFillPicker
10682
11077
  /**
10683
11078
  * A comprehensive fill picker component supporting solid colors, gradients, images, video, and webcam.
@@ -11110,10 +11505,15 @@ class FigFillPicker extends HTMLElement {
11110
11505
  this.#dialog.addEventListener("close", onDialogClose);
11111
11506
 
11112
11507
  this.#dialogOpenObserver = new MutationObserver(() => {
11113
- const isOpen = this.#dialog.hasAttribute("open") && this.#dialog.getAttribute("open") !== "false";
11508
+ const isOpen =
11509
+ this.#dialog.hasAttribute("open") &&
11510
+ this.#dialog.getAttribute("open") !== "false";
11114
11511
  if (!isOpen) onDialogClose();
11115
11512
  });
11116
- this.#dialogOpenObserver.observe(this.#dialog, { attributes: true, attributeFilter: ["open"] });
11513
+ this.#dialogOpenObserver.observe(this.#dialog, {
11514
+ attributes: true,
11515
+ attributeFilter: ["open"],
11516
+ });
11117
11517
 
11118
11518
  // Initialize built-in tabs (skip any overridden by custom slots)
11119
11519
  const builtinInits = {
@@ -11209,6 +11609,8 @@ class FigFillPicker extends HTMLElement {
11209
11609
  <fig-preview class="fig-fill-picker-color-area">
11210
11610
  <canvas width="200" height="200"></canvas>
11211
11611
  <fig-handle
11612
+ type="color"
11613
+ color="${this.#hsvToHex({ ...this.#color, a: 1 })}"
11212
11614
  drag
11213
11615
  drag-surface=".fig-fill-picker-color-area"
11214
11616
  drag-axes="x,y"
@@ -11871,7 +12273,9 @@ class FigFillPicker extends HTMLElement {
11871
12273
  });
11872
12274
 
11873
12275
  // Embedded gradient bar input
11874
- const gradientBarInput = container.querySelector(".fig-fill-picker-gradient-bar-input");
12276
+ const gradientBarInput = container.querySelector(
12277
+ ".fig-fill-picker-gradient-bar-input",
12278
+ );
11875
12279
  if (gradientBarInput) {
11876
12280
  const syncFromBarInput = (e) => {
11877
12281
  e.stopPropagation();
@@ -11944,7 +12348,10 @@ class FigFillPicker extends HTMLElement {
11944
12348
  if (barInput) {
11945
12349
  barInput.setAttribute(
11946
12350
  "value",
11947
- JSON.stringify({ type: "gradient", gradient: gradientToValueShape(this.#gradient) }),
12351
+ JSON.stringify({
12352
+ type: "gradient",
12353
+ gradient: gradientToValueShape(this.#gradient),
12354
+ }),
11948
12355
  );
11949
12356
  }
11950
12357
 
@@ -11973,7 +12380,8 @@ class FigFillPicker extends HTMLElement {
11973
12380
  if (colorInput) colorInput.setAttribute("value", stop.color);
11974
12381
  const removeBtn = row.querySelector(".fig-fill-picker-stop-remove");
11975
12382
  if (removeBtn) {
11976
- if (this.#gradient.stops.length <= 2) removeBtn.setAttribute("disabled", "");
12383
+ if (this.#gradient.stops.length <= 2)
12384
+ removeBtn.setAttribute("disabled", "");
11977
12385
  else removeBtn.removeAttribute("disabled");
11978
12386
  }
11979
12387
  });
@@ -12902,10 +13310,20 @@ class FigColorTip extends HTMLElement {
12902
13310
  }
12903
13311
  this.removeEventListener("click", this.#handleControlClick);
12904
13312
 
12905
- const color = this.#normalizeColor(this.getAttribute("value"));
13313
+ const rawValue = (this.getAttribute("value") || "").trim();
13314
+ const color = this.#normalizeColor(rawValue);
13315
+ const alpha = this.#extractAlpha(rawValue);
12906
13316
  const alphaAttr = this.#alphaEnabled ? "" : 'alpha="false"';
13317
+ const pickerValue =
13318
+ alpha < 1
13319
+ ? JSON.stringify({
13320
+ type: "solid",
13321
+ color,
13322
+ opacity: Math.round(alpha * 100),
13323
+ })
13324
+ : JSON.stringify({ type: "solid", color });
12907
13325
  this.innerHTML = `
12908
- <fig-fill-picker mode="solid" ${alphaAttr} value='${JSON.stringify({ type: "solid", color })}'>
13326
+ <fig-fill-picker mode="solid" ${alphaAttr} value='${pickerValue}'>
12909
13327
  <fig-chit background="${color}"></fig-chit>
12910
13328
  </fig-fill-picker>`;
12911
13329
 
@@ -12940,6 +13358,19 @@ class FigColorTip extends HTMLElement {
12940
13358
  return "#D9D9D9";
12941
13359
  }
12942
13360
 
13361
+ #extractAlpha(colorValue) {
13362
+ if (!colorValue) return 1;
13363
+ const v = String(colorValue).trim();
13364
+ if (v.startsWith("#") && v.length === 9) {
13365
+ return parseInt(v.slice(7, 9), 16) / 255;
13366
+ }
13367
+ const rgbaMatch = v.match(
13368
+ /rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*([\d.]+)\s*\)/i,
13369
+ );
13370
+ if (rgbaMatch) return parseFloat(rgbaMatch[1]);
13371
+ return 1;
13372
+ }
13373
+
12943
13374
  #normalizeColor(colorValue) {
12944
13375
  if (!colorValue) return "#D9D9D9";
12945
13376
  const value = String(colorValue).trim();
@@ -12983,17 +13414,20 @@ class FigColorTip extends HTMLElement {
12983
13414
  }
12984
13415
 
12985
13416
  #syncFromAttributes() {
12986
- const color = this.#normalizeColor(this.getAttribute("value"));
12987
- if (this.getAttribute("value") !== color) {
13417
+ const rawAttr = this.getAttribute("value");
13418
+ const color = this.#normalizeColor(rawAttr);
13419
+ const alpha = this.#extractAlpha(rawAttr);
13420
+ if (rawAttr !== color && alpha >= 1) {
12988
13421
  this.setAttribute("value", color);
12989
13422
  return;
12990
13423
  }
12991
13424
 
12992
13425
  if (this.#fillPicker) {
12993
- this.#fillPicker.setAttribute(
12994
- "value",
12995
- JSON.stringify({ type: "solid", color }),
12996
- );
13426
+ const pickerVal =
13427
+ alpha < 1
13428
+ ? { type: "solid", color, opacity: Math.round(alpha * 100) }
13429
+ : { type: "solid", color };
13430
+ this.#fillPicker.setAttribute("value", JSON.stringify(pickerVal));
12997
13431
  if (this.#alphaEnabled) {
12998
13432
  this.#fillPicker.removeAttribute("alpha");
12999
13433
  } else {
@@ -13170,7 +13604,15 @@ class FigChooser extends HTMLElement {
13170
13604
  }
13171
13605
 
13172
13606
  static get observedAttributes() {
13173
- return ["value", "disabled", "choice-element", "drag", "overflow", "loop", "padding"];
13607
+ return [
13608
+ "value",
13609
+ "disabled",
13610
+ "choice-element",
13611
+ "drag",
13612
+ "overflow",
13613
+ "loop",
13614
+ "padding",
13615
+ ];
13174
13616
  }
13175
13617
 
13176
13618
  get #overflowMode() {
@@ -13796,7 +14238,13 @@ class FigCanvasControl extends HTMLElement {
13796
14238
 
13797
14239
  attributeChangedCallback(name, oldVal, newVal) {
13798
14240
  if (oldVal === newVal) return;
13799
- if (name === "value" && !this.#isDragging && !this.#isSecondDragging && !this.#isRadiusDragging && !this.#isAngleDragging) {
14241
+ if (
14242
+ name === "value" &&
14243
+ !this.#isDragging &&
14244
+ !this.#isSecondDragging &&
14245
+ !this.#isRadiusDragging &&
14246
+ !this.#isAngleDragging
14247
+ ) {
13800
14248
  this.#parseValue();
13801
14249
  if (this.#pointHandle) this.#syncPositions();
13802
14250
  else this.#render();
@@ -13817,11 +14265,14 @@ class FigCanvasControl extends HTMLElement {
13817
14265
  }
13818
14266
  if (name === "snapping" && this.#pointHandle) {
13819
14267
  this.#pointHandle.setAttribute("drag-snapping", newVal || "false");
13820
- if (this.#secondHandle) this.#secondHandle.setAttribute("drag-snapping", newVal || "false");
14268
+ if (this.#secondHandle)
14269
+ this.#secondHandle.setAttribute("drag-snapping", newVal || "false");
13821
14270
  }
13822
14271
  if (name === "name") {
13823
- if (this.#pointTooltip) this.#pointTooltip.setAttribute("text", this.#pointTipText);
13824
- if (this.#secondTooltip) this.#secondTooltip.setAttribute("text", this.#secondTipText);
14272
+ if (this.#pointTooltip)
14273
+ this.#pointTooltip.setAttribute("text", this.#pointTipText);
14274
+ if (this.#secondTooltip)
14275
+ this.#secondTooltip.setAttribute("text", this.#secondTipText);
13825
14276
  }
13826
14277
  }
13827
14278
 
@@ -13846,13 +14297,16 @@ class FigCanvasControl extends HTMLElement {
13846
14297
  if (typeof v.angle === "number") this.#angle = v.angle;
13847
14298
  if (typeof v.x2 === "number") this.#x2 = v.x2;
13848
14299
  if (typeof v.y2 === "number") this.#y2 = v.y2;
13849
- } catch { /* ignore */ }
14300
+ } catch {
14301
+ /* ignore */
14302
+ }
13850
14303
  }
13851
14304
 
13852
14305
  get value() {
13853
14306
  const v = { x: this.#x, y: this.#y };
13854
14307
  if (this.#type === "color") {
13855
- const color = this.getAttribute("color") || this.#pointHandle?.getAttribute("color");
14308
+ const color =
14309
+ this.getAttribute("color") || this.#pointHandle?.getAttribute("color");
13856
14310
  if (color) v.color = color;
13857
14311
  }
13858
14312
  if (this.#hasRadius) {
@@ -14153,11 +14607,15 @@ class FigCanvasControl extends HTMLElement {
14153
14607
  }
14154
14608
 
14155
14609
  #emitInput() {
14156
- this.dispatchEvent(new CustomEvent("input", { bubbles: true, detail: this.value }));
14610
+ this.dispatchEvent(
14611
+ new CustomEvent("input", { bubbles: true, detail: this.value }),
14612
+ );
14157
14613
  }
14158
14614
 
14159
14615
  #emitChange() {
14160
- this.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: this.value }));
14616
+ this.dispatchEvent(
14617
+ new CustomEvent("change", { bubbles: true, detail: this.value }),
14618
+ );
14161
14619
  }
14162
14620
 
14163
14621
  #syncValueAttribute() {
@@ -14189,7 +14647,9 @@ class FigCanvasControl extends HTMLElement {
14189
14647
  }
14190
14648
  this.#syncPositions();
14191
14649
  if (this.#hasSecondPoint) {
14192
- document.body.style.cursor = this.#resizeCursorSvg(this.#pointPointLineDeg());
14650
+ document.body.style.cursor = this.#resizeCursorSvg(
14651
+ this.#pointPointLineDeg(),
14652
+ );
14193
14653
  }
14194
14654
  this.#emitInput();
14195
14655
  });
@@ -14205,14 +14665,17 @@ class FigCanvasControl extends HTMLElement {
14205
14665
  const py = e.detail?.py ?? this.#y / 100;
14206
14666
  this.#x = Math.round(Math.max(0, Math.min(100, px * 100)));
14207
14667
  this.#y = Math.round(Math.max(0, Math.min(100, py * 100)));
14208
- if (this.#pointTooltip && this.#type !== "color") this.#pointTooltip.removeAttribute("show");
14668
+ if (this.#pointTooltip && this.#type !== "color")
14669
+ this.#pointTooltip.removeAttribute("show");
14209
14670
  if (this.#hasSecondPoint) {
14210
14671
  document.body.style.cursor = this.#prevBodyCursor ?? "";
14211
14672
  }
14212
14673
  this.#syncPositions();
14213
14674
  this.#syncValueAttribute();
14214
14675
  this.#emitChange();
14215
- requestAnimationFrame(() => { this.#isDragging = false; });
14676
+ requestAnimationFrame(() => {
14677
+ this.#isDragging = false;
14678
+ });
14216
14679
  });
14217
14680
 
14218
14681
  if (this.#angleHandle) {
@@ -14229,8 +14692,8 @@ class FigCanvasControl extends HTMLElement {
14229
14692
  const hy = e.detail?.y ?? 0;
14230
14693
  const hw = this.#angleHandle.offsetWidth / 2;
14231
14694
  const hh = this.#angleHandle.offsetHeight / 2;
14232
- const dx = (hx + hw) - cx;
14233
- const dy = (hy + hh) - cy;
14695
+ const dx = hx + hw - cx;
14696
+ const dy = hy + hh - cy;
14234
14697
  let angle = (Math.atan2(dy, dx) * 180) / Math.PI;
14235
14698
  if (this.#shouldSnap(e.detail?.shiftKey)) {
14236
14699
  angle = Math.round(angle / 15) * 15;
@@ -14255,7 +14718,10 @@ class FigCanvasControl extends HTMLElement {
14255
14718
  }
14256
14719
 
14257
14720
  if (this.#angleTooltip) {
14258
- this.#angleTooltip.setAttribute("text", `Angle ${Math.round(this.#angle)}°`);
14721
+ this.#angleTooltip.setAttribute(
14722
+ "text",
14723
+ `Angle ${Math.round(this.#angle)}°`,
14724
+ );
14259
14725
  this.#angleTooltip.setAttribute("show", "true");
14260
14726
  this.#angleTooltip.showPopup?.();
14261
14727
  }
@@ -14270,7 +14736,9 @@ class FigCanvasControl extends HTMLElement {
14270
14736
  this.#syncPositions();
14271
14737
  this.#syncValueAttribute();
14272
14738
  this.#emitChange();
14273
- requestAnimationFrame(() => { this.#isAngleDragging = false; });
14739
+ requestAnimationFrame(() => {
14740
+ this.#isAngleDragging = false;
14741
+ });
14274
14742
  });
14275
14743
 
14276
14744
  this.#angleHandle.addEventListener("hitareadown", (e) => {
@@ -14303,7 +14771,11 @@ class FigCanvasControl extends HTMLElement {
14303
14771
  angle = Math.round(angle / 15) * 15;
14304
14772
  }
14305
14773
  this.#angle = angle;
14306
- if (this.#angleTooltip) this.#angleTooltip.setAttribute("text", `Angle ${Math.round(angle)}°`);
14774
+ if (this.#angleTooltip)
14775
+ this.#angleTooltip.setAttribute(
14776
+ "text",
14777
+ `Angle ${Math.round(angle)}°`,
14778
+ );
14307
14779
  this.#syncPositions();
14308
14780
  const curDeg = Math.round(angle);
14309
14781
  if (curDeg !== lastCursorDeg) {
@@ -14346,7 +14818,9 @@ class FigCanvasControl extends HTMLElement {
14346
14818
  this.#secondTooltip.showPopup?.();
14347
14819
  }
14348
14820
  this.#syncPositions();
14349
- document.body.style.cursor = this.#resizeCursorSvg(this.#pointPointLineDeg());
14821
+ document.body.style.cursor = this.#resizeCursorSvg(
14822
+ this.#pointPointLineDeg(),
14823
+ );
14350
14824
  this.#emitInput();
14351
14825
  });
14352
14826
 
@@ -14357,7 +14831,9 @@ class FigCanvasControl extends HTMLElement {
14357
14831
  this.#syncPositions();
14358
14832
  this.#syncValueAttribute();
14359
14833
  this.#emitChange();
14360
- requestAnimationFrame(() => { this.#isSecondDragging = false; });
14834
+ requestAnimationFrame(() => {
14835
+ this.#isSecondDragging = false;
14836
+ });
14361
14837
  });
14362
14838
 
14363
14839
  this.#setupPointPointHitArea(this.#pointHandle, true);
@@ -14421,7 +14897,9 @@ class FigCanvasControl extends HTMLElement {
14421
14897
  this.#y2 = newPctY;
14422
14898
  }
14423
14899
  this.#syncPositions();
14424
- const curDeg = Math.round(isFirst ? this.#pointPointLineDeg() + 180 : this.#pointPointLineDeg());
14900
+ const curDeg = Math.round(
14901
+ isFirst ? this.#pointPointLineDeg() + 180 : this.#pointPointLineDeg(),
14902
+ );
14425
14903
  if (curDeg !== lastCursorDeg) {
14426
14904
  lastCursorDeg = curDeg;
14427
14905
  document.body.style.cursor = this.#rotateCursorSvg(curDeg);
@@ -14453,7 +14931,10 @@ class FigCanvasControl extends HTMLElement {
14453
14931
  const rect = container.getBoundingClientRect();
14454
14932
  const cx = (this.#x / 100) * rect.width;
14455
14933
  const cy = (this.#y / 100) * rect.height;
14456
- const deg = (Math.atan2(e.clientY - rect.top - cy, e.clientX - rect.left - cx) * 180) / Math.PI;
14934
+ const deg =
14935
+ (Math.atan2(e.clientY - rect.top - cy, e.clientX - rect.left - cx) *
14936
+ 180) /
14937
+ Math.PI;
14457
14938
  circle.style.cursor = this.#resizeCursorSvg(deg);
14458
14939
  });
14459
14940
  const onDown = (e) => {
@@ -14475,7 +14956,10 @@ class FigCanvasControl extends HTMLElement {
14475
14956
  const rect0 = container.getBoundingClientRect();
14476
14957
  const cx0 = (this.#x / 100) * rect0.width;
14477
14958
  const cy0 = (this.#y / 100) * rect0.height;
14478
- const initDeg = (Math.atan2(e.clientY - rect0.top - cy0, e.clientX - rect0.left - cx0) * 180) / Math.PI;
14959
+ const initDeg =
14960
+ (Math.atan2(e.clientY - rect0.top - cy0, e.clientX - rect0.left - cx0) *
14961
+ 180) /
14962
+ Math.PI;
14479
14963
  let lastCursorDeg = Math.round(initDeg);
14480
14964
  document.body.style.cursor = this.#resizeCursorSvg(lastCursorDeg);
14481
14965
 
@@ -14506,7 +14990,8 @@ class FigCanvasControl extends HTMLElement {
14506
14990
  } else {
14507
14991
  this.#radius = Math.max(0, dist);
14508
14992
  }
14509
- if (this.#radiusTooltip) this.#radiusTooltip.setAttribute("text", this.#formatRadius());
14993
+ if (this.#radiusTooltip)
14994
+ this.#radiusTooltip.setAttribute("text", this.#formatRadius());
14510
14995
  this.#syncPositions();
14511
14996
  this.#emitInput();
14512
14997
  };
@@ -14527,7 +15012,8 @@ class FigCanvasControl extends HTMLElement {
14527
15012
  window.addEventListener("pointerup", onUp);
14528
15013
  };
14529
15014
  circle.addEventListener("pointerdown", onDown);
14530
- this._radiusDragCleanup = () => circle.removeEventListener("pointerdown", onDown);
15015
+ this._radiusDragCleanup = () =>
15016
+ circle.removeEventListener("pointerdown", onDown);
14531
15017
  }
14532
15018
 
14533
15019
  #teardownRadiusDrag() {
@@ -14689,8 +15175,10 @@ class FigHandle extends HTMLElement {
14689
15175
  };
14690
15176
 
14691
15177
  const axes = this.#axes;
14692
- if (axes.x) this.style.left = `${Math.round(resolve(xToken, rect.width, hw))}px`;
14693
- if (axes.y) this.style.top = `${Math.round(resolve(yToken, rect.height, hh))}px`;
15178
+ if (axes.x)
15179
+ this.style.left = `${Math.round(resolve(xToken, rect.width, hw))}px`;
15180
+ if (axes.y)
15181
+ this.style.top = `${Math.round(resolve(yToken, rect.height, hh))}px`;
14694
15182
  }
14695
15183
 
14696
15184
  #syncValueAttribute() {
@@ -14707,16 +15195,25 @@ class FigHandle extends HTMLElement {
14707
15195
  const raw = this.getAttribute("hit-area");
14708
15196
  if (!raw) return null;
14709
15197
  const tokens = raw.trim().split(/\s+/);
14710
- let vPad = 0, hPad = 0, circle = false;
15198
+ let vPad = 0,
15199
+ hPad = 0,
15200
+ circle = false;
14711
15201
  const nums = [];
14712
15202
  for (const t of tokens) {
14713
- if (t === "circle") { circle = true; continue; }
15203
+ if (t === "circle") {
15204
+ circle = true;
15205
+ continue;
15206
+ }
14714
15207
  const n = parseFloat(t);
14715
15208
  if (Number.isFinite(n)) nums.push(n);
14716
15209
  }
14717
- if (nums.length >= 2) { vPad = nums[0]; hPad = nums[1]; }
14718
- else if (nums.length === 1) { vPad = nums[0]; hPad = nums[0]; }
14719
- else return null;
15210
+ if (nums.length >= 2) {
15211
+ vPad = nums[0];
15212
+ hPad = nums[1];
15213
+ } else if (nums.length === 1) {
15214
+ vPad = nums[0];
15215
+ hPad = nums[0];
15216
+ } else return null;
14720
15217
  return { vPad, hPad, circle };
14721
15218
  }
14722
15219
 
@@ -14737,7 +15234,10 @@ class FigHandle extends HTMLElement {
14737
15234
  this.prepend(el);
14738
15235
  this.#hitAreaEl = el;
14739
15236
  }
14740
- this.style.setProperty("--fig-handle-hit-area-size", String(parsed.hPad * 2));
15237
+ this.style.setProperty(
15238
+ "--fig-handle-hit-area-size",
15239
+ String(parsed.hPad * 2),
15240
+ );
14741
15241
  if (parsed.circle) {
14742
15242
  this.#hitAreaEl.style.borderRadius = "50%";
14743
15243
  } else {
@@ -14751,10 +15251,12 @@ class FigHandle extends HTMLElement {
14751
15251
  if (this.#hitAreaMode === "delegate") {
14752
15252
  e.preventDefault();
14753
15253
  e.stopPropagation();
14754
- this.dispatchEvent(new CustomEvent("hitareadown", {
14755
- bubbles: true,
14756
- detail: { originalEvent: e },
14757
- }));
15254
+ this.dispatchEvent(
15255
+ new CustomEvent("hitareadown", {
15256
+ bubbles: true,
15257
+ detail: { originalEvent: e },
15258
+ }),
15259
+ );
14758
15260
  } else {
14759
15261
  this.#onPointerDown(e);
14760
15262
  }
@@ -14774,7 +15276,10 @@ class FigHandle extends HTMLElement {
14774
15276
  disconnectedCallback() {
14775
15277
  this.#teardownDrag();
14776
15278
  this.#hideColorTip();
14777
- if (this.#hitAreaEl) { this.#hitAreaEl.remove(); this.#hitAreaEl = null; }
15279
+ if (this.#hitAreaEl) {
15280
+ this.#hitAreaEl.remove();
15281
+ this.#hitAreaEl = null;
15282
+ }
14778
15283
  this.removeEventListener("click", this.#handleSelect);
14779
15284
  document.removeEventListener("pointerdown", this.#handleDeselect);
14780
15285
  document.removeEventListener("keydown", this.#handleKeyDown);
@@ -14783,7 +15288,8 @@ class FigHandle extends HTMLElement {
14783
15288
  select() {
14784
15289
  if (this.hasAttribute("disabled")) return;
14785
15290
  this.setAttribute("selected", "");
14786
- if (this.getAttribute("type") === "color" && !this.#isDragging) this.#showColorTip();
15291
+ if (this.getAttribute("type") === "color" && !this.#isDragging)
15292
+ this.#showColorTip();
14787
15293
  }
14788
15294
 
14789
15295
  deselect() {
@@ -14886,8 +15392,8 @@ class FigHandle extends HTMLElement {
14886
15392
  lastRect = rect;
14887
15393
  const currentLeft = parseFloat(this.style.left) || 0;
14888
15394
  const currentTop = parseFloat(this.style.top) || 0;
14889
- const rawX = (clientX - offsetX) - rect.left - handleW / 2;
14890
- const rawY = (clientY - offsetY) - rect.top - handleH / 2;
15395
+ const rawX = clientX - offsetX - rect.left - handleW / 2;
15396
+ const rawY = clientY - offsetY - rect.top - handleH / 2;
14891
15397
 
14892
15398
  const clampedX = Math.max(
14893
15399
  -handleW / 2,
@@ -15033,16 +15539,32 @@ class FigHandle extends HTMLElement {
15033
15539
  #handleColorTipInput = (e) => {
15034
15540
  e.stopPropagation();
15035
15541
  if (e.detail?.color) {
15036
- this.setAttribute("color", this.#colorWithOpacity(e.detail.color, e.detail.opacity));
15037
- this.dispatchEvent(new CustomEvent("input", { bubbles: true, detail: { color: e.detail.color, opacity: e.detail.opacity } }));
15542
+ this.setAttribute(
15543
+ "color",
15544
+ this.#colorWithOpacity(e.detail.color, e.detail.opacity),
15545
+ );
15546
+ this.dispatchEvent(
15547
+ new CustomEvent("input", {
15548
+ bubbles: true,
15549
+ detail: { color: e.detail.color, opacity: e.detail.opacity },
15550
+ }),
15551
+ );
15038
15552
  }
15039
15553
  };
15040
15554
 
15041
15555
  #handleColorTipChange = (e) => {
15042
15556
  e.stopPropagation();
15043
15557
  if (e.detail?.color) {
15044
- this.setAttribute("color", this.#colorWithOpacity(e.detail.color, e.detail.opacity));
15045
- this.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: { color: e.detail.color, opacity: e.detail.opacity } }));
15558
+ this.setAttribute(
15559
+ "color",
15560
+ this.#colorWithOpacity(e.detail.color, e.detail.opacity),
15561
+ );
15562
+ this.dispatchEvent(
15563
+ new CustomEvent("change", {
15564
+ bubbles: true,
15565
+ detail: { color: e.detail.color, opacity: e.detail.opacity },
15566
+ }),
15567
+ );
15046
15568
  }
15047
15569
  };
15048
15570