@rogieking/figui3 2.10.5 → 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 (2) hide show
  1. package/fig.js +209 -26
  2. package/package.json +1 -1
package/fig.js CHANGED
@@ -4554,14 +4554,16 @@ customElements.define("fig-input-joystick", FigInputJoystick);
4554
4554
  * @attr {number} opposite - The opposite value of the angle.
4555
4555
  */
4556
4556
  class FigInputAngle extends HTMLElement {
4557
- // Declare private fields first
4557
+ // Private fields
4558
4558
  #adjacent;
4559
4559
  #opposite;
4560
+ #prevRawAngle = null;
4561
+ #boundHandleRawChange;
4560
4562
 
4561
4563
  constructor() {
4562
4564
  super();
4563
4565
 
4564
- this.angle = 0; // Angle in degrees
4566
+ this.angle = 0;
4565
4567
  this.#adjacent = 1;
4566
4568
  this.#opposite = 0;
4567
4569
  this.isDragging = false;
@@ -4569,6 +4571,11 @@ class FigInputAngle extends HTMLElement {
4569
4571
  this.handle = null;
4570
4572
  this.angleInput = null;
4571
4573
  this.plane = null;
4574
+ this.units = "°";
4575
+ this.min = null;
4576
+ this.max = null;
4577
+
4578
+ this.#boundHandleRawChange = this.#handleRawChange.bind(this);
4572
4579
  }
4573
4580
 
4574
4581
  connectedCallback() {
@@ -4577,8 +4584,18 @@ class FigInputAngle extends HTMLElement {
4577
4584
  this.precision = parseInt(this.precision);
4578
4585
  this.text = this.getAttribute("text") === "true";
4579
4586
 
4580
- this.#render();
4587
+ let rawUnits = this.getAttribute("units") || "°";
4588
+ if (rawUnits === "deg") rawUnits = "°";
4589
+ this.units = rawUnits;
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;
4581
4597
 
4598
+ this.#render();
4582
4599
  this.#setupListeners();
4583
4600
 
4584
4601
  this.#syncHandlePosition();
@@ -4600,25 +4617,89 @@ class FigInputAngle extends HTMLElement {
4600
4617
  }
4601
4618
 
4602
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}"` : "";
4603
4623
  return `
4604
4624
  <div class="fig-input-angle-plane" tabindex="0">
4605
4625
  <div class="fig-input-angle-handle"></div>
4606
4626
  </div>
4607
4627
  ${
4608
4628
  this.text
4609
- ? `<fig-input-number
4629
+ ? `<fig-input-number
4610
4630
  name="angle"
4611
- step="0.1"
4631
+ step="${step}"
4612
4632
  value="${this.angle}"
4613
- min="0"
4614
- max="360"
4615
- units="°">
4633
+ ${minAttr}
4634
+ ${maxAttr}
4635
+ units="${this.units}">
4616
4636
  </fig-input-number>`
4617
4637
  : ""
4618
4638
  }
4619
4639
  `;
4620
4640
  }
4621
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
+
4622
4703
  #setupListeners() {
4623
4704
  this.handle = this.querySelector(".fig-input-angle-handle");
4624
4705
  this.plane = this.querySelector(".fig-input-angle-plane");
@@ -4636,16 +4717,35 @@ class FigInputAngle extends HTMLElement {
4636
4717
  this.#handleAngleInput.bind(this),
4637
4718
  );
4638
4719
  }
4720
+ // Capture-phase listener for unit suffix parsing
4721
+ this.addEventListener("change", this.#boundHandleRawChange, true);
4639
4722
  }
4640
4723
 
4641
4724
  #cleanupListeners() {
4642
- this.plane.removeEventListener("mousedown", this.#handleMouseDown);
4643
- this.plane.removeEventListener("touchstart", this.#handleTouchStart);
4725
+ this.plane?.removeEventListener("mousedown", this.#handleMouseDown);
4726
+ this.plane?.removeEventListener("touchstart", this.#handleTouchStart);
4644
4727
  window.removeEventListener("keydown", this.#handleKeyDown);
4645
4728
  window.removeEventListener("keyup", this.#handleKeyUp);
4646
4729
  if (this.text && this.angleInput) {
4647
4730
  this.angleInput.removeEventListener("input", this.#handleAngleInput);
4648
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
+ }
4649
4749
  }
4650
4750
 
4651
4751
  #handleAngleInput(e) {
@@ -4657,8 +4757,11 @@ class FigInputAngle extends HTMLElement {
4657
4757
  this.#emitChangeEvent();
4658
4758
  }
4659
4759
 
4760
+ // --- Angle calculation ---
4761
+
4660
4762
  #calculateAdjacentAndOpposite() {
4661
- const radians = (this.angle * Math.PI) / 180;
4763
+ const degrees = this.#toDegrees(this.angle);
4764
+ const radians = (degrees * Math.PI) / 180;
4662
4765
  this.#adjacent = Math.cos(radians);
4663
4766
  this.#opposite = Math.sin(radians);
4664
4767
  }
@@ -4669,16 +4772,46 @@ class FigInputAngle extends HTMLElement {
4669
4772
  return Math.round(angle / increment) * increment;
4670
4773
  }
4671
4774
 
4672
- #updateAngle(e) {
4775
+ #getRawAngle(e) {
4673
4776
  const rect = this.plane.getBoundingClientRect();
4674
4777
  const centerX = rect.left + rect.width / 2;
4675
4778
  const centerY = rect.top + rect.height / 2;
4676
4779
  const deltaX = e.clientX - centerX;
4677
4780
  const deltaY = e.clientY - centerY;
4678
- let angle = ((Math.atan2(deltaY, deltaX) * 180) / Math.PI + 360) % 360;
4781
+ return (Math.atan2(deltaY, deltaX) * 180) / Math.PI;
4782
+ }
4679
4783
 
4680
- angle = this.#snapToIncrement(angle);
4681
- 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
+ }
4682
4815
 
4683
4816
  this.#calculateAdjacentAndOpposite();
4684
4817
 
@@ -4690,6 +4823,8 @@ class FigInputAngle extends HTMLElement {
4690
4823
  this.#emitInputEvent();
4691
4824
  }
4692
4825
 
4826
+ // --- Event dispatching ---
4827
+
4693
4828
  #emitInputEvent() {
4694
4829
  this.dispatchEvent(
4695
4830
  new CustomEvent("input", {
@@ -4708,9 +4843,12 @@ class FigInputAngle extends HTMLElement {
4708
4843
  );
4709
4844
  }
4710
4845
 
4846
+ // --- Handle position ---
4847
+
4711
4848
  #syncHandlePosition() {
4712
4849
  if (this.handle) {
4713
- const radians = (this.angle * Math.PI) / 180;
4850
+ const degrees = this.#toDegrees(this.angle);
4851
+ const radians = (degrees * Math.PI) / 180;
4714
4852
  const radius = this.plane.offsetWidth / 2 - this.handle.offsetWidth / 2;
4715
4853
  const x = Math.cos(radians) * radius;
4716
4854
  const y = Math.sin(radians) * radius;
@@ -4718,8 +4856,11 @@ class FigInputAngle extends HTMLElement {
4718
4856
  }
4719
4857
  }
4720
4858
 
4859
+ // --- Mouse/Touch handlers ---
4860
+
4721
4861
  #handleMouseDown(e) {
4722
4862
  this.isDragging = true;
4863
+ this.#prevRawAngle = null;
4723
4864
  this.#updateAngle(e);
4724
4865
 
4725
4866
  const handleMouseMove = (e) => {
@@ -4729,6 +4870,7 @@ class FigInputAngle extends HTMLElement {
4729
4870
 
4730
4871
  const handleMouseUp = () => {
4731
4872
  this.isDragging = false;
4873
+ this.#prevRawAngle = null;
4732
4874
  this.plane.classList.remove("dragging");
4733
4875
  window.removeEventListener("mousemove", handleMouseMove);
4734
4876
  window.removeEventListener("mouseup", handleMouseUp);
@@ -4742,6 +4884,7 @@ class FigInputAngle extends HTMLElement {
4742
4884
  #handleTouchStart(e) {
4743
4885
  e.preventDefault();
4744
4886
  this.isDragging = true;
4887
+ this.#prevRawAngle = null;
4745
4888
  this.#updateAngle(e.touches[0]);
4746
4889
 
4747
4890
  const handleTouchMove = (e) => {
@@ -4751,6 +4894,7 @@ class FigInputAngle extends HTMLElement {
4751
4894
 
4752
4895
  const handleTouchEnd = () => {
4753
4896
  this.isDragging = false;
4897
+ this.#prevRawAngle = null;
4754
4898
  this.plane.classList.remove("dragging");
4755
4899
  window.removeEventListener("touchmove", handleTouchMove);
4756
4900
  window.removeEventListener("touchend", handleTouchEnd);
@@ -4761,6 +4905,8 @@ class FigInputAngle extends HTMLElement {
4761
4905
  window.addEventListener("touchend", handleTouchEnd);
4762
4906
  }
4763
4907
 
4908
+ // --- Keyboard handlers ---
4909
+
4764
4910
  #handleKeyDown(e) {
4765
4911
  if (e.key === "Shift") this.isShiftHeld = true;
4766
4912
  }
@@ -4773,8 +4919,10 @@ class FigInputAngle extends HTMLElement {
4773
4919
  this.plane?.focus();
4774
4920
  }
4775
4921
 
4922
+ // --- Attributes ---
4923
+
4776
4924
  static get observedAttributes() {
4777
- return ["value", "precision", "text"];
4925
+ return ["value", "precision", "text", "min", "max", "units"];
4778
4926
  }
4779
4927
 
4780
4928
  get value() {
@@ -4800,15 +4948,50 @@ class FigInputAngle extends HTMLElement {
4800
4948
  }
4801
4949
 
4802
4950
  attributeChangedCallback(name, oldValue, newValue) {
4803
- if (name === "value") {
4804
- this.value = Number(newValue);
4805
- }
4806
- if (name === "precision") {
4807
- this.precision = parseInt(newValue);
4808
- }
4809
- if (name === "text" && newValue !== oldValue) {
4810
- this.text = newValue.toLowerCase() === "true";
4811
- 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;
4812
4995
  }
4813
4996
  }
4814
4997
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "2.10.5",
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",