@boostdev/design-system-components 1.2.0 → 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 (126) hide show
  1. package/AGENTS.md +25 -4
  2. package/dist/client.cjs +58 -54
  3. package/dist/client.css +503 -525
  4. package/dist/client.js +58 -54
  5. package/dist/index.cjs +58 -54
  6. package/dist/index.css +503 -525
  7. package/dist/index.js +58 -54
  8. package/dist/web-components/{chunk-6MH5UWUD.js → chunk-2FGATTGT.js} +5 -0
  9. package/dist/web-components/{chunk-5IPHEONG.js → chunk-6I2DBFQ7.js} +1 -0
  10. package/dist/web-components/{chunk-DI46Q2EA.js → chunk-OCODKRVZ.js} +57 -82
  11. package/dist/web-components/index.d.ts +571 -1
  12. package/dist/web-components/index.js +1501 -4
  13. package/dist/web-components/interaction/bds-collapsible.js +1 -1
  14. package/dist/web-components/interaction/bds-drawer.js +1 -1
  15. package/dist/web-components/interaction/form/bds-segmented-control.js +1 -1
  16. package/package.json +1 -1
  17. package/src/components/interaction/Drawer/Drawer.module.css +37 -62
  18. package/src/components/interaction/Drawer/Drawer.tsx +21 -17
  19. package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +8 -4
  20. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +13 -6
  21. package/src/stories/Introduction.mdx +2 -1
  22. package/src/web-components/index.ts +12 -0
  23. package/src/web-components/interaction/BdsAccordion.mdx +80 -28
  24. package/src/web-components/interaction/BdsAccordion.stories.tsx +67 -58
  25. package/src/web-components/interaction/BdsCollapsible.mdx +93 -23
  26. package/src/web-components/interaction/BdsCollapsible.stories.tsx +29 -48
  27. package/src/web-components/interaction/BdsDialog.mdx +88 -27
  28. package/src/web-components/interaction/BdsDialog.stories.tsx +129 -47
  29. package/src/web-components/interaction/BdsDrawer.mdx +85 -27
  30. package/src/web-components/interaction/BdsDrawer.stories.tsx +161 -31
  31. package/src/web-components/interaction/BdsDropdownMenu.mdx +108 -0
  32. package/src/web-components/interaction/BdsDropdownMenu.stories.tsx +91 -0
  33. package/src/web-components/interaction/BdsSkipLink.mdx +72 -16
  34. package/src/web-components/interaction/BdsSkipLink.stories.tsx +47 -34
  35. package/src/web-components/interaction/BdsTabs.mdx +97 -27
  36. package/src/web-components/interaction/BdsTabs.stories.tsx +69 -55
  37. package/src/web-components/interaction/BdsTooltip.mdx +84 -18
  38. package/src/web-components/interaction/BdsTooltip.stories.tsx +49 -30
  39. package/src/web-components/interaction/bds-collapsible.ts +1 -0
  40. package/src/web-components/interaction/bds-drawer.ts +59 -82
  41. package/src/web-components/interaction/bds-dropdown-menu-item.ts +124 -0
  42. package/src/web-components/interaction/bds-dropdown-menu.spec.ts +102 -0
  43. package/src/web-components/interaction/bds-dropdown-menu.ts +200 -0
  44. package/src/web-components/interaction/form/BdsCheckbox.mdx +57 -20
  45. package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +35 -36
  46. package/src/web-components/interaction/form/BdsCheckboxGroup.mdx +70 -0
  47. package/src/web-components/interaction/form/BdsCheckboxGroup.stories.tsx +81 -0
  48. package/src/web-components/interaction/form/BdsCombobox.mdx +52 -33
  49. package/src/web-components/interaction/form/BdsCombobox.stories.tsx +55 -53
  50. package/src/web-components/interaction/form/BdsFileInput.mdx +59 -24
  51. package/src/web-components/interaction/form/BdsFileInput.stories.tsx +38 -53
  52. package/src/web-components/interaction/form/BdsFormInput.mdx +98 -0
  53. package/src/web-components/interaction/form/BdsFormInput.stories.tsx +139 -0
  54. package/src/web-components/interaction/form/BdsNumberInput.mdx +51 -22
  55. package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +24 -46
  56. package/src/web-components/interaction/form/BdsRadio.mdx +53 -22
  57. package/src/web-components/interaction/form/BdsRadio.stories.tsx +19 -51
  58. package/src/web-components/interaction/form/BdsRadioGroup.mdx +73 -0
  59. package/src/web-components/interaction/form/BdsRadioGroup.stories.tsx +88 -0
  60. package/src/web-components/interaction/form/BdsSegmentedControl.mdx +76 -23
  61. package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +64 -54
  62. package/src/web-components/interaction/form/BdsSelect.mdx +59 -21
  63. package/src/web-components/interaction/form/BdsSelect.stories.tsx +47 -56
  64. package/src/web-components/interaction/form/BdsSlider.mdx +55 -27
  65. package/src/web-components/interaction/form/BdsSlider.stories.tsx +23 -44
  66. package/src/web-components/interaction/form/BdsSwitch.mdx +53 -20
  67. package/src/web-components/interaction/form/BdsSwitch.stories.tsx +22 -37
  68. package/src/web-components/interaction/form/BdsTextarea.mdx +54 -22
  69. package/src/web-components/interaction/form/BdsTextarea.stories.tsx +35 -44
  70. package/src/web-components/interaction/form/bds-checkbox-group.spec.ts +55 -0
  71. package/src/web-components/interaction/form/bds-checkbox-group.ts +117 -0
  72. package/src/web-components/interaction/form/bds-form-input.spec.ts +83 -0
  73. package/src/web-components/interaction/form/bds-form-input.ts +268 -0
  74. package/src/web-components/interaction/form/bds-radio-group.spec.ts +62 -0
  75. package/src/web-components/interaction/form/bds-radio-group.ts +142 -0
  76. package/src/web-components/interaction/form/bds-segmented-control.ts +5 -0
  77. package/src/web-components/ui/BdsAvatar.mdx +29 -20
  78. package/src/web-components/ui/BdsAvatar.stories.tsx +14 -28
  79. package/src/web-components/ui/BdsBreadcrumb.mdx +22 -19
  80. package/src/web-components/ui/BdsBreadcrumb.stories.tsx +11 -21
  81. package/src/web-components/ui/BdsButtonGroup.mdx +69 -0
  82. package/src/web-components/ui/BdsButtonGroup.stories.tsx +66 -0
  83. package/src/web-components/ui/BdsCalendar.mdx +93 -0
  84. package/src/web-components/ui/BdsCalendar.stories.tsx +63 -0
  85. package/src/web-components/ui/BdsCard.mdx +30 -25
  86. package/src/web-components/ui/BdsCard.stories.tsx +19 -42
  87. package/src/web-components/ui/BdsCarousel.mdx +83 -0
  88. package/src/web-components/ui/BdsCarousel.stories.tsx +96 -0
  89. package/src/web-components/ui/BdsDescriptionList.mdx +13 -16
  90. package/src/web-components/ui/BdsDescriptionList.stories.tsx +34 -49
  91. package/src/web-components/ui/BdsIconWrapper.mdx +20 -20
  92. package/src/web-components/ui/BdsIconWrapper.stories.tsx +46 -41
  93. package/src/web-components/ui/BdsLink.mdx +21 -17
  94. package/src/web-components/ui/BdsLink.stories.tsx +19 -20
  95. package/src/web-components/ui/BdsLoading.mdx +9 -13
  96. package/src/web-components/ui/BdsLoading.stories.tsx +8 -10
  97. package/src/web-components/ui/BdsNotificationBanner.mdx +32 -37
  98. package/src/web-components/ui/BdsNotificationBanner.stories.tsx +38 -47
  99. package/src/web-components/ui/BdsPagination.mdx +74 -0
  100. package/src/web-components/ui/BdsPagination.stories.tsx +36 -0
  101. package/src/web-components/ui/BdsProgress.mdx +24 -17
  102. package/src/web-components/ui/BdsProgress.stories.tsx +55 -29
  103. package/src/web-components/ui/BdsProgressCircle.mdx +24 -18
  104. package/src/web-components/ui/BdsProgressCircle.stories.tsx +17 -29
  105. package/src/web-components/ui/BdsRating.mdx +22 -22
  106. package/src/web-components/ui/BdsRating.stories.tsx +19 -13
  107. package/src/web-components/ui/BdsSectionHeader.mdx +31 -28
  108. package/src/web-components/ui/BdsSectionHeader.stories.tsx +21 -23
  109. package/src/web-components/ui/BdsSeparator.mdx +22 -8
  110. package/src/web-components/ui/BdsSeparator.stories.tsx +27 -19
  111. package/src/web-components/ui/BdsSkeleton.mdx +20 -26
  112. package/src/web-components/ui/BdsSkeleton.stories.tsx +23 -23
  113. package/src/web-components/ui/BdsTable.mdx +81 -0
  114. package/src/web-components/ui/BdsTable.stories.tsx +83 -0
  115. package/src/web-components/ui/BdsTypography.mdx +20 -29
  116. package/src/web-components/ui/BdsTypography.stories.tsx +19 -18
  117. package/src/web-components/ui/bds-button-group.spec.ts +40 -0
  118. package/src/web-components/ui/bds-button-group.ts +78 -0
  119. package/src/web-components/ui/bds-calendar.spec.ts +91 -0
  120. package/src/web-components/ui/bds-calendar.ts +427 -0
  121. package/src/web-components/ui/bds-carousel.spec.ts +64 -0
  122. package/src/web-components/ui/bds-carousel.ts +296 -0
  123. package/src/web-components/ui/bds-pagination.spec.ts +67 -0
  124. package/src/web-components/ui/bds-pagination.ts +197 -0
  125. package/src/web-components/ui/bds-table.spec.ts +45 -0
  126. package/src/web-components/ui/bds-table.ts +96 -0
