@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,579 @@
1
+ import { LitElement, html, css, nothing } from 'lit';
2
+ import '../ds-checkbox/ds-checkbox.js';
3
+ import '../ds-radio/ds-radio.js';
4
+ import '../ds-thumbnail/ds-thumbnail.js';
5
+
6
+ /**
7
+ * Rich List Item — a media-rich, interactive list item for complex list patterns.
8
+ *
9
+ * @element ds-rich-list-item
10
+ *
11
+ * @prop {boolean} selected - Whether the item is selected
12
+ * @prop {boolean} disabled - Whether the item is disabled
13
+ * @prop {boolean} clickable - Enables hover/active/focus interaction
14
+ * @prop {string} href - Makes item navigable (renders anchor behavior)
15
+ * @prop {string} value - Value for selection/form association
16
+ * @prop {boolean} multiline - If true, min-height instead of fixed height, no truncation
17
+ *
18
+ * @slot media - 1:1 aspect ratio container (40×40px)
19
+ * @slot title - Primary text
20
+ * @slot description - Secondary text
21
+ * @slot custom - Free-form customizable area
22
+ * @slot action-group - Expects ds-button-group
23
+ *
24
+ * @fires change - Fired when selection state changes
25
+ * @fires rich-list-item-click - Fired when a clickable item is clicked
26
+ *
27
+ * @csspart container - The main item container
28
+ */
29
+ export class DsRichListItem extends LitElement {
30
+ static properties = {
31
+ selected: { type: Boolean, reflect: true },
32
+ disabled: { type: Boolean, reflect: true },
33
+ clickable: { type: Boolean, reflect: true },
34
+ href: { type: String, reflect: true },
35
+ value: { type: String },
36
+ multiline: { type: Boolean, reflect: true },
37
+ // Internal: set by parent ds-rich-list
38
+ _selectable: { type: String, attribute: false },
39
+ _name: { type: String, attribute: false },
40
+ // Internal: track which slots are populated
41
+ _hasMedia: { type: Boolean, attribute: false },
42
+ _hasCustom: { type: Boolean, attribute: false },
43
+ _hasActionGroup: { type: Boolean, attribute: false },
44
+ // Media helper props
45
+ media: { type: String, reflect: true },
46
+ mediaAlt: { type: String, attribute: 'media-alt' }
47
+ };
48
+
49
+ static styles = css`
50
+ :host {
51
+ display: block;
52
+ outline: none;
53
+ }
54
+
55
+ /* ─── Container ─── */
56
+ .item {
57
+ display: flex;
58
+ align-items: flex-start; /* Top alignment */
59
+ min-height: 72px; /* Consistent minimum height */
60
+ box-sizing: border-box;
61
+ padding: var(--ds-space-md);
62
+ gap: var(--ds-space-md);
63
+ background: var(--ds-color-bg-default);
64
+ border: 1px solid var(--ds-color-border-default);
65
+ border-radius: var(--ds-radius-container);
66
+ position: relative;
67
+ transition: box-shadow 0.2s ease, border-color 0.2s ease;
68
+ cursor: default;
69
+ }
70
+
71
+ /* Multiline: allow growth */
72
+ :host([multiline]) .item {
73
+ height: auto;
74
+ }
75
+
76
+ /* ─── Interactive (clickable, href, or selectable) ─── */
77
+ :host([interactive]) .item {
78
+ cursor: pointer;
79
+ }
80
+
81
+ :host([interactive]:not([disabled])) .item:hover {
82
+ box-shadow: var(--ds-elevation-floating);
83
+ }
84
+
85
+ /* ─── Focus ─── */
86
+ :host(:focus-visible) .item {
87
+ outline: 2px solid var(--ds-color-border-focus);
88
+ outline-offset: 2px;
89
+ }
90
+
91
+ /* ─── Selected ─── */
92
+ :host([selected]) .item {
93
+ border-color: var(--ds-color-border-selected-secondary);
94
+ }
95
+
96
+ /* ─── Disabled ─── */
97
+ :host([disabled]) .item {
98
+ pointer-events: none;
99
+ cursor: not-allowed;
100
+ }
101
+
102
+ :host([disabled]) .item:hover {
103
+ box-shadow: none;
104
+ }
105
+
106
+ :host([disabled]) .media-container {
107
+ opacity: 0.5;
108
+ }
109
+
110
+ :host([disabled]) .content ::slotted(*) {
111
+ color: var(--ds-color-text-disabled);
112
+ }
113
+
114
+ /* ─── Selection Control ─── */
115
+ .selection-control {
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ flex-shrink: 0;
120
+ position: relative;
121
+ z-index: 2;
122
+ width: var(--ds-size-40); /* Explicit width for alignment */
123
+ height: var(--ds-size-40); /* Explicit height for alignment */
124
+ }
125
+
126
+ .selection-control ds-checkbox,
127
+ .selection-control ds-radio {
128
+ pointer-events: none; /* Let the row handle click */
129
+ }
130
+
131
+ /* ─── Media Container ─── */
132
+ .media-container {
133
+ width: var(--ds-size-40);
134
+ height: var(--ds-size-40);
135
+ flex-shrink: 0;
136
+ overflow: hidden;
137
+ border-radius: var(--ds-radius-media);
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: center;
141
+ }
142
+
143
+ .media-container ::slotted(*) {
144
+ width: 100%;
145
+ height: 100%;
146
+ object-fit: cover;
147
+ display: block;
148
+ }
149
+
150
+ /* Fallback img rendered via media prop uses ds-thumbnail */
151
+ .media-container ds-thumbnail {
152
+ width: 100%;
153
+ height: 100%;
154
+ display: block;
155
+ }
156
+
157
+ /* Hide container when slot is empty AND not forced by parent */
158
+ :host(:not([has-media]):not([force-media])) .media-container {
159
+ display: none;
160
+ }
161
+
162
+ /* ─── Content (Title + Description) ─── */
163
+ .content {
164
+ flex: 1;
165
+ min-width: 0; /* Critical for truncation */
166
+ display: flex;
167
+ flex-direction: column;
168
+ justify-content: center; /* Center content within the 40px min-height if single line */
169
+ min-height: var(--ds-size-40); /* Aligns with media/checkbox */
170
+ gap: 0; /* Remove gap as requested */
171
+ }
172
+
173
+ .content ::slotted([slot="title"]) {
174
+ font: var(--ds-typo-content-body-bold);
175
+ color: var(--ds-color-text-default);
176
+ white-space: nowrap;
177
+ overflow: hidden;
178
+ text-overflow: ellipsis;
179
+ display: block;
180
+ }
181
+
182
+ .content ::slotted([slot="description"]) {
183
+ font: var(--ds-typo-content-body-regular);
184
+ color: var(--ds-color-text-secondary);
185
+ white-space: nowrap;
186
+ overflow: hidden;
187
+ text-overflow: ellipsis;
188
+ display: block;
189
+ }
190
+
191
+ /* Multiline: disable truncation */
192
+ :host([multiline]) .content ::slotted([slot="title"]),
193
+ :host([multiline]) .content ::slotted([slot="description"]) {
194
+ white-space: normal;
195
+ overflow: visible;
196
+ text-overflow: clip;
197
+ }
198
+
199
+ /* ─── Custom Slot ─── */
200
+ .custom-container {
201
+ display: flex;
202
+ align-items: center;
203
+ flex-shrink: 0;
204
+ position: relative;
205
+ z-index: 2;
206
+ min-width: var(--ds-rich-list-custom-width, auto);
207
+ height: var(--ds-size-40); /* Force 40px height for vertical centering alignment */
208
+ }
209
+
210
+ :host(:not([has-custom]):not([force-custom])) .custom-container {
211
+ display: none;
212
+ }
213
+
214
+ /* ─── Action Group ─── */
215
+ .action-group-container {
216
+ display: flex;
217
+ align-items: center;
218
+ flex-shrink: 0;
219
+ position: relative;
220
+ z-index: 2;
221
+ height: var(--ds-size-40); /* Force 40px height for vertical centering alignment */
222
+ }
223
+
224
+ :host(:not([has-action-group]):not([force-action-group])) .action-group-container {
225
+ display: none;
226
+ }
227
+
228
+ /* ─── Link (hidden anchor for href) ─── */
229
+ a.item-link {
230
+ position: absolute;
231
+ inset: 0;
232
+ z-index: 1;
233
+ opacity: 0;
234
+ }
235
+ `;
236
+
237
+ constructor() {
238
+ super();
239
+ this.selected = false;
240
+ this.disabled = false;
241
+ this.clickable = false;
242
+ this.href = '';
243
+ this.value = '';
244
+ this.multiline = false;
245
+ this._selectable = 'none';
246
+ this._name = '';
247
+ this._hasMedia = false;
248
+ this._hasCustom = false;
249
+ this._hasActionGroup = false;
250
+ }
251
+
252
+ connectedCallback() {
253
+ super.connectedCallback();
254
+ // Set ARIA role based on context
255
+ this._updateRole();
256
+ // Reflect interactive state for CSS
257
+ this.toggleAttribute('interactive', this._isInteractive());
258
+ // Host-level keydown for Enter/Space activation when item has focus
259
+ this.addEventListener('keydown', this._handleHostKeydown);
260
+ }
261
+
262
+ disconnectedCallback() {
263
+ super.disconnectedCallback();
264
+ this.removeEventListener('keydown', this._handleHostKeydown);
265
+ }
266
+
267
+ updated(changedProperties) {
268
+ if (changedProperties.has('_selectable') || changedProperties.has('disabled')) {
269
+ this._updateRole();
270
+ }
271
+ if (changedProperties.has('disabled')) {
272
+ this._updateDisabledState();
273
+ }
274
+ if (changedProperties.has('clickable') || changedProperties.has('href') || changedProperties.has('_selectable') || changedProperties.has('disabled')) {
275
+ this.toggleAttribute('interactive', this._isInteractive());
276
+ }
277
+ if (changedProperties.has('media')) {
278
+ // Allow re-evaluating has-media if prop changes (though usually slotchange handles initial)
279
+ // We need to ensure has-media is true if media prop is set
280
+ this.toggleAttribute('has-media', !!this.media || this._hasMedia);
281
+ }
282
+ }
283
+
284
+ _updateRole() {
285
+ if (this._selectable !== 'none') {
286
+ this.setAttribute('role', 'option');
287
+ this.setAttribute('aria-selected', String(this.selected));
288
+ } else {
289
+ this.setAttribute('role', 'listitem');
290
+ this.removeAttribute('aria-selected');
291
+ }
292
+
293
+ if (this.disabled) {
294
+ this.setAttribute('aria-disabled', 'true');
295
+ } else {
296
+ this.removeAttribute('aria-disabled');
297
+ }
298
+ }
299
+
300
+ _updateDisabledState() {
301
+ const slots = ['action-group', 'custom'];
302
+ slots.forEach(slotName => {
303
+ const slot = this.shadowRoot.querySelector(`slot[name="${slotName}"]`);
304
+ if (slot) {
305
+ const assignedElements = slot.assignedElements({ flatten: true });
306
+ assignedElements.forEach(el => {
307
+ // Try to set disabled property if available
308
+ if ('disabled' in el) {
309
+ el.disabled = this.disabled;
310
+ }
311
+ // Also try looking for children that might be buttons (e.g. inside ds-button-group)
312
+ if (el.tagName === 'DS-BUTTON-GROUP') {
313
+ Array.from(el.children).forEach(child => {
314
+ if ('disabled' in child) {
315
+ child.disabled = this.disabled;
316
+ }
317
+ });
318
+ }
319
+ });
320
+ }
321
+ });
322
+ }
323
+
324
+ _isInteractive() {
325
+ if (this.disabled) return false;
326
+ return this.clickable || this.href || this._selectable !== 'none';
327
+ }
328
+
329
+ _isEventFromInteractiveChild(e) {
330
+ const path = e.composedPath();
331
+
332
+ for (const el of path) {
333
+ if (el === this) break; // Stop checking when we reach the host
334
+ if (el === window || el === document || el === this.shadowRoot) continue;
335
+ if (!el.tagName) continue;
336
+
337
+ // Allow the main item link (href overlay)
338
+ if (el.classList && el.classList.contains('item-link')) continue;
339
+
340
+ const tag = el.tagName.toLowerCase();
341
+ const interactiveTags = ['button', 'a', 'input', 'select', 'textarea', 'label'];
342
+
343
+ // Check for DS components that are interactive
344
+ // Exclude non-interactive ones
345
+ const isDsComponent = tag.startsWith('ds-') && !['ds-icon', 'ds-avatar', 'ds-badge'].includes(tag);
346
+
347
+ if (interactiveTags.includes(tag) || isDsComponent || el.hasAttribute('onclick') || el.getAttribute('role') === 'button') {
348
+ return true;
349
+ }
350
+ }
351
+ return false;
352
+ }
353
+
354
+ _handleClick(e) {
355
+ if (this.disabled) return;
356
+ if (this._isEventFromInteractiveChild(e)) return;
357
+ const path = e.composedPath();
358
+
359
+ // Selection logic
360
+ if (this._selectable === 'single') {
361
+ if (!this.selected) {
362
+ this.selected = true;
363
+ this._dispatchChange();
364
+ }
365
+ } else if (this._selectable === 'multiple') {
366
+ this.selected = !this.selected;
367
+ this._dispatchChange();
368
+ }
369
+
370
+ // Clickable event
371
+ if (this.clickable || this.href) {
372
+ this.dispatchEvent(new CustomEvent('rich-list-item-click', {
373
+ detail: { value: this.value, href: this.href },
374
+ bubbles: true,
375
+ composed: true
376
+ }));
377
+
378
+ // Navigate if href
379
+ // Note: If the user clicked a.item-link, the browser handles navigation naturally.
380
+ // We only need manual navigation if the click was on the padding/div.
381
+ // But if specific href logic is needed (like checking meta keys), we keep it.
382
+ // However, verify we don't double-navigate if a.item-link was clicked.
383
+ const clickedLink = path.find(el => el.classList && el.classList.contains('item-link'));
384
+ if (this.href && !clickedLink && !e.defaultPrevented) {
385
+ window.open(this.href, e.metaKey || e.ctrlKey ? '_blank' : '_self');
386
+ }
387
+ }
388
+ }
389
+
390
+ _handleHostKeydown = (e) => {
391
+ if (this.disabled) return;
392
+ // Only handle when the host itself is the target (not bubbled from internal elements)
393
+ if (e.target !== this) return;
394
+
395
+ if (e.key === 'Enter' || e.key === ' ') {
396
+ e.preventDefault();
397
+ this._activate(e);
398
+ }
399
+ }
400
+
401
+ _handleKeydown(e) {
402
+ if (this._isEventFromInteractiveChild(e)) return;
403
+
404
+ // This handles keydowns from within the shadow DOM (e.g. inner .item div)
405
+ if (e.key === 'Enter' || e.key === ' ') {
406
+ e.preventDefault();
407
+ this._activate(e);
408
+ }
409
+ }
410
+
411
+ _activate(e) {
412
+ // Selection logic
413
+ if (this._selectable === 'single') {
414
+ if (!this.selected) {
415
+ this.selected = true;
416
+ this._dispatchChange();
417
+ }
418
+ } else if (this._selectable === 'multiple') {
419
+ this.selected = !this.selected;
420
+ this._dispatchChange();
421
+ }
422
+
423
+ // Clickable event
424
+ if (this.clickable || this.href) {
425
+ this.dispatchEvent(new CustomEvent('rich-list-item-click', {
426
+ detail: { value: this.value, href: this.href },
427
+ bubbles: true,
428
+ composed: true
429
+ }));
430
+
431
+ if (this.href) {
432
+ window.open(this.href, e.metaKey || e.ctrlKey ? '_blank' : '_self');
433
+ }
434
+ }
435
+ }
436
+
437
+ _dispatchChange() {
438
+ this.setAttribute('aria-selected', String(this.selected));
439
+ this.dispatchEvent(new CustomEvent('change', {
440
+ detail: { selected: this.selected, value: this.value },
441
+ bubbles: true,
442
+ composed: true
443
+ }));
444
+ }
445
+
446
+ _handleSlotChange(slotName, e) {
447
+ const slot = e.target;
448
+ const nodes = slot.assignedNodes({ flatten: true });
449
+ const hasContent = this._hasVisibleContent(nodes);
450
+
451
+ let changed = false;
452
+
453
+ if (slotName === 'media') {
454
+ if (this._hasMedia !== hasContent) {
455
+ this._hasMedia = hasContent;
456
+ this.toggleAttribute('has-media', hasContent || !!this.media);
457
+ changed = true;
458
+ }
459
+ } else if (slotName === 'custom') {
460
+ if (this._hasCustom !== hasContent) {
461
+ this._hasCustom = hasContent;
462
+ this.toggleAttribute('has-custom', hasContent);
463
+ this._updateDisabledState();
464
+ changed = true;
465
+ }
466
+ } else if (slotName === 'action-group') {
467
+ if (this._hasActionGroup !== hasContent) {
468
+ this._hasActionGroup = hasContent;
469
+ this.toggleAttribute('has-action-group', hasContent);
470
+ this._updateDisabledState();
471
+ changed = true;
472
+ }
473
+ }
474
+
475
+ if (changed) {
476
+ this.dispatchEvent(new CustomEvent('rich-list-item-structure-change', {
477
+ bubbles: true,
478
+ composed: true
479
+ }));
480
+ }
481
+ }
482
+
483
+ _hasVisibleContent(nodes) {
484
+ if (!nodes) return false;
485
+ return Array.from(nodes).some(node => {
486
+ // Text nodes: check if they contain more than just whitespace
487
+ if (node.nodeType === Node.TEXT_NODE) {
488
+ return node.textContent.trim() !== '';
489
+ }
490
+ // Element nodes:
491
+ if (node.nodeType === Node.ELEMENT_NODE) {
492
+ // Recursively check children of generic containers (div, span)
493
+ // This prevents <span slot="action-group"> </span> from counting as content
494
+ if (['DIV', 'SPAN'].includes(node.tagName)) {
495
+ // If the element has direct attributes that might make it visible (like styles or classes),
496
+ // we should probably consider it "visible" content.
497
+ // But for now, let's focus on the "empty wrapper" case.
498
+ // If it has children, check them.
499
+ if (node.childNodes.length > 0) {
500
+ return this._hasVisibleContent(node.childNodes);
501
+ }
502
+ // Empty div/span -> not visible
503
+ return false;
504
+ }
505
+ // Other elements (img, button, ds-icon, etc) are considered visible content
506
+ return true;
507
+ }
508
+ return false;
509
+ });
510
+ }
511
+
512
+ _renderSelectionControl() {
513
+ if (this._selectable === 'single') {
514
+ return html`
515
+ <div class="selection-control">
516
+ <ds-radio
517
+ standalone
518
+ ?checked=${this.selected}
519
+ ?disabled=${this.disabled}
520
+ name=${this._name}
521
+ value=${this.value}
522
+ tabindex="-1"
523
+ ></ds-radio>
524
+ </div>
525
+ `;
526
+ }
527
+
528
+ if (this._selectable === 'multiple') {
529
+ return html`
530
+ <div class="selection-control">
531
+ <ds-checkbox
532
+ standalone
533
+ ?checked=${this.selected}
534
+ ?disabled=${this.disabled}
535
+ tabindex="-1"
536
+ ></ds-checkbox>
537
+ </div>
538
+ `;
539
+ }
540
+
541
+ return nothing;
542
+ }
543
+
544
+ render() {
545
+ return html`
546
+ <div
547
+ class="item"
548
+ part="container"
549
+ @click=${this._handleClick}
550
+ @keydown=${this._handleKeydown}
551
+ >
552
+ ${this.href ? html`<a class="item-link" href=${this.href} tabindex="-1" aria-hidden="true"></a>` : nothing}
553
+
554
+ ${this._renderSelectionControl()}
555
+
556
+ <div class="media-container">
557
+ <slot name="media" @slotchange=${(e) => this._handleSlotChange('media', e)}>
558
+ ${this.media ? html`<ds-thumbnail src="${this.media}" alt="${this.mediaAlt || ''}" fit="cover"></ds-thumbnail>` : nothing}
559
+ </slot>
560
+ </div>
561
+
562
+ <div class="content">
563
+ <slot name="title"></slot>
564
+ <slot name="description"></slot>
565
+ </div>
566
+
567
+ <div class="custom-container">
568
+ <slot name="custom" @slotchange=${(e) => this._handleSlotChange('custom', e)}></slot>
569
+ </div>
570
+
571
+ <div class="action-group-container">
572
+ <slot name="action-group" @slotchange=${(e) => this._handleSlotChange('action-group', e)}></slot>
573
+ </div>
574
+ </div>
575
+ `;
576
+ }
577
+ }
578
+
579
+ customElements.define('ds-rich-list-item', DsRichListItem);