@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,308 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import './ds-slider.js';
3
+
4
+ describe('ds-slider', () => {
5
+ let container;
6
+
7
+ beforeEach(() => {
8
+ container = document.createElement('div');
9
+ document.body.appendChild(container);
10
+ });
11
+
12
+ afterEach(() => {
13
+ container.remove();
14
+ });
15
+
16
+ it('renders with default values', async () => {
17
+ container.innerHTML = '<ds-slider></ds-slider>';
18
+ const element = container.querySelector('ds-slider');
19
+ await new Promise(resolve => setTimeout(resolve, 0));
20
+
21
+ expect(element.value).toBe(0);
22
+ expect(element.min).toBe(0);
23
+ expect(element.max).toBe(100);
24
+ expect(element.step).toBe(1);
25
+ expect(element.disabled).toBe(false);
26
+
27
+ const input = element.shadowRoot.querySelector('input[type="range"]');
28
+ expect(input).toBeTruthy();
29
+ });
30
+
31
+ it('renders with custom value and range', async () => {
32
+ container.innerHTML = '<ds-slider value="50" min="10" max="90"></ds-slider>';
33
+ const element = container.querySelector('ds-slider');
34
+ await new Promise(resolve => setTimeout(resolve, 0));
35
+
36
+ expect(element.value).toBe(50);
37
+ expect(element.min).toBe(10);
38
+ expect(element.max).toBe(90);
39
+ });
40
+
41
+ it('renders label when provided', async () => {
42
+ container.innerHTML = '<ds-slider label="Volume"></ds-slider>';
43
+ const element = container.querySelector('ds-slider');
44
+ await new Promise(resolve => setTimeout(resolve, 0));
45
+
46
+ const labelRow = element.shadowRoot.querySelector('.label-row');
47
+ expect(labelRow).toBeTruthy();
48
+ expect(labelRow.textContent).toContain('Volume');
49
+ });
50
+
51
+ it('renders helpText when provided', async () => {
52
+ container.innerHTML = '<ds-slider help-text="Some help"></ds-slider>';
53
+ const element = container.querySelector('ds-slider');
54
+ await new Promise(resolve => setTimeout(resolve, 0));
55
+
56
+ const message = element.shadowRoot.querySelector('.field-message');
57
+ expect(message).toBeTruthy();
58
+ expect(message.textContent).toContain('Some help');
59
+ });
60
+
61
+ it('renders errorText when validationStatus is error', async () => {
62
+ container.innerHTML = '<ds-slider error-text="Error msg" validation-status="error"></ds-slider>';
63
+ const element = container.querySelector('ds-slider');
64
+ await new Promise(resolve => setTimeout(resolve, 0));
65
+
66
+ const message = element.shadowRoot.querySelector('.field-message--error');
67
+ expect(message).toBeTruthy();
68
+ expect(message.textContent).toContain('Error msg');
69
+ });
70
+
71
+ it('dispatches input event during interaction', async () => {
72
+ container.innerHTML = '<ds-slider value="50"></ds-slider>';
73
+ const element = container.querySelector('ds-slider');
74
+ await new Promise(resolve => setTimeout(resolve, 0));
75
+
76
+ let eventFired = false;
77
+ let eventValue = null;
78
+ element.addEventListener('input', (e) => {
79
+ eventFired = true;
80
+ eventValue = e.detail.value;
81
+ });
82
+
83
+ const input = element.shadowRoot.querySelector('input[type="range"]');
84
+ input.value = 75;
85
+ input.dispatchEvent(new Event('input', { bubbles: true }));
86
+ await new Promise(resolve => setTimeout(resolve, 0));
87
+
88
+ expect(eventFired).toBe(true);
89
+ expect(eventValue).toBe(75);
90
+ });
91
+
92
+ it('dispatches change event on release', async () => {
93
+ container.innerHTML = '<ds-slider value="50"></ds-slider>';
94
+ const element = container.querySelector('ds-slider');
95
+ await new Promise(resolve => setTimeout(resolve, 0));
96
+
97
+ let eventFired = false;
98
+ element.addEventListener('change', () => {
99
+ eventFired = true;
100
+ });
101
+
102
+ const input = element.shadowRoot.querySelector('input[type="range"]');
103
+ input.dispatchEvent(new Event('change', { bubbles: true }));
104
+ await new Promise(resolve => setTimeout(resolve, 0));
105
+
106
+ expect(eventFired).toBe(true);
107
+ });
108
+
109
+ it('respects step attribute', async () => {
110
+ container.innerHTML = '<ds-slider step="10"></ds-slider>';
111
+ const element = container.querySelector('ds-slider');
112
+ await new Promise(resolve => setTimeout(resolve, 0));
113
+
114
+ const input = element.shadowRoot.querySelector('input[type="range"]');
115
+ expect(input.step).toBe('10');
116
+ });
117
+
118
+ it('reflects disabled state', async () => {
119
+ container.innerHTML = '<ds-slider disabled></ds-slider>';
120
+ const element = container.querySelector('ds-slider');
121
+ await new Promise(resolve => setTimeout(resolve, 0));
122
+
123
+ expect(element.disabled).toBe(true);
124
+ expect(element.hasAttribute('disabled')).toBe(true);
125
+
126
+ const input = element.shadowRoot.querySelector('input[type="range"]');
127
+ expect(input.disabled).toBe(true);
128
+ });
129
+
130
+ it('focus() method focuses the input', async () => {
131
+ container.innerHTML = '<ds-slider></ds-slider>';
132
+ const element = container.querySelector('ds-slider');
133
+ await new Promise(resolve => setTimeout(resolve, 0));
134
+
135
+ element.focus();
136
+ await new Promise(resolve => setTimeout(resolve, 0));
137
+
138
+ const input = element.shadowRoot.querySelector('input[type="range"]');
139
+ expect(element.shadowRoot.activeElement).toBe(input);
140
+ });
141
+
142
+ it('renders thumb with icon', async () => {
143
+ container.innerHTML = '<ds-slider></ds-slider>';
144
+ const element = container.querySelector('ds-slider');
145
+ await new Promise(resolve => setTimeout(resolve, 0));
146
+
147
+ const thumb = element.shadowRoot.querySelector('.thumb');
148
+ expect(thumb).toBeTruthy();
149
+
150
+ const icon = thumb.querySelector('ds-icon');
151
+ expect(icon).toBeTruthy();
152
+ expect(icon.getAttribute('name')).toBe('circle-filled');
153
+ });
154
+
155
+ // TOOLTIP TESTS
156
+ it('shows tooltip on focus', async () => {
157
+ container.innerHTML = '<ds-slider></ds-slider>';
158
+ const element = container.querySelector('ds-slider');
159
+ await new Promise(resolve => setTimeout(resolve, 0));
160
+
161
+ const input = element.shadowRoot.querySelector('input[type="range"]');
162
+ input.dispatchEvent(new Event('focus'));
163
+ await new Promise(resolve => setTimeout(resolve, 0));
164
+
165
+ expect(element._showTooltip).toBe(true);
166
+ });
167
+
168
+ it('hides tooltip on blur', async () => {
169
+ container.innerHTML = '<ds-slider></ds-slider>';
170
+ const element = container.querySelector('ds-slider');
171
+ await new Promise(resolve => setTimeout(resolve, 0));
172
+
173
+ const input = element.shadowRoot.querySelector('input[type="range"]');
174
+
175
+ // Focus first
176
+ input.dispatchEvent(new Event('focus'));
177
+ await new Promise(resolve => setTimeout(resolve, 0));
178
+ expect(element._showTooltip).toBe(true);
179
+
180
+ // Then blur
181
+ input.dispatchEvent(new Event('blur'));
182
+ await new Promise(resolve => setTimeout(resolve, 0));
183
+
184
+ expect(element._showTooltip).toBe(false);
185
+ });
186
+
187
+ it('shows tooltip while dragging', async () => {
188
+ container.innerHTML = '<ds-slider value="50"></ds-slider>';
189
+ const element = container.querySelector('ds-slider');
190
+ await new Promise(resolve => setTimeout(resolve, 0));
191
+
192
+ const input = element.shadowRoot.querySelector('input[type="range"]');
193
+
194
+ // Simulate mousedown (start drag)
195
+ element._handleMouseDown();
196
+ expect(element._isDragging).toBe(true);
197
+
198
+ // Trigger input event
199
+ input.value = 75;
200
+ input.dispatchEvent(new Event('input', { bubbles: true }));
201
+ await new Promise(resolve => setTimeout(resolve, 0));
202
+
203
+ expect(element._showTooltip).toBe(true);
204
+ });
205
+
206
+
207
+ it('updates tooltip content as value changes', async () => {
208
+ container.innerHTML = '<ds-slider value="25"></ds-slider>';
209
+ const element = container.querySelector('ds-slider');
210
+ await new Promise(resolve => setTimeout(resolve, 0));
211
+
212
+ const input = element.shadowRoot.querySelector('input[type="range"]');
213
+
214
+ input.dispatchEvent(new Event('focus'));
215
+ await new Promise(resolve => setTimeout(resolve, 0));
216
+
217
+ // Change value
218
+ input.value = 75;
219
+ input.dispatchEvent(new Event('input', { bubbles: true }));
220
+ await new Promise(resolve => setTimeout(resolve, 0));
221
+
222
+ expect(element.value).toBe(75);
223
+ const tooltip = element.shadowRoot.querySelector('ds-tooltip');
224
+ expect(tooltip).toBeTruthy();
225
+ });
226
+
227
+ // VALIDATION TESTS
228
+ it('applies error styling when validation-status="error"', async () => {
229
+ container.innerHTML = '<ds-slider validation-status="error"></ds-slider>';
230
+ const element = container.querySelector('ds-slider');
231
+ await new Promise(resolve => setTimeout(resolve, 0));
232
+
233
+ expect(element.validationStatus).toBe('error');
234
+ });
235
+
236
+ it('renders error message with icon', async () => {
237
+ container.innerHTML = '<ds-slider error-text="Invalid value" validation-status="error"></ds-slider>';
238
+ const element = container.querySelector('ds-slider');
239
+ await new Promise(resolve => setTimeout(resolve, 0));
240
+
241
+ const message = element.shadowRoot.querySelector('.field-message--error');
242
+ expect(message).toBeTruthy();
243
+ expect(message.textContent).toContain('Invalid value');
244
+
245
+ const errorIcon = element.shadowRoot.querySelector('.field-message--error ds-icon');
246
+ expect(errorIcon).toBeTruthy();
247
+ });
248
+
249
+
250
+
251
+ // EDGE CASES
252
+ it('handles value < min (clamps to min)', async () => {
253
+ container.innerHTML = '<ds-slider min="10" max="100"></ds-slider>';
254
+ const element = container.querySelector('ds-slider');
255
+ await new Promise(resolve => setTimeout(resolve, 0));
256
+
257
+ element.value = 5; // Below min
258
+ await new Promise(resolve => setTimeout(resolve, 0));
259
+
260
+ const input = element.shadowRoot.querySelector('input[type="range"]');
261
+ expect(parseFloat(input.value)).toBeGreaterThanOrEqual(10);
262
+ });
263
+
264
+ it('handles value > max (clamps to max)', async () => {
265
+ container.innerHTML = '<ds-slider min="0" max="100"></ds-slider>';
266
+ const element = container.querySelector('ds-slider');
267
+ await new Promise(resolve => setTimeout(resolve, 0));
268
+
269
+ element.value = 150; // Above max
270
+ await new Promise(resolve => setTimeout(resolve, 0));
271
+
272
+ const input = element.shadowRoot.querySelector('input[type="range"]');
273
+ expect(parseFloat(input.value)).toBeLessThanOrEqual(100);
274
+ });
275
+
276
+ it('handles step="0.1" for decimal values', async () => {
277
+ container.innerHTML = '<ds-slider step="0.1" value="5.5"></ds-slider>';
278
+ const element = container.querySelector('ds-slider');
279
+ await new Promise(resolve => setTimeout(resolve, 0));
280
+
281
+ const input = element.shadowRoot.querySelector('input[type="range"]');
282
+ expect(input.step).toBe('0.1');
283
+ expect(element.value).toBe(5.5);
284
+ });
285
+
286
+ it('tracks isDragging state on mousedown', async () => {
287
+ container.innerHTML = '<ds-slider></ds-slider>';
288
+ const element = container.querySelector('ds-slider');
289
+ await new Promise(resolve => setTimeout(resolve, 0));
290
+
291
+ expect(element._isDragging).toBe(false);
292
+
293
+ element._handleMouseDown();
294
+ expect(element._isDragging).toBe(true);
295
+ });
296
+
297
+ it('resets isDragging on mouseup', async () => {
298
+ container.innerHTML = '<ds-slider></ds-slider>';
299
+ const element = container.querySelector('ds-slider');
300
+ await new Promise(resolve => setTimeout(resolve, 0));
301
+
302
+ element._handleMouseDown();
303
+ expect(element._isDragging).toBe(true);
304
+
305
+ element._handleMouseUp();
306
+ expect(element._isDragging).toBe(false);
307
+ });
308
+ });
@@ -0,0 +1,173 @@
1
+ import { LitElement, html, css } from 'lit';
2
+
3
+ /**
4
+ * @element ds-spinner
5
+ * @summary A loading spinner with two orbiting circles creating a 3D illusion.
6
+ *
7
+ * @prop {string} variant - 'default' | 'inverse'. Default: 'default'.
8
+ */
9
+ export class DsSpinner extends LitElement {
10
+ static properties = {
11
+ variant: { type: String, reflect: true },
12
+ size: { type: String, reflect: true },
13
+ };
14
+
15
+ static styles = css`
16
+ /*
17
+ VARIABLES FOR SIZING
18
+ Default: Base 32, Max 48, Min 21, Offset 20, Border 2
19
+ Inline: Base 12, Max 16, Min 9, Offset 7, Border 1
20
+ */
21
+ :host {
22
+ display: inline-block;
23
+ --spinner-size-base: var(--ds-size-32, 32px);
24
+ --spinner-size-max: var(--ds-size-48, 48px);
25
+ --spinner-size-min: 21px; /* Calculated: no token */
26
+ --spinner-translate-offset: var(--ds-size-20, 20px);
27
+ --spinner-border-width: var(--ds-size-2, 2px);
28
+ width: 80px;
29
+ height: 48px;
30
+ }
31
+
32
+ :host([size="inline"]) {
33
+ --spinner-size-base: var(--ds-size-12, 12px);
34
+ --spinner-size-max: var(--ds-size-16, 16px);
35
+ --spinner-size-min: 9px; /* Calculated: no token */
36
+ --spinner-translate-offset: 7px; /* Calculated: no token */
37
+ --spinner-border-width: 1px; /* No 1px token available */
38
+ width: 32px;
39
+ height: 16px;
40
+ }
41
+
42
+ .spinner-container {
43
+ position: relative;
44
+ width: 100%;
45
+ height: 100%;
46
+ display: flex;
47
+ align-items: center;
48
+ justify-content: center;
49
+ }
50
+
51
+ .circle {
52
+ position: absolute;
53
+ width: var(--spinner-size-base);
54
+ height: var(--spinner-size-base);
55
+ border-radius: 50%;
56
+ border-width: var(--spinner-border-width);
57
+ border-style: solid;
58
+ box-sizing: border-box;
59
+ top: 50%;
60
+ left: 50%;
61
+ transform: translate(-50%, -50%); /* Start centered */
62
+ will-change: width, height, transform, z-index;
63
+ }
64
+
65
+ /* DEFAULT VARIANT */
66
+ :host([variant="default"]) .circle,
67
+ :host(:not([variant])) .circle {
68
+ background-color: var(--ds-color-bg-default);
69
+ border-color: var(--ds-color-border-brand);
70
+ }
71
+
72
+ /* INVERSE VARIANT */
73
+ :host([variant="inverse"]) .circle {
74
+ background-color: var(--ds-color-bg-brand);
75
+ border-color: var(--ds-color-border-inverse);
76
+ }
77
+
78
+ .circle-1 {
79
+ animation: orbit-1 2s linear infinite;
80
+ z-index: 1;
81
+ }
82
+
83
+ .circle-2 {
84
+ animation: orbit-2 2s linear infinite;
85
+ z-index: 0;
86
+ }
87
+
88
+ @keyframes orbit-1 {
89
+ 0% {
90
+ transform: translate(calc(-50% - var(--spinner-translate-offset)), -50%); /* Left */
91
+ width: var(--spinner-size-base);
92
+ height: var(--spinner-size-base);
93
+ z-index: 1;
94
+ }
95
+ 25% {
96
+ transform: translate(-50%, -50%); /* Center */
97
+ width: var(--spinner-size-max);
98
+ height: var(--spinner-size-max);
99
+ z-index: 2; /* Front */
100
+ }
101
+ 50% {
102
+ transform: translate(calc(-50% + var(--spinner-translate-offset)), -50%); /* Right */
103
+ width: var(--spinner-size-base);
104
+ height: var(--spinner-size-base);
105
+ z-index: 1;
106
+ }
107
+ 75% {
108
+ transform: translate(-50%, -50%); /* Center */
109
+ width: var(--spinner-size-min);
110
+ height: var(--spinner-size-min);
111
+ z-index: 0; /* Back */
112
+ }
113
+ 100% {
114
+ transform: translate(calc(-50% - var(--spinner-translate-offset)), -50%); /* Left */
115
+ width: var(--spinner-size-base);
116
+ height: var(--spinner-size-base);
117
+ z-index: 1;
118
+ }
119
+ }
120
+
121
+ @keyframes orbit-2 {
122
+ 0% {
123
+ transform: translate(calc(-50% + var(--spinner-translate-offset)), -50%); /* Right */
124
+ width: var(--spinner-size-base);
125
+ height: var(--spinner-size-base);
126
+ z-index: 0;
127
+ }
128
+ 25% {
129
+ transform: translate(-50%, -50%); /* Center */
130
+ width: var(--spinner-size-min);
131
+ height: var(--spinner-size-min);
132
+ z-index: 0; /* Back */
133
+ }
134
+ 50% {
135
+ transform: translate(calc(-50% - var(--spinner-translate-offset)), -50%); /* Left */
136
+ width: var(--spinner-size-base);
137
+ height: var(--spinner-size-base);
138
+ z-index: 1;
139
+ }
140
+ 75% {
141
+ transform: translate(-50%, -50%); /* Center */
142
+ width: var(--spinner-size-max);
143
+ height: var(--spinner-size-max);
144
+ z-index: 2; /* Front */
145
+ }
146
+ 100% {
147
+ transform: translate(calc(-50% + var(--spinner-translate-offset)), -50%); /* Right */
148
+ width: var(--spinner-size-base);
149
+ height: var(--spinner-size-base);
150
+ z-index: 0;
151
+ }
152
+ }
153
+ `;
154
+
155
+ constructor() {
156
+ super();
157
+ this.variant = 'default';
158
+ this.size = 'default';
159
+ }
160
+
161
+ render() {
162
+ return html`
163
+ <div class="spinner-container" aria-label="Loading" role="status">
164
+ <div class="circle circle-1"></div>
165
+ <div class="circle circle-2"></div>
166
+ </div>
167
+ `;
168
+ }
169
+ }
170
+
171
+ if (!customElements.get('ds-spinner')) {
172
+ customElements.define('ds-spinner', DsSpinner);
173
+ }
@@ -0,0 +1,52 @@
1
+ import { html } from 'lit';
2
+ import './ds-spinner.js';
3
+
4
+ export default {
5
+ title: 'Components/Spinner',
6
+ component: 'ds-spinner',
7
+ argTypes: {
8
+ variant: {
9
+ control: 'select',
10
+ options: ['default', 'inverse']
11
+ },
12
+ size: {
13
+ control: 'select',
14
+ options: ['default', 'inline']
15
+ }
16
+ },
17
+ };
18
+
19
+ const Template = (args) => html`
20
+ <ds-spinner .variant=${args.variant} .size=${args.size}></ds-spinner>
21
+ `;
22
+
23
+ export const Default = Template.bind({});
24
+ Default.args = {
25
+ variant: 'default',
26
+ };
27
+
28
+ export const Inverse = Template.bind({});
29
+ Inverse.args = {
30
+ variant: 'inverse',
31
+ };
32
+
33
+ export const Inline = Template.bind({});
34
+ Inline.args = {
35
+ size: 'inline',
36
+ };
37
+
38
+ export const OverlayExample = Template.bind({});
39
+ OverlayExample.args = {
40
+ variant: 'default',
41
+ size: 'default'
42
+ };
43
+ OverlayExample.decorators = [
44
+ (Story) => html`
45
+ <div style="position: relative; width: 300px; height: 150px; background: #f0f0f0; display: flex; align-items: center; justify-content: center; border: 1px solid #ccc;">
46
+ <p>Content Loading...</p>
47
+ <div style="position: absolute; inset: 0; background: rgba(255,255,255,0.8); display: flex; align-items: center; justify-content: center;">
48
+ ${Story()}
49
+ </div>
50
+ </div>
51
+ `
52
+ ];
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import './ds-spinner.js';
3
+
4
+ describe('ds-spinner', () => {
5
+ let container;
6
+
7
+ beforeEach(() => {
8
+ container = document.createElement('div');
9
+ document.body.appendChild(container);
10
+ });
11
+
12
+ afterEach(() => {
13
+ container.remove();
14
+ });
15
+
16
+ it('renders with default role', async () => {
17
+ container.innerHTML = '<ds-spinner></ds-spinner>';
18
+ const el = container.querySelector('ds-spinner');
19
+ await new Promise(r => setTimeout(r, 50));
20
+
21
+ const spinner = el.shadowRoot.querySelector('.spinner-container');
22
+ expect(spinner.getAttribute('role')).toBe('status');
23
+ expect(spinner.getAttribute('aria-label')).toBe('Loading');
24
+ });
25
+
26
+ it('renders two circles', async () => {
27
+ container.innerHTML = '<ds-spinner></ds-spinner>';
28
+ const el = container.querySelector('ds-spinner');
29
+ await new Promise(r => setTimeout(r, 50));
30
+
31
+ const circles = el.shadowRoot.querySelectorAll('.circle');
32
+ expect(circles.length).toBe(2);
33
+ });
34
+
35
+ it('applies inverse variant attribute', async () => {
36
+ container.innerHTML = '<ds-spinner variant="inverse"></ds-spinner>';
37
+ const el = container.querySelector('ds-spinner');
38
+ await new Promise(r => setTimeout(r, 50));
39
+
40
+ expect(el.getAttribute('variant')).toBe('inverse');
41
+ });
42
+
43
+ it('applies inline size attribute', async () => {
44
+ container.innerHTML = '<ds-spinner size="inline"></ds-spinner>';
45
+ const el = container.querySelector('ds-spinner');
46
+ await new Promise(r => setTimeout(r, 50));
47
+
48
+ expect(el.getAttribute('size')).toBe('inline');
49
+ });
50
+ });
@@ -0,0 +1,88 @@
1
+ import { LitElement, html, css } from 'lit';
2
+
3
+ /**
4
+ * Status Border — reusable visual indicator for status states.
5
+ * Used across rich list items, cards, and KPIs.
6
+ *
7
+ * @element ds-status-border
8
+ *
9
+ * @prop {string} status - Status type (critical, danger, fail, warning-major, warning-minor, success, info, inprogress, pending, neutral, new)
10
+ * @prop {string} orientation - vertical (default) or horizontal
11
+ * @prop {boolean} selected - Increases thickness from 2px to 4px
12
+ * @prop {boolean} disabled - Visually disables the border (opacity: 0.5)
13
+ *
14
+ * @csspart border - The border element
15
+ */
16
+ export class DsStatusBorder extends LitElement {
17
+ static properties = {
18
+ status: { type: String, reflect: true },
19
+ orientation: { type: String, reflect: true },
20
+ selected: { type: Boolean, reflect: true },
21
+ disabled: { type: Boolean, reflect: true }
22
+ };
23
+
24
+ static styles = css`
25
+ :host {
26
+ display: block;
27
+ }
28
+
29
+ /* ─── Status Color Mapping ─── */
30
+ :host([status="critical"]) { --status-color: var(--ds-color-status-critical); }
31
+ :host([status="danger"]) { --status-color: var(--ds-color-status-danger); }
32
+ :host([status="fail"]) { --status-color: var(--ds-color-status-fail); }
33
+ :host([status="warning-major"]) { --status-color: var(--ds-color-status-warning-major); }
34
+ :host([status="warning-minor"]) { --status-color: var(--ds-color-status-warning-minor); }
35
+ :host([status="success"]) { --status-color: var(--ds-color-status-success); }
36
+ :host([status="info"]) { --status-color: var(--ds-color-status-info); }
37
+ :host([status="inprogress"]) { --status-color: var(--ds-color-status-inprogress); }
38
+ :host([status="pending"]) { --status-color: var(--ds-color-status-pending); }
39
+ :host([status="neutral"]) { --status-color: var(--ds-color-status-neutral); }
40
+ :host([status="new"]) { --status-color: var(--ds-color-status-new); }
41
+
42
+ /* ─── Border Element ─── */
43
+ .border {
44
+ background: var(--status-color, var(--ds-color-status-neutral));
45
+ width: 100%;
46
+ height: 100%;
47
+ }
48
+
49
+ /* ─── Vertical (default) ─── */
50
+ :host(:not([orientation="horizontal"])) {
51
+ width: 2px;
52
+ height: 100%;
53
+ }
54
+
55
+ :host(:not([orientation="horizontal"])[selected]) {
56
+ width: 4px;
57
+ }
58
+
59
+ /* ─── Horizontal ─── */
60
+ :host([orientation="horizontal"]) {
61
+ width: 100%;
62
+ height: 2px;
63
+ }
64
+
65
+ :host([orientation="horizontal"][selected]) {
66
+ height: 4px;
67
+ }
68
+
69
+ /* ─── Disabled ─── */
70
+ :host([disabled]) {
71
+ opacity: 0.5;
72
+ }
73
+ `;
74
+
75
+ constructor() {
76
+ super();
77
+ this.status = 'neutral';
78
+ this.orientation = 'vertical';
79
+ this.selected = false;
80
+ this.disabled = false;
81
+ }
82
+
83
+ render() {
84
+ return html`<div class="border" part="border"></div>`;
85
+ }
86
+ }
87
+
88
+ customElements.define('ds-status-border', DsStatusBorder);