@sondor-technology/sondor-components 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/dist/components/alert/alert.constants.d.ts +3 -0
  2. package/dist/components/alert/alert.constants.d.ts.map +1 -0
  3. package/dist/components/alert/alert.constants.js +2 -0
  4. package/dist/components/alert/alert.constants.js.map +1 -0
  5. package/dist/components/alert/alert.d.ts +4 -3
  6. package/dist/components/alert/alert.d.ts.map +1 -1
  7. package/dist/components/alert/alert.js +21 -11
  8. package/dist/components/alert/alert.js.map +1 -0
  9. package/dist/components/badge/badge.constants.d.ts +3 -0
  10. package/dist/components/badge/badge.constants.d.ts.map +1 -0
  11. package/dist/components/badge/badge.constants.js +2 -0
  12. package/dist/components/badge/badge.constants.js.map +1 -0
  13. package/dist/components/badge/badge.d.ts +1 -1
  14. package/dist/components/badge/badge.d.ts.map +1 -1
  15. package/dist/components/badge/badge.js +3 -2
  16. package/dist/components/badge/badge.js.map +1 -0
  17. package/dist/components/breadcrumb/breadcrumb.d.ts.map +1 -1
  18. package/dist/components/breadcrumb/breadcrumb.js +2 -1
  19. package/dist/components/breadcrumb/breadcrumb.js.map +1 -0
  20. package/dist/components/button/button.constants.d.ts +7 -0
  21. package/dist/components/button/button.constants.d.ts.map +1 -0
  22. package/dist/components/button/button.constants.js +4 -0
  23. package/dist/components/button/button.constants.js.map +1 -0
  24. package/dist/components/button/button.d.ts +8 -4
  25. package/dist/components/button/button.d.ts.map +1 -1
  26. package/dist/components/button/button.js +181 -137
  27. package/dist/components/button/button.js.map +1 -0
  28. package/dist/components/button-group/button-group.d.ts +6 -0
  29. package/dist/components/button-group/button-group.d.ts.map +1 -1
  30. package/dist/components/button-group/button-group.js +17 -21
  31. package/dist/components/button-group/button-group.js.map +1 -0
  32. package/dist/components/checkbox/checkbox.constants.d.ts +3 -0
  33. package/dist/components/checkbox/checkbox.constants.d.ts.map +1 -0
  34. package/dist/components/checkbox/checkbox.constants.js +2 -0
  35. package/dist/components/checkbox/checkbox.constants.js.map +1 -0
  36. package/dist/components/checkbox/checkbox.d.ts +5 -1
  37. package/dist/components/checkbox/checkbox.d.ts.map +1 -1
  38. package/dist/components/checkbox/checkbox.js +21 -5
  39. package/dist/components/checkbox/checkbox.js.map +1 -0
  40. package/dist/components/code-block/code-block.d.ts +12 -7
  41. package/dist/components/code-block/code-block.d.ts.map +1 -1
  42. package/dist/components/code-block/code-block.js +130 -66
  43. package/dist/components/code-block/code-block.js.map +1 -0
  44. package/dist/components/content-card/content-card.d.ts.map +1 -1
  45. package/dist/components/content-card/content-card.js +10 -15
  46. package/dist/components/content-card/content-card.js.map +1 -0
  47. package/dist/components/divider/divider.constants.d.ts +5 -0
  48. package/dist/components/divider/divider.constants.d.ts.map +1 -0
  49. package/dist/components/divider/divider.constants.js +3 -0
  50. package/dist/components/divider/divider.constants.js.map +1 -0
  51. package/dist/components/divider/divider.d.ts +4 -2
  52. package/dist/components/divider/divider.d.ts.map +1 -1
  53. package/dist/components/divider/divider.js +18 -9
  54. package/dist/components/divider/divider.js.map +1 -0
  55. package/dist/components/drawer/drawer.d.ts +7 -4
  56. package/dist/components/drawer/drawer.d.ts.map +1 -1
  57. package/dist/components/drawer/drawer.js +73 -30
  58. package/dist/components/drawer/drawer.js.map +1 -0
  59. package/dist/components/field/field.d.ts +2 -2
  60. package/dist/components/field/field.d.ts.map +1 -1
  61. package/dist/components/field/field.js +8 -9
  62. package/dist/components/field/field.js.map +1 -0
  63. package/dist/components/footer/footer.d.ts +8 -1
  64. package/dist/components/footer/footer.d.ts.map +1 -1
  65. package/dist/components/footer/footer.js +42 -12
  66. package/dist/components/footer/footer.js.map +1 -0
  67. package/dist/components/form/form-step.js +1 -0
  68. package/dist/components/form/form-step.js.map +1 -0
  69. package/dist/components/form/form.d.ts +3 -0
  70. package/dist/components/form/form.d.ts.map +1 -1
  71. package/dist/components/form/form.js +107 -67
  72. package/dist/components/form/form.js.map +1 -0
  73. package/dist/components/grid/grid-row.d.ts +3 -3
  74. package/dist/components/grid/grid-row.d.ts.map +1 -1
  75. package/dist/components/grid/grid-row.js +36 -22
  76. package/dist/components/grid/grid-row.js.map +1 -0
  77. package/dist/components/grid/grid.d.ts +2 -2
  78. package/dist/components/grid/grid.d.ts.map +1 -1
  79. package/dist/components/grid/grid.js +5 -2
  80. package/dist/components/grid/grid.js.map +1 -0
  81. package/dist/components/icon/icon-paths.js +1 -0
  82. package/dist/components/icon/icon-paths.js.map +1 -0
  83. package/dist/components/icon/icon.js +1 -0
  84. package/dist/components/icon/icon.js.map +1 -0
  85. package/dist/components/input/input.d.ts +14 -0
  86. package/dist/components/input/input.d.ts.map +1 -1
  87. package/dist/components/input/input.js +30 -37
  88. package/dist/components/input/input.js.map +1 -0
  89. package/dist/components/link/link.d.ts +6 -6
  90. package/dist/components/link/link.d.ts.map +1 -1
  91. package/dist/components/link/link.js +42 -19
  92. package/dist/components/link/link.js.map +1 -0
  93. package/dist/components/metadata/metadata.d.ts +3 -3
  94. package/dist/components/metadata/metadata.d.ts.map +1 -1
  95. package/dist/components/metadata/metadata.js +23 -20
  96. package/dist/components/metadata/metadata.js.map +1 -0
  97. package/dist/components/modal/modal.d.ts +4 -16
  98. package/dist/components/modal/modal.d.ts.map +1 -1
  99. package/dist/components/modal/modal.js +99 -62
  100. package/dist/components/modal/modal.js.map +1 -0
  101. package/dist/components/page-layout/page-layout.d.ts +4 -3
  102. package/dist/components/page-layout/page-layout.d.ts.map +1 -1
  103. package/dist/components/page-layout/page-layout.js +96 -69
  104. package/dist/components/page-layout/page-layout.js.map +1 -0
  105. package/dist/components/radio/radio.d.ts +8 -0
  106. package/dist/components/radio/radio.d.ts.map +1 -1
  107. package/dist/components/radio/radio.js +8 -12
  108. package/dist/components/radio/radio.js.map +1 -0
  109. package/dist/components/radio-group/radio-group.d.ts +6 -1
  110. package/dist/components/radio-group/radio-group.d.ts.map +1 -1
  111. package/dist/components/radio-group/radio-group.js +44 -18
  112. package/dist/components/radio-group/radio-group.js.map +1 -0
  113. package/dist/components/router/router.d.ts +5 -7
  114. package/dist/components/router/router.d.ts.map +1 -1
  115. package/dist/components/router/router.js +54 -38
  116. package/dist/components/router/router.js.map +1 -0
  117. package/dist/components/select/select.d.ts +41 -7
  118. package/dist/components/select/select.d.ts.map +1 -1
  119. package/dist/components/select/select.js +467 -150
  120. package/dist/components/select/select.js.map +1 -0
  121. package/dist/components/skeleton/skeleton.d.ts +3 -2
  122. package/dist/components/skeleton/skeleton.d.ts.map +1 -1
  123. package/dist/components/skeleton/skeleton.js +10 -13
  124. package/dist/components/skeleton/skeleton.js.map +1 -0
  125. package/dist/components/switch/switch.d.ts +12 -0
  126. package/dist/components/switch/switch.d.ts.map +1 -1
  127. package/dist/components/switch/switch.js +28 -22
  128. package/dist/components/switch/switch.js.map +1 -0
  129. package/dist/components/table/data-table.d.ts +3 -25
  130. package/dist/components/table/data-table.d.ts.map +1 -1
  131. package/dist/components/table/data-table.js +211 -146
  132. package/dist/components/table/data-table.js.map +1 -0
  133. package/dist/components/table/table.d.ts +3 -3
  134. package/dist/components/table/table.d.ts.map +1 -1
  135. package/dist/components/table/table.js +39 -20
  136. package/dist/components/table/table.js.map +1 -0
  137. package/dist/components/tabs/tab-panel.d.ts +8 -1
  138. package/dist/components/tabs/tab-panel.d.ts.map +1 -1
  139. package/dist/components/tabs/tab-panel.js +21 -11
  140. package/dist/components/tabs/tab-panel.js.map +1 -0
  141. package/dist/components/tabs/tab.d.ts +10 -1
  142. package/dist/components/tabs/tab.d.ts.map +1 -1
  143. package/dist/components/tabs/tab.js +46 -28
  144. package/dist/components/tabs/tab.js.map +1 -0
  145. package/dist/components/tabs/tabs.d.ts +9 -0
  146. package/dist/components/tabs/tabs.d.ts.map +1 -1
  147. package/dist/components/tabs/tabs.js +76 -13
  148. package/dist/components/tabs/tabs.js.map +1 -0
  149. package/dist/components/textarea/textarea.d.ts +13 -0
  150. package/dist/components/textarea/textarea.d.ts.map +1 -1
  151. package/dist/components/textarea/textarea.js +59 -13
  152. package/dist/components/textarea/textarea.js.map +1 -0
  153. package/dist/components/theme-toggle/theme-toggle.constants.d.ts +3 -0
  154. package/dist/components/theme-toggle/theme-toggle.constants.d.ts.map +1 -0
  155. package/dist/components/theme-toggle/theme-toggle.constants.js +2 -0
  156. package/dist/components/theme-toggle/theme-toggle.constants.js.map +1 -0
  157. package/dist/components/theme-toggle/theme-toggle.d.ts +3 -2
  158. package/dist/components/theme-toggle/theme-toggle.d.ts.map +1 -1
  159. package/dist/components/theme-toggle/theme-toggle.js +46 -19
  160. package/dist/components/theme-toggle/theme-toggle.js.map +1 -0
  161. package/dist/components/vertical-nav/vertical-nav-group.d.ts +5 -2
  162. package/dist/components/vertical-nav/vertical-nav-group.d.ts.map +1 -1
  163. package/dist/components/vertical-nav/vertical-nav-group.js +50 -14
  164. package/dist/components/vertical-nav/vertical-nav-group.js.map +1 -0
  165. package/dist/components/vertical-nav/vertical-nav-item.d.ts +3 -2
  166. package/dist/components/vertical-nav/vertical-nav-item.d.ts.map +1 -1
  167. package/dist/components/vertical-nav/vertical-nav-item.js +30 -8
  168. package/dist/components/vertical-nav/vertical-nav-item.js.map +1 -0
  169. package/dist/styles/index.css +50 -17
  170. package/dist/styles/reset.js +1 -0
  171. package/dist/styles/reset.js.map +1 -0
  172. package/package.json +2 -1
  173. package/src/components/alert/alert.constants.ts +2 -0
  174. package/src/components/alert/alert.ts +24 -15
  175. package/src/components/badge/badge.constants.ts +2 -0
  176. package/src/components/badge/badge.ts +3 -4
  177. package/src/components/breadcrumb/breadcrumb.ts +2 -2
  178. package/src/components/button/button.constants.ts +8 -0
  179. package/src/components/button/button.ts +184 -144
  180. package/src/components/button-group/button-group.ts +23 -21
  181. package/src/components/checkbox/checkbox.constants.ts +2 -0
  182. package/src/components/checkbox/checkbox.ts +33 -18
  183. package/src/components/code-block/code-block.ts +157 -84
  184. package/src/components/content-card/content-card.ts +10 -16
  185. package/src/components/divider/divider.constants.ts +5 -0
  186. package/src/components/divider/divider.ts +22 -11
  187. package/src/components/drawer/drawer.ts +79 -34
  188. package/src/components/field/field.ts +9 -11
  189. package/src/components/footer/footer.ts +43 -12
  190. package/src/components/form/form.ts +120 -83
  191. package/src/components/grid/grid-row.ts +45 -32
  192. package/src/components/grid/grid.ts +7 -4
  193. package/src/components/input/input.ts +37 -41
  194. package/src/components/link/link.ts +45 -21
  195. package/src/components/metadata/metadata.ts +26 -24
  196. package/src/components/modal/modal.ts +108 -74
  197. package/src/components/page-layout/page-layout.ts +98 -73
  198. package/src/components/radio/radio.ts +8 -13
  199. package/src/components/radio-group/radio-group.ts +45 -21
  200. package/src/components/router/router.ts +73 -50
  201. package/src/components/select/select.ts +502 -131
  202. package/src/components/skeleton/skeleton.ts +14 -14
  203. package/src/components/switch/switch.ts +31 -25
  204. package/src/components/table/data-table.ts +248 -177
  205. package/src/components/table/table.ts +52 -29
  206. package/src/components/tabs/tab-panel.ts +30 -13
  207. package/src/components/tabs/tab.ts +64 -33
  208. package/src/components/tabs/tabs.ts +82 -14
  209. package/src/components/textarea/textarea.ts +63 -15
  210. package/src/components/theme-toggle/theme-toggle.constants.ts +2 -0
  211. package/src/components/theme-toggle/theme-toggle.ts +47 -22
  212. package/src/components/vertical-nav/vertical-nav-group.ts +57 -18
  213. package/src/components/vertical-nav/vertical-nav-item.ts +35 -12
  214. package/src/styles/globals.css +27 -4
  215. package/src/styles/theme.css +39 -31
  216. package/tsconfig.json +2 -1
  217. package/dist/components/card/card.d.ts +0 -11
  218. package/dist/components/card/card.d.ts.map +0 -1
  219. package/dist/components/card/card.js +0 -54
  220. package/src/components/card/card.ts +0 -53
