@rogieking/figui3 1.9.3 → 1.9.5

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/AUDIT.md ADDED
@@ -0,0 +1,183 @@
1
+ # Code Audit Report for fig.js
2
+
3
+ ## Critical Bugs
4
+
5
+ ### 1. **Infinite Recursion in FigButton.type getter (Line 58-59)**
6
+ **Location:** `FigButton` class, lines 58-59
7
+ **Issue:** The getter returns `this.type`, which calls itself infinitely
8
+ ```javascript
9
+ get type() {
10
+ return this.type; // ❌ Infinite recursion!
11
+ }
12
+ ```
13
+ **Fix:** Should return `this.getAttribute("type")` or use a private field
14
+ ```javascript
15
+ get type() {
16
+ return this.getAttribute("type") || "button";
17
+ }
18
+ ```
19
+
20
+ ### 2. **Impossible Logic Condition (Multiple locations)**
21
+ **Locations:**
22
+ - Line 103 (FigButton)
23
+ - Line 1043 (FigSlider)
24
+ - Line 1304 (FigInputText)
25
+
26
+ **Issue:** The condition `newValue === undefined && newValue !== null` is logically impossible. A value cannot be both `undefined` and not `null` simultaneously.
27
+
28
+ **Current code:**
29
+ ```javascript
30
+ this.disabled = this.input.disabled =
31
+ newValue === "true" ||
32
+ (newValue === undefined && newValue !== null);
33
+ ```
34
+
35
+ **Fix:** Should be:
36
+ ```javascript
37
+ this.disabled = this.input.disabled =
38
+ newValue !== null && newValue !== "false";
39
+ ```
40
+
41
+ ## Documentation Issues
42
+
43
+ ### 3. **Missing Documentation for Utility Functions**
44
+ **Location:** Lines 1-6
45
+ **Issue:** `figUniqueId()` and `figSupportsPopover()` lack JSDoc comments
46
+
47
+ **Fix:** Add documentation:
48
+ ```javascript
49
+ /**
50
+ * Generates a unique ID string using timestamp and random values
51
+ * @returns {string} A unique identifier
52
+ */
53
+ function figUniqueId() {
54
+ return Date.now().toString(36) + Math.random().toString(36).substring(2);
55
+ }
56
+
57
+ /**
58
+ * Checks if the browser supports the native popover API
59
+ * @returns {boolean} True if popover is supported
60
+ */
61
+ function figSupportsPopover() {
62
+ return HTMLElement.prototype.hasOwnProperty("popover");
63
+ }
64
+ ```
65
+
66
+ ### 4. **Incomplete Documentation for FigButton**
67
+ **Location:** Line 10
68
+ **Issue:** Documentation mentions "button", "toggle", "submit" but code also supports "link" type (line 81)
69
+
70
+ **Fix:** Update documentation:
71
+ ```javascript
72
+ * @attr {string} type - The button type: "button" (default), "toggle", "submit", or "link"
73
+ ```
74
+
75
+ ### 5. **Missing Documentation for FigButton Attributes**
76
+ **Location:** FigButton class
77
+ **Issue:** Missing documentation for `href` and `target` attributes used in link type
78
+
79
+ **Fix:** Add to JSDoc:
80
+ ```javascript
81
+ * @attr {string} href - URL for link type buttons
82
+ * @attr {string} target - Target window for link type buttons
83
+ ```
84
+
85
+ ### 6. **Missing Documentation for FigInputNumber**
86
+ **Location:** Line 1358
87
+ **Issue:** Missing `composed` property in CustomEvent documentation (though it's used in code)
88
+
89
+ ## Potential Issues
90
+
91
+ ### 7. **Unused Function**
92
+ **Location:** Line 4-6
93
+ **Issue:** `figSupportsPopover()` is defined but never used in the codebase
94
+
95
+ **Recommendation:** Either use it or remove it
96
+
97
+ ### 8. **Inconsistent Disabled Attribute Handling**
98
+ **Location:** Multiple components
99
+ **Issue:** Different components handle disabled attributes slightly differently:
100
+ - Some check `newValue === "true" || (newValue === undefined && newValue !== null)`
101
+ - Others check `newValue !== null && newValue !== "false"`
102
+
103
+ **Recommendation:** Standardize on one approach (preferably the second, which is correct)
104
+
105
+ ### 9. **Missing Error Handling**
106
+ **Location:** Multiple locations
107
+ **Issue:** No error handling for:
108
+ - `querySelector` calls that might return null
109
+ - `getAttribute` calls that might return unexpected values
110
+ - Event listener setup failures
111
+
112
+ **Example:** Line 54 - `this.button` might be null if querySelector fails
113
+
114
+ ### 10. **Potential Memory Leak**
115
+ **Location:** FigTooltip, line 310
116
+ **Issue:** Event listener added in `destroy()` method:
117
+ ```javascript
118
+ destroy() {
119
+ if (this.popup) {
120
+ this.popup.remove();
121
+ }
122
+ document.body.addEventListener("click", this.hidePopupOutsideClick); // ❌ Added but never removed
123
+ }
124
+ ```
125
+
126
+ ### 11. **Inconsistent Value Type Handling**
127
+ **Location:** FigInputNumber, line 1393-1394
128
+ **Issue:** Value is converted to Number but can be empty string:
129
+ ```javascript
130
+ this.value = valueAttr !== null && valueAttr !== "" ? Number(valueAttr) : "";
131
+ ```
132
+ This creates inconsistent types (Number vs String)
133
+
134
+ ### 12. **Missing Input Validation**
135
+ **Location:** FigInputNumber, line 1551
136
+ **Issue:** No validation that `numericValue` is a valid number before division:
137
+ ```javascript
138
+ this.value = Number(numericValue) / (this.transform || 1);
139
+ ```
140
+ If `numericValue` is empty or invalid, this could result in NaN
141
+
142
+ ## Code Quality Issues
143
+
144
+ ### 13. **Magic Numbers**
145
+ **Location:** Multiple locations
146
+ **Issue:** Hard-coded values without explanation:
147
+ - Line 246: `delay = 500` (default delay)
148
+ - Line 1617: `precision = 2` (default precision)
149
+ - Line 1635: `Math.round(num * 100) / 100` (rounding logic)
150
+
151
+ **Recommendation:** Extract to named constants or document them
152
+
153
+ ### 14. **Inconsistent Naming**
154
+ **Location:** FigSlider
155
+ **Issue:** Method named `#handleTextInput` but now works with `figInputNumber`
156
+
157
+ **Recommendation:** Rename to `#handleNumberInput` for clarity
158
+
159
+ ### 15. **Missing Type Checks**
160
+ **Location:** Multiple locations
161
+ **Issue:** No type validation for attributes that should be numbers:
162
+ - `min`, `max`, `step`, `transform` attributes
163
+
164
+ **Recommendation:** Add validation or use `Number()` with NaN checks
165
+
166
+ ## Summary
167
+
168
+ **Critical Bugs:** 2
169
+ **Documentation Issues:** 4
170
+ **Potential Issues:** 6
171
+ **Code Quality Issues:** 3
172
+
173
+ **Total Issues Found:** 15
174
+
175
+ ## Priority Fixes
176
+
177
+ 1. **HIGH:** Fix infinite recursion in FigButton.type getter
178
+ 2. **HIGH:** Fix impossible logic condition in disabled attribute handling
179
+ 3. **MEDIUM:** Add missing documentation
180
+ 4. **MEDIUM:** Fix memory leak in FigTooltip.destroy()
181
+ 5. **LOW:** Standardize disabled attribute handling
182
+ 6. **LOW:** Add input validation
183
+
package/components.css CHANGED
@@ -513,7 +513,8 @@ input[type="password"],
513
513
  }
