@rogieking/figui3 8.9.16 → 8.9.18
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-editor/SKILL.md +9 -5
- package/.cursor/skills/figui3/SKILL.md +1 -1
- package/README.md +15 -5
- package/components.css +17 -2
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-editor.js +3 -3
- package/dist/fig.css +1 -1
- package/dist/fig.js +7 -7
- package/fig-editor.css +10 -5
- package/fig-editor.js +363 -153
- package/fig.js +127 -28
- package/package.json +1 -1
package/fig.js
CHANGED
|
@@ -322,7 +322,7 @@ fig-select-options > .fig-overflow-end::before,
|
|
|
322
322
|
|
|
323
323
|
const FIG_SHADOW_ICON_CSS = `
|
|
324
324
|
fig-icon {
|
|
325
|
-
contain:
|
|
325
|
+
contain: size;
|
|
326
326
|
--size: var(--spacer-4);
|
|
327
327
|
display: inline-flex;
|
|
328
328
|
width: var(--size);
|
|
@@ -8679,6 +8679,11 @@ function gradientInterpolationClause(gradient) {
|
|
|
8679
8679
|
return `in ${normalized.interpolationSpace}`;
|
|
8680
8680
|
}
|
|
8681
8681
|
|
|
8682
|
+
function figCssUrl(url) {
|
|
8683
|
+
if (!url) return "";
|
|
8684
|
+
return `url("${String(url).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}")`;
|
|
8685
|
+
}
|
|
8686
|
+
|
|
8682
8687
|
function figHexToRGB(hex) {
|
|
8683
8688
|
const h = hex.replace(/^#/, "");
|
|
8684
8689
|
return {
|
|
@@ -8929,11 +8934,14 @@ function hslToSRGB(h, s, l) {
|
|
|
8929
8934
|
}
|
|
8930
8935
|
|
|
8931
8936
|
/**
|
|
8932
|
-
* A fill input that supports solid colors, gradients, images, and
|
|
8937
|
+
* A fill input that supports solid colors, gradients, images, videos, and webcam.
|
|
8933
8938
|
* @attr {string} value - JSON string with fill data
|
|
8934
8939
|
* @attr {boolean} disabled - Whether the input is disabled
|
|
8940
|
+
* @attr {string} webcam-mode - Forwarded to fig-fill-picker: `live` (default) or `snapshot`
|
|
8941
|
+
* @attr {string} default-video - Forwarded sample clip URL for the Video tab
|
|
8935
8942
|
* @fires input - When the fill value changes
|
|
8936
8943
|
* @fires change - When the fill value is committed
|
|
8944
|
+
* @fires webcamstream - `{ stream, deviceId }` forwarded from the inner picker
|
|
8937
8945
|
*/
|
|
8938
8946
|
class FigInputFill extends HTMLElement {
|
|
8939
8947
|
#fillType = "solid";
|
|
@@ -8955,8 +8963,22 @@ class FigInputFill extends HTMLElement {
|
|
|
8955
8963
|
],
|
|
8956
8964
|
};
|
|
8957
8965
|
#image = { url: null, scaleMode: "fill", scale: 50, opacity: 1 };
|
|
8958
|
-
#video = {
|
|
8959
|
-
|
|
8966
|
+
#video = {
|
|
8967
|
+
url: null,
|
|
8968
|
+
poster: null,
|
|
8969
|
+
scaleMode: "fill",
|
|
8970
|
+
scale: 50,
|
|
8971
|
+
opacity: 1,
|
|
8972
|
+
missing: true,
|
|
8973
|
+
};
|
|
8974
|
+
#webcam = {
|
|
8975
|
+
live: true,
|
|
8976
|
+
snapshot: null,
|
|
8977
|
+
deviceId: null,
|
|
8978
|
+
scaleMode: "fill",
|
|
8979
|
+
scale: 50,
|
|
8980
|
+
opacity: 1,
|
|
8981
|
+
};
|
|
8960
8982
|
|
|
8961
8983
|
constructor() {
|
|
8962
8984
|
super();
|
|
@@ -8968,6 +8990,8 @@ class FigInputFill extends HTMLElement {
|
|
|
8968
8990
|
"disabled",
|
|
8969
8991
|
"mode",
|
|
8970
8992
|
"alpha",
|
|
8993
|
+
"webcam-mode",
|
|
8994
|
+
"default-video",
|
|
8971
8995
|
"aria-label",
|
|
8972
8996
|
"aria-describedby",
|
|
8973
8997
|
"aria-invalid",
|
|
@@ -9023,11 +9047,22 @@ class FigInputFill extends HTMLElement {
|
|
|
9023
9047
|
if (parsed.image) this.#image = { ...this.#image, ...parsed.image };
|
|
9024
9048
|
break;
|
|
9025
9049
|
case "video":
|
|
9026
|
-
if (parsed.video)
|
|
9050
|
+
if (parsed.video) {
|
|
9051
|
+
this.#video = { ...this.#video, ...parsed.video };
|
|
9052
|
+
this.#video.missing = !this.#video.url;
|
|
9053
|
+
}
|
|
9027
9054
|
break;
|
|
9028
9055
|
case "webcam":
|
|
9029
|
-
if (parsed.webcam)
|
|
9030
|
-
|
|
9056
|
+
if (parsed.webcam && typeof parsed.webcam === "object") {
|
|
9057
|
+
const { stream: _ignored, ...rest } = parsed.webcam;
|
|
9058
|
+
this.#webcam = { ...this.#webcam, ...rest };
|
|
9059
|
+
} else if (parsed.image?.url) {
|
|
9060
|
+
this.#webcam.snapshot = parsed.image.url;
|
|
9061
|
+
if (parsed.image.scaleMode)
|
|
9062
|
+
this.#webcam.scaleMode = parsed.image.scaleMode;
|
|
9063
|
+
if (parsed.image.scale != null)
|
|
9064
|
+
this.#webcam.scale = parsed.image.scale;
|
|
9065
|
+
}
|
|
9031
9066
|
if (parsed.opacity !== undefined)
|
|
9032
9067
|
this.#webcam.opacity = parsed.opacity;
|
|
9033
9068
|
break;
|
|
@@ -9065,6 +9100,11 @@ class FigInputFill extends HTMLElement {
|
|
|
9065
9100
|
}
|
|
9066
9101
|
}
|
|
9067
9102
|
if (!attrs["dialog-position"]) attrs["dialog-position"] = "left";
|
|
9103
|
+
const webcamMode = this.getAttribute("webcam-mode");
|
|
9104
|
+
if (webcamMode && !attrs["webcam-mode"]) attrs["webcam-mode"] = webcamMode;
|
|
9105
|
+
const defaultVideo = this.getAttribute("default-video");
|
|
9106
|
+
if (defaultVideo && !attrs["default-video"])
|
|
9107
|
+
attrs["default-video"] = defaultVideo;
|
|
9068
9108
|
return attrs;
|
|
9069
9109
|
}
|
|
9070
9110
|
|
|
@@ -9096,16 +9136,22 @@ class FigInputFill extends HTMLElement {
|
|
|
9096
9136
|
}
|
|
9097
9137
|
}
|
|
9098
9138
|
case "image":
|
|
9099
|
-
return this.#image.url ?
|
|
9139
|
+
return this.#image.url ? figCssUrl(this.#image.url) : "#D9D9D9";
|
|
9140
|
+
case "video":
|
|
9141
|
+
return this.#video.poster ? figCssUrl(this.#video.poster) : "#D9D9D9";
|
|
9100
9142
|
case "webcam":
|
|
9101
9143
|
return this.#webcam.snapshot
|
|
9102
|
-
?
|
|
9144
|
+
? figCssUrl(this.#webcam.snapshot)
|
|
9103
9145
|
: "#D9D9D9";
|
|
9104
9146
|
default:
|
|
9105
9147
|
return "#D9D9D9";
|
|
9106
9148
|
}
|
|
9107
9149
|
}
|
|
9108
9150
|
|
|
9151
|
+
#gradientCss() {
|
|
9152
|
+
return this.#fillPickerSwatchBackground();
|
|
9153
|
+
}
|
|
9154
|
+
|
|
9109
9155
|
#normalizeFillOpacity(value, fallback = 1) {
|
|
9110
9156
|
if (value === undefined || value === null || value === "") return fallback;
|
|
9111
9157
|
const n = Number(value);
|
|
@@ -9319,10 +9365,15 @@ class FigInputFill extends HTMLElement {
|
|
|
9319
9365
|
if (detail.image) this.#image = detail.image;
|
|
9320
9366
|
break;
|
|
9321
9367
|
case "video":
|
|
9322
|
-
if (detail.video) this.#video = detail.video;
|
|
9368
|
+
if (detail.video) this.#video = { ...this.#video, ...detail.video };
|
|
9323
9369
|
break;
|
|
9324
9370
|
case "webcam":
|
|
9325
|
-
if (detail.
|
|
9371
|
+
if (detail.webcam) {
|
|
9372
|
+
const { stream: _ignored, ...rest } = detail.webcam;
|
|
9373
|
+
this.#webcam = { ...this.#webcam, ...rest };
|
|
9374
|
+
} else if (detail.image?.url) {
|
|
9375
|
+
this.#webcam.snapshot = detail.image.url;
|
|
9376
|
+
}
|
|
9326
9377
|
break;
|
|
9327
9378
|
}
|
|
9328
9379
|
// Update controls (don't re-render to keep dialog open)
|
|
@@ -9331,6 +9382,7 @@ class FigInputFill extends HTMLElement {
|
|
|
9331
9382
|
} else {
|
|
9332
9383
|
this.#updateControls();
|
|
9333
9384
|
}
|
|
9385
|
+
this.#syncSwatch();
|
|
9334
9386
|
|
|
9335
9387
|
this.#emitInput();
|
|
9336
9388
|
});
|
|
@@ -9339,6 +9391,16 @@ class FigInputFill extends HTMLElement {
|
|
|
9339
9391
|
e.stopPropagation();
|
|
9340
9392
|
this.#emitChange();
|
|
9341
9393
|
});
|
|
9394
|
+
|
|
9395
|
+
this.#fillPicker.addEventListener("webcamstream", (e) => {
|
|
9396
|
+
this.dispatchEvent(
|
|
9397
|
+
new CustomEvent("webcamstream", {
|
|
9398
|
+
bubbles: true,
|
|
9399
|
+
composed: true,
|
|
9400
|
+
detail: e.detail,
|
|
9401
|
+
}),
|
|
9402
|
+
);
|
|
9403
|
+
});
|
|
9342
9404
|
}
|
|
9343
9405
|
|
|
9344
9406
|
// Hex input (solid only)
|
|
@@ -9381,8 +9443,7 @@ class FigInputFill extends HTMLElement {
|
|
|
9381
9443
|
break;
|
|
9382
9444
|
}
|
|
9383
9445
|
this.#updateFillPicker();
|
|
9384
|
-
|
|
9385
|
-
this.#updateSwatchAlpha(alpha);
|
|
9446
|
+
this.#syncSwatch();
|
|
9386
9447
|
this.#emitInput();
|
|
9387
9448
|
});
|
|
9388
9449
|
this.#opacityInput.addEventListener("change", (e) => {
|
|
@@ -9542,15 +9603,19 @@ class FigInputFill extends HTMLElement {
|
|
|
9542
9603
|
if (this.#fillPicker) {
|
|
9543
9604
|
this.#fillPicker.setAttribute("value", JSON.stringify(this.value));
|
|
9544
9605
|
}
|
|
9606
|
+
this.#syncSwatch();
|
|
9607
|
+
}
|
|
9608
|
+
|
|
9609
|
+
#syncSwatch() {
|
|
9610
|
+
const swatch = this.#fillPicker?.querySelector("fig-swatch");
|
|
9611
|
+
if (!swatch) return;
|
|
9612
|
+
swatch.setAttribute("background", this.#fillPickerSwatchBackground());
|
|
9613
|
+
swatch.setAttribute("alpha", this.#fillPickerSwatchAlpha());
|
|
9545
9614
|
}
|
|
9546
9615
|
|
|
9547
9616
|
#updateSwatchAlpha(alpha) {
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
if (swatch) {
|
|
9551
|
-
swatch.setAttribute("alpha", alpha);
|
|
9552
|
-
}
|
|
9553
|
-
}
|
|
9617
|
+
const swatch = this.#fillPicker?.querySelector("fig-swatch");
|
|
9618
|
+
if (swatch) swatch.setAttribute("alpha", alpha);
|
|
9554
9619
|
}
|
|
9555
9620
|
|
|
9556
9621
|
#emitInput() {
|
|
@@ -9571,11 +9636,43 @@ class FigInputFill extends HTMLElement {
|
|
|
9571
9636
|
);
|
|
9572
9637
|
}
|
|
9573
9638
|
|
|
9639
|
+
get webcamStream() {
|
|
9640
|
+
return this.#fillPicker?.webcamStream ?? null;
|
|
9641
|
+
}
|
|
9642
|
+
|
|
9643
|
+
releaseWebcam() {
|
|
9644
|
+
this.#fillPicker?.releaseWebcam?.();
|
|
9645
|
+
}
|
|
9646
|
+
|
|
9647
|
+
#videoValue() {
|
|
9648
|
+
const url = this.#video.url ?? null;
|
|
9649
|
+
return {
|
|
9650
|
+
url,
|
|
9651
|
+
poster: this.#video.poster ?? null,
|
|
9652
|
+
scaleMode: this.#video.scaleMode || "fill",
|
|
9653
|
+
scale: this.#video.scale ?? 50,
|
|
9654
|
+
opacity: this.#video.opacity ?? 1,
|
|
9655
|
+
...(url ? {} : { missing: true }),
|
|
9656
|
+
};
|
|
9657
|
+
}
|
|
9658
|
+
|
|
9659
|
+
#webcamValue() {
|
|
9660
|
+
return {
|
|
9661
|
+
live: this.#webcam.live !== false,
|
|
9662
|
+
snapshot: this.#webcam.snapshot ?? null,
|
|
9663
|
+
deviceId: this.#webcam.deviceId ?? null,
|
|
9664
|
+
scaleMode: this.#webcam.scaleMode || "fill",
|
|
9665
|
+
scale: this.#webcam.scale ?? 50,
|
|
9666
|
+
opacity: this.#webcam.opacity ?? 1,
|
|
9667
|
+
};
|
|
9668
|
+
}
|
|
9669
|
+
|
|
9574
9670
|
get value() {
|
|
9575
9671
|
switch (this.#fillType) {
|
|
9576
9672
|
case "solid":
|
|
9577
9673
|
return {
|
|
9578
9674
|
type: "solid",
|
|
9675
|
+
colorSpace: "srgb",
|
|
9579
9676
|
color: this.#solid.color,
|
|
9580
9677
|
alpha: this.#solid.alpha,
|
|
9581
9678
|
opacity: Math.round(this.#solid.alpha * 100), // FigFillPicker expects opacity 0-100
|
|
@@ -9583,22 +9680,27 @@ class FigInputFill extends HTMLElement {
|
|
|
9583
9680
|
case "gradient":
|
|
9584
9681
|
return {
|
|
9585
9682
|
type: "gradient",
|
|
9683
|
+
colorSpace: "srgb",
|
|
9586
9684
|
gradient: gradientToValueShape(this.#gradient),
|
|
9685
|
+
css: this.#gradientCss(),
|
|
9587
9686
|
};
|
|
9588
9687
|
case "image":
|
|
9589
9688
|
return {
|
|
9590
9689
|
type: "image",
|
|
9690
|
+
colorSpace: "srgb",
|
|
9591
9691
|
image: { ...this.#image },
|
|
9592
9692
|
};
|
|
9593
9693
|
case "video":
|
|
9594
9694
|
return {
|
|
9595
9695
|
type: "video",
|
|
9596
|
-
|
|
9696
|
+
colorSpace: "srgb",
|
|
9697
|
+
video: this.#videoValue(),
|
|
9597
9698
|
};
|
|
9598
9699
|
case "webcam":
|
|
9599
9700
|
return {
|
|
9600
9701
|
type: "webcam",
|
|
9601
|
-
|
|
9702
|
+
colorSpace: "srgb",
|
|
9703
|
+
webcam: this.#webcamValue(),
|
|
9602
9704
|
};
|
|
9603
9705
|
default:
|
|
9604
9706
|
return { type: this.#fillType };
|
|
@@ -9633,7 +9735,8 @@ class FigInputFill extends HTMLElement {
|
|
|
9633
9735
|
this.#syncDisabled();
|
|
9634
9736
|
break;
|
|
9635
9737
|
case "mode":
|
|
9636
|
-
|
|
9738
|
+
case "webcam-mode":
|
|
9739
|
+
case "default-video":
|
|
9637
9740
|
if (this.#fillPicker) {
|
|
9638
9741
|
if (newValue) {
|
|
9639
9742
|
this.#fillPicker.setAttribute(name, newValue);
|
|
@@ -13279,13 +13382,9 @@ class FigMediaControls extends HTMLElement {
|
|
|
13279
13382
|
tooltip.setAttribute("text", "Play");
|
|
13280
13383
|
const btn = document.createElement("fig-button");
|
|
13281
13384
|
btn.setAttribute("variant", "ghost");
|
|
13282
|
-
btn.setAttribute("size", "small");
|
|
13283
13385
|
btn.setAttribute("icon", "true");
|
|
13284
13386
|
btn.setAttribute("aria-label", "Play");
|
|
13285
|
-
|
|
13286
|
-
className: "fig-media-controls-play-icon",
|
|
13287
|
-
});
|
|
13288
|
-
btn.append(icon);
|
|
13387
|
+
btn.append(createFigIcon("play"));
|
|
13289
13388
|
tooltip.append(btn);
|
|
13290
13389
|
btn.addEventListener("click", (e) => {
|
|
13291
13390
|
e.preventDefault();
|
|
@@ -13375,7 +13474,7 @@ class FigMediaControls extends HTMLElement {
|
|
|
13375
13474
|
const playing = this.playing;
|
|
13376
13475
|
this.#playBtn.setAttribute("aria-label", playing ? "Pause" : "Play");
|
|
13377
13476
|
this.#playTooltip?.setAttribute("text", playing ? "Pause" : "Play");
|
|
13378
|
-
const icon = this.#playBtn.querySelector("
|
|
13477
|
+
const icon = this.#playBtn.querySelector("fig-icon");
|
|
13379
13478
|
if (icon) {
|
|
13380
13479
|
icon.setAttribute("name", playing ? "pause" : "play");
|
|
13381
13480
|
}
|
package/package.json
CHANGED