@sondor-technology/sondor-components 1.0.0-1165-initial-setup.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 +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 +116 -0
  54. package/dist/components/icon/icon-paths.d.ts.map +1 -0
  55. package/dist/components/icon/icon-paths.js +121 -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 +9 -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 +111 -0
  116. package/dist/components/vertical-nav/vertical-nav-item.d.ts +8 -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 +65 -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 +192 -0
  124. package/src/components/alert/alert.ts +101 -0
  125. package/src/components/badge/badge.ts +97 -0
  126. package/src/components/breadcrumb/breadcrumb.ts +99 -0
  127. package/src/components/button/button.ts +281 -0
  128. package/src/components/button-group/button-group.ts +67 -0
  129. package/src/components/card/card.ts +53 -0
  130. package/src/components/checkbox/checkbox.ts +238 -0
  131. package/src/components/code-block/code-block.ts +257 -0
  132. package/src/components/content-card/content-card.ts +134 -0
  133. package/src/components/divider/divider.ts +69 -0
  134. package/src/components/drawer/drawer.ts +218 -0
  135. package/src/components/field/field.ts +31 -0
  136. package/src/components/footer/footer.ts +58 -0
  137. package/src/components/form/form-step.ts +32 -0
  138. package/src/components/form/form.ts +376 -0
  139. package/src/components/grid/grid-row.ts +76 -0
  140. package/src/components/grid/grid.ts +37 -0
  141. package/src/components/icon/icon-paths.ts +124 -0
  142. package/src/components/icon/icon.ts +83 -0
  143. package/src/components/input/input.ts +248 -0
  144. package/src/components/link/link.ts +98 -0
  145. package/src/components/metadata/metadata.ts +67 -0
  146. package/src/components/modal/modal.ts +159 -0
  147. package/src/components/page-layout/page-layout.ts +705 -0
  148. package/src/components/radio/radio.ts +142 -0
  149. package/src/components/radio-group/radio-group.ts +157 -0
  150. package/src/components/router/router.ts +47 -0
  151. package/src/components/select/select.ts +162 -0
  152. package/src/components/skeleton/skeleton.ts +68 -0
  153. package/src/components/switch/switch.ts +181 -0
  154. package/src/components/table/data-table.ts +557 -0
  155. package/src/components/table/table.ts +154 -0
  156. package/src/components/tabs/tab-panel.ts +31 -0
  157. package/src/components/tabs/tab.ts +77 -0
  158. package/src/components/tabs/tabs.ts +88 -0
  159. package/src/components/textarea/textarea.ts +215 -0
  160. package/src/components/theme-toggle/theme-toggle.ts +131 -0
  161. package/src/components/vertical-nav/vertical-nav-group.ts +97 -0
  162. package/src/components/vertical-nav/vertical-nav-item.ts +50 -0
  163. package/src/styles/globals.css +58 -0
  164. package/src/styles/index.css +80 -0
  165. package/src/styles/reset.ts +51 -0
  166. package/src/styles/theme.css +59 -0
  167. package/src/styles/typography.css +26 -0
  168. package/tsconfig.json +16 -0
