@sondor-technology/sondor-components 1.0.0 → 1.0.1-develop.3

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 (168) hide show
  1. package/README.md +8 -0
  2. package/dist/components/alert/alert.d.ts +13 -0
  3. package/dist/components/alert/alert.d.ts.map +1 -0
  4. package/dist/components/alert/alert.js +105 -0
  5. package/dist/components/badge/badge.d.ts +13 -0
  6. package/dist/components/badge/badge.d.ts.map +1 -0
  7. package/dist/components/badge/badge.js +101 -0
  8. package/dist/components/breadcrumb/breadcrumb.d.ts +19 -0
  9. package/dist/components/breadcrumb/breadcrumb.d.ts.map +1 -0
  10. package/dist/components/breadcrumb/breadcrumb.js +99 -0
  11. package/dist/components/button/button.d.ts +23 -0
  12. package/dist/components/button/button.d.ts.map +1 -0
  13. package/dist/components/button/button.js +304 -0
  14. package/dist/components/button-group/button-group.d.ts +6 -0
  15. package/dist/components/button-group/button-group.d.ts.map +1 -0
  16. package/dist/components/button-group/button-group.js +74 -0
  17. package/dist/components/card/card.d.ts +11 -0
  18. package/dist/components/card/card.d.ts.map +1 -0
  19. package/dist/components/card/card.js +54 -0
  20. package/dist/components/checkbox/checkbox.d.ts +28 -0
  21. package/dist/components/checkbox/checkbox.d.ts.map +1 -0
  22. package/dist/components/checkbox/checkbox.js +247 -0
  23. package/dist/components/code-block/code-block.d.ts +20 -0
  24. package/dist/components/code-block/code-block.d.ts.map +1 -0
  25. package/dist/components/code-block/code-block.js +260 -0
  26. package/dist/components/content-card/content-card.d.ts +13 -0
  27. package/dist/components/content-card/content-card.d.ts.map +1 -0
  28. package/dist/components/content-card/content-card.js +140 -0
  29. package/dist/components/divider/divider.d.ts +16 -0
  30. package/dist/components/divider/divider.d.ts.map +1 -0
  31. package/dist/components/divider/divider.js +82 -0
  32. package/dist/components/drawer/drawer.d.ts +22 -0
  33. package/dist/components/drawer/drawer.d.ts.map +1 -0
  34. package/dist/components/drawer/drawer.js +226 -0
  35. package/dist/components/field/field.d.ts +11 -0
  36. package/dist/components/field/field.d.ts.map +1 -0
  37. package/dist/components/field/field.js +32 -0
  38. package/dist/components/footer/footer.d.ts +6 -0
  39. package/dist/components/footer/footer.d.ts.map +1 -0
  40. package/dist/components/footer/footer.js +65 -0
  41. package/dist/components/form/form-step.d.ts +13 -0
  42. package/dist/components/form/form-step.d.ts.map +1 -0
  43. package/dist/components/form/form-step.js +41 -0
  44. package/dist/components/form/form.d.ts +25 -0
  45. package/dist/components/form/form.d.ts.map +1 -0
  46. package/dist/components/form/form.js +358 -0
  47. package/dist/components/grid/grid-row.d.ts +17 -0
  48. package/dist/components/grid/grid-row.d.ts.map +1 -0
  49. package/dist/components/grid/grid-row.js +92 -0
  50. package/dist/components/grid/grid.d.ts +13 -0
  51. package/dist/components/grid/grid.d.ts.map +1 -0
  52. package/dist/components/grid/grid.js +42 -0
  53. package/dist/components/icon/icon-paths.d.ts +132 -0
  54. package/dist/components/icon/icon-paths.d.ts.map +1 -0
  55. package/dist/components/icon/icon-paths.js +137 -0
  56. package/dist/components/icon/icon.d.ts +16 -0
  57. package/dist/components/icon/icon.d.ts.map +1 -0
  58. package/dist/components/icon/icon.js +95 -0
  59. package/dist/components/input/input.d.ts +31 -0
  60. package/dist/components/input/input.d.ts.map +1 -0
  61. package/dist/components/input/input.js +268 -0
  62. package/dist/components/link/link.d.ts +16 -0
  63. package/dist/components/link/link.d.ts.map +1 -0
  64. package/dist/components/link/link.js +116 -0
  65. package/dist/components/metadata/metadata.d.ts +18 -0
  66. package/dist/components/metadata/metadata.d.ts.map +1 -0
  67. package/dist/components/metadata/metadata.js +71 -0
  68. package/dist/components/modal/modal.d.ts +24 -0
  69. package/dist/components/modal/modal.d.ts.map +1 -0
  70. package/dist/components/modal/modal.js +163 -0
  71. package/dist/components/page-layout/page-layout.d.ts +46 -0
  72. package/dist/components/page-layout/page-layout.d.ts.map +1 -0
  73. package/dist/components/page-layout/page-layout.js +697 -0
  74. package/dist/components/radio/radio.d.ts +16 -0
  75. package/dist/components/radio/radio.d.ts.map +1 -0
  76. package/dist/components/radio/radio.js +152 -0
  77. package/dist/components/radio-group/radio-group.d.ts +27 -0
  78. package/dist/components/radio-group/radio-group.d.ts.map +1 -0
  79. package/dist/components/radio-group/radio-group.js +161 -0
  80. package/dist/components/router/router.d.ts +7 -0
  81. package/dist/components/router/router.d.ts.map +1 -0
  82. package/dist/components/router/router.js +37 -0
  83. package/dist/components/select/select.d.ts +16 -0
  84. package/dist/components/select/select.d.ts.map +1 -0
  85. package/dist/components/select/select.js +172 -0
  86. package/dist/components/skeleton/skeleton.d.ts +17 -0
  87. package/dist/components/skeleton/skeleton.d.ts.map +1 -0
  88. package/dist/components/skeleton/skeleton.js +78 -0
  89. package/dist/components/switch/switch.d.ts +25 -0
  90. package/dist/components/switch/switch.d.ts.map +1 -0
  91. package/dist/components/switch/switch.js +185 -0
  92. package/dist/components/table/data-table.d.ts +72 -0
  93. package/dist/components/table/data-table.d.ts.map +1 -0
  94. package/dist/components/table/data-table.js +551 -0
  95. package/dist/components/table/table.d.ts +22 -0
  96. package/dist/components/table/table.d.ts.map +1 -0
  97. package/dist/components/table/table.js +167 -0
  98. package/dist/components/tabs/tab-panel.d.ts +8 -0
  99. package/dist/components/tabs/tab-panel.d.ts.map +1 -0
  100. package/dist/components/tabs/tab-panel.js +46 -0
  101. package/dist/components/tabs/tab.d.ts +9 -0
  102. package/dist/components/tabs/tab.d.ts.map +1 -0
  103. package/dist/components/tabs/tab.js +91 -0
  104. package/dist/components/tabs/tabs.d.ts +14 -0
  105. package/dist/components/tabs/tabs.d.ts.map +1 -0
  106. package/dist/components/tabs/tabs.js +85 -0
  107. package/dist/components/textarea/textarea.d.ts +36 -0
  108. package/dist/components/textarea/textarea.d.ts.map +1 -0
  109. package/dist/components/textarea/textarea.js +231 -0
  110. package/dist/components/theme-toggle/theme-toggle.d.ts +15 -0
  111. package/dist/components/theme-toggle/theme-toggle.d.ts.map +1 -0
  112. package/dist/components/theme-toggle/theme-toggle.js +130 -0
  113. package/dist/components/vertical-nav/vertical-nav-group.d.ts +10 -0
  114. package/dist/components/vertical-nav/vertical-nav-group.d.ts.map +1 -0
  115. package/dist/components/vertical-nav/vertical-nav-group.js +127 -0
  116. package/dist/components/vertical-nav/vertical-nav-item.d.ts +9 -0
  117. package/dist/components/vertical-nav/vertical-nav-item.d.ts.map +1 -0
  118. package/dist/components/vertical-nav/vertical-nav-item.js +85 -0
  119. package/dist/styles/index.css +171 -0
  120. package/dist/styles/reset.d.ts +2 -0
  121. package/dist/styles/reset.d.ts.map +1 -0
  122. package/dist/styles/reset.js +50 -0
  123. package/package.json +7 -2
  124. package/src/components/alert/alert.ts +100 -100
  125. package/src/components/badge/badge.ts +96 -96
  126. package/src/components/breadcrumb/breadcrumb.ts +98 -98
  127. package/src/components/button/button.ts +281 -281
  128. package/src/components/button-group/button-group.ts +67 -67
  129. package/src/components/card/card.ts +52 -52
  130. package/src/components/checkbox/checkbox.ts +238 -238
  131. package/src/components/code-block/code-block.ts +256 -256
  132. package/src/components/content-card/content-card.ts +133 -133
  133. package/src/components/divider/divider.ts +68 -68
  134. package/src/components/drawer/drawer.ts +217 -217
  135. package/src/components/field/field.ts +30 -30
  136. package/src/components/footer/footer.ts +58 -58
  137. package/src/components/form/form-step.ts +32 -32
  138. package/src/components/form/form.ts +376 -376
  139. package/src/components/grid/grid-row.ts +75 -75
  140. package/src/components/grid/grid.ts +36 -36
  141. package/src/components/icon/icon-paths.ts +140 -124
  142. package/src/components/icon/icon.ts +82 -82
  143. package/src/components/input/input.ts +248 -248
  144. package/src/components/link/link.ts +97 -97
  145. package/src/components/metadata/metadata.ts +66 -66
  146. package/src/components/modal/modal.ts +158 -158
  147. package/src/components/page-layout/page-layout.ts +704 -704
  148. package/src/components/radio/radio.ts +142 -142
  149. package/src/components/radio-group/radio-group.ts +157 -157
  150. package/src/components/router/router.ts +46 -46
  151. package/src/components/select/select.ts +161 -161
  152. package/src/components/skeleton/skeleton.ts +67 -67
  153. package/src/components/switch/switch.ts +181 -181
  154. package/src/components/table/data-table.ts +556 -556
  155. package/src/components/table/table.ts +153 -153
  156. package/src/components/tabs/tab-panel.ts +30 -30
  157. package/src/components/tabs/tab.ts +76 -76
  158. package/src/components/tabs/tabs.ts +88 -88
  159. package/src/components/textarea/textarea.ts +215 -215
  160. package/src/components/theme-toggle/theme-toggle.ts +131 -131
  161. package/src/components/vertical-nav/vertical-nav-group.ts +109 -96
  162. package/src/components/vertical-nav/vertical-nav-item.ts +66 -49
  163. package/src/styles/globals.css +58 -58
  164. package/src/styles/index.css +80 -80
  165. package/src/styles/reset.ts +50 -50
  166. package/src/styles/theme.css +58 -58
  167. package/src/styles/typography.css +25 -25
  168. package/tsconfig.json +15 -15
