@rogieking/figui3 8.9.20 → 8.9.22
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/.cursor/skills/fig-lab/SKILL.md +3 -3
- package/components.css +19 -0
- package/dist/components.css +1 -1
- package/dist/fig-editor.js +3 -3
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +1 -1
- package/dist/fig.css +1 -1
- package/dist/fig.js +2 -2
- package/fig-editor.js +95 -12
- package/fig-lab.css +29 -8
- package/fig-lab.js +23 -0
- package/fig.js +20 -2
- package/package.json +1 -1
package/fig-editor.js
CHANGED
|
@@ -29,6 +29,18 @@ function figEditorCssUrl(url) {
|
|
|
29
29
|
return `url("${String(url).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}")`;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function figEditorLooksLikeVideoUrl(raw) {
|
|
33
|
+
const text = String(raw ?? "").trim();
|
|
34
|
+
if (!text || text.startsWith("{") || text.startsWith("#")) return false;
|
|
35
|
+
if (text.startsWith("data:video/")) return true;
|
|
36
|
+
try {
|
|
37
|
+
const path = new URL(text, "https://fig.local").pathname;
|
|
38
|
+
return /\.(mp4|webm|mov|m4v|ogv)$/i.test(path);
|
|
39
|
+
} catch {
|
|
40
|
+
return /\.(mp4|webm|mov|m4v|ogv)(?:[?#]|$)/i.test(text);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
function figEditorCreateIcon(name, options = {}) {
|
|
33
45
|
const icon = document.createElement("fig-icon");
|
|
34
46
|
if (name) icon.setAttribute("name", name);
|
|
@@ -1692,6 +1704,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1692
1704
|
#boundTriggerKeydown = null;
|
|
1693
1705
|
#rafIds = new Set();
|
|
1694
1706
|
#ownedBlobUrls = new Set();
|
|
1707
|
+
#videoPosterCapturing = null;
|
|
1695
1708
|
|
|
1696
1709
|
constructor() {
|
|
1697
1710
|
super();
|
|
@@ -1721,6 +1734,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
1721
1734
|
this.#setupTrigger();
|
|
1722
1735
|
this.#parseValue();
|
|
1723
1736
|
this.#updateSwatch();
|
|
1737
|
+
this.#ensureVideoPoster();
|
|
1724
1738
|
});
|
|
1725
1739
|
}
|
|
1726
1740
|
|
|
@@ -2117,8 +2131,12 @@ class FigFillPicker extends HTMLElement {
|
|
|
2117
2131
|
}
|
|
2118
2132
|
if (parsed.image) this.#image = { ...this.#image, ...parsed.image };
|
|
2119
2133
|
if (parsed.video) {
|
|
2134
|
+
const prevUrl = this.#video.url;
|
|
2120
2135
|
this.#video = { ...this.#video, ...parsed.video };
|
|
2121
2136
|
this.#video.missing = !this.#video.url;
|
|
2137
|
+
if (this.#video.url !== prevUrl && parsed.video.poster == null) {
|
|
2138
|
+
this.#revokeVideoPoster();
|
|
2139
|
+
}
|
|
2122
2140
|
}
|
|
2123
2141
|
this.#applyParsedWebcam(parsed);
|
|
2124
2142
|
|
|
@@ -2128,10 +2146,17 @@ class FigFillPicker extends HTMLElement {
|
|
|
2128
2146
|
this.#customData[parsed.type] = rest;
|
|
2129
2147
|
}
|
|
2130
2148
|
} catch (e) {
|
|
2131
|
-
// If not JSON, treat as hex color
|
|
2132
2149
|
if (valueAttr.startsWith("#")) {
|
|
2133
2150
|
this.#fillType = "solid";
|
|
2134
2151
|
this.#color = this.#hexToHSV(valueAttr);
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
if (figEditorLooksLikeVideoUrl(valueAttr)) {
|
|
2155
|
+
const prevUrl = this.#video.url;
|
|
2156
|
+
this.#fillType = "video";
|
|
2157
|
+
this.#video.url = valueAttr.trim();
|
|
2158
|
+
this.#video.missing = false;
|
|
2159
|
+
if (this.#video.url !== prevUrl) this.#revokeVideoPoster();
|
|
2135
2160
|
}
|
|
2136
2161
|
}
|
|
2137
2162
|
}
|
|
@@ -2228,6 +2253,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
2228
2253
|
if (this.#isDisabled()) return;
|
|
2229
2254
|
if (!this.#dialog) {
|
|
2230
2255
|
this.#createDialog();
|
|
2256
|
+
} else {
|
|
2257
|
+
this.#dialog.anchor = this.anchorElement || this.#trigger;
|
|
2231
2258
|
}
|
|
2232
2259
|
|
|
2233
2260
|
this.#valueAtOpen = JSON.stringify(this.value);
|
|
@@ -4427,7 +4454,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
4427
4454
|
"aspect-ratio": "1/1",
|
|
4428
4455
|
fit: "cover",
|
|
4429
4456
|
checkerboard: "true",
|
|
4430
|
-
autoplay: "true",
|
|
4431
4457
|
controls: true,
|
|
4432
4458
|
muted: "true",
|
|
4433
4459
|
loop: "true",
|
|
@@ -4466,8 +4492,67 @@ class FigFillPicker extends HTMLElement {
|
|
|
4466
4492
|
this.#captureVideoPoster(fallback, { emit });
|
|
4467
4493
|
}
|
|
4468
4494
|
|
|
4495
|
+
#ensureVideoPoster({ emit = true } = {}) {
|
|
4496
|
+
if (this.#fillType !== "video") return;
|
|
4497
|
+
const src = this.#video.url;
|
|
4498
|
+
if (!src || this.#video.poster) return;
|
|
4499
|
+
this.#captureVideoPoster(src, { emit });
|
|
4500
|
+
}
|
|
4501
|
+
|
|
4502
|
+
async #videoFrameBitmap(video) {
|
|
4503
|
+
if (typeof video.requestVideoFrameCallback === "function") {
|
|
4504
|
+
await Promise.race([
|
|
4505
|
+
new Promise((resolve) => video.requestVideoFrameCallback(() => resolve())),
|
|
4506
|
+
new Promise((resolve) => setTimeout(resolve, 80)),
|
|
4507
|
+
]);
|
|
4508
|
+
} else {
|
|
4509
|
+
try {
|
|
4510
|
+
await video.play();
|
|
4511
|
+
video.pause();
|
|
4512
|
+
} catch {
|
|
4513
|
+
/* use the current decoded frame */
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
if (typeof createImageBitmap === "function") {
|
|
4517
|
+
try {
|
|
4518
|
+
return await createImageBitmap(video);
|
|
4519
|
+
} catch {
|
|
4520
|
+
/* tainted or undecoded — fall back to drawImage(video) */
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
return null;
|
|
4524
|
+
}
|
|
4525
|
+
|
|
4526
|
+
async #blobFromVideoFrame(video) {
|
|
4527
|
+
const bitmap = await this.#videoFrameBitmap(video);
|
|
4528
|
+
const width = bitmap?.width || video.videoWidth;
|
|
4529
|
+
const height = bitmap?.height || video.videoHeight;
|
|
4530
|
+
if (!width || !height) {
|
|
4531
|
+
bitmap?.close?.();
|
|
4532
|
+
return null;
|
|
4533
|
+
}
|
|
4534
|
+
const canvas =
|
|
4535
|
+
typeof OffscreenCanvas === "function"
|
|
4536
|
+
? new OffscreenCanvas(width, height)
|
|
4537
|
+
: Object.assign(document.createElement("canvas"), { width, height });
|
|
4538
|
+
const ctx = canvas.getContext("2d");
|
|
4539
|
+
if (!ctx) {
|
|
4540
|
+
bitmap?.close?.();
|
|
4541
|
+
return null;
|
|
4542
|
+
}
|
|
4543
|
+
try {
|
|
4544
|
+
ctx.drawImage(bitmap || video, 0, 0, width, height);
|
|
4545
|
+
} finally {
|
|
4546
|
+
bitmap?.close?.();
|
|
4547
|
+
}
|
|
4548
|
+
return canvas.convertToBlob
|
|
4549
|
+
? canvas.convertToBlob({ type: "image/jpeg", quality: 0.85 })
|
|
4550
|
+
: new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", 0.85));
|
|
4551
|
+
}
|
|
4552
|
+
|
|
4469
4553
|
async #captureVideoPoster(src, { emit = true } = {}) {
|
|
4470
|
-
if (!src) return;
|
|
4554
|
+
if (!src || this.#videoPosterCapturing === src) return;
|
|
4555
|
+
this.#videoPosterCapturing = src;
|
|
4471
4556
|
const video = document.createElement("video");
|
|
4472
4557
|
video.muted = true;
|
|
4473
4558
|
video.playsInline = true;
|
|
@@ -4481,14 +4566,8 @@ class FigFillPicker extends HTMLElement {
|
|
|
4481
4566
|
video.src = src;
|
|
4482
4567
|
});
|
|
4483
4568
|
if (!video.videoWidth || !video.videoHeight) return;
|
|
4484
|
-
const
|
|
4485
|
-
|
|
4486
|
-
canvas.height = video.videoHeight;
|
|
4487
|
-
canvas.getContext("2d").drawImage(video, 0, 0);
|
|
4488
|
-
const blob = await new Promise((resolve) =>
|
|
4489
|
-
canvas.toBlob(resolve, "image/jpeg", 0.85),
|
|
4490
|
-
);
|
|
4491
|
-
if (!blob) return;
|
|
4569
|
+
const blob = await this.#blobFromVideoFrame(video);
|
|
4570
|
+
if (!blob || this.#video.url !== src) return;
|
|
4492
4571
|
this.#revokeVideoPoster();
|
|
4493
4572
|
this.#video.poster = URL.createObjectURL(blob);
|
|
4494
4573
|
this.#ownedBlobUrls.add(this.#video.poster);
|
|
@@ -4496,6 +4575,10 @@ class FigFillPicker extends HTMLElement {
|
|
|
4496
4575
|
if (emit) this.#emitInput();
|
|
4497
4576
|
} catch {
|
|
4498
4577
|
// Cross-origin or decode failures leave the swatch empty until a poster exists.
|
|
4578
|
+
} finally {
|
|
4579
|
+
video.removeAttribute("src");
|
|
4580
|
+
video.load();
|
|
4581
|
+
if (this.#videoPosterCapturing === src) this.#videoPosterCapturing = null;
|
|
4499
4582
|
}
|
|
4500
4583
|
}
|
|
4501
4584
|
|
|
@@ -4521,7 +4604,6 @@ class FigFillPicker extends HTMLElement {
|
|
|
4521
4604
|
this.#video.url = src;
|
|
4522
4605
|
this.#video.missing = false;
|
|
4523
4606
|
this.#updateVideoPreviewStyle(preview);
|
|
4524
|
-
preview.play?.();
|
|
4525
4607
|
this.#captureVideoPoster(src);
|
|
4526
4608
|
this.#updateSwatch();
|
|
4527
4609
|
this.#emitInput();
|
|
@@ -5184,6 +5266,7 @@ class FigFillPicker extends HTMLElement {
|
|
|
5184
5266
|
case "value":
|
|
5185
5267
|
this.#parseValue();
|
|
5186
5268
|
this.#updateSwatch();
|
|
5269
|
+
this.#ensureVideoPoster();
|
|
5187
5270
|
if (this.#dialog?.open && !this.#isDraggingColor) {
|
|
5188
5271
|
this.#refreshDialogUI();
|
|
5189
5272
|
this.#lastChangeValue = JSON.stringify(this.value);
|
package/fig-lab.css
CHANGED
|
@@ -658,20 +658,41 @@ propskit-gradient {
|
|
|
658
658
|
}
|
|
659
659
|
|
|
660
660
|
:is(propskit-color, propskit-fill, propskit-gradient) {
|
|
661
|
+
outline: none;
|
|
662
|
+
|
|
661
663
|
fig-field:not([direction="vertical"]) {
|
|
662
664
|
outline: none;
|
|
665
|
+
}
|
|
663
666
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
+
&:focus-within fig-field:not([direction="vertical"]),
|
|
668
|
+
&.has-popup-open fig-field:not([direction="vertical"]) {
|
|
669
|
+
outline: var(--figma-focus-outline);
|
|
670
|
+
outline-offset: var(--figma-focus-outline-offset);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/* Same split as fig-input-gradient: chrome outlines, inner focus does not. */
|
|
674
|
+
:is(fig-swatch, fig-chit, fig-input-gradient, fig-input-fill, fig-fill-picker) {
|
|
675
|
+
--selected: 0 !important;
|
|
676
|
+
|
|
677
|
+
&:hover,
|
|
678
|
+
&:focus,
|
|
679
|
+
&:active {
|
|
680
|
+
outline: 0 !important;
|
|
667
681
|
}
|
|
668
682
|
|
|
669
|
-
:
|
|
670
|
-
|
|
671
|
-
&.has-popup-open {
|
|
672
|
-
outline: none !important;
|
|
673
|
-
}
|
|
683
|
+
&:focus-within:not(:focus) {
|
|
684
|
+
outline: 0 !important;
|
|
674
685
|
}
|
|
686
|
+
|
|
687
|
+
&:focus-within:not(:focus) :is(input, fig-swatch, fig-chit) {
|
|
688
|
+
outline: 0 !important;
|
|
689
|
+
outline-offset: 0 !important;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
input[type="color"]:is(:focus, :focus-visible) {
|
|
694
|
+
outline: 0 !important;
|
|
695
|
+
outline-offset: 0 !important;
|
|
675
696
|
}
|
|
676
697
|
}
|
|
677
698
|
|
package/fig-lab.js
CHANGED
|
@@ -728,6 +728,25 @@ function figLabCssUrl(url) {
|
|
|
728
728
|
return `url("${String(url).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}")`;
|
|
729
729
|
}
|
|
730
730
|
|
|
731
|
+
function figLabLooksLikeVideoUrl(raw) {
|
|
732
|
+
const text = String(raw ?? "").trim();
|
|
733
|
+
if (!text || text.startsWith("{") || text.startsWith("#")) return false;
|
|
734
|
+
if (text.startsWith("data:video/")) return true;
|
|
735
|
+
try {
|
|
736
|
+
const path = new URL(text, "https://fig.local").pathname;
|
|
737
|
+
return /\.(mp4|webm|mov|m4v|ogv)$/i.test(path);
|
|
738
|
+
} catch {
|
|
739
|
+
return /\.(mp4|webm|mov|m4v|ogv)(?:[?#]|$)/i.test(text);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function figLabVideoFillFromUrl(url) {
|
|
744
|
+
return {
|
|
745
|
+
type: "video",
|
|
746
|
+
video: { url, scaleMode: "fill", scale: 50 },
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
731
750
|
function figLabParseFillValue(raw) {
|
|
732
751
|
if (raw && typeof raw === "object") return raw;
|
|
733
752
|
const text = String(raw ?? "").trim();
|
|
@@ -742,6 +761,7 @@ function figLabParseFillValue(raw) {
|
|
|
742
761
|
const { color, alpha } = figLabParseSolidColor(text);
|
|
743
762
|
return { type: "solid", color, alpha };
|
|
744
763
|
}
|
|
764
|
+
if (figLabLooksLikeVideoUrl(text)) return figLabVideoFillFromUrl(text);
|
|
745
765
|
return { type: "solid", color: "#D9D9D9", alpha: 1 };
|
|
746
766
|
}
|
|
747
767
|
|
|
@@ -922,6 +942,7 @@ class PropskitColor extends HTMLElement {
|
|
|
922
942
|
const label = customLabel || document.createElement("label");
|
|
923
943
|
const picker = document.createElement("fig-fill-picker");
|
|
924
944
|
const swatch = document.createElement("fig-swatch");
|
|
945
|
+
picker.anchorElement = this;
|
|
925
946
|
picker.append(swatch);
|
|
926
947
|
swatch.setAttribute("tabindex", "0");
|
|
927
948
|
for (const node of initialChildren) {
|
|
@@ -1278,6 +1299,7 @@ class PropskitFill extends HTMLElement {
|
|
|
1278
1299
|
const label = customLabel || document.createElement("label");
|
|
1279
1300
|
const picker = document.createElement("fig-fill-picker");
|
|
1280
1301
|
const swatch = document.createElement("fig-swatch");
|
|
1302
|
+
picker.anchorElement = this;
|
|
1281
1303
|
picker.append(swatch);
|
|
1282
1304
|
swatch.setAttribute("tabindex", "0");
|
|
1283
1305
|
for (const node of initialChildren) {
|
|
@@ -1646,6 +1668,7 @@ class PropskitGradient extends HTMLElement {
|
|
|
1646
1668
|
const field = document.createElement("fig-field");
|
|
1647
1669
|
const label = customLabel || document.createElement("label");
|
|
1648
1670
|
const input = document.createElement("fig-input-gradient");
|
|
1671
|
+
input.anchorElement = this;
|
|
1649
1672
|
|
|
1650
1673
|
field.append(label, input);
|
|
1651
1674
|
this.#field = field;
|
package/fig.js
CHANGED
|
@@ -8684,6 +8684,18 @@ function figCssUrl(url) {
|
|
|
8684
8684
|
return `url("${String(url).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}")`;
|
|
8685
8685
|
}
|
|
8686
8686
|
|
|
8687
|
+
function figLooksLikeVideoUrl(raw) {
|
|
8688
|
+
const text = String(raw ?? "").trim();
|
|
8689
|
+
if (!text || text.startsWith("{") || text.startsWith("#")) return false;
|
|
8690
|
+
if (text.startsWith("data:video/")) return true;
|
|
8691
|
+
try {
|
|
8692
|
+
const path = new URL(text, "https://fig.local").pathname;
|
|
8693
|
+
return /\.(mp4|webm|mov|m4v|ogv)$/i.test(path);
|
|
8694
|
+
} catch {
|
|
8695
|
+
return /\.(mp4|webm|mov|m4v|ogv)(?:[?#]|$)/i.test(text);
|
|
8696
|
+
}
|
|
8697
|
+
}
|
|
8698
|
+
|
|
8687
8699
|
function figHexToRGB(hex) {
|
|
8688
8700
|
const h = hex.replace(/^#/, "");
|
|
8689
8701
|
return {
|
|
@@ -9077,7 +9089,6 @@ class FigInputFill extends HTMLElement {
|
|
|
9077
9089
|
}
|
|
9078
9090
|
}
|
|
9079
9091
|
} catch (e) {
|
|
9080
|
-
// If not JSON, treat as hex color
|
|
9081
9092
|
if (valueAttr.startsWith("#")) {
|
|
9082
9093
|
this.#fillType = "solid";
|
|
9083
9094
|
this.#solid.color = valueAttr.slice(0, 7);
|
|
@@ -9085,6 +9096,12 @@ class FigInputFill extends HTMLElement {
|
|
|
9085
9096
|
const alphaHex = valueAttr.slice(7, 9);
|
|
9086
9097
|
this.#solid.alpha = parseInt(alphaHex, 16) / 255;
|
|
9087
9098
|
}
|
|
9099
|
+
return;
|
|
9100
|
+
}
|
|
9101
|
+
if (figLooksLikeVideoUrl(valueAttr)) {
|
|
9102
|
+
this.#fillType = "video";
|
|
9103
|
+
this.#video.url = valueAttr.trim();
|
|
9104
|
+
this.#video.missing = false;
|
|
9088
9105
|
}
|
|
9089
9106
|
}
|
|
9090
9107
|
}
|
|
@@ -10363,6 +10380,7 @@ class FigInputGradient extends HTMLElement {
|
|
|
10363
10380
|
#colorObserver = null;
|
|
10364
10381
|
#repositionRAF = null;
|
|
10365
10382
|
#pickerDefinitionPromise = null;
|
|
10383
|
+
anchorElement = null;
|
|
10366
10384
|
#gradient = {
|
|
10367
10385
|
type: "linear",
|
|
10368
10386
|
angle: 90,
|
|
@@ -10733,7 +10751,7 @@ class FigInputGradient extends HTMLElement {
|
|
|
10733
10751
|
#setupPickerEvents() {
|
|
10734
10752
|
const picker = this.querySelector("fig-fill-picker");
|
|
10735
10753
|
if (!picker) return;
|
|
10736
|
-
picker.anchorElement = this;
|
|
10754
|
+
picker.anchorElement = this.anchorElement || this;
|
|
10737
10755
|
|
|
10738
10756
|
const syncFromPicker = (e) => {
|
|
10739
10757
|
e.stopPropagation();
|
package/package.json
CHANGED