@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,232 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import '../ds-icon-button/ds-icon-button.js';
3
+ import '../ds-input/ds-input.js';
4
+
5
+ /**
6
+ * Pagination component for navigating through multiple pages of content.
7
+ *
8
+ * @element ds-pagination
9
+ *
10
+ * @prop {number} total - Total number of pages (required).
11
+ * @prop {number} current - Current active page (default: 1).
12
+ * @prop {string} variant - Pagination style: 'default' | 'dot' (default: 'default').
13
+ *
14
+ * @fires ds-change - Fired when the page changes. Detail: { page: number }
15
+ */
16
+ export class DsPagination extends LitElement {
17
+ static properties = {
18
+ total: { type: Number },
19
+ current: { type: Number },
20
+ variant: { type: String }
21
+ };
22
+
23
+ static styles = css`
24
+ :host {
25
+ display: inline-block;
26
+ vertical-align: middle;
27
+ }
28
+
29
+ /* Reset & Base Layout */
30
+ .ds-pagination {
31
+ display: block;
32
+ }
33
+
34
+ .ds-pagination__list {
35
+ display: flex;
36
+ align-items: center;
37
+ list-style: none;
38
+ margin: 0;
39
+ padding: 0;
40
+ gap: var(--ds-space-xs, 4px); /* Base 4px gap between most items */
41
+ }
42
+
43
+ .ds-pagination__item {
44
+ display: flex;
45
+ align-items: center;
46
+ }
47
+
48
+ /* Input Group Specifics (Default Variant) */
49
+
50
+ /* The item containing the input group needs extra spacing */
51
+ .ds-pagination__item--input {
52
+ margin: 0 var(--ds-space-xs, 4px); /* Adds 4px extra on each side. Total gap = 4px(gap) + 4px(margin) = 8px */
53
+ }
54
+
55
+ .ds-pagination__input-group {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: var(--ds-space-sm, 8px); /* Space between Input and Total Text */
59
+ }
60
+
61
+ .ds-pagination__text {
62
+ font: var(--ds-typo-content-body-regular);
63
+ color: var(--ds-color-text-default);
64
+ white-space: nowrap;
65
+ }
66
+
67
+ /* Screen reader only - for live region */
68
+ .sr-only {
69
+ position: absolute;
70
+ width: 1px;
71
+ height: 1px;
72
+ padding: 0;
73
+ margin: -1px;
74
+ overflow: hidden;
75
+ clip: rect(0, 0, 0, 0);
76
+ white-space: nowrap;
77
+ border: 0;
78
+ }
79
+
80
+ /* Dot Variant Specifics */
81
+ .variant-dot .ds-pagination__list {
82
+ gap: var(--ds-space-xs, 4px);
83
+ }
84
+
85
+ .variant-dot ds-icon-button::part(button) {
86
+ color: var(--ds-color-icon-secondary);
87
+ }
88
+ `;
89
+
90
+ constructor() {
91
+ super();
92
+ this.total = 1;
93
+ this.current = 1;
94
+ this.variant = 'default';
95
+ }
96
+
97
+ _dispatchChange(newPage) {
98
+ if (newPage >= 1 && newPage <= this.total && newPage !== this.current) {
99
+ this.current = newPage;
100
+ this.dispatchEvent(new CustomEvent('ds-change', {
101
+ detail: { page: this.current },
102
+ bubbles: true,
103
+ composed: true
104
+ }));
105
+ }
106
+ }
107
+
108
+ _handleFirst() { this._dispatchChange(1); }
109
+ _handlePrev() { this._dispatchChange(this.current - 1); }
110
+ _handleNext() { this._dispatchChange(this.current + 1); }
111
+ _handleLast() { this._dispatchChange(this.total); }
112
+
113
+ _handleInputFocus(e) {
114
+ // Use new public API instead of shadow DOM access
115
+ e.target.selectAll();
116
+ }
117
+
118
+ _handleInputChange(e) {
119
+ // ds-input now handles clamping via min/max/clamp props
120
+ // We just need to dispatch the change if value differs
121
+ const val = parseInt(e.target.value, 10);
122
+
123
+ if (!isNaN(val) && val !== this.current) {
124
+ this._dispatchChange(val);
125
+ }
126
+ }
127
+
128
+ renderDefault() {
129
+ return html`
130
+ <nav class="ds-pagination" aria-label="Pagination">
131
+ <ul class="ds-pagination__list">
132
+ <li class="ds-pagination__item">
133
+ <ds-icon-button
134
+ icon="first-page"
135
+ variant="action"
136
+ size="m"
137
+ ?disabled=${this.current === 1}
138
+ aria-label="First page"
139
+ @click=${this._handleFirst}
140
+ ></ds-icon-button>
141
+ </li>
142
+ <li class="ds-pagination__item">
143
+ <ds-icon-button
144
+ icon="chevron-left"
145
+ variant="action"
146
+ size="m"
147
+ ?disabled=${this.current === 1}
148
+ aria-label="Previous page"
149
+ @click=${this._handlePrev}
150
+ ></ds-icon-button>
151
+ </li>
152
+
153
+ <li class="ds-pagination__item ds-pagination__item--input">
154
+ <div class="ds-pagination__input-group">
155
+ <ds-input
156
+ class="page-input"
157
+ type="number"
158
+ width="60px"
159
+ text-align="center"
160
+ hide-steppers
161
+ autocomplete="off"
162
+ min="1"
163
+ .max=${this.total}
164
+ clamp
165
+ .value=${this.current.toString()}
166
+ aria-label="Current page"
167
+ @change=${this._handleInputChange}
168
+ @focus=${this._handleInputFocus}
169
+ ></ds-input>
170
+ <span class="ds-pagination__text">/ ${this.total}</span>
171
+ </div>
172
+ </li>
173
+
174
+ <li class="ds-pagination__item">
175
+ <ds-icon-button
176
+ icon="chevron-right"
177
+ variant="action"
178
+ size="m"
179
+ ?disabled=${this.current === this.total}
180
+ aria-label="Next page"
181
+ @click=${this._handleNext}
182
+ ></ds-icon-button>
183
+ </li>
184
+ <li class="ds-pagination__item">
185
+ <ds-icon-button
186
+ icon="last-page"
187
+ variant="action"
188
+ size="m"
189
+ ?disabled=${this.current === this.total}
190
+ aria-label="Last page"
191
+ @click=${this._handleLast}
192
+ ></ds-icon-button>
193
+ </li>
194
+ </ul>
195
+
196
+ <!-- Live region for accessibility -->
197
+ <span class="sr-only" role="status" aria-live="polite">
198
+ Page ${this.current} of ${this.total}
199
+ </span>
200
+ </nav>
201
+ `;
202
+ }
203
+
204
+ renderDot() {
205
+ const pages = Array.from({ length: this.total }, (_, i) => i + 1);
206
+
207
+ return html`
208
+ <nav class="ds-pagination variant-dot" aria-label="Pagination">
209
+ <ul class="ds-pagination__list">
210
+ ${pages.map(page => html`
211
+ <li class="ds-pagination__item">
212
+ <ds-icon-button
213
+ icon=${page === this.current ? 'circle-filled' : 'circle'}
214
+ variant="action"
215
+ size="m"
216
+ aria-label="Page ${page}"
217
+ aria-current=${page === this.current ? 'page' : 'false'}
218
+ @click=${() => this._dispatchChange(page)}
219
+ ></ds-icon-button>
220
+ </li>
221
+ `)}
222
+ </ul>
223
+ </nav>
224
+ `;
225
+ }
226
+
227
+ render() {
228
+ return this.variant === 'dot' ? this.renderDot() : this.renderDefault();
229
+ }
230
+ }
231
+
232
+ customElements.define('ds-pagination', DsPagination);
@@ -0,0 +1,63 @@
1
+ import { html } from 'lit';
2
+ import './ds-pagination.js';
3
+
4
+ export default {
5
+ title: 'Components/Pagination',
6
+ component: 'ds-pagination',
7
+ parameters: {
8
+ actions: {
9
+ handles: ['ds-change'],
10
+ },
11
+ },
12
+ argTypes: {
13
+ total: { control: 'number' },
14
+ current: { control: 'number' },
15
+ variant: {
16
+ control: { type: 'select' },
17
+ options: ['default', 'dot'],
18
+ },
19
+ },
20
+ };
21
+
22
+ const Template = (args) => html`
23
+ <ds-pagination
24
+ .total=${args.total}
25
+ .current=${args.current}
26
+ .variant=${args.variant}
27
+ ></ds-pagination>
28
+ `;
29
+
30
+ export const Default = Template.bind({});
31
+ Default.args = {
32
+ total: 10,
33
+ current: 1,
34
+ variant: 'default',
35
+ };
36
+
37
+ export const MiddlePage = Template.bind({});
38
+ MiddlePage.args = {
39
+ total: 10,
40
+ current: 5,
41
+ variant: 'default',
42
+ };
43
+
44
+ export const LastPage = Template.bind({});
45
+ LastPage.args = {
46
+ total: 10,
47
+ current: 10,
48
+ variant: 'default',
49
+ };
50
+
51
+ export const DotVariant = Template.bind({});
52
+ DotVariant.args = {
53
+ total: 5,
54
+ current: 1,
55
+ variant: 'dot',
56
+ };
57
+
58
+ export const DotVariantMax = Template.bind({});
59
+ DotVariantMax.args = {
60
+ total: 6,
61
+ current: 3,
62
+ variant: 'dot',
63
+ };
@@ -0,0 +1,141 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import './ds-pagination.js';
3
+
4
+ describe('ds-pagination', () => {
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('should render default variant with correct initial state', async () => {
17
+ container.innerHTML = '<ds-pagination total="10"></ds-pagination>';
18
+ const el = container.querySelector('ds-pagination');
19
+ await new Promise(resolve => setTimeout(resolve, 0));
20
+
21
+ const nav = el.shadowRoot.querySelector('nav.ds-pagination');
22
+ expect(nav).to.exist;
23
+
24
+ const list = el.shadowRoot.querySelector('ul.ds-pagination__list');
25
+ expect(list).to.exist;
26
+
27
+ const buttons = el.shadowRoot.querySelectorAll('ds-icon-button');
28
+ expect(buttons.length).toBe(4); // First, Prev, Next, Last
29
+
30
+ expect(buttons[0].hasAttribute('disabled')).toBe(true);
31
+ expect(buttons[1].hasAttribute('disabled')).toBe(true);
32
+ expect(buttons[2].hasAttribute('disabled')).toBe(false);
33
+ expect(buttons[3].hasAttribute('disabled')).toBe(false);
34
+
35
+ const input = el.shadowRoot.querySelector('ds-input');
36
+ expect(input.getAttribute('type')).toBe('number');
37
+ expect(input.hasAttribute('hide-steppers')).toBe(true);
38
+ expect(input.value).toBe('1');
39
+ });
40
+
41
+ it('should handle navigation via buttons', async () => {
42
+ container.innerHTML = '<ds-pagination total="10" current="5"></ds-pagination>';
43
+ const el = container.querySelector('ds-pagination');
44
+ await new Promise(resolve => setTimeout(resolve, 0));
45
+
46
+ const buttons = el.shadowRoot.querySelectorAll('ds-icon-button');
47
+
48
+ // Next button (index 2)
49
+ buttons[2].click();
50
+ await new Promise(resolve => setTimeout(resolve, 0));
51
+ expect(el.current).toBe(6);
52
+
53
+ // Prev button (index 1)
54
+ buttons[1].click();
55
+ await new Promise(resolve => setTimeout(resolve, 0));
56
+ expect(el.current).toBe(5);
57
+ });
58
+
59
+ it('should handle input change (valid number)', async () => {
60
+ container.innerHTML = '<ds-pagination total="10"></ds-pagination>';
61
+ const el = container.querySelector('ds-pagination');
62
+ await new Promise(resolve => setTimeout(resolve, 0));
63
+
64
+ const input = el.shadowRoot.querySelector('ds-input');
65
+ input.value = '5';
66
+ input.dispatchEvent(new CustomEvent('change', { detail: { value: '5' } }));
67
+
68
+ await new Promise(resolve => setTimeout(resolve, 0));
69
+ expect(el.current).toBe(5);
70
+ });
71
+
72
+ it('should clamp input values via ds-input clamp prop', async () => {
73
+ container.innerHTML = '<ds-pagination total="10" current="5"></ds-pagination>';
74
+ const el = container.querySelector('ds-pagination');
75
+ await new Promise(resolve => setTimeout(resolve, 0));
76
+
77
+ const input = el.shadowRoot.querySelector('ds-input');
78
+ const nativeInput = input.shadowRoot.querySelector('input');
79
+
80
+ // Assert autocomplete off
81
+ expect(input.getAttribute('autocomplete')).toBe('off');
82
+
83
+ // Assert width and clamp props are set
84
+ expect(input.getAttribute('width')).toBe('60px');
85
+ expect(input.hasAttribute('clamp')).toBe(true);
86
+ expect(input.getAttribute('min')).toBe('1');
87
+
88
+ // Test max clamp (ds-input clamps on blur)
89
+ nativeInput.value = '15';
90
+ input.value = '15';
91
+ nativeInput.dispatchEvent(new Event('blur'));
92
+ await new Promise(resolve => setTimeout(resolve, 0));
93
+
94
+ // ds-input should have clamped the value to max (10)
95
+ expect(input.value).toBe('10');
96
+
97
+ // Test min clamp
98
+ nativeInput.value = '0';
99
+ input.value = '0';
100
+ nativeInput.dispatchEvent(new Event('blur'));
101
+ await new Promise(resolve => setTimeout(resolve, 0));
102
+
103
+ expect(input.value).toBe('1');
104
+ });
105
+
106
+ it('should clamp on Enter key via ds-input', async () => {
107
+ container.innerHTML = '<ds-pagination total="10"></ds-pagination>';
108
+ const el = container.querySelector('ds-pagination');
109
+ await new Promise(resolve => setTimeout(resolve, 0));
110
+
111
+ const input = el.shadowRoot.querySelector('ds-input');
112
+ const nativeInput = input.shadowRoot.querySelector('input');
113
+
114
+ // Type an out-of-range value
115
+ nativeInput.value = '99';
116
+ input.value = '99';
117
+
118
+ // Press Enter - ds-input should clamp
119
+ nativeInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
120
+ await new Promise(resolve => setTimeout(resolve, 0));
121
+
122
+ // Input should be clamped to max
123
+ expect(input.value).toBe('10');
124
+ });
125
+
126
+ it('should render dot variant', async () => {
127
+ container.innerHTML = '<ds-pagination variant="dot" total="5"></ds-pagination>';
128
+ const el = container.querySelector('ds-pagination');
129
+ await new Promise(resolve => setTimeout(resolve, 0));
130
+
131
+ const nav = el.shadowRoot.querySelector('nav.variant-dot');
132
+ expect(nav).to.exist;
133
+
134
+ const dots = el.shadowRoot.querySelectorAll('ds-icon-button');
135
+ expect(dots.length).toBe(5);
136
+
137
+ // Default current is 1 (index 0)
138
+ expect(dots[0].getAttribute('icon')).toBe('circle-filled');
139
+ expect(dots[1].getAttribute('icon')).toBe('circle');
140
+ });
141
+ });
@@ -0,0 +1 @@
1
+ export { DsPagination } from './ds-pagination.js';
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import axe from 'axe-core';
3
+ import './ds-progress-bar.js';
4
+
5
+ describe('ds-progress-bar a11y', () => {
6
+ let container;
7
+
8
+ beforeEach(() => {
9
+ container = document.createElement('div');
10
+ document.body.appendChild(container);
11
+ });
12
+
13
+ afterEach(() => {
14
+ container.remove();
15
+ });
16
+
17
+ it('passes accessibility checks', async () => {
18
+ container.innerHTML = '<ds-progress-bar value="50" label="Loading content"></ds-progress-bar>';
19
+ const el = container.querySelector('ds-progress-bar');
20
+ await new Promise(r => setTimeout(r, 50));
21
+
22
+ const results = await axe.run(el);
23
+ expect(results.violations.length).toBe(0);
24
+ });
25
+ });
@@ -0,0 +1,81 @@
1
+ import { LitElement, html, css } from 'lit';
2
+
3
+ /**
4
+ * @element ds-progress-bar
5
+ * @summary A linear progress bar component.
6
+ *
7
+ * @prop {number} value - Current progress value. Default: 0.
8
+ * @prop {number} max - Maximum progress value. Default: 100.
9
+ * @prop {string} status - 'default' | 'error' | 'finished'. Default: 'default'.
10
+ * @prop {string} label - Accessible label.
11
+ */
12
+ export class DsProgressBar extends LitElement {
13
+ static properties = {
14
+ value: { type: Number },
15
+ max: { type: Number },
16
+ status: { type: String, reflect: true },
17
+ label: { type: String },
18
+ };
19
+
20
+ static styles = css`
21
+ :host {
22
+ display: block;
23
+ width: 100%;
24
+ }
25
+
26
+ .progress-track {
27
+ background-color: var(--ds-color-border-default);
28
+ height: 4px;
29
+ border-radius: var(--ds-radius-container, 0px); /* Sharp / Container radius */
30
+ overflow: hidden;
31
+ width: 100%;
32
+ }
33
+
34
+ .progress-indicator {
35
+ height: 100%;
36
+ background-color: var(--ds-color-border-brand);
37
+ transition: width 0.2s ease-in-out;
38
+ width: 0%;
39
+ }
40
+
41
+ /* ERROR STATUS */
42
+ :host([status="error"]) .progress-indicator {
43
+ background-color: var(--ds-color-border-error);
44
+ }
45
+ `;
46
+
47
+ constructor() {
48
+ super();
49
+ this.value = 0;
50
+ this.max = 100;
51
+ this.status = 'default';
52
+ }
53
+
54
+ render() {
55
+ const max = (this.max && this.max > 0) ? this.max : 100;
56
+ const value = this.value || 0;
57
+
58
+ let percentage = Math.min(100, Math.max(0, (value / max) * 100));
59
+
60
+ if (this.status === 'finished') {
61
+ percentage = 100;
62
+ }
63
+
64
+ return html`
65
+ <div
66
+ role="progressbar"
67
+ class="progress-track"
68
+ aria-valuenow="${this.status === 'finished' ? max : value}"
69
+ aria-valuemin="0"
70
+ aria-valuemax="${max}"
71
+ aria-label="${this.label || ''}"
72
+ >
73
+ <div class="progress-indicator" style="width: ${percentage}%;"></div>
74
+ </div>
75
+ `;
76
+ }
77
+ }
78
+
79
+ if (!customElements.get('ds-progress-bar')) {
80
+ customElements.define('ds-progress-bar', DsProgressBar);
81
+ }
@@ -0,0 +1,69 @@
1
+ import { html } from 'lit';
2
+ import { ifDefined } from 'lit/directives/if-defined.js';
3
+ import './ds-progress-bar.js';
4
+
5
+ export default {
6
+ title: 'Components/Progress Bar',
7
+ component: 'ds-progress-bar',
8
+ argTypes: {
9
+ value: { control: 'number' },
10
+ max: { control: 'number' },
11
+ status: {
12
+ control: 'select',
13
+ options: ['default', 'error', 'finished']
14
+ },
15
+ label: { control: 'text' }
16
+ },
17
+ };
18
+
19
+ const Template = (args) => html`
20
+ <div style="width: 300px;">
21
+ <ds-progress-bar
22
+ .value=${args.value}
23
+ .max=${args.max}
24
+ .status=${ifDefined(args.status)}
25
+ label=${ifDefined(args.label)}
26
+ ></ds-progress-bar>
27
+ </div>
28
+ `;
29
+
30
+ export const Default = Template.bind({});
31
+ Default.args = {
32
+ value: 50,
33
+ max: 100,
34
+ status: 'default',
35
+ label: 'Loading...',
36
+ };
37
+
38
+ export const Full = Template.bind({});
39
+ Full.args = {
40
+ value: 100,
41
+ max: 100,
42
+ status: 'default',
43
+ };
44
+
45
+ export const Empty = Template.bind({});
46
+ Empty.args = {
47
+ value: 0,
48
+ max: 100,
49
+ };
50
+
51
+ export const ErrorState = Template.bind({});
52
+ ErrorState.args = {
53
+ value: 75,
54
+ max: 100,
55
+ status: 'error',
56
+ label: 'Upload Failed',
57
+ };
58
+
59
+ export const LargeScale = Template.bind({});
60
+ LargeScale.args = {
61
+ value: 450,
62
+ max: 1000,
63
+ status: 'default',
64
+ label: 'Processing Items',
65
+ };
66
+ LargeScale.argTypes = {
67
+ value: { control: { type: 'range', min: 0, max: 1000, step: 10 } },
68
+ max: { control: { type: 'number' } }
69
+ };
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import './ds-progress-bar.js';
3
+
4
+ describe('ds-progress-bar', () => {
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-progress-bar></ds-progress-bar>';
18
+ const el = container.querySelector('ds-progress-bar');
19
+ await new Promise(r => setTimeout(r, 50));
20
+
21
+ const track = el.shadowRoot.querySelector('.progress-track');
22
+ expect(track).toBeTruthy();
23
+ expect(track.getAttribute('role')).toBe('progressbar');
24
+ expect(track.getAttribute('aria-valuenow')).toBe('0');
25
+ });
26
+
27
+ it('reflects value updates in width', async () => {
28
+ container.innerHTML = '<ds-progress-bar value="50" max="100"></ds-progress-bar>';
29
+ const el = container.querySelector('ds-progress-bar');
30
+ await new Promise(r => setTimeout(r, 50));
31
+
32
+ const indicator = el.shadowRoot.querySelector('.progress-indicator');
33
+ expect(indicator.style.width).toBe('50%');
34
+ });
35
+
36
+ it('calculates percentage correctly with custom max', async () => {
37
+ container.innerHTML = '<ds-progress-bar value="50" max="200"></ds-progress-bar>';
38
+ const el = container.querySelector('ds-progress-bar');
39
+ await new Promise(r => setTimeout(r, 50));
40
+
41
+ const indicator = el.shadowRoot.querySelector('.progress-indicator');
42
+ // 50 / 200 = 25%
43
+ expect(indicator.style.width).toBe('25%');
44
+ });
45
+
46
+ it('applies error styling', async () => {
47
+ container.innerHTML = '<ds-progress-bar status="error"></ds-progress-bar>';
48
+ const el = container.querySelector('ds-progress-bar');
49
+ await new Promise(r => setTimeout(r, 50));
50
+
51
+ // We check if the attribute is reflected (Lit reflect: true)
52
+ expect(el.getAttribute('status')).toBe('error');
53
+
54
+ // We can also check computed style if needed, but checking the attribute reflection is usually enough for the test if the CSS relies on :host([status="error"])
55
+ const indicator = el.shadowRoot.querySelector('.progress-indicator');
56
+ const style = getComputedStyle(indicator);
57
+ // Since we are not in a full browser environment with variables loaded, checking the exact color might be tricky unless mocked.
58
+ // Verification of logic is sufficient here.
59
+ });
60
+ });