@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,194 @@
1
+ import { LitElement, html, css } from 'lit';
2
+
3
+ /**
4
+ * @element ds-breadcrumbs
5
+ * @summary A navigation container for breadcrumb items.
6
+ *
7
+ * @slot - Default slot for ds-breadcrumb-item elements.
8
+ */
9
+ export class DsBreadcrumbs extends LitElement {
10
+ static properties = {
11
+ mode: { type: String, reflect: true },
12
+ expanded: { type: Boolean, reflect: true }
13
+ };
14
+
15
+ static styles = css`
16
+ :host {
17
+ display: block;
18
+ width: 100%;
19
+ }
20
+
21
+ ol {
22
+ display: flex;
23
+ flex-wrap: wrap;
24
+ list-style: none;
25
+ margin: 0;
26
+ padding: 0;
27
+ align-items: center;
28
+ gap: var(--ds-space-sm, 8px);
29
+ }
30
+
31
+ /* Scroll Mode, Collapse Mode, and formatting */
32
+ :host([mode="scroll"]) ol,
33
+ :host([mode="collapse"]) ol {
34
+ flex-wrap: nowrap;
35
+ }
36
+
37
+ /* When expanded in collapse mode: ALWAYS scroll */
38
+ :host([mode="collapse"][expanded]) ol {
39
+ flex-wrap: nowrap;
40
+ overflow-x: auto;
41
+ }
42
+
43
+ :host([mode="scroll"]) ol,
44
+ :host([mode="collapse"]:not([expanded])) ol {
45
+ overflow-x: auto;
46
+ white-space: nowrap;
47
+ scrollbar-width: none;
48
+ }
49
+
50
+ /* Hide scrollbar for strictly scrolled views (collapsed state) */
51
+ :host([mode="scroll"]) ol::-webkit-scrollbar,
52
+ :host([mode="collapse"]:not([expanded])) ol::-webkit-scrollbar {
53
+ display: none;
54
+ }
55
+
56
+ /* Prevent shrinking */
57
+ :host([mode="scroll"]) ::slotted(ds-breadcrumb-item),
58
+ :host([mode="collapse"]) ::slotted(ds-breadcrumb-item) {
59
+ flex-shrink: 0;
60
+ }
61
+
62
+ /* Hide the separator of the last item */
63
+ ::slotted(ds-breadcrumb-item:last-child) {
64
+ --ds-breadcrumb-separator-display: none;
65
+ }
66
+ `;
67
+
68
+ constructor() {
69
+ super();
70
+ this.mode = 'wrap';
71
+ this.expanded = false;
72
+ this._resizeObserver = null;
73
+ }
74
+
75
+ connectedCallback() {
76
+ super.connectedCallback();
77
+ this.addEventListener('ds-expand', this._handleExpand);
78
+
79
+ this.updateComplete.then(() => {
80
+ this._setupResizeObserver();
81
+ });
82
+ }
83
+
84
+ disconnectedCallback() {
85
+ super.disconnectedCallback();
86
+ this.removeEventListener('ds-expand', this._handleExpand);
87
+ if (this._resizeObserver) {
88
+ this._resizeObserver.disconnect();
89
+ }
90
+ }
91
+
92
+ updated(changedProperties) {
93
+ if (changedProperties.has('mode')) {
94
+ this.expanded = false;
95
+
96
+ if (this.mode === 'collapse') {
97
+ this._setupResizeObserver();
98
+ this._checkOverflow();
99
+ } else {
100
+ if (this._resizeObserver) {
101
+ this._resizeObserver.disconnect();
102
+ this._resizeObserver = null;
103
+ }
104
+ this._resetItems();
105
+ }
106
+ }
107
+
108
+ if (changedProperties.has('expanded') && this.expanded) {
109
+ // If expanded changed to true, ensured items are visible
110
+ this._resetItems();
111
+ }
112
+ }
113
+
114
+ _setupResizeObserver() {
115
+ if (this.mode !== 'collapse') return;
116
+ if (this._resizeObserver) return;
117
+
118
+ this._resizeObserver = new ResizeObserver((entries) => {
119
+ window.requestAnimationFrame(() => {
120
+ if (!this.expanded) {
121
+ this._checkOverflow();
122
+ }
123
+ });
124
+ });
125
+ this._resizeObserver.observe(this);
126
+ }
127
+
128
+ _checkOverflow() {
129
+ if (this.mode !== 'collapse' || this.expanded) return;
130
+
131
+ const ol = this.shadowRoot.querySelector('ol');
132
+ if (!ol) return;
133
+
134
+ // 1. Reset to full width to measure
135
+ this._resetItems();
136
+
137
+ // 2. Check if overflowing
138
+ if (Math.round(ol.scrollWidth) > Math.round(ol.clientWidth)) {
139
+ this._applyCollapse();
140
+ }
141
+ }
142
+
143
+ _resetItems() {
144
+ const slot = this.shadowRoot.querySelector('slot');
145
+ if (!slot) return;
146
+
147
+ const items = slot.assignedElements();
148
+ items.forEach(item => {
149
+ item.style.display = '';
150
+ item.removeAttribute('collapsed');
151
+ });
152
+ }
153
+
154
+ _applyCollapse() {
155
+ const slot = this.shadowRoot.querySelector('slot');
156
+ if (!slot) return;
157
+
158
+ const items = slot.assignedElements();
159
+ if (items.length <= 2) return;
160
+
161
+ // Logic: Keep First, Keep Last. Transform [1] to trigger. Hide others.
162
+ const trigger = items[1];
163
+ const last = items[items.length - 1];
164
+
165
+ if (!trigger || trigger === last) return;
166
+
167
+ trigger.setAttribute('collapsed', '');
168
+
169
+ for (let i = 2; i < items.length - 1; i++) {
170
+ items[i].style.display = 'none';
171
+ }
172
+ }
173
+
174
+ _handleExpand() {
175
+ if (this.mode === 'collapse') {
176
+ this._manuallyExpanded = true;
177
+ this._resetItems();
178
+ }
179
+ }
180
+
181
+ render() {
182
+ return html`
183
+ <nav aria-label="Breadcrumb">
184
+ <ol>
185
+ <slot></slot>
186
+ </ol>
187
+ </nav>
188
+ `;
189
+ }
190
+ }
191
+
192
+ if (!customElements.get('ds-breadcrumbs')) {
193
+ customElements.define('ds-breadcrumbs', DsBreadcrumbs);
194
+ }
@@ -0,0 +1,54 @@
1
+ import { html } from 'lit';
2
+ import './ds-breadcrumbs.js';
3
+ import '../ds-breadcrumb-item/ds-breadcrumb-item.js';
4
+
5
+ export default {
6
+ title: 'Components/Breadcrumbs',
7
+ component: 'ds-breadcrumbs',
8
+ argTypes: {
9
+ mode: {
10
+ control: { type: 'select' },
11
+ options: ['wrap', 'scroll', 'collapse']
12
+ },
13
+
14
+ },
15
+ };
16
+
17
+ export const Playground = (args) => html`
18
+ <div style="max-width: 800px; border: 1px dashed #ccc; padding: 1rem; resize: horizontal; overflow: hidden; height: 100px; width: 100%;">
19
+ <ds-breadcrumbs mode="${args.mode || 'wrap'}">
20
+ <ds-breadcrumb-item href="/" label="Home"></ds-breadcrumb-item>
21
+ <ds-breadcrumb-item href="/level1" label="Level 1"></ds-breadcrumb-item>
22
+ <ds-breadcrumb-item href="/level2" label="Level 2"></ds-breadcrumb-item>
23
+ <ds-breadcrumb-item href="/level3" label="Level 3"></ds-breadcrumb-item>
24
+ <ds-breadcrumb-item href="/level4" label="Level 4"></ds-breadcrumb-item>
25
+ <ds-breadcrumb-item href="/level5" label="Level 5"></ds-breadcrumb-item>
26
+ <ds-breadcrumb-item href="/level6" label="Level 6"></ds-breadcrumb-item>
27
+ <ds-breadcrumb-item href="/level7" label="Level 7"></ds-breadcrumb-item>
28
+ <ds-breadcrumb-item label="Current Page" current></ds-breadcrumb-item>
29
+ </ds-breadcrumbs>
30
+ </div>
31
+ <p style="font-size: 12px; color: #666; margin-top: 8px;">
32
+ <strong>Current Mode:</strong> ${args.mode}<br>
33
+ Resize the dashed box to test responsive behaviors. If in collapse mode, click '...' to expand (will scroll).
34
+ </p>
35
+ `;
36
+ Playground.args = {
37
+ mode: 'collapse',
38
+ };
39
+
40
+ export const Default = () => html`
41
+ <ds-breadcrumbs>
42
+ <ds-breadcrumb-item href="/" label="Home"></ds-breadcrumb-item>
43
+ <ds-breadcrumb-item href="/components" label="Components"></ds-breadcrumb-item>
44
+ <ds-breadcrumb-item label="Breadcrumbs" current></ds-breadcrumb-item>
45
+ </ds-breadcrumbs>
46
+ `;
47
+
48
+ export const TrailingLink = () => html`
49
+ <ds-breadcrumbs>
50
+ <ds-breadcrumb-item href="/" label="Home"></ds-breadcrumb-item>
51
+ <ds-breadcrumb-item href="/section" label="Section"></ds-breadcrumb-item>
52
+ <ds-breadcrumb-item href="/page" label="Page Label (Link)"></ds-breadcrumb-item>
53
+ </ds-breadcrumbs>
54
+ `;
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import './ds-breadcrumbs.js';
3
+ import '../ds-breadcrumb-item/ds-breadcrumb-item.js';
4
+
5
+ describe('ds-breadcrumbs', () => {
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('renders accessible navigation structure', async () => {
18
+ container.innerHTML = `
19
+ <ds-breadcrumbs>
20
+ <ds-breadcrumb-item href="#" label="Home"></ds-breadcrumb-item>
21
+ </ds-breadcrumbs>
22
+ `;
23
+ const el = container.querySelector('ds-breadcrumbs');
24
+ await new Promise(r => setTimeout(r, 50));
25
+
26
+ const nav = el.shadowRoot.querySelector('nav');
27
+ expect(nav).toBeTruthy();
28
+ expect(nav.getAttribute('aria-label')).toBe('Breadcrumb');
29
+
30
+ const ol = el.shadowRoot.querySelector('ol');
31
+ expect(ol).toBeTruthy();
32
+ });
33
+ });
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import axe from 'axe-core';
3
+ import './ds-button.js';
4
+
5
+ describe('ds-button 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('should pass axe accessibility checks for all variants', async () => {
18
+ const variants = ['primary', 'secondary', 'outline', 'action', 'tertiary'];
19
+
20
+ // Disable color-contrast rule for now since we are in a test environment without full styles context
21
+ // and some tokens might not evaluate to actual colors if CSS vars aren't fully resolved in JSDOM
22
+ const options = {
23
+ rules: {
24
+ 'color-contrast': { enabled: false }
25
+ }
26
+ };
27
+
28
+ for (const variant of variants) {
29
+ container.innerHTML = `<ds-button variant="${variant}">Button</ds-button>`;
30
+ await new Promise(resolve => setTimeout(resolve, 100));
31
+
32
+ const results = await axe.run(container, options);
33
+ if (results.violations.length > 0) {
34
+ console.log(`Violations for ${variant}:`, JSON.stringify(results.violations, null, 2));
35
+ }
36
+ expect(results.violations).toHaveLength(0);
37
+ }
38
+ });
39
+
40
+ it('should be accessible when disabled', async () => {
41
+ container.innerHTML = '<ds-button disabled>Disabled Button</ds-button>';
42
+ await new Promise(resolve => setTimeout(resolve, 100));
43
+
44
+ const results = await axe.run(container, {
45
+ rules: { 'color-contrast': { enabled: false } }
46
+ });
47
+ expect(results.violations).toHaveLength(0);
48
+ });
49
+ });
@@ -0,0 +1,205 @@
1
+ import { LitElement, html, css } from 'lit';
2
+
3
+ /**
4
+ * Button component for the Design System
5
+ *
6
+ * @element ds-button
7
+ *
8
+ * @prop {string} variant - Button style variant: 'primary' | 'secondary' | 'outline' | 'action' | 'tertiary' (default: 'primary')
9
+ * @prop {boolean} disabled - Whether the button is disabled
10
+ *
11
+ * @slot - Button label content
12
+ * @slot icon-start - Icon to display before the label
13
+ * @slot icon-end - Icon to display after the label
14
+ */
15
+ export class DsButton extends LitElement {
16
+ static properties = {
17
+ variant: { type: String, reflect: true },
18
+ disabled: { type: Boolean, reflect: true }
19
+ };
20
+
21
+ static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true };
22
+
23
+ static styles = css`
24
+ :host {
25
+ display: inline-block;
26
+ vertical-align: middle;
27
+ }
28
+
29
+ :host([full-width]) {
30
+ display: block;
31
+ width: 100%;
32
+ }
33
+
34
+ :host([full-width]) button,
35
+ :host([style*="width: 100%"]) button {
36
+ width: 100%;
37
+ }
38
+
39
+ button {
40
+ appearance: none;
41
+ display: inline-flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ /* Layout */
45
+ /* Height = 20px (line-height) + 10px (padding-y) + 2px (border) = 32px */
46
+ /* Padding calculation to achieve 32px height:
47
+ Target 32px - 20px (line-height) - 2px (border) = 10px total vertical padding
48
+ Top/Bottom padding = 5px
49
+ */
50
+ padding: 5px var(--ds-space-sm);
51
+ border: 1px solid transparent; /* Always present border to maintain layout consistency */
52
+ border-radius: var(--ds-radius-action, 999px);
53
+ background: transparent;
54
+ width: 100%;
55
+
56
+ /* Typography */
57
+ color: inherit;
58
+ font: var(--ds-typo-content-body-bold);
59
+ text-decoration: none;
60
+
61
+ /* Interaction */
62
+ cursor: pointer;
63
+ appearance: none;
64
+ box-sizing: border-box;
65
+ transition: background-color 0.2s, color 0.2s, border-color 0.2s;
66
+ }
67
+
68
+ /* Label slot wrapper */
69
+ .label {
70
+ padding: 0 var(--ds-space-xs); /* 4px horizontal padding */
71
+ }
72
+
73
+ /* Disabled state */
74
+ :host([disabled]) button {
75
+ cursor: not-allowed;
76
+ pointer-events: none;
77
+ }
78
+
79
+ :host([disabled]) {
80
+ pointer-events: none;
81
+ }
82
+
83
+ /* Focus */
84
+ button:focus-visible {
85
+ outline: 2px solid var(--ds-color-border-focus);
86
+ outline-offset: 2px;
87
+ }
88
+
89
+ /*
90
+ * VARIANTS
91
+ */
92
+
93
+ /* PRIMARY */
94
+ :host([variant="primary"]) button {
95
+ background: var(--ds-color-bg-brand);
96
+ color: var(--ds-color-text-inverse);
97
+ }
98
+ :host([variant="primary"]:not([disabled])) button:hover {
99
+ background: var(--ds-color-bg-brand-hover);
100
+ }
101
+ :host([variant="primary"]:not([disabled])) button:active {
102
+ background: var(--ds-color-bg-brand-pressed);
103
+ }
104
+
105
+ /* SECONDARY */
106
+ :host([variant="secondary"]) button {
107
+ background: var(--ds-color-bg-secondary);
108
+ color: var(--ds-color-text-default);
109
+ border-color: var(--ds-color-border-strong);
110
+ }
111
+ :host([variant="secondary"]:not([disabled])) button:hover {
112
+ background: var(--ds-color-bg-hover);
113
+ }
114
+ :host([variant="secondary"]:not([disabled])) button:active {
115
+ background: var(--ds-color-bg-pressed);
116
+ }
117
+
118
+ /* OUTLINE */
119
+ :host([variant="outline"]) button {
120
+ background: transparent;
121
+ color: var(--ds-color-text-brand);
122
+ border-color: var(--ds-color-border-brand);
123
+ }
124
+ :host([variant="outline"]:not([disabled])) button:hover {
125
+ background: var(--ds-color-bg-hover);
126
+ color: var(--ds-color-text-brand-hover);
127
+ border-color: var(--ds-color-border-brand-hover);
128
+ }
129
+ :host([variant="outline"]:not([disabled])) button:active {
130
+ background: var(--ds-color-bg-pressed);
131
+ color: var(--ds-color-text-brand-pressed);
132
+ border-color: var(--ds-color-border-brand-pressed);
133
+ }
134
+
135
+ /* ACTION */
136
+ :host([variant="action"]) button {
137
+ background: transparent;
138
+ color: var(--ds-color-text-default);
139
+ }
140
+ :host([variant="action"]:not([disabled])) button:hover {
141
+ background: var(--ds-color-bg-hover);
142
+ }
143
+ :host([variant="action"]:not([disabled])) button:active {
144
+ background: var(--ds-color-bg-pressed);
145
+ }
146
+
147
+ /* TERTIARY */
148
+ :host([variant="tertiary"]) button {
149
+ background: transparent;
150
+ color: var(--ds-color-text-brand);
151
+ }
152
+ :host([variant="tertiary"]:not([disabled])) button:hover {
153
+ background: var(--ds-color-bg-hover);
154
+ color: var(--ds-color-text-brand-hover);
155
+ }
156
+ :host([variant="tertiary"]:not([disabled])) button:active {
157
+ background: var(--ds-color-bg-pressed);
158
+ color: var(--ds-color-text-brand-pressed);
159
+ }
160
+
161
+ /* DISABLED STATE for all variants */
162
+ :host([disabled]) button {
163
+ background: var(--ds-color-bg-disabled);
164
+ color: var(--ds-color-text-disabled);
165
+ border-color: transparent;
166
+ }
167
+ :host([variant="secondary"][disabled]) button {
168
+ border-color: var(--ds-color-border-disabled); /* Secondary keeps border or not? Usually disabled buttons have no border or disabled border. Let's use border-disabled if border exists. */
169
+ }
170
+ :host([variant="outline"][disabled]) button {
171
+ border-color: var(--ds-color-border-disabled);
172
+ }
173
+
174
+ :host([variant="action"][disabled]) button,
175
+ :host([variant="tertiary"][disabled]) button {
176
+ background: transparent;
177
+ }
178
+
179
+ /* Icon styling within button */
180
+ ::slotted(ds-icon) {
181
+ /* Icons inherit color from parent button text color (currentColor) */
182
+ --size: var(--ds-icon-size-sm, 20px);
183
+ }
184
+ `;
185
+
186
+ constructor() {
187
+ super();
188
+ this.variant = 'primary';
189
+ this.disabled = false;
190
+ }
191
+
192
+ render() {
193
+ return html`
194
+ <button ?disabled=${this.disabled} part="button">
195
+ <slot name="icon-start"></slot>
196
+ <span class="label">
197
+ <slot></slot>
198
+ </span>
199
+ <slot name="icon-end"></slot>
200
+ </button>
201
+ `;
202
+ }
203
+ }
204
+
205
+ customElements.define('ds-button', DsButton);
@@ -0,0 +1,141 @@
1
+ import { Meta, Canvas, Controls } from '@storybook/blocks';
2
+ import * as DsButtonStories from './ds-button.stories';
3
+
4
+ <Meta of={DsButtonStories} />
5
+
6
+ # Button
7
+
8
+ Buttons allow users to initiate or confirm actions.
9
+
10
+ ## Playground
11
+ Interact with the button properties using the controls below.
12
+
13
+ <Canvas of={DsButtonStories.Primary} />
14
+ <Controls of={DsButtonStories.Primary} />
15
+
16
+ ## Usage
17
+
18
+ ### When to use
19
+ - To initiate or confirm an action.
20
+ - To display specific actions that require clear labeling.
21
+
22
+ ### When not to use
23
+ - To display static or read-only information.
24
+ - For navigation to other pages or external resources—use a text link instead.
25
+
26
+ ## Best Practices
27
+
28
+ ### General
29
+ - ✅ **Single Primary:** Keep a single primary button per screen to guide the user's main action.
30
+ - ✅ **Clarity:** Buttons must always have a label for clarity and accessibility.
31
+ - ✅ **Icons:** Place icons to emphasize the action or add clarity; never use icons purely for decoration.
32
+ - ✅ **Width:** Maintain a minimum width of **72px** for buttons to ensure touch accessibility.
33
+
34
+ ### Grouping & Layout
35
+ - ✅ **Limit:** Group no more than **three buttons** together.
36
+ - ✅ **Spacing:** Maintain at least **8px** spacing between grouped buttons.
37
+ - ✅ **Alignment:**
38
+ - **Left (F-pattern):** On full screens.
39
+ - **Right (Z-pattern):** In dialogs, modals, and dropdowns.
40
+ - ✅ **Wizards:** In multi-step tasks, place "Next" on the right.
41
+ - ✅ **Headers:** In page headers, the primary action may be placed on the right.
42
+ - ✅ **Mobile:** Stack buttons vertically in small containers, with the main action on top.
43
+ - ✅ **Text Wrapping:** Button labels should wrap to a new line if they are too long for the available space.
44
+
45
+ ## Types
46
+
47
+ ### Primary
48
+ The primary button should draw attention to the most important action on a screen, so reserve it for actions that are essential to the experience. This helps create a clear visual hierarchy and keeps users focused on what matters most.
49
+
50
+ <Canvas of={DsButtonStories.Primary} />
51
+
52
+ ### Secondary
53
+ The secondary button supports less critical actions and complements the primary button.
54
+
55
+ <Canvas of={DsButtonStories.Secondary} />
56
+
57
+ ### Outline
58
+ Outline buttons have less prominence than a primary button and slightly more prominence than a tertiary button. It can be used on its own or when there is no clear distinction between multiple important actions on a screen.
59
+
60
+ <Canvas of={DsButtonStories.Outline} />
61
+
62
+ ### Tertiary
63
+ Tertiary buttons have the lowest prominence, so they should be used for low impact actions and/or actions that are not directly related with the primary button.
64
+
65
+ <Canvas of={DsButtonStories.Tertiary} />
66
+
67
+ ### Action
68
+ Action buttons let users complete routine tasks or make selections within a workflow. They’re designed to be subtle controls and usually appear inside containers like action bars, so they don’t compete with primary call-to-action elements.
69
+
70
+ <Canvas of={DsButtonStories.Action} />
71
+
72
+ ## Functionality & States
73
+
74
+ ### Icon Start
75
+ Use an icon at the start of the label to reinforce the action's meaning or add visual context.
76
+
77
+ <Canvas of={DsButtonStories.WithIconStart} />
78
+
79
+ ### Icon End
80
+ Use an icon at the end of the label to indicate direction (e.g., "Next", "Continue") or a dropdown action.
81
+
82
+ <Canvas of={DsButtonStories.WithIconEnd} />
83
+
84
+ ### Both Icons
85
+ Buttons can support icons on both sides if the context requires it, though this should be used sparingly.
86
+
87
+ <Canvas of={DsButtonStories.WithBothIcons} />
88
+
89
+ ### Disabled
90
+ Use the disabled state to indicate that an action is currently unavailable (e.g., missing form data).
91
+
92
+ <Canvas of={DsButtonStories.Disabled} />
93
+
94
+ ## Accessibility
95
+
96
+ <table style={{ width: '100%' }}>
97
+ <thead>
98
+ <tr>
99
+ <th>Attribute</th>
100
+ <th>Value</th>
101
+ <th>Notes</th>
102
+ </tr>
103
+ </thead>
104
+ <tbody>
105
+ <tr>
106
+ <td><code>role</code></td>
107
+ <td><code>button</code></td>
108
+ <td>Native button element provides this</td>
109
+ </tr>
110
+ <tr>
111
+ <td><code>disabled</code></td>
112
+ <td><code>true/false</code></td>
113
+ <td>Disables interaction and updates <code>aria-disabled</code></td>
114
+ </tr>
115
+ </tbody>
116
+ </table>
117
+
118
+ ### Keyboard Support
119
+
120
+ <table style={{ width: '100%' }}>
121
+ <thead>
122
+ <tr>
123
+ <th>Key</th>
124
+ <th>Action</th>
125
+ </tr>
126
+ </thead>
127
+ <tbody>
128
+ <tr>
129
+ <td><kbd>Enter</kbd> / <kbd>Space</kbd></td>
130
+ <td>Activates the button</td>
131
+ </tr>
132
+ <tr>
133
+ <td><kbd>Tab</kbd></td>
134
+ <td>Moves focus to next focusable element</td>
135
+ </tr>
136
+ <tr>
137
+ <td><kbd>Shift</kbd> + <kbd>Tab</kbd></td>
138
+ <td>Moves focus to previous focusable element</td>
139
+ </tr>
140
+ </tbody>
141
+ </table>