@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,246 @@
1
+ import { LitElement, html, css, nothing } from 'lit';
2
+ import '../ds-rich-list-item/ds-rich-list-item.js';
3
+
4
+ /**
5
+ * Rich List — container for ds-rich-list-item elements.
6
+ * Manages selection mode and layout.
7
+ *
8
+ * @element ds-rich-list
9
+ *
10
+ * @prop {string} selectable - Selection mode: 'single' | 'multiple' | 'none'
11
+ * @prop {string} name - Form name (used for radio group in single mode)
12
+ *
13
+ * @slot - ds-rich-list-item elements
14
+ *
15
+ * @fires change - Fired when selection changes, with detail: { value, values }
16
+ */
17
+ export class DsRichList extends LitElement {
18
+ static properties = {
19
+ selectable: { type: String, reflect: true },
20
+ name: { type: String },
21
+ customWidth: { type: String, attribute: 'custom-width' }
22
+ };
23
+
24
+ static styles = css`
25
+ :host {
26
+ display: block;
27
+ }
28
+
29
+ .list {
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: var(--ds-space-md, 16px);
33
+ }
34
+ `;
35
+
36
+ constructor() {
37
+ super();
38
+ this.selectable = 'none';
39
+ this.name = '';
40
+ this.customWidth = '';
41
+ this._focusedIndex = 0;
42
+ }
43
+
44
+ connectedCallback() {
45
+ super.connectedCallback();
46
+ this.addEventListener('change', this._handleItemChange);
47
+ this.addEventListener('keydown', this._handleKeydown);
48
+ this.addEventListener('focusin', this._handleFocusIn);
49
+ this.addEventListener('rich-list-item-structure-change', this._handleStructureChange);
50
+ }
51
+
52
+ disconnectedCallback() {
53
+ super.disconnectedCallback();
54
+ this.removeEventListener('change', this._handleItemChange);
55
+ this.removeEventListener('keydown', this._handleKeydown);
56
+ this.removeEventListener('focusin', this._handleFocusIn);
57
+ this.removeEventListener('rich-list-item-structure-change', this._handleStructureChange);
58
+ }
59
+
60
+ firstUpdated() {
61
+ this._propagateSelectable();
62
+ this._propagateLayout();
63
+ }
64
+
65
+ updated(changedProperties) {
66
+ if (changedProperties.has('selectable') || changedProperties.has('name')) {
67
+ this._propagateSelectable();
68
+ }
69
+ if (changedProperties.has('customWidth')) {
70
+ this._propagateLayout();
71
+ }
72
+ }
73
+
74
+ _getItems() {
75
+ const slot = this.shadowRoot.querySelector('slot');
76
+ if (!slot) return [];
77
+ return slot.assignedElements({ flatten: true })
78
+ .filter(el => el.tagName === 'DS-RICH-LIST-ITEM');
79
+ }
80
+
81
+ _propagateSelectable() {
82
+ const items = this._getItems();
83
+ items.forEach(item => {
84
+ item._selectable = this.selectable;
85
+ item._name = this.name;
86
+ });
87
+ this._manageTabindex();
88
+ }
89
+
90
+ _propagateLayout() {
91
+ const items = this._getItems();
92
+ if (!items.length) return;
93
+
94
+ // Detect which slots are used by any child
95
+ const hasAnyMedia = items.some(item => item.hasAttribute('has-media'));
96
+ const hasAnyCustom = items.some(item => item.hasAttribute('has-custom'));
97
+ const hasAnyActions = items.some(item => item.hasAttribute('has-action-group'));
98
+
99
+ // Force all children to show these columns
100
+ items.forEach(item => {
101
+ item.toggleAttribute('force-media', hasAnyMedia);
102
+ item.toggleAttribute('force-custom', hasAnyCustom);
103
+ item.toggleAttribute('force-action-group', hasAnyActions);
104
+
105
+ // Propagate custom width if specified
106
+ if (this.customWidth) {
107
+ item.style.setProperty('--ds-rich-list-custom-width', this.customWidth);
108
+ } else {
109
+ item.style.removeProperty('--ds-rich-list-custom-width');
110
+ }
111
+ });
112
+ }
113
+
114
+ _handleFocusIn = (e) => {
115
+ // Track which item is focused for arrow key navigation
116
+ const items = this._getItems();
117
+ const index = items.indexOf(e.target);
118
+ if (index !== -1) {
119
+ this._focusedIndex = index;
120
+ }
121
+ }
122
+
123
+ _handleKeydown = (e) => {
124
+ const items = this._getItems();
125
+ if (!items.length) return;
126
+
127
+ // Interactive elements inside items should not trigger navigation
128
+ const path = e.composedPath();
129
+ const targetTag = path[0]?.tagName;
130
+ if (['INPUT', 'TEXTAREA', 'SELECT'].includes(targetTag)) return;
131
+
132
+ if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(e.key)) {
133
+ e.preventDefault();
134
+
135
+ const interactiveItems = items.filter(item => item._isInteractive && item._isInteractive());
136
+ if (!interactiveItems.length) return;
137
+
138
+ // Find current focused index in interactive items
139
+ const currentFocused = items[this._focusedIndex ?? 0];
140
+ let currentInteractiveIndex = interactiveItems.indexOf(currentFocused);
141
+ if (currentInteractiveIndex === -1) currentInteractiveIndex = 0;
142
+
143
+ if (e.key === 'ArrowDown') {
144
+ currentInteractiveIndex = Math.min(interactiveItems.length - 1, currentInteractiveIndex + 1);
145
+ } else if (e.key === 'ArrowUp') {
146
+ currentInteractiveIndex = Math.max(0, currentInteractiveIndex - 1);
147
+ } else if (e.key === 'Home') {
148
+ currentInteractiveIndex = 0;
149
+ } else if (e.key === 'End') {
150
+ currentInteractiveIndex = interactiveItems.length - 1;
151
+ }
152
+
153
+ // Update focused index to the actual index in all items
154
+ this._focusedIndex = items.indexOf(interactiveItems[currentInteractiveIndex]);
155
+ this._manageTabindex();
156
+ items[this._focusedIndex].focus();
157
+ }
158
+ }
159
+
160
+ _manageTabindex() {
161
+ const items = this._getItems();
162
+
163
+ items.forEach((item) => {
164
+ // Interactive items are all tabbable, non-interactive are excluded
165
+ if (item._isInteractive && item._isInteractive()) {
166
+ item.setAttribute('tabindex', '0');
167
+ } else {
168
+ item.removeAttribute('tabindex');
169
+ }
170
+ });
171
+ }
172
+
173
+ _handleItemChange = (e) => {
174
+ // Only handle events from direct ds-rich-list-item children
175
+ const target = e.target;
176
+ if (target.tagName !== 'DS-RICH-LIST-ITEM') return;
177
+
178
+ // Stop item-level change from reaching external listeners;
179
+ // only the list's aggregated change event should be visible.
180
+ e.stopImmediatePropagation();
181
+
182
+ // For single selection, deselect siblings
183
+ if (this.selectable === 'single') {
184
+ const items = this._getItems();
185
+ items.forEach(item => {
186
+ if (item !== target && item.selected) {
187
+ item.selected = false;
188
+ item.setAttribute('aria-selected', 'false');
189
+ }
190
+ });
191
+ }
192
+
193
+ // Dispatch aggregated change event
194
+ this._dispatchChange();
195
+ };
196
+
197
+ _dispatchChange() {
198
+ const items = this._getItems();
199
+ const selectedItems = items.filter(item => item.selected);
200
+ const values = selectedItems.map(item => item.value);
201
+
202
+ this.dispatchEvent(new CustomEvent('change', {
203
+ detail: {
204
+ value: values.length === 1 ? values[0] : values.length === 0 ? '' : values,
205
+ values
206
+ },
207
+ bubbles: true,
208
+ composed: true
209
+ }));
210
+ }
211
+
212
+ _handleSlotChange(e) {
213
+ this._propagateSelectable();
214
+ // Re-scan layout when items are added/removed
215
+ if (this._layoutRequestId) cancelAnimationFrame(this._layoutRequestId);
216
+ this._layoutRequestId = requestAnimationFrame(() => {
217
+ this._propagateLayout();
218
+ this._layoutRequestId = null;
219
+ });
220
+ }
221
+
222
+ _handleStructureChange = (e) => {
223
+ // Stop the event from bubbling out of the list
224
+ e.stopPropagation();
225
+
226
+ // Debounce layout update
227
+ if (this._layoutRequestId) cancelAnimationFrame(this._layoutRequestId);
228
+ this._layoutRequestId = requestAnimationFrame(() => {
229
+ this._propagateLayout();
230
+ this._layoutRequestId = null;
231
+ });
232
+ }
233
+
234
+ render() {
235
+ const role = this.selectable !== 'none' ? 'listbox' : 'list';
236
+ const ariaMulti = this.selectable === 'multiple' ? 'true' : undefined;
237
+
238
+ return html`
239
+ <div class="list" role=${role} aria-multiselectable=${ariaMulti || nothing}>
240
+ <slot @slotchange=${this._handleSlotChange}></slot>
241
+ </div>
242
+ `;
243
+ }
244
+ }
245
+
246
+ customElements.define('ds-rich-list', DsRichList);
@@ -0,0 +1,368 @@
1
+ import './ds-rich-list.js';
2
+ import '../ds-rich-list-item/ds-rich-list-item.js';
3
+ import '../ds-tag-status/ds-tag-status.js';
4
+ import '../ds-button-group/ds-button-group.js';
5
+ import '../ds-button/ds-button.js';
6
+ import '../ds-icon-button/ds-icon-button.js';
7
+ import '../ds-avatar/ds-avatar.js';
8
+
9
+ export default {
10
+ title: 'Components/Rich List',
11
+ component: 'ds-rich-list',
12
+ argTypes: {
13
+ selectable: {
14
+ control: 'select',
15
+ options: ['none', 'single', 'multiple']
16
+ },
17
+ name: { control: 'text' }
18
+ }
19
+ };
20
+
21
+ /* ─── Helper: create a rich list item with common content ─── */
22
+ function createItem({ title = 'Item Title', description = 'Item description text', disabled = false, clickable = false, href = '', value = '', selected = false, showMedia = true, showActions = true, showCustom = false }) {
23
+ const item = document.createElement('ds-rich-list-item');
24
+ if (disabled) item.disabled = true;
25
+ if (clickable) item.clickable = true;
26
+ if (href) item.href = href;
27
+ if (value) item.value = value;
28
+ if (selected) item.selected = true;
29
+
30
+ // Media slot (now via prop)
31
+ if (showMedia) {
32
+ // Consistent placeholder image (data URI for robustness)
33
+ item.setAttribute('media', 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22 width%3D%2240%22 height%3D%2240%22 viewBox%3D%220 0 40 40%22%3E%3Crect width%3D%2240%22 height%3D%2240%22 fill%3D%22%23e0e0e0%22%2F%3E%3Ctext x%3D%2250%25%22 y%3D%2250%25%22 dominant-baseline%3D%22middle%22 text-anchor%3D%22middle%22 font-family%3D%22sans-serif%22 font-size%3D%2210%22 fill%3D%22%23757575%22%3EIMG%3C%2Ftext%3E%3C%2Fsvg%3E');
34
+ item.setAttribute('media-alt', title);
35
+ }
36
+
37
+ // Title slot
38
+ const titleEl = document.createElement('span');
39
+ titleEl.setAttribute('slot', 'title');
40
+ titleEl.textContent = title;
41
+ item.appendChild(titleEl);
42
+
43
+ // Description slot
44
+ const descEl = document.createElement('span');
45
+ descEl.setAttribute('slot', 'description');
46
+ descEl.textContent = description;
47
+ item.appendChild(descEl);
48
+
49
+ // Custom slot
50
+ if (showCustom) {
51
+ const customEl = document.createElement('span');
52
+ customEl.setAttribute('slot', 'custom');
53
+ customEl.textContent = '€99.99';
54
+ customEl.style.font = 'var(--ds-typo-content-body-bold)';
55
+ customEl.style.color = 'var(--ds-color-text-default)';
56
+ customEl.style.whiteSpace = 'nowrap';
57
+ item.appendChild(customEl);
58
+ }
59
+
60
+ // Action group slot
61
+ if (showActions) {
62
+ const btnGroup = document.createElement('ds-button-group');
63
+ btnGroup.setAttribute('slot', 'action-group');
64
+
65
+ const editBtn = document.createElement('ds-icon-button');
66
+ editBtn.setAttribute('variant', 'action');
67
+ editBtn.setAttribute('icon', 'edit');
68
+ editBtn.setAttribute('aria-label', 'Edit');
69
+
70
+ const deleteBtn = document.createElement('ds-icon-button');
71
+ deleteBtn.setAttribute('variant', 'action');
72
+ deleteBtn.setAttribute('icon', 'delete');
73
+ deleteBtn.setAttribute('aria-label', 'Delete');
74
+
75
+ btnGroup.appendChild(editBtn);
76
+ btnGroup.appendChild(deleteBtn);
77
+ item.appendChild(btnGroup);
78
+ }
79
+
80
+ return item;
81
+ }
82
+
83
+ /* ─── Stories ─── */
84
+
85
+ export const Default = {
86
+ render: () => {
87
+ const list = document.createElement('ds-rich-list');
88
+ list.style.maxWidth = '720px';
89
+
90
+ for (let i = 1; i <= 3; i++) {
91
+ list.appendChild(createItem({
92
+ title: `Project ${i}`,
93
+ description: `Description for project ${i}`,
94
+ clickable: true,
95
+ value: `project-${i}`
96
+ }));
97
+ }
98
+
99
+ return list;
100
+ }
101
+ };
102
+
103
+ export const WithHref = {
104
+ render: () => {
105
+ const list = document.createElement('ds-rich-list');
106
+ list.style.maxWidth = '720px';
107
+
108
+ list.appendChild(createItem({
109
+ title: 'Google',
110
+ description: 'Navigate to Google',
111
+ href: 'https://google.com',
112
+ value: 'google'
113
+ }));
114
+ list.appendChild(createItem({
115
+ title: 'GitHub',
116
+ description: 'Navigate to GitHub',
117
+ href: 'https://github.com',
118
+ value: 'github'
119
+ }));
120
+
121
+ return list;
122
+ }
123
+ };
124
+
125
+ export const SingleSelection = {
126
+ render: () => {
127
+ const list = document.createElement('ds-rich-list');
128
+ list.selectable = 'single';
129
+ list.name = 'plan';
130
+ list.style.maxWidth = '720px';
131
+
132
+ list.appendChild(createItem({
133
+ title: 'Free Plan',
134
+ description: 'Basic features for personal use',
135
+ value: 'free',
136
+ showCustom: true,
137
+ selected: true
138
+ }));
139
+ list.appendChild(createItem({
140
+ title: 'Pro Plan',
141
+ description: 'Advanced features for professionals',
142
+ value: 'pro',
143
+ showCustom: true
144
+ }));
145
+ list.appendChild(createItem({
146
+ title: 'Enterprise Plan',
147
+ description: 'Full platform for large teams',
148
+ value: 'enterprise',
149
+ showCustom: true
150
+ }));
151
+
152
+ return list;
153
+ }
154
+ };
155
+
156
+ export const MultipleSelection = {
157
+ render: () => {
158
+ const list = document.createElement('ds-rich-list');
159
+ list.selectable = 'multiple';
160
+ list.style.maxWidth = '720px';
161
+
162
+ list.appendChild(createItem({
163
+ title: 'Email Notifications',
164
+ description: 'Receive updates via email',
165
+ value: 'email',
166
+ selected: true,
167
+ showActions: false
168
+ }));
169
+ list.appendChild(createItem({
170
+ title: 'Push Notifications',
171
+ description: 'Receive push notifications on your device',
172
+ value: 'push',
173
+ showActions: false
174
+ }));
175
+ list.appendChild(createItem({
176
+ title: 'SMS Notifications',
177
+ description: 'Receive text messages',
178
+ value: 'sms',
179
+ showActions: false,
180
+ disabled: true
181
+ }));
182
+
183
+ return list;
184
+ }
185
+ };
186
+
187
+ export const DisabledState = {
188
+ render: () => {
189
+ const list = document.createElement('ds-rich-list');
190
+ list.style.maxWidth = '720px';
191
+
192
+ list.appendChild(createItem({
193
+ title: 'Active Item',
194
+ description: 'This item is interactive',
195
+ clickable: true,
196
+ value: 'active'
197
+ }));
198
+ list.appendChild(createItem({
199
+ title: 'Disabled Item',
200
+ description: 'This item is disabled',
201
+ disabled: true,
202
+ value: 'disabled'
203
+ }));
204
+
205
+ return list;
206
+ }
207
+ };
208
+
209
+ export const Multiline = {
210
+ render: () => {
211
+ const list = document.createElement('ds-rich-list');
212
+ list.style.maxWidth = '720px';
213
+
214
+ const item = createItem({
215
+ title: 'Long Content Item',
216
+ description: 'This is a much longer description that demonstrates how the multiline variant works. Instead of truncating the content, it allows the item to grow vertically to accommodate the full text.',
217
+ clickable: true,
218
+ value: 'long'
219
+ });
220
+ item.multiline = true;
221
+ list.appendChild(item);
222
+
223
+ list.appendChild(createItem({
224
+ title: 'Normal Item',
225
+ description: 'This item has the default fixed height with truncation for comparison',
226
+ clickable: true,
227
+ value: 'normal'
228
+ }));
229
+
230
+ return list;
231
+ }
232
+ };
233
+
234
+ export const NoMedia = {
235
+ render: () => {
236
+ const list = document.createElement('ds-rich-list');
237
+ list.selectable = 'single';
238
+ list.name = 'option';
239
+ list.style.maxWidth = '720px';
240
+
241
+ list.appendChild(createItem({
242
+ title: 'Option A',
243
+ description: 'First option without media',
244
+ value: 'a',
245
+ showMedia: false,
246
+ showActions: false,
247
+ selected: true
248
+ }));
249
+ list.appendChild(createItem({
250
+ title: 'Option B',
251
+ description: 'Second option without media',
252
+ value: 'b',
253
+ showMedia: false,
254
+ showActions: false
255
+ }));
256
+
257
+ return list;
258
+ }
259
+ };
260
+
261
+ export const NonInteractive = {
262
+ render: () => {
263
+ const list = document.createElement('ds-rich-list');
264
+ list.style.maxWidth = '720px';
265
+
266
+ list.appendChild(createItem({
267
+ title: 'Display-Only Item',
268
+ description: 'This item has no selection, click, or navigation — only internal actions work',
269
+ value: 'display-1',
270
+ showCustom: true
271
+ }));
272
+ list.appendChild(createItem({
273
+ title: 'Another Static Item',
274
+ description: 'Hover effect does not apply to the row itself',
275
+ value: 'display-2',
276
+ showCustom: true
277
+ }));
278
+ list.appendChild(createItem({
279
+ title: 'Info Row',
280
+ description: 'Tab skips the row entirely and goes straight to the Edit/Delete buttons',
281
+ value: 'display-3',
282
+ showCustom: true,
283
+ showActions: false
284
+ }));
285
+
286
+ return list;
287
+ }
288
+ };
289
+
290
+ export const ComplexCustom = {
291
+ render: () => {
292
+ const list = document.createElement('ds-rich-list');
293
+ list.style.maxWidth = '720px';
294
+ list.setAttribute('custom-width', '200px');
295
+
296
+ // Helper to create complex custom content
297
+ const createCustomContent = (tagLabel, tagVariant, date, price) => {
298
+ const container = document.createElement('div');
299
+ container.setAttribute('slot', 'custom');
300
+ container.style.cssText = 'display: flex; justify-content: space-between; width: 100%; gap: var(--ds-space-sm, 8px); align-items: baseline;';
301
+
302
+ // Left side: tag + date
303
+ const leftSide = document.createElement('div');
304
+ leftSide.style.cssText = 'display: flex; gap: var(--ds-space-sm, 8px); align-items: baseline;';
305
+
306
+ const tagEl = document.createElement('ds-tag-status');
307
+ tagEl.setAttribute('label', tagLabel);
308
+ tagEl.setAttribute('variant', tagVariant);
309
+
310
+ const dateEl = document.createElement('span');
311
+ dateEl.textContent = date;
312
+ dateEl.style.cssText = 'font: var(--ds-typo-content-body-regular); color: var(--ds-color-text-secondary);';
313
+
314
+ leftSide.appendChild(tagEl);
315
+ leftSide.appendChild(dateEl);
316
+
317
+ // Right side: price
318
+ const priceEl = document.createElement('span');
319
+ priceEl.textContent = price;
320
+ priceEl.style.cssText = 'font: var(--ds-typo-content-body-bold); color: var(--ds-color-text-default); white-space: nowrap;';
321
+
322
+ container.appendChild(leftSide);
323
+ container.appendChild(priceEl);
324
+
325
+ return container;
326
+ };
327
+
328
+ // Item 1: Full content
329
+ const item1 = createItem({
330
+ title: 'Project Alpha',
331
+ description: 'Enterprise solution with full features',
332
+ clickable: true,
333
+ value: 'alpha'
334
+ });
335
+ item1.appendChild(createCustomContent('Active', 'success', 'Jan 2026', '€99.99'));
336
+ list.appendChild(item1);
337
+
338
+ // Item 2: Different content, but same columns
339
+ const item2 = createItem({
340
+ title: 'Project Beta',
341
+ description: 'Standard package for small teams',
342
+ clickable: true,
343
+ value: 'beta'
344
+ });
345
+ item2.appendChild(createCustomContent('Pending', 'warning', 'Feb 2026', '€1,499.00'));
346
+ list.appendChild(item2);
347
+
348
+ // Item 3: No custom content → column is still reserved
349
+ list.appendChild(createItem({
350
+ title: 'Project Gamma',
351
+ description: 'This item has no custom content, but the column space is reserved',
352
+ clickable: true,
353
+ value: 'gamma'
354
+ }));
355
+
356
+ // Item 4: Only actions, no custom → both columns consistent
357
+ list.appendChild(createItem({
358
+ title: 'Project Delta',
359
+ description: 'Minimal item for demonstration',
360
+ clickable: true,
361
+ value: 'delta',
362
+ showMedia: false
363
+ }));
364
+
365
+ return list;
366
+ }
367
+ };
368
+