@nuvia-ui/components 4.0.1

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 (230) hide show
  1. package/package.json +27 -0
  2. package/src/ds-accordion/ds-accordion-item.js +288 -0
  3. package/src/ds-accordion/ds-accordion-item.stories.js +82 -0
  4. package/src/ds-accordion/ds-accordion.a11y.test.js +92 -0
  5. package/src/ds-accordion/ds-accordion.js +68 -0
  6. package/src/ds-accordion/ds-accordion.stories.js +118 -0
  7. package/src/ds-accordion/ds-accordion.test.js +146 -0
  8. package/src/ds-accordion/index.js +2 -0
  9. package/src/ds-action-bar/ds-action-bar.js +116 -0
  10. package/src/ds-action-bar/ds-action-bar.stories.js +86 -0
  11. package/src/ds-action-bar/ds-action-bar.test.js +64 -0
  12. package/src/ds-action-bar/index.js +1 -0
  13. package/src/ds-alert/ds-alert.a11y.test.js +151 -0
  14. package/src/ds-alert/ds-alert.js +223 -0
  15. package/src/ds-alert/ds-alert.mdx +142 -0
  16. package/src/ds-alert/ds-alert.stories.js +166 -0
  17. package/src/ds-alert/ds-alert.test.js +256 -0
  18. package/src/ds-alert/index.js +1 -0
  19. package/src/ds-avatar/ds-avatar.a11y.test.js +45 -0
  20. package/src/ds-avatar/ds-avatar.js +216 -0
  21. package/src/ds-avatar/ds-avatar.stories.js +120 -0
  22. package/src/ds-avatar/ds-avatar.test.js +83 -0
  23. package/src/ds-avatar/index.js +1 -0
  24. package/src/ds-avatar-extended/ds-avatar-extended.a11y.test.js +29 -0
  25. package/src/ds-avatar-extended/ds-avatar-extended.js +108 -0
  26. package/src/ds-avatar-extended/ds-avatar-extended.stories.js +93 -0
  27. package/src/ds-avatar-extended/ds-avatar-extended.test.js +66 -0
  28. package/src/ds-avatar-extended/index.js +1 -0
  29. package/src/ds-banner/ds-banner.a11y.test.js +51 -0
  30. package/src/ds-banner/ds-banner.js +233 -0
  31. package/src/ds-banner/ds-banner.stories.js +185 -0
  32. package/src/ds-banner/ds-banner.test.js +116 -0
  33. package/src/ds-banner/index.js +1 -0
  34. package/src/ds-breadcrumb-item/ds-breadcrumb-item.js +135 -0
  35. package/src/ds-breadcrumb-item/ds-breadcrumb-item.stories.js +49 -0
  36. package/src/ds-breadcrumb-item/ds-breadcrumb-item.test.js +55 -0
  37. package/src/ds-breadcrumbs/ds-breadcrumbs.js +194 -0
  38. package/src/ds-breadcrumbs/ds-breadcrumbs.stories.js +54 -0
  39. package/src/ds-breadcrumbs/ds-breadcrumbs.test.js +33 -0
  40. package/src/ds-button/ds-button.a11y.test.js +49 -0
  41. package/src/ds-button/ds-button.js +205 -0
  42. package/src/ds-button/ds-button.mdx +141 -0
  43. package/src/ds-button/ds-button.stories.js +152 -0
  44. package/src/ds-button/ds-button.test.js +62 -0
  45. package/src/ds-button/index.js +1 -0
  46. package/src/ds-button-group/ds-button-group.js +82 -0
  47. package/src/ds-button-group/ds-button-group.mdx +39 -0
  48. package/src/ds-button-group/ds-button-group.stories.js +47 -0
  49. package/src/ds-button-group/ds-button-group.test.js +47 -0
  50. package/src/ds-button-group/index.js +1 -0
  51. package/src/ds-checkbox/ds-checkbox.a11y.test.js +79 -0
  52. package/src/ds-checkbox/ds-checkbox.js +271 -0
  53. package/src/ds-checkbox/ds-checkbox.stories.js +77 -0
  54. package/src/ds-checkbox/ds-checkbox.test.js +191 -0
  55. package/src/ds-checkbox/index.js +1 -0
  56. package/src/ds-checkbox-group/ds-checkbox-group.a11y.test.js +146 -0
  57. package/src/ds-checkbox-group/ds-checkbox-group.js +235 -0
  58. package/src/ds-checkbox-group/ds-checkbox-group.stories.js +210 -0
  59. package/src/ds-checkbox-group/ds-checkbox-group.test.js +150 -0
  60. package/src/ds-checkbox-group/index.js +1 -0
  61. package/src/ds-dialog/ds-dialog.js +466 -0
  62. package/src/ds-dialog/ds-dialog.stories.js +274 -0
  63. package/src/ds-dialog/ds-dialog.test.js +441 -0
  64. package/src/ds-dialog/index.js +1 -0
  65. package/src/ds-dropdown/ds-dropdown.a11y.test.js +80 -0
  66. package/src/ds-dropdown/ds-dropdown.js +891 -0
  67. package/src/ds-dropdown/ds-dropdown.stories.js +259 -0
  68. package/src/ds-dropdown/ds-dropdown.test.js +268 -0
  69. package/src/ds-dropdown/index.js +1 -0
  70. package/src/ds-dropdown-group/ds-dropdown-group.js +55 -0
  71. package/src/ds-dropdown-panel/ds-dropdown-panel.js +34 -0
  72. package/src/ds-file-uploaded/ds-file-uploaded.a11y.test.js +40 -0
  73. package/src/ds-file-uploaded/ds-file-uploaded.js +135 -0
  74. package/src/ds-file-uploaded/ds-file-uploaded.mdx +33 -0
  75. package/src/ds-file-uploaded/ds-file-uploaded.stories.js +81 -0
  76. package/src/ds-file-uploaded/ds-file-uploaded.test.js +85 -0
  77. package/src/ds-file-uploader/ds-file-uploader.a11y.test.js +61 -0
  78. package/src/ds-file-uploader/ds-file-uploader.js +442 -0
  79. package/src/ds-file-uploader/ds-file-uploader.mdx +44 -0
  80. package/src/ds-file-uploader/ds-file-uploader.stories.js +76 -0
  81. package/src/ds-file-uploader/ds-file-uploader.test.js +142 -0
  82. package/src/ds-header/ds-header.a11y.test.js +38 -0
  83. package/src/ds-header/ds-header.js +149 -0
  84. package/src/ds-header/ds-header.stories.js +63 -0
  85. package/src/ds-header/ds-header.test.js +52 -0
  86. package/src/ds-header/index.js +1 -0
  87. package/src/ds-header-nav/ds-header-nav.a11y.test.js +69 -0
  88. package/src/ds-header-nav/ds-header-nav.js +114 -0
  89. package/src/ds-header-nav/ds-header-nav.stories.js +17 -0
  90. package/src/ds-header-nav/ds-header-nav.test.js +93 -0
  91. package/src/ds-header-nav-item/ds-header-nav-item.a11y.test.js +71 -0
  92. package/src/ds-header-nav-item/ds-header-nav-item.js +124 -0
  93. package/src/ds-header-nav-item/ds-header-nav-item.stories.js +43 -0
  94. package/src/ds-header-nav-item/ds-header-nav-item.test.js +61 -0
  95. package/src/ds-icon/ds-icon.a11y.test.js +49 -0
  96. package/src/ds-icon/ds-icon.js +75 -0
  97. package/src/ds-icon/ds-icon.mdx +36 -0
  98. package/src/ds-icon/ds-icon.stories.js +88 -0
  99. package/src/ds-icon/ds-icon.test.js +97 -0
  100. package/src/ds-icon/index.js +1 -0
  101. package/src/ds-icon-button/ds-icon-button.a11y.test.js +55 -0
  102. package/src/ds-icon-button/ds-icon-button.js +224 -0
  103. package/src/ds-icon-button/ds-icon-button.mdx +131 -0
  104. package/src/ds-icon-button/ds-icon-button.stories.js +128 -0
  105. package/src/ds-icon-button/ds-icon-button.test.js +90 -0
  106. package/src/ds-icon-button/index.js +1 -0
  107. package/src/ds-input/ds-input.a11y.test.js +145 -0
  108. package/src/ds-input/ds-input.js +645 -0
  109. package/src/ds-input/ds-input.mdx +251 -0
  110. package/src/ds-input/ds-input.stories.js +298 -0
  111. package/src/ds-input/ds-input.test.js +792 -0
  112. package/src/ds-input/index.js +1 -0
  113. package/src/ds-link/ds-link.js +111 -0
  114. package/src/ds-link/ds-link.stories.js +56 -0
  115. package/src/ds-link/ds-link.test.js +74 -0
  116. package/src/ds-list-item/ds-list-item.a11y.test.js +39 -0
  117. package/src/ds-list-item/ds-list-item.js +292 -0
  118. package/src/ds-list-item/ds-list-item.stories.js +101 -0
  119. package/src/ds-list-item/ds-list-item.test.js +63 -0
  120. package/src/ds-menu/ds-menu.js +30 -0
  121. package/src/ds-menu/ds-menu.stories.js +120 -0
  122. package/src/ds-menu/ds-menu.test.js +123 -0
  123. package/src/ds-menu-group/ds-menu-group.js +101 -0
  124. package/src/ds-menu-group/ds-menu-group.stories.js +99 -0
  125. package/src/ds-nav-item/ds-nav-item.a11y.test.js +91 -0
  126. package/src/ds-nav-item/ds-nav-item.js +307 -0
  127. package/src/ds-nav-item/ds-nav-item.stories.js +99 -0
  128. package/src/ds-nav-item/ds-nav-item.test.js +169 -0
  129. package/src/ds-nav-item/index.js +1 -0
  130. package/src/ds-nav-vertical/ds-nav-vertical.a11y.test.js +69 -0
  131. package/src/ds-nav-vertical/ds-nav-vertical.js +173 -0
  132. package/src/ds-nav-vertical/ds-nav-vertical.stories.js +124 -0
  133. package/src/ds-nav-vertical/ds-nav-vertical.test.js +176 -0
  134. package/src/ds-nav-vertical/index.js +1 -0
  135. package/src/ds-pagination/ds-pagination.a11y.test.js +50 -0
  136. package/src/ds-pagination/ds-pagination.js +232 -0
  137. package/src/ds-pagination/ds-pagination.stories.js +63 -0
  138. package/src/ds-pagination/ds-pagination.test.js +141 -0
  139. package/src/ds-pagination/index.js +1 -0
  140. package/src/ds-progress-bar/ds-progress-bar.a11y.test.js +25 -0
  141. package/src/ds-progress-bar/ds-progress-bar.js +81 -0
  142. package/src/ds-progress-bar/ds-progress-bar.stories.js +69 -0
  143. package/src/ds-progress-bar/ds-progress-bar.test.js +60 -0
  144. package/src/ds-radio/ds-radio.a11y.test.js +69 -0
  145. package/src/ds-radio/ds-radio.js +240 -0
  146. package/src/ds-radio/ds-radio.stories.js +102 -0
  147. package/src/ds-radio/ds-radio.test.js +114 -0
  148. package/src/ds-radio/index.js +1 -0
  149. package/src/ds-radio-group/ds-radio-group.a11y.test.js +164 -0
  150. package/src/ds-radio-group/ds-radio-group.js +257 -0
  151. package/src/ds-radio-group/ds-radio-group.stories.js +247 -0
  152. package/src/ds-radio-group/ds-radio-group.test.js +194 -0
  153. package/src/ds-radio-group/index.js +1 -0
  154. package/src/ds-rich-list/ds-rich-list.js +246 -0
  155. package/src/ds-rich-list/ds-rich-list.stories.js +368 -0
  156. package/src/ds-rich-list/ds-rich-list.test.js +293 -0
  157. package/src/ds-rich-list-item/ds-rich-list-item.js +579 -0
  158. package/src/ds-rich-list-item/ds-rich-list-item.stories.js +197 -0
  159. package/src/ds-rich-list-item/ds-rich-list-item.test.js +434 -0
  160. package/src/ds-slider/ds-slider.js +399 -0
  161. package/src/ds-slider/ds-slider.stories.js +107 -0
  162. package/src/ds-slider/ds-slider.test.js +308 -0
  163. package/src/ds-spinner/ds-spinner.js +173 -0
  164. package/src/ds-spinner/ds-spinner.stories.js +52 -0
  165. package/src/ds-spinner/ds-spinner.test.js +50 -0
  166. package/src/ds-status-border/ds-status-border.js +88 -0
  167. package/src/ds-status-border/ds-status-border.stories.js +242 -0
  168. package/src/ds-status-border/ds-status-border.test.js +168 -0
  169. package/src/ds-stepper/ds-stepper.a11y.test.js +198 -0
  170. package/src/ds-stepper/ds-stepper.js +207 -0
  171. package/src/ds-stepper/ds-stepper.stories.js +530 -0
  172. package/src/ds-stepper/ds-stepper.test.js +311 -0
  173. package/src/ds-stepper-item/ds-stepper-item.js +485 -0
  174. package/src/ds-stepper-item/ds-stepper-item.stories.js +288 -0
  175. package/src/ds-switch/ds-switch.js +348 -0
  176. package/src/ds-switch/ds-switch.stories.js +145 -0
  177. package/src/ds-switch/ds-switch.test.js +226 -0
  178. package/src/ds-switch/index.js +1 -0
  179. package/src/ds-tab-item/ds-tab-item.js +341 -0
  180. package/src/ds-tab-item/ds-tab-item.stories.js +69 -0
  181. package/src/ds-tabs/ds-tab-panel.js +48 -0
  182. package/src/ds-tabs/ds-tabs.a11y.test.js +56 -0
  183. package/src/ds-tabs/ds-tabs.js +180 -0
  184. package/src/ds-tabs/ds-tabs.stories.js +152 -0
  185. package/src/ds-tabs/ds-tabs.test.js +306 -0
  186. package/src/ds-tabs/index.js +3 -0
  187. package/src/ds-tag-action/ds-tag-action.a11y.test.js +32 -0
  188. package/src/ds-tag-action/ds-tag-action.js +185 -0
  189. package/src/ds-tag-action/ds-tag-action.stories.js +55 -0
  190. package/src/ds-tag-action/ds-tag-action.test.js +44 -0
  191. package/src/ds-tag-removable/ds-tag-removable.a11y.test.js +24 -0
  192. package/src/ds-tag-removable/ds-tag-removable.js +146 -0
  193. package/src/ds-tag-removable/ds-tag-removable.stories.js +52 -0
  194. package/src/ds-tag-removable/ds-tag-removable.test.js +46 -0
  195. package/src/ds-tag-status/ds-tag-status.a11y.test.js +93 -0
  196. package/src/ds-tag-status/ds-tag-status.js +164 -0
  197. package/src/ds-tag-status/ds-tag-status.stories.js +200 -0
  198. package/src/ds-tag-status/ds-tag-status.test.js +140 -0
  199. package/src/ds-tag-status/index.js +1 -0
  200. package/src/ds-textarea/ds-textarea-clearable.test.js +89 -0
  201. package/src/ds-textarea/ds-textarea.a11y.test.js +66 -0
  202. package/src/ds-textarea/ds-textarea.js +505 -0
  203. package/src/ds-textarea/ds-textarea.stories.js +335 -0
  204. package/src/ds-textarea/ds-textarea.test.js +218 -0
  205. package/src/ds-textarea/index.js +1 -0
  206. package/src/ds-thumbnail/ds-thumbnail.js +207 -0
  207. package/src/ds-thumbnail/ds-thumbnail.stories.js +217 -0
  208. package/src/ds-thumbnail/ds-thumbnail.test.js +220 -0
  209. package/src/ds-toast/ds-toast-provider.js +110 -0
  210. package/src/ds-toast/ds-toast.a11y.test.js +34 -0
  211. package/src/ds-toast/ds-toast.js +243 -0
  212. package/src/ds-toast/ds-toast.stories.js +143 -0
  213. package/src/ds-toast/ds-toast.test.js +93 -0
  214. package/src/ds-toast/index.js +2 -0
  215. package/src/ds-tooltip/ds-tooltip.a11y.test.js +110 -0
  216. package/src/ds-tooltip/ds-tooltip.js +217 -0
  217. package/src/ds-tooltip/ds-tooltip.mdx +75 -0
  218. package/src/ds-tooltip/ds-tooltip.stories.js +72 -0
  219. package/src/ds-tooltip/ds-tooltip.test.js +191 -0
  220. package/src/ds-tooltip/index.js +1 -0
  221. package/src/ds-tooltip/positioner.js +117 -0
  222. package/src/index.js +50 -0
  223. package/src/mixins/field-label.mixin.js +113 -0
  224. package/src/mixins/field-message.mixin.js +66 -0
  225. package/src/token-provider/index.js +1 -0
  226. package/src/token-provider/token-provider.a11y.test.js +44 -0
  227. package/src/token-provider/token-provider.js +85 -0
  228. package/src/token-provider/token-provider.stories.js +105 -0
  229. package/src/token-provider/token-provider.test.js +134 -0
  230. package/src/utils/number-input.utils.js +42 -0
