@reliability-design/web 1.3.0 → 1.4.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 (123) hide show
  1. package/dist/components/rel-accordion/README.md +13 -13
  2. package/dist/components/rel-accordion/rel-accordion.ts +80 -45
  3. package/dist/components/rel-alert/README.md +15 -15
  4. package/dist/components/rel-alert/rel-alert.ts +161 -159
  5. package/dist/components/rel-avatar/README.md +15 -15
  6. package/dist/components/rel-avatar/rel-avatar.ts +42 -38
  7. package/dist/components/rel-badge/README.md +5 -5
  8. package/dist/components/rel-badge/rel-badge.ts +69 -17
  9. package/dist/components/rel-breadcrumb/README.md +7 -7
  10. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +16 -13
  11. package/dist/components/rel-button/README.md +20 -20
  12. package/dist/components/rel-button/rel-button.ts +327 -105
  13. package/dist/components/rel-calendar/README.md +5 -5
  14. package/dist/components/rel-calendar/rel-calendar.ts +61 -45
  15. package/dist/components/rel-card/README.md +6 -6
  16. package/dist/components/rel-card/rel-card.ts +88 -35
  17. package/dist/components/rel-cardmedia/README.md +9 -9
  18. package/dist/components/rel-cardmedia/rel-cardmedia.ts +27 -20
  19. package/dist/components/rel-carousel/README.md +3 -3
  20. package/dist/components/rel-carousel/rel-carousel.ts +174 -90
  21. package/dist/components/rel-chart/README.md +8 -8
  22. package/dist/components/rel-chart/rel-chart.ts +59 -41
  23. package/dist/components/rel-checkbox/README.md +7 -7
  24. package/dist/components/rel-checkbox/rel-checkbox.ts +61 -61
  25. package/dist/components/rel-chip/README.md +16 -16
  26. package/dist/components/rel-chip/rel-chip.ts +143 -111
  27. package/dist/components/rel-container/README.md +11 -11
  28. package/dist/components/rel-container/rel-container.ts +35 -9
  29. package/dist/components/rel-date-picker/README.md +43 -7
  30. package/dist/components/rel-date-picker/date-format.test.ts +43 -0
  31. package/dist/components/rel-date-picker/date-format.ts +47 -0
  32. package/dist/components/rel-date-picker/rel-date-picker.ts +257 -83
  33. package/dist/components/rel-divider/README.md +9 -9
  34. package/dist/components/rel-divider/rel-divider.ts +10 -9
  35. package/dist/components/rel-drawer/README.md +1 -1
  36. package/dist/components/rel-drawer/rel-drawer-footer.ts +1 -1
  37. package/dist/components/rel-drawer/rel-drawer.ts +56 -28
  38. package/dist/components/rel-kbd/README.md +20 -20
  39. package/dist/components/rel-kbd/rel-kbd.ts +119 -25
  40. package/dist/components/rel-layout/README.md +1 -1
  41. package/dist/components/rel-layout/rel-layout.ts +2 -2
  42. package/dist/components/rel-menu/README.md +15 -15
  43. package/dist/components/rel-menu/rel-menu.ts +155 -88
  44. package/dist/components/rel-menubar/README.md +20 -20
  45. package/dist/components/rel-menubar/rel-menubar.ts +47 -31
  46. package/dist/components/rel-nav-menu/README.md +21 -21
  47. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +17 -14
  48. package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +33 -14
  49. package/dist/components/rel-nav-menu/rel-nav-menu.ts +14 -7
  50. package/dist/components/rel-navbar/README.md +7 -7
  51. package/dist/components/rel-navbar/rel-navbar.ts +12 -11
  52. package/dist/components/rel-otp/README.md +3 -3
  53. package/dist/components/rel-otp/rel-otp.ts +46 -30
  54. package/dist/components/rel-pagination/README.md +18 -18
  55. package/dist/components/rel-pagination/rel-pagination.ts +141 -90
  56. package/dist/components/rel-popover/README.md +13 -13
  57. package/dist/components/rel-popover/rel-popover-content.ts +39 -14
  58. package/dist/components/rel-popover/rel-popover.ts +18 -7
  59. package/dist/components/rel-progress/README.md +5 -5
  60. package/dist/components/rel-progress/rel-progress.ts +16 -12
  61. package/dist/components/rel-radio/README.md +9 -9
  62. package/dist/components/rel-radio/rel-radio.ts +64 -50
  63. package/dist/components/rel-range-picker/rel-range-picker.ts +120 -117
  64. package/dist/components/rel-resizable/README.md +4 -4
  65. package/dist/components/rel-resizable/rel-resizable-group.ts +15 -4
  66. package/dist/components/rel-resizable/rel-resizable-handle.ts +13 -9
  67. package/dist/components/rel-resizable/rel-resizable-panel.ts +11 -0
  68. package/dist/components/rel-segmented-button/README.md +10 -10
  69. package/dist/components/rel-segmented-button/rel-segmented-button.ts +39 -40
  70. package/dist/components/rel-select/README.md +14 -14
  71. package/dist/components/rel-select/rel-select.ts +120 -188
  72. package/dist/components/rel-sheet/README.md +10 -10
  73. package/dist/components/rel-sheet/rel-sheet.ts +82 -23
  74. package/dist/components/rel-sidebar/README.md +5 -5
  75. package/dist/components/rel-sidebar/rel-sidebar.ts +75 -60
  76. package/dist/components/rel-skeleton/rel-skeleton.ts +8 -13
  77. package/dist/components/rel-slider/README.md +10 -10
  78. package/dist/components/rel-slider/rel-slider.ts +230 -57
  79. package/dist/components/rel-snackbar/README.md +9 -9
  80. package/dist/components/rel-snackbar/rel-snackbar.ts +44 -32
  81. package/dist/components/rel-stepper/README.md +10 -10
  82. package/dist/components/rel-stepper/rel-stepper.ts +110 -46
  83. package/dist/components/rel-switch/README.md +5 -5
  84. package/dist/components/rel-switch/rel-switch.ts +23 -12
  85. package/dist/components/rel-tab/README.md +13 -13
  86. package/dist/components/rel-tab/rel-tab.ts +73 -23
  87. package/dist/components/rel-table/README.md +7 -7
  88. package/dist/components/rel-table/rel-table-body.ts +2 -1
  89. package/dist/components/rel-table/rel-table-caption.ts +10 -7
  90. package/dist/components/rel-table/rel-table-cell.ts +7 -6
  91. package/dist/components/rel-table/rel-table-footer.ts +4 -3
  92. package/dist/components/rel-table/rel-table-head.ts +11 -9
  93. package/dist/components/rel-table/rel-table-header.ts +2 -1
  94. package/dist/components/rel-table/rel-table-row.ts +5 -4
  95. package/dist/components/rel-table/rel-table.ts +16 -7
  96. package/dist/components/rel-textfield/README.md +11 -11
  97. package/dist/components/rel-textfield/rel-textfield.ts +718 -397
  98. package/dist/components/rel-toast/README.md +11 -11
  99. package/dist/components/rel-toast/rel-toast.ts +33 -30
  100. package/dist/components/rel-tooltip/README.md +7 -7
  101. package/dist/components/rel-tooltip/rel-tooltip.ts +160 -123
  102. package/dist/components/rel-topbar/rel-topbar-actions.ts +5 -2
  103. package/dist/components/rel-topbar/rel-topbar-brand.ts +12 -5
  104. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +14 -8
  105. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +8 -5
  106. package/dist/components/rel-topbar/rel-topbar-item.ts +27 -22
  107. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +4 -1
  108. package/dist/components/rel-topbar/rel-topbar-profile.ts +71 -50
  109. package/dist/components/rel-topbar/rel-topbar-utilities.ts +5 -2
  110. package/dist/components/rel-topbar/rel-topbar.ts +37 -29
  111. package/dist/index.d.mts +227 -40
  112. package/dist/index.d.ts +227 -40
  113. package/dist/index.js +3930 -2247
  114. package/dist/index.js.map +1 -1
  115. package/dist/index.mjs +4311 -2629
  116. package/dist/index.mjs.map +1 -1
  117. package/dist/styles/vars.css +653 -346
  118. package/dist/vars-WY2XCF5G.css +653 -0
  119. package/package.json +3 -2
  120. package/dist/components/rel-range-picker/rel-range-picker.css +0 -349
  121. package/dist/styles/vars-v2.css +0 -525
  122. package/dist/vars-I2PC74XD.css +0 -346
  123. package/dist/vars-v2-VMGWTFJZ.css +0 -525
