@repobit/dex-system-design 1.0.1 → 2.0.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 (136) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +43 -12
  3. package/package.json +6 -4
  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,7 +1,7 @@
1
1
  import { html } from 'lit';
2
2
  import './badge.js';
3
3
 
4
- const TONES = ['default', 'primary', 'success', 'warning', 'error', 'info'];
4
+ const TONES = ['neutral', 'brand', 'success', 'warning', 'error', 'info'];
5
5
  const SIZES = ['sm', 'md', 'lg'];
6
6
  const VARIANTS = ['shade', 'fill'];
7
7
 
@@ -35,10 +35,10 @@ matched to the token that already carries it.
35
35
  ### Usage
36
36
  \`\`\`html
37
37
  <bd-badge tone="success" variant="shade">Save 44%</bd-badge>
38
- <bd-badge tone="primary" variant="fill" size="sm">New</bd-badge>
38
+ <bd-badge tone="brand" variant="fill" size="sm">New</bd-badge>
39
39
  <bd-badge mode="count" tone="error" variant="fill">3</bd-badge>
40
40
  <bd-badge mode="dot" tone="warning" variant="fill"></bd-badge>
41
- <bd-badge tone="primary" variant="fill" icon="family">Family Pack</bd-badge>
41
+ <bd-badge tone="brand" variant="fill" icon="family">Family Pack</bd-badge>
42
42
  \`\`\`
43
43
 
44
44
  ### Props
@@ -46,7 +46,7 @@ matched to the token that already carries it.
46
46
  |---|---|---|
47
47
  | \`mode\` | \`text\`, \`dot\`, \`count\` | \`text\` |
48
48
  | \`size\` | \`sm\`, \`md\`, \`lg\` | \`md\` |
49
- | \`tone\` | \`default\`, \`primary\`, \`success\`, \`warning\`, \`error\`, \`info\` | \`default\` |
49
+ | \`tone\` | \`neutral\`, \`brand\`, \`success\`, \`warning\`, \`error\`, \`info\` | \`neutral\` |
50
50
  | \`variant\` | \`shade\`, \`fill\` | \`shade\` |
51
51
  | \`icon\` | \`individual\`, \`family\`, \`''\` | \`''\` |
52
52
  | \`label\` | any string — accessible name for \`mode="dot"\` | tone name |
@@ -107,7 +107,7 @@ matched to the token that already carries it.
107
107
  control : { type: 'select' },
108
108
  options : TONES,
109
109
  description: 'Semantic colour pairing.',
110
- table : { type: { summary: 'string' }, defaultValue: { summary: 'default' }, category: 'Appearance' }
110
+ table : { type: { summary: 'string' }, defaultValue: { summary: 'neutral' }, category: 'Appearance' }
111
111
  },
112
112
  variant: {
113
113
  control : { type: 'inline-radio' },
@@ -135,7 +135,7 @@ matched to the token that already carries it.
135
135
  args: {
136
136
  mode : 'text',
137
137
  size : 'md',
138
- tone : 'default',
138
+ tone : 'neutral',
139
139
  variant: 'shade',
140
140
  icon : '',
141
141
  label : '',
@@ -172,7 +172,7 @@ export const Sizes = {
172
172
  render : () => html`
173
173
  ${VARIANTS.map(variant => html`
174
174
  ${row(variant, SIZES.map(size => html`
175
- <bd-badge size="${size}" tone="primary" variant="${variant}">${size}</bd-badge>
175
+ <bd-badge size="${size}" tone="brand" variant="${variant}">${size}</bd-badge>
176
176
  `))}
177
177
  `)}
178
178
  `
@@ -212,7 +212,7 @@ export const WithIcon = {
212
212
  <bd-badge tone="success" icon="family">Family Pack</bd-badge>
213
213
  `)}
214
214
  ${row('fill', html`
215
- <bd-badge variant="fill" tone="primary" icon="individual">Individual</bd-badge>
215
+ <bd-badge variant="fill" tone="brand" icon="individual">Individual</bd-badge>
216
216
  <bd-badge variant="fill" tone="success" icon="family">Family Pack</bd-badge>
217
217
  `)}