@@ -0,0 +1,218 @@
1
+ import { LitElement, html, css, nothing } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { resetStyles } from '../../styles/reset.js';
4
+
5
+ export type DrawerPlacement = 'left' | 'right';
6
+
7
+ @customElement('st-drawer')
8
+ export class StDrawer extends LitElement {
9
+ @property({ type: Boolean, reflect: true }) open = false;
10
+ @property({ type: String, reflect: true }) placement: DrawerPlacement = 'left';
11
+ @property({ type: Boolean, attribute: 'mobile-only', reflect: true }) mobileOnly = false;
12
+
13
+ private _touchStartX = 0;
14
+ private _currentTranslateX = 0;
15
+ private _isDragging = false;
16
+
17
+ static styles = [
18
+ resetStyles,
19
+ css`
20
+ :host {
21
+ display: contents;
22
+ }
23
+
24
+ :host([mobile-only]) {
25
+ display: block;
26
+ }
27
+ @media (min-width: 768px) {
28
+ :host([mobile-only]) {
29
+ display: none !important;
30
+ }
31
+ }
32
+
33
+ .overlay {
34
+ position: fixed;
35
+ top: 0;
36
+ left: 0;
37
+ width: 100vw;
38
+ height: 100vh;
39
+ background-color: rgba(0, 0, 0, 0.5);
40
+ backdrop-filter: blur(2px);
41
+ z-index: 1000;
42
+ display: flex;
43
+ opacity: 0;
44
+ pointer-events: none;
45
+ transition: opacity 0.25s ease-in-out;
46
+ }
47
+
48
+ :host([open]) .overlay {
49
+ opacity: 1;
50
+ pointer-events: auto;
51
+ }
52
+
53
+ .panel {
54
+ position: fixed;
55
+ top: 0;
56
+ bottom: 0;
57
+ width: auto;
58
+ max-width: 90vw;
59
+ background-color: var(--st-color-surface, #ffffff);
60
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
61
+ display: flex;
62
+ flex-direction: column;
63
+ z-index: 1001;
64
+ overflow-y: auto;
65
+ transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
66
+ touch-action: pan-y;
67
+ }
68
+
69
+ /* Slide from Left */
70
+ :host([placement="left"]) .panel {
71
+ left: 0;
72
+ transform: translateX(-100%);
73
+ }
74
+ :host([placement="left"][open]) .panel {
75
+ transform: translateX(0);
76
+ }
77
+
78
+ /* Slide from Right */
79
+ :host([placement="right"]) .panel {
80
+ right: 0;
81
+ transform: translateX(100%);
82
+ }
83
+ :host([placement="right"][open]) .panel {
84
+ transform: translateX(0);
85
+ }
86
+
87
+ .header {
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: space-between;
91
+ padding: var(--st-spacing-md) var(--st-spacing-lg);
92
+ border-bottom: 1px solid var(--st-color-border, #e2e8f0);
93
+ }
94
+
95
+ .title {
96
+ font-weight: var(--st-font-weight-bold);
97
+ font-size: var(--st-font-size-sm);
98
+ color: var(--st-color-text);
99
+ text-transform: uppercase;
100
+ letter-spacing: 0.05em;
101
+ }
102
+
103
+ .close-btn {
104
+ background: transparent;
105
+ border: none;
106
+ cursor: pointer;
107
+ color: var(--st-color-text-muted);
108
+ padding: 0.25rem;
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ border-radius: var(--st-radius-sm, 0.375rem);
113
+ }
114
+
115
+ .close-btn:hover {
116
+ color: var(--st-color-text);
117
+ background-color: var(--st-color-surface-muted, #f1f5f9);
118
+ }
119
+
120
+ .close-btn svg {
121
+ width: 1.25rem;
122
+ height: 1.25rem;
123
+ stroke: currentColor;
124
+ stroke-width: 2;
125
+ stroke-linecap: round;
126
+ stroke-linejoin: round;
127
+ fill: none;
128
+ }
129
+
130
+ .body {
131
+ padding: var(--st-spacing-lg);
132
+ flex: 1;
133
+ }
134
+ `
135
+ ];
136
+
137
+ private _close() {
138
+ this.open = false;
139
+ const panel = this.shadowRoot?.querySelector('.panel') as HTMLElement;
140
+ if (panel) panel.style.transform = '';
141
+ this.dispatchEvent(new CustomEvent('st-close', { bubbles: true, composed: true }));
142
+ }
143
+
144
+ private _handleTouchStart(e: TouchEvent) {
145
+ this._touchStartX = e.touches[0].clientX;
146
+ this._isDragging = true;
147
+ const panel = this.shadowRoot?.querySelector('.panel') as HTMLElement;
148
+ if (panel) panel.style.transition = 'none';
149
+ }
150
+
151
+ private _handleTouchMove(e: TouchEvent) {
152
+ if (!this._isDragging) return;
153
+ const currentX = e.touches[0].clientX;
154
+ const diff = currentX - this._touchStartX;
155
+ const panel = this.shadowRoot?.querySelector('.panel') as HTMLElement;
156
+
157
+ if (!panel) return;
158
+
159
+ if (this.placement === 'left' && diff < 0) {
160
+ this._currentTranslateX = diff;
161
+ panel.style.transform = `translateX(${diff}px)`;
162
+ } else if (this.placement === 'right' && diff > 0) {
163
+ this._currentTranslateX = diff;
164
+ panel.style.transform = `translateX(${diff}px)`;
165
+ }
166
+ }
167
+
168
+ private _handleTouchEnd() {
169
+ if (!this._isDragging) return;
170
+ this._isDragging = false;
171
+ const panel = this.shadowRoot?.querySelector('.panel') as HTMLElement;
172
+ if (panel) panel.style.transition = '';
173
+
174
+ const threshold = 70;
175
+ if (
176
+ (this.placement === 'left' && this._currentTranslateX < -threshold) ||
177
+ (this.placement === 'right' && this._currentTranslateX > threshold)
178
+ ) {
179
+ this._close();
180
+ } else if (panel) {
181
+ panel.style.transform = '';
182
+ }
183
+ this._currentTranslateX = 0;
184
+ }
185
+
186
+ render() {
187
+ if (!this.open) return nothing;
188
+
189
+ return html`
190
+ <div class="overlay" @click=${this._close}></div>
191
+ <div
192
+ class="panel"
193
+ role="dialog"
194
+ aria-modal="true"
195
+ @click=${(e: Event) => e.stopPropagation()}
196
+ @touchstart=${this._handleTouchStart}
197
+ @touchmove=${this._handleTouchMove}
198
+ @touchend=${this._handleTouchEnd}
199
+ >
200
+ <div class="header">
201
+ <span class="title"><slot name="header">Menu</slot></span>
202
+ <button class="close-btn" @click=${this._close} aria-label="Close Drawer">
203
+ <svg viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"></path></svg>
204
+ </button>
205
+ </div>
206
+ <div class="body">
207
+ <slot></slot>
208
+ </div>
209
+ </div>
210
+ `;
211
+ }
212
+ }
213
+
214
+ declare global {
215
+ interface HTMLElementTagNameMap {
216
+ 'st-drawer': StDrawer;
217
+ }
218
+ }
@@ -0,0 +1,31 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, } from 'lit/decorators.js';
3
+ import { resetStyles } from '../../styles/reset';
4
+
5
+ @customElement('st-field')
6
+ export class StField extends LitElement {
7
+ static styles = [
8
+ resetStyles,
9
+ css`
10
+ :host {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: var(--st-spacing-xs, 0.25rem);
14
+ width: 100%;
15
+ margin-bottom: var(--st-spacing-md, 0.25rem);
16
+ }
17
+ `
18
+ ];
19
+
20
+ render() {
21
+ return html`
22
+ <slot></slot>
23
+ `;
24
+ }
25
+ }
26
+
27
+ declare global {
28
+ interface HTMLElementTagNameMap {
29
+ 'st-field': StField;
30
+ }
31
+ }
@@ -0,0 +1,58 @@
1
+ import {LitElement, html, css} from "lit";
2
+ import {customElement} from "lit/decorators.js";
3
+ import {resetStyles} from "../../styles/reset";
4
+
5
+ @customElement("st-footer")
6
+ export class StFooter extends LitElement {
7
+ static styles = [
8
+ resetStyles,
9
+ css`
10
+ :host {
11
+ display: block;
12
+ border-top: 1px solid var(--st-color-border);
13
+ background-color: var(--st-color-surface);
14
+ padding: var(--st-spacing-lg, 2rem) 0;
15
+ margin-top: auto; /* Pushes footer to the bottom of the page */
16
+ }
17
+
18
+ footer {
19
+ display: flex;
20
+ flex-direction: column;
21
+ align-items: center;
22
+ gap: var(--st-spacing-md, 1rem);
23
+ max-width: 1200px;
24
+ margin: 0 auto;
25
+ padding: 0 var(--st-spacing-md, 1rem);
26
+ color: var(--st-color-text-muted);
27
+ font-size: 0.875rem;
28
+ }
29
+
30
+ /* Wrapper for footer links */
31
+ .links {
32
+ display: flex;
33
+ gap: var(--st-spacing-md, 1rem);
34
+ }
35
+
36
+ ::slotted(a) {
37
+ color: var(--st-color-text-muted);
38
+ text-decoration: none;
39
+ transition: color 0.2s;
40
+ }
41
+
42
+ ::slotted(a:hover) {
43
+ color: var(--st-color-primary);
44
+ }
45
+ `,
46
+ ];
47
+
48
+ render() {
49
+ return html`
50
+ <footer>
51
+ <slot></slot>
52
+ <div class="links">
53
+ <span>&copy; ${new Date().getFullYear()} Sondor.</span>
54
+ </div>
55
+ </footer>
56
+ `;
57
+ }
58
+ }
@@ -0,0 +1,32 @@
1
+ import {LitElement, html, css} from "lit";
2
+ import {customElement, property} from "lit/decorators.js";
3
+ import {resetStyles} from "../../styles/reset.js";
4
+
5
+ @customElement("st-form-step")
6
+ export class StFormStep extends LitElement {
7
+ @property({type: String}) title = "";
8
+ @property({type: Boolean, reflect: true}) active = false;
9
+
10
+ static styles = [
11
+ resetStyles,
12
+ css`
13
+ :host {
14
+ display: block;
15
+ }
16
+ /* Hide inactive steps completely from the layout and accessibility tree */
17
+ :host(:not([active])) {
18
+ display: none;
19
+ }
20
+ `,
21
+ ];
22
+
23
+ render() {
24
+ return html`<slot></slot>`;
25
+ }
26
+ }
27
+
28
+ declare global {
29
+ interface HTMLElementTagNameMap {
30
+ "st-form-step": StFormStep;
31
+ }
32
+ }
@@ -0,0 +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
+ }