@boostdev/design-system-components 1.1.0 → 1.1.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 (72) hide show
  1. package/AGENTS.md +46 -5
  2. package/dist/client.cjs +206 -113
  3. package/dist/client.css +664 -624
  4. package/dist/client.d.cts +9 -3
  5. package/dist/client.d.ts +9 -3
  6. package/dist/client.js +241 -149
  7. package/dist/index.cjs +206 -113
  8. package/dist/index.css +664 -624
  9. package/dist/index.d.cts +9 -3
  10. package/dist/index.d.ts +9 -3
  11. package/dist/index.js +241 -149
  12. package/dist/web-components.d.ts +1 -0
  13. package/dist/web-components.js +10 -0
  14. package/package.json +1 -1
  15. package/src/components/interaction/Button/Button.module.css +1 -1
  16. package/src/components/interaction/Button/Button.native.mdx +72 -0
  17. package/src/components/interaction/Command/Command.module.css +7 -7
  18. package/src/components/interaction/Command/Command.spec.tsx +37 -0
  19. package/src/components/interaction/Command/Command.tsx +37 -2
  20. package/src/components/interaction/Dialog/Dialog.module.css +10 -13
  21. package/src/components/interaction/Dialog/Dialog.spec.tsx +53 -1
  22. package/src/components/interaction/Dialog/Dialog.stories.tsx +17 -0
  23. package/src/components/interaction/Dialog/Dialog.tsx +50 -16
  24. package/src/components/interaction/Drawer/Drawer.module.css +11 -11
  25. package/src/components/interaction/Drawer/Drawer.spec.tsx +48 -1
  26. package/src/components/interaction/Drawer/Drawer.tsx +38 -4
  27. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +6 -6
  28. package/src/components/interaction/Popover/Popover.module.css +74 -15
  29. package/src/components/interaction/Popover/Popover.spec.tsx +94 -59
  30. package/src/components/interaction/Popover/Popover.tsx +44 -60
  31. package/src/components/interaction/Toast/Toast.module.css +7 -7
  32. package/src/components/interaction/form/Checkbox/Checkbox.module.css +6 -6
  33. package/src/components/interaction/form/Checkbox/Checkbox.native.mdx +74 -0
  34. package/src/components/interaction/form/Combobox/Combobox.module.css +7 -8
  35. package/src/components/interaction/form/FileInput/FileInput.module.css +4 -4
  36. package/src/components/interaction/form/NumberInput/NumberInput.module.css +1 -1
  37. package/src/components/interaction/form/Radio/Radio.module.css +6 -6
  38. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +5 -6
  39. package/src/components/interaction/form/Select/Select.module.css +3 -3
  40. package/src/components/interaction/form/Slider/Slider.module.css +6 -6
  41. package/src/components/interaction/form/Switch/Switch.module.css +8 -8
  42. package/src/components/interaction/form/Textarea/Textarea.module.css +1 -1
  43. package/src/components/layout/IconWrapper/IconWrapper.module.css +2 -2
  44. package/src/components/ui/Accordion/Accordion.module.css +5 -5
  45. package/src/components/ui/Alert/Alert.native.mdx +62 -0
  46. package/src/components/ui/Avatar/Avatar.native.mdx +50 -0
  47. package/src/components/ui/Badge/Badge.native.mdx +54 -0
  48. package/src/components/ui/Calendar/Calendar.module.css +7 -7
  49. package/src/components/ui/Carousel/Carousel.module.css +5 -5
  50. package/src/components/ui/Collapsible/Collapsible.module.css +4 -4
  51. package/src/components/ui/Link/Link.module.css +2 -2
  52. package/src/components/ui/Loading/Loading.module.css +7 -7
  53. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +3 -3
  54. package/src/components/ui/Pagination/Pagination.module.css +6 -6
  55. package/src/components/ui/Progress/Progress.module.css +5 -5
  56. package/src/components/ui/Separator/Separator.module.css +3 -3
  57. package/src/components/ui/Skeleton/Skeleton.module.css +1 -1
  58. package/src/components/ui/Table/Table.module.css +9 -9
  59. package/src/components/ui/Tabs/Tabs.module.css +3 -3
  60. package/src/components/ui/Tooltip/Tooltip.module.css +8 -8
  61. package/src/components/ui/Typography/Typography.native.mdx +56 -0
  62. package/src/polyfill-invoker-commands.ts +68 -0
  63. package/src/react-augment.d.ts +19 -0
  64. package/src/stories/ReactNative.mdx +121 -0
  65. package/src/web-components/interaction/BdsButton.mdx +106 -0
  66. package/src/web-components/interaction/BdsPopover.mdx +120 -0
  67. package/src/web-components/interaction/BdsToastProvider.mdx +94 -0
  68. package/src/web-components/interaction/bds-popover.spec.ts +49 -1
  69. package/src/web-components/interaction/bds-popover.ts +12 -0
  70. package/src/web-components/test/helpers.ts +8 -0
  71. package/src/web-components/ui/BdsAlert.mdx +90 -0
  72. package/src/web-components/ui/BdsBadge.mdx +74 -0
