@repobit/dex-system-design 1.0.1 → 2.0.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 (136) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +43 -12
  3. package/package.json +4 -2
  4. package/schemas/component-manifest.json +207 -49
  5. package/schemas/manifest-omissions.json +2 -0
  6. package/src/components/accordion/accordion.css.js +12 -1
  7. package/src/components/agent-endpoints/agent-endpoints.js +1 -1
  8. package/src/components/alert/alert.css.js +22 -1
  9. package/src/components/anchor/anchor-nav.css.js +6 -1
  10. package/src/components/annotation/annotation.css.js +6 -1
  11. package/src/components/architecture.stories.js +6 -4
  12. package/src/components/avatar/avatar.js +37 -11
  13. package/src/components/avatar/avatar.stories.js +10 -6
  14. package/src/components/awards/awards-flex.css.js +31 -129
  15. package/src/components/awards/awards-flex.js +66 -116
  16. package/src/components/awards/awards-flex.stories.js +8 -6
  17. package/src/components/back/back.css.js +6 -1
  18. package/src/components/back/back.js +24 -12
  19. package/src/components/back/back.stories.js +3 -3
  20. package/src/components/badge/badge.js +6 -3
  21. package/src/components/badge/badge.stories.js +13 -9
  22. package/src/components/bento-grid/bento-grid.js +1 -1
  23. package/src/components/blockquote/blockquote.css.js +12 -3
  24. package/src/components/blockquote/blockquote.js +12 -5
  25. package/src/components/blockquote/blockquote.stories.js +6 -4
  26. package/src/components/button/button.css.js +46 -25
  27. package/src/components/button/button.js +44 -43
  28. package/src/components/button/button.stories.js +2 -2
  29. package/src/components/caption/caption.css.js +10 -0
  30. package/src/components/caption/caption.stories.js +4 -4
  31. package/src/components/carousel/carousel.css.js +19 -116
  32. package/src/components/carousel/carousel.js +70 -74
  33. package/src/components/carousel/carousel.stories.js +9 -8
  34. package/src/components/checkbox/checkbox.css.js +6 -1
  35. package/src/components/code/code.css.js +6 -1
  36. package/src/components/code/code.js +48 -7
  37. package/src/components/code/code.stories.js +7 -4
  38. package/src/components/divider/divider-horizontal.js +11 -3
  39. package/src/components/divider/divider-vertical.js +12 -5
  40. package/src/components/divider/divider.css.js +4 -1
  41. package/src/components/divider/divider.stories.js +6 -6
  42. package/src/components/dropdown/dropdown.js +67 -13
  43. package/src/components/dropdown/dropdown.stories.js +15 -14
  44. package/src/components/fieldset/fieldset.stories.js +41 -16
  45. package/src/components/footer/footer-nav-menu.css.js +6 -1
  46. package/src/components/footer/footer.css.js +6 -1
  47. package/src/components/header/header-code.js +1 -1
  48. package/src/components/header/header.css.js +9 -4
  49. package/src/components/heading/heading.stories.js +64 -31
  50. package/src/components/highlight/highlight.js +74 -34
  51. package/src/components/highlight/highlight.stories.js +5 -4
  52. package/src/components/icons/icon-element.js +49 -6
  53. package/src/components/icons/icons.stories.js +7 -0
  54. package/src/components/image/image.css.js +9 -98
  55. package/src/components/image/image.js +7 -2
  56. package/src/components/input/custom-form.stories.js +14 -19
  57. package/src/components/input/input-clipboard.css.js +6 -1
  58. package/src/components/input/input-clipboard.js +56 -8
  59. package/src/components/input/input.js +19 -2
  60. package/src/components/label/label.js +87 -1
  61. package/src/components/label/label.stories.js +21 -12
  62. package/src/components/link/link.css.js +52 -3
  63. package/src/components/link/link.js +52 -17
  64. package/src/components/link/link.stories.js +114 -3
  65. package/src/components/list/list-item/list-item.css.js +4 -6
  66. package/src/components/list/list-item/list-item.js +53 -5
  67. package/src/components/list/list-item/list-item.stories.js +22 -0
  68. package/src/components/list/list.js +27 -0
  69. package/src/components/modal/modal.css.js +6 -1
  70. package/src/components/pagination-dots/pagination-dots.css.js +161 -0
  71. package/src/components/pagination-dots/pagination-dots.js +202 -0
  72. package/src/components/pagination-dots/pagination-dots.stories.js +171 -0
  73. package/src/components/paragraph/paragraph.css.js +37 -1
  74. package/src/components/paragraph/paragraph.js +19 -2
  75. package/src/components/paragraph/paragraph.stories.js +40 -6
  76. package/src/components/picture/picture.css.js +8 -67
  77. package/src/components/picture/picture.js +15 -8
  78. package/src/components/pricing-cards/pricing-card.css.js +12 -2
  79. package/src/components/pricing-cards/pricing-card.js +4 -4
  80. package/src/components/progress/progress.js +24 -4
  81. package/src/components/progress/progress.stories.js +8 -6
  82. package/src/components/radio/radio.css.js +6 -1
  83. package/src/components/radio/radio.js +48 -3
  84. package/src/components/radio/radio.stories.js +1 -1
  85. package/src/components/shared/form-field.css.js +30 -4
  86. package/src/components/shared/token-scales.js +16 -0
  87. package/src/components/spinner/spinner.css.js +31 -4
  88. package/src/components/spinner/spinner.js +26 -4
  89. package/src/components/spinner/spinner.stories.js +10 -7
  90. package/src/components/switch/switch.css.js +21 -3
  91. package/src/components/switch/switch.js +8 -1
  92. package/src/components/switch/switch.stories.js +2 -1
  93. package/src/components/tab-list/tab-list.css.js +6 -1
  94. package/src/components/terminal/terminal-cursor.js +11 -0
  95. package/src/components/terminal/terminal-message.js +1 -1
  96. package/src/components/termsOfUse/terms.css.js +6 -1
  97. package/src/components/textarea/textarea.stories.js +23 -0
  98. package/src/components/tooltip/tooltip.css.js +24 -1
  99. package/src/components/tooltip/tooltip.js +135 -5
  100. package/src/components/tooltip/tooltip.stories.js +20 -14
  101. package/src/components/trust-panel/trust-panel.js +2 -2
  102. package/src/components/video/video.js +38 -3
  103. package/src/components/video/video.stories.js +10 -5
  104. package/src/components/video-player/video-player.css.js +6 -1
  105. package/src/tokens/elevation.stories.js +2 -3
  106. package/src/tokens/focus.stories.js +81 -15
  107. package/src/tokens/motion.stories.js +0 -6
  108. package/src/tokens/tokens.css +174 -97
  109. package/src/tokens/tokens.js +174 -97
  110. package/src/tokens/vertical-rhythm.stories.js +1 -1
  111. package/src/stories/Button.js +0 -20
  112. package/src/stories/Button.stories.js +0 -46
  113. package/src/stories/Configure.mdx +0 -364
  114. package/src/stories/Header.js +0 -45
  115. package/src/stories/Header.stories.js +0 -24
  116. package/src/stories/Page.js +0 -62
  117. package/src/stories/Page.stories.js +0 -20
  118. package/src/stories/assets/accessibility.png +0 -0
  119. package/src/stories/assets/accessibility.svg +0 -1
  120. package/src/stories/assets/addon-library.png +0 -0
  121. package/src/stories/assets/assets.png +0 -0
  122. package/src/stories/assets/avif-test-image.avif +0 -0
  123. package/src/stories/assets/context.png +0 -0
  124. package/src/stories/assets/discord.svg +0 -1
  125. package/src/stories/assets/docs.png +0 -0
  126. package/src/stories/assets/figma-plugin.png +0 -0
  127. package/src/stories/assets/github.svg +0 -1
  128. package/src/stories/assets/share.png +0 -0
  129. package/src/stories/assets/styling.png +0 -0
  130. package/src/stories/assets/testing.png +0 -0
  131. package/src/stories/assets/theming.png +0 -0
  132. package/src/stories/assets/tutorials.svg +0 -1
  133. package/src/stories/assets/youtube.svg +0 -1
  134. package/src/stories/button.css +0 -30
  135. package/src/stories/header.css +0 -32
  136. package/src/stories/page.css +0 -68
