@rogieking/figui3 6.20.2 → 6.21.1

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
@@ -223,12 +223,18 @@ function figDefineCustomizedBuiltIn(name, constructor, options) {
223
223
  });
224
224
  }
225
225
 
226
+ function figDefineElement(name, constructor) {
227
+ if (!customElements.get(name)) {
228
+ customElements.define(name, constructor);
229
+ }
230
+ }
231
+
226
232
  function figUniqueId() {
227
233
  return Date.now().toString(36) + Math.random().toString(36).substring(2);
228
234
  }
229
235
 
230
236
  /** Zero-size portal for fixed overlays so they never affect body layout metrics. */
231
- function figGetOverlayRoot() {
237
+ function figGetOverlayRoot(source) {
232
238
  if (!document.body) return null;
233
239
  const attr = "data-figui-overlay-root";
234
240
  let root = document.body.querySelector(`:scope > [${attr}]`);
@@ -237,9 +243,40 @@ function figGetOverlayRoot() {
237
243
  root.setAttribute(attr, "");
238
244
  document.body.append(root);
239
245
  }
246
+ figSyncOverlayThemeFromSource(root, source);
240
247
  return root;
241
248
  }
242
249
 
250
+ /** Copy PropsKit / FigUI theme onto the overlay portal so portaled popups stay themed. */
251
+ function figSyncOverlayThemeFromSource(overlayRoot, source) {
252
+ if (!overlayRoot) return;
253
+ const panel =
254
+ (source instanceof Element
255
+ ? source.closest(".figui-root")
256
+ : null) || document.querySelector(".figui-root");
257
+ if (!panel) return;
258
+
259
+ const themeAttr = panel.getAttribute("theme");
260
+ const theme =
261
+ themeAttr === "light" || themeAttr === "dark" || themeAttr === "system"
262
+ ? themeAttr
263
+ : panel.classList.contains("figma-dark")
264
+ ? "dark"
265
+ : panel.classList.contains("figma-light")
266
+ ? "light"
267
+ : "system";
268
+
269
+ overlayRoot.classList.toggle("figma-light", theme === "light");
270
+ overlayRoot.classList.toggle("figma-dark", theme === "dark");
271
+ if (theme === "system") {
272
+ overlayRoot.style.setProperty("color-scheme", "light dark");
273
+ } else {
274
+ overlayRoot.style.setProperty("color-scheme", theme);
275
+ }
276
+ if (themeAttr) overlayRoot.setAttribute("theme", theme);
277
+ else overlayRoot.removeAttribute("theme");
278
+ }
279
+
243
280
  let _figZCounter = 10000;
244
281
  function figGetHighestZIndex() {
245
282
  return _figZCounter++;
@@ -520,7 +557,7 @@ class FigButton extends HTMLElement {
520
557
  this.removeEventListener("keydown", this.#boundHandleControlKeydown);
521
558
  }
522
559
  }
523
- customElements.define("fig-button", FigButton);
560
+ figDefineElement("fig-button", FigButton);
524
561
 
525
562
  /**
526
563
  * A custom dropdown/select element.
@@ -823,7 +860,7 @@ class FigDropdown extends HTMLElement {
823
860
  }
824
861
  }
825
862
 
826
- customElements.define("fig-dropdown", FigDropdown);
863
+ figDefineElement("fig-dropdown", FigDropdown);
827
864
 
828
865
  /* Tooltip */
829
866
  /**
@@ -1045,13 +1082,13 @@ class FigTooltip extends HTMLElement {
1045
1082
  // - Without popover support, fall back to today's behavior: nearest open
1046
1083
  // <dialog> ancestor if present, else document.body.
1047
1084
  if (supportsPopover) {
1048
- (figGetOverlayRoot() ?? document.body).append(this.popup);
1085
+ (figGetOverlayRoot(this) ?? document.body).append(this.popup);
1049
1086
  } else {
1050
1087
  const parentDialog = this.closest("dialog");
1051
1088
  if (parentDialog && parentDialog.open) {
1052
1089
  parentDialog.append(this.popup);
1053
1090
  } else {
1054
- (figGetOverlayRoot() ?? document.body).append(this.popup);
1091
+ (figGetOverlayRoot(this) ?? document.body).append(this.popup);
1055
1092
  }
1056
1093
  }
1057
1094
 
@@ -1433,13 +1470,13 @@ class FigTooltip extends HTMLElement {
1433
1470
  popup.append(content);
1434
1471
 
1435
1472
  if (supportsPopover) {
1436
- (figGetOverlayRoot() ?? document.body).append(popup);
1473
+ (figGetOverlayRoot(anchor) ?? document.body).append(popup);
1437
1474
  } else {
1438
1475
  const parentDialog = anchor.closest?.("dialog");
1439
1476
  if (parentDialog && parentDialog.open) {
1440
1477
  parentDialog.append(popup);
1441
1478
  } else {
1442
- (figGetOverlayRoot() ?? document.body).append(popup);
1479
+ (figGetOverlayRoot(anchor) ?? document.body).append(popup);
1443
1480
  }
1444
1481
  }
1445
1482
 
@@ -1464,7 +1501,7 @@ class FigTooltip extends HTMLElement {
1464
1501
  }
1465
1502
  }
1466
1503
 
1467
- customElements.define("fig-tooltip", FigTooltip);
1504
+ figDefineElement("fig-tooltip", FigTooltip);
1468
1505
 
1469
1506
  /* Text Truncation */
1470
1507
  class FigTruncate extends HTMLElement {
@@ -1543,7 +1580,7 @@ class FigTruncate extends HTMLElement {
1543
1580
  FigTooltip.hide(this);
1544
1581
  }
1545
1582
  }
1546
- customElements.define("fig-truncate", FigTruncate);
1583
+ figDefineElement("fig-truncate", FigTruncate);
1547
1584
 
1548
1585
  /* Dialog */
1549
1586
  /**
@@ -3837,7 +3874,7 @@ class FigTab extends HTMLElement {
3837
3874
  }
3838
3875
  }
3839
3876
  }
3840
- customElements.define("fig-tab", FigTab);
3877
+ figDefineElement("fig-tab", FigTab);
3841
3878
 
3842
3879
  /**
3843
3880
  * A custom tabs container element.
@@ -4167,7 +4204,7 @@ class FigTabs extends HTMLElement {
4167
4204
  }
4168
4205
  }
4169
4206
  }
4170
- customElements.define("fig-tabs", FigTabs);
4207
+ figDefineElement("fig-tabs", FigTabs);
4171
4208
 
4172
4209
  /* Segmented Control */
4173
4210
  /**
@@ -4255,7 +4292,7 @@ class FigSegment extends HTMLElement {
4255
4292
  }
4256
4293
  }
4257
4294
  }
4258
- customElements.define("fig-segment", FigSegment);
4295
+ figDefineElement("fig-segment", FigSegment);
4259
4296
 
4260
4297
  /**
4261
4298
  * A custom segmented control container element.
@@ -4739,7 +4776,7 @@ class FigSegmentedControl extends HTMLElement {
4739
4776
  }
4740
4777
  }
4741
4778
  }
4742
- customElements.define("fig-segmented-control", FigSegmentedControl);
4779
+ figDefineElement("fig-segmented-control", FigSegmentedControl);
4743
4780
 
4744
4781
  /* Options */
4745
4782
  /**
@@ -5084,7 +5121,7 @@ class FigOptions extends HTMLElement {
5084
5121
  }
5085
5122
  }
5086
5123
  }
5087
- customElements.define("fig-options", FigOptions);
5124
+ figDefineElement("fig-options", FigOptions);
5088
5125
 
5089
5126
  /* Slider */
5090
5127
  /**
@@ -5769,7 +5806,7 @@ class FigSlider extends HTMLElement {
5769
5806
  }
5770
5807
  }
5771
5808
  }
5772
- customElements.define("fig-slider", FigSlider);
5809
+ figDefineElement("fig-slider", FigSlider);
5773
5810
 
5774
5811
  /**
5775
5812
  * A custom text input element.
@@ -6373,7 +6410,7 @@ class FigInputText extends HTMLElement {
6373
6410
  }
6374
6411
  }
6375
6412
  }
6376
- customElements.define("fig-input-text", FigInputText);
6413
+ figDefineElement("fig-input-text", FigInputText);
6377
6414
 
6378
6415
  /**
6379
6416
  * A custom numeric input element that uses type="text" with inputmode="decimal".
@@ -7078,7 +7115,7 @@ class FigInputNumber extends HTMLElement {
7078
7115
  }
7079
7116
  }
7080
7117
  }
7081
- customElements.define("fig-input-number", FigInputNumber);
7118
+ figDefineElement("fig-input-number", FigInputNumber);
7082
7119
 
7083
7120
  /* Avatar */
7084
7121
  class FigAvatar extends HTMLElement {
@@ -7130,7 +7167,7 @@ class FigAvatar extends HTMLElement {
7130
7167
  }
7131
7168
  }
7132
7169
  }
7133
- customElements.define("fig-avatar", FigAvatar);
7170
+ figDefineElement("fig-avatar", FigAvatar);
7134
7171
 
7135
7172
  /* Form Field */
7136
7173
  class FigField extends HTMLElement {
@@ -7329,7 +7366,7 @@ class FigField extends HTMLElement {
7329
7366
  }
7330
7367
  }
7331
7368
  }