@@ -0,0 +1,268 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ /**
4
+ * `<bds-form-input>` — form-associated text input with label, hint, and error support.
5
+ *
6
+ * Participates in HTML forms via the Form-Associated Custom Elements API.
7
+ *
8
+ * Attributes:
9
+ * name — form field name
10
+ * type — input type: text | email | password | search | url | tel (default: "text")
11
+ * value — current value
12
+ * placeholder — placeholder text
13
+ * label — visible label text
14
+ * hint — helper text shown below the input
15
+ * error — error message; when set the input enters error state
16
+ * disabled — disables the input
17
+ * required — marks the field as required
18
+ *
19
+ * Slots:
20
+ * prefix — icon or text rendered before the input
21
+ * suffix — icon or text rendered after the input
22
+ *
23
+ * Events:
24
+ * bds-change — fired on value change; `detail: { value: string }`
25
+ * bds-input — fired on every keystroke; `detail: { value: string }`
26
+ *
27
+ * @example
28
+ * <bds-form-input name="email" type="email" label="Email address" required></bds-form-input>
29
+ */
30
+ export class BdsFormInput extends LitElement {
31
+ static formAssociated = true;
32
+
33
+ static styles = css`
34
+ :host {
35
+ display: block;
36
+ }
37
+
38
+ :host([disabled]) {
39
+ opacity: 0.5;
40
+ pointer-events: none;
41
+ }
42
+
43
+ .label {
44
+ display: block;
45
+ font-size: var(--bds-font_size--body--s);
46
+ font-family: var(--bds-font_family--body);
47
+ font-weight: var(--bds-font_weight--semibold);
48
+ color: var(--bds-color_on-bg);
49
+ margin-block-end: var(--bds-space_xxs);
50
+ }
51
+
52
+ .required {
53
+ color: var(--bds-color_error);
54
+ margin-inline-start: var(--bds-space_xxxs);
55
+ }
56
+
57
+ .wrapper {
58
+ position: relative;
59
+ display: flex;
60
+ align-items: center;
61
+ --bdc_color: currentcolor;
62
+ --bdc-outline_radius: var(--bds-border_radius--xs);
63
+
64
+ border-radius: var(--formInput_radius, var(--bdc-outline_radius));
65
+ box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--formInput_shadow, var(--bdc-outline_shadow));
66
+ background-color: var(--bds-color_bg);
67
+ transition: --bdc_color var(--bds-animation_transition-duration) var(--bds-animation_easing);
68
+ }
69
+
70
+ :host([error]) .wrapper {
71
+ --bdc_color: var(--bds-color_error);
72
+ }
73
+
74
+ .wrapper:focus-within {
75
+ --bdc_color: var(--bdc_color--focus);
76
+ }
77
+
78
+ :host([error]) .wrapper:focus-within {
79
+ --bdc_color: var(--bds-color_error);
80
+ }
81
+
82
+ .input {
83
+ flex: 1;
84
+ min-inline-size: 0;
85
+ border: none;
86
+ background: transparent;
87
+ padding: var(--bds-space_s);
88
+ font-size: var(--bds-font_size--body);
89
+ font-family: var(--bds-font_family--body);
90
+ color: var(--bds-color_on-bg);
91
+ outline: none;
92
+ }
93
+
94
+ .input::placeholder {
95
+ color: var(--bds-color_on-bg--subtle);
96
+ }
97
+
98
+ .input:focus-visible {
99
+ outline: none;
100
+ }
101
+
102
+ .wrapper:focus-within {
103
+ outline: var(--bds-outline_default);
104
+ outline-offset: var(--bds-outline_offset);
105
+ border-radius: var(--formInput_radius, var(--bdc-outline_radius));
106
+ }
107
+
108
+ .prefix,
109
+ .suffix {
110
+ display: flex;
111
+ align-items: center;
112
+ color: var(--bds-color_on-bg--subtle);
113
+ flex-shrink: 0;
114
+ }
115
+
116
+ .prefix {
117
+ padding-inline-start: var(--bds-space_s);
118
+ }
119
+
120
+ .suffix {
121
+ padding-inline-end: var(--bds-space_s);
122
+ }
123
+
124
+ .prefix:empty,
125
+ .suffix:empty {
126
+ display: none;
127
+ }
128
+
129
+ .message {
130
+ margin-block-start: var(--bds-space_xxs);
131
+ font-size: var(--bds-font_size--body--s);
132
+ font-family: var(--bds-font_family--body);
133
+ }
134
+
135
+ .message--error {
136
+ color: var(--bds-color_error);
137
+ }
138
+
139
+ .message--hint {
140
+ color: var(--bds-color_on-bg--subtle);
141
+ }
142
+ `;
143
+
144
+ static properties = {
145
+ name: { type: String, reflect: true },
146
+ type: { type: String, reflect: true },
147
+ value: { type: String },
148
+ placeholder: { type: String },
149
+ label: { type: String },
150
+ hint: { type: String },
151
+ error: { type: String, reflect: true },
152
+ disabled: { type: Boolean, reflect: true },
153
+ required: { type: Boolean, reflect: true },
154
+ };
155
+
156
+ declare name: string;
157
+ declare type: string;
158
+ declare value: string;
159
+ declare placeholder: string;
160
+ declare label: string;
161
+ declare hint: string | undefined;
162
+ declare error: string | undefined;
163
+ declare disabled: boolean;
164
+ declare required: boolean;
165
+
166
+ private _internals!: ElementInternals;
167
+ private readonly _uid = Math.random().toString(36).slice(2, 8);
168
+
169
+ constructor() {
170
+ super();
171
+ this._internals = this.attachInternals();
172
+ this.name = '';
173
+ this.type = 'text';
174
+ this.value = '';
175
+ this.placeholder = '';
176
+ this.label = '';
177
+ this.disabled = false;
178
+ this.required = false;
179
+ }
180
+
181
+ updated(changed: Map<string, unknown>) {
182
+ super.updated(changed);
183
+ if (changed.has('value')) {
184
+ this._internals.setFormValue?.(this.value);
185
+ }
186
+ if (changed.has('required') || changed.has('value')) {
187
+ if (this.required && !this.value) {
188
+ this._internals.setValidity?.({ valueMissing: true }, 'Required');
189
+ } else {
190
+ this._internals.setValidity?.({});
191
+ }
192
+ }
193
+ }
194
+
195
+ formResetCallback() {
196
+ this.value = '';
197
+ this._internals.setFormValue?.('');
198
+ }
199
+
200
+ private _handleInput(e: Event) {
201
+ const input = e.target as HTMLInputElement;
202
+ this.value = input.value;
203
+ this.dispatchEvent(new CustomEvent('bds-input', {
204
+ detail: { value: this.value },
205
+ bubbles: true,
206
+ composed: true,
207
+ }));
208
+ }
209
+
210
+ private _handleChange(e: Event) {
211
+ const input = e.target as HTMLInputElement;
212
+ this.value = input.value;
213
+ this.dispatchEvent(new CustomEvent('bds-change', {
214
+ detail: { value: this.value },
215
+ bubbles: true,
216
+ composed: true,
217
+ }));
218
+ }
219
+
220
+ render() {
221
+ const id = `bds-form-input-${this._uid}`;
222
+ const hintId = `${id}-hint`;
223
+ const errorId = `${id}-error`;
224
+ const describedBy = [this.error && errorId, this.hint && hintId].filter(Boolean).join(' ') || undefined;
225
+
226
+ return html`
227
+ ${this.label
228
+ ? html`
229
+ <label class="label" for=${id}>
230
+ ${this.label}
231
+ ${this.required ? html`<span class="required" aria-hidden="true">*</span>` : ''}
232
+ </label>
233
+ `
234
+ : ''}
235
+ <div class="wrapper">
236
+ <span class="prefix"><slot name="prefix"></slot></span>
237
+ <input
238
+ id=${id}
239
+ class="input"
240
+ type=${this.type}
241
+ .value=${this.value}
242
+ placeholder=${this.placeholder}
243
+ ?disabled=${this.disabled}
244
+ ?required=${this.required}
245
+ aria-invalid=${this.error ? 'true' : 'false'}
246
+ aria-describedby=${describedBy ?? ''}
247
+ aria-required=${this.required ? 'true' : 'false'}
248
+ @input=${this._handleInput}
249
+ @change=${this._handleChange}
250
+ />
251
+ <span class="suffix"><slot name="suffix"></slot></span>
252
+ </div>
253
+ ${this.error
254
+ ? html`<p id=${errorId} class="message message--error" role="alert">${this.error}</p>`
255
+ : this.hint
256
+ ? html`<p id=${hintId} class="message message--hint">${this.hint}</p>`
257
+ : ''}
258
+ `;
259
+ }
260
+ }
261
+
262
+ customElements.define('bds-form-input', BdsFormInput);
263
+
264
+ declare global {
265
+ interface HTMLElementTagNameMap {
266
+ 'bds-form-input': BdsFormInput;
267
+ }
268
+ }
@@ -0,0 +1,62 @@
1
+ import { fixture, cleanup } from '../../test/helpers';
2
+ import { BdsRadioGroup } from './bds-radio-group';
3
+
4
+ describe('bds-radio-group', () => {
5
+ it('is registered as a custom element', () => {
6
+ expect(customElements.get('bds-radio-group')).toBe(BdsRadioGroup);
7
+ });
8
+
9
+ it('renders a fieldset', async () => {
10
+ const el = await fixture('<bds-radio-group label="Options"></bds-radio-group>');
11
+ expect(el.shadowRoot!.querySelector('fieldset')).not.toBeNull();
12
+ cleanup(el);
13
+ });
14
+
15
+ it('renders the label as a legend', async () => {
16
+ const el = await fixture('<bds-radio-group label="Select one"></bds-radio-group>');
17
+ const legend = el.shadowRoot!.querySelector('legend');
18
+ expect(legend?.textContent?.trim()).toContain('Select one');
19
+ cleanup(el);
20
+ });
21
+
22
+ it('disables the fieldset when disabled', async () => {
23
+ const el = await fixture('<bds-radio-group label="Test" disabled></bds-radio-group>');
24
+ const fieldset = el.shadowRoot!.querySelector('fieldset')!;
25
+ expect(fieldset.disabled).toBe(true);
26
+ cleanup(el);
27
+ });
28
+
29
+ it('renders a slot for radio children', async () => {
30
+ const el = await fixture('<bds-radio-group label="Test"></bds-radio-group>');
31
+ expect(el.shadowRoot!.querySelector('slot')).not.toBeNull();
32
+ cleanup(el);
33
+ });
34
+
35
+ it('renders error message when error attribute is set', async () => {
36
+ const el = await fixture('<bds-radio-group label="Test" error="Required field"></bds-radio-group>');
37
+ const message = el.shadowRoot!.querySelector('.message--error');
38
+ expect(message?.textContent?.trim()).toBe('Required field');
39
+ cleanup(el);
40
+ });
41
+
42
+ it('reflects name attribute', async () => {
43
+ const el = await fixture('<bds-radio-group label="Test" name="contact"></bds-radio-group>') as BdsRadioGroup;
44
+ expect(el.name).toBe('contact');
45
+ cleanup(el);
46
+ });
47
+
48
+ it('propagates name to slotted bds-radio elements', async () => {
49
+ // Define a minimal bds-radio stub so querySelectorAll finds it
50
+ if (!customElements.get('bds-radio-stub')) {
51
+ customElements.define('bds-radio-stub', class extends HTMLElement {});
52
+ }
53
+ const el = await fixture(
54
+ '<bds-radio-group label="Contact" name="contact"><bds-radio value="email">Email</bds-radio></bds-radio-group>',
55
+ ) as BdsRadioGroup;
56
+ // Wait for propagation promise
57
+ await Promise.resolve();
58
+ const radio = el.querySelector('bds-radio');
59
+ expect(radio?.getAttribute('name')).toBe('contact');
60
+ cleanup(el);
61
+ });
62
+ });
@@ -0,0 +1,142 @@
1
+ import { LitElement, css, html } from 'lit';
2
+
3
+ /**
4
+ * `<bds-radio-group>` — groups related radio buttons with a shared label and name.
5
+ *
6
+ * Renders a `<fieldset>` with a `<legend>`. Propagates the `name` attribute to
7
+ * all slotted `<bds-radio>` elements so radios form a mutually-exclusive set.
8
+ *
9
+ * Attributes:
10
+ * label — visible group label (rendered as `<legend>`)
11
+ * name — shared name forwarded to all slotted `<bds-radio>` elements
12
+ * error — error message shown below items
13
+ * hint — hint text shown below items
14
+ * disabled — disables the entire fieldset
15
+ * required — marks the group as required
16
+ *
17
+ * Slots:
18
+ * (default) — `<bds-radio>` elements
19
+ *
20
+ * @example
21
+ * <bds-radio-group label="Preferred contact" name="contact">
22
+ * <bds-radio value="email">Email</bds-radio>
23
+ * <bds-radio value="phone">Phone</bds-radio>
24
+ * </bds-radio-group>
25
+ */
26
+ export class BdsRadioGroup extends LitElement {
27
+ static styles = css`
28
+ :host {
29
+ display: block;
30
+ }
31
+
32
+ fieldset {
33
+ border: none;
34
+ margin: 0;
35
+ padding: 0;
36
+ }
37
+
38
+ .legend {
39
+ font-size: var(--bds-font_size--body--s);
40
+ font-family: var(--bds-font_family--body);
41
+ font-weight: var(--bds-font_weight--semibold);
42
+ color: var(--bds-color_on-bg);
43
+ margin-block-end: var(--bds-space_xs);
44
+ }
45
+
46
+ .required {
47
+ color: var(--bds-color_error);
48
+ margin-inline-start: var(--bds-space_xxxs);
49
+ }
50
+
51
+ .items {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: var(--bds-space_xs);
55
+ }
56
+
57
+ .message {
58
+ margin-block-start: var(--bds-space_xs);
59
+ font-size: var(--bds-font_size--body--s);
60
+ font-family: var(--bds-font_family--body);
61
+ }
62
+
63
+ .message--error {
64
+ color: var(--bds-color_error);
65
+ }
66
+
67
+ .message--hint {
68
+ color: var(--bds-color_on-bg--subtle);
69
+ }
70
+ `;
71
+
72
+ static properties = {
73
+ label: { type: String },
74
+ name: { type: String, reflect: true },
75
+ error: { type: String },
76
+ hint: { type: String },
77
+ disabled: { type: Boolean, reflect: true },
78
+ required: { type: Boolean, reflect: true },
79
+ };
80
+
81
+ declare label: string;
82
+ declare name: string;
83
+ declare error: string | undefined;
84
+ declare hint: string | undefined;
85
+ declare disabled: boolean;
86
+ declare required: boolean;
87
+
88
+ constructor() {
89
+ super();
90
+ this.label = '';
91
+ this.name = '';
92
+ this.disabled = false;
93
+ this.required = false;
94
+ }
95
+
96
+ private _propagateName = () => {
97
+ if (!this.name) return;
98
+ this.querySelectorAll('bds-radio').forEach(radio => {
99
+ (radio as HTMLElement).setAttribute('name', this.name);
100
+ });
101
+ };
102
+
103
+ connectedCallback() {
104
+ super.connectedCallback();
105
+ // Propagate name after slot content has been parsed
106
+ Promise.resolve().then(this._propagateName);
107
+ }
108
+
109
+ updated(changed: Map<string, unknown>) {
110
+ super.updated(changed);
111
+ if (changed.has('name')) {
112
+ this._propagateName();
113
+ }
114
+ }
115
+
116
+ render() {
117
+ return html`
118
+ <fieldset ?disabled=${this.disabled}>
119
+ <legend class="legend">
120
+ ${this.label}
121
+ ${this.required ? html`<span class="required" aria-hidden="true"> *</span>` : ''}
122
+ </legend>
123
+ <div class="items">
124
+ <slot @slotchange=${this._propagateName}></slot>
125
+ </div>
126
+ ${this.error
127
+ ? html`<p class="message message--error" role="alert">${this.error}</p>`
128
+ : this.hint
129
+ ? html`<p class="message message--hint">${this.hint}</p>`
130
+ : ''}
131
+ </fieldset>
132
+ `;
133
+ }
134
+ }
135
+
136
+ customElements.define('bds-radio-group', BdsRadioGroup);
137
+
138
+ declare global {
139
+ interface HTMLElementTagNameMap {
140
+ 'bds-radio-group': BdsRadioGroup;
141
+ }
142
+ }
@@ -131,8 +131,13 @@ export class BdsSegmentedControl extends LitElement {
131
131
  display: none;
132
132
  }
