@rogieking/figui3 8.9.15 → 8.9.17

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
@@ -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);
@@ -17337,7 +17440,7 @@ figDefineElement("fig-choice", FigChoice);
17337
17440
  * @attr {string} choice-element - CSS selector for child choices (default: "fig-choice")
17338
17441
  * @attr {string} layout - Layout mode: "vertical" (default), "horizontal", "grid"
17339
17442
  * @attr {number} columns - Number of columns when layout="grid" (default: 2)
17340
- * @attr {string} value - Value of the currently selected choice
17443
+ * @attr {string} value - Selected choice value. Omit to select the first choice; `value=""` means none.
17341
17444
  * @attr {boolean} disabled - Whether the chooser is disabled
17342
17445
  */
17343
17446
  class FigChooser extends HTMLElement {
@@ -17414,12 +17517,10 @@ class FigChooser extends HTMLElement {
17414
17517
  choice.removeAttribute("selected");
17415
17518
  }
17416
17519
  }
17417
- this.#selectedChoice = element;
17520
+ this.#selectedChoice = element ?? null;
17418
17521
  const val = element?.getAttribute("value") ?? "";
17419
17522
  if (this.getAttribute("value") !== val) {
17420
- if (val) {
17421
- this.setAttribute("value", val);
17422
- }
17523
+ this.setAttribute("value", val);
17423
17524
  }
17424
17525
  this.#scrollToChoice(element);
17425
17526
  }
@@ -17429,7 +17530,10 @@ class FigChooser extends HTMLElement {
17429
17530
  }
17430
17531
 
17431
17532
  set value(val) {
17432
- if (val === null || val === undefined || val === "") return;
17533
+ if (val === null || val === undefined || val === "") {
17534
+ this.setAttribute("value", "");
17535
+ return;
17536
+ }
17433
17537
  this.setAttribute("value", String(val));
17434
17538
  }
17435
17539
 
@@ -17495,8 +17599,14 @@ class FigChooser extends HTMLElement {
17495
17599
  }
17496
17600
 
17497
17601
  attributeChangedCallback(name, oldValue, newValue) {
17498
- if (name === "value" && newValue !== oldValue && newValue) {
17499
- this.#selectByValue(newValue);
17602
+ if (name === "value" && newValue !== oldValue) {
17603
+ if (newValue === "") {
17604
+ this.selectedChoice = null;
17605
+ } else if (newValue == null) {
17606
+ this.#syncSelection();
17607
+ } else {
17608
+ this.#selectByValue(newValue);
17609
+ }
17500
17610
  }
17501
17611
  if (name === "disabled") {
17502
17612
  this.#syncDisabledChoices();
@@ -17537,7 +17647,12 @@ class FigChooser extends HTMLElement {
17537
17647
  return;
17538
17648
  }
17539
17649
 
17650
+ const hasValueAttr = this.hasAttribute("value");
17540
17651
  const valueAttr = this.getAttribute("value");
17652
+ if (hasValueAttr && valueAttr === "") {
17653
+ this.selectedChoice = null;
17654
+ return;
17655
+ }
17541
17656
  if (valueAttr && this.#selectByValue(valueAttr)) return;
17542
17657
 
17543
17658
  const alreadySelected = choices.find((choice) =>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "8.9.15",
3
+ "version": "8.9.17",
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",