@rogieking/figui3 2.19.1 → 2.21.0

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 (4) hide show
  1. package/components.css +34 -66
  2. package/fig.js +230 -117
  3. package/index.html +213 -6
  4. package/package.json +1 -1
package/components.css CHANGED
@@ -305,73 +305,37 @@
305
305
  --icon-rotate: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.2325 6.47442C11.2088 5.49811 12.7917 5.49811 13.768 6.47442L15.2931 7.99955H14.0002C13.7241 7.99955 13.5002 8.2234 13.5002 8.49955C13.5002 8.77569 13.7241 8.99955 14.0002 8.99955H16.5002C16.7764 8.99955 17.0002 8.77569 17.0002 8.49955V5.99955C17.0002 5.7234 16.7764 5.49955 16.5002 5.49955C16.2241 5.49955 16.0002 5.7234 16.0002 5.99955V7.29244L14.4751 5.76731C13.1083 4.40048 10.8922 4.40048 9.52537 5.76731L7.14669 8.14599C6.95143 8.34126 6.95143 8.65784 7.14669 8.8531C7.34195 9.04836 7.65854 9.04836 7.8538 8.8531L10.2325 6.47442ZM13.0609 9.64599C12.4751 9.06021 11.5254 9.06021 10.9396 9.64599L7.64669 12.9389C7.06091 13.5247 7.0609 14.4744 7.64669 15.0602L10.9396 18.3531C11.5254 18.9389 12.4751 18.9389 13.0609 18.3531L16.3538 15.0602C16.9396 14.4744 16.9396 13.5247 16.3538 12.9389L13.0609 9.64599ZM11.6467 10.3531C11.842 10.1578 12.1585 10.1578 12.3538 10.3531L15.6467 13.646C15.842 13.8413 15.842 14.1578 15.6467 14.3531L12.3538 17.646C12.1585 17.8413 11.842 17.8413 11.6467 17.646L8.3538 14.3531C8.15854 14.1578 8.15854 13.8413 8.3538 13.646L11.6467 10.3531Z' fill='currentColor'/%3E%3C/svg%3E");
306
306
  --icon-swap: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8.35355 6.35355C8.54882 6.15829 8.54882 5.84171 8.35355 5.64645C8.15829 5.45118 7.84171 5.45118 7.64645 5.64645L5.14645 8.14645C4.95118 8.34171 4.95118 8.65829 5.14645 8.85355L7.64645 11.3536C7.84171 11.5488 8.15829 11.5488 8.35355 11.3536C8.54882 11.1583 8.54882 10.8417 8.35355 10.6464L6.70711 9H18.5C18.7761 9 19 8.77614 19 8.5C19 8.22386 18.7761 8 18.5 8H6.70711L8.35355 6.35355ZM15.6464 13.3536C15.4512 13.1583 15.4512 12.8417 15.6464 12.6464C15.8417 12.4512 16.1583 12.4512 16.3536 12.6464L18.8536 15.1464C19.0488 15.3417 19.0488 15.6583 18.8536 15.8536L16.3536 18.3536C16.1583 18.5488 15.8417 18.5488 15.6464 18.3536C15.4512 18.1583 15.4512 17.8417 15.6464 17.6464L17.2929 16H5.5C5.22386 16 5 15.7761 5 15.5C5 15.2239 5.22386 15 5.5 15H17.2929L15.6464 13.3536Z' fill='currentColor'/%3E%3C/svg%3E");
307
307
 
308
- /* Elevations - light theme defaults */
309
- --figma-elevation-500-modal-window:
310
- 0 0 0.5px 0 rgba(0, 0, 0, 0.08), 0 10px 24px 0 rgba(0, 0, 0, 0.18),
311
- 0 2px 5px 0 rgba(0, 0, 0, 0.15);
312
-
308
+ /* Elevations light-dark() handles theme switching inline */
313
309
  --figma-elevation-100:
314
- 0px 0px 0.5px 0px rgba(0, 0, 0, 0.3), 0px 1px 3px 0px rgba(0, 0, 0, 0.15);
310
+ 0px 0px 0.5px 0px light-dark(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.5)),
311
+ inset 0px 0.75px 0px 0px light-dark(transparent, rgba(255, 255, 255, 0.1)),
312
+ 0px 1px 3px 0px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.4));
313
+
315
314
  --figma-elevation-200:
316
- 0 0 0.5px 0 rgba(0, 0, 0, 0.18), 0 3px 8px 0 rgba(0, 0, 0, 0.1),
317
- 0 1px 3px 0 rgba(0, 0, 0, 0.1);
315
+ 0px 0px 0.5px 0px light-dark(rgba(0, 0, 0, 0.18), transparent),
316
+ 0px 3px 8px 0px light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.35)),
317
+ 0px 1px 3px 0px light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.5)),
318
+ inset 0px 0.5px 0px 0px light-dark(transparent, rgba(255, 255, 255, 0.08)),
319
+ inset 0px 0px 0.5px 0px light-dark(transparent, rgba(255, 255, 255, 0.3));
320
+
318
321
  --figma-elevation-400-menu-panel:
319
- 0px 0px 0.5px 0px rgba(0, 0, 0, 0.12),
320
- 0px 10px 16px 0px rgba(0, 0, 0, 0.12), 0px 2px 5px 0px rgba(0, 0, 0, 0.15);
322
+ 0px 0px 0.5px 0px light-dark(rgba(0, 0, 0, 0.12), transparent),
323
+ 0px 10px 16px 0px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.35)),
324
+ 0px 2px 5px 0px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.35)),
325
+ inset 0px 0.5px 0px 0px light-dark(transparent, rgba(255, 255, 255, 0.08)),
326
+ inset 0px 0.75px 0px 0px light-dark(transparent, rgba(255, 255, 255, 0.075));
327
+
321
328
  --figma-elevation-500-modal-window:
322
- 0px 0px 0.5px 0px rgba(0, 0, 0, 0.08),
323
- 0px 10px 24px 0px rgba(0, 0, 0, 0.18), 0px 2px 5px 0px rgba(0, 0, 0, 0.15);
324
- --handle-shadow: 0px 0 0 0.5px rgba(0, 0, 0, 0.1), var(--figma-elevation-100);
325
- }
329
+ 0px 0px 0.5px 0px light-dark(rgba(0, 0, 0, 0.08), transparent),
330
+ 0px 10px 24px 0px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.45)),
331
+ 0px 2px 5px 0px light-dark(rgba(0, 0, 0, 0.15), transparent),
332
+ 0px 3px 5px 0px light-dark(transparent, rgba(0, 0, 0, 0.35)),
333
+ inset 0px 0.75px 0px 0px light-dark(transparent, rgba(255, 255, 255, 0.1));
326
334
 