@@ -0,0 +1,645 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { FieldMessageMixin, fieldMessageStyles } from '../mixins/field-message.mixin.js';
3
+ import { fieldLabelStyles } from '../mixins/field-label.mixin.js';
4
+ import '../ds-icon/ds-icon.js';
5
+ import '../ds-icon-button/ds-icon-button.js';
6
+ import '../ds-tooltip/ds-tooltip.js';
7
+ import { clampValue } from '../utils/number-input.utils.js';
8
+
9
+ /**
10
+ * Input component for forms with label, help text, and validation states
11
+ *
12
+ * @element ds-input
13
+ *
14
+ * @prop {string} label - Label text (optional)
15
+ * @prop {string} info - Info tooltip text for info button
16
+ * @prop {string} labelPosition - Label position: 'top' | 'inline-start' (default: 'top')
17
+ * @prop {string} labelWidth - Fixed label width for inline layout (e.g., '100px')
18
+ * @prop {string} placeholder - Placeholder text
19
+ * @prop {string} value - Input value
20
+ * @prop {string} type - Input type: 'text' | 'email' | 'password' | 'number' (default: 'text')
21
+ * @prop {string} helper - Help text below field
22
+ * @prop {string} validationStatus - Validation state: 'error' | 'success' | undefined
23
+ * @prop {string} validationMessage - Validation message to display
24
+ * @prop {boolean} disabled - Disabled state
25
+ * @prop {boolean} readonly - Read-only state
26
+ * @prop {boolean} required - Required field
27
+ * @prop {boolean} showPasswordToggle - Show password visibility toggle for password inputs
28
+ * @prop {boolean} clearable - Show clear button when input is focused and has value
29
+ * @prop {boolean} hideSteppers - Hide increment/decrement buttons for number inputs
30
+ * @prop {number} step - Step increment for number inputs (default: 1)
31
+ * @prop {string} autocomplete - Autocomplete attribute for native input
32
+ * @prop {string} width - Width of the input (CSS value, e.g. '60px', '100%')
33
+ * @prop {string} textAlign - Text alignment within input ('left' | 'center' | 'right')
34
+ * @prop {number} min - Minimum value for number inputs
35
+ * @prop {number} max - Maximum value for number inputs
36
+ * @prop {boolean} clamp - Enable auto-clamping on blur/Enter for number inputs
37
+ *
38
+ * @slot prefix - Custom content before input field
39
+ * @slot suffix - Custom content after input field
40
+ * @slot action - Custom icon/button inside field (right side)
41
+ *
42
+ * @fires input - Fired when input value changes
43
+ * @fires change - Fired when input loses focus
44
+ * @fires info-click - Fired when info button is clicked
45
+ */
46
+ export class DsInput extends FieldMessageMixin(LitElement) {
47
+ static properties = {
48
+ label: { type: String },
49
+ info: { type: String },
50
+ labelPosition: { type: String, attribute: 'label-position' },
51
+ labelWidth: { type: String, attribute: 'label-width' },
52
+ placeholder: { type: String },
53
+ value: { type: String },
54
+ type: { type: String },
55
+ helper: { type: String },
56
+ validationStatus: { type: String, reflect: true, attribute: 'validation-status' },
57
+ validationMessage: { type: String, attribute: 'validation-message' },
58
+ disabled: { type: Boolean, reflect: true },
59
+ readonly: { type: Boolean, reflect: true },
60
+ required: { type: Boolean, reflect: true },
61
+ showPasswordToggle: { type: Boolean, attribute: 'show-password-toggle' },
62
+ clearable: { type: Boolean },
63
+ hideSteppers: { type: Boolean, attribute: 'hide-steppers' },
64
+ step: { type: Number },
65
+ autocomplete: { type: String },
66
+ width: { type: String },
67
+ textAlign: { type: String, attribute: 'text-align' },
68
+ min: { type: Number },
69
+ max: { type: Number },
70
+ clamp: { type: Boolean },
71
+ _hasPrefix: { state: true },
72
+ _hasSuffix: { state: true },
73
+ _isPasswordVisible: { state: true },
74
+ _isFocused: { state: true }
75
+ };
76
+
77
+ static styles = css`
78
+ :host {
79
+ display: block;
80
+ width: var(--ds-input-computed-width, var(--ds-input-width, 100%));
81
+ box-sizing: border-box;
82
+ min-width: 0; /* Enable flex shrinking by default */
83
+ --input-height: 32px;
84
+ --input-padding-y: 5px;
85
+ --input-padding-x: var(--ds-space-sm);
86
+ }
87
+
88
+ .field-wrapper {
89
+ display: flex;
90
+ flex-direction: column;
91
+ gap: var(--ds-space-xs); /* 4px gap between sections */
92
+ }
93
+
94
+ /* Label styles from mixin */
95
+ ${fieldLabelStyles}
96
+
97
+ /* Input container */
98
+ .input-container {
99
+ display: flex;
100
+ align-items: center;
101
+ }
102
+
103
+ /* Field wrapper with border */
104
+ .input-field {
105
+ flex: 1;
106
+ display: flex;
107
+ width: 100%;
108
+ align-items: center;
109
+ gap: var(--ds-space-xs);
110
+
111
+ /* Height calculation: 20px (line-height) + 10px (padding) + 2px (border) = 32px */
112
+ height: var(--input-height);
113
+ padding: var(--input-padding-y) var(--input-padding-x);
114
+ border: 1px solid var(--ds-color-border-strong);
115
+ border-radius: var(--ds-radius-input);
116
+ background: var(--ds-color-bg-default);
117
+
118
+ box-sizing: border-box;
119
+ transition: background-color 0.2s, border-color 0.2s;
120
+ }
121
+
122
+ /* Input element */
123
+ input {
124
+ flex: 1;
125
+ border: none;
126
+ background: transparent;
127
+ outline: none;
128
+
129
+ font: var(--ds-typo-content-body-regular);
130
+ color: var(--ds-color-text-default);
131
+
132
+ box-sizing: border-box;
133
+ padding: 0;
134
+ margin: 0;
135
+ min-width: 0; /* Allow input to shrink */
136
+ height: 100%;
137
+ -moz-appearance: textfield; /* Firefox */
138
+ }
139
+
140
+ /* Hide native spin buttons */
141
+ input::-webkit-outer-spin-button,
142
+ input::-webkit-inner-spin-button {
143
+ -webkit-appearance: none;
144
+ margin: 0;
145
+ }
146
+
147
+ input::placeholder {
148
+ color: var(--ds-color-text-secondary);
149
+ }
150
+
151
+ /* Prefix/Suffix text slots - Hidden by default unless content detected */
152
+ .prefix-wrapper,
153
+ .suffix-wrapper {
154
+ font: var(--ds-typo-content-body-regular);
155
+ color: var(--ds-color-text-secondary);
156
+ white-space: nowrap;
157
+ display: none;
158
+ }
159
+
160
+ .prefix-wrapper.has-content {
161
+ display: block;
162
+ margin-right: var(--ds-space-sm);
163
+ }
164
+
165
+ .suffix-wrapper.has-content {
166
+ display: block;
167
+ margin-left: var(--ds-space-sm);
168
+ }
169
+
170
+ /* Slotted action content - constrain size */
171
+ ::slotted([slot="action"]) {
172
+ display: flex;
173
+ align-items: center;
174
+ flex-shrink: 0;
175
+ max-height: calc(var(--input-height) - 10px);
176
+ max-width: calc(var(--input-height) - 10px);
177
+ }
178
+
179
+ /* States */
180
+
181
+ /* Hover */
182
+ :host(:not([disabled]):not([readonly])) .input-field:hover {
183
+ background: var(--ds-color-bg-hover);
184
+ }
185
+
186
+ /* Focus */
187
+ :host(:not([disabled]):not([readonly])) .input-field:focus-within {
188
+ border-color: var(--ds-color-border-focus);
189
+ outline: 2px solid var(--ds-color-border-focus);
190
+ outline-offset: -1px;
191
+ }
192
+
193
+ /* Disabled - no visible border */
194
+ :host([disabled]) .input-field {
195
+ background: var(--ds-color-bg-disabled);
196
+ border: 1px solid transparent;
197
+ cursor: not-allowed;
198
+ }
199
+
200
+ :host([disabled]) input {
201
+ color: var(--ds-color-text-disabled);
202
+ cursor: not-allowed;
203
+ }
204
+
205
+ /* Read-only - no visible border, no background */
206
+ :host([readonly]) .input-field {
207
+ background: transparent;
208
+ border: 1px solid transparent;
209
+ }
210
+
211
+ :host([readonly]) input {
212
+ cursor: default;
213
+ }
214
+
215
+ /* Validation states */
216
+ :host([validation-status="error"]) .input-field {
217
+ border-color: var(--ds-color-border-error);
218
+ }
219
+
220
+ :host([validation-status="success"]) .input-field {
221
+ border-color: var(--ds-color-border-success, var(--ds-color-border-strong));
222
+ }
223
+
224
+ /* Message styles from mixin */
225
+ ${fieldMessageStyles}
226
+ `;
227
+
228
+ constructor() {
229
+ super();
230
+ this.label = '';
231
+ this.info = '';
232
+ this.labelPosition = 'top';
233
+ this.labelWidth = '';
234
+ this.placeholder = '';
235
+ this.value = '';
236
+ this.type = 'text';
237
+ this.helper = '';
238
+ this.validationStatus = undefined;
239
+ this.validationMessage = '';
240
+ this.disabled = false;
241
+ this.readonly = false;
242
+ this.required = false;
243
+ this.showPasswordToggle = false;
244
+ this.clearable = false;
245
+ this.hideSteppers = false;
246
+ this.step = 1;
247
+ this.autocomplete = undefined;
248
+ this.width = undefined;
249
+ this.textAlign = undefined;
250
+ this.min = undefined;
251
+ this.max = undefined;
252
+ this.clamp = false;
253
+ this._hasPrefix = false;
254
+ this._hasSuffix = false;
255
+ this._isPasswordVisible = false;
256
+ this._isFocused = false;
257
+ }
258
+
259
+ /**
260
+ * Select all text in the input field.
261
+ * @public
262
+ */
263
+ selectAll() {
264
+ const input = this.shadowRoot?.querySelector('input');
265
+ input?.select();
266
+ }
267
+
268
+ /**
269
+ * Force-set the value, bypassing reactive update detection.
270
+ * Used when external logic clamps/transforms the value.
271
+ * @param {string} value - The value to set
272
+ * @public
273
+ */
274
+ forceValue(value) {
275
+ this.value = value;
276
+ const input = this.shadowRoot?.querySelector('input');
277
+ if (input) input.value = value;
278
+ this.requestUpdate();
279
+ }
280
+
281
+ updated(changedProperties) {
282
+ super.updated?.(changedProperties);
283
+
284
+ // Handle width prop
285
+ if (changedProperties.has('width')) {
286
+ if (this.width) {
287
+ this.style.setProperty('--ds-input-computed-width', this.width);
288
+ } else {
289
+ this.style.removeProperty('--ds-input-computed-width');
290
+ }
291
+ }
292
+
293
+ // Handle textAlign prop
294
+ if (changedProperties.has('textAlign')) {
295
+ const input = this.shadowRoot?.querySelector('input');
296
+ if (input) {
297
+ input.style.textAlign = this.textAlign || '';
298
+ }
299
+ }
300
+ }
301
+
302
+ _handleSlotChange(e) {
303
+ const name = e.target.getAttribute('name');
304
+ const nodes = e.target.assignedNodes({ flatten: true });
305
+ // Check if there are any non-text nodes or non-empty text nodes
306
+ const hasContent = nodes.some(node =>
307
+ node.nodeType !== Node.TEXT_NODE || node.textContent.trim() !== ''
308
+ );
309
+
310
+ if (name === 'prefix') this._hasPrefix = hasContent;
311
+ if (name === 'suffix') this._hasSuffix = hasContent;
312
+
313
+ this.requestUpdate();
314
+ }
315
+
316
+ _togglePassword() {
317
+ this._isPasswordVisible = !this._isPasswordVisible;
318
+ }
319
+
320
+ _incrementNumber() {
321
+ if (this.readonly || this.disabled) return;
322
+ const input = this.shadowRoot.querySelector('input');
323
+ const currentValue = parseFloat(input.value) || 0;
324
+ const step = this.step || 1;
325
+ let newValue = currentValue + step;
326
+
327
+ // Clamp to max if defined
328
+ if (this.max !== undefined && newValue > this.max) {
329
+ newValue = this.max;
330
+ }
331
+
332
+ // Don't update if already at max
333
+ if (this.max !== undefined && currentValue >= this.max) return;
334
+
335
+ // Preserve decimal precision based on step
336
+ const decimalPlaces = (step.toString().split('.')[1] || '').length;
337
+ const formattedValue = decimalPlaces > 0 ? newValue.toFixed(decimalPlaces) : newValue.toString();
338
+
339
+ input.value = formattedValue;
340
+ this.value = formattedValue;
341
+ this._dispatchInputEvent();
342
+ }
343
+
344
+ _decrementNumber() {
345
+ if (this.readonly || this.disabled) return;
346
+ const input = this.shadowRoot.querySelector('input');
347
+ const currentValue = parseFloat(input.value) || 0;
348
+ const step = this.step || 1;
349
+ let newValue = currentValue - step;
350
+
351
+ // Clamp to min if defined
352
+ if (this.min !== undefined && newValue < this.min) {
353
+ newValue = this.min;
354
+ }
355
+
356
+ // Don't update if already at min
357
+ if (this.min !== undefined && currentValue <= this.min) return;
358
+
359
+ // Preserve decimal precision based on step
360
+ const decimalPlaces = (step.toString().split('.')[1] || '').length;
361
+ const formattedValue = decimalPlaces > 0 ? newValue.toFixed(decimalPlaces) : newValue.toString();
362
+
363
+ input.value = formattedValue;
364
+ this.value = formattedValue;
365
+ this._dispatchInputEvent();
366
+ }
367
+
368
+ _dispatchInputEvent() {
369
+ this.dispatchEvent(new CustomEvent('input', {
370
+ detail: { value: this.value },
371
+ bubbles: true,
372
+ composed: true
373
+ }));
374
+ }
375
+
376
+ _handleClear() {
377
+ this.value = '';
378
+ const input = this.shadowRoot.querySelector('input');
379
+ if (input) {
380
+ input.value = '';
381
+ input.focus();
382
+ }
383
+ this._dispatchInputEvent();
384
+ }
385
+
386
+ _handleInputFocus(e) {
387
+ this._isFocused = true;
388
+ if (this.value && e.target.value) {
389
+ e.target.select();
390
+ }
391
+ }
392
+
393
+ _handleInputBlur() {
394
+ this._isFocused = false;
395
+ this._applyClampingIfNeeded();
396
+ }
397
+
398
+ _handleKeyDown(e) {
399
+ if (e.key === 'Escape' && this.value && this.clearable) {
400
+ this._handleClear();
401
+ e.preventDefault();
402
+ }
403
+ // Apply clamping on Enter for number inputs
404
+ if (e.key === 'Enter' && this.type === 'number' && this.clamp) {
405
+ this._applyClampingIfNeeded();
406
+ }
407
+ // Handle Arrow Up/Down for number inputs with min/max constraints
408
+ if (this.type === 'number' && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
409
+ // Always prevent default to override browser's native number input behavior
410
+ e.preventDefault();
411
+
412
+ const currentValue = parseFloat(this.value) || 0;
413
+ const step = this.step || 1;
414
+ let newValue;
415
+
416
+ if (e.key === 'ArrowUp') {
417
+ // Already at max, do nothing
418
+ if (this.max !== undefined && currentValue >= this.max) return;
419
+
420
+ newValue = currentValue + step;
421
+ // Clamp to max if defined
422
+ if (this.max !== undefined && newValue > this.max) {
423
+ newValue = this.max;
424
+ }
425
+ } else {
426
+ // Already at min, do nothing
427
+ if (this.min !== undefined && currentValue <= this.min) return;
428
+
429
+ newValue = currentValue - step;
430
+ // Clamp to min if defined
431
+ if (this.min !== undefined && newValue < this.min) {
432
+ newValue = this.min;
433
+ }
434
+ }
435
+
436
+ // Preserve decimal precision
437
+ const decimalPlaces = (step.toString().split('.')[1] || '').length;
438
+ const formattedValue = decimalPlaces > 0 ? newValue.toFixed(decimalPlaces) : newValue.toString();
439
+
440
+ const input = this.shadowRoot.querySelector('input');
441
+ input.value = formattedValue;
442
+ this.value = formattedValue;
443
+ this._dispatchInputEvent();
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Apply clamping logic for number inputs with min/max.
449
+ * @private
450
+ */
451
+ _applyClampingIfNeeded() {
452
+ if (this.type !== 'number' || !this.clamp) return;
453
+ if (this.min === undefined && this.max === undefined) return;
454
+
455
+ const min = this.min ?? -Infinity;
456
+ const max = this.max ?? Infinity;
457
+ const fallback = this.min ?? 0;
458
+
459
+ const clamped = clampValue(this.value, min, max, fallback);
460
+ const clampedStr = clamped.toString();
461
+
462
+ if (clampedStr !== this.value) {
463
+ this.forceValue(clampedStr);
464
+ }
465
+ }
466
+
467
+ render() {
468
+ const hasError = this.validationStatus === 'error';
469
+ const errorMessage = hasError ? this.validationMessage : '';
470
+ const inputId = 'input-field';
471
+ const messageId = 'field-message';
472
+ const isInline = this.labelPosition === 'inline-start';
473
+ const wrapperClass = isInline ? 'field-wrapper inline-label' : 'field-wrapper';
474
+ const labelStyle = isInline && this.labelWidth ? `width: ${this.labelWidth}` : '';
475
+
476
+ // Determine input type (handle password toggle)
477
+ let inputType = this.type;
478
+ if (this.type === 'password' && this._isPasswordVisible) {
479
+ inputType = 'text';
480
+ }
481
+
482
+ const inputContent = html`
483
+ <div class="input-container">
484
+ <div class="prefix-wrapper ${this._hasPrefix ? 'has-content' : ''}">
485
+ <slot name="prefix" @slotchange=${this._handleSlotChange}></slot>
486
+ </div>
487
+
488
+ <div class="input-field" part="field">
489
+ <input
490
+ id="${inputId}"
491
+ type=${inputType}
492
+ .value=${this.value}
493
+ placeholder=${this.placeholder}
494
+ ?disabled=${this.disabled}
495
+ ?readonly=${this.readonly}
496
+ ?required=${this.required}
497
+ autocomplete=${this.autocomplete || 'off'}
498
+ min=${this.min !== undefined ? this.min : ''}
499
+ max=${this.max !== undefined ? this.max : ''}
500
+ style="text-align: ${this.textAlign || 'left'}"
501
+ aria-invalid=${hasError ? 'true' : 'false'}
502
+ aria-required=${this.required ? 'true' : 'false'}
503
+ aria-describedby=${this.helper || errorMessage ? messageId : ''}
504
+ aria-label=${!this.label ? this.placeholder || 'Input' : ''}
505
+ @input=${this._handleInput}
506
+ @change=${this._handleChange}
507
+ @focus=${this._handleInputFocus}
508
+ @blur=${this._handleInputBlur}
509
+ @keydown=${this._handleKeyDown}
510
+ part="input"
511
+ />
512
+
513
+ ${this.clearable && this.value && this._isFocused ? html`
514
+ <ds-icon-button
515
+ icon="close"
516
+ variant="action"
517
+ size="s"
518
+ aria-label="Clear input"
519
+ tabindex="-1"
520
+ @mousedown=${(e) => e.preventDefault()}
521
+ @click=${this._handleClear}
522
+ ></ds-icon-button>
523
+ ` : ''}
524
+
525
+ ${this.type === 'password' && this.showPasswordToggle ? html`
526
+ <ds-icon-button
527
+ icon="${this._isPasswordVisible ? 'visibility-off' : 'visibility'}"
528
+ variant="action"
529
+ size="s"
530
+ aria-label="${this._isPasswordVisible ? 'Hide password' : 'Show password'}"
531
+ @click=${this._togglePassword}
532
+ ></ds-icon-button>
533
+ ` : ''}
534
+
535
+ ${this.type === 'number' && !this.hideSteppers ? html`
536
+ <ds-icon-button
537
+ icon="remove"
538
+ variant="action"
539
+ size="s"
540
+ aria-label="Decrease value"
541
+ @click=${this._decrementNumber}
542
+ ?disabled=${this.disabled || this.readonly || (this.min !== undefined && parseFloat(this.value) <= this.min)}
543
+ ></ds-icon-button>
544
+ <ds-icon-button
545
+ icon="add"
546
+ variant="action"
547
+ size="s"
548
+ aria-label="Increase value"
549
+ @click=${this._incrementNumber}
550
+ ?disabled=${this.disabled || this.readonly || (this.max !== undefined && parseFloat(this.value) >= this.max)}
551
+ ></ds-icon-button>
552
+ ` : ''}
553
+
554
+ <slot name="action"></slot>
555
+ </div>
556
+
557
+ <div class="suffix-wrapper ${this._hasSuffix ? 'has-content' : ''}">
558
+ <slot name="suffix" @slotchange=${this._handleSlotChange}></slot>
559
+ </div>
560
+ </div>
561
+
562
+ ${this.helper || errorMessage ? html`
563
+ <div id="${messageId}">
564
+ ${this.renderFieldMessage(this.helper, errorMessage)}
565
+ </div>
566
+ ` : ''}
567
+ `;
568
+
569
+ if (isInline) {
570
+ return html`
571
+ <div class="${wrapperClass}">
572
+ <div class="label-row" part="label-row" style="${labelStyle}">
573
+ ${this.label ? html`<label for="${inputId}">${this.label}</label>` : ''}
574
+ ${this.info ? html`
575
+ <ds-tooltip content="${this.info}" placement="top">
576
+ <ds-icon-button
577
+ icon="info"
578
+ variant="action"
579
+ size="s"
580
+ aria-label="More information"
581
+ @click=${this._handleInfoClick}
582
+ ></ds-icon-button>
583
+ </ds-tooltip>
584
+ ` : ''}
585
+ </div>
586
+ <div class="field-content">
587
+ ${inputContent}
588
+ </div>
589
+ </div>
590
+ `;
591
+ }
592
+
593
+ return html`
594
+ <div class="field-wrapper">
595
+ ${this.label ? html`
596
+ <div class="label-row" part="label-row">
597
+ <label for="${inputId}">${this.label}</label>
598
+ ${this.info ? html`
599
+ <ds-tooltip content="${this.info}" placement="top">
600
+ <ds-icon-button
601
+ icon="info"
602
+ variant="action"
603
+ size="s"
604
+ aria-label="More information"
605
+ @click=${this._handleInfoClick}
606
+ ></ds-icon-button>
607
+ </ds-tooltip>
608
+ ` : ''}
609
+ </div>
610
+ ` : ''}
611
+ ${inputContent}
612
+ </div>
613
+ `;
614
+ }
615
+
616
+ _handleInfoClick(e) {
617
+ e.preventDefault();
618
+ e.stopPropagation();
619
+ this.dispatchEvent(new CustomEvent('info-click', {
620
+ bubbles: true,
621
+ composed: true,
622
+ detail: { info: this.info }
623
+ }));
624
+ }
625
+
626
+ _handleInput(e) {
627
+ this.value = e.target.value;
628
+
629
+ this.dispatchEvent(new CustomEvent('input', {
630
+ detail: { value: this.value },
631
+ bubbles: true,
632
+ composed: true
633
+ }));
634
+ }
635
+
636
+ _handleChange(e) {
637
+ this.dispatchEvent(new CustomEvent('change', {
638
+ detail: { value: this.value },
639
+ bubbles: true,
640
+ composed: true
641
+ }));
642
+ }
643
+ }
644
+
645
+ customElements.define('ds-input', DsInput);