@boostdev/design-system-components 2.16.0 → 2.18.0

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 (97) hide show
  1. package/AGENTS.md +5 -2
  2. package/dist/client.cjs +57 -57
  3. package/dist/client.css +641 -631
  4. package/dist/client.js +57 -57
  5. package/dist/index.cjs +57 -57
  6. package/dist/index.css +641 -631
  7. package/dist/index.js +57 -57
  8. package/dist/web-components/{chunk-XI2U6U26.js → chunk-2FNCV46J.js} +4 -5
  9. package/dist/web-components/{chunk-SYNLFKD3.js → chunk-3LQKFSMR.js} +6 -0
  10. package/dist/web-components/{chunk-RZH5IVB5.js → chunk-5ATTKUCP.js} +2 -2
  11. package/dist/web-components/{chunk-XJU7KITO.js → chunk-5QA7JOC3.js} +4 -5
  12. package/dist/web-components/{chunk-EYCW7TKB.js → chunk-7GEAT2TY.js} +4 -5
  13. package/dist/web-components/chunk-FYTISJN3.js +18 -0
  14. package/dist/web-components/{chunk-LHFUXZ52.js → chunk-ISV56UWR.js} +4 -8
  15. package/dist/web-components/{chunk-YLDQOESI.js → chunk-K3VK6VEL.js} +6 -5
  16. package/dist/web-components/{chunk-GQNQBEYR.js → chunk-QZMFNJSN.js} +4 -5
  17. package/dist/web-components/{chunk-BPYKZVDK.js → chunk-SJLMW2VR.js} +2 -2
  18. package/dist/web-components/{chunk-GPJWLAKU.js → chunk-VGPJ5L6N.js} +6 -5
  19. package/dist/web-components/{chunk-AGI6YQRC.js → chunk-WOFGYU6E.js} +4 -5
  20. package/dist/web-components/{chunk-2MJTO2IW.js → chunk-WWWJCSXN.js} +2 -2
  21. package/dist/web-components/index.js +13 -12
  22. package/dist/web-components/interaction/form/bds-checkbox-group.js +1 -1
  23. package/dist/web-components/interaction/form/bds-combobox.js +2 -1
  24. package/dist/web-components/interaction/form/bds-form-input.js +2 -1
  25. package/dist/web-components/interaction/form/bds-number-input.js +2 -1
  26. package/dist/web-components/interaction/form/bds-radio-group.js +1 -1
  27. package/dist/web-components/interaction/form/bds-select.js +2 -1
  28. package/dist/web-components/interaction/form/bds-slider.js +2 -1
  29. package/dist/web-components/interaction/form/bds-textarea.js +2 -1
  30. package/dist/web-components/ui/bds-alert.js +1 -1
  31. package/dist/web-components/ui/bds-badge.js +1 -1
  32. package/dist/web-components/ui/bds-notification-banner.js +1 -1
  33. package/dist/web-components/ui/bds-rating.js +1 -1
  34. package/package.json +1 -1
  35. package/src/components/interaction/Rating/Rating.mdx +4 -1
  36. package/src/components/interaction/Rating/Rating.module.css +9 -0
  37. package/src/components/interaction/Rating/Rating.spec.tsx +9 -0
  38. package/src/components/interaction/form/Checkbox/Checkbox.tsx +1 -1
  39. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.mdx +1 -1
  40. package/src/components/interaction/form/CheckboxGroup/CheckboxGroup.spec.tsx +5 -0
  41. package/src/components/interaction/form/Combobox/Combobox.mdx +2 -0
  42. package/src/components/interaction/form/FormInput/FormInput.mdx +4 -0
  43. package/src/components/interaction/form/NumberInput/NumberInput.mdx +2 -0
  44. package/src/components/interaction/form/Radio/Radio.tsx +1 -1
  45. package/src/components/interaction/form/RadioGroup/RadioGroup.mdx +1 -1
  46. package/src/components/interaction/form/RadioGroup/RadioGroup.spec.tsx +5 -0
  47. package/src/components/interaction/form/Select/Select.mdx +2 -0
  48. package/src/components/interaction/form/Slider/Slider.mdx +2 -0
  49. package/src/components/interaction/form/Switch/Switch.tsx +1 -1
  50. package/src/components/interaction/form/Textarea/Textarea.mdx +2 -0
  51. package/src/components/interaction/form/atoms/Label.module.css +10 -0
  52. package/src/components/interaction/form/atoms/Label.spec.tsx +48 -0
  53. package/src/components/interaction/form/atoms/Label.stories.tsx +3 -0
  54. package/src/components/interaction/form/atoms/Label.tsx +4 -2
  55. package/src/components/ui/Alert/Alert.mdx +1 -1
  56. package/src/components/ui/Alert/Alert.module.css +2 -2
  57. package/src/components/ui/Alert/Alert.spec.tsx +6 -0
  58. package/src/components/ui/Badge/Badge.mdx +1 -1
  59. package/src/components/ui/Badge/Badge.module.css +2 -2
  60. package/src/components/ui/Badge/Badge.spec.tsx +8 -0
  61. package/src/components/ui/NotificationBanner/NotificationBanner.mdx +13 -1
  62. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +2 -2
  63. package/src/components/ui/NotificationBanner/NotificationBanner.spec.tsx +6 -0
  64. package/src/test/token-contract.spec.ts +1 -0
  65. package/src/web-components/interaction/form/BdsCheckboxGroup.mdx +14 -0
  66. package/src/web-components/interaction/form/BdsCombobox.mdx +4 -0
  67. package/src/web-components/interaction/form/BdsFormInput.mdx +4 -0
  68. package/src/web-components/interaction/form/BdsNumberInput.mdx +4 -0
  69. package/src/web-components/interaction/form/BdsRadioGroup.mdx +11 -1
  70. package/src/web-components/interaction/form/BdsSelect.mdx +4 -0
  71. package/src/web-components/interaction/form/BdsSlider.mdx +4 -0
  72. package/src/web-components/interaction/form/BdsTextarea.mdx +4 -0
  73. package/src/web-components/interaction/form/bds-checkbox-group.spec.ts +11 -1
  74. package/src/web-components/interaction/form/bds-checkbox-group.ts +6 -5
  75. package/src/web-components/interaction/form/bds-combobox.ts +2 -5
  76. package/src/web-components/interaction/form/bds-form-input.ts +2 -5
  77. package/src/web-components/interaction/form/bds-number-input.ts +2 -5
  78. package/src/web-components/interaction/form/bds-radio-group.spec.ts +11 -1
  79. package/src/web-components/interaction/form/bds-radio-group.ts +6 -5
  80. package/src/web-components/interaction/form/bds-select.ts +2 -5
  81. package/src/web-components/interaction/form/bds-slider.ts +2 -8
  82. package/src/web-components/interaction/form/bds-textarea.ts +2 -5
  83. package/src/web-components/interaction/form/field-label.spec.ts +42 -0
  84. package/src/web-components/interaction/form/field-label.ts +20 -0
  85. package/src/web-components/test/helpers.ts +20 -0
  86. package/src/web-components/ui/BdsAlert.mdx +1 -0
  87. package/src/web-components/ui/BdsBadge.mdx +1 -0
  88. package/src/web-components/ui/BdsNotificationBanner.mdx +1 -1
  89. package/src/web-components/ui/BdsRating.mdx +4 -1
  90. package/src/web-components/ui/bds-alert.spec.ts +7 -0
  91. package/src/web-components/ui/bds-alert.ts +2 -2
  92. package/src/web-components/ui/bds-badge.spec.ts +6 -0
  93. package/src/web-components/ui/bds-badge.ts +2 -2
  94. package/src/web-components/ui/bds-notification-banner.spec.ts +7 -0
  95. package/src/web-components/ui/bds-notification-banner.ts +2 -2
  96. package/src/web-components/ui/bds-rating.spec.ts +7 -0
  97. package/src/web-components/ui/bds-rating.ts +6 -0