7332
- customElements.define("fig-field", FigField);
7369
+ figDefineElement("fig-field", FigField);
7333
7370
 
7334
7371
  /* Color swatch */
7335
7372
  class FigInputColor extends HTMLElement {
@@ -8025,7 +8062,7 @@ class FigInputColor extends HTMLElement {
8025
8062
  return { r, g, b, a };
8026
8063
  }
8027
8064
  }
8028
- customElements.define("fig-input-color", FigInputColor);
8065
+ figDefineElement("fig-input-color", FigInputColor);
8029
8066
 
8030
8067
  /* Input Fill */
8031
8068
  const GRADIENT_INTERPOLATION_SPACES = [
@@ -8034,6 +8071,7 @@ const GRADIENT_INTERPOLATION_SPACES = [
8034
8071
  "display-p3",
8035
8072
  "oklab",
8036
8073
  "oklch",
8074
+ "hsl",
8037
8075
  ];
8038
8076
  const GRADIENT_HUE_INTERPOLATIONS = [
8039
8077
  "shorter",
@@ -8041,6 +8079,7 @@ const GRADIENT_HUE_INTERPOLATIONS = [
8041
8079
  "increasing",
8042
8080
  "decreasing",
8043
8081
  ];
8082
+ const GRADIENT_HUE_SPACES = new Set(["oklch", "hsl"]);
8044
8083
 
8045
8084
  const GRADIENT_PICKER_SPACES = ["srgb", "srgb-linear", "oklab", "oklch"];
8046
8085
 
@@ -8069,7 +8108,7 @@ function gradientToValueShape(gradient) {
8069
8108
  ...normalized,
8070
8109
  interpolationSpace: normalized.interpolationSpace,
8071
8110
  };
8072
- if (normalized.interpolationSpace === "oklch") {
8111
+ if (GRADIENT_HUE_SPACES.has(normalized.interpolationSpace)) {
8073
8112
  output.hueInterpolation = normalized.hueInterpolation;
8074
8113
  } else {
8075
8114
  delete output.hueInterpolation;
@@ -8082,8 +8121,8 @@ function gradientInterpolationClause(gradient) {
8082
8121
  if (normalized.interpolationSpace === "srgb") {
8083
8122
  return "";
8084
8123
  }
8085
- if (normalized.interpolationSpace === "oklch") {
8086
- return `in oklch ${normalized.hueInterpolation} hue`;
8124
+ if (GRADIENT_HUE_SPACES.has(normalized.interpolationSpace)) {
8125
+ return `in ${normalized.interpolationSpace} ${normalized.hueInterpolation} hue`;
8087
8126
  }
8088
8127
  return `in ${normalized.interpolationSpace}`;
8089
8128
  }
@@ -8232,6 +8271,25 @@ function figSampleGradientAt(
8232
8271
  r = rgb.r;
8233
8272
  g = rgb.g;
8234
8273
  b = rgb.b;
8274
+ } else if (space === "hsl") {
8275
+ const hsl1 = figRgbToHsl(c1.r, c1.g, c1.b);
8276
+ const hsl2 = figRgbToHsl(c2.r, c2.g, c2.b);
8277
+ const H = figInterpolateHue(
8278
+ hsl1.h,
8279
+ hsl2.h,
8280
+ t,
8281
+ hueInterpolation || "shorter",
8282
+ );
8283
+ const S = hsl1.s + (hsl2.s - hsl1.s) * t;
8284
+ const L = hsl1.l + (hsl2.l - hsl1.l) * t;
8285
+ const rgb = hslToSRGB(H, S, L);
8286
+ r = rgb[0];
8287
+ g = rgb[1];
8288
+ b = rgb[2];
8289
+ } else if (space === "srgb") {
8290
+ r = Math.round(c1.r + (c2.r - c1.r) * t);
8291
+ g = Math.round(c1.g + (c2.g - c1.g) * t);
8292
+ b = Math.round(c1.b + (c2.b - c1.b) * t);
8235
8293
  } else {
8236
8294
  const lab1 = figRGBToOklab(c1.r, c1.g, c1.b);
8237
8295
  const lab2 = figRGBToOklab(c2.r, c2.g, c2.b);
@@ -8247,6 +8305,33 @@ function figSampleGradientAt(
8247
8305
  return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`.toUpperCase();
8248
8306
  }
8249
8307
 
8308
+ function figRgbToHsl(r, g, b) {
8309
+ const R = r / 255;
8310
+ const G = g / 255;
8311
+ const B = b / 255;
8312
+ const max = Math.max(R, G, B);
8313
+ const min = Math.min(R, G, B);
8314
+ const l = (max + min) / 2;
8315
+ if (max === min) {
8316
+ return { h: 0, s: 0, l: l * 100 };
8317
+ }
8318
+ const d = max - min;
8319
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
8320
+ let h;
8321
+ switch (max) {
8322
+ case R:
8323
+ h = ((G - B) / d + (G < B ? 6 : 0)) / 6;
8324
+ break;
8325
+ case G:
8326
+ h = ((B - R) / d + 2) / 6;
8327
+ break;
8328
+ default:
8329
+ h = ((R - G) / d + 4) / 6;
8330
+ break;
8331
+ }
8332
+ return { h: h * 360, s: s * 100, l: l * 100 };
8333
+ }
8334
+
8250
8335
  function hslToP3(h, s, l) {
8251
8336
  const sRGB = hslToSRGB(h, s, l);
8252
8337
  return sRGB.map((c) => +(c / 255).toFixed(4));
@@ -8311,6 +8396,7 @@ class FigInputFill extends HTMLElement {
8311
8396
  angle: 180,
8312
8397
  interpolationSpace: "srgb",
8313
8398
  hueInterpolation: "shorter",
8399
+ opacity: 1,
8314
8400
  stops: [
8315
8401
  { position: 0, color: "#D9D9D9", opacity: 100 },
8316
8402
  { position: 100, color: "#737373", opacity: 100 },
@@ -8371,6 +8457,15 @@ class FigInputFill extends HTMLElement {
8371
8457
  ...this.#gradient,
8372
8458
  ...parsed.gradient,
8373
8459
  });
8460
+ this.#gradient.opacity = this.#normalizeFillOpacity(
8461
+ parsed.gradient.opacity ?? parsed.opacity ?? parsed.alpha,
8462
+ this.#gradient.opacity ?? 1,
8463
+ );
8464
+ } else if (parsed.opacity !== undefined || parsed.alpha !== undefined) {
8465
+ this.#gradient.opacity = this.#normalizeFillOpacity(
8466
+ parsed.opacity ?? parsed.alpha,
8467
+ this.#gradient.opacity ?? 1,
8468
+ );
8374
8469
  }
8375
8470
  break;
8376
8471
  case "image":
@@ -8446,10 +8541,20 @@ class FigInputFill extends HTMLElement {
8446
8541
  }
8447
8542
  }
8448
8543
 
8544
+ #normalizeFillOpacity(value, fallback = 1) {
8545
+ if (value === undefined || value === null || value === "") return fallback;
8546
+ const n = Number(value);
8547
+ if (!Number.isFinite(n)) return fallback;
8548
+ if (n > 1) return Math.max(0, Math.min(1, n / 100));
8549
+ return Math.max(0, Math.min(1, n));
8550
+ }
8551
+
8449
8552
  #fillPickerSwatchAlpha() {
8450
8553
  switch (this.#fillType) {
8451
8554
  case "solid":
8452
8555
  return this.#solid.alpha;
8556
+ case "gradient":
8557
+ return this.#gradient.opacity ?? 1;
8453
8558
  case "image":
8454
8559
  return this.#image.opacity ?? 1;
8455
8560
  case "video":
@@ -8507,9 +8612,9 @@ class FigInputFill extends HTMLElement {
8507
8612
  const opacityHtml = (value) =>
8508
8613
  showAlpha
8509
8614
  ? `<fig-tooltip text="Opacity">
8510
- <fig-input-number
8615
+ <fig-input-number
8511
8616
  class="fig-input-fill-opacity"
8512
- placeholder="##"
8617
+ placeholder="##"
8513
8618
  min="0"
8514
8619
  max="100"
8515
8620
  value="${value}"
@@ -8539,7 +8644,8 @@ class FigInputFill extends HTMLElement {
8539
8644
  this.#gradient.type.charAt(0).toUpperCase() +
8540
8645
  this.#gradient.type.slice(1);
8541
8646
  controlsHtml = `
8542
- <label class="fig-input-fill-label">${figEscapeAttribute(gradientLabel)}</label>`;
8647
+ <label class="fig-input-fill-label">${figEscapeAttribute(gradientLabel)}</label>
8648
+ ${opacityHtml(Math.round((this.#gradient.opacity ?? 1) * 100))}`;
8543
8649
  break;
8544
8650
  }
8545
8651
 
@@ -8679,6 +8785,7 @@ class FigInputFill extends HTMLElement {
8679
8785
  this.#solid.alpha = alpha;
8680
8786
  break;
8681
8787
  case "gradient":
8788
+ this.#gradient.opacity = alpha;
8682
8789
  break;
8683
8790
  case "image":
8684
8791
  this.#image.opacity = alpha;
@@ -8727,6 +8834,12 @@ class FigInputFill extends HTMLElement {
8727
8834
  this.#gradient.type.slice(1);
8728
8835
  label.textContent = newLabel;
8729
8836
  }
8837
+ if (this.#opacityInput) {
8838
+ this.#opacityInput.setAttribute(
8839
+ "value",
8840
+ Math.round((this.#gradient.opacity ?? 1) * 100),
8841
+ );
8842
+ }
8730
8843
  break;
8731
8844
  }
8732
8845
  case "image":
@@ -8803,7 +8916,18 @@ class FigInputFill extends HTMLElement {
8803
8916
  this.#gradient.type.charAt(0).toUpperCase() +
8804
8917
  this.#gradient.type.slice(1);
8805
8918
  controlsHtml = `
8806
- <label class="fig-input-fill-label">${figEscapeAttribute(gradientLabel)}</label>`;
8919
+ <label class="fig-input-fill-label">${figEscapeAttribute(gradientLabel)}</label>
8920
+ ${showAlpha ? `<fig-tooltip text="Opacity">
8921
+ <fig-input-number
8922
+ class="fig-input-fill-opacity"
8923
+ placeholder="##"
8924
+ min="0"
8925
+ max="100"
8926
+ value="${Math.round((this.#gradient.opacity ?? 1) * 100)}"
8927
+ units="%"
8928
+ ${disabled ? "disabled" : ""}>
8929
+ </fig-input-number>
8930
+ </fig-tooltip>` : ""}`;
8807
8931
  break;
8808
8932
  }
8809
8933
  case "image":
@@ -8899,6 +9023,7 @@ class FigInputFill extends HTMLElement {
8899
9023
  this.#solid.alpha = alpha;
8900
9024
  break;
8901
9025
  case "gradient":
9026
+ this.#gradient.opacity = alpha;
8902
9027
  break;
8903
9028
  case "image":
8904
9029
  this.#image.opacity = alpha;
@@ -9038,7 +9163,7 @@ class FigInputFill extends HTMLElement {
9038
9163
  }
9039
9164
  }
9040
9165
  }
9041
- customElements.define("fig-input-fill", FigInputFill);
9166
+ figDefineElement("fig-input-fill", FigInputFill);
9042
9167
 
9043
9168
  /* Input Palette */
9044
9169
  /**
@@ -9498,7 +9623,7 @@ class FigInputPalette extends HTMLElement {
9498
9623
  );
9499
9624
  }
9500
9625
  }
9501
- customElements.define("fig-input-palette", FigInputPalette);
9626
+ figDefineElement("fig-input-palette", FigInputPalette);
9502
9627
 
9503
9628
  /* Input Gradient */
9504
9629
  /**
@@ -9743,6 +9868,11 @@ class FigInputGradient extends HTMLElement {
9743
9868
  .join(", ");
9744
9869
  const interp = gradientInterpolationClause(gradient);
9745
9870
  const interpolation = interp ? ` ${interp}` : "";
9871
+ // Inline stop editor always previews L→R so handles match the track axis,
9872
+ // regardless of linear angle / radial / angular type.
9873
+ if (this.#editMode === "true") {
9874
+ return `linear-gradient(to right${interpolation}, ${stops})`;
9875
+ }
9746
9876
  if (gradient.type === "radial") {
9747
9877
  return `radial-gradient(circle at ${gradient.centerX}% ${gradient.centerY}%${interpolation}, ${stops})`;
9748
9878
  }
@@ -10405,7 +10535,7 @@ class FigInputGradient extends HTMLElement {
10405
10535
  }
10406
10536
  }
10407
10537
  }
10408
- customElements.define("fig-input-gradient", FigInputGradient);
10538
+ figDefineElement("fig-input-gradient", FigInputGradient);
10409
10539
 
10410
10540
  /* Checkbox */
10411
10541
  /**
@@ -10643,7 +10773,7 @@ class FigCheckbox extends HTMLElement {
10643
10773
  );
10644
10774
  }
10645
10775
  }
10646
- customElements.define("fig-checkbox", FigCheckbox);
10776
+ figDefineElement("fig-checkbox", FigCheckbox);
10647
10777
 
10648
10778
  /* Radio */
10649
10779
  /**
@@ -10663,7 +10793,7 @@ class FigRadio extends FigCheckbox {
10663
10793
  else this.input.removeAttribute("name");
10664
10794
  }
10665
10795
  }
10666
- customElements.define("fig-radio", FigRadio);
10796
+ figDefineElement("fig-radio", FigRadio);
10667
10797
 
10668
10798
  /* Switch */
10669
10799
  /**
@@ -10680,7 +10810,7 @@ class FigSwitch extends FigCheckbox {
10680
10810
  this.input.setAttribute("role", "switch");
10681
10811
  }
10682
10812
  }
10683
- customElements.define("fig-switch", FigSwitch);
10813
+ figDefineElement("fig-switch", FigSwitch);
10684
10814
 
10685
10815
  /* Combo Input */
10686
10816
  /**
@@ -10975,7 +11105,7 @@ class FigComboInput extends HTMLElement {
10975
11105
  }
10976
11106
  }
10977
11107
  }
10978
- customElements.define("fig-combo-input", FigComboInput);
11108
+ figDefineElement("fig-combo-input", FigComboInput);
10979
11109
 
10980
11110
  /* Swatch */
10981
11111
  /**
@@ -11191,7 +11321,7 @@ class FigSwatch extends HTMLElement {
11191
11321
  }
11192
11322
  }
11193
11323
  }
11194
- customElements.define("fig-swatch", FigSwatch);
11324
+ figDefineElement("fig-swatch", FigSwatch);
11195
11325
 
11196
11326
  /* Media */
11197
11327
  /**
@@ -11213,6 +11343,9 @@ customElements.define("fig-swatch", FigSwatch);
11213
11343
  * @attr {string} aria-label - Accessible label forwarded to generated video
11214
11344
  * @attr {string} aria-labelledby - Accessible label reference forwarded to generated video
11215
11345
  *
11346
+ * When `controls` are shown and no video `src` is loaded, fig-media sets
11347
+ * `disabled` on the associated `fig-media-controls` until media is available.
11348
+ *
11216
11349
  * Sizing model:
11217
11350
  * - Default: host shrinkwraps to its inner <img>/<video> intrinsic size.
11218
11351
  * - `size` attribute applies a token-sized square.
@@ -11551,12 +11684,41 @@ class FigMedia extends HTMLElement {
11551
11684
  this.#mediaEl.playsInline = true;
11552
11685
  this.#syncMediaAccessibility();
11553
11686
  this.#syncControlsVisibility();
11687
+ this.#syncControlsDisabled();
11554
11688
  }
11555
11689
 
11556
11690
  get mediaEl() {
11557
11691
  return this.#mediaEl;
11558
11692
  }
11559
11693
 
11694
+ #hasPlayableVideo() {
11695
+ if (this.mediaKind !== "video") return false;
11696
+ return Boolean(this.#currentMediaSrc());
11697
+ }
11698
+
11699
+ #syncControlsDisabled() {
11700
+ const controls =
11701
+ this.#controlsEl ||
11702
+ this.querySelector(":scope > fig-media-controls");
11703
+ if (!controls) return;
11704
+ if (this.mediaKind !== "video") return;
11705
+
11706
+ if (this.#hasPlayableVideo()) {
11707
+ // Only clear disabled when we set it for the empty state.
11708
+ if (controls.hasAttribute("data-fig-empty-disabled")) {
11709
+ controls.removeAttribute("disabled");
11710
+ controls.removeAttribute("data-fig-empty-disabled");
11711
+ }
11712
+ return;
11713
+ }
11714
+
11715
+ controls.setAttribute("disabled", "");
11716
+ controls.setAttribute("data-fig-empty-disabled", "");
11717
+ controls.playing = false;
11718
+ controls.time = 0;
11719
+ controls.duration = 0;
11720
+ }
11721
+
11560
11722
  #syncControlsVisibility() {
11561
11723
  if (this.mediaKind !== "video") {
11562
11724
  this.#removeControls();
@@ -11571,6 +11733,7 @@ class FigMedia extends HTMLElement {
11571
11733
  this.#controlsEl = userControls;
11572
11734
  }
11573
11735
  this.#wireControlsToMedia();
11736
+ this.#syncControlsDisabled();
11574
11737
  return;
11575
11738
  }
11576
11739
  if (this.#isEnabledAttr("controls", false)) {
@@ -11590,6 +11753,7 @@ class FigMedia extends HTMLElement {
11590
11753
  if (existingControls) {
11591
11754
  this.#controlsEl = existingControls;
11592
11755
  this.#wireControlsToMedia();
11756
+ this.#syncControlsDisabled();
11593
11757
  return;
11594
11758
  }
11595
11759
  const controls = document.createElement("fig-media-controls");
@@ -11597,6 +11761,7 @@ class FigMedia extends HTMLElement {
11597
11761
  this.append(controls);
11598
11762
  this.#controlsEl = controls;
11599
11763
  this.#wireControlsToMedia();
11764
+ this.#syncControlsDisabled();
11600
11765
  }
11601
11766
 
11602
11767
  #wireControlsToMedia() {
@@ -11605,6 +11770,7 @@ class FigMedia extends HTMLElement {
11605
11770
  this.#controlsWiredFor === this.#mediaEl &&
11606
11771
  this.#controlsWiredControls === this.#controlsEl
11607
11772
  ) {
11773
+ this.#syncControlsDisabled();
11608
11774
  return;
11609
11775
  }
11610
11776
  this.#unwireControls();
@@ -11616,6 +11782,8 @@ class FigMedia extends HTMLElement {
11616
11782
 
11617
11783
  let pendingSeekTime = null;
11618
11784
  const syncFromVideo = () => {
11785
+ this.#syncControlsDisabled();
11786
+ if (!this.#hasPlayableVideo()) return;
11619
11787
  controls.playing = !video.paused && !video.ended;
11620
11788
  if (Number.isFinite(video.duration)) controls.duration = video.duration;
11621
11789
  if (pendingSeekTime !== null) {
@@ -11628,11 +11796,13 @@ class FigMedia extends HTMLElement {
11628
11796
  controls.time = video.currentTime || 0;
11629
11797
  };
11630
11798
  const onPlay = () => {
11799
+ if (!this.#hasPlayableVideo()) return;
11631
11800
  const p = video.play?.();
11632
11801
  if (p && typeof p.catch === "function") p.catch(() => {});
11633
11802
  };
11634
11803
  const onPause = () => video.pause?.();
11635
11804
  const onSeek = (e) => {
11805
+ if (!this.#hasPlayableVideo()) return;
11636
11806
  const next = Number(e?.detail?.time);
11637
11807
  if (!Number.isFinite(next)) return;
11638
11808
  pendingSeekTime = next;
@@ -11907,21 +12077,21 @@ class FigMedia extends HTMLElement {
11907
12077
  }
11908
12078
  }
11909
12079
 
11910
- customElements.define("fig-media", FigMedia);
12080
+ figDefineElement("fig-media", FigMedia);
11911
12081
 
11912
12082
  class FigImage extends FigMedia {
11913
12083
  get mediaKind() {
11914
12084
  return "image";
11915
12085
  }
11916
12086
  }
11917
- customElements.define("fig-image", FigImage);
12087
+ figDefineElement("fig-image", FigImage);
11918
12088
 
11919
12089
  class FigVideo extends FigMedia {
11920
12090
  get mediaKind() {
11921
12091
  return "video";
11922
12092
  }
11923
12093
  }
11924
- customElements.define("fig-video", FigVideo);
12094
+ figDefineElement("fig-video", FigVideo);
11925
12095
 
11926
12096
  /**
11927
12097
  * <fig-card> — Media card with optional link, selection chrome, and truncated label.
@@ -12194,7 +12364,7 @@ class FigCard extends HTMLElement {
12194
12364
  }
12195
12365
  }
12196
12366
  }
12197
- customElements.define("fig-card", FigCard);
12367
+ figDefineElement("fig-card", FigCard);
12198
12368
 
12199
12369
  /**
12200
12370
  * <fig-media-controls> — Standalone playback controls UI.
@@ -12206,13 +12376,14 @@ customElements.define("fig-card", FigCard);
12206
12376
  * - `playing` (boolean presence) — current play/pause state
12207
12377
  * - `duration` (number, seconds) — total track length
12208
12378
  * - `time` (number, seconds) — current playhead position
12379
+ * - `disabled` (boolean) — disables play/seek interaction
12209
12380
  *
12210
12381
  * Events:
12211
12382
  * - `play` — emitted when the user toggles playback on (detail: { playing: true })
12212
12383
  * - `pause` — emitted when the user toggles playback off (detail: { playing: false })
12213
12384
  * - `seek` — emitted when the user drags the scrubber (detail: { time })
12214
12385
  *
12215
- * Properties: `playing`, `duration`, `time` mirror the attributes.
12386
+ * Properties: `playing`, `duration`, `time`, `disabled` mirror the attributes.
12216
12387
  */
12217
12388
  class FigMediaControls extends HTMLElement {
12218
12389
  #playBtn = null;
@@ -12223,13 +12394,14 @@ class FigMediaControls extends HTMLElement {
12223
12394
  #rendered = false;
12224
12395
 
12225
12396
  static get observedAttributes() {
12226
- return ["playing", "duration", "time"];
12397
+ return ["playing", "duration", "time", "disabled"];
12227
12398
  }
12228
12399
 
12229
12400
  connectedCallback() {
12230
12401
  this.#render();
12231
12402
  this.#syncPlayingUi();
12232
12403
  this.#syncTimeUi();
12404
+ this.#syncDisabled();
12233
12405
  }
12234
12406
 
12235
12407
  get playing() {
@@ -12266,10 +12438,19 @@ class FigMediaControls extends HTMLElement {
12266
12438
  this.setAttribute("time", String(n));
12267
12439
  }
12268
12440
 
12441
+ get disabled() {
12442
+ return figBooleanAttribute(this, "disabled");
12443
+ }
12444
+ set disabled(value) {
12445
+ if (value) this.setAttribute("disabled", "");
12446
+ else this.removeAttribute("disabled");
12447
+ }
12448
+
12269
12449
  attributeChangedCallback(name) {
12270
12450
  if (!this.#rendered) return;
12271
12451
  if (name === "playing") this.#syncPlayingUi();
12272
12452
  if (name === "duration" || name === "time") this.#syncTimeUi();
12453
+ if (name === "disabled") this.#syncDisabled();
12273
12454
  }
12274
12455
 
12275
12456
  #render() {
@@ -12315,6 +12496,11 @@ class FigMediaControls extends HTMLElement {
12315
12496
  timeEl.textContent = this.#formatTime(this.time);
12316
12497
 
12317
12498
  const handleSeek = (e) => {
12499
+ if (this.disabled) {
12500
+ e.preventDefault?.();
12501
+ e.stopPropagation();
12502
+ return;
12503
+ }
12318
12504
  const host = e.currentTarget;
12319
12505
  const next = Number(host?.value);
12320
12506
  if (!Number.isFinite(next)) return;
@@ -12340,6 +12526,20 @@ class FigMediaControls extends HTMLElement {
12340
12526
  this.#playTooltip = tooltip;
12341
12527
  this.#timeSlider = slider;
12342
12528
  this.#timeEl = timeEl;
12529
+ this.#syncDisabled();
12530
+ }
12531
+
12532
+ #syncDisabled() {
12533
+ const disabled = this.disabled;
12534
+ this.setAttribute("aria-disabled", disabled ? "true" : "false");
12535
+ if (this.#playBtn) {
12536
+ if (disabled) this.#playBtn.setAttribute("disabled", "");
12537
+ else this.#playBtn.removeAttribute("disabled");
12538
+ }
12539
+ if (this.#timeSlider) {
12540
+ if (disabled) this.#timeSlider.setAttribute("disabled", "");
12541
+ else this.#timeSlider.removeAttribute("disabled");
12542
+ }
12343
12543
  }
12344
12544
 
12345
12545
  #formatTime(seconds) {
@@ -12385,6 +12585,7 @@ class FigMediaControls extends HTMLElement {
12385
12585
  }
12386
12586
 
12387
12587
  toggle() {
12588
+ if (this.disabled) return;
12388
12589
  const next = !this.playing;
12389
12590
  this.playing = next;
12390
12591
  this.dispatchEvent(
@@ -12397,16 +12598,16 @@ class FigMediaControls extends HTMLElement {
12397
12598
  }
12398
12599
 
12399
12600
  play() {
12400
- if (this.playing) return;
12601
+ if (this.disabled || this.playing) return;
12401
12602
  this.toggle();
12402
12603
  }
12403
12604
 
12404
12605
  pause() {
12405
- if (!this.playing) return;
12606
+ if (this.disabled || !this.playing) return;
12406
12607
  this.toggle();
12407
12608
  }
12408
12609
  }
12409
- customElements.define("fig-media-controls", FigMediaControls);
12610
+ figDefineElement("fig-media-controls", FigMediaControls);
12410
12611
 
12411
12612
  /* File Upload Input */
12412
12613
  class FigInputFile extends HTMLElement {
@@ -12670,7 +12871,7 @@ class FigInputFile extends HTMLElement {
12670
12871
  }
12671
12872
  }
12672
12873
  }
12673
- customElements.define("fig-input-file", FigInputFile);
12874
+ figDefineElement("fig-input-file", FigInputFile);
12674
12875
 
12675
12876
  /**
12676
12877
  * A bezier / spring easing curve editor with draggable control points.
@@ -13807,7 +14008,7 @@ class FigEasingCurve extends HTMLElement {
13807
14008
  document.addEventListener("pointerup", onUp);
13808
14009
  }
13809
14010
  }
13810
- customElements.define("fig-easing-curve", FigEasingCurve);
14011
+ figDefineElement("fig-easing-curve", FigEasingCurve);
13811
14012
 
13812
14013
  /**
13813
14014
  * A 3D rotation control with an interactive cube preview.
@@ -14177,7 +14378,7 @@ class Fig3DRotate extends HTMLElement {
14177
14378
  this.#container.addEventListener("lostpointercapture", onEnd);
14178
14379
  }
14179
14380
  }
14180
- customElements.define("fig-3d-rotate", Fig3DRotate);
14381
+ figDefineElement("fig-3d-rotate", Fig3DRotate);
14181
14382
 
14182
14383
  /**
14183
14384
  * A transform-origin grid control with draggable handle.
@@ -14727,7 +14928,7 @@ class FigOriginGrid extends HTMLElement {
14727
14928
  bindValueInput(this.#yInput, "y");
14728
14929
  }
14729
14930
  }
14730
- customElements.define("fig-origin-grid", FigOriginGrid);
14931
+ figDefineElement("fig-origin-grid", FigOriginGrid);
14731
14932
 
14732
14933
  /**
14733
14934
  * A custom joystick input element.
@@ -15152,7 +15353,7 @@ class FigInputJoystick extends HTMLElement {
15152
15353
  }
15153
15354
  }
15154
15355
 
15155
- customElements.define("fig-joystick", FigInputJoystick);
15356
+ figDefineElement("fig-joystick", FigInputJoystick);
15156
15357
 
15157
15358
 
15158
15359
  // FigInputAngle moved to fig-lab.js
@@ -15209,7 +15410,7 @@ class FigShimmer extends HTMLElement {
15209
15410
  }
15210
15411
  }
15211
15412
  }
15212
- customElements.define("fig-shimmer", FigShimmer);
15413
+ figDefineElement("fig-shimmer", FigShimmer);
15213
15414
 
15214
15415
  // FigSkeleton
15215
15416
  class FigSkeleton extends FigShimmer {
@@ -15219,7 +15420,7 @@ class FigSkeleton extends FigShimmer {
15219
15420
  this.setAttribute("inert", "");
15220
15421
  }
15221
15422
  }
15222
- customElements.define("fig-skeleton", FigSkeleton);
15423
+ figDefineElement("fig-skeleton", FigSkeleton);
15223
15424
 
15224
15425
  // FigGroup
15225
15426
  class FigGroup extends HTMLElement {
@@ -15365,7 +15566,7 @@ class FigGroup extends HTMLElement {
15365
15566
  }
15366
15567
  }
15367
15568
  }
15368
- customElements.define("fig-group", FigGroup);
15569
+ figDefineElement("fig-group", FigGroup);
15369
15570
 
15370
15571
  /**
15371
15572
  * A presentational header element used inside fig-dialog, fig-group, and other containers.
@@ -15375,7 +15576,7 @@ customElements.define("fig-group", FigGroup);
15375
15576
  * @attr {boolean} dialog-header - Marks this as a dialog header (auto-generated by fig-dialog)
15376
15577
  */
15377
15578
  class FigHeader extends HTMLElement {}
15378
- customElements.define("fig-header", FigHeader);
15579
+ figDefineElement("fig-header", FigHeader);
15379
15580
 
15380
15581
  /**
15381
15582
  * fig-footer
@@ -15384,7 +15585,7 @@ customElements.define("fig-header", FigHeader);
15384
15585
  * @attr {boolean} sticky - Pins the footer to the bottom of its scroll container
15385
15586
  */
15386
15587
  class FigFooter extends HTMLElement {}
15387
- customElements.define("fig-footer", FigFooter);
15588
+ figDefineElement("fig-footer", FigFooter);
15388
15589
 
15389
15590
  /* Presentational elements (CSS-only, no behavior) */
15390
15591
  class FigSpinner extends HTMLElement {
@@ -15395,7 +15596,7 @@ class FigSpinner extends HTMLElement {
15395
15596
  }
15396
15597
  }
15397
15598
  }
15398
- customElements.define("fig-spinner", FigSpinner);
15599
+ figDefineElement("fig-spinner", FigSpinner);
15399
15600
 
15400
15601
  /**
15401
15602
  * A styled visual preview layer for arbitrary content such as images, canvas,
@@ -15425,7 +15626,315 @@ class FigPreview extends HTMLElement {
15425
15626
  }
15426
15627
  }
15427
15628
  }
15428
- customElements.define("fig-preview", FigPreview);
15629
+ figDefineElement("fig-preview", FigPreview);
15630
+
15631
+ /**
15632
+ * Compact swatch previewing gradient color-space interpolation.
15633
+ * Polar: CSS conic-gradient masked (SVG data-URL, round linecaps) to an arc.
15634
+ * Non-polar: CSS linear-gradient masked to a horizontal round-capped stroke.
15635
+ * Accepts the same `value` shape as fig-input-gradient / fig-fill-picker.
15636
+ *
15637
+ * @element fig-interpolation-swatch
15638
+ * @attr {string} value - JSON `{ type: "gradient", gradient: { … } }` (or a bare gradient object)
15639
+ * @attr {string} size - `small` (default, 24px) or `large` (32px)
15640
+ */
15641
+ class FigInterpolationSwatch extends HTMLElement {
15642
+ static #HUE_SPACES = new Set(["oklch", "hsl"]);
15643
+ static #CX = 10;
15644
+ static #CY = 10;
15645
+ static #R = 8;
15646
+ static #STROKE = 3;
15647
+ // Polar endpoints ≈ 10 o'clock → 2 o'clock (SVG deg: 0 = east, CW).
15648
+ // CSS conic `from` is 0 = north; convert with +90.
15649
+ static #START_DEG = 210;
15650
+ static #DEFAULT_GRADIENT = {
15651
+ type: "linear",
15652
+ angle: 135,
15653
+ interpolationSpace: "srgb",
15654
+ hueInterpolation: "shorter",
15655
+ stops: [
15656
+ { color: "#FF0000", position: 0, opacity: 100 },
15657
+ { color: "#4F9EFF", position: 100, opacity: 100 },
15658
+ ],
15659
+ };
15660
+
15661
+ #rendered = false;
15662
+ #svgEl = null;
15663
+ #fillEl = null;
15664
+ #gradient = { ...FigInterpolationSwatch.#DEFAULT_GRADIENT };
15665
+
15666
+ static get observedAttributes() {
15667
+ return ["value"];
15668
+ }
15669
+
15670
+ get value() {
15671
+ return {
15672
+ type: "gradient",
15673
+ gradient: { ...this.#gradient },
15674
+ };
15675
+ }
15676
+
15677
+ set value(val) {
15678
+ if (val == null || val === "") {
15679
+ this.removeAttribute("value");
15680
+ return;
15681
+ }
15682
+ if (typeof val === "string") {
15683
+ this.setAttribute("value", val);
15684
+ return;
15685
+ }
15686
+ this.setAttribute("value", JSON.stringify(val));
15687
+ }
15688
+
15689
+ connectedCallback() {
15690
+ this.#ensureA11y();
15691
+ this.#parseValue();
15692
+ this.#render();
15693
+ this.#updatePreview();
15694
+ }
15695
+
15696
+ attributeChangedCallback(name, oldValue, newValue) {
15697
+ if (oldValue === newValue) return;
15698
+ if (name !== "value") return;
15699
+ this.#parseValue();
15700
+ if (this.#rendered) this.#updatePreview();
15701
+ }
15702
+
15703
+ #ensureA11y() {
15704
+ const named =
15705
+ this.hasAttribute("aria-label") || this.hasAttribute("aria-labelledby");
15706
+ if (!named && !this.hasAttribute("aria-hidden")) {
15707
+ this.setAttribute("aria-hidden", "true");
15708
+ }
15709
+ }
15710
+
15711
+ #parseValue() {
15712
+ const valueAttr = this.getAttribute("value");
15713
+ if (!valueAttr) {
15714
+ this.#gradient = {
15715
+ ...FigInterpolationSwatch.#DEFAULT_GRADIENT,
15716
+ stops: FigInterpolationSwatch.#DEFAULT_GRADIENT.stops.map((s) => ({
15717
+ ...s,
15718
+ })),
15719
+ };
15720
+ return;
15721
+ }
15722
+ try {
15723
+ const parsed = JSON.parse(valueAttr);
15724
+ const gradient = parsed?.type === "gradient" && parsed.gradient
15725
+ ? parsed.gradient
15726
+ : parsed?.gradient
15727
+ ? parsed.gradient
15728
+ : parsed;
15729
+ if (!gradient || typeof gradient !== "object") return;
15730
+ this.#gradient = this.#normalizeGradient({
15731
+ ...FigInterpolationSwatch.#DEFAULT_GRADIENT,
15732
+ ...gradient,
15733
+ });
15734
+ } catch {
15735
+ // Keep current/default gradient on invalid JSON.
15736
+ }
15737
+ }
15738
+
15739
+ #normalizeGradient(gradient) {
15740
+ const next = { ...(gradient ?? {}) };
15741
+ const interpolationSpace = String(
15742
+ next.interpolationSpace ?? "srgb",
15743
+ ).toLowerCase();
15744
+ const hueInterpolation = String(
15745
+ next.hueInterpolation ?? "shorter",
15746
+ ).toLowerCase();
15747
+ const stops = Array.isArray(next.stops)
15748
+ ? next.stops.map((stop) => ({
15749
+ color: String(stop?.color || "#D9D9D9").replace(
15750
+ /^(#(?:[0-9a-f]{6})).*/i,
15751
+ "$1",
15752
+ ),
15753
+ position: stop?.position ?? 0,
15754
+ opacity: stop?.opacity ?? 100,
15755
+ }))
15756
+ : FigInterpolationSwatch.#DEFAULT_GRADIENT.stops.map((s) => ({ ...s }));
15757
+ if (stops.length < 2) {
15758
+ return {
15759
+ ...FigInterpolationSwatch.#DEFAULT_GRADIENT,
15760
+ stops: FigInterpolationSwatch.#DEFAULT_GRADIENT.stops.map((s) => ({
15761
+ ...s,
15762
+ })),
15763
+ };
15764
+ }
15765
+ return {
15766
+ type: ["linear", "radial", "angular"].includes(next.type)
15767
+ ? next.type
15768
+ : "linear",
15769
+ angle: Number.isFinite(Number(next.angle)) ? Number(next.angle) : 135,
15770
+ interpolationSpace,
15771
+ hueInterpolation,
15772
+ stops,
15773
+ };
15774
+ }
15775
+
15776
+ #isPolar() {
15777
+ return FigInterpolationSwatch.#HUE_SPACES.has(
15778
+ this.#gradient.interpolationSpace || "srgb",
15779
+ );
15780
+ }
15781
+
15782
+ #hueForColor(color) {
15783
+ const { r, g, b } = figHexToRGB(color);
15784
+ if (this.#gradient.interpolationSpace === "hsl") {
15785
+ return figRgbToHsl(r, g, b).h;
15786
+ }
15787
+ const lab = figRGBToOklab(r, g, b);
15788
+ const hue = figOklabToOklch(lab.l, lab.a, lab.b).h;
15789
+ return ((hue % 360) + 360) % 360;
15790
+ }
15791
+
15792
+ #polarArcGeometry() {
15793
+ const stops = this.#sortedStops();
15794
+ const startHue = this.#hueForColor(stops[0]?.color || "#FF0000");
15795
+ const endHue = this.#hueForColor(
15796
+ stops[stops.length - 1]?.color || "#4F9EFF",
15797
+ );
15798
+ const startDeg = FigInterpolationSwatch.#START_DEG - startHue;
15799
+ const endDeg = FigInterpolationSwatch.#START_DEG - endHue;
15800
+ const clockwiseSweep = ((endDeg - startDeg) % 360 + 360) % 360;
15801
+ const counterclockwiseSweep =
15802
+ clockwiseSweep === 0 ? 0 : clockwiseSweep - 360;
15803
+ const method = this.#gradient.hueInterpolation || "shorter";
15804
+
15805
+ let sweepDeg;
15806
+ if (method === "increasing") {
15807
+ sweepDeg = counterclockwiseSweep;
15808
+ } else if (method === "decreasing") {
15809
+ sweepDeg = clockwiseSweep;
15810
+ } else if (method === "longer") {
15811
+ sweepDeg =
15812
+ clockwiseSweep < 180 ? counterclockwiseSweep : clockwiseSweep;
15813
+ } else {
15814
+ sweepDeg =
15815
+ clockwiseSweep <= 180 ? clockwiseSweep : counterclockwiseSweep;
15816
+ }
15817
+
15818
+ // A round cap extends beyond the path endpoint. Inset the centerline so
15819
+ // the visible cap edges, rather than their centers, land on the hues.
15820
+ const direction = Math.sign(sweepDeg);
15821
+ const capAngle =
15822
+ (Math.asin(FigInterpolationSwatch.#STROKE / 2 / FigInterpolationSwatch.#R) *
15823
+ 180) /
15824
+ Math.PI;
15825
+ const inset = Math.min(
15826
+ capAngle,
15827
+ Math.max(0, (Math.abs(sweepDeg) - 0.01) / 2),
15828
+ );
15829
+ return {
15830
+ startDeg: startDeg + direction * inset,
15831
+ sweepDeg: sweepDeg - direction * inset * 2,
15832
+ };
15833
+ }
15834
+
15835
+ #pointOnCircle(deg) {
15836
+ const rad = (deg * Math.PI) / 180;
15837
+ return {
15838
+ x: FigInterpolationSwatch.#CX + FigInterpolationSwatch.#R * Math.cos(rad),
15839
+ y: FigInterpolationSwatch.#CY + FigInterpolationSwatch.#R * Math.sin(rad),
15840
+ };
15841
+ }
15842
+
15843
+ #arcMaskPath(startDeg, sweepDeg) {
15844
+ const endDeg = startDeg + sweepDeg;
15845
+ const start = this.#pointOnCircle(startDeg);
15846
+ const end = this.#pointOnCircle(endDeg);
15847
+ const largeArc = Math.abs(sweepDeg) > 180 ? 1 : 0;
15848
+ const sweepFlag = sweepDeg >= 0 ? 1 : 0;
15849
+ return `M ${start.x} ${start.y} A ${FigInterpolationSwatch.#R} ${FigInterpolationSwatch.#R} 0 ${largeArc} ${sweepFlag} ${end.x} ${end.y}`;
15850
+ }
15851
+
15852
+ #lineMaskPath() {
15853
+ const start = this.#pointOnCircle(180);
15854
+ const end = this.#pointOnCircle(0);
15855
+ return `M ${start.x} ${start.y} L ${end.x} ${end.y}`;
15856
+ }
15857
+
15858
+ #maskImageForPath(d) {
15859
+ const stroke = FigInterpolationSwatch.#STROKE;
15860
+ const svg =
15861
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none">` +
15862
+ `<path d="${d}" fill="none" stroke="white" stroke-width="${stroke}" ` +
15863
+ `stroke-linecap="round" stroke-linejoin="round"/>` +
15864
+ `</svg>`;
15865
+ return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
15866
+ }
15867
+
15868
+ #sortedStops() {
15869
+ return [...this.#gradient.stops].sort(
15870
+ (a, b) => (a.position ?? 0) - (b.position ?? 0),
15871
+ );
15872
+ }
15873
+
15874
+ #cssInterpolationClause() {
15875
+ const space = this.#gradient.interpolationSpace || "srgb";
15876
+ if (space === "srgb") return "";
15877
+ if (FigInterpolationSwatch.#HUE_SPACES.has(space)) {
15878
+ return ` in ${space} ${this.#gradient.hueInterpolation || "shorter"} hue`;
15879
+ }
15880
+ return ` in ${space}`;
15881
+ }
15882
+
15883
+ #previewBackground() {
15884
+ const stops = this.#sortedStops();
15885
+ const clause = this.#cssInterpolationClause();
15886
+ if (this.#isPolar()) {
15887
+ // Fixed hue wheel. The mask maps gradient endpoint hues onto this wheel.
15888
+ const cssFrom = (FigInterpolationSwatch.#START_DEG + 90) % 360;
15889
+ const space = this.#gradient.interpolationSpace;
15890
+ const wheelColor = (hue) =>
15891
+ space === "oklch"
15892
+ ? `oklch(65% 0.25 ${hue})`
15893
+ : `hsl(${hue} 100% 50%)`;
15894
+ const wheelStops = [0, 300, 240, 180, 120, 60, 0]
15895
+ .map(wheelColor)
15896
+ .join(", ");
15897
+ return `conic-gradient(from ${cssFrom}deg in ${space} decreasing hue, ${wheelStops})`;
15898
+ }
15899
+ const stopList = stops
15900
+ .map((s) => `${s.color} ${s.position ?? 0}%`)
15901
+ .join(", ");
15902
+ return `linear-gradient(90deg${clause}, ${stopList})`;
15903
+ }
15904
+
15905
+ #render() {
15906
+ if (this.#rendered) return;
15907
+ const stroke = FigInterpolationSwatch.#STROKE;
15908
+ this.innerHTML = `
15909
+ <svg class="fig-interpolation-swatch-svg" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
15910
+ <circle class="fig-interpolation-swatch-rim" cx="10" cy="10" r="8" fill="none" stroke="currentColor" stroke-width="${stroke}"/>
15911
+ </svg>
15912
+ <div class="fig-interpolation-swatch-fill" aria-hidden="true"></div>
15913
+ `;
15914
+ this.#svgEl = this.querySelector(".fig-interpolation-swatch-svg");
15915
+ this.#fillEl = this.querySelector(".fig-interpolation-swatch-fill");
15916
+ this.#rendered = true;
15917
+ }
15918
+
15919
+ #updatePreview() {
15920
+ if (!this.#fillEl) return;
15921
+
15922
+ const polar = this.#isPolar();
15923
+ if (this.#svgEl) this.#svgEl.style.display = polar ? "" : "none";
15924
+
15925
+ const d = polar
15926
+ ? (() => {
15927
+ const { startDeg, sweepDeg } = this.#polarArcGeometry();
15928
+ return this.#arcMaskPath(startDeg, sweepDeg);
15929
+ })()
15930
+ : this.#lineMaskPath();
15931
+ const mask = this.#maskImageForPath(d);
15932
+ this.#fillEl.style.setProperty("-webkit-mask-image", mask);
15933
+ this.#fillEl.style.maskImage = mask;
15934
+ this.#fillEl.style.background = this.#previewBackground();
15935
+ }
15936
+ }
15937
+ figDefineElement("fig-interpolation-swatch", FigInterpolationSwatch);
15429
15938
 
15430
15939
  /** @type {Record<string, string | { medium: string, small: string }>} */
15431
15940
  const FIG_ICON_TOKENS = {
@@ -15512,19 +16021,19 @@ class FigIcon extends HTMLElement {
15512
16021
  }
15513
16022
  }
15514
16023
  }
15515
- customElements.define("fig-icon", FigIcon);
16024
+ figDefineElement("fig-icon", FigIcon);
15516
16025
 
15517
16026
  class FigContent extends HTMLElement {}
15518
- customElements.define("fig-content", FigContent);
16027
+ figDefineElement("fig-content", FigContent);
15519
16028
 
15520
16029
  class FigTabContent extends HTMLElement {}
15521
- customElements.define("fig-tab-content", FigTabContent);
16030
+ figDefineElement("fig-tab-content", FigTabContent);
15522
16031
 
15523
16032
  class FigButtonCombo extends HTMLElement {}
15524
- customElements.define("fig-button-combo", FigButtonCombo);
16033
+ figDefineElement("fig-button-combo", FigButtonCombo);
15525
16034
 
15526
16035
  class FigInputCombo extends HTMLElement {}
15527
- customElements.define("fig-input-combo", FigInputCombo);
16036
+ figDefineElement("fig-input-combo", FigInputCombo);
15528
16037
 
15529
16038
 
15530
16039
 
@@ -15902,7 +16411,7 @@ class FigColorTip extends HTMLElement {
15902
16411
  this.toggleAttribute("disabled", Boolean(value));
15903
16412
  }
15904
16413
  }
15905
- customElements.define("fig-color-tip", FigColorTip);
16414
+ figDefineElement("fig-color-tip", FigColorTip);
15906
16415
 
15907
16416
  /* Choice */
15908
16417
  /**
@@ -15951,7 +16460,7 @@ class FigChoice extends HTMLElement {
15951
16460
  }
15952
16461
  }
15953
16462
  }
15954
- customElements.define("fig-choice", FigChoice);
16463
+ figDefineElement("fig-choice", FigChoice);
15955
16464
 
15956
16465
  /* Chooser */
15957
16466
  /**
@@ -16556,7 +17065,7 @@ class FigChooser extends HTMLElement {
16556
17065
  this.#mutationObserver.observe(this, { childList: true, subtree: false });
16557
17066
  }
16558
17067
  }
16559
- customElements.define("fig-chooser", FigChooser);
17068
+ figDefineElement("fig-chooser", FigChooser);
16560
17069
 
16561
17070
  /* Handle */
16562
17071
  class FigHandle extends HTMLElement {
@@ -17478,7 +17987,7 @@ class FigHandle extends HTMLElement {
17478
17987
  return { x, y, px, py };
17479
17988
  }
17480
17989
  }
17481
- customElements.define("fig-handle", FigHandle);
17990
+ figDefineElement("fig-handle", FigHandle);
17482
17991
 
17483
17992
  // ─── Menu ────────────────────────────────────────────────────────────────────
17484
17993
 
@@ -17526,7 +18035,7 @@ class FigMenuItem extends HTMLElement {
17526
18035
  }
17527
18036
  }
17528
18037
  }
17529
- customElements.define("fig-menu-item", FigMenuItem);
18038
+ figDefineElement("fig-menu-item", FigMenuItem);
17530
18039
 
17531
18040
  /**
17532
18041
  * Visual divider between menu item groups.
@@ -17564,7 +18073,7 @@ class FigMenuSeparator extends HTMLElement {
17564
18073
  else this.removeAttribute("aria-label");
17565
18074
  }
17566
18075
  }
17567
- customElements.define("fig-menu-separator", FigMenuSeparator);
18076
+ figDefineElement("fig-menu-separator", FigMenuSeparator);
17568
18077
 
17569
18078
  class FigMenu extends HTMLElement {
17570
18079
  #popup = null;
@@ -18007,4 +18516,4 @@ class FigMenu extends HTMLElement {
18007
18516
  this.#trigger?.setAttribute("aria-expanded", "false");
18008
18517
  }
18009
18518
  }
18010
- customElements.define("fig-menu", FigMenu);
18519
+ figDefineElement("fig-menu", FigMenu);