@sondor-technology/sondor-components 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/dist/components/alert/alert.constants.d.ts +3 -0
  2. package/dist/components/alert/alert.constants.d.ts.map +1 -0
  3. package/dist/components/alert/alert.constants.js +2 -0
  4. package/dist/components/alert/alert.constants.js.map +1 -0
  5. package/dist/components/alert/alert.d.ts +4 -3
  6. package/dist/components/alert/alert.d.ts.map +1 -1
  7. package/dist/components/alert/alert.js +21 -11
  8. package/dist/components/alert/alert.js.map +1 -0
  9. package/dist/components/badge/badge.constants.d.ts +3 -0
  10. package/dist/components/badge/badge.constants.d.ts.map +1 -0
  11. package/dist/components/badge/badge.constants.js +2 -0
  12. package/dist/components/badge/badge.constants.js.map +1 -0
  13. package/dist/components/badge/badge.d.ts +1 -1
  14. package/dist/components/badge/badge.d.ts.map +1 -1
  15. package/dist/components/badge/badge.js +3 -2
  16. package/dist/components/badge/badge.js.map +1 -0
  17. package/dist/components/breadcrumb/breadcrumb.d.ts.map +1 -1
  18. package/dist/components/breadcrumb/breadcrumb.js +2 -1
  19. package/dist/components/breadcrumb/breadcrumb.js.map +1 -0
  20. package/dist/components/button/button.constants.d.ts +7 -0
  21. package/dist/components/button/button.constants.d.ts.map +1 -0
  22. package/dist/components/button/button.constants.js +4 -0
  23. package/dist/components/button/button.constants.js.map +1 -0
  24. package/dist/components/button/button.d.ts +8 -4
  25. package/dist/components/button/button.d.ts.map +1 -1
  26. package/dist/components/button/button.js +181 -137
  27. package/dist/components/button/button.js.map +1 -0
  28. package/dist/components/button-group/button-group.d.ts +6 -0
  29. package/dist/components/button-group/button-group.d.ts.map +1 -1
  30. package/dist/components/button-group/button-group.js +17 -21
  31. package/dist/components/button-group/button-group.js.map +1 -0
  32. package/dist/components/checkbox/checkbox.constants.d.ts +3 -0
  33. package/dist/components/checkbox/checkbox.constants.d.ts.map +1 -0
  34. package/dist/components/checkbox/checkbox.constants.js +2 -0
  35. package/dist/components/checkbox/checkbox.constants.js.map +1 -0
  36. package/dist/components/checkbox/checkbox.d.ts +5 -1
  37. package/dist/components/checkbox/checkbox.d.ts.map +1 -1
  38. package/dist/components/checkbox/checkbox.js +21 -5
  39. package/dist/components/checkbox/checkbox.js.map +1 -0
  40. package/dist/components/code-block/code-block.d.ts +12 -7
  41. package/dist/components/code-block/code-block.d.ts.map +1 -1
  42. package/dist/components/code-block/code-block.js +130 -66
  43. package/dist/components/code-block/code-block.js.map +1 -0
  44. package/dist/components/content-card/content-card.d.ts.map +1 -1
  45. package/dist/components/content-card/content-card.js +10 -15
  46. package/dist/components/content-card/content-card.js.map +1 -0
  47. package/dist/components/divider/divider.constants.d.ts +5 -0
  48. package/dist/components/divider/divider.constants.d.ts.map +1 -0
  49. package/dist/components/divider/divider.constants.js +3 -0
  50. package/dist/components/divider/divider.constants.js.map +1 -0
  51. package/dist/components/divider/divider.d.ts +4 -2
  52. package/dist/components/divider/divider.d.ts.map +1 -1
  53. package/dist/components/divider/divider.js +18 -9
  54. package/dist/components/divider/divider.js.map +1 -0
  55. package/dist/components/drawer/drawer.d.ts +7 -4
  56. package/dist/components/drawer/drawer.d.ts.map +1 -1
  57. package/dist/components/drawer/drawer.js +73 -30
  58. package/dist/components/drawer/drawer.js.map +1 -0
  59. package/dist/components/field/field.d.ts +2 -2
  60. package/dist/components/field/field.d.ts.map +1 -1
  61. package/dist/components/field/field.js +8 -9
  62. package/dist/components/field/field.js.map +1 -0
  63. package/dist/components/footer/footer.d.ts +8 -1
  64. package/dist/components/footer/footer.d.ts.map +1 -1
  65. package/dist/components/footer/footer.js +42 -12
  66. package/dist/components/footer/footer.js.map +1 -0
  67. package/dist/components/form/form-step.js +1 -0
  68. package/dist/components/form/form-step.js.map +1 -0
  69. package/dist/components/form/form.d.ts +3 -0
  70. package/dist/components/form/form.d.ts.map +1 -1
  71. package/dist/components/form/form.js +107 -67
  72. package/dist/components/form/form.js.map +1 -0
  73. package/dist/components/grid/grid-row.d.ts +3 -3
  74. package/dist/components/grid/grid-row.d.ts.map +1 -1
  75. package/dist/components/grid/grid-row.js +36 -22
  76. package/dist/components/grid/grid-row.js.map +1 -0
  77. package/dist/components/grid/grid.d.ts +2 -2
  78. package/dist/components/grid/grid.d.ts.map +1 -1
  79. package/dist/components/grid/grid.js +5 -2
  80. package/dist/components/grid/grid.js.map +1 -0
  81. package/dist/components/icon/icon-paths.js +1 -0
  82. package/dist/components/icon/icon-paths.js.map +1 -0
  83. package/dist/components/icon/icon.js +1 -0
  84. package/dist/components/icon/icon.js.map +1 -0
  85. package/dist/components/input/input.d.ts +14 -0
  86. package/dist/components/input/input.d.ts.map +1 -1
  87. package/dist/components/input/input.js +30 -37
  88. package/dist/components/input/input.js.map +1 -0
  89. package/dist/components/link/link.d.ts +6 -6
  90. package/dist/components/link/link.d.ts.map +1 -1
  91. package/dist/components/link/link.js +42 -19
  92. package/dist/components/link/link.js.map +1 -0
  93. package/dist/components/metadata/metadata.d.ts +3 -3
  94. package/dist/components/metadata/metadata.d.ts.map +1 -1
  95. package/dist/components/metadata/metadata.js +23 -20
  96. package/dist/components/metadata/metadata.js.map +1 -0
  97. package/dist/components/modal/modal.d.ts +4 -16
  98. package/dist/components/modal/modal.d.ts.map +1 -1
  99. package/dist/components/modal/modal.js +99 -62
  100. package/dist/components/modal/modal.js.map +1 -0
  101. package/dist/components/page-layout/page-layout.d.ts +4 -3
  102. package/dist/components/page-layout/page-layout.d.ts.map +1 -1
  103. package/dist/components/page-layout/page-layout.js +96 -69
  104. package/dist/components/page-layout/page-layout.js.map +1 -0
  105. package/dist/components/radio/radio.d.ts +8 -0
  106. package/dist/components/radio/radio.d.ts.map +1 -1
  107. package/dist/components/radio/radio.js +8 -12
  108. package/dist/components/radio/radio.js.map +1 -0
  109. package/dist/components/radio-group/radio-group.d.ts +6 -1
  110. package/dist/components/radio-group/radio-group.d.ts.map +1 -1
  111. package/dist/components/radio-group/radio-group.js +44 -18
  112. package/dist/components/radio-group/radio-group.js.map +1 -0
  113. package/dist/components/router/router.d.ts +5 -7
  114. package/dist/components/router/router.d.ts.map +1 -1
  115. package/dist/components/router/router.js +54 -38
  116. package/dist/components/router/router.js.map +1 -0
  117. package/dist/components/select/select.d.ts +41 -7
  118. package/dist/components/select/select.d.ts.map +1 -1
  119. package/dist/components/select/select.js +467 -150
  120. package/dist/components/select/select.js.map +1 -0
  121. package/dist/components/skeleton/skeleton.d.ts +3 -2
  122. package/dist/components/skeleton/skeleton.d.ts.map +1 -1
  123. package/dist/components/skeleton/skeleton.js +10 -13
  124. package/dist/components/skeleton/skeleton.js.map +1 -0
  125. package/dist/components/switch/switch.d.ts +12 -0
  126. package/dist/components/switch/switch.d.ts.map +1 -1
  127. package/dist/components/switch/switch.js +28 -22
  128. package/dist/components/switch/switch.js.map +1 -0
  129. package/dist/components/table/data-table.d.ts +3 -25
  130. package/dist/components/table/data-table.d.ts.map +1 -1
  131. package/dist/components/table/data-table.js +211 -146
  132. package/dist/components/table/data-table.js.map +1 -0
  133. package/dist/components/table/table.d.ts +3 -3
  134. package/dist/components/table/table.d.ts.map +1 -1
  135. package/dist/components/table/table.js +39 -20
  136. package/dist/components/table/table.js.map +1 -0
  137. package/dist/components/tabs/tab-panel.d.ts +8 -1
  138. package/dist/components/tabs/tab-panel.d.ts.map +1 -1
  139. package/dist/components/tabs/tab-panel.js +21 -11
  140. package/dist/components/tabs/tab-panel.js.map +1 -0
  141. package/dist/components/tabs/tab.d.ts +10 -1
  142. package/dist/components/tabs/tab.d.ts.map +1 -1
  143. package/dist/components/tabs/tab.js +46 -28
  144. package/dist/components/tabs/tab.js.map +1 -0
  145. package/dist/components/tabs/tabs.d.ts +9 -0
  146. package/dist/components/tabs/tabs.d.ts.map +1 -1
  147. package/dist/components/tabs/tabs.js +76 -13
  148. package/dist/components/tabs/tabs.js.map +1 -0
  149. package/dist/components/textarea/textarea.d.ts +13 -0
  150. package/dist/components/textarea/textarea.d.ts.map +1 -1
  151. package/dist/components/textarea/textarea.js +59 -13
  152. package/dist/components/textarea/textarea.js.map +1 -0
  153. package/dist/components/theme-toggle/theme-toggle.constants.d.ts +3 -0
  154. package/dist/components/theme-toggle/theme-toggle.constants.d.ts.map +1 -0
  155. package/dist/components/theme-toggle/theme-toggle.constants.js +2 -0
  156. package/dist/components/theme-toggle/theme-toggle.constants.js.map +1 -0
  157. package/dist/components/theme-toggle/theme-toggle.d.ts +3 -2
  158. package/dist/components/theme-toggle/theme-toggle.d.ts.map +1 -1
  159. package/dist/components/theme-toggle/theme-toggle.js +46 -19
  160. package/dist/components/theme-toggle/theme-toggle.js.map +1 -0
  161. package/dist/components/vertical-nav/vertical-nav-group.d.ts +5 -2
  162. package/dist/components/vertical-nav/vertical-nav-group.d.ts.map +1 -1
  163. package/dist/components/vertical-nav/vertical-nav-group.js +50 -14
  164. package/dist/components/vertical-nav/vertical-nav-group.js.map +1 -0
  165. package/dist/components/vertical-nav/vertical-nav-item.d.ts +3 -2
  166. package/dist/components/vertical-nav/vertical-nav-item.d.ts.map +1 -1
  167. package/dist/components/vertical-nav/vertical-nav-item.js +30 -8
  168. package/dist/components/vertical-nav/vertical-nav-item.js.map +1 -0
  169. package/dist/styles/index.css +50 -17
  170. package/dist/styles/reset.js +1 -0
  171. package/dist/styles/reset.js.map +1 -0
  172. package/package.json +2 -1
  173. package/src/components/alert/alert.constants.ts +2 -0
  174. package/src/components/alert/alert.ts +24 -15
  175. package/src/components/badge/badge.constants.ts +2 -0
  176. package/src/components/badge/badge.ts +3 -4
  177. package/src/components/breadcrumb/breadcrumb.ts +2 -2
  178. package/src/components/button/button.constants.ts +8 -0
  179. package/src/components/button/button.ts +184 -144
  180. package/src/components/button-group/button-group.ts +23 -21
  181. package/src/components/checkbox/checkbox.constants.ts +2 -0
  182. package/src/components/checkbox/checkbox.ts +33 -18
  183. package/src/components/code-block/code-block.ts +157 -84
  184. package/src/components/content-card/content-card.ts +10 -16
  185. package/src/components/divider/divider.constants.ts +5 -0
  186. package/src/components/divider/divider.ts +22 -11
  187. package/src/components/drawer/drawer.ts +79 -34
  188. package/src/components/field/field.ts +9 -11
  189. package/src/components/footer/footer.ts +43 -12
  190. package/src/components/form/form.ts +120 -83
  191. package/src/components/grid/grid-row.ts +45 -32
  192. package/src/components/grid/grid.ts +7 -4
  193. package/src/components/input/input.ts +37 -41
  194. package/src/components/link/link.ts +45 -21
  195. package/src/components/metadata/metadata.ts +26 -24
  196. package/src/components/modal/modal.ts +108 -74
  197. package/src/components/page-layout/page-layout.ts +98 -73
  198. package/src/components/radio/radio.ts +8 -13
  199. package/src/components/radio-group/radio-group.ts +45 -21
  200. package/src/components/router/router.ts +73 -50
  201. package/src/components/select/select.ts +502 -131
  202. package/src/components/skeleton/skeleton.ts +14 -14
  203. package/src/components/switch/switch.ts +31 -25
  204. package/src/components/table/data-table.ts +248 -177
  205. package/src/components/table/table.ts +52 -29
  206. package/src/components/tabs/tab-panel.ts +30 -13
  207. package/src/components/tabs/tab.ts +64 -33
  208. package/src/components/tabs/tabs.ts +82 -14
  209. package/src/components/textarea/textarea.ts +63 -15
  210. package/src/components/theme-toggle/theme-toggle.constants.ts +2 -0
  211. package/src/components/theme-toggle/theme-toggle.ts +47 -22
  212. package/src/components/vertical-nav/vertical-nav-group.ts +57 -18
  213. package/src/components/vertical-nav/vertical-nav-item.ts +35 -12
  214. package/src/styles/globals.css +27 -4
  215. package/src/styles/theme.css +39 -31
  216. package/tsconfig.json +2 -1
  217. package/dist/components/card/card.d.ts +0 -11
  218. package/dist/components/card/card.d.ts.map +0 -1
  219. package/dist/components/card/card.js +0 -54
  220. package/src/components/card/card.ts +0 -53
