@sondor-technology/sondor-components 1.0.0-1165-initial-setup.11 → 1.0.0

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/package.json +2 -3
  2. package/src/components/alert/alert.ts +100 -100
  3. package/src/components/badge/badge.ts +96 -96
  4. package/src/components/breadcrumb/breadcrumb.ts +98 -98
  5. package/src/components/button/button.ts +281 -281
  6. package/src/components/button-group/button-group.ts +67 -67
  7. package/src/components/card/card.ts +52 -52
  8. package/src/components/checkbox/checkbox.ts +238 -238
  9. package/src/components/code-block/code-block.ts +256 -256
  10. package/src/components/content-card/content-card.ts +133 -133
  11. package/src/components/divider/divider.ts +68 -68
  12. package/src/components/drawer/drawer.ts +217 -217
  13. package/src/components/field/field.ts +30 -30
  14. package/src/components/footer/footer.ts +58 -58
  15. package/src/components/form/form-step.ts +32 -32
  16. package/src/components/form/form.ts +376 -376
  17. package/src/components/grid/grid-row.ts +75 -75
  18. package/src/components/grid/grid.ts +36 -36
  19. package/src/components/icon/icon-paths.ts +124 -124
  20. package/src/components/icon/icon.ts +82 -82
  21. package/src/components/input/input.ts +248 -248
  22. package/src/components/link/link.ts +97 -97
  23. package/src/components/metadata/metadata.ts +66 -66
  24. package/src/components/modal/modal.ts +158 -158
  25. package/src/components/page-layout/page-layout.ts +704 -704
  26. package/src/components/radio/radio.ts +142 -142
  27. package/src/components/radio-group/radio-group.ts +157 -157
  28. package/src/components/router/router.ts +46 -46
  29. package/src/components/select/select.ts +161 -161
  30. package/src/components/skeleton/skeleton.ts +67 -67
  31. package/src/components/switch/switch.ts +181 -181
  32. package/src/components/table/data-table.ts +556 -556
  33. package/src/components/table/table.ts +153 -153
  34. package/src/components/tabs/tab-panel.ts +30 -30
  35. package/src/components/tabs/tab.ts +76 -76
  36. package/src/components/tabs/tabs.ts +88 -88
  37. package/src/components/textarea/textarea.ts +215 -215
  38. package/src/components/theme-toggle/theme-toggle.ts +131 -131
  39. package/src/components/vertical-nav/vertical-nav-group.ts +96 -96
  40. package/src/components/vertical-nav/vertical-nav-item.ts +49 -49
  41. package/src/styles/globals.css +58 -58
  42. package/src/styles/index.css +80 -80
  43. package/src/styles/reset.ts +50 -50
  44. package/src/styles/theme.css +58 -58
  45. package/src/styles/typography.css +25 -25
  46. package/tsconfig.json +15 -15
  47. package/README.md +0 -8
  48. package/dist/components/alert/alert.d.ts +0 -13
  49. package/dist/components/alert/alert.d.ts.map +0 -1
  50. package/dist/components/alert/alert.js +0 -105
  51. package/dist/components/badge/badge.d.ts +0 -13
  52. package/dist/components/badge/badge.d.ts.map +0 -1
  53. package/dist/components/badge/badge.js +0 -101
  54. package/dist/components/breadcrumb/breadcrumb.d.ts +0 -19
  55. package/dist/components/breadcrumb/breadcrumb.d.ts.map +0 -1
  56. package/dist/components/breadcrumb/breadcrumb.js +0 -99
  57. package/dist/components/button/button.d.ts +0 -23
  58. package/dist/components/button/button.d.ts.map +0 -1
  59. package/dist/components/button/button.js +0 -304
  60. package/dist/components/button-group/button-group.d.ts +0 -6
  61. package/dist/components/button-group/button-group.d.ts.map +0 -1
  62. package/dist/components/button-group/button-group.js +0 -74
  63. package/dist/components/card/card.d.ts +0 -11
  64. package/dist/components/card/card.d.ts.map +0 -1
  65. package/dist/components/card/card.js +0 -54
  66. package/dist/components/checkbox/checkbox.d.ts +0 -28
  67. package/dist/components/checkbox/checkbox.d.ts.map +0 -1
  68. package/dist/components/checkbox/checkbox.js +0 -247
  69. package/dist/components/code-block/code-block.d.ts +0 -20
  70. package/dist/components/code-block/code-block.d.ts.map +0 -1
  71. package/dist/components/code-block/code-block.js +0 -260
  72. package/dist/components/content-card/content-card.d.ts +0 -13
  73. package/dist/components/content-card/content-card.d.ts.map +0 -1
  74. package/dist/components/content-card/content-card.js +0 -140
  75. package/dist/components/divider/divider.d.ts +0 -16
  76. package/dist/components/divider/divider.d.ts.map +0 -1
  77. package/dist/components/divider/divider.js +0 -82
  78. package/dist/components/drawer/drawer.d.ts +0 -22
  79. package/dist/components/drawer/drawer.d.ts.map +0 -1
  80. package/dist/components/drawer/drawer.js +0 -226
  81. package/dist/components/field/field.d.ts +0 -11
  82. package/dist/components/field/field.d.ts.map +0 -1
  83. package/dist/components/field/field.js +0 -32
  84. package/dist/components/footer/footer.d.ts +0 -6
  85. package/dist/components/footer/footer.d.ts.map +0 -1
  86. package/dist/components/footer/footer.js +0 -65
  87. package/dist/components/form/form-step.d.ts +0 -13
  88. package/dist/components/form/form-step.d.ts.map +0 -1
  89. package/dist/components/form/form-step.js +0 -41
  90. package/dist/components/form/form.d.ts +0 -25
  91. package/dist/components/form/form.d.ts.map +0 -1
  92. package/dist/components/form/form.js +0 -358
  93. package/dist/components/grid/grid-row.d.ts +0 -17
  94. package/dist/components/grid/grid-row.d.ts.map +0 -1
  95. package/dist/components/grid/grid-row.js +0 -92
  96. package/dist/components/grid/grid.d.ts +0 -13
  97. package/dist/components/grid/grid.d.ts.map +0 -1
  98. package/dist/components/grid/grid.js +0 -42
  99. package/dist/components/icon/icon-paths.d.ts +0 -116
  100. package/dist/components/icon/icon-paths.d.ts.map +0 -1
  101. package/dist/components/icon/icon-paths.js +0 -121
  102. package/dist/components/icon/icon.d.ts +0 -16
  103. package/dist/components/icon/icon.d.ts.map +0 -1
  104. package/dist/components/icon/icon.js +0 -95
  105. package/dist/components/input/input.d.ts +0 -31
  106. package/dist/components/input/input.d.ts.map +0 -1
  107. package/dist/components/input/input.js +0 -268
  108. package/dist/components/link/link.d.ts +0 -16
  109. package/dist/components/link/link.d.ts.map +0 -1
  110. package/dist/components/link/link.js +0 -116
  111. package/dist/components/metadata/metadata.d.ts +0 -18
  112. package/dist/components/metadata/metadata.d.ts.map +0 -1
  113. package/dist/components/metadata/metadata.js +0 -71
  114. package/dist/components/modal/modal.d.ts +0 -24
  115. package/dist/components/modal/modal.d.ts.map +0 -1
  116. package/dist/components/modal/modal.js +0 -163
  117. package/dist/components/page-layout/page-layout.d.ts +0 -46
  118. package/dist/components/page-layout/page-layout.d.ts.map +0 -1
  119. package/dist/components/page-layout/page-layout.js +0 -697
  120. package/dist/components/radio/radio.d.ts +0 -16
  121. package/dist/components/radio/radio.d.ts.map +0 -1
  122. package/dist/components/radio/radio.js +0 -152
  123. package/dist/components/radio-group/radio-group.d.ts +0 -27
  124. package/dist/components/radio-group/radio-group.d.ts.map +0 -1
  125. package/dist/components/radio-group/radio-group.js +0 -161
  126. package/dist/components/router/router.d.ts +0 -7
  127. package/dist/components/router/router.d.ts.map +0 -1
  128. package/dist/components/router/router.js +0 -37
  129. package/dist/components/select/select.d.ts +0 -16
  130. package/dist/components/select/select.d.ts.map +0 -1
  131. package/dist/components/select/select.js +0 -172
  132. package/dist/components/skeleton/skeleton.d.ts +0 -17
  133. package/dist/components/skeleton/skeleton.d.ts.map +0 -1
  134. package/dist/components/skeleton/skeleton.js +0 -78
  135. package/dist/components/switch/switch.d.ts +0 -25
  136. package/dist/components/switch/switch.d.ts.map +0 -1
  137. package/dist/components/switch/switch.js +0 -185
  138. package/dist/components/table/data-table.d.ts +0 -72
  139. package/dist/components/table/data-table.d.ts.map +0 -1
  140. package/dist/components/table/data-table.js +0 -551
  141. package/dist/components/table/table.d.ts +0 -22
  142. package/dist/components/table/table.d.ts.map +0 -1
  143. package/dist/components/table/table.js +0 -167
  144. package/dist/components/tabs/tab-panel.d.ts +0 -8
  145. package/dist/components/tabs/tab-panel.d.ts.map +0 -1
  146. package/dist/components/tabs/tab-panel.js +0 -46
  147. package/dist/components/tabs/tab.d.ts +0 -9
  148. package/dist/components/tabs/tab.d.ts.map +0 -1
  149. package/dist/components/tabs/tab.js +0 -91
  150. package/dist/components/tabs/tabs.d.ts +0 -14
  151. package/dist/components/tabs/tabs.d.ts.map +0 -1
  152. package/dist/components/tabs/tabs.js +0 -85
  153. package/dist/components/textarea/textarea.d.ts +0 -36
  154. package/dist/components/textarea/textarea.d.ts.map +0 -1
  155. package/dist/components/textarea/textarea.js +0 -231
  156. package/dist/components/theme-toggle/theme-toggle.d.ts +0 -15
  157. package/dist/components/theme-toggle/theme-toggle.d.ts.map +0 -1
  158. package/dist/components/theme-toggle/theme-toggle.js +0 -130
  159. package/dist/components/vertical-nav/vertical-nav-group.d.ts +0 -9
  160. package/dist/components/vertical-nav/vertical-nav-group.d.ts.map +0 -1
  161. package/dist/components/vertical-nav/vertical-nav-group.js +0 -111
  162. package/dist/components/vertical-nav/vertical-nav-item.d.ts +0 -8
  163. package/dist/components/vertical-nav/vertical-nav-item.d.ts.map +0 -1
  164. package/dist/components/vertical-nav/vertical-nav-item.js +0 -65
  165. package/dist/styles/index.css +0 -171
  166. package/dist/styles/reset.d.ts +0 -2
  167. package/dist/styles/reset.d.ts.map +0 -1
  168. package/dist/styles/reset.js +0 -50
@@ -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
+ }