@rogieking/figui3 8.9.46 → 8.9.48

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
@@ -391,6 +391,7 @@ class FigSelectOptions extends HTMLElement {
391
391
  #navStart = null;
392
392
  #navEnd = null;
393
393
  #resizeObserver = null;
394
+ #mutationObserver = null;
394
395
  #boundSyncOverflow = this.syncOverflow.bind(this);
395
396
 
396
397
  connectedCallback() {
@@ -402,6 +403,13 @@ class FigSelectOptions extends HTMLElement {
402
403
  this.#resizeObserver?.disconnect();
403
404
  this.#resizeObserver = new ResizeObserver(() => this.syncOverflow());
404
405
  this.#resizeObserver.observe(this);
406
+ this.#mutationObserver?.disconnect();
407
+ this.#mutationObserver = new MutationObserver(() => {
408
+ if (!this.isConnected) return;
409
+ this.#ensureNavButtons();
410
+ this.syncOverflow();
411
+ });
412
+ this.#mutationObserver.observe(this, { childList: true });
405
413
  requestAnimationFrame(() => this.syncOverflow());
406
414
  }
407
415
 
@@ -409,6 +417,8 @@ class FigSelectOptions extends HTMLElement {
409
417
  this.removeEventListener("scroll", this.#boundSyncOverflow);
410
418
  this.#resizeObserver?.disconnect();
411
419
  this.#resizeObserver = null;
420
+ this.#mutationObserver?.disconnect();
421
+ this.#mutationObserver = null;
412
422
  this.#removeNavButtons();
413
423
  }
414
424
 
@@ -446,6 +456,7 @@ class FigSelectOptions extends HTMLElement {
446
456
  this.contains(this.#navStart) &&
447
457
  this.contains(this.#navEnd)
448
458
  ) {
459
+ this.#positionNavButtons();
449
460
  return;
450
461
  }
451
462
  this.#removeNavButtons();
@@ -458,8 +469,16 @@ class FigSelectOptions extends HTMLElement {
458
469
  });
459
470
  this.#navStart = buttons.start;
460
471
  this.#navEnd = buttons.end;
461
- this.prepend(this.#navStart);
462
- this.append(this.#navEnd);
472
+ this.#positionNavButtons();
473
+ }
474
+
475
+ #positionNavButtons() {
476
+ if (this.firstElementChild !== this.#navStart) {
477
+ this.prepend(this.#navStart);
478
+ }
479
+ if (this.lastElementChild !== this.#navEnd) {
480
+ this.append(this.#navEnd);
481
+ }
463
482
  }
464
483
 
465
484
  #removeNavButtons() {
package/fig-lab.css CHANGED
@@ -2093,14 +2093,21 @@ fig-canvas-control {
2093
2093
  rgba(255, 255, 255, 0.5)
2094
2094
  );
2095
2095
  --fig-canvas-control-radius-stroke: #0D99FF;
2096
- --fig-canvas-control-radius-stroke-halo: #FFFFFF;
2096
+ /* Deprecated compatibility alias. */
2097
+ --fig-canvas-control-radius-stroke-halo: rgba(255,255,255,0);
2098
+ --fig-canvas-control-stroke-halo: var(--fig-canvas-control-radius-stroke-halo);
2099
+ --fig-canvas-control-stroke-halo-hover: rgba(255,255,255,1);
2097
2100
  --fig-canvas-control-line-stroke-hover: #0D99FF;
2098
2101
  --fig-canvas-control-line-stroke-active: #0D99FF;
2099
2102
  --fig-canvas-control-line-stroke-width: 1px;
2100
2103
  --fig-canvas-control-line-stroke-width-hover: 2px;
2101
2104
 
2102
- &:has(.fig-canvas-control-radius-hit:hover) {
2105
+ &:has(
2106
+ .fig-canvas-control-radius-hit:hover,
2107
+ .fig-canvas-control-angle-line-hit:hover
2108
+ ) {
2103
2109
  --fig-canvas-control-line-stroke-width: var(--fig-canvas-control-line-stroke-width-hover);
2110
+ --fig-canvas-control-stroke-halo: var(--fig-canvas-control-stroke-halo-hover);
2104
2111
  }
2105
2112
 
2106
2113
  & > fig-handle,
@@ -2164,7 +2171,7 @@ fig-canvas-control {
2164
2171
  }
2165
2172
 
2166
2173
  .fig-canvas-control-radius-halo {
2167
- stroke: var(--fig-canvas-control-radius-stroke-halo);
2174
+ stroke: var(--fig-canvas-control-stroke-halo);
2168
2175
  stroke-width: calc(var(--fig-canvas-control-line-stroke-width) + 2px);
2169
2176
  filter: none;
2170
2177
  }
@@ -2195,7 +2202,7 @@ fig-canvas-control {
2195
2202
  }
2196
2203
 
2197
2204
  .fig-canvas-control-angle-line-halo {
2198
- stroke: var(--fig-canvas-control-radius-stroke-halo);
2205
+ stroke: var(--fig-canvas-control-stroke-halo);
2199
2206
  stroke-width: calc(var(--fig-canvas-control-line-stroke-width) + 2px);
2200
2207
  stroke-linecap: butt;
2201
2208
  filter: none;
package/fig-lab.js CHANGED
@@ -7351,6 +7351,10 @@ class PropskitWheel extends HTMLElement {
7351
7351
  figLabDefineElement("propskit-wheel", PropskitWheel);
7352
7352
 
7353
7353
  /* Canvas Control */
7354
+ /**
7355
+ * A composite spatial control built from one or more fig-handle elements.
7356
+ * @attr {number} precision - Decimal places for positions, radius, angle, and internal handle dragging (default 2).
7357
+ */
7354
7358
  class FigCanvasControl extends HTMLElement {
7355
7359
  static observedAttributes = [
7356
7360
  "type",
@@ -7361,6 +7365,7 @@ class FigCanvasControl extends HTMLElement {
7361
7365
  "disabled",
7362
7366
  "drag-surface",
7363
7367
  "snapping",
7368
+ "precision",
7364
7369
  ];
7365
7370
 
7366
7371
  #x = 50;
@@ -7429,6 +7434,31 @@ class FigCanvasControl extends HTMLElement {
7429
7434
  return "false";
7430
7435
  }
7431
7436
 
7437
+ get #precision() {
7438
+ const parsed = Number(this.getAttribute("precision") ?? 2);
7439
+ if (!Number.isFinite(parsed)) return 2;
7440
+ return Math.max(0, Math.min(8, Math.trunc(parsed)));
7441
+ }
7442
+
7443
+ #roundPosition(value) {
7444
+ const factor = 10 ** this.#precision;
7445
+ const rounded = Math.round(value * factor) / factor;
7446
+ return Object.is(rounded, -0) ? 0 : rounded;
7447
+ }
7448
+
7449
+ #syncHandlePrecision() {
7450
+ const precision = this.getAttribute("precision");
7451
+ for (const handle of [
7452
+ this.#pointHandle,
7453
+ this.#angleHandle,
7454
+ this.#secondHandle,
7455
+ ]) {
7456
+ if (!handle) continue;
7457
+ if (precision === null) handle.removeAttribute("precision");
7458
+ else handle.setAttribute("precision", String(this.#precision));
7459
+ }
7460
+ }
7461
+
7432
7462
  #shouldSnap(shiftKey) {
7433
7463
  const mode = this.#snappingMode;
7434
7464
  if (mode === "true") return true;
@@ -7442,7 +7472,7 @@ class FigCanvasControl extends HTMLElement {
7442
7472
  const parts = name.split(",");
7443
7473
  return parts[0].trim();
7444
7474
  }
7445
- return `${Math.round(this.#x)}%, ${Math.round(this.#y)}%`;
7475
+ return `${this.#roundPosition(this.#x)}%, ${this.#roundPosition(this.#y)}%`;
7446
7476
  }
7447
7477
 
7448
7478
  get #secondTipText() {
@@ -7451,7 +7481,7 @@ class FigCanvasControl extends HTMLElement {
7451
7481
  const parts = name.split(",");
7452
7482
  if (parts.length > 1) return parts[1].trim();
7453
7483
  }
7454
- return `${Math.round(this.#x2)}%, ${Math.round(this.#y2)}%`;
7484
+ return `${this.#roundPosition(this.#x2)}%, ${this.#roundPosition(this.#y2)}%`;
7455
7485
  }
7456
7486
 
7457
7487
  get #dragSurface() {
@@ -7482,8 +7512,9 @@ class FigCanvasControl extends HTMLElement {
7482
7512
  }
7483
7513
 
7484
7514
  #formatRadius() {
7485
- if (this.#radiusIsPercent) return `Radius ${Math.round(this.#radius)}%`;
7486
- return `Radius ${Math.round(this.#radius)}`;
7515
+ const radius = this.#roundPosition(this.#radius);
7516
+ if (this.#radiusIsPercent) return `Radius ${radius}%`;
7517
+ return `Radius ${radius}`;
7487
7518
  }
7488
7519
 
7489
7520
  connectedCallback() {
@@ -7534,6 +7565,18 @@ class FigCanvasControl extends HTMLElement {
7534
7565
  if (this.#secondHandle)
7535
7566
  this.#secondHandle.setAttribute("drag-snapping", newVal || "false");
7536
7567
  }
7568
+ if (name === "precision") {
7569
+ if (!this.isConnected) return;
7570
+ this.#syncHandlePrecision();
7571
+ this.#x = this.#roundPosition(this.#x);
7572
+ this.#y = this.#roundPosition(this.#y);
7573
+ this.#x2 = this.#roundPosition(this.#x2);
7574
+ this.#y2 = this.#roundPosition(this.#y2);
7575
+ this.#radius = this.#roundPosition(this.#radius);
7576
+ this.#angle = this.#roundPosition(this.#angle);
7577
+ this.#syncPositions();
7578
+ this.#syncValueAttribute();
7579
+ }
7537
7580
  if (name === "name") {
7538
7581
  if (this.#pointTooltip)
7539
7582
  this.#pointTooltip.setAttribute("text", this.#pointTipText);
@@ -7547,22 +7590,23 @@ class FigCanvasControl extends HTMLElement {
7547
7590
  if (!raw) return;
7548
7591
  try {
7549
7592
  const v = JSON.parse(raw);
7550
- if (typeof v.x === "number") this.#x = v.x;
7551
- if (typeof v.y === "number") this.#y = v.y;
7593
+ if (typeof v.x === "number") this.#x = this.#roundPosition(v.x);
7594
+ if (typeof v.y === "number") this.#y = this.#roundPosition(v.y);
7552
7595
  if (v.radius !== undefined) {
7553
7596
  const rs = String(v.radius);
7554
7597
  if (rs.endsWith("%")) {
7555
7598
  this.#radiusIsPercent = true;
7556
- this.#radius = parseFloat(rs);
7599
+ this.#radius = this.#roundPosition(parseFloat(rs));
7557
7600
  } else {
7558
7601
  this.#radiusIsPercent = false;
7559
- this.#radius = parseFloat(rs);
7602
+ this.#radius = this.#roundPosition(parseFloat(rs));
7560
7603
  }
7561
7604
  if (!Number.isFinite(this.#radius)) this.#radius = 0;
7562
7605
  }
7563
- if (typeof v.angle === "number") this.#angle = v.angle;
7564
- if (typeof v.x2 === "number") this.#x2 = v.x2;
7565
- if (typeof v.y2 === "number") this.#y2 = v.y2;
7606
+ if (typeof v.angle === "number")
7607
+ this.#angle = this.#roundPosition(v.angle);
7608
+ if (typeof v.x2 === "number") this.#x2 = this.#roundPosition(v.x2);
7609
+ if (typeof v.y2 === "number") this.#y2 = this.#roundPosition(v.y2);
7566
7610
  if (
7567
7611
  this.#type === "color" &&
7568
7612
  typeof v.color === "string" &&
@@ -7577,19 +7621,23 @@ class FigCanvasControl extends HTMLElement {
7577
7621
  }
7578
7622
 
7579
7623
  get value() {
7580
- const v = { x: this.#x, y: this.#y };
7624
+ const v = {
7625
+ x: this.#roundPosition(this.#x),
7626
+ y: this.#roundPosition(this.#y),
7627
+ };
7581
7628
  if (this.#type === "color") {
7582
7629
  const color =
7583
7630
  this.getAttribute("color") || this.#pointHandle?.getAttribute("color");
7584
7631
  if (color) v.color = color;
7585
7632
  }
7586
7633
  if (this.#hasRadius) {
7587
- v.radius = this.#radiusIsPercent ? `${this.#radius}%` : this.#radius;
7634
+ const radius = this.#roundPosition(this.#radius);
7635
+ v.radius = this.#radiusIsPercent ? `${radius}%` : radius;
7588
7636
  }
7589
- if (this.#hasAngle) v.angle = this.#angle;
7637
+ if (this.#hasAngle) v.angle = this.#roundPosition(this.#angle);
7590
7638
  if (this.#hasSecondPoint) {
7591
- v.x2 = this.#x2;
7592
- v.y2 = this.#y2;
7639
+ v.x2 = this.#roundPosition(this.#x2);
7640
+ v.y2 = this.#roundPosition(this.#y2);
7593
7641
  }
7594
7642
  return v;
7595
7643
  }
@@ -7627,6 +7675,9 @@ class FigCanvasControl extends HTMLElement {
7627
7675
  handle.setAttribute("drag-axes", "x,y");
7628
7676
  handle.setAttribute("drag-snapping", this.#snappingMode);
7629
7677
  handle.setAttribute("value", `${this.#x}% ${this.#y}%`);
7678
+ if (this.hasAttribute("precision")) {
7679
+ handle.setAttribute("precision", String(this.#precision));
7680
+ }
7630
7681
  if (disabled) handle.setAttribute("disabled", "");
7631
7682
  if (type === "color") {
7632
7683
  handle.setAttribute("type", "color");
@@ -7890,7 +7941,7 @@ class FigCanvasControl extends HTMLElement {
7890
7941
  this.#wireHoverTooltip(
7891
7942
  this.#angleHandle,
7892
7943
  () => this.#angleTooltip,
7893
- () => `Angle ${Math.round(this.#angle)}°`,
7944
+ () => `Angle ${this.#roundPosition(this.#angle)}°`,
7894
7945
  () => this.#hasActiveInteraction(),
7895
7946
  );
7896
7947
  }
@@ -8049,10 +8100,10 @@ class FigCanvasControl extends HTMLElement {
8049
8100
  const maxDy = 100 - Math.max(y0, y20);
8050
8101
  const dxPct = Math.max(minDx, Math.min(maxDx, dxPctRaw));
8051
8102
  const dyPct = Math.max(minDy, Math.min(maxDy, dyPctRaw));
8052
- this.#x = x0 + dxPct;
8053
- this.#y = y0 + dyPct;
8054
- this.#x2 = x20 + dxPct;
8055
- this.#y2 = y20 + dyPct;
8103
+ this.#x = this.#roundPosition(x0 + dxPct);
8104
+ this.#y = this.#roundPosition(y0 + dyPct);
8105
+ this.#x2 = this.#roundPosition(x20 + dxPct);
8106
+ this.#y2 = this.#roundPosition(y20 + dyPct);
8056
8107
  this.#syncPositions();
8057
8108
  this.#emitInput();
8058
8109
  };
@@ -8087,6 +8138,9 @@ class FigCanvasControl extends HTMLElement {
8087
8138
  handle.setAttribute("size", "small");
8088
8139
  handle.setAttribute("hit-area", "12 circle");
8089
8140
  handle.setAttribute("hit-area-mode", "delegate");
8141
+ if (this.hasAttribute("precision")) {
8142
+ handle.setAttribute("precision", String(this.#precision));
8143
+ }
8090
8144
  if (disabled) handle.setAttribute("disabled", "");
8091
8145
  this.#angleHandle = handle;
8092
8146
 
@@ -8095,7 +8149,7 @@ class FigCanvasControl extends HTMLElement {
8095
8149
  tip.setAttribute("action", "manual");
8096
8150
  tip.setAttribute("theme", "canvas");
8097
8151
  tip.setAttribute("pointer", "false");
8098
- tip.setAttribute("text", `${Math.round(this.#angle)}°`);
8152
+ tip.setAttribute("text", `${this.#roundPosition(this.#angle)}°`);
8099
8153
  tip.appendChild(handle);
8100
8154
  this.appendChild(tip);
8101
8155
  this.#angleTooltip = tip;
@@ -8114,6 +8168,9 @@ class FigCanvasControl extends HTMLElement {
8114
8168
  handle.setAttribute("hit-area", "12 circle");
8115
8169
  handle.setAttribute("hit-area-mode", "delegate");
8116
8170
  handle.setAttribute("value", `${this.#x2}% ${this.#y2}%`);
8171
+ if (this.hasAttribute("precision")) {
8172
+ handle.setAttribute("precision", String(this.#precision));
8173
+ }
8117
8174
  if (disabled) handle.setAttribute("disabled", "");
8118
8175
  this.#secondHandle = handle;
8119
8176
 
@@ -8305,8 +8362,12 @@ class FigCanvasControl extends HTMLElement {
8305
8362
  this.#isDragging = true;
8306
8363
  const px = e.detail?.px ?? this.#x / 100;
8307
8364
  const py = e.detail?.py ?? this.#y / 100;
8308
- this.#x = Math.round(Math.max(0, Math.min(100, px * 100)));
8309
- this.#y = Math.round(Math.max(0, Math.min(100, py * 100)));
8365
+ this.#x = this.#roundPosition(
8366
+ Math.max(0, Math.min(100, px * 100)),
8367
+ );
8368
+ this.#y = this.#roundPosition(
8369
+ Math.max(0, Math.min(100, py * 100)),
8370
+ );
8310
8371
  if (this.#pointTooltip) {
8311
8372
  this.#pointTooltip.removeAttribute("show");
8312
8373
  this.#pointTooltip.hidePopup?.();
@@ -8327,8 +8388,12 @@ class FigCanvasControl extends HTMLElement {
8327
8388
  }
8328
8389
  const px = e.detail?.px ?? this.#x / 100;
8329
8390
  const py = e.detail?.py ?? this.#y / 100;
8330
- this.#x = Math.round(Math.max(0, Math.min(100, px * 100)));
8331
- this.#y = Math.round(Math.max(0, Math.min(100, py * 100)));
8391
+ this.#x = this.#roundPosition(
8392
+ Math.max(0, Math.min(100, px * 100)),
8393
+ );
8394
+ this.#y = this.#roundPosition(
8395
+ Math.max(0, Math.min(100, py * 100)),
8396
+ );
8332
8397
  if (this.#pointTooltip) this.#pointTooltip.removeAttribute("show");
8333
8398
  this.#syncPositions();
8334
8399
  this.#syncValueAttribute();
@@ -8358,7 +8423,7 @@ class FigCanvasControl extends HTMLElement {
8358
8423
  if (this.#shouldSnap(e.detail?.shiftKey)) {
8359
8424
  angle = Math.round(angle / 15) * 15;
8360
8425
  }
8361
- this.#angle = angle;
8426
+ this.#angle = this.#roundPosition(angle);
8362
8427
 
8363
8428
  let dist = Math.sqrt(dx * dx + dy * dy);
8364
8429
  if (this.#shouldSnap(e.detail?.shiftKey)) {
@@ -8372,15 +8437,17 @@ class FigCanvasControl extends HTMLElement {
8372
8437
  }
8373
8438
  }
8374
8439
  if (this.#radiusIsPercent) {
8375
- this.#radius = Math.max(0, (dist / rect.width) * 100);
8440
+ this.#radius = this.#roundPosition(
8441
+ Math.max(0, (dist / rect.width) * 100),
8442
+ );
8376
8443
  } else {
8377
- this.#radius = Math.max(0, dist);
8444
+ this.#radius = this.#roundPosition(Math.max(0, dist));
8378
8445
  }
8379
8446
 
8380
8447
  if (this.#angleTooltip) {
8381
8448
  this.#angleTooltip.setAttribute(
8382
8449
  "text",
8383
- `Angle ${Math.round(this.#angle)}°`,
8450
+ `Angle ${this.#roundPosition(this.#angle)}°`,
8384
8451
  );
8385
8452
  this.#angleTooltip.setAttribute("show", "true");
8386
8453
  this.#angleTooltip.showPopup?.();
@@ -8427,11 +8494,11 @@ class FigCanvasControl extends HTMLElement {
8427
8494
  if (this.#shouldSnap(ev.shiftKey)) {
8428
8495
  angle = Math.round(angle / 15) * 15;
8429
8496
  }
8430
- this.#angle = angle;
8497
+ this.#angle = this.#roundPosition(angle);
8431
8498
  if (this.#angleTooltip)
8432
8499
  this.#angleTooltip.setAttribute(
8433
8500
  "text",
8434
- `Angle ${Math.round(angle)}°`,
8501
+ `Angle ${this.#roundPosition(angle)}°`,
8435
8502
  );
8436
8503
  this.#syncPositions();
8437
8504
  this.#emitInput();
@@ -8464,8 +8531,12 @@ class FigCanvasControl extends HTMLElement {
8464
8531
  this.#isSecondDragging = true;
8465
8532
  const px = e.detail?.px ?? this.#x2 / 100;
8466
8533
  const py = e.detail?.py ?? this.#y2 / 100;
8467
- this.#x2 = Math.round(Math.max(0, Math.min(100, px * 100)));
8468
- this.#y2 = Math.round(Math.max(0, Math.min(100, py * 100)));
8534
+ this.#x2 = this.#roundPosition(
8535
+ Math.max(0, Math.min(100, px * 100)),
8536
+ );
8537
+ this.#y2 = this.#roundPosition(
8538
+ Math.max(0, Math.min(100, py * 100)),
8539
+ );
8469
8540
  if (this.#secondTooltip) {
8470
8541
  this.#secondTooltip.removeAttribute("show");
8471
8542
  this.#secondTooltip.hidePopup?.();
@@ -8534,11 +8605,11 @@ class FigCanvasControl extends HTMLElement {
8534
8605
  const newPctX = Math.max(0, Math.min(100, (nx / r.width) * 100));
8535
8606
  const newPctY = Math.max(0, Math.min(100, (ny / r.height) * 100));
8536
8607
  if (isFirst) {
8537
- this.#x = newPctX;
8538
- this.#y = newPctY;
8608
+ this.#x = this.#roundPosition(newPctX);
8609
+ this.#y = this.#roundPosition(newPctY);
8539
8610
  } else {
8540
- this.#x2 = newPctX;
8541
- this.#y2 = newPctY;
8611
+ this.#x2 = this.#roundPosition(newPctX);
8612
+ this.#y2 = this.#roundPosition(newPctY);
8542
8613
  }
8543
8614
  this.#syncPositions();
8544
8615
  this.#emitInput();
@@ -8643,9 +8714,11 @@ class FigCanvasControl extends HTMLElement {
8643
8714
  }
8644
8715
  }
8645
8716
  if (this.#radiusIsPercent) {
8646
- this.#radius = Math.max(0, (dist / rect.width) * 100);
8717
+ this.#radius = this.#roundPosition(
8718
+ Math.max(0, (dist / rect.width) * 100),
8719
+ );
8647
8720
  } else {
8648
- this.#radius = Math.max(0, dist);
8721
+ this.#radius = this.#roundPosition(Math.max(0, dist));
8649
8722
  }
8650
8723
  if (this.#radiusTooltip) {
8651
8724
  this.#radiusTooltip.setAttribute("text", this.#formatRadius());
package/fig.js CHANGED
@@ -4522,6 +4522,7 @@ class FigTabs extends HTMLElement {
4522
4522
  this.contains(this.#navStart) &&
4523
4523
  this.contains(this.#navEnd)
4524
4524
  ) {
4525
+ this.#positionNavButtons();
4525
4526
  return;
4526
4527
  }
4527
4528
 
@@ -4535,8 +4536,16 @@ class FigTabs extends HTMLElement {
4535
4536
  });
4536
4537
  this.#navStart = buttons.start;
4537
4538
  this.#navEnd = buttons.end;
4538
- this.prepend(this.#navStart);
4539
- this.append(this.#navEnd);
4539
+ this.#positionNavButtons();
4540
+ }
4541
+
4542
+ #positionNavButtons() {
4543
+ if (this.firstElementChild !== this.#navStart) {
4544
+ this.prepend(this.#navStart);
4545
+ }
4546
+ if (this.lastElementChild !== this.#navEnd) {
4547
+ this.append(this.#navEnd);
4548
+ }
4540
4549
  }
4541
4550
 
4542
4551
  #handleKeyDown(event) {
@@ -18261,6 +18270,7 @@ class FigChooser extends HTMLElement {
18261
18270
  this.contains(this.#navStart) &&
18262
18271
  this.contains(this.#navEnd)
18263
18272
  ) {
18273
+ this.#positionNavButtons();
18264
18274
  return;
18265
18275
  }
18266
18276
 
@@ -18279,9 +18289,16 @@ class FigChooser extends HTMLElement {
18279
18289
  });
18280
18290
  this.#navStart = buttons.start;
18281
18291
  this.#navEnd = buttons.end;
18292
+ this.#positionNavButtons();
18293
+ }
18282
18294
 
18283
- this.prepend(this.#navStart);
18284
- this.append(this.#navEnd);
18295
+ #positionNavButtons() {
18296
+ if (this.firstElementChild !== this.#navStart) {
18297
+ this.prepend(this.#navStart);
18298
+ }
18299
+ if (this.lastElementChild !== this.#navEnd) {
18300
+ this.append(this.#navEnd);
18301
+ }
18285
18302
  }
18286
18303
 
18287
18304
  #scrollByPage(direction) {
@@ -18403,6 +18420,10 @@ class FigChooser extends HTMLElement {
18403
18420
  figDefineElement("fig-chooser", FigChooser);
18404
18421
 
18405
18422
  /* Handle */
18423
+ /**
18424
+ * A draggable handle positioned within a drag surface.
18425
+ * @attr {number} precision - Decimal places for position values and pixel dragging (default 2).
18426
+ */
18406
18427
  class FigHandle extends HTMLElement {
18407
18428
  static observedAttributes = [
18408
18429
  "color",
@@ -18413,6 +18434,7 @@ class FigHandle extends HTMLElement {
18413
18434
  "drag-axes",
18414
18435
  "drag-snapping",
18415
18436
  "value",
18437
+ "precision",
18416
18438
  "type",
18417
18439
  "tip",
18418
18440
  "hit-area",
@@ -18495,13 +18517,37 @@ class FigHandle extends HTMLElement {
18495
18517
  const container = this.#getContainer();
18496
18518
  if (!container) return "0% 0%";
18497
18519
  const { px, py } = this.#positionDetail(container.getBoundingClientRect());
18498
- return `${Math.round(px * 100)}% ${Math.round(py * 100)}%`;
18520
+ return `${(px * 100).toFixed(this.precision)}% ${(py * 100).toFixed(this.precision)}%`;
18499
18521
  }
18500
18522
 
18501
18523
  set value(v) {
18502
18524
  this.setAttribute("value", v ?? "0% 0%");
18503
18525
  }
18504
18526
 
18527
+ get precision() {
18528
+ const parsed = Number(this.getAttribute("precision") ?? 2);
18529
+ if (!Number.isFinite(parsed)) return 2;
18530
+ return Math.max(0, Math.min(10, Math.trunc(parsed)));
18531
+ }
18532
+
18533
+ set precision(value) {
18534
+ if (value === null || value === undefined || value === "") {
18535
+ this.removeAttribute("precision");
18536
+ } else {
18537
+ const parsed = Number(value);
18538
+ const precision = Number.isFinite(parsed)
18539
+ ? Math.max(0, Math.min(10, Math.trunc(parsed)))
18540
+ : 2;
18541
+ this.setAttribute("precision", String(precision));
18542
+ }
18543
+ }
18544
+
18545
+ #roundCoordinate(value) {
18546
+ const factor = 10 ** this.precision;
18547
+ const rounded = Math.round(value * factor) / factor;
18548
+ return Object.is(rounded, -0) ? 0 : rounded;
18549
+ }
18550
+
18505
18551
  #parseValue(str) {
18506
18552
  const normalized = str == null ? "" : String(str).trim();
18507
18553
  if (!normalized) return { xPct: 0, yPct: 0 };
@@ -18541,7 +18587,7 @@ class FigHandle extends HTMLElement {
18541
18587
 
18542
18588
  const resolveResponsive = (token) => {
18543
18589
  const pct = typeof token === "number" ? token : 0;
18544
- return `${pct}%`;
18590
+ return `${this.#roundCoordinate(pct)}%`;
18545
18591
  };
18546
18592
 
18547
18593
  const axes = this.#axes;
@@ -18549,12 +18595,16 @@ class FigHandle extends HTMLElement {
18549
18595
  if (axes.x) {
18550
18596
  const xPx = resolvePx(xToken, rect.width);
18551
18597
  this.style.left =
18552
- xPx === null ? resolveResponsive(xToken) : `${Math.round(xPx)}px`;
18598
+ xPx === null
18599
+ ? resolveResponsive(xToken)
18600
+ : `${this.#roundCoordinate(xPx)}px`;
18553
18601
  }
18554
18602
  if (axes.y) {
18555
18603
  const yPx = resolvePx(yToken, rect.height);
18556
18604
  this.style.top =
18557
- yPx === null ? resolveResponsive(yToken) : `${Math.round(yPx)}px`;
18605
+ yPx === null
18606
+ ? resolveResponsive(yToken)
18607
+ : `${this.#roundCoordinate(yPx)}px`;
18558
18608
  }
18559
18609
  }
18560
18610
 
@@ -18783,8 +18833,12 @@ class FigHandle extends HTMLElement {
18783
18833
  px = Math.max(0, Math.min(1, px));
18784
18834
  py = Math.max(0, Math.min(1, py));
18785
18835
  this.#syncPositionTranslate(axes);
18786
- if (axes.x) this.style.left = `${Math.round(px * rect.width)}px`;
18787
- if (axes.y) this.style.top = `${Math.round(py * rect.height)}px`;
18836
+ if (axes.x) {
18837
+ this.style.left = `${this.#roundCoordinate(px * rect.width)}px`;
18838
+ }
18839
+ if (axes.y) {
18840
+ this.style.top = `${this.#roundCoordinate(py * rect.height)}px`;
18841
+ }
18788
18842
  this.#syncValueAttribute();
18789
18843
  const detail = {
18790
18844
  ...this.#positionDetail(rect),
@@ -18824,6 +18878,15 @@ class FigHandle extends HTMLElement {
18824
18878
  if (name === "value" && !this.#applyingValue && !this.#isDragging) {
18825
18879
  this.#applyValue(value);
18826
18880
  }
18881
+ if (
18882
+ name === "precision" &&
18883
+ this.isConnected &&
18884
+ !this.#isDragging &&
18885
+ this.hasAttribute("value")
18886
+ ) {
18887
+ this.#applyValue(this.getAttribute("value"));
18888
+ this.#syncValueAttribute();
18889
+ }
18827
18890
  if (name === "tip") {
18828
18891
  this.#hideColorTip();
18829
18892
  if (this.#tipMode) {
@@ -18926,10 +18989,14 @@ class FigHandle extends HTMLElement {
18926
18989
 
18927
18990
  this.#syncPositionTranslate(axes);
18928
18991
  if (axes.x) {
18929
- this.style.left = `${Math.round(Math.max(0, Math.min(rect.width, centerX * rect.width)))}px`;
18992
+ this.style.left = `${this.#roundCoordinate(
18993
+ Math.max(0, Math.min(rect.width, centerX * rect.width)),
18994
+ )}px`;
18930
18995
  }
18931
18996
  if (axes.y) {
18932
- this.style.top = `${Math.round(Math.max(0, Math.min(rect.height, centerY * rect.height)))}px`;
18997
+ this.style.top = `${this.#roundCoordinate(
18998
+ Math.max(0, Math.min(rect.height, centerY * rect.height)),
18999
+ )}px`;
18933
19000
  }
18934
19001
  };
18935
19002
 
@@ -19360,7 +19427,12 @@ class FigHandle extends HTMLElement {
19360
19427
  const centerY = y + hh;
19361
19428
  const px = rect.width > 0 ? centerX / rect.width : 0;
19362
19429
  const py = rect.height > 0 ? centerY / rect.height : 0;
19363
- return { x, y, px, py };
19430
+ return {
19431
+ x: this.#roundCoordinate(x),
19432
+ y: this.#roundCoordinate(y),
19433
+ px,
19434
+ py,
19435
+ };
19364
19436
  }
19365
19437
  }
19366
19438
  figDefineElement("fig-handle", FigHandle);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "8.9.46",
3
+ "version": "8.9.48",
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": "SEE LICENSE IN LICENSE",