@@ -1,162 +1,533 @@
1
- import { LitElement, html, css } from 'lit';
2
- import { customElement, property } from 'lit/decorators.js';
3
- import { resetStyles } from '../../styles/reset.js';
1
+ import {LitElement, html, css, PropertyValues} from "lit";
2
+ import {customElement, property, state, query} from "lit/decorators.js";
3
+ import {classMap} from "lit/directives/class-map.js";
4
4
 
5
- @customElement('st-select')
5
+ export interface SelectOption {
6
+ label: string;
7
+ value: string;
8
+ }
9
+
10
+ export type SelectAutocomplete =
11
+ | "on"
12
+ | "off"
13
+ | "name"
14
+ | "honorific-prefix"
15
+ | "given-name"
16
+ | "additional-name"
17
+ | "family-name"
18
+ | "honorific-suffix"
19
+ | "nickname"
20
+ | "email"
21
+ | "username"
22
+ | "new-password"
23
+ | "current-password"
24
+ | "one-time-code"
25
+ | "organization"
26
+ | "organization-title"
27
+ | "url"
28
+ | "photo"
29
+ | "webauthn"
30
+ | "street-address"
31
+ | "address-line1"
32
+ | "address-line2"
33
+ | "address-line3"
34
+ | "address-level1"
35
+ | "address-level2"
36
+ | "address-level3"
37
+ | "address-level4"
38
+ | "country"
39
+ | "country-name"
40
+ | "postal-code"
41
+ | "cc-name"
42
+ | "cc-given-name"
43
+ | "cc-additional-name"
44
+ | "cc-family-name"
45
+ | "cc-number"
46
+ | "cc-exp"
47
+ | "cc-exp-month"
48
+ | "cc-exp-year"
49
+ | "cc-csc"
50
+ | "cc-type"
51
+ | "transaction-currency"
52
+ | "transaction-amount"
53
+ | "language"
54
+ | "bday"
55
+ | "bday-day"
56
+ | "bday-month"
57
+ | "bday-year"
58
+ | "sex"
59
+ | "tel"
60
+ | "tel-country-code"
61
+ | "tel-national"
62
+ | "tel-area-code"
63
+ | "tel-local"
64
+ | "tel-local-prefix"
65
+ | "tel-local-suffix"
66
+ | "tel-extension"
67
+ | "impp"
68
+ | (string & {});
69
+
70
+ @customElement("st-select")
6
71
  export class StSelect extends LitElement {
7
- @property({ type: String, reflect: true }) size: 'sm' | 'md' | 'lg' = 'md';
8
- @property({ type: String, reflect: true }) variant: 'outline' | 'filled' = 'outline';
9
- @property({ type: String, reflect: true }) color: 'default' | 'primary' | 'error' = 'default';
10
-
11
- // Internal state to handle the chevron animation when the slotted select is focused
12
- @property({ type: Boolean, reflect: true }) focused = false;
72
+ static formAssociated = true;
73
+ private internals: ElementInternals;
74
+
75
+ @property({type: String}) name = "";
76
+ @property({type: String}) label = "";
77
+ @property({type: String}) value = "";
78
+ @property({type: Boolean}) required = false;
79
+ @property({type: Boolean}) multiple = false;
80
+ @property({type: Boolean}) searchable = false;
81
+ @property({type: Array}) options: SelectOption[] = [];
82
+ @property({type: String}) error = "";
83
+
84
+ @property({type: Boolean, reflect: true}) disabled = false;
85
+ @property({type: Boolean, reflect: true}) invalid = false;
86
+ @property({type: String}) autocomplete: SelectAutocomplete = "off";
87
+
88
+ @state() private open = false;
89
+ @state() private filterText = "";
90
+ @state() private nativeError = "";
91
+ @state() private touched = false;
92
+
93
+ @query(".combobox") private buttonElement!: HTMLElement;
94
+ @query(".search-input") private searchInputEl?: HTMLInputElement;
95
+
96
+ static styles = css`
97
+ :host {
98
+ display: block;
99
+ font-family: inherit;
100
+ position: relative;
101
+ }
102
+
103
+ label {
104
+ display: block;
105
+ font-size: var(--st-font-size-sm);
106
+ font-weight: 500;
107
+ color: var(--st-color-text-muted);
108
+ margin-bottom: var(--st-spacing-xs);
109
+ }
110
+
111
+ .combobox {
112
+ display: flex;
113
+ justify-content: space-between;
114
+ align-items: center;
115
+ width: 100%;
116
+ min-height: 42px;
117
+ padding: var(--st-spacing-xs) var(--st-spacing-md);
118
+ background: var(--st-color-surface);
119
+ border: 1px solid var(--st-color-border);
120
+ border-radius: var(--st-radius-md);
121
+ font-size: var(--st-font-size-base);
122
+ color: var(--st-color-text);
123
+ cursor: pointer;
124
+ text-align: left;
125
+ box-sizing: border-box;
126
+ transition:
127
+ border-color 0.2s,
128
+ box-shadow 0.2s,
129
+ background-color 0.2s;
130
+ }
131
+
132
+ .combobox:disabled {
133
+ background: var(--st-color-surface-muted);
134
+ cursor: not-allowed;
135
+ opacity: 0.7;
136
+ }
137
+
138
+ .combobox.is-invalid {
139
+ border-color: var(--st-color-danger, #dc2626);
140
+ }
141
+
142
+ .combobox:focus-visible {
143
+ outline: none;
144
+ border-color: var(--st-color-primary);
145
+ box-shadow: 0 0 0 2px var(--st-color-primary-muted);
146
+ }
147
+
148
+ .combobox.is-placeholder {
149
+ color: var(--st-color-text-muted);
150
+ }
151
+
152
+ .selection-container {
153
+ display: flex;
154
+ flex-wrap: wrap;
155
+ gap: 4px;
156
+ align-items: center;
157
+ flex: 1;
158
+ overflow: hidden;
159
+ }
160
+
161
+ .chip {
162
+ display: inline-flex;
163
+ align-items: center;
164
+ background: var(--st-color-surface-muted);
165
+ border: 1px solid var(--st-color-border);
166
+ border-radius: var(--st-radius-sm);
167
+ padding: 2px 6px;
168
+ font-size: var(--st-font-size-xs);
169
+ color: var(--st-color-text);
170
+ }
171
+
172
+ .listbox-container {
173
+ position: absolute;
174
+ top: calc(100% + 4px);
175
+ left: 0;
176
+ right: 0;
177
+ z-index: 10;
178
+ background: var(--st-color-surface);
179
+ border: 1px solid var(--st-color-border);
180
+ border-radius: var(--st-radius-md);
181
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
182
+ overflow: hidden;
183
+ }
184
+
185
+ .search-wrapper {
186
+ padding: var(--st-spacing-xs);
187
+ border-bottom: 1px solid var(--st-color-border);
188
+ }
189
+
190
+ .search-input {
191
+ width: 100%;
192
+ padding: var(--st-spacing-xs) var(--st-spacing-sm);
193
+ background: var(--st-color-surface-muted);
194
+ border: 1px solid var(--st-color-border);
195
+ border-radius: var(--st-radius-sm);
196
+ font-size: var(--st-font-size-sm);
197
+ color: var(--st-color-text);
198
+ box-sizing: border-box;
199
+ }
200
+
201
+ .search-input:focus {
202
+ outline: none;
203
+ border-color: var(--st-color-primary);
204
+ }
205
+
206
+ .listbox {
207
+ margin: 0;
208
+ padding: var(--st-spacing-xs) 0;
209
+ max-height: 220px;
210
+ overflow-y: auto;
211
+ list-style: none;
212
+ }
213
+
214
+ .option {
215
+ padding: var(--st-spacing-sm) var(--st-spacing-md);
216
+ cursor: pointer;
217
+ color: var(--st-color-text);
218
+ display: flex;
219
+ justify-content: space-between;
220
+ align-items: center;
221
+ }
222
+
223
+ .option:hover {
224
+ background: var(--st-color-surface-muted);
225
+ }
226
+
227
+ .option[aria-selected="true"] {
228
+ font-weight: 500;
229
+ color: var(--st-color-primary);
230
+ }
231
+
232
+ .error-msg {
233
+ display: block;
234
+ margin-top: var(--st-spacing-xs);
235
+ font-size: var(--st-font-size-xs);
236
+ color: var(--st-color-danger, #dc2626);
237
+ }
238
+
239
+ .hidden-native {
240
+ position: absolute;
241
+ width: 1px;
242
+ height: 1px;
243
+ padding: 0;
244
+ margin: -1px;
245
+ overflow: hidden;
246
+ clip: rect(0, 0, 0, 0);
247
+ border: 0;
248
+ }
249
+ `;
13
250
 
14
251
  constructor() {
15
252
  super();
16
- // Track focus states to animate the chevron
17
- this.addEventListener('focusin', () => this.focused = true);
18
- this.addEventListener('focusout', () => this.focused = false);
19
- }
20
-
21
- static styles = [
22
- resetStyles,
23
- css`
24
- :host {
25
- display: inline-block;
26
- width: 100%;
27
- position: relative;
28
- }
253
+ this.internals = this.attachInternals();
254
+ this.handleClickOutside = this.handleClickOutside.bind(this);
255
+ }
29
256
 
30
- .select-wrapper {
31
- position: relative;
32
- display: flex;
33
- align-items: center;
34
- width: 100%;
35
- }
257
+ public checkValidity(): boolean {
258
+ return this.internals.checkValidity();
259
+ }
36
260
 
37
- /*
38
- Target the native select provided by the user in the Light DOM.
39
- This completely bypasses the browser rendering bug.
40
- */
41
- ::slotted(select) {
42
- appearance: none;
43
- -webkit-appearance: none;
44
- width: 100%;
45
- font-family: inherit;
46
- background-color: transparent;
47
- border-radius: var(--st-radius-md, 0.5rem);
48
- color: var(--st-color-text);
49
- cursor: pointer;
50
- outline: none;
51
- transition: border-color 0.2s, box-shadow 0.2s, background-color 0.2s;
52
- }
261
+ public reportValidity(): boolean {
262
+ const isValid = this.checkValidity();
263
+ this.touched = true;
264
+ this.invalid = !isValid;
265
+ return isValid;
266
+ }
53
267
 
54
- ::slotted(select:disabled) {
55
- opacity: 0.6;
56
- cursor: not-allowed;
57
- }
268
+ public override focus(options?: FocusOptions) {
269
+ this.buttonElement?.focus(options);
270
+ }
58
271
 
59
- /* ==========================================================
60
- Sizes
61
- ========================================================== */
62
- :host([size="sm"]) ::slotted(select) {
63
- padding: 0.25rem 2rem 0.25rem 0.75rem;
64
- font-size: 0.875rem;
65
- height: 2rem;
66
- }
67
- :host([size="md"]) ::slotted(select) {
68
- padding: 0.5rem 2.5rem 0.5rem 1rem;
69
- font-size: 1rem;
70
- height: 2.5rem;
71
- }
72
- :host([size="lg"]) ::slotted(select) {
73
- padding: 0.75rem 3rem 0.75rem 1.25rem;
74
- font-size: 1.125rem;
75
- height: 3rem;
76
- }
272
+ connectedCallback() {
273
+ super.connectedCallback();
274
+ document.addEventListener("click", this.handleClickOutside);
77
275
 
78
- /* Multiple select formatting (No fixed height, no right padding for chevron) */
79
- ::slotted(select[multiple]) {
80
- height: auto !important;
81
- padding-right: 1rem !important;
82
- }
276
+ this.addEventListener("invalid", () => {
277
+ this.touched = true;
278
+ this.invalid = true;
279
+ });
280
+ }
83
281
 
84
- /* ==========================================================
85
- Variants & Colors
86
- ========================================================== */
87
- :host([variant="outline"]) ::slotted(select) {
88
- border: 1px solid var(--st-color-border);
89
- }
90
- :host([variant="outline"][color="default"]) ::slotted(select:focus-visible) {
91
- border-color: var(--st-color-primary);
92
- box-shadow: 0 0 0 2px rgba(var(--st-color-primary-rgb, 99, 102, 241), 0.2);
93
- }
282
+ disconnectedCallback() {
283
+ document.removeEventListener("click", this.handleClickOutside);
284
+ super.disconnectedCallback();
285
+ }
94
286
 
95
- :host([variant="filled"]) ::slotted(select) {
96
- border: 1px solid transparent;
97
- background-color: var(--st-color-surface-hover, rgba(0, 0, 0, 0.05));
98
- }
99
- :host([variant="filled"]) ::slotted(select:not(:disabled):hover) {
100
- background-color: var(--st-color-surface-active, rgba(0, 0, 0, 0.1));
101
- }
102
- :host([variant="filled"][color="default"]) ::slotted(select:focus-visible) {
103
- border-color: var(--st-color-primary);
104
- background-color: transparent;
105
- }
287
+ override willUpdate(changedProperties: PropertyValues) {
288
+ super.willUpdate(changedProperties);
289
+ if (
290
+ changedProperties.has("value") ||
291
+ changedProperties.has("required") ||
292
+ changedProperties.has("error")
293
+ ) {
294
+ this.validate();
295
+ }
296
+ }
106
297
 
107
- /* Error Color State */
108
- :host([color="error"]) ::slotted(select) {
109
- border-color: var(--st-color-error, #ef4444);
110
- }
111
- :host([color="error"]) ::slotted(select:focus-visible) {
112
- box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
113
- }
298
+ private get selectedValues(): string[] {
299
+ if (!this.value) return [];
300
+ return this.value
301
+ .split(",")
302
+ .map((v) => v.trim())
303
+ .filter(Boolean);
304
+ }
114
305
 
115
- /* Primary Color State */
116
- :host([color="primary"]) ::slotted(select) {
117
- border-color: var(--st-color-primary);
118
- }
306
+ private validate() {
307
+ this.internals.setFormValue(this.value);
119
308
 
120
- /* ==========================================================
121
- Chevron Icon
122
- ========================================================== */
123
- .chevron {
124
- position: absolute;
125
- right: 0.75rem;
126
- width: 1.25rem;
127
- height: 1.25rem;
128
- pointer-events: none;
129
- color: var(--st-color-text-muted);
130
- transition: transform 0.2s ease, color 0.2s ease;
131
- }
309
+ const isMissing = this.required && this.selectedValues.length === 0;
310
+
311
+ if (this.error) {
312
+ this.internals.setValidity({customError: true}, this.error);
313
+ this.nativeError = this.error;
314
+ } else if (isMissing) {
315
+ const msg = "Please make a selection.";
316
+ this.internals.setValidity({valueMissing: true}, msg);
317
+ this.nativeError = msg;
318
+ } else {
319
+ this.internals.setValidity({});
320
+ this.nativeError = "";
321
+ this.invalid = false;
322
+ }
323
+ }
132
324
 
133
- /* Animate chevron when the wrapper detects focus */
134
- :host([focused]) .chevron {
135
- transform: rotate(180deg);
136
- color: var(--st-color-primary);
325
+ formResetCallback() {
326
+ const initialValue = this.getAttribute("value") || "";
327
+ this.value = initialValue;
328
+ this.touched = false;
329
+ this.validate();
330
+ }
331
+
332
+ private handleClickOutside(e: Event) {
333
+ const path = e.composedPath();
334
+ if (!path.includes(this)) {
335
+ if (this.open) {
336
+ this.touched = true;
137
337
  }
338
+ this.open = false;
339
+ this.filterText = "";
340
+ }
341
+ }
342
+
343
+ private toggleOpen() {
344
+ if (this.disabled) return;
138
345
 
139
- /* Hide chevron if the user slots a native multiple select */
140
- ::slotted(select[multiple]) ~ .chevron {
141
- display: none;
346
+ if (this.open) {
347
+ this.touched = true;
348
+ }
349
+
350
+ this.open = !this.open;
351
+ if (this.open) {
352
+ setTimeout(() => {
353
+ if (this.searchable && this.searchInputEl) {
354
+ this.searchInputEl.focus();
355
+ } else {
356
+ const first = this.shadowRoot?.querySelector(".option") as HTMLElement;
357
+ first?.focus();
358
+ }
359
+ }, 0);
360
+ } else {
361
+ this.filterText = "";
362
+ }
363
+ }
364
+
365
+ private handleOptionClick(optValue: string, e: MouseEvent) {
366
+ e.stopPropagation();
367
+ this.touched = true;
368
+
369
+ if (this.multiple) {
370
+ const current = this.selectedValues;
371
+ const index = current.indexOf(optValue);
372
+ let updated: string[];
373
+ if (index > -1) {
374
+ updated = current.filter((v) => v !== optValue);
375
+ } else {
376
+ updated = [...current, optValue];
142
377
  }
143
- `
144
- ];
378
+ this.value = updated.join(",");
379
+ } else {
380
+ this.value = optValue;
381
+ this.open = false;
382
+ this.buttonElement?.focus();
383
+ }
384
+
385
+ this.dispatchEvent(new Event("change", {bubbles: true, composed: true}));
386
+ }
387
+
388
+ private handleSearchInput(e: InputEvent) {
389
+ this.filterText = (e.target as HTMLInputElement).value;
390
+ }
391
+
392
+ private handleButtonKeydown(e: KeyboardEvent) {
393
+ if (this.disabled) return;
394
+
395
+ if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
396
+ e.preventDefault();
397
+ this.toggleOpen();
398
+ }
399
+ }
145
400
 
