@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/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: strict;
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 videos.
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 = { url: null, scaleMode: "fill", opacity: 1 };
8959
- #webcam = { snapshot: null, opacity: 1 };
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) this.#video = { ...this.#video, ...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
- this.#webcam = { ...this.#webcam, ...parsed.webcam };
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 ? `url(${this.#image.url})` : "#D9D9D9";
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
- ? `url(${this.#webcam.snapshot})`
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.image?.url) this.#webcam.snapshot = detail.image.url;
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
- // Update the swatch's alpha
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
- if (this.#fillPicker) {
9549
- const swatch = this.#fillPicker.querySelector("fig-swatch");
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
- video: { ...this.#video },
9696
+ colorSpace: "srgb",
9697
+ video: this.#videoValue(),
9597
9698
  };
9598
9699
  case "webcam":
9599
9700
  return {
9600
9701
  type: "webcam",
9601
- webcam: { ...this.#webcam },
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
- // Pass through to internal fill picker
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
- const icon = createFigIcon("play", {
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(".fig-media-controls-play-icon");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "8.9.16",
3
+ "version": "8.9.18",
4
4
  "description": "A lightweight web components library for building Figma plugin and widget UIs with native look and feel",
5
5
  "author": "Rogie King",
6
6
  "license": "MIT",