@@ -1,5 +1,6 @@
1
1
  import { html } from 'lit';
2
2
  import './paragraph.js';
3
+ import '../heading/heading.js';
3
4
 
4
5
  export default {
5
6
  title : 'Atoms/Paragraph',
@@ -51,9 +52,15 @@ layout around them, not to the paragraph itself.
51
52
  argTypes: {
52
53
  variant: {
53
54
  control : { type: 'select' },
54
- options : ['large', 'regular', 'small'],
55
- description: 'Typography size variant, reflected as an attribute for CSS targeting',
56
- table : { type: { summary: "'large'|'regular'|'small'" }, defaultValue: { summary: 'regular' }, category: 'Appearance' }
55
+ options : ['lead', 'large', 'regular', 'small', 'xsmall', 'eyebrow'],
56
+ description: 'Typography role, reflected as an attribute for CSS targeting. `eyebrow` is the short brand-ink label above a section title.',
57
+ table : { type: { summary: "'lead'|'large'|'regular'|'small'|'xsmall'|'eyebrow'" }, defaultValue: { summary: 'regular' }, category: 'Appearance' }
58
+ },
59
+ color: {
60
+ control : { type: 'select' },
61
+ options : ['', 'primary', 'strong', 'muted', 'subtle', 'inverse', 'success', 'warning', 'error'],
62
+ description: 'A semantic colour role, the same set `bd-h` and `bd-display` take. `primary` is brand blue; `inverse` is white on a dark or brand band.',
63
+ table : { type: { summary: 'string' }, defaultValue: { summary: "''" }, category: 'Appearance' }
57
64
  },
58
65
  padding: {
59
66
  control : 'text',
@@ -145,9 +152,36 @@ export const MultiParagraph = {
145
152
  parameters: { docs: { description: { story: 'Three paragraphs of different kinds used together as a typical content block.' } } }
146
153
  };
147
154
 
155
+ export const Eyebrow = {
156
+ name : 'Eyebrow',
157
+ render: () => html`
158
+ <div style="display:flex; flex-direction:column; gap:var(--spacing-16); max-width:588px;">
159
+ <bd-p variant="eyebrow">15+ years of AI-driven security innovation</bd-p>
160
+ <bd-h as="h2" visual-level="h2">AI Across Bitdefender</bd-h>
161
+ </div>
162
+ <div style="margin-top:var(--spacing-32); background:var(--color-surface-brand); padding:var(--spacing-24); border-radius:var(--radius-md); max-width:588px;">
163
+ <bd-p variant="eyebrow" color="inverse">On a brand band, color="inverse"</bd-p>
164
+ </div>
165
+ `,
166
+ parameters: { docs: { description: { story: 'The label above a section title, in text-brand: the label-large role (16px Medium 500 at 1.5) from lg up, and label-default (14px, same weight and leading) below lg, as the homepage draws it on tablet and mobile. It is how all five eyebrows in the package are spelled now (bento grid, header code, trust panel twice, agent endpoints), and what the homepage draws. `color` overrides the ink.' } } }
167
+ };
168
+
169
+ export const Color = {
170
+ name : 'Color',
171
+ render: () => html`
172
+ <div style="display:flex; flex-direction:column; gap:var(--spacing-12); max-width:588px;">
173
+ ${['strong', 'primary', 'muted', 'subtle', 'success', 'warning', 'error'].map(c => html`<bd-p color="${c}">color="${c}" — ${sampleText}</bd-p>`)}
174
+ </div>
175
+ <div style="margin-top:var(--spacing-24); background:var(--color-surface-brand); padding:var(--spacing-24); border-radius:var(--radius-md); max-width:588px;">
176
+ <bd-p variant="large" color="inverse">color="inverse" — Bitdefender protects consumers, small businesses, and enterprises with cybersecurity powered by world-class threat intelligence.</bd-p>
177
+ </div>
178
+ `,
179
+ parameters: { docs: { description: { story: 'A semantic role, resolved to a token. Without it a paragraph\'s ink could only come from the composing component\'s CSS.' } } }
180
+ };
181
+
148
182
  export const Playground = {
149
183
  name : '🛝 Playground',
150
- args : { kind: 'regular' },
151
- render : (args) => html`<bd-p variant="${args.kind}">${sampleText}</bd-p>`,
152
- parameters: { docs: { description: { story: 'Interactive playground. Switch between large, regular, and small via Controls.' } } }
184
+ args : { kind: 'regular', color: '' },
185
+ render : (args) => html`<bd-p variant="${args.kind}" color="${args.color}">${sampleText}</bd-p>`,
186
+ parameters: { docs: { description: { story: 'Interactive playground. Switch the variant and the colour role via Controls.' } } }
153
187
  };
@@ -68,53 +68,11 @@ export default css`
68
68
  border-radius: var(--radius-full);
69
69
  }
70
70
 
71
- /* Responsive Behavior */
72
- .bd-picture.responsive {
73
- width: 100%;
74
- }
75
-
76
- .bd-picture.responsive img {
77
- width: 100%;
78
- height: auto;
79
- }
80
-
81
- /* Aspect Ratio Containers */
82
- .bd-picture.aspect-16-9 {
83
- aspect-ratio: 16 / 9;
84
- }
85
-
86
- .bd-picture.aspect-16-9 img {
87
- width: 100%;
88
- height: 100%;
89
- object-fit: cover;
90
- }
91
-
92
- .bd-picture.aspect-4-3 {
93
- aspect-ratio: 4 / 3;
94
- }
95
-
96
- .bd-picture.aspect-4-3 img {
97
- width: 100%;
98
- height: 100%;
99
- object-fit: cover;
100
- }
101
-
102
- .bd-picture.aspect-1-1 {
103
- aspect-ratio: 1 / 1;
104
- }
105
-
106
- .bd-picture.aspect-1-1 img {
107
- width: 100%;
108
- height: 100%;
109
- object-fit: cover;
110
- }
111
-
112
- .bd-picture:focus-within {
113
- /* the one focus ring — see Foundations/Focus */
114
- outline: none;
115
- box-shadow: var(--focus-outline-primary);
116
- border-radius: var(--focus-outline-radius);
117
- }
71
+ /* Gone from here, because nothing could reach them: responsive,
72
+ aspect-16-9 / 4-3 / 1-1 and the hover-zoom and hover-lift effects had
73
+ no prop that sets them -- fit is written as fit-*, so
74
+ not even a misspelt fit could produce one. :focus-within ringed a
75
+ <picture> holding nothing focusable. */
118
76
 
119
77
  /* Loading state.
120
78
 
@@ -169,8 +127,9 @@ export default css`
169
127
  color: var(--color-text-muted);
170
128
  }
171
129
 
172
- /* One sentence for this state, shared with bd-img, and a local property
173
- rather than a string in the stylesheet so it can be translated. */
130
+ /* A local property rather than a string in the stylesheet, so it can be
131
+ translated. It keeps the --bd-img- name it always had, which is the one
132
+ pages already set. */
174
133
  .bd-picture:not([loaded])::before {
175
134
  content: var(--bd-img-loading-label, "Loading image");
176
135
  /* no role: the pre-load placeholder -- a glyph/marker/placeholder box, not running
@@ -199,16 +158,6 @@ export default css`
199
158
  transform: scale(1);
200
159
  }
201
160
 
202
- /* Hover effects */
203
- .bd-picture.hover-zoom:hover img {
204
- transform: scale(1.05);
205
- }
206
-
207
- .bd-picture.hover-lift:hover {
208
- transform: translateY(-2px);
209
- transition: transform var(--transition-duration-slow) var(--transition-easing-smooth);
210
- }
211
-
212
161
  /* Performance optimizations */
213
162
  .bd-picture img {
214
163
  will-change: transform, opacity;
@@ -248,14 +197,6 @@ export default css`
248
197
  .bd-picture img {
249
198
  transition: none;
250
199
  }
251
-
252
- .bd-picture.hover-zoom:hover img {
253
- transform: none;
254
- }
255
-
256
- .bd-picture.hover-lift:hover {
257
- transform: none;
258
- }
259
200
  }
260
201
 
261
202
  /* No @media (prefers-color-scheme: dark) block. This was the one atom that
@@ -5,6 +5,7 @@ import pictureCSS from "./picture.css.js";
5
5
 
6
6
  // the radius steps picture.css.js implements — the full --radius-* scale
7
7
  const RADIUS_STEPS = ['none', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', 'full'];
8
+ const FITS = ['cover', 'contain', 'fill', 'none', 'scale-down'];
8
9
 
9
10
  export class BdPicture extends LitElement {
10
11
  static properties = {
@@ -50,20 +51,26 @@ export class BdPicture extends LitElement {
50
51
  }
51
52
 
52
53
  render() {
54
+ // fit becomes a class, so only the values picture.css.js implements get
55
+ // through; an unknown one used to leave the image with no object-fit
53
56
  const classes = [
54
57
  'bd-picture',
55
- `fit-${this.fit}`,
58
+ `fit-${FITS.includes(this.fit) ? this.fit : 'cover'}`,
56
59
  `radius-${RADIUS_STEPS.includes(this.radius) ? this.radius : 'none'}`
57
60
  ].filter(Boolean).join(' ');
58
61
 
62
+ // Each source's optional attributes are left off when not given. type
63
+ // used to be written as-is, so a source without one rendered
64
+ // type="undefined" -- a MIME type no browser supports, so it skipped the
65
+ // source and fell through to the next one or the <img>.
59
66
  return html`
60
67
  <picture class="${classes}">
61
68
  ${this.sources.map(source => html`
62
- <source
63
- type="${source.type}"
69
+ <source
70
+ type="${source.type || nothing}"
64
71
  srcset="${source.srcset}"
65
- media="${source.media || ''}"
66
- sizes="${source.sizes || ''}"
72
+ media="${source.media || nothing}"
73
+ sizes="${source.sizes || nothing}"
67
74
  >
68
75
  `)}
69
76
  <img
@@ -102,9 +109,9 @@ export class BdPicture extends LitElement {
102
109
  and a default-configured bd-picture then renders a fully loaded image at
103
110
  opacity 0. The two defects had to be fixed together or neither is visible.
104
111
 
105
- bd-img is the shape this follows. It keys its placeholder on
106
- `:not([src])` -- a fact the element already knows -- and never hides the
107
- image at all, which is why bd-img has no equivalent bug. The minimal
112
+ bd-img never hides its image at all, which is why it has no equivalent
113
+ bug -- and no placeholder either, since an <img> cannot draw one. The
114
+ minimal
108
115
  change here is to make this component tell the truth it already asks CSS
109
116
  about, rather than to restyle it: the fade on load is a deliberate
110
117
  nicety, and it is kept.
@@ -20,12 +20,22 @@ export default css`
20
20
 
21
21
  button:focus-visible {
22
22
  /* the one focus ring — see Design Tokens/Focus */
23
- outline: none;
23
+ /* forced-colors mode does not paint box-shadow, so the ring
24
+ vanishes there; it does paint an outline, in a system colour. This one
25
+ is transparent everywhere else, and the offset lands it on the ring's
26
+ outer band. */
27
+ outline: var(--border-width-2) solid transparent;
28
+ outline-offset: var(--focus-outline-offset);
24
29
  box-shadow: var(--focus-outline-primary);
25
30
  }
26
31
  a:focus-visible {
27
32
  /* the one focus ring — see Design Tokens/Focus */
28
- outline: none;
33
+ /* forced-colors mode does not paint box-shadow, so the ring
34
+ vanishes there; it does paint an outline, in a system colour. This one
35
+ is transparent everywhere else, and the offset lands it on the ring's
36
+ outer band. */
37
+ outline: var(--border-width-2) solid transparent;
38
+ outline-offset: var(--focus-outline-offset);
29
39
  box-shadow: var(--focus-outline-primary);
30
40
  border-radius: var(--focus-outline-radius);
31
41
  }
@@ -204,7 +204,7 @@ class PricingCard extends LitElement {
204
204
  </bd-badge>
205
205
 
206
206
  <bd-p variant="small" class="badge-subtitle">1 account & 5 devices</bd-p>
207
- <bd-divider-horizontal color="subtle"></bd-divider-horizontal>
207
+ <bd-divider-horizontal color="subtle" opacity="0.25"></bd-divider-horizontal>
208
208
  </div>
209
209
 
210
210
  <!-- Pricing section with store integration -->
@@ -249,7 +249,7 @@ class PricingCard extends LitElement {
249
249
  </bd-button>
250
250
  </div>
251
251
 
252
- <bd-divider-horizontal color="subtle"></bd-divider-horizontal>
252
+ <bd-divider-horizontal color="subtle" opacity="0.25"></bd-divider-horizontal>
253
253
 
254
254
  <!-- Features sections -->
255
255
  ${this.features ? html`<pricing-card-features .shadow=${this.shadow}></pricing-card-features>` : ""}
@@ -316,7 +316,7 @@ class PricingCardFamily extends LitElement {
316
316
  </bd-badge>
317
317
 
318
318
  <bd-p variant="small" class="badge-subtitle">5 accounts & 25 devices</bd-p>
319
- <bd-divider-horizontal color="subtle"></bd-divider-horizontal>
319
+ <bd-divider-horizontal color="subtle" opacity="0.25"></bd-divider-horizontal>
320
320
  </div>
321
321
 
322
322
  <!-- Pricing section with store integration -->
@@ -361,7 +361,7 @@ class PricingCardFamily extends LitElement {
361
361
  </bd-button>
362
362
  </div>
363
363
 
364
- <bd-divider-horizontal color="subtle"></bd-divider-horizontal>
364
+ <bd-divider-horizontal color="subtle" opacity="0.25"></bd-divider-horizontal>
365
365
 
366
366
  <!-- Features sections -->
367
367
  ${this.features
@@ -3,6 +3,9 @@ import { LitElement, html, nothing } from 'lit';
3
3
  import '../../tokens/tokens.js';
4
4
  import { progressCSS } from './progress.css.js';
5
5
 
6
+ /* The tones progress.css.js styles. No tone is the default fill. */
7
+ const TONES = ['info', 'success', 'error'];
8
+
6
9
  export class BdProgress extends LitElement {
7
10
  static styles = [progressCSS];
8
11
 
@@ -20,20 +23,37 @@ export class BdProgress extends LitElement {
20
23
  this.label = '';
21
24
  }
22
25
 
26
+ /* tone reflects, so an unknown one used to sit on the element as if it
27
+ meant something -- tone="warning" or the retired "default" -- while the
28
+ bar drew the default fill. It is cleared instead, so the attribute says
29
+ what is drawn. */
30
+ willUpdate(changed) {
31
+ if (changed.has('tone') && this.tone != null && !TONES.includes(this.tone)) this.tone = undefined;
32
+ }
33
+
23
34
  render() {
35
+ /* One clamped value for all three readers. Only the fill used to be
36
+ clamped: value 150 read "150%" beside a full bar and reported an
37
+ aria-valuenow above aria-valuemax, a negative value drew nothing and
38
+ read "-20%", and max 0 read "NaN%". A max that is not a positive number
39
+ falls back to the default, as an unknown variant does elsewhere. */
40
+ const max = Number.isFinite(this.max) && this.max > 0 ? this.max : 100;
41
+ const value = Number.isFinite(this.value) ? Math.min(Math.max(this.value, 0), max) : 0;
42
+ const percent = (value / max) * 100;
43
+
24
44
  return html`
25
45
  ${this.label
26
- ? html`<span class="label"><span>${this.label}</span><span>${Math.round((this.value / this.max) * 100)}%</span></span>`
46
+ ? html`<span class="label"><span>${this.label}</span><span>${Math.round(percent)}%</span></span>`
27
47
  : nothing}
28
48
  <div
29
49
  class="track"
30
50
  role="progressbar"
31
- aria-valuenow=${this.value}
51
+ aria-valuenow=${value}
32
52
  aria-valuemin="0"
33
- aria-valuemax=${this.max}
53
+ aria-valuemax=${max}
34
54
  aria-label=${this.label || 'Progress'}
35
55
  >
36
- <div class="fill" style="width: ${Math.min(100, (this.value / this.max) * 100)}%"></div>
56
+ <div class="fill" style="width: ${percent}%"></div>
37
57
  </div>
38
58
  `;
39
59
  }
@@ -24,15 +24,17 @@ quantity something is. For work of unknown length use \`<bd-spinner>\`.
24
24
  | \`value\` | number, current progress | \`0\` |
25
25
  | \`max\` | number, the total | \`100\` |
26
26
  | \`label\` | caption shown above the bar, with a percentage | \`''\` |
27
- | \`tone\` | \`default\`, \`success\`, \`error\` | \`default\` |
27
+ | \`tone\` | \`info\`, \`success\`, \`error\` — anything else is cleared | unset: the default fill |
28
28
 
29
29
  ### Notes
30
30
  When \`label\` is set, a row above the bar shows the label on the left and the
31
31
  rounded percentage on the right, so the number is visible and not only
32
32
  announced.
33
33
 
34
- The fill is clamped at 100%, so a \`value\` above \`max\` cannot overflow the
35
- track.
34
+ \`value\` is clamped to 0…\`max\`, and the fill, the percentage and
35
+ \`aria-valuenow\` all read the clamped number, so a value above \`max\` shows a
36
+ full bar, "100%" and a valuenow equal to the max. A \`max\` that is not a
37
+ positive number falls back to \`100\`.
36
38
 
37
39
  ### Accessibility
38
40
  The track carries \`role="progressbar"\` with \`aria-valuenow\`, \`aria-valuemin\`
@@ -61,9 +63,9 @@ and \`aria-valuemax\`, so assistive tech reports real progress rather than just
61
63
  },
62
64
  tone: {
63
65
  control : { type: 'select' },
64
- options : ['default', 'success', 'error'],
65
- description: 'Colour of the fill.',
66
- table : { type: { summary: 'string' }, defaultValue: { summary: "default" }, category: 'Appearance' }
66
+ options : ['', 'info', 'success', 'error'],
67
+ description: 'Colour of the fill. Unset is the default fill; a value that is not a tone is cleared.',
68
+ table : { type: { summary: "'info' | 'success' | 'error'" }, defaultValue: { summary: 'unset' }, category: 'Appearance' }
67
69
  }
68
70
  }
69
71
  };
@@ -39,7 +39,12 @@ export default css`
39
39
  /* the one focus ring — see Foundations/Focus. A two-colour ring whose
40
40
  white inner band keeps it visible on a coloured or dark surface, which a
41
41
  single-colour outline cannot do. */
42
- outline: none;
42
+ /* forced-colors mode does not paint box-shadow, so the ring
43
+ vanishes there; it does paint an outline, in a system colour. This one
44
+ is transparent everywhere else, and the offset lands it on the ring's
45
+ outer band. */
46
+ outline: var(--border-width-2) solid transparent;
47
+ outline-offset: var(--focus-outline-offset);
43
48
  box-shadow: var(--focus-outline-primary);
44
49
  border-radius: var(--focus-outline-radius);
45
50
  }
@@ -20,7 +20,11 @@ class BdRadio extends LitElement {
20
20
  static styles = [radioCSS];
21
21
 
22
22
  static properties = {
23
- name : { type: String },
23
+ /* Reflected because the group is found by it: _group selects same-name
24
+ radios by attribute, so a name set as a property -- radio.name = "plan",
25
+ which is what a framework binding does -- left the radio in no group at
26
+ all, with no exclusivity, no arrow keys and its own tab stop. */
27
+ name : { type: String, reflect: true },
24
28
  value : { type: String },
25
29
  checked : { type: Boolean, reflect: true },
26
30
  disabled: { type: Boolean, reflect: true },
@@ -43,7 +47,7 @@ class BdRadio extends LitElement {
43
47
  if (!this.name) return [this];
44
48
  const root = this.getRootNode();
45
49
  const scope = root instanceof Document || root instanceof ShadowRoot ? root : document;
46
- return [...scope.querySelectorAll(`bd-radio[name="${this.name}"]`)];
50
+ return [...scope.querySelectorAll(`bd-radio[name="${CSS.escape(this.name)}"]`)];
47
51
  }
48
52
 
49
53
  /**
@@ -57,6 +61,45 @@ class BdRadio extends LitElement {
57
61
  return group.find(r => !r.disabled) === this;
58
62
  }
59
63
 
64
+ /* The tab stop is a group decision, but each radio renders only when its own
65
+ properties change. So when one radio's checked or disabled flips, or one
66
+ joins or leaves, the rest have to be told to re-render: clicking radio 3
67
+ in a group with nothing selected used to leave radio 1 at tabindex 0
68
+ beside it -- two tab stops in one group. The peers' own checked and
69
+ disabled do not change, so this does not cascade. */
70
+ _refreshPeers(group) {
71
+ for (const radio of group) {
72
+ if (radio !== this) radio.requestUpdate?.();
73
+ }
74
+ }
75
+
76
+ connectedCallback() {
77
+ super.connectedCallback();
78
+ this._refreshPeers(this._group);
79
+ }
80
+
81
+ disconnectedCallback() {
82
+ super.disconnectedCallback();
83
+ // detached, this radio can no longer find its root, so it asks the one it
84
+ // was connected in
85
+ const root = this._root;
86
+ if (this.name && root) {
87
+ this._refreshPeers(root.querySelectorAll(`bd-radio[name="${CSS.escape(this.name)}"]`));
88
+ }
89
+ }
90
+
91
+ updated(changed) {
92
+ this._root = this.isConnected ? this.getRootNode() : this._root;
93
+ if (changed.has("checked") || changed.has("disabled") || changed.has("name")) {
94
+ this._refreshPeers(this._group);
95
+ }
96
+ // a radio renamed out of a group leaves its old peers to re-decide too
97
+ const oldName = changed.get("name");
98
+ if (oldName && this._root) {
99
+ this._refreshPeers(this._root.querySelectorAll(`bd-radio[name="${CSS.escape(oldName)}"]`));
100
+ }
101
+ }
102
+
60
103
  _select() {
61
104
  if (this.disabled || this.checked) return;
62
105
 
@@ -153,8 +196,10 @@ class BdToggleRadio extends LitElement {
153
196
  this.checked = !this.checked;
154
197
 
155
198
  this.dispatchEvent(
199
+ // name was declared and read by nothing; it is carried here, as
200
+ // bd-radio carries it, so a handler can tell which control changed
156
201
  new CustomEvent("change", {
157
- detail : { checked: this.checked, value: this.checked ? this.value : null },
202
+ detail : { checked: this.checked, value: this.checked ? this.value : null, name: this.name },
158
203
  bubbles : true,
159
204
  composed: true
160
205
  })
@@ -69,7 +69,7 @@ Two deviations from the design, both deliberate:
69
69
 
70
70
  ### Events
71
71
  - \`change\` — \`detail: { name, value }\` on \`bd-radio\`;
72
- \`detail: { checked, value }\` on \`bd-toggle-radio\`.
72
+ \`detail: { checked, value, name }\` on \`bd-toggle-radio\`.
73
73
  `
74
74
  }
75
75
  }
@@ -106,12 +106,17 @@ export const formFieldCSS = css`
106
106
  shows the indicator for a pointer click there too, because typing is
107
107
  expected — but the select is a control you click and walk away from, and
108
108
  it was drawing the full ring on every mouse press where bd-button drew
109
- nothing. .focused is the component's own prop, not a user interaction, so
110
- it stays unconditional. */
111
- .form-input-bd:focus-visible,
112
- .form-input-bd.focused {
109
+ nothing. There is no .focused class any more: a field's focused prop now
110
+ moves real focus, so :focus-visible is the only thing that draws this. */
111
+ .form-input-bd:focus-visible {
113
112
  border-color: var(--form-input-focus-borderColor);
114
113
  box-shadow: var(--focus-outline-primary);
114
+ /* forced-colors mode does not paint box-shadow, so the ring
115
+ vanishes there; it does paint an outline, in a system colour. This one
116
+ is transparent everywhere else, and the offset lands it on the ring's
117
+ outer band. */
118
+ outline: var(--border-width-2) solid transparent;
119
+ outline-offset: var(--focus-outline-offset);
115
120
  }
116
121
 
117
122
  /* ---- sizes ------------------------------------------------------------
@@ -139,6 +144,27 @@ export const formFieldCSS = css`
139
144
  border-radius: var(--form-input-large-borderRadius);
140
145
  }
141
146
 
147
+ /* The text's own room. --form-input-*-height is padding plus the line box,
148
+ and the box is border-box, so the 1px border on each edge came out of the
149
+ content: the line box stood 2px taller than the space left for it. An
150
+ input centres its line and paints into the padding, so it never showed;
151
+ a select does neither -- it clips at the content box -- and at md the
152
+ descenders of "Italy gjpqy ș ț" lost their bottoms. Taking the border out
153
+ of the block padding instead gives the line all of its box and leaves
154
+ every height where it was, on its button. The textarea is left out: its
155
+ height comes from its rows, so this would only make it 2px shorter. */
156
+ .input-sm:not(textarea) {
157
+ padding-block: calc(var(--spacing-8) - var(--form-input-default-borderWidth));
158
+ }
159
+
160
+ .input-md:not(textarea) {
161
+ padding-block: calc(var(--spacing-12) - var(--form-input-default-borderWidth));
162
+ }
163
+
164
+ .input-lg:not(textarea) {
165
+ padding-block: calc(var(--spacing-16) - var(--form-input-default-borderWidth));
166
+ }
167
+
142
168
  ::placeholder {
143
169
  color: var(--color-text-subtle);
144
170
  opacity: 1;
@@ -141,6 +141,22 @@ export const fontFamily = onScale(FONT_FAMILY_STEPS, '--typography-fontFamily-')
141
141
  export const letterSpacing = onScale(LETTER_SPACING_STEPS, '--typography-letterSpacing-');
142
142
  export const radius = onScale(RADIUS_STEPS, '--radius-');
143
143
 
144
+ /* The border-width steps. A px spelling of a step -- "2px" -- is read as the
145
+ step, because that is what callers wrote before there was a scale here. */
146
+ export const BORDER_WIDTH_STEPS = ['0', '1', '2', '3', '4', '8'];
147
+ export const borderWidth = value => {
148
+ const step = String(value ?? '').replace(/px$/, '');
149
+ return BORDER_WIDTH_STEPS.includes(step) ? `var(--border-width-${step})` : '';
150
+ };
151
+
152
+ /* Not a scale: a fraction. An opacity given as a prop is kept only when it is
153
+ a number from 0 to 1, so a stray string never reaches an inline style. */
154
+ export const opacity = value => {
155
+ if (value === '' || value == null) return '';
156
+ const n = Number(value);
157
+ return Number.isFinite(n) && n >= 0 && n <= 1 ? String(n) : '';
158
+ };
159
+
144
160
  export const space = name =>
145
161
  (Object.hasOwn(SPACE_STEPS, name) ? `var(--spacing-${SPACE_STEPS[name]})` : '');
146
162
 
@@ -33,6 +33,19 @@ export const spinnerCSS = css`
33
33
  --bd-spinner-pulse-fill: color-mix(in srgb, var(--color-neutral-0) 30%, transparent);
34
34
  }
35
35
 
36
+ /* The status region: text for assistive technology, nothing on screen. */
37
+ .announce {
38
+ position: absolute;
39
+ /* no token: 1px is the clip idiom's own measure, not a border -- the same
40
+ sanctioned literal as visually-hidden.css.js. */
41
+ width: 1px;
42
+ height: 1px;
43
+ margin: -1px;
44
+ overflow: hidden;
45
+ clip-path: inset(50%);
46
+ white-space: nowrap;
47
+ }
48
+
36
49
  .spinner {
37
50
  width: var(--dimension-24);
38
51
  height: var(--dimension-24);
@@ -47,13 +60,27 @@ export const spinnerCSS = css`
47
60
 
48
61
  @keyframes bd-spin { to { transform: rotate(360deg); } }
49
62
 
50
- /* a spinner that cannot spin should still say something, so fall back to a
51
- pulse rather than freezing solid */
63
+ /* A spinner that cannot spin should still say something, so it pulses
64
+ rather than freezing solid -- but only the disc inside it. This used to
65
+ repaint the arc in the track colour and pulse the whole ring, so what was
66
+ left was the track alone: 1.66:1 against the page, 1.21:1 at the pulse's
67
+ low point, under the 3:1 WCAG 1.4.11 asks of a graphic that conveys
68
+ state. The arc now stays, still and at full strength (4.53:1 on the page,
69
+ 3.64:1 on the disc), and the disc behind it carries the motion. Pulsing
70
+ the whole spinner instead would take the arc to 1.79:1 at the bottom of
71
+ each pulse. */
52
72
  @media (prefers-reduced-motion: reduce) {
53
73
  .spinner {
54
- animation: bd-pulse var(--transition-duration-slower) var(--transition-easing-smooth) infinite alternate;
55
- border-top-color: var(--bd-spinner-track);
74
+ position: relative;
75
+ animation: none;
76
+ }
77
+ .spinner::before {
78
+ content: "";
79
+ position: absolute;
80
+ inset: var(--spacing-0);
81
+ border-radius: var(--radius-full);
56
82
  background: var(--bd-spinner-pulse-fill);
83
+ animation: bd-pulse var(--transition-duration-slower) var(--transition-easing-smooth) infinite alternate;
57
84
  }
58
85
  @keyframes bd-pulse { to { opacity: var(--opacity-pulse); } }
59
86
  }
@@ -8,12 +8,13 @@ export class BdSpinner extends LitElement {
8
8
  static styles = [spinnerCSS];
9
9
 
10
10
  static properties = {
11
- size : { type: String, reflect: true },
12
- label : { type: String },
11
+ size : { type: String, reflect: true },
12
+ label : { type: String },
13
13
  /* The surface the spinner sits on. Its default arc and track were both
14
14
  chosen against a white page, so on a filled panel it all but disappears.
15
15
  Same attribute, same meaning, as on bd-button. */
16
- surface: { type: String, reflect: true }
16
+ surface : { type: String, reflect: true },
17
+ _announce: { type: Boolean, state: true }
17
18
  };
18
19
 
19
20
  constructor() {
@@ -21,6 +22,26 @@ export class BdSpinner extends LitElement {
21
22
  this.size = 'md';
22
23
  this.label = 'Loading';
23
24
  this.surface = 'light';
25
+ this._announce = false;
26
+ }
27
+
28
+ /* WHY THE TEXT ARRIVES LATE.
29
+ This was an empty role="status" named by aria-label. A live region is
30
+ announced when its CONTENT changes, and a name is not content, so it
31
+ never had anything to say: nothing announced the wait at all.
32
+
33
+ The label is now text inside the region. It is rendered empty first and
34
+ filled a moment later, because the region has to be in the accessibility
35
+ tree before its text arrives -- a region inserted already holding its
36
+ text is, to many screen readers, not a change. A later label change is
37
+ a content change too, so it is announced as well. */
38
+ firstUpdated() {
39
+ this._announceTimer = setTimeout(() => { this._announce = true; }, 100);
40
+ }
41
+
42
+ disconnectedCallback() {
43
+ clearTimeout(this._announceTimer);
44
+ super.disconnectedCallback();
24
45
  }
25
46
 
26
47
  willUpdate(changed) {
@@ -30,7 +51,8 @@ export class BdSpinner extends LitElement {
30
51
 
31
52
  render() {
32
53
  return html`
33
- <span class="spinner" role="status" aria-label=${this.label}></span>
54
+ <span class="spinner" aria-hidden="true"></span>
55
+ <span class="announce" role="status">${this._announce ? this.label : ''}</span>
34
56
  `;
35
57
  }
36
58
  }