514
514
  }
515
515
 
516
- fig-input-text {
516
+ fig-input-text,
517
+ fig-input-number {
517
518
  &:has([slot="append"]) input[type="number"] {
518
519
  &::-webkit-outer-spin-button,
519
520
  &::-webkit-inner-spin-button {
@@ -572,109 +573,170 @@ input[type="text"][list] {
572
573
  }
573
574
  }
574
575
 
575
- /* Not enough support for this yet
576
576
  @supports (appearance: base-select) {
577
- select {
578
- appearance: base-select;
577
+ fig-dropdown[variant="neue"] {
578
+ select {
579
+ appearance: base-select;
580
+ --option-height: 1.5rem;
579
581
 
580
- option::checkmark {
581
- content: var(--icon-checkmark);
582
- display: block;
583
- width: 1rem;
584
- height: 1rem;
585
- }
586
-
587
- option {
588
- display: flex;
589
- gap: var(--spacer-1);
590
- padding: 0 var(--spacer-4) 0 calc(var(--spacer-1) * 2 + var(--spacer-1));
591
- font-weight: var(--body-medium-fontWeight);
592
- color: var(--figma-color-text-menu);
593
- position: relative;
594
- &[hidden] {
595
- display: none;
596
- }
597
- &:not(:checked):before {
598
- content: "";
582
+ option::checkmark {
583
+ content: var(--icon-checkmark);
599
584
  display: block;
600
- position: absolute;
601
- inset: 0 var(--spacer-2);
602
- border-radius: var(--radius-medium);
603
- z-index: -1;
604
- background-color: transparent;
585
+ width: 1rem;
586
+ height: 1rem;
605
587
  }
606
- &:not(:disabled) {
607
- &:hover,
608
- &:active,
609
- &:focus {
588
+
589
+ option {
590
+ display: flex;
591
+ gap: var(--spacer-1);
592
+ padding: 0 var(--spacer-4) 0 calc(var(--spacer-1) * 2 + var(--spacer-1));
593
+ font-weight: var(--body-medium-fontWeight);
594
+ color: var(--figma-color-text-menu);
595
+ position: relative;
596
+ &[hidden] {
597
+ display: none;
598
+ }
599
+ &:not(:checked):before {
600
+ content: "";
601
+ display: block;
602
+ position: absolute;
603
+ inset: 0 var(--spacer-2);
604
+ border-radius: var(--radius-medium);
605
+ z-index: -1;
610
606
  background-color: transparent;
611
- outline: 0;
607
+ }
608
+ &:not(:disabled) {
609
+ &:hover,
610
+ &:active,
611
+ &:focus {
612
+ background-color: transparent;
613
+ outline: 0;
614
+ &:before {
615
+ background-color: var(--figma-color-bg-menu-hover);
616
+ }
617
+ }
618
+ }
619
+ }
620
+
621
+ optgroup {
622
+ color: var(--figma-color-text-menu-secondary);
623
+ text-align: left;
624
+ position: relative;
625
+ padding: 0 var(--spacer-1) 0 calc(var(--spacer-1) * 2 + var(--spacer-1));
626
+ font-weight: var(--body-medium-fontWeight);
627
+ &::-internal-optgroup-label {
628
+ display: none;
629
+ }
630
+ legend {
631
+ padding: var(--spacer-1, 0.3rem) var(--spacer-1, 1rem);
632
+ }
633
+ option {
634
+ margin: 0 calc(var(--spacer-1) * -1);
635
+ margin-left: calc((var(--spacer-1) * 2 + var(--spacer-1)) * -1);
636
+ }
637
+ &:not(:first-child) {
638
+ margin-top: var(--spacer-2);
639
+ padding-top: var(--spacer-2);
640
+
612
641
  &:before {
613
- background-color: var(--figma-color-bg-menu-hover);
642
+ content: "";
643
+ display: block;
644
+ position: absolute;
645
+ left: 0;
646
+ right: 0;
647
+ top: 1px;
648
+ height: 1px;
649
+ background-color: var(--figma-color-border-menu);
650
+ margin-bottom: var(--spacer-2);
614
651
  }
615
652
  }
616
653
  }
654
+ option[hidden="true"]:first-child + optgroup {
655
+ margin-top: 0;
656
+ padding-top: 0;
657
+ &:before {
658
+ display: none;
659
+ }
660
+ }
661
+ }
662
+ ::picker-icon {
663
+ display: none;
617
664
  }
618
665
 
619
- optgroup {
620
- color: var(--figma-color-text-menu-secondary);
621
- text-align: left;
622
- position: relative;
623
- padding: 0 var(--spacer-1) 0 calc(var(--spacer-1) * 2 + var(--spacer-1));
624
- font-weight: var(--body-medium-fontWeight);
625
- &::-internal-optgroup-label {
626
- display: none;
666
+ ::picker(select) {
667
+ position-area: auto;
668
+ align-self: auto;
669
+ position-try-fallbacks: none;
670
+ max-block-size: 100vh;
671
+ appearance: base-select;
672
+ scrollbar-width: thin;
673
+ outline: 0;
674
+ scrollbar-color: var(--figma-color-text-menu-tertiary)
675
+ var(--figma-color-bg-menu);
676
+ border-radius: var(--radius-large);
677
+ border: 0;
678
+ background-color: var(--figma-color-bg-menu);
679
+ padding: var(--spacer-2) 0;
680
+ box-shadow: var(--figma-elevation-400-menu-panel);
681
+ }
682
+ select {
683
+ &:has(:nth-child(1):checked) {
684
+ &::picker(select) {
685
+ top: calc(var(--option-height) * -1 - var(--spacer-2));
686
+ }
627
687
  }
628
- legend {
629
- padding: var(--spacer-1, 0.3rem) var(--spacer-1, 1rem);
688
+
689
+ &:has(:nth-child(2):checked) {
690
+ &::picker(select) {
691
+ top: calc(var(--option-height) * -2 - var(--spacer-2));
692
+ }
630
693
  }
631
- option {
632
- margin: 0 calc(var(--spacer-1) * -1);
633
- margin-left: calc((var(--spacer-1) * 2 + var(--spacer-1)) * -1);
694
+
695
+ &:has(:nth-child(3):checked) {
696
+ &::picker(select) {
697
+ top: calc(var(--option-height) * -3 - var(--spacer-2));
698
+ }
634
699
  }
635
- &:not(:first-child) {
636
- margin-top: var(--spacer-2);
637
- padding-top: var(--spacer-2);
638
700
 
639
- &:before {
640
- content: "";
641
- display: block;
642
- position: absolute;
643
- left: 0;
644
- right: 0;
645
- top: 1px;
646
- height: 1px;
647
- background-color: var(--figma-color-border-menu);
648
- margin-bottom: var(--spacer-2);
701
+ &:has(:nth-child(4):checked) {
702
+ &::picker(select) {
703
+ top: calc(var(--option-height) * -4 - var(--spacer-2));
649
704
  }
650
705
  }
651
- }
652
- option[hidden="true"]:first-child + optgroup {
653
- margin-top: 0;
654
- padding-top: 0;
655
- &:before {
656
- display: none;
706
+ &:has(:nth-child(5):checked) {
707
+ &::picker(select) {
708
+ top: calc(var(--option-height) * -5 - var(--spacer-2));
709
+ }
710
+ }
711
+ &:has(:nth-child(6):checked) {
712
+ &::picker(select) {
713
+ top: calc(var(--option-height) * -6 - var(--spacer-2));
714
+ }
715
+ }
716
+ &:has(:nth-child(7):checked) {
717
+ &::picker(select) {
718
+ top: calc(var(--option-height) * -7 - var(--spacer-2));
719
+ }
720
+ }
721
+ &:has(:nth-child(8):checked) {
722
+ &::picker(select) {
723
+ top: calc(var(--option-height) * -8 - var(--spacer-2));
724
+ }
725
+ }
726
+ &:has(:nth-child(9):checked) {
727
+ &::picker(select) {
728
+ top: calc(var(--option-height) * -9 - var(--spacer-2));
729
+ }
730
+ }
731
+ &:has(:nth-child(10):checked) {
732
+ &::picker(select) {
733
+ top: calc(var(--option-height) * -10 - var(--spacer-2));
734
+ }
657
735
  }
658
736
  }
659
737
  }
660
- ::picker-icon {
661
- display: none;
662
- }
663
-
664
- ::picker(select) {
665
- appearance: base-select;
666
- scrollbar-width: thin;
667
- outline: 0;
668
- scrollbar-color: var(--figma-color-text-menu-tertiary)
669
- var(--figma-color-bg-menu);
670
- border-radius: var(--radius-large);
671
- border: 0;
672
- background-color: var(--figma-color-bg-menu);
673
- padding: var(--spacer-2) 0;
674
- box-shadow: var(--figma-elevation-400-menu-panel);
675
- }
676
738
  }
677
- */
739
+
678
740
  input[type="text"][list]:hover,
679
741
  input[type="text"][list]:active,
680
742
  input[type="text"][list]:focus,
@@ -1913,7 +1975,8 @@ fig-slider {
1913
1975
  box-shadow: none;
1914
1976
  background-color: var(--figma-color-bg-tertiary);
1915
1977
  }
1916
- fig-input-text {
1978
+ fig-input-text,
1979
+ fig-input-number {
1917
1980
  border-top-left-radius: 0;
1918
1981
  border-bottom-left-radius: 0;
1919
1982
  border-left: 1px solid var(--figma-color-bg);
@@ -1921,7 +1984,8 @@ fig-slider {
1921
1984
 
1922
1985
  &:hover,
1923
1986
  &:focus-within {
1924
- fig-input-text {
1987
+ fig-input-text,
1988
+ fig-input-number {
1925
1989
  height: auto;
1926
1990
  }
1927
1991
  }
@@ -2235,7 +2299,8 @@ hstack,
2235
2299
  flex-wrap: nowrap;
2236
2300
  }
2237
2301
 
2238
- fig-input-text {
2302
+ fig-input-text,
2303
+ fig-input-number {
2239
2304
  background-color: var(--figma-color-bg-secondary);
2240
2305
  border: 0;
2241
2306
  border-radius: var(--radius-medium);
@@ -2322,8 +2387,8 @@ fig-input-text {
2322
2387
 
2323
2388
  fig-input-color {
2324
2389
  & fig-input-text:not([type]),
2325
- & fig-input-text[type="text"] {
2326
- min-width: 6em;
2390
+ & fig-input-text[type="text"],
2391
+ & fig-input-number {
2327
2392
  display: inline-flex;
2328
2393
  flex-direction: column;
2329
2394
  align-items: stretch;
@@ -2334,10 +2399,17 @@ fig-input-color {
2334
2399
  }
2335
2400
  }
2336
2401
 
2337
- & fig-input-text[type="number"] {
2338
- width: 3.5rem;
2402
+ & fig-input-text[type="number"],
2403
+ & fig-input-number {
2404
+ width: 3rem;
2339
2405
  display: inline-flex;
2340
2406
  }
2407
+ & > * {
2408
+ flex: 1;
2409
+ fig-input-text {
2410
+ flex: 1;
2411
+ }
2412
+ }
2341
2413
  }
2342
2414
 
2343
2415
  fig-slider {
@@ -2347,8 +2419,9 @@ fig-slider {
2347
2419
  flex-grow: 1;
2348
2420
  }
2349
2421
 
2350
- & fig-input-text[type="number"] {
2351
- width: 3.5rem;
2422
+ & fig-input-text[type="number"],
2423
+ & fig-input-number {
2424
+ width: 3rem;
2352
2425
  }
2353
2426
  }
2354
2427
 
package/example.html CHANGED
@@ -27,15 +27,43 @@
27
27
  <fig-spinner></fig-spinner>
28
28
  </fig-header>
29
29
 
30
+ <br /><br /><br /><br /><br /><br /><br /><br />
31
+
32
+
33
+
30
34
 
31
35
  <fig-field direction="horizontal">
32
36
  <label>Position</label>
33
- <fig-dropdown value="outside">
37
+ <fig-dropdown value="outside"
38
+ variant="neue">
34
39
  <option value="inside">Inside</option>
35
40
  <option value="center">Center</option>
36
41
  <option value="outside">Outside</option>
37
42
  </fig-dropdown>
38
43
  </fig-field>
44
+ <fig-field direction="horizontal"
45
+ style="width: 320px;">
46
+ <label>Color</label>
47
+ <fig-input-color alpha="true"
48
+ full
49
+ text="true"
50
+ value="#000000"></fig-input-color>
51
+ </fig-field>
52
+ <fig-field direction="horizontal"
53
+ style="width: 320px;">
54
+ <label>Opacity</label>
55
+ <fig-slider alpha="true"
56
+ text="true"
57
+ value="50"
58
+ full
59
+ variant="neue"
60
+ min="0"
61
+ max="100"
62
+ step="1"
63
+ units="%"></fig-slider>
64
+ </fig-field>
65
+
66
+ <br /><br /><br /><br /><br /><br /><br /><br />
39
67
 
40
68
 
41
69
 
@@ -675,6 +703,111 @@
675
703
  <span slot="prepend">X</span>
676
704
  </fig-input-text>
677
705
  </fig-field>
706
+ <fig-header>
707
+ <h2>Number input</h2>
708
+ </fig-header>
709
+ <fig-field>
710
+ <label>Basic number input</label>
711
+ <fig-input-number value="100"></fig-input-number>
712
+ </fig-field>
713
+ <fig-field>
714
+ <label>With units (percentage)</label>
715
+ <fig-input-number value="100"
716
+ units="%"></fig-input-number>
717
+ </fig-field>
718
+ <fig-field>
719
+ <label>With units (degrees)</label>
720
+ <fig-input-number value="45"
721
+ units="°"
722
+ min="0"
723
+ max="360"
724
+ step="1"></fig-input-number>
725
+ </fig-field>
726
+ <fig-field>
727
+ <label>With prefix unit (currency)</label>
728
+ <fig-input-number value="50"
729
+ units="$"
730
+ unit-position="prefix"
731
+ min="0"
732
+ step="0.01"></fig-input-number>
733
+ </fig-field>
734
+ <fig-field>
735
+ <label>With transform (like slider)</label>
736
+ <fig-input-number value="0.5"
737
+ units="%"
738
+ transform="100"
739
+ min="0"
740
+ max="1"
741
+ step="0.01"></fig-input-number>
742
+ </fig-field>
743
+ <fig-field>
744
+ <label>With min/max constraints</label>
745
+ <fig-input-number value="25"
746
+ units="px"
747
+ min="0"
748
+ max="1000"
749
+ step="1"></fig-input-number>
750
+ </fig-field>
751
+ <fig-field>
752
+ <label>With placeholder</label>
753
+ <fig-input-number placeholder="Enter value"
754
+ units="%"
755
+ min="0"
756
+ max="100"></fig-input-number>
757
+ </fig-field>
758
+ <fig-field>
759
+ <label>Disabled</label>
760
+ <fig-input-number value="75"
761
+ units="%"
762
+ disabled></fig-input-number>
763
+ </fig-field>
764
+ <fig-field>
765
+ <label>With name attribute</label>
766
+ <fig-input-number value="42"
767
+ units="px"
768
+ name="width"></fig-input-number>
769
+ </fig-field>
770
+ <fig-field>
771
+ <label>Small step (decimals)</label>
772
+ <fig-input-number value="1.5"
773
+ units="rem"
774
+ min="0"
775
+ max="10"
776
+ step="0.1"></fig-input-number>
777
+ </fig-field>
778
+ <fig-field>
779
+ <label>Large range with transform</label>
780
+ <fig-input-number value="0.75"
781
+ units="%"
782
+ transform="100"
783
+ min="0"
784
+ max="1"
785
+ step="0.01"></fig-input-number>
786
+ </fig-field>
787
+ <fig-field>
788
+ <label>Negative values allowed</label>
789
+ <fig-input-number value="-10"
790
+ units="px"
791
+ min="-100"
792
+ max="100"
793
+ step="1"></fig-input-number>
794
+ </fig-field>
795
+ <fig-field>
796
+ <label>With append slot</label>
797
+ <fig-input-number value="90"
798
+ units="°"
799
+ min="0"
800
+ max="360">
801
+ <span slot="append">deg</span>
802
+ </fig-input-number>
803
+ </fig-field>
804
+ <fig-field>
805
+ <label>With prepend slot</label>
806
+ <fig-input-number value="50"
807
+ units="%">
808
+ <span slot="prepend">O</span>
809
+ </fig-input-number>
810
+ </fig-field>
678
811
  <fig-header>
679
812
  <h2>Color input</h2>
680
813
  </fig-header>
package/fig.js CHANGED
@@ -1,15 +1,25 @@
1
+ /**
2
+ * Generates a unique ID string using timestamp and random values
3
+ * @returns {string} A unique identifier
4
+ */
1
5
  function figUniqueId() {
2
6
  return Date.now().toString(36) + Math.random().toString(36).substring(2);
3
7
  }
8
+ /**
9
+ * Checks if the browser supports the native popover API
10
+ * @returns {boolean} True if popover is supported
11
+ */
4
12
  function figSupportsPopover() {
5
13
  return HTMLElement.prototype.hasOwnProperty("popover");
6
14
  }
7
15
 
8
16
  /**
9
17
  * A custom button element that supports different types and states.
10
- * @attr {string} type - The button type: "button" (default), "toggle", or "submit"
18
+ * @attr {string} type - The button type: "button" (default), "toggle", "submit", or "link"
11
19
  * @attr {boolean} selected - Whether the button is in a selected state
12
20
  * @attr {boolean} disabled - Whether the button is disabled
21
+ * @attr {string} href - URL for link type buttons
22
+ * @attr {string} target - Target window for link type buttons (e.g., "_blank")
13
23
  */
14
24
  class FigButton extends HTMLElement {
15
25
  type;
@@ -56,7 +66,7 @@ class FigButton extends HTMLElement {
56
66
  }
57
67
 
58
68
  get type() {
59
- return this.type;
69
+ return this.getAttribute("type") || "button";
60
70
  }
61
71
  set type(value) {
62
72
  this.setAttribute("type", value);
@@ -99,8 +109,7 @@ class FigButton extends HTMLElement {
99
109
  switch (name) {
100
110
  case "disabled":
101
111
  this.disabled = this.button.disabled =
102
- newValue === "true" ||
103
- (newValue === undefined && newValue !== null);
112
+ newValue !== null && newValue !== "false";
104
113
  break;
105
114
  case "type":
106
115
  this.type = newValue;
@@ -237,6 +246,7 @@ customElements.define("fig-dropdown", FigDropdown);
237
246
  class FigTooltip extends HTMLElement {
238
247
  #boundHideOnChromeOpen;
239
248
  #boundHideOnDragStart;
249
+ #boundHidePopupOutsideClick;
240
250
  #touchTimeout;
241
251
  #isTouching = false;
242
252
  constructor() {
@@ -248,6 +258,7 @@ class FigTooltip extends HTMLElement {
248
258
  // Bind methods that will be used as event listeners
249
259
  this.#boundHideOnChromeOpen = this.#hideOnChromeOpen.bind(this);
250
260
  this.#boundHideOnDragStart = this.hidePopup.bind(this);
261
+ this.#boundHidePopupOutsideClick = this.hidePopupOutsideClick.bind(this);
251
262
  }
252
263
  connectedCallback() {
253
264
  this.setup();
@@ -265,6 +276,14 @@ class FigTooltip extends HTMLElement {
265
276
  // Remove mousedown listener
266
277
  this.removeEventListener("mousedown", this.#boundHideOnDragStart);
267
278
 
279
+ // Remove click outside listener for click action
280
+ if (this.action === "click") {
281
+ document.body.removeEventListener(
282
+ "click",
283
+ this.#boundHidePopupOutsideClick
284
+ );
285
+ }
286
+
268
287
  // Clean up touch-related timers and listeners
269
288
  clearTimeout(this.#touchTimeout);
270
289
  if (this.action === "hover") {
@@ -307,7 +326,13 @@ class FigTooltip extends HTMLElement {
307
326
  if (this.popup) {
308
327
  this.popup.remove();
309
328
  }
310
- document.body.addEventListener("click", this.hidePopupOutsideClick);
329
+ // Remove the click outside listener if it was added
330
+ if (this.action === "click") {
331
+ document.body.removeEventListener(
332
+ "click",
333
+ this.#boundHidePopupOutsideClick
334
+ );
335
+ }
311
336
  }
312
337
  isTouchDevice() {
313
338
  return (
@@ -344,10 +369,7 @@ class FigTooltip extends HTMLElement {
344
369
  });
345
370
  } else if (this.action === "click") {
346
371
  this.addEventListener("click", this.showDelayedPopup.bind(this));
347
- document.body.addEventListener(
348
- "click",
349
- this.hidePopupOutsideClick.bind(this)
350
- );
372
+ document.body.addEventListener("click", this.#boundHidePopupOutsideClick);
351
373
 
352
374
  // Touch support for better mobile responsiveness
353
375
  this.addEventListener("touchstart", this.showDelayedPopup.bind(this), {
@@ -665,10 +687,12 @@ class FigTab extends HTMLElement {
665
687
  requestAnimationFrame(() => {
666
688
  if (typeof this.getAttribute("content") === "string") {
667
689
  this.content = document.querySelector(this.getAttribute("content"));
668
- if (this.#selected) {
669
- this.content.style.display = "block";
670
- } else {
671
- this.content.style.display = "none";
690
+ if (this.content) {
691
+ if (this.#selected) {
692
+ this.content.style.display = "block";
693
+ } else {
694
+ this.content.style.display = "none";
695
+ }
672
696
  }
673
697
  }
674
698
  });
@@ -889,20 +913,15 @@ class FigSlider extends HTMLElement {
889
913
  </div>`;
890
914
  if (this.text) {
891
915
  html = `${slider}
892
- <fig-input-text
916
+ <fig-input-number
893
917
  placeholder="##"
894
- type="number"
895
918
  min="${this.min}"
896
919
  max="${this.max}"
897
920
  transform="${this.transform}"
898
921
  step="${this.step}"
899
- value="${this.value}">
900
- ${
901
- this.units
902
- ? `<span slot="append">${this.units}</span>`
903
- : ""
904
- }
905
- </fig-input-text>`;
922
+ value="${this.value}"
923
+ ${this.units ? `units="${this.units}"` : ""}>
924
+ </fig-input-number>`;
906
925
  } else {
907
926
  html = slider;
908
927
  }
@@ -923,7 +942,7 @@ class FigSlider extends HTMLElement {
923
942
  }
924
943
 
925
944
  this.datalist = this.querySelector("datalist");
926
- this.figInputText = this.querySelector("fig-input-text");
945
+ this.figInputNumber = this.querySelector("fig-input-number");
927
946
  if (this.datalist) {
928
947
  this.inputContainer.append(this.datalist);
929
948
  this.datalist.setAttribute(
@@ -959,12 +978,15 @@ class FigSlider extends HTMLElement {
959
978
  defaultOption.setAttribute("default", "true");
960
979
  }
961
980
  }
962
- if (this.figInputText) {
963
- this.figInputText.removeEventListener(
981
+ if (this.figInputNumber) {
982
+ this.figInputNumber.removeEventListener(
983
+ "input",
984
+ this.#boundHandleTextInput
985
+ );
986
+ this.figInputNumber.addEventListener(
964
987
  "input",
965
988
  this.#boundHandleTextInput
966
989
  );
967
- this.figInputText.addEventListener("input", this.#boundHandleTextInput);
968
990
  }
969
991
 
970
992
  this.#syncValue();
@@ -976,8 +998,8 @@ class FigSlider extends HTMLElement {
976
998
  }
977
999
 
978
1000
  #handleTextInput() {
979
- if (this.figInputText) {
980
- this.value = this.input.value = this.figInputText.value;
1001
+ if (this.figInputNumber) {
1002
+ this.value = this.input.value = this.figInputNumber.value;
981
1003
  this.#syncProperties();
982
1004
  this.dispatchEvent(
983
1005
  new CustomEvent("input", { detail: this.value, bubbles: true })
@@ -1000,8 +1022,8 @@ class FigSlider extends HTMLElement {
1000
1022
  let val = this.input.value;
1001
1023
  this.value = val;
1002
1024
  this.#syncProperties();
1003
- if (this.figInputText) {
1004
- this.figInputText.setAttribute("value", val);
1025
+ if (this.figInputNumber) {
1026
+ this.figInputNumber.setAttribute("value", val);
1005
1027
  }
1006
1028
  }
1007
1029
 
@@ -1039,23 +1061,22 @@ class FigSlider extends HTMLElement {
1039
1061
  break;
1040
1062
  case "disabled":
1041
1063
  this.disabled = this.input.disabled =
1042
- newValue === "true" ||
1043
- (newValue === undefined && newValue !== null);
1044
- if (this.figInputText) {
1045
- this.figInputText.disabled = this.disabled;
1046
- this.figInputText.setAttribute("disabled", this.disabled);
1064
+ newValue !== null && newValue !== "false";
1065
+ if (this.figInputNumber) {
1066
+ this.figInputNumber.disabled = this.disabled;
1067
+ this.figInputNumber.setAttribute("disabled", this.disabled);
1047
1068
  }
1048
1069
  break;
1049
1070
  case "value":
1050
1071
  this.value = newValue;
1051
- if (this.figInputText) {
1052
- this.figInputText.setAttribute("value", newValue);
1072
+ if (this.figInputNumber) {
1073
+ this.figInputNumber.setAttribute("value", newValue);
1053
1074
  }
1054
1075
  break;
1055
1076
  case "transform":
1056
1077
  this.transform = Number(newValue) || 1;
1057
- if (this.figInputText) {
1058
- this.figInputText.setAttribute("transform", this.transform);
1078
+ if (this.figInputNumber) {
1079
+ this.figInputNumber.setAttribute("transform", this.transform);
1059
1080
  }
1060
1081
  break;
1061
1082
  case "min":
@@ -1300,8 +1321,7 @@ class FigInputText extends HTMLElement {
1300
1321
  switch (name) {
1301
1322
  case "disabled":
1302
1323
  this.disabled = this.input.disabled =
1303
- newValue === "true" ||
1304
- (newValue === undefined && newValue !== null);
1324
+ newValue !== null && newValue !== "false";
1305
1325
  break;
1306
1326
  case "transform":
1307
1327
  if (this.type === "number") {
@@ -1341,6 +1361,362 @@ class FigInputText extends HTMLElement {
1341
1361
  }
1342
1362
  window.customElements.define("fig-input-text", FigInputText);
1343
1363
 
1364
+ /**
1365
+ * A custom numeric input element that uses type="text" with inputmode="decimal".
1366
+ * Supports units display and all standard number input attributes.
1367
+ * @attr {string} value - The current numeric value
1368
+ * @attr {string} placeholder - Placeholder text
1369
+ * @attr {boolean} disabled - Whether the input is disabled
1370
+ * @attr {number} min - Minimum value
1371
+ * @attr {number} max - Maximum value
1372
+ * @attr {number} step - Step increment
1373
+ * @attr {number} transform - A multiplier for displayed number values
1374
+ * @attr {string} units - Unit string to append/prepend to displayed value (e.g., "%", "°", "$")
1375
+ * @attr {string} unit-position - Position of unit: "suffix" (default) or "prefix"
1376
+ * @attr {string} name - Form field name
1377
+ */
1378
+ class FigInputNumber extends HTMLElement {
1379
+ #boundMouseMove;
1380
+ #boundMouseUp;
1381
+ #boundMouseDown;
1382
+ #boundInputChange;
1383
+ #boundInput;
1384
+ #boundFocus;
1385
+ #boundBlur;
1386
+ #units;
1387
+ #unitPosition;
1388
+
1389
+ constructor() {
1390
+ super();
1391
+ // Pre-bind the event handlers once
1392
+ this.#boundMouseMove = this.#handleMouseMove.bind(this);
1393
+ this.#boundMouseUp = this.#handleMouseUp.bind(this);
1394
+ this.#boundMouseDown = this.#handleMouseDown.bind(this);
1395
+ this.#boundInputChange = (e) => {
1396
+ e.stopPropagation();
1397
+ this.#handleInputChange(e);
1398
+ };
1399
+ this.#boundInput = (e) => {
1400
+ e.stopPropagation();
1401
+ this.#handleInput(e);
1402
+ };
1403
+ this.#boundFocus = (e) => {
1404
+ this.#handleFocus(e);
1405
+ };
1406
+ this.#boundBlur = (e) => {
1407
+ this.#handleBlur(e);
1408
+ };
1409
+ }
1410
+
1411
+ connectedCallback() {
1412
+ const valueAttr = this.getAttribute("value");
1413
+ this.value =
1414
+ valueAttr !== null && valueAttr !== "" ? Number(valueAttr) : "";
1415
+ this.placeholder = this.getAttribute("placeholder") || "";
1416
+ this.name = this.getAttribute("name") || null;
1417
+ this.#units = this.getAttribute("units") || "";
1418
+ this.#unitPosition = this.getAttribute("unit-position") || "suffix";
1419
+
1420
+ if (this.getAttribute("step")) {
1421
+ this.step = Number(this.getAttribute("step"));
1422
+ }
1423
+ if (this.getAttribute("min")) {
1424
+ this.min = Number(this.getAttribute("min"));
1425
+ }
1426
+ if (this.getAttribute("max")) {
1427
+ this.max = Number(this.getAttribute("max"));
1428
+ }
1429
+ this.transform = Number(this.getAttribute("transform") || 1);
1430
+
1431
+ let html = `<input
1432
+ type="text"
1433
+ inputmode="decimal"
1434
+ ${this.name ? `name="${this.name}"` : ""}
1435
+ placeholder="${this.placeholder}"
1436
+ value="${this.#formatWithUnit(this.value)}" />`;
1437
+
1438
+ //child nodes hack
1439
+ requestAnimationFrame(() => {
1440
+ let append = this.querySelector("[slot=append]");
1441
+ let prepend = this.querySelector("[slot=prepend]");
1442
+
1443
+ this.innerHTML = html;
1444
+
1445
+ if (prepend) {
1446
+ prepend.addEventListener("click", this.focus.bind(this));
1447
+ this.prepend(prepend);
1448
+ }
1449
+ if (append) {
1450
+ append.addEventListener("click", this.focus.bind(this));
1451
+ this.append(append);
1452
+ }
1453
+
1454
+ this.input = this.querySelector("input");
1455
+
1456
+ if (this.getAttribute("min")) {
1457
+ this.min = Number(this.getAttribute("min"));
1458
+ }
1459
+ if (this.getAttribute("max")) {
1460
+ this.max = Number(this.getAttribute("max"));
1461
+ }
1462
+ if (this.getAttribute("step")) {
1463
+ this.step = Number(this.getAttribute("step"));
1464
+ }
1465
+
1466
+ // Set disabled state if present
1467
+ if (this.hasAttribute("disabled")) {
1468
+ const disabledAttr = this.getAttribute("disabled");
1469
+ this.disabled = this.input.disabled = disabledAttr !== "false";
1470
+ }
1471
+
1472
+ this.addEventListener("pointerdown", this.#boundMouseDown);
1473
+ this.input.removeEventListener("change", this.#boundInputChange);
1474
+ this.input.addEventListener("change", this.#boundInputChange);
1475
+ this.input.removeEventListener("input", this.#boundInput);
1476
+ this.input.addEventListener("input", this.#boundInput);
1477
+ this.input.removeEventListener("focus", this.#boundFocus);
1478
+ this.input.addEventListener("focus", this.#boundFocus);
1479
+ this.input.removeEventListener("blur", this.#boundBlur);
1480
+ this.input.addEventListener("blur", this.#boundBlur);
1481
+ });
1482
+ }
1483
+
1484
+ focus() {
1485
+ this.input.focus();
1486
+ }
1487
+
1488
+ #getNumericValue(str) {
1489
+ if (!str) return "";
1490
+ if (!this.#units) {
1491
+ // No units, just extract numeric value
1492
+ let value = str.replace(/[^\d.-]/g, "");
1493
+ // Prevent multiple decimal points
1494
+ const parts = value.split(".");
1495
+ if (parts.length > 2) {
1496
+ value = parts[0] + "." + parts.slice(1).join("");
1497
+ }
1498
+ return value;
1499
+ }
1500
+ let value = str.replace(this.#units, "").trim();
1501
+ value = value.replace(/[^\d.-]/g, "");
1502
+ // Prevent multiple decimal points
1503
+ const parts = value.split(".");
1504
+ if (parts.length > 2) {
1505
+ value = parts[0] + "." + parts.slice(1).join("");
1506
+ }
1507
+ return value;
1508
+ }
1509
+
1510
+ #formatWithUnit(numericValue) {
1511
+ if (
1512
+ numericValue === "" ||
1513
+ numericValue === null ||
1514
+ numericValue === undefined
1515
+ )
1516
+ return "";
1517
+ // numericValue is the internal (non-transformed) value
1518
+ // For display, we apply transform and format
1519
+ let displayValue = Number(numericValue) * (this.transform || 1);
1520
+ if (isNaN(displayValue)) return "";
1521
+ displayValue = this.#formatNumber(displayValue);
1522
+ if (!this.#units) return displayValue.toString();
1523
+ if (this.#unitPosition === "prefix") {
1524
+ return this.#units + displayValue;
1525
+ } else {
1526
+ return displayValue + this.#units;
1527
+ }
1528
+ }
1529
+
1530
+ #transformNumber(value) {
1531
+ if (value === "" || value === null || value === undefined) return "";
1532
+ let transformed = Number(value) * (this.transform || 1);
1533
+ transformed = this.#formatNumber(transformed);
1534
+ return transformed.toString();
1535
+ }
1536
+
1537
+ #handleFocus(e) {
1538
+ setTimeout(() => {
1539
+ const value = e.target.value;
1540
+ if (value && this.#units) {
1541
+ if (this.#unitPosition === "prefix") {
1542
+ e.target.setSelectionRange(this.#units.length, value.length);
1543
+ } else {
1544
+ const unitPos = value.indexOf(this.#units);
1545
+ if (unitPos > -1) {
1546
+ e.target.setSelectionRange(0, unitPos);
1547
+ }
1548
+ }
1549
+ }
1550
+ }, 0);
1551
+ }
1552
+
1553
+ #handleBlur(e) {
1554
+ let numericValue = this.#getNumericValue(e.target.value);
1555
+ if (numericValue !== "") {
1556
+ let val = Number(numericValue) / (this.transform || 1);
1557
+ val = this.#sanitizeInput(val, false);
1558
+ this.value = val;
1559
+ e.target.value = this.#formatWithUnit(this.value);
1560
+ } else {
1561
+ this.value = "";
1562
+ e.target.value = "";
1563
+ }
1564
+ this.dispatchEvent(
1565
+ new CustomEvent("change", { detail: this.value, bubbles: true })
1566
+ );
1567
+ }
1568
+
1569
+ #handleInput(e) {
1570
+ let numericValue = this.#getNumericValue(e.target.value);
1571
+ if (numericValue !== "") {
1572
+ this.value = Number(numericValue) / (this.transform || 1);
1573
+ } else {
1574
+ this.value = "";
1575
+ }
1576
+ this.dispatchEvent(
1577
+ new CustomEvent("input", { detail: this.value, bubbles: true })
1578
+ );
1579
+ }
1580
+
1581
+ #handleInputChange(e) {
1582
+ e.stopPropagation();
1583
+ let numericValue = this.#getNumericValue(e.target.value);
1584
+ if (numericValue !== "") {
1585
+ let val = Number(numericValue) / (this.transform || 1);
1586
+ val = this.#sanitizeInput(val, false);
1587
+ this.value = val;
1588
+ e.target.value = this.#formatWithUnit(this.value);
1589
+ } else {
1590
+ this.value = "";
1591
+ e.target.value = "";
1592
+ }
1593
+ this.dispatchEvent(
1594
+ new CustomEvent("input", { detail: this.value, bubbles: true })
1595
+ );
1596
+ this.dispatchEvent(
1597
+ new CustomEvent("change", { detail: this.value, bubbles: true })
1598
+ );
1599
+ }
1600
+
1601
+ #handleMouseMove(e) {
1602
+ if (this.disabled) return;
1603
+ if (e.altKey) {
1604
+ let step = (this.step || 1) * e.movementX;
1605
+ let numericValue = this.#getNumericValue(this.input.value);
1606
+ let value = Number(numericValue) / (this.transform || 1) + step;
1607
+ value = this.#sanitizeInput(value, false);
1608
+ this.value = value;
1609
+ this.input.value = this.#formatWithUnit(this.value);
1610
+ this.dispatchEvent(new CustomEvent("input", { bubbles: true }));
1611
+ this.dispatchEvent(new CustomEvent("change", { bubbles: true }));
1612
+ }
1613
+ }
1614
+
1615
+ #handleMouseDown(e) {
1616
+ if (this.disabled) return;
1617
+ if (e.altKey) {
1618
+ this.input.style.cursor =
1619
+ this.style.cursor =
1620
+ document.body.style.cursor =
1621
+ "ew-resize";
1622
+ this.style.userSelect = "none";
1623
+ // Use the pre-bound handlers
1624
+ window.addEventListener("pointermove", this.#boundMouseMove);
1625
+ window.addEventListener("pointerup", this.#boundMouseUp);
1626
+ }
1627
+ }
1628
+
1629
+ #handleMouseUp(e) {
1630
+ this.input.style.cursor =
1631
+ this.style.cursor =
1632
+ document.body.style.cursor =
1633
+ "";
1634
+ this.style.userSelect = "all";
1635
+ // Remove the pre-bound handlers
1636
+ window.removeEventListener("pointermove", this.#boundMouseMove);
1637
+ window.removeEventListener("pointerup", this.#boundMouseUp);
1638
+ }
1639
+
1640
+ #sanitizeInput(value, transform = true) {
1641
+ let sanitized = Number(value);
1642
+ if (isNaN(sanitized)) return "";
1643
+ if (typeof this.min === "number") {
1644
+ sanitized = Math.max(this.min, sanitized);
1645
+ }
1646
+ if (typeof this.max === "number") {
1647
+ sanitized = Math.min(this.max, sanitized);
1648
+ }
1649
+ sanitized = this.#formatNumber(sanitized);
1650
+ return sanitized;
1651
+ }
1652
+
1653
+ #formatNumber(num, precision = 2) {
1654
+ // Check if the number has any decimal places after rounding
1655
+ const rounded = Math.round(num * 100) / 100;
1656
+ return Number.isInteger(rounded) ? rounded : rounded.toFixed(precision);
1657
+ }
1658
+
1659
+ static get observedAttributes() {
1660
+ return [
1661
+ "value",
1662
+ "placeholder",
1663
+ "disabled",
1664
+ "step",
1665
+ "min",
1666
+ "max",
1667
+ "transform",
1668
+ "name",
1669
+ "units",
1670
+ "unit-position",
1671
+ ];
1672
+ }
1673
+
1674
+ attributeChangedCallback(name, oldValue, newValue) {
1675
+ if (this.input) {
1676
+ switch (name) {
1677
+ case "disabled":
1678
+ this.disabled = this.input.disabled =
1679
+ newValue !== null && newValue !== "false";
1680
+ break;
1681
+ case "units":
1682
+ this.#units = newValue || "";
1683
+ this.input.value = this.#formatWithUnit(this.value);
1684
+ break;
1685
+ case "unit-position":
1686
+ this.#unitPosition = newValue || "suffix";
1687
+ this.input.value = this.#formatWithUnit(this.value);
1688
+ break;
1689
+ case "transform":
1690
+ this.transform = Number(newValue) || 1;
1691
+ this.input.value = this.#formatWithUnit(this.value);
1692
+ break;
1693
+ case "value":
1694
+ let value =
1695
+ newValue !== null && newValue !== "" ? Number(newValue) : "";
1696
+ if (value !== "") {
1697
+ value = this.#sanitizeInput(value, false);
1698
+ }
1699
+ this.value = value;
1700
+ this.input.value = this.#formatWithUnit(this.value);
1701
+ break;
1702
+ case "min":
1703
+ case "max":
1704
+ case "step":
1705
+ this[name] = Number(newValue);
1706
+ break;
1707
+ case "name":
1708
+ this[name] = this.input[name] = newValue;
1709
+ this.input.setAttribute("name", newValue);
1710
+ break;
1711
+ default:
1712
+ this[name] = this.input[name] = newValue;
1713
+ break;
1714
+ }
1715
+ }
1716
+ }
1717
+ }
1718
+ window.customElements.define("fig-input-number", FigInputNumber);
1719
+
1344
1720
  /* Avatar */
1345
1721
  class FigAvatar extends HTMLElement {
1346
1722
  constructor() {
@@ -1438,14 +1814,13 @@ class FigInputColor extends HTMLElement {
1438
1814
  </fig-input-text>`;
1439
1815
  if (this.getAttribute("alpha") === "true") {
1440
1816
  label += `<fig-tooltip text="Opacity">
1441
- <fig-input-text
1817
+ <fig-input-number
1442
1818
  placeholder="##"
1443
- type="number"
1444
1819
  min="0"
1445
1820
  max="100"
1446
- value="${this.alpha}">
1447
- <span slot="append">%</slot>
1448
- </fig-input-text>
1821
+ value="${this.alpha}"
1822
+ units="%">
1823
+ </fig-input-number>
1449
1824
  </fig-tooltip>`;
1450
1825
  }
1451
1826
  html = `<div class="input-combo">
@@ -1460,7 +1835,7 @@ class FigInputColor extends HTMLElement {
1460
1835
  requestAnimationFrame(() => {
1461
1836
  this.#swatch = this.querySelector("fig-chit[type=color]");
1462
1837
  this.#textInput = this.querySelector("fig-input-text:not([type=number])");
1463
- this.#alphaInput = this.querySelector("fig-input-text[type=number]");
1838
+ this.#alphaInput = this.querySelector("fig-input-number");
1464
1839
 
1465
1840
  this.#swatch.disabled = this.hasAttribute("disabled");
1466
1841
  this.#swatch.addEventListener("input", this.#handleInput.bind(this));
@@ -1522,7 +1897,9 @@ class FigInputColor extends HTMLElement {
1522
1897
  #handleAlphaInput(event) {
1523
1898
  //do not propagate to onInput handler for web component
1524
1899
  event.stopPropagation();
1525
- const alpha = Math.round((event.target.value / 100) * 255);
1900
+ // fig-input-number stores numeric value internally, ensure it's a number
1901
+ const alphaValue = Number(event.target.value) || 0;
1902
+ const alpha = Math.round((alphaValue / 100) * 255);
1526
1903
  const alphaHex = alpha.toString(16).padStart(2, "0");
1527
1904
  this.#setValues(this.hexOpaque + alphaHex);
1528
1905
  this.#emitInputEvent();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "1.9.3",
3
+ "version": "1.9.5",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "devDependencies": {