@rogieking/figui3 4.1.1 → 4.1.4

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/components.css +62 -11
  2. package/fig.js +334 -0
  3. package/package.json +1 -1
package/components.css CHANGED
@@ -639,7 +639,7 @@ input[type="text"][list] {
639
639
  padding-left: 0;
640
640
  }
641
641
  & > button {
642
- display: flex;
642
+ display: inline-block;
643
643
  align-items: center;
644
644
  gap: var(--spacer-1);
645
645
  width: 100%;
@@ -944,7 +944,7 @@ fig-button {
944
944
  box-shadow: inset 0 0 0 1px var(--figma-color-border-selected);
945
945
  }
946
946
  svg {
947
- *[fill] {
947
+ *[fill]:not([fill="none"]) {
948
948
  fill: currentColor;
949
949
  }
950
950
  *[stroke]:not([stroke="none"]) {
@@ -1377,7 +1377,7 @@ fig-input-file {
1377
1377
 
1378
1378
  fig-image {
1379
1379
  --image-size: 2rem;
1380
- --fit: cover;
1380
+ --fit: contain;
1381
1381
  --image-width: var(--image-size);
1382
1382
  --image-height: var(--image-size);
1383
1383
  --aspect-ratio: 1 / 1;
@@ -2728,6 +2728,9 @@ dialog[is="fig-popup"] {
2728
2728
  background-color: var(--figma-color-bg-menu);
2729
2729
  color: var(--figma-color-text-menu);
2730
2730
  color-scheme: dark;
2731
+ padding: var(--spacer-2) 0;
2732
+ min-width: 6rem;
2733
+ box-shadow: var(--figma-elevation-100);
2731
2734
  }
2732
2735
 
2733
2736
  &[variant="popover"] {
@@ -3899,7 +3902,7 @@ fig-layer {
3899
3902
  position: relative;
3900
3903
  user-select: none;
3901
3904
  width: 100%;
3902
- padding: 0 var(--spacer-2);
3905
+ padding: 0 var(--spacer-3);
3903
3906
  border-radius: var(--radius-medium);
3904
3907
 
3905
3908
  /* When layer has children */
@@ -3916,7 +3919,6 @@ fig-layer {
3916
3919
  align-items: center;
3917
3920
  gap: var(--spacer-1);
3918
3921
  padding: var(--spacer-1) var(--spacer-2);
3919
- margin-left: var(--spacer-1);
3920
3922
  height: var(--spacer-4);
3921
3923
  border-radius: var(--radius-medium);
3922
3924
  position: relative;
@@ -3935,7 +3937,7 @@ fig-layer {
3935
3937
  flex-shrink: 0;
3936
3938
  transition: transform 0.15s;
3937
3939
  position: absolute;
3938
- left: calc(var(--spacer-2-5) * -1);
3940
+ left: calc(var(--spacer-3) * -1);
3939
3941
  top: var(--spacer-1);
3940
3942
  rotate: -90deg;
3941
3943
  }
@@ -4541,10 +4543,11 @@ fig-preview {
4541
4543
  /* Chooser */
4542
4544
  fig-chooser {
4543
4545
  --chooser-fade-size: var(--spacer-4);
4546
+ --fig-chooser-gap: var(--spacer-2);
4544
4547
 
4545
4548
  display: flex;
4546
4549
  flex-direction: column;
4547
- gap: 1px;
4550
+ gap: var(--fig-chooser-gap);
4548
4551
  overflow: visible auto;
4549
4552
  scrollbar-width: none;
4550
4553
  scroll-snap-type: y mandatory;
@@ -4554,7 +4557,7 @@ fig-chooser {
4554
4557
  }
4555
4558
 
4556
4559
  &[padding="false"] {
4557
- gap: var(--spacer-2);
4560
+ gap: var(--fig-chooser-gap);
4558
4561
  fig-choice {
4559
4562
  --fig-choice-padding: 0px;
4560
4563
  }
@@ -4726,8 +4729,8 @@ fig-chooser {
4726
4729
  }
4727
4730
 
4728
4731
  fig-choice {
4729
- --fig-choice-selection-ring-width: 1.25px;
4730
- --fig-choice-padding: var(--spacer-2);
4732
+ --fig-choice-selection-ring-width: 1.5px;
4733
+ --fig-choice-padding: 0px;
4731
4734
  --fig-choice-border-radius: calc(
4732
4735
  var(--radius-medium) + var(--fig-choice-padding)
4733
4736
  );
@@ -4737,7 +4740,6 @@ fig-choice {
4737
4740
  border-radius: var(--fig-choice-border-radius);
4738
4741
  gap: var(--spacer-2);
4739
4742
  outline: none;
4740
- border: 1px solid transparent;
4741
4743
  cursor: default;
4742
4744
 
4743
4745
  padding: var(--fig-choice-padding);
@@ -4953,3 +4955,52 @@ fig-color-tip {
4953
4955
  }
4954
4956
  }
4955
4957
  }
4958
+
4959
+ /* Menu */
4960
+ fig-menu {
4961
+ display: contents;
4962
+ }
4963
+
4964
+ fig-menu-item {
4965
+ --fig-menu-item-padding: var(--spacer-3);
4966
+ display: flex;
4967
+ align-items: center;
4968
+ gap: var(--spacer-2);
4969
+ padding: 0 var(--fig-menu-item-padding);
4970
+ height: var(--spacer-4);
4971
+ cursor: default;
4972
+ border-radius: var(--radius-medium);
4973
+ color: var(--figma-color-text-menu);
4974
+ font-weight: var(--body-medium-fontWeight);
4975
+ position: relative;
4976
+
4977
+ &::before {
4978
+ content: "";
4979
+ display: block;
4980
+ position: absolute;
4981
+ inset: 0 var(--spacer-2);
4982
+ border-radius: var(--radius-medium);
4983
+ z-index: -1;
4984
+ background-color: transparent;
4985
+ }
4986
+
4987
+ &:hover,
4988
+ &:focus-visible {
4989
+ outline: none;
4990
+ &::before {
4991
+ background-color: var(--figma-color-bg-menu-hover);
4992
+ }
4993
+ }
4994
+
4995
+ &[disabled]:not([disabled="false"]) {
4996
+ opacity: 0.4;
4997
+ pointer-events: none;
4998
+ }
4999
+ }
5000
+
5001
+ fig-menu-separator {
5002
+ display: block;
5003
+ height: 1px;
5004
+ background: var(--figma-color-border);
5005
+ margin: var(--spacer-2) 0;
5006
+ }
package/fig.js CHANGED
@@ -7933,6 +7933,7 @@ class FigImage extends HTMLElement {
7933
7933
  }
7934
7934
 
7935
7935
  #handleFileInput(e) {
7936
+ if (e.target !== this.#fileInput) return;
7936
7937
  const file = e.detail?.files?.[0];
7937
7938
 
7938
7939
  if (!file) {
@@ -7968,7 +7969,9 @@ class FigImage extends HTMLElement {
7968
7969
  );
7969
7970
 
7970
7971
  if (this.#fileInput) {
7972
+ this.#fileInput.removeEventListener("change", this.#boundHandleFileInput);
7971
7973
  this.#fileInput.clear();
7974
+ this.#fileInput.addEventListener("change", this.#boundHandleFileInput);
7972
7975
  this.#fileInput.setAttribute("label", "Replace");
7973
7976
  }
7974
7977
  }
@@ -14237,3 +14240,334 @@ class FigHandle extends HTMLElement {
14237
14240
  }
14238
14241
  }
14239
14242
  customElements.define("fig-handle", FigHandle);
14243
+
14244
+ // ─── Menu ────────────────────────────────────────────────────────────────────
14245
+
14246
+ class FigMenuItem extends HTMLElement {
14247
+ static get observedAttributes() {
14248
+ return ["value", "disabled"];
14249
+ }
14250
+
14251
+ get value() {
14252
+ return this.getAttribute("value") || "";
14253
+ }
14254
+
14255
+ set value(val) {
14256
+ this.setAttribute("value", val ?? "");
14257
+ }
14258
+
14259
+ connectedCallback() {
14260
+ if (!this.hasAttribute("role")) {
14261
+ this.setAttribute("role", "menuitem");
14262
+ }
14263
+ if (!this.hasAttribute("tabindex")) {
14264
+ this.setAttribute("tabindex", "-1");
14265
+ }
14266
+ }
14267
+
14268
+ attributeChangedCallback() {}
14269
+ }
14270
+ customElements.define("fig-menu-item", FigMenuItem);
14271
+
14272
+ class FigMenuSeparator extends HTMLElement {
14273
+ connectedCallback() {
14274
+ if (!this.hasAttribute("role")) {
14275
+ this.setAttribute("role", "separator");
14276
+ }
14277
+ }
14278
+ }
14279
+ customElements.define("fig-menu-separator", FigMenuSeparator);
14280
+
14281
+ class FigMenu extends HTMLElement {
14282
+ #popup = null;
14283
+ #trigger = null;
14284
+ #observer = null;
14285
+ #boundTriggerClick;
14286
+ #boundPopupClick;
14287
+ #boundPopupKeydown;
14288
+ #boundPopupClose;
14289
+ #focusedIndex = -1;
14290
+
14291
+ static get observedAttributes() {
14292
+ return ["position", "offset", "closedby", "disabled", "open"];
14293
+ }
14294
+
14295
+ constructor() {
14296
+ super();
14297
+ this.#boundTriggerClick = this.#handleTriggerClick.bind(this);
14298
+ this.#boundPopupClick = this.#handlePopupClick.bind(this);
14299
+ this.#boundPopupKeydown = this.#handlePopupKeydown.bind(this);
14300
+ this.#boundPopupClose = this.#handlePopupClose.bind(this);
14301
+ }
14302
+
14303
+ get value() {
14304
+ return this.getAttribute("value") || "";
14305
+ }
14306
+
14307
+ set value(val) {
14308
+ this.setAttribute("value", val ?? "");
14309
+ }
14310
+
14311
+ get open() {
14312
+ return this.hasAttribute("open") && this.getAttribute("open") !== "false";
14313
+ }
14314
+
14315
+ set open(val) {
14316
+ if (val) {
14317
+ this.setAttribute("open", "");
14318
+ } else {
14319
+ this.removeAttribute("open");
14320
+ }
14321
+ }
14322
+
14323
+ connectedCallback() {
14324
+ this.style.display = "contents";
14325
+
14326
+ this.#detectTrigger();
14327
+ this.#createPopup();
14328
+ this.#moveItemsToPopup();
14329
+ this.#setupListeners();
14330
+ this.#setupObserver();
14331
+ this.#syncDisabled();
14332
+
14333
+ if (this.open) {
14334
+ this.#openMenu();
14335
+ }
14336
+ }
14337
+
14338
+ disconnectedCallback() {
14339
+ this.#teardownListeners();
14340
+ if (this.#observer) {
14341
+ this.#observer.disconnect();
14342
+ this.#observer = null;
14343
+ }
14344
+ if (this.#popup) {
14345
+ this.#popup.removeEventListener("close", this.#boundPopupClose);
14346
+ this.#popup.remove();
14347
+ this.#popup = null;
14348
+ }
14349
+ }
14350
+
14351
+ attributeChangedCallback(name, oldValue, newValue) {
14352
+ if (oldValue === newValue) return;
14353
+
14354
+ if (name === "open") {
14355
+ if (newValue === null || newValue === "false") {
14356
+ this.#closeMenu();
14357
+ } else {
14358
+ this.#openMenu();
14359
+ }
14360
+ return;
14361
+ }
14362
+
14363
+ if (name === "disabled") {
14364
+ if (this.#trigger) {
14365
+ if (newValue !== null && newValue !== "false") {
14366
+ this.#trigger.setAttribute("disabled", "");
14367
+ } else {
14368
+ this.#trigger.removeAttribute("disabled");
14369
+ }
14370
+ }
14371
+ return;
14372
+ }
14373
+
14374
+ if (this.#popup && (name === "position" || name === "offset" || name === "closedby")) {
14375
+ if (newValue !== null) {
14376
+ this.#popup.setAttribute(name, newValue);
14377
+ } else {
14378
+ this.#popup.removeAttribute(name);
14379
+ }
14380
+ }
14381
+ }
14382
+
14383
+ #detectTrigger() {
14384
+ this.#trigger =
14385
+ this.querySelector("[fig-menu-trigger]") ||
14386
+ this.querySelector(":scope > :not(fig-menu-item):not(fig-menu-separator)");
14387
+ }
14388
+
14389
+ #createPopup() {
14390
+ this.#popup = document.createElement("dialog", { is: "fig-popup" });
14391
+ this.#popup.setAttribute("is", "fig-popup");
14392
+ this.#popup.setAttribute("theme", "menu");
14393
+ this.#popup.setAttribute("role", "menu");
14394
+
14395
+ const position = this.getAttribute("position") || "bottom left";
14396
+ this.#popup.setAttribute("position", position);
14397
+
14398
+ const offset = this.getAttribute("offset");
14399
+ if (offset) this.#popup.setAttribute("offset", offset);
14400
+
14401
+ const closedby = this.getAttribute("closedby");
14402
+ if (closedby) this.#popup.setAttribute("closedby", closedby);
14403
+
14404
+ if (this.#trigger) {
14405
+ this.#popup.anchor = this.#trigger;
14406
+ }
14407
+
14408
+ this.#popup.addEventListener("close", this.#boundPopupClose);
14409
+ this.appendChild(this.#popup);
14410
+ }
14411
+
14412
+ #moveItemsToPopup() {
14413
+ const items = Array.from(this.querySelectorAll(
14414
+ ":scope > fig-menu-item, :scope > fig-menu-separator"
14415
+ ));
14416
+ for (const item of items) {
14417
+ this.#popup.appendChild(item);
14418
+ }
14419
+ }
14420
+
14421
+ #setupListeners() {
14422
+ if (this.#trigger) {
14423
+ this.#trigger.addEventListener("click", this.#boundTriggerClick);
14424
+ this.#trigger.setAttribute("aria-haspopup", "menu");
14425
+ this.#trigger.setAttribute("aria-expanded", "false");
14426
+ }
14427
+ if (this.#popup) {
14428
+ this.#popup.addEventListener("click", this.#boundPopupClick);
14429
+ this.#popup.addEventListener("keydown", this.#boundPopupKeydown);
14430
+ }
14431
+ }
14432
+
14433
+ #teardownListeners() {
14434
+ if (this.#trigger) {
14435
+ this.#trigger.removeEventListener("click", this.#boundTriggerClick);
14436
+ }
14437
+ if (this.#popup) {
14438
+ this.#popup.removeEventListener("click", this.#boundPopupClick);
14439
+ this.#popup.removeEventListener("keydown", this.#boundPopupKeydown);
14440
+ }
14441
+ }
14442
+
14443
+ #setupObserver() {
14444
+ this.#observer = new MutationObserver((mutations) => {
14445
+ for (const mutation of mutations) {
14446
+ for (const node of mutation.addedNodes) {
14447
+ if (
14448
+ node.nodeType === 1 &&
14449
+ (node.tagName === "FIG-MENU-ITEM" || node.tagName === "FIG-MENU-SEPARATOR") &&
14450
+ node.parentElement === this
14451
+ ) {
14452
+ this.#popup.appendChild(node);
14453
+ }
14454
+ }
14455
+ }
14456
+ });
14457
+ this.#observer.observe(this, { childList: true });
14458
+ }
14459
+
14460
+ #getItems() {
14461
+ if (!this.#popup) return [];
14462
+ return Array.from(this.#popup.querySelectorAll("fig-menu-item:not([disabled]):not([disabled='true'])"));
14463
+ }
14464
+
14465
+ #syncDisabled() {
14466
+ if (!this.#trigger) return;
14467
+ const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
14468
+ if (disabled) {
14469
+ this.#trigger.setAttribute("disabled", "");
14470
+ } else {
14471
+ this.#trigger.removeAttribute("disabled");
14472
+ }
14473
+ }
14474
+
14475
+ #handleTriggerClick(e) {
14476
+ if (this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") return;
14477
+ e.stopPropagation();
14478
+ if (this.open) {
14479
+ this.open = false;
14480
+ } else {
14481
+ this.open = true;
14482
+ }
14483
+ }
14484
+
14485
+ #handlePopupClick(e) {
14486
+ const item = e.target.closest("fig-menu-item");
14487
+ if (!item) return;
14488
+ if (item.hasAttribute("disabled") && item.getAttribute("disabled") !== "false") return;
14489
+
14490
+ this.#selectItem(item);
14491
+ }
14492
+
14493
+ #handlePopupKeydown(e) {
14494
+ const items = this.#getItems();
14495
+ if (!items.length) return;
14496
+
14497
+ switch (e.key) {
14498
+ case "ArrowDown": {
14499
+ e.preventDefault();
14500
+ this.#focusedIndex = Math.min(this.#focusedIndex + 1, items.length - 1);
14501
+ items[this.#focusedIndex]?.focus();
14502
+ break;
14503
+ }
14504
+ case "ArrowUp": {
14505
+ e.preventDefault();
14506
+ this.#focusedIndex = Math.max(this.#focusedIndex - 1, 0);
14507
+ items[this.#focusedIndex]?.focus();
14508
+ break;
14509
+ }
14510
+ case "Home": {
14511
+ e.preventDefault();
14512
+ this.#focusedIndex = 0;
14513
+ items[0]?.focus();
14514
+ break;
14515
+ }
14516
+ case "End": {
14517
+ e.preventDefault();
14518
+ this.#focusedIndex = items.length - 1;
14519
+ items[this.#focusedIndex]?.focus();
14520
+ break;
14521
+ }
14522
+ case "Enter":
14523
+ case " ": {
14524
+ e.preventDefault();
14525
+ const focused = items[this.#focusedIndex];
14526
+ if (focused) this.#selectItem(focused);
14527
+ break;
14528
+ }
14529
+ }
14530
+ }
14531
+
14532
+ #handlePopupClose() {
14533
+ if (this.hasAttribute("open")) {
14534
+ this.removeAttribute("open");
14535
+ }
14536
+ if (this.#trigger) {
14537
+ this.#trigger.setAttribute("aria-expanded", "false");
14538
+ this.#trigger.focus();
14539
+ }
14540
+ this.#focusedIndex = -1;
14541
+ }
14542
+
14543
+ #selectItem(item) {
14544
+ const value = item.getAttribute("value") || item.textContent.trim();
14545
+ this.setAttribute("value", value);
14546
+ this.dispatchEvent(
14547
+ new CustomEvent("change", {
14548
+ detail: { value, item },
14549
+ bubbles: true,
14550
+ })
14551
+ );
14552
+ this.open = false;
14553
+ }
14554
+
14555
+ #openMenu() {
14556
+ if (!this.#popup) return;
14557
+ this.#popup.open = true;
14558
+ if (this.#trigger) {
14559
+ this.#trigger.setAttribute("aria-expanded", "true");
14560
+ }
14561
+ this.#focusedIndex = 0;
14562
+ requestAnimationFrame(() => {
14563
+ const items = this.#getItems();
14564
+ if (items.length) items[0].focus();
14565
+ });
14566
+ }
14567
+
14568
+ #closeMenu() {
14569
+ if (!this.#popup) return;
14570
+ this.#popup.open = false;
14571
+ }
14572
+ }
14573
+ customElements.define("fig-menu", FigMenu);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "4.1.1",
3
+ "version": "4.1.4",
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",