@@ -6,4 +6,14 @@
6
6
  color: var(--bds-color_on-bg);
7
7
  cursor: pointer;
8
8
  }
9
+
10
+ /* The label above a field. Same hooks and defaults as the web component
11
+ field labels (fieldLabelStyles in web-components/interaction/form/field-label.ts),
12
+ so a React form and a bds-* form look the same. */
13
+ .--field {
14
+ font-family: var(--formLabel_font-family, var(--bds-font_family--body));
15
+ font-size: var(--formLabel_font-size, var(--bds-font_size--body--s));
16
+ font-weight: var(--formLabel_font-weight, var(--bds-font_weight--semibold));
17
+ color: var(--formLabel_color, var(--bds-color_on-bg));
18
+ }
9
19
  }
@@ -0,0 +1,48 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { Label } from './Label';
3
+ import { FormInput } from '../FormInput';
4
+ import { Select } from '../Select';
5
+ import { Checkbox } from '../Checkbox';
6
+ import { Radio } from '../Radio';
7
+ import { Switch } from '../Switch';
8
+
9
+ describe('Label', () => {
10
+ it('labels the control with the given id', () => {
11
+ render(<Label id="email" label="Email" />);
12
+ expect(screen.getByText('Email').getAttribute('for')).toBe('email');
13
+ });
14
+
15
+ it('uses the field look by default', () => {
16
+ render(<Label id="email" label="Email" />);
17
+ expect(screen.getByText('Email').className).toContain('--field');
18
+ });
19
+
20
+ it('drops the field look for an option label', () => {
21
+ render(<Label id="terms" label="Accept terms" variant="option" />);
22
+ expect(screen.getByText('Accept terms').className).not.toContain('--field');
23
+ });
24
+
25
+ it('gives field labels the field look', () => {
26
+ render(
27
+ <>
28
+ <FormInput label="Email" name="email" />
29
+ <Select label="Country" name="country" options={[{ value: 'nl', label: 'Netherlands' }]} />
30
+ </>
31
+ );
32
+ expect(screen.getByText('Email').className).toContain('--field');
33
+ expect(screen.getByText('Country').className).toContain('--field');
34
+ });
35
+
36
+ it('keeps checkbox, radio and switch labels on the option look', () => {
37
+ render(
38
+ <>
39
+ <Checkbox label="Accept terms" name="terms" />
40
+ <Radio label="Pick up" name="delivery" value="pickup" />
41
+ <Switch label="Notifications" name="notifications" />
42
+ </>
43
+ );
44
+ ['Accept terms', 'Pick up', 'Notifications'].forEach(text =>
45
+ expect(screen.getByText(text).className).not.toContain('--field')
46
+ );
47
+ });
48
+ });
@@ -22,12 +22,15 @@ export const Required: Story = {
22
22
  },
