@rogieking/figui3 8.9.21 → 8.9.23

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-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
  }
@@ -4429,7 +4454,6 @@ class FigFillPicker extends HTMLElement {
4429
4454
  "aspect-ratio": "1/1",
4430
4455
  fit: "cover",
4431
4456
  checkerboard: "true",
4432
- autoplay: "true",
4433
4457
  controls: true,
4434
4458
  muted: "true",
4435
4459
  loop: "true",
@@ -4451,7 +4475,7 @@ class FigFillPicker extends HTMLElement {
4451
4475
  #applyDefaultVideo({ emit = true } = {}) {
4452
4476
  if (this.#video.url) {
4453
4477
  this.#video.missing = false;
4454
- if (!this.#video.poster) this.#captureVideoPoster(this.#video.url, { emit });
4478
+ if (!this.#video.poster) this.#captureVideoPoster(this.#video.url);
4455
4479
  return;
4456
4480
  }
4457
4481
  const fallback = this.getAttribute("default-video");
@@ -4465,11 +4489,71 @@ class FigFillPicker extends HTMLElement {
4465
4489
  ".fig-fill-picker-video-preview",
4466
4490
  );
4467
4491
  if (preview) this.#updateVideoPreviewStyle(preview);
4468
- this.#captureVideoPoster(fallback, { emit });
4492
+ this.#captureVideoPoster(fallback);
4493
+ if (emit) this.#emitInput();
4494
+ }
4495
+
4496
+ #ensureVideoPoster() {
4497
+ if (this.#fillType !== "video") return;
4498
+ const src = this.#video.url;
4499
+ if (!src || this.#video.poster) return;
4500
+ this.#captureVideoPoster(src);
4469
4501
  }
4470
4502
 
4471
- async #captureVideoPoster(src, { emit = true } = {}) {
4472
- if (!src) return;
4503
+ async #videoFrameBitmap(video) {
4504
+ if (typeof video.requestVideoFrameCallback === "function") {
4505
+ await Promise.race([
4506
+ new Promise((resolve) => video.requestVideoFrameCallback(() => resolve())),
4507
+ new Promise((resolve) => setTimeout(resolve, 80)),
4508
+ ]);
4509
+ } else {
4510
+ try {
4511
+ await video.play();
4512
+ video.pause();
4513
+ } catch {
4514
+ /* use the current decoded frame */
4515
+ }
4516
+ }
4517
+ if (typeof createImageBitmap === "function") {
4518
+ try {
4519
+ return await createImageBitmap(video);
4520
+ } catch {
4521
+ /* tainted or undecoded — fall back to drawImage(video) */
4522
+ }
4523
+ }
4524
+ return null;
4525
+ }
4526
+
4527
+ async #blobFromVideoFrame(video) {
4528
+ const bitmap = await this.#videoFrameBitmap(video);
4529
+ const width = bitmap?.width || video.videoWidth;
4530
+ const height = bitmap?.height || video.videoHeight;
4531
+ if (!width || !height) {
4532
+ bitmap?.close?.();
4533
+ return null;
4534
+ }
4535
+ const canvas =
4536
+ typeof OffscreenCanvas === "function"
4537
+ ? new OffscreenCanvas(width, height)
4538
+ : Object.assign(document.createElement("canvas"), { width, height });
4539
+ const ctx = canvas.getContext("2d");
4540
+ if (!ctx) {
4541
+ bitmap?.close?.();
4542
+ return null;
4543
+ }
4544
+ try {
4545
+ ctx.drawImage(bitmap || video, 0, 0, width, height);
4546
+ } finally {
4547
+ bitmap?.close?.();
4548
+ }
4549
+ return canvas.convertToBlob
4550
+ ? canvas.convertToBlob({ type: "image/jpeg", quality: 0.85 })
4551
+ : new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", 0.85));
4552
+ }
4553
+
4554
+ async #captureVideoPoster(src, { emit = false } = {}) {
4555
+ if (!src || this.#videoPosterCapturing === src) return;
4556
+ this.#videoPosterCapturing = src;
4473
4557
  const video = document.createElement("video");
4474
4558
  video.muted = true;
4475
4559
  video.playsInline = true;
@@ -4483,14 +4567,8 @@ class FigFillPicker extends HTMLElement {
4483
4567
  video.src = src;
4484
4568
  });
4485
4569
  if (!video.videoWidth || !video.videoHeight) return;
4486
- const canvas = document.createElement("canvas");
4487
- canvas.width = video.videoWidth;
4488
- canvas.height = video.videoHeight;
4489
- canvas.getContext("2d").drawImage(video, 0, 0);
4490
- const blob = await new Promise((resolve) =>
4491
- canvas.toBlob(resolve, "image/jpeg", 0.85),
4492
- );
4493
- if (!blob) return;
4570
+ const blob = await this.#blobFromVideoFrame(video);
4571
+ if (!blob || this.#video.url !== src) return;
4494
4572
  this.#revokeVideoPoster();
4495
4573
  this.#video.poster = URL.createObjectURL(blob);
4496
4574
  this.#ownedBlobUrls.add(this.#video.poster);
@@ -4498,6 +4576,10 @@ class FigFillPicker extends HTMLElement {
4498
4576
  if (emit) this.#emitInput();
4499
4577
  } catch {
4500
4578
  // Cross-origin or decode failures leave the swatch empty until a poster exists.
4579
+ } finally {
4580
+ video.removeAttribute("src");
4581
+ video.load();
4582
+ if (this.#videoPosterCapturing === src) this.#videoPosterCapturing = null;
4501
4583
  }
4502
4584
  }
4503
4585
 
@@ -4523,7 +4605,6 @@ class FigFillPicker extends HTMLElement {
4523
4605
  this.#video.url = src;
4524
4606
  this.#video.missing = false;
4525
4607
  this.#updateVideoPreviewStyle(preview);
4526
- preview.play?.();
4527
4608
  this.#captureVideoPoster(src);
4528
4609
  this.#updateSwatch();
4529
4610
  this.#emitInput();
@@ -5186,6 +5267,7 @@ class FigFillPicker extends HTMLElement {
5186
5267
  case "value":
5187
5268
  this.#parseValue();
5188
5269
  this.#updateSwatch();
5270
+ this.#ensureVideoPoster();
5189
5271
  if (this.#dialog?.open && !this.#isDraggingColor) {
5190
5272
  this.#refreshDialogUI();
5191
5273
  this.#lastChangeValue = JSON.stringify(this.value);
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
 
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
  }
@@ -10631,8 +10648,17 @@ class FigInputGradient extends HTMLElement {
10631
10648
  .join(", ");
10632
10649
  const interp = gradientInterpolationClause(gradient);
10633
10650
  const interpolation = interp ? ` ${interp}` : "";
10634
- // Compact swatch always previews L→R, regardless of linear angle /
10635
- // radial / angular type (stop handles also use this axis when editing).
10651
+ if (this.#editMode === "picker") {
10652
+ switch (gradient.type) {
10653
+ case "radial":
10654
+ return `radial-gradient(circle at ${gradient.centerX}% ${gradient.centerY}%${interpolation}, ${stops})`;
10655
+ case "angular":
10656
+ return `conic-gradient(from ${gradient.angle}deg${interpolation}, ${stops})`;
10657
+ default:
10658
+ return `linear-gradient(${gradient.angle}deg${interpolation}, ${stops})`;
10659
+ }
10660
+ }
10661
+ // Inline editor swatch previews L→R so stop handles share that axis.
10636
10662
  return `linear-gradient(to right${interpolation}, ${stops})`;
10637
10663
  }
10638
10664
 
@@ -10744,6 +10770,11 @@ class FigInputGradient extends HTMLElement {
10744
10770
  ...this.#gradient,
10745
10771
  ...detail.gradient,
10746
10772
  });
10773
+ if (this.#editMode === "picker" && typeof detail.css === "string" && detail.css) {
10774
+ this.#swatch?.setAttribute("background", detail.css);
10775
+ this.#syncSwatchSize();
10776
+ return;
10777
+ }
10747
10778
  this.#syncSwatch();
10748
10779
  };
10749
10780
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "8.9.21",
3
+ "version": "8.9.23",
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",