@rogieking/figui3 2.21.0 → 2.22.1

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.
Files changed (3) hide show
  1. package/fig.js +106 -80
  2. package/index.html +15 -2
  3. package/package.json +1 -1
package/fig.js CHANGED
@@ -1072,7 +1072,7 @@ class FigPopup extends HTMLDialogElement {
1072
1072
  constructor() {
1073
1073
  super();
1074
1074
  this.#boundReposition = this.#queueReposition.bind(this);
1075
- this.#boundScroll = this.#queueReposition.bind(this);
1075
+ this.#boundScroll = () => { if (this.open) this.#positionPopup(); };
1076
1076
  this.#boundOutsidePointerDown = this.#handleOutsidePointerDown.bind(this);
1077
1077
  this.#boundPointerDown = this.#handlePointerDown.bind(this);
1078
1078
  this.#boundPointerMove = this.#handlePointerMove.bind(this);
@@ -1177,8 +1177,6 @@ class FigPopup extends HTMLDialogElement {
1177
1177
  this.#setupDragListeners();
1178
1178
  } else {
1179
1179
  this.#removeDragListeners();
1180
- const header = this.querySelector("fig-header, header");
1181
- if (header) header.style.cursor = "";
1182
1180
  }
1183
1181
  return;
1184
1182
  }
@@ -1267,7 +1265,7 @@ class FigPopup extends HTMLDialogElement {
1267
1265
  }
1268
1266
 
1269
1267
  window.addEventListener("resize", this.#boundReposition);
1270
- window.addEventListener("scroll", this.#boundScroll, true);
1268
+ window.addEventListener("scroll", this.#boundScroll, { capture: true, passive: true });
1271
1269
  }
1272
1270
 
1273
1271
  #teardownObservers() {
@@ -1284,7 +1282,7 @@ class FigPopup extends HTMLDialogElement {
1284
1282
  this.#mutationObserver = null;
1285
1283
  }
1286
1284
  window.removeEventListener("resize", this.#boundReposition);
1287
- window.removeEventListener("scroll", this.#boundScroll, true);
1285
+ window.removeEventListener("scroll", this.#boundScroll, { capture: true, passive: true });
1288
1286
  }
1289
1287
 
1290
1288
  #handleOutsidePointerDown(event) {
@@ -1298,19 +1296,32 @@ class FigPopup extends HTMLDialogElement {
1298
1296
  const anchor = this.#resolveAnchor();
1299
1297
  if (anchor && anchor.contains(target)) return;
1300
1298
 
1299
+ if (this.#isInsideDescendantPopup(target)) return;
1300
+
1301
1301
  this.open = false;
1302
1302
  }
1303
1303
 
1304
+ #isInsideDescendantPopup(target) {
1305
+ const targetDialog = target.closest?.('dialog[is="fig-popup"]');
1306
+ if (!targetDialog || targetDialog === this) return false;
1307
+
1308
+ let current = targetDialog;
1309
+ const visited = new Set();
1310
+ while (current && !visited.has(current)) {
1311
+ visited.add(current);
1312
+ const popupAnchor = current.anchor;
1313
+ if (!(popupAnchor instanceof Element)) break;
1314
+ if (this.contains(popupAnchor)) return true;
1315
+ current = popupAnchor.closest?.('dialog[is="fig-popup"]');
1316
+ }
1317
+ return false;
1318
+ }
1319
+
1304
1320
  // ---- Drag support ----
1305
1321
 
1306
1322
  #setupDragListeners() {
1307
1323
  if (this.drag) {
1308
1324
  this.addEventListener("pointerdown", this.#boundPointerDown);
1309
- const handleSelector = this.getAttribute("handle");
1310
- const handleEl = handleSelector
1311
- ? this.querySelector(handleSelector)
1312
- : this.querySelector("fig-header, header");
1313
- if (handleEl) handleEl.style.cursor = "grab";
1314
1325
  }
1315
1326
  }
1316
1327
 
@@ -1527,53 +1538,67 @@ class FigPopup extends HTMLDialogElement {
1527
1538
  };
1528
1539
  }
1529
1540
 
1530
- #getOrder(preferred, axis) {
1531
- const verticalMap = {
1532
- top: ["top", "bottom", "center"],
1533
- center: ["center", "top", "bottom"],
1534
- bottom: ["bottom", "top", "center"],
1535
- };
1536
- const horizontalMap = {
1537
- left: ["left", "right", "center"],
1538
- center: ["center", "left", "right"],
1539
- right: ["right", "left", "center"],
1540
- };
1541
+ #getPlacementCandidates(vertical, horizontal, shorthand) {
1542
+ const opp = { top: "bottom", bottom: "top", left: "right", right: "left", center: "center" };
1543
+
1544
+ if (shorthand) {
1545
+ const isHorizontal = shorthand === "left" || shorthand === "right";
1546
+ const perp = isHorizontal ? ["top", "bottom"] : ["left", "right"];
1547
+ return [
1548
+ { v: vertical, h: horizontal, s: shorthand },
1549
+ { v: vertical, h: horizontal, s: opp[shorthand] },
1550
+ { v: vertical, h: horizontal, s: perp[0] },
1551
+ { v: vertical, h: horizontal, s: perp[1] },
1552
+ ];
1553
+ }
1541
1554
 
1542
- return axis === "vertical"
1543
- ? verticalMap[preferred] || verticalMap.top
1544
- : horizontalMap[preferred] || horizontalMap.center;
1555
+ if (vertical === "center") {
1556
+ return [
1557
+ { v: "center", h: horizontal, s: null },
1558
+ { v: "center", h: opp[horizontal], s: null },
1559
+ { v: "top", h: horizontal, s: null },
1560
+ { v: "bottom", h: horizontal, s: null },
1561
+ { v: "top", h: opp[horizontal], s: null },
1562
+ { v: "bottom", h: opp[horizontal], s: null },
1563
+ ];
1564
+ }
1565
+
1566
+ if (horizontal === "center") {
1567
+ return [
1568
+ { v: vertical, h: "center", s: null },
1569
+ { v: opp[vertical], h: "center", s: null },
1570
+ { v: vertical, h: "left", s: null },
1571
+ { v: vertical, h: "right", s: null },
1572
+ { v: opp[vertical], h: "left", s: null },
1573
+ { v: opp[vertical], h: "right", s: null },
1574
+ ];
1575
+ }
1576
+
1577
+ return [
1578
+ { v: vertical, h: horizontal, s: null },
1579
+ { v: opp[vertical], h: horizontal, s: null },
1580
+ { v: vertical, h: opp[horizontal], s: null },
1581
+ { v: opp[vertical], h: opp[horizontal], s: null },
1582
+ ];
1545
1583
  }
1546
1584
 
1547
1585
  #computeCoords(anchorRect, popupRect, vertical, horizontal, offset, shorthand) {
1548
1586
  let top;
1549
1587
  let left;
1550
1588
 
1551
- // Shorthand support:
1552
- // position="right" => top edges aligned, popup sits to the right of anchor.
1553
- // position="left" => top edges aligned, popup sits to the left of anchor.
1554
- if (shorthand === "right") {
1555
- return {
1556
- top: anchorRect.top,
1557
- left: anchorRect.right + offset.xPx,
1558
- };
1589
+ if (shorthand === "left" || shorthand === "right") {
1590
+ left = shorthand === "left"
1591
+ ? anchorRect.left - popupRect.width - offset.xPx
1592
+ : anchorRect.right + offset.xPx;
1593
+ top = anchorRect.top;
1594
+ return { top, left };
1559
1595
  }
1560
- if (shorthand === "left") {
1561
- return {
1562
- top: anchorRect.top,
1563
- left: anchorRect.left - popupRect.width - offset.xPx,
1564
- };
1565
- }
1566
- if (shorthand === "top") {
1567
- return {
1568
- top: anchorRect.top - popupRect.height - offset.yPx,
1569
- left: anchorRect.left,
1570
- };
1571
- }
1572
- if (shorthand === "bottom") {
1573
- return {
1574
- top: anchorRect.bottom + offset.yPx,
1575
- left: anchorRect.left,
1576
- };
1596
+ if (shorthand === "top" || shorthand === "bottom") {
1597
+ top = shorthand === "top"
1598
+ ? anchorRect.top - popupRect.height - offset.yPx
1599
+ : anchorRect.bottom + offset.yPx;
1600
+ left = anchorRect.left;
1601
+ return { top, left };
1577
1602
  }
1578
1603
 
1579
1604
  if (vertical === "top") {
@@ -1695,8 +1720,6 @@ class FigPopup extends HTMLDialogElement {
1695
1720
  const popupRect = this.getBoundingClientRect();
1696
1721
  const offset = this.#parseOffset();
1697
1722
  const { vertical, horizontal, shorthand } = this.#parsePosition();
1698
- const verticalOrder = this.#getOrder(vertical, "vertical");
1699
- const horizontalOrder = this.#getOrder(horizontal, "horizontal");
1700
1723
  const anchor = this.#resolveAnchor();
1701
1724
 
1702
1725
  if (!anchor) {
@@ -1712,31 +1735,38 @@ class FigPopup extends HTMLDialogElement {
1712
1735
  }
1713
1736
 
1714
1737
  const anchorRect = anchor.getBoundingClientRect();
1738
+ const candidates = this.#getPlacementCandidates(vertical, horizontal, shorthand);
1715
1739
  let best = null;
1716
1740
  let bestSide = "top";
1717
1741
  let bestScore = Number.POSITIVE_INFINITY;
1718
1742
 
1719
- for (const v of verticalOrder) {
1720
- for (const h of horizontalOrder) {
1721
- const coords = this.#computeCoords(
1722
- anchorRect,
1723
- popupRect,
1724
- v,
1725
- h,
1726
- offset,
1727
- shorthand
1728
- );
1729
- const placementSide = this.#getPlacementSide(v, h, shorthand);
1743
+ for (const { v, h, s } of candidates) {
1744
+ const coords = this.#computeCoords(anchorRect, popupRect, v, h, offset, s);
1745
+ const placementSide = this.#getPlacementSide(v, h, s);
1746
+
1747
+ if (s) {
1748
+ // Shorthand: clamp cross-axis to viewport, check primary axis fit
1749
+ const clamped = this.#clamp(coords, popupRect);
1750
+ const primaryFits = (s === "left" || s === "right")
1751
+ ? (coords.left >= this.#viewportPadding && coords.left + popupRect.width <= window.innerWidth - this.#viewportPadding)
1752
+ : (coords.top >= this.#viewportPadding && coords.top + popupRect.height <= window.innerHeight - this.#viewportPadding);
1753
+ if (primaryFits) {
1754
+ this.style.left = `${clamped.left}px`;
1755
+ this.style.top = `${clamped.top}px`;
1756
+ this.#updatePopoverBeak(anchorRect, popupRect, clamped.left, clamped.top, placementSide);
1757
+ return;
1758
+ }
1759
+ const score = this.#overflowScore(coords, popupRect);
1760
+ if (score < bestScore) {
1761
+ bestScore = score;
1762
+ best = clamped;
1763
+ bestSide = placementSide;
1764
+ }
1765
+ } else {
1730
1766
  if (this.#fits(coords, popupRect)) {
1731
1767
  this.style.left = `${coords.left}px`;
1732
1768
  this.style.top = `${coords.top}px`;
1733
- this.#updatePopoverBeak(
1734
- anchorRect,
1735
- popupRect,
1736
- coords.left,
1737
- coords.top,
1738
- placementSide
1739
- );
1769
+ this.#updatePopoverBeak(anchorRect, popupRect, coords.left, coords.top, placementSide);
1740
1770
  return;
1741
1771
  }
1742
1772
  const score = this.#overflowScore(coords, popupRect);
@@ -1751,13 +1781,7 @@ class FigPopup extends HTMLDialogElement {
1751
1781
  const clamped = this.#clamp(best || { left: 0, top: 0 }, popupRect);
1752
1782
  this.style.left = `${clamped.left}px`;
1753
1783
  this.style.top = `${clamped.top}px`;
1754
- this.#updatePopoverBeak(
1755
- anchorRect,
1756
- popupRect,
1757
- clamped.left,
1758
- clamped.top,
1759
- bestSide
1760
- );
1784
+ this.#updatePopoverBeak(anchorRect, popupRect, clamped.left, clamped.top, bestSide);
1761
1785
  }
1762
1786
 
1763
1787
  #queueReposition() {
@@ -3352,6 +3376,8 @@ class FigInputColor extends HTMLElement {
3352
3376
  const showAlpha = this.getAttribute("alpha") === "true";
3353
3377
  const experimental = this.getAttribute("experimental");
3354
3378
  const expAttr = experimental ? `experimental="${experimental}"` : "";
3379
+ const dialogPos = this.getAttribute("dialog-position") || "left";
3380
+ const dialogPosAttr = `dialog-position="${dialogPos}"`;
3355
3381
 
3356
3382
  let html = ``;
3357
3383
  if (this.getAttribute("text")) {
@@ -3376,7 +3402,7 @@ class FigInputColor extends HTMLElement {
3376
3402
  let swatchElement = "";
3377
3403
  if (!hidePicker) {
3378
3404
  swatchElement = useFigmaPicker
3379
- ? `<fig-fill-picker mode="solid" dialog-position="left" ${expAttr} ${
3405
+ ? `<fig-fill-picker mode="solid" ${dialogPosAttr} ${expAttr} ${
3380
3406
  showAlpha ? "" : 'alpha="false"'
3381
3407
  } value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
3382
3408
  this.alpha
@@ -3394,7 +3420,7 @@ class FigInputColor extends HTMLElement {
3394
3420
  html = ``;
3395
3421
  } else {
3396
3422
  html = useFigmaPicker
3397
- ? `<fig-fill-picker mode="solid" dialog-position="left" ${expAttr} ${
3423
+ ? `<fig-fill-picker mode="solid" ${dialogPosAttr} ${expAttr} ${
3398
3424
  showAlpha ? "" : 'alpha="false"'
3399
3425
  } value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
3400
3426
  this.alpha
@@ -3586,7 +3612,7 @@ class FigInputColor extends HTMLElement {
3586
3612
  }
3587
3613
 
3588
3614
  static get observedAttributes() {
3589
- return ["value", "style", "mode", "picker", "experimental"];
3615
+ return ["value", "style", "mode", "picker", "experimental", "dialog-position"];
3590
3616
  }
3591
3617
 
3592
3618
  get mode() {
@@ -6957,7 +6983,7 @@ class FigFillPicker extends HTMLElement {
6957
6983
  <fig-input-number class="fig-fill-picker-stop-position" min="0" max="100" value="${
6958
6984
  stop.position
6959
6985
  }" units="%"></fig-input-number>
6960
- <fig-input-color class="fig-fill-picker-stop-color" text="true" alpha="true" picker="figma" value="${
6986
+ <fig-input-color class="fig-fill-picker-stop-color" text="true" alpha="true" picker="figma" dialog-position="right" value="${
6961
6987
  stop.color
6962
6988
  }"></fig-input-color>
6963
6989
  <fig-button icon variant="ghost" class="fig-fill-picker-stop-remove" ${
package/index.html CHANGED
@@ -24,7 +24,9 @@
24
24
 
25
25
  nav {
26
26
  position: fixed;
27
- width: 240px;
27
+ width: 20vw;
28
+ min-width: 128px;
29
+ max-width: 320px;
28
30
  height: 100vh;
29
31
  overflow-y: auto;
30
32
  background: var(--figma-color-bg-secondary);
@@ -105,7 +107,7 @@
105
107
  }
106
108
 
107
109
  main {
108
- margin-left: 240px;
110
+ margin-left: clamp(128px, 20vw, 320px);
109
111
  padding: 24px 32px;
110
112
  max-width: 800px;
111
113
  min-height: 100vh;
@@ -1806,6 +1808,13 @@
1806
1808
  alpha="true"
1807
1809
  picker="figma"></fig-input-color>
1808
1810
 
1811
+ <h4>Figma Picker (Experimental Modern)</h4>
1812
+ <fig-input-color value="#E84393"
1813
+ text="true"
1814
+ alpha="true"
1815
+ picker="figma"
1816
+ experimental="modern"></fig-input-color>
1817
+
1809
1818
  <h4>No Picker (text input only)</h4>
1810
1819
  <fig-input-color value="#9747FF"
1811
1820
  text="true"
@@ -1995,6 +2004,10 @@
1995
2004
  <h3>Webcam Fill</h3>
1996
2005
  <fig-input-fill value='{"type":"webcam","webcam":{"opacity":1}}'></fig-input-fill>
1997
2006
 
2007
+ <h3>Experimental Modern</h3>
2008
+ <fig-input-fill experimental="modern"
2009
+ value='{"type":"solid","color":"#E84393","alpha":1}'></fig-input-fill>
2010
+
1998
2011
  <h3>Disabled</h3>
1999
2012
  <fig-input-fill disabled
2000
2013
  value='{"type":"solid","color":"#AA96DA","alpha":1}'></fig-input-fill>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "2.21.0",
3
+ "version": "2.22.1",
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",