23
23
  };
24
24
 
25
+ export const Option: Story = { args: { id: 'input-option', label: 'Accept the terms', variant: 'option' } };
26
+
25
27
  export const AllVariants: Story = {
26
28
  render: () => (
27
29
  <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
28
30
  <Label id="a" label="Plain label" />
29
31
  <Label id="b" label="Label for a text input" />
30
32
  <Label id="c" label={<><strong>Bold</strong> label with markup</>} />
33
+ <Label id="d" label="Option label beside a checkbox" variant="option" />
31
34
  </div>
32
35
  ),
33
36
  };
@@ -6,11 +6,13 @@ import type { WithClassName } from '../../../../types';
6
6
  interface LabelProps extends WithClassName {
7
7
  id: string;
8
8
  label: ReactNode;
9
+ /** `field` is the label above a control, `option` the text beside a checkbox, radio or switch. */
10
+ variant?: 'field' | 'option';
9
11
  }
10
12
 
11
- export const Label = ({ label, id, className }: LabelProps) => {
13
+ export const Label = ({ label, id, className, variant = 'field' }: LabelProps) => {
12
14
  return (
13
- <label htmlFor={id} className={cn(css.label, className)}>
15
+ <label htmlFor={id} className={cn(css.label, variant === 'field' && css['--field'], className)}>
14
16
  {label}
15
17
  </label>
16
18
  );
@@ -116,7 +116,7 @@ Each variant's actual default resolution:
116
116
  <tr><td>`info`</td><td>`--bds-color_blue--subtle`</td><td>`--bds-color_on-blue--subtle`</td><td>`--bds-color_blue`</td></tr>
117
117
  <tr><td>`success`</td><td>`--bds-color_success--subtle`</td><td>`--bds-color_on-success--subtle`</td><td>`--bds-color_success`</td></tr>
118
118
  <tr><td>`warning`</td><td>`--bds-color_orange--subtle`</td><td>`--bds-color_on-orange--subtle`</td><td>`--bds-color_warning`</td></tr>
119
- <tr><td>`error`</td><td>relative-colour mix of `--bds-color_error` (12% alpha)</td><td>`--bds-color_on-bg`</td><td>`--bds-color_error`</td></tr>
119
+ <tr><td>`error`</td><td>`--bds-color_error--subtle`</td><td>`--bds-color_on-error--subtle`</td><td>`--bds-color_error`</td></tr>
120
120
  </tbody>
121
121
  </table>
122
122
 
@@ -40,8 +40,8 @@
40
40
  }
41
41
 
42
42
  .--variant_error {
43
- --alert_bg--variant: var(--alert_bg--error, rgb(from var(--bds-color_error) r g b / 12%));
44
- --alert_text--variant: var(--alert_text--error, var(--bds-color_on-bg));
43
+ --alert_bg--variant: var(--alert_bg--error, var(--bds-color_error--subtle));
44
+ --alert_text--variant: var(--alert_text--error, var(--bds-color_on-error--subtle));
45
45
  --alert_border--variant: var(--alert_border--error, var(--bds-color_error));
46
46
  }
47
47
 
@@ -101,4 +101,10 @@ describe('Alert', () => {
101
101
  expect(cssSource).toContain(`--alert_accent--${v}`);
102
102
  });
103
103
  });
104
+
105
+ it('paints the error variant with the error subtle pair, like the other variants', () => {
106
+ expect(cssSource).toMatch(/\.--variant_error \{[^}]*--alert_bg--variant:\s*var\(--alert_bg--error,\s*var\(--bds-color_error--subtle\)\)/);
107
+ expect(cssSource).toMatch(/\.--variant_error \{[^}]*--alert_text--variant:\s*var\(--alert_text--error,\s*var\(--bds-color_on-error--subtle\)\)/);
108
+ expect(cssSource).not.toContain('rgb(from var(--bds-color_error)');
109
+ });
104
110
  });
@@ -75,7 +75,7 @@ The override surface is **layered**. Setting a public token on the badge (or any
75
75
  </tbody>
76
76
  </table>
77
77
 
78
- Defaults resolve to foundation tokens per variant: `success`/`error`/`warning` map directly to `--bds-color_{variant}` / `--bds-color_on-{variant}`. `primary` maps to `--bds-color_interactive` / `--bds-color_on-interactive`, and `secondary` maps to `--bds-color_bg` / `--bds-color_on-bg` — neither follows the generic `{variant}` naming.
78
+ Defaults resolve to foundation tokens per variant: `success`/`error`/`warning` map directly to `--bds-color_{variant}` / `--bds-color_on-{variant}`. `primary` maps to `--bds-color_interactive` / `--bds-color_on-interactive`, and `secondary` maps to the neutral `--bds-color_grey` / `--bds-color_on-grey`, so it has a solid surface like the other variants. Neither follows the generic `{variant}` naming.
79
79
 
80
80
 
81
81
  ## Accessibility
@@ -22,8 +22,8 @@
22
22
  }
23
23
 
