@rogieking/figui3 8.9.32 → 8.9.34

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-lab.js CHANGED
@@ -5546,43 +5546,26 @@ class PropskitSlider extends HTMLElement {
5546
5546
  figLabDefineElement("propskit-slider", PropskitSlider);
5547
5547
 
5548
5548
  /**
5549
- * Full-surface numeric wheel: SVG ticks + fig-input-number.
5549
+ * Standalone interactive numeric wheel.
5550
5550
  *
5551
- * @attr {string} label - Field label. Omitted defaults to "Value"; authored values (including blank) are used as-is.
5552
- * @attr {string} units - Optional unit forwarded to fig-input-number. Time aliases are normalized.
5553
5551
  * @attr {number} value - Numeric value. Unbounded unless min/max are set.
5554
5552
  * @attr {number} min - Inclusive lower bound. Omitted = unbounded below.
5555
5553
  * @attr {number} max - Inclusive upper bound. Omitted = unbounded above.
5556
- * @attr {string} default - Reset target.
5557
- * @attr {number} step - Scrub/keyboard/wheel increment. Defaults: s=0.1, ms=100, otherwise 1.
5558
- * @attr {number} precision - Display decimals forwarded to fig-input-number. Defaults: s=2, otherwise 0.
5559
- * @attr {boolean|string} elastic - Enables resisted handle movement while scrubbing. Defaults to true.
5560
- * @attr {string} size - Defaults to the large row; set to "small" for compact.
5561
- * @attr {boolean|string} disabled - Disables wheel and number.
5562
- * @attr {string} variant - Set to "minimal" for compact chrome.
5563
- * @fires input - Composed event while dragging or typing.
5564
- * @fires change - Composed event on commit.
5554
+ * @attr {number} step - Increment. Defaults to 1.
5555
+ * @attr {boolean|string} spin - Keeps ticks synchronized to value. Defaults to true.
5556
+ * @attr {boolean|string} elastic - Enables resisted handle movement. Defaults to true.
5557
+ * @attr {boolean|string} disabled - Disables interaction and focus.
5558
+ * @fires input - Numeric composed event during interaction.
5559
+ * @fires change - Numeric composed event on commit.
5565
5560
  */
5566
- class PropskitWheel extends HTMLElement {
5561
+ class FigInputWheel extends HTMLElement {
5567
5562
  static #TICK_COUNT = 33;
5568
5563
  static #HALF_FOV_DEG = 60;
5569
5564
  static #PERSPECTIVE_K = 0.55;
5570
- static #DRAGGING_BODY_CLASS = "fig-propskit-wheel-dragging";
5571
- static #RESERVED_ATTRS = new Set([
5572
- "label",
5573
- "size",
5574
- "disabled",
5575
- "variant",
5576
- "elastic",
5577
- "default",
5578
- "class",
5579
- "style",
5580
- "id",
5581
- "oninput",
5582
- "onchange",
5583
- ]);
5565
+ static #DRAGGING_BODY_CLASS = "fig-input-wheel-dragging";
5584
5566
 
5585
5567
  #surface = null;
5568
+ #track = null;
5586
5569
  #wheel = null;
5587
5570
  #svg = null;
5588
5571
  #tickPath = null;
@@ -5593,13 +5576,7 @@ class PropskitWheel extends HTMLElement {
5593
5576
  #layoutFrame = 0;
5594
5577
  #keyboardAnimationFrame = 0;
5595
5578
  #keyboardSettleTimer = 0;
5596
- #label = null;
5597
- #input = null;
5598
- #hasCustomLabel = false;
5599
- #observer = null;
5600
5579
  #resizeObserver = null;
5601
- #managedInputAttrs = new Set();
5602
- #initialValue = null;
5603
5580
  #isDragging = false;
5604
5581
  #dragPointerId = null;
5605
5582
  #dragStartX = 0;
@@ -5609,168 +5586,115 @@ class PropskitWheel extends HTMLElement {
5609
5586
  #elasticMaxPx = 0;
5610
5587
  #elasticRangeRect = null;
5611
5588
  #elasticHostWidth = 0;
5612
- #numberPointerId = null;
5613
- #numberPointerStartX = 0;
5614
- #numberPointerStartY = 0;
5615
- #numberPointerStartValue = 0;
5616
- #isNumberScrubbing = false;
5617
- #suppressNumberClick = false;
5618
- #numberClickResetTimer = 0;
5619
- #boundHandleInput = null;
5620
- #boundHandleChange = null;
5621
5589
  #boundPointerDown = this.#handlePointerDown.bind(this);
5622
5590
  #boundPointerMove = this.#handlePointerMove.bind(this);
5623
5591
  #boundPointerUp = this.#handlePointerUp.bind(this);
5624
- #boundNumberPointerDown = this.#handleNumberPointerDown.bind(this);
5625
- #boundNumberPointerMove = this.#handleNumberPointerMove.bind(this);
5626
- #boundNumberPointerEnd = this.#handleNumberPointerEnd.bind(this);
5627
5592
  #boundWheel = this.#handleWheel.bind(this);
5628
5593
  #boundKeyDown = this.#handleKeyDown.bind(this);
5629
- #boundClick = this.#handleClick.bind(this);
5630
5594
 
5631
5595
  static get observedAttributes() {
5632
- return ["label", "units", "value", "disabled", "step", "precision", "min", "max"];
5596
+ return [
5597
+ "value",
5598
+ "disabled",
5599
+ "step",
5600
+ "min",
5601
+ "max",
5602
+ "spin",
5603
+ "elastic",
5604
+ ];
5633
5605
  }
5634
5606
 
5635
5607
  connectedCallback() {
5636
5608
  if (!this.#surface) this.#initialize();
5637
- this.#syncLabel();
5638
5609
  this.#syncDisabled();
5639
- this.#syncInputAttributes();
5640
5610
  this.#syncValueFromHost();
5641
5611
  this.#bindEvents();
5642
- figLabConnectPropskitResetMenu(this);
5643
- this.#observer?.disconnect();
5644
- this.#observer = new MutationObserver((mutations) => {
5645
- let syncInput = false;
5646
- for (const mutation of mutations) {
5647
- if (mutation.type !== "attributes") continue;
5648
- const name = mutation.attributeName;
5649
- if (
5650
- !PropskitWheel.#RESERVED_ATTRS.has(name) &&
5651
- !PropskitWheel.observedAttributes.includes(name) &&
5652
- !name?.startsWith("data-")
5653
- ) {
5654
- syncInput = true;
5655
- }
5656
- }
5657
- if (syncInput) this.#syncInputAttributes();
5658
- });
5659
- this.#observer.observe(this, { attributes: true });
5660
5612
  this.#resizeObserver?.disconnect();
5661
- this.#resizeObserver = new ResizeObserver((entries) => {
5662
- const rect = entries.at(-1)?.contentRect;
5663
- if (rect) this.#setWheelSize(rect.width, rect.height);
5664
- });
5665
- this.#resizeObserver.observe(this.#surface);
5613
+ if (globalThis.ResizeObserver) {
5614
+ this.#resizeObserver = new ResizeObserver((entries) => {
5615
+ const rect = entries.at(-1)?.contentRect;
5616
+ if (rect) this.#setWheelSize(rect.width, rect.height);
5617
+ });
5618
+ this.#resizeObserver.observe(this);
5619
+ }
5666
5620
  this.#queueWheelLayout();
5667
5621
  }
5668
5622
 