@@ -0,0 +1,121 @@
1
+ import { Meta } from '@storybook/blocks';
2
+
3
+ <Meta title="React Native/Overview" />
4
+
5
+ # React Native
6
+
7
+ > **Status: alpha** — components and APIs may change before stable release.
8
+
9
+ `@boostdev/components` ships native implementations for 19 components. These use React Native primitives (`View`, `Text`, `Pressable`, `StyleSheet`) rather than HTML elements, and consume a dedicated token layer instead of CSS custom properties.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @boostdev/components
15
+ # or
16
+ pnpm add @boostdev/components
17
+ ```
18
+
19
+ Metro automatically resolves the native entry point via the `"react-native"` export condition in `package.json` — no extra configuration needed.
20
+
21
+ ```ts
22
+ // Import is identical to the web version:
23
+ import { Button, Alert, Badge, Avatar, Typography } from '@boostdev/components';
24
+ ```
25
+
26
+ ## Setup — ThemeProvider
27
+
28
+ Wrap your app (or the relevant subtree) in `ThemeProvider` to give native components access to the design token context:
29
+
30
+ ```tsx
31
+ import { ThemeProvider } from '@boostdev/components/native/ThemeContext';
32
+
33
+ export default function App() {
34
+ return (
35
+ <ThemeProvider>
36
+ {/* your app */}
37
+ </ThemeProvider>
38
+ );
39
+ }
40
+ ```
41
+
42
+ `ThemeProvider` reads the current colour scheme via `useColorScheme()` and applies the corresponding dark/light token set automatically.
43
+
44
+ ## Design Tokens
45
+
46
+ Native components consume tokens from `src/native/tokens.ts` — a JS object mirroring the CSS custom properties from `@boostdev/design-system-foundation`. Token categories:
47
+
48
+ | Category | Example |
49
+ |----------|---------|
50
+ | `colors` | `tokens.colors.bg`, `tokens.colors.interactive` |
51
+ | `spacing` | `tokens.spacing.xs`, `tokens.spacing.m` |
52
+ | `font` | `tokens.font.sizeBody`, `tokens.font.weightBold` |
53
+ | `border` | `tokens.border.radiusS`, `tokens.border.radiusM` |
54
+ | `zIndex` | `tokens.zIndex.modal` |
55
+ | `animation` | `tokens.animation.duration` |
56
+
57
+ Tokens are accessed inside components via `useTheme()`:
58
+
59
+ ```tsx
60
+ import { useTheme } from '@boostdev/components/native/ThemeContext';
61
+
62
+ function MyComponent() {
63
+ const { tokens } = useTheme();
64
+ return <View style={{ backgroundColor: tokens.colors.bg }} />;
65
+ }
66
+ ```
67
+
68
+ ## Available Components
69
+
70
+ ### UI
71
+
72
+ | Component | Description |
73
+ |-----------|-------------|
74
+ | `Typography` | Text with semantic variants: `h1`, `h2`, `h3`, `body`, `body_s` |
75
+ | `Badge` | Short status label with 5 semantic variants |
76
+ | `Avatar` | Initials-based avatar with 3 sizes |
77
+ | `Alert` | Inline status message with optional title and dismiss |
78
+ | `NotificationBanner` | Full-width banner notification |
79
+ | `Progress` | Linear progress bar |
80
+ | `Skeleton` | Loading placeholder |
81
+ | `Loading` | Animated loading spinner |
82
+ | `Separator` | Visual divider |
83
+
84
+ ### Layout
85
+
86
+ | Component | Description |
87
+ |-----------|-------------|
88
+ | `Card` | Elevated content container |
89
+ | `SectionHeader` | Section heading with optional action |
90
+ | `ButtonGroup` | Horizontal or vertical button row |
91
+ | `IconWrapper` | Consistent icon sizing wrapper |
92
+
93
+ ### Interaction
94
+
95
+ | Component | Description |
96
+ |-----------|-------------|
97
+ | `Button` | Primary action element with `onPress` |
98
+
99
+ ### Form
100
+
101
+ | Component | Description |
102
+ |-----------|-------------|
103
+ | `Checkbox` | Controlled checkbox with label, hint, and error |
104
+ | `Radio` | Controlled radio button |
105
+ | `Switch` | Toggle switch |
106
+ | `Label` | Form field label atom |
107
+ | `Message` | Form field hint/error atom |
108
+
109
+ ## Key differences from the web components
110
+
111
+ | Web | React Native |
112
+ |-----|-------------|
113
+ | `onClick` | `onPress` |
114
+ | `aria-label` | `accessibilityLabel` |
115
+ | CSS `className` | `StyleSheet` / `style` prop |
116
+ | `children` as string/HTML | `children` must be `<Text>` nodes (RN restriction) |
117
+ | Design tokens via CSS custom properties | Design tokens via `useTheme()` JS object |
118
+
119
+ ## Dark mode
120
+
121
+ `ThemeProvider` subscribes to `useColorScheme()` automatically. No extra work is needed — components adapt their colours when the device switches between light and dark mode.
@@ -0,0 +1,106 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './BdsButton.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # &lt;bds-button&gt;
7
+
8
+ Framework-agnostic button custom element. Renders as `<button>` or `<a>` depending on whether `href` is set. Matches the React `Button` component in variants, sizes, and theming.
9
+
10
+ > **Status: experimental** — API may change before stable release.
11
+
12
+ ## Installation
13
+
14
+ ```js
15
+ // Register all web components at once
16
+ import '@boostdev/components/web-components';
17
+ ```
18
+
19
+ Or in HTML without a bundler:
20
+
21
+ ```html
22
+ <script type="module">
23
+ import '@boostdev/components/web-components';
24
+ </script>
25
+ ```
26
+
27
+ ## Examples
28
+
29
+ ### All variants & sizes
30
+ <Canvas of={Stories.AllVariants} />
31
+
32
+ ### Default
33
+ <Canvas of={Stories.Default} />
34
+
35
+ ### Ghost
36
+ <Canvas of={Stories.Ghost} />
37
+
38
+ ### Sizes
39
+ <Canvas of={Stories.Small} />
40
+ <Canvas of={Stories.Medium} />
41
+ <Canvas of={Stories.Large} />
42
+
43
+ ### Pulse (CTA emphasis)
44
+ <Canvas of={Stories.WithPulse} />
45
+
46
+ ### Disabled
47
+ <Canvas of={Stories.Disabled} />
48
+
49
+ ## Props
50
+
51
+ <ArgTypes of={Stories} />
52
+
53
+ ## Attributes
54
+
55
+ | Attribute | Type | Default | Description |
56
+ |-----------|------|---------|-------------|
57
+ | `variant` | `"default" \| "ghost"` | `"default"` | Visual style — filled or transparent outlined |
58
+ | `size` | `"small" \| "medium" \| "large"` | `"medium"` | Height and padding scale |
59
+ | `disabled` | boolean | `false` | Prevents interaction, sets `aria-disabled` |
60
+ | `href` | string | — | Renders as `<a>` when set |
61
+ | `target` | string | — | Forwarded to `<a>` when `href` is set |
62
+ | `rel` | string | — | Forwarded to `<a>` when `href` is set |
63
+ | `type` | `"button" \| "submit" \| "reset"` | `"button"` | Native button type |
64
+ | `has-pulse` | boolean | `false` | Adds animated ring for CTA emphasis |
65
+ | `aria-label` | string | — | Accessible label (required for icon-only buttons) |
66
+
67
+ ## Slots
68
+
69
+ | Slot | Description |
70
+ |------|-------------|
71
+ | `(default)` | Button label text |
72
+ | `icon-start` | Icon placed before the label |
73
+ | `icon-end` | Icon placed after the label |
74
+
75
+ ## CSS Custom Properties
76
+
77
+ | Property | Description |
78
+ |----------|-------------|
79
+ | `--button_color` | Identity colour — fills the default variant, outlines the ghost variant |
80
+ | `--button_on-color` | Text/icon colour on the button surface. Pair with `--button_color` |
81
+
82
+ ## Invoker Commands API
83
+
84
+ `<bds-button>` can control a `<bds-popover>` declaratively using `commandfor` and `command`:
85
+
86
+ ```html
87
+ <bds-button commandfor="my-popover" command="--toggle">Open popover</bds-button>
88
+ <bds-popover id="my-popover">
89
+ <p>Popover content</p>
90
+ </bds-popover>
91
+ ```
92
+
93
+ ## Usage in plain HTML
94
+
95
+ ```html
96
+ <bds-button variant="default" size="medium">Save</bds-button>
97
+ <bds-button variant="ghost">Cancel</bds-button>
98
+ <bds-button href="/dashboard">Go to dashboard</bds-button>
99
+ ```
100
+
101
+ ## Accessibility
102
+
103
+ - Always provide a label via slot content or `aria-label` (required for icon-only buttons)
104
+ - `disabled` sets `aria-disabled="true"` and prevents click events
105
+ - When rendered as `<a>`, `href` must point to a valid destination
106
+ - `has-pulse` animation respects `prefers-reduced-motion`
@@ -0,0 +1,120 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './BdsPopover.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # &lt;bds-popover&gt;
7
+
8
+ Framework-agnostic popover custom element. A non-modal floating panel anchored to a trigger element. Uses CSS Anchor Positioning for automatic repositioning when near viewport edges.
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
+ ## Examples
19
+
20
+ ### Default (bottom)
21
+ <Canvas of={Stories.Default} />
22
+
23
+ ### Top
24
+ <Canvas of={Stories.Top} />
25
+
26
+ ### Right
27
+ <Canvas of={Stories.Right} />
28
+
29
+ ### Left
30
+ <Canvas of={Stories.Left} />
31
+
32
+ ### With rich content
33
+ <Canvas of={Stories.WithRichContent} />
34
+
35
+ ### All placements
36
+ <Canvas of={Stories.AllPlacements} />
37
+
38
+ ## Props
39
+
40
+ <ArgTypes of={Stories} />
41
+
42
+ ## Attributes
43
+
44
+ | Attribute | Type | Default | Description |
45
+ |-----------|------|---------|-------------|
46
+ | `placement` | `"bottom" \| "top" \| "left" \| "right"` | `"bottom"` | Preferred panel position relative to the trigger |
47
+ | `open` | boolean | `false` | Controls panel visibility; reflects on the element |
48
+
49
+ ## Slots
50
+
51
+ | Slot | Description |
52
+ |------|-------------|
53
+ | `trigger` | Element that opens and closes the panel on click |
54
+ | `(default)` | Panel content |
55
+
56
+ ## Events
57
+
58
+ | Event | Detail | Bubbles | Composed | Description |
59
+ |-------|--------|---------|----------|-------------|
60
+ | `bds-open` | — | Yes | Yes | Fired when the panel opens |
61
+ | `bds-close` | — | Yes | Yes | Fired when the panel closes |
62
+
63
+ ## Behaviour
64
+
65
+ - Clicking the trigger toggles the panel open/closed
66
+ - Pressing `Escape` closes an open panel
67
+ - Clicking outside the element closes the panel
68
+ - CSS Anchor Positioning automatically tries fallback placements (`@position-try`) when the preferred placement would overflow the viewport
69
+ - Each instance gets a unique CSS anchor name — multiple popovers on the same page don't interfere
70
+
71
+ ## Invoker Commands API
72
+
73
+ Control the popover declaratively from any `<button>` using `commandfor` and `command` — no JavaScript required:
74
+
75
+ ```html
76
+ <button commandfor="my-popover" command="--toggle">Open</button>
77
+ <button commandfor="my-popover" command="--open">Open only</button>
78
+ <button commandfor="my-popover" command="--close">Close only</button>
79
+
80
+ <bds-popover id="my-popover">
81
+ <p>Panel content</p>
82
+ </bds-popover>
83
+ ```
84
+
85
+ ## Usage in plain HTML
86
+
87
+ ```html
88
+ <bds-popover placement="bottom">
89
+ <button slot="trigger">Open menu</button>
90
+ <nav>
91
+ <a href="/profile">Profile</a>
92
+ <a href="/settings">Settings</a>
93
+ <a href="/logout">Log out</a>
94
+ </nav>
95
+ </bds-popover>
96
+ ```
97
+
98
+ Listen for open/close events:
99
+
100
+ ```js
101
+ const popover = document.querySelector('bds-popover');
102
+ popover.addEventListener('bds-open', () => console.log('opened'));
103
+ popover.addEventListener('bds-close', () => console.log('closed'));
104
+ ```
105
+
106
+ ## CSS Custom Properties
107
+
108
+ | Property | Default | Description |
109
+ |----------|---------|-------------|
110
+ | `--popover_color` | `--bds-color_bg` | Panel background |
111
+ | `--popover_on-color` | `--bds-color_on-bg` | Panel text colour |
112
+ | `--popover_shadow` | `--bds-shadow_m` | Panel drop shadow |
113
+ | `--popover_z-index` | `100` | Stacking order |
114
+
115
+ ## Accessibility
116
+
117
+ - The panel has `role="region"` and `aria-label="Popover"`
118
+ - The trigger element retains its own role (e.g. `<button>`) — no ARIA is added by the component
119
+ - For menus, use a `<nav>` or `role="menu"` inside the default slot as appropriate
120
+ - The component does not trap focus — use `<bds-dialog>` for modal, focus-trapped overlays
@@ -0,0 +1,94 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './BdsToastProvider.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # &lt;bds-toast-provider&gt;
7
+
8
+ Framework-agnostic toast notification provider. Wrap your page content in `<bds-toast-provider>` to enable toast notifications that render in the browser top layer (above fixed/sticky elements). Auto-dismisses after 5 seconds.
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
+ ## Examples
19
+
20
+ ### Static API
21
+ <Canvas of={Stories.Default} />
22
+
23
+ ### Event API (no import required)
24
+ <Canvas of={Stories.EventAPI} />
25
+
26
+ ## Slots
27
+
28
+ | Slot | Description |
29
+ |------|-------------|
30
+ | `(default)` | Page content. `<bds-toast-provider>` renders as `display: contents` so it is invisible in the layout |
31
+
32
+ ## Three ways to show a toast
33
+
34
+ ### 1. Static method (import required)
35
+
36
+ ```js
37
+ import { BdsToastProvider } from '@boostdev/components/web-components';
38
+
39
+ BdsToastProvider.show('File saved', 'success');
40
+ BdsToastProvider.show('Something went wrong', 'error');
41
+ ```
42
+
43
+ ### 2. Instance method
44
+
45
+ ```js
46
+ const provider = document.querySelector('bds-toast-provider');
47
+ provider.showToast('Settings updated', 'info');
48
+ ```
49
+
50
+ ### 3. Document event (no import required)
51
+
52
+ Useful in frameworks or contexts where importing the class is inconvenient:
53
+
54
+ ```js
55
+ document.dispatchEvent(new CustomEvent('bds-show-toast', {
56
+ detail: { message: 'Copied to clipboard', variant: 'success' },
57
+ }));
58
+ ```
59
+
60
+ ## Toast variants
61
+
62
+ | Variant | Use for |
63
+ |---------|---------|
64
+ | `info` | Neutral status updates |
65
+ | `success` | Completed actions |
66
+ | `warning` | Potential issues the user should know about |
67
+ | `error` | Failures requiring attention |
68
+
69
+ ## Setup in plain HTML
70
+
71
+ ```html
72
+ <bds-toast-provider>
73
+ <!-- all your page content goes here -->
74
+ <main>…</main>
75
+ </bds-toast-provider>
76
+
77
+ <script type="module">
78
+ import '@boostdev/components/web-components';
79
+ // or use the document event API — no import needed
80
+ </script>
81
+ ```
82
+
83
+ ## Behaviour
84
+
85
+ - Toasts render in the browser top layer via the Popover API (`popover="manual"`) — they appear above `position: fixed` elements and `z-index` stacking
86
+ - Each toast auto-dismisses after **5000 ms**
87
+ - Multiple toasts stack vertically and each has its own dismissal timer
88
+ - A visible dismiss (×) button is always present on each toast
89
+
90
+ ## Accessibility
91
+
92
+ - Each toast renders with `role="status"` + `aria-live="polite"` by default so screen readers announce it without interrupting the user
93
+ - `error` toasts use `role="alert"` + `aria-live="assertive"` for immediate announcement
94
+ - The dismiss button has an accessible label
@@ -1,4 +1,4 @@
1
- import { fixture, cleanup } from '../test/helpers';
1
+ import { fixture, cleanup, makeCommandEvent } from '../test/helpers';
2
2
  import { BdsPopover } from './bds-popover';
3
3
 
4
4
  describe('bds-popover', () => {
@@ -114,6 +114,54 @@ describe('bds-popover', () => {
114
114
  cleanup(el);
115
115
  });
116
116
 
117
+ describe('Invoker Commands API (command event on host)', () => {
118
+ it('opens on --open command', async () => {
119
+ const el = await fixture('<bds-popover id="p1"><button slot="trigger">T</button></bds-popover>') as BdsPopover;
120
+ el.dispatchEvent(makeCommandEvent('--open'));
121
+ await el.updateComplete;
122
+ expect(el.open).toBe(true);
123
+ cleanup(el);
124
+ });
125
+
126
+ it('closes on --close command', async () => {
127
+ const el = await fixture('<bds-popover id="p2" open><button slot="trigger">T</button></bds-popover>') as BdsPopover;
128
+ await el.updateComplete;
129
+ el.dispatchEvent(makeCommandEvent('--close'));
130
+ await el.updateComplete;
131
+ expect(el.open).toBe(false);
132
+ cleanup(el);
133
+ });
134
+
135
+ it('toggles on --toggle command', async () => {
136
+ const el = await fixture('<bds-popover id="p3"><button slot="trigger">T</button></bds-popover>') as BdsPopover;
137
+ el.dispatchEvent(makeCommandEvent('--toggle'));
138
+ await el.updateComplete;
139
+ expect(el.open).toBe(true);
140
+ el.dispatchEvent(makeCommandEvent('--toggle'));
141
+ await el.updateComplete;
142
+ expect(el.open).toBe(false);
143
+ cleanup(el);
144
+ });
145
+
146
+ it('fires bds-open event on --open command', async () => {
147
+ const el = await fixture('<bds-popover id="p4"><button slot="trigger">T</button></bds-popover>') as BdsPopover;
148
+ const handler = vi.fn();
149
+ el.addEventListener('bds-open', handler);
150
+ el.dispatchEvent(makeCommandEvent('--open'));
151
+ await el.updateComplete;
152
+ expect(handler).toHaveBeenCalledOnce();
153
+ cleanup(el);
154
+ });
155
+
156
+ it('ignores unknown commands', async () => {
157
+ const el = await fixture('<bds-popover id="p5"><button slot="trigger">T</button></bds-popover>') as BdsPopover;
158
+ el.dispatchEvent(makeCommandEvent('--unknown'));
159
+ await el.updateComplete;
160
+ expect(el.open).toBe(false);
161
+ cleanup(el);
162
+ });
163
+ });
164
+
117
165
  it('two instances get different anchor names', async () => {
118
166
  const a = await fixture('<bds-popover><button slot="trigger">A</button></bds-popover>');
119
167
  const b = await fixture('<bds-popover><button slot="trigger">B</button></bds-popover>');
@@ -157,18 +157,28 @@ export class BdsPopover extends LitElement {
157
157
  this.style.setProperty('--bds-popover-anchor', this._anchorName);
158
158
  document.addEventListener('keydown', this._onKeydown);
159
159
  document.addEventListener('mousedown', this._onOutsideClick);
160
+ // Invoker Commands API — host is in light DOM so commandfor can reference its id.
161
+ this.addEventListener('command', this._onCommand);
160
162
  }
161
163
 
162
164
  disconnectedCallback() {
163
165
  super.disconnectedCallback();
164
166
  document.removeEventListener('keydown', this._onKeydown);
165
167
  document.removeEventListener('mousedown', this._onOutsideClick);
168
+ this.removeEventListener('command', this._onCommand);
166
169
  }
167
170
 
168
171
  private _onKeydown = (e: KeyboardEvent) => {
169
172
  if (this.open && e.key === 'Escape') this._close();
170
173
  };
171
174
 
175
+ private _onCommand = (e: Event) => {
176
+ const command = (e as CommandEvent).command;
177
+ if (command === '--toggle') this._toggle();
178
+ else if (command === '--open') this._open();
179
+ else if (command === '--close') this._close();
180
+ };
181
+
172
182
  private _onOutsideClick = (e: MouseEvent) => {
173
183
  if (this.open && !this.contains(e.target as Node)) this._close();
174
184
  };
@@ -182,11 +192,13 @@ export class BdsPopover extends LitElement {
182
192
  }
183
193
 
184
194
  private _open() {
195
+ if (this.open) return;
185
196
  this.open = true;
186
197
  this.dispatchEvent(new CustomEvent('bds-open', { bubbles: true, composed: true }));
187
198
  }
188
199
 
189
200
  private _close() {
201
+ if (!this.open) return;
190
202
  this.open = false;
191
203
  this.dispatchEvent(new CustomEvent('bds-close', { bubbles: true, composed: true }));
192
204
  }
@@ -12,3 +12,11 @@ export async function fixture(html: string): Promise<HTMLElement> {
12
12
  export function cleanup(el: HTMLElement) {
13
13
  el.remove();
14
14
  }
15
+
16
+ /** Create a synthetic Invoker Commands API `command` event for testing. */
17
+ export function makeCommandEvent(command: string): Event {
18
+ const event = new Event('command', { bubbles: false, cancelable: true });
19
+ Object.defineProperty(event, 'command', { value: command, configurable: true });
20
+ Object.defineProperty(event, 'source', { value: null, configurable: true });
21
+ return event;
22
+ }
@@ -0,0 +1,90 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as Stories from './BdsAlert.stories';
3
+
4
+ <Meta of={Stories} />
5
+
6
+ # &lt;bds-alert&gt;
7
+
8
+ Framework-agnostic alert custom element for inline status messages. Supports four semantic variants, an optional title, and a dismissible mode.
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
+ ## Examples
19
+
20
+ ### All variants
21
+ <Canvas of={Stories.AllVariants} />
22
+
23
+ ### Info
24
+ <Canvas of={Stories.Info} />
25
+
26
+ ### Success
27
+ <Canvas of={Stories.Success} />
28
+
29
+ ### Warning
30
+ <Canvas of={Stories.Warning} />
31
+
32
+ ### Error
33
+ <Canvas of={Stories.Error} />
34
+
35
+ ### With title
36
+ <Canvas of={Stories.WithTitle} />
37
+
38
+ ### Dismissible
39
+ <Canvas of={Stories.Dismissible} />
40
+
41
+ ## Props
42
+
43
+ <ArgTypes of={Stories} />
44
+
45
+ ## Attributes
46
+
47
+ | Attribute | Type | Default | Description |
48
+ |-----------|------|---------|-------------|
49
+ | `variant` | `"info" \| "success" \| "warning" \| "error"` | `"info"` | Semantic colour and icon |
50
+ | `title` | string | — | Optional heading displayed above body content |
51
+ | `dismissible` | boolean | `false` | Shows a dismiss (×) button |
52
+
53
+ ## Slots
54
+
55
+ | Slot | Description |
56
+ |------|-------------|
57
+ | `(default)` | Alert body text or content |
58
+ | `icon` | Custom icon, placed left of the content area |
59
+
60
+ ## Events
61
+
62
+ | Event | Detail | Bubbles | Composed | Description |
63
+ |-------|--------|---------|----------|-------------|
64
+ | `bds-dismiss` | — | Yes | Yes | Fired when the dismiss button is clicked |
65
+
66
+ ## Usage in plain HTML
67
+
68
+ ```html
69
+ <bds-alert variant="success" title="Saved">
70
+ Your changes have been saved successfully.
71
+ </bds-alert>
72
+
73
+ <bds-alert variant="error" dismissible>
74
+ Something went wrong. Please try again.
75
+ </bds-alert>
76
+ ```
77
+
78
+ Listen for dismiss:
79
+
80
+ ```js
81
+ document.querySelector('bds-alert').addEventListener('bds-dismiss', () => {
82
+ // remove or hide the element
83
+ });
84
+ ```
85
+
86
+ ## Accessibility
87
+
88
+ - `error` and `warning` use `role="alert"` + `aria-live="assertive"` — content is announced immediately
89
+ - `info` and `success` use `role="status"` + `aria-live="polite"` — content is announced at the next opportunity
90
+ - The dismiss button has an accessible label; do not rely solely on its icon