24
24
  .--variant_secondary {
25
- --badge_bg--variant: var(--badge_bg--secondary, var(--bds-color_bg));
26
- --badge_text--variant: var(--badge_text--secondary, var(--bds-color_on-bg));
25
+ --badge_bg--variant: var(--badge_bg--secondary, var(--bds-color_grey));
26
+ --badge_text--variant: var(--badge_text--secondary, var(--bds-color_on-grey));
27
27
  }
28
28
 
29
29
  .--variant_success {
@@ -1,3 +1,5 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
1
3
  import { render, screen } from '@testing-library/react';
2
4
  import { Badge } from './Badge';
3
5
 
@@ -16,4 +18,10 @@ describe('Badge', () => {
16
18
  render(<Badge className="custom">Test</Badge>);
17
19
  expect(screen.getByText('Test')).toHaveClass('custom');
18
20
  });
21
+
22
+ it('gives the secondary variant a solid neutral surface through its own hooks', () => {
23
+ const css = readFileSync(resolve(process.cwd(), 'src/components/ui/Badge/Badge.module.css'), 'utf8');
24
+ expect(css).toMatch(/\.--variant_secondary \{[^}]*--badge_bg--variant:\s*var\(--badge_bg--secondary,\s*var\(--bds-color_grey\)\)/);
25
+ expect(css).toMatch(/\.--variant_secondary \{[^}]*--badge_text--variant:\s*var\(--badge_text--secondary,\s*var\(--bds-color_on-grey\)\)/);
26
+ });
19
27
  });
@@ -90,7 +90,19 @@ The override surface is **layered**. Setting a public token on the banner (or an
90
90
  </tbody>
91
91
  </table>
92
92
 
93
- Defaults resolve to the corresponding foundation tokens.
93
+ Each variant's actual default resolution:
94
+
95
+ <table>
96
+ <thead>
97
+ <tr><th>Variant</th><th>Background default</th><th>Text default</th><th>Border default</th></tr>
98
+ </thead>
99
+ <tbody>
100
+ <tr><td>`info`</td><td>`--bds-color_blue--subtle`</td><td>`--bds-color_on-blue--subtle`</td><td>`--bds-color_blue`</td></tr>
101
+ <tr><td>`success`</td><td>`--bds-color_success--subtle`</td><td>`--bds-color_on-success--subtle`</td><td>`--bds-color_success`</td></tr>
102
+ <tr><td>`warning`</td><td>`--bds-color_orange--subtle`</td><td>`--bds-color_on-orange--subtle`</td><td>`--bds-color_warning`</td></tr>
103
+ <tr><td>`error`</td><td>`--bds-color_error--subtle`</td><td>`--bds-color_on-error--subtle`</td><td>`--bds-color_error`</td></tr>
104
+ </tbody>
105
+ </table>
94
106
 
95
107
 
96
108
  ## Accessibility
@@ -34,8 +34,8 @@
34
34
  }
35
35
 
36
36
  .--variant_error {
37
- --banner_bg--variant: var(--banner_bg--error, rgb(from var(--bds-color_error) r g b / 12%));
38
- --banner_text--variant: var(--banner_text--error, var(--bds-color_on-bg));
37
+ --banner_bg--variant: var(--banner_bg--error, var(--bds-color_error--subtle));
38
+ --banner_text--variant: var(--banner_text--error, var(--bds-color_on-error--subtle));
39
39
  --banner_border--variant: var(--banner_border--error, var(--bds-color_error));
40
40
  }
41
41
 
@@ -88,4 +88,10 @@ describe('NotificationBanner', () => {
88
88
  expect(cssSource).toContain(`--banner_accent--${v}`);
89
89
  });
90
90
  });
91
+
92
+ it('paints the error variant with the error subtle pair, like the other variants', () => {
93
+ expect(cssSource).toMatch(/\.--variant_error \{[^}]*--banner_bg--variant:\s*var\(--banner_bg--error,\s*var\(--bds-color_error--subtle\)\)/);
94
+ expect(cssSource).toMatch(/\.--variant_error \{[^}]*--banner_text--variant:\s*var\(--banner_text--error,\s*var\(--bds-color_on-error--subtle\)\)/);
95
+ expect(cssSource).not.toContain('rgb(from var(--bds-color_error)');
96
+ });
91
97
  });
@@ -96,6 +96,7 @@ const NON_TEXT_MARKS: { file: string; rule: string }[] = [
96
96
  { file: 'components/ui/Accordion/Accordion.module.css', rule: '.item' },
97
97
  { file: 'components/ui/Tabs/Tabs.module.css', rule: '.tabList' },
98
98
  { file: 'components/ui/Table/Table.module.css', rule: '.th' },
99
+ { file: 'components/interaction/Rating/Rating.module.css', rule: '.star' },
99
100
  ];
100
101
 
