@rogieking/figui3 11.1.6 → 11.2.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/fig.js CHANGED
@@ -8526,6 +8526,7 @@ class FigField extends HTMLElement {
8526
8526
  #boundLabelEnter = null;
8527
8527
  #boundLabelLeave = null;
8528
8528
  #childrenObserver = null;
8529
+ #inputObserver = null;
8529
8530
 
8530
8531
  constructor() {
8531
8532
  super();
@@ -8570,16 +8571,22 @@ class FigField extends HTMLElement {
8570
8571
  node.nodeName.toLowerCase().startsWith("fig-") &&
8571
8572
  !(node instanceof Element && node.classList.contains("fig-field-chevron")),
8572
8573
  );
8574
+ this.#observeInputAttributes();
8573
8575
 
8574
8576
  // Popup/menu hosts also expose `open`, but that is overlay state — not a
8575
8577
  // fig-field disclosure. Only treat true expand/collapse children as toggleable.
8576
8578
  const inputTag = this.input?.localName ?? "";
8579
+ const paletteEditMode = this.input?.getAttribute?.("edit");
8580
+ const nonInlinePalette =
8581
+ inputTag === "fig-input-palette" &&
8582
+ (paletteEditMode === "false" || paletteEditMode === "picker");
8577
8583
  const popupOpenHost =
8578
8584
  inputTag === "fig-select" ||
8579
8585
  inputTag === "fig-menu" ||
8580
8586
  inputTag === "fig-dropdown" ||
8581
8587
  inputTag === "fig-tooltip" ||
8582
8588
  inputTag === "fig-fill-picker" ||
8589
+ nonInlinePalette ||
8583
8590
  (this.input instanceof HTMLDialogElement);
8584
8591
  this.#toggleable = !!(
8585
8592
  this.input &&
@@ -8629,9 +8636,27 @@ class FigField extends HTMLElement {
8629
8636
  }
8630
8637
  }
8631
8638
 
8639
+ #observeInputAttributes() {
8640
+ this.#inputObserver?.disconnect();
8641
+ this.#inputObserver = null;
8642
+ if (
8643
+ this.input?.localName !== "fig-input-palette" ||
8644
+ typeof MutationObserver === "undefined"
8645
+ ) {
8646
+ return;
8647
+ }
8648
+ this.#inputObserver = new MutationObserver(() => this.#setup());
8649
+ this.#inputObserver.observe(this.input, {
8650
+ attributes: true,
8651
+ attributeFilter: ["edit"],
8652
+ });
8653
+ }
8654
+
8632
8655
  disconnectedCallback() {
8633
8656
  this.#childrenObserver?.disconnect();
8634
8657
  this.#childrenObserver = null;
8658
+ this.#inputObserver?.disconnect();
8659
+ this.#inputObserver = null;
8635
8660
  if (this.label) FigTooltip.hide(this.label);
8636
8661
  if (this.label && this.#boundToggle) {
8637
8662
  this.label.removeEventListener("click", this.#boundToggle);
@@ -10732,6 +10757,7 @@ figDefineElement("fig-input-fill", FigInputFill);
10732
10757
  * @attr {boolean} fixed - When set (or `fixed="true"`), palette length is locked — no add or remove
10733
10758
  * @attr {number} min - Minimum number of colors (default: 2)
10734
10759
  * @attr {number} max - Maximum number of colors (default: 8); add button hidden at max
10760
+ * @attr {string} edit - Editing mode: "true" (inline, default), "false" (preview), or "picker"
10735
10761
  * @fires input - During color editing (detail: full color array)
10736
10762
  * @fires change - On committed color edits or add (detail: full color array)
10737
10763
  */
@@ -10740,11 +10766,26 @@ class FigInputPalette extends HTMLElement {
10740
10766
  #inlinePickers = [];
10741
10767
  #expandedPickers = [];
10742
10768
  #renderRAF = null;
10769
+ #pickerDefinitionPromise = null;
10770
+ #pickerPopup = null;
10771
+ #pickerEditor = null;
10772
+ #syncingValueAttribute = false;
10743
10773
  #boundHandleKeyDown = this.#handleKeyDown.bind(this);
10744
10774
  #boundHandleHostFocus = () => this.focus();
10775
+ #boundPickerClosed = () => {
10776
+ if (!this.#pickerPopup) return;
10777
+ if (
10778
+ this.#pickerPopup.hasAttribute("open") ||
10779
+ this.#pickerPopup.matches?.(":open") ||
10780
+ this.#pickerPopup.matches?.(":popover-open")
10781
+ ) {
10782
+ return;
10783
+ }
10784
+ if (this.open) this.open = false;
10785
+ };
10745
10786
 
10746
10787
  static get observedAttributes() {
10747
- return ["value", "disabled", "min", "max", "open", "fixed"];
10788
+ return ["value", "disabled", "min", "max", "open", "fixed", "edit"];
10748
10789
  }
10749
10790
 
10750
10791
  get open() {
@@ -10753,15 +10794,16 @@ class FigInputPalette extends HTMLElement {
10753
10794
 
10754
10795
  set open(value) {
10755
10796
  const was = this.open;
10756
- if (value) {
10797
+ if (value && this.#editMode !== "false") {
10757
10798
  this.setAttribute("open", "");
10758
10799
  } else {
10759
10800
  this.removeAttribute("open");
10760
10801
  }
10761
- if (was !== !!value) {
10802
+ const next = this.open;
10803
+ if (was !== next) {
10762
10804
  this.dispatchEvent(
10763
10805
  new CustomEvent("openchange", {
10764
- detail: { open: !!value },
10806
+ detail: { open: next },
10765
10807
  bubbles: true,
10766
10808
  }),
10767
10809
  );
@@ -10774,6 +10816,13 @@ class FigInputPalette extends HTMLElement {
10774
10816
  );
10775
10817
  }
10776
10818
 
10819
+ get #editMode() {
10820
+ const value = this.getAttribute("edit");
10821
+ if (value === "false") return "false";
10822
+ if (value === "picker") return "picker";
10823
+ return "true";
10824
+ }
10825
+
10777
10826
  get #min() {
10778
10827
  const v = parseInt(this.getAttribute("min"));
10779
10828
  return isNaN(v) ? 2 : v;
@@ -10796,6 +10845,7 @@ class FigInputPalette extends HTMLElement {
10796
10845
  if (!this.isConnected) return;
10797
10846
  this.#parseValue();
10798
10847
  this.#render();
10848
+ this.#renderPickerWhenAvailable();
10799
10849
  });
10800
10850
  }
10801
10851
 
@@ -10808,25 +10858,27 @@ class FigInputPalette extends HTMLElement {
10808
10858
  this.removeEventListener("focus", this.#boundHandleHostFocus);
10809
10859
  this.#inlinePickers = [];
10810
10860
  this.#expandedPickers = [];
10861
+ this.#destroyPickerPopup();
10811
10862
  }
10812
10863
 
10813
10864
  #handleKeyDown(event) {
10814
10865
  if (event.key !== "Enter" && event.key !== " ") return;
10815
10866
  if (event.target !== this.querySelector(".palette-colors-inline")) return;
10816
10867
  if (this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") return;
10868
+ if (this.#editMode === "false") return;
10817
10869
  event.preventDefault();
10818
10870
  event.stopPropagation();
10819
10871
  this.open = true;
10820
- this.querySelector(".palette-colors-inline")?.setAttribute("aria-expanded", "true");
10821
10872
  }
10822
10873
 
10823
10874
  attributeChangedCallback(name, oldValue, newValue) {
10824
- if (oldValue === newValue) return;
10875
+ if (oldValue === newValue || this.#syncingValueAttribute) return;
10825
10876
 
10826
10877
  switch (name) {
10827
10878
  case "value":
10828
10879
  this.#parseValue();
10829
10880
  this.#syncPickers();
10881
+ this.#syncPickerEditor();
10830
10882
  break;
10831
10883
  case "disabled":
10832
10884
  this.#syncDisabled();
@@ -10835,9 +10887,17 @@ class FigInputPalette extends HTMLElement {
10835
10887
  case "max":
10836
10888
  case "fixed":
10837
10889
  this.#render();
10890
+ this.#syncPickerEditor();
10838
10891
  break;
10839
10892
  case "open":
10840
10893
  this.#syncTriggerState();
10894
+ this.#syncPickerOpenState();
10895
+ break;
10896
+ case "edit":
10897
+ if (this.#editMode === "false" && this.open) this.open = false;
10898
+ if (this.#editMode !== "picker") this.#destroyPickerPopup();
10899
+ this.#render();
10900
+ this.#renderPickerWhenAvailable();
10841
10901
  break;
10842
10902
  }
10843
10903
  }
@@ -10925,6 +10985,8 @@ class FigInputPalette extends HTMLElement {
10925
10985
  const disabled =
10926
10986
  this.hasAttribute("disabled") &&
10927
10987
  this.getAttribute("disabled") !== "false";
10988
+ const editMode = this.#editMode;
10989
+ const interactive = editMode !== "false";
10928
10990
 
10929
10991
  this.replaceChildren();
10930
10992
  this.#inlinePickers = [];
@@ -10932,10 +10994,16 @@ class FigInputPalette extends HTMLElement {
10932
10994
 
10933
10995
  const inlineWrap = document.createElement("div");
10934
10996
  inlineWrap.className = "palette-colors-inline";
10935
- inlineWrap.setAttribute("role", "button");
10936
- inlineWrap.setAttribute("tabindex", disabled ? "-1" : "+0");
10937
- inlineWrap.setAttribute("aria-expanded", String(this.open));
10938
- inlineWrap.setAttribute("aria-label", "Edit palette colors");
10997
+ if (interactive) {
10998
+ inlineWrap.setAttribute("role", "button");
10999
+ inlineWrap.setAttribute("tabindex", disabled ? "-1" : "+0");
11000
+ inlineWrap.setAttribute("aria-expanded", String(this.open));
11001
+ inlineWrap.setAttribute("aria-label", "Edit palette colors");
11002
+ if (editMode === "picker") inlineWrap.setAttribute("aria-haspopup", "dialog");
11003
+ } else {
11004
+ inlineWrap.setAttribute("role", "img");
11005
+ inlineWrap.setAttribute("aria-label", "Palette colors");
11006
+ }
10939
11007
  inlineWrap.addEventListener("blur", () => {
10940
11008
  inlineWrap.style.removeProperty("outline");
10941
11009
  inlineWrap.style.removeProperty("outline-offset");
@@ -10947,9 +11015,8 @@ class FigInputPalette extends HTMLElement {
10947
11015
  )
10948
11016
  return;
10949
11017
  this.open = true;
10950
- inlineWrap.setAttribute("aria-expanded", "true");
10951
11018
  };
10952
- inlineWrap.addEventListener("click", openPalette);
11019
+ if (interactive) inlineWrap.addEventListener("click", openPalette);
10953
11020
 
10954
11021
  const wrap = document.createElement("div");
10955
11022
  wrap.className = "palette-colors";
@@ -10962,17 +11029,143 @@ class FigInputPalette extends HTMLElement {
10962
11029
  this.appendChild(inlineWrap);
10963
11030
  this.#syncTriggerState();
10964
11031
 
10965
- if (!this.#isFixed) this.#createAddButton(disabled, this);
11032
+ if (editMode === "true") {
11033
+ if (!this.#isFixed) this.#createAddButton(disabled, this);
10966
11034
 
10967
- const expandedWrap = document.createElement("div");
10968
- expandedWrap.className = "palette-colors-expanded";
10969
- this.#colors.forEach((entry, i) => {
10970
- expandedWrap.appendChild(this.#createPicker(entry, i, disabled));
10971
- if (!this.#isFixed) {
10972
- expandedWrap.appendChild(this.#createRemoveButton(i, disabled));
10973
- }
10974
- });
10975
- this.appendChild(expandedWrap);
11035
+ const expandedWrap = document.createElement("div");
11036
+ expandedWrap.className = "palette-colors-expanded";
11037
+ this.#colors.forEach((entry, i) => {
11038
+ expandedWrap.appendChild(this.#createPicker(entry, i, disabled));
11039
+ if (!this.#isFixed) {
11040
+ expandedWrap.appendChild(this.#createRemoveButton(i, disabled));
11041
+ }
11042
+ });
11043
+ this.appendChild(expandedWrap);
11044
+ }
11045
+ }
11046
+
11047
+ #renderPickerWhenAvailable() {
11048
+ if (
11049
+ this.#editMode !== "picker" ||
11050
+ customElements.get("fig-palette-editor")
11051
+ ) {
11052
+ this.#syncPickerOpenState();
11053
+ return;
11054
+ }
11055
+ this.#pickerDefinitionPromise ??= customElements
11056
+ .whenDefined("fig-palette-editor")
11057
+ .then(() => {
11058
+ if (!this.isConnected || this.#editMode !== "picker") return;
11059
+ this.#syncPickerOpenState();
11060
+ });
11061
+ }
11062
+
11063
+ #ensurePickerPopup() {
11064
+ if (this.#pickerPopup?.isConnected) return this.#pickerPopup;
11065
+ if (!customElements.get("fig-palette-editor")) return null;
11066
+
11067
+ const popup = document.createElement("dialog", { is: "fig-popup" });
11068
+ popup.setAttribute("is", "fig-popup");
11069
+ popup.setAttribute("title", "Edit palette");
11070
+ popup.setAttribute("position", "bottom left");
11071
+ popup.setAttribute("offset", "0 8");
11072
+ popup.setAttribute("viewport-margin", "8");
11073
+ popup.setAttribute("popover", "manual");
11074
+ popup.className = "fig-palette-editor-popup";
11075
+ popup.id = figUniqueId();
11076
+ popup.anchor = this;
11077
+
11078
+ const editor = document.createElement("fig-palette-editor");
11079
+ const content = figCreateElement(
11080
+ "fig-content",
11081
+ { className: "fig-palette-editor-content" },
11082
+ editor,
11083
+ );
11084
+ popup.append(content);
11085
+
11086
+ const handleEditorEvent = (event) => {
11087
+ event.stopPropagation();
11088
+ if (!Array.isArray(event.detail)) return;
11089
+ this.#colors = event.detail.map((entry) => ({
11090
+ color: entry?.color || "#D9D9D9",
11091
+ alpha: Number.isFinite(Number(entry?.alpha))
11092
+ ? Math.max(0, Math.min(1, Number(entry.alpha)))
11093
+ : 1,
11094
+ }));
11095
+ this.#syncingValueAttribute = true;
11096
+ this.setAttribute("value", JSON.stringify(this.#colors));
11097
+ this.#syncingValueAttribute = false;
11098
+ this.#syncPickers();
11099
+ this.dispatchEvent(
11100
+ new CustomEvent(event.type, {
11101
+ bubbles: true,
11102
+ detail: this.value,
11103
+ }),
11104
+ );
11105
+ };
11106
+ editor.addEventListener("input", handleEditorEvent);
11107
+ editor.addEventListener("change", handleEditorEvent);
11108
+ popup.addEventListener("close", this.#boundPickerClosed);
11109
+ popup.addEventListener("toggle", this.#boundPickerClosed);
11110
+
11111
+ (figGetOverlayRoot() ?? document.body).append(popup);
11112
+ this.#pickerPopup = popup;
11113
+ this.#pickerEditor = editor;
11114
+ this.querySelector(".palette-colors-inline")?.setAttribute(
11115
+ "aria-controls",
11116
+ popup.id,
11117
+ );
11118
+ this.#syncPickerEditor();
11119
+ return popup;
11120
+ }
11121
+
11122
+ #syncPickerEditor() {
11123
+ const editor = this.#pickerEditor;
11124
+ if (!editor) return;
11125
+ editor.setAttribute("value", JSON.stringify(this.#colors));
11126
+ editor.setAttribute("min", String(this.#min));
11127
+ editor.setAttribute("max", String(this.#max));
11128
+ editor.toggleAttribute("fixed", this.#isFixed);
11129
+ const disabled =
11130
+ this.hasAttribute("disabled") &&
11131
+ this.getAttribute("disabled") !== "false";
11132
+ editor.toggleAttribute("disabled", disabled);
11133
+ }
11134
+
11135
+ #syncPickerOpenState() {
11136
+ if (!this.isConnected || this.#editMode !== "picker") return;
11137
+ if (!customElements.get("fig-palette-editor")) {
11138
+ this.#renderPickerWhenAvailable();
11139
+ return;
11140
+ }
11141
+ if (!this.open) {
11142
+ if (this.#pickerPopup) this.#pickerPopup.open = false;
11143
+ return;
11144
+ }
11145
+ const disabled =
11146
+ this.hasAttribute("disabled") &&
11147
+ this.getAttribute("disabled") !== "false";
11148
+ if (disabled) {
11149
+ this.open = false;
11150
+ return;
11151
+ }
11152
+ const popup = this.#ensurePickerPopup();
11153
+ if (!popup) return;
11154
+ this.#syncPickerEditor();
11155
+ popup.open = true;
11156
+ }
11157
+
11158
+ #destroyPickerPopup() {
11159
+ if (!this.#pickerPopup) return;
11160
+ this.#pickerPopup.removeEventListener("close", this.#boundPickerClosed);
11161
+ this.#pickerPopup.removeEventListener("toggle", this.#boundPickerClosed);
11162
+ this.#pickerPopup.open = false;
11163
+ this.#pickerPopup.remove();
11164
+ this.querySelector(".palette-colors-inline")?.removeAttribute(
11165
+ "aria-controls",
11166
+ );
11167
+ this.#pickerPopup = null;
11168
+ this.#pickerEditor = null;
10976
11169
  }
10977
11170
 
10978
11171
  #createPicker(entry, index, disabled, { inline = false } = {}) {
@@ -11158,6 +11351,10 @@ class FigInputPalette extends HTMLElement {
11158
11351
  }
11159
11352
  this.#syncRemoveButtons(disabled);
11160
11353
  this.#syncTriggerState();
11354
+ this.#syncPickerEditor();
11355
+ if (disabled && this.#editMode === "picker" && this.open) {
11356
+ this.open = false;
11357
+ }
11161
11358
  }
11162
11359
 
11163
11360
  #syncTriggerState() {
@@ -11166,13 +11363,26 @@ class FigInputPalette extends HTMLElement {
11166
11363
  const disabled =
11167
11364
  this.hasAttribute("disabled") &&
11168
11365
  this.getAttribute("disabled") !== "false";
11366
+ if (this.#editMode === "false") {
11367
+ trigger.removeAttribute("tabindex");
11368
+ trigger.removeAttribute("aria-disabled");
11369
+ trigger.removeAttribute("aria-expanded");
11370
+ trigger.removeAttribute("aria-haspopup");
11371
+ return;
11372
+ }
11169
11373
  trigger.setAttribute("tabindex", disabled ? "-1" : "+0");
11170
11374
  trigger.setAttribute("aria-disabled", String(disabled));
11171
11375
  trigger.setAttribute("aria-expanded", String(this.open));
11376
+ if (this.#editMode === "picker") {
11377
+ trigger.setAttribute("aria-haspopup", "dialog");
11378
+ } else {
11379
+ trigger.removeAttribute("aria-haspopup");
11380
+ }
11172
11381
  }
11173
11382
 
11174
11383
  focus() {
11175
11384
  if (
11385
+ this.#editMode === "false" ||
11176
11386
  this.hasAttribute("disabled") &&
11177
11387
  this.getAttribute("disabled") !== "false"
11178
11388
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "11.1.6",
3
+ "version": "11.2.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": "SEE LICENSE IN LICENSE",