5669
5623
  disconnectedCallback() {
5670
- this.#observer?.disconnect();
5671
5624
  this.#resizeObserver?.disconnect();
5672
5625
  if (this.#layoutFrame) cancelAnimationFrame(this.#layoutFrame);
5673
5626
  this.#layoutFrame = 0;
5674
5627
  this.#stopKeyboardAnimation();
5675
5628
  this.#unbindEvents();
5676
5629
  this.#stopDrag();
5677
- figLabDisconnectPropskitResetMenu(this);
5678
5630
  }
5679
5631
 
5680
5632
  attributeChangedCallback(name, oldValue, newValue) {
5681
5633
  if (oldValue === newValue || !this.#surface) return;
5682
- if (name === "label") this.#syncLabel();
5683
5634
  if (name === "disabled") this.#syncDisabled();
5684
5635
  if (name === "value" && !this.#isDragging) {
5685
5636
  this.#syncValueFromHost();
5686
5637
  }
5687
- if (name === "units" || name === "step" || name === "precision") {
5688
- this.#syncInputAttributes();
5638
+ if (name === "step") {
5639
+ this.#syncWheelAria();
5689
5640
  this.#queueWheelLayout();
5690
5641
  }
5642
+ if (name === "spin") this.#queueWheelLayout();
5691
5643
  if (name === "min" || name === "max") {
5692
- this.#syncInputAttributes();
5693
5644
  this.#commitValue(this.#numericValue());
5694
5645
  }
5695
5646
  }
5696
5647
 
5697
5648
  #initialize() {
5698
- this.#initialValue = this.getAttribute("value") ?? "0";
5699
- const initialChildren = Array.from(this.childNodes).filter(
5700
- (node) =>
5701
- node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()),
5702
- );
5703
- const customLabel = initialChildren.find(
5704
- (node) => node.nodeType === Node.ELEMENT_NODE && node.matches("label"),
5705
- );
5706
-
5707
- const surface = figLabCreateElement("div", {
5708
- className: "propskit-wheel-surface",
5709
- });
5710
- const wheel = figLabCreateElement("div", {
5711
- className: "propskit-wheel-wheel",
5712
- tabindex: "0",
5713
- role: "spinbutton",
5714
- });
5649
+ this.#surface = this;
5650
+ this.#track = this;
5651
+ this.#wheel = this;
5652
+ this.setAttribute("role", "spinbutton");
5653
+ if (
5654
+ !this.hasAttribute("aria-label") &&
5655
+ !this.hasAttribute("aria-labelledby")
5656
+ ) {
5657
+ this.setAttribute("aria-label", "Value");
5658
+ }
5659
+ this.setAttribute("tabindex", "0");
5715
5660
  const svg = figLabCreateSvgElement("svg", {
5716
- className: "propskit-wheel-wheel-svg",
5661
+ className: "fig-input-wheel-svg",
5717
5662
  "aria-hidden": "true",
5718
5663
  });
5719
- const label = customLabel || document.createElement("label");
5720
- const input = document.createElement("fig-input-number");
5721
- const labelId = figLabUniqueId("propskit-wheel-label");
5722
- label.id = labelId;
5723
- wheel.setAttribute("aria-labelledby", labelId);
5724
-
5725
5664
  const handle = figLabCreateElement("div", {
5726
- className: "propskit-wheel-handle",
5665
+ className: "fig-input-wheel-handle",
5727
5666
  });
5728
- wheel.append(svg);
5729
- surface.append(wheel, handle, label, input);
5730
- this.#surface = surface;
5731
- this.#wheel = wheel;
5732
5667
  this.#svg = svg;
5733
- this.#label = label;
5734
- this.#input = input;
5735
- this.#hasCustomLabel = Boolean(customLabel);
5736
- this.replaceChildren(surface);
5668
+ this.replaceChildren(svg, handle);
5737
5669
 
5738
5670
  const metricProbe = document.createElement("div");
5739
5671
  metricProbe.setAttribute("aria-hidden", "true");
5740
5672
  metricProbe.style.cssText =
5741
5673
  "position:absolute;visibility:hidden;pointer-events:none;left:0;top:0";
5742
5674
  const maxEl = document.createElement("div");
5743
- maxEl.style.height = "var(--propskit-wheel-tick-height)";
5675
+ maxEl.style.height = "var(--fig-input-wheel-tick-height)";
5744
5676
  const minEl = document.createElement("div");
5745
- minEl.style.height = "var(--propskit-wheel-tick-height-min)";
5677
+ minEl.style.height = "var(--fig-input-wheel-tick-height-min)";
5746
5678
  const maxWidthEl = document.createElement("div");
5747
- maxWidthEl.style.width = "var(--propskit-wheel-tick-width)";
5679
+ maxWidthEl.style.width = "var(--fig-input-wheel-tick-width)";
5748
5680
  const minWidthEl = document.createElement("div");
5749
- minWidthEl.style.width = "var(--propskit-wheel-tick-width-min)";
5750
- const insetEl = document.createElement("div");
5751
- insetEl.style.width = "var(--propskit-wheel-tick-inset)";
5752
- metricProbe.append(maxEl, minEl, maxWidthEl, minWidthEl, insetEl);
5753
- surface.append(metricProbe);
5681
+ minWidthEl.style.width = "var(--fig-input-wheel-tick-width-min)";
5682
+ metricProbe.append(maxEl, minEl, maxWidthEl, minWidthEl);
5683
+ this.append(metricProbe);
5754
5684
  this.#tickMetricProbe = {
5755
5685
  maxEl,
5756
5686
  minEl,
5757
5687
  maxWidthEl,
5758
5688
  minWidthEl,
5759
- insetEl,
5760
5689
  };
5761
5690
 
5762
- for (const node of initialChildren) {
5763
- if (node === customLabel) continue;
5764
- input.appendChild(node);
5765
- }
5766
-
5767
5691
  this.#ensureTickPath();
5768
5692
  }
5769
5693
 
5770
5694
  #ensureTickPath() {
5771
5695
  if (!this.#svg || this.#tickPath) return;
5772
5696
  this.#tickPath = figLabCreateSvgElement("path", {
5773
- className: "propskit-wheel-tick",
5697
+ className: "fig-input-wheel-tick",
5774
5698
  });
5775
5699
  this.#svg.append(this.#tickPath);
5776
5700
  }
@@ -5784,60 +5708,20 @@ class PropskitWheel extends HTMLElement {
5784
5708
  this.#tickMetricProbe?.maxWidthEl.getBoundingClientRect().width || 2,
5785
5709
  minW:
5786
5710
  this.#tickMetricProbe?.minWidthEl.getBoundingClientRect().width || 1,
5787
- inset: this.#tickMetricProbe?.insetEl.getBoundingClientRect().width || 0,
5788
5711
  };
5789
5712
  return this.#tickMetrics;
5790
5713
  }
5791
5714
 
5792
- #units() {
5793
- const raw = (this.getAttribute("units") || "").trim();
5794
- const normalized = raw.toLowerCase();
5795
- if (
5796
- normalized === "ms" ||
5797
- normalized === "millisecond" ||
5798
- normalized === "milliseconds"
5799
- ) {
5800
- return "ms";
5801
- }
5802
- if (
5803
- normalized === "s" ||
5804
- normalized === "second" ||
5805
- normalized === "seconds"
5806
- ) {
5807
- return "s";
5808
- }
5809
- return raw;
5810
- }
5811
-
5812
- #defaultStep() {
5813
- const units = this.#units();
5814
- if (units === "s") return 0.1;
5815
- if (units === "ms") return 100;
5816
- return 1;
5817
- }
5818
-
5819
- #defaultPrecision() {
5820
- return this.#units() === "s" ? 2 : 0;
5821
- }
5822
-
5823
5715
  #step() {
5824
5716
  if (this.hasAttribute("step")) {
5825
5717
  const parsed = Number(this.getAttribute("step"));
5826
5718
  if (parsed > 0) return parsed;
5827
5719
  }
5828
- return this.#defaultStep();
5829
- }
5830
-
5831
- #precision() {
5832
- if (this.hasAttribute("precision")) {
5833
- const parsed = Number(this.getAttribute("precision"));
5834
- if (Number.isInteger(parsed) && parsed >= 0) return parsed;
5835
- }
5836
- return this.#defaultPrecision();
5720
+ return 1;
5837
5721
  }
5838
5722
 
5839
5723
  #numericValue() {
5840
- const parsed = Number(this.getAttribute("value") ?? this.#input?.value ?? 0);
5724
+ const parsed = Number(this.getAttribute("value") ?? 0);
5841
5725
  return Number.isFinite(parsed) ? parsed : 0;
5842
5726
  }
5843
5727
 
