@rogieking/figui3 4.15.4 → 4.15.8

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
@@ -7308,6 +7308,10 @@ class FigInputGradient extends HTMLElement {
7308
7308
  });
7309
7309
  }
7310
7310
 
7311
+ refreshLayout() {
7312
+ this.#repositionHandles();
7313
+ }
7314
+
7311
7315
  #syncHandles() {
7312
7316
  if (!this.#track) return;
7313
7317
  const handles = this.#track.querySelectorAll(
@@ -11811,6 +11815,13 @@ customElements.define("fig-footer", FigFooter);
11811
11815
  class FigSpinner extends HTMLElement {}
11812
11816
  customElements.define("fig-spinner", FigSpinner);
11813
11817
 
11818
+ /**
11819
+ * A styled visual preview layer for arbitrary content such as images, canvas,
11820
+ * video, SVG, or custom rendered surfaces.
11821
+ */
11822
+ class FigPreview extends HTMLElement {}
11823
+ customElements.define("fig-preview", FigPreview);
11824
+
11814
11825
  /** @type {Record<string, string>} */
11815
11826
  const FIG_ICON_TOKENS = {
11816
11827
  chevron: "--icon-16-chevron",
@@ -12410,6 +12421,11 @@ class FigFillPicker extends HTMLElement {
12410
12421
  // Use RAF to ensure layout is complete before updating angle input
12411
12422
  requestAnimationFrame(() => {
12412
12423
  this.#updateGradientUI();
12424
+ const barInput = tab.querySelector(".fig-fill-picker-gradient-bar-input");
12425
+ barInput?.refreshLayout?.();
12426
+ requestAnimationFrame(() => {
12427
+ barInput?.refreshLayout?.();
12428
+ });
12413
12429
  });
12414
12430
  }
12415
12431
 
@@ -13347,16 +13363,11 @@ class FigFillPicker extends HTMLElement {
13347
13363
  </fig-dropdown>
13348
13364
  <fig-input-number class="fig-fill-picker-scale" min="1" max="200" value="${
13349
13365
  this.#image.scale
13350
- }" units="%" style="display: none;"></fig-input-number>
13366
+ }" units="%" ${
13367
+ this.#image.scaleMode === "tile" ? "" : 'style="display: none;"'
13368
+ }></fig-input-number>
13351
13369
  </fig-field>