@@ -1,23 +1,23 @@
1
- import { LitElement, html, css } from 'lit';
2
- import { customElement, property } from 'lit/decorators.js';
3
- import { resetStyles } from '../../styles/reset.js';
1
+ import {LitElement, html, css} from "lit";
2
+ import {customElement, property} from "lit/decorators.js";
3
+ import {resetStyles} from "../../styles/reset.js";
4
4
 
5
- export type TableMode = 'horizontal' | 'vertical';
5
+ export type TableMode = "horizontal" | "vertical";
6
6
 
7
7
  export interface TableColumn {
8
8
  header: string;
9
9
  key: string;
10
10
  }
11
11
 
12
- @customElement('st-table')
12
+ @customElement("st-table")
13
13
  export class StTable extends LitElement {
14
- @property({ type: Array }) columns: TableColumn[] = [];
15
- @property({ type: Array }) data: Record<string, any>[] = [];
16
-
17
- @property({ type: String, reflect: true }) mode: TableMode = 'horizontal';
18
- @property({ type: Boolean, reflect: true }) striped = false;
19
- @property({ type: Boolean, reflect: true }) hoverable = false;
20
- @property({ type: Boolean, reflect: true }) dense = false;
14
+ @property({type: Array}) columns: TableColumn[] = [];
15
+ @property({type: Array}) data: Record<string, any>[] = [];
16
+
17
+ @property({type: String, reflect: true}) mode: TableMode = "horizontal";
18
+ @property({type: Boolean, reflect: true}) striped = false;
19
+ @property({type: Boolean, reflect: true}) hoverable = false;
20
+ @property({type: Boolean, reflect: true}) dense = false;
21
21
 
22
22
  static styles = [
23
23
  resetStyles,
@@ -61,11 +61,11 @@ export class StTable extends LitElement {
61
61
  }
62
62
 
63
63
  :host([striped]) tbody tr:nth-of-type(odd) {
64
- background-color: var(--st-color-surface-muted);
64
+ background-color: var(--st-color-surface-muted, #f8fafc);
65
65
  }
66
66
 
67
67
  :host([hoverable]) tbody tr:hover {
68
- background-color: var(--st-color-surface-hover);
68
+ background-color: var(--st-color-surface-hover, #f1f5f9);
69
69
  }
70
70
 
71
71
  /* ==========================================================
@@ -120,26 +120,49 @@ export class StTable extends LitElement {
120
120
  padding-right: var(--st-spacing-md);
121
121
  color: var(--st-color-text-muted);
122
122
  }
123
- `
123
+
124
+ .empty-state {
125
+ text-align: center;
126
+ padding: 2rem !important;
127
+ color: var(--st-color-text-muted);
128
+ font-style: italic;
129
+ }
130
+ `,
124
131
  ];
125
132
 
126
133
  render() {
127
134
  return html`
128
135
  <div class="table-wrapper">
129
- <table>
130
- <thead>
131
- <tr>
132
- ${this.columns.map(col => html`<th scope="col">${col.header}</th>`)}
136
+ <!-- 🚀 FIX: Explicit ARIA roles map to preserve semantics when CSS 'display' changes -->
137
+ <table role="table">
138
+ <thead role="rowgroup">
139
+ <tr role="row">
140
+ ${this.columns.map(
141
+ (col) => html`<th role="columnheader" scope="col">${col.header}</th>`,
142
+ )}
133
143
  </tr>
134
144
  </thead>
135
- <tbody>
136
- ${this.data.map(row => html`
137
- <tr>
138
- ${this.columns.map(col => html`
139
- <td data-label=${col.header}>${row[col.key]}</td>
140
- `)}
141
- </tr>
142
- `)}
145
+ <tbody role="rowgroup">
146
+ ${this.data.length === 0
147
+ ? html`
148
+ <tr role="row">
149
+ <td role="cell" colspan=${this.columns.length} class="empty-state">
150
+ No data available.
151
+ </td>
152
+ </tr>
153
+ `
154
+ : this.data.map(
155
+ (row) => html`
156
+ <tr role="row">
157
+ ${this.columns.map(
158
+ // 🚀 FIX: Nullish coalescing prevents printing "undefined" for sparse data objects
159
+ (col) => html`
160
+ <td role="cell" data-label=${col.header}>${row[col.key] ?? ""}</td>
161
+ `,
162
+ )}
163
+ </tr>
164
+ `,
165
+ )}
143
166
  </tbody>
144
167
  </table>
145
168
  </div>
@@ -149,6 +172,6 @@ export class StTable extends LitElement {
149
172
 
150
173
  declare global {
151
174
  interface HTMLElementTagNameMap {
152
- 'st-table': StTable;
175
+ "st-table": StTable;
153
176
  }
154
- }
177
+ }
@@ -1,11 +1,11 @@
1
- import { LitElement, html, css } from 'lit';
2
- import { customElement, property } from 'lit/decorators.js';
3
- import { resetStyles } from '../../styles/reset.js';
1
+ import {LitElement, html, css, PropertyValues} from "lit";
2
+ import {customElement, property} from "lit/decorators.js";
3
+ import {resetStyles} from "../../styles/reset.js";
4
4
 
5
- @customElement('st-tab-panel')
5
+ @customElement("st-tab-panel")
6
6
  export class StTabPanel extends LitElement {
7
- @property({ type: String, reflect: true }) name = '';
8
- @property({ type: Boolean, reflect: true }) active = false;
7
+ @property({type: String, reflect: true}) name = "";
8
+ @property({type: Boolean, reflect: true}) active = false;
9
9
 
10
10
  static styles = [
11
11
  resetStyles,
@@ -18,14 +18,31 @@ export class StTabPanel extends LitElement {
18
18
  :host(:not([active])) {
19
19
  display: none;
20
20
  }
21
- `
21
+ `,
22
22
  ];
23
23
 
24
+ connectedCallback() {
25
+ super.connectedCallback();
26
+ // 🚀 FIX: Expose role and ID on the host to link perfectly with the orchestrator
27
+ this.id = this.id || `panel-${this.name}`;
28
+ this.setAttribute("role", "tabpanel");
29
+ this.setAttribute("aria-hidden", this.active ? "false" : "true");
30
+ }
31
+
32
+ override updated(changedProperties: PropertyValues) {
33
+ super.updated(changedProperties);
34
+ if (changedProperties.has("active")) {
35
+ this.setAttribute("aria-hidden", this.active ? "false" : "true");
36
+ }
37
+ }
38
+
24
39
  render() {
25
- return html`
26
- <div role="tabpanel" aria-hidden=${this.active ? 'false' : 'true'}>
27
- <slot></slot>
28
- </div>
29
- `;
40
+ return html`<slot></slot>`;
41
+ }
42
+ }
43
+
44
+ declare global {
45
+ interface HTMLElementTagNameMap {
46
+ "st-tab-panel": StTabPanel;
30
47
  }
31
- }
48
+ }
@@ -1,25 +1,21 @@
1
- import { LitElement, html, css } from 'lit';
2
- import { customElement, property } from 'lit/decorators.js';
3
- import { resetStyles } from '../../styles/reset.js';
1
+ import {LitElement, html, css, PropertyValues} from "lit";
2
+ import {customElement, property} from "lit/decorators.js";
3
+ import {resetStyles} from "../../styles/reset.js";
4
4
 
5
- @customElement('st-tab')
5
+ @customElement("st-tab")
6
6
  export class StTab extends LitElement {
7
- @property({ type: String, reflect: true }) panel = '';
8
- @property({ type: Boolean, reflect: true }) active = false;
7
+ @property({type: String, reflect: true}) panel = "";
8
+ @property({type: Boolean, reflect: true}) active = false;
9
9
 
10
10
  static styles = [
11
11
  resetStyles,
12
12
  css`
13
+ /* 🚀 FIX: Style the host element directly to act as the interactive tab */
13
14
  :host {
14
- display: inline-block;
15
- }
16
-
17
- button {
18
15
  display: inline-flex;
19
16
  align-items: center;
20
17
  gap: var(--st-spacing-sm);
21
18
  background: transparent;
22
- border: none;
23
19
  border-bottom: 2px solid transparent;
24
20
  padding: var(--st-spacing-sm) var(--st-spacing-md);
25
21
  color: var(--st-color-text-muted);
@@ -28,20 +24,19 @@ export class StTab extends LitElement {
28
24
  cursor: pointer;
29
25
  transition: all 0.2s ease;
30
26
  white-space: nowrap;
27
+ outline: none; /* Focus outline handled below */
31
28
  }
32
29
 
33
- button:hover {
30
+ :host(:hover) {
34
31
  color: var(--st-color-text);
35
32
  }
36
33
 
37
- button:focus-visible {
38
- outline: none;
34
+ :host(:focus-visible) {
39
35
  background-color: var(--st-color-surface);
40
36
  border-radius: 4px 4px 0 0;
41
37
  }
42
38
 
43
- /* Active State */
44
- :host([active]) button {
39
+ :host([active]) {
45
40
  color: var(--st-color-primary);
46
41
  border-bottom-color: var(--st-color-primary);
47
42
  }
@@ -51,27 +46,63 @@ export class StTab extends LitElement {
51
46
  height: 1.125rem;
52
47
  fill: currentColor;
53
48
  }
54
- `
49
+ `,
55
50
  ];
56
51
 
52
+ connectedCallback() {
53
+ super.connectedCallback();
54
+
55
+ // 🚀 FIX: Apply the ARIA role and attributes to the Host element
56
+ this.id = this.id || `tab-${this.panel}`;
57
+ this.setAttribute("role", "tab");
58
+ this.setAttribute("aria-selected", this.active ? "true" : "false");
59
+ this.setAttribute("tabindex", this.active ? "0" : "-1");
60
+
61
+ // Attach native event listeners since we are no longer using a button
62
+ this.addEventListener("click", this._handleClick);
63
+ this.addEventListener("keydown", this._handleKeyDown);
64
+ }
65
+
66
+ disconnectedCallback() {
67
+ super.disconnectedCallback();
68
+ this.removeEventListener("click", this._handleClick);
69
+ this.removeEventListener("keydown", this._handleKeyDown);
70
+ }
71
+
72
+ override updated(changedProperties: PropertyValues) {
73
+ super.updated(changedProperties);
74
+ if (changedProperties.has("active")) {
75
+ // Sync ARIA states when active changes
76
+ this.setAttribute("aria-selected", this.active ? "true" : "false");
77
+ this.setAttribute("tabindex", this.active ? "0" : "-1");
78
+ }
79
+ }
80
+
57
81
  private _handleClick() {
58
- this.dispatchEvent(new CustomEvent('st-tab-select', {
59
- detail: { panel: this.panel },
60
- bubbles: true,
61
- composed: true
62
- }));
82
+ this.dispatchEvent(
83
+ new CustomEvent("st-tab-select", {
84
+ detail: {panel: this.panel},
85
+ bubbles: true,
86
+ composed: true,
87
+ }),
88
+ );
89
+ }
90
+
91
+ private _handleKeyDown(e: KeyboardEvent) {
92
+ // 🚀 FIX: Ensure Enter and Space keys activate the tab natively
93
+ if (e.key === "Enter" || e.key === " ") {
94
+ e.preventDefault();
95
+ this._handleClick();
96
+ }
63
97
  }
64
98
 
65
99
  render() {
66
- return html`
67
- <button
68
- role="tab"
69
- aria-selected=${this.active ? 'true' : 'false'}
70
- tabindex=${this.active ? '0' : '-1'}
71
- @click=${this._handleClick}
72
- >
73
- <slot></slot>
74
- </button>
75
- `;
100
+ return html`<slot></slot>`;
101
+ }
102
+ }
103
+
104
+ declare global {
105
+ interface HTMLElementTagNameMap {
106
+ "st-tab": StTab;
76
107
  }
77
- }
108
+ }
@@ -2,7 +2,7 @@ import "./tab.js";
2
2
  import "./tab-panel.js";
3
3
 
4
4
  import {LitElement, html, css} from "lit";
5
- import {customElement, queryAssignedElements} from "lit/decorators.js";
5
+ import {customElement, queryAssignedElements, property} from "lit/decorators.js";
6
6
  import {resetStyles} from "../../styles/reset.js";
7
7
  import type {StTab} from "./tab.js";
8
8
  import type {StTabPanel} from "./tab-panel.js";
@@ -15,6 +15,9 @@ export class StTabs extends LitElement {
15
15
  @queryAssignedElements({selector: "st-tab-panel"})
16
16
  private _panels!: StTabPanel[];
17
17
 
18
+ // 🚀 FEATURE: Syncs the active tab with the URL Hash
19
+ @property({type: Boolean, attribute: "sync-url"}) syncUrl = false;
20
+
18
21
  static styles = [
19
22
  resetStyles,
20
23
  css`
@@ -24,15 +27,14 @@ export class StTabs extends LitElement {
24
27
 
25
28
  .nav-container {
26
29
  display: flex;
27
- /* Creates the track line underneath the tabs */
28
30
  border-bottom: 1px solid var(--st-color-border);
29
31
  gap: var(--st-spacing-xs);
30
- overflow-x: auto; /* Allows tabs to scroll horizontally on small screens */
31
- scrollbar-width: none; /* Hides scrollbar on Firefox */
32
+ overflow-x: auto;
33
+ scrollbar-width: none;
32
34
  }
33
35
 
34
36
  .nav-container::-webkit-scrollbar {
35
- display: none; /* Hides scrollbar on Chrome/Safari */
37
+ display: none;
36
38
  }
37
39
  `,
38
40
  ];
@@ -40,44 +42,104 @@ export class StTabs extends LitElement {
40
42
  connectedCallback() {
41
43
  super.connectedCallback();
42
44
  this.addEventListener("st-tab-select", this._handleTabSelect as EventListener);
45
+ window.addEventListener("hashchange", this._handleHashChange);
43
46
  }
44
47
 
45
48
  disconnectedCallback() {
46
49
  super.disconnectedCallback();
47
50
  this.removeEventListener("st-tab-select", this._handleTabSelect as EventListener);
51
+ window.removeEventListener("hashchange", this._handleHashChange);
48
52
  }
49
53
 
54
+ private _handleHashChange = () => {
55
+ if (!this.syncUrl) return;
56
+ const hash = window.location.hash.replace("#", "");
57
+ if (hash && this._tabs.some((t) => t.panel === hash)) {
58
+ this._activateTab(hash, false); // Don't push state if we are responding to a history change
59
+ }
60
+ };
61
+
50
62
  private _handleTabSelect(e: CustomEvent<{panel: string}>) {
51
- // 1. Stop the event here so parent <st-tabs> instances don't react to it
52
63
  e.stopPropagation();
64
+ this._activateTab(e.detail.panel, true);
65
+ }
53
66
 
54
- const targetPanel = e.detail.panel;
55
-
56
- // Update active states on all tabs
67
+ private _activateTab(targetPanel: string, updateUrl: boolean) {
57
68
  this._tabs.forEach((tab) => {
58
69
  tab.active = tab.panel === targetPanel;
59
70
  });
60
71
 
61
- // Update active states on all panels
62
72
  this._panels.forEach((panel) => {
63
73
  panel.active = panel.name === targetPanel;
64
74
  });
75
+
76
+ if (updateUrl && this.syncUrl) {
77
+ window.history.replaceState(null, "", `#${targetPanel}`);
78
+ }
79
+
80
+ this.dispatchEvent(
81
+ new CustomEvent("change", {
82
+ detail: {panel: targetPanel},
83
+ bubbles: true,
84
+ composed: true,
85
+ }),
86
+ );
65
87
  }
66
88
 
67
- // Ensures the first tab is active by default if none are explicitly set
68
89
  private _handleSlotChange() {
69
90
  if (!this._tabs.length || !this._panels.length) return;
70
91
 
92
+ // Link ARIA attributes securely
93
+ this._tabs.forEach((t) => {
94
+ t.setAttribute("aria-controls", `panel-${t.panel}`);
95
+ });
96
+ this._panels.forEach((p) => {
97
+ p.setAttribute("aria-labelledby", `tab-${p.name}`);
98
+ });
99
+
100
+ // 1. If sync-url is on, and hash exists, try to load it
101
+ if (this.syncUrl) {
102
+ const hash = window.location.hash.replace("#", "");
103
+ if (hash && this._tabs.some((t) => t.panel === hash)) {
104
+ this._activateTab(hash, false);
105
+ return;
106
+ }
107
+ }
108
+
109
+ // 2. Otherwise fallback to the first active tab, or the very first tab
71
110
  const hasActiveTab = this._tabs.some((tab) => tab.active);
72
111
  if (!hasActiveTab) {
73
- this._tabs[0].active = true;
74
- this._panels[0].active = true;
112
+ this._activateTab(this._tabs[0].panel, false);
75
113
  }
76
114
  }
77
115
 
116
+ // 🚀 FEATURE: W3C Keyboard Navigation (Arrows, Home, End)
117
+ private _handleKeyDown(e: KeyboardEvent) {
118
+ if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return;
119
+
120
+ const tabs = this._tabs;
121
+ const currentIndex = tabs.findIndex((t) => t.active);
122
+ let nextIndex = currentIndex;
123
+
124
+ if (e.key === "ArrowRight") {
125
+ nextIndex = (currentIndex + 1) % tabs.length;
126
+ } else if (e.key === "ArrowLeft") {
127
+ nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
128
+ } else if (e.key === "Home") {
129
+ nextIndex = 0;
130
+ } else if (e.key === "End") {
131
+ nextIndex = tabs.length - 1;
132
+ }
133
+
134
+ e.preventDefault();
135
+ const targetTab = tabs[nextIndex];
136
+ targetTab.focus();
137
+ this._activateTab(targetTab.panel, true);
138
+ }
139
+
78
140
  render() {
79
141
  return html`
80
- <div class="nav-container" role="tablist">
142
+ <div class="nav-container" role="tablist" @keydown=${this._handleKeyDown}>
81
143
  <slot name="nav" @slotchange=${this._handleSlotChange}></slot>
82
144
  </div>
83
145
  <div class="panels-container">
@@ -86,3 +148,9 @@ export class StTabs extends LitElement {
86
148
  `;
87
149
  }
88
150
  }
151
+
152
+ declare global {
153
+ interface HTMLElementTagNameMap {
154
+ "st-tabs": StTabs;
155
+ }
156
+ }
@@ -10,6 +10,9 @@ export class StTextarea extends LitElement {
10
10
  static formAssociated = true;
11
11
  private _internals: ElementInternals;
12
12
 
13
+ // 🚀 FIX: Forwards host clicks and keyboard tabs to the hidden native input
14
+ static shadowRootOptions = {...LitElement.shadowRootOptions, delegatesFocus: true};
15
+
13
16
  @query("textarea") private _textareaEl!: HTMLTextAreaElement;
14
17
 
15
18
  @property({type: String, reflect: true}) name = "";
@@ -22,6 +25,12 @@ export class StTextarea extends LitElement {
22
25
  @property({type: Number}) rows = 4;
23
26
  @property({type: String, reflect: true}) resize: TextareaResize = "vertical";
24
27
 
28
+ // 🚀 FEATURE: Auto-resize support
29
+ @property({type: Boolean, attribute: "auto-resize"}) autoResize = false;
30
+
31
+ // Security / Privacy
32
+ @property({type: String, attribute: "spellcheck"}) spellcheckAttr?: "true" | "false";
33
+
25
34
  // Validation
26
35
  @property({type: Boolean, reflect: true}) required = false;
27
36
  @property({type: Number}) minlength?: number;
@@ -32,16 +41,18 @@ export class StTextarea extends LitElement {
32
41
  @property({type: Boolean, reflect: true}) invalid = false;
33
42
  @property({type: Boolean, reflect: true}) readonly = false;
34
43
 
44
+ private _defaultValue = "";
45
+
35
46
  constructor() {
36
47
  super();
37
48
  this._internals = this.attachInternals();
38
49
 
39
- // 1. Trigger error state if the user clicks away and the field is invalid
40
50
  this.addEventListener("focusout", () => {
41
- this.invalid = !this._textareaEl.validity.valid;
51
+ if (this._textareaEl) {
52
+ this.invalid = !this._textareaEl.validity.valid;
53
+ }
42
54
  });
43
55
 
44
- // 2. Trigger error state if the st-form tries to submit and fails
45
56
  this.addEventListener("invalid", () => {
46
57
  this.invalid = true;
47
58
  });
@@ -68,6 +79,7 @@ export class StTextarea extends LitElement {
68
79
 
69
80
  textarea {
70
81
  width: 100%;
82
+ box-sizing: border-box; /* 🚀 FIX: Prevent padding from causing overflow */
71
83
  padding: var(--st-spacing-sm) var(--st-spacing-md);
72
84
  font-size: var(--st-font-size-base);
73
85
  font-family: inherit;
@@ -78,12 +90,10 @@ export class StTextarea extends LitElement {
78
90
  transition:
79
91
  border-color 0.2s ease,
80
92
  box-shadow 0.2s ease;
81
-
82
- /* Map the resize property */
83
93
  resize: vertical;
84
94
  }
85
95
 
86
- /* Dynamically handle resize constraints based on the host attribute */
96
+ /* Dynamically handle resize constraints */
87
97
  :host([resize="none"]) textarea {
88
98
  resize: none;
89
99
  }
@@ -94,6 +104,12 @@ export class StTextarea extends LitElement {
94
104
  resize: both;
95
105
  }
96
106
 
107
+ /* Auto-resize overrides manual resize */
108
+ :host([auto-resize]) textarea {
109
+ resize: none;
110
+ overflow-y: hidden;
111
+ }
112
+
97
113
  textarea::placeholder {
98
114
  color: var(--st-color-text-muted);
99
115
  }
@@ -106,15 +122,15 @@ export class StTextarea extends LitElement {
106
122
 
107
123
  /* Validation States */
108
124
  :host([invalid]) textarea {
109
- border-color: var(--st-color-error);
125
+ border-color: var(--st-color-error, #dc2626);
110
126
  }
111
127
 
112
128
  :host([invalid]) textarea:focus {
113
- box-shadow: 0 0 0 1px var(--st-color-error);
129
+ box-shadow: 0 0 0 1px var(--st-color-error, #dc2626);
114
130
  }
115
131
 
116
132
  :host([invalid]) .help-text {
117
- color: var(--st-color-error);
133
+ color: var(--st-color-error, #dc2626);
118
134
  }
119
135
 
120
136
  /* Disabled & Readonly */
@@ -136,15 +152,32 @@ export class StTextarea extends LitElement {
136
152
  `,
137
153
  ];
138
154
 
155
+ override firstUpdated(changedProperties: PropertyValues<this>) {
156
+ super.firstUpdated(changedProperties);
157
+ this._defaultValue = this.value;
158
+ if (this.autoResize) this._adjustHeight();
159
+ }
160
+
139
161
  override updated(changedProperties: PropertyValues<this>) {
140
162
  super.updated(changedProperties);
141
163
 
142
164
  if (changedProperties.has("value")) {
143
165
  this._internals.setFormValue(this.value);
166
+ if (this.autoResize) this._adjustHeight();
144
167
  }
168
+
145
169
  this._syncValidity();
146
170
  }
147
171
 
172
+ private async _adjustHeight() {
173
+ await this.updateComplete;
174
+ if (!this._textareaEl) return;
175
+
176
+ // Reset height to auto to force scrollHeight to shrink if text is deleted
177
+ this._textareaEl.style.height = "auto";
178
+ this._textareaEl.style.height = `${this._textareaEl.scrollHeight}px`;
179
+ }
180
+
148
181
  private _syncValidity() {
149
182
  if (!this._textareaEl) return;
150
183
  this._internals.setValidity(
@@ -155,15 +188,21 @@ export class StTextarea extends LitElement {
155
188
  }
156
189
 
157
190
  formResetCallback() {
158
- this.value = "";
159
- this._internals.setFormValue("");
160
- this.updateComplete.then(() => this._syncValidity());
191
+ // 🚀 FIX: Revert to the initial value instead of hardcoding an empty string
192
+ this.value = this._defaultValue;
193
+ this.invalid = false;
194
+ this._internals.setFormValue(this.value);
195
+ this.updateComplete.then(() => {
196
+ this._syncValidity();
197
+ if (this.autoResize) this._adjustHeight();
198
+ });
161
199
  }
162
200
 
163
- checkValidity() {
201
+ public checkValidity() {
164
202
  return this._internals.checkValidity();
165
203
  }
166
- reportValidity() {
204
+
205
+ public reportValidity() {
167
206
  return this._internals.reportValidity();
168
207
  }
169
208
 
@@ -175,6 +214,10 @@ export class StTextarea extends LitElement {
175
214
  this.invalid = false;
176
215
  }
177
216
 
217
+ if (this.autoResize) {
218
+ this._adjustHeight();
219
+ }
220
+
178
221
  this.dispatchEvent(new Event("input", {bubbles: true, composed: true}));
179
222
  }
180
223
 
@@ -183,6 +226,8 @@ export class StTextarea extends LitElement {
183
226
  }
184
227
 
185
228
  render() {
229
+ const helpTextId = "help-text";
230
+
186
231
  return html`
187
232
  <div class="form-control">
188
233
  ${this.label ? html`<label for="textarea">${this.label}</label>` : ""}
@@ -198,11 +243,14 @@ export class StTextarea extends LitElement {
198
243
  ?required=${this.required}
199
244
  minlength=${ifDefined(this.minlength)}
200
245
  maxlength=${ifDefined(this.maxlength)}
246
+ spellcheck=${ifDefined(this.spellcheck)}
247
+ aria-invalid=${this.invalid ? "true" : "false"}
248
+ aria-describedby=${ifDefined(this.helpText ? helpTextId : undefined)}
201
249
  @input=${this._handleInput}
202
250
  @change=${this._handleChange}
203
251
  ></textarea>
204
252
 
205
- ${this.helpText ? html`<div class="help-text">${this.helpText}</div>` : ""}
253
+ ${this.helpText ? html`<div id=${helpTextId} class="help-text">${this.helpText}</div>` : ""}
206
254
  </div>
207
255
  `;
208
256
  }
@@ -0,0 +1,2 @@
1
+ export const themeModes = ["light", "system", "dark"] as const;
2
+ export type ThemeMode = typeof themeModes[number];