@boostdev/design-system-components 1.1.2 → 1.2.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 (213) hide show
  1. package/AGENTS.md +38 -14
  2. package/dist/client.cjs +85 -103
  3. package/dist/client.css +610 -601
  4. package/dist/client.d.cts +13 -16
  5. package/dist/client.d.ts +13 -16
  6. package/dist/client.js +95 -113
  7. package/dist/index.cjs +85 -103
  8. package/dist/index.css +610 -601
  9. package/dist/index.d.cts +13 -16
  10. package/dist/index.d.ts +13 -16
  11. package/dist/index.js +95 -113
  12. package/dist/native/index.cjs +9 -8
  13. package/dist/native/index.d.cts +1 -1
  14. package/dist/native/index.d.ts +1 -1
  15. package/dist/native/index.js +9 -8
  16. package/dist/web-components/{chunk-K5CE6HSB.js → chunk-2FGATTGT.js} +79 -17
  17. package/dist/web-components/{chunk-O4UFI2RX.js → chunk-3REOIRDW.js} +13 -11
  18. package/dist/web-components/{chunk-5IPHEONG.js → chunk-6I2DBFQ7.js} +1 -0
  19. package/dist/web-components/{chunk-VM3YHGQ2.js → chunk-AJSXNDAP.js} +1 -2
  20. package/dist/web-components/{chunk-LSC4N56Q.js → chunk-BX3IPVOB.js} +1 -2
  21. package/dist/web-components/{chunk-CAPI25CU.js → chunk-DZRSJGPB.js} +3 -3
  22. package/dist/web-components/{chunk-VSS5ITNE.js → chunk-EBJM3VD4.js} +1 -3
  23. package/dist/web-components/{chunk-O7I63SQX.js → chunk-HHHRF2PS.js} +6 -12
  24. package/dist/web-components/{chunk-OTX4COSV.js → chunk-JUKB3BUP.js} +3 -4
  25. package/dist/web-components/{chunk-IQGLIQSO.js → chunk-N6IMUOY4.js} +2 -5
  26. package/dist/web-components/{chunk-CJ5ZOO42.js → chunk-OCODKRVZ.js} +68 -44
  27. package/dist/web-components/{chunk-QDRH46TF.js → chunk-QLZ2362S.js} +1 -3
  28. package/dist/web-components/{chunk-IK3PTFZ4.js → chunk-T6AETZRP.js} +1 -3
  29. package/dist/web-components/{chunk-4BX6AWXX.js → chunk-X3FKVHLK.js} +2 -4
  30. package/dist/web-components/{chunk-A67LCT7C.js → chunk-XNA6WTXG.js} +3 -4
  31. package/dist/web-components/{chunk-XIENXR7H.js → chunk-YRXCVKHV.js} +1 -3
  32. package/dist/web-components/{chunk-6T3RKUKG.js → chunk-ZTC6GRP7.js} +1 -2
  33. package/dist/web-components/globals.js +1 -1
  34. package/dist/web-components/index.d.ts +571 -1
  35. package/dist/web-components/index.js +1515 -18
  36. package/dist/web-components/interaction/bds-accordion.js +1 -1
  37. package/dist/web-components/interaction/bds-button.d.ts +2 -2
  38. package/dist/web-components/interaction/bds-button.js +1 -1
  39. package/dist/web-components/interaction/bds-collapsible.js +1 -1
  40. package/dist/web-components/interaction/bds-dialog.js +1 -1
  41. package/dist/web-components/interaction/bds-drawer.js +1 -1
  42. package/dist/web-components/interaction/form/bds-checkbox.js +1 -1
  43. package/dist/web-components/interaction/form/bds-combobox.js +1 -1
  44. package/dist/web-components/interaction/form/bds-number-input.js +1 -1
  45. package/dist/web-components/interaction/form/bds-radio.js +1 -1
  46. package/dist/web-components/interaction/form/bds-segmented-control.d.ts +20 -3
  47. package/dist/web-components/interaction/form/bds-segmented-control.js +1 -1
  48. package/dist/web-components/interaction/form/bds-select.js +1 -1
  49. package/dist/web-components/interaction/form/bds-switch.js +1 -1
  50. package/dist/web-components/interaction/form/bds-textarea.js +1 -1
  51. package/dist/web-components/ui/bds-alert.js +1 -1
  52. package/dist/web-components/ui/bds-card.js +1 -1
  53. package/dist/web-components/ui/bds-notification-banner.js +1 -1
  54. package/package.json +2 -2
  55. package/src/components/interaction/Button/Button.module.css +12 -11
  56. package/src/components/interaction/Button/Button.native.tsx +10 -9
  57. package/src/components/interaction/Button/Button.tsx +1 -1
  58. package/src/components/interaction/Command/Command.module.css +3 -7
  59. package/src/components/interaction/Dialog/Dialog.module.css +7 -5
  60. package/src/components/interaction/Drawer/Drawer.module.css +48 -25
  61. package/src/components/interaction/Drawer/Drawer.tsx +21 -17
  62. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +1 -3
  63. package/src/components/interaction/Popover/Popover.module.css +1 -3
  64. package/src/components/interaction/form/Checkbox/Checkbox.module.css +1 -3
  65. package/src/components/interaction/form/Combobox/Combobox.module.css +2 -6
  66. package/src/components/interaction/form/FormInput/FormInput.module.css +1 -3
  67. package/src/components/interaction/form/NumberInput/NumberInput.module.css +16 -4
  68. package/src/components/interaction/form/Radio/Radio.module.css +6 -16
  69. package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +74 -25
  70. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +65 -36
  71. package/src/components/interaction/form/SegmentedControl/SegmentedControl.spec.tsx +127 -56
  72. package/src/components/interaction/form/SegmentedControl/SegmentedControl.stories.tsx +89 -72
  73. package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +40 -62
  74. package/src/components/interaction/form/SegmentedControl/index.ts +1 -1
  75. package/src/components/interaction/form/Select/Select.module.css +2 -4
  76. package/src/components/interaction/form/Switch/Switch.module.css +5 -7
  77. package/src/components/interaction/form/Switch/Switch.native.spec.tsx +9 -9
  78. package/src/components/interaction/form/Switch/Switch.native.tsx +2 -2
  79. package/src/components/interaction/form/Textarea/Textarea.module.css +1 -3
  80. package/src/components/interaction/form/atoms/InputContainer.stories.tsx +64 -0
  81. package/src/components/interaction/form/atoms/Label.stories.tsx +33 -0
  82. package/src/components/interaction/form/atoms/Message.stories.tsx +33 -0
  83. package/src/components/layout/Card/Card.module.css +1 -3
  84. package/src/components/ui/Accordion/Accordion.module.css +1 -2
  85. package/src/components/ui/Alert/Alert.module.css +1 -2
  86. package/src/components/ui/Calendar/Calendar.module.css +2 -5
  87. package/src/components/ui/Carousel/Carousel.module.css +1 -3
  88. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +1 -2
  89. package/src/components/ui/Pagination/Pagination.module.css +1 -2
  90. package/src/components/ui/SkipLink/SkipLink.module.css +1 -2
  91. package/src/components/ui/Table/Table.module.css +1 -2
  92. package/src/css/bdc.css +16 -11
  93. package/src/index.ts +1 -1
  94. package/src/stories/Introduction.mdx +2 -1
  95. package/src/web-components/globals.ts +3 -3
  96. package/src/web-components/index.ts +12 -0
  97. package/src/web-components/interaction/BdsAccordion.mdx +103 -0
  98. package/src/web-components/interaction/BdsAccordion.stories.tsx +94 -0
  99. package/src/web-components/interaction/BdsCollapsible.mdx +116 -0
  100. package/src/web-components/interaction/BdsCollapsible.stories.tsx +68 -0
  101. package/src/web-components/interaction/BdsDialog.mdx +110 -0
  102. package/src/web-components/interaction/BdsDialog.stories.tsx +157 -0
  103. package/src/web-components/interaction/BdsDrawer.mdx +108 -0
  104. package/src/web-components/interaction/BdsDrawer.stories.tsx +196 -0
  105. package/src/web-components/interaction/BdsDropdownMenu.mdx +108 -0
  106. package/src/web-components/interaction/BdsDropdownMenu.stories.tsx +91 -0
  107. package/src/web-components/interaction/BdsSkipLink.mdx +90 -0
  108. package/src/web-components/interaction/BdsSkipLink.stories.tsx +68 -0
  109. package/src/web-components/interaction/BdsTabs.mdx +117 -0
  110. package/src/web-components/interaction/BdsTabs.stories.tsx +94 -0
  111. package/src/web-components/interaction/BdsTooltip.mdx +104 -0
  112. package/src/web-components/interaction/BdsTooltip.stories.tsx +87 -0
  113. package/src/web-components/interaction/bds-accordion.ts +1 -2
  114. package/src/web-components/interaction/bds-button.ts +15 -13
  115. package/src/web-components/interaction/bds-collapsible.ts +1 -0
  116. package/src/web-components/interaction/bds-dialog.ts +2 -5
  117. package/src/web-components/interaction/bds-drawer.ts +70 -44
  118. package/src/web-components/interaction/bds-dropdown-menu-item.ts +124 -0
  119. package/src/web-components/interaction/bds-dropdown-menu.spec.ts +102 -0
  120. package/src/web-components/interaction/bds-dropdown-menu.ts +200 -0
  121. package/src/web-components/interaction/form/BdsCheckbox.mdx +76 -0
  122. package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +72 -0
  123. package/src/web-components/interaction/form/BdsCheckboxGroup.mdx +70 -0
  124. package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +81 -0
  125. package/src/web-components/interaction/form/BdsCombobox.mdx +71 -0
  126. package/src/web-components/interaction/form/BdsCombobox.stories.tsx +93 -0
  127. package/src/web-components/interaction/form/BdsFileInput.mdx +79 -0
  128. package/src/web-components/interaction/form/BdsFileInput.stories.tsx +69 -0
  129. package/src/web-components/interaction/form/BdsFormInput.mdx +98 -0
  130. package/src/web-components/interaction/form/BdsFormInput.stories.tsx +139 -0
  131. package/src/web-components/interaction/form/BdsNumberInput.mdx +70 -0
  132. package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +68 -0
  133. package/src/web-components/interaction/form/BdsRadio.mdx +72 -0
  134. package/src/web-components/interaction/form/BdsRadio.stories.tsx +58 -0
  135. package/src/web-components/interaction/form/BdsRadioGroup.mdx +73 -0
  136. package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +88 -0
  137. package/src/web-components/interaction/form/BdsSegmentedControl.mdx +95 -0
  138. package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +102 -0
  139. package/src/web-components/interaction/form/BdsSelect.mdx +76 -0
  140. package/src/web-components/interaction/form/BdsSelect.stories.tsx +84 -0
  141. package/src/web-components/interaction/form/BdsSlider.mdx +77 -0
  142. package/src/web-components/interaction/form/BdsSlider.stories.tsx +68 -0
  143. package/src/web-components/interaction/form/BdsSwitch.mdx +72 -0
  144. package/src/web-components/interaction/form/BdsSwitch.stories.tsx +58 -0
  145. package/src/web-components/interaction/form/BdsTextarea.mdx +73 -0
  146. package/src/web-components/interaction/form/BdsTextarea.stories.tsx +70 -0
  147. package/src/web-components/interaction/form/bds-checkbox-group.spec.ts +55 -0
  148. package/src/web-components/interaction/form/bds-checkbox-group.ts +117 -0
  149. package/src/web-components/interaction/form/bds-checkbox.ts +1 -3
  150. package/src/web-components/interaction/form/bds-combobox.ts +1 -3
  151. package/src/web-components/interaction/form/bds-form-input.spec.ts +83 -0
  152. package/src/web-components/interaction/form/bds-form-input.ts +268 -0
  153. package/src/web-components/interaction/form/bds-number-input.ts +3 -4
  154. package/src/web-components/interaction/form/bds-radio-group.spec.ts +62 -0
  155. package/src/web-components/interaction/form/bds-radio-group.ts +142 -0
  156. package/src/web-components/interaction/form/bds-radio.ts +6 -12
  157. package/src/web-components/interaction/form/bds-segmented-control.ts +96 -20
  158. package/src/web-components/interaction/form/bds-select.ts +2 -4
  159. package/src/web-components/interaction/form/bds-switch.ts +3 -4
  160. package/src/web-components/interaction/form/bds-textarea.ts +1 -3
  161. package/src/web-components/ui/BdsAvatar.mdx +76 -0
  162. package/src/web-components/ui/BdsAvatar.stories.tsx +48 -0
  163. package/src/web-components/ui/BdsBreadcrumb.mdx +66 -0
  164. package/src/web-components/ui/BdsBreadcrumb.stories.tsx +49 -0
  165. package/src/web-components/ui/BdsButtonGroup.mdx +69 -0
  166. package/src/web-components/ui/BdsButtonGroup.stories.tsx +66 -0
  167. package/src/web-components/ui/BdsCalendar.mdx +93 -0
  168. package/src/web-components/ui/BdsCalendar.stories.tsx +63 -0
  169. package/src/web-components/ui/BdsCard.mdx +88 -0
  170. package/src/web-components/ui/BdsCard.stories.tsx +67 -0
  171. package/src/web-components/ui/BdsCarousel.mdx +83 -0
  172. package/src/web-components/ui/BdsCarousel.stories.tsx +96 -0
  173. package/src/web-components/ui/BdsDescriptionList.mdx +64 -0
  174. package/src/web-components/ui/BdsDescriptionList.stories.tsx +64 -0
  175. package/src/web-components/ui/BdsIconWrapper.mdx +70 -0
  176. package/src/web-components/ui/BdsIconWrapper.stories.tsx +81 -0
  177. package/src/web-components/ui/BdsLink.mdx +79 -0
  178. package/src/web-components/ui/BdsLink.stories.tsx +63 -0
  179. package/src/web-components/ui/BdsLoading.mdx +58 -0
  180. package/src/web-components/ui/BdsLoading.stories.tsx +35 -0
  181. package/src/web-components/ui/BdsNotificationBanner.mdx +85 -0
  182. package/src/web-components/ui/BdsNotificationBanner.stories.tsx +84 -0
  183. package/src/web-components/ui/BdsPagination.mdx +74 -0
  184. package/src/web-components/ui/BdsPagination.stories.tsx +36 -0
  185. package/src/web-components/ui/BdsProgress.mdx +70 -0
  186. package/src/web-components/ui/BdsProgress.stories.tsx +96 -0
  187. package/src/web-components/ui/BdsProgressCircle.mdx +70 -0
  188. package/src/web-components/ui/BdsProgressCircle.stories.tsx +57 -0
  189. package/src/web-components/ui/BdsRating.mdx +61 -0
  190. package/src/web-components/ui/BdsRating.stories.tsx +45 -0
  191. package/src/web-components/ui/BdsSectionHeader.mdx +72 -0
  192. package/src/web-components/ui/BdsSectionHeader.stories.tsx +54 -0
  193. package/src/web-components/ui/BdsSeparator.mdx +77 -0
  194. package/src/web-components/ui/BdsSeparator.stories.tsx +55 -0
  195. package/src/web-components/ui/BdsSkeleton.mdx +61 -0
  196. package/src/web-components/ui/BdsSkeleton.stories.tsx +50 -0
  197. package/src/web-components/ui/BdsTable.mdx +81 -0
  198. package/src/web-components/ui/BdsTable.stories.tsx +83 -0
  199. package/src/web-components/ui/BdsTypography.mdx +74 -0
  200. package/src/web-components/ui/BdsTypography.stories.tsx +52 -0
  201. package/src/web-components/ui/bds-alert.ts +1 -2
  202. package/src/web-components/ui/bds-button-group.spec.ts +40 -0
  203. package/src/web-components/ui/bds-button-group.ts +78 -0
  204. package/src/web-components/ui/bds-calendar.spec.ts +91 -0
  205. package/src/web-components/ui/bds-calendar.ts +427 -0
  206. package/src/web-components/ui/bds-card.ts +1 -3
  207. package/src/web-components/ui/bds-carousel.spec.ts +64 -0
  208. package/src/web-components/ui/bds-carousel.ts +296 -0
  209. package/src/web-components/ui/bds-notification-banner.ts +1 -2
  210. package/src/web-components/ui/bds-pagination.spec.ts +67 -0
  211. package/src/web-components/ui/bds-pagination.ts +197 -0
  212. package/src/web-components/ui/bds-table.spec.ts +45 -0
  213. package/src/web-components/ui/bds-table.ts +96 -0
