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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/package.json +2 -3
  2. package/src/components/alert/alert.ts +100 -100
  3. package/src/components/badge/badge.ts +96 -96
  4. package/src/components/breadcrumb/breadcrumb.ts +98 -98
  5. package/src/components/button/button.ts +281 -281
  6. package/src/components/button-group/button-group.ts +67 -67
  7. package/src/components/card/card.ts +52 -52
  8. package/src/components/checkbox/checkbox.ts +238 -238
  9. package/src/components/code-block/code-block.ts +256 -256
  10. package/src/components/content-card/content-card.ts +133 -133
  11. package/src/components/divider/divider.ts +68 -68
  12. package/src/components/drawer/drawer.ts +217 -217
  13. package/src/components/field/field.ts +30 -30
  14. package/src/components/footer/footer.ts +58 -58
  15. package/src/components/form/form-step.ts +32 -32
  16. package/src/components/form/form.ts +376 -376
  17. package/src/components/grid/grid-row.ts +75 -75
  18. package/src/components/grid/grid.ts +36 -36
  19. package/src/components/icon/icon-paths.ts +124 -124
  20. package/src/components/icon/icon.ts +82 -82
  21. package/src/components/input/input.ts +248 -248
  22. package/src/components/link/link.ts +97 -97
  23. package/src/components/metadata/metadata.ts +66 -66
  24. package/src/components/modal/modal.ts +158 -158
  25. package/src/components/page-layout/page-layout.ts +704 -704
  26. package/src/components/radio/radio.ts +142 -142
  27. package/src/components/radio-group/radio-group.ts +157 -157
  28. package/src/components/router/router.ts +46 -46
  29. package/src/components/select/select.ts +161 -161
  30. package/src/components/skeleton/skeleton.ts +67 -67
  31. package/src/components/switch/switch.ts +181 -181
  32. package/src/components/table/data-table.ts +556 -556
  33. package/src/components/table/table.ts +153 -153
  34. package/src/components/tabs/tab-panel.ts +30 -30
  35. package/src/components/tabs/tab.ts +76 -76
  36. package/src/components/tabs/tabs.ts +88 -88
  37. package/src/components/textarea/textarea.ts +215 -215
  38. package/src/components/theme-toggle/theme-toggle.ts +131 -131
  39. package/src/components/vertical-nav/vertical-nav-group.ts +96 -96
  40. package/src/components/vertical-nav/vertical-nav-item.ts +49 -49
  41. package/src/styles/globals.css +58 -58
  42. package/src/styles/index.css +80 -80
  43. package/src/styles/reset.ts +50 -50
  44. package/src/styles/theme.css +58 -58
  45. package/src/styles/typography.css +25 -25
  46. package/tsconfig.json +15 -15
  47. package/README.md +0 -8
  48. package/dist/components/alert/alert.d.ts +0 -13
  49. package/dist/components/alert/alert.d.ts.map +0 -1
  50. package/dist/components/alert/alert.js +0 -105
  51. package/dist/components/badge/badge.d.ts +0 -13
  52. package/dist/components/badge/badge.d.ts.map +0 -1
  53. package/dist/components/badge/badge.js +0 -101
  54. package/dist/components/breadcrumb/breadcrumb.d.ts +0 -19
  55. package/dist/components/breadcrumb/breadcrumb.d.ts.map +0 -1
  56. package/dist/components/breadcrumb/breadcrumb.js +0 -99
  57. package/dist/components/button/button.d.ts +0 -23
  58. package/dist/components/button/button.d.ts.map +0 -1
  59. package/dist/components/button/button.js +0 -304
  60. package/dist/components/button-group/button-group.d.ts +0 -6
  61. package/dist/components/button-group/button-group.d.ts.map +0 -1
  62. package/dist/components/button-group/button-group.js +0 -74
  63. package/dist/components/card/card.d.ts +0 -11
  64. package/dist/components/card/card.d.ts.map +0 -1
  65. package/dist/components/card/card.js +0 -54
  66. package/dist/components/checkbox/checkbox.d.ts +0 -28
  67. package/dist/components/checkbox/checkbox.d.ts.map +0 -1
  68. package/dist/components/checkbox/checkbox.js +0 -247
  69. package/dist/components/code-block/code-block.d.ts +0 -20
  70. package/dist/components/code-block/code-block.d.ts.map +0 -1
  71. package/dist/components/code-block/code-block.js +0 -260
  72. package/dist/components/content-card/content-card.d.ts +0 -13
  73. package/dist/components/content-card/content-card.d.ts.map +0 -1
  74. package/dist/components/content-card/content-card.js +0 -140
  75. package/dist/components/divider/divider.d.ts +0 -16
  76. package/dist/components/divider/divider.d.ts.map +0 -1
  77. package/dist/components/divider/divider.js +0 -82
  78. package/dist/components/drawer/drawer.d.ts +0 -22
  79. package/dist/components/drawer/drawer.d.ts.map +0 -1
  80. package/dist/components/drawer/drawer.js +0 -226
  81. package/dist/components/field/field.d.ts +0 -11
  82. package/dist/components/field/field.d.ts.map +0 -1
  83. package/dist/components/field/field.js +0 -32
  84. package/dist/components/footer/footer.d.ts +0 -6
  85. package/dist/components/footer/footer.d.ts.map +0 -1
  86. package/dist/components/footer/footer.js +0 -65
  87. package/dist/components/form/form-step.d.ts +0 -13
  88. package/dist/components/form/form-step.d.ts.map +0 -1
  89. package/dist/components/form/form-step.js +0 -41
  90. package/dist/components/form/form.d.ts +0 -25
  91. package/dist/components/form/form.d.ts.map +0 -1
  92. package/dist/components/form/form.js +0 -358
  93. package/dist/components/grid/grid-row.d.ts +0 -17
  94. package/dist/components/grid/grid-row.d.ts.map +0 -1
  95. package/dist/components/grid/grid-row.js +0 -92
  96. package/dist/components/grid/grid.d.ts +0 -13
  97. package/dist/components/grid/grid.d.ts.map +0 -1
  98. package/dist/components/grid/grid.js +0 -42
  99. package/dist/components/icon/icon-paths.d.ts +0 -116
  100. package/dist/components/icon/icon-paths.d.ts.map +0 -1
  101. package/dist/components/icon/icon-paths.js +0 -121
  102. package/dist/components/icon/icon.d.ts +0 -16
  103. package/dist/components/icon/icon.d.ts.map +0 -1
  104. package/dist/components/icon/icon.js +0 -95
  105. package/dist/components/input/input.d.ts +0 -31
  106. package/dist/components/input/input.d.ts.map +0 -1
  107. package/dist/components/input/input.js +0 -268
  108. package/dist/components/link/link.d.ts +0 -16
  109. package/dist/components/link/link.d.ts.map +0 -1
  110. package/dist/components/link/link.js +0 -116
  111. package/dist/components/metadata/metadata.d.ts +0 -18
  112. package/dist/components/metadata/metadata.d.ts.map +0 -1
  113. package/dist/components/metadata/metadata.js +0 -71
  114. package/dist/components/modal/modal.d.ts +0 -24
  115. package/dist/components/modal/modal.d.ts.map +0 -1
  116. package/dist/components/modal/modal.js +0 -163
  117. package/dist/components/page-layout/page-layout.d.ts +0 -46
  118. package/dist/components/page-layout/page-layout.d.ts.map +0 -1
  119. package/dist/components/page-layout/page-layout.js +0 -697
  120. package/dist/components/radio/radio.d.ts +0 -16
  121. package/dist/components/radio/radio.d.ts.map +0 -1
  122. package/dist/components/radio/radio.js +0 -152
  123. package/dist/components/radio-group/radio-group.d.ts +0 -27
  124. package/dist/components/radio-group/radio-group.d.ts.map +0 -1
  125. package/dist/components/radio-group/radio-group.js +0 -161
  126. package/dist/components/router/router.d.ts +0 -7
  127. package/dist/components/router/router.d.ts.map +0 -1
  128. package/dist/components/router/router.js +0 -37
  129. package/dist/components/select/select.d.ts +0 -16
  130. package/dist/components/select/select.d.ts.map +0 -1
  131. package/dist/components/select/select.js +0 -172
  132. package/dist/components/skeleton/skeleton.d.ts +0 -17
  133. package/dist/components/skeleton/skeleton.d.ts.map +0 -1
  134. package/dist/components/skeleton/skeleton.js +0 -78
  135. package/dist/components/switch/switch.d.ts +0 -25
  136. package/dist/components/switch/switch.d.ts.map +0 -1
  137. package/dist/components/switch/switch.js +0 -185
  138. package/dist/components/table/data-table.d.ts +0 -72
  139. package/dist/components/table/data-table.d.ts.map +0 -1
  140. package/dist/components/table/data-table.js +0 -551
  141. package/dist/components/table/table.d.ts +0 -22
  142. package/dist/components/table/table.d.ts.map +0 -1
  143. package/dist/components/table/table.js +0 -167
  144. package/dist/components/tabs/tab-panel.d.ts +0 -8
  145. package/dist/components/tabs/tab-panel.d.ts.map +0 -1
  146. package/dist/components/tabs/tab-panel.js +0 -46
  147. package/dist/components/tabs/tab.d.ts +0 -9
  148. package/dist/components/tabs/tab.d.ts.map +0 -1
  149. package/dist/components/tabs/tab.js +0 -91
  150. package/dist/components/tabs/tabs.d.ts +0 -14
  151. package/dist/components/tabs/tabs.d.ts.map +0 -1
  152. package/dist/components/tabs/tabs.js +0 -85
  153. package/dist/components/textarea/textarea.d.ts +0 -36
  154. package/dist/components/textarea/textarea.d.ts.map +0 -1
  155. package/dist/components/textarea/textarea.js +0 -231
  156. package/dist/components/theme-toggle/theme-toggle.d.ts +0 -15
  157. package/dist/components/theme-toggle/theme-toggle.d.ts.map +0 -1
  158. package/dist/components/theme-toggle/theme-toggle.js +0 -130
  159. package/dist/components/vertical-nav/vertical-nav-group.d.ts +0 -9
  160. package/dist/components/vertical-nav/vertical-nav-group.d.ts.map +0 -1
  161. package/dist/components/vertical-nav/vertical-nav-group.js +0 -111
  162. package/dist/components/vertical-nav/vertical-nav-item.d.ts +0 -8
  163. package/dist/components/vertical-nav/vertical-nav-item.d.ts.map +0 -1
  164. package/dist/components/vertical-nav/vertical-nav-item.js +0 -65
  165. package/dist/styles/index.css +0 -171
  166. package/dist/styles/reset.d.ts +0 -2
  167. package/dist/styles/reset.d.ts.map +0 -1
  168. package/dist/styles/reset.js +0 -50