218
218
  `
@@ -220,11 +220,15 @@ export const WithIcon = {
220
220
 
221
221
  export const Legacy = {
222
222
  name : 'Legacy variants',
223
- parameters: { docs: { description: { story: 'Pre-`ds-badge` markup still renders, mapped onto the new axes.' } } },
223
+ parameters: { docs: { description: { story: 'Pre-`ds-badge` markup still renders, mapped onto the new axes: the old variants, and the two retired tone names, `default` (now `neutral`) and `primary` (now `brand`). Write the new names; these are here so a page still using the old ones is shown to work.' } } },
224
224
  render : () => html`
225
225
  ${row('mapped', html`
226
226
  <bd-badge variant="bd-light-blue">bd-light-blue</bd-badge>
227
227
  <bd-badge variant="bd-light-green">bd-light-green</bd-badge>
228
228
  `)}
229
+ ${row('tones', html`
230
+ <bd-badge tone="default">tone="default" → neutral</bd-badge>
231
+ <bd-badge tone="primary">tone="primary" → brand</bd-badge>
232
+ `)}
229
233
  `
230
234
  };
@@ -220,7 +220,7 @@ class BentoGrid extends LitElement {
220
220
  <div class="bd-bentogrid-header">
221
221
  ${this.eyebrow
222
222
  ? html`
223
- <bd-p variant="regular" class="bd-bentogrid-eyebrow">
223
+ <bd-p variant="eyebrow" class="bd-bentogrid-eyebrow">
224
224
  <slot name="eyebrow">${this.eyebrow}</slot>
225
225
  </bd-p>
226
226
  `
@@ -4,8 +4,17 @@ import { css } from 'lit';
4
4
  export const blockquoteCSS = css`
5
5
  :host { display: block; }
6
6
 
7
+ /* .quote is the outer element: the <figure> when there is an attribution,
8
+ the bare <blockquote> when there is not. The bar and the type live on it,
9
+ so the caption inherits the quotation's family and leading the way the
10
+ <footer> did when it sat inside the <blockquote> -- the rhythm the note
11
+ below is about. */
12
+ figure,
7
13
  blockquote {
8
14
  margin: 0;
15
+ }
16
+
17
+ .quote {
9
18
  padding-left: var(--spacing-16);
10
19
  border-left: var(--border-width-4) solid var(--color-border-brand);
11
20
  font-family: var(--typography-body-large-fontFamily);
@@ -15,7 +24,7 @@ export const blockquoteCSS = css`
15
24
  color: var(--color-text-primary);
16
25
  }
17
26
 
18
- footer {
27
+ figcaption {
19
28
  margin-top: var(--spacing-8);
20
29
  font-size: var(--typography-caption-default-fontSize);
21
30
  /* no role: two roles are each a near-miss, from opposite directions, and
@@ -24,8 +33,8 @@ export const blockquoteCSS = css`
24
33
  .
25
34
  label-small matches this text's size and weight exactly -- but adopting
26
35
  it would add letterSpacing-wide, which has no job on mixed-case sentence
27
- text because nothing here crowds, and would replace the 1.625 leading
28
- this inherits from the quote it attributes with 1.5. That inherited
36
+ text because nothing here crowds, and would replace the leading this
37
+ inherits from the quote it attributes with 1.5. That inherited
29
38
  rhythm is the one thing in this rule that means something: the credit
30
39
  line sits on the same measure as the quotation it belongs to, where 1.5
31
40
  merely comes attached to the role.
@@ -17,12 +17,19 @@ export class BdBlockquote extends LitElement {
17
17
  this.attribution = '';
18
18
  }
19
19
 
20
+ /* WHERE THE ATTRIBUTION GOES.
21
+ It used to be a <footer> inside the <blockquote>, and everything inside a
22
+ blockquote is the quotation, so the name of the person quoted was read as
23
+ part of what they said. The HTML spec's pattern puts the credit outside
24
+ the quote: a <figure> holding the <blockquote> and a <figcaption>. With
25
+ no attribution there is no caption, so the bare <blockquote> renders, as
26
+ before -- a figure with nothing to name it would only add an empty
27
+ "figure" for a screen reader to step through. */
20
28
  render() {
21
- return html`
22
- <blockquote cite=${this.cite || nothing}>
23
- <slot></slot>${this.attribution ? html`<footer>\u2014 ${this.attribution}</footer>` : nothing}
24
- </blockquote>
25
- `;
29
+ const quote = html`<blockquote class=${this.attribution ? nothing : 'quote'} cite=${this.cite || nothing}><slot></slot></blockquote>`;
30
+ return this.attribution
31
+ ? html`<figure class="quote">${quote}<figcaption>\u2014 ${this.attribution}</figcaption></figure>`
32
+ : quote;
26
33
  }
27
34
  }