@@ -37,44 +37,65 @@ export class BdsDrawer extends LitElement {
37
37
 
38
38
  .drawer {
39
39
  position: fixed;
40
- inset: 0;
40
+ inset-block: 0;
41
+ inset-inline: auto 0;
42
+ inline-size: min(28rem, 90vw);
43
+ max-inline-size: 100vw;
44
+ block-size: 100dvh;
45
+ max-block-size: 100dvh;
41
46
  margin: 0;
42
47
  padding: 0;
43
- inline-size: 100%;
44
- max-inline-size: 100%;
45
- block-size: 100%;
46
- max-block-size: 100%;
48
+ display: flex;
49
+ flex-direction: column;
50
+ background-color: var(--drawer_color, var(--bds-color_bg));
51
+ color: var(--drawer_on-color, var(--bds-color_on-bg));
47
52
  border: none;
48
- background: transparent;
53
+ box-shadow: var(--bds-shadow_xl);
49
54
  overflow: hidden;
55
+ translate: 100% 0;
56
+ transition:
57
+ translate var(--bds-animation_transition-duration) var(--bds-animation_easing),
58
+ display var(--bds-animation_transition-duration) var(--bds-animation_easing) allow-discrete,
59
+ overlay var(--bds-animation_transition-duration) var(--bds-animation_easing) allow-discrete;
60
+ }
61
+
62
+ :host([side='left']) .drawer {
63
+ inset-inline: 0 auto;
64
+ translate: -100% 0;
65
+ }
66
+
67
+ .drawer[open] {
68
+ translate: 0 0;
69
+ }
70
+
71
+ @starting-style {
72
+ .drawer[open] {
73
+ translate: 100% 0;
74
+ }
75
+
76
+ :host([side='left']) .drawer[open] {
77
+ translate: -100% 0;
78
+ }
50
79
  }
51
80
 
52
81
  .drawer::backdrop {
53
82
  background-color: var(--color_backdrop, rgb(0 0 0 / 50%));
54
83
  backdrop-filter: blur(3px);
84
+ opacity: 0;
85
+ transition:
86
+ opacity var(--bds-animation_transition-duration) var(--bds-animation_easing),
87
+ display var(--bds-animation_transition-duration) allow-discrete,
88
+ overlay var(--bds-animation_transition-duration) allow-discrete;
55
89
  }
56
90
 
57
- .panel {
58
- position: absolute;
59
- inset-block: 0;
60
- display: flex;
61
- flex-direction: column;
62
- inline-size: min(28rem, 90vw);
63
- block-size: 100%;
64
- background-color: var(--drawer_color, var(--bds-color_bg));
65
- color: var(--drawer_on-color, var(--bds-color_on-bg));
66
- box-shadow: var(--bds-shadow_xl);
67
- overflow: hidden;
68
- transition: var(--bds-animation_transition);
69
- }
70
-
71
- :host([side='right']) .panel {
72
- inset-inline-end: 0;
91
+ .drawer[open]::backdrop {
92
+ opacity: 1;
73
93
  }
74
94
 
75
- :host([side='left']) .panel,
76
- :host(:not([side])) .panel {
77
- inset-inline-start: 0;
95
+ @starting-style {
96
+ .drawer[open]::backdrop {
97
+ opacity: 0;
98
+ }
78
99
  }
79
100
 
80
101
  .header {
@@ -128,7 +149,8 @@ export class BdsDrawer extends LitElement {
128
149
  }
129
150
 
130
151
  @media (prefers-reduced-motion: reduce) {
131
- .panel {
152
+ .drawer,
153
+ .drawer::backdrop {
132
154
  transition: none;
133
155
  }
134
156
  }
@@ -202,7 +224,13 @@ export class BdsDrawer extends LitElement {
202
224
  }
203
225
 
204
226
  private _handleBackdropClick(e: MouseEvent) {
205
- if (e.target === this._dialog) this._closeDrawer();
227
+ const dialog = this._dialog;
228
+ if (!dialog) return;
229
+ const rect = dialog.getBoundingClientRect();
230
+ const outside =
231
+ e.clientX < rect.left || e.clientX > rect.right ||
232
+ e.clientY < rect.top || e.clientY > rect.bottom;
233
+ if (outside) this._closeDrawer();
206
234
  }
207
235
 
208
236
  private _handleCancel(e: Event) {
@@ -234,23 +262,21 @@ export class BdsDrawer extends LitElement {
234
262
  @cancel=${this._handleCancel}
235
263
  @keydown=${this._handleKeydown}
236
264
  >
237
- <div class="panel">
238
- <div class="header">
239
- <span class="title"><slot name="title"></slot></span>
240
- <button
241
- type="button"
242
- class="close-button"
243
- aria-label="Close drawer"
244
- @click=${this._closeDrawer}
245
- >
246
- <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
247
- <path stroke-linecap="round" stroke-linejoin="round" d="M18 6L6 18M6 6l12 12" />
248
- </svg>
249
- </button>
250
- </div>
251
- <div class="body">
252
- <slot></slot>
253
- </div>
265
+ <div class="header">
266
+ <span class="title"><slot name="title"></slot></span>
267
+ <button
268
+ type="button"
269
+ class="close-button"
270
+ aria-label="Close drawer"
271
+ @click=${this._closeDrawer}
272
+ >
273
+ <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
274
+ <path stroke-linecap="round" stroke-linejoin="round" d="M18 6L6 18M6 6l12 12" />
275
+ </svg>
276
+ </button>
277
+ </div>
278
+ <div class="body">
279
+ <slot></slot>
254
280
  </div>
255
281
  </dialog>
256
282
  `;
@@ -0,0 +1,124 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ /**
4
+ * `<bds-dropdown-menu-item>` — a single item inside a `<bds-dropdown-menu>`.
5
+ *
6
+ * Attributes:
7
+ * value — value associated with this item
8
+ * disabled — disables this item
9
+ * destructive — renders the item in a destructive (red) style
10
+ *
11
+ * Slots:
12
+ * (default) — item label text
13
+ *
14
+ * Events:
15
+ * bds-select — fired when the item is clicked or activated; `detail: { value: string }`
16
+ *
17
+ * @example
18
+ * <bds-dropdown-menu-item value="delete" destructive>Delete</bds-dropdown-menu-item>
19
+ */
20
+ export class BdsDropdownMenuItem extends LitElement {
21
+ static styles = css`
22
+ :host {
23
+ display: block;
24
+ }
25
+
26
+ :host([disabled]) {
27
+ opacity: 0.5;
28
+ pointer-events: none;
29
+ }
30
+
31
+ .item {
32
+ display: flex;
33
+ align-items: center;
34
+ gap: var(--bds-space_xs);
35
+ inline-size: 100%;
36
+ padding: var(--bds-space_xs) var(--bds-space_s);
37
+ font-size: var(--bds-font_size--body);
38
+ font-family: var(--bds-font_family--body);
39
+ color: var(--bds-color_on-bg);
40
+ background: none;
41
+ border: none;
42
+ text-align: start;
43
+ cursor: pointer;
44
+ border-radius: var(--bds-border_radius--xs);
45
+ transition: var(--bds-animation_transition);
46
+ }
47
+
48
+ :host([destructive]) .item {
49
+ color: var(--bds-color_error);
50
+ }
51
+
52
+ @media (hover: hover) and (pointer: fine) {
53
+ .item:hover {
54
+ background-color: var(--bds-color_bg--subtle);
55
+ }
56
+
57
+ :host([destructive]) .item:hover {
58
+ background-color: color-mix(in srgb, var(--bds-color_error) 10%, transparent);
59
+ }
60
+ }
61
+
62
+ .item:focus-visible {
63
+ outline: var(--bds-outline_default);
64
+ outline-offset: var(--bds-outline_offset);
65
+ }
66
+ `;
67
+
68
+ static properties = {
69
+ value: { type: String, reflect: true },
70
+ disabled: { type: Boolean, reflect: true },
71
+ destructive: { type: Boolean, reflect: true },
72
+ };
73
+
74
+ declare value: string;
75
+ declare disabled: boolean;
76
+ declare destructive: boolean;
77
+
78
+ constructor() {
79
+ super();
80
+ this.value = '';
81
+ this.disabled = false;
82
+ this.destructive = false;
83
+ }
84
+
85
+ private _handleClick() {
86
+ if (this.disabled) return;
87
+ this.dispatchEvent(new CustomEvent('bds-select', {
88
+ detail: { value: this.value },
89
+ bubbles: true,
90
+ composed: true,
91
+ }));
92
+ }
93
+
94
+ private _handleKeydown(e: KeyboardEvent) {
95
+ if (e.key === 'Enter' || e.key === ' ') {
96
+ e.preventDefault();
97
+ this._handleClick();
98
+ }
99
+ }
100
+
101
+ render() {
102
+ return html`
103
+ <button
104
+ class="item"
105
+ type="button"
106
+ role="menuitem"
107
+ tabindex=${this.disabled ? '-1' : '0'}
108
+ ?disabled=${this.disabled}
109
+ @click=${this._handleClick}
110
+ @keydown=${this._handleKeydown}
111
+ >
112
+ <slot></slot>
113
+ </button>
114
+ `;
115
+ }
116
+ }
117
+
118
+ customElements.define('bds-dropdown-menu-item', BdsDropdownMenuItem);
119
+
120
+ declare global {
121
+ interface HTMLElementTagNameMap {
122
+ 'bds-dropdown-menu-item': BdsDropdownMenuItem;
123
+ }
124
+ }
@@ -0,0 +1,102 @@
1
+ import { fixture, cleanup } from '../test/helpers';
2
+ import { BdsDropdownMenu } from './bds-dropdown-menu';
3
+ import { BdsDropdownMenuItem } from './bds-dropdown-menu-item';
4
+
5
+ describe('bds-dropdown-menu', () => {
6
+ it('is registered as a custom element', () => {
7
+ expect(customElements.get('bds-dropdown-menu')).toBe(BdsDropdownMenu);
8
+ });
9
+
10
+ it('is closed by default', async () => {
11
+ const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
12
+ expect(el.open).toBe(false);
13
+ cleanup(el);
14
+ });
15
+
16
+ it('open attribute is absent when closed', async () => {
17
+ const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
18
+ expect(el.open).toBe(false);
19
+ expect(el.hasAttribute('open')).toBe(false);
20
+ cleanup(el);
21
+ });
22
+
23
+ it('fires bds-open when _open() is called', async () => {
24
+ const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
25
+ const events: string[] = [];
26
+ el.addEventListener('bds-open', () => events.push('open'));
27
+ el._open();
28
+ expect(events).toContain('open');
29
+ cleanup(el);
30
+ });
31
+
32
+ it('fires bds-close when _close() is called', async () => {
33
+ const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
34
+ el._open();
35
+ const events: string[] = [];
36
+ el.addEventListener('bds-close', () => events.push('close'));
37
+ el._close();
38
+ expect(events).toContain('close');
39
+ cleanup(el);
40
+ });
41
+
42
+ it('does not fire bds-open if already open (state guard)', async () => {
43
+ const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
44
+ el._open();
45
+ const events: string[] = [];
46
+ el.addEventListener('bds-open', () => events.push('open'));
47
+ el._open(); // call again
48
+ expect(events.length).toBe(0);
49
+ cleanup(el);
50
+ });
51
+
52
+ it('does not fire bds-close if already closed (state guard)', async () => {
53
+ const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
54
+ const events: string[] = [];
55
+ el.addEventListener('bds-close', () => events.push('close'));
56
+ el._close(); // already closed
57
+ expect(events.length).toBe(0);
58
+ cleanup(el);
59
+ });
60
+
61
+ it('closes on Escape key', async () => {
62
+ const el = await fixture('<bds-dropdown-menu></bds-dropdown-menu>') as BdsDropdownMenu;
63
+ el._open();
64
+ expect(el.open).toBe(true);
65
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
66
+ expect(el.open).toBe(false);
67
+ cleanup(el);
68
+ });
69
+ });
70
+
71
+ describe('bds-dropdown-menu-item', () => {
72
+ it('is registered as a custom element', () => {
73
+ expect(customElements.get('bds-dropdown-menu-item')).toBe(BdsDropdownMenuItem);
74
+ });
75
+
76
+ it('renders a button with role="menuitem"', async () => {
77
+ const el = await fixture('<bds-dropdown-menu-item value="edit">Edit</bds-dropdown-menu-item>');
78
+ const btn = el.shadowRoot!.querySelector('button')!;
79
+ expect(btn.getAttribute('role')).toBe('menuitem');
80
+ cleanup(el);
81
+ });
82
+
83
+ it('fires bds-select on click', async () => {
84
+ const el = await fixture('<bds-dropdown-menu-item value="edit">Edit</bds-dropdown-menu-item>');
85
+ const events: string[] = [];
86
+ el.addEventListener('bds-select', (e: Event) => {
87
+ events.push((e as CustomEvent).detail.value);
88
+ });
89
+ el.shadowRoot!.querySelector<HTMLButtonElement>('button')!.click();
90
+ expect(events).toEqual(['edit']);
91
+ cleanup(el);
92
+ });
93
+
94
+ it('does not fire bds-select when disabled', async () => {
95
+ const el = await fixture('<bds-dropdown-menu-item value="edit" disabled>Edit</bds-dropdown-menu-item>');
96
+ const events: string[] = [];
97
+ el.addEventListener('bds-select', () => events.push('selected'));
98
+ el.shadowRoot!.querySelector<HTMLButtonElement>('button')!.click();
99
+ expect(events.length).toBe(0);
100
+ cleanup(el);
101
+ });
102
+ });
@@ -0,0 +1,200 @@
1
+ import { LitElement, css, html } from 'lit';
2
+ import type { BdsDropdownMenuItem } from './bds-dropdown-menu-item';
3
+
4
+ export type DropdownMenuPlacement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
5
+
6
+ /**
7
+ * `<bds-dropdown-menu>` — anchored dropdown menu with keyboard navigation.
8
+ *
9
+ * Uses the native Popover API for the floating panel (light-dismiss, top-layer).
10
+ * The trigger slot element controls open/close. The component handles keyboard
11
+ * navigation (Arrow keys, Escape, Tab) and focus management.
12
+ *
13
+ * Attributes:
14
+ * open — controls open/closed state (reflected)
15
+ * placement — menu position relative to trigger (default: `"bottom-start"`)
16
+ *
17
+ * Slots:
18
+ * trigger — the activating element (button or similar)
19
+ * (default) — `<bds-dropdown-menu-item>` elements
20
+ *
21
+ * Events:
22
+ * bds-open — fired when the menu opens
23
+ * bds-close — fired when the menu closes
24
+ *
25
+ * @example
26
+ * <bds-dropdown-menu>
27
+ * <button slot="trigger">Options</button>
28
+ * <bds-dropdown-menu-item value="edit">Edit</bds-dropdown-menu-item>
29
+ * <bds-dropdown-menu-item value="delete" destructive>Delete</bds-dropdown-menu-item>
30
+ * </bds-dropdown-menu>
31
+ */
32
+ export class BdsDropdownMenu extends LitElement {
33
+ static styles = css`
34
+ :host {
35
+ display: inline-block;
36
+ position: relative;
37
+ }
38
+
39
+ .panel {
40
+ position: absolute;
41
+ inset-block-start: calc(100% + var(--bds-space_xxs));
42
+ inset-inline-start: 0;
43
+ min-inline-size: 10rem;
44
+ background-color: var(--bds-color_bg);
45
+ border-radius: var(--bds-border_radius--s);
46
+ box-shadow: var(--bds-shadow_m);
47
+ padding: var(--bds-space_xxs);
48
+ z-index: var(--bds-z-index_dropdown, 200);
49
+ display: none;
50
+ flex-direction: column;
51
+ }
52
+
53
+ :host([placement='bottom-end']) .panel {
54
+ inset-inline-start: auto;
55
+ inset-inline-end: 0;
56
+ }
57
+
58
+ :host([placement='top-start']) .panel {
59
+ inset-block-start: auto;
60
+ inset-block-end: calc(100% + var(--bds-space_xxs));
61
+ inset-inline-start: 0;
62
+ }
63
+
64
+ :host([placement='top-end']) .panel {
65
+ inset-block-start: auto;
66
+ inset-block-end: calc(100% + var(--bds-space_xxs));
67
+ inset-inline-start: auto;
68
+ inset-inline-end: 0;
69
+ }
70
+
71
+ :host([open]) .panel {
72
+ display: flex;
73
+ }
74
+ `;
75
+
76
+ static properties = {
77
+ open: { type: Boolean, reflect: true },
78
+ placement: { type: String, reflect: true },
79
+ };
80
+
81
+ declare open: boolean;
82
+ declare placement: DropdownMenuPlacement;
83
+
84
+ constructor() {
85
+ super();
86
+ this.open = false;
87
+ this.placement = 'bottom-start';
88
+ }
89
+
90
+ connectedCallback() {
91
+ super.connectedCallback();
92
+ document.addEventListener('click', this._handleOutsideClick);
93
+ this.addEventListener('keydown', this._handleKeydown);
94
+ this.addEventListener('bds-select', this._handleItemSelect);
95
+ }
96
+
97
+ disconnectedCallback() {
98
+ super.disconnectedCallback();
99
+ document.removeEventListener('click', this._handleOutsideClick);
100
+ this.removeEventListener('keydown', this._handleKeydown);
101
+ this.removeEventListener('bds-select', this._handleItemSelect);
102
+ }
103
+
104
+ private get _items(): BdsDropdownMenuItem[] {
105
+ return [...this.querySelectorAll<BdsDropdownMenuItem>('bds-dropdown-menu-item')].filter(
106
+ item => !item.disabled,
107
+ );
108
+ }
109
+
110
+ private get _trigger(): HTMLElement | null {
111
+ const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name="trigger"]');
112
+ return (slot?.assignedElements()[0] as HTMLElement) ?? null;
113
+ }
114
+
115
+ _open() {
116
+ if (this.open) return;
117
+ this.open = true;
118
+ this.dispatchEvent(new CustomEvent('bds-open', { bubbles: true, composed: true }));
119
+ // Focus first item after render
120
+ this.updateComplete.then(() => {
121
+ this._items[0]?.shadowRoot?.querySelector<HTMLElement>('button')?.focus();
122
+ });
123
+ }
124
+
125
+ _close() {
126
+ if (!this.open) return;
127
+ this.open = false;
128
+ this.dispatchEvent(new CustomEvent('bds-close', { bubbles: true, composed: true }));
129
+ this._trigger?.focus();
130
+ }
131
+
132
+ private _handleTriggerClick = (e: Event) => {
133
+ e.stopPropagation();
134
+ if (this.open) { this._close(); } else { this._open(); }
135
+ };
136
+
137
+ private _handleOutsideClick = (e: Event) => {
138
+ if (this.open && !this.contains(e.target as Node)) {
139
+ this._close();
140
+ }
141
+ };
142
+
143
+ private _handleKeydown = (e: KeyboardEvent) => {
144
+ if (!this.open) return;
145
+
146
+ const items = this._items;
147
+ const focused = items.findIndex(item =>
148
+ item.shadowRoot?.activeElement !== null &&
149
+ item.shadowRoot?.querySelector('button') === item.shadowRoot?.activeElement,
150
+ );
151
+
152
+ if (e.key === 'Escape') {
153
+ e.preventDefault();
154
+ this._close();
155
+ } else if (e.key === 'ArrowDown') {
156
+ e.preventDefault();
157
+ const next = (focused + 1) % items.length;
158
+ items[next]?.shadowRoot?.querySelector<HTMLElement>('button')?.focus();
159
+ } else if (e.key === 'ArrowUp') {
160
+ e.preventDefault();
161
+ const prev = (focused - 1 + items.length) % items.length;
162
+ items[prev]?.shadowRoot?.querySelector<HTMLElement>('button')?.focus();
163
+ } else if (e.key === 'Tab') {
164
+ this._close();
165
+ }
166
+ };
167
+
168
+ private _handleItemSelect = () => {
169
+ this._close();
170
+ };
171
+
172
+ private _onTriggerSlotChange() {
173
+ const trigger = this._trigger;
174
+ if (trigger) {
175
+ trigger.removeEventListener('click', this._handleTriggerClick);
176
+ trigger.addEventListener('click', this._handleTriggerClick);
177
+ }
178
+ }
179
+
180
+ render() {
181
+ return html`
182
+ <slot name="trigger" @slotchange=${this._onTriggerSlotChange}></slot>
183
+ <div
184
+ class="panel"
185
+ role="menu"
186
+ aria-hidden=${!this.open ? 'true' : 'false'}
187
+ >
188
+ <slot></slot>
189
+ </div>
190
+ `;
191
+ }
192
+ }
193
+
194
+ customElements.define('bds-dropdown-menu', BdsDropdownMenu);
195
+
196
+ declare global {
197
+ interface HTMLElementTagNameMap {
198
+ 'bds-dropdown-menu': BdsDropdownMenu;
199
+ }
200
+ }
@@ -0,0 +1,76 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './BdsCheckbox.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # &lt;bds-checkbox&gt;
7
+
8
+ Form-associated checkbox custom element. Participates in HTML forms via the Form-Associated Custom Elements API — native browser validation (required, etc.) works without any wrapper.
9
+
10
+ > **Status: experimental** — API may change before stable release.
11
+
12
+ ## Installation
13
+
14
+ ```js
15
+ import '@boostdev/components/web-components';
16
+ ```
17
+
18
+ ## When to use
19
+
20
+ Use `<bds-checkbox>` whenever you need a single boolean toggle that participates in a form. For grouping multiple checkboxes under a shared label, wrap them in `<bds-checkbox-group>`.
21
+
22
+ ## Examples
23
+
24
+ ### Default
25
+ <Canvas of={Stories.Default} />
26
+
27
+ ### Checked
28
+ <Canvas of={Stories.Checked} />
29
+
30
+ ### Required
31
+ <Canvas of={Stories.Required} />
32
+
33
+ ### Disabled
34
+ <Canvas of={Stories.Disabled} />
35
+
36
+ ### Visual group (without group wrapper)
37
+ <Canvas of={Stories.WithGroup} />
38
+
39
+ ## Props
40
+
41
+ <ArgTypes of={Stories} />
42
+
43
+ ## Attributes
44
+
45
+ | Attribute | Type | Default | Description |
46
+ |------------|---------|---------|--------------------------------------------------|
47
+ | `name` | string | `""` | Form field name |
48
+ | `value` | string | `"on"` | Value submitted when checked |
49
+ | `checked` | boolean | `false` | Whether the checkbox is checked |
50
+ | `required` | boolean | `false` | Marks the field as required |
51
+ | `disabled` | boolean | `false` | Disables the checkbox |
52
+
53
+ ## Slots
54
+
55
+ | Slot | Description |
56
+ |-------------|--------------------|
57
+ | `(default)` | Label text content |
58
+
59
+ ## Events
60
+
61
+ | Event | Detail | Description |
62
+ |----------|--------|-------------------------------------|
63
+ | `change` | — | Native change event forwarded from the inner `<input>` |
64
+
65
+ ## Usage in plain HTML
66
+
67
+ ```html
68
+ <bds-checkbox name="agree" value="yes" required>I agree to the terms</bds-checkbox>
69
+ ```
70
+
71
+ ## Accessibility
72
+
73
+ - The inner `<input type="checkbox">` is associated with a `<label>` via matching `id`/`for` attributes
74
+ - `required` triggers native browser validation
75
+ - `disabled` sets `pointer-events: none` and reduces opacity
76
+ - Use `<bds-checkbox-group>` for accessible grouping with `<fieldset>`/`<legend>`