@@ -1,83 +1,83 @@
1
- import { LitElement, html, css } from 'lit';
2
- import { customElement, property } from 'lit/decorators.js';
3
- import { iconPaths, IconName } from './icon-paths.js';
4
- import { resetStyles } from '../../styles/reset.js';
5
-
6
- @customElement('st-icon')
7
- export class StIcon extends LitElement {
8
- @property({ type: String }) name: IconName | string = '';
9
- @property({ type: String }) variant: 'outline' | 'solid' = 'outline';
10
-
11
- // Semantic Colors
12
- @property({ type: String, reflect: true }) color: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' = 'default';
13
-
14
- // Standardized T-Shirt Sizes
15
- @property({ type: String, reflect: true }) size: 'sm' | 'md' | 'lg' | 'xl' = 'md';
16
-
17
- static styles = [
18
- resetStyles,
19
- css`
20
- :host {
21
- display: inline-flex;
22
- align-items: center;
23
- justify-content: center;
24
- color: inherit;
25
-
26
- /* Defaults to md (1.5rem) if no size attribute is matched */
27
- width: var(--icon-size, 1.5rem);
28
- height: var(--icon-size, 1.5rem);
29
- }
30
-
31
- /* ==========================================================
32
- Size Variants
33
- ========================================================== */
34
- :host([size="sm"]) { --icon-size: 1.25rem; } /* 20px - Matches small buttons/inputs */
35
- :host([size="md"]) { --icon-size: 1.5rem; } /* 24px - Default standard size */
36
- :host([size="lg"]) { --icon-size: 2rem; } /* 32px - Large callouts */
37
- :host([size="xl"]) { --icon-size: 2.5rem; } /* 40px - Hero sections/Cards */
38
-
39
- /* ==========================================================
40
- Color Variants
41
- ========================================================== */
42
- :host([color="primary"]) { color: var(--st-color-primary); }
43
- :host([color="secondary"]) { color: var(--st-color-secondary); }
44
- :host([color="success"]) { color: var(--st-color-success); }
45
- :host([color="warning"]) { color: var(--st-color-warning); }
46
- :host([color="error"]) { color: var(--st-color-error); }
47
- :host([color="info"]) { color: var(--st-color-info); }
48
-
49
- svg {
50
- width: 100%;
51
- height: 100%;
52
- display: block;
53
- }
54
- `
55
- ];
56
-
57
- render() {
58
- const iconData = (iconPaths as Record<string, any>)[this.name];
59
- if (!iconData) return html``;
60
-
61
- const pathData = iconData[this.variant] || iconData.outline;
62
-
63
- return html`
64
- <svg
65
- viewBox="0 0 24 24"
66
- xmlns="http://www.w3.org/2000/svg"
67
- fill="${this.variant === 'solid' ? 'currentColor' : 'none'}"
68
- stroke="${this.variant === 'outline' ? 'currentColor' : 'none'}"
69
- stroke-width="${this.variant === 'outline' ? '2' : '0'}"
70
- stroke-linecap="round"
71
- stroke-linejoin="round"
72
- >
73
- ${pathData}
74
- </svg>
75
- `;
76
- }
77
- }
78
-
79
- declare global {
80
- interface HTMLElementTagNameMap {
81
- 'st-icon': StIcon;
82
- }
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { iconPaths, IconName } from './icon-paths.js';
4
+ import { resetStyles } from '../../styles/reset.js';
5
+
6
+ @customElement('st-icon')
7
+ export class StIcon extends LitElement {
8
+ @property({ type: String }) name: IconName | string = '';
9
+ @property({ type: String }) variant: 'outline' | 'solid' = 'outline';
10
+
11
+ // Semantic Colors
12
+ @property({ type: String, reflect: true }) color: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' = 'default';
13
+
14
+ // Standardized T-Shirt Sizes
15
+ @property({ type: String, reflect: true }) size: 'sm' | 'md' | 'lg' | 'xl' = 'md';
16
+
17
+ static styles = [
18
+ resetStyles,
19
+ css`
20
+ :host {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ color: inherit;
25
+
26
+ /* Defaults to md (1.5rem) if no size attribute is matched */
27
+ width: var(--icon-size, 1.5rem);
28
+ height: var(--icon-size, 1.5rem);
29
+ }
30
+
31
+ /* ==========================================================
32
+ Size Variants
33
+ ========================================================== */
34
+ :host([size="sm"]) { --icon-size: 1.25rem; } /* 20px - Matches small buttons/inputs */
35
+ :host([size="md"]) { --icon-size: 1.5rem; } /* 24px - Default standard size */
36
+ :host([size="lg"]) { --icon-size: 2rem; } /* 32px - Large callouts */
37
+ :host([size="xl"]) { --icon-size: 2.5rem; } /* 40px - Hero sections/Cards */
38
+
39
+ /* ==========================================================
40
+ Color Variants
41
+ ========================================================== */
42
+ :host([color="primary"]) { color: var(--st-color-primary); }
43
+ :host([color="secondary"]) { color: var(--st-color-secondary); }
44
+ :host([color="success"]) { color: var(--st-color-success); }
45
+ :host([color="warning"]) { color: var(--st-color-warning); }
46
+ :host([color="error"]) { color: var(--st-color-error); }
47
+ :host([color="info"]) { color: var(--st-color-info); }
48
+
49
+ svg {
50
+ width: 100%;
51
+ height: 100%;
52
+ display: block;
53
+ }
54
+ `
55
+ ];
56
+
57
+ render() {
58
+ const iconData = (iconPaths as Record<string, any>)[this.name];
59
+ if (!iconData) return html``;
60
+
61
+ const pathData = iconData[this.variant] || iconData.outline;
62
+
63
+ return html`
64
+ <svg
65
+ viewBox="0 0 24 24"
66
+ xmlns="http://www.w3.org/2000/svg"
67
+ fill="${this.variant === 'solid' ? 'currentColor' : 'none'}"
68
+ stroke="${this.variant === 'outline' ? 'currentColor' : 'none'}"
69
+ stroke-width="${this.variant === 'outline' ? '2' : '0'}"
70
+ stroke-linecap="round"
71
+ stroke-linejoin="round"
72
+ >
73
+ ${pathData}
74
+ </svg>
75
+ `;
76
+ }
77
+ }
78
+
79
+ declare global {
80
+ interface HTMLElementTagNameMap {
81
+ 'st-icon': StIcon;
82
+ }
83
83
  }
@@ -1,248 +1,248 @@
1
- import {LitElement, html, css, PropertyValues} from "lit";
2
- import {customElement, property, query} from "lit/decorators.js";
3
- import {ifDefined} from "lit/directives/if-defined.js"; // Helper for optional attributes
4
- import {resetStyles} from "../../styles/reset.js";
5
-
6
- export type InputType = "text" | "password" | "email" | "number" | "search" | "url";
7
-
8
- @customElement("st-input")
9
- export class StInput extends LitElement {
10
- static formAssociated = true;
11
- private _internals: ElementInternals;
12
-
13
- @query("input") private _inputEl!: HTMLInputElement;
14
- @property({type: String, reflect: true}) name = "";
15
- @property({type: String}) label = "";
16
- @property({type: String}) value = "";
17
- @property({type: String}) placeholder = "";
18
- @property({type: String}) type: InputType = "text";
19
- @property({attribute: "help-text"}) helpText = "";
20
-
21
- // Validation Properties
22
- @property({type: Boolean, reflect: true}) required = false;
23
- @property({type: Number}) minlength?: number;
24
- @property({type: Number}) maxlength?: number;
25
- @property({type: String}) pattern?: string;
26
-
27
- @property({type: Boolean, reflect: true}) disabled = false;
28
- @property({type: Boolean, reflect: true}) invalid = false;
29
- @property({type: Boolean, reflect: true}) readonly = false;
30
-
31
- constructor() {
32
- super();
33
- this._internals = this.attachInternals();
34
-
35
- // 1. Trigger error state if the user clicks away and the field is invalid
36
- this.addEventListener("focusout", () => {
37
- this.invalid = !this._inputEl.validity.valid;
38
- });
39
-
40
- // 2. Trigger error state if the st-form tries to submit and fails
41
- this.addEventListener("invalid", () => {
42
- this.invalid = true;
43
- });
44
- }
45
-
46
- static styles = [
47
- resetStyles,
48
- css`
49
- :host {
50
- display: block;
51
- }
52
-
53
- .form-control {
54
- display: flex;
55
- flex-direction: column;
56
- gap: var(--st-spacing-xs);
57
- }
58
-
59
- label {
60
- font-size: var(--st-font-size-sm);
61
- font-weight: var(--st-font-weight-medium);
62
- color: var(--st-color-text);
63
- margin-bottom: 0.5em;
64
- }
65
-
66
- /* 1. The Wrapper acts as the visual input field */
67
- .input-wrapper {
68
- display: flex;
69
- align-items: center;
70
- gap: var(--st-spacing-sm);
71
- width: 100%;
72
-
73
- background-color: var(--st-color-bg);
74
- border: 1px solid var(--st-color-border);
75
- border-radius: 4px;
76
-
77
- /* Horizontal padding on the wrapper, vertical on the input */
78
- padding: 0 var(--st-spacing-md);
79
- transition:
80
- border-color 0.2s ease,
81
- box-shadow 0.2s ease;
82
- }
83
-
84
- /* 2. Apply focus ring to the wrapper when the internal input is focused */
85
- .input-wrapper:focus-within {
86
- border-color: var(--st-color-primary);
87
- box-shadow: 0 0 0 1px var(--st-color-primary);
88
- }
89
-
90
- /* 3. The actual input becomes transparent and fills the wrapper */
91
- input {
92
- flex: 1;
93
- width: 100%;
94
- border: none;
95
- background: transparent;
96
- outline: none;
97
-
98
- /* Vertical padding stretches the wrapper */
99
- padding: var(--st-spacing-sm) 0;
100
- font-size: var(--st-font-size-base);
101
- color: var(--st-color-text);
102
- }
103
-
104
- input::placeholder {
105
- color: var(--st-color-text-muted);
106
- }
107
-
108
- /* Slotted Icons */
109
- ::slotted(svg) {
110
- width: 1.25em;
111
- height: 1.25em;
112
- /* Default icons to the muted text color */
113
- color: var(--st-color-text-muted);
114
- fill: currentColor;
115
- flex-shrink: 0;
116
- }
117
-
118
- /* Interactive Slotted Elements (like clear buttons) */
119
- ::slotted(st-button) {
120
- /* Pull the button back slightly so it aligns nicely with the input padding */
121
- margin-right: calc(var(--st-spacing-sm) * -1);
122
- }
123
-
124
- /* Invalid State */
125
- :host([invalid]) .input-wrapper {
126
- border-color: var(--st-color-error);
127
- }
128
- :host([invalid]) .input-wrapper:focus-within {
129
- box-shadow: 0 0 0 1px var(--st-color-error);
130
- }
131
- :host([invalid]) .help-text {
132
- color: var(--st-color-error);
133
- }
134
-
135
- /* Disabled & Readonly States */
136
- :host([disabled]) .input-wrapper {
137
- opacity: 0.5;
138
- cursor: not-allowed;
139
- background-color: var(--st-color-surface);
140
- }
141
- :host([disabled]) input {
142
- cursor: not-allowed;
143
- }
144
- :host([readonly]) .input-wrapper {
145
- background-color: var(--st-color-surface);
146
- }
147
-
148
- .help-text {
149
- font-size: var(--st-font-size-xs);
150
- color: var(--st-color-text-muted);
151
- margin: 0;
152
- }
153
-
154
- :host([invalid]) .input-wrapper {
155
- border-color: var(--st-color-error);
156
- }
157
- :host([invalid]) .help-text {
158
- color: var(--st-color-error);
159
- }
160
- `,
161
- ];
162
-
163
- override updated(changedProperties: PropertyValues<this>) {
164
- super.updated(changedProperties);
165
-
166
- // Sync the value to the form engine
167
- if (changedProperties.has("value")) {
168
- this._internals.setFormValue(this.value);
169
- }
170
-
171
- // Sync validity whenever properties change (like value or required status)
172
- this._syncValidity();
173
- }
174
-
175
- private _syncValidity() {
176
- if (!this._inputEl) return;
177
-
178
- // setValidity arguments: (ValidityState object, Custom Error Message, Anchor Element for Tooltip)
179
- this._internals.setValidity(
180
- this._inputEl.validity,
181
- this._inputEl.validationMessage,
182
- this._inputEl,
183
- );
184
- }
185
-
186
- formResetCallback() {
187
- this.value = "";
188
- this._internals.setFormValue("");
189
- // Need to wait for the render cycle to finish before checking validity on reset
190
- this.updateComplete.then(() => this._syncValidity());
191
- }
192
-
193
- // Expose standard native methods so external scripts can call them on the component
194
- checkValidity() {
195
- return this._internals.checkValidity();
196
- }
197
-
198
- reportValidity() {
199
- return this._internals.reportValidity();
200
- }
201
-
202
- private _handleInput(e: Event) {
203
- const target = e.target as HTMLTextAreaElement;
204
- this.value = target.value;
205
-
206
- if (this.invalid && target.validity.valid) {
207
- this.invalid = false;
208
- }
209
-
210
- this.dispatchEvent(new Event("input", {bubbles: true, composed: true}));
211
- }
212
-
213
- private _handleChange(e: Event) {
214
- this.dispatchEvent(new Event("change", {bubbles: true, composed: true}));
215
- }
216
-
217
- render() {
218
- return html`
219
- <div class="form-control">
220
- ${this.label ? html`<label for="input">${this.label}</label>` : ""}
221
-
222
- <div class="input-wrapper">
223
- <slot name="prefix"></slot>
224
-
225
- <input
226
- id="input"
227
- type=${this.type}
228
- .name=${this.name}
229
- .value=${this.value}
230
- placeholder=${this.placeholder}
231
- ?disabled=${this.disabled}
232
- ?readonly=${this.readonly}
233
- ?required=${this.required}
234
- minlength=${ifDefined(this.minlength)}
235
- maxlength=${ifDefined(this.maxlength)}
236
- pattern=${ifDefined(this.pattern)}
237
- @input=${this._handleInput}
238
- @change=${this._handleChange}
239
- />
240
-
241
- <slot name="suffix"></slot>
242
- </div>
243
-
244
- ${this.helpText ? html`<div class="help-text">${this.helpText}</div>` : ""}
245
- </div>
246
- `;
247
- }
248
- }
1
+ import {LitElement, html, css, PropertyValues} from "lit";
2
+ import {customElement, property, query} from "lit/decorators.js";
3
+ import {ifDefined} from "lit/directives/if-defined.js"; // Helper for optional attributes
4
+ import {resetStyles} from "../../styles/reset.js";
5
+
6
+ export type InputType = "text" | "password" | "email" | "number" | "search" | "url";
7
+
8
+ @customElement("st-input")
9
+ export class StInput extends LitElement {
10
+ static formAssociated = true;
11
+ private _internals: ElementInternals;
12
+
13
+ @query("input") private _inputEl!: HTMLInputElement;
14
+ @property({type: String, reflect: true}) name = "";
15
+ @property({type: String}) label = "";
16
+ @property({type: String}) value = "";
17
+ @property({type: String}) placeholder = "";
18
+ @property({type: String}) type: InputType = "text";
19
+ @property({attribute: "help-text"}) helpText = "";
20
+
21
+ // Validation Properties
22
+ @property({type: Boolean, reflect: true}) required = false;
23
+ @property({type: Number}) minlength?: number;
24
+ @property({type: Number}) maxlength?: number;
25
+ @property({type: String}) pattern?: string;
26
+
27
+ @property({type: Boolean, reflect: true}) disabled = false;
28
+ @property({type: Boolean, reflect: true}) invalid = false;
29
+ @property({type: Boolean, reflect: true}) readonly = false;
30
+
31
+ constructor() {
32
+ super();
33
+ this._internals = this.attachInternals();
34
+
35
+ // 1. Trigger error state if the user clicks away and the field is invalid
36
+ this.addEventListener("focusout", () => {
37
+ this.invalid = !this._inputEl.validity.valid;
38
+ });
39
+
40
+ // 2. Trigger error state if the st-form tries to submit and fails
41
+ this.addEventListener("invalid", () => {
42
+ this.invalid = true;
43
+ });
44
+ }
45
+
46
+ static styles = [
47
+ resetStyles,
48
+ css`
49
+ :host {
50
+ display: block;
51
+ }
52
+
53
+ .form-control {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: var(--st-spacing-xs);
57
+ }
58
+
59
+ label {
60
+ font-size: var(--st-font-size-sm);
61
+ font-weight: var(--st-font-weight-medium);
62
+ color: var(--st-color-text);
63
+ margin-bottom: 0.5em;
64
+ }
65
+
66
+ /* 1. The Wrapper acts as the visual input field */
67
+ .input-wrapper {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: var(--st-spacing-sm);
71
+ width: 100%;
72
+
73
+ background-color: var(--st-color-bg);
74
+ border: 1px solid var(--st-color-border);
75
+ border-radius: 4px;
76
+
77
+ /* Horizontal padding on the wrapper, vertical on the input */
78
+ padding: 0 var(--st-spacing-md);
79
+ transition:
80
+ border-color 0.2s ease,
81
+ box-shadow 0.2s ease;
82
+ }
83
+
84
+ /* 2. Apply focus ring to the wrapper when the internal input is focused */
85
+ .input-wrapper:focus-within {
86
+ border-color: var(--st-color-primary);
87
+ box-shadow: 0 0 0 1px var(--st-color-primary);
88
+ }
89
+
90
+ /* 3. The actual input becomes transparent and fills the wrapper */
91
+ input {
92
+ flex: 1;
93
+ width: 100%;
94
+ border: none;
95
+ background: transparent;
96
+ outline: none;
97
+
98
+ /* Vertical padding stretches the wrapper */
99
+ padding: var(--st-spacing-sm) 0;
100
+ font-size: var(--st-font-size-base);
101
+ color: var(--st-color-text);
102
+ }
103
+
104
+ input::placeholder {
105
+ color: var(--st-color-text-muted);
106
+ }
107
+
108
+ /* Slotted Icons */
109
+ ::slotted(svg) {
110
+ width: 1.25em;
111
+ height: 1.25em;
112
+ /* Default icons to the muted text color */
113
+ color: var(--st-color-text-muted);
114
+ fill: currentColor;
115
+ flex-shrink: 0;
116
+ }
117
+
118
+ /* Interactive Slotted Elements (like clear buttons) */
119
+ ::slotted(st-button) {
120
+ /* Pull the button back slightly so it aligns nicely with the input padding */
121
+ margin-right: calc(var(--st-spacing-sm) * -1);
122
+ }
123
+
124
+ /* Invalid State */
125
+ :host([invalid]) .input-wrapper {
126
+ border-color: var(--st-color-error);
127
+ }
128
+ :host([invalid]) .input-wrapper:focus-within {
129
+ box-shadow: 0 0 0 1px var(--st-color-error);
130
+ }
131
+ :host([invalid]) .help-text {
132
+ color: var(--st-color-error);
133
+ }
134
+
135
+ /* Disabled & Readonly States */
136
+ :host([disabled]) .input-wrapper {
137
+ opacity: 0.5;
138
+ cursor: not-allowed;
139
+ background-color: var(--st-color-surface);
140
+ }
141
+ :host([disabled]) input {
142
+ cursor: not-allowed;
143
+ }
144
+ :host([readonly]) .input-wrapper {
145
+ background-color: var(--st-color-surface);
146
+ }
147
+
148
+ .help-text {
149
+ font-size: var(--st-font-size-xs);
150
+ color: var(--st-color-text-muted);
151
+ margin: 0;
152
+ }
153
+
154
+ :host([invalid]) .input-wrapper {
155
+ border-color: var(--st-color-error);
156
+ }
157
+ :host([invalid]) .help-text {
158
+ color: var(--st-color-error);
159
+ }
160
+ `,
161
+ ];
162
+
163
+ override updated(changedProperties: PropertyValues<this>) {
164
+ super.updated(changedProperties);
165
+
166
+ // Sync the value to the form engine
167
+ if (changedProperties.has("value")) {
168
+ this._internals.setFormValue(this.value);
169
+ }
170
+
171
+ // Sync validity whenever properties change (like value or required status)
172
+ this._syncValidity();
173
+ }
174
+
175
+ private _syncValidity() {
176
+ if (!this._inputEl) return;
177
+
178
+ // setValidity arguments: (ValidityState object, Custom Error Message, Anchor Element for Tooltip)
179
+ this._internals.setValidity(
180
+ this._inputEl.validity,
181
+ this._inputEl.validationMessage,
182
+ this._inputEl,
183
+ );
184
+ }
185
+
186
+ formResetCallback() {
187
+ this.value = "";
188
+ this._internals.setFormValue("");
189
+ // Need to wait for the render cycle to finish before checking validity on reset
190
+ this.updateComplete.then(() => this._syncValidity());
191
+ }
192
+
193
+ // Expose standard native methods so external scripts can call them on the component
194
+ checkValidity() {
195
+ return this._internals.checkValidity();
196
+ }
197
+
198
+ reportValidity() {
199
+ return this._internals.reportValidity();
200
+ }
201
+
202
+ private _handleInput(e: Event) {
203
+ const target = e.target as HTMLTextAreaElement;
204
+ this.value = target.value;
205
+
206
+ if (this.invalid && target.validity.valid) {
207
+ this.invalid = false;
208
+ }
209
+
210
+ this.dispatchEvent(new Event("input", {bubbles: true, composed: true}));
211
+ }
212
+
213
+ private _handleChange(e: Event) {
214
+ this.dispatchEvent(new Event("change", {bubbles: true, composed: true}));
215
+ }
216
+
217
+ render() {
218
+ return html`
219
+ <div class="form-control">
220
+ ${this.label ? html`<label for="input">${this.label}</label>` : ""}
221
+
222
+ <div class="input-wrapper">
223
+ <slot name="prefix"></slot>
224
+
225
+ <input
226
+ id="input"
227
+ type=${this.type}
228
+ .name=${this.name}
229
+ .value=${this.value}
230
+ placeholder=${this.placeholder}
231
+ ?disabled=${this.disabled}
232
+ ?readonly=${this.readonly}
233
+ ?required=${this.required}
234
+ minlength=${ifDefined(this.minlength)}
235
+ maxlength=${ifDefined(this.maxlength)}
236
+ pattern=${ifDefined(this.pattern)}
237
+ @input=${this._handleInput}
238
+ @change=${this._handleChange}
239
+ />
240
+
241
+ <slot name="suffix"></slot>
242
+ </div>
243
+
244
+ ${this.helpText ? html`<div class="help-text">${this.helpText}</div>` : ""}
245
+ </div>
246
+ `;
247
+ }
248
+ }