@@ -5894,9 +5778,6 @@ class PropskitWheel extends HTMLElement {
5894
5778
  if (this.getAttribute("value") !== asString) {
5895
5779
  this.setAttribute("value", asString);
5896
5780
  }
5897
- if (this.#input?.getAttribute("value") !== asString) {
5898
- this.#input?.setAttribute("value", asString);
5899
- }
5900
5781
  this.#syncWheelAria(next);
5901
5782
  this.#queueWheelLayout();
5902
5783
  if (emit) {
@@ -5914,53 +5795,35 @@ class PropskitWheel extends HTMLElement {
5914
5795
  #syncValueFromHost() {
5915
5796
  const value = this.#numericValue();
5916
5797
  const clamped = this.#clamp(value);
5917
- if (clamped !== value) {
5798
+ if (
5799
+ clamped !== value ||
5800
+ this.getAttribute("value") !== String(clamped)
5801
+ ) {
5918
5802
  this.#commitValue(clamped);
5919
5803
  return;
5920
5804
  }
5921
- const asString = String(value);
5922
- if (this.#input?.getAttribute("value") !== asString) {
5923
- this.#input?.setAttribute("value", asString);
5924
- }
5925
5805
  this.#syncWheelAria(value);
5926
5806
  this.#queueWheelLayout();
5927
5807
  }
5928
5808
 
5929
5809
  #syncWheelAria(value = this.#numericValue()) {
5930
5810
  if (!this.#wheel) return;
5931
- const units = this.#units();
5932
5811
  const min = this.#boundMin();
5933
5812
  const max = this.#boundMax();
5934
5813
  this.#wheel.setAttribute("aria-valuenow", String(value));
5935
- this.#wheel.setAttribute(
5936
- "aria-valuetext",
5937
- units
5938
- ? `${value} ${
5939
- units === "s" ? "seconds" : units === "ms" ? "milliseconds" : units
5940
- }`
5941
- : String(value),
5942
- );
5814
+ this.#wheel.setAttribute("aria-valuetext", String(value));
5943
5815
  if (min === null) this.#wheel.removeAttribute("aria-valuemin");
5944
5816
  else this.#wheel.setAttribute("aria-valuemin", String(min));
5945
5817
  if (max === null) this.#wheel.removeAttribute("aria-valuemax");
5946
5818
  else this.#wheel.setAttribute("aria-valuemax", String(max));
5947
5819
  }
5948
5820
 
5949
- #syncLabel() {
5950
- if (!this.#label) return;
5951
- if (this.#hasCustomLabel) return;
5952
- if (!this.hasAttribute("label")) {
5953
- this.#label.textContent = "Value";
5954
- return;
5955
- }
5956
- this.#label.textContent = this.getAttribute("label") ?? "";
5957
- }
5958
-
5959
5821
  #syncDisabled() {
5960
5822
  const disabled = figLabBooleanAttribute(this, "disabled");
5961
- this.#input?.toggleAttribute("disabled", disabled);
5962
5823
  if (!this.#wheel) return;
5963
5824
  if (disabled) {
5825
+ if (this.#isDragging) this.#stopDrag();
5826
+ this.#stopKeyboardAnimation();
5964
5827
  this.setAttribute("aria-disabled", "true");
5965
5828
  this.#wheel.setAttribute("tabindex", "-1");
5966
5829
  this.#wheel.setAttribute("aria-disabled", "true");
@@ -5971,39 +5834,6 @@ class PropskitWheel extends HTMLElement {
5971
5834
  }
5972
5835
  }
5973
5836
 
5974
- #getForwardedInputAttrNames() {
5975
- return this.getAttributeNames().filter(
5976
- (name) =>
5977
- !PropskitWheel.#RESERVED_ATTRS.has(name) && !name.startsWith("data-"),
5978
- );
5979
- }
5980
-
5981
- #syncInputAttributes() {
5982
- if (!this.#input) return;
5983
- const inputAttrs = this.#getForwardedInputAttrNames();
5984
- const nextManaged = new Set(inputAttrs);
5985
- for (const attrName of this.#managedInputAttrs) {
5986
- if (!nextManaged.has(attrName)) this.#input.removeAttribute(attrName);
5987
- }
5988
- for (const attrName of inputAttrs) {
5989
- this.#input.setAttribute(attrName, this.getAttribute(attrName) ?? "");
5990
- }
5991
- const units = this.#units();
5992
- if (units) this.#input.setAttribute("units", units);
5993
- else this.#input.removeAttribute("units");
5994
- nextManaged.add("units");
5995
- if (!this.hasAttribute("step")) {
5996
- this.#input.setAttribute("step", String(this.#defaultStep()));
5997
- nextManaged.add("step");
5998
- }
5999
- if (!this.hasAttribute("precision")) {
6000
- this.#input.setAttribute("precision", String(this.#defaultPrecision()));
6001
- nextManaged.add("precision");
6002
- }
6003
- this.#managedInputAttrs = nextManaged;
6004
- this.#syncWheelAria();
6005
- }
6006
-
6007
5837
  #setWheelSize(width, height) {
6008
5838
  if (!this.#svg || width < 1 || height < 1) return;
6009
5839
  if (width === this.#wheelWidth && height === this.#wheelHeight) return;
@@ -6038,23 +5868,25 @@ class PropskitWheel extends HTMLElement {
6038
5868
  const height = this.#wheelHeight;
6039
5869
  if (width < 1 || height < 1) return;
6040
5870
 
6041
- const value = this.#visualValue ?? this.#numericValue();
6042
- const tickStep = 360 / PropskitWheel.#TICK_COUNT;
5871
+ const value =
5872
+ this.getAttribute("spin") === "false"
5873
+ ? (this.#boundMin() ?? 0)
5874
+ : (this.#visualValue ?? this.#numericValue());
5875
+ const tickStep = 360 / FigInputWheel.#TICK_COUNT;
6043
5876
  const step = this.#step();
6044
5877
  const base = this.#boundMin() ?? 0;
6045
5878
  const offsetDeg = ((value - base) / step) * tickStep;
6046
- const k = PropskitWheel.#PERSPECTIVE_K;
5879
+ const k = FigInputWheel.#PERSPECTIVE_K;
6047
5880
  const cx = width / 2;
6048
5881
  const cy = height / 2;
6049
5882
 
6050
- const { maxH, minH, maxW, minW, inset } = this.#readTickMetrics();
6051
- const drawableWidth = Math.max(0, width - inset * 2);
6052
- const radius = drawableWidth * 0.42;
6053
- const fov = PropskitWheel.#HALF_FOV_DEG;
6054
- const visibleHalf = Math.max(drawableWidth / 2, 1);
5883
+ const { maxH, minH, maxW, minW } = this.#readTickMetrics();
5884
+ const fov = FigInputWheel.#HALF_FOV_DEG;
5885
+ const visibleHalf = Math.max(width / 2, 1);
5886
+ const radius = visibleHalf * 0.84;
6055
5887
  const commands = [];
6056
5888
 
6057
- for (let index = 0; index < PropskitWheel.#TICK_COUNT; index += 1) {
5889
+ for (let index = 0; index < FigInputWheel.#TICK_COUNT; index += 1) {
6058
5890
  let theta = offsetDeg + index * tickStep;
6059
5891
  theta = ((((theta + 180) % 360) + 360) % 360) - 180;
6060
5892
  if (theta < -fov || theta >= fov) continue;
@@ -6084,106 +5916,93 @@ class PropskitWheel extends HTMLElement {
6084
5916
  }
6085
5917
 
6086
5918
  #bindEvents() {
6087
- this.#boundHandleInput ??= this.#forwardInputEvent.bind(this, "input");
6088
- this.#boundHandleChange ??= this.#forwardInputEvent.bind(this, "change");
6089
- this.#input?.addEventListener("input", this.#boundHandleInput);
6090
- this.#input?.addEventListener("change", this.#boundHandleChange);
6091
5919
  this.#surface?.addEventListener("pointerdown", this.#boundPointerDown);
6092
- this.addEventListener("pointerdown", this.#boundNumberPointerDown, {
6093
- capture: true,
6094
- });
6095
5920
  this.#wheel?.addEventListener("wheel", this.#boundWheel, { passive: false });
6096
5921
  this.#wheel?.addEventListener("keydown", this.#boundKeyDown);
6097
- this.addEventListener("click", this.#boundClick, true);
6098
5922
  }
6099
5923
 
6100
5924
  #unbindEvents() {
6101
- this.#input?.removeEventListener("input", this.#boundHandleInput);
6102
- this.#input?.removeEventListener("change", this.#boundHandleChange);
6103
5925
  this.#surface?.removeEventListener("pointerdown", this.#boundPointerDown);
6104
- this.removeEventListener("pointerdown", this.#boundNumberPointerDown, {
6105
- capture: true,
6106
- });
6107
5926
  this.#wheel?.removeEventListener("wheel", this.#boundWheel);
6108
5927
  this.#wheel?.removeEventListener("keydown", this.#boundKeyDown);
6109
- this.removeEventListener("click", this.#boundClick, true);
6110
- this.#stopNumberTracking();
6111
- clearTimeout(this.#numberClickResetTimer);
6112
- this.#numberClickResetTimer = 0;
6113
5928
  window.removeEventListener("pointermove", this.#boundPointerMove);
6114
5929
  window.removeEventListener("pointerup", this.#boundPointerUp);
6115
5930
  window.removeEventListener("pointercancel", this.#boundPointerUp);
6116
5931
  }
6117
5932
 
6118
- #forwardInputEvent(type, event) {
6119
- event.stopImmediatePropagation();
6120
- if (figLabBooleanAttribute(this, "disabled")) return;
6121
- const raw =
6122
- event instanceof CustomEvent && event.detail !== undefined
6123
- ? event.detail
6124
- : this.#input?.value;
6125
- const parsed = Number(raw);
6126
- const next = Number.isFinite(parsed) ? parsed : this.#numericValue();
6127
- this.#commitValue(next, { emit: type });
6128
- }
6129
-
6130
- #handleClick(event) {
6131
- if (figLabBooleanAttribute(this, "disabled")) return;
6132
- if (
6133
- event.target instanceof Element &&
6134
- event.target.closest("fig-input-number, fig-menu")
6135
- ) {
6136
- if (
6137
- this.#suppressNumberClick &&
6138
- event.target.closest("fig-input-number")
6139
- ) {
6140
- event.preventDefault();
6141
- event.stopImmediatePropagation();
6142
- this.#suppressNumberClick = false;
6143
- }
6144
- return;
6145
- }
6146
- this.#wheel?.focus();
6147
- }
6148
-
6149
5933
  #handlePointerDown(event) {
6150
5934
  if (figLabBooleanAttribute(this, "disabled")) return;
6151
5935
  if (event.button !== 0) return;
6152
- if (
6153
- event.target instanceof Element &&
6154
- event.target.closest("fig-input-number, fig-menu")
6155
- ) {
6156
- return;
6157
- }
6158
5936
  event.preventDefault();
6159
- this.#stopKeyboardAnimation();
6160
- this.#isDragging = true;
6161
- this.#dragPointerId = event.pointerId;
6162
- this.#dragStartX = event.clientX;
6163
- this.#dragStartValue = this.#numericValue();
6164
- this.#startElasticPull();
6165
- this.setAttribute("data-active", "");
6166
- document.body.classList.add(PropskitWheel.#DRAGGING_BODY_CLASS);
6167
- this.#wheel?.focus();
5937
+ if (!this.beginScrub(event)) return;
6168
5938
  this.#surface?.setPointerCapture?.(event.pointerId);
6169
5939
  window.addEventListener("pointermove", this.#boundPointerMove);
6170
5940
  window.addEventListener("pointerup", this.#boundPointerUp);
6171
5941
  window.addEventListener("pointercancel", this.#boundPointerUp);
6172
5942
  }
6173
5943
 
5944
+ beginScrub(start = {}) {
5945
+ if (figLabBooleanAttribute(this, "disabled")) return false;
5946
+ const clientX =
5947
+ typeof start === "number" ? start : Number(start?.clientX ?? start?.x);
5948
+ if (!Number.isFinite(clientX)) return false;
5949
+ this.#stopKeyboardAnimation();
5950
+ this.#isDragging = true;
5951
+ this.#dragPointerId =
5952
+ start && typeof start === "object" && start.pointerId !== undefined
5953
+ ? start.pointerId
5954
+ : null;
5955
+ this.#dragStartX = clientX;
5956
+ const requestedStart =
5957
+ start && typeof start === "object"
5958
+ ? Number(start.startValue)
5959
+ : Number.NaN;
5960
+ this.#dragStartValue = Number.isFinite(requestedStart)
5961
+ ? this.#clamp(requestedStart)
5962
+ : this.#numericValue();
5963
+ this.#startElasticPull();
5964
+ this.setAttribute("data-fig-input-wheel-active", "");
5965
+ document.body.classList.add(FigInputWheel.#DRAGGING_BODY_CLASS);
5966
+ this.focus();
5967
+ return true;
5968
+ }
5969
+
5970
+ updateScrub(position, speed) {
5971
+ if (!this.#isDragging) return this.#numericValue();
5972
+ const clientX =
5973
+ typeof position === "number"
5974
+ ? position
5975
+ : Number(position?.clientX ?? position?.x);
5976
+ if (!Number.isFinite(clientX)) return this.#numericValue();
5977
+ const multiplier =
5978
+ speed ??
5979
+ (typeof position === "object" && position?.shiftKey ? 10 : 1);
5980
+ this.#updateDrag(clientX, multiplier);
5981
+ return this.#numericValue();
5982
+ }
5983
+
5984
+ endScrub(commit = true) {
5985
+ if (!this.#isDragging) return this.#numericValue();
5986
+ if (commit && this.#numericValue() !== this.#dragStartValue) {
5987
+ this.#commitValue(this.#numericValue(), { snap: true, emit: "change" });
5988
+ }
5989
+ this.#stopDrag();
5990
+ return this.#numericValue();
5991
+ }
5992
+
6174
5993
  #handlePointerMove(event) {
6175
5994
  if (!this.#isDragging) return;
6176
5995
  if (this.#dragPointerId !== null && event.pointerId !== this.#dragPointerId) {
6177
5996
  return;
6178
5997
  }
6179
- this.#updateDrag(event.clientX, event.shiftKey ? 10 : 1);
5998
+ this.updateScrub(event);
6180
5999
  }
6181
6000
 
6182
6001
  #updateDrag(clientX, speed = 1) {
6183
6002
  const width = this.#wheel?.clientWidth || 1;
6184
6003
  const visibleSteps =
6185
- PropskitWheel.#TICK_COUNT *
6186
- ((PropskitWheel.#HALF_FOV_DEG * 2) / 360);
6004
+ FigInputWheel.#TICK_COUNT *
6005
+ ((FigInputWheel.#HALF_FOV_DEG * 2) / 360);
6187
6006
  const visibleUnits = this.#step() * visibleSteps;
6188
6007
  const delta =
6189
6008
  (clientX - this.#dragStartX) * (visibleUnits / width) * speed;
@@ -6201,18 +6020,15 @@ class PropskitWheel extends HTMLElement {
6201
6020
  if (this.#dragPointerId !== null && event.pointerId !== this.#dragPointerId) {
6202
6021
  return;
6203
6022
  }
6204
- if (this.#numericValue() !== this.#dragStartValue) {
6205
- this.#commitValue(this.#numericValue(), { snap: true, emit: "change" });
6206
- }
6207
- this.#stopDrag();
6023
+ this.endScrub();
6208
6024
  }
6209
6025
 
6210
6026
  #stopDrag() {
6211
6027
  this.#isDragging = false;
6212
6028
  this.#dragPointerId = null;
6213
6029
  this.#visualValue = null;
6214
- this.removeAttribute("data-active");
6215
- document.body.classList.remove(PropskitWheel.#DRAGGING_BODY_CLASS);
6030
+ this.removeAttribute("data-fig-input-wheel-active");
6031
+ document.body.classList.remove(FigInputWheel.#DRAGGING_BODY_CLASS);
6216
6032
  this.#resetElasticPull();
6217
6033
  window.removeEventListener("pointermove", this.#boundPointerMove);
6218
6034
  window.removeEventListener("pointerup", this.#boundPointerUp);
@@ -6220,107 +6036,14 @@ class PropskitWheel extends HTMLElement {
6220
6036
  this.#queueWheelLayout();
6221
6037
  }
6222
6038
 
6223
- #handleNumberPointerDown(event) {
6224
- if (
6225
- event.button !== 0 ||
6226
- event.altKey ||
6227
- figLabBooleanAttribute(this, "disabled") ||
6228
- !(event.target instanceof Element)
6229
- ) {
6230
- return;
6231
- }
6232
- const input = event.target.closest("fig-input-number input");
6233
- if (!input || input === document.activeElement) return;
6234
-
6235
- event.preventDefault();
6236
- event.stopImmediatePropagation();
6237
- this.#stopNumberTracking();
6238
- this.#numberPointerId = event.pointerId;
6239
- this.#numberPointerStartX = event.clientX;
6240
- this.#numberPointerStartY = event.clientY;
6241
- this.#numberPointerStartValue = this.#numericValue();
6242
- window.addEventListener("pointermove", this.#boundNumberPointerMove);
6243
- window.addEventListener("pointerup", this.#boundNumberPointerEnd, {
6244
- once: true,
6245
- });
6246
- window.addEventListener("pointercancel", this.#boundNumberPointerEnd, {
6247
- once: true,
6248
- });
6249
- window.addEventListener("blur", this.#boundNumberPointerEnd, {
6250
- once: true,
6251
- });
6252
- }
6253
-
6254
- #handleNumberPointerMove(event) {
6255
- if (event.pointerId !== this.#numberPointerId) return;
6256
- if (event.buttons === 0) {
6257
- this.#handleNumberPointerEnd(event);
6258
- return;
6259
- }
6260
- if (!this.#isNumberScrubbing) {
6261
- const distance = Math.hypot(
6262
- event.clientX - this.#numberPointerStartX,
6263
- event.clientY - this.#numberPointerStartY,
6264
- );
6265
- if (distance < 4) return;
6266
- this.#stopKeyboardAnimation();
6267
- this.#isNumberScrubbing = true;
6268
- this.setAttribute("data-number-scrubbing", "");
6269
- this.#isDragging = true;
6270
- this.#dragPointerId = event.pointerId;
6271
- this.#dragStartX = this.#numberPointerStartX;
6272
- this.#dragStartValue = this.#numberPointerStartValue;
6273
- this.#startElasticPull();
6274
- this.setAttribute("data-active", "");
6275
- document.body.classList.add(PropskitWheel.#DRAGGING_BODY_CLASS);
6276
- this.#wheel?.focus();
6277
- }
6278
- this.#updateDrag(event.clientX, event.shiftKey ? 10 : 1);
6279
- }
6280
-
6281
- #handleNumberPointerEnd(event) {
6282
- if (
6283
- event?.pointerId !== undefined &&
6284
- this.#numberPointerId !== null &&
6285
- event.pointerId !== this.#numberPointerId
6286
- ) {
6287
- return;
6288
- }
6289
- const wasScrubbing = this.#isNumberScrubbing;
6290
- this.#stopNumberTracking();
6291
- if (wasScrubbing) {
6292
- this.#commitValue(this.#numericValue(), { snap: true, emit: "change" });
6293
- this.#stopDrag();
6294
- this.#suppressNumberClick = true;
6295
- clearTimeout(this.#numberClickResetTimer);
6296
- this.#numberClickResetTimer = window.setTimeout(() => {
6297
- this.#suppressNumberClick = false;
6298
- this.#numberClickResetTimer = 0;
6299
- }, 0);
6300
- requestAnimationFrame(() => this.#wheel?.focus());
6301
- return;
6302
- }
6303
- this.#input?.querySelector("input")?.focus();
6304
- }
6305
-
6306
- #stopNumberTracking() {
6307
- window.removeEventListener("pointermove", this.#boundNumberPointerMove);
6308
- window.removeEventListener("pointerup", this.#boundNumberPointerEnd);
6309
- window.removeEventListener("pointercancel", this.#boundNumberPointerEnd);
6310
- window.removeEventListener("blur", this.#boundNumberPointerEnd);
6311
- this.#numberPointerId = null;
6312
- this.#isNumberScrubbing = false;
6313
- this.removeAttribute("data-number-scrubbing");
6314
- }
6315
-
6316
6039
  #startElasticPull() {
6317
6040
  this.#resetElasticPull();
6318
6041
  if (this.getAttribute("elastic") === "false") return;
6319
6042
  this.#handleDragMaxPx = this.#readCssLength(
6320
- "--propskit-wheel-handle-drag-max",
6043
+ "--fig-input-wheel-handle-drag-max",
6321
6044
  );
6322
6045
  this.#elasticMaxPx = this.#readCssLength(
6323
- "--propskit-wheel-elastic-distance",
6046
+ "--fig-input-wheel-elastic-distance",
6324
6047
  );
6325
6048
  const rect = this.#wheel?.getBoundingClientRect();
6326
6049
  if (rect) {
@@ -6367,9 +6090,9 @@ class PropskitWheel extends HTMLElement {
6367
6090
  Math.tanh(
6368
6091
  dragDelta / Math.max(1, this.#handleDragMaxPx * 3),
6369
6092
  );
6370
- this.dataset.elasticDragging = "true";
6093
+ this.setAttribute("data-fig-input-wheel-elastic-dragging", "");
6371
6094
  this.style.setProperty(
6372
- "--propskit-wheel-handle-drag-offset",
6095
+ "--fig-input-wheel-handle-drag-offset",
6373
6096
  `${offset}px`,
6374
6097
  );
6375
6098
 
@@ -6382,17 +6105,17 @@ class PropskitWheel extends HTMLElement {
6382
6105
  : 0
6383
6106
  : 0;
6384
6107
  if (!overshoot || !this.#elasticMaxPx) {
6385
- this.style.removeProperty("--propskit-wheel-elastic-scale");
6386
- this.style.removeProperty("--propskit-wheel-elastic-origin");
6108
+ this.style.removeProperty("--fig-input-wheel-elastic-scale");
6109
+ this.style.removeProperty("--fig-input-wheel-elastic-origin");
6387
6110
  return;
6388
6111
  }
6389
6112
  const stretch = Math.min(this.#elasticMaxPx, Math.abs(overshoot) * 0.5);
6390
6113
  const scale = this.#elasticHostWidth
6391
6114
  ? (this.#elasticHostWidth + stretch) / this.#elasticHostWidth
6392
6115
  : 1;
6393
- this.style.setProperty("--propskit-wheel-elastic-scale", `${scale}`);
6116
+ this.style.setProperty("--fig-input-wheel-elastic-scale", `${scale}`);
6394
6117
  this.style.setProperty(
6395
- "--propskit-wheel-elastic-origin",
6118
+ "--fig-input-wheel-elastic-origin",
6396
6119
  overshoot < 0 ? "right center" : "left center",
6397
6120
  );
6398
6121
  }
@@ -6406,10 +6129,10 @@ class PropskitWheel extends HTMLElement {
6406
6129
  }
6407
6130
 
6408
6131
  #clearElasticPull() {
6409
- this.removeAttribute("data-elastic-dragging");
6410
- this.style.removeProperty("--propskit-wheel-handle-drag-offset");
6411
- this.style.removeProperty("--propskit-wheel-elastic-scale");
6412
- this.style.removeProperty("--propskit-wheel-elastic-origin");
6132
+ this.removeAttribute("data-fig-input-wheel-elastic-dragging");
6133
+ this.style.removeProperty("--fig-input-wheel-handle-drag-offset");
6134
+ this.style.removeProperty("--fig-input-wheel-elastic-scale");
6135
+ this.style.removeProperty("--fig-input-wheel-elastic-origin");
6413
6136
  }
6414
6137
 
6415
6138
  #handleWheel(event) {
@@ -6469,7 +6192,13 @@ class PropskitWheel extends HTMLElement {
6469
6192
  this.#commitValue(this.#numericValue(), { emit: "change" });
6470
6193
  }
6471
6194
 
6472
- #animateKeyboardMovement(from, to, direction, multiplier) {
6195
+ #animateKeyboardMovement(
6196
+ from,
6197
+ to,
6198
+ direction,
6199
+ multiplier,
6200
+ animateHandle = true,
6201
+ ) {
6473
6202
  if (
6474
6203
  from === to ||
6475
6204
  globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches
@@ -6484,25 +6213,26 @@ class PropskitWheel extends HTMLElement {
6484
6213
  }
6485
6214
  clearTimeout(this.#keyboardSettleTimer);
6486
6215
  this.#keyboardSettleTimer = 0;
6487
- this.removeAttribute("data-keyboard-settling");
6216
+ this.removeAttribute("data-fig-input-wheel-keyboard-settling");
6488
6217
  const visualFrom = this.#visualValue ?? from;
6489
6218
  const handleFrom =
6490
6219
  Number.parseFloat(
6491
- this.style.getPropertyValue("--propskit-wheel-handle-drag-offset"),
6220
+ this.style.getPropertyValue("--fig-input-wheel-handle-drag-offset"),
6492
6221
  ) || 0;
6493
6222
  const duration = multiplier > 1 ? 120 : 90;
6494
6223
  const startedAt = performance.now();
6495
- const animateHandle = this.getAttribute("elastic") !== "false";
6496
- this.setAttribute("data-keyboard-moving", "");
6224
+ const shouldAnimateHandle =
6225
+ animateHandle && this.getAttribute("elastic") !== "false";
6226
+ this.setAttribute("data-fig-input-wheel-keyboard-moving", "");
6497
6227
 
6498
6228
  const frame = (now) => {
6499
6229
  const progress = Math.min(1, (now - startedAt) / duration);
6500
6230
  this.#visualValue = visualFrom + (to - visualFrom) * progress;
6501
- if (animateHandle) {
6231
+ if (shouldAnimateHandle) {
6502
6232
  const eased = Math.sin((progress * Math.PI) / 2);
6503
6233
  const offset = handleFrom + (direction * 4 - handleFrom) * eased;
6504
6234
  this.style.setProperty(
6505
- "--propskit-wheel-handle-drag-offset",
6235
+ "--fig-input-wheel-handle-drag-offset",
6506
6236
  `${offset}px`,
6507
6237
  );
6508
6238
  }
@@ -6515,11 +6245,11 @@ class PropskitWheel extends HTMLElement {
6515
6245
 
6516
6246
  this.#keyboardAnimationFrame = 0;
6517
6247
  this.#visualValue = null;
6518
- this.removeAttribute("data-keyboard-moving");
6519
- this.setAttribute("data-keyboard-settling", "");
6520
- this.style.removeProperty("--propskit-wheel-handle-drag-offset");
6248
+ this.removeAttribute("data-fig-input-wheel-keyboard-moving");
6249
+ this.setAttribute("data-fig-input-wheel-keyboard-settling", "");
6250
+ this.style.removeProperty("--fig-input-wheel-handle-drag-offset");
6521
6251
  this.#keyboardSettleTimer = window.setTimeout(() => {
6522
- this.removeAttribute("data-keyboard-settling");
6252
+ this.removeAttribute("data-fig-input-wheel-keyboard-settling");
6523
6253
  this.#keyboardSettleTimer = 0;
6524
6254
  }, 140);
6525
6255
  this.#queueWheelLayout();
@@ -6536,9 +6266,9 @@ class PropskitWheel extends HTMLElement {
6536
6266
  clearTimeout(this.#keyboardSettleTimer);
6537
6267
  this.#keyboardSettleTimer = 0;
6538
6268
  this.#visualValue = null;
6539
- this.removeAttribute("data-keyboard-moving");
6540
- this.removeAttribute("data-keyboard-settling");
6541
- this.style.removeProperty("--propskit-wheel-handle-drag-offset");
6269
+ this.removeAttribute("data-fig-input-wheel-keyboard-moving");
6270
+ this.removeAttribute("data-fig-input-wheel-keyboard-settling");
6271
+ this.style.removeProperty("--fig-input-wheel-handle-drag-offset");
6542
6272
  if (this.isConnected) this.#queueWheelLayout();
6543
6273
  }
6544
6274
 
@@ -6555,6 +6285,25 @@ class PropskitWheel extends HTMLElement {
6555
6285
  this.#commitValue(Number.isFinite(parsed) ? parsed : 0);
6556
6286
  }
6557
6287
 
6288
+ spinTo(nextValue) {
6289
+ const previousValue = this.#numericValue();
6290
+ this.value = nextValue;
6291
+ const value = this.#numericValue();
6292
+ if (this.getAttribute("spin") === "false") {
6293
+ this.#stopKeyboardAnimation();
6294
+ return value;
6295
+ }
6296
+ const direction = Math.sign(value - previousValue);
6297
+ this.#animateKeyboardMovement(
6298
+ previousValue,
6299
+ value,
6300
+ direction,
6301
+ Math.abs(value - previousValue) > this.#step() ? 10 : 1,
6302
+ false,
6303
+ );
6304
+ return value;
6305
+ }
6306
+
6558
6307
  get min() {
6559
6308
  return this.#boundMin();
6560
6309
  }
@@ -6571,6 +6320,585 @@ class PropskitWheel extends HTMLElement {
6571
6320
  this.#writeBound("max", nextValue);
6572
6321
  }
6573
6322
 
6323
+ get step() {
6324
+ return this.#step();
6325
+ }
6326
+
6327
+ focus(options) {
6328
+ HTMLElement.prototype.focus.call(this, options);
6329
+ }
6330
+ }
6331
+ figLabDefineElement("fig-input-wheel", FigInputWheel);
6332
+
6333
+ /**
6334
+ * Labeled numeric wheel with an optional editable number field.
6335
+ *
6336
+ * @attr {string} label - Field label. Defaults to "Value".
6337
+ * @attr {string} default - Reset target.
6338
+ * @attr {number} precision - Number field display decimals.
6339
+ * @attr {boolean|string} spin - Keeps wheel ticks synchronized to value. Defaults to true.
6340
+ * @attr {boolean|string} text - Shows the number field. Defaults to true.
6341
+ * @attr {string} size - Set to "small" for compact sizing.
6342
+ * @attr {string} variant - Set to "minimal" for compact chrome.
6343
+ * @fires input - Retargeted numeric composed event.
6344
+ * @fires change - Retargeted numeric composed event.
6345
+ */
6346
+ class PropskitWheel extends HTMLElement {
6347
+ static #RESERVED_ATTRS = new Set([
6348
+ "label",
6349
+ "size",
6350
+ "disabled",
6351
+ "variant",
6352
+ "elastic",
6353
+ "spin",
6354
+ "text",
6355
+ "default",
6356
+ "class",
6357
+ "style",
6358
+ "id",
6359
+ "oninput",
6360
+ "onchange",
6361
+ ]);
6362
+
6363
+ static get observedAttributes() {
6364
+ return [
6365
+ "label",
6366
+ "units",
6367
+ "value",
6368
+ "disabled",
6369
+ "step",
6370
+ "precision",
6371
+ "min",
6372
+ "max",
6373
+ "elastic",
6374
+ "spin",
6375
+ "text",
6376
+ ];
6377
+ }
6378
+
6379
+ #surface = null;
6380
+ #wheel = null;
6381
+ #label = null;
6382
+ #input = null;
6383
+ #hasCustomLabel = false;
6384
+ #observer = null;
6385
+ #elasticObserver = null;
6386
+ #managedInputAttrs = new Set();
6387
+ #initialValue = null;
6388
+ #numberPointerId = null;
6389
+ #numberPointerStartX = 0;
6390
+ #numberPointerStartY = 0;
6391
+ #numberPointerStartValue = 0;
6392
+ #isNumberScrubbing = false;
6393
+ #suppressNumberClick = false;
6394
+ #numberClickResetTimer = 0;
6395
+ #boundPrimitiveInput = this.#handlePrimitiveEvent.bind(this, "input");
6396
+ #boundPrimitiveChange = this.#handlePrimitiveEvent.bind(this, "change");
6397
+ #boundNumberInput = this.#handleNumberEvent.bind(this, "input");
6398
+ #boundNumberChange = this.#handleNumberEvent.bind(this, "change");
6399
+ #boundNumberPointerDown = this.#handleNumberPointerDown.bind(this);
6400
+ #boundNumberPointerMove = this.#handleNumberPointerMove.bind(this);
6401
+ #boundNumberPointerEnd = this.#handleNumberPointerEnd.bind(this);
6402
+ #boundClick = this.#handleClick.bind(this);
6403
+
6404
+ connectedCallback() {
6405
+ if (!this.#surface) this.#initialize();
6406
+ this.#syncLabel();
6407
+ this.#syncText();
6408
+ this.#syncPrimitive();
6409
+ this.#syncInputAttributes();
6410
+ this.#bindEvents();
6411
+ figLabConnectPropskitResetMenu(this);
6412
+ this.#observer?.disconnect();
6413
+ this.#observer = new MutationObserver((mutations) => {
6414
+ if (
6415
+ mutations.some(
6416
+ ({ type, attributeName }) =>
6417
+ type === "attributes" &&
6418
+ attributeName &&
6419
+ !PropskitWheel.#RESERVED_ATTRS.has(attributeName) &&
6420
+ !PropskitWheel.observedAttributes.includes(attributeName) &&
6421
+ !attributeName.startsWith("data-"),
6422
+ )
6423
+ ) {
6424
+ this.#syncInputAttributes();
6425
+ }
6426
+ });
6427
+ this.#observer.observe(this, { attributes: true });
6428
+ this.#elasticObserver?.disconnect();
6429
+ this.#elasticObserver = new MutationObserver(() => {
6430
+ this.#syncElasticComposition();
6431
+ });
6432
+ if (this.#wheel) {
6433
+ this.#elasticObserver.observe(this.#wheel, {
6434
+ attributes: true,
6435
+ attributeFilter: ["style", "data-fig-input-wheel-elastic-dragging"],
6436
+ });
6437
+ }
6438
+ this.#syncElasticComposition();
6439
+ }
6440
+
6441
+ disconnectedCallback() {
6442
+ this.#observer?.disconnect();
6443
+ this.#elasticObserver?.disconnect();
6444
+ this.#elasticObserver = null;
6445
+ this.#clearElasticComposition();
6446
+ this.#unbindEvents();
6447
+ this.#stopNumberTracking();
6448
+ clearTimeout(this.#numberClickResetTimer);
6449
+ this.#numberClickResetTimer = 0;
6450
+ this.#wheel?.endScrub(false);
6451
+ figLabDisconnectPropskitResetMenu(this);
6452
+ }
6453
+
6454
+ attributeChangedCallback(name, oldValue, newValue) {
6455
+ if (oldValue === newValue || !this.#surface) return;
6456
+ if (name === "label") this.#syncLabel();
6457
+ if (name === "text") this.#syncText();
6458
+ if (
6459
+ [
6460
+ "units",
6461
+ "value",
6462
+ "disabled",
6463
+ "step",
6464
+ "min",
6465
+ "max",
6466
+ "elastic",
6467
+ "spin",
6468
+ ].includes(name)
6469
+ ) {
6470
+ this.#syncPrimitive();
6471
+ }
6472
+ if (
6473
+ ["units", "value", "disabled", "step", "precision", "min", "max"].includes(
6474
+ name,
6475
+ )
6476
+ ) {
6477
+ this.#syncInputAttributes();
6478
+ }
6479
+ }
6480
+
6481
+ #initialize() {
6482
+ this.#initialValue = this.getAttribute("value") ?? "0";
6483
+ const initialChildren = Array.from(this.childNodes).filter(
6484
+ (node) =>
6485
+ node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()),
6486
+ );
6487
+ const customLabel = initialChildren.find(
6488
+ (node) => node.nodeType === Node.ELEMENT_NODE && node.matches("label"),
6489
+ );
6490
+ const surface = figLabCreateElement("div", {
6491
+ className: "propskit-wheel-surface",
6492
+ });
6493
+ const label = customLabel || document.createElement("label");
6494
+ const wheel = document.createElement("fig-input-wheel");
6495
+ const input = document.createElement("fig-input-number");
6496
+ const labelId = figLabUniqueId("propskit-wheel-label");
6497
+ label.id = labelId;
6498
+ wheel.setAttribute("aria-labelledby", labelId);
6499
+ input.setAttribute("aria-labelledby", labelId);
6500
+ surface.append(label, wheel, input);
6501
+ this.#surface = surface;
6502
+ this.#wheel = wheel;
6503
+ this.#label = label;
6504
+ this.#input = input;
6505
+ this.#hasCustomLabel = Boolean(customLabel);
6506
+ this.replaceChildren(surface);
6507
+ for (const node of initialChildren) {
6508
+ if (node !== customLabel) input.appendChild(node);
6509
+ }
6510
+ }
6511
+
6512
+ #syncLabel() {
6513
+ if (!this.#label) return;
6514
+ if (!this.#hasCustomLabel) {
6515
+ this.#label.textContent = this.hasAttribute("label")
6516
+ ? (this.getAttribute("label") ?? "")
6517
+ : "Value";
6518
+ }
6519
+ this.toggleAttribute(
6520
+ "data-label-empty",
6521
+ !(this.#label.textContent ?? "").trim(),
6522
+ );
6523
+ }
6524
+
6525
+ #syncText() {
6526
+ if (!this.#surface || !this.#input) return;
6527
+ const enabled = this.getAttribute("text") !== "false";
6528
+ const isInserted = this.#input.parentElement === this.#surface;
6529
+ if (enabled && !isInserted) this.#surface.append(this.#input);
6530
+ else if (!enabled && isInserted) this.#input.remove();
6531
+ }
6532
+
6533
+ #mirrorAttribute(name) {
6534
+ if (!this.#wheel) return;
6535
+ if (this.hasAttribute(name)) {
6536
+ this.#wheel.setAttribute(name, this.getAttribute(name) ?? "");
6537
+ } else {
6538
+ this.#wheel.removeAttribute(name);
6539
+ }
6540
+ }
6541
+
6542
+ #syncPrimitive() {
6543
+ if (!this.#wheel) return;
6544
+ this.#wheel.removeAttribute("units");
6545
+ for (const name of ["min", "max", "elastic", "spin"]) {
6546
+ this.#mirrorAttribute(name);
6547
+ }
6548
+ if (this.hasAttribute("step")) this.#mirrorAttribute("step");
6549
+ else this.#wheel.setAttribute("step", String(this.#defaultStep()));
6550
+ this.#wheel.toggleAttribute(
6551
+ "disabled",
6552
+ figLabBooleanAttribute(this, "disabled"),
6553
+ );
6554
+ this.#wheel.value = this.getAttribute("value") ?? "0";
6555
+ const normalized = this.#wheel.value;
6556
+ if (this.getAttribute("value") !== normalized) {
6557
+ this.setAttribute("value", normalized);
6558
+ }
6559
+ if (this.#input?.getAttribute("value") !== normalized) {
6560
+ this.#input?.setAttribute("value", normalized);
6561
+ }
6562
+ this.#syncPrimitiveAria();
6563
+ }
6564
+
6565
+ #syncElasticComposition() {
6566
+ if (!this.#wheel) return;
6567
+ const active = this.#wheel.hasAttribute(
6568
+ "data-fig-input-wheel-elastic-dragging",
6569
+ );
6570
+ if (!active) {
6571
+ this.#clearElasticComposition();
6572
+ return;
6573
+ }
6574
+ this.toggleAttribute("data-propskit-wheel-elastic-dragging", true);
6575
+ this.style.setProperty(
6576
+ "--propskit-wheel-elastic-scale",
6577
+ this.#wheel.style.getPropertyValue("--fig-input-wheel-elastic-scale") ||
6578
+ "1",
6579
+ );
6580
+ this.style.setProperty(
6581
+ "--propskit-wheel-elastic-origin",
6582
+ this.#wheel.style.getPropertyValue("--fig-input-wheel-elastic-origin") ||
6583
+ "left center",
6584
+ );
6585
+ }
6586
+
6587
+ #clearElasticComposition() {
6588
+ this.removeAttribute("data-propskit-wheel-elastic-dragging");
6589
+ this.style.removeProperty("--propskit-wheel-elastic-scale");
6590
+ this.style.removeProperty("--propskit-wheel-elastic-origin");
6591
+ }
6592
+
6593
+ #getForwardedInputAttrNames() {
6594
+ return this.getAttributeNames().filter(
6595
+ (name) =>
6596
+ !PropskitWheel.#RESERVED_ATTRS.has(name) &&
6597
+ !PropskitWheel.observedAttributes.includes(name) &&
6598
+ !name.startsWith("data-"),
6599
+ );
6600
+ }
6601
+
6602
+ #units() {
6603
+ const raw = (this.getAttribute("units") || "").trim();
6604
+ const normalized = raw.toLowerCase();
6605
+ if (
6606
+ normalized === "ms" ||
6607
+ normalized === "millisecond" ||
6608
+ normalized === "milliseconds"
6609
+ ) {
6610
+ return "ms";
6611
+ }
6612
+ if (
6613
+ normalized === "s" ||
6614
+ normalized === "second" ||
6615
+ normalized === "seconds"
6616
+ ) {
6617
+ return "s";
6618
+ }
6619
+ return raw;
6620
+ }
6621
+
6622
+ #defaultStep() {
6623
+ const units = this.#units();
6624
+ if (units === "s") return 0.1;
6625
+ if (units === "ms") return 100;
6626
+ return 1;
6627
+ }
6628
+
6629
+ #defaultPrecision() {
6630
+ return this.#units() === "s" ? 2 : 0;
6631
+ }
6632
+
6633
+ #syncPrimitiveAria() {
6634
+ if (!this.#wheel) return;
6635
+ const value = Number(this.#wheel.value);
6636
+ const units = this.#units();
6637
+ this.#wheel.setAttribute(
6638
+ "aria-valuetext",
6639
+ units
6640
+ ? `${value} ${
6641
+ units === "s" ? "seconds" : units === "ms" ? "milliseconds" : units
6642
+ }`
6643
+ : String(value),
6644
+ );
6645
+ }
6646
+
6647
+ #syncInputAttributes() {
6648
+ if (!this.#input || !this.#wheel) return;
6649
+ const forwarded = this.#getForwardedInputAttrNames();
6650
+ const nextManaged = new Set(forwarded);
6651
+ for (const name of this.#managedInputAttrs) {
6652
+ if (!nextManaged.has(name)) this.#input.removeAttribute(name);
6653
+ }
6654
+ for (const name of forwarded) {
6655
+ this.#input.setAttribute(name, this.getAttribute(name) ?? "");
6656
+ }
6657
+ const units = this.#units();
6658
+ if (units) this.#input.setAttribute("units", units);
6659
+ else this.#input.removeAttribute("units");
6660
+ this.#input.setAttribute("step", String(this.#wheel.step));
6661
+ if (this.hasAttribute("precision")) {
6662
+ this.#input.setAttribute(
6663
+ "precision",
6664
+ this.getAttribute("precision") ?? "",
6665
+ );
6666
+ } else {
6667
+ this.#input.setAttribute("precision", String(this.#defaultPrecision()));
6668
+ }
6669
+ for (const name of ["min", "max"]) {
6670
+ const value = this.#wheel[name];
6671
+ if (value === null) this.#input.removeAttribute(name);
6672
+ else this.#input.setAttribute(name, String(value));
6673
+ }
6674
+ this.#input.toggleAttribute(
6675
+ "disabled",
6676
+ figLabBooleanAttribute(this, "disabled"),
6677
+ );
6678
+ this.#input.setAttribute("value", this.#wheel.value);
6679
+ this.#managedInputAttrs = nextManaged;
6680
+ }
6681
+
6682
+ #bindEvents() {
6683
+ this.#unbindEvents();
6684
+ this.#wheel?.addEventListener("input", this.#boundPrimitiveInput);
6685
+ this.#wheel?.addEventListener("change", this.#boundPrimitiveChange);
6686
+ this.#input?.addEventListener("input", this.#boundNumberInput);
6687
+ this.#input?.addEventListener("change", this.#boundNumberChange);
6688
+ this.addEventListener("pointerdown", this.#boundNumberPointerDown, {
6689
+ capture: true,
6690
+ });
6691
+ this.addEventListener("click", this.#boundClick, true);
6692
+ }
6693
+
6694
+ #unbindEvents() {
6695
+ this.#wheel?.removeEventListener("input", this.#boundPrimitiveInput);
6696
+ this.#wheel?.removeEventListener("change", this.#boundPrimitiveChange);
6697
+ this.#input?.removeEventListener("input", this.#boundNumberInput);
6698
+ this.#input?.removeEventListener("change", this.#boundNumberChange);
6699
+ this.removeEventListener("pointerdown", this.#boundNumberPointerDown, {
6700
+ capture: true,
6701
+ });
6702
+ this.removeEventListener("click", this.#boundClick, true);
6703
+ }
6704
+
6705
+ #setSynchronizedValue(value, { spin = false } = {}) {
6706
+ const parsed = Number(value);
6707
+ if (!this.#wheel) {
6708
+ const normalized = Number.isFinite(parsed) ? parsed : 0;
6709
+ this.setAttribute("value", String(normalized));
6710
+ return normalized;
6711
+ }
6712
+ const nextValue = Number.isFinite(parsed) ? parsed : 0;
6713
+ if (spin) this.#wheel.spinTo(nextValue);
6714
+ else this.#wheel.value = nextValue;
6715
+ const normalized = this.#wheel.value;
6716
+ if (this.getAttribute("value") !== normalized) {
6717
+ this.setAttribute("value", normalized);
6718
+ }
6719
+ if (this.#input?.getAttribute("value") !== normalized) {
6720
+ this.#input?.setAttribute("value", normalized);
6721
+ }
6722
+ this.#syncPrimitiveAria();
6723
+ return Number(normalized);
6724
+ }
6725
+
6726
+ #emit(type, value) {
6727
+ this.dispatchEvent(
6728
+ new CustomEvent(type, {
6729
+ detail: value,
6730
+ bubbles: true,
6731
+ cancelable: true,
6732
+ composed: true,
6733
+ }),
6734
+ );
6735
+ }
6736
+
6737
+ #handlePrimitiveEvent(type, event) {
6738
+ event.stopImmediatePropagation();
6739
+ const value = this.#setSynchronizedValue(event.detail);
6740
+ this.#emit(type, value);
6741
+ }
6742
+
6743
+ #handleNumberEvent(type, event) {
6744
+ event.stopImmediatePropagation();
6745
+ if (figLabBooleanAttribute(this, "disabled")) return;
6746
+ const raw =
6747
+ event instanceof CustomEvent && event.detail !== undefined
6748
+ ? event.detail
6749
+ : this.#input?.value;
6750
+ const value = this.#setSynchronizedValue(raw, { spin: type === "input" });
6751
+ this.#emit(type, value);
6752
+ }
6753
+
6754
+ #handleNumberPointerDown(event) {
6755
+ if (
6756
+ event.button !== 0 ||
6757
+ event.altKey ||
6758
+ figLabBooleanAttribute(this, "disabled") ||
6759
+ !(event.target instanceof Element)
6760
+ ) {
6761
+ return;
6762
+ }
6763
+ const input = event.target.closest("fig-input-number input");
6764
+ if (!input || input === document.activeElement) return;
6765
+ event.preventDefault();
6766
+ event.stopImmediatePropagation();
6767
+ this.#stopNumberTracking();
6768
+ this.#numberPointerId = event.pointerId;
6769
+ this.#numberPointerStartX = event.clientX;
6770
+ this.#numberPointerStartY = event.clientY;
6771
+ this.#numberPointerStartValue = Number(this.#wheel?.value ?? 0);
6772
+ window.addEventListener("pointermove", this.#boundNumberPointerMove);
6773
+ window.addEventListener("pointerup", this.#boundNumberPointerEnd, {
6774
+ once: true,
6775
+ });
6776
+ window.addEventListener("pointercancel", this.#boundNumberPointerEnd, {
6777
+ once: true,
6778
+ });
6779
+ window.addEventListener("blur", this.#boundNumberPointerEnd, {
6780
+ once: true,
6781
+ });
6782
+ }
6783
+
6784
+ #handleNumberPointerMove(event) {
6785
+ if (event.pointerId !== this.#numberPointerId) return;
6786
+ if (event.buttons === 0) {
6787
+ this.#handleNumberPointerEnd(event);
6788
+ return;
6789
+ }
6790
+ if (!this.#isNumberScrubbing) {
6791
+ const distance = Math.hypot(
6792
+ event.clientX - this.#numberPointerStartX,
6793
+ event.clientY - this.#numberPointerStartY,
6794
+ );
6795
+ if (distance < 4) return;
6796
+ this.#isNumberScrubbing = true;
6797
+ this.setAttribute("data-number-scrubbing", "");
6798
+ this.#wheel?.beginScrub({
6799
+ clientX: this.#numberPointerStartX,
6800
+ pointerId: event.pointerId,
6801
+ startValue: this.#numberPointerStartValue,
6802
+ });
6803
+ }
6804
+ this.#wheel?.updateScrub(event);
6805
+ }
6806
+
6807
+ #handleNumberPointerEnd(event) {
6808
+ if (
6809
+ event?.pointerId !== undefined &&
6810
+ this.#numberPointerId !== null &&
6811
+ event.pointerId !== this.#numberPointerId
6812
+ ) {
6813
+ return;
6814
+ }
6815
+ const wasScrubbing = this.#isNumberScrubbing;
6816
+ this.#stopNumberTracking();
6817
+ if (wasScrubbing) {
6818
+ this.#wheel?.endScrub();
6819
+ this.#suppressNumberClick = true;
6820
+ clearTimeout(this.#numberClickResetTimer);
6821
+ this.#numberClickResetTimer = window.setTimeout(() => {
6822
+ this.#suppressNumberClick = false;
6823
+ this.#numberClickResetTimer = 0;
6824
+ }, 0);
6825
+ requestAnimationFrame(() => this.#wheel?.focus());
6826
+ return;
6827
+ }
6828
+ this.#input?.querySelector("input")?.focus();
6829
+ }
6830
+
6831
+ #stopNumberTracking() {
6832
+ window.removeEventListener("pointermove", this.#boundNumberPointerMove);
6833
+ window.removeEventListener("pointerup", this.#boundNumberPointerEnd);
6834
+ window.removeEventListener("pointercancel", this.#boundNumberPointerEnd);
6835
+ window.removeEventListener("blur", this.#boundNumberPointerEnd);
6836
+ this.#numberPointerId = null;
6837
+ this.#isNumberScrubbing = false;
6838
+ this.removeAttribute("data-number-scrubbing");
6839
+ }
6840
+
6841
+ #handleClick(event) {
6842
+ if (figLabBooleanAttribute(this, "disabled")) return;
6843
+ if (
6844
+ event.target instanceof Element &&
6845
+ event.target.closest("fig-input-number, fig-menu")
6846
+ ) {
6847
+ if (
6848
+ this.#suppressNumberClick &&
6849
+ event.target.closest("fig-input-number")
6850
+ ) {
6851
+ event.preventDefault();
6852
+ event.stopImmediatePropagation();
6853
+ this.#suppressNumberClick = false;
6854
+ }
6855
+ return;
6856
+ }
6857
+ if (
6858
+ !(event.target instanceof Element) ||
6859
+ !event.target.closest("fig-input-wheel")
6860
+ ) {
6861
+ this.#wheel?.focus();
6862
+ }
6863
+ }
6864
+
6865
+ get value() {
6866
+ return this.getAttribute("value") ?? this.#wheel?.value ?? "0";
6867
+ }
6868
+
6869
+ set value(nextValue) {
6870
+ const parsed = Number(nextValue);
6871
+ this.#setSynchronizedValue(Number.isFinite(parsed) ? parsed : 0);
6872
+ }
6873
+
6874
+ get min() {
6875
+ return this.#wheel?.min ?? null;
6876
+ }
6877
+
6878
+ set min(nextValue) {
6879
+ if (nextValue === null || nextValue === undefined || nextValue === "") {
6880
+ this.removeAttribute("min");
6881
+ } else {
6882
+ const parsed = Number(nextValue);
6883
+ if (Number.isFinite(parsed)) this.setAttribute("min", String(parsed));
6884
+ else this.removeAttribute("min");
6885
+ }
6886
+ }
6887
+
6888
+ get max() {
6889
+ return this.#wheel?.max ?? null;
6890
+ }
6891
+
6892
+ set max(nextValue) {
6893
+ if (nextValue === null || nextValue === undefined || nextValue === "") {
6894
+ this.removeAttribute("max");
6895
+ } else {
6896
+ const parsed = Number(nextValue);
6897
+ if (Number.isFinite(parsed)) this.setAttribute("max", String(parsed));
6898
+ else this.removeAttribute("max");
6899
+ }
6900
+ }
6901
+
6574
6902
  get defaultValue() {
6575
6903
  return this.getAttribute("default") ?? this.#initialValue ?? "0";
6576
6904
  }
@@ -6581,8 +6909,9 @@ class PropskitWheel extends HTMLElement {
6581
6909
 
6582
6910
  resetToDefault() {
6583
6911
  const parsed = Number(this.defaultValue);
6584
- const value = Number.isFinite(parsed) ? parsed : 0;
6585
- this.#commitValue(value);
6912
+ const value = this.#setSynchronizedValue(
6913
+ Number.isFinite(parsed) ? parsed : 0,
6914
+ );
6586
6915
  figLabEmitPropskitReset(this, value);
6587
6916
  }
6588
6917
 
@@ -9756,6 +10085,7 @@ class FigReorder extends HTMLElement {
9756
10085
  "fig-input-gradient",
9757
10086
  "fig-easing-curve",
9758
10087
  "fig-input-angle",
10088
+ "fig-input-wheel",
9759
10089
  "fig-input-joystick",
9760
10090
  "fig-canvas-control",
9761
10091
  "propskit-color-point",