@rogieking/figui3 2.20.0 → 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.
package/components.css CHANGED
@@ -1412,7 +1412,8 @@ input[type="checkbox"]:not(.switch) {
1412
1412
  /* Light theme checkbox hover (black checkmark preview) */
1413
1413
  /* @media is a no-JS fallback — ignored when setTheme() sets classes */
1414
1414
  @media (prefers-color-scheme: light) {
1415
- :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 {
1416
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");
1417
1418
  }
1418
1419
  }
@@ -2030,12 +2031,15 @@ dialog[is="fig-popup"] {
2030
2031
  position: fixed;
2031
2032
  margin: 0;
2032
2033
  min-width: 0;
2033
- width: max-content;
2034
- max-width: calc(100vw - var(--spacer-4));
2035
- max-height: calc(100vh - var(--spacer-4));
2036
- padding: var(--spacer-2);
2034
+ padding: 0;
2037
2035
  overflow: auto;
2038
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
+
2039
2043
  &[open] {
2040
2044
  display: block;
2041
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);
@@ -1246,10 +1295,128 @@ class FigPopup extends HTMLDialogElement {
1246
1295
  if (!(target instanceof Node)) return;
1247
1296
  if (this.contains(target)) return;
1248
1297
 
1298
+ const anchor = this.#resolveAnchor();
1299
+ if (anchor && anchor.contains(target)) return;
1300
+
1249
1301
  this.open = false;
1250
1302
  }
1251
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
+
1252
1417
  #resolveAnchor() {
1418
+ if (this.#anchorRef) return this.#anchorRef;
1419
+
1253
1420
  const selector = this.getAttribute("anchor");
1254
1421
  if (!selector) return null;
1255
1422
 
@@ -1417,12 +1584,24 @@ class FigPopup extends HTMLDialogElement {
1417
1584
  top = anchorRect.top + (anchorRect.height - popupRect.height) / 2;
1418
1585
  }
1419
1586
 
1420
- if (horizontal === "left") {
1421
- left = anchorRect.left - popupRect.width - offset.xPx;
1422
- } else if (horizontal === "right") {
1423
- 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
+ }
1424
1596
  } else {
1425
- 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
+ }
1426
1605
  }
1427
1606
 
1428
1607
  return { top, left };
@@ -3197,7 +3376,7 @@ class FigInputColor extends HTMLElement {
3197
3376
  let swatchElement = "";
3198
3377
  if (!hidePicker) {
3199
3378
  swatchElement = useFigmaPicker
3200
- ? `<fig-fill-picker mode="solid" ${expAttr} ${
3379
+ ? `<fig-fill-picker mode="solid" dialog-position="left" ${expAttr} ${
3201
3380
  showAlpha ? "" : 'alpha="false"'
3202
3381
  } value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
3203
3382
  this.alpha
@@ -3215,7 +3394,7 @@ class FigInputColor extends HTMLElement {
3215
3394
  html = ``;
3216
3395
  } else {
3217
3396
  html = useFigmaPicker
3218
- ? `<fig-fill-picker mode="solid" ${expAttr} ${
3397
+ ? `<fig-fill-picker mode="solid" dialog-position="left" ${expAttr} ${
3219
3398
  showAlpha ? "" : 'alpha="false"'
3220
3399
  } value='{"type":"solid","color":"${this.hexOpaque}","opacity":${
3221
3400
  this.alpha
@@ -3743,7 +3922,7 @@ class FigInputFill extends HTMLElement {
3743
3922
  const experimentalAttr = this.getAttribute("experimental");
3744
3923
  this.innerHTML = `
3745
3924
  <div class="input-combo">
3746
- <fig-fill-picker value='${fillPickerValue}' ${
3925
+ <fig-fill-picker dialog-position="left" value='${fillPickerValue}' ${
3747
3926
  disabled ? "disabled" : ""
3748
3927
  } ${modeAttr ? `mode="${modeAttr}"` : ""} ${experimentalAttr ? `experimental="${experimentalAttr}"` : ""}></fig-fill-picker>
3749
3928
  ${controlsHtml}
@@ -6007,7 +6186,7 @@ customElements.define("fig-layer", FigLayer);
6007
6186
  * @attr {string} value - JSON-encoded fill value
6008
6187
  * @attr {boolean} disabled - Whether the picker is disabled
6009
6188
  * @attr {boolean} alpha - Whether to show alpha/opacity controls (default: true)
6010
- * @attr {string} dialog-position - Position of the dialog (passed to fig-dialog)
6189
+ * @attr {string} dialog-position - Position of the popup (default: "left")
6011
6190
  */
6012
6191
  class FigFillPicker extends HTMLElement {
6013
6192
  #trigger = null;
@@ -6213,21 +6392,10 @@ class FigFillPicker extends HTMLElement {
6213
6392
  this.#createDialog();
6214
6393
  }
6215
6394
 
6216
- // Position off-screen first to prevent scroll jump
6217
- this.#dialog.style.position = "fixed";
6218
- this.#dialog.style.top = "-9999px";
6219
- this.#dialog.style.left = "-9999px";
6220
-
6221
- this.#dialog.show();
6222
6395
  this.#switchTab(this.#fillType);
6396
+ this.#dialog.open = true;
6223
6397
 
6224
- // Position after dialog has rendered and has dimensions
6225
- // Use nested RAF to ensure canvas is fully ready for drawing
6226
6398
  requestAnimationFrame(() => {
6227
- this.#positionDialog();
6228
- this.#dialog.setAttribute("closedby", "any");
6229
-
6230
- // Second RAF ensures the dialog is visible and canvas is ready
6231
6399
  requestAnimationFrame(() => {
6232
6400
  this.#drawColorArea();
6233
6401
  this.#updateHandlePosition();
@@ -6235,72 +6403,17 @@ class FigFillPicker extends HTMLElement {
6235
6403
  });
6236
6404
  }
6237
6405
 
6238
- #positionDialog() {
6239
- const triggerRect = this.#trigger.getBoundingClientRect();
6240
- const dialogRect = this.#dialog.getBoundingClientRect();
6241
- const padding = 8; // Gap between trigger and dialog
6242
- const viewportPadding = 16; // Min distance from viewport edges
6243
-
6244
- // Calculate available space in each direction
6245
- const spaceBelow =
6246
- window.innerHeight - triggerRect.bottom - viewportPadding;
6247
- const spaceAbove = triggerRect.top - viewportPadding;
6248
- const spaceRight = window.innerWidth - triggerRect.left - viewportPadding;
6249
- const spaceLeft = triggerRect.right - viewportPadding;
6250
-
6251
- let top, left;
6252
-
6253
- // Vertical positioning: prefer below, fallback to above
6254
- if (spaceBelow >= dialogRect.height || spaceBelow >= spaceAbove) {
6255
- // Position below trigger
6256
- top = triggerRect.bottom + padding;
6257
- } else {
6258
- // Position above trigger
6259
- top = triggerRect.top - dialogRect.height - padding;
6260
- }
6261
-
6262
- // Horizontal positioning: align left edge with trigger, adjust if needed
6263
- left = triggerRect.left;
6264
-
6265
- // Adjust if dialog would go off right edge
6266
- if (left + dialogRect.width > window.innerWidth - viewportPadding) {
6267
- left = window.innerWidth - dialogRect.width - viewportPadding;
6268
- }
6269
-
6270
- // Adjust if dialog would go off left edge
6271
- if (left < viewportPadding) {
6272
- left = viewportPadding;
6273
- }
6274
-
6275
- // Clamp vertical position to viewport
6276
- if (top < viewportPadding) {
6277
- top = viewportPadding;
6278
- }
6279
- if (top + dialogRect.height > window.innerHeight - viewportPadding) {
6280
- top = window.innerHeight - dialogRect.height - viewportPadding;
6281
- }
6282
-
6283
- // Apply position (override fig-dialog's default positioning)
6284
- this.#dialog.style.position = "fixed";
6285
- this.#dialog.style.top = `${top}px`;
6286
- this.#dialog.style.left = `${left}px`;
6287
- this.#dialog.style.bottom = "auto";
6288
- this.#dialog.style.right = "auto";
6289
- this.#dialog.style.margin = "0";
6290
- }
6291
-
6292
6406
  #createDialog() {
6293
- this.#dialog = document.createElement("dialog", { is: "fig-dialog" });
6294
- this.#dialog.setAttribute("is", "fig-dialog");
6407
+ this.#dialog = document.createElement("dialog", { is: "fig-popup" });
6408
+ this.#dialog.setAttribute("is", "fig-popup");
6295
6409
  this.#dialog.setAttribute("drag", "true");
6296
6410
  this.#dialog.setAttribute("handle", "fig-header");
6411
+ this.#dialog.setAttribute("autoresize", "false");
6297
6412
  this.#dialog.classList.add("fig-fill-picker-dialog");
6298
6413
 
6299
- // Forward dialog attributes
6300
- const dialogPosition = this.getAttribute("dialog-position");
6301
- if (dialogPosition) {
6302
- this.#dialog.setAttribute("position", dialogPosition);
6303
- }
6414
+ this.#dialog.anchor = this.#trigger;
6415
+ const dialogPosition = this.getAttribute("dialog-position") || "left";
6416
+ this.#dialog.setAttribute("position", dialogPosition);
6304
6417
 
6305
6418
  // Check for allowed modes (supports comma-separated values like "solid,gradient")
6306
6419
  const mode = this.getAttribute("mode");
@@ -6333,7 +6446,7 @@ class FigFillPicker extends HTMLElement {
6333
6446
 
6334
6447
  let headerContent;
6335
6448
  if (allowedModes.length === 1) {
6336
- 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>`;
6337
6450
  } else {
6338
6451
  const options = allowedModes
6339
6452
  .map((m) => `<option value="${m}">${modeLabels[m]}</option>`)
@@ -6346,7 +6459,7 @@ class FigFillPicker extends HTMLElement {
6346
6459
  this.#dialog.innerHTML = `
6347
6460
  <fig-header>
6348
6461
  ${headerContent}
6349
- <fig-button icon variant="ghost" close-dialog>
6462
+ <fig-button icon variant="ghost" class="fig-fill-picker-close">
6350
6463
  <span class="fig-mask-icon" style="--icon: var(--icon-close)"></span>
6351
6464
  </fig-button>
6352
6465
  </fig-header>
@@ -6369,11 +6482,10 @@ class FigFillPicker extends HTMLElement {
6369
6482
  });
6370
6483
  }
6371
6484
 
6372
- // Close button
6373
6485
  this.#dialog
6374
- .querySelector("fig-button[close-dialog]")
6486
+ .querySelector(".fig-fill-picker-close")
6375
6487
  .addEventListener("click", () => {
6376
- this.#dialog.close();
6488
+ this.#dialog.open = false;
6377
6489
  });
6378
6490
 
6379
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;
@@ -3109,6 +3109,28 @@
3109
3109
  &lt;dialog is="fig-popup" anchor="#popup-open-right-single" position="right" offset="12 8"&gt;
3110
3110
  ...
3111
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
+
3112
3134
  </section>
3113
3135
  <hr>
3114
3136
 
@@ -4452,6 +4474,7 @@ button.addEventListener('click', () => {
4452
4474
  ['popup-open-center-left', 'popup-close-center-left', 'popup-center-left'],
4453
4475
  ['popup-open-top-single', 'popup-close-top-single', 'popup-top-single'],
4454
4476
  ['popup-open-right-single', 'popup-close-right-single', 'popup-right-single'],
4477
+ ['popup-open-left-single', 'popup-close-left-single', 'popup-left-single'],
4455
4478
  ];
4456
4479
 
4457
4480
  popupExamples.forEach(([openId, closeId, popupId]) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "2.20.0",
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",