327
- /* Dark theme overrides for non-color values (shadows, elevations) */
328
- /* light-dark() only works for colors, so these use class-based switching */
329
- /* The @media block is a no-JS fallback — ignored when setTheme() sets classes */
330
- @media (prefers-color-scheme: dark) {
331
- :root:not(.figma-dark):not(.figma-light) {
332
- --handle-shadow:
333
- 0px 0 0 0.75px rgba(0, 0, 0, 0.1),
334
- 0px 0px 0.5px 0px rgba(255, 255, 255, 0.1);
335
- --figma-elevation-100:
336
- 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
337
- 0px 0.75px 0px 0px rgba(255, 255, 255, 0.1) inset,
338
- 0px 1px 3px 0px rgba(0, 0, 0, 0.4);
339
- --figma-elevation-200:
340
- 0px 3px 8px rgba(0, 0, 0, 0.35), 0px 1px 3px rgba(0, 0, 0, 0.5),
341
- inset 0px 0.5px 0px rgba(255, 255, 255, 0.08),
342
- inset 0px 0px 0.5px rgba(255, 255, 255, 0.3);
343
- --figma-elevation-400-menu-panel:
344
- 0px 0.5px 0px 0px rgba(255, 255, 255, 0.08) inset,
345
- 0px 10px 16px 0px rgba(0, 0, 0, 0.35),
346
- inset 0px 0.75px 0px rgba(255, 255, 255, 0.075),
347
- 0px 2px 5px 0px rgba(0, 0, 0, 0.35);
348
- --figma-elevation-500-modal-window:
349
- 0px 10px 24px rgba(0, 0, 0, 0.45), 0px 3px 5px rgba(0, 0, 0, 0.35),
350
- inset 0px 0.75px 0px rgba(255, 255, 255, 0.1);
351
- }
352
- }
353
-
354
- /* Class-based dark theme override (primary mechanism via setTheme()) */
355
- :root.figma-dark {
356
335
  --handle-shadow:
357
- 0px 0 0 0.75px rgba(0, 0, 0, 0.1),
358
- 0px 0px 0.5px 0px rgba(255, 255, 255, 0.1);
359
- --figma-elevation-100:
360
- 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
361
- 0px 0.75px 0px 0px rgba(255, 255, 255, 0.1) inset,
362
- 0px 1px 3px 0px rgba(0, 0, 0, 0.4);
363
- --figma-elevation-200:
364
- 0px 3px 8px rgba(0, 0, 0, 0.35), 0px 1px 3px rgba(0, 0, 0, 0.5),
365
- inset 0px 0.5px 0px rgba(255, 255, 255, 0.08),
366
- inset 0px 0px 0.5px rgba(255, 255, 255, 0.3);
367
- --figma-elevation-400-menu-panel:
368
- 0px 0.5px 0px 0px rgba(255, 255, 255, 0.08) inset,
369
- 0px 10px 16px 0px rgba(0, 0, 0, 0.35),
370
- inset 0px 0.75px 0px rgba(255, 255, 255, 0.075),
371
- 0px 2px 5px 0px rgba(0, 0, 0, 0.35);
372
- --figma-elevation-500-modal-window:
373
- 0px 10px 24px rgba(0, 0, 0, 0.45), 0px 3px 5px rgba(0, 0, 0, 0.35),
374
- inset 0px 0.75px 0px rgba(255, 255, 255, 0.1);
336
+ 0px 0px 0px 0.5px rgba(0, 0, 0, 0.1),
337
+ 0px 0px 0.5px 0px light-dark(rgba(0, 0, 0, 0.3), rgba(255, 255, 255, 0.1)),
338
+ 0px 1px 3px 0px light-dark(rgba(0, 0, 0, 0.15), transparent);
375
339
  }
376
340
 
377
341
  button,
