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

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 +31 -0
  21. package/dist/components/checkbox/checkbox.d.ts.map +1 -0
  22. package/dist/components/checkbox/checkbox.js +265 -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 +14 -0
  42. package/dist/components/form/form-step.d.ts.map +1 -0
  43. package/dist/components/form/form-step.js +44 -0
  44. package/dist/components/form/form.d.ts +30 -0
  45. package/dist/components/form/form.d.ts.map +1 -0
  46. package/dist/components/form/form.js +456 -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 +17 -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 +35 -0
  69. package/dist/components/modal/modal.d.ts.map +1 -0
  70. package/dist/components/modal/modal.js +251 -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 +31 -0
  81. package/dist/components/router/router.d.ts.map +1 -0
  82. package/dist/components/router/router.js +97 -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 +16 -0
  87. package/dist/components/skeleton/skeleton.d.ts.map +1 -0
  88. package/dist/components/skeleton/skeleton.js +149 -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 +15 -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 +14 -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 +266 -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 +36 -32
  138. package/src/components/form/form.ts +492 -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 +84 -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 +246 -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 +133 -46
  151. package/src/components/select/select.ts +161 -161
  152. package/src/components/skeleton/skeleton.ts +134 -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 +115 -96
  162. package/src/components/vertical-nav/vertical-nav-item.ts +72 -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,492 @@
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, PropertyValues} from "lit";
2
+ import {customElement, property, state, 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
+ @query("form") private _formEl!: HTMLFormElement;
17
+
18
+ private _modalFooterEl: HTMLElement | null = null;
19
+
20
+ private get _steps(): StFormStep[] {
21
+ return Array.from(this.querySelectorAll("st-form-step"));
22
+ }
23
+
24
+ static styles = [
25
+ resetStyles,
26
+ css`
27
+ :host {
28
+ display: block;
29
+ position: relative;
30
+ }
31
+
32
+ form {
33
+ display: flex;
34
+ gap: var(--st-spacing-md);
35
+ }
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
+ .progress-nav {
59
+ display: flex;
60
+ align-items: center;
61
+ margin-bottom: var(--st-spacing-xl);
62
+ padding-bottom: var(--st-spacing-md);
63
+ overflow-x: auto;
64
+ scrollbar-width: none;
65
+ }
66
+
67
+ .step-indicator {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: var(--st-spacing-sm);
71
+ color: var(--st-color-text-muted);
72
+ font-size: var(--st-font-size-sm);
73
+ font-weight: var(--st-font-weight-medium);
74
+ transition: color 0.2s ease;
75
+ }
76
+
77
+ .step-indicator.active {
78
+ color: var(--st-color-text);
79
+ }
80
+ .step-indicator.completed {
81
+ color: var(--st-color-success);
82
+ }
83
+
84
+ .step-circle {
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ width: 1.75rem;
89
+ height: 1.75rem;
90
+ border-radius: 50%;
91
+ border: 2px solid currentColor;
92
+ font-size: 0.875rem;
93
+ background-color: var(--st-color-bg);
94
+ transition: all 0.2s ease;
95
+ }
96
+
97
+ .step-indicator.active .step-circle {
98
+ background-color: var(--st-color-primary);
99
+ border-color: var(--st-color-primary);
100
+ color: var(--st-color-bg);
101
+ }
102
+
103
+ .step-indicator.completed .step-circle {
104
+ border-color: var(--st-color-success);
105
+ background-color: var(--st-color-success-bg, transparent);
106
+ }
107
+
108
+ .step-connector {
109
+ flex: 1;
110
+ height: 2px;
111
+ min-width: 24px;
112
+ margin: 0 var(--st-spacing-sm);
113
+ background-color: var(--st-color-border);
114
+ transition: background-color 0.2s ease;
115
+ }
116
+
117
+ .step-connector.completed {
118
+ background-color: var(--st-color-success);
119
+ }
120
+
121
+ .form-actions {
122
+ display: flex;
123
+ gap: var(--st-spacing-md);
124
+ margin-top: var(--st-spacing-lg);
125
+ justify-content: flex-end;
126
+ }
127
+
128
+ .loading-overlay {
129
+ position: absolute;
130
+ inset: 0;
131
+ background: rgba(var(--st-color-bg-rgb, 255, 255, 255), 0.7);
132
+ backdrop-filter: blur(2px);
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ z-index: 10;
137
+ border-radius: inherit;
138
+ color: var(--st-color-text);
139
+ font-weight: var(--st-font-weight-medium);
140
+ gap: var(--st-spacing-sm);
141
+ }
142
+
143
+ @keyframes pulse {
144
+ 0%,
145
+ 100% {
146
+ opacity: 1;
147
+ }
148
+ 50% {
149
+ opacity: 0.5;
150
+ }
151
+ }
152
+
153
+ .loading-overlay st-icon {
154
+ animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
155
+ }
156
+ `,
157
+ ];
158
+
159
+ override connectedCallback() {
160
+ super.connectedCallback();
161
+ this.updateComplete.then(() => {
162
+ this._updateStepVisibility();
163
+ this._updateModalFooter();
164
+ });
165
+ }
166
+
167
+ override disconnectedCallback() {
168
+ super.disconnectedCallback();
169
+ if (this._modalFooterEl) {
170
+ this._modalFooterEl.remove();
171
+ this._modalFooterEl = null;
172
+ }
173
+ }
174
+
175
+ override updated(changedProperties: PropertyValues<this>) {
176
+ super.updated(changedProperties);
177
+ this._updateModalFooter();
178
+ }
179
+
180
+ private _updateModalFooter() {
181
+ const modal = this.closest("st-modal");
182
+ if (modal) {
183
+ if (!this._modalFooterEl) {
184
+ this._modalFooterEl = document.createElement("div");
185
+ this._modalFooterEl.setAttribute("slot", "footer");
186
+ this._modalFooterEl.style.display = "flex";
187
+ this._modalFooterEl.style.gap = "var(--st-spacing-md)";
188
+ this._modalFooterEl.style.width = "100%";
189
+ this._modalFooterEl.style.justifyContent = "flex-end";
190
+ this._modalFooterEl.style.alignItems = "center";
191
+ modal.appendChild(this._modalFooterEl);
192
+ }
193
+
194
+ const steps = this._steps;
195
+ const hasSteps = steps.length > 0;
196
+ const isLastStep = hasSteps ? this._currentStepIndex === steps.length - 1 : true;
197
+
198
+ // Imperative DOM management prevents event listener loss and re-rendering conflicts
199
+ this._modalFooterEl.innerHTML = "";
200
+
201
+ if (hasSteps && this._currentStepIndex > 0) {
202
+ const backBtn = document.createElement("st-button");
203
+ backBtn.setAttribute("variant", "secondary");
204
+ backBtn.setAttribute("type", "button");
205
+ if (this.loading) backBtn.setAttribute("disabled", "");
206
+ backBtn.textContent = "Back";
207
+ backBtn.addEventListener("click", (e) => {
208
+ e.preventDefault();
209
+ this._prevStep();
210
+ });
211
+ this._modalFooterEl.appendChild(backBtn);
212
+ }
213
+
214
+ const submitBtn = document.createElement("st-button");
215
+ submitBtn.setAttribute("color", "primary");
216
+ submitBtn.setAttribute("type", "button");
217
+ if (this.loading) submitBtn.setAttribute("disabled", "");
218
+ submitBtn.textContent = this.loading ? "Submitting..." : (isLastStep ? "Submit" : "Continue");
219
+ submitBtn.addEventListener("click", (e) => {
220
+ e.preventDefault();
221
+ this._triggerSubmission();
222
+ });
223
+ this._modalFooterEl.appendChild(submitBtn);
224
+
225
+ } else if (this._modalFooterEl) {
226
+ this._modalFooterEl.remove();
227
+ this._modalFooterEl = null;
228
+ }
229
+ }
230
+
231
+ private _handleSlotChange() {
232
+ this._updateStepVisibility();
233
+ this._updateModalFooter();
234
+ }
235
+
236
+ private _updateStepVisibility() {
237
+ const steps = this._steps;
238
+ steps.forEach((step, index) => {
239
+ step.active = index === this._currentStepIndex;
240
+ });
241
+ this.requestUpdate();
242
+ }
243
+
244
+ private async _validateCurrentContext(): Promise<boolean> {
245
+ const steps = this._steps;
246
+ const scope: ParentNode = steps.length > 0
247
+ ? steps[this._currentStepIndex]
248
+ : this;
249
+
250
+ let isValid = true;
251
+
252
+ const inputs = Array.from(
253
+ scope.querySelectorAll<
254
+ HTMLElement & {
255
+ checkValidity?: () => boolean;
256
+ reportValidity?: () => boolean;
257
+ required?: boolean;
258
+ value?: any;
259
+ checked?: boolean;
260
+ invalid?: boolean;
261
+ }
262
+ >("st-input, st-select, st-textarea, st-checkbox, st-radio, input, select, textarea"),
263
+ );
264
+
265
+ for (const input of inputs) {
266
+ if (input.hasAttribute("disabled") || (input as any).disabled) continue;
267
+
268
+ let fieldValid = true;
269
+
270
+ if (typeof input.checkValidity === "function") {
271
+ fieldValid = input.checkValidity();
272
+ } else {
273
+ const isRequired = input.hasAttribute("required") || input.required;
274
+ if (isRequired) {
275
+ const isToggle = input.tagName === "ST-CHECKBOX" || input.tagName === "ST-RADIO";
276
+ const val = isToggle ? input.checked : input.value;
277
+ if (val === undefined || val === null || val === "" || val === false) {
278
+ fieldValid = false;
279
+ }
280
+ }
281
+ }
282
+
283
+ if (!fieldValid) {
284
+ if (typeof input.reportValidity === "function") {
285
+ input.reportValidity();
286
+ } else if ("invalid" in input) {
287
+ input.invalid = true;
288
+ }
289
+ if (isValid && typeof (input as any).focus === "function") {
290
+ (input as any).focus();
291
+ }
292
+ isValid = false;
293
+ }
294
+ }
295
+
296
+ if (steps.length === 0 && this._formEl) {
297
+ if (!this._formEl.checkValidity()) {
298
+ isValid = false;
299
+ }
300
+ }
301
+
302
+ if (isValid && steps.length > 0) {
303
+ const currentStep = steps[this._currentStepIndex] as StFormStep & {
304
+ validate?: () => Promise<boolean> | boolean;
305
+ };
306
+
307
+ if (typeof currentStep.validate === "function") {
308
+ try {
309
+ this.loading = true;
310
+ const stepPassed = await currentStep.validate();
311
+ if (!stepPassed) {
312
+ isValid = false;
313
+ }
314
+ } catch {
315
+ isValid = false;
316
+ } finally {
317
+ this.loading = false;
318
+ }
319
+ }
320
+ }
321
+
322
+ return isValid;
323
+ }
324
+
325
+ private _triggerSubmission() {
326
+ this._formEl.requestSubmit();
327
+ }
328
+
329
+ private _prevStep() {
330
+ if (this._currentStepIndex > 0) {
331
+ this._currentStepIndex--;
332
+ this._updateStepVisibility();
333
+ }
334
+ }
335
+
336
+ private _extractFormData(): FormData {
337
+ const formData = new FormData();
338
+
339
+ const fields = this.querySelectorAll<
340
+ HTMLElement & {name: string; value: any; checked?: boolean; files?: FileList}
341
+ >("[name]");
342
+
343
+ fields.forEach((field) => {
344
+ if (field.hasAttribute("disabled")) return;
345
+
346
+ const isToggle =
347
+ field.tagName === "ST-RADIO" ||
348
+ field.tagName === "ST-CHECKBOX" ||
349
+ field.getAttribute("type") === "radio" ||
350
+ field.getAttribute("type") === "checkbox";
351
+
352
+ if (isToggle) {
353
+ if (field.checked) {
354
+ formData.append(field.name, field.value || "on");
355
+ }
356
+ } else if (
357
+ field.tagName === "INPUT" &&
358
+ field.getAttribute("type") === "file" &&
359
+ field.files
360
+ ) {
361
+ Array.from(field.files).forEach((file) => formData.append(field.name, file));
362
+ } else if (field.value !== undefined) {
363
+ formData.append(field.name, field.value);
364
+ }
365
+ });
366
+
367
+ return formData;
368
+ }
369
+
370
+ private async _handleSubmit(e: Event) {
371
+ e.preventDefault();
372
+ e.stopPropagation();
373
+
374
+ const isValid = await this._validateCurrentContext();
375
+ if (!isValid) return;
376
+
377
+ const steps = this._steps;
378
+ const hasSteps = steps.length > 0;
379
+ const isLastStep = hasSteps ? this._currentStepIndex === steps.length - 1 : true;
380
+
381
+ if (hasSteps && !isLastStep) {
382
+ this._currentStepIndex++;
383
+ this._updateStepVisibility();
384
+ return;
385
+ }
386
+
387
+ const formData = this._extractFormData();
388
+
389
+ this.dispatchEvent(
390
+ new CustomEvent("submit", {
391
+ bubbles: true,
392
+ composed: true,
393
+ cancelable: true,
394
+ detail: {formData},
395
+ }),
396
+ );
397
+ }
398
+
399
+ render() {
400
+ const steps = this._steps;
401
+ const hasSteps = steps.length > 0;
402
+ const isInModal = !!this.closest("st-modal");
403
+
404
+ return html`
405
+ <div class="${hasSteps ? "has-steps" : ""}" aria-busy=${this.loading}>
406
+ ${this.loading
407
+ ? html`
408
+ <div class="loading-overlay">
409
+ <st-icon name="cloud-download" color="primary"></st-icon> Processing...
410
+ </div>
411
+ `
412
+ : ""}
413
+ ${hasSteps
414
+ ? html`
415
+ <div class="progress-nav" role="tablist">
416
+ ${steps.map((step, index) => {
417
+ const isActive = index === this._currentStepIndex;
418
+ const isCompleted = index < this._currentStepIndex;
419
+
420
+ return html`
421
+ <div
422
+ class="step-indicator ${isActive ? "active" : ""} ${isCompleted
423
+ ? "completed"
424
+ : ""}"
425
+ role="tab"
426
+ aria-selected=${isActive}
427
+ >
428
+ <div class="step-circle">
429
+ ${isCompleted
430
+ ? html`<st-icon name="check-circle" size="sm"></st-icon>`
431
+ : html`${index + 1}`}
432
+ </div>
433
+ <span>${step.title || `Step ${index + 1}`}</span>
434
+ </div>
435
+ ${index < steps.length - 1
436
+ ? html` <div class="step-connector ${isCompleted ? "completed" : ""}"></div> `
437
+ : ""}
438
+ `;
439
+ })}
440
+ </div>
441
+ `
442
+ : ""}
443
+ ${this.error
444
+ ? html`
445
+ <st-alert color="error" style="margin-bottom: var(--st-spacing-lg);">
446
+ <st-icon slot="icon" name="alert-triangle"></st-icon>
447
+ ${this.error}
448
+ </st-alert>
449
+ `
450
+ : ""}
451
+
452
+ <form @submit=${this._handleSubmit}>
453
+ <slot @slotchange=${this._handleSlotChange}></slot>
454
+
455
+ ${!isInModal
456
+ ? html`
457
+ <div class="form-actions">
458
+ ${hasSteps && this._currentStepIndex > 0
459
+ ? html`
460
+ <st-button
461
+ variant="secondary"
462
+ type="button"
463
+ @click=${this._prevStep}
464
+ ?disabled=${this.loading}
465
+ >
466
+ Back
467
+ </st-button>
468
+ `
469
+ : ""}
470
+
471
+ <st-button
472
+ color="primary"
473
+ type="button"
474
+ @click=${this._triggerSubmission}
475
+ ?disabled=${this.loading}
476
+ >
477
+ ${this.loading ? "Submitting..." : hasSteps && this._currentStepIndex === steps.length - 1 ? "Submit" : hasSteps ? "Continue" : "Submit"}
478
+ </st-button>
479
+ </div>
480
+ `
481
+ : ""}
482
+ </form>
483
+ </div>
484
+ `;
485
+ }
486
+ }
487
+
488
+ declare global {
489
+ interface HTMLElementTagNameMap {
490
+ "st-form": StForm;
491
+ }
492
+ }