133
133
 
134
+ :host([variant='outline']) .item {
135
+ background-color: var(--segmented_item-bg, transparent);
136
+ }
137
+
134
138
  :host([variant='outline']) .item.--active {
135
139
  color: var(--segmented_color--active, var(--bds-color_interactive));
140
+ background-color: var(--segmented_item-bg--active, var(--bds-color_bg));
136
141
  }
137
142
 
138
143
  :host([variant='outline']) .indicator {
@@ -5,7 +5,7 @@ import * as Stories from './BdsAvatar.stories';
5
5
 
6
6
  # &lt;bds-avatar&gt;
7
7
 
8
- Framework-agnostic Avatar custom element. Displays a profile image when `src` is set; falls back to initials derived from the `name` attribute.
8
+ Framework-agnostic Avatar custom element. Displays a user photo when `src` is provided, or falls back to initials derived from the `name` attribute.
9
9
 
10
10
  > **Status: experimental** — API may change before stable release.
11
11
 
@@ -15,16 +15,27 @@ Framework-agnostic Avatar custom element. Displays a profile image when `src` is
15
15
  import '@boostdev/components/web-components';
16
16
  ```
17
17
 
18
+ ## When to use
19
+
20
+ - As a user profile image in navigation bars, comment threads, and team lists
21
+ - As an initials fallback when a photo is unavailable
22
+
18
23
  ## Examples
19
24
 
20
25
  ### All sizes
21
26
  <Canvas of={Stories.AllSizes} />
22
27
 
23
- ### With initials
28
+ ### Default (initials)
24
29
  <Canvas of={Stories.Default} />
25
30
 
26
- ### All variants
27
- <Canvas of={Stories.AllVariants} />
31
+ ### With image
32
+ <Canvas of={Stories.WithImage} />
33
+
34
+ ### Small
35
+ <Canvas of={Stories.Small} />
36
+
37
+ ### Large
38
+ <Canvas of={Stories.Large} />
28
39
 
29
40
  ## Props
30
41
 
@@ -34,34 +45,32 @@ import '@boostdev/components/web-components';
34
45
 
35
46
  | Attribute | Type | Default | Description |
36
47
  |-----------|------|---------|-------------|
37
- | `name` | string | — | Full name; used to compute initials and as `aria-label` fallback |
38
- | `src` | string | | Image URL; when set, shows the image instead of initials |
39
- | `alt` | string | — | Alt text for the image (defaults to `name`) |
40
- | `size` | `"small" \| "medium" \| "large"` | `"medium"` | Avatar diameter (`2em` / `3em` / `4.5em`) |
48
+ | `src` | string | — | Photo URL; when set the image is shown |
49
+ | `alt` | string | `name` or `"User avatar"` | Alt text for the `<img>` element |
50
+ | `name` | string | — | Full name; used to compute up to two initials and as `aria-label` |
51
+ | `size` | `"small" \| "medium" \| "large"` | `"medium"` | Diameter scale |
41
52
 
42
53
  ## CSS Custom Properties
43
54
 
44
55
  | Property | Description |
45
56
  |----------|-------------|
46
- | `--avatar_size` | Override the diameter directly |
47
- | `--avatar_bg` | Fallback background colour (defaults to `--bds-color_blue`) |
48
- | `--avatar_text` | Initials text colour (defaults to `--bds-color_on-blue`) |
57
+ | `--avatar_bg` | Background colour of the fallback initials circle |
58
+ | `--avatar_text` | Text colour of the initials |
49
59
 
50
60
  ## Usage in plain HTML
51
61
 
52
62
  ```html
53
- <!-- Initials fallback -->
54
- <bds-avatar name="Jane Doe"></bds-avatar>
63
+ <!-- With photo -->
64
+ <bds-avatar src="/photo.jpg" name="Jane Doe"></bds-avatar>
55
65
 
56
- <!-- With image -->
57
- <bds-avatar src="/path/to/photo.jpg" name="Jane Doe" alt="Jane Doe's avatar"></bds-avatar>
66
+ <!-- Initials only -->
67
+ <bds-avatar name="Jane Doe" size="large"></bds-avatar>
58
68
 
59
- <!-- Large size -->
60
- <bds-avatar name="John Smith" size="large"></bds-avatar>
69
+ <!-- Small -->
70
+ <bds-avatar name="JD" size="small"></bds-avatar>
61
71
  ```
62
72
 
63
73
  ## Accessibility
64
74
 
65
- - When showing initials: renders `role="img"` with `aria-label` set to the `name` value
66
- - When showing an image: renders a standard `<img>` with `alt` text
67
- - Initials are hidden from assistive technology (`aria-hidden="true"`) — the label is on the container
75
+ - Without `src`: renders `role="img"` with `aria-label` set to `name` (or "User avatar")
76
+ - With `src`: renders `<img>` with `alt` set to `alt`, `name`, or "User avatar"
@@ -1,19 +1,19 @@
1
1
  import React from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react';
3
- import '../index';
3
+ import '../index'; // auto-registers all custom elements
4
4
 
5
5
  function BdsAvatar({
6
- name,
7
6
  src,
8
7
  alt,
9
- size,
8
+ name,
9
+ size = 'medium',
10
10
  }: {
11
- name?: string;
12
11
  src?: string;
13
12
  alt?: string;
13
+ name?: string;
14
14
  size?: 'small' | 'medium' | 'large';
15
15
  }) {
16
- return React.createElement('bds-avatar', { name, src, alt, size });
16
+ return React.createElement('bds-avatar', { src, alt, name, size });
17
17
  }
18
18
 
19
19
  const meta = {
@@ -29,34 +29,20 @@ const meta = {
29
29
  export default meta;
30
30
  type Story = StoryObj<typeof meta>;
31
31
 
32
- export const Default: Story = { args: { name: 'Jane Doe' } };
33
- export const SingleInitial: Story = { args: { name: 'Jane' } };
32
+ export const Default: Story = { args: { name: 'Jane Doe', size: 'medium' } };
33
+ export const WithImage: Story = {
34
+ args: { src: 'https://i.pravatar.cc/150?img=47', name: 'Jane Doe', size: 'medium' },
35
+ };
34
36
  export const Small: Story = { args: { name: 'Jane Doe', size: 'small' } };
35
- export const Medium: Story = { args: { name: 'Jane Doe', size: 'medium' } };
36
37
  export const Large: Story = { args: { name: 'Jane Doe', size: 'large' } };
38
+ export const InitialsOnly: Story = { args: { name: 'John Smith', size: 'medium' } };
37
39
 
38
40
  export const AllSizes: Story = {
39
41
  render: () => (
40
- <div style={{ display: 'flex', alignItems: 'center', gap: '24px' }}>
41
- {(['small', 'medium', 'large'] as const).map(s => (
42
- <div key={s} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
43
- <BdsAvatar name="Jane Doe" size={s} />
44
- <span style={{ fontSize: '0.75rem', color: '#888' }}>{s}</span>
45
- </div>
46
- ))}
47
- </div>
48
- ),
49
- };
50
-
51
- export const AllVariants: Story = {
52
- render: () => (
53
- <div style={{ display: 'flex', alignItems: 'center', gap: '16px', flexWrap: 'wrap' }}>
54
- <BdsAvatar name="Jane Doe" />
55
- <BdsAvatar name="John Smith" />
56
- <BdsAvatar name="Alice" />
57
- <BdsAvatar name="Bob" />
58
- <BdsAvatar name="Jane Doe" size="small" />
59
- <BdsAvatar name="Jane Doe" size="large" />
42
+ <div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
43
+ {React.createElement('bds-avatar', { name: 'Alice Brown', size: 'small' })}
44
+ {React.createElement('bds-avatar', { name: 'Alice Brown', size: 'medium' })}
45
+ {React.createElement('bds-avatar', { name: 'Alice Brown', size: 'large' })}
60
46
  </div>
61
47
  ),
62
48
  };