28
35
 
@@ -28,9 +28,11 @@ optional attribution.
28
28
  | \`cite\` | URL of the source | \`''\` |
29
29
 
30
30
  ### Notes
31
- \`attribution\` renders as a \`<footer>\` inside the \`<blockquote>\`, prefixed with
32
- an em dash. That is the pairing the HTML spec recommends for a credit that
33
- belongs to the quote itself.
31
+ \`attribution\` renders as a \`<figcaption>\`, prefixed with an em dash, beside
32
+ the \`<blockquote>\` inside a \`<figure>\`. That is the HTML spec's pattern for
33
+ crediting a quotation: everything inside a \`<blockquote>\` is the quote, so the
34
+ name of the person quoted belongs outside it. Without an attribution there is
35
+ no figure, only the \`<blockquote>\`.
34
36
 
35
37
  \`cite\` maps to the native \`cite\` attribute. It is **metadata only** — browsers
36
38
  do not render it and it is not a link, so if the source should be visible or
@@ -42,7 +44,7 @@ clickable, put it in \`attribution\` or in the slotted content.
42
44
  argTypes: {
43
45
  attribution: {
44
46
  control : 'text',
45
- description: 'Who said it. Renders as a footer inside the blockquote, prefixed with an em dash.',
47
+ description: 'Who said it. Renders as a figcaption beside the blockquote in a figure, prefixed with an em dash.',
46
48
  table : { type: { summary: 'string' }, defaultValue: { summary: "" }, category: 'Content' }
47
49
  },
48
50
  cite: {
@@ -79,6 +79,11 @@ export default css`
79
79
  boundary, so the value is switched rather than the rule */
80
80
  --bd-button-accent-ring: var(--button-accent-ring-default);
81
81
 
82
+ /* the focus ring is switched the same way: surface="dark" swaps the blue
83
+ outline for a white one. white and outline-white do not read this; see
84
+ FOCUS. */
85
+ --bd-button-focus-ring: var(--button-shared-focus-outline);
86
+
82
87
  --bd-button-border-width: var(--border-width-2);
83
88
  }
84
89
 
@@ -121,14 +126,29 @@ export default css`
121
126
  FOCUS
122
127
  The design specifies no focus state, but 2.4.7 requires one.
123
128
 
129
+ The ring is an outline held 2px off the control, and the 2px between is
130
+ left transparent, so it is whatever surface the button sits on. That is
131
+ the one thing code can do here that Figma cannot, and it is why the two
132
+ differ. Figma builds a ring from spread shadows, every band of which is a
133
+ painted colour, so its library paints the gap and keeps one ring per
134
+ surface: focus/ring-primary on light, focus/ring-inverse on dark and
135
+ brand. Painted in code, that gap is what went wrong twice: a white band
136
+ merged into a white fill on surface="dark", and the neutral-900 band
137
+ that replaced it drew a black outline round every button on a brand
138
+ hero. A transparent gap cannot do either, on any surface.
139
+
140
+ So only the colour switches: blue on light, white under surface="dark",
141
+ which covers a brand hero and a neutral one alike; the numbers are on
142
+ --focus-outline-traditional. Every variant takes the same ring, ghost
143
+ included. An outline is also kept in forced-colors mode, where a
144
+ box-shadow ring is not drawn at all.
145
+
146
+ The rule leaves box-shadow alone, so an outlined variant's own boundary
147
+ in --bd-button-edge stays put while focused.
148
+
124
149
  This used to be driven from JavaScript: two requestAnimationFrame hops
125
150
  checked :focus-visible and set a data attribute the CSS then styled.
126
- :focus-visible does that natively, so the detection is gone and the ring
127
- is the shared one from Foundations/Focus — a two-colour ring whose white
128
- inner band keeps it visible on a coloured hero, which the old
129
- single-colour outline could not do. It is composed with
130
- --bd-button-edge rather than replacing it, so an outlined variant keeps
131
- its own boundary while focused; see the note on that property.
151
+ :focus-visible does that natively.
132
152
  ============================================ */
133
153
  button:focus,
134
154
  a:focus {
@@ -136,21 +156,17 @@ export default css`
136
156
  }