@@ -1,397 +1,718 @@
1
- import { LitElement, html, css } from "lit";
2
- import { customElement, property, state } from "lit/decorators.js";
3
- import "../../styles/vars-v2.css";
4
- import "@reliability-design/icons/rel-icon";
5
-
6
- export type TextFieldState = "default" | "error" | "disabled";
7
- export type TextFieldSize = "small" | "medium" | "large";
8
- export type TextFieldVariant = "default" | "underline";
9
-
10
- @customElement("rel-textfield")
11
- export class RelTextfield extends LitElement {
12
- @property({ type: String }) label?: string;
13
- @property({ type: String }) placeholder: string = "";
14
- @property({ type: String }) type: string = "text";
15
- @property({ type: String }) value: string = "";
16
- @property({ type: String }) leftIcon?: string;
17
- @property({ type: String }) rightIcon?: string;
18
- @property({ type: String }) supportingText?: string;
19
- @property({ type: String }) state: TextFieldState = "default";
20
- @property({ type: String }) size: TextFieldSize = "medium";
21
- @property({ type: Boolean, reflect: true }) floatingLabel = false;
22
- @property({ type: String, reflect: true }) variant: TextFieldVariant = "default";
23
-
24
- @state() private internalState: "default" | "focused" = "default";
25
- private _wasError = false;
26
-
27
- private uniqueId = `textfield-${Math.random().toString(36).slice(2)}`;
28
-
29
- static styles = css`
30
- :host {
31
- display: block;
32
- font-family: var(--rel-fontFamilies-primary);
33
- }
34
-
35
- /* Wrapper */
36
- .textfield-wrapper {
37
- display: flex;
38
- flex-direction: column;
39
- gap: var(--rel-spacing-space-2xs);
40
- width: 100%;
41
- }
42
-
43
- /* Label */
44
- .textfield-label {
45
- font-size: var(--rel-fontSizes-sm);
46
- font-weight: var(--rel-fontWeights-medium);
47
- color: var(--rel-color-text-heading);
48
- }
49
-
50
- /* Floating label variant */
51
- :host([floatinglabel]) .textfield-wrapper {
52
- gap: 0;
53
- }
54
-
55
- :host([floatinglabel]) .textfield-container {
56
- overflow: visible;
57
- position: relative;
58
- padding-top: 0;
59
- }
60
-
61
- :host([floatinglabel]) .textfield-label {
62
- position: absolute;
63
- left: var(--rel-spacing-space-sm);
64
- top: 50%;
65
- transform: translateY(-50%);
66
- font-size: var(--rel-fontSizes-md);
67
- font-weight: var(--rel-fontWeights-regular);
68
- color: var(--rel-color-text-placeholder);
69
- pointer-events: none;
70
- transition: top 0.15s ease, font-size 0.15s ease, color 0.15s ease, transform 0.15s ease;
71
- background: var(--rel-color-bg-primary);
72
- padding: 0 2px;
73
- z-index: 1;
74
- line-height: 1;
75
- white-space: nowrap;
76
- }
77
-
78
- :host([floatinglabel]) .textfield-wrapper.focused .textfield-label,
79
- :host([floatinglabel]) .textfield-wrapper.has-value .textfield-label {
80
- top: 0;
81
- transform: translateY(-50%);
82
- font-size: var(--rel-fontSizes-xs);
83
- color: var(--rel-color-text-primary);
84
- }
85
-
86
- :host([floatinglabel]) .textfield-wrapper.error .textfield-label {
87
- color: var(--rel-error-text);
88
- }
89
-
90
- :host([floatinglabel]) .textfield-wrapper.focused:not(.error) .textfield-label {
91
- color: var(--rel-color-text-primary);
92
- }
93
-
94
- :host([floatinglabel]) .textfield-wrapper.focused.error .textfield-label,
95
- :host([floatinglabel]) .textfield-wrapper.has-value.error .textfield-label {
96
- top: 0;
97
- transform: translateY(-50%);
98
- font-size: var(--rel-fontSizes-xs);
99
- color: var(--rel-error-text);
100
- }
101
-
102
- :host([floatinglabel]) .textfield-input {
103
- padding-top: var(--rel-spacing-space-sm);
104
- padding-bottom: var(--rel-spacing-space-sm);
105
- }
106
-
107
- :host([floatinglabel]) .textfield-input.has-left-icon {
108
- padding-left: var(--rel-spacing-space-2xl);
109
- }
110
-
111
- :host([floatinglabel]) .textfield-label.has-left-icon {
112
- left: var(--rel-spacing-space-2xl);
113
- }
114
-
115
- :host([floatinglabel]) .textfield-wrapper.focused .textfield-label.has-left-icon,
116
- :host([floatinglabel]) .textfield-wrapper.has-value .textfield-label.has-left-icon {
117
- left: var(--rel-spacing-space-sm);
118
- }
119
-
120
- :host([floatinglabel]) .textfield-wrapper.disabled .textfield-label {
121
- background: var(--rel-color-bg-tertiary);
122
- }
123
-
124
- /* Container */
125
- .textfield-container {
126
- display: flex;
127
- align-items: center;
128
- position: relative;
129
- border: 1px solid var(--rel-color-border-default);
130
- border-radius: var(--rel-borders-radius-border-radius-100);
131
- background: var(--rel-color-bg-primary);
132
- overflow: hidden;
133
- transition: border 0.2s;
134
- }
135
-
136
- .textfield-container.disabled {
137
- background: var(--rel-color-bg-tertiary);
138
- cursor: not-allowed;
139
- }
140
-
141
- /* Input */
142
- .textfield-input {
143
- flex: 1;
144
- border: none;
145
- outline: none;
146
- font-size: var(--rel-fontSizes-md);
147
- padding: var(--rel-spacing-space-xs) var(--rel-spacing-space-sm);
148
- background: transparent;
149
- font-family: var(--rel-fontFamilies-primary);
150
- color: var(--rel-color-text-primary);
151
- }
152
-
153
- .textfield-input:disabled {
154
- cursor: not-allowed;
155
- color: var(--rel-color-text-disabled);
156
- }
157
-
158
- .icon {
159
- position: absolute;
160
- display: flex;
161
- pointer-events: none;
162
- color: var(--rel-color-text-placeholder);
163
- }
164
-
165
- .icon.left {
166
- left: 8px;
167
- }
168
-
169
- .icon.right {
170
- right: 8px;
171
- }
172
-
173
- .textfield-wrapper.small .textfield-input {
174
- padding: var(--rel-spacing-space-xs) var(--rel-spacing-space-sm);
175
- font-size: var(--rel-fontSizes-sm);
176
- }
177
-
178
- .textfield-wrapper.medium .textfield-input {
179
- padding: var(--rel-spacing-space-sm) var(--rel-spacing-space-md);
180
- font-size: var(--rel-fontSizes-md);
181
- }
182
-
183
- .textfield-wrapper.large .textfield-input {
184
- padding: var(--rel-spacing-space-md) var(--rel-spacing-space-md);
185
- font-size: var(--rel-fontSizes-lg);
186
- }
187
-
188
- /* Adjust padding when icons are present */
189
- .textfield-wrapper .textfield-input.has-left-icon {
190
- padding-left: var(--rel-spacing-space-2xl);
191
- }
192
-
193
- .textfield-wrapper .textfield-input.has-right-icon {
194
- padding-right: var(--rel-spacing-space-2xl);
195
- }
196
-
197
- /* States */
198
- .textfield-wrapper.focused .textfield-container {
199
- border-color: var(--rel-color-text-primary);
200
- }
201
-
202
- .textfield-input:focus-visible {
203
- outline: none;
204
- }
205
-
206
- .textfield-wrapper.focused .textfield-container,
207
- .textfield-container:focus-within {
208
- border-color: var(--rel-color-text-primary);
209
- box-shadow: 0 0 0 1px var(--rel-color-text-primary);
210
- }
211
-
212
- .textfield-wrapper.error .textfield-container {
213
- border-color: var(--rel-error-surface-default);
214
- box-shadow: none;
215
- }
216
-
217
- .textfield-wrapper.disabled .textfield-container {
218
- border-color: var(--rel-color-border-default);
219
- }
220
-
221
- /* Supporting text */
222
- .textfield-support {
223
- font-size: var(--rel-fontSizes-xs);
224
- color: var(--rel-color-text-placeholder);
225
- }
226
-
227
- .textfield-support.error {
228
- color: var(--rel-error-surface-default, #CC0000);
229
- }
230
-
231
- /* Placeholder */
232
- .textfield-input::placeholder {
233
- color: var(--rel-color-text-placeholder);
234
- }
235
-
236
- :host([floatinglabel]) .textfield-input::placeholder {
237
- opacity: 0;
238
- }
239
-
240
- :host([floatinglabel]) .textfield-wrapper.focused .textfield-input::placeholder {
241
- opacity: 1;
242
- }
243
-
244
- /* Suppress Chrome autofill blue background */
245
- .textfield-input:-webkit-autofill,
246
- .textfield-input:-webkit-autofill:hover,
247
- .textfield-input:-webkit-autofill:focus {
248
- -webkit-box-shadow: 0 0 0px 1000px var(--rel-color-bg-primary) inset;
249
- -webkit-text-fill-color: var(--rel-color-text-primary);
250
- }
251
-
252
- /* ── Underline variant ── */
253
- :host([variant="underline"]) .textfield-container {
254
- border: none;
255
- border-bottom: 2px solid var(--rel-color-border-default);
256
- border-radius: 0;
257
- background: var(--rel-color-bg-tertiary, #f5f5f5);
258
- box-shadow: none;
259
- overflow: visible;
260
- }
261
-
262
- :host([variant="underline"]) .textfield-wrapper.focused .textfield-container,
263
- :host([variant="underline"]) .textfield-container:focus-within {
264
- border-bottom: 2px solid var(--rel-color-text-primary);
265
- box-shadow: none;
266
- }
267
-
268
- :host([variant="underline"]) .textfield-wrapper.error .textfield-container {
269
- border-bottom: 2px solid var(--rel-error-surface-default, #CC0000);
270
- box-shadow: none;
271
- }
272
-
273
- :host([variant="underline"]) .textfield-wrapper.disabled .textfield-container {
274
- border-bottom: 2px solid var(--rel-color-border-default);
275
- background: var(--rel-color-bg-tertiary);
276
- }
277
-
278
- /* Remove border radius on input for underline variant */
279
- :host([variant="underline"]) .textfield-input {
280
- background: transparent;
281
- }
282
-
283
- /* Dark mode — underline variant uses same border colors as default */
284
- :host-context([data-theme="dark"]) :host([variant="underline"]) .textfield-container,
285
- :host-context(.dark) :host([variant="underline"]) .textfield-container {
286
- background: var(--rel-color-bg-tertiary);
287
- border-bottom-color: var(--rel-color-border-default);
288
- }
289
- `;
290
-
291
- connectedCallback() {
292
- super.connectedCallback();
293
- const leftIcon = this.getAttribute('lefticon') || this.getAttribute('leftIcon');
294
- if (leftIcon) this.leftIcon = leftIcon;
295
- const rightIcon = this.getAttribute('righticon') || this.getAttribute('rightIcon');
296
- if (rightIcon) this.rightIcon = rightIcon;
297
- }
298
-
299
- private handleFocus() {
300
- if (this.state !== "disabled") {
301
- this.internalState = "focused";
302
- }
303
- }
304
-
305
- private handleBlur() {
306
- if (this.internalState === "focused") {
307
- this.internalState = "default";
308
- }
309
- }
310
-
311
- private handleInput(e: Event) {
312
- const input = e.target as HTMLInputElement;
313
- this.value = input.value;
314
- if (this.state === "error" && this.value) {
315
- this._wasError = true;
316
- this.state = "default";
317
- } else if (this._wasError && !this.value) {
318
- this._wasError = false;
319
- this.state = "error";
320
- }
321
- this.dispatchEvent(
322
- new CustomEvent<string>("rel-change", {
323
- detail: this.value,
324
- bubbles: true,
325
- composed: true,
326
- })
327
- );
328
- }
329
-
330
- render() {
331
- const wrapperClasses = [
332
- "textfield-wrapper",
333
- this.size,
334
- this.state,
335
- this.internalState === "focused" ? "focused" : "",
336
- this.value ? "has-value" : "",
337
- ].filter(Boolean).join(" ");
338
-
339
- const inputClasses = [
340
- "textfield-input",
341
- this.leftIcon ? "has-left-icon" : "",
342
- this.rightIcon ? "has-right-icon" : "",
343
- ]
344
- .filter(Boolean)
345
- .join(" ");
346
-
347
- return html`
348
- <div class="${wrapperClasses}">
349
- ${!this.floatingLabel && this.label
350
- ? html`<label class="textfield-label" for="${this.uniqueId}">${this.label}</label>`
351
- : ""}
352
-
353
- <div
354
- class="textfield-container ${this.state === "disabled"
355
- ? "disabled"
356
- : ""}"
357
- >
358
- ${this.floatingLabel && this.label
359
- ? html`<label
360
- class="textfield-label${this.leftIcon ? ' has-left-icon' : ''}"
361
- for="${this.uniqueId}">${this.label}</label>`
362
- : ""}
363
- ${this.leftIcon
364
- ? html`<rel-icon name="${this.leftIcon}" size="sm" class="icon left"></rel-icon>`
365
- : ""}
366
-
367
- <input
368
- type="${this.type}"
369
- id="${this.uniqueId}"
370
- class="${inputClasses}"
371
- placeholder="${this.placeholder}"
372
- .value="${this.value}"
373
- ?disabled="${this.state === "disabled"}"
374
- aria-invalid="${this.state === "error"}"
375
- @focus="${this.handleFocus}"
376
- @blur="${this.handleBlur}"
377
- @input="${this.handleInput}"
378
- />
379
-
380
- ${this.rightIcon
381
- ? html`<rel-icon name="${this.rightIcon}" size="sm" class="icon right"></rel-icon>`
382
- : ""}
383
- </div>
384
-
385
- ${this.supportingText
386
- ? html`<div
387
- class="textfield-support ${this.state === "error"
388
- ? "error"
389
- : ""}"
390
- >
391
- ${this.supportingText}
392
- </div>`
393
- : ""}
394
- </div>
395
- `;
396
- }
397
- }
1
+ import { LitElement, html, css, nothing } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+ import "@reliability-design/icons/rel-icon";
4
+
5
+ export type TextFieldState = "default" | "error" | "disabled";
6
+ export type TextFieldSize = "small" | "medium" | "large";
7
+ export type TextFieldVariant = "default" | "underline" | "legend";
8
+
9
+ @customElement("rel-textfield")
10
+ export class RelTextfield extends LitElement {
11
+ @property({ type: String }) label?: string;
12
+ @property({ type: String }) placeholder: string = "";
13
+ @property({ type: String }) type: string = "text";
14
+ @property({ type: String }) value: string = "";
15
+ @property({ type: String }) leftIcon?: string;
16
+ @property({ type: String }) rightIcon?: string;
17
+ @property({ type: String }) supportingText?: string;
18
+ @property({ type: String }) state: TextFieldState = "default";
19
+ @property({ type: String }) size: TextFieldSize = "medium";
20
+ @property({ type: Boolean, reflect: true }) floatingLabel = false;
21
+ @property({ type: String, reflect: true }) variant: TextFieldVariant = "default";
22
+ @property({ type: Boolean }) disabled = false;
23
+ @property({ type: Boolean }) required = false;
24
+ @property({ type: Boolean, reflect: true }) multiline = false;
25
+ @property({ type: Number }) rows = 3;
26
+
27
+ // ── Standardized input API (senior-team spec) ──────────────────────────────
28
+ // Prefer these over the deprecated aliases: prefixIcon (was leftIcon),
29
+ // helperText (was supportingText), error (was state="error").
30
+ @property({ type: String }) prefixIcon?: string;
31
+ @property({ type: String }) prefixIconAriaLabel?: string;
32
+ @property({ type: Boolean }) error = false;
33
+ @property({ type: String }) errorMessage?: string;
34
+ @property({ type: String }) helperText?: string;
35
+ @property({ type: Boolean }) readonly = false;
36
+ @property({ type: Number }) maxLength?: number;
37
+ @property({ type: String }) name?: string;
38
+ @property({ type: Boolean, reflect: true }) fullWidth = true;
39
+
40
+ @state() private internalState: "default" | "focused" = "default";
41
+ private _wasError = false;
42
+
43
+ private uniqueId = `textfield-${Math.random().toString(36).slice(2)}`;
44
+
45
+ private get isDisabled() {
46
+ return this.disabled || this.state === "disabled";
47
+ }
48
+
49
+ private get isError() {
50
+ return this.error || this.state === "error";
51
+ }
52
+
53
+ // prefixIcon supersedes the deprecated leftIcon
54
+ private get leadingIcon() {
55
+ return this.prefixIcon ?? this.leftIcon;
56
+ }
57
+
58
+ // helperText supersedes the deprecated supportingText
59
+ private get helper() {
60
+ return this.helperText ?? this.supportingText;
61
+ }
62
+
63
+ static styles = css`
64
+ :host {
65
+ color: var(--rel-text-default);
66
+ display: block;
67
+ font-family: var(--rel-fonts-font-family);
68
+ }
69
+
70
+ /* Wrapper */
71
+ .textfield-wrapper {
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: var(--rel-spacing-2xs);
75
+ width: 100%;
76
+ }
77
+
78
+ /* Label */
79
+ .textfield-label {
80
+ color: var(--rel-text-label);
81
+
82
+ font-family: var(--rel-body-sm-medium-font-family);
83
+
84
+ font-weight: var(--rel-body-sm-medium-font-weight);
85
+
86
+ font-size: var(--rel-body-sm-medium-font-size);
87
+
88
+ line-height: var(--rel-body-sm-medium-line-height);
89
+
90
+ letter-spacing: var(--rel-body-sm-medium-letter-spacing);
91
+
92
+ }
93
+
94
+ .required-mark {
95
+ color: var(--rel-text-danger);
96
+ margin-left: 2px;
97
+ }
98
+
99
+ /* Floating label variant */
100
+ :host([floatinglabel]) .textfield-wrapper {
101
+ gap: 0;
102
+ }
103
+
104
+ :host([floatinglabel]) .textfield-container {
105
+ overflow: visible;
106
+ position: relative;
107
+ padding-top: 0;
108
+ }
109
+
110
+ :host([floatinglabel]) .textfield-label {
111
+ position: absolute;
112
+ left: var(--rel-spacing-sm);
113
+ top: 50%;
114
+ transform: translateY(-50%);
115
+ color: var(--rel-text-placeholder);
116
+ pointer-events: none;
117
+ transition: top 0.15s ease, font-size 0.15s ease, color 0.15s ease, transform 0.15s ease;
118
+ background: var(--rel-bg-neutral);
119
+ padding: 0 2px;
120
+ z-index: 1;
121
+ white-space: nowrap;
122
+ font-family: var(--rel-body-md-regular-font-family);
123
+ font-weight: var(--rel-body-md-regular-font-weight);
124
+ font-size: var(--rel-body-md-regular-font-size);
125
+ line-height: var(--rel-body-md-regular-line-height);
126
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
127
+ }
128
+
129
+ /* Floating label in multiline mode anchors to top instead of vertical center */
130
+ :host([floatinglabel][multiline]) .textfield-label {
131
+ top: var(--rel-spacing-sm);
132
+ transform: none;
133
+ }
134
+
135
+ :host([floatinglabel]) .textfield-wrapper.focused .textfield-label,
136
+ :host([floatinglabel]) .textfield-wrapper.has-value .textfield-label {
137
+ top: 0;
138
+ transform: translateY(-50%);
139
+ color: var(--rel-text-default);
140
+ font-family: var(--rel-body-xs-regular-font-family);
141
+ font-weight: var(--rel-body-xs-regular-font-weight);
142
+ font-size: var(--rel-body-xs-regular-font-size);
143
+ line-height: var(--rel-body-xs-regular-line-height);
144
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
145
+ }
146
+
147
+ :host([floatinglabel]) .textfield-wrapper.error .textfield-label {
148
+ color: var(--rel-text-danger);
149
+ }
150
+
151
+ :host([floatinglabel]) .textfield-wrapper.focused:not(.error) .textfield-label {
152
+ color: var(--rel-text-default);
153
+ }
154
+
155
+ :host([floatinglabel]) .textfield-wrapper.focused.error .textfield-label,
156
+ :host([floatinglabel]) .textfield-wrapper.has-value.error .textfield-label {
157
+ top: 0;
158
+ transform: translateY(-50%);
159
+ color: var(--rel-text-danger);
160
+ font-family: var(--rel-body-xs-regular-font-family);
161
+ font-weight: var(--rel-body-xs-regular-font-weight);
162
+ font-size: var(--rel-body-xs-regular-font-size);
163
+ line-height: var(--rel-body-xs-regular-line-height);
164
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
165
+ }
166
+
167
+ :host([floatinglabel]) .textfield-input {
168
+ padding-top: var(--rel-spacing-sm);
169
+ padding-bottom: var(--rel-spacing-sm);
170
+ }
171
+
172
+ :host([floatinglabel]) .textfield-input.has-left-icon {
173
+ padding-left: var(--rel-spacing-2xl);
174
+ }
175
+
176
+ :host([floatinglabel]) .textfield-label.has-left-icon {
177
+ left: var(--rel-spacing-2xl);
178
+ }
179
+
180
+ :host([floatinglabel]) .textfield-wrapper.focused .textfield-label.has-left-icon,
181
+ :host([floatinglabel]) .textfield-wrapper.has-value .textfield-label.has-left-icon {
182
+ left: var(--rel-spacing-sm);
183
+ }
184
+
185
+ :host([floatinglabel]) .textfield-wrapper.disabled .textfield-label {
186
+ background: var(--rel-bg-neutral-elevated-1);
187
+ }
188
+
189
+ /* Container */
190
+ .textfield-container {
191
+ display: flex;
192
+ align-items: center;
193
+ position: relative;
194
+ border: 1px solid var(--rel-border-default);
195
+ border-radius: var(--rel-border-radius-xs);
196
+ background: var(--rel-bg-neutral);
197
+ overflow: hidden;
198
+ transition: border 0.2s;
199
+ }
200
+
201
+ .textfield-container.disabled {
202
+ background: var(--rel-bg-neutral-elevated-1);
203
+ cursor: not-allowed;
204
+ }
205
+
206
+ .textfield-container.multiline {
207
+ overflow: visible;
208
+ align-items: flex-start;
209
+ }
210
+
211
+ /* Input */
212
+ .textfield-input {
213
+ flex: 1;
214
+ border: none;
215
+ outline: none;
216
+ padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
217
+ background: transparent;
218
+ color: var(--rel-text-body);
219
+ font-family: var(--rel-body-md-regular-font-family);
220
+ font-weight: var(--rel-body-md-regular-font-weight);
221
+ font-size: var(--rel-body-md-regular-font-size);
222
+ line-height: var(--rel-body-md-regular-line-height);
223
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
224
+ }
225
+
226
+ .textfield-input:disabled {
227
+ cursor: not-allowed;
228
+ color: var(--rel-text-disabled);
229
+ }
230
+
231
+ /* Textarea */
232
+ textarea.textfield-input {
233
+ resize: vertical;
234
+ min-height: 80px;
235
+ width: 100%;
236
+ box-sizing: border-box;
237
+ }
238
+
239
+ textarea.textfield-input::-webkit-scrollbar {
240
+ width: 4px;
241
+ }
242
+ textarea.textfield-input::-webkit-scrollbar-track {
243
+ background: transparent;
244
+ }
245
+ textarea.textfield-input::-webkit-scrollbar-thumb {
246
+ background: var(--rel-text-placeholder);
247
+ border-radius: 2px;
248
+ }
249
+
250
+ .icon {
251
+ position: absolute;
252
+ display: flex;
253
+ pointer-events: none;
254
+ color: var(--rel-text-placeholder);
255
+ }
256
+
257
+ .icon.left {
258
+ left: 8px;
259
+ }
260
+
261
+ .icon.right {
262
+ right: 8px;
263
+ }
264
+
265
+ .textfield-wrapper.small .textfield-support {
266
+ font-weight: var(--rel-body-xs-medium-font-weight);
267
+ }
268
+
269
+ .textfield-wrapper.small .textfield-input {
270
+ padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
271
+ font-family: var(--rel-body-sm-regular-font-family);
272
+ font-weight: var(--rel-body-sm-regular-font-weight);
273
+ font-size: var(--rel-body-sm-regular-font-size);
274
+ line-height: var(--rel-body-sm-regular-line-height);
275
+ letter-spacing: var(--rel-body-sm-regular-letter-spacing);
276
+ }
277
+
278
+ .textfield-wrapper.medium .textfield-input {
279
+ padding: var(--rel-spacing-sm) var(--rel-spacing-md);
280
+ font-family: var(--rel-body-md-regular-font-family);
281
+ font-weight: var(--rel-body-md-regular-font-weight);
282
+ font-size: var(--rel-body-md-regular-font-size);
283
+ line-height: var(--rel-body-md-regular-line-height);
284
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
285
+ }
286
+
287
+ .textfield-wrapper.large .textfield-input {
288
+ padding: var(--rel-spacing-md) var(--rel-spacing-md);
289
+ font-family: var(--rel-body-lg-regular-font-family);
290
+ font-weight: var(--rel-body-lg-regular-font-weight);
291
+ font-size: var(--rel-body-lg-regular-font-size);
292
+ line-height: var(--rel-body-lg-regular-line-height);
293
+ letter-spacing: var(--rel-body-lg-regular-letter-spacing);
294
+ }
295
+
296
+ /* Adjust padding when icons are present */
297
+ .textfield-wrapper .textfield-input.has-left-icon {
298
+ padding-left: var(--rel-spacing-2xl);
299
+ }
300
+
301
+ .textfield-wrapper .textfield-input.has-right-icon {
302
+ padding-right: var(--rel-spacing-2xl);
303
+ }
304
+
305
+ /* States */
306
+ /* Hover (Figma State=Hover): border-strong at 1px — no focus ring */
307
+ .textfield-wrapper:not(.focused):not(.error):not(.disabled) .textfield-container:hover {
308
+ border-color: var(--rel-border-strong);
309
+ }
310
+
311
+ .textfield-wrapper.focused .textfield-container {
312
+ border-color: var(--rel-text-default);
313
+ }
314
+
315
+ .textfield-input:focus-visible {
316
+ outline: none;
317
+ }
318
+
319
+ .textfield-wrapper.focused .textfield-container,
320
+ .textfield-container:focus-within {
321
+ border-color: var(--rel-border-strong);
322
+ box-shadow: 0 0 0 1px var(--rel-border-strong);
323
+ }
324
+
325
+ .textfield-wrapper.error .textfield-container,
326
+ .textfield-wrapper.error .textfield-container:focus-within {
327
+ border-color: var(--rel-border-danger);
328
+ box-shadow: none;
329
+ }
330
+
331
+ .textfield-wrapper.disabled .textfield-container {
332
+ border-color: var(--rel-border-default);
333
+ }
334
+
335
+ .textfield-wrapper.disabled .textfield-label {
336
+ color: var(--rel-text-disabled);
337
+ }
338
+
339
+ /* Supporting text */
340
+ .textfield-support {
341
+ color: var(--rel-text-caption);
342
+
343
+ font-family: var(--rel-body-xs-regular-font-family);
344
+
345
+ font-weight: var(--rel-body-xs-regular-font-weight);
346
+
347
+ font-size: var(--rel-body-xs-regular-font-size);
348
+
349
+ line-height: var(--rel-body-xs-regular-line-height);
350
+
351
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
352
+
353
+ }
354
+
355
+ .textfield-support.error {
356
+ color: var(--rel-text-danger);
357
+ }
358
+
359
+ /* Placeholder */
360
+ .textfield-input::placeholder {
361
+ color: var(--rel-text-placeholder);
362
+ }
363
+
364
+ :host([floatinglabel]) .textfield-input::placeholder {
365
+ opacity: 0;
366
+ }
367
+
368
+ :host([floatinglabel]) .textfield-wrapper.focused .textfield-input::placeholder {
369
+ opacity: 1;
370
+ }
371
+
372
+ /* Suppress Chrome autofill blue background */
373
+ .textfield-input:-webkit-autofill,
374
+ .textfield-input:-webkit-autofill:hover,
375
+ .textfield-input:-webkit-autofill:focus {
376
+ -webkit-box-shadow: 0 0 0px 1000px var(--rel-bg-surface) inset;
377
+ -webkit-text-fill-color: var(--rel-text-default);
378
+ }
379
+
380
+ /* ── Underline variant ── */
381
+ :host([variant="underline"]) .textfield-container {
382
+ border: none;
383
+ border-bottom: 2px solid var(--rel-border-default);
384
+ border-radius: 0;
385
+ background: var(--rel-bg-neutral-elevated-1);
386
+ box-shadow: none;
387
+ overflow: visible;
388
+ }
389
+
390
+ :host([variant="underline"]) .textfield-wrapper:not(.focused):not(.error):not(.disabled) .textfield-container:hover {
391
+ border-bottom-color: var(--rel-border-strong);
392
+ }
393
+
394
+ :host([variant="underline"]) .textfield-wrapper.focused .textfield-container,
395
+ :host([variant="underline"]) .textfield-container:focus-within {
396
+ border-bottom: 2px solid var(--rel-text-default);
397
+ box-shadow: none;
398
+ }
399
+
400
+ :host([variant="underline"]) .textfield-wrapper.error .textfield-container {
401
+ border-bottom: 2px solid var(--rel-border-danger);
402
+ box-shadow: none;
403
+ }
404
+
405
+ :host([variant="underline"]) .textfield-wrapper.disabled .textfield-container {
406
+ border-bottom: 2px solid var(--rel-border-default);
407
+ background: var(--rel-bg-neutral-elevated-1);
408
+ }
409
+
410
+ /* Remove border radius on input for underline variant */
411
+ :host([variant="underline"]) .textfield-input {
412
+ background: transparent;
413
+ }
414
+
415
+ /* Dark mode — underline variant uses same border colors as default */
416
+ :host-context([data-theme="dark"]) :host([variant="underline"]) .textfield-container,
417
+ :host-context(.dark) :host([variant="underline"]) .textfield-container {
418
+ background: var(--rel-bg-neutral-elevated-1);
419
+ border-bottom-color: var(--rel-border-default);
420
+ }
421
+
422
+ /* ── Full-width control (fullWidth defaults true → block, fills parent) ── */
423
+ :host(:not([fullwidth])) {
424
+ display: inline-block;
425
+ width: auto;
426
+ }
427
+ :host(:not([fullwidth])) .textfield-wrapper {
428
+ width: auto;
429
+ }
430
+
431
+ /* ── Legend (notched-outline) variant ──
432
+ Native <fieldset>/<legend>: the legend notches the top border and stays
433
+ fixed in place across all states (no float/animation). */
434
+ :host([variant="legend"]) .field-fieldset {
435
+ position: relative;
436
+ margin: 0;
437
+ min-inline-size: 0;
438
+ padding: 0;
439
+ border: 1px solid var(--rel-border-default);
440
+ border-radius: var(--rel-border-radius-xs);
441
+ background: var(--rel-bg-neutral);
442
+ transition: border-color 0.2s;
443
+ }
444
+
445
+ :host([variant="legend"]) .field-legend {
446
+ margin-left: var(--rel-spacing-sm);
447
+ padding: 0 var(--rel-spacing-2xs);
448
+ color: var(--rel-text-label);
449
+ font-family: var(--rel-body-sm-medium-font-family);
450
+ font-weight: var(--rel-body-sm-medium-font-weight);
451
+ font-size: var(--rel-body-sm-medium-font-size);
452
+ line-height: var(--rel-body-sm-medium-line-height);
453
+ letter-spacing: var(--rel-body-sm-medium-letter-spacing);
454
+ }
455
+
456
+ :host([variant="legend"]) .textfield-input {
457
+ width: 100%;
458
+ box-sizing: border-box;
459
+ }
460
+
461
+ /* Prefix icon (legend): vertically centered; input inset via has-left-icon */
462
+ :host([variant="legend"]) .icon.left {
463
+ top: 50%;
464
+ left: var(--rel-spacing-sm);
465
+ transform: translateY(-50%);
466
+ }
467
+ :host([variant="legend"]) .field-fieldset.multiline .icon.left {
468
+ top: var(--rel-spacing-sm);
469
+ transform: none;
470
+ }
471
+
472
+ /* Hover */
473
+ :host([variant="legend"]) .field-fieldset:hover {
474
+ border-color: var(--rel-border-accent);
475
+ border-color: var(--rel-text-placeholder);
476
+ }
477
+
478
+ /* Focus — border-color only. A box-shadow/outline ring hugs the full
479
+ rectangular border-box and would draw across the notch, appearing to
480
+ strike through the legend. The legend also recolors on focus (below). */
481
+ :host([variant="legend"]) .textfield-wrapper.focused .field-fieldset,
482
+ :host([variant="legend"]) .field-fieldset:focus-within {
483
+ border-color: var(--rel-border-focus);
484
+ box-shadow: none;
485
+ }
486
+ :host([variant="legend"]) .textfield-wrapper.focused .field-legend,
487
+ :host([variant="legend"]) .field-fieldset:focus-within .field-legend {
488
+ color: var(--rel-border-focus);
489
+ }
490
+
491
+ /* Error */
492
+ :host([variant="legend"]) .textfield-wrapper.error .field-fieldset {
493
+ border-color: var(--rel-border-danger);
494
+ box-shadow: none;
495
+ }
496
+ :host([variant="legend"]) .textfield-wrapper.error .field-legend {
497
+ color: var(--rel-text-danger);
498
+ }
499
+
500
+ /* Disabled */
501
+ :host([variant="legend"]) .textfield-wrapper.disabled .field-fieldset {
502
+ background: var(--rel-bg-neutral-elevated-1);
503
+ border-color: var(--rel-border-default);
504
+ cursor: not-allowed;
505
+ }
506
+ :host([variant="legend"]) .textfield-wrapper.disabled .field-legend {
507
+ color: var(--rel-text-disabled);
508
+ }
509
+
510
+ /* Readonly */
511
+ :host([variant="legend"]) .textfield-wrapper.readonly .field-fieldset {
512
+ cursor: default;
513
+ }
514
+
515
+ /* Legend font tracks size */
516
+ :host([variant="legend"]) .textfield-wrapper.small .field-legend {
517
+
518
+ font-family: var(--rel-body-xs-regular-font-family);
519
+
520
+ font-weight: var(--rel-body-xs-regular-font-weight);
521
+
522
+ font-size: var(--rel-body-xs-regular-font-size);
523
+
524
+ line-height: var(--rel-body-xs-regular-line-height);
525
+
526
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
527
+
528
+ }
529
+ :host([variant="legend"]) .textfield-wrapper.large .field-legend {
530
+
531
+ font-family: var(--rel-body-md-regular-font-family);
532
+
533
+ font-weight: var(--rel-body-md-regular-font-weight);
534
+
535
+ font-size: var(--rel-body-md-regular-font-size);
536
+
537
+ line-height: var(--rel-body-md-regular-line-height);
538
+
539
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
540
+
541
+ }
542
+ `;
543
+
544
+ connectedCallback() {
545
+ super.connectedCallback();
546
+ const leftIcon = this.getAttribute('lefticon') || this.getAttribute('leftIcon');
547
+ if (leftIcon) this.leftIcon = leftIcon;
548
+ const rightIcon = this.getAttribute('righticon') || this.getAttribute('rightIcon');
549
+ if (rightIcon) this.rightIcon = rightIcon;
550
+ }
551
+
552
+ private handleFocus() {
553
+ if (!this.isDisabled) {
554
+ this.internalState = "focused";
555
+ }
556
+ }
557
+
558
+ private handleBlur() {
559
+ if (this.internalState === "focused") {
560
+ this.internalState = "default";
561
+ }
562
+ }
563
+
564
+ private handleInput(e: Event) {
565
+ const input = e.target as HTMLInputElement;
566
+ this.value = input.value;
567
+ if (this.state === "error" && this.value) {
568
+ this._wasError = true;
569
+ this.state = "default";
570
+ } else if (this._wasError && !this.value) {
571
+ this._wasError = false;
572
+ this.state = "error";
573
+ }
574
+ this.dispatchEvent(
575
+ new CustomEvent<string>("rel-change", {
576
+ detail: this.value,
577
+ bubbles: true,
578
+ composed: true,
579
+ })
580
+ );
581
+ }
582
+
583
+ render() {
584
+ const isError = this.isError;
585
+ const stateClass = this.isDisabled ? "disabled" : isError ? "error" : "default";
586
+ const leadingIcon = this.leadingIcon;
587
+ const inputId = this.id || this.uniqueId;
588
+ const legendId = `${inputId}-legend`;
589
+
590
+ // errorMessage replaces the helper only when in error AND explicitly set;
591
+ // otherwise the (deprecated) supportingText/helperText shows as before.
592
+ const belowText = isError && this.errorMessage ? this.errorMessage : this.helper;
593
+ const describedById = belowText ? `${inputId}-desc` : undefined;
594
+
595
+ const wrapperClasses = [
596
+ "textfield-wrapper",
597
+ this.size,
598
+ stateClass,
599
+ this.readonly ? "readonly" : "",
600
+ this.internalState === "focused" ? "focused" : "",
601
+ this.value ? "has-value" : "",
602
+ ].filter(Boolean).join(" ");
603
+
604
+ const inputClasses = [
605
+ "textfield-input",
606
+ !this.multiline && leadingIcon ? "has-left-icon" : "",
607
+ !this.multiline && this.rightIcon && this.variant !== "legend" ? "has-right-icon" : "",
608
+ ]
609
+ .filter(Boolean)
610
+ .join(" ");
611
+
612
+ const labelTemplate = (extraClass = "") => {
613
+ if (!this.label) return "";
614
+ return html`<label
615
+ part="label"
616
+ class="textfield-label${extraClass}"
617
+ for="${inputId}"
618
+ >${this.label}${this.required ? html`<span class="required-mark" aria-hidden="true">*</span>` : ""}</label>`;
619
+ };
620
+
621
+ const ariaLabelledby = this.variant === "legend" && this.label ? legendId : undefined;
622
+
623
+ const inputTemplate = this.multiline
624
+ ? html`<textarea part="input"
625
+ id="${inputId}"
626
+ class="${inputClasses}"
627
+ placeholder="${this.placeholder}"
628
+ .value="${this.value}"
629
+ rows="${this.rows}"
630
+ name="${this.name ?? nothing}"
631
+ maxlength="${this.maxLength ?? nothing}"
632
+ ?disabled="${this.isDisabled}"
633
+ ?readonly="${this.readonly}"
634
+ ?required="${this.required}"
635
+ aria-invalid="${isError}"
636
+ aria-required="${this.required}"
637
+ aria-labelledby="${ariaLabelledby ?? nothing}"
638
+ aria-describedby="${describedById ?? nothing}"
639
+ @focus="${this.handleFocus}"
640
+ @blur="${this.handleBlur}"
641
+ @input="${this.handleInput}"
642
+ ></textarea>`
643
+ : html`<input part="input"
644
+ type="${this.type}"
645
+ id="${inputId}"
646
+ class="${inputClasses}"
647
+ placeholder="${this.placeholder}"
648
+ .value="${this.value}"
649
+ name="${this.name ?? nothing}"
650
+ maxlength="${this.maxLength ?? nothing}"
651
+ ?disabled="${this.isDisabled}"
652
+ ?readonly="${this.readonly}"
653
+ ?required="${this.required}"
654
+ aria-invalid="${isError}"
655
+ aria-required="${this.required}"
656
+ aria-labelledby="${ariaLabelledby ?? nothing}"
657
+ aria-describedby="${describedById ?? nothing}"
658
+ @focus="${this.handleFocus}"
659
+ @blur="${this.handleBlur}"
660
+ @input="${this.handleInput}"
661
+ />`;
662
+
663
+ const leadingIconTemplate = leadingIcon
664
+ ? html`<rel-icon
665
+ name="${leadingIcon}"
666
+ size="sm"
667
+ class="icon left"
668
+ role="${this.prefixIconAriaLabel ? "img" : nothing}"
669
+ aria-label="${this.prefixIconAriaLabel ?? nothing}"
670
+ aria-hidden="${this.prefixIconAriaLabel ? nothing : "true"}"
671
+ ></rel-icon>`
672
+ : "";
673
+
674
+ const belowTemplate = belowText
675
+ ? html`<div part="helper" class="textfield-support ${isError ? "error" : ""}" id="${describedById}">
676
+ ${belowText}
677
+ </div>`
678
+ : "";
679
+
680
+ if (this.variant === "legend") {
681
+ const fieldsetClass = ["field-fieldset", this.multiline ? "multiline" : ""].filter(Boolean).join(" ");
682
+ // The <legend> MUST be the fieldset's first child with no preceding
683
+ // whitespace/text or binding-comment, otherwise the browser won't cut the
684
+ // notch in the top border. So it's rendered flush against the tag open and
685
+ // is always present (only its content is dynamic).
686
+ return html`
687
+ <div class="${wrapperClasses}">
688
+ <fieldset part="control" class="${fieldsetClass}"><legend class="field-legend" id="${legendId}">${this.label ?? ""}${this.required ? html`<span class="required-mark" aria-hidden="true">*</span>` : ""}</legend>
689
+ ${!this.multiline ? leadingIconTemplate : ""}
690
+ ${inputTemplate}
691
+ </fieldset>
692
+ ${belowTemplate}
693
+ </div>
694
+ `;
695
+ }
696
+
697
+ return html`
698
+ <div class="${wrapperClasses}">
699
+ ${!this.floatingLabel ? labelTemplate() : ""}
700
+
701
+ <div part="control" class="${["textfield-container", this.isDisabled ? "disabled" : "", this.multiline ? "multiline" : ""].filter(Boolean).join(" ")}">
702
+ ${this.floatingLabel
703
+ ? labelTemplate(leadingIcon && !this.multiline ? " has-left-icon" : "")
704
+ : ""}
705
+ ${!this.multiline ? leadingIconTemplate : ""}
706
+
707
+ ${inputTemplate}
708
+
709
+ ${!this.multiline && this.rightIcon
710
+ ? html`<rel-icon name="${this.rightIcon}" size="sm" class="icon right"></rel-icon>`
711
+ : ""}
712
+ </div>
713
+
714
+ ${belowTemplate}
715
+ </div>
716
+ `;
717
+ }
718
+ }