@rogieking/figui3 1.3.2 → 1.3.3

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 (4) hide show
  1. package/example.html +10 -1
  2. package/fig.css +54 -16
  3. package/fig.js +316 -25
  4. package/package.json +1 -1
package/example.html CHANGED
@@ -23,6 +23,12 @@
23
23
  <h2>UI3 Components</h2>
24
24
  </fig-header>
25
25
  <fig-content>
26
+ <fig-input-angle text="true"
27
+ value="0"
28
+ onInput="console.log(event.target.value)"></fig-input-angle>
29
+ <br /><br />
30
+ <fig-input-joystick></fig-input-joystick>
31
+ <br /><br />
26
32
  <fig-input-joystick text="true"
27
33
  value="0,0"
28
34
  onInput="console.log(event.target.value)"></fig-input-joystick>
@@ -445,8 +451,11 @@
445
451
  </fig-header>
446
452
  <fig-field>
447
453
  <label>Switches</label>
448
- <fig-switch on="true"
454
+ <fig-switch checked="true"
455
+ onInput="console.log(event.target.checked)"
449
456
  label="On"></fig-switch>
457
+ <fig-switch checked="false"
458
+ label="Off"></fig-switch>
450
459
  <fig-switch indeterminate="true"
451
460
  label="Indeterminate"></fig-switch>
452
461
  <fig-switch disabled
package/fig.css CHANGED
@@ -2263,18 +2263,6 @@ fig-segmented-control {
2263
2263
  }
2264
2264
  }
2265
2265
 