137
157
 
138
158
  .button:focus-visible {
139
- outline: none;
140
- box-shadow: var(--focus-outline-primary), var(--bd-button-edge);
159
+ outline: var(--bd-button-focus-ring);
160
+ outline-offset: var(--button-shared-focus-outlineOffset);
141
161
  }
142
162
 
143
- /* A white control needs the ring the other way up. --focus-outline-primary
144
- puts its WHITE band against the control, which is what keeps it visible on
145
- a coloured hero -- and is exactly wrong when the control is itself white:
146
- measured on Button — White / Outline-White, the inner band was 1.00:1
147
- against the button's own fill and the blue band 2.38:1 against the hero
148
- (1.75:1 at the gradient's light end), so neither reached the 3:1 WCAG
149
- 2.4.11 asks. outline-white is here too because restoring its boundary
150
- above puts a white border where the white band used to sit on the hero. */
163
+ /* white and outline-white are presets for a coloured banner and ignore
164
+ surface, so they take the white ring on their own: 4.53:1 on blue-500,
165
+ 7.95:1 at the light end of the banner gradient. A blue ring there was
166
+ 2.38:1, and 1.75:1 at that end. */
151
167
  .button--white:focus-visible,
152
168
  .button--outline-white:focus-visible {
153
- box-shadow: var(--focus-outline-inverse), var(--bd-button-edge);
169
+ outline: var(--focus-outline-traditional-inverse);
154
170
  }
155
171
 