@@ -1,376 +1,376 @@
1
- import {LitElement, html, css} from "lit";
2
- import {customElement, property, state, queryAssignedElements, query} from "lit/decorators.js";
3
- import {resetStyles} from "../../styles/reset.js";
4
- import type {StFormStep} from "./form-step.js";
5
-
6
- export type FormLayout = "vertical" | "horizontal" | "inline";
7
-
8
- @customElement("st-form")
9
- export class StForm extends LitElement {
10
- @property({type: String, reflect: true}) layout: FormLayout = "vertical";
11
- @property({type: Boolean, reflect: true}) loading = false;
12
- @property({type: String}) error = "";
13
-
14
- @state() private _currentStepIndex = 0;
15
-
16
- @queryAssignedElements({selector: "st-form-step"})
17
- private _stepElements!: StFormStep[];
18
-
19
- @query("form") private _formEl!: HTMLFormElement;
20
-
21
- static styles = [
22
- resetStyles,
23
- css`
24
- :host {
25
- display: block;
26
- position: relative;
27
- }
28
-
29
- form {
30
- display: flex;
31
- gap: var(--st-spacing-md);
32
- }
33
-
34
- /* ==========================================================
35
- Layouts
36
- ========================================================== */
37
- :host([layout="vertical"]) form {
38
- flex-direction: column;
39
- }
40
-
41
- :host([layout="horizontal"]) form {
42
- display: grid;
43
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
44
- align-items: start;
45
- }
46
-
47
- :host([layout="inline"]) form {
48
- flex-direction: row;
49
- align-items: flex-end;
50
- flex-wrap: wrap;
51
- }
52
-
53
- .has-steps form {
54
- display: flex;
55
- flex-direction: column;
56
- }
57
-
58
- /* ==========================================================
59
- Progress Navigation
60
- ========================================================== */
61
- .progress-nav {
62
- display: flex;
63
- align-items: center;
64
- margin-bottom: var(--st-spacing-xl);
65
- padding-bottom: var(--st-spacing-md);
66
- overflow-x: auto;
67
- scrollbar-width: none;
68
- }
69
-
70
- .step-indicator {
71
- display: flex;
72
- align-items: center;
73
- gap: var(--st-spacing-sm);
74
- color: var(--st-color-text-muted);
75
- font-size: var(--st-font-size-sm);
76
- font-weight: var(--st-font-weight-medium);
77
- transition: color 0.2s ease;
78
- }
79
-
80
- .step-indicator.active {
81
- color: var(--st-color-text);
82
- }
83
- .step-indicator.completed {
84
- color: var(--st-color-success);
85
- }
86
-
87
- .step-circle {
88
- display: flex;
89
- align-items: center;
90
- justify-content: center;
91
- width: 1.75rem;
92
- height: 1.75rem;
93
- border-radius: 50%;
94
- border: 2px solid currentColor;
95
- font-size: 0.875rem;
96
- background-color: var(--st-color-bg);
97
- transition: all 0.2s ease;
98
- }
99
-
100
- .step-indicator.active .step-circle {
101
- background-color: var(--st-color-primary);
102
- border-color: var(--st-color-primary);
103
- color: var(--st-color-bg);
104
- }
105
-
106
- .step-indicator.completed .step-circle {
107
- border-color: var(--st-color-success);
108
- background-color: var(--st-color-success-bg, transparent);
109
- }
110
-
111
- .step-connector {
112
- flex: 1;
113
- height: 2px;
114
- min-width: 24px;
115
- margin: 0 var(--st-spacing-sm);
116
- background-color: var(--st-color-border);
117
- transition: background-color 0.2s ease;
118
- }
119
-
120
- .step-connector.completed {
121
- background-color: var(--st-color-success);
122
- }
123
-
124
- /* ==========================================================
125
- Actions & States
126
- ========================================================== */
127
- .form-actions {
128
- display: flex;
129
- gap: var(--st-spacing-md);
130
- margin-top: var(--st-spacing-lg);
131
- justify-content: flex-end;
132
- }
133
-
134
- .loading-overlay {
135
- position: absolute;
136
- inset: 0;
137
- background: rgba(var(--st-color-bg-rgb, 255, 255, 255), 0.7);
138
- backdrop-filter: blur(2px);
139
- display: flex;
140
- align-items: center;
141
- justify-content: center;
142
- z-index: 10;
143
- border-radius: inherit;
144
- color: var(--st-color-text);
145
- font-weight: var(--st-font-weight-medium);
146
- gap: var(--st-spacing-sm);
147
- }
148
-
149
- @keyframes pulse {
150
- 0%,
151
- 100% {
152
- opacity: 1;
153
- }
154
- 50% {
155
- opacity: 0.5;
156
- }
157
- }
158
-
159
- .loading-overlay st-icon {
160
- animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
161
- }
162
- `,
163
- ];
164
-
165
- private _handleSlotChange() {
166
- if (this._stepElements.length > 0) {
167
- this._updateStepVisibility();
168
- }
169
- }
170
-
171
- private _updateStepVisibility() {
172
- this._stepElements.forEach((step, index) => {
173
- step.active = index === this._currentStepIndex;
174
- });
175
- this.requestUpdate();
176
- }
177
-
178
- private _validateCurrentContext(): boolean {
179
- if (this._stepElements.length === 0) {
180
- return this._formEl.checkValidity();
181
- }
182
-
183
- const currentStep = this._stepElements[this._currentStepIndex];
184
- let isValid = true;
185
-
186
- // Find all custom and native form inputs assigned to the current step
187
- const inputs = Array.from(
188
- currentStep.querySelectorAll<
189
- HTMLElement & {checkValidity?: () => boolean; reportValidity?: () => void}
190
- >("*"),
191
- );
192
-
193
- for (const input of inputs) {
194
- if (typeof input.checkValidity === "function") {
195
- if (!input.checkValidity()) {
196
- // Focus and report on the first invalid element encountered
197
- if (isValid && typeof input.reportValidity === "function") {
198
- input.reportValidity();
199
- }
200
- isValid = false;
201
- }
202
- }
203
- }
204
-
205
- return isValid;
206
- }
207
-
208
- private _triggerSubmission() {
209
- // HTML5 method that triggers form validation and fires the native submit event
210
- this._formEl.requestSubmit();
211
- }
212
-
213
- private _prevStep() {
214
- if (this._currentStepIndex > 0) {
215
- this._currentStepIndex--;
216
- this._updateStepVisibility();
217
- }
218
- }
219
-
220
- private _extractFormData(): FormData {
221
- const formData = new FormData();
222
-
223
- // Query all light-DOM children that have a 'name' attribute
224
- const fields = this.querySelectorAll<
225
- HTMLElement & {name: string; value: any; checked?: boolean; files?: FileList}
226
- >("[name]");
227
-
228
- fields.forEach((field) => {
229
- // Standard HTML form behavior: ignore disabled fields
230
- if (field.hasAttribute("disabled")) return;
231
-
232
- const isToggle =
233
- field.tagName === "ST-RADIO" ||
234
- field.tagName === "ST-CHECKBOX" ||
235
- field.getAttribute("type") === "radio" ||
236
- field.getAttribute("type") === "checkbox";
237
-
238
- if (isToggle) {
239
- // Only append radios/checkboxes if they are actively checked
240
- if (field.checked) {
241
- formData.append(field.name, field.value || "on");
242
- }
243
- } else if (
244
- field.tagName === "INPUT" &&
245
- field.getAttribute("type") === "file" &&
246
- field.files
247
- ) {
248
- // Handle native file uploads
249
- Array.from(field.files).forEach((file) => formData.append(field.name, file));
250
- } else if (field.value !== undefined) {
251
- // Handle st-input, st-select, st-textarea, and standard inputs
252
- formData.append(field.name, field.value);
253
- }
254
- });
255
-
256
- return formData;
257
- }
258
-
259
- private _handleSubmit(e: Event) {
260
- e.preventDefault();
261
- e.stopPropagation();
262
-
263
- if (!this._validateCurrentContext()) return;
264
-
265
- const hasSteps = this._stepElements.length > 0;
266
- const isLastStep = hasSteps ? this._currentStepIndex === this._stepElements.length - 1 : true;
267
-
268
- if (hasSteps && !isLastStep) {
269
- this._currentStepIndex++;
270
- this._updateStepVisibility();
271
- return;
272
- }
273
-
274
- // Use our custom extractor to pierce the Shadow DOM boundary
275
- const formData = this._extractFormData();
276
-
277
- this.dispatchEvent(
278
- new CustomEvent("submit", {
279
- bubbles: true,
280
- composed: true,
281
- cancelable: true,
282
- detail: {formData},
283
- }),
284
- );
285
- }
286
-
287
- render() {
288
- const hasSteps = this._stepElements?.length > 0;
289
- const isLastStep = hasSteps ? this._currentStepIndex === this._stepElements.length - 1 : true;
290
-
291
- return html`
292
- <div class="${hasSteps ? "has-steps" : ""}" aria-busy=${this.loading}>
293
- ${this.loading
294
- ? html`
295
- <div class="loading-overlay">
296
- <st-icon name="cloud-download" color="primary"></st-icon> Processing...
297
- </div>
298
- `
299
- : ""}
300
- ${hasSteps
301
- ? html`
302
- <div class="progress-nav" role="tablist">
303
- ${this._stepElements.map((step, index) => {
304
- const isActive = index === this._currentStepIndex;
305
- const isCompleted = index < this._currentStepIndex;
306
-
307
- return html`
308
- <div
309
- class="step-indicator ${isActive ? "active" : ""} ${isCompleted
310
- ? "completed"
311
- : ""}"
312
- role="tab"
313
- aria-selected=${isActive}
314
- >
315
- <div class="step-circle">
316
- ${isCompleted
317
- ? html`<st-icon name="check-circle" size="sm"></st-icon>`
318
- : html`${index + 1}`}
319
- </div>
320
- <span>${step.title || `Step ${index + 1}`}</span>
321
- </div>
322
- ${index < this._stepElements.length - 1
323
- ? html` <div class="step-connector ${isCompleted ? "completed" : ""}"></div> `
324
- : ""}
325
- `;
326
- })}
327
- </div>
328
- `
329
- : ""}
330
- ${this.error
331
- ? html`
332
- <st-alert color="error" style="margin-bottom: var(--st-spacing-lg);">
333
- <st-icon slot="icon" name="alert-triangle"></st-icon>
334
- ${this.error}
335
- </st-alert>
336
- `
337
- : ""}
338
-
339
- <form @submit=${this._handleSubmit}>
340
- <slot @slotchange=${this._handleSlotChange}></slot>
341
-
342
- <div class="form-actions">
343
- ${hasSteps && this._currentStepIndex > 0
344
- ? html`
345
- <st-button
346
- variant="secondary"
347
- type="button"
348
- @click=${this._prevStep}
349
- ?disabled=${this.loading}
350
- >
351
- Back
352
- </st-button>
353
- `
354
- : ""}
355
-
356
- <!-- Type is "button", but click triggers requestSubmit() on the internal form -->
357
- <st-button
358
- color="primary"
359
- type="button"
360
- @click=${this._triggerSubmission}
361
- ?disabled=${this.loading}
362
- >
363
- ${this.loading ? "Submitting..." : isLastStep ? "Submit" : "Continue"}
364
- </st-button>
365
- </div>
366
- </form>
367
- </div>
368
- `;
369
- }
370
- }
371
-
372
- declare global {
373
- interface HTMLElementTagNameMap {
374
- "st-form": StForm;
375
- }
376
- }
1
+ import {LitElement, html, css} from "lit";
2
+ import {customElement, property, state, queryAssignedElements, query} from "lit/decorators.js";
3
+ import {resetStyles} from "../../styles/reset.js";
4
+ import type {StFormStep} from "./form-step.js";
5
+
6
+ export type FormLayout = "vertical" | "horizontal" | "inline";
7
+
8
+ @customElement("st-form")
9
+ export class StForm extends LitElement {
10
+ @property({type: String, reflect: true}) layout: FormLayout = "vertical";
11
+ @property({type: Boolean, reflect: true}) loading = false;
12
+ @property({type: String}) error = "";
13
+
14
+ @state() private _currentStepIndex = 0;
15
+
16
+ @queryAssignedElements({selector: "st-form-step"})
17
+ private _stepElements!: StFormStep[];
18
+
19
+ @query("form") private _formEl!: HTMLFormElement;
20
+
21
+ static styles = [
22
+ resetStyles,
23
+ css`
24
+ :host {
25
+ display: block;
26
+ position: relative;
27
+ }
28
+
29
+ form {
30
+ display: flex;
31
+ gap: var(--st-spacing-md);
32
+ }
33
+
34
+ /* ==========================================================
35
+ Layouts
36
+ ========================================================== */
37
+ :host([layout="vertical"]) form {
38
+ flex-direction: column;
39
+ }
40
+
41
+ :host([layout="horizontal"]) form {
42
+ display: grid;
43
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
44
+ align-items: start;
45
+ }
46
+
47
+ :host([layout="inline"]) form {
48
+ flex-direction: row;
49
+ align-items: flex-end;
50
+ flex-wrap: wrap;
51
+ }
52
+
53
+ .has-steps form {
54
+ display: flex;
55
+ flex-direction: column;
56
+ }
57
+
58
+ /* ==========================================================
59
+ Progress Navigation
60
+ ========================================================== */
61
+ .progress-nav {
62
+ display: flex;
63
+ align-items: center;
64
+ margin-bottom: var(--st-spacing-xl);
65
+ padding-bottom: var(--st-spacing-md);
66
+ overflow-x: auto;
67
+ scrollbar-width: none;
68
+ }
69
+
70
+ .step-indicator {
71
+ display: flex;
72
+ align-items: center;
73
+ gap: var(--st-spacing-sm);
74
+ color: var(--st-color-text-muted);
75
+ font-size: var(--st-font-size-sm);
76
+ font-weight: var(--st-font-weight-medium);
77
+ transition: color 0.2s ease;
78
+ }
79
+
80
+ .step-indicator.active {
81
+ color: var(--st-color-text);
82
+ }
83
+ .step-indicator.completed {
84
+ color: var(--st-color-success);
85
+ }
86
+
87
+ .step-circle {
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ width: 1.75rem;
92
+ height: 1.75rem;
93
+ border-radius: 50%;
94
+ border: 2px solid currentColor;
95
+ font-size: 0.875rem;
96
+ background-color: var(--st-color-bg);
97
+ transition: all 0.2s ease;
98
+ }
99
+
100
+ .step-indicator.active .step-circle {
101
+ background-color: var(--st-color-primary);
102
+ border-color: var(--st-color-primary);
103
+ color: var(--st-color-bg);
104
+ }
105
+
106
+ .step-indicator.completed .step-circle {
107
+ border-color: var(--st-color-success);
108
+ background-color: var(--st-color-success-bg, transparent);
109
+ }
110
+
111
+ .step-connector {
112
+ flex: 1;
113
+ height: 2px;
114
+ min-width: 24px;
115
+ margin: 0 var(--st-spacing-sm);
116
+ background-color: var(--st-color-border);
117
+ transition: background-color 0.2s ease;
118
+ }
119
+
120
+ .step-connector.completed {
121
+ background-color: var(--st-color-success);
122
+ }
123
+
124
+ /* ==========================================================
125
+ Actions & States
126
+ ========================================================== */
127
+ .form-actions {
128
+ display: flex;
129
+ gap: var(--st-spacing-md);
130
+ margin-top: var(--st-spacing-lg);
131
+ justify-content: flex-end;
132
+ }
133
+
134
+ .loading-overlay {
135
+ position: absolute;
136
+ inset: 0;
137
+ background: rgba(var(--st-color-bg-rgb, 255, 255, 255), 0.7);
138
+ backdrop-filter: blur(2px);
139
+ display: flex;
140
+ align-items: center;
141
+ justify-content: center;
142
+ z-index: 10;
143
+ border-radius: inherit;
144
+ color: var(--st-color-text);
145
+ font-weight: var(--st-font-weight-medium);
146
+ gap: var(--st-spacing-sm);
147
+ }
148
+
149
+ @keyframes pulse {
150
+ 0%,
151
+ 100% {
152
+ opacity: 1;
153
+ }
154
+ 50% {
155
+ opacity: 0.5;
156
+ }
157
+ }
158
+
159
+ .loading-overlay st-icon {
160
+ animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
161
+ }
162
+ `,
163
+ ];
164
+
165
+ private _handleSlotChange() {
166
+ if (this._stepElements.length > 0) {
167
+ this._updateStepVisibility();
168
+ }
169
+ }
170
+
171
+ private _updateStepVisibility() {
172
+ this._stepElements.forEach((step, index) => {
173
+ step.active = index === this._currentStepIndex;
174
+ });
175
+ this.requestUpdate();
176
+ }
177
+
178
+ private _validateCurrentContext(): boolean {
179
+ if (this._stepElements.length === 0) {
180
+ return this._formEl.checkValidity();
181
+ }
182
+
183
+ const currentStep = this._stepElements[this._currentStepIndex];
184
+ let isValid = true;
185
+
186
+ // Find all custom and native form inputs assigned to the current step
187
+ const inputs = Array.from(
188
+ currentStep.querySelectorAll<
189
+ HTMLElement & {checkValidity?: () => boolean; reportValidity?: () => void}
190
+ >("*"),
191
+ );
192
+
193
+ for (const input of inputs) {
194
+ if (typeof input.checkValidity === "function") {
195
+ if (!input.checkValidity()) {
196
+ // Focus and report on the first invalid element encountered
197
+ if (isValid && typeof input.reportValidity === "function") {
198
+ input.reportValidity();
199
+ }
200
+ isValid = false;
201
+ }
202
+ }
203
+ }
204
+
205
+ return isValid;
206
+ }
207
+
208
+ private _triggerSubmission() {
209
+ // HTML5 method that triggers form validation and fires the native submit event
210
+ this._formEl.requestSubmit();
211
+ }
212
+
213
+ private _prevStep() {
214
+ if (this._currentStepIndex > 0) {
215
+ this._currentStepIndex--;
216
+ this._updateStepVisibility();
217
+ }
218
+ }
219
+
220
+ private _extractFormData(): FormData {
221
+ const formData = new FormData();
222
+
223
+ // Query all light-DOM children that have a 'name' attribute
224
+ const fields = this.querySelectorAll<
225
+ HTMLElement & {name: string; value: any; checked?: boolean; files?: FileList}
226
+ >("[name]");
227
+
228
+ fields.forEach((field) => {
229
+ // Standard HTML form behavior: ignore disabled fields
230
+ if (field.hasAttribute("disabled")) return;
231
+
232
+ const isToggle =
233
+ field.tagName === "ST-RADIO" ||
234
+ field.tagName === "ST-CHECKBOX" ||
235
+ field.getAttribute("type") === "radio" ||
236
+ field.getAttribute("type") === "checkbox";
237
+
238
+ if (isToggle) {
239
+ // Only append radios/checkboxes if they are actively checked
240
+ if (field.checked) {
241
+ formData.append(field.name, field.value || "on");
242
+ }
243
+ } else if (
244
+ field.tagName === "INPUT" &&
245
+ field.getAttribute("type") === "file" &&
246
+ field.files
247
+ ) {
248
+ // Handle native file uploads
249
+ Array.from(field.files).forEach((file) => formData.append(field.name, file));
250
+ } else if (field.value !== undefined) {
251
+ // Handle st-input, st-select, st-textarea, and standard inputs
252
+ formData.append(field.name, field.value);
253
+ }
254
+ });
255
+
256
+ return formData;
257
+ }
258
+
259
+ private _handleSubmit(e: Event) {
260
+ e.preventDefault();
261
+ e.stopPropagation();
262
+
263
+ if (!this._validateCurrentContext()) return;
264
+
265
+ const hasSteps = this._stepElements.length > 0;
266
+ const isLastStep = hasSteps ? this._currentStepIndex === this._stepElements.length - 1 : true;
267
+
268
+ if (hasSteps && !isLastStep) {
269
+ this._currentStepIndex++;
270
+ this._updateStepVisibility();
271
+ return;
272
+ }
273
+
274
+ // Use our custom extractor to pierce the Shadow DOM boundary
275
+ const formData = this._extractFormData();
276
+
277
+ this.dispatchEvent(
278
+ new CustomEvent("submit", {
279
+ bubbles: true,
280
+ composed: true,
281
+ cancelable: true,
282
+ detail: {formData},
283
+ }),
284
+ );
285
+ }
286
+
287
+ render() {
288
+ const hasSteps = this._stepElements?.length > 0;
289
+ const isLastStep = hasSteps ? this._currentStepIndex === this._stepElements.length - 1 : true;
290
+
291
+ return html`
292
+ <div class="${hasSteps ? "has-steps" : ""}" aria-busy=${this.loading}>
293
+ ${this.loading
294
+ ? html`
295
+ <div class="loading-overlay">
296
+ <st-icon name="cloud-download" color="primary"></st-icon> Processing...
297
+ </div>
298
+ `
299
+ : ""}
300
+ ${hasSteps
301
+ ? html`
302
+ <div class="progress-nav" role="tablist">
303
+ ${this._stepElements.map((step, index) => {
304
+ const isActive = index === this._currentStepIndex;
305
+ const isCompleted = index < this._currentStepIndex;
306
+
307
+ return html`
308
+ <div
309
+ class="step-indicator ${isActive ? "active" : ""} ${isCompleted
310
+ ? "completed"
311
+ : ""}"
312
+ role="tab"
313
+ aria-selected=${isActive}
314
+ >
315
+ <div class="step-circle">
316
+ ${isCompleted
317
+ ? html`<st-icon name="check-circle" size="sm"></st-icon>`
318
+ : html`${index + 1}`}
319
+ </div>
320
+ <span>${step.title || `Step ${index + 1}`}</span>
321
+ </div>
322
+ ${index < this._stepElements.length - 1
323
+ ? html` <div class="step-connector ${isCompleted ? "completed" : ""}"></div> `
324
+ : ""}
325
+ `;
326
+ })}
327
+ </div>
328
+ `
329
+ : ""}
330
+ ${this.error
331
+ ? html`
332
+ <st-alert color="error" style="margin-bottom: var(--st-spacing-lg);">
333
+ <st-icon slot="icon" name="alert-triangle"></st-icon>
334
+ ${this.error}
335
+ </st-alert>
336
+ `
337
+ : ""}
338
+
339
+ <form @submit=${this._handleSubmit}>
340
+ <slot @slotchange=${this._handleSlotChange}></slot>
341
+
342
+ <div class="form-actions">
343
+ ${hasSteps && this._currentStepIndex > 0
344
+ ? html`
345
+ <st-button
346
+ variant="secondary"
347
+ type="button"
348
+ @click=${this._prevStep}
349
+ ?disabled=${this.loading}
350
+ >
351
+ Back
352
+ </st-button>
353
+ `
354
+ : ""}
355
+
356
+ <!-- Type is "button", but click triggers requestSubmit() on the internal form -->
357
+ <st-button
358
+ color="primary"
359
+ type="button"
360
+ @click=${this._triggerSubmission}
361
+ ?disabled=${this.loading}
362
+ >
363
+ ${this.loading ? "Submitting..." : isLastStep ? "Submit" : "Continue"}
364
+ </st-button>
365
+ </div>
366
+ </form>
367
+ </div>
368
+ `;
369
+ }
370
+ }
371
+
372
+ declare global {
373
+ interface HTMLElementTagNameMap {
374
+ "st-form": StForm;
375
+ }
376
+ }