@@ -1448,7 +1412,8 @@ input[type="checkbox"]:not(.switch) {
1448
1412
  /* Light theme checkbox hover (black checkmark preview) */
1449
1413
  /* @media is a no-JS fallback — ignored when setTheme() sets classes */
1450
1414
  @media (prefers-color-scheme: light) {
1451
- :root:not(.figma-dark):not(.figma-light) input[type="checkbox"]:not(.switch):not(:disabled):hover {
1415
+ :root:not(.figma-dark):not(.figma-light)
1416
+ input[type="checkbox"]:not(.switch):not(:disabled):hover {
1452
1417
  background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.50012 7.5L7.50012 10.5L11.5001 4.5' stroke='black' opacity='0.25' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.125' /%3E%3C/svg%3E%0A");
1453
1418
  }
1454
1419
  }
@@ -2066,12 +2031,15 @@ dialog[is="fig-popup"] {
2066
2031
  position: fixed;
2067
2032
  margin: 0;
2068
2033
  min-width: 0;
2069
- width: max-content;
2070
- max-width: calc(100vw - var(--spacer-4));
2071
- max-height: calc(100vh - var(--spacer-4));
2072
- padding: var(--spacer-2);
2034
+ padding: 0;
2073
2035
  overflow: auto;
2074
2036
 
2037
+ &[autoresize]:not([autoresize="false"]) {
2038
+ width: max-content;
2039
+ max-width: calc(100vw - var(--spacer-4));
2040
+ max-height: calc(100vh - var(--spacer-4));
2041
+ }
2042
+
2075
2043
  &[open] {
2076
2044
  display: block;
2077
2045
  }
package/fig.js CHANGED
@@ -863,13 +863,12 @@ class FigDialog extends HTMLDialogElement {
863
863
  #setupDragListeners() {
864
864
  if (this.drag) {
865
865
  this.addEventListener("pointerdown", this.#boundPointerDown);
866
- // Set move cursor on handle element (or fig-header by default)
867
866
  const handleSelector = this.getAttribute("handle");
868
867
  const handleEl = handleSelector
869
868
  ? this.querySelector(handleSelector)
870
869
  : this.querySelector("fig-header, header");
871
870
  if (handleEl) {
872
- handleEl.style.cursor = "move";
871
+ handleEl.style.cursor = "grab";
873
872
  }
874
873
  }
875
874
  }
@@ -977,16 +976,12 @@ class FigDialog extends HTMLDialogElement {
977
976
  const dy = Math.abs(e.clientY - this.#dragStartPos.y);
978
977
 
979
978
  if (dx > this.#dragThreshold || dy > this.#dragThreshold) {
980
- // Start actual drag
981
979
  this.#isDragging = true;
982
980
  this.#dragPending = false;
983
981
  this.setPointerCapture(e.pointerId);
982
+ this.style.cursor = "grabbing";
984
983
 
985
- // Get current position from computed style
986
984
  const rect = this.getBoundingClientRect();
987
-
988
- // Convert to pixel-based top/left positioning for dragging
989
- // (clears margin: auto centering)
990
985
  this.style.top = `${rect.top}px`;
991
986
  this.style.left = `${rect.left}px`;
992
987
  this.style.bottom = "auto";
@@ -997,21 +992,15 @@ class FigDialog extends HTMLDialogElement {
997
992
 
998
993
  if (!this.#isDragging) return;
999
994
 
1000
- // Calculate new position based on pointer position minus offset
1001
- const newLeft = e.clientX - this.#dragOffset.x;
1002
- const newTop = e.clientY - this.#dragOffset.y;
1003
-
1004
- // Apply position directly with pixels
1005
- this.style.left = `${newLeft}px`;
1006
- this.style.top = `${newTop}px`;
1007
-
995
+ this.style.left = `${e.clientX - this.#dragOffset.x}px`;
996
+ this.style.top = `${e.clientY - this.#dragOffset.y}px`;
1008
997
  e.preventDefault();
1009
998
  }
1010
999
 
1011
1000
  #handlePointerUp(e) {
1012
- // Clean up pending or active drag
1013
1001
  if (this.#isDragging) {
1014
1002
  this.releasePointerCapture(e.pointerId);
1003
+ this.style.cursor = "";
1015
1004
  }
1016
1005
 
1017
1006
  this.#isDragging = false;
@@ -1035,7 +1024,6 @@ class FigDialog extends HTMLDialogElement {
1035
1024
  this.#setupDragListeners();
1036
1025
  } else {
1037
1026
  this.#removeDragListeners();
1038
- // Remove move cursor from header
1039
1027
  const header = this.querySelector("fig-header, header");
1040
1028
  if (header) {
1041
1029
  header.style.cursor = "";
@@ -1070,16 +1058,29 @@ class FigPopup extends HTMLDialogElement {
1070
1058
  #boundScroll;
1071
1059
  #boundOutsidePointerDown;
1072
1060
  #rafId = null;
1061
+ #anchorRef = null;
1062
+
1063
+ #isDragging = false;
1064
+ #dragPending = false;
1065
+ #dragStartPos = { x: 0, y: 0 };
1066
+ #dragOffset = { x: 0, y: 0 };
1067
+ #dragThreshold = 3;
1068
+ #boundPointerDown;
1069
+ #boundPointerMove;
1070
+ #boundPointerUp;
1073
1071
 
1074
1072
  constructor() {
1075
1073
  super();
1076
1074
  this.#boundReposition = this.#queueReposition.bind(this);
1077
1075
  this.#boundScroll = this.#queueReposition.bind(this);
1078
1076
  this.#boundOutsidePointerDown = this.#handleOutsidePointerDown.bind(this);
1077
+ this.#boundPointerDown = this.#handlePointerDown.bind(this);
1078
+ this.#boundPointerMove = this.#handlePointerMove.bind(this);
1079
+ this.#boundPointerUp = this.#handlePointerUp.bind(this);
1079
1080
  }
1080
1081
 
1081
1082
  static get observedAttributes() {
1082
- return ["open", "anchor", "position", "offset", "variant", "theme"];
1083
+ return ["open", "anchor", "position", "offset", "variant", "theme", "drag", "handle", "autoresize"];
1083
1084
  }
1084
1085
 
1085
1086
  get open() {
@@ -1096,6 +1097,22 @@ class FigPopup extends HTMLDialogElement {
1096
1097
  this.setAttribute("open", "true");
1097
1098
  }
1098
1099
 
1100
+ get anchor() {
1101
+ return this.#anchorRef ?? this.getAttribute("anchor");
1102
+ }
1103
+
1104
+ set anchor(value) {
1105
+ if (value instanceof Element) {
1106
+ this.#anchorRef = value;
1107
+ } else if (typeof value === "string") {
1108
+ this.#anchorRef = null;
1109
+ this.setAttribute("anchor", value);
1110
+ } else {
1111
+ this.#anchorRef = null;
1112
+ }
1113
+ if (this.open) this.#queueReposition();
1114
+ }
1115
+
1099
1116
  connectedCallback() {
1100
1117
  if (!this.hasAttribute("position")) {
1101
1118
  this.setAttribute("position", "top center");
@@ -1103,11 +1120,13 @@ class FigPopup extends HTMLDialogElement {
1103
1120
  if (!this.hasAttribute("role")) {
1104
1121
  this.setAttribute("role", "dialog");
1105
1122
  }
1106
- // Default dialog outside-close behavior.
1107
1123
  if (!this.hasAttribute("closedby")) {
1108
1124
  this.setAttribute("closedby", "any");
1109
1125
  }
1110
1126
 
1127
+ this.drag =
1128
+ this.hasAttribute("drag") && this.getAttribute("drag") !== "false";
1129
+
1111
1130
  this.addEventListener("close", () => {
1112
1131
  this.#teardownObservers();
1113
1132
  if (this.hasAttribute("open")) {
@@ -1115,6 +1134,10 @@ class FigPopup extends HTMLDialogElement {
1115
1134
  }
1116
1135
  });
1117
1136
 
1137
+ requestAnimationFrame(() => {
1138
+ this.#setupDragListeners();
1139
+ });
1140
+
1118
1141
  if (this.open) {
1119
1142
  this.#showPopup();
1120
1143
  } else {
@@ -1124,6 +1147,7 @@ class FigPopup extends HTMLDialogElement {
1124
1147
 
1125
1148
  disconnectedCallback() {
1126
1149
  this.#teardownObservers();
1150
+ this.#removeDragListeners();
1127
1151
  document.removeEventListener(
1128
1152
  "pointerdown",
1129
1153
  this.#boundOutsidePointerDown,
@@ -1147,6 +1171,18 @@ class FigPopup extends HTMLDialogElement {
1147
1171
  return;
1148
1172
  }
1149
1173
 
1174
+ if (name === "drag") {
1175
+ this.drag = newValue !== null && newValue !== "false";
1176
+ if (this.drag) {
1177
+ this.#setupDragListeners();
1178
+ } else {
1179
+ this.#removeDragListeners();
1180
+ const header = this.querySelector("fig-header, header");
1181
+ if (header) header.style.cursor = "";
1182
+ }
1183
+ return;
1184
+ }
1185
+
1150
1186
  if (this.open) {
1151
1187
  this.#queueReposition();
1152
1188
  this.#setupObservers();
@@ -1176,9 +1212,15 @@ class FigPopup extends HTMLDialogElement {
1176
1212
  document.addEventListener("pointerdown", this.#boundOutsidePointerDown, true);
1177
1213
  this.#queueReposition();
1178
1214
  this.#isPopupActive = true;
1215
+
1216
+ const anchor = this.#resolveAnchor();
1217
+ if (anchor) anchor.classList.add("has-popup-open");
1179
1218
  }
1180
1219
 
1181
1220
  #hidePopup() {
1221
+ const anchor = this.#resolveAnchor();
1222
+ if (anchor) anchor.classList.remove("has-popup-open");
1223
+
1182
1224
  this.#isPopupActive = false;
1183
1225
  this.#teardownObservers();
1184
1226
  document.removeEventListener(
@@ -1196,6 +1238,11 @@ class FigPopup extends HTMLDialogElement {
1196
1238
  }
1197
1239
  }
1198
1240
 
1241
+ get autoresize() {
1242
+ const val = this.getAttribute("autoresize");
1243
+ return val === null || val !== "false";
1244
+ }
1245
+
1199
1246
  #setupObservers() {
1200
1247
  this.#teardownObservers();
1201
1248
 
@@ -1205,17 +1252,19 @@ class FigPopup extends HTMLDialogElement {
1205
1252
  this.#anchorObserver.observe(anchor);
1206
1253
  }
1207
1254
 
1208
- if ("ResizeObserver" in window) {
1209
- this.#contentObserver = new ResizeObserver(this.#boundReposition);
1210
- this.#contentObserver.observe(this);
1211
- }
1255
+ if (this.autoresize) {
1256
+ if ("ResizeObserver" in window) {
1257
+ this.#contentObserver = new ResizeObserver(this.#boundReposition);
1258
+ this.#contentObserver.observe(this);
1259
+ }
1212
1260
 
1213
- this.#mutationObserver = new MutationObserver(this.#boundReposition);
1214
- this.#mutationObserver.observe(this, {
1215
- childList: true,
1216
- subtree: true,
1217
- characterData: true,
1218
- });
1261
+ this.#mutationObserver = new MutationObserver(this.#boundReposition);
1262
+ this.#mutationObserver.observe(this, {
1263
+ childList: true,
1264
+ subtree: true,
1265
+ characterData: true,
1266
+ });
1267
+ }
1219
1268
 
1220
1269
  window.addEventListener("resize", this.#boundReposition);
1221
1270
  window.addEventListener("scroll", this.#boundScroll, true);
@@ -1240,15 +1289,134 @@ class FigPopup extends HTMLDialogElement {
1240
1289
 
1241
1290
  #handleOutsidePointerDown(event) {
1242
1291
  if (!this.open || !super.open) return;
1292
+ const closedby = this.getAttribute("closedby");
1293
+ if (closedby === "none" || closedby === "closerequest") return;
1243
1294
  const target = event.target;
1244
1295
  if (!(target instanceof Node)) return;
1245
1296
  if (this.contains(target)) return;
1246
1297
 
1247
- // Fallback for browsers that do not honor dialog closedby consistently.
1298
+ const anchor = this.#resolveAnchor();
1299
+ if (anchor && anchor.contains(target)) return;
1300
+
1248
1301
  this.open = false;
1249
1302
  }
1250
1303
 
1304
+ // ---- Drag support ----
1305
+
1306
+ #setupDragListeners() {
1307
+ if (this.drag) {
1308
+ 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
+ }
1315
+ }
1316
+
1317
+ #removeDragListeners() {
1318
+ this.removeEventListener("pointerdown", this.#boundPointerDown);
1319
+ document.removeEventListener("pointermove", this.#boundPointerMove);
1320
+ document.removeEventListener("pointerup", this.#boundPointerUp);
1321
+ }
1322
+
1323
+ #isInteractiveElement(element) {
1324
+ const interactiveSelectors = [
1325
+ "input", "button", "select", "textarea", "a",
1326
+ "label", "details", "summary",
1327
+ '[contenteditable="true"]', "[tabindex]",
1328
+ ];
1329
+
1330
+ const nonInteractiveFigElements = [
1331
+ "FIG-HEADER", "FIG-DIALOG", "FIG-POPUP", "FIG-FIELD",
1332
+ "FIG-TOOLTIP", "FIG-CONTENT", "FIG-TABS", "FIG-TAB",
1333
+ "FIG-POPOVER", "FIG-SHIMMER", "FIG-LAYER", "FIG-FILL-PICKER",
1334
+ ];
1335
+
1336
+ const isInteractive = (el) =>
1337
+ interactiveSelectors.some((s) => el.matches?.(s)) ||
1338
+ (el.tagName?.startsWith("FIG-") &&
1339
+ !nonInteractiveFigElements.includes(el.tagName));
1340
+
1341
+ if (isInteractive(element)) return true;
1342
+
1343
+ let parent = element.parentElement;
1344
+ while (parent && parent !== this) {
1345
+ if (isInteractive(parent)) return true;
1346
+ parent = parent.parentElement;
1347
+ }
1348
+
1349
+ return false;
1350
+ }
1351
+
1352
+ #handlePointerDown(e) {
1353
+ if (!this.drag) return;
1354
+ if (this.#isInteractiveElement(e.target)) return;
1355
+
1356
+ const handleSelector = this.getAttribute("handle");
1357
+ if (handleSelector && handleSelector.trim()) {
1358
+ const handleEl = this.querySelector(handleSelector);
1359
+ if (!handleEl || !handleEl.contains(e.target)) return;
1360
+ }
1361
+
1362
+ this.#dragPending = true;
1363
+ this.#dragStartPos.x = e.clientX;
1364
+ this.#dragStartPos.y = e.clientY;
1365
+
1366
+ const rect = this.getBoundingClientRect();
1367
+ this.#dragOffset.x = e.clientX - rect.left;
1368
+ this.#dragOffset.y = e.clientY - rect.top;
1369
+
1370
+ document.addEventListener("pointermove", this.#boundPointerMove);
1371
+ document.addEventListener("pointerup", this.#boundPointerUp);
1372
+ }
1373
+
1374
+ #handlePointerMove(e) {
1375
+ if (this.#dragPending && !this.#isDragging) {
1376
+ const dx = Math.abs(e.clientX - this.#dragStartPos.x);
1377
+ const dy = Math.abs(e.clientY - this.#dragStartPos.y);
1378
+
1379
+ if (dx > this.#dragThreshold || dy > this.#dragThreshold) {
1380
+ this.#isDragging = true;
1381
+ this.#dragPending = false;
1382
+ this.setPointerCapture(e.pointerId);
1383
+ this.style.cursor = "grabbing";
1384
+
1385
+ const rect = this.getBoundingClientRect();
1386
+ this.style.top = `${rect.top}px`;
1387
+ this.style.left = `${rect.left}px`;
1388
+ this.style.bottom = "auto";
1389
+ this.style.right = "auto";
1390
+ this.style.margin = "0";
1391
+ }
1392
+ }
1393
+
1394
+ if (!this.#isDragging) return;
1395
+
1396
+ this.style.left = `${e.clientX - this.#dragOffset.x}px`;
1397
+ this.style.top = `${e.clientY - this.#dragOffset.y}px`;
1398
+ e.preventDefault();
1399
+ }
1400
+
1401
+ #handlePointerUp(e) {
1402
+ if (this.#isDragging) {
1403
+ this.releasePointerCapture(e.pointerId);
1404
+ this.style.cursor = "";
1405
+ }
1406
+
1407
+ this.#isDragging = false;
1408
+ this.#dragPending = false;
1409
+
1410
+ document.removeEventListener("pointermove", this.#boundPointerMove);
1411
+ document.removeEventListener("pointerup", this.#boundPointerUp);
1412
+ e.preventDefault();
1413
+ }
1414
+
1415
+ // ---- Anchor resolution ----
1416
+
1251
1417
  #resolveAnchor() {
1418
+ if (this.#anchorRef) return this.#anchorRef;
1419
+
1252
1420
  const selector = this.getAttribute("anchor");
1253
1421
  if (!selector) return null;
1254
1422
 
@@ -1416,12 +1584,24 @@ class FigPopup extends HTMLDialogElement {
1416
1584
  top = anchorRect.top + (anchorRect.height - popupRect.height) / 2;
1417
1585
  }
1418
1586
 
1419
- if (horizontal === "left") {
1420
- left = anchorRect.left - popupRect.width - offset.xPx;
1421
- } else if (horizontal === "right") {
1422
- left = anchorRect.right + offset.xPx;
1587
+ if (vertical === "center") {
1588
+ // Side placement: popup beside the anchor
1589
+ if (horizontal === "left") {
1590
+ left = anchorRect.left - popupRect.width - offset.xPx;
1591
+ } else if (horizontal === "right") {
1592
+ left = anchorRect.right + offset.xPx;
1593
+ } else {
1594
+ left = anchorRect.left + (anchorRect.width - popupRect.width) / 2;
1595
+ }
1423
1596
  } else {
1424
- left = anchorRect.left + (anchorRect.width - popupRect.width) / 2;
1597
+ // Edge alignment: popup above/below, aligned to anchor edge
1598
+ if (horizontal === "left") {
1599
+ left = anchorRect.left + offset.xPx;
1600
+ } else if (horizontal === "right") {
1601
+ left = anchorRect.right - popupRect.width - offset.xPx;
1602
+ } else {
1603
+ left = anchorRect.left + (anchorRect.width - popupRect.width) / 2;
1604
+ }
1425
1605
  }
1426
1606
 
1427
1607
  return { top, left };
@@ -3196,7 +3376,7 @@ class FigInputColor extends HTMLElement {
3196
3376
  let swatchElement = "";
3197
3377
  if (!hidePicker) {
3198
3378
  swatchElement = useFigmaPicker
3199
- ? `<fig-fill-picker mode="solid" ${expAttr} ${
3379
+ ? `<fig-fill-picker mode="solid" dialog-position="left" ${expAttr} ${
3200
3380
  showAlpha ? "" : 'alpha="false"'
3201
3381
  } value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
3202
3382
  this.alpha
@@ -3214,7 +3394,7 @@ class FigInputColor extends HTMLElement {
3214
3394
  html = ``;
3215
3395
  } else {
3216
3396
  html = useFigmaPicker
3217
- ? `<fig-fill-picker mode="solid" ${expAttr} ${
3397
+ ? `<fig-fill-picker mode="solid" dialog-position="left" ${expAttr} ${
3218
3398
  showAlpha ? "" : 'alpha="false"'
3219
3399
  } value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
3220
3400
  this.alpha
@@ -3742,7 +3922,7 @@ class FigInputFill extends HTMLElement {
3742
3922
  const experimentalAttr = this.getAttribute("experimental");
3743
3923
  this.innerHTML = `
3744
3924
  <div class="input-combo">
3745
- <fig-fill-picker value='${fillPickerValue}' ${
3925
+ <fig-fill-picker dialog-position="left" value='${fillPickerValue}' ${
3746
3926
  disabled ? "disabled" : ""
3747
3927
  } ${modeAttr ? `mode="${modeAttr}"` : ""} ${experimentalAttr ? `experimental="${experimentalAttr}"` : ""}></fig-fill-picker>
3748
3928
  ${controlsHtml}
@@ -6006,7 +6186,7 @@ customElements.define("fig-layer", FigLayer);
6006
6186
  * @attr {string} value - JSON-encoded fill value
6007
6187
  * @attr {boolean} disabled - Whether the picker is disabled
6008
6188
  * @attr {boolean} alpha - Whether to show alpha/opacity controls (default: true)
6009
- * @attr {string} dialog-position - Position of the dialog (passed to fig-dialog)
6189
+ * @attr {string} dialog-position - Position of the popup (default: "left")
6010
6190
  */
6011
6191
  class FigFillPicker extends HTMLElement {
6012
6192
  #trigger = null;
@@ -6212,21 +6392,10 @@ class FigFillPicker extends HTMLElement {
6212
6392
  this.#createDialog();
6213
6393
  }
6214
6394
 
6215
- // Position off-screen first to prevent scroll jump
6216
- this.#dialog.style.position = "fixed";
6217
- this.#dialog.style.top = "-9999px";
6218
- this.#dialog.style.left = "-9999px";
6219
-
6220
- this.#dialog.show();
6221
6395
  this.#switchTab(this.#fillType);
6396
+ this.#dialog.open = true;
6222
6397
 
6223
- // Position after dialog has rendered and has dimensions
6224
- // Use nested RAF to ensure canvas is fully ready for drawing
6225
6398
  requestAnimationFrame(() => {
6226
- this.#positionDialog();
6227
- this.#dialog.setAttribute("closedby", "any");
6228
-
6229
- // Second RAF ensures the dialog is visible and canvas is ready
6230
6399
  requestAnimationFrame(() => {
6231
6400
  this.#drawColorArea();
6232
6401
  this.#updateHandlePosition();
@@ -6234,72 +6403,17 @@ class FigFillPicker extends HTMLElement {
6234
6403
  });
6235
6404
  }
6236
6405
 
6237
- #positionDialog() {
6238
- const triggerRect = this.#trigger.getBoundingClientRect();
6239
- const dialogRect = this.#dialog.getBoundingClientRect();
6240
- const padding = 8; // Gap between trigger and dialog
6241
- const viewportPadding = 16; // Min distance from viewport edges
6242
-
6243
- // Calculate available space in each direction
6244
- const spaceBelow =
6245
- window.innerHeight - triggerRect.bottom - viewportPadding;
6246
- const spaceAbove = triggerRect.top - viewportPadding;
6247
- const spaceRight = window.innerWidth - triggerRect.left - viewportPadding;
6248
- const spaceLeft = triggerRect.right - viewportPadding;
6249
-
6250
- let top, left;
6251
-
6252
- // Vertical positioning: prefer below, fallback to above
6253
- if (spaceBelow >= dialogRect.height || spaceBelow >= spaceAbove) {
6254
- // Position below trigger
6255
- top = triggerRect.bottom + padding;
6256
- } else {
6257
- // Position above trigger
6258
- top = triggerRect.top - dialogRect.height - padding;
6259
- }
6260
-
6261
- // Horizontal positioning: align left edge with trigger, adjust if needed
6262
- left = triggerRect.left;
6263
-
6264
- // Adjust if dialog would go off right edge
6265
- if (left + dialogRect.width > window.innerWidth - viewportPadding) {
6266
- left = window.innerWidth - dialogRect.width - viewportPadding;
6267
- }
6268
-
6269
- // Adjust if dialog would go off left edge
6270
- if (left < viewportPadding) {
6271
- left = viewportPadding;
6272
- }
6273
-
6274
- // Clamp vertical position to viewport
6275
- if (top < viewportPadding) {
6276
- top = viewportPadding;
6277
- }
6278
- if (top + dialogRect.height > window.innerHeight - viewportPadding) {
6279
- top = window.innerHeight - dialogRect.height - viewportPadding;
6280
- }
6281
-
6282
- // Apply position (override fig-dialog's default positioning)
6283
- this.#dialog.style.position = "fixed";
6284
- this.#dialog.style.top = `${top}px`;
6285
- this.#dialog.style.left = `${left}px`;
6286
- this.#dialog.style.bottom = "auto";
6287
- this.#dialog.style.right = "auto";
6288
- this.#dialog.style.margin = "0";
6289
- }
6290
-
6291
6406
  #createDialog() {
6292
- this.#dialog = document.createElement("dialog", { is: "fig-dialog" });
6293
- this.#dialog.setAttribute("is", "fig-dialog");
6407
+ this.#dialog = document.createElement("dialog", { is: "fig-popup" });
6408
+ this.#dialog.setAttribute("is", "fig-popup");
6294
6409
  this.#dialog.setAttribute("drag", "true");
6295
6410
  this.#dialog.setAttribute("handle", "fig-header");
6411
+ this.#dialog.setAttribute("autoresize", "false");
6296
6412
  this.#dialog.classList.add("fig-fill-picker-dialog");
6297
6413
 
6298
- // Forward dialog attributes
6299
- const dialogPosition = this.getAttribute("dialog-position");
6300
- if (dialogPosition) {
6301
- this.#dialog.setAttribute("position", dialogPosition);
6302
- }
6414
+ this.#dialog.anchor = this.#trigger;
6415
+ const dialogPosition = this.getAttribute("dialog-position") || "left";
6416
+ this.#dialog.setAttribute("position", dialogPosition);
6303
6417
 
6304
6418
  // Check for allowed modes (supports comma-separated values like "solid,gradient")
6305
6419
  const mode = this.getAttribute("mode");
@@ -6332,7 +6446,7 @@ class FigFillPicker extends HTMLElement {
6332
6446
 
6333
6447
  let headerContent;
6334
6448
  if (allowedModes.length === 1) {
6335
- headerContent = `<span class="fig-fill-picker-type-label">${modeLabels[allowedModes[0]]}</span>`;
6449
+ headerContent = `<h3 class="fig-fill-picker-type-label">${modeLabels[allowedModes[0]]}</h3>`;
6336
6450
  } else {
6337
6451
  const options = allowedModes
6338
6452
  .map((m) => `<option value="${m}">${modeLabels[m]}</option>`)
@@ -6345,7 +6459,7 @@ class FigFillPicker extends HTMLElement {
6345
6459
  this.#dialog.innerHTML = `
6346
6460
  <fig-header>
6347
6461
  ${headerContent}
6348
- <fig-button icon variant="ghost" close-dialog>
6462
+ <fig-button icon variant="ghost" class="fig-fill-picker-close">
6349
6463
  <span class="fig-mask-icon" style="--icon: var(--icon-close)"></span>
6350
6464
  </fig-button>
6351
6465
  </fig-header>
@@ -6368,11 +6482,10 @@ class FigFillPicker extends HTMLElement {
6368
6482
  });
6369
6483
  }
6370
6484
 
6371
- // Close button
6372
6485
  this.#dialog
6373
- .querySelector("fig-button[close-dialog]")
6486
+ .querySelector(".fig-fill-picker-close")
6374
6487
  .addEventListener("click", () => {
6375
- this.#dialog.close();
6488
+ this.#dialog.open = false;
6376
6489
  });
6377
6490
 
6378
6491
  // Emit change on close
package/index.html CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  nav {
26
26
  position: fixed;
27
- width: 180px;
27
+ width: 240px;
28
28
  height: 100vh;
29
29
  overflow-y: auto;
30
30
  background: var(--figma-color-bg-secondary);
@@ -105,7 +105,7 @@
105
105
  }
106
106
 
107
107
  main {
108
- margin-left: 180px;
108
+ margin-left: 240px;
109
109
  padding: 24px 32px;
110
110
  max-width: 800px;
111
111
  min-height: 100vh;
@@ -158,6 +158,36 @@
158
158
  border-radius: 4px;
159
159
  color: var(--figma-color-text);
160
160
  }
161
+
162
+ .toolbelt {
163
+ position: fixed;
164
+ bottom: 0.75rem;
165
+ left: 0;
166
+ right: 0;
167
+ width: fit-content;
168
+ margin: 0 auto;
169
+ display: flex;
170
+ align-items: center;
171
+ gap: var(--spacer-2);
172
+ padding: var(--spacer-2);
173
+ background-color: var(--figma-color-bg);
174
+ border-radius: var(--radius-large);
175
+ box-shadow: var(--figma-elevation-200);
176
+ z-index: 100;
177
+
178
+ &>section {
179
+ display: flex;
180
+ padding: 0;
181
+ align-items: center;
182
+ margin: 0;
183
+ gap: var(--spacer-2);
184
+
185
+ &:not(:first-child):not(:only-child) {
186
+ border-left: 1px solid var(--figma-color-border);
187
+ padding-left: var(--spacer-2);
188
+ }
189
+ }
190
+ }
161
191
  </style>
162
192
  </head>
163
193
 
@@ -296,7 +326,8 @@
296
326
  <label>Input (with icon)</label>
297
327
  <fig-button variant="input">
298
328
  Options
299
- <span class="fig-mask-icon" style="--icon: var(--icon-chevron); --size: 1rem"></span>
329
+ <span class="fig-mask-icon"
330
+ style="--icon: var(--icon-chevron); --size: 1rem"></span>
300
331
  </fig-button>
301
332
  </fig-field>
302
333
 
@@ -392,7 +423,8 @@
392
423
  <label>Icon (input)</label>
393
424
  <fig-button variant="input"
394
425
  icon="true">
395
- <span class="fig-mask-icon" style="--icon: var(--icon-chevron); --size: 1rem"></span>
426
+ <span class="fig-mask-icon"
427
+ style="--icon: var(--icon-chevron); --size: 1rem"></span>
396
428
  </fig-button>
397
429
  </fig-field>
398
430
  <fig-field direction="horizontal">
@@ -863,6 +895,115 @@
863
895
  <fig-button close-dialog>Close</fig-button>
864
896
  </footer>
865
897
  </dialog>
898
+
899
+ <h3>Non-Modal Dialog</h3>
900
+ <p style="font-size: 12px; color: var(--figma-color-text-secondary); margin-bottom: 12px;">
901
+ Use <code>.show()</code> instead of <code>.showModal()</code> to open a non-modal dialog.
902
+ It won't create a backdrop or trap focus, and the rest of the page remains interactive.
903
+ </p>
904
+ <pre><code>&lt;dialog id="my-dialog" is="fig-dialog" drag="true"&gt;
905
+ ...
906
+ &lt;/dialog&gt;
907
+
908
+ &lt;script&gt;
909
+ // Non-modal — no backdrop, page stays interactive
910
+ document.getElementById('my-dialog').show();
911
+
912
+ // Modal — backdrop + focus trap
913
+ document.getElementById('my-dialog').showModal();
914
+ &lt;/script&gt;</code></pre>
915
+ <fig-button onclick="document.getElementById('non-modal-dialog').show()">Open Non-Modal Dialog</fig-button>
916
+
917
+ <dialog id="non-modal-dialog"
918
+ is="fig-dialog"
919
+ drag="true"
920
+ position="center center">
921
+ <fig-header>
922
+ <h3>Non-Modal Dialog</h3>
923
+ <fig-tooltip text="Close">
924
+ <fig-button variant="ghost"
925
+ icon="true"
926
+ close-dialog>
927
+ <span class="fig-mask-icon"
928
+ style="--icon: var(--icon-close)"></span>
929
+ </fig-button>
930
+ </fig-tooltip>
931
+ </fig-header>
932
+ <fig-content>
933
+ <p>This dialog is non-modal — you can still interact with the page behind it.</p>
934
+ <fig-field direction="horizontal">
935
+ <label>Opacity</label>
936
+ <fig-slider value="75"
937
+ min="0"
938
+ max="100"></fig-slider>
939
+ </fig-field>
940
+ </fig-content>
941
+ <footer>
942
+ <fig-button close-dialog>Done</fig-button>
943
+ </footer>
944
+ </dialog>
945
+
946
+ <h3>closedby Attribute</h3>
947
+ <p style="font-size: 12px; color: var(--figma-color-text-secondary); margin-bottom: 12px;">
948
+ The <code>closedby</code> attribute controls how the dialog can be dismissed.
949
+ <code>fig-dialog</code> defaults to <code>closedby="any"</code> (light dismiss).
950
+ </p>
951
+ <pre><code>&lt;!-- Click outside, Esc key, or close button all dismiss --&gt;
952
+ &lt;dialog is="fig-dialog" closedby="any"&gt;...&lt;/dialog&gt;
953
+
954
+ &lt;!-- Only Esc key or close button dismiss (no click-outside) --&gt;
955
+ &lt;dialog is="fig-dialog" closedby="closerequest"&gt;...&lt;/dialog&gt;
956
+
957
+ &lt;!-- Only explicit close button dismisses --&gt;
958
+ &lt;dialog is="fig-dialog" closedby="none"&gt;...&lt;/dialog&gt;</code></pre>
959
+ <hstack>
960
+ <fig-button variant="secondary"
961
+ onclick="document.getElementById('closedby-any-dialog').show()">closedby="any"</fig-button>
962
+ <fig-button variant="secondary"
963
+ onclick="document.getElementById('closedby-none-dialog').show()">closedby="none"</fig-button>
964
+ </hstack>
965
+
966
+ <dialog id="closedby-any-dialog"
967
+ is="fig-dialog"
968
+ drag="true"
969
+ closedby="any"
970
+ position="center center">
971
+ <fig-header>
972
+ <h3>Light Dismiss</h3>
973
+ <fig-tooltip text="Close">
974
+ <fig-button variant="ghost"
975
+ icon="true"
976
+ close-dialog>
977
+ <span class="fig-mask-icon"
978
+ style="--icon: var(--icon-close)"></span>
979
+ </fig-button>
980
+ </fig-tooltip>
981
+ </fig-header>
982
+ <fig-content>
983
+ <p>Click anywhere outside this dialog to close it.</p>
984
+ </fig-content>
985
+ </dialog>
986
+
987
+ <dialog id="closedby-none-dialog"
988
+ is="fig-dialog"
989
+ drag="true"
990
+ closedby="none"
991
+ position="center center">
992
+ <fig-header>
993
+ <h3>No Light Dismiss</h3>
994
+ <fig-tooltip text="Close">
995
+ <fig-button variant="ghost"
996
+ icon="true"
997
+ close-dialog>
998
+ <span class="fig-mask-icon"
999
+ style="--icon: var(--icon-close)"></span>
1000
+ </fig-button>
1001
+ </fig-tooltip>
1002
+ </fig-header>
1003
+ <fig-content>
1004
+ <p>This dialog can only be closed with the close button.</p>
1005
+ </fig-content>
1006
+ </dialog>
866
1007
  </section>
867
1008
  <hr>
868
1009
 
@@ -2968,6 +3109,28 @@
2968
3109
  &lt;dialog is="fig-popup" anchor="#popup-open-right-single" position="right" offset="12 8"&gt;
2969
3110
  ...
2970
3111
  &lt;/dialog&gt;</code></pre>
3112
+
3113
+ <h4>Left (top edges aligned)</h4>
3114
+ <hstack>
3115
+ <fig-button id="popup-open-left-single">Open Left (single)</fig-button>
3116
+ <fig-button id="popup-close-left-single"
3117
+ variant="secondary">Close</fig-button>
3118
+ </hstack>
3119
+ <dialog id="popup-left-single"
3120
+ is="fig-popup"
3121
+ anchor="#popup-open-left-single"
3122
+ position="left"
3123
+ offset="12 8">
3124
+ <vstack style="min-width: 11rem;">
3125
+ <strong style="padding: 0 var(--spacer-1);">Left Shorthand</strong>
3126
+ <fig-input-text placeholder="position='left'"></fig-input-text>
3127
+ </vstack>
3128
+ </dialog>
3129
+ <pre><code>&lt;fig-button id="popup-open-left-single"&gt;Open Left (single)&lt;/fig-button&gt;
3130
+ &lt;dialog is="fig-popup" anchor="#popup-open-left-single" position="left" offset="12 8"&gt;
3131
+ ...
3132
+ &lt;/dialog&gt;</code></pre>
3133
+
2971
3134
  </section>
2972
3135
  <hr>
2973
3136
 
@@ -4113,6 +4276,50 @@ button.addEventListener('click', () => {
4113
4276
  </fig-footer>
4114
4277
  </main>
4115
4278
 
4279
+ <div class="toolbelt">
4280
+ <section>
4281
+ <fig-button variant="ghost"
4282
+ icon="true">
4283
+ <span class="fig-mask-icon"
4284
+ style="--icon: var(--icon-back)"></span>
4285
+ </fig-button>
4286
+ <fig-button variant="ghost"
4287
+ icon="true">
4288
+ <span class="fig-mask-icon"
4289
+ style="--icon: var(--icon-forward)"></span>
4290
+ </fig-button>
4291
+ </section>
4292
+ <section>
4293
+ <fig-button variant="ghost"
4294
+ icon="true">
4295
+ <span class="fig-mask-icon"
4296
+ style="--icon: var(--icon-add)"></span>
4297
+ </fig-button>
4298
+ <fig-button variant="ghost"
4299
+ icon="true">
4300
+ <span class="fig-mask-icon"
4301
+ style="--icon: var(--icon-minus)"></span>
4302
+ </fig-button>
4303
+ </section>
4304
+ <section>
4305
+ <fig-button variant="ghost"
4306
+ icon="true">
4307
+ <span class="fig-mask-icon"
4308
+ style="--icon: var(--icon-swap)"></span>
4309
+ </fig-button>
4310
+ <fig-button variant="ghost"
4311
+ icon="true">
4312
+ <span class="fig-mask-icon"
4313
+ style="--icon: var(--icon-rotate)"></span>
4314
+ </fig-button>
4315
+ <fig-button variant="ghost"
4316
+ icon="true">
4317
+ <span class="fig-mask-icon"
4318
+ style="--icon: var(--icon-eyedropper)"></span>
4319
+ </fig-button>
4320
+ </section>
4321
+ </div>
4322
+
4116
4323
  <script>
4117
4324
  // Highlight nav item based on hash
4118
4325
  function updateActiveNav() {
@@ -4180,8 +4387,7 @@ button.addEventListener('click', () => {
4180
4387
 
4181
4388
  function setTheme(isDark) {
4182
4389
  document.documentElement.style.colorScheme = isDark ? 'dark' : 'light';
4183
- document.documentElement.classList.toggle('figma-dark', isDark);
4184
- document.documentElement.classList.toggle('figma-light', !isDark);
4390
+
4185
4391
  localStorage.setItem('theme', isDark ? 'dark' : 'light');
4186
4392
  // Sync switch state
4187
4393
  if (isDark) {
@@ -4268,6 +4474,7 @@ button.addEventListener('click', () => {
4268
4474
  ['popup-open-center-left', 'popup-close-center-left', 'popup-center-left'],
4269
4475
  ['popup-open-top-single', 'popup-close-top-single', 'popup-top-single'],
4270
4476
  ['popup-open-right-single', 'popup-close-right-single', 'popup-right-single'],
4477
+ ['popup-open-left-single', 'popup-close-left-single', 'popup-left-single'],
4271
4478
  ];
4272
4479
 
4273
4480
  popupExamples.forEach(([openId, closeId, popupId]) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "2.19.1",
3
+ "version": "2.21.0",
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",