@rogieking/figui3 2.39.0 → 3.0.1

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/README.md +7 -6
  2. package/components.css +88 -24
  3. package/fig.js +197 -65
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -665,24 +665,25 @@ An image display or upload component.
665
665
 
666
666
  ---
667
667
 
668
- ### Input Joystick (`<fig-input-joystick>`)
668
+ ### Joystick (`<fig-joystick>`)
669
669
 
670
670
  A 2D position input control.
671
671
 
672
672
  | Attribute | Type | Default | Description |
673
673
  |-----------|------|---------|-------------|
674
- | `value` | string | | Position: `"x,y"` (0-1 range) |
674
+ | `value` | string | `"50% 50%"` | Position as percentages (for example `"50% 50%"` or `"25% 80%"`) |
675
675
  | `precision` | number | — | Decimal places |
676
676
  | `transform` | number | — | Output scaling |
677
- | `text` | boolean | `false` | Show X/Y inputs |
677
+ | `fields` | boolean | `false` | Show X/Y inputs |
678
678
  | `coordinates` | string | `"screen"` | Coordinate system: `"screen"` (0,0 top-left) or `"math"` (0,0 bottom-left) |
679
+ | `aspect-ratio` | string | `"1 / 1"` | Plane container ratio (for example `16 / 9`) |
679
680
 
680
681
  ```html
681
- <fig-input-joystick value="0.5,0.5"></fig-input-joystick>
682
- <fig-input-joystick value="0.5,0.5" text="true" precision="2"></fig-input-joystick>
682
+ <fig-joystick value="50% 50%"></fig-joystick>
683
+ <fig-joystick value="50% 50%" fields="true" precision="2"></fig-joystick>
683
684
 
684
685
  <!-- Math coordinates (Y-axis inverted: 0,0 at bottom-left) -->
685
- <fig-input-joystick value="0.5,0.5" coordinates="math" text="true"></fig-input-joystick>
686
+ <fig-joystick value="50% 50%" coordinates="math" fields="true"></fig-joystick>
686
687
  ```
687
688
 
688
689
  ---
package/components.css CHANGED
@@ -1791,7 +1791,8 @@ fig-origin-grid {
1791
1791
 
1792
1792
  &.overflow-right {
1793
1793
  --origin-overflow-nudge-x: calc(
1794
- var(--origin-overflow-nudge-base) * var(--origin-overflow-nudge-x-scale) * -1
1794
+ var(--origin-overflow-nudge-base) *
1795
+ var(--origin-overflow-nudge-x-scale) * -1
1795
1796
  );
1796
1797
  }
1797
1798
 
@@ -1803,7 +1804,8 @@ fig-origin-grid {
1803
1804
 
1804
1805
  &.overflow-down {
1805
1806
  --origin-overflow-nudge-y: calc(
1806
- var(--origin-overflow-nudge-base) * var(--origin-overflow-nudge-y-scale) * -1
1807
+ var(--origin-overflow-nudge-base) *
1808
+ var(--origin-overflow-nudge-y-scale) * -1
1807
1809
  );
1808
1810
  }
1809
1811
  }
@@ -3234,11 +3236,14 @@ fig-segmented-control {
3234
3236
  }
3235
3237
  }
3236
3238
 
