@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
@@ -0,0 +1,157 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import '../index'; // auto-registers all custom elements
4
+
5
+ // Thin React wrapper for bds-dialog + its trigger button
6
+ function BdsDialog({
7
+ open,
8
+ triggerLabel = 'Open dialog',
9
+ children,
10
+ }: {
11
+ open?: boolean;
12
+ triggerLabel?: string;
13
+ children?: React.ReactNode;
14
+ }) {
15
+ const id = React.useId();
16
+ const [isOpen, setIsOpen] = React.useState(open ?? false);
17
+ const dialogRef = React.useRef<HTMLElement>(null);
18
+
19
+ React.useEffect(() => {
20
+ setIsOpen(open ?? false);
21
+ }, [open]);
22
+
23
+ React.useEffect(() => {
24
+ const el = dialogRef.current;
25
+ if (!el) return;
26
+ const handler = () => setIsOpen(false);
27
+ el.addEventListener('bds-close', handler);
28
+ return () => el.removeEventListener('bds-close', handler);
29
+ }, []);
30
+
31
+ return (
32
+ <div>
33
+ <button
34
+ type="button"
35
+ onClick={() => setIsOpen(true)}
36
+ style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}
37
+ >
38
+ {triggerLabel}
39
+ </button>
40
+ {React.createElement(
41
+ 'bds-dialog',
42
+ {
43
+ id,
44
+ ref: dialogRef,
45
+ open: isOpen || undefined,
46
+ },
47
+ children ?? React.createElement('p', { style: { margin: 0 } }, 'Dialog content goes here.'),
48
+ )}
49
+ </div>
50
+ );
51
+ }
52
+
53
+ const meta = {
54
+ title: 'Web Components/Interaction/Dialog',
55
+ component: BdsDialog,
56
+ tags: ['!stable', 'experimental'],
57
+ parameters: { layout: 'centered' },
58
+ argTypes: {
59
+ triggerLabel: { control: 'text' },
60
+ },
61
+ } satisfies Meta<typeof BdsDialog>;
62
+
63
+ export default meta;
64
+ type Story = StoryObj<typeof meta>;
65
+
66
+ export const Default: Story = {
67
+ args: { triggerLabel: 'Open dialog' },
68
+ };
69
+
70
+ function WithFormDialog() {
71
+ const id = React.useId();
72
+ const [isOpen, setIsOpen] = React.useState(false);
73
+ const dialogRef = React.useRef<HTMLElement>(null);
74
+
75
+ React.useEffect(() => {
76
+ const el = dialogRef.current;
77
+ if (!el) return;
78
+ const handler = () => setIsOpen(false);
79
+ el.addEventListener('bds-close', handler);
80
+ return () => el.removeEventListener('bds-close', handler);
81
+ }, []);
82
+
83
+ return (
84
+ <div>
85
+ <button
86
+ type="button"
87
+ onClick={() => setIsOpen(true)}
88
+ style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}
89
+ >
90
+ Open form dialog
91
+ </button>
92
+ {React.createElement(
93
+ 'bds-dialog',
94
+ {
95
+ id,
96
+ ref: dialogRef,
97
+ open: isOpen || undefined,
98
+ },
99
+ React.createElement(
100
+ 'form',
101
+ {
102
+ style: { display: 'flex', flexDirection: 'column', gap: '1rem', minWidth: '280px' },
103
+ onSubmit: (e: React.FormEvent) => { e.preventDefault(); setIsOpen(false); },
104
+ },
105
+ React.createElement('h2', { style: { margin: 0 } }, 'Edit profile'),
106
+ React.createElement(
107
+ 'label',
108
+ { style: { display: 'flex', flexDirection: 'column', gap: '0.25rem' } },
109
+ 'Name',
110
+ React.createElement('input', { type: 'text', defaultValue: 'Jane Doe', style: { padding: '0.5rem' } }),
111
+ ),
112
+ React.createElement(
113
+ 'label',
114
+ { style: { display: 'flex', flexDirection: 'column', gap: '0.25rem' } },
115
+ 'Email',
116
+ React.createElement('input', { type: 'email', defaultValue: 'jane@example.com', style: { padding: '0.5rem' } }),
117
+ ),
118
+ React.createElement(
119
+ 'div',
120
+ { style: { display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' } },
121
+ React.createElement('button', { type: 'button', onClick: () => setIsOpen(false) }, 'Cancel'),
122
+ React.createElement('button', { type: 'submit' }, 'Save'),
123
+ ),
124
+ ),
125
+ )}
126
+ </div>
127
+ );
128
+ }
129
+
130
+ export const WithForm: Story = {
131
+ render: () => React.createElement(WithFormDialog),
132
+ };
133
+
134
+ export const InvokerCommands: Story = {
135
+ render: () => {
136
+ const dialogId = 'story-dialog-invoker';
137
+ return (
138
+ <div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
139
+ {React.createElement(
140
+ 'button',
141
+ { commandFor: dialogId, command: 'show-modal', style: { padding: '0.5rem 1rem', cursor: 'pointer' } },
142
+ 'Open with Invoker Command',
143
+ )}
144
+ {React.createElement(
145
+ 'bds-dialog',
146
+ { id: dialogId },
147
+ React.createElement(
148
+ 'div',
149
+ { style: { minWidth: '260px' } },
150
+ React.createElement('h2', { style: { margin: '0 0 0.5rem' } }, 'Invoker Commands'),
151
+ React.createElement('p', { style: { margin: 0 } }, 'This dialog was opened using the Invoker Commands API (commandfor + command="show-modal").'),
152
+ ),
153
+ )}
154
+ </div>
155
+ );
156
+ },
157
+ };
@@ -0,0 +1,108 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './BdsDrawer.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # &lt;bds-drawer&gt;
7
+
8
+ Framework-agnostic Drawer (slide-in panel) custom element. Same modal infrastructure as `<bds-dialog>` but positioned as a side panel that slides in from either side of the viewport.
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
+ Or in HTML without a bundler:
19
+
20
+ ```html
21
+ <script type="module">
22
+ import '@boostdev/components/web-components';
23
+ </script>
24
+ ```
25
+
26
+ ## When to use
27
+
28
+ - Navigation panels (mobile nav, side menus)
29
+ - Settings or configuration panels that don't interrupt the primary flow
30
+ - Detail views that overlay the current content
31
+
32
+ ## When not to use
33
+
34
+ - Short confirmations — use `<bds-dialog>` instead
35
+ - Persistent side content that is always visible — use a regular sidebar layout
36
+
37
+ ## Examples
38
+
39
+ ### Default (right side)
40
+ <Canvas of={Stories.Default} />
41
+
42
+ ### Left side
43
+ <Canvas of={Stories.LeftSide} />
44
+
45
+ ### With rich content
46
+ <Canvas of={Stories.WithRichContent} />
47
+
48
+ ### All placements
49
+ <Canvas of={Stories.AllPlacements} />
50
+
51
+ ## Props
52
+
53
+ <ArgTypes of={Stories} />
54
+
55
+ ## Attributes
56
+
57
+ | Attribute | Type | Default | Description |
58
+ |-----------|------|---------|-------------|
59
+ | `open` | boolean | `false` | Controlled open state |
60
+ | `side` | `"left" \| "right"` | `"right"` | Which side the panel slides in from |
61
+
62
+ ## Slots
63
+
64
+ | Slot | Description |
65
+ |------|-------------|
66
+ | `title` | Drawer header title text |
67
+ | `(default)` | Drawer body content |
68
+
69
+ ## Events
70
+
71
+ | Event | Bubbles | Description |
72
+ |-------|---------|-------------|
73
+ | `bds-open` | Yes | Fired when the drawer opens |
74
+ | `bds-close` | Yes | Fired when the drawer closes (backdrop click, Escape, or close button) |
75
+
76
+ ## Invoker Commands API
77
+
78
+ Open or close the drawer declaratively using the Invoker Commands API:
79
+
80
+ ```html
81
+ <button commandfor="my-drawer" command="show-modal">Open</button>
82
+ <bds-drawer id="my-drawer" side="right">
83
+ <span slot="title">Settings</span>
84
+ <p>Drawer content</p>
85
+ </bds-drawer>
86
+ ```
87
+
88
+ ## Usage in plain HTML
89
+
90
+ ```html
91
+ <bds-drawer id="my-drawer" side="right">
92
+ <span slot="title">Settings</span>
93
+ <p>Adjust your preferences here.</p>
94
+ </bds-drawer>
95
+
96
+ <button onclick="document.getElementById('my-drawer').open = true">Open settings</button>
97
+ ```
98
+
99
+ ## Accessibility
100
+
101
+ - Renders a native `<dialog>` element with `aria-modal="true"`
102
+ - Focus is trapped inside the drawer while open (Tab / Shift+Tab cycle)
103
+ - Escape key closes the drawer
104
+ - Clicking the backdrop closes the drawer
105
+ - Focus returns to the triggering element on close
106
+ - A close button is provided with `aria-label="Close drawer"`
107
+ - `document.body.overflow` is set to `hidden` while the drawer is open
108
+ - Slide-in animation respects `prefers-reduced-motion`
@@ -0,0 +1,196 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import type { DrawerSide } from './bds-drawer';
4
+ import '../index'; // auto-registers all custom elements
5
+
6
+ // Thin React wrapper so Storybook controls work with the custom element's props
7
+ function BdsDrawer({
8
+ side = 'right',
9
+ triggerLabel = 'Open drawer',
10
+ title = 'Drawer',
11
+ children,
12
+ }: {
13
+ side?: DrawerSide;
14
+ triggerLabel?: string;
15
+ title?: string;
16
+ children?: React.ReactNode;
17
+ }) {
18
+ const id = React.useId();
19
+ const [isOpen, setIsOpen] = React.useState(false);
20
+ const drawerRef = React.useRef<HTMLElement>(null);
21
+
22
+ React.useEffect(() => {
23
+ const el = drawerRef.current;
24
+ if (!el) return;
25
+ const handler = () => setIsOpen(false);
26
+ el.addEventListener('bds-close', handler);
27
+ return () => el.removeEventListener('bds-close', handler);
28
+ }, []);
29
+
30
+ return (
31
+ <div>
32
+ <button
33
+ type="button"
34
+ onClick={() => setIsOpen(true)}
35
+ style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}
36
+ >
37
+ {triggerLabel}
38
+ </button>
39
+ {React.createElement(
40
+ 'bds-drawer',
41
+ {
42
+ id,
43
+ ref: drawerRef,
44
+ side,
45
+ open: isOpen || undefined,
46
+ },
47
+ React.createElement('span', { slot: 'title' }, title),
48
+ children ?? React.createElement('p', { style: { margin: 0 } }, 'Drawer body content goes here.'),
49
+ )}
50
+ </div>
51
+ );
52
+ }
53
+
54
+ const meta = {
55
+ title: 'Web Components/Interaction/Drawer',
56
+ component: BdsDrawer,
57
+ tags: ['!stable', 'experimental'],
58
+ parameters: { layout: 'centered' },
59
+ argTypes: {
60
+ side: { control: 'select', options: ['left', 'right'] },
61
+ triggerLabel: { control: 'text' },
62
+ title: { control: 'text' },
63
+ },
64
+ } satisfies Meta<typeof BdsDrawer>;
65
+
66
+ export default meta;
67
+ type Story = StoryObj<typeof meta>;
68
+
69
+ export const Default: Story = {
70
+ args: { side: 'right', title: 'Settings', triggerLabel: 'Open drawer' },
71
+ };
72
+
73
+ export const LeftSide: Story = {
74
+ args: { side: 'left', title: 'Navigation', triggerLabel: 'Open left drawer' },
75
+ };
76
+
77
+ function RichDrawer() {
78
+ const id = React.useId();
79
+ const [isOpen, setIsOpen] = React.useState(false);
80
+ const drawerRef = React.useRef<HTMLElement>(null);
81
+
82
+ React.useEffect(() => {
83
+ const el = drawerRef.current;
84
+ if (!el) return;
85
+ const handler = () => setIsOpen(false);
86
+ el.addEventListener('bds-close', handler);
87
+ return () => el.removeEventListener('bds-close', handler);
88
+ }, []);
89
+
90
+ return (
91
+ <div>
92
+ <button
93
+ type="button"
94
+ onClick={() => setIsOpen(true)}
95
+ style={{ padding: '0.5rem 1rem', cursor: 'pointer' }}
96
+ >
97
+ Open rich drawer
98
+ </button>
99
+ {React.createElement(
100
+ 'bds-drawer',
101
+ {
102
+ id,
103
+ ref: drawerRef,
104
+ side: 'right',
105
+ open: isOpen || undefined,
106
+ },
107
+ React.createElement('span', { slot: 'title' }, 'Edit settings'),
108
+ React.createElement(
109
+ 'form',
110
+ {
111
+ style: { display: 'flex', flexDirection: 'column', gap: '1rem' },
112
+ onSubmit: (e: React.FormEvent) => { e.preventDefault(); setIsOpen(false); },
113
+ },
114
+ React.createElement(
115
+ 'label',
116
+ { style: { display: 'flex', flexDirection: 'column', gap: '0.25rem' } },
117
+ 'Display name',
118
+ React.createElement('input', { type: 'text', defaultValue: 'Jane Doe', style: { padding: '0.5rem' } }),
119
+ ),
120
+ React.createElement(
121
+ 'label',
122
+ { style: { display: 'flex', flexDirection: 'column', gap: '0.25rem' } },
123
+ 'Language',
124
+ React.createElement(
125
+ 'select',
126
+ { style: { padding: '0.5rem' } },
127
+ React.createElement('option', null, 'English'),
128
+ React.createElement('option', null, 'Dutch'),
129
+ React.createElement('option', null, 'French'),
130
+ ),
131
+ ),
132
+ React.createElement(
133
+ 'button',
134
+ { type: 'submit', style: { padding: '0.5rem 1rem', cursor: 'pointer' } },
135
+ 'Save changes',
136
+ ),
137
+ ),
138
+ )}
139
+ </div>
140
+ );
141
+ }
142
+
143
+ export const WithRichContent: Story = {
144
+ render: () => React.createElement(RichDrawer),
145
+ };
146
+
147
+ function AllSides() {
148
+ const [openSide, setOpenSide] = React.useState<DrawerSide | null>(null);
149
+ const leftRef = React.useRef<HTMLElement>(null);
150
+ const rightRef = React.useRef<HTMLElement>(null);
151
+
152
+ React.useEffect(() => {
153
+ const close = () => setOpenSide(null);
154
+ const left = leftRef.current;
155
+ const right = rightRef.current;
156
+ left?.addEventListener('bds-close', close);
157
+ right?.addEventListener('bds-close', close);
158
+ return () => {
159
+ left?.removeEventListener('bds-close', close);
160
+ right?.removeEventListener('bds-close', close);
161
+ };
162
+ }, []);
163
+
164
+ return (
165
+ <div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
166
+ {(['left', 'right'] as const).map(side =>
167
+ React.createElement(
168
+ 'button',
169
+ {
170
+ key: side,
171
+ type: 'button',
172
+ onClick: () => setOpenSide(side),
173
+ style: { padding: '0.5rem 1rem', cursor: 'pointer' },
174
+ },
175
+ `Open ${side}`,
176
+ ),
177
+ )}
178
+ {React.createElement(
179
+ 'bds-drawer',
180
+ { ref: leftRef, side: 'left', open: openSide === 'left' || undefined },
181
+ React.createElement('span', { slot: 'title' }, 'Left drawer'),
182
+ React.createElement('p', { style: { margin: 0 } }, 'This drawer slides in from the left.'),
183
+ )}
184
+ {React.createElement(
185
+ 'bds-drawer',
186
+ { ref: rightRef, side: 'right', open: openSide === 'right' || undefined },
187
+ React.createElement('span', { slot: 'title' }, 'Right drawer'),
188
+ React.createElement('p', { style: { margin: 0 } }, 'This drawer slides in from the right.'),
189
+ )}
190
+ </div>
191
+ );
192
+ }
193
+
194
+ export const AllPlacements: Story = {
195
+ render: () => React.createElement(AllSides),
196
+ };
@@ -0,0 +1,108 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './BdsDropdownMenu.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # &lt;bds-dropdown-menu&gt;
7
+
8
+ Framework-agnostic Dropdown Menu custom element. A trigger-anchored menu panel with keyboard navigation, light-dismiss, and support for disabled and destructive items.
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
+ Or in HTML without a bundler:
19
+
20
+ ```html
21
+ <script type="module">
22
+ import '@boostdev/components/web-components';
23
+ </script>
24
+ ```
25
+
26
+ ## When to use
27
+
28
+ - Contextual action menus (e.g. row actions in a table)
29
+ - Overflow menus triggered by a kebab (⋮) or ellipsis (…) button
30
+ - Any set of related actions that shouldn't always be visible
31
+
32
+ ## When not to use
33
+
34
+ - For selecting a value from a list — use `<bds-select>` or `<bds-combobox>` instead
35
+ - For navigation links — use a nav element or `<bds-popover>`
36
+
37
+ ## Examples
38
+
39
+ ### Default
40
+ <Canvas of={Stories.Default} />
41
+
42
+ ### With destructive item
43
+ <Canvas of={Stories.WithDestructiveItem} />
44
+
45
+ ### With disabled item
46
+ <Canvas of={Stories.WithDisabledItem} />
47
+
48
+ ### All placements
49
+ <Canvas of={Stories.AllPlacements} />
50
+
51
+ ## Props
52
+
53
+ <ArgTypes of={Stories} />
54
+
55
+ ## Attributes — `<bds-dropdown-menu>`
56
+
57
+ | Attribute | Type | Default | Description |
58
+ |-----------|------|---------|-------------|
59
+ | `open` | boolean | `false` | Controls open/closed state (reflected) |
60
+ | `placement` | `"bottom-start" \| "bottom-end" \| "top-start" \| "top-end"` | `"bottom-start"` | Menu position relative to the trigger |
61
+
62
+ ## Attributes — `<bds-dropdown-menu-item>`
63
+
64
+ | Attribute | Type | Default | Description |
65
+ |-----------|------|---------|-------------|
66
+ | `value` | string | `""` | Value emitted in `bds-select` event detail |
67
+ | `disabled` | boolean | `false` | Prevents interaction; item is excluded from keyboard navigation |
68
+ | `destructive` | boolean | `false` | Renders the item in a destructive (error colour) style |
69
+
70
+ ## Slots — `<bds-dropdown-menu>`
71
+
72
+ | Slot | Description |
73
+ |------|-------------|
74
+ | `trigger` | The activating element (typically a `<button>`) |
75
+ | `(default)` | `<bds-dropdown-menu-item>` elements |
76
+
77
+ ## Slots — `<bds-dropdown-menu-item>`
78
+
79
+ | Slot | Description |
80
+ |------|-------------|
81
+ | `(default)` | Item label text |
82
+
83
+ ## Events
84
+
85
+ | Event | Fired by | Bubbles | Detail | Description |
86
+ |-------|----------|---------|--------|-------------|
87
+ | `bds-open` | `<bds-dropdown-menu>` | Yes | — | Fired when the menu opens |
88
+ | `bds-close` | `<bds-dropdown-menu>` | Yes | — | Fired when the menu closes |
89
+ | `bds-select` | `<bds-dropdown-menu-item>` | Yes | `{ value: string }` | Fired when an item is clicked or activated via keyboard |
90
+
91
+ ## Usage in plain HTML
92
+
93
+ ```html
94
+ <bds-dropdown-menu placement="bottom-start">
95
+ <button slot="trigger" type="button">Options</button>
96
+ <bds-dropdown-menu-item value="edit">Edit</bds-dropdown-menu-item>
97
+ <bds-dropdown-menu-item value="duplicate">Duplicate</bds-dropdown-menu-item>
98
+ <bds-dropdown-menu-item value="delete" destructive>Delete</bds-dropdown-menu-item>
99
+ </bds-dropdown-menu>
100
+ ```
101
+
102
+ ## Accessibility
103
+
104
+ - The menu panel has `role="menu"` and `aria-hidden` toggled on open/close
105
+ - Each item renders a `<button role="menuitem">`
106
+ - Keyboard navigation: ArrowDown / ArrowUp moves between enabled items; Escape closes and returns focus to the trigger; Tab closes the menu
107
+ - Disabled items are excluded from keyboard navigation
108
+ - Focus returns to the trigger element on close
@@ -0,0 +1,91 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import type { DropdownMenuPlacement } from './bds-dropdown-menu';
4
+ import '../index'; // auto-registers all custom elements
5
+
6
+ // Thin React wrapper so Storybook controls work with the custom element's props
7
+ function BdsDropdownMenu({
8
+ placement = 'bottom-start',
9
+ triggerLabel = 'Options',
10
+ children,
11
+ }: {
12
+ placement?: DropdownMenuPlacement;
13
+ triggerLabel?: string;
14
+ children?: React.ReactNode;
15
+ }) {
16
+ return React.createElement(
17
+ 'bds-dropdown-menu',
18
+ { placement },
19
+ React.createElement('button', { slot: 'trigger', type: 'button' }, triggerLabel),
20
+ children ?? [
21
+ React.createElement('bds-dropdown-menu-item', { key: 'edit', value: 'edit' }, 'Edit'),
22
+ React.createElement('bds-dropdown-menu-item', { key: 'duplicate', value: 'duplicate' }, 'Duplicate'),
23
+ React.createElement('bds-dropdown-menu-item', { key: 'archive', value: 'archive' }, 'Archive'),
24
+ ],
25
+ );
26
+ }
27
+
28
+ const meta = {
29
+ title: 'Web Components/Interaction/DropdownMenu',
30
+ component: BdsDropdownMenu,
31
+ tags: ['!stable', 'experimental'],
32
+ parameters: { layout: 'centered' },
33
+ argTypes: {
34
+ placement: {
35
+ control: 'select',
36
+ options: ['bottom-start', 'bottom-end', 'top-start', 'top-end'],
37
+ },
38
+ triggerLabel: { control: 'text' },
39
+ },
40
+ } satisfies Meta<typeof BdsDropdownMenu>;
41
+
42
+ export default meta;
43
+ type Story = StoryObj<typeof meta>;
44
+
45
+ export const Default: Story = {
46
+ args: { placement: 'bottom-start', triggerLabel: 'Options' },
47
+ };
48
+
49
+ export const WithDestructiveItem: Story = {
50
+ render: () =>
51
+ React.createElement(
52
+ 'bds-dropdown-menu',
53
+ { placement: 'bottom-start' },
54
+ React.createElement('button', { slot: 'trigger', type: 'button' }, 'Manage'),
55
+ React.createElement('bds-dropdown-menu-item', { value: 'edit' }, 'Edit'),
56
+ React.createElement('bds-dropdown-menu-item', { value: 'duplicate' }, 'Duplicate'),
57
+ React.createElement('bds-dropdown-menu-item', { value: 'delete', destructive: '' }, 'Delete'),
58
+ ),
59
+ };
60
+
61
+ export const WithDisabledItem: Story = {
62
+ render: () =>
63
+ React.createElement(
64
+ 'bds-dropdown-menu',
65
+ { placement: 'bottom-start' },
66
+ React.createElement('button', { slot: 'trigger', type: 'button' }, 'Actions'),
67
+ React.createElement('bds-dropdown-menu-item', { value: 'view' }, 'View'),
68
+ React.createElement('bds-dropdown-menu-item', { value: 'export', disabled: '' }, 'Export (unavailable)'),
69
+ React.createElement('bds-dropdown-menu-item', { value: 'share' }, 'Share'),
70
+ ),
71
+ };
72
+
73
+ export const AllPlacements: Story = {
74
+ render: () => (
75
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, auto)', gap: '6rem', padding: '5rem' }}>
76
+ {(['bottom-start', 'bottom-end', 'top-start', 'top-end'] as const).map(p =>
77
+ React.createElement(
78
+ 'div',
79
+ { key: p, style: { display: 'flex', justifyContent: 'center' } },
80
+ React.createElement(
81
+ 'bds-dropdown-menu',
82
+ { placement: p },
83
+ React.createElement('button', { slot: 'trigger', type: 'button' }, p),
84
+ React.createElement('bds-dropdown-menu-item', { value: 'a' }, 'Item A'),
85
+ React.createElement('bds-dropdown-menu-item', { value: 'b' }, 'Item B'),
86
+ ),
87
+ ),
88
+ )}
89
+ </div>
90
+ ),
91
+ };