@rogieking/figui3 5.1.0 → 6.0.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-editor.js CHANGED
@@ -1,6 +1,62 @@
1
- import { gradientToValueShape, gradientInterpolationClause, normalizeGradientConfig } from "./fig.js";
2
-
3
1
  // FigFillPicker
2
+ const GRADIENT_INTERPOLATION_SPACES = [
3
+ "srgb",
4
+ "srgb-linear",
5
+ "display-p3",
6
+ "oklab",
7
+ "oklch",
8
+ ];
9
+ const GRADIENT_HUE_INTERPOLATIONS = [
10
+ "shorter",
11
+ "longer",
12
+ "increasing",
13
+ "decreasing",
14
+ ];
15
+
16
+ function normalizeGradientConfig(gradient) {
17
+ const next = { ...(gradient ?? {}) };
18
+ let interpolationSpace = String(
19
+ next.interpolationSpace ?? "oklab",
20
+ ).toLowerCase();
21
+ if (!GRADIENT_INTERPOLATION_SPACES.includes(interpolationSpace)) {
22
+ interpolationSpace = "oklab";
23
+ }
24
+ if (interpolationSpace === "srgb" || interpolationSpace === "display-p3") {
25
+ interpolationSpace = "oklab";
26
+ }
27
+ next.interpolationSpace = interpolationSpace;
28
+
29
+ const hueInterpolation = String(
30
+ next.hueInterpolation ?? "shorter",
31
+ ).toLowerCase();
32
+ next.hueInterpolation = GRADIENT_HUE_INTERPOLATIONS.includes(hueInterpolation)
33
+ ? hueInterpolation
34
+ : "shorter";
35
+ return next;
36
+ }
37
+
38
+ function gradientToValueShape(gradient) {
39
+ const normalized = normalizeGradientConfig(gradient);
40
+ const output = {
41
+ ...normalized,
42
+ interpolationSpace: normalized.interpolationSpace,
43
+ };
44
+ if (normalized.interpolationSpace === "oklch") {
45
+ output.hueInterpolation = normalized.hueInterpolation;
46
+ } else {
47
+ delete output.hueInterpolation;
48
+ }
49
+ return output;
50
+ }
51
+
52
+ function gradientInterpolationClause(gradient) {
53
+ const normalized = normalizeGradientConfig(gradient);
54
+ if (normalized.interpolationSpace === "oklch") {
55
+ return `in oklch ${normalized.hueInterpolation} hue`;
56
+ }
57
+ return `in ${normalized.interpolationSpace}`;
58
+ }
59
+
4
60
  /**
5
61
  * A comprehensive fill picker component supporting solid colors, gradients, images, video, and webcam.
6
62
  * Uses display: contents and wraps a trigger element that opens a dialog picker.
@@ -1079,7 +1135,7 @@ class FigFillPicker extends HTMLElement {
1079
1135
  </fig-tooltip>
1080
1136
  </fig-field>
1081
1137
  <fig-preview class="fig-fill-picker-gradient-preview">
1082
- <fig-input-gradient class="fig-fill-picker-gradient-bar-input" edit="true" size="large" value='${JSON.stringify({ type: "gradient", gradient: gradientToValueShape(this.#gradient) })}'></fig-input-gradient>
1138
+ <fig-input-gradient class="fig-fill-picker-gradient-bar-input" edit="true" mode="tip" size="large" value='${JSON.stringify({ type: "gradient", gradient: gradientToValueShape(this.#gradient) })}'></fig-input-gradient>
1083
1139
  </fig-preview>
1084
1140
  <fig-field class="fig-fill-picker-gradient-interpolation">
1085
1141
  <label>Mixing</label>
package/fig.js CHANGED
@@ -3632,6 +3632,8 @@ customElements.define("fig-options", FigOptions);
3632
3632
  class FigSlider extends HTMLElement {
3633
3633
  #isInteracting = false;
3634
3634
  #showEmptyTextValue = false;
3635
+ #isSyncingValueAttribute = false;
3636
+ #value = "";
3635
3637
  // Private fields declarations
3636
3638
  #typeDefaults = {
3637
3639
  range: { min: 0, max: 100, step: 1 },
@@ -3826,6 +3828,38 @@ class FigSlider extends HTMLElement {
3826
3828
  this.#regenerateInnerHTML();
3827
3829
  }
3828
3830
 
3831
+ get value() {
3832
+ if (this.#value !== "") return this.#value;
3833
+ const rawValue = this.getAttribute("value");
3834
+ if (rawValue !== null) return String(this.#normalizeSliderValue(rawValue));
3835
+ return "";
3836
+ }
3837
+
3838
+ set value(value) {
3839
+ const rawValue = value === null || value === undefined ? "" : String(value);
3840
+ const hasParsedBounds = this.min !== undefined || this.max !== undefined;
3841
+ const normalized = hasParsedBounds
3842
+ ? String(this.#normalizeSliderValue(rawValue))
3843
+ : rawValue;
3844
+ this.#value = normalized;
3845
+ if (this.getAttribute("value") !== normalized) {
3846
+ this.#isSyncingValueAttribute = true;
3847
+ this.setAttribute("value", normalized);
3848
+ this.#isSyncingValueAttribute = false;
3849
+ }
3850
+ if (this.input && this.input.value !== normalized) {
3851
+ this.input.value = normalized;
3852
+ this.input.setAttribute("aria-valuenow", normalized);
3853
+ }
3854
+ if (this.figInputNumber) {
3855
+ this.figInputNumber.setAttribute(
3856
+ "value",
3857
+ this.#showEmptyTextValue ? "" : normalized,
3858
+ );
3859
+ }
3860
+ if (this.input) this.#syncProperties();
3861
+ }
3862
+
3829
3863
  disconnectedCallback() {
3830
3864
  if (this.input) {
3831
3865
  this.input.removeEventListener("input", this.#boundHandleInput);
@@ -3978,6 +4012,7 @@ class FigSlider extends HTMLElement {
3978
4012
  }
3979
4013
 
3980
4014
  attributeChangedCallback(name, oldValue, newValue) {
4015
+ if (name === "value" && this.#isSyncingValueAttribute) return;
3981
4016
  if (this.input) {
3982
4017
  switch (name) {
3983
4018
  case "color":
@@ -5815,7 +5850,7 @@ const GRADIENT_HUE_INTERPOLATIONS = [
5815
5850
 
5816
5851
  const GRADIENT_PICKER_SPACES = ["srgb-linear", "oklab", "oklch"];
5817
5852
 
5818
- export function normalizeGradientConfig(gradient) {
5853
+ function normalizeGradientConfig(gradient) {
5819
5854
  const next = { ...(gradient ?? {}) };
5820
5855
  let interpolationSpace = String(
5821
5856
  next.interpolationSpace ?? "oklab",
@@ -5837,7 +5872,7 @@ export function normalizeGradientConfig(gradient) {
5837
5872
  return next;
5838
5873
  }
5839
5874
 
5840
- export function gradientToValueShape(gradient) {
5875
+ function gradientToValueShape(gradient) {
5841
5876
  const normalized = normalizeGradientConfig(gradient);
5842
5877
  const output = {
5843
5878
  ...normalized,
@@ -5851,7 +5886,7 @@ export function gradientToValueShape(gradient) {
5851
5886
  return output;
5852
5887
  }
5853
5888
 
5854
- export function gradientInterpolationClause(gradient) {
5889
+ function gradientInterpolationClause(gradient) {
5855
5890
  const normalized = normalizeGradientConfig(gradient);
5856
5891
  if (normalized.interpolationSpace === "oklch") {
5857
5892
  return `in oklch ${normalized.hueInterpolation} hue`;
@@ -7239,7 +7274,7 @@ class FigInputGradient extends HTMLElement {
7239
7274
  }
7240
7275
 
7241
7276
  static get observedAttributes() {
7242
- return ["value", "disabled", "edit"];
7277
+ return ["value", "disabled", "edit", "mode"];
7243
7278
  }
7244
7279
 
7245
7280
  get #editMode() {
@@ -7253,6 +7288,10 @@ class FigInputGradient extends HTMLElement {
7253
7288
  return this.#editMode === "true";
7254
7289
  }
7255
7290
 
7291
+ get #stopHandleMode() {
7292
+ return this.getAttribute("mode") === "tip" ? "tip" : "handle";
7293
+ }
7294
+
7256
7295
  connectedCallback() {
7257
7296
  this.#parseValue();
7258
7297
  this.#render();
@@ -7404,10 +7443,11 @@ class FigInputGradient extends HTMLElement {
7404
7443
 
7405
7444
  #buildStopHandles() {
7406
7445
  const disabled = this.hasAttribute("disabled");
7446
+ const tipAttr = this.#stopHandleMode === "tip" ? ' tip="color"' : "";
7407
7447
  return this.#gradient.stops
7408
7448
  .map(
7409
7449
  (stop, i) =>
7410
- `<fig-tooltip action="manual" text="${Math.round(stop.position)}%"><fig-handle drag drag-axes="x" drag-surface=".fig-input-gradient-track" type="color" color-tip color="${this.#stopColorCSS(stop)}" value="${stop.position}% 50%" hit-area="4" data-stop-index="${i}"${disabled ? " disabled" : ""}></fig-handle></fig-tooltip>`,
7450
+ `<fig-tooltip action="manual" text="${Math.round(stop.position)}%"><fig-handle drag drag-axes="x" drag-surface=".fig-input-gradient-track" type="color"${tipAttr} color="${this.#stopColorCSS(stop)}" value="${stop.position}% 50%" hit-area="4" data-stop-index="${i}"${disabled ? " disabled" : ""}></fig-handle></fig-tooltip>`,
7411
7451
  )
7412
7452
  .join("");
7413
7453
  }
@@ -7488,8 +7528,7 @@ class FigInputGradient extends HTMLElement {
7488
7528
  const ghost = document.createElement("fig-handle");
7489
7529
  ghost.classList.add("fig-input-gradient-ghost");
7490
7530
  ghost.setAttribute("type", "color");
7491
- ghost.setAttribute("color-tip", "");
7492
- ghost.setAttribute("control", "add");
7531
+ if (this.#stopHandleMode === "tip") ghost.setAttribute("tip", "add");
7493
7532
  ghost.style.position = "absolute";
7494
7533
  ghost.style.top = "50%";
7495
7534
  ghost.style.transform = "translate(-50%, -50%)";
@@ -7645,6 +7684,7 @@ class FigInputGradient extends HTMLElement {
7645
7684
  const ghost = this.#ghostHandle;
7646
7685
  this.#track.innerHTML = this.#buildStopHandles();
7647
7686
  if (ghost) this.#track.appendChild(ghost);
7687
+ this.#syncHandleMode();
7648
7688
  this.#reobserveHandleColors();
7649
7689
  requestAnimationFrame(() => this.#repositionHandles());
7650
7690
  return;
@@ -7656,9 +7696,30 @@ class FigInputGradient extends HTMLElement {
7656
7696
  h.dataset.stopIndex = i;
7657
7697
  h.setAttribute("value", `${stop.position}% 50%`);
7658
7698
  h.setAttribute("color", this.#stopColorCSS(stop));
7699
+ if (this.#stopHandleMode === "tip") {
7700
+ h.setAttribute("tip", "color");
7701
+ } else {
7702
+ h.removeAttribute("tip");
7703
+ }
7659
7704
  const tip = h.closest("fig-tooltip");
7660
7705
  if (tip) tip.setAttribute("text", `${Math.round(stop.position)}%`);
7661
7706
  }
7707
+ this.#syncHandleMode();
7708
+ }
7709
+
7710
+ #syncHandleMode() {
7711
+ if (!this.#track) return;
7712
+ const useTip = this.#stopHandleMode === "tip";
7713
+ this.#track
7714
+ .querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)")
7715
+ .forEach((handle) => {
7716
+ if (useTip) handle.setAttribute("tip", "color");
7717
+ else handle.removeAttribute("tip");
7718
+ });
7719
+ if (this.#ghostHandle) {
7720
+ if (useTip) this.#ghostHandle.setAttribute("tip", "add");
7721
+ else this.#ghostHandle.removeAttribute("tip");
7722
+ }
7662
7723
  }
7663
7724
 
7664
7725
  #reobserveHandleColors() {
@@ -7957,6 +8018,9 @@ class FigInputGradient extends HTMLElement {
7957
8018
  document.removeEventListener("keydown", this.#onKeyDown);
7958
8019
  }
7959
8020
  break;
8021
+ case "mode":
8022
+ this.#syncHandleMode();
8023
+ break;
7960
8024
  }
7961
8025
  }
7962
8026
 
@@ -11994,10 +12058,16 @@ customElements.define("fig-joystick", FigInputJoystick);
11994
12058
  // FigInputAngle moved to fig-lab.js
11995
12059
  // FigShimmer
11996
12060
  class FigShimmer extends HTMLElement {
12061
+ get durationPropertyName() {
12062
+ return this.localName === "fig-skeleton"
12063
+ ? "--fig-skeleton-duration"
12064
+ : "--fig-shimmer-duration";
12065
+ }
12066
+
11997
12067
  connectedCallback() {
11998
12068
  const duration = this.getAttribute("duration");
11999
12069
  if (duration) {
12000
- this.style.setProperty("--shimmer-duration", duration);
12070
+ this.style.setProperty(this.durationPropertyName, duration);
12001
12071
  }
12002
12072
  }
12003
12073
 
@@ -12019,7 +12089,7 @@ class FigShimmer extends HTMLElement {
12019
12089
 
12020
12090
  attributeChangedCallback(name, oldValue, newValue) {
12021
12091
  if (name === "duration") {
12022
- this.style.setProperty("--shimmer-duration", newValue || "1.5s");
12092
+ this.style.setProperty(this.durationPropertyName, newValue || "1.5s");
12023
12093
  }
12024
12094
  // playing is handled purely by CSS attribute selectors
12025
12095
  }
@@ -13353,8 +13423,7 @@ class FigHandle extends HTMLElement {
13353
13423
  "drag-snapping",
13354
13424
  "value",
13355
13425
  "type",
13356
- "control",
13357
- "color-tip",
13426
+ "tip",
13358
13427
  "hit-area",
13359
13428
  "hit-area-mode",
13360
13429
  ];
@@ -13368,24 +13437,11 @@ class FigHandle extends HTMLElement {
13368
13437
  #nativeColorInput = null;
13369
13438
  #hitAreaEl = null;
13370
13439
 
13371
- get #controlMode() {
13372
- return this.getAttribute("control") || null;
13373
- }
13374
-
13375
- get #hasControlMode() {
13376
- return this.#controlMode === "add" || this.#controlMode === "remove";
13377
- }
13378
-
13379
- get #usesColorTip() {
13380
- return (
13381
- this.#hasControlMode ||
13382
- (this.hasAttribute("color-tip") &&
13383
- this.getAttribute("color-tip") !== "false")
13384
- );
13385
- }
13386
-
13387
- get #isGhost() {
13388
- return this.classList.contains("fig-input-gradient-ghost");
13440
+ get #tipMode() {
13441
+ const mode = (this.getAttribute("tip") || "").trim().toLowerCase();
13442
+ return mode === "color" || mode === "add" || mode === "remove"
13443
+ ? mode
13444
+ : null;
13389
13445
  }
13390
13446
 
13391
13447
  get #canOpenColorPicker() {
@@ -13601,7 +13657,7 @@ class FigHandle extends HTMLElement {
13601
13657
  document.addEventListener("keydown", this.#handleKeyDown);
13602
13658
  const initial = this.getAttribute("value");
13603
13659
  if (initial) this.#applyValue(initial);
13604
- if (this.#hasControlMode) this.#showColorTip();
13660
+ if (this.#tipMode) this.#showColorTip();
13605
13661
  }
13606
13662
 
13607
13663
  disconnectedCallback() {
@@ -13621,22 +13677,13 @@ class FigHandle extends HTMLElement {
13621
13677
  select() {
13622
13678
  if (this.hasAttribute("disabled")) return;
13623
13679
  this.setAttribute("selected", "");
13624
- if (
13625
- this.getAttribute("type") === "color" &&
13626
- this.#canOpenColorPicker &&
13627
- !this.#isDragging &&
13628
- this.#usesColorTip
13629
- )
13630
- this.#showColorTip();
13631
13680
  }
13632
13681
 
13633
13682
  deselect() {
13634
13683
  this.removeAttribute("selected");
13635
- this.#hideColorTip();
13636
13684
  }
13637
13685
 
13638
13686
  #handleSelect = (e) => {
13639
- if (this.#hasControlMode) return;
13640
13687
  if (this.#didDrag) {
13641
13688
  this.#didDrag = false;
13642
13689
  return;
@@ -13644,7 +13691,7 @@ class FigHandle extends HTMLElement {
13644
13691
  if (
13645
13692
  this.getAttribute("type") === "color" &&
13646
13693
  this.#canOpenColorPicker &&
13647
- !this.#usesColorTip
13694
+ !this.#tipMode
13648
13695
  ) {
13649
13696
  this.#openDirectColorPicker();
13650
13697
  return;
@@ -13653,7 +13700,6 @@ class FigHandle extends HTMLElement {
13653
13700
  };
13654
13701
 
13655
13702
  #handleDeselect = (e) => {
13656
- if (this.#hasControlMode) return;
13657
13703
  if (this.contains(e.target)) return;
13658
13704
  if ((this.#colorTip || this.#directColorPicker) && e.target.closest?.("dialog, [popover]")) return;
13659
13705
  this.deselect();
@@ -13665,9 +13711,7 @@ class FigHandle extends HTMLElement {
13665
13711
  if (this.getAttribute("type") !== "color") return;
13666
13712
  if (!this.#canOpenColorPicker) return;
13667
13713
  e.preventDefault();
13668
- if (this.#usesColorTip) {
13669
- if (!this.#colorTip) this.#showColorTip();
13670
- } else {
13714
+ if (!this.#tipMode) {
13671
13715
  this.#openDirectColorPicker();
13672
13716
  }
13673
13717
  };
@@ -13679,30 +13723,22 @@ class FigHandle extends HTMLElement {
13679
13723
  } else {
13680
13724
  this.style.setProperty("--fill", value);
13681
13725
  }
13682
- if (this.#colorTip && value) {
13726
+ if (this.#colorTip && this.#tipMode === "color" && value) {
13683
13727
  this.#colorTip.setAttribute("value", value);
13684
13728
  }
13685
13729
  this.#syncDirectColorPickerValue();
13686
13730
  }
13687
13731
  if (name === "drag") this.#syncDrag();
13688
13732
  if (name === "hit-area") this.#syncHitArea();
13733
+ if (name === "selected") this.#syncColorTipSelected();
13689
13734
  if (name === "value" && !this.#applyingValue && !this.#isDragging) {
13690
13735
  this.#applyValue(value);
13691
13736
  }
13692
- if (name === "control") {
13693
- if (this.#hasControlMode) {
13737
+ if (name === "tip") {
13738
+ this.#hideColorTip();
13739
+ if (this.#tipMode) {
13694
13740
  this.#removeDirectColorPicker();
13695
- this.#hideColorTip();
13696
13741
  this.#showColorTip();
13697
- } else {
13698
- this.#hideColorTip();
13699
- }
13700
- }
13701
- if (name === "color-tip") {
13702
- if (this.#usesColorTip) {
13703
- this.#removeDirectColorPicker();
13704
- } else {
13705
- this.#hideColorTip();
13706
13742
  }
13707
13743
  }
13708
13744
  }
@@ -13852,6 +13888,7 @@ class FigHandle extends HTMLElement {
13852
13888
  }
13853
13889
 
13854
13890
  showColorTip() {
13891
+ if (!this.#tipMode) return;
13855
13892
  if (this.#colorTip) {
13856
13893
  this.#colorTip.style.display = "";
13857
13894
  return;
@@ -13959,7 +13996,6 @@ class FigHandle extends HTMLElement {
13959
13996
 
13960
13997
  #openDirectColorPicker() {
13961
13998
  if (this.hasAttribute("disabled")) return;
13962
- this.#hideColorTip();
13963
13999
  const picker = this.#ensureDirectColorPicker();
13964
14000
  if (!picker) {
13965
14001
  this.#openNativeColorPicker();
@@ -14017,19 +14053,19 @@ class FigHandle extends HTMLElement {
14017
14053
 
14018
14054
  #closeColorPickerForDrag() {
14019
14055
  if (this.getAttribute("type") !== "color") return;
14020
- this.#hideColorTip();
14021
14056
  this.#directColorPicker?.close();
14022
14057
  }
14023
14058
 
14024
14059
  #showColorTip() {
14060
+ const mode = this.#tipMode;
14061
+ if (!mode) return;
14025
14062
  if (this.#colorTip) return;
14026
14063
  const tip = document.createElement("fig-color-tip");
14027
- if (this.#hasControlMode) {
14028
- tip.setAttribute("control", this.#controlMode);
14064
+ if (mode === "add" || mode === "remove") {
14065
+ tip.setAttribute("control", mode);
14029
14066
  } else {
14030
14067
  tip.setAttribute("value", this.getAttribute("color") || "#D9D9D9");
14031
14068
  tip.setAttribute("alpha", "true");
14032
- tip.setAttribute("selected", "");
14033
14069
  }
14034
14070
  tip.addEventListener("input", this.#handleColorTipInput);
14035
14071
  tip.addEventListener("change", this.#handleColorTipChange);
@@ -14037,6 +14073,7 @@ class FigHandle extends HTMLElement {
14037
14073
  tip.addEventListener("remove", this.#handleColorTipControl);
14038
14074
  this.appendChild(tip);
14039
14075
  this.#colorTip = tip;
14076
+ this.#syncColorTipSelected();
14040
14077
  }
14041
14078
 
14042
14079
  #hideColorTip() {
@@ -14049,6 +14086,13 @@ class FigHandle extends HTMLElement {
14049
14086
  this.#colorTip = null;
14050
14087
  }
14051
14088
 
14089
+ #syncColorTipSelected() {
14090
+ if (!this.#colorTip || this.#tipMode !== "color") return;
14091
+ const selected =
14092
+ this.hasAttribute("selected") && this.getAttribute("selected") !== "false";
14093
+ this.#colorTip.toggleAttribute("selected", selected);
14094
+ }
14095
+
14052
14096
  #colorWithOpacity(hex, opacity) {
14053
14097
  if (opacity === undefined || opacity >= 100) return hex;
14054
14098
  const { r, g, b } = figHexToRGB(hex);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "5.1.0",
3
+ "version": "6.0.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",