@rogieking/figui3 2.10.4 → 2.11.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 +1 -0
  2. package/fig.js +217 -26
  3. package/package.json +1 -1
package/components.css CHANGED
@@ -1911,6 +1911,7 @@ fig-slider {
1911
1911
  }
1912
1912
  fig-input-text,
1913
1913
  fig-input-number {
1914
+ flex-basis: 5rem;
1914
1915
  border-top-left-radius: 0;
1915
1916
  border-bottom-left-radius: 0;
1916
1917
  border-left: 1px solid var(--figma-color-bg);
package/fig.js CHANGED
@@ -1587,6 +1587,8 @@ class FigSlider extends HTMLElement {
1587
1587
  "color",
1588
1588
  "units",
1589
1589
  "transform",
1590
+ "text",
1591
+ "default",
1590
1592
  ];
1591
1593
  }
1592
1594
 
@@ -1611,6 +1613,8 @@ class FigSlider extends HTMLElement {
1611
1613
  break;
1612
1614
  case "value":
1613
1615
  this.value = newValue;
1616
+ this.input.value = newValue;
1617
+ this.#syncProperties();
1614
1618
  if (this.figInputNumber) {
1615
1619
  this.figInputNumber.setAttribute("value", newValue);
1616
1620
  }
@@ -1621,6 +1625,10 @@ class FigSlider extends HTMLElement {
1621
1625
  this.figInputNumber.setAttribute("transform", this.transform);
1622
1626
  }
1623
1627
  break;
1628
+ case "default":
1629
+ this.default = newValue;
1630
+ this.#syncProperties();
1631
+ break;
1624
1632
  case "min":
1625
1633
  case "max":
1626
1634
  case "step":
@@ -4546,14 +4554,16 @@ customElements.define("fig-input-joystick", FigInputJoystick);
4546
4554
  * @attr {number} opposite - The opposite value of the angle.
4547
4555
  */
4548
4556
  class FigInputAngle extends HTMLElement {
4549
- // Declare private fields first
4557
+ // Private fields
4550
4558
  #adjacent;
4551
4559
  #opposite;
4560
+ #prevRawAngle = null;
4561
+ #boundHandleRawChange;
4552
4562
 
4553
4563
  constructor() {
4554
4564
  super();
4555
4565
 
4556
- this.angle = 0; // Angle in degrees
4566
+ this.angle = 0;
4557
4567
  this.#adjacent = 1;
4558
4568
  this.#opposite = 0;
4559
4569
  this.isDragging = false;
@@ -4561,6 +4571,11 @@ class FigInputAngle extends HTMLElement {
4561
4571
  this.handle = null;
4562
4572
  this.angleInput = null;
4563
4573
  this.plane = null;
4574
+ this.units = "°";
4575
+ this.min = null;
4576
+ this.max = null;
4577
+
4578
+ this.#boundHandleRawChange = this.#handleRawChange.bind(this);
4564
4579
  }
4565
4580
 
4566
4581
  connectedCallback() {
@@ -4569,8 +4584,18 @@ class FigInputAngle extends HTMLElement {
4569
4584
  this.precision = parseInt(this.precision);
4570
4585
  this.text = this.getAttribute("text") === "true";
4571
4586
 
4572
- this.#render();
4587
+ let rawUnits = this.getAttribute("units") || "°";
4588
+ if (rawUnits === "deg") rawUnits = "°";
4589
+ this.units = rawUnits;
4573
4590
 
4591
+ this.min = this.hasAttribute("min")
4592
+ ? Number(this.getAttribute("min"))
4593
+ : null;
4594
+ this.max = this.hasAttribute("max")
4595
+ ? Number(this.getAttribute("max"))
4596
+ : null;
4597
+
4598
+ this.#render();
4574
4599
  this.#setupListeners();
4575
4600
 
4576
4601
  this.#syncHandlePosition();
@@ -4592,25 +4617,89 @@ class FigInputAngle extends HTMLElement {
4592
4617
  }
4593
4618
 
4594
4619
  #getInnerHTML() {
4620
+ const step = this.#getStepForUnit();
4621
+ const minAttr = this.min !== null ? `min="${this.min}"` : "";
4622
+ const maxAttr = this.max !== null ? `max="${this.max}"` : "";
4595
4623
  return `
4596
4624
  <div class="fig-input-angle-plane" tabindex="0">
4597
4625
  <div class="fig-input-angle-handle"></div>
4598
4626
  </div>
4599
4627
  ${
4600
4628
  this.text
4601
- ? `<fig-input-number
4629
+ ? `<fig-input-number
4602
4630
  name="angle"
4603
- step="0.1"
4631
+ step="${step}"
4604
4632
  value="${this.angle}"
4605
- min="0"
4606
- max="360"
4607
- units="°">
4633
+ ${minAttr}
4634
+ ${maxAttr}
4635
+ units="${this.units}">
4608
4636
  </fig-input-number>`
4609
4637
  : ""
4610
4638
  }
4611
4639
  `;
4612
4640
  }
4613
4641
 
4642
+ #getStepForUnit() {
4643
+ switch (this.units) {
4644
+ case "rad":
4645
+ return 0.01;
4646
+ case "turn":
4647
+ return 0.001;
4648
+ default:
4649
+ return 0.1;
4650
+ }
4651
+ }
4652
+
4653
+ // --- Unit conversion helpers ---
4654
+
4655
+ #toDegrees(value) {
4656
+ switch (this.units) {
4657
+ case "rad":
4658
+ return (value * 180) / Math.PI;
4659
+ case "turn":
4660
+ return value * 360;
4661
+ default:
4662
+ return value;
4663
+ }
4664
+ }
4665
+
4666
+ #fromDegrees(degrees) {
4667
+ switch (this.units) {
4668
+ case "rad":
4669
+ return (degrees * Math.PI) / 180;
4670
+ case "turn":
4671
+ return degrees / 360;
4672
+ default:
4673
+ return degrees;
4674
+ }
4675
+ }
4676
+
4677
+ #convertAngle(value, fromUnit, toUnit) {
4678
+ // Convert to degrees first
4679
+ let degrees;
4680
+ switch (fromUnit) {
4681
+ case "rad":
4682
+ degrees = (value * 180) / Math.PI;
4683
+ break;
4684
+ case "turn":
4685
+ degrees = value * 360;
4686
+ break;
4687
+ default:
4688
+ degrees = value;
4689
+ }
4690
+ // Convert from degrees to target
4691
+ switch (toUnit) {
4692
+ case "rad":
4693
+ return (degrees * Math.PI) / 180;
4694
+ case "turn":
4695
+ return degrees / 360;
4696
+ default:
4697
+ return degrees;
4698
+ }
4699
+ }
4700
+
4701
+ // --- Event listeners ---
4702
+
4614
4703
  #setupListeners() {
4615
4704
  this.handle = this.querySelector(".fig-input-angle-handle");
4616
4705
  this.plane = this.querySelector(".fig-input-angle-plane");
@@ -4628,16 +4717,35 @@ class FigInputAngle extends HTMLElement {
4628
4717
  this.#handleAngleInput.bind(this),
4629
4718
  );
4630
4719
  }
4720
+ // Capture-phase listener for unit suffix parsing
4721
+ this.addEventListener("change", this.#boundHandleRawChange, true);
4631
4722
  }
4632
4723
 
4633
4724
  #cleanupListeners() {
4634
- this.plane.removeEventListener("mousedown", this.#handleMouseDown);
4635
- this.plane.removeEventListener("touchstart", this.#handleTouchStart);
4725
+ this.plane?.removeEventListener("mousedown", this.#handleMouseDown);
4726
+ this.plane?.removeEventListener("touchstart", this.#handleTouchStart);
4636
4727
  window.removeEventListener("keydown", this.#handleKeyDown);
4637
4728
  window.removeEventListener("keyup", this.#handleKeyUp);
4638
4729
  if (this.text && this.angleInput) {
4639
4730
  this.angleInput.removeEventListener("input", this.#handleAngleInput);
4640
4731
  }
4732
+ this.removeEventListener("change", this.#boundHandleRawChange, true);
4733
+ }
4734
+
4735
+ #handleRawChange(e) {
4736
+ // Only intercept native change events from the raw <input> element
4737
+ if (!e.target?.matches?.("input")) return;
4738
+ const raw = e.target.value;
4739
+ const match = raw.match(/^(-?\d*\.?\d+)\s*(turn|rad|deg|°)$/i);
4740
+ if (match) {
4741
+ const num = parseFloat(match[1]);
4742
+ let fromUnit = match[2].toLowerCase();
4743
+ if (fromUnit === "deg") fromUnit = "°";
4744
+ if (fromUnit !== this.units) {
4745
+ const converted = this.#convertAngle(num, fromUnit, this.units);
4746
+ e.target.value = String(converted);
4747
+ }
4748
+ }
4641
4749
  }
4642
4750
 
4643
4751
  #handleAngleInput(e) {
@@ -4649,8 +4757,11 @@ class FigInputAngle extends HTMLElement {
4649
4757
  this.#emitChangeEvent();
4650
4758
  }
4651
4759
 
4760
+ // --- Angle calculation ---
4761
+
4652
4762
  #calculateAdjacentAndOpposite() {
4653
- const radians = (this.angle * Math.PI) / 180;
4763
+ const degrees = this.#toDegrees(this.angle);
4764
+ const radians = (degrees * Math.PI) / 180;
4654
4765
  this.#adjacent = Math.cos(radians);
4655
4766
  this.#opposite = Math.sin(radians);
4656
4767
  }
@@ -4661,16 +4772,46 @@ class FigInputAngle extends HTMLElement {
4661
4772
  return Math.round(angle / increment) * increment;
4662
4773
  }
4663
4774
 
4664
- #updateAngle(e) {
4775
+ #getRawAngle(e) {
4665
4776
  const rect = this.plane.getBoundingClientRect();
4666
4777
  const centerX = rect.left + rect.width / 2;
4667
4778
  const centerY = rect.top + rect.height / 2;
4668
4779
  const deltaX = e.clientX - centerX;
4669
4780
  const deltaY = e.clientY - centerY;
4670
- let angle = ((Math.atan2(deltaY, deltaX) * 180) / Math.PI + 360) % 360;
4781
+ return (Math.atan2(deltaY, deltaX) * 180) / Math.PI;
4782
+ }
4671
4783
 
4672
- angle = this.#snapToIncrement(angle);
4673
- this.angle = angle;
4784
+ #updateAngle(e) {
4785
+ let rawAngle = this.#getRawAngle(e);
4786
+ // Normalize to 0-360 for snap and positioning
4787
+ let normalizedAngle = ((rawAngle % 360) + 360) % 360;
4788
+ normalizedAngle = this.#snapToIncrement(normalizedAngle);
4789
+
4790
+ const isBounded = this.min !== null || this.max !== null;
4791
+
4792
+ if (isBounded) {
4793
+ // Bounded: absolute position
4794
+ this.angle = this.#fromDegrees(normalizedAngle);
4795
+ } else {
4796
+ // Unbounded: cumulative winding
4797
+ if (this.#prevRawAngle === null) {
4798
+ // First event of this drag — snap to clicked position, preserving revolution
4799
+ this.#prevRawAngle = normalizedAngle;
4800
+ const currentDeg = this.#toDegrees(this.angle);
4801
+ const currentMod = ((currentDeg % 360) + 360) % 360;
4802
+ let delta = normalizedAngle - currentMod;
4803
+ if (delta > 180) delta -= 360;
4804
+ if (delta < -180) delta += 360;
4805
+ this.angle += this.#fromDegrees(delta);
4806
+ } else {
4807
+ // Subsequent events — accumulate delta
4808
+ let delta = normalizedAngle - this.#prevRawAngle;
4809
+ if (delta > 180) delta -= 360;
4810
+ if (delta < -180) delta += 360;
4811
+ this.angle += this.#fromDegrees(delta);
4812
+ this.#prevRawAngle = normalizedAngle;
4813
+ }
4814
+ }
4674
4815
 
4675
4816
  this.#calculateAdjacentAndOpposite();
4676
4817
 
@@ -4682,6 +4823,8 @@ class FigInputAngle extends HTMLElement {
4682
4823
  this.#emitInputEvent();
4683
4824
  }
4684
4825
 
4826
+ // --- Event dispatching ---
4827
+
4685
4828
  #emitInputEvent() {
4686
4829
  this.dispatchEvent(
4687
4830
  new CustomEvent("input", {
@@ -4700,9 +4843,12 @@ class FigInputAngle extends HTMLElement {
4700
4843
  );
4701
4844
  }
4702
4845
 
4846
+ // --- Handle position ---
4847
+
4703
4848
  #syncHandlePosition() {
4704
4849
  if (this.handle) {
4705
- const radians = (this.angle * Math.PI) / 180;
4850
+ const degrees = this.#toDegrees(this.angle);
4851
+ const radians = (degrees * Math.PI) / 180;
4706
4852
  const radius = this.plane.offsetWidth / 2 - this.handle.offsetWidth / 2;
4707
4853
  const x = Math.cos(radians) * radius;
4708
4854
  const y = Math.sin(radians) * radius;
@@ -4710,8 +4856,11 @@ class FigInputAngle extends HTMLElement {
4710
4856
  }
4711
4857
  }
4712
4858
 
4859
+ // --- Mouse/Touch handlers ---
4860
+
4713
4861
  #handleMouseDown(e) {
4714
4862
  this.isDragging = true;
4863
+ this.#prevRawAngle = null;
4715
4864
  this.#updateAngle(e);
4716
4865
 
4717
4866
  const handleMouseMove = (e) => {
@@ -4721,6 +4870,7 @@ class FigInputAngle extends HTMLElement {
4721
4870
 
4722
4871
  const handleMouseUp = () => {
4723
4872
  this.isDragging = false;
4873
+ this.#prevRawAngle = null;
4724
4874
  this.plane.classList.remove("dragging");
4725
4875
  window.removeEventListener("mousemove", handleMouseMove);
4726
4876
  window.removeEventListener("mouseup", handleMouseUp);
@@ -4734,6 +4884,7 @@ class FigInputAngle extends HTMLElement {
4734
4884
  #handleTouchStart(e) {
4735
4885
  e.preventDefault();
4736
4886
  this.isDragging = true;
4887
+ this.#prevRawAngle = null;
4737
4888
  this.#updateAngle(e.touches[0]);
4738
4889
 
4739
4890
  const handleTouchMove = (e) => {
@@ -4743,6 +4894,7 @@ class FigInputAngle extends HTMLElement {
4743
4894
 
4744
4895
  const handleTouchEnd = () => {
4745
4896
  this.isDragging = false;
4897
+ this.#prevRawAngle = null;
4746
4898
  this.plane.classList.remove("dragging");
4747
4899
  window.removeEventListener("touchmove", handleTouchMove);
4748
4900
  window.removeEventListener("touchend", handleTouchEnd);
@@ -4753,6 +4905,8 @@ class FigInputAngle extends HTMLElement {
4753
4905
  window.addEventListener("touchend", handleTouchEnd);
4754
4906
  }
4755
4907
 
4908
+ // --- Keyboard handlers ---
4909
+
4756
4910
  #handleKeyDown(e) {
4757
4911
  if (e.key === "Shift") this.isShiftHeld = true;
4758
4912
  }
@@ -4765,8 +4919,10 @@ class FigInputAngle extends HTMLElement {
4765
4919
  this.plane?.focus();
4766
4920
  }
4767
4921
 
4922
+ // --- Attributes ---
4923
+
4768
4924
  static get observedAttributes() {
4769
- return ["value", "precision", "text"];
4925
+ return ["value", "precision", "text", "min", "max", "units"];
4770
4926
  }
4771
4927
 
4772
4928
  get value() {
@@ -4792,15 +4948,50 @@ class FigInputAngle extends HTMLElement {
4792
4948
  }
4793
4949
 
4794
4950
  attributeChangedCallback(name, oldValue, newValue) {
4795
- if (name === "value") {
4796
- this.value = Number(newValue);
4797
- }
4798
- if (name === "precision") {
4799
- this.precision = parseInt(newValue);
4800
- }
4801
- if (name === "text" && newValue !== oldValue) {
4802
- this.text = newValue.toLowerCase() === "true";
4803
- this.#render();
4951
+ switch (name) {
4952
+ case "value":
4953
+ this.value = Number(newValue);
4954
+ break;
4955
+ case "precision":
4956
+ this.precision = parseInt(newValue);
4957
+ break;
4958
+ case "text":
4959
+ if (newValue !== oldValue) {
4960
+ this.text = newValue?.toLowerCase() === "true";
4961
+ if (this.plane) {
4962
+ this.#render();
4963
+ this.#setupListeners();
4964
+ this.#syncHandlePosition();
4965
+ }
4966
+ }
4967
+ break;
4968
+ case "units": {
4969
+ let units = newValue || "°";
4970
+ if (units === "deg") units = "°";
4971
+ this.units = units;
4972
+ if (this.plane) {
4973
+ this.#render();
4974
+ this.#setupListeners();
4975
+ this.#syncHandlePosition();
4976
+ }
4977
+ break;
4978
+ }
4979
+ case "min":
4980
+ this.min = newValue !== null ? Number(newValue) : null;
4981
+ if (this.plane) {
4982
+ this.#render();
4983
+ this.#setupListeners();
4984
+ this.#syncHandlePosition();
4985
+ }
4986
+ break;
4987
+ case "max":
4988
+ this.max = newValue !== null ? Number(newValue) : null;
4989
+ if (this.plane) {
4990
+ this.#render();
4991
+ this.#setupListeners();
4992
+ this.#syncHandlePosition();
4993
+ }
4994
+ break;
4804
4995
  }
4805
4996
  }
4806
4997
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "2.10.4",
3
+ "version": "2.11.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",