2266
- /* Utilities */
2267
-
2268
- @keyframes fig-spinner-spin {
2269
- from {
2270
- transform: rotate(0deg);
2271
- }
2272
-
2273
- to {
2274
- transform: rotate(360deg);
2275
- }
2276
- }
2277
-
2278
2266
  fig-input-joystick {
2279
2267
  --size: 1.5rem;
2280
2268
  display: inline-flex;
@@ -2296,10 +2284,10 @@ fig-input-joystick {
2296
2284
  width: var(--size);
2297
2285
  height: var(--size);
2298
2286
  flex-shrink: 0;
2299
- &.dragging,
2300
- &:hover {
2287
+ &.dragging {
2301
2288
  cursor: grab;
2302
2289
  --size: 3rem;
2290
+ z-index: 2;
2303
2291
  }
2304
2292
  }
2305
2293
  .fig-input-joystick-plane > * {
@@ -2335,8 +2323,7 @@ fig-input-joystick {
2335
2323
  pointer-events: none;
2336
2324
  }
2337
2325
  }
2338
- .fig-input-joystick-plane.dragging .fig-input-joystick-guides,
2339
- .fig-input-joystick-plane:hover .fig-input-joystick-guides {
2326
+ .fig-input-joystick-plane.dragging .fig-input-joystick-guides {
2340
2327
  background: linear-gradient(
2341
2328
  45deg,
2342
2329
  transparent calc(50% - 0.5px),
@@ -2364,3 +2351,54 @@ fig-input-joystick {
2364
2351
  transform: translate(-50%, -50%);
2365
2352
  }
2366
2353
  }
2354
+
2355
+ fig-input-angle {
2356
+ --size: 1.5rem;
2357
+ display: inline-flex;
2358
+ gap: var(--spacer-2);
2359
+
2360
+ .fig-input-angle-plane {
2361
+ display: inline-grid;
2362
+ place-items: center;
2363
+ width: var(--size);
2364
+ height: var(--size);
2365
+ aspect-ratio: 1/1;
2366
+ flex-shrink: 0;
2367
+ background-color: var(--figma-color-bg-secondary);
2368
+ border-radius: 100%;
2369
+ box-shadow: inset 0 0 0 1px var(--figma-color-border);
2370
+ &.dragging {
2371
+ cursor: grab;
2372
+ --size: 3rem;
2373
+ z-index: 2;
2374
+ }
2375
+ }
2376
+ .fig-input-angle-handle {
2377
+ display: inline-grid;
2378
+ place-items: center;
2379
+ grid-area: 1/1;
2380
+ width: calc(0.5rem + 2px);
2381
+ height: calc(0.5rem + 2px);
2382
+ &:before {
2383
+ content: "";
2384
+ display: block;
2385
+ width: 0.5rem;
2386
+ height: 0.5rem;
2387
+ background: var(--figma-color-icon-onbrand);
2388
+ box-shadow: var(--handle-shadow);
2389
+ border-radius: 50%;
2390
+ }
2391
+ }
2392
+ }
2393
+
2394
+ /* Utilities */
2395
+
2396
+ @keyframes fig-spinner-spin {
2397
+ from {
2398
+ transform: rotate(0deg);
2399
+ }
2400
+
2401
+ to {
2402
+ transform: rotate(360deg);
2403
+ }
2404
+ }
package/fig.js CHANGED
@@ -1671,6 +1671,7 @@ class FigImage extends HTMLElement {
1671
1671
  this.innerHTML = this.#getInnerHTML();
1672
1672
  this.#updateRefs();
1673
1673
  }
1674
+
1674
1675
  #updateRefs() {
1675
1676
  requestAnimationFrame(() => {
1676
1677
  this.chit = this.querySelector("fig-chit");
@@ -1712,6 +1713,13 @@ class FigImage extends HTMLElement {
1712
1713
  }
1713
1714
  window.customElements.define("fig-image", FigImage);
1714
1715
 
1716
+ /**
1717
+ * A custom joystick input element.
1718
+ * @attr {string} value - The current position of the joystick (e.g., "0.5,0.5").
1719
+ * @attr {number} precision - The number of decimal places for the output.
1720
+ * @attr {number} transform - A scaling factor for the output.
1721
+ * @attr {boolean} text - Whether to display text inputs for X and Y values.
1722
+ */
1715
1723
  class FigInputJoystick extends HTMLElement {
1716
1724
  constructor() {
1717
1725
  super();
@@ -1720,6 +1728,10 @@ class FigInputJoystick extends HTMLElement {
1720
1728
  this.value = [0.5, 0.5];
1721
1729
  this.isDragging = false;
1722
1730
  this.isShiftHeld = false;
1731
+ this.plane = null;
1732
+ this.cursor = null;
1733
+ this.xInput = null;
1734
+ this.yInput = null;
1723
1735
 
1724
1736
  // Initialize position
1725
1737
  requestAnimationFrame(() => {
@@ -1734,12 +1746,21 @@ class FigInputJoystick extends HTMLElement {
1734
1746
  this.#setupListeners();
1735
1747
 
1736
1748
  this.#syncHandlePosition();
1737
- if (this.text) {
1738
- this.xInput.value = this.position.x.toFixed(this.precision);
1739
- this.yInput.value = this.position.y.toFixed(this.precision);
1749
+ if (this.text && this.xInput && this.yInput) {
1750
+ this.xInput.setAttribute(
1751
+ "value",
1752
+ this.position.x.toFixed(this.precision)
1753
+ );
1754
+ this.yInput.setAttribute(
1755
+ "value",
1756
+ this.position.y.toFixed(this.precision)
1757
+ );
1740
1758
  }
1741
1759
  });
1742
1760
  }
1761
+ disconnectedCallback() {
1762
+ this.#cleanupListeners();
1763
+ }
1743
1764
 
1744
1765
  #render() {
1745
1766
  this.innerHTML = this.#getInnerHTML();
@@ -1780,23 +1801,32 @@ class FigInputJoystick extends HTMLElement {
1780
1801
  #setupListeners() {
1781
1802
  this.plane = this.querySelector(".fig-input-joystick-plane");
1782
1803
  this.cursor = this.querySelector(".fig-input-joystick-handle");
1783
- if (this.text) {
1784
- this.xInput = this.querySelector("fig-input-text[name='x']");
1785
- this.yInput = this.querySelector("fig-input-text[name='y']");
1786
- }
1787
-
1788
1804
  this.plane.addEventListener("mousedown", this.#handleMouseDown.bind(this));
1805
+ this.plane.addEventListener(
1806
+ "touchstart",
1807
+ this.#handleTouchStart.bind(this)
1808
+ );
1789
1809
  window.addEventListener("keydown", this.#handleKeyDown.bind(this));
1790
1810
  window.addEventListener("keyup", this.#handleKeyUp.bind(this));
1811
+ if (this.text && this.xInput && this.yInput) {
1812
+ this.xInput.addEventListener("input", this.#handleXInput.bind(this));
1813
+ this.yInput.addEventListener("input", this.#handleYInput.bind(this));
1814
+ }
1815
+ }
1791
1816
 
1792
- this.xInput.addEventListener("input", this.#handleXInput.bind(this));
1793
- this.yInput.addEventListener("input", this.#handleYInput.bind(this));
1817
+ #cleanupListeners() {
1818
+ this.plane.removeEventListener("mousedown", this.#handleMouseDown);
1819
+ window.removeEventListener("keydown", this.#handleKeyDown);
1820
+ window.removeEventListener("keyup", this.#handleKeyUp);
1821
+ if (this.text && this.xInput && this.yInput) {
1822
+ this.xInput.removeEventListener("input", this.#handleXInput);
1823
+ this.yInput.removeEventListener("input", this.#handleYInput);
1824
+ }
1794
1825
  }
1795
1826
 
1796
1827
  #handleXInput(e) {
1797
1828
  e.stopPropagation();
1798
1829
  this.position.x = Number(e.target.value);
1799
- this.value = [this.position.x, this.position.y];
1800
1830
  this.#syncHandlePosition();
1801
1831
  this.#emitInputEvent();
1802
1832
  this.#emitChangeEvent();
@@ -1805,7 +1835,6 @@ class FigInputJoystick extends HTMLElement {
1805
1835
  #handleYInput(e) {
1806
1836
  e.stopPropagation();
1807
1837
  this.position.y = Number(e.target.value);
1808
- this.value = [this.position.x, this.position.y];
1809
1838
  this.#syncHandlePosition();
1810
1839
  this.#emitInputEvent();
1811
1840
  this.#emitChangeEvent();
@@ -1837,17 +1866,17 @@ class FigInputJoystick extends HTMLElement {
1837
1866
 
1838
1867
  const snapped = this.#snapToDiagonal(x, y);
1839
1868
  this.position = snapped;
1840
- this.value = [snapped.x, snapped.y];
1841
1869
 
1842
1870
  this.cursor.style.left = `${snapped.x * 100}%`;
1843
- this.cursor.style.top = `${(1 - snapped.y) * 100}%`; // Invert Y for display
1844
- if (this.text) {
1871
+ this.cursor.style.top = `${snapped.y * 100}%`; // Invert Y for display
1872
+ if (this.text && this.xInput && this.yInput) {
1845
1873
  this.xInput.setAttribute("value", snapped.x.toFixed(3));
1846
1874
  this.yInput.setAttribute("value", snapped.y.toFixed(3));
1847
1875
  }
1848
1876
 
1849
1877
  this.#emitInputEvent();
1850
1878
  }
1879
+
1851
1880
  #emitInputEvent() {
1852
1881
  this.dispatchEvent(
1853
1882
  new CustomEvent("input", {
@@ -1875,12 +1904,13 @@ class FigInputJoystick extends HTMLElement {
1875
1904
 
1876
1905
  #handleMouseDown(e) {
1877
1906
  this.isDragging = true;
1878
- this.plane.classList.add("dragging");
1907
+
1879
1908
  this.#updatePosition(e);
1880
1909
 
1881
1910
  this.plane.style.cursor = "grabbing";
1882
1911
 
1883
1912
  const handleMouseMove = (e) => {
1913
+ this.plane.classList.add("dragging");
1884
1914
  if (this.isDragging) this.#updatePosition(e);
1885
1915
  };
1886
1916
 
@@ -1897,6 +1927,26 @@ class FigInputJoystick extends HTMLElement {
1897
1927
  window.addEventListener("mouseup", handleMouseUp);
1898
1928
  }
1899
1929
 
1930
+ #handleTouchStart(e) {
1931
+ e.preventDefault();
1932
+ this.isDragging = true;
1933
+ this.#updatePosition(e.touches[0]);
1934
+
1935
+ const handleTouchMove = (e) => {
1936
+ if (this.isDragging) this.#updatePosition(e.touches[0]);
1937
+ };
1938
+
1939
+ const handleTouchEnd = () => {
1940
+ this.isDragging = false;
1941
+ window.removeEventListener("touchmove", handleTouchMove);
1942
+ window.removeEventListener("touchend", handleTouchEnd);
1943
+ this.#emitChangeEvent();
1944
+ };
1945
+
1946
+ window.addEventListener("touchmove", handleTouchMove);
1947
+ window.addEventListener("touchend", handleTouchEnd);
1948
+ }
1949
+
1900
1950
  #handleKeyDown(e) {
1901
1951
  if (e.key === "Shift") this.isShiftHeld = true;
1902
1952
  }
@@ -1907,25 +1957,266 @@ class FigInputJoystick extends HTMLElement {
1907
1957
  static get observedAttributes() {
1908
1958
  return ["value", "precision", "transform", "text"];
1909
1959
  }
1960
+ get value() {
1961
+ return [this.position.x, this.position.y];
1962
+ }
1963
+ set value(value) {
1964
+ let v = value.toString().split(",").map(Number);
1965
+ this.position = { x: v[0], y: v[1] };
1966
+ this.#syncHandlePosition();
1967
+ }
1910
1968
  attributeChangedCallback(name, oldValue, newValue) {
1911
1969
  if (name === "value") {
1912
- this.value = newValue.split(",").map(Number);
1913
- this.position = { x: this.value[0], y: this.value[1] };
1914
- this.#syncHandlePosition();
1970
+ this.value = newValue;
1915
1971
  }
1916
1972
  if (name === "precision") {
1917
- this.precision = newValue;
1918
- this.precision = parseInt(this.precision);
1973
+ this.precision = parseInt(newValue);
1919
1974
  }
1920
1975
  if (name === "transform") {
1921
- this.transform = newValue;
1922
- this.transform = Number(this.transform);
1976
+ this.transform = Number(newValue);
1923
1977
  }
1924
- if (name === "text") {
1978
+ if (name === "text" && newValue !== oldValue) {
1925
1979
  this.text = newValue.toLowerCase() === "true";
1926
- this.innerHTML = this.#getInnerHTML();
1980
+ this.#render();
1927
1981
  }
1928
1982
  }
1929
1983
  }
1930
1984
 
1931
1985
  customElements.define("fig-input-joystick", FigInputJoystick);
1986
+
1987
+ /**
1988
+ * A custom angle chooser input element.
1989
+ * @attr {number} value - The current angle of the handle in degrees.
1990
+ * @attr {number} precision - The number of decimal places for the output.
1991
+ * @attr {boolean} text - Whether to display a text input for the angle value.
1992
+ */
1993
+ class FigInputAngle extends HTMLElement {
1994
+ constructor() {
1995
+ super();
1996
+
1997
+ this.angle = 0; // Angle in degrees
1998
+ this.isDragging = false;
1999
+ this.isShiftHeld = false;
2000
+ this.handle = null;
2001
+ this.angleInput = null;
2002
+ this.plane = null;
2003
+
2004
+ // Initialize position
2005
+ requestAnimationFrame(() => {
2006
+ this.precision = this.getAttribute("precision") || 1;
2007
+ this.precision = parseInt(this.precision);
2008
+ this.text = this.getAttribute("text") === "true";
2009
+
2010
+ this.#render();
2011
+
2012
+ this.#setupListeners();
2013
+
2014
+ this.#syncHandlePosition();
2015
+ if (this.text && this.angleInput) {
2016
+ this.angleInput.setAttribute(
2017
+ "value",
2018
+ this.angle.toFixed(this.precision)
2019
+ );
2020
+ }
2021
+ });
2022
+ }
2023
+
2024
+ disconnectedCallback() {
2025
+ this.#cleanupListeners();
2026
+ }
2027
+
2028
+ #render() {
2029
+ this.innerHTML = this.#getInnerHTML();
2030
+ }
2031
+
2032
+ #getInnerHTML() {
2033
+ return `
2034
+ <div class="fig-input-angle-plane">
2035
+ <div class="fig-input-angle-handle"></div>
2036
+ </div>
2037
+ ${
2038
+ this.text
2039
+ ? `<fig-input-text
2040
+ type="number"
2041
+ name="angle"
2042
+ step="0.1"
2043
+ value="${this.angle}"
2044
+ min="0"
2045
+ max="360">
2046
+ <span slot="append">°</span>
2047
+ </fig-input-text>`
2048
+ : ""
2049
+ }
2050
+ `;
2051
+ }
2052
+
2053
+ #setupListeners() {
2054
+ this.handle = this.querySelector(".fig-input-angle-handle");
2055
+ this.plane = this.querySelector(".fig-input-angle-plane");
2056
+ this.angleInput = this.querySelector("fig-input-text[name='angle']");
2057
+ this.plane.addEventListener("mousedown", this.#handleMouseDown.bind(this));
2058
+ this.plane.addEventListener(
2059
+ "touchstart",
2060
+ this.#handleTouchStart.bind(this)
2061
+ );
2062
+ window.addEventListener("keydown", this.#handleKeyDown.bind(this));
2063
+ window.addEventListener("keyup", this.#handleKeyUp.bind(this));
2064
+ if (this.text && this.angleInput) {
2065
+ this.angleInput = this.querySelector("fig-input-text");
2066
+ this.angleInput.addEventListener(
2067
+ "input",
2068
+ this.#handleAngleInput.bind(this)
2069
+ );
2070
+ }
2071
+ }
2072
+
2073
+ #cleanupListeners() {
2074
+ this.plane.removeEventListener("mousedown", this.#handleMouseDown);
2075
+ this.plane.removeEventListener("touchstart", this.#handleTouchStart);
2076
+ window.removeEventListener("keydown", this.#handleKeyDown);
2077
+ window.removeEventListener("keyup", this.#handleKeyUp);
2078
+ if (this.text && this.angleInput) {
2079
+ this.angleInput.removeEventListener("input", this.#handleAngleInput);
2080
+ }
2081
+ }
2082
+
2083
+ #handleAngleInput(e) {
2084
+ e.stopPropagation();
2085
+ this.angle = Number(e.target.value);
2086
+ this.#syncHandlePosition();
2087
+ this.#emitInputEvent();
2088
+ this.#emitChangeEvent();
2089
+ }
2090
+
2091
+ #snapToIncrement(angle) {
2092
+ if (!this.isShiftHeld) return angle;
2093
+ const increment = 45;
2094
+ return Math.round(angle / increment) * increment;
2095
+ }
2096
+
2097
+ #updateAngle(e) {
2098
+ const rect = this.plane.getBoundingClientRect();
2099
+ const centerX = rect.left + rect.width / 2;
2100
+ const centerY = rect.top + rect.height / 2;
2101
+ const deltaX = e.clientX - centerX;
2102
+ const deltaY = e.clientY - centerY;
2103
+ let angle = ((Math.atan2(deltaY, deltaX) * 180) / Math.PI + 360) % 360;
2104
+
2105
+ angle = this.#snapToIncrement(angle);
2106
+ this.angle = angle;
2107
+
2108
+ this.#syncHandlePosition();
2109
+ if (this.text && this.angleInput) {
2110
+ this.angleInput.setAttribute("value", this.angle.toFixed(this.precision));
2111
+ }
2112
+
2113
+ this.#emitInputEvent();
2114
+ }
2115
+
2116
+ #emitInputEvent() {
2117
+ this.dispatchEvent(
2118
+ new CustomEvent("input", {
2119
+ bubbles: true,
2120
+ cancelable: true,
2121
+ })
2122
+ );
2123
+ }
2124
+
2125
+ #emitChangeEvent() {
2126
+ this.dispatchEvent(
2127
+ new CustomEvent("change", {
2128
+ bubbles: true,
2129
+ cancelable: true,
2130
+ })
2131
+ );
2132
+ }
2133
+
2134
+ #syncHandlePosition() {
2135
+ if (this.handle) {
2136
+ const radians = (this.angle * Math.PI) / 180;
2137
+ const radius = this.plane.offsetWidth / 2 - this.handle.offsetWidth / 2;
2138
+ const x = Math.cos(radians) * radius;
2139
+ const y = Math.sin(radians) * radius;
2140
+ this.handle.style.transform = `translate(${x}px, ${y}px)`;
2141
+ }
2142
+ }
2143
+
2144
+ #handleMouseDown(e) {
2145
+ this.isDragging = true;
2146
+ this.#updateAngle(e);
2147
+
2148
+ const handleMouseMove = (e) => {
2149
+ if (this.isDragging) this.#updateAngle(e);
2150
+ };
2151
+
2152
+ const handleMouseUp = () => {
2153
+ this.isDragging = false;
2154
+ window.removeEventListener("mousemove", handleMouseMove);
2155
+ window.removeEventListener("mouseup", handleMouseUp);
2156
+ this.#emitChangeEvent();
2157
+ };
2158
+
2159
+ window.addEventListener("mousemove", handleMouseMove);
2160
+ window.addEventListener("mouseup", handleMouseUp);
2161
+ }
2162
+
2163
+ #handleTouchStart(e) {
2164
+ e.preventDefault();
2165
+ this.isDragging = true;
2166
+ this.#updateAngle(e.touches[0]);
2167
+
2168
+ const handleTouchMove = (e) => {
2169
+ if (this.isDragging) this.#updateAngle(e.touches[0]);
2170
+ };
2171
+
2172
+ const handleTouchEnd = () => {
2173
+ this.isDragging = false;
2174
+ window.removeEventListener("touchmove", handleTouchMove);
2175
+ window.removeEventListener("touchend", handleTouchEnd);
2176
+ this.#emitChangeEvent();
2177
+ };
2178
+
2179
+ window.addEventListener("touchmove", handleTouchMove);
2180
+ window.addEventListener("touchend", handleTouchEnd);
2181
+ }
2182
+
2183
+ #handleKeyDown(e) {
2184
+ if (e.key === "Shift") this.isShiftHeld = true;
2185
+ }
2186
+
2187
+ #handleKeyUp(e) {
2188
+ if (e.key === "Shift") this.isShiftHeld = false;
2189
+ }
2190
+
2191
+ static get observedAttributes() {
2192
+ return ["value", "precision", "text"];
2193
+ }
2194
+
2195
+ get value() {
2196
+ return this.angle;
2197
+ }
2198
+
2199
+ set value(value) {
2200
+ if (isNaN(value)) {
2201
+ console.error("Invalid value: must be a number.");
2202
+ return;
2203
+ }
2204
+ this.angle = value;
2205
+ this.#syncHandlePosition();
2206
+ }
2207
+
2208
+ attributeChangedCallback(name, oldValue, newValue) {
2209
+ if (name === "value") {
2210
+ this.value = Number(newValue);
2211
+ }
2212
+ if (name === "precision") {
2213
+ this.precision = parseInt(newValue);
2214
+ }
2215
+ if (name === "text" && newValue !== oldValue) {
2216
+ this.text = newValue.toLowerCase() === "true";
2217
+ this.#render();
2218
+ }
2219
+ }
2220
+ }
2221
+
2222
+ customElements.define("fig-input-angle", FigInputAngle);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "devDependencies": {