3237
- fig-input-joystick {
3238
- --size: 1.5rem;
3239
- display: inline-flex;
3239
+ fig-joystick {
3240
+ --size: 100%;
3241
+ --aspect-ratio: 1 / 1;
3242
+ display: flex;
3243
+ flex-direction: column;
3240
3244
  gap: var(--spacer-2);
3241
3245
  user-select: none;
3246
+ width: var(--size);
3242
3247
 
3243
3248
  /* When inside horizontal fig-field, grow to fill and let inputs expand */
3244
3249
  fig-field[direction="horizontal"] & {
@@ -3250,10 +3255,21 @@ fig-input-joystick {
3250
3255
  width: auto;
3251
3256
  }
3252
3257
  }
3258
+
3259
+ .joystick-values {
3260
+ display: grid;
3261
+ grid-template-columns: 1fr 1fr;
3262
+ gap: var(--spacer-2);
3263
+ }
3264
+
3265
+ .joystick-values > fig-input-number {
3266
+ width: 100%;
3267
+ }
3268
+
3253
3269
  .fig-input-joystick-plane-container {
3254
3270
  display: flex;
3255
- width: 1.5rem;
3256
- height: 1.5rem;
3271
+ width: 100%;
3272
+ aspect-ratio: var(--aspect-ratio);
3257
3273
  place-items: center;
3258
3274
  flex-shrink: 0;
3259
3275
  flex-grow: 0;
@@ -3264,22 +3280,61 @@ fig-input-joystick {
3264
3280
  outline: 0;
3265
3281
  }
3266
3282
  }
3283
+
3284
+ .fig-joystick-axis-label {
3285
+ position: absolute;
3286
+ z-index: 1;
3287
+ pointer-events: none;
3288
+ user-select: none;
3289
+ color: var(--figma-color-text-tertiary);
3290
+ font-size: 10px;
3291
+ line-height: 1;
3292
+ letter-spacing: 0.01em;
3293
+ font-weight: 700;
3294
+ text-shadow:
3295
+ 1px 0 0 var(--figma-color-bg-secondary),
3296
+ -1px 0 0 var(--figma-color-bg-secondary),
3297
+ 0 1px 0 var(--figma-color-bg-secondary),
3298
+ 0 -1px 0 var(--figma-color-bg-secondary);
3299
+
3300
+ &.top {
3301
+ top: var(--spacer-2);
3302
+ left: 50%;
3303
+ transform: translateX(-50%);
3304
+ }
3305
+
3306
+ &.bottom {
3307
+ bottom: var(--spacer-2);
3308
+ left: 50%;
3309
+ transform: translateX(-50%);
3310
+ }
3311
+
3312
+ &.left {
3313
+ left: var(--spacer-2);
3314
+ top: 50%;
3315
+ transform: translateY(-50%) rotate(-90deg);
3316
+ transform-origin: center;
3317
+
3318
+ &.no-rotate {
3319
+ transform: translateY(-50%);
3320
+ }
3321
+ }
3322
+
3323
+ &.right {
3324
+ right: var(--spacer-2);
3325
+ top: 50%;
3326
+ transform: translateY(-50%) rotate(90deg);
3327
+ transform-origin: center;
3328
+ }
3329
+ }
3330
+
3267
3331
  .fig-input-joystick-plane {
3268
3332
  display: inline-grid;
3269
3333
  place-items: center;
3270
3334
  position: relative;
3271
- width: var(--size);
3272
- height: var(--size);
3335
+ width: 100%;
3336
+ height: 100%;
3273
3337
  flex-shrink: 0;
3274
- &.dragging {
3275
- cursor: grab;
3276
- --size: 3rem;
3277
- z-index: 2;
3278
- position: absolute;
3279
- top: 50%;
3280
- left: 50%;
3281
- transform: translate(-50%, -50%);
3282
- }
3283
3338
  }
3284
3339
  .fig-input-joystick-plane > * {
3285
3340
  grid-area: 1/1;
@@ -3288,7 +3343,6 @@ fig-input-joystick {
3288
3343
  position: absolute;
3289
3344
  width: var(--size);
3290
3345
  height: var(--size);
3291
- box-shadow: inset 0 0 0 1px var(--figma-color-border);
3292
3346
  border-radius: var(--radius-medium);
3293
3347
  overflow: hidden;
3294
3348
  background: var(--figma-color-bg-secondary);
@@ -3339,13 +3393,23 @@ fig-input-joystick {
3339
3393
 
3340
3394
  .fig-input-joystick-handle {
3341
3395
  position: absolute;
3342
- z-index: 1;
3343
- width: 0.5rem;
3344
- height: 0.5rem;
3345
- background: var(--handle-color);
3346
- box-shadow: var(--handle-shadow);
3396
+ z-index: 2;
3397
+ width: 1rem;
3398
+ height: 1rem;
3399
+ background: var(--figma-color-bg-brand);
3400
+ box-shadow:
3401
+ inset 0 0 0 0.125rem var(--handle-color),
3402
+ 0px 0 0 0.5px rgba(0, 0, 0, 0.1),
3403
+ var(--figma-elevation-200);
3347
3404
  border-radius: 50%;
3348
3405
  transform: translate(-50%, -50%);
3406
+ transition: box-shadow var(--slider-transition);
3407
+ &:hover {
3408
+ box-shadow:
3409
+ inset 0 0 0 0.25rem var(--handle-color),
3410
+ 0px 0 0 0.5px rgba(0, 0, 0, 0.1),
3411
+ var(--figma-elevation-200);
3412
+ }
3349
3413
  }
3350
3414
  }
3351
3415
 
package/fig.js CHANGED
@@ -7495,12 +7495,24 @@ customElements.define("fig-origin-grid", FigOriginGrid);
7495
7495
 
7496
7496
  /**
7497
7497
  * A custom joystick input element.
7498
- * @attr {string} value - The current position of the joystick (e.g., "0.5,0.5").
7498
+ * @attr {string} value - The current position of the joystick (e.g., "50% 50%").
7499
7499
  * @attr {number} precision - The number of decimal places for the output.
7500
7500
  * @attr {number} transform - A scaling factor for the output.
7501
- * @attr {boolean} text - Whether to display text inputs for X and Y values.
7501
+ * @attr {boolean} fields - Whether to display X and Y inputs.
7502
+ * @attr {string} aspect-ratio - Aspect ratio for the joystick plane container.
7503
+ * @attr {string} axis-labels - Space-delimited labels. 1 token: top. 2 tokens: x y. 4 tokens: left right top bottom.
7502
7504
  */
7503
7505
  class FigInputJoystick extends HTMLElement {
7506
+ #boundMouseDown = null;
7507
+ #boundTouchStart = null;
7508
+ #boundKeyDown = null;
7509
+ #boundKeyUp = null;
7510
+ #boundXInput = null;
7511
+ #boundYInput = null;
7512
+ #boundXFocusOut = null;
7513
+ #boundYFocusOut = null;
7514
+ #isSyncingValueAttr = false;
7515
+
7504
7516
  constructor() {
7505
7517
  super();
7506
7518
 
@@ -7513,6 +7525,14 @@ class FigInputJoystick extends HTMLElement {
7513
7525
  this.yInput = null;
7514
7526
  this.coordinates = "screen"; // "screen" (0,0 top-left) or "math" (0,0 bottom-left)
7515
7527
  this.#initialized = false;
7528
+ this.#boundMouseDown = (e) => this.#handleMouseDown(e);
7529
+ this.#boundTouchStart = (e) => this.#handleTouchStart(e);
7530
+ this.#boundKeyDown = (e) => this.#handleKeyDown(e);
7531
+ this.#boundKeyUp = (e) => this.#handleKeyUp(e);
7532
+ this.#boundXInput = (e) => this.#handleXInput(e);
7533
+ this.#boundYInput = (e) => this.#handleYInput(e);
7534
+ this.#boundXFocusOut = () => this.#handleFieldFocusOut();
7535
+ this.#boundYFocusOut = () => this.#handleFieldFocusOut();
7516
7536
  }
7517
7537
 
7518
7538
  #initialized = false;
@@ -7524,12 +7544,16 @@ class FigInputJoystick extends HTMLElement {
7524
7544
  this.precision = parseInt(this.precision);
7525
7545
  this.transform = this.getAttribute("transform") || 1;
7526
7546
  this.transform = Number(this.transform);
7527
- this.text = this.getAttribute("text") === "true";
7528
7547
  this.coordinates = this.getAttribute("coordinates") || "screen";
7548
+ this.#syncAspectRatioVar(this.getAttribute("aspect-ratio"));
7549
+ if (!this.hasAttribute("value")) {
7550
+ this.setAttribute("value", "50% 50%");
7551
+ }
7529
7552
 
7530
7553
  this.#render();
7531
7554
  this.#setupListeners();
7532
7555
  this.#syncHandlePosition();
7556
+ this.#syncValueAttribute();
7533
7557
  this.#initialized = true;
7534
7558
  });
7535
7559
  }
@@ -7538,41 +7562,102 @@ class FigInputJoystick extends HTMLElement {
7538
7562
  #displayY(y) {
7539
7563
  return this.coordinates === "math" ? 1 - y : y;
7540
7564
  }
7565
+
7566
+ #syncAspectRatioVar(value) {
7567
+ if (value && value.trim()) {
7568
+ this.style.setProperty("--aspect-ratio", value.trim());
7569
+ } else {
7570
+ this.style.removeProperty("--aspect-ratio");
7571
+ }
7572
+ }
7573
+
7541
7574
  disconnectedCallback() {
7542
7575
  this.#cleanupListeners();
7543
7576
  }
7544
7577
 
7578
+ get #fieldsEnabled() {
7579
+ const fields = this.getAttribute("fields");
7580
+ if (fields === null) return false;
7581
+ return fields.toLowerCase() !== "false";
7582
+ }
7583
+
7545
7584
  #render() {
7546
7585
  this.innerHTML = this.#getInnerHTML();
7547
7586
  }
7587
+
7588
+ #getAxisLabels() {
7589
+ const raw = (this.getAttribute("axis-labels") || "").trim();
7590
+ if (!raw) {
7591
+ return { left: "", right: "", top: "", bottom: "", leftNoRotate: false };
7592
+ }
7593
+ const tokens = raw.split(/\s+/).filter(Boolean);
7594
+ if (tokens.length === 1) {
7595
+ return {
7596
+ left: "",
7597
+ right: "",
7598
+ top: tokens[0],
7599
+ bottom: "",
7600
+ leftNoRotate: false,
7601
+ };
7602
+ }
7603
+ if (tokens.length === 2) {
7604
+ const [x, y] = tokens;
7605
+ return { left: x, right: "", top: "", bottom: y, leftNoRotate: true };
7606
+ }
7607
+ if (tokens.length === 4) {
7608
+ const [left, right, top, bottom] = tokens;
7609
+ return { left, right, top, bottom, leftNoRotate: false };
7610
+ }
7611
+ return { left: "", right: "", top: "", bottom: "", leftNoRotate: false };
7612
+ }
7613
+
7548
7614
  #getInnerHTML() {