13352
- <div class="fig-fill-picker-media-preview">
13353
- <div class="fig-fill-picker-checkerboard"></div>
13354
- <div class="fig-fill-picker-image-preview"></div>
13355
- <fig-button variant="overlay" class="fig-fill-picker-upload">
13356
- Upload from computer
13357
- <input type="file" accept="image/*" style="display: none;" />
13358
- </fig-button>
13359
- </div>
13370
+ <fig-image class="fig-fill-picker-media-preview fig-fill-picker-image-preview" upload="true" label="Upload from computer" size="auto" aspect-ratio="1/1" fit="cover" checkerboard="true"></fig-image>
13360
13371
  `;
13361
13372
 
13362
13373
  this.#setupImageEvents(container);
@@ -13367,8 +13378,6 @@ class FigFillPicker extends HTMLElement {
13367
13378
  ".fig-fill-picker-scale-mode",
13368
13379
  );
13369
13380
  const scaleInput = container.querySelector(".fig-fill-picker-scale");
13370
- const uploadBtn = container.querySelector(".fig-fill-picker-upload");
13371
- const fileInput = container.querySelector('input[type="file"]');
13372
13381
  const preview = container.querySelector(".fig-fill-picker-image-preview");
13373
13382
 
13374
13383
  scaleModeDropdown.addEventListener("change", (e) => {
@@ -13386,88 +13395,106 @@ class FigFillPicker extends HTMLElement {
13386
13395
  this.#emitInput();
13387
13396
  });
13388
13397
 
13389
- uploadBtn.addEventListener("click", () => {
13390
- fileInput.click();
13398
+ preview.addEventListener("loaded", (e) => {
13399
+ const src = e.detail?.src || preview.src;
13400
+ if (!src) return;
13401
+ this.#image.url = src;
13402
+ this.#updateImagePreview(preview);
13403
+ this.#updateChit();
13404
+ this.#emitInput();
13391
13405
  });
13392
13406
 
13393
- fileInput.addEventListener("change", (e) => {
13394
- const file = e.target.files[0];
13395
- if (file) {
13396
- const reader = new FileReader();
13397
- reader.onload = (e) => {
13398
- this.#image.url = e.target.result;
13399
- this.#updateImagePreview(preview);
13400
- this.#updateChit();
13401
- this.#emitInput();
13402
- };
13403
- reader.readAsDataURL(file);
13404
- }
13407
+ preview.addEventListener("change", () => {
13408
+ if (preview.src) return;
13409
+ this.#image.url = null;
13410
+ this.#updateImagePreview(preview);
13411
+ this.#updateChit();
13412
+ this.#emitInput();
13405
13413
  });
13406
13414
 
13407
- // Drag and drop
13408
- const previewArea = container.querySelector(
13409
- ".fig-fill-picker-media-preview",
13410
- );
13411
- previewArea.addEventListener("dragover", (e) => {
13412
- e.preventDefault();
13413
- previewArea.classList.add("dragover");
13414
- });
13415
- previewArea.addEventListener("dragleave", () => {
13416
- previewArea.classList.remove("dragover");
13417
- });
13418
- previewArea.addEventListener("drop", (e) => {
13419
- e.preventDefault();
13420
- previewArea.classList.remove("dragover");
13421
- const file = e.dataTransfer.files[0];
13422
- if (file && file.type.startsWith("image/")) {
13423
- const reader = new FileReader();
13424
- reader.onload = (e) => {
13425
- this.#image.url = e.target.result;
13426
- this.#updateImagePreview(preview);
13427
- this.#updateChit();
13428
- this.#emitInput();
13429
- };
13430
- reader.readAsDataURL(file);
13431
- }
13432
- });
13415
+ this.#updateImagePreview(preview);
13433
13416
  }
13434
13417
 
13435
13418
  #updateImagePreview(element) {
13436
- const container = element.closest(".fig-fill-picker-media-preview");
13437
13419
  if (!this.#image.url) {
13438
- element.style.display = "none";
13439
- container?.classList.remove("has-media");
13420
+ element.removeAttribute("src");
13421
+ element.classList.remove("has-media", "is-tiled");
13422
+ element.style.backgroundImage = "";
13423
+ element.style.backgroundPosition = "";
13424
+ element.style.backgroundRepeat = "";
13425
+ element.style.backgroundSize = "";
13440
13426
  return;
13441
13427
  }
13442
13428
 
13443
- element.style.display = "block";
13444
- container?.classList.add("has-media");
13445
- element.style.backgroundImage = `url(${this.#image.url})`;
13446
- element.style.backgroundPosition = "center";
13429
+ element.setAttribute("src", this.#image.url);
13430
+ element.classList.add("has-media");
13431
+ element.style.backgroundImage = "";
13432
+ element.style.backgroundPosition = "";
13433
+ element.style.backgroundRepeat = "";
13434
+ element.style.backgroundSize = "";
13435
+ element.mediaEl?.style.removeProperty("opacity");
13436
+
13437
+ const fileInput = element.querySelector("fig-input-file[data-generated]");
13438
+ if (fileInput) {
13439
+ fileInput.setAttribute("label", "Replace");
13440
+ fileInput.removeAttribute("url");
13441
+ }
13447
13442
 
13448
13443
  switch (this.#image.scaleMode) {
13449
13444
  case "fill":
13450
- element.style.backgroundSize = "cover";
13451
- element.style.backgroundRepeat = "no-repeat";
13452
- break;
13453
- case "fit":
13454
- element.style.backgroundSize = "contain";
13455
- element.style.backgroundRepeat = "no-repeat";
13445
+ element.classList.remove("is-tiled");
13446
+ element.setAttribute("fit", "cover");
13456
13447
  break;
13457
13448
  case "crop":
13458
- element.style.backgroundSize = "cover";
13459
- element.style.backgroundRepeat = "no-repeat";
13449
+ element.classList.remove("is-tiled");
13450
+ element.setAttribute("fit", "cover");
13451
+ break;
13452
+ case "fit":
13453
+ element.classList.remove("is-tiled");
13454
+ element.setAttribute("fit", "contain");
13460
13455
  break;
13461
13456
  case "tile":
13457
+ element.classList.add("is-tiled");
13458
+ element.setAttribute("fit", "none");
13459
+ element.style.backgroundImage = `url(${this.#image.url})`;
13460
+ element.style.backgroundPosition = "top left";
13462
13461
  element.style.backgroundSize = `${this.#image.scale}%`;
13463
13462
  element.style.backgroundRepeat = "repeat";
13464
- element.style.backgroundPosition = "top left";
13463
+ if (element.mediaEl) element.mediaEl.style.opacity = "0";
13465
13464
  break;
13466
13465
  }
13467
13466
  }
13468
13467
 
13469
13468
  // For video elements (still uses object-fit)
13470
13469
  #updateVideoPreviewStyle(element) {
13470
+ if (element.tagName === "FIG-MEDIA") {
13471
+ if (!this.#video.url) {
13472
+ element.removeAttribute("src");
13473
+ element.classList.remove("has-media");
13474
+ return;
13475
+ }
13476
+
13477
+ element.setAttribute("src", this.#video.url);
13478
+ element.classList.add("has-media");
13479
+
13480
+ const fileInput = element.querySelector("fig-input-file[data-generated]");
13481
+ if (fileInput) {
13482
+ fileInput.setAttribute("label", "Replace");
13483
+ fileInput.removeAttribute("url");
13484
+ }
13485
+
13486
+ switch (this.#video.scaleMode) {
13487
+ case "fill":
13488
+ case "crop":
13489
+ element.setAttribute("fit", "cover");
13490
+ break;
13491
+ case "fit":
13492
+ element.setAttribute("fit", "contain");
13493
+ break;
13494
+ }
13495
+ return;
13496
+ }
13497
+
13471
13498
  element.style.objectPosition = "center";
13472
13499
  element.style.width = "100%";
13473
13500
  element.style.height = "100%";
@@ -13499,14 +13526,7 @@ class FigFillPicker extends HTMLElement {
13499
13526
  <option value="crop">Crop</option>
13500
13527
  </fig-dropdown>
13501
13528
  </fig-field>
13502
- <div class="fig-fill-picker-media-preview">
13503
- <div class="fig-fill-picker-checkerboard"></div>
13504
- <video class="fig-fill-picker-video-preview" style="display: none;" muted loop></video>
13505
- <fig-button variant="overlay" class="fig-fill-picker-upload">
13506
- Upload from computer
13507
- <input type="file" accept="video/*" style="display: none;" />
13508
- </fig-button>
13509
- </div>
13529
+ <fig-media class="fig-fill-picker-media-preview fig-fill-picker-video-preview" type="video" upload="true" label="Upload from computer" size="auto" aspect-ratio="1/1" fit="cover" checkerboard="true" autoplay="true" muted="true" loop="true"></fig-media>
13510
13530
  `;
13511
13531
 
13512
13532
  this.#setupVideoEvents(container);
@@ -13516,8 +13536,6 @@ class FigFillPicker extends HTMLElement {
13516
13536
  const scaleModeDropdown = container.querySelector(
13517
13537
  ".fig-fill-picker-scale-mode",
13518
13538
  );
13519
- const uploadBtn = container.querySelector(".fig-fill-picker-upload");
13520
- const fileInput = container.querySelector('input[type="file"]');
13521
13539
  const preview = container.querySelector(".fig-fill-picker-video-preview");
13522
13540
 
13523
13541
  scaleModeDropdown.addEventListener("change", (e) => {
@@ -13527,51 +13545,25 @@ class FigFillPicker extends HTMLElement {
13527
13545
  this.#emitInput();
13528
13546
  });
13529
13547
 
13530
- uploadBtn.addEventListener("click", () => {
13531
- fileInput.click();
13548
+ preview.addEventListener("loaded", (e) => {
13549
+ const src = e.detail?.src || preview.src;
13550
+ if (!src) return;
13551
+ this.#video.url = src;
13552
+ this.#updateVideoPreviewStyle(preview);
13553
+ preview.play?.();
13554
+ this.#updateChit();
13555
+ this.#emitInput();
13532
13556
  });
13533
13557
 
13534
- // Drag and drop
13535
- const previewArea = container.querySelector(
13536
- ".fig-fill-picker-media-preview",
13537
- );
13538
-
13539
- fileInput.addEventListener("change", (e) => {
13540
- const file = e.target.files[0];
13541
- if (file) {
13542
- this.#video.url = URL.createObjectURL(file);
13543
- preview.src = this.#video.url;
13544
- preview.style.display = "block";
13545
- preview.play();
13546
- previewArea.classList.add("has-media");
13547
- this.#updateVideoPreviewStyle(preview);
13548
- this.#updateChit();
13549
- this.#emitInput();
13550
- }
13558
+ preview.addEventListener("change", () => {
13559
+ if (preview.src) return;
13560
+ this.#video.url = null;
13561
+ this.#updateVideoPreviewStyle(preview);
13562
+ this.#updateChit();
13563
+ this.#emitInput();
13551
13564
  });
13552
13565
 
13553
- previewArea.addEventListener("dragover", (e) => {
13554
- e.preventDefault();
13555
- previewArea.classList.add("dragover");
13556
- });
13557
- previewArea.addEventListener("dragleave", () => {
13558
- previewArea.classList.remove("dragover");
13559
- });
13560
- previewArea.addEventListener("drop", (e) => {
13561
- e.preventDefault();
13562
- previewArea.classList.remove("dragover");
13563
- const file = e.dataTransfer.files[0];
13564
- if (file && file.type.startsWith("video/")) {
13565
- this.#video.url = URL.createObjectURL(file);
13566
- preview.src = this.#video.url;
13567
- preview.style.display = "block";
13568
- preview.play();
13569
- previewArea.classList.add("has-media");
13570
- this.#updateVideoPreviewStyle(preview);
13571
- this.#updateChit();
13572
- this.#emitInput();
13573
- }
13574
- });
13566
+ this.#updateVideoPreviewStyle(preview);
13575
13567
  }
13576
13568
 
13577
13569
  // ============ WEBCAM TAB ============
@@ -15078,14 +15070,8 @@ class FigHandle extends HTMLElement {
15078
15070
  get value() {
15079
15071
  const container = this.#getContainer();
15080
15072
  if (!container) return "0% 0%";
15081
- const rect = container.getBoundingClientRect();
15082
- const hw = this.offsetWidth / 2;
15083
- const hh = this.offsetHeight / 2;
15084
- const x = parseFloat(this.style.left) || 0;
15085
- const y = parseFloat(this.style.top) || 0;
15086
- const px = rect.width > 0 ? ((x + hw) / rect.width) * 100 : 0;
15087
- const py = rect.height > 0 ? ((y + hh) / rect.height) * 100 : 0;
15088
- return `${Math.round(px)}% ${Math.round(py)}%`;
15073
+ const { px, py } = this.#positionDetail(container.getBoundingClientRect());
15074
+ return `${Math.round(px * 100)}% ${Math.round(py * 100)}%`;
15089
15075
  }
15090
15076
 
15091
15077
  set value(v) {
@@ -15124,26 +15110,32 @@ class FigHandle extends HTMLElement {
15124
15110
  const hw = this.offsetWidth / 2;
15125
15111
  const hh = this.offsetHeight / 2;
15126
15112
 
15127
- const resolve = (token, containerDim, halfHandle) => {
15113
+ const resolvePx = (token, containerDim, halfHandle) => {
15128
15114
  if (token && typeof token === "object" && "px" in token) {
15129
15115
  return Math.max(
15130
15116
  -halfHandle,
15131
15117
  Math.min(containerDim - halfHandle, token.px - halfHandle),
15132
15118
  );
15133
15119
  }
15120
+ return null;
15121
+ };
15122
+
15123
+ const resolveResponsive = (token, halfHandle) => {
15134
15124
  const pct = typeof token === "number" ? token : 0;
15135
- const center = (pct / 100) * containerDim;
15136
- return Math.max(
15137
- -halfHandle,
15138
- Math.min(containerDim - halfHandle, center - halfHandle),
15139
- );
15125
+ return `calc(${pct}% - ${halfHandle}px)`;
15140
15126
  };
15141
15127
 
15142
15128
  const axes = this.#axes;
15143
- if (axes.x)
15144
- this.style.left = `${Math.round(resolve(xToken, rect.width, hw))}px`;
15145
- if (axes.y)
15146
- this.style.top = `${Math.round(resolve(yToken, rect.height, hh))}px`;
15129
+ if (axes.x) {
15130
+ const xPx = resolvePx(xToken, rect.width, hw);
15131
+ this.style.left =
15132
+ xPx === null ? resolveResponsive(xToken, hw) : `${Math.round(xPx)}px`;
15133
+ }
15134
+ if (axes.y) {
15135
+ const yPx = resolvePx(yToken, rect.height, hh);
15136
+ this.style.top =
15137
+ yPx === null ? resolveResponsive(yToken, hh) : `${Math.round(yPx)}px`;
15138
+ }
15147
15139
  }
15148
15140
 
15149
15141
  #syncValueAttribute() {
@@ -15372,8 +15364,9 @@ class FigHandle extends HTMLElement {
15372
15364
  const clampAndApply = (clientX, clientY, shiftKey = false) => {
15373
15365
  const rect = container.getBoundingClientRect();
15374
15366
  lastRect = rect;
15375
- const currentLeft = parseFloat(this.style.left) || 0;
15376
- const currentTop = parseFloat(this.style.top) || 0;
15367
+ const currentPosition = this.#positionDetail(rect);
15368
+ const currentLeft = currentPosition.x;
15369
+ const currentTop = currentPosition.y;
15377
15370
  const rawX = clientX - offsetX - rect.left - handleW / 2;
15378
15371
  const rawY = clientY - offsetY - rect.top - handleH / 2;
15379
15372
 
@@ -15447,6 +15440,7 @@ class FigHandle extends HTMLElement {
15447
15440
  if (this.#didDrag) {
15448
15441
  clampAndApply(e.clientX, e.clientY, e.shiftKey);
15449
15442
  this.#syncValueAttribute();
15443
+ this.#applyValue(this.getAttribute("value"));
15450
15444
  this.dispatchEvent(
15451
15445
  new CustomEvent("change", {
15452
15446
  bubbles: true,
@@ -15710,12 +15704,17 @@ class FigHandle extends HTMLElement {
15710
15704
  };
15711
15705
 
15712
15706
  #positionDetail(containerRect) {
15707
+ const rect = containerRect || this.#getContainer()?.getBoundingClientRect();
15708
+ if (!rect) return { x: 0, y: 0, px: 0, py: 0 };
15709
+ const handleRect = this.getBoundingClientRect();
15713
15710
  const hw = this.offsetWidth / 2;
15714
15711
  const hh = this.offsetHeight / 2;
15715
- const x = parseFloat(this.style.left) || 0;
15716
- const y = parseFloat(this.style.top) || 0;
15717
- const px = containerRect.width > 0 ? (x + hw) / containerRect.width : 0;
15718
- const py = containerRect.height > 0 ? (y + hh) / containerRect.height : 0;
15712
+ const x = handleRect.left - rect.left;
15713
+ const y = handleRect.top - rect.top;
15714
+ const centerX = x + hw;
15715
+ const centerY = y + hh;
15716
+ const px = rect.width > 0 ? centerX / rect.width : 0;
15717
+ const py = rect.height > 0 ? centerY / rect.height : 0;
15719
15718
  return { x, y, px, py };
15720
15719
  }
15721
15720
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "4.15.4",
3
+ "version": "4.15.8",
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",