101
102
  describe('non-text marks use the as-surface member of the colour set', () => {
@@ -53,6 +53,20 @@ Use `<bds-checkbox-group>` when you have multiple related checkboxes that belong
53
53
  |-------------|-----------------------------------|
54
54
  | `(default)` | `<bds-checkbox>` elements |
55
55
 
56
+ ## Overridable CSS custom properties
57
+
58
+ Legend hooks mirror the React `CheckboxGroup` (same names and defaults), so a theme carries over between the two implementations. Set them on the group or any ancestor.
59
+
60
+ | Variable | Default | Description |
61
+ |----------|---------|-------------|
62
+ | `--checkboxGroup_legend-color` | `var(--bds-color_on-bg)` | Legend text colour. |
63
+ | `--checkboxGroup_legend-font-family` | `var(--bds-font_family--body)` | Legend font family. |
64
+ | `--checkboxGroup_legend-font-size` | `var(--bds-font_size--body--s)` | Legend font size, the same as the field labels (`--formLabel_font-size`). |
65
+ | `--checkboxGroup_legend-font-weight` | `var(--bds-font_weight--medium)` | Legend font weight. |
66
+ | `--checkboxGroup_legend-gap` | `var(--bds-space_xs)` | Space below the legend before the items. |
67
+
68
+ Style slotted `<bds-checkbox>` elements via their own tokens.
69
+
56
70
  ## Usage in plain HTML
57
71
 
58
72
  ```html
@@ -96,6 +96,10 @@ _None._
96
96
  ></bds-combobox>
97
97
  ```
98
98
 
99
+ ## Label
100
+
101
+ The `label` reads the shared label hooks (`--formLabel_*`: font-family, font-size, font-weight, color), listed on `<bds-form-input>`. The React field labels use the same hooks and defaults.
102
+
99
103
  ## Error state
100
104
 
101
105
  With `error`, the message gets a warning icon and the field ring widens to `--bdc-outline_width--error` (2px). Message hooks (`--formMessage_*`: font-size, gap, icon-size, color--error, font-weight--error, color--hint, font-style--hint) are listed on `<bds-form-input>`.
@@ -88,6 +88,10 @@ Token names mirror the React `FormInput` component so themes carry over between
88
88
  | `--formInput_border` | `none` | Border (layered on top of the inset focus/error ring) |
89
89
  | `--formInput_radius` | `var(--bds-border_radius--full)` | Corner radius |
90
90
  | `--formInput_shadow` | `var(--bdc-outline_shadow)` | Box shadow |
91
+ | `--formLabel_font-family` | `var(--bds-font_family--body)` | Font family of the label above the field (shared by all form controls with a label above the field) |
92
+ | `--formLabel_font-size` | `var(--bds-font_size--body--s)` | Label font size |
93
+ | `--formLabel_font-weight` | `var(--bds-font_weight--semibold)` | Label font weight |
94
+ | `--formLabel_color` | `var(--bds-color_on-bg)` | Label colour |
91
95
  | `--formMessage_font-size` | `var(--bds-font_size--body--s)` | Font size of the error and hint message (shared by all form controls) |
92
96
  | `--formMessage_gap` | `var(--bds-space_xxs)` | Gap between the error icon and the error text |
93
97
  | `--formMessage_icon-size` | `1.15em` | Size of the error icon |
@@ -73,6 +73,10 @@ Use `<bds-number-input>` for quantity selectors, counters, or any numeric field
73
73
  <bds-number-input name="qty" label="Quantity" value="1" min="0" max="99" step="1"></bds-number-input>
74
74
  ```
75
75
 
76
+ ## Label
77
+
78
+ The `label` reads the shared label hooks (`--formLabel_*`: font-family, font-size, font-weight, color), listed on `<bds-form-input>`. The React field labels use the same hooks and defaults.
79
+
76
80
  ## Error state
77
81
 
78
82
  With `error`, the message gets a warning icon and the field ring widens to `--bdc-outline_width--error` (2px). Message hooks (`--formMessage_*`: font-size, gap, icon-size, color--error, font-weight--error, color--hint, font-style--hint) are listed on `<bds-form-input>`.
@@ -56,7 +56,17 @@ Use `<bds-radio-group>` whenever you have a set of mutually exclusive options. T
56
56
 
57
57
  ## Overridable CSS custom properties
58
58
 
59
- The legend uses `var(--bds-font_weight--medium)` (matching the React `RadioGroup`). `bds-radio-group` has no component-local override tokens of its own — style slotted `<bds-radio>` elements directly via their own tokens.
59
+ Legend hooks mirror the React `RadioGroup` (same names and defaults), so a theme carries over between the two implementations. Set them on the group or any ancestor.
60
+
61
+ | Variable | Default | Description |
62
+ |----------|---------|-------------|
63
+ | `--radioGroup_legend-color` | `var(--bds-color_on-bg)` | Legend text colour. |
64
+ | `--radioGroup_legend-font-family` | `var(--bds-font_family--body)` | Legend font family. |
65
+ | `--radioGroup_legend-font-size` | `var(--bds-font_size--body--s)` | Legend font size, the same as the field labels (`--formLabel_font-size`). |
66
+ | `--radioGroup_legend-font-weight` | `var(--bds-font_weight--medium)` | Legend font weight. |
67
+ | `--radioGroup_legend-gap` | `var(--bds-space_xs)` | Space below the legend before the items. |
68
+
69
+ Style slotted `<bds-radio>` elements via their own tokens.
60
70
 
61
71
  ## Usage in plain HTML
62
72
 
@@ -82,6 +82,10 @@ When `label` is omitted, set `aria-label` on the host element (`<bds-select aria
82
82
  ></bds-select>
83
83
  ```
84
84
 
85
+ ## Label
86
+
87
+ The `label` reads the shared label hooks (`--formLabel_*`: font-family, font-size, font-weight, color), listed on `<bds-form-input>`. The React field labels use the same hooks and defaults.
88
+
85
89
  ## Error state
86
90
 
87
91
  With `error`, the message gets a warning icon and the field ring widens to `--bdc-outline_width--error` (2px). Message hooks (`--formMessage_*`: font-size, gap, icon-size, color--error, font-weight--error, color--hint, font-style--hint) are listed on `<bds-form-input>`.
@@ -84,6 +84,10 @@ Use `<bds-slider>` for continuous or stepped numeric ranges — volume controls,
84
84
  <bds-slider name="volume" label="Volume" value="50" min="0" max="100" step="5" show-value></bds-slider>
85
85
  ```
86
86
 
87
+ ## Label
88
+
89
+ The `label` reads the shared label hooks (`--formLabel_*`: font-family, font-size, font-weight, color), listed on `<bds-form-input>`. The React field labels use the same hooks and defaults.
90
+
87
91
  ## Error state
88
92
 
89
93
  With `error`, the message gets a warning icon. Message hooks (`--formMessage_*`: font-size, gap, icon-size, color--error, font-weight--error, color--hint, font-style--hint) are listed on `<bds-form-input>`.
@@ -88,6 +88,10 @@ Use `<bds-textarea>` for multiline text entry — messages, comments, descriptio
88
88
  <bds-textarea name="feedback" label="Feedback" error="Feedback is required"></bds-textarea>
89
89
  ```
90
90
 
91
+ ## Label
92
+
93
+ The `label` reads the shared label hooks (`--formLabel_*`: font-family, font-size, font-weight, color), listed on `<bds-form-input>`. The React field labels use the same hooks and defaults.
94
+
91
95
  ## Error state
92
96
 
93
97
  With `error`, the message gets a warning icon and the field ring widens to `--bdc-outline_width--error` (2px). Message hooks (`--formMessage_*`: font-size, gap, icon-size, color--error, font-weight--error, color--hint, font-style--hint) are listed on `<bds-form-input>`.
@@ -1,4 +1,4 @@
1
- import { fixture, cleanup } from '../../test/helpers';
1
+ import { fixture, cleanup, readSource, ruleDeclarations } from '../../test/helpers';
2
2
  import { BdsCheckboxGroup } from './bds-checkbox-group';
3
3
 
4
4
  describe('bds-checkbox-group', () => {
@@ -136,6 +136,16 @@ describe('bds-checkbox-group', () => {
136
136
  cleanup(el);
137
137
  });
138
138
 
139
+ it('styles the legend through the same hooks and defaults as the React CheckboxGroup', () => {
140
+ const react = ruleDeclarations(readSource('src/components/interaction/form/CheckboxGroup/CheckboxGroup.module.css'), '.legend');
141
+ expect(ruleDeclarations(BdsCheckboxGroup.styles.toString(), '.legend')).toEqual(react);
142
+ });
143
+
144
+ it('sizes the legend like the field labels', () => {
145
+ expect(ruleDeclarations(BdsCheckboxGroup.styles.toString(), '.legend')['font-size'])
146
+ .toBe('var(--checkboxGroup_legend-font-size, var(--bds-font_size--body--s))');
147
+ });
148
+
139
149
  it('uses medium font-weight token for the legend (not semibold)', () => {
140
150
  const legendRule = BdsCheckboxGroup.styles.toString().match(/\.legend \{[^}]*\}/)?.[0] ?? '';
141
151
  expect(legendRule).toContain('--bds-font_weight--medium');
@@ -63,11 +63,12 @@ export class BdsCheckboxGroup extends LitElement {
63
63
  }
64
64
 
65
65
  .legend {
66
- font-size: var(--bds-font_size--body--s);
67
- font-family: var(--bds-font_family--body);
68
- font-weight: var(--bds-font_weight--medium);
69
- color: var(--bds-color_on-bg);
70
- margin-block-end: var(--bds-space_xs);
66
+ padding: 0;
67
+ font-family: var(--checkboxGroup_legend-font-family, var(--bds-font_family--body));
68
+ font-size: var(--checkboxGroup_legend-font-size, var(--bds-font_size--body--s));
69
+ font-weight: var(--checkboxGroup_legend-font-weight, var(--bds-font_weight--medium));
70
+ color: var(--checkboxGroup_legend-color, var(--bds-color_on-bg));
71
+ margin-block-end: var(--checkboxGroup_legend-gap, var(--bds-space_xs));
71
72
  }
72
73
 
73
74
  .required {
@@ -1,4 +1,5 @@
1
1
  import { LitElement, css, html, nothing } from 'lit';
2
+ import { fieldLabelStyles } from './field-label';
2
3
  import { fieldMessageStyles, renderFieldMessage } from './field-message';
3
4
 
4
5
  export interface ComboboxOption {
@@ -59,11 +60,6 @@ export class BdsCombobox extends LitElement {
59
60
  }
60
61
 
61
62
  .label {
62
- display: block;
63
- font-size: var(--bds-font_size--body--s);
64
- font-family: var(--bds-font_family--body);
65
- font-weight: var(--bds-font_weight--semibold);
66
- color: var(--bds-color_on-bg);
67
63
  margin-block-end: var(--bds-space_xxs);
68
64
  }
69
65
 
@@ -179,6 +175,7 @@ export class BdsCombobox extends LitElement {
179
175
  color: var(--bds-color_on-bg--subtle);
180
176
  }
181
177
 
178
+ ${fieldLabelStyles}
182
179
  ${fieldMessageStyles}
183
180
  `;
184
181
 
@@ -1,5 +1,6 @@
1
1
  import { LitElement, css, html } from 'lit';
2
2
  import { ifDefined } from 'lit/directives/if-defined.js';
3
+ import { fieldLabelStyles } from './field-label';
3
4
  import { fieldMessageStyles, renderFieldMessage } from './field-message';
4
5
 
5
6
  /**
@@ -53,11 +54,6 @@ export class BdsFormInput extends LitElement {
53
54
  }
54
55
 
55
56
  .label {
56
- display: block;
57
- font-size: var(--bds-font_size--body--s);
58
- font-family: var(--bds-font_family--body);
59
- font-weight: var(--bds-font_weight--semibold);
60
- color: var(--bds-color_on-bg);
61
57
  margin-block-end: var(--bds-space_xxs);
62
58
  }
63
59
 
@@ -143,6 +139,7 @@ export class BdsFormInput extends LitElement {
143
139
  display: none;
144
140
  }
145
141
 
142
+ ${fieldLabelStyles}
146
143
  ${fieldMessageStyles}
147
144
  `;
148
145
 
@@ -1,4 +1,5 @@
1
1
  import { LitElement, css, html } from 'lit';
2
+ import { fieldLabelStyles } from './field-label';
2
3
  import { fieldMessageStyles, renderFieldMessage } from './field-message';
3
4
 
4
5
  /**
@@ -46,11 +47,6 @@ export class BdsNumberInput extends LitElement {
46
47
  }
47
48
 
48
49
  .label {
49
- display: block;
50
- font-size: var(--bds-font_size--body--s);
51
- font-family: var(--bds-font_family--body);
52
- font-weight: var(--bds-font_weight--semibold);
53
- color: var(--bds-color_on-bg);
54
50
  margin-block-end: var(--bds-space_xxs);
55
51
  }
56
52
 
@@ -157,6 +153,7 @@ export class BdsNumberInput extends LitElement {
157
153
  color: var(--numberInput_stepper-color, var(--bds-color_interactive));
158
154
  }
159
155
 
156
+ ${fieldLabelStyles}
160
157
  ${fieldMessageStyles}
161
158
  `;
162
159
 
@@ -1,4 +1,4 @@
1
- import { fixture, cleanup } from '../../test/helpers';
1
+ import { fixture, cleanup, readSource, ruleDeclarations } from '../../test/helpers';
2
2
  import { BdsRadioGroup } from './bds-radio-group';
3
3
 
4
4
  describe('bds-radio-group', () => {
@@ -111,6 +111,16 @@ describe('bds-radio-group', () => {
111
111
  cleanup(el);
112
112
  });
113
113
 
114
+ it('styles the legend through the same hooks and defaults as the React RadioGroup', () => {
115
+ const react = ruleDeclarations(readSource('src/components/interaction/form/RadioGroup/RadioGroup.module.css'), '.legend');
116
+ expect(ruleDeclarations(BdsRadioGroup.styles.toString(), '.legend')).toEqual(react);
117
+ });
118
+
119
+ it('sizes the legend like the field labels', () => {
120
+ expect(ruleDeclarations(BdsRadioGroup.styles.toString(), '.legend')['font-size'])
121
+ .toBe('var(--radioGroup_legend-font-size, var(--bds-font_size--body--s))');
122
+ });
123
+
114
124
  it('uses medium font-weight token for the legend (not semibold)', () => {
115
125
  const legendRule = BdsRadioGroup.styles.toString().match(/\.legend \{[^}]*\}/)?.[0] ?? '';
116
126
  expect(legendRule).toContain('--bds-font_weight--medium');
@@ -50,11 +50,12 @@ export class BdsRadioGroup extends LitElement {
50
50
  }
51
51
 
52
52
  .legend {
53
- font-size: var(--bds-font_size--body--s);
54
- font-family: var(--bds-font_family--body);
55
- font-weight: var(--bds-font_weight--medium);
56
- color: var(--bds-color_on-bg);
57
- margin-block-end: var(--bds-space_xs);
53
+ padding: 0;
54
+ font-family: var(--radioGroup_legend-font-family, var(--bds-font_family--body));
55
+ font-size: var(--radioGroup_legend-font-size, var(--bds-font_size--body--s));
56
+ font-weight: var(--radioGroup_legend-font-weight, var(--bds-font_weight--medium));
57
+ color: var(--radioGroup_legend-color, var(--bds-color_on-bg));
58
+ margin-block-end: var(--radioGroup_legend-gap, var(--bds-space_xs));
58
59
  }
59
60
 
60
61
  .required {
@@ -1,4 +1,5 @@
1
1
  import { LitElement, css, html, nothing } from 'lit';
2
+ import { fieldLabelStyles } from './field-label';
2
3
  import { fieldMessageStyles, renderFieldMessage } from './field-message';
3
4
 
4
5
  export interface SelectOption {
@@ -62,11 +63,6 @@ export class BdsSelect extends LitElement {
62
63
  }
63
64
 
64
65
  .label {
65
- display: block;
66
- font-size: var(--bds-font_size--body--s);
67
- font-family: var(--bds-font_family--body);
68
- font-weight: var(--bds-font_weight--semibold);
69
- color: var(--bds-color_on-bg);
70
66
  margin-block-end: var(--bds-space_xxs);
71
67
  }
72
68
 
@@ -131,6 +127,7 @@ export class BdsSelect extends LitElement {
131
127
  block-size: 100%;
132
128
  }
133
129
 
130
+ ${fieldLabelStyles}
134
131
  ${fieldMessageStyles}
135
132
  `;
136
133
 
@@ -1,4 +1,5 @@
1
1
  import { LitElement, css, html } from 'lit';
2
+ import { fieldLabelStyles } from './field-label';
2
3
  import { fieldMessageStyles, renderFieldMessage } from './field-message';
3
4
 
4
5
  /**
@@ -60,14 +61,6 @@ export class BdsSlider extends LitElement {
60
61
  margin-block-end: var(--bds-space_xxs);
61
62
  }
62
63
 
63
- .label {
64
- display: block;
65
- font-size: var(--bds-font_size--body--s);
66
- font-family: var(--bds-font_family--body);
67
- font-weight: var(--bds-font_weight--semibold);
68
- color: var(--bds-color_on-bg);
69
- }
70
-
71
64
  .value {
72
65
  font-size: var(--bds-font_size--body--s);
73
66
  font-variant-numeric: tabular-nums;
@@ -157,6 +150,7 @@ export class BdsSlider extends LitElement {
157
150
  }
158
151
  }
159
152
 
153
+ ${fieldLabelStyles}
160
154
  ${fieldMessageStyles}
161
155
  `;
162
156
 
@@ -1,4 +1,5 @@
1
1
  import { LitElement, css, html } from 'lit';
2
+ import { fieldLabelStyles } from './field-label';
2
3
  import { fieldMessageStyles, renderFieldMessage } from './field-message';
3
4
 
4
5
  /**
@@ -49,11 +50,6 @@ export class BdsTextarea extends LitElement {
49
50
  }
50
51
 
51
52
  .label {
52
- display: block;
53
- font-size: var(--bds-font_size--body--s);
54
- font-family: var(--bds-font_family--body);
55
- font-weight: var(--bds-font_weight--semibold);
56
- color: var(--bds-color_on-bg);
57
53
  margin-block-end: var(--bds-space_xxs);
58
54
  }
59
55
 
@@ -108,6 +104,7 @@ export class BdsTextarea extends LitElement {
108
104
  --bdc-outline_width: var(--bdc-outline_width--error, 2px);
109
105
  }
110
106
 
107
+ ${fieldLabelStyles}
111
108
  ${fieldMessageStyles}
112
109
  `;
113
110
 
@@ -0,0 +1,42 @@
1
+ import { readSource, ruleDeclarations } from '../../test/helpers';
2
+ import { fieldLabelStyles } from './field-label';
3
+ import { BdsFormInput } from './bds-form-input';
4
+ import { BdsNumberInput } from './bds-number-input';
5
+ import { BdsCombobox } from './bds-combobox';
6
+ import { BdsSelect } from './bds-select';
7
+ import { BdsTextarea } from './bds-textarea';
8
+ import { BdsSlider } from './bds-slider';
9
+
10
+ const typographyProps = ['font-family', 'font-size', 'font-weight', 'color'];
11
+
12
+ describe('field-label', () => {
13
+ it('reads every label look from a --formLabel_* hook', () => {
14
+ expect(ruleDeclarations(fieldLabelStyles.toString(), '.label')).toEqual({
15
+ display: 'block',
16
+ 'font-family': 'var(--formLabel_font-family, var(--bds-font_family--body))',
17
+ 'font-size': 'var(--formLabel_font-size, var(--bds-font_size--body--s))',
18
+ 'font-weight': 'var(--formLabel_font-weight, var(--bds-font_weight--semibold))',
19
+ color: 'var(--formLabel_color, var(--bds-color_on-bg))',
20
+ });
21
+ });
22
+
23
+ it('matches the field variant of the React Label atom', () => {
24
+ const react = ruleDeclarations(readSource('src/components/interaction/form/atoms/Label.module.css'), '.--field');
25
+ const wc = ruleDeclarations(fieldLabelStyles.toString(), '.label');
26
+ expect(Object.keys(react).sort()).toEqual([...typographyProps].sort());
27
+ typographyProps.forEach(prop => expect(react[prop]).toBe(wc[prop]));
28
+ });
29
+
30
+ it.each([
31
+ ['bds-form-input', BdsFormInput],
32
+ ['bds-number-input', BdsNumberInput],
33
+ ['bds-combobox', BdsCombobox],
34
+ ['bds-select', BdsSelect],
35
+ ['bds-textarea', BdsTextarea],
36
+ ['bds-slider', BdsSlider],
37
+ ])('%s styles its label with the shared field label', (_tag, ctor) => {
38
+ const css = ctor.styles.toString();
39
+ expect(css).toContain(fieldLabelStyles.toString());
40
+ expect(css.replace(fieldLabelStyles.toString(), '')).not.toMatch(/\.label \{[^}]*font-(size|weight)/);
41
+ });
42
+ });