146
401
  render() {
402
+ const currentValues = this.selectedValues;
403
+ const selectedOptions = this.options.filter((opt) => currentValues.includes(opt.value));
404
+
405
+ const isInvalid =
406
+ this.invalid || (this.touched && !this.internals.validity.valid) || !!this.error;
407
+ const displayError = isInvalid ? this.error || this.nativeError : "";
408
+
409
+ const filteredOptions = this.searchable
410
+ ? this.options.filter((opt) =>
411
+ opt.label.toLowerCase().includes(this.filterText.toLowerCase()),
412
+ )
413
+ : this.options;
414
+
147
415
  return html`
148
- <div class="select-wrapper">
149
- <slot></slot>
150
- <svg class="chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
416
+ ${this.label ? html`<label id="select-label">${this.label}</label>` : ""}
417
+
418
+ <button
419
+ type="button"
420
+ ?disabled=${this.disabled}
421
+ class=${classMap({
422
+ combobox: true,
423
+ "is-placeholder": selectedOptions.length === 0,
424
+ "is-invalid": isInvalid,
425
+ })}
426
+ role="combobox"
427
+ aria-haspopup="listbox"
428
+ aria-expanded="${this.open}"
429
+ aria-labelledby="select-label"
430
+ aria-controls="select-listbox"
431
+ @click=${this.toggleOpen}
432
+ @keydown=${this.handleButtonKeydown}
433
+ >
434
+ <div class="selection-container">
435
+ ${selectedOptions.length > 0
436
+ ? this.multiple
437
+ ? selectedOptions.map((opt) => html`<span class="chip">${opt.label}</span>`)
438
+ : html`<span>${selectedOptions[0].label}</span>`
439
+ : html`<span>Select option...</span>`}
440
+ </div>
441
+ <svg
442
+ style="width:16px;height:16px;flex-shrink:0"
443
+ viewBox="0 0 24 24"
444
+ fill="none"
445
+ stroke="currentColor"
446
+ stroke-width="2"
447
+ >
151
448
  <polyline points="6 9 12 15 18 9"></polyline>
152
449
  </svg>
153
- </div>
450
+ </button>
451
+
452
+ ${this.open
453
+ ? html`
454
+ <div class="listbox-container">
455
+ ${this.searchable
456
+ ? html`
457
+ <div class="search-wrapper" @click=${(e: Event) => e.stopPropagation()}>
458
+ <input
459
+ type="text"
460
+ class="search-input"
461
+ placeholder="Search options..."
462
+ autocomplete="off"
463
+ spellcheck="false"
464
+ autocapitalize="none"
465
+ .value=${this.filterText}
466
+ @input=${this.handleSearchInput}
467
+ />
468
+ </div>
469
+ `
470
+ : ""}
471
+
472
+ <ul
473
+ id="select-listbox"
474
+ class="listbox"
475
+ role="listbox"
476
+ aria-multiselectable="${this.multiple}"
477
+ >
478
+ ${filteredOptions.length > 0
479
+ ? filteredOptions.map((opt) => {
480
+ const isSelected = currentValues.includes(opt.value);
481
+ return html`
482
+ <li
483
+ class="option"
484
+ role="option"
485
+ aria-selected="${isSelected}"
486
+ @click=${(e: MouseEvent) => this.handleOptionClick(opt.value, e)}
487
+ >
488
+ <span>${opt.label}</span>
489
+ ${this.multiple && isSelected ? html`<span>✓</span>` : ""}
490
+ </li>
491
+ `;
492
+ })
493
+ : html`<li
494
+ class="option"
495
+ role="option"
496
+ aria-disabled="true"
497
+ style="color:var(--st-color-text-muted); cursor:default;"
498
+ >
499
+ No options found
500
+ </li>`}
501
+ </ul>
502
+ </div>
503
+ `
504
+ : ""}
505
+ ${displayError ? html`<span class="error-msg">${displayError}</span>` : ""}
506
+
507
+ <select
508
+ class="hidden-native"
509
+ name="${this.name}"
510
+ .autocomplete="${this.autocomplete as any}"
511
+ ?multiple=${this.multiple}
512
+ tabindex="-1"
513
+ aria-hidden="true"
514
+ ?disabled=${this.disabled}
515
+ >
516
+ <option value=""></option>
517
+ ${this.options.map(
518
+ (opt) => html`
519
+ <option value="${opt.value}" ?selected=${currentValues.includes(opt.value)}>
520
+ ${opt.label}
521
+ </option>
522
+ `,
523
+ )}
524
+ </select>
154
525
  `;
155
526
  }
156
527
  }
157
528
 
158
529
  declare global {
159
530
  interface HTMLElementTagNameMap {
160
- 'st-select': StSelect;
531
+ "st-select": StSelect;
161
532
  }
162
- }
533
+ }