156
172
  a.button {
@@ -175,14 +191,14 @@ export default css`
175
191
  vertical-align: middle;
176
192
  box-sizing: border-box;
177
193
 
178
- /* The variant's own boundary, held in a custom property so the focus ring
179
- can COMPOSE with it. box-shadow is a single list, not additive, and
180
- .button:focus-visible (0,2,0) outranks .button--ghost (0,1,0) -- so a
181
- focus rule that declared box-shadow directly erased the border of every
182
- outlined variant. A focused ghost button lost its dark outline and wore
183
- a blue ring instead, which read as a tertiary button. The default is a
184
- zero-spread transparent shadow rather than the keyword none, so the value
185
- a valid shadow the focus rule can append to. */
194
+ /* The variant's own boundary, held in a custom property so each state can
195
+ switch it. It was introduced while the focus ring was a box-shadow, so
196
+ the ring could COMPOSE with it: box-shadow is a single list, not
197
+ additive, and .button:focus-visible (0,2,0) outranks .button--ghost
198
+ (0,1,0) -- so a focus rule that declared box-shadow directly erased the
199
+ border of every outlined variant, and a focused ghost button read as a
200
+ tertiary one. The ring is an outline now and does not touch box-shadow;
201
+ a focus rule that goes back to declaring it brings that defect back. */
186
202
  --bd-button-edge: 0 0 0 0 transparent;
187
203
  box-shadow: var(--bd-button-edge);
188
204
 
@@ -557,6 +573,11 @@ export default css`
557
573
  /* unchanged red, plus a boundary against the hero */
558
574
  --bd-button-accent-ring: var(--color-neutral-0);
559
575
 
576
+ /* a white ring, the one colour that clears 3:1 on a brand hero and a
577
+ neutral one alike, which this surface has to cover without knowing
578
+ which it is on. The gap inside it is the hero itself; see FOCUS. */
579
+ --bd-button-focus-ring: var(--focus-outline-traditional-inverse);
580
+
560
581
  /* muted downward instead of upward: a neutral-100 fill would read as an
561
582
  active control on a dark hero. Exempt from contrast under WCAG 1.4.3. */
562
583
  --bd-button-disabled-bg: var(--color-neutral-700); /* no role: on-dark ground; see the block note */
@@ -29,6 +29,14 @@ const SIZE_CLASS = { sm: "button--small", md: "button--medium", lg: "button--lar
29
29
  const SURFACES = ["light", "dark"];
30
30
  const SHAPES = ["default", "rounded"];
31
31
 
32
+ /* custom-click bubbles and is composed, like every sibling event in the
33
+ package -- bd-checkbox's and bd-radio's change, bd-switch's and
34
+ bd-custom-input's bd-change. It was a plain Event, which does not bubble,
35
+ so it reached a listener on the button itself and nothing above it: a form
36
+ or a card delegating from a wrapper heard nothing. composed carries it out
37
+ of any shadow root the button is rendered inside, too. */
38
+ const clickEvent = () => new CustomEvent("custom-click", { bubbles: true, composed: true });
39
+
32
40
  /**
33
41
  * Everything bd-button and bd-button-link share.
34
42
  *
@@ -42,31 +50,37 @@ class BdButtonBase extends LitElement {
42
50
  static styles = [buttonsCSS];
43
51
 
44
52
  static properties = {
45
- label : { type: String },
46
- size : { type: String },
47
- variant : { type: String, reflect: true },
53
+ label : { type: String },
54
+ size : { type: String },
55
+ variant : { type: String, reflect: true },
48
56
  /** @deprecated pass `variant`. kind meant four different things across the
49
57
  atoms; on this one it always meant appearance. */
50
- kind : { type: String },
51
- fullWidth : { type: Boolean, attribute: "fullwidth" },
52
- strong : { type: Boolean },
53
- disabled : { type: Boolean, reflect: true },
54
- loading : { type: Boolean, reflect: true },
55
- fontSize : { type: String, attribute: "font-size" },
56
- fontWeight: { type: String, attribute: "font-weight" },
57
- align : { type: String },
58
- surface : { type: String, reflect: true },
58
+ kind : { type: String },
59
+ fullWidth : { type: Boolean, attribute: "fullwidth" },
60
+ strong : { type: Boolean },
61
+ disabled : { type: Boolean, reflect: true },
62
+ loading : { type: Boolean, reflect: true },
63
+ fontSize : { type: String, attribute: "font-size" },
64
+ fontWeight : { type: String, attribute: "font-weight" },
65
+ align : { type: String },
66
+ surface : { type: String, reflect: true },
59
67
  /** "rounded" takes the pill corner; see .button--rounded. */
60
- shape : { type: String, reflect: true },
68
+ shape : { type: String, reflect: true },
61
69
  /** The accent variant reveals a trailing arrow on hover; this opts out. */
62
- noArrow : { type: Boolean, attribute: "no-arrow" },
70
+ noArrow : { type: Boolean, attribute: "no-arrow" },
63
71
  /* A circle holding one icon instead of a label: the homepage's audience
64
72
  cards and the mobile menu toggle. A mode, not a variant, because every
65
73
  variant, size, surface and state applies to it unchanged -- which is how
66
74
  the Button icon sets in the foundation Figma file are drawn. With no
67
75
  visible text there is nothing to name the control, so label becomes
68
76
  required rather than optional. */
69
- iconOnly : { type: Boolean, attribute: "icon-only", reflect: true }
77
+ iconOnly : { type: Boolean, attribute: "icon-only", reflect: true },
78
+ /* Extra text a screen reader reads after the name, through
79
+ aria-describedby on the inner control. It has to be carried in here:
80
+ the control is in this shadow root, so an aria-describedby written on
81
+ the host, or pointing at an id outside, never reaches it. bd-tooltip
82
+ sets it on a bd-button trigger; a page can set it directly. */
83
+ description: { type: String }
70
84
  };
71
85
 
72
86
  constructor() {
@@ -90,6 +104,7 @@ class BdButtonBase extends LitElement {
90
104
  this.shape = "default";
91
105
  this.noArrow = false;
92
106
  this.iconOnly = false;
107
+ this.description = "";
93
108
  }
94
109
 
95
110
  willUpdate(changed) {
@@ -162,6 +177,14 @@ class BdButtonBase extends LitElement {
162
177
  * lg 24 against padding 8, 12 and 16 -- so the size passes straight through
163
178
  * and the circle comes out 32, 44 and 56.
164
179
  */
180
+ get _describedBy() {
181
+ return this.description ? "description" : nothing;
182
+ }
183
+
184
+ get _description() {
185
+ return this.description ? html`<span id="description" hidden>${this.description}</span>` : nothing;
186
+ }
187
+
165
188
  get _iconContent() {
166
189
  if (this.loading) return html`<span class="spinner" aria-hidden="true"></span>`;
167
190
  return html`<slot><bd-arrow-icon size="${this.size}" aria-hidden="true"></bd-arrow-icon></slot>`;
@@ -173,7 +196,7 @@ class BdButtonBase extends LitElement {
173
196
  e.stopPropagation();
174
197
  return;
175
198
  }
176
- this.dispatchEvent(new Event("custom-click"));
199
+ this.dispatchEvent(clickEvent());
177
200
  }
178
201
  }
179
202
 
@@ -186,10 +209,11 @@ class Button extends BdButtonBase {
186
209
  class="${this._classes}"
187
210
  aria-label="${this.label || nothing}"
188
211
  aria-busy="${this.loading ? "true" : nothing}"
212
+ aria-describedby="${this._describedBy}"
189
213
  ?disabled="${this.disabled}"
190
214
  style="${this._inlineStyle}"
191
215
  @click=${this._handleClick}
192
- >${this._content}</button>
216
+ >${this._content}</button>${this._description}
193
217
  `;
194
218
  }
195
219
  }
@@ -211,35 +235,11 @@ class ButtonLink extends BdButtonBase {
211
235
  super();
212
236
  this.href = "";
213
237
  this.buyLink = false;
214
- this.shadow = true;
215
238
  this.align = "left";
216
239
  this.target = "";
217
240
  this.rel = "";
218
241
  }
219
242
 
220
- _onFocusin(e) {
221
- const el = e.currentTarget;
222
- requestAnimationFrame(() =>
223
- requestAnimationFrame(() => {
224
- if (el.matches(':focus-visible')) {
225
- this.setAttribute('data-bd-button-focus', '');
226
- }
227
- })
228
- );
229
- }
230
-
231
- _onFocusout() {
232
- this.removeAttribute('data-bd-button-focus');
233
- }
234
-
235
- get fullWidthClass() {
236
- return this.fullWidth ? "max--width" : "";
237
- }
238
-
239
- get strongClass() {
240
- return this.strong ? "button--bold" : "";
241
- }
242
-
243
243
  /* Reverse tabnabbing is the reason rather than convention: a new tab opened
244
244
  from here keeps a window.opener handle back unless rel says otherwise. An
245
245
  explicit rel always wins, so a consumer can still opt out deliberately. */
@@ -265,11 +265,12 @@ class ButtonLink extends BdButtonBase {
265
265
  aria-label="${this.label || nothing}"
266
266
  aria-busy="${this.loading ? "true" : nothing}"
267
267
  aria-disabled="${this.disabled ? "true" : nothing}"
268
+ aria-describedby="${this._describedBy}"
268
269
  style="${this._inlineStyle}"
269
270
  @click=${this._handleClick}
270
271
  ?data-store-render="${this.buyLink}"
271
272
  ?data-store-buy-link="${this.buyLink}"
272
- >${this._content}</a>
273
+ >${this._content}</a>${this._description}
273
274
  `;
274
275
  }
275
276
  }
@@ -297,7 +298,7 @@ class ContactButton extends LitElement {
297
298
  }
298
299
 
299
300
  _handleClick() {
300
- this.dispatchEvent(new Event("custom-click"));
301
+ this.dispatchEvent(clickEvent());
301
302
  }
302
303
 
303
304
  render() {
@@ -100,7 +100,7 @@ variant, so every \`variant\`, \`size\`, \`surface\` and state applies unchanged
100
100
  | \`right\` | Aligns button to the right |
101
101
 
102
102
  ### Events
103
- - \`custom-click\` — dispatched on click or Enter/Space keypress
103
+ - \`custom-click\` — dispatched on click or Enter/Space keypress. It bubbles and is composed, so a wrapper can listen for it
104
104
  `
105
105
  }
106
106
  }
@@ -430,7 +430,7 @@ export const ButtonKeyboard = {
430
430
  <p style="font-size:14px; margin-bottom:8px;">Tab to focus, press Enter or Space to trigger.</p>
431
431
  <bd-button variant="primary" size="md" label="Keyboard test">Press me</bd-button>
432
432
  `,
433
- parameters: { docs: { description: { story: 'Demonstrates keyboard activation. Enter/Space trigger `custom-click` and toggle the `active` class briefly.' } } }
433
+ parameters: { docs: { description: { story: 'Demonstrates keyboard activation. Enter and Space trigger `custom-click`.' } } }
434
434
  };
435
435
 
436
436
  // ─── bd-button-link Stories ────────────────────────────────────────────────
@@ -13,7 +13,17 @@ export const captionCSS = css`
13
13
  color: var(--typography-caption-default-color);
14
14
  }
15
15
 
16
+ /* sm takes the caption-small role whole. Today it differs from
17
+ caption-default in tracking alone -- 0.025em against 0, the design's
18
+ caption/small -- so reading only letter-spacing drew the same thing, but
19
+ left the role's other four axes unread, and a change to any of them would
20
+ never have reached the element. The colour stays the default's: the role
21
+ carries none of its own. */
16
22
  :host([size='sm']) figcaption {
23
+ font-family: var(--typography-caption-small-fontFamily);
24
+ font-size: var(--typography-caption-small-fontSize);
25
+ font-weight: var(--typography-caption-small-fontWeight);
26
+ line-height: var(--typography-caption-small-lineHeight);
17
27
  letter-spacing: var(--typography-caption-small-letterSpacing);
18
28
  }
19
29
  `;
@@ -23,11 +23,11 @@ export default {
23
23
  ### Props
24
24
  | Prop | Values | Default |
25
25
  |---|---|---|
26
- | \`size\` | \`default\`, \`small\` | \`default\` |
26
+ | \`size\` | \`md\`, \`sm\` — \`sm\` is the same size tracked wider (\`caption-small\`); \`default\` / \`small\` still resolve | \`md\` |
27
27
 
28
28
  ### Contrast
29
29
  The caption is \`--color-neutral-600\`, which is 6.59:1 on white and stays above
30
- 4.5:1 at the \`small\` size too. Caption text is the most common place a design
30
+ 4.5:1 at the \`sm\` size too. Caption text is the most common place a design
31
31
  system slips below AA, because it is both small and grey — anything lighter
32
32
  than \`neutral-600\` would not clear it.
33
33
 
@@ -46,9 +46,9 @@ and reach for this only where the styling alone is wanted.
46
46
  argTypes: {
47
47
  size: {
48
48
  control : { type: 'select' },
49
- options : ['default', 'small'],
49
+ options : ['md', 'sm'],
50
50
  description: 'Type size of the caption.',
51
- table : { type: { summary: 'string' }, defaultValue: { summary: "default" }, category: 'Appearance' }
51
+ table : { type: { summary: 'string' }, defaultValue: { summary: 'md' }, category: 'Appearance' }
52
52
  }
53
53
  }
54
54
  };