@rogieking/figui3 6.20.1 → 6.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +240 -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 +591 -0
- package/fig-editor.css +157 -17
- package/fig-editor.js +457 -187
- package/fig-lab.css +51 -42
- package/fig-lab.js +125 -9
- package/fig.js +538 -18
- 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
|
@@ -228,7 +228,7 @@ function figUniqueId() {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
/** Zero-size portal for fixed overlays so they never affect body layout metrics. */
|
|
231
|
-
function figGetOverlayRoot() {
|
|
231
|
+
function figGetOverlayRoot(source) {
|
|
232
232
|
if (!document.body) return null;
|
|
233
233
|
const attr = "data-figui-overlay-root";
|
|
234
234
|
let root = document.body.querySelector(`:scope > [${attr}]`);
|
|
@@ -237,9 +237,40 @@ function figGetOverlayRoot() {
|
|
|
237
237
|
root.setAttribute(attr, "");
|
|
238
238
|
document.body.append(root);
|
|
239
239
|
}
|
|
240
|
+
figSyncOverlayThemeFromSource(root, source);
|
|
240
241
|
return root;
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
/** Copy PropsKit / FigUI theme onto the overlay portal so portaled popups stay themed. */
|
|
245
|
+
function figSyncOverlayThemeFromSource(overlayRoot, source) {
|
|
246
|
+
if (!overlayRoot) return;
|
|
247
|
+
const panel =
|
|
248
|
+
(source instanceof Element
|
|
249
|
+
? source.closest(".figui-root")
|
|
250
|
+
: null) || document.querySelector(".figui-root");
|
|
251
|
+
if (!panel) return;
|
|
252
|
+
|
|
253
|
+
const themeAttr = panel.getAttribute("theme");
|
|
254
|
+
const theme =
|
|
255
|
+
themeAttr === "light" || themeAttr === "dark" || themeAttr === "system"
|
|
256
|
+
? themeAttr
|
|
257
|
+
: panel.classList.contains("figma-dark")
|
|
258
|
+
? "dark"
|
|
259
|
+
: panel.classList.contains("figma-light")
|
|
260
|
+
? "light"
|
|
261
|
+
: "system";
|
|
262
|
+
|
|
263
|
+
overlayRoot.classList.toggle("figma-light", theme === "light");
|
|
264
|
+
overlayRoot.classList.toggle("figma-dark", theme === "dark");
|
|
265
|
+
if (theme === "system") {
|
|
266
|
+
overlayRoot.style.setProperty("color-scheme", "light dark");
|
|
267
|
+
} else {
|
|
268
|
+
overlayRoot.style.setProperty("color-scheme", theme);
|
|
269
|
+
}
|
|
270
|
+
if (themeAttr) overlayRoot.setAttribute("theme", theme);
|
|
271
|
+
else overlayRoot.removeAttribute("theme");
|
|
272
|
+
}
|
|
273
|
+
|
|
243
274
|
let _figZCounter = 10000;
|
|
244
275
|
function figGetHighestZIndex() {
|
|
245
276
|
return _figZCounter++;
|
|
@@ -834,6 +865,9 @@ customElements.define("fig-dropdown", FigDropdown);
|
|
|
834
865
|
* @attr {string} theme - Optional theme passed to the underlying popup (e.g. "brand").
|
|
835
866
|
* @attr {string} pointer - "false" to hide the beak.
|
|
836
867
|
* @attr {boolean} show - When set, force-show the tooltip (ignores hide).
|
|
868
|
+
*
|
|
869
|
+
* Call {@link FigTooltip.dismissHoverTooltips} when opening menus/dialogs so
|
|
870
|
+
* hover tips do not stay visible over interactive overlays.
|
|
837
871
|
*/
|
|
838
872
|
class FigTooltip extends HTMLElement {
|
|
839
873
|
static #lastShownAt = 0;
|
|
@@ -1042,13 +1076,13 @@ class FigTooltip extends HTMLElement {
|
|
|
1042
1076
|
// - Without popover support, fall back to today's behavior: nearest open
|
|
1043
1077
|
// <dialog> ancestor if present, else document.body.
|
|
1044
1078
|
if (supportsPopover) {
|
|
1045
|
-
(figGetOverlayRoot() ?? document.body).append(this.popup);
|
|
1079
|
+
(figGetOverlayRoot(this) ?? document.body).append(this.popup);
|
|
1046
1080
|
} else {
|
|
1047
1081
|
const parentDialog = this.closest("dialog");
|
|
1048
1082
|
if (parentDialog && parentDialog.open) {
|
|
1049
1083
|
parentDialog.append(this.popup);
|
|
1050
1084
|
} else {
|
|
1051
|
-
(figGetOverlayRoot() ?? document.body).append(this.popup);
|
|
1085
|
+
(figGetOverlayRoot(this) ?? document.body).append(this.popup);
|
|
1052
1086
|
}
|
|
1053
1087
|
}
|
|
1054
1088
|
|
|
@@ -1331,7 +1365,7 @@ class FigTooltip extends HTMLElement {
|
|
|
1331
1365
|
FigTooltip.#documentExitListenersReady = true;
|
|
1332
1366
|
|
|
1333
1367
|
const handlePointerLeftDocument = () => {
|
|
1334
|
-
FigTooltip
|
|
1368
|
+
FigTooltip.dismissHoverTooltips();
|
|
1335
1369
|
};
|
|
1336
1370
|
const handleViewportChange = () => {
|
|
1337
1371
|
FigTooltip.#dismissTooltipsOnViewportChange();
|
|
@@ -1372,13 +1406,20 @@ class FigTooltip extends HTMLElement {
|
|
|
1372
1406
|
});
|
|
1373
1407
|
}
|
|
1374
1408
|
|
|
1375
|
-
|
|
1409
|
+
/**
|
|
1410
|
+
* Dismiss open/pending hover tooltips and programmatic {@link FigTooltip.show} tips.
|
|
1411
|
+
* Skips `show`-persisted and `action="click"` tooltips. Call when opening menus/dialogs.
|
|
1412
|
+
*/
|
|
1413
|
+
static dismissHoverTooltips() {
|
|
1376
1414
|
for (const node of document.querySelectorAll("fig-tooltip")) {
|
|
1377
1415
|
if (!(node instanceof FigTooltip)) continue;
|
|
1378
1416
|
if (node.action !== "hover") continue;
|
|
1379
1417
|
if (node.#showPersisted) continue;
|
|
1380
1418
|
if (node.isOpen || node.timeout) node.hidePopup();
|
|
1381
1419
|
}
|
|
1420
|
+
for (const anchor of Array.from(FigTooltip.#programmaticAnchors)) {
|
|
1421
|
+
FigTooltip.hide(anchor);
|
|
1422
|
+
}
|
|
1382
1423
|
}
|
|
1383
1424
|
|
|
1384
1425
|
static #programmatic = new WeakMap();
|
|
@@ -1423,13 +1464,13 @@ class FigTooltip extends HTMLElement {
|
|
|
1423
1464
|
popup.append(content);
|
|
1424
1465
|
|
|
1425
1466
|
if (supportsPopover) {
|
|
1426
|
-
(figGetOverlayRoot() ?? document.body).append(popup);
|
|
1467
|
+
(figGetOverlayRoot(anchor) ?? document.body).append(popup);
|
|
1427
1468
|
} else {
|
|
1428
1469
|
const parentDialog = anchor.closest?.("dialog");
|
|
1429
1470
|
if (parentDialog && parentDialog.open) {
|
|
1430
1471
|
parentDialog.append(popup);
|
|
1431
1472
|
} else {
|
|
1432
|
-
(figGetOverlayRoot() ?? document.body).append(popup);
|
|
1473
|
+
(figGetOverlayRoot(anchor) ?? document.body).append(popup);
|
|
1433
1474
|
}
|
|
1434
1475
|
}
|
|
1435
1476
|
|
|
@@ -1660,6 +1701,7 @@ class FigDialog extends HTMLDialogElement {
|
|
|
1660
1701
|
// other FigUI overlays (same helper as fig-popup).
|
|
1661
1702
|
this._assignStackingOrder();
|
|
1662
1703
|
this.addEventListener("close", this._boundRestoreFocus, { once: true });
|
|
1704
|
+
FigTooltip.dismissHoverTooltips();
|
|
1663
1705
|
return super.show();
|
|
1664
1706
|
}
|
|
1665
1707
|
|
|
@@ -1668,6 +1710,7 @@ class FigDialog extends HTMLDialogElement {
|
|
|
1668
1710
|
// Top layer owns stacking; clear any prior non-modal inline z-index.
|
|
1669
1711
|
this._clearStackingOrder();
|
|
1670
1712
|
this.addEventListener("close", this._boundRestoreFocus, { once: true });
|
|
1713
|
+
FigTooltip.dismissHoverTooltips();
|
|
1671
1714
|
return super.showModal();
|
|
1672
1715
|
}
|
|
1673
1716
|
|
|
@@ -2651,6 +2694,11 @@ class FigPopup extends HTMLDialogElement {
|
|
|
2651
2694
|
|
|
2652
2695
|
const anchor = this.resolveAnchor();
|
|
2653
2696
|
if (anchor?.classList) anchor.classList.add("has-popup-open");
|
|
2697
|
+
|
|
2698
|
+
// Tooltips lose to interactive overlays (menus, selects, popovers).
|
|
2699
|
+
if (this.getAttribute("variant") !== "tooltip") {
|
|
2700
|
+
FigTooltip.dismissHoverTooltips();
|
|
2701
|
+
}
|
|
2654
2702
|
}
|
|
2655
2703
|
|
|
2656
2704
|
hidePopup() {
|
|
@@ -8017,6 +8065,7 @@ const GRADIENT_INTERPOLATION_SPACES = [
|
|
|
8017
8065
|
"display-p3",
|
|
8018
8066
|
"oklab",
|
|
8019
8067
|
"oklch",
|
|
8068
|
+
"hsl",
|
|
8020
8069
|
];
|
|
8021
8070
|
const GRADIENT_HUE_INTERPOLATIONS = [
|
|
8022
8071
|
"shorter",
|
|
@@ -8024,6 +8073,7 @@ const GRADIENT_HUE_INTERPOLATIONS = [
|
|
|
8024
8073
|
"increasing",
|
|
8025
8074
|
"decreasing",
|
|
8026
8075
|
];
|
|
8076
|
+
const GRADIENT_HUE_SPACES = new Set(["oklch", "hsl"]);
|
|
8027
8077
|
|
|
8028
8078
|
const GRADIENT_PICKER_SPACES = ["srgb", "srgb-linear", "oklab", "oklch"];
|
|
8029
8079
|
|
|
@@ -8052,7 +8102,7 @@ function gradientToValueShape(gradient) {
|
|
|
8052
8102
|
...normalized,
|
|
8053
8103
|
interpolationSpace: normalized.interpolationSpace,
|
|
8054
8104
|
};
|
|
8055
|
-
if (normalized.interpolationSpace
|
|
8105
|
+
if (GRADIENT_HUE_SPACES.has(normalized.interpolationSpace)) {
|
|
8056
8106
|
output.hueInterpolation = normalized.hueInterpolation;
|
|
8057
8107
|
} else {
|
|
8058
8108
|
delete output.hueInterpolation;
|
|
@@ -8065,8 +8115,8 @@ function gradientInterpolationClause(gradient) {
|
|
|
8065
8115
|
if (normalized.interpolationSpace === "srgb") {
|
|
8066
8116
|
return "";
|
|
8067
8117
|
}
|
|
8068
|
-
if (normalized.interpolationSpace
|
|
8069
|
-
return `in
|
|
8118
|
+
if (GRADIENT_HUE_SPACES.has(normalized.interpolationSpace)) {
|
|
8119
|
+
return `in ${normalized.interpolationSpace} ${normalized.hueInterpolation} hue`;
|
|
8070
8120
|
}
|
|
8071
8121
|
return `in ${normalized.interpolationSpace}`;
|
|
8072
8122
|
}
|
|
@@ -8215,6 +8265,25 @@ function figSampleGradientAt(
|
|
|
8215
8265
|
r = rgb.r;
|
|
8216
8266
|
g = rgb.g;
|
|
8217
8267
|
b = rgb.b;
|
|
8268
|
+
} else if (space === "hsl") {
|
|
8269
|
+
const hsl1 = figRgbToHsl(c1.r, c1.g, c1.b);
|
|
8270
|
+
const hsl2 = figRgbToHsl(c2.r, c2.g, c2.b);
|
|
8271
|
+
const H = figInterpolateHue(
|
|
8272
|
+
hsl1.h,
|
|
8273
|
+
hsl2.h,
|
|
8274
|
+
t,
|
|
8275
|
+
hueInterpolation || "shorter",
|
|
8276
|
+
);
|
|
8277
|
+
const S = hsl1.s + (hsl2.s - hsl1.s) * t;
|
|
8278
|
+
const L = hsl1.l + (hsl2.l - hsl1.l) * t;
|
|
8279
|
+
const rgb = hslToSRGB(H, S, L);
|
|
8280
|
+
r = rgb[0];
|
|
8281
|
+
g = rgb[1];
|
|
8282
|
+
b = rgb[2];
|
|
8283
|
+
} else if (space === "srgb") {
|
|
8284
|
+
r = Math.round(c1.r + (c2.r - c1.r) * t);
|
|
8285
|
+
g = Math.round(c1.g + (c2.g - c1.g) * t);
|
|
8286
|
+
b = Math.round(c1.b + (c2.b - c1.b) * t);
|
|
8218
8287
|
} else {
|
|
8219
8288
|
const lab1 = figRGBToOklab(c1.r, c1.g, c1.b);
|
|
8220
8289
|
const lab2 = figRGBToOklab(c2.r, c2.g, c2.b);
|
|
@@ -8230,6 +8299,33 @@ function figSampleGradientAt(
|
|
|
8230
8299
|
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`.toUpperCase();
|
|
8231
8300
|
}
|
|
8232
8301
|
|
|
8302
|
+
function figRgbToHsl(r, g, b) {
|
|
8303
|
+
const R = r / 255;
|
|
8304
|
+
const G = g / 255;
|
|
8305
|
+
const B = b / 255;
|
|
8306
|
+
const max = Math.max(R, G, B);
|
|
8307
|
+
const min = Math.min(R, G, B);
|
|
8308
|
+
const l = (max + min) / 2;
|
|
8309
|
+
if (max === min) {
|
|
8310
|
+
return { h: 0, s: 0, l: l * 100 };
|
|
8311
|
+
}
|
|
8312
|
+
const d = max - min;
|
|
8313
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
8314
|
+
let h;
|
|
8315
|
+
switch (max) {
|
|
8316
|
+
case R:
|
|
8317
|
+
h = ((G - B) / d + (G < B ? 6 : 0)) / 6;
|
|
8318
|
+
break;
|
|
8319
|
+
case G:
|
|
8320
|
+
h = ((B - R) / d + 2) / 6;
|
|
8321
|
+
break;
|
|
8322
|
+
default:
|
|
8323
|
+
h = ((R - G) / d + 4) / 6;
|
|
8324
|
+
break;
|
|
8325
|
+
}
|
|
8326
|
+
return { h: h * 360, s: s * 100, l: l * 100 };
|
|
8327
|
+
}
|
|
8328
|
+
|
|
8233
8329
|
function hslToP3(h, s, l) {
|
|
8234
8330
|
const sRGB = hslToSRGB(h, s, l);
|
|
8235
8331
|
return sRGB.map((c) => +(c / 255).toFixed(4));
|
|
@@ -8294,6 +8390,7 @@ class FigInputFill extends HTMLElement {
|
|
|
8294
8390
|
angle: 180,
|
|
8295
8391
|
interpolationSpace: "srgb",
|
|
8296
8392
|
hueInterpolation: "shorter",
|
|
8393
|
+
opacity: 1,
|
|
8297
8394
|
stops: [
|
|
8298
8395
|
{ position: 0, color: "#D9D9D9", opacity: 100 },
|
|
8299
8396
|
{ position: 100, color: "#737373", opacity: 100 },
|
|
@@ -8354,6 +8451,15 @@ class FigInputFill extends HTMLElement {
|
|
|
8354
8451
|
...this.#gradient,
|
|
8355
8452
|
...parsed.gradient,
|
|
8356
8453
|
});
|
|
8454
|
+
this.#gradient.opacity = this.#normalizeFillOpacity(
|
|
8455
|
+
parsed.gradient.opacity ?? parsed.opacity ?? parsed.alpha,
|
|
8456
|
+
this.#gradient.opacity ?? 1,
|
|
8457
|
+
);
|
|
8458
|
+
} else if (parsed.opacity !== undefined || parsed.alpha !== undefined) {
|
|
8459
|
+
this.#gradient.opacity = this.#normalizeFillOpacity(
|
|
8460
|
+
parsed.opacity ?? parsed.alpha,
|
|
8461
|
+
this.#gradient.opacity ?? 1,
|
|
8462
|
+
);
|
|
8357
8463
|
}
|
|
8358
8464
|
break;
|
|
8359
8465
|
case "image":
|
|
@@ -8429,10 +8535,20 @@ class FigInputFill extends HTMLElement {
|
|
|
8429
8535
|
}
|
|
8430
8536
|
}
|
|
8431
8537
|
|
|
8538
|
+
#normalizeFillOpacity(value, fallback = 1) {
|
|
8539
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
8540
|
+
const n = Number(value);
|
|
8541
|
+
if (!Number.isFinite(n)) return fallback;
|
|
8542
|
+
if (n > 1) return Math.max(0, Math.min(1, n / 100));
|
|
8543
|
+
return Math.max(0, Math.min(1, n));
|
|
8544
|
+
}
|
|
8545
|
+
|
|
8432
8546
|
#fillPickerSwatchAlpha() {
|
|
8433
8547
|
switch (this.#fillType) {
|
|
8434
8548
|
case "solid":
|
|
8435
8549
|
return this.#solid.alpha;
|
|
8550
|
+
case "gradient":
|
|
8551
|
+
return this.#gradient.opacity ?? 1;
|
|
8436
8552
|
case "image":
|
|
8437
8553
|
return this.#image.opacity ?? 1;
|
|
8438
8554
|
case "video":
|
|
@@ -8490,9 +8606,9 @@ class FigInputFill extends HTMLElement {
|
|
|
8490
8606
|
const opacityHtml = (value) =>
|
|
8491
8607
|
showAlpha
|
|
8492
8608
|
? `<fig-tooltip text="Opacity">
|
|
8493
|
-
<fig-input-number
|
|
8609
|
+
<fig-input-number
|
|
8494
8610
|
class="fig-input-fill-opacity"
|
|
8495
|
-
placeholder="##"
|
|
8611
|
+
placeholder="##"
|
|
8496
8612
|
min="0"
|
|
8497
8613
|
max="100"
|
|
8498
8614
|
value="${value}"
|
|
@@ -8522,7 +8638,8 @@ class FigInputFill extends HTMLElement {
|
|
|
8522
8638
|
this.#gradient.type.charAt(0).toUpperCase() +
|
|
8523
8639
|
this.#gradient.type.slice(1);
|
|
8524
8640
|
controlsHtml = `
|
|
8525
|
-
<label class="fig-input-fill-label">${figEscapeAttribute(gradientLabel)}</label
|
|
8641
|
+
<label class="fig-input-fill-label">${figEscapeAttribute(gradientLabel)}</label>
|
|
8642
|
+
${opacityHtml(Math.round((this.#gradient.opacity ?? 1) * 100))}`;
|
|
8526
8643
|
break;
|
|
8527
8644
|
}
|
|
8528
8645
|
|
|
@@ -8662,6 +8779,7 @@ class FigInputFill extends HTMLElement {
|
|
|
8662
8779
|
this.#solid.alpha = alpha;
|
|
8663
8780
|
break;
|
|
8664
8781
|
case "gradient":
|
|
8782
|
+
this.#gradient.opacity = alpha;
|
|
8665
8783
|
break;
|
|
8666
8784
|
case "image":
|
|
8667
8785
|
this.#image.opacity = alpha;
|
|
@@ -8710,6 +8828,12 @@ class FigInputFill extends HTMLElement {
|
|
|
8710
8828
|
this.#gradient.type.slice(1);
|
|
8711
8829
|
label.textContent = newLabel;
|
|
8712
8830
|
}
|
|
8831
|
+
if (this.#opacityInput) {
|
|
8832
|
+
this.#opacityInput.setAttribute(
|
|
8833
|
+
"value",
|
|
8834
|
+
Math.round((this.#gradient.opacity ?? 1) * 100),
|
|
8835
|
+
);
|
|
8836
|
+
}
|
|
8713
8837
|
break;
|
|
8714
8838
|
}
|
|
8715
8839
|
case "image":
|
|
@@ -8786,7 +8910,18 @@ class FigInputFill extends HTMLElement {
|
|
|
8786
8910
|
this.#gradient.type.charAt(0).toUpperCase() +
|
|
8787
8911
|
this.#gradient.type.slice(1);
|
|
8788
8912
|
controlsHtml = `
|
|
8789
|
-
<label class="fig-input-fill-label">${figEscapeAttribute(gradientLabel)}</label
|
|
8913
|
+
<label class="fig-input-fill-label">${figEscapeAttribute(gradientLabel)}</label>
|
|
8914
|
+
${showAlpha ? `<fig-tooltip text="Opacity">
|
|
8915
|
+
<fig-input-number
|
|
8916
|
+
class="fig-input-fill-opacity"
|
|
8917
|
+
placeholder="##"
|
|
8918
|
+
min="0"
|
|
8919
|
+
max="100"
|
|
8920
|
+
value="${Math.round((this.#gradient.opacity ?? 1) * 100)}"
|
|
8921
|
+
units="%"
|
|
8922
|
+
${disabled ? "disabled" : ""}>
|
|
8923
|
+
</fig-input-number>
|
|
8924
|
+
</fig-tooltip>` : ""}`;
|
|
8790
8925
|
break;
|
|
8791
8926
|
}
|
|
8792
8927
|
case "image":
|
|
@@ -8882,6 +9017,7 @@ class FigInputFill extends HTMLElement {
|
|
|
8882
9017
|
this.#solid.alpha = alpha;
|
|
8883
9018
|
break;
|
|
8884
9019
|
case "gradient":
|
|
9020
|
+
this.#gradient.opacity = alpha;
|
|
8885
9021
|
break;
|
|
8886
9022
|
case "image":
|
|
8887
9023
|
this.#image.opacity = alpha;
|
|
@@ -9726,6 +9862,11 @@ class FigInputGradient extends HTMLElement {
|
|
|
9726
9862
|
.join(", ");
|
|
9727
9863
|
const interp = gradientInterpolationClause(gradient);
|
|
9728
9864
|
const interpolation = interp ? ` ${interp}` : "";
|
|
9865
|
+
// Inline stop editor always previews L→R so handles match the track axis,
|
|
9866
|
+
// regardless of linear angle / radial / angular type.
|
|
9867
|
+
if (this.#editMode === "true") {
|
|
9868
|
+
return `linear-gradient(to right${interpolation}, ${stops})`;
|
|
9869
|
+
}
|
|
9729
9870
|
if (gradient.type === "radial") {
|
|
9730
9871
|
return `radial-gradient(circle at ${gradient.centerX}% ${gradient.centerY}%${interpolation}, ${stops})`;
|
|
9731
9872
|
}
|
|
@@ -11196,6 +11337,9 @@ customElements.define("fig-swatch", FigSwatch);
|
|
|
11196
11337
|
* @attr {string} aria-label - Accessible label forwarded to generated video
|
|
11197
11338
|
* @attr {string} aria-labelledby - Accessible label reference forwarded to generated video
|
|
11198
11339
|
*
|
|
11340
|
+
* When `controls` are shown and no video `src` is loaded, fig-media sets
|
|
11341
|
+
* `disabled` on the associated `fig-media-controls` until media is available.
|
|
11342
|
+
*
|
|
11199
11343
|
* Sizing model:
|
|
11200
11344
|
* - Default: host shrinkwraps to its inner <img>/<video> intrinsic size.
|
|
11201
11345
|
* - `size` attribute applies a token-sized square.
|
|
@@ -11534,12 +11678,41 @@ class FigMedia extends HTMLElement {
|
|
|
11534
11678
|
this.#mediaEl.playsInline = true;
|
|
11535
11679
|
this.#syncMediaAccessibility();
|
|
11536
11680
|
this.#syncControlsVisibility();
|
|
11681
|
+
this.#syncControlsDisabled();
|
|
11537
11682
|
}
|
|
11538
11683
|
|
|
11539
11684
|
get mediaEl() {
|
|
11540
11685
|
return this.#mediaEl;
|
|
11541
11686
|
}
|
|
11542
11687
|
|
|
11688
|
+
#hasPlayableVideo() {
|
|
11689
|
+
if (this.mediaKind !== "video") return false;
|
|
11690
|
+
return Boolean(this.#currentMediaSrc());
|
|
11691
|
+
}
|
|
11692
|
+
|
|
11693
|
+
#syncControlsDisabled() {
|
|
11694
|
+
const controls =
|
|
11695
|
+
this.#controlsEl ||
|
|
11696
|
+
this.querySelector(":scope > fig-media-controls");
|
|
11697
|
+
if (!controls) return;
|
|
11698
|
+
if (this.mediaKind !== "video") return;
|
|
11699
|
+
|
|
11700
|
+
if (this.#hasPlayableVideo()) {
|
|
11701
|
+
// Only clear disabled when we set it for the empty state.
|
|
11702
|
+
if (controls.hasAttribute("data-fig-empty-disabled")) {
|
|
11703
|
+
controls.removeAttribute("disabled");
|
|
11704
|
+
controls.removeAttribute("data-fig-empty-disabled");
|
|
11705
|
+
}
|
|
11706
|
+
return;
|
|
11707
|
+
}
|
|
11708
|
+
|
|
11709
|
+
controls.setAttribute("disabled", "");
|
|
11710
|
+
controls.setAttribute("data-fig-empty-disabled", "");
|
|
11711
|
+
controls.playing = false;
|
|
11712
|
+
controls.time = 0;
|
|
11713
|
+
controls.duration = 0;
|
|
11714
|
+
}
|
|
11715
|
+
|
|
11543
11716
|
#syncControlsVisibility() {
|
|
11544
11717
|
if (this.mediaKind !== "video") {
|
|
11545
11718
|
this.#removeControls();
|
|
@@ -11554,6 +11727,7 @@ class FigMedia extends HTMLElement {
|
|
|
11554
11727
|
this.#controlsEl = userControls;
|
|
11555
11728
|
}
|
|
11556
11729
|
this.#wireControlsToMedia();
|
|
11730
|
+
this.#syncControlsDisabled();
|
|
11557
11731
|
return;
|
|
11558
11732
|
}
|
|
11559
11733
|
if (this.#isEnabledAttr("controls", false)) {
|
|
@@ -11573,6 +11747,7 @@ class FigMedia extends HTMLElement {
|
|
|
11573
11747
|
if (existingControls) {
|
|
11574
11748
|
this.#controlsEl = existingControls;
|
|
11575
11749
|
this.#wireControlsToMedia();
|
|
11750
|
+
this.#syncControlsDisabled();
|
|
11576
11751
|
return;
|
|
11577
11752
|
}
|
|
11578
11753
|
const controls = document.createElement("fig-media-controls");
|
|
@@ -11580,6 +11755,7 @@ class FigMedia extends HTMLElement {
|
|
|
11580
11755
|
this.append(controls);
|
|
11581
11756
|
this.#controlsEl = controls;
|
|
11582
11757
|
this.#wireControlsToMedia();
|
|
11758
|
+
this.#syncControlsDisabled();
|
|
11583
11759
|
}
|
|
11584
11760
|
|
|
11585
11761
|
#wireControlsToMedia() {
|
|
@@ -11588,6 +11764,7 @@ class FigMedia extends HTMLElement {
|
|
|
11588
11764
|
this.#controlsWiredFor === this.#mediaEl &&
|
|
11589
11765
|
this.#controlsWiredControls === this.#controlsEl
|
|
11590
11766
|
) {
|
|
11767
|
+
this.#syncControlsDisabled();
|
|
11591
11768
|
return;
|
|
11592
11769
|
}
|
|
11593
11770
|
this.#unwireControls();
|
|
@@ -11599,6 +11776,8 @@ class FigMedia extends HTMLElement {
|
|
|
11599
11776
|
|
|
11600
11777
|
let pendingSeekTime = null;
|
|
11601
11778
|
const syncFromVideo = () => {
|
|
11779
|
+
this.#syncControlsDisabled();
|
|
11780
|
+
if (!this.#hasPlayableVideo()) return;
|
|
11602
11781
|
controls.playing = !video.paused && !video.ended;
|
|
11603
11782
|
if (Number.isFinite(video.duration)) controls.duration = video.duration;
|
|
11604
11783
|
if (pendingSeekTime !== null) {
|
|
@@ -11611,11 +11790,13 @@ class FigMedia extends HTMLElement {
|
|
|
11611
11790
|
controls.time = video.currentTime || 0;
|
|
11612
11791
|
};
|
|
11613
11792
|
const onPlay = () => {
|
|
11793
|
+
if (!this.#hasPlayableVideo()) return;
|
|
11614
11794
|
const p = video.play?.();
|
|
11615
11795
|
if (p && typeof p.catch === "function") p.catch(() => {});
|
|
11616
11796
|
};
|
|
11617
11797
|
const onPause = () => video.pause?.();
|
|
11618
11798
|
const onSeek = (e) => {
|
|
11799
|
+
if (!this.#hasPlayableVideo()) return;
|
|
11619
11800
|
const next = Number(e?.detail?.time);
|
|
11620
11801
|
if (!Number.isFinite(next)) return;
|
|
11621
11802
|
pendingSeekTime = next;
|
|
@@ -12189,13 +12370,14 @@ customElements.define("fig-card", FigCard);
|
|
|
12189
12370
|
* - `playing` (boolean presence) — current play/pause state
|
|
12190
12371
|
* - `duration` (number, seconds) — total track length
|
|
12191
12372
|
* - `time` (number, seconds) — current playhead position
|
|
12373
|
+
* - `disabled` (boolean) — disables play/seek interaction
|
|
12192
12374
|
*
|
|
12193
12375
|
* Events:
|
|
12194
12376
|
* - `play` — emitted when the user toggles playback on (detail: { playing: true })
|
|
12195
12377
|
* - `pause` — emitted when the user toggles playback off (detail: { playing: false })
|
|
12196
12378
|
* - `seek` — emitted when the user drags the scrubber (detail: { time })
|
|
12197
12379
|
*
|
|
12198
|
-
* Properties: `playing`, `duration`, `time` mirror the attributes.
|
|
12380
|
+
* Properties: `playing`, `duration`, `time`, `disabled` mirror the attributes.
|
|
12199
12381
|
*/
|
|
12200
12382
|
class FigMediaControls extends HTMLElement {
|
|
12201
12383
|
#playBtn = null;
|
|
@@ -12206,13 +12388,14 @@ class FigMediaControls extends HTMLElement {
|
|
|
12206
12388
|
#rendered = false;
|
|
12207
12389
|
|
|
12208
12390
|
static get observedAttributes() {
|
|
12209
|
-
return ["playing", "duration", "time"];
|
|
12391
|
+
return ["playing", "duration", "time", "disabled"];
|
|
12210
12392
|
}
|
|
12211
12393
|
|
|
12212
12394
|
connectedCallback() {
|
|
12213
12395
|
this.#render();
|
|
12214
12396
|
this.#syncPlayingUi();
|
|
12215
12397
|
this.#syncTimeUi();
|
|
12398
|
+
this.#syncDisabled();
|
|
12216
12399
|
}
|
|
12217
12400
|
|
|
12218
12401
|
get playing() {
|
|
@@ -12249,10 +12432,19 @@ class FigMediaControls extends HTMLElement {
|
|
|
12249
12432
|
this.setAttribute("time", String(n));
|
|
12250
12433
|
}
|
|
12251
12434
|
|
|
12435
|
+
get disabled() {
|
|
12436
|
+
return figBooleanAttribute(this, "disabled");
|
|
12437
|
+
}
|
|
12438
|
+
set disabled(value) {
|
|
12439
|
+
if (value) this.setAttribute("disabled", "");
|
|
12440
|
+
else this.removeAttribute("disabled");
|
|
12441
|
+
}
|
|
12442
|
+
|
|
12252
12443
|
attributeChangedCallback(name) {
|
|
12253
12444
|
if (!this.#rendered) return;
|
|
12254
12445
|
if (name === "playing") this.#syncPlayingUi();
|
|
12255
12446
|
if (name === "duration" || name === "time") this.#syncTimeUi();
|
|
12447
|
+
if (name === "disabled") this.#syncDisabled();
|
|
12256
12448
|
}
|
|
12257
12449
|
|
|
12258
12450
|
#render() {
|
|
@@ -12298,6 +12490,11 @@ class FigMediaControls extends HTMLElement {
|
|
|
12298
12490
|
timeEl.textContent = this.#formatTime(this.time);
|
|
12299
12491
|
|
|
12300
12492
|
const handleSeek = (e) => {
|
|
12493
|
+
if (this.disabled) {
|
|
12494
|
+
e.preventDefault?.();
|
|
12495
|
+
e.stopPropagation();
|
|
12496
|
+
return;
|
|
12497
|
+
}
|
|
12301
12498
|
const host = e.currentTarget;
|
|
12302
12499
|
const next = Number(host?.value);
|
|
12303
12500
|
if (!Number.isFinite(next)) return;
|
|
@@ -12323,6 +12520,20 @@ class FigMediaControls extends HTMLElement {
|
|
|
12323
12520
|
this.#playTooltip = tooltip;
|
|
12324
12521
|
this.#timeSlider = slider;
|
|
12325
12522
|
this.#timeEl = timeEl;
|
|
12523
|
+
this.#syncDisabled();
|
|
12524
|
+
}
|
|
12525
|
+
|
|
12526
|
+
#syncDisabled() {
|
|
12527
|
+
const disabled = this.disabled;
|
|
12528
|
+
this.setAttribute("aria-disabled", disabled ? "true" : "false");
|
|
12529
|
+
if (this.#playBtn) {
|
|
12530
|
+
if (disabled) this.#playBtn.setAttribute("disabled", "");
|
|
12531
|
+
else this.#playBtn.removeAttribute("disabled");
|
|
12532
|
+
}
|
|
12533
|
+
if (this.#timeSlider) {
|
|
12534
|
+
if (disabled) this.#timeSlider.setAttribute("disabled", "");
|
|
12535
|
+
else this.#timeSlider.removeAttribute("disabled");
|
|
12536
|
+
}
|
|
12326
12537
|
}
|
|
12327
12538
|
|
|
12328
12539
|
#formatTime(seconds) {
|
|
@@ -12368,6 +12579,7 @@ class FigMediaControls extends HTMLElement {
|
|
|
12368
12579
|
}
|
|
12369
12580
|
|
|
12370
12581
|
toggle() {
|
|
12582
|
+
if (this.disabled) return;
|
|
12371
12583
|
const next = !this.playing;
|
|
12372
12584
|
this.playing = next;
|
|
12373
12585
|
this.dispatchEvent(
|
|
@@ -12380,12 +12592,12 @@ class FigMediaControls extends HTMLElement {
|
|
|
12380
12592
|
}
|
|
12381
12593
|
|
|
12382
12594
|
play() {
|
|
12383
|
-
if (this.playing) return;
|
|
12595
|
+
if (this.disabled || this.playing) return;
|
|
12384
12596
|
this.toggle();
|
|
12385
12597
|
}
|
|
12386
12598
|
|
|
12387
12599
|
pause() {
|
|
12388
|
-
if (!this.playing) return;
|
|
12600
|
+
if (this.disabled || !this.playing) return;
|
|
12389
12601
|
this.toggle();
|
|
12390
12602
|
}
|
|
12391
12603
|
}
|
|
@@ -15410,6 +15622,314 @@ class FigPreview extends HTMLElement {
|
|
|
15410
15622
|
}
|
|
15411
15623
|
customElements.define("fig-preview", FigPreview);
|
|
15412
15624
|
|
|
15625
|
+
/**
|
|
15626
|
+
* Compact swatch previewing gradient color-space interpolation.
|
|
15627
|
+
* Polar: CSS conic-gradient masked (SVG data-URL, round linecaps) to an arc.
|
|
15628
|
+
* Non-polar: CSS linear-gradient masked to a horizontal round-capped stroke.
|
|
15629
|
+
* Accepts the same `value` shape as fig-input-gradient / fig-fill-picker.
|
|
15630
|
+
*
|
|
15631
|
+
* @element fig-interpolation-swatch
|
|
15632
|
+
* @attr {string} value - JSON `{ type: "gradient", gradient: { … } }` (or a bare gradient object)
|
|
15633
|
+
* @attr {string} size - `small` (default, 24px) or `large` (32px)
|
|
15634
|
+
*/
|
|
15635
|
+
class FigInterpolationSwatch extends HTMLElement {
|
|
15636
|
+
static #HUE_SPACES = new Set(["oklch", "hsl"]);
|
|
15637
|
+
static #CX = 10;
|
|
15638
|
+
static #CY = 10;
|
|
15639
|
+
static #R = 8;
|
|
15640
|
+
static #STROKE = 3;
|
|
15641
|
+
// Polar endpoints ≈ 10 o'clock → 2 o'clock (SVG deg: 0 = east, CW).
|
|
15642
|
+
// CSS conic `from` is 0 = north; convert with +90.
|
|
15643
|
+
static #START_DEG = 210;
|
|
15644
|
+
static #DEFAULT_GRADIENT = {
|
|
15645
|
+
type: "linear",
|
|
15646
|
+
angle: 135,
|
|
15647
|
+
interpolationSpace: "srgb",
|
|
15648
|
+
hueInterpolation: "shorter",
|
|
15649
|
+
stops: [
|
|
15650
|
+
{ color: "#FF0000", position: 0, opacity: 100 },
|
|
15651
|
+
{ color: "#4F9EFF", position: 100, opacity: 100 },
|
|
15652
|
+
],
|
|
15653
|
+
};
|
|
15654
|
+
|
|
15655
|
+
#rendered = false;
|
|
15656
|
+
#svgEl = null;
|
|
15657
|
+
#fillEl = null;
|
|
15658
|
+
#gradient = { ...FigInterpolationSwatch.#DEFAULT_GRADIENT };
|
|
15659
|
+
|
|
15660
|
+
static get observedAttributes() {
|
|
15661
|
+
return ["value"];
|
|
15662
|
+
}
|
|
15663
|
+
|
|
15664
|
+
get value() {
|
|
15665
|
+
return {
|
|
15666
|
+
type: "gradient",
|
|
15667
|
+
gradient: { ...this.#gradient },
|
|
15668
|
+
};
|
|
15669
|
+
}
|
|
15670
|
+
|
|
15671
|
+
set value(val) {
|
|
15672
|
+
if (val == null || val === "") {
|
|
15673
|
+
this.removeAttribute("value");
|
|
15674
|
+
return;
|
|
15675
|
+
}
|
|
15676
|
+
if (typeof val === "string") {
|
|
15677
|
+
this.setAttribute("value", val);
|
|
15678
|
+
return;
|
|
15679
|
+
}
|
|
15680
|
+
this.setAttribute("value", JSON.stringify(val));
|
|
15681
|
+
}
|
|
15682
|
+
|
|
15683
|
+
connectedCallback() {
|
|
15684
|
+
this.#ensureA11y();
|
|
15685
|
+
this.#parseValue();
|
|
15686
|
+
this.#render();
|
|
15687
|
+
this.#updatePreview();
|
|
15688
|
+
}
|
|
15689
|
+
|
|
15690
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
15691
|
+
if (oldValue === newValue) return;
|
|
15692
|
+
if (name !== "value") return;
|
|
15693
|
+
this.#parseValue();
|
|
15694
|
+
if (this.#rendered) this.#updatePreview();
|
|
15695
|
+
}
|
|
15696
|
+
|
|
15697
|
+
#ensureA11y() {
|
|
15698
|
+
const named =
|
|
15699
|
+
this.hasAttribute("aria-label") || this.hasAttribute("aria-labelledby");
|
|
15700
|
+
if (!named && !this.hasAttribute("aria-hidden")) {
|
|
15701
|
+
this.setAttribute("aria-hidden", "true");
|
|
15702
|
+
}
|
|
15703
|
+
}
|
|
15704
|
+
|
|
15705
|
+
#parseValue() {
|
|
15706
|
+
const valueAttr = this.getAttribute("value");
|
|
15707
|
+
if (!valueAttr) {
|
|
15708
|
+
this.#gradient = {
|
|
15709
|
+
...FigInterpolationSwatch.#DEFAULT_GRADIENT,
|
|
15710
|
+
stops: FigInterpolationSwatch.#DEFAULT_GRADIENT.stops.map((s) => ({
|
|
15711
|
+
...s,
|
|
15712
|
+
})),
|
|
15713
|
+
};
|
|
15714
|
+
return;
|
|
15715
|
+
}
|
|
15716
|
+
try {
|
|
15717
|
+
const parsed = JSON.parse(valueAttr);
|
|
15718
|
+
const gradient = parsed?.type === "gradient" && parsed.gradient
|
|
15719
|
+
? parsed.gradient
|
|
15720
|
+
: parsed?.gradient
|
|
15721
|
+
? parsed.gradient
|
|
15722
|
+
: parsed;
|
|
15723
|
+
if (!gradient || typeof gradient !== "object") return;
|
|
15724
|
+
this.#gradient = this.#normalizeGradient({
|
|
15725
|
+
...FigInterpolationSwatch.#DEFAULT_GRADIENT,
|
|
15726
|
+
...gradient,
|
|
15727
|
+
});
|
|
15728
|
+
} catch {
|
|
15729
|
+
// Keep current/default gradient on invalid JSON.
|
|
15730
|
+
}
|
|
15731
|
+
}
|
|
15732
|
+
|
|
15733
|
+
#normalizeGradient(gradient) {
|
|
15734
|
+
const next = { ...(gradient ?? {}) };
|
|
15735
|
+
const interpolationSpace = String(
|
|
15736
|
+
next.interpolationSpace ?? "srgb",
|
|
15737
|
+
).toLowerCase();
|
|
15738
|
+
const hueInterpolation = String(
|
|
15739
|
+
next.hueInterpolation ?? "shorter",
|
|
15740
|
+
).toLowerCase();
|
|
15741
|
+
const stops = Array.isArray(next.stops)
|
|
15742
|
+
? next.stops.map((stop) => ({
|
|
15743
|
+
color: String(stop?.color || "#D9D9D9").replace(
|
|
15744
|
+
/^(#(?:[0-9a-f]{6})).*/i,
|
|
15745
|
+
"$1",
|
|
15746
|
+
),
|
|
15747
|
+
position: stop?.position ?? 0,
|
|
15748
|
+
opacity: stop?.opacity ?? 100,
|
|
15749
|
+
}))
|
|
15750
|
+
: FigInterpolationSwatch.#DEFAULT_GRADIENT.stops.map((s) => ({ ...s }));
|
|
15751
|
+
if (stops.length < 2) {
|
|
15752
|
+
return {
|
|
15753
|
+
...FigInterpolationSwatch.#DEFAULT_GRADIENT,
|
|
15754
|
+
stops: FigInterpolationSwatch.#DEFAULT_GRADIENT.stops.map((s) => ({
|
|
15755
|
+
...s,
|
|
15756
|
+
})),
|
|
15757
|
+
};
|
|
15758
|
+
}
|
|
15759
|
+
return {
|
|
15760
|
+
type: ["linear", "radial", "angular"].includes(next.type)
|
|
15761
|
+
? next.type
|
|
15762
|
+
: "linear",
|
|
15763
|
+
angle: Number.isFinite(Number(next.angle)) ? Number(next.angle) : 135,
|
|
15764
|
+
interpolationSpace,
|
|
15765
|
+
hueInterpolation,
|
|
15766
|
+
stops,
|
|
15767
|
+
};
|
|
15768
|
+
}
|
|
15769
|
+
|
|
15770
|
+
#isPolar() {
|
|
15771
|
+
return FigInterpolationSwatch.#HUE_SPACES.has(
|
|
15772
|
+
this.#gradient.interpolationSpace || "srgb",
|
|
15773
|
+
);
|
|
15774
|
+
}
|
|
15775
|
+
|
|
15776
|
+
#hueForColor(color) {
|
|
15777
|
+
const { r, g, b } = figHexToRGB(color);
|
|
15778
|
+
if (this.#gradient.interpolationSpace === "hsl") {
|
|
15779
|
+
return figRgbToHsl(r, g, b).h;
|
|
15780
|
+
}
|
|
15781
|
+
const lab = figRGBToOklab(r, g, b);
|
|
15782
|
+
const hue = figOklabToOklch(lab.l, lab.a, lab.b).h;
|
|
15783
|
+
return ((hue % 360) + 360) % 360;
|
|
15784
|
+
}
|
|
15785
|
+
|
|
15786
|
+
#polarArcGeometry() {
|
|
15787
|
+
const stops = this.#sortedStops();
|
|
15788
|
+
const startHue = this.#hueForColor(stops[0]?.color || "#FF0000");
|
|
15789
|
+
const endHue = this.#hueForColor(
|
|
15790
|
+
stops[stops.length - 1]?.color || "#4F9EFF",
|
|
15791
|
+
);
|
|
15792
|
+
const startDeg = FigInterpolationSwatch.#START_DEG - startHue;
|
|
15793
|
+
const endDeg = FigInterpolationSwatch.#START_DEG - endHue;
|
|
15794
|
+
const clockwiseSweep = ((endDeg - startDeg) % 360 + 360) % 360;
|
|
15795
|
+
const counterclockwiseSweep =
|
|
15796
|
+
clockwiseSweep === 0 ? 0 : clockwiseSweep - 360;
|
|
15797
|
+
const method = this.#gradient.hueInterpolation || "shorter";
|
|
15798
|
+
|
|
15799
|
+
let sweepDeg;
|
|
15800
|
+
if (method === "increasing") {
|
|
15801
|
+
sweepDeg = counterclockwiseSweep;
|
|
15802
|
+
} else if (method === "decreasing") {
|
|
15803
|
+
sweepDeg = clockwiseSweep;
|
|
15804
|
+
} else if (method === "longer") {
|
|
15805
|
+
sweepDeg =
|
|
15806
|
+
clockwiseSweep < 180 ? counterclockwiseSweep : clockwiseSweep;
|
|
15807
|
+
} else {
|
|
15808
|
+
sweepDeg =
|
|
15809
|
+
clockwiseSweep <= 180 ? clockwiseSweep : counterclockwiseSweep;
|
|
15810
|
+
}
|
|
15811
|
+
|
|
15812
|
+
// A round cap extends beyond the path endpoint. Inset the centerline so
|
|
15813
|
+
// the visible cap edges, rather than their centers, land on the hues.
|
|
15814
|
+
const direction = Math.sign(sweepDeg);
|
|
15815
|
+
const capAngle =
|
|
15816
|
+
(Math.asin(FigInterpolationSwatch.#STROKE / 2 / FigInterpolationSwatch.#R) *
|
|
15817
|
+
180) /
|
|
15818
|
+
Math.PI;
|
|
15819
|
+
const inset = Math.min(
|
|
15820
|
+
capAngle,
|
|
15821
|
+
Math.max(0, (Math.abs(sweepDeg) - 0.01) / 2),
|
|
15822
|
+
);
|
|
15823
|
+
return {
|
|
15824
|
+
startDeg: startDeg + direction * inset,
|
|
15825
|
+
sweepDeg: sweepDeg - direction * inset * 2,
|
|
15826
|
+
};
|
|
15827
|
+
}
|
|
15828
|
+
|
|
15829
|
+
#pointOnCircle(deg) {
|
|
15830
|
+
const rad = (deg * Math.PI) / 180;
|
|
15831
|
+
return {
|
|
15832
|
+
x: FigInterpolationSwatch.#CX + FigInterpolationSwatch.#R * Math.cos(rad),
|
|
15833
|
+
y: FigInterpolationSwatch.#CY + FigInterpolationSwatch.#R * Math.sin(rad),
|
|
15834
|
+
};
|
|
15835
|
+
}
|
|
15836
|
+
|
|
15837
|
+
#arcMaskPath(startDeg, sweepDeg) {
|
|
15838
|
+
const endDeg = startDeg + sweepDeg;
|
|
15839
|
+
const start = this.#pointOnCircle(startDeg);
|
|
15840
|
+
const end = this.#pointOnCircle(endDeg);
|
|
15841
|
+
const largeArc = Math.abs(sweepDeg) > 180 ? 1 : 0;
|
|
15842
|
+
const sweepFlag = sweepDeg >= 0 ? 1 : 0;
|
|
15843
|
+
return `M ${start.x} ${start.y} A ${FigInterpolationSwatch.#R} ${FigInterpolationSwatch.#R} 0 ${largeArc} ${sweepFlag} ${end.x} ${end.y}`;
|
|
15844
|
+
}
|
|
15845
|
+
|
|
15846
|
+
#lineMaskPath() {
|
|
15847
|
+
const start = this.#pointOnCircle(180);
|
|
15848
|
+
const end = this.#pointOnCircle(0);
|
|
15849
|
+
return `M ${start.x} ${start.y} L ${end.x} ${end.y}`;
|
|
15850
|
+
}
|
|
15851
|
+
|
|
15852
|
+
#maskImageForPath(d) {
|
|
15853
|
+
const stroke = FigInterpolationSwatch.#STROKE;
|
|
15854
|
+
const svg =
|
|
15855
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none">` +
|
|
15856
|
+
`<path d="${d}" fill="none" stroke="white" stroke-width="${stroke}" ` +
|
|
15857
|
+
`stroke-linecap="round" stroke-linejoin="round"/>` +
|
|
15858
|
+
`</svg>`;
|
|
15859
|
+
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
|
|
15860
|
+
}
|
|
15861
|
+
|
|
15862
|
+
#sortedStops() {
|
|
15863
|
+
return [...this.#gradient.stops].sort(
|
|
15864
|
+
(a, b) => (a.position ?? 0) - (b.position ?? 0),
|
|
15865
|
+
);
|
|
15866
|
+
}
|
|
15867
|
+
|
|
15868
|
+
#cssInterpolationClause() {
|
|
15869
|
+
const space = this.#gradient.interpolationSpace || "srgb";
|
|
15870
|
+
if (space === "srgb") return "";
|
|
15871
|
+
if (FigInterpolationSwatch.#HUE_SPACES.has(space)) {
|
|
15872
|
+
return ` in ${space} ${this.#gradient.hueInterpolation || "shorter"} hue`;
|
|
15873
|
+
}
|
|
15874
|
+
return ` in ${space}`;
|
|
15875
|
+
}
|
|
15876
|
+
|
|
15877
|
+
#previewBackground() {
|
|
15878
|
+
const stops = this.#sortedStops();
|
|
15879
|
+
const clause = this.#cssInterpolationClause();
|
|
15880
|
+
if (this.#isPolar()) {
|
|
15881
|
+
// Fixed hue wheel. The mask maps gradient endpoint hues onto this wheel.
|
|
15882
|
+
const cssFrom = (FigInterpolationSwatch.#START_DEG + 90) % 360;
|
|
15883
|
+
const space = this.#gradient.interpolationSpace;
|
|
15884
|
+
const wheelColor = (hue) =>
|
|
15885
|
+
space === "oklch"
|
|
15886
|
+
? `oklch(65% 0.25 ${hue})`
|
|
15887
|
+
: `hsl(${hue} 100% 50%)`;
|
|
15888
|
+
const wheelStops = [0, 300, 240, 180, 120, 60, 0]
|
|
15889
|
+
.map(wheelColor)
|
|
15890
|
+
.join(", ");
|
|
15891
|
+
return `conic-gradient(from ${cssFrom}deg in ${space} decreasing hue, ${wheelStops})`;
|
|
15892
|
+
}
|
|
15893
|
+
const stopList = stops
|
|
15894
|
+
.map((s) => `${s.color} ${s.position ?? 0}%`)
|
|
15895
|
+
.join(", ");
|
|
15896
|
+
return `linear-gradient(90deg${clause}, ${stopList})`;
|
|
15897
|
+
}
|
|
15898
|
+
|
|
15899
|
+
#render() {
|
|
15900
|
+
if (this.#rendered) return;
|
|
15901
|
+
const stroke = FigInterpolationSwatch.#STROKE;
|
|
15902
|
+
this.innerHTML = `
|
|
15903
|
+
<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">
|
|
15904
|
+
<circle class="fig-interpolation-swatch-rim" cx="10" cy="10" r="8" fill="none" stroke="currentColor" stroke-width="${stroke}"/>
|
|
15905
|
+
</svg>
|
|
15906
|
+
<div class="fig-interpolation-swatch-fill" aria-hidden="true"></div>
|
|
15907
|
+
`;
|
|
15908
|
+
this.#svgEl = this.querySelector(".fig-interpolation-swatch-svg");
|
|
15909
|
+
this.#fillEl = this.querySelector(".fig-interpolation-swatch-fill");
|
|
15910
|
+
this.#rendered = true;
|
|
15911
|
+
}
|
|
15912
|
+
|
|
15913
|
+
#updatePreview() {
|
|
15914
|
+
if (!this.#fillEl) return;
|
|
15915
|
+
|
|
15916
|
+
const polar = this.#isPolar();
|
|
15917
|
+
if (this.#svgEl) this.#svgEl.style.display = polar ? "" : "none";
|
|
15918
|
+
|
|
15919
|
+
const d = polar
|
|
15920
|
+
? (() => {
|
|
15921
|
+
const { startDeg, sweepDeg } = this.#polarArcGeometry();
|
|
15922
|
+
return this.#arcMaskPath(startDeg, sweepDeg);
|
|
15923
|
+
})()
|
|
15924
|
+
: this.#lineMaskPath();
|
|
15925
|
+
const mask = this.#maskImageForPath(d);
|
|
15926
|
+
this.#fillEl.style.setProperty("-webkit-mask-image", mask);
|
|
15927
|
+
this.#fillEl.style.maskImage = mask;
|
|
15928
|
+
this.#fillEl.style.background = this.#previewBackground();
|
|
15929
|
+
}
|
|
15930
|
+
}
|
|
15931
|
+
customElements.define("fig-interpolation-swatch", FigInterpolationSwatch);
|
|
15932
|
+
|
|
15413
15933
|
/** @type {Record<string, string | { medium: string, small: string }>} */
|
|
15414
15934
|
const FIG_ICON_TOKENS = {
|
|
15415
15935
|
chevron: "--icon-16-chevron",
|