7615
+ const axisLabels = this.#getAxisLabels();
7616
+ const labelsMarkup = [
7617
+ axisLabels.left
7618
+ ? `<label class="fig-joystick-axis-label left${axisLabels.leftNoRotate ? " no-rotate" : ""}" aria-hidden="true">${axisLabels.left}</label>`
7619
+ : "",
7620
+ axisLabels.right
7621
+ ? `<label class="fig-joystick-axis-label right" aria-hidden="true">${axisLabels.right}</label>`
7622
+ : "",
7623
+ axisLabels.top
7624
+ ? `<label class="fig-joystick-axis-label top" aria-hidden="true">${axisLabels.top}</label>`
7625
+ : "",
7626
+ axisLabels.bottom
7627
+ ? `<label class="fig-joystick-axis-label bottom" aria-hidden="true">${axisLabels.bottom}</label>`
7628
+ : "",
7629
+ ].join("");
7630
+
7549
7631
  return `
7550
7632
  <div class="fig-input-joystick-plane-container" tabindex="0">
7633
+ ${labelsMarkup}
7551
7634
  <div class="fig-input-joystick-plane">
7552
7635
  <div class="fig-input-joystick-guides"></div>
7553
7636
  <div class="fig-input-joystick-handle"></div>
7554
7637
  </div>
7555
7638
  </div>
7556
7639
  ${
7557
- this.text
7558
- ? `<fig-input-number
7559
- name="x"
7560
- step="1"
7561
- value="${this.position.x * 100}"
7562
- min="0"
7563
- max="100"
7564
- units="%">
7565
- <span slot="prepend">X</span>
7566
- </fig-input-number>
7567
- <fig-input-number
7568
- name="y"
7569
- step="1"
7570
- min="0"
7571
- max="100"
7572
- value="${this.position.y * 100}"
7573
- units="%">
7574
- <span slot="prepend">Y</span>
7575
- </fig-input-number>`
7640
+ this.#fieldsEnabled
7641
+ ? `<div class="joystick-values">
7642
+ <fig-input-number
7643
+ name="x"
7644
+ step="1"
7645
+ value="${(this.position.x * 100).toFixed(this.precision)}"
7646
+ min="0"
7647
+ max="100"
7648
+ units="%">
7649
+ <span slot="prepend">X</span>
7650
+ </fig-input-number>
7651
+ <fig-input-number
7652
+ name="y"
7653
+ step="1"
7654
+ min="0"
7655
+ max="100"
7656
+ value="${(this.position.y * 100).toFixed(this.precision)}"
7657
+ units="%">
7658
+ <span slot="prepend">Y</span>
7659
+ </fig-input-number>
7660
+ </div>`
7576
7661
  : ""
7577
7662
  }
7578
7663
  `;
