@rogieking/figui3 3.13.1 → 3.14.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 (3) hide show
  1. package/components.css +49 -23
  2. package/fig.js +340 -157
  3. package/package.json +1 -1
package/components.css CHANGED
@@ -1204,10 +1204,8 @@ fig-chit {
1204
1204
  box-shadow: inset 0 0 0 1px var(--figma-color-border-selected);
1205
1205
  }
1206
1206
 
1207
+ &[size="medium"],
1207
1208
  &[size="large"] {
1208
- --size: 1.75rem;
1209
-
1210
- /* Large size: swatch fills the whole area */
1211
1209
  input[type="color"] {
1212
1210
  padding: 0;
1213
1211
  width: var(--size);
@@ -1233,7 +1231,6 @@ fig-chit {
1233
1231
  inset 0 0 0 3px var(--figma-color-bg);
1234
1232
  }
1235
1233
 
1236
- /* Large gradient/image: also fill the area */
1237
1234
  &[data-type="gradient"]::after,
1238
1235
  &[data-type="image"]::after,
1239
1236
  &[data-type="gradient"]::before,
@@ -1244,6 +1241,14 @@ fig-chit {
1244
1241
  }
1245
1242
  }
1246
1243
 
1244
+ &[size="medium"] {
1245
+ --size: 1.5rem;
1246
+ }
1247
+
1248
+ &[size="large"] {
1249
+ --size: 2rem;
1250
+ }
1251
+
1247
1252
  &[disabled] {
1248
1253
  pointer-events: none;
1249
1254
  }
@@ -3174,14 +3179,12 @@ fig-input-gradient {
3174
3179
  }
3175
3180
  fig-chit {
3176
3181
  flex: 1 1 auto;
3177
- width: 100%;
3178
- min-width: 0;
3182
+ width: 100% !important;
3183
+ min-width: 0 !important;
3179
3184
  &::before,
3180
3185
  &::after {
3181
- width: auto;
3182
- height: auto;
3186
+ width: 100% !important;
3183
3187
  place-self: stretch;
3184
- border-radius: var(--radius-large) !important;
3185
3188
  }
3186
3189
  &[data-type="gradient"]::after {
3187
3190
  width: calc(100% - 0.625rem);
@@ -3193,12 +3196,24 @@ fig-input-gradient {
3193
3196
 
3194
3197
  .fig-input-gradient-track {
3195
3198
  position: absolute;
3196
- inset: calc(var(--spacer-1) + 2px) var(--spacer-2-5);
3199
+ display: flex;
3200
+ align-items: center;
3201
+ inset: 0;
3197
3202
  pointer-events: auto;
3198
3203
 
3199
3204
  fig-handle {
3200
3205
  pointer-events: auto;
3201
3206
  cursor: default;
3207
+
3208
+ &.dragging {
3209
+ fig-color-tip {
3210
+ display: none;
3211
+ }
3212
+ }
3213
+
3214
+ &:hover {
3215
+ z-index: 5;
3216
+ }
3202
3217
  }
3203
3218
  }
3204
3219
  }
@@ -4244,12 +4259,6 @@ fig-preview {
4244
4259
  }
4245
4260
 
4246
4261
  .fig-fill-picker-color-area fig-handle {
4247
- --width: 1rem;
4248
- --height: 1rem;
4249
- --border-radius: 50%;
4250
- --box-shadow:
4251
- inset 0 0 0 0.125rem var(--handle-color),
4252
- 0px 0 0 0.5px rgba(0, 0, 0, 0.1), var(--elevation-100-canvas);
4253
4262
  z-index: 1;
4254
4263
  }
4255
4264
 
@@ -4743,8 +4752,8 @@ fig-choice {
4743
4752
  }
4744
4753
 
4745
4754
  fig-handle {
4746
- --width: 0.75rem;
4747
- --height: 0.75rem;
4755
+ --width: 0.875rem;
4756
+ --height: 0.875rem;
4748
4757
  --fill: var(--figma-color-bg-brand);
4749
4758
  --border-radius: 50%;
4750
4759
  --ring-width: 1.25px;
@@ -4754,15 +4763,27 @@ fig-handle {
4754
4763
  --outline: none;
4755
4764
  --border: none;
4756
4765
 
4757
- display: inline-block;
4766
+ display: inline-grid;
4767
+ place-items: center;
4758
4768
  width: var(--width);
4759
4769
  height: var(--height);
4760
- background: var(--fill);
4770
+ background: var(--handle-color);
4761
4771
  border-radius: var(--border-radius);
4762
4772
  box-shadow: var(--box-shadow);
4763
4773
  outline: var(--outline);
4764
4774
  border: var(--border);
4765
4775
 
4776
+ &::before {
4777
+ content: "";
4778
+ color-scheme: light only;
4779
+ width: calc(var(--width) - 4px);
4780
+ height: calc(var(--height) - 4px);
4781
+ background: var(--fill);
4782
+ border-radius: var(--border-radius);
4783
+ box-shadow: inset 0 0 0 1px var(--figma-color-bordertranslucent);
4784
+ place-self: center;
4785
+ }
4786
+
4766
4787
  &[size="small"] {
4767
4788
  --width: 0.5625rem;
4768
4789
  --height: 0.5625rem;
@@ -4772,10 +4793,10 @@ fig-handle {
4772
4793
  position: absolute;
4773
4794
  touch-action: none;
4774
4795
  }
4775
-
4796
+ &:hover,
4776
4797
  &[selected]:not([selected="false"]) {
4777
4798
  outline: var(--ring-width) solid var(--figma-color-border-selected);
4778
- outline-offset: var(--ring-width);
4799
+ outline-offset: 0;
4779
4800
  }
4780
4801
 
4781
4802
  &[disabled]:not([disabled="false"]),
@@ -4786,7 +4807,7 @@ fig-handle {
4786
4807
  }
4787
4808
 
4788
4809
  &[type="color"],
4789
- &[add]:not([add="false"]) {
4810
+ &[control] {
4790
4811
  overflow: visible;
4791
4812
 
4792
4813
  fig-color-tip {
@@ -4823,6 +4844,11 @@ fig-color-tip {
4823
4844
  drop-shadow(0px 2.5px 6px rgba(0, 0, 0, 0.13))
4824
4845
  drop-shadow(0px 0px 0.5px rgba(0, 0, 0, 0.15));
4825
4846
 
4847
+ &[control="add"],
4848
+ &[control="remove"] {
4849
+ cursor: pointer;
4850
+ }
4851
+
4826
4852
  fig-chit {
4827
4853
  input[type="color"] {
4828
4854
  background: transparent;
package/fig.js CHANGED
@@ -659,7 +659,12 @@ class FigTooltip extends HTMLElement {
659
659
  };
660
660
  }
661
661
 
662
+ get #showPersisted() {
663
+ return this.hasAttribute("show") && this.getAttribute("show") !== "false";
664
+ }
665
+
662
666
  showDelayedPopup() {
667
+ if (this.#showPersisted) return;
663
668
  this.render();
664
669
  clearTimeout(this.timeout);
665
670
  const warm =
@@ -669,10 +674,12 @@ class FigTooltip extends HTMLElement {
669
674
  }
670
675
 
671
676
  showPopup() {
677
+ if (!this.popup) this.render();
678
+ this.popup.style.display = "block";
679
+ this.popup.style.visibility = "hidden";
672
680
  this.#repositionPopup();
673
681
  this.popup.style.opacity = "1";
674
682
  this.popup.style.visibility = "visible";
675
- this.popup.style.display = "block";
676
683
  this.popup.style.pointerEvents = "all";
677
684
  this.popup.style.zIndex = figGetHighestZIndex() + 1;
678
685
 
@@ -725,6 +732,7 @@ class FigTooltip extends HTMLElement {
725
732
  }
726
733
 
727
734
  hidePopup() {
735
+ if (this.#showPersisted) return;
728
736
  clearTimeout(this.timeout);
729
737
  clearTimeout(this.#touchTimeout);
730
738
  this.#stopObserving();
@@ -824,7 +832,28 @@ class FigTooltip extends HTMLElement {
824
832
  }
825
833
 
826
834
  static get observedAttributes() {
827
- return ["action", "delay", "open"];
835
+ return ["action", "delay", "open", "show", "text"];
836
+ }
837
+ get text() {
838
+ return this.getAttribute("text") ?? "";
839
+ }
840
+ set text(value) {
841
+ this.setAttribute("text", value);
842
+ }
843
+ #updateText(value) {
844
+ if (!this.popup) return;
845
+ const content = this.popup.firstElementChild ?? this.popup.firstChild;
846
+ if (!content) return;
847
+ content.innerText = value;
848
+ content.style.width = "";
849
+ const textNode = content.childNodes[0];
850
+ if (textNode) {
851
+ const range = document.createRange();
852
+ range.setStartBefore(textNode);
853
+ range.setEndAfter(textNode);
854
+ content.style.width = `${range.getBoundingClientRect().width}px`;
855
+ }
856
+ if (this.isOpen) this.#repositionPopup();
828
857
  }
829
858
  get open() {
830
859
  return this.hasAttribute("open") && this.getAttribute("open") === "true";
@@ -851,6 +880,17 @@ class FigTooltip extends HTMLElement {
851
880
  });
852
881
  }
853
882
  }
883
+ if (name === "show") {
884
+ const on = newValue !== null && newValue !== "false";
885
+ if (on) {
886
+ this.showPopup();
887
+ } else {
888
+ this.hidePopup();
889
+ }
890
+ }
891
+ if (name === "text") {
892
+ this.#updateText(newValue ?? "");
893
+ }
854
894
  }
855
895
 
856
896
  #hideOnChromeOpen(e) {
@@ -5651,8 +5691,9 @@ customElements.define("fig-input-fill", FigInputFill);
5651
5691
  * @fires change - When the gradient value is committed
5652
5692
  */
5653
5693
  class FigInputGradient extends HTMLElement {
5654
- #fillPicker;
5694
+ #chit;
5655
5695
  #track;
5696
+ #handleDragging = false;
5656
5697
  #colorObserver = null;
5657
5698
  #gradient = {
5658
5699
  type: "linear",
@@ -5670,14 +5711,46 @@ class FigInputGradient extends HTMLElement {
5670
5711
  }
5671
5712
 
5672
5713
  static get observedAttributes() {
5673
- return ["value", "disabled", "experimental", "picker-anchor"];
5714
+ return ["value", "disabled"];
5674
5715
  }
5675
5716
 
5676
5717
  connectedCallback() {
5677
5718
  this.#parseValue();
5678
5719
  this.#render();
5720
+ document.addEventListener("keydown", this.#onKeyDown);
5721
+ }
5722
+
5723
+ disconnectedCallback() {
5724
+ document.removeEventListener("keydown", this.#onKeyDown);
5679
5725
  }
5680
5726
 
5727
+ #onKeyDown = (e) => {
5728
+ if (e.key !== "Delete" && e.key !== "Backspace") return;
5729
+ const active = document.activeElement;
5730
+ if (
5731
+ active &&
5732
+ (active.tagName === "INPUT" ||
5733
+ active.tagName === "TEXTAREA" ||
5734
+ active.isContentEditable)
5735
+ )
5736
+ return;
5737
+ if (this.#gradient.stops.length <= 2) return;
5738
+ if (!this.#track) return;
5739
+ const selected = this.#track.querySelector(
5740
+ "fig-handle[selected]:not(.fig-input-gradient-ghost)",
5741
+ );
5742
+ if (!selected) return;
5743
+ const idx = parseInt(selected.dataset.stopIndex, 10);
5744
+ if (isNaN(idx) || !this.#gradient.stops[idx]) return;
5745
+ e.preventDefault();
5746
+ selected.removeAttribute("selected");
5747
+ this.#gradient.stops.splice(idx, 1);
5748
+ this.#syncHandles();
5749
+ this.#syncChit();
5750
+ this.#emitInput();
5751
+ this.#emitChange();
5752
+ };
5753
+
5681
5754
  #parseValue() {
5682
5755
  const valueAttr = this.getAttribute("value");
5683
5756
  if (!valueAttr) return;
@@ -5701,19 +5774,12 @@ class FigInputGradient extends HTMLElement {
5701
5774
  }
5702
5775
  }
5703
5776
 
5704
- #buildFillPickerAttrs() {
5705
- const attrs = {};
5706
- const experimental = this.getAttribute("experimental");
5707
- if (experimental) attrs["experimental"] = experimental;
5708
- for (const { name, value } of this.attributes) {
5709
- if (name.startsWith("picker-") && name !== "picker-anchor") {
5710
- attrs[name.slice(7)] = value;
5711
- }
5712
- }
5713
- if (!attrs["dialog-position"]) attrs["dialog-position"] = "left";
5714
- return Object.entries(attrs)
5715
- .map(([k, v]) => `${k}="${v}"`)
5716
- .join(" ");
5777
+ #buildGradientCSS() {
5778
+ const sorted = [...this.#gradient.stops].sort(
5779
+ (a, b) => a.position - b.position,
5780
+ );
5781
+ const stops = sorted.map((s) => `${s.color} ${s.position}%`).join(", ");
5782
+ return `linear-gradient(${this.#gradient.angle}deg, ${stops})`;
5717
5783
  }
5718
5784
 
5719
5785
  #buildStopHandles() {
@@ -5721,23 +5787,19 @@ class FigInputGradient extends HTMLElement {
5721
5787
  return this.#gradient.stops
5722
5788
  .map(
5723
5789
  (stop, i) =>
5724
- `<fig-handle drag drag-axes="x" type="color" color="${stop.color}" value="${stop.position}% 50%" data-stop-index="${i}"${disabled ? " disabled" : ""}></fig-handle>`,
5790
+ `<fig-tooltip action="manual" text="${Math.round(stop.position)}%"><fig-handle drag drag-axes="x" drag-surface=".fig-input-gradient-track" type="color" color="${stop.color}" value="${stop.position}% 50%" data-stop-index="${i}"${disabled ? " disabled" : ""}></fig-handle></fig-tooltip>`,
5725
5791
  )
5726
5792
  .join("");
5727
5793
  }
5728
5794
 
5729
5795
  #ghostHandle = null;
5730
- #ghostTooltip = null;
5731
5796
 
5732
5797
  #render() {
5733
5798
  const disabled = this.hasAttribute("disabled");
5734
- const fillPickerValue = JSON.stringify(this.value);
5735
- const fpAttrs = this.#buildFillPickerAttrs();
5736
5799
  this.innerHTML = `
5737
- <fig-fill-picker mode="gradient" ${fpAttrs} value='${fillPickerValue}' ${
5738
- disabled ? "disabled" : ""
5739
- }></fig-fill-picker>
5800
+ <fig-chit size="medium" background="${this.#buildGradientCSS()}"${disabled ? " disabled" : ""}></fig-chit>
5740
5801
  <div class="fig-input-gradient-track">${this.#buildStopHandles()}</div>`;
5802
+ this.#chit = this.querySelector("fig-chit");
5741
5803
  this.#track = this.querySelector(".fig-input-gradient-track");
5742
5804
  this.#setupGhostHandle();
5743
5805
  this.#setupEventListeners();
@@ -5768,22 +5830,31 @@ class FigInputGradient extends HTMLElement {
5768
5830
 
5769
5831
  const ghost = document.createElement("fig-handle");
5770
5832
  ghost.classList.add("fig-input-gradient-ghost");
5771
- ghost.setAttribute("add", "");
5772
5833
  ghost.style.position = "absolute";
5773
5834
  ghost.style.top = "50%";
5774
5835
  ghost.style.transform = "translate(-50%, -50%)";
5775
5836
  ghost.style.pointerEvents = "none";
5776
5837
  ghost.style.opacity = "0";
5777
5838
  ghost.style.transition = "opacity 0.15s";
5839
+ ghost.style.overflow = "visible";
5840
+
5841
+ const tip = document.createElement("fig-color-tip");
5842
+ tip.setAttribute("control", "add");
5843
+ tip.style.position = "absolute";
5844
+ tip.style.bottom = "calc(100% + 6px)";
5845
+ tip.style.left = "50%";
5846
+ tip.style.transform = "translateX(-50%)";
5847
+ tip.style.zIndex = "10";
5848
+ ghost.appendChild(tip);
5778
5849
 
5779
5850
  this.#track.appendChild(ghost);
5780
5851
  this.#ghostHandle = ghost;
5781
- this.#ghostTooltip = null;
5782
5852
 
5783
5853
  this.addEventListener("pointerenter", this.#onTrackEnter);
5784
5854
  this.addEventListener("pointermove", this.#onTrackMove);
5785
5855
  this.addEventListener("pointerleave", this.#onTrackLeave);
5786
5856
  this.addEventListener("click", this.#onTrackClick);
5857
+ this.addEventListener("dblclick", this.#onTrackDblClick);
5787
5858
  }
5788
5859
 
5789
5860
  #showGhost() {
@@ -5797,6 +5868,7 @@ class FigInputGradient extends HTMLElement {
5797
5868
  }
5798
5869
 
5799
5870
  #onTrackEnter = () => {
5871
+ if (this.#handleDragging) return;
5800
5872
  this.#showGhost();
5801
5873
  };
5802
5874
 
@@ -5805,6 +5877,10 @@ class FigInputGradient extends HTMLElement {
5805
5877
  };
5806
5878
 
5807
5879
  #onTrackMove = (e) => {
5880
+ if (this.#handleDragging) {
5881
+ this.#hideGhost();
5882
+ return;
5883
+ }
5808
5884
  if (!this.#ghostHandle || !this.#track) return;
5809
5885
  if (e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) {
5810
5886
  this.#hideGhost();
@@ -5821,9 +5897,37 @@ class FigInputGradient extends HTMLElement {
5821
5897
  this.#showGhost();
5822
5898
  };
5823
5899
 
5900
+ #distributeStops() {
5901
+ const count = this.#gradient.stops.length;
5902
+ if (count < 2) return;
5903
+ for (let i = 0; i < count; i++) {
5904
+ this.#gradient.stops[i].position = Math.round((i / (count - 1)) * 100);
5905
+ }
5906
+ this.#syncHandles();
5907
+ this.#syncChit();
5908
+ this.#emitInput();
5909
+ this.#emitChange();
5910
+ this.#track?.querySelectorAll("fig-handle[selected]").forEach((h) => {
5911
+ h.removeAttribute("selected");
5912
+ });
5913
+ }
5914
+
5915
+ #onTrackDblClick = (e) => {
5916
+ if (!this.#track) return;
5917
+ if (!e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) return;
5918
+ this.#distributeStops();
5919
+ };
5920
+
5824
5921
  #onTrackClick = (e) => {
5825
5922
  if (!this.#track) return;
5826
- if (e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) return;
5923
+ if (this.#handleDragging) return;
5924
+ if (e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) {
5925
+ if (e.shiftKey) {
5926
+ this.#distributeStops();
5927
+ e.stopPropagation();
5928
+ }
5929
+ return;
5930
+ }
5827
5931
  const trackRect = this.#track.getBoundingClientRect();
5828
5932
  const pct = Math.max(
5829
5933
  0,
@@ -5837,7 +5941,7 @@ class FigInputGradient extends HTMLElement {
5837
5941
  (s) => s.position === position && s.color === color,
5838
5942
  );
5839
5943
  this.#syncHandles();
5840
- this.#syncFillPicker();
5944
+ this.#syncChit();
5841
5945
  this.#emitInput();
5842
5946
  this.#emitChange();
5843
5947
 
@@ -5870,6 +5974,8 @@ class FigInputGradient extends HTMLElement {
5870
5974
  const stop = stops[i];
5871
5975
  h.setAttribute("value", `${stop.position}% 50%`);
5872
5976
  h.setAttribute("color", stop.color);
5977
+ const tip = h.closest("fig-tooltip");
5978
+ if (tip) tip.setAttribute("text", `${Math.round(stop.position)}%`);
5873
5979
  }
5874
5980
  }
5875
5981
 
@@ -5886,92 +5992,155 @@ class FigInputGradient extends HTMLElement {
5886
5992
  });
5887
5993
  }
5888
5994
 
5889
- #syncFillPicker() {
5890
- if (!this.#fillPicker) return;
5891
- this.#fillPicker.setAttribute("value", JSON.stringify(this.value));
5995
+ #syncStopIndices() {
5996
+ if (!this.#track) return;
5997
+ const handles = this.#track.querySelectorAll(
5998
+ "fig-handle:not(.fig-input-gradient-ghost)",
5999
+ );
6000
+ const stops = this.#gradient.stops;
6001
+ const used = new Set();
6002
+ handles.forEach((h) => {
6003
+ const pos = Math.round(parseFloat(h.getAttribute("value")) || 0);
6004
+ const color = (h.getAttribute("color") || "").toUpperCase();
6005
+ let best = -1;
6006
+ for (let i = 0; i < stops.length; i++) {
6007
+ if (used.has(i)) continue;
6008
+ if (
6009
+ stops[i].position === pos &&
6010
+ stops[i].color.toUpperCase() === color
6011
+ ) {
6012
+ best = i;
6013
+ break;
6014
+ }
6015
+ }
6016
+ if (best === -1) {
6017
+ let minDist = Infinity;
6018
+ for (let i = 0; i < stops.length; i++) {
6019
+ if (used.has(i)) continue;
6020
+ const d = Math.abs(stops[i].position - pos);
6021
+ if (d < minDist) {
6022
+ minDist = d;
6023
+ best = i;
6024
+ }
6025
+ }
6026
+ }
6027
+ if (best !== -1) {
6028
+ used.add(best);
6029
+ h.dataset.stopIndex = best;
6030
+ }
6031
+ });
6032
+ }
6033
+
6034
+ #syncChit() {
6035
+ if (!this.#chit) return;
6036
+ this.#chit.setAttribute("background", this.#buildGradientCSS());
5892
6037
  }
5893
6038
 
5894
6039
  #setupEventListeners() {
5895
- requestAnimationFrame(() => {
5896
- this.#fillPicker = this.querySelector("fig-fill-picker");
5897
- if (!this.#fillPicker) return;
6040
+ if (!this.#track) return;
5898
6041
 
5899
- const anchor = this.getAttribute("picker-anchor");
5900
- if (!anchor || anchor === "self") {
5901
- this.#fillPicker.anchorElement = this;
6042
+ this.#track.addEventListener("input", (e) => {
6043
+ const handle = e.target.closest("fig-handle");
6044
+ if (!handle) return;
6045
+ e.stopPropagation();
6046
+ if (!this.#handleDragging) handle.style.zIndex = "5";
6047
+ this.#handleDragging = true;
6048
+ const idx = parseInt(handle.dataset.stopIndex, 10);
6049
+ if (isNaN(idx) || !this.#gradient.stops[idx]) return;
6050
+ const px = e.detail?.px ?? 0;
6051
+ const rawPosition = Math.round(px * 100);
6052
+ let position = rawPosition;
6053
+ const trackW = this.#track.getBoundingClientRect().width;
6054
+ if (e.detail?.shiftKey) {
6055
+ position = Math.round(position / 10) * 10;
5902
6056
  } else {
5903
- const el = document.querySelector(anchor);
5904
- if (el) this.#fillPicker.anchorElement = el;
6057
+ const snapPct = trackW > 0 ? (5 / trackW) * 100 : 0;
6058
+ for (let i = 0; i < this.#gradient.stops.length; i++) {
6059
+ if (i === idx) continue;
6060
+ if (
6061
+ Math.abs(this.#gradient.stops[i].position - position) <= snapPct
6062
+ ) {
6063
+ position = this.#gradient.stops[i].position;
6064
+ break;
6065
+ }
6066
+ }
6067
+ }
6068
+ this.#gradient.stops[idx].position = position;
6069
+ if (position !== rawPosition) {
6070
+ handle.style.left = `${(position / 100) * trackW - handle.offsetWidth / 2}px`;
5905
6071
  }
6072
+ const tooltip = handle.closest("fig-tooltip");
6073
+ if (tooltip) {
6074
+ tooltip.text = `${Math.round(position)}%`;
6075
+ if (!tooltip.hasAttribute("show")) tooltip.setAttribute("show", "true");
6076
+ }
6077
+ this.#syncChit();
6078
+ this.#emitInput();
6079
+ });
5906
6080
 
5907
- this.#fillPicker.addEventListener("input", (e) => {
5908
- e.stopPropagation();
5909
- const detail = e.detail;
5910
- if (detail?.gradient) {
5911
- this.#gradient = normalizeGradientConfig({
5912
- ...this.#gradient,
5913
- ...detail.gradient,
5914
- });
5915
- this.#syncHandles();
5916
- this.#emitInput();
6081
+ this.#track.addEventListener("change", (e) => {
6082
+ const handle = e.target.closest("fig-handle");
6083
+ if (!handle) return;
6084
+ e.stopPropagation();
6085
+ handle.style.zIndex = "";
6086
+ const tooltip = handle.closest("fig-tooltip");
6087
+ if (tooltip) tooltip.removeAttribute("show");
6088
+ const idx = parseInt(handle.dataset.stopIndex, 10);
6089
+ if (isNaN(idx) || !this.#gradient.stops[idx]) return;
6090
+ const px = e.detail?.px ?? 0;
6091
+ let position = Math.round(px * 100);
6092
+ const trackW = this.#track.getBoundingClientRect().width;
6093
+ const snapPct = trackW > 0 ? (5 / trackW) * 100 : 0;
6094
+ for (let i = 0; i < this.#gradient.stops.length; i++) {
6095
+ if (i === idx) continue;
6096
+ if (Math.abs(this.#gradient.stops[i].position - position) <= snapPct) {
6097
+ position = this.#gradient.stops[i].position;
6098
+ break;
5917
6099
  }
6100
+ }
6101
+ this.#gradient.stops[idx].position = position;
6102
+ handle.style.left = `${(position / 100) * trackW - handle.offsetWidth / 2}px`;
6103
+ this.#gradient.stops.sort((a, b) => a.position - b.position);
6104
+ this.#syncStopIndices();
6105
+ this.#syncChit();
6106
+ this.#emitChange();
6107
+ const swallow = (evt) => {
6108
+ evt.stopPropagation();
6109
+ evt.preventDefault();
6110
+ };
6111
+ this.#track.addEventListener("click", swallow, {
6112
+ capture: true,
6113
+ once: true,
5918
6114
  });
5919
-
5920
- this.#fillPicker.addEventListener("change", (e) => {
5921
- e.stopPropagation();
5922
- this.#emitChange();
6115
+ requestAnimationFrame(() => {
6116
+ this.#handleDragging = false;
6117
+ this.#track.removeEventListener("click", swallow, { capture: true });
5923
6118
  });
6119
+ });
5924
6120
 
5925
- if (this.#track) {
5926
- this.#track.addEventListener("input", (e) => {
5927
- const handle = e.target.closest("fig-handle");
5928
- if (!handle) return;
5929
- e.stopPropagation();
5930
- const idx = parseInt(handle.dataset.stopIndex, 10);
5931
- if (isNaN(idx) || !this.#gradient.stops[idx]) return;
5932
- const px = e.detail?.px ?? 0;
5933
- this.#gradient.stops[idx].position = Math.round(px * 100);
5934
- this.#syncFillPicker();
6121
+ this.#colorObserver = new MutationObserver((mutations) => {
6122
+ for (const m of mutations) {
6123
+ if (m.attributeName !== "color") continue;
6124
+ const handle = m.target;
6125
+ if (handle.classList.contains("fig-input-gradient-ghost")) continue;
6126
+ const idx = parseInt(handle.dataset.stopIndex, 10);
6127
+ if (isNaN(idx) || !this.#gradient.stops[idx]) continue;
6128
+ const newColor = handle.getAttribute("color");
6129
+ if (newColor && newColor !== this.#gradient.stops[idx].color) {
6130
+ this.#gradient.stops[idx].color = newColor;
6131
+ this.#syncChit();
5935
6132
  this.#emitInput();
5936
- });
5937
-
5938
- this.#track.addEventListener("change", (e) => {
5939
- const handle = e.target.closest("fig-handle");
5940
- if (!handle) return;
5941
- e.stopPropagation();
5942
- const idx = parseInt(handle.dataset.stopIndex, 10);
5943
- if (isNaN(idx) || !this.#gradient.stops[idx]) return;
5944
- const px = e.detail?.px ?? 0;
5945
- this.#gradient.stops[idx].position = Math.round(px * 100);
5946
- this.#syncFillPicker();
5947
- this.#emitChange();
5948
- });
5949
-
5950
- this.#colorObserver = new MutationObserver((mutations) => {
5951
- for (const m of mutations) {
5952
- if (m.attributeName !== "color") continue;
5953
- const handle = m.target;
5954
- if (handle.classList.contains("fig-input-gradient-ghost")) continue;
5955
- const idx = parseInt(handle.dataset.stopIndex, 10);
5956
- if (isNaN(idx) || !this.#gradient.stops[idx]) continue;
5957
- const newColor = handle.getAttribute("color");
5958
- if (newColor && newColor !== this.#gradient.stops[idx].color) {
5959
- this.#gradient.stops[idx].color = newColor;
5960
- this.#syncFillPicker();
5961
- this.#emitInput();
5962
- }
5963
- }
5964
- });
5965
- this.#track
5966
- .querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)")
5967
- .forEach((h) => {
5968
- this.#colorObserver.observe(h, {
5969
- attributes: true,
5970
- attributeFilter: ["color"],
5971
- });
5972
- });
6133
+ }
5973
6134
  }
5974
6135
  });
6136
+ this.#track
6137
+ .querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)")
6138
+ .forEach((h) => {
6139
+ this.#colorObserver.observe(h, {
6140
+ attributes: true,
6141
+ attributeFilter: ["color"],
6142
+ });
6143
+ });
5975
6144
  }
5976
6145
 
5977
6146
  #emitInput() {
@@ -6012,15 +6181,11 @@ class FigInputGradient extends HTMLElement {
6012
6181
  switch (name) {
6013
6182
  case "value":
6014
6183
  this.#parseValue();
6015
- if (this.#fillPicker) {
6016
- this.#syncFillPicker();
6017
- }
6184
+ this.#syncChit();
6018
6185
  this.#syncHandles();
6019
6186
  break;
6020
6187
  case "disabled":
6021
- case "experimental":
6022
- case "picker-anchor":
6023
- if (this.#fillPicker) this.#render();
6188
+ this.#render();
6024
6189
  break;
6025
6190
  }
6026
6191
  }
@@ -10295,6 +10460,7 @@ class FigFillPicker extends HTMLElement {
10295
10460
  drag
10296
10461
  drag-surface=".fig-fill-picker-color-area"
10297
10462
  drag-axes="x,y"
10463
+ drag-snapping="modifier"
10298
10464
  ></fig-handle>
10299
10465
  </fig-preview>
10300
10466
  <div class="fig-fill-picker-sliders">
@@ -11882,12 +12048,11 @@ class FigColorTip extends HTMLElement {
11882
12048
  #boundHandleChange = this.#handlePickerChange.bind(this);
11883
12049
 
11884
12050
  static get observedAttributes() {
11885
- return ["value", "selected", "disabled", "alpha", "add"];
12051
+ return ["value", "selected", "disabled", "alpha", "control"];
11886
12052
  }
11887
12053
 
11888
- get #isAddMode() {
11889
- const v = this.getAttribute("add");
11890
- return v !== null && v !== "false";
12054
+ get #controlMode() {
12055
+ return this.getAttribute("control") || "color";
11891
12056
  }
11892
12057
 
11893
12058
  connectedCallback() {
@@ -11897,7 +12062,7 @@ class FigColorTip extends HTMLElement {
11897
12062
 
11898
12063
  disconnectedCallback() {
11899
12064
  this.#teardownListeners();
11900
- this.removeEventListener("click", this.#handleAddClick);
12065
+ this.removeEventListener("click", this.#handleControlClick);
11901
12066
  }
11902
12067
 
11903
12068
  #teardownListeners() {
@@ -11922,14 +12087,16 @@ class FigColorTip extends HTMLElement {
11922
12087
  }
11923
12088
 
11924
12089
  #render() {
11925
- if (this.#isAddMode) {
11926
- this.innerHTML = `<span class="fig-mask-icon" style="--icon: var(--icon-add)"></span>`;
12090
+ const mode = this.#controlMode;
12091
+ if (mode === "add" || mode === "remove") {
12092
+ const icon = mode === "add" ? "var(--icon-add)" : "var(--icon-minus)";
12093
+ this.innerHTML = `<fig-button icon variant="ghost"><span class="fig-mask-icon" style="--icon: ${icon}"></span></fig-button>`;
11927
12094
  this.#fillPicker = null;
11928
12095
  this.#chit = null;
11929
- this.addEventListener("click", this.#handleAddClick);
12096
+ this.addEventListener("click", this.#handleControlClick);
11930
12097
  return;
11931
12098
  }
11932
- this.removeEventListener("click", this.#handleAddClick);
12099
+ this.removeEventListener("click", this.#handleControlClick);
11933
12100
 
11934
12101
  const color = this.#normalizeColor(this.getAttribute("value"));
11935
12102
  const alphaAttr = this.#alphaEnabled ? "" : 'alpha="false"';
@@ -11949,9 +12116,10 @@ class FigColorTip extends HTMLElement {
11949
12116
  });
11950
12117
  }
11951
12118
 
11952
- #handleAddClick = () => {
12119
+ #handleControlClick = () => {
12120
+ const mode = this.#controlMode;
11953
12121
  this.dispatchEvent(
11954
- new CustomEvent("add", { bubbles: true, composed: true }),
12122
+ new CustomEvent(mode, { bubbles: true, composed: true }),
11955
12123
  );
11956
12124
  };
11957
12125
 
@@ -12083,7 +12251,7 @@ class FigColorTip extends HTMLElement {
12083
12251
  if (!this.isConnected) return;
12084
12252
 
12085
12253
  switch (name) {
12086
- case "add":
12254
+ case "control":
12087
12255
  this.#render();
12088
12256
  break;
12089
12257
  case "value":
@@ -12695,7 +12863,7 @@ class FigHandle extends HTMLElement {
12695
12863
  "drag-snapping",
12696
12864
  "value",
12697
12865
  "type",
12698
- "add",
12866
+ "control",
12699
12867
  ];
12700
12868
 
12701
12869
  #isDragging = false;
@@ -12704,8 +12872,16 @@ class FigHandle extends HTMLElement {
12704
12872
  #applyingValue = false;
12705
12873
  #colorTip = null;
12706
12874
 
12707
- get #isAddMode() {
12708
- return this.hasAttribute("add") && this.getAttribute("add") !== "false";
12875
+ get #controlMode() {
12876
+ return this.getAttribute("control") || null;
12877
+ }
12878
+
12879
+ get #hasControlMode() {
12880
+ return this.#controlMode === "add" || this.#controlMode === "remove";
12881
+ }
12882
+
12883
+ get #isGhost() {
12884
+ return this.classList.contains("fig-input-gradient-ghost");
12709
12885
  }
12710
12886
 
12711
12887
  get #dragEnabled() {
@@ -12838,7 +13014,7 @@ class FigHandle extends HTMLElement {
12838
13014
  document.addEventListener("pointerdown", this.#handleDeselect);
12839
13015
  const initial = this.getAttribute("value");
12840
13016
  if (initial) this.#applyValue(initial);
12841
- if (this.#isAddMode) this.#showColorTip();
13017
+ if (this.#hasControlMode && !this.#isGhost) this.#showColorTip();
12842
13018
  }
12843
13019
 
12844
13020
  disconnectedCallback() {
@@ -12850,7 +13026,7 @@ class FigHandle extends HTMLElement {
12850
13026
 
12851
13027
  #handleSelect = (e) => {
12852
13028
  if (this.hasAttribute("disabled")) return;
12853
- if (this.#isAddMode) return;
13029
+ if (this.#hasControlMode) return;
12854
13030
  if (this.#didDrag) {
12855
13031
  this.#didDrag = false;
12856
13032
  return;
@@ -12860,7 +13036,7 @@ class FigHandle extends HTMLElement {
12860
13036
  };
12861
13037
 
12862
13038
  #handleDeselect = (e) => {
12863
- if (this.#isAddMode) return;
13039
+ if (this.#hasControlMode) return;
12864
13040
  if (this.contains(e.target)) return;
12865
13041
  if (this.#colorTip && e.target.closest?.("dialog, [popover]")) return;
12866
13042
  this.removeAttribute("selected");
@@ -12869,7 +13045,7 @@ class FigHandle extends HTMLElement {
12869
13045
 
12870
13046
  attributeChangedCallback(name, _old, value) {
12871
13047
  if (name === "color") {
12872
- if (!value || value === "false") {
13048
+ if (!value || value === "false" || value === "true") {
12873
13049
  this.style.removeProperty("--fill");
12874
13050
  } else {
12875
13051
  this.style.setProperty("--fill", value);
@@ -12879,8 +13055,9 @@ class FigHandle extends HTMLElement {
12879
13055
  if (name === "value" && !this.#applyingValue && !this.#isDragging) {
12880
13056
  this.#applyValue(value);
12881
13057
  }
12882
- if (name === "add") {
12883
- if (this.#isAddMode) {
13058
+ if (name === "control" && !this.#isGhost) {
13059
+ if (this.#hasControlMode) {
13060
+ this.#hideColorTip();
12884
13061
  this.#showColorTip();
12885
13062
  } else {
12886
13063
  this.#hideColorTip();
@@ -12962,28 +13139,21 @@ class FigHandle extends HTMLElement {
12962
13139
  }
12963
13140
  };
12964
13141
 
12965
- const isColorType = this.getAttribute("type") === "color";
12966
- if (!isColorType) {
12967
- clampAndApply(e.clientX, e.clientY, e.shiftKey);
12968
- }
12969
- this.style.cursor = "grabbing";
12970
- if (!isColorType) {
12971
- this.dispatchEvent(
12972
- new CustomEvent("input", {
12973
- bubbles: true,
12974
- detail: this.#positionDetail(containerRect),
12975
- }),
12976
- );
12977
- }
12978
-
12979
13142
  const onMove = (e) => {
12980
13143
  if (!this.#isDragging) return;
13144
+ if (!this.#didDrag) {
13145
+ this.classList.add("dragging");
13146
+ this.style.cursor = "grabbing";
13147
+ }
12981
13148
  this.#didDrag = true;
12982
13149
  clampAndApply(e.clientX, e.clientY, e.shiftKey);
12983
13150
  this.dispatchEvent(
12984
13151
  new CustomEvent("input", {
12985
13152
  bubbles: true,
12986
- detail: this.#positionDetail(container.getBoundingClientRect()),
13153
+ detail: {
13154
+ ...this.#positionDetail(container.getBoundingClientRect()),
13155
+ shiftKey: e.shiftKey,
13156
+ },
12987
13157
  }),
12988
13158
  );
12989
13159
  };
@@ -12991,18 +13161,30 @@ class FigHandle extends HTMLElement {
12991
13161
  const onUp = (e) => {
12992
13162
  this.#isDragging = false;
12993
13163
  this.style.cursor = "";
13164
+ this.classList.remove("dragging");
12994
13165
  window.removeEventListener("pointermove", onMove);
12995
13166
  window.removeEventListener("pointerup", onUp);
12996
- if (this.#didDrag || !isColorType) {
13167
+ if (this.#didDrag) {
12997
13168
  clampAndApply(e.clientX, e.clientY, e.shiftKey);
13169
+ this.#syncValueAttribute();
13170
+ this.dispatchEvent(
13171
+ new CustomEvent("change", {
13172
+ bubbles: true,
13173
+ detail: this.#positionDetail(container.getBoundingClientRect()),
13174
+ }),
13175
+ );
13176
+ const swallowClick = (evt) => {
13177
+ evt.stopPropagation();
13178
+ evt.preventDefault();
13179
+ };
13180
+ this.addEventListener("click", swallowClick, {
13181
+ capture: true,
13182
+ once: true,
13183
+ });
13184
+ } else {
13185
+ this.#syncValueAttribute();
12998
13186
  }
12999
- this.#syncValueAttribute();
13000
- this.dispatchEvent(
13001
- new CustomEvent("change", {
13002
- bubbles: true,
13003
- detail: this.#positionDetail(container.getBoundingClientRect()),
13004
- }),
13005
- );
13187
+ this.#didDrag = false;
13006
13188
  };
13007
13189
 
13008
13190
  window.addEventListener("pointermove", onMove);
@@ -13012,17 +13194,17 @@ class FigHandle extends HTMLElement {
13012
13194
  #showColorTip() {
13013
13195
  if (this.#colorTip) return;
13014
13196
  const tip = document.createElement("fig-color-tip");
13015
- if (this.#isAddMode) {
13016
- tip.setAttribute("add", "");
13197
+ if (this.#hasControlMode) {
13198
+ tip.setAttribute("control", this.#controlMode);
13017
13199
  } else {
13018
13200
  tip.setAttribute("value", this.getAttribute("color") || "#D9D9D9");
13019
13201
  tip.setAttribute("alpha", "true");
13020
13202
  tip.setAttribute("selected", "");
13021
13203
  }
13022
- tip.addEventListener("pointerdown", (e) => e.stopPropagation());
13023
13204
  tip.addEventListener("input", this.#handleColorTipInput);
13024
13205
  tip.addEventListener("change", this.#handleColorTipChange);
13025
- tip.addEventListener("add", this.#handleColorTipAdd);
13206
+ tip.addEventListener("add", this.#handleColorTipControl);
13207
+ tip.addEventListener("remove", this.#handleColorTipControl);
13026
13208
  this.appendChild(tip);
13027
13209
  this.#colorTip = tip;
13028
13210
  }
@@ -13031,7 +13213,8 @@ class FigHandle extends HTMLElement {
13031
13213
  if (!this.#colorTip) return;
13032
13214
  this.#colorTip.removeEventListener("input", this.#handleColorTipInput);
13033
13215
  this.#colorTip.removeEventListener("change", this.#handleColorTipChange);
13034
- this.#colorTip.removeEventListener("add", this.#handleColorTipAdd);
13216
+ this.#colorTip.removeEventListener("add", this.#handleColorTipControl);
13217
+ this.#colorTip.removeEventListener("remove", this.#handleColorTipControl);
13035
13218
  this.#colorTip.remove();
13036
13219
  this.#colorTip = null;
13037
13220
  }
@@ -13046,10 +13229,10 @@ class FigHandle extends HTMLElement {
13046
13229
  if (e.detail?.color) this.setAttribute("color", e.detail.color);
13047
13230
  };
13048
13231
 
13049
- #handleColorTipAdd = (e) => {
13232
+ #handleColorTipControl = (e) => {
13050
13233
  e.stopPropagation();
13051
13234
  this.dispatchEvent(
13052
- new CustomEvent("add", { bubbles: true, composed: true }),
13235
+ new CustomEvent(e.type, { bubbles: true, composed: true }),
13053
13236
  );
13054
13237
  };
13055
13238
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "3.13.1",
3
+ "version": "3.14.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",