@rogieking/figui3 4.15.4 → 4.15.9

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",
@@ -12202,6 +12213,7 @@ class FigFillPicker extends HTMLElement {
12202
12213
  this.#dialog.anchor = this.anchorElement || this.#trigger;
12203
12214
  const dialogPosition = this.getAttribute("dialog-position") || "left";
12204
12215
  this.#dialog.setAttribute("position", dialogPosition);
12216
+ this.#dialog.setAttribute("offset", this.getAttribute("dialog-offset") || "8 8");
12205
12217
 
12206
12218
  const builtinModes = ["solid", "gradient", "image", "video", "webcam"];
12207
12219
  const builtinLabels = {
@@ -12410,6 +12422,11 @@ class FigFillPicker extends HTMLElement {
12410
12422
  // Use RAF to ensure layout is complete before updating angle input
12411
12423
  requestAnimationFrame(() => {
12412
12424
  this.#updateGradientUI();
12425
+ const barInput = tab.querySelector(".fig-fill-picker-gradient-bar-input");
12426
+ barInput?.refreshLayout?.();
12427
+ requestAnimationFrame(() => {
12428
+ barInput?.refreshLayout?.();
12429
+ });
12413
12430
  });
12414
12431
  }
12415
12432
 
@@ -12430,6 +12447,7 @@ class FigFillPicker extends HTMLElement {
12430
12447
  <fig-handle
12431
12448
  type="color"
12432
12449
  color="${this.#hsvToHex({ ...this.#color, a: 1 })}"
12450
+ data-no-color-picker
12433
12451
  drag
12434
12452
  drag-surface=".fig-fill-picker-color-area"
12435
12453
  drag-axes="x,y"
@@ -13347,16 +13365,11 @@ class FigFillPicker extends HTMLElement {
13347
13365
  </fig-dropdown>
13348
13366
  <fig-input-number class="fig-fill-picker-scale" min="1" max="200" value="${
13349
13367
  this.#image.scale
13350
- }" units="%" style="display: none;"></fig-input-number>
13368
+ }" units="%" ${
13369
+ this.#image.scaleMode === "tile" ? "" : 'style="display: none;"'
13370
+ }></fig-input-number>
13351
13371
  </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>
13372
+ <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
13373
  `;
13361
13374
 
13362
13375
  this.#setupImageEvents(container);
@@ -13367,8 +13380,6 @@ class FigFillPicker extends HTMLElement {
13367
13380
  ".fig-fill-picker-scale-mode",
13368
13381
  );
13369
13382
  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
13383
  const preview = container.querySelector(".fig-fill-picker-image-preview");
13373
13384
 
13374
13385
  scaleModeDropdown.addEventListener("change", (e) => {
@@ -13386,88 +13397,106 @@ class FigFillPicker extends HTMLElement {
13386
13397
  this.#emitInput();
13387
13398
  });
13388
13399
 
13389
- uploadBtn.addEventListener("click", () => {
13390
- fileInput.click();
13400
+ preview.addEventListener("loaded", (e) => {
13401
+ const src = e.detail?.src || preview.src;
13402
+ if (!src) return;
13403
+ this.#image.url = src;
13404
+ this.#updateImagePreview(preview);
13405
+ this.#updateChit();
13406
+ this.#emitInput();
13391
13407
  });
13392
13408
 
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
- }
13409
+ preview.addEventListener("change", () => {
13410
+ if (preview.src) return;
13411
+ this.#image.url = null;
13412
+ this.#updateImagePreview(preview);
13413
+ this.#updateChit();
13414
+ this.#emitInput();
13405
13415
  });
13406
13416
 
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
- });
13417
+ this.#updateImagePreview(preview);
13433
13418
  }
13434
13419
 
13435
13420
  #updateImagePreview(element) {
13436
- const container = element.closest(".fig-fill-picker-media-preview");
13437
13421
  if (!this.#image.url) {
13438
- element.style.display = "none";
13439
- container?.classList.remove("has-media");
13422
+ element.removeAttribute("src");
13423
+ element.classList.remove("has-media", "is-tiled");
13424
+ element.style.backgroundImage = "";
13425
+ element.style.backgroundPosition = "";
13426
+ element.style.backgroundRepeat = "";
13427
+ element.style.backgroundSize = "";
13440
13428
  return;
13441
13429
  }
13442
13430
 
13443
- element.style.display = "block";
13444
- container?.classList.add("has-media");
13445
- element.style.backgroundImage = `url(${this.#image.url})`;
13446
- element.style.backgroundPosition = "center";
13431
+ element.setAttribute("src", this.#image.url);
13432
+ element.classList.add("has-media");
13433
+ element.style.backgroundImage = "";
13434
+ element.style.backgroundPosition = "";
13435
+ element.style.backgroundRepeat = "";
13436
+ element.style.backgroundSize = "";
13437
+ element.mediaEl?.style.removeProperty("opacity");
13438
+
13439
+ const fileInput = element.querySelector("fig-input-file[data-generated]");
13440
+ if (fileInput) {
13441
+ fileInput.setAttribute("label", "Replace");
13442
+ fileInput.removeAttribute("url");
13443
+ }
13447
13444
 
13448
13445
  switch (this.#image.scaleMode) {
13449
13446
  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";
13447
+ element.classList.remove("is-tiled");
13448
+ element.setAttribute("fit", "cover");
13456
13449
  break;
13457
13450
  case "crop":
13458
- element.style.backgroundSize = "cover";
13459
- element.style.backgroundRepeat = "no-repeat";
13451
+ element.classList.remove("is-tiled");
13452
+ element.setAttribute("fit", "cover");
13453
+ break;
13454
+ case "fit":
13455
+ element.classList.remove("is-tiled");
13456
+ element.setAttribute("fit", "contain");
13460
13457
  break;
13461
13458
  case "tile":
13459
+ element.classList.add("is-tiled");
13460
+ element.setAttribute("fit", "none");
13461
+ element.style.backgroundImage = `url(${this.#image.url})`;
13462
+ element.style.backgroundPosition = "top left";
13462
13463
  element.style.backgroundSize = `${this.#image.scale}%`;
13463
13464
  element.style.backgroundRepeat = "repeat";
13464
- element.style.backgroundPosition = "top left";
13465
+ if (element.mediaEl) element.mediaEl.style.opacity = "0";
13465
13466
  break;
13466
13467
  }
13467
13468
  }
13468
13469
 
13469
13470
  // For video elements (still uses object-fit)
13470
13471
  #updateVideoPreviewStyle(element) {
13472
+ if (element.tagName === "FIG-MEDIA") {
13473
+ if (!this.#video.url) {
13474
+ element.removeAttribute("src");
13475
+ element.classList.remove("has-media");
13476
+ return;
13477
+ }
13478
+
13479
+ element.setAttribute("src", this.#video.url);
13480
+ element.classList.add("has-media");
13481
+
13482
+ const fileInput = element.querySelector("fig-input-file[data-generated]");
13483
+ if (fileInput) {
13484
+ fileInput.setAttribute("label", "Replace");
13485
+ fileInput.removeAttribute("url");
13486
+ }
13487
+
13488
+ switch (this.#video.scaleMode) {
13489
+ case "fill":
13490
+ case "crop":
13491
+ element.setAttribute("fit", "cover");
13492
+ break;
13493
+ case "fit":
13494
+ element.setAttribute("fit", "contain");
13495
+ break;
13496
+ }
13497
+ return;
13498
+ }
13499
+
13471
13500
  element.style.objectPosition = "center";
13472
13501
  element.style.width = "100%";
13473
13502
  element.style.height = "100%";
@@ -13499,14 +13528,7 @@ class FigFillPicker extends HTMLElement {
13499
13528
  <option value="crop">Crop</option>
13500
13529
  </fig-dropdown>
13501
13530
  </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>
13531
+ <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
13532
  `;
13511
13533
 
13512
13534
  this.#setupVideoEvents(container);
@@ -13516,8 +13538,6 @@ class FigFillPicker extends HTMLElement {
13516
13538
  const scaleModeDropdown = container.querySelector(
13517
13539
  ".fig-fill-picker-scale-mode",
13518
13540
  );
13519
- const uploadBtn = container.querySelector(".fig-fill-picker-upload");
13520
- const fileInput = container.querySelector('input[type="file"]');
13521
13541
  const preview = container.querySelector(".fig-fill-picker-video-preview");
13522
13542
 
13523
13543
  scaleModeDropdown.addEventListener("change", (e) => {
@@ -13527,51 +13547,25 @@ class FigFillPicker extends HTMLElement {
13527
13547
  this.#emitInput();
13528
13548
  });
13529
13549
 
13530
- uploadBtn.addEventListener("click", () => {
13531
- fileInput.click();
13550
+ preview.addEventListener("loaded", (e) => {
13551
+ const src = e.detail?.src || preview.src;
13552
+ if (!src) return;
13553
+ this.#video.url = src;
13554
+ this.#updateVideoPreviewStyle(preview);
13555
+ preview.play?.();
13556
+ this.#updateChit();
13557
+ this.#emitInput();
13532
13558
  });
13533
13559
 
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
- }
13560
+ preview.addEventListener("change", () => {
13561
+ if (preview.src) return;
13562
+ this.#video.url = null;
13563
+ this.#updateVideoPreviewStyle(preview);
13564
+ this.#updateChit();
13565
+ this.#emitInput();
13551
13566
  });
13552
13567
 
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
- });
13568
+ this.#updateVideoPreviewStyle(preview);
13575
13569
  }
13576
13570
 
13577
13571
  // ============ WEBCAM TAB ============
@@ -15026,6 +15020,10 @@ class FigHandle extends HTMLElement {
15026
15020
  return this.classList.contains("fig-input-gradient-ghost");
15027
15021
  }
15028
15022
 
15023
+ get #canOpenColorPicker() {
15024
+ return !this.hasAttribute("data-no-color-picker");
15025
+ }
15026
+
15029
15027
  get #dragEnabled() {
15030
15028
  const v = this.getAttribute("drag");
15031
15029
  return v !== null && v !== "false";
@@ -15078,14 +15076,8 @@ class FigHandle extends HTMLElement {
15078
15076
  get value() {
15079
15077
  const container = this.#getContainer();
15080
15078
  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)}%`;
15079
+ const { px, py } = this.#positionDetail(container.getBoundingClientRect());
15080
+ return `${Math.round(px * 100)}% ${Math.round(py * 100)}%`;
15089
15081
  }
15090
15082
 
15091
15083
  set value(v) {
@@ -15124,26 +15116,32 @@ class FigHandle extends HTMLElement {
15124
15116
  const hw = this.offsetWidth / 2;
15125
15117
  const hh = this.offsetHeight / 2;
15126
15118
 
15127
- const resolve = (token, containerDim, halfHandle) => {
15119
+ const resolvePx = (token, containerDim, halfHandle) => {
15128
15120
  if (token && typeof token === "object" && "px" in token) {
15129
15121
  return Math.max(
15130
15122
  -halfHandle,
15131
15123
  Math.min(containerDim - halfHandle, token.px - halfHandle),
15132
15124
  );
15133
15125
  }
15126
+ return null;
15127
+ };
15128
+
15129
+ const resolveResponsive = (token, halfHandle) => {
15134
15130
  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
- );
15131
+ return `calc(${pct}% - ${halfHandle}px)`;
15140
15132
  };
15141
15133
 
15142
15134
  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`;
15135
+ if (axes.x) {
15136
+ const xPx = resolvePx(xToken, rect.width, hw);
15137
+ this.style.left =
15138
+ xPx === null ? resolveResponsive(xToken, hw) : `${Math.round(xPx)}px`;
15139
+ }
15140
+ if (axes.y) {
15141
+ const yPx = resolvePx(yToken, rect.height, hh);
15142
+ this.style.top =
15143
+ yPx === null ? resolveResponsive(yToken, hh) : `${Math.round(yPx)}px`;
15144
+ }
15147
15145
  }
15148
15146
 
15149
15147
  #syncValueAttribute() {
@@ -15254,7 +15252,12 @@ class FigHandle extends HTMLElement {
15254
15252
  select() {
15255
15253
  if (this.hasAttribute("disabled")) return;
15256
15254
  this.setAttribute("selected", "");
15257
- if (this.getAttribute("type") === "color" && !this.#isDragging && this.#usesColorTip)
15255
+ if (
15256
+ this.getAttribute("type") === "color" &&
15257
+ this.#canOpenColorPicker &&
15258
+ !this.#isDragging &&
15259
+ this.#usesColorTip
15260
+ )
15258
15261
  this.#showColorTip();
15259
15262
  }
15260
15263
 
@@ -15269,7 +15272,11 @@ class FigHandle extends HTMLElement {
15269
15272
  this.#didDrag = false;
15270
15273
  return;
15271
15274
  }
15272
- if (this.getAttribute("type") === "color" && !this.#usesColorTip) {
15275
+ if (
15276
+ this.getAttribute("type") === "color" &&
15277
+ this.#canOpenColorPicker &&
15278
+ !this.#usesColorTip
15279
+ ) {
15273
15280
  this.#openDirectColorPicker();
15274
15281
  return;
15275
15282
  }
@@ -15287,6 +15294,7 @@ class FigHandle extends HTMLElement {
15287
15294
  if (e.key !== "Enter" && e.key !== " ") return;
15288
15295
  if (!this.hasAttribute("selected")) return;
15289
15296
  if (this.getAttribute("type") !== "color") return;
15297
+ if (!this.#canOpenColorPicker) return;
15290
15298
  e.preventDefault();
15291
15299
  if (this.#usesColorTip) {
15292
15300
  if (!this.#colorTip) this.#showColorTip();
@@ -15372,8 +15380,9 @@ class FigHandle extends HTMLElement {
15372
15380
  const clampAndApply = (clientX, clientY, shiftKey = false) => {
15373
15381
  const rect = container.getBoundingClientRect();
15374
15382
  lastRect = rect;
15375
- const currentLeft = parseFloat(this.style.left) || 0;
15376
- const currentTop = parseFloat(this.style.top) || 0;
15383
+ const currentPosition = this.#positionDetail(rect);
15384
+ const currentLeft = currentPosition.x;
15385
+ const currentTop = currentPosition.y;
15377
15386
  const rawX = clientX - offsetX - rect.left - handleW / 2;
15378
15387
  const rawY = clientY - offsetY - rect.top - handleH / 2;
15379
15388
 
@@ -15421,6 +15430,7 @@ class FigHandle extends HTMLElement {
15421
15430
  const dx = e.clientX - startX;
15422
15431
  const dy = e.clientY - startY;
15423
15432
  if (dx * dx + dy * dy < DRAG_THRESHOLD * DRAG_THRESHOLD) return;
15433
+ this.#closeColorPickerForDrag();
15424
15434
  this.classList.add("dragging");
15425
15435
  this.style.cursor = "grabbing";
15426
15436
  if (!this.hasAttribute("selected")) this.select();
@@ -15447,6 +15457,7 @@ class FigHandle extends HTMLElement {
15447
15457
  if (this.#didDrag) {
15448
15458
  clampAndApply(e.clientX, e.clientY, e.shiftKey);
15449
15459
  this.#syncValueAttribute();
15460
+ this.#applyValue(this.getAttribute("value"));
15450
15461
  this.dispatchEvent(
15451
15462
  new CustomEvent("change", {
15452
15463
  bubbles: true,
@@ -15560,6 +15571,7 @@ class FigHandle extends HTMLElement {
15560
15571
  const picker = document.createElement("fig-fill-picker");
15561
15572
  picker.setAttribute("mode", "solid");
15562
15573
  picker.setAttribute("alpha", "true");
15574
+ picker.setAttribute("dialog-offset", "8 8");
15563
15575
  picker.setAttribute("value", this.#directColorPickerValue());
15564
15576
  picker.anchorElement = this;
15565
15577
 
@@ -15595,6 +15607,12 @@ class FigHandle extends HTMLElement {
15595
15607
  this.removeAttribute("selected");
15596
15608
  }
15597
15609
 
15610
+ #closeColorPickerForDrag() {
15611
+ if (this.getAttribute("type") !== "color") return;
15612
+ this.#hideColorTip();
15613
+ this.#directColorPicker?.close();
15614
+ }
15615
+
15598
15616
  #showColorTip() {
15599
15617
  if (this.#colorTip) return;
15600
15618
  const tip = document.createElement("fig-color-tip");
@@ -15710,12 +15728,17 @@ class FigHandle extends HTMLElement {
15710
15728
  };
15711
15729
 
15712
15730
  #positionDetail(containerRect) {
15731
+ const rect = containerRect || this.#getContainer()?.getBoundingClientRect();
15732
+ if (!rect) return { x: 0, y: 0, px: 0, py: 0 };
15733
+ const handleRect = this.getBoundingClientRect();
15713
15734
  const hw = this.offsetWidth / 2;
15714
15735
  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;
15736
+ const x = handleRect.left - rect.left;
15737
+ const y = handleRect.top - rect.top;
15738
+ const centerX = x + hw;
15739
+ const centerY = y + hh;
15740
+ const px = rect.width > 0 ? centerX / rect.width : 0;
15741
+ const py = rect.height > 0 ? centerY / rect.height : 0;
15719
15742
  return { x, y, px, py };
15720
15743
  }
15721
15744
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "4.15.4",
3
+ "version": "4.15.9",
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",