@@ -7583,45 +7668,57 @@ class FigInputJoystick extends HTMLElement {
7583
7668
  this.cursor = this.querySelector(".fig-input-joystick-handle");
7584
7669
  this.xInput = this.querySelector("fig-input-number[name='x']");
7585
7670
  this.yInput = this.querySelector("fig-input-number[name='y']");
7586
- this.plane.addEventListener("mousedown", this.#handleMouseDown.bind(this));
7587
- this.plane.addEventListener(
7588
- "touchstart",
7589
- this.#handleTouchStart.bind(this),
7590
- );
7591
- window.addEventListener("keydown", this.#handleKeyDown.bind(this));
7592
- window.addEventListener("keyup", this.#handleKeyUp.bind(this));
7593
- if (this.text && this.xInput && this.yInput) {
7594
- this.xInput.addEventListener("input", this.#handleXInput.bind(this));
7595
- this.yInput.addEventListener("input", this.#handleYInput.bind(this));
7671
+ this.plane.addEventListener("mousedown", this.#boundMouseDown);
7672
+ this.plane.addEventListener("touchstart", this.#boundTouchStart);
7673
+ window.addEventListener("keydown", this.#boundKeyDown);
7674
+ window.addEventListener("keyup", this.#boundKeyUp);
7675
+ if (this.#fieldsEnabled && this.xInput && this.yInput) {
7676
+ this.xInput.addEventListener("input", this.#boundXInput);
7677
+ this.xInput.addEventListener("change", this.#boundXInput);
7678
+ this.xInput.addEventListener("focusout", this.#boundXFocusOut);
7679
+ this.yInput.addEventListener("input", this.#boundYInput);
7680
+ this.yInput.addEventListener("change", this.#boundYInput);
7681
+ this.yInput.addEventListener("focusout", this.#boundYFocusOut);
7596
7682
  }
7597
7683
  }
7598
7684
 
7599
7685
  #cleanupListeners() {
7600
7686
  if (this.plane) {
7601
- this.plane.removeEventListener("mousedown", this.#handleMouseDown);
7602
- this.plane.removeEventListener("touchstart", this.#handleTouchStart);
7687
+ this.plane.removeEventListener("mousedown", this.#boundMouseDown);
7688
+ this.plane.removeEventListener("touchstart", this.#boundTouchStart);
7603
7689
  }
7604
- window.removeEventListener("keydown", this.#handleKeyDown);
7605
- window.removeEventListener("keyup", this.#handleKeyUp);
7606
- if (this.text && this.xInput && this.yInput) {
7607
- this.xInput.removeEventListener("input", this.#handleXInput);
7608
- this.yInput.removeEventListener("input", this.#handleYInput);
7690
+ window.removeEventListener("keydown", this.#boundKeyDown);
7691
+ window.removeEventListener("keyup", this.#boundKeyUp);
7692
+ if (this.#fieldsEnabled && this.xInput && this.yInput) {
7693
+ this.xInput.removeEventListener("input", this.#boundXInput);
7694
+ this.xInput.removeEventListener("change", this.#boundXInput);
7695
+ this.xInput.removeEventListener("focusout", this.#boundXFocusOut);
7696
+ this.yInput.removeEventListener("input", this.#boundYInput);
7697
+ this.yInput.removeEventListener("change", this.#boundYInput);
7698
+ this.yInput.removeEventListener("focusout", this.#boundYFocusOut);
7609
7699
  }
7610
7700
  }
7611
7701
 
7612
7702
  #handleXInput(e) {
7613
- e.stopPropagation();
7614
- this.position.x = Number(e.target.value) / 100; // Convert from percentage to decimal
7703
+ const next = Number.parseFloat(e.target.value);
7704
+ if (!Number.isFinite(next)) return;
7705
+ this.position.x = Math.max(0, Math.min(1, next / 100));
7615
7706
  this.#syncHandlePosition();
7707
+ this.#syncValueAttribute();
7616
7708
  this.#emitInputEvent();
7617
- this.#emitChangeEvent();
7618
7709
  }
7619
7710
 
7620
7711
  #handleYInput(e) {
7621
- e.stopPropagation();
7622
- this.position.y = Number(e.target.value) / 100; // Convert from percentage to decimal
7712
+ const next = Number.parseFloat(e.target.value);
7713
+ if (!Number.isFinite(next)) return;
7714
+ this.position.y = Math.max(0, Math.min(1, next / 100));
7623
7715
  this.#syncHandlePosition();
7716
+ this.#syncValueAttribute();
7624
7717
  this.#emitInputEvent();
7718
+ }
7719
+
7720
+ #handleFieldFocusOut() {
7721
+ this.#syncValueAttribute();
7625
7722
  this.#emitChangeEvent();
7626
7723
  }
7627
7724
 
@@ -7661,11 +7758,12 @@ class FigInputJoystick extends HTMLElement {
7661
7758
  const displayY = this.#displayY(snapped.y);
7662
7759
  this.cursor.style.left = `${snapped.x * 100}%`;
7663
7760
  this.cursor.style.top = `${displayY * 100}%`;
7664
- if (this.text && this.xInput && this.yInput) {
7761
+ if (this.#fieldsEnabled && this.xInput && this.yInput) {
7665
7762
  this.xInput.setAttribute("value", Math.round(snapped.x * 100));
7666
7763
  this.yInput.setAttribute("value", Math.round(snapped.y * 100));
7667
7764
  }
7668
7765
 
7766
+ this.#syncValueAttribute();
7669
7767
  this.#emitInputEvent();
7670
7768
  }
7671
7769
 
@@ -7694,12 +7792,20 @@ class FigInputJoystick extends HTMLElement {
7694
7792
  this.cursor.style.top = `${displayY * 100}%`;
7695
7793
  }
7696
7794
  // Also sync text inputs if they exist (convert to percentage 0-100)
7697
- if (this.text && this.xInput && this.yInput) {
7795
+ if (this.#fieldsEnabled && this.xInput && this.yInput) {
7698
7796
  this.xInput.setAttribute("value", Math.round(this.position.x * 100));
7699
7797
  this.yInput.setAttribute("value", Math.round(this.position.y * 100));
7700
7798
  }
7701
7799
  }
7702
7800
 
7801
+ #syncValueAttribute() {
7802
+ const next = this.value;
7803
+ if (this.getAttribute("value") === next) return;
7804
+ this.#isSyncingValueAttr = true;
7805
+ this.setAttribute("value", next);
7806
+ this.#isSyncingValueAttr = false;
7807
+ }
7808
+
7703
7809
  #handleMouseDown(e) {
7704
7810
  this.isDragging = true;
7705
7811
 
@@ -7718,6 +7824,7 @@ class FigInputJoystick extends HTMLElement {
7718
7824
  this.plane.style.cursor = "";
7719
7825
  window.removeEventListener("mousemove", handleMouseMove);
7720
7826
  window.removeEventListener("mouseup", handleMouseUp);
7827
+ this.#syncValueAttribute();
7721
7828
  this.#emitChangeEvent();
7722
7829
  };
7723
7830
 
@@ -7740,6 +7847,7 @@ class FigInputJoystick extends HTMLElement {
7740
7847
  this.plane.classList.remove("dragging");
7741
7848
  window.removeEventListener("touchmove", handleTouchMove);
7742
7849
  window.removeEventListener("touchend", handleTouchEnd);
7850
+ this.#syncValueAttribute();
7743
7851
  this.#emitChangeEvent();
7744
7852
  };
7745
7853
 
@@ -7759,32 +7867,48 @@ class FigInputJoystick extends HTMLElement {
7759
7867
  container?.focus();
7760
7868
  }
7761
7869
  static get observedAttributes() {
7762
- return ["value", "precision", "transform", "text", "coordinates"];
7763
- }
7764
- get value() {
7765
- // Return as percentage values (0-100)
7766
7870
  return [
7767
- Math.round(this.position.x * 100),
7768
- Math.round(this.position.y * 100),
7871
+ "value",
7872
+ "precision",
7873
+ "transform",
7874
+ "fields",
7875
+ "coordinates",
7876
+ "aspect-ratio",
7877
+ "axis-labels",
7769
7878
  ];
7770
7879
  }
7880
+ get value() {
7881
+ return `${Math.round(this.position.x * 100)}% ${Math.round(this.position.y * 100)}%`;
7882
+ }
7771
7883
  set value(value) {
7772
- // Parse value, strip % symbols if present, convert from 0-100 to 0-1
7773
- const v = value
7774
- .toString()
7775
- .split(",")
7776
- .map((s) => {
7777
- const num = parseFloat(s.replace(/%/g, "").trim());
7778
- return isNaN(num) ? 0.5 : num / 100; // Convert from percentage to decimal, default to 0.5 if invalid
7779
- });
7780
- this.position = { x: v[0] ?? 0.5, y: v[1] ?? 0.5 };
7884
+ const normalized = value == null ? "" : String(value).trim();
7885
+ if (!normalized) {
7886
+ this.position = { x: 0.5, y: 0.5 };
7887
+ } else {
7888
+ const parts = normalized.split(/[\s,]+/).filter(Boolean);
7889
+ const parseAxis = (token) => {
7890
+ if (!token) return 0.5;
7891
+ const isPercent = token.includes("%");
7892
+ const numeric = Number.parseFloat(token.replace(/%/g, "").trim());
7893
+ if (!Number.isFinite(numeric)) return 0.5;
7894
+ const decimal = isPercent || Math.abs(numeric) > 1 ? numeric / 100 : numeric;
7895
+ return Math.max(0, Math.min(1, decimal));
7896
+ };
7897
+ const x = parseAxis(parts[0]);
7898
+ const y = parseAxis(parts[1] ?? parts[0]);
7899
+ this.position = { x, y };
7900
+ }
7781
7901
  if (this.#initialized) {
7782
7902
  this.#syncHandlePosition();
7783
7903
  }
7784
7904
  }
7785
7905
  attributeChangedCallback(name, oldValue, newValue) {
7906
+ if (name === "aspect-ratio") {
7907
+ this.#syncAspectRatioVar(newValue);
7908
+ return;
7909
+ }
7786
7910
  if (name === "value") {
7787
- if (this.isDragging) return;
7911
+ if (this.#isSyncingValueAttr || this.isDragging) return;
7788
7912
  this.value = newValue;
7789
7913
  }
7790
7914
  if (name === "precision") {
@@ -7793,9 +7917,17 @@ class FigInputJoystick extends HTMLElement {
7793
7917
  if (name === "transform") {
7794
7918
  this.transform = Number(newValue);
7795
7919
  }
7796
- if (name === "text" && newValue !== oldValue) {
7797
- this.text = newValue.toLowerCase() === "true";
7920
+ if (name === "fields" && newValue !== oldValue) {
7921
+ this.#cleanupListeners();
7922
+ this.#render();
7923
+ this.#setupListeners();
7924
+ this.#syncHandlePosition();
7925
+ }
7926
+ if (name === "axis-labels" && newValue !== oldValue) {
7927
+ this.#cleanupListeners();
7798
7928
  this.#render();
7929
+ this.#setupListeners();
7930
+ this.#syncHandlePosition();
7799
7931
  }
7800
7932
  if (name === "coordinates") {
7801
7933
  this.coordinates = newValue || "screen";
@@ -7804,7 +7936,7 @@ class FigInputJoystick extends HTMLElement {
7804
7936
  }
7805
7937
  }
7806
7938
 
7807
- customElements.define("fig-input-joystick", FigInputJoystick);
7939
+ customElements.define("fig-joystick", FigInputJoystick);
7808
7940
 
7809
7941
  /**
7810
7942
  * A custom angle chooser input element.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "2.39.0",
3
+ "version": "3.0.1",
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",