@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/README.md +50 -1
- package/components.css +63 -2
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-editor.js +115 -98
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +16 -16
- package/dist/fig.css +1 -1
- package/dist/fig.js +60 -43
- package/dist/propskit.css +1 -0
- package/dist/propskit.js +1 -0
- package/fig-editor.css +157 -17
- package/fig-editor.js +464 -188
- package/fig-lab.css +51 -42
- package/fig-lab.js +145 -23
- package/fig.js +577 -68
- package/package.json +33 -4
- package/propskit-core.js +556 -0
- package/propskit.d.ts +98 -0
- package/propskit.js +15 -0
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
8086
|
-
return `in
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16024
|
+
figDefineElement("fig-icon", FigIcon);
|
|
15516
16025
|
|
|
15517
16026
|
class FigContent extends HTMLElement {}
|
|
15518
|
-
|
|
16027
|
+
figDefineElement("fig-content", FigContent);
|
|
15519
16028
|
|
|
15520
16029
|
class FigTabContent extends HTMLElement {}
|
|
15521
|
-
|
|
16030
|
+
figDefineElement("fig-tab-content", FigTabContent);
|
|
15522
16031
|
|
|
15523
16032
|
class FigButtonCombo extends HTMLElement {}
|
|
15524
|
-
|
|
16033
|
+
figDefineElement("fig-button-combo", FigButtonCombo);
|
|
15525
16034
|
|
|
15526
16035
|
class FigInputCombo extends HTMLElement {}
|
|
15527
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18519
|
+
figDefineElement("fig-menu", FigMenu);
|