@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
@@ -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
  }
@@ -22,16 +22,19 @@ unknown. When you know the proportion done, use \`<bd-progress>\` instead.
22
22
  ### Props
23
23
  | Prop | Values | Default |
24
24
  |---|---|---|
25
- | \`size\` | \`small\`, \`default\`, \`large\` | \`default\` |
25
+ | \`size\` | \`sm\`, \`md\`, \`lg\` — \`small\` / \`default\` / \`large\` still resolve | \`md\` |
26
26
  | \`label\` | the accessible name | \`Loading\` |
27
27
 
28
28
  ### Accessibility
29
- It carries \`role="status"\` with \`aria-label\` from \`label\`, so the wait is
30
- announced politely rather than interrupting. Give it a specific \`label\` where
29
+ \`label\` is the text of a visually hidden \`role="status"\` region, so the wait
30
+ is announced politely rather than interrupting. The text arrives just after
31
+ the region does, which is what lets a screen reader treat it as news, and a
32
+ later change to \`label\` is announced too. Give it a specific \`label\` where
31
33
  you can — "Checking your licence" is more use than the default "Loading".
32
34
 
33
- Under \`prefers-reduced-motion\` it stops spinning and pulses instead, so it
34
- still reads as active without rotating.
35
+ Under \`prefers-reduced-motion\` it stops spinning: the arc stays, still and
36
+ at full contrast, and the disc behind it pulses, so it still reads as active
37
+ without rotating and never fades below 3:1.
35
38
 
36
39
  ### Note on colour
37
40
  The spinner's track and head are fixed at \`--color-neutral-200\` and
@@ -47,9 +50,9 @@ let the two converge.
47
50
  argTypes: {
48
51
  size: {
49
52
  control : { type: 'select' },
50
- options : ['small', 'default', 'large'],
53
+ options : ['sm', 'md', 'lg'],
51
54
  description: 'Diameter of the spinner.',
52
- table : { type: { summary: 'string' }, defaultValue: { summary: "default" }, category: 'Appearance' }
55
+ table : { type: { summary: 'string' }, defaultValue: { summary: 'md' }, category: 'Appearance' }
53
56
  },
54
57
  label: {
55
58
  control : 'text',
@@ -58,10 +58,22 @@ export const switchCSS = css`
58
58
  flex: none;
59
59
  }
60
60
 
61
+ /* The thumb's inset and travel are derived from the two boxes they sit
62
+ between, in px like those boxes. They were --spacing-2 and --spacing-16,
63
+ which are rem, inside a track that is --dimension-* and so is not: at a
64
+ 24px root the thumb rode 3px in and travelled 24px, and finished at 47px
65
+ in a 40px track. A knob has to stay on the track it belongs to at any text
66
+ size, so all four lengths are now one unit. Inset is half the height left
67
+ over; travel is the width left over after both insets: 2px and 16px. */
68
+ .track {
69
+ --bd-switch-inset: calc((var(--dimension-24) - var(--dimension-20)) / 2);
70
+ --bd-switch-travel: calc(var(--dimension-40) - var(--dimension-20) - 2 * var(--bd-switch-inset));
71
+ }
72
+
61
73
  .thumb {
62
74
  position: absolute;
63
- top: var(--spacing-2);
64
- left: var(--spacing-2);
75
+ top: var(--bd-switch-inset);
76
+ left: var(--bd-switch-inset);
65
77
  width: var(--dimension-20);
66
78
  height: var(--dimension-20);
67
79
  /* the knob on the track, white on both tracks: --color-action-on-fill,
@@ -81,13 +93,19 @@ export const switchCSS = css`
81
93
  If a second component ever needs it, that is when it earns a name. */
82
94
  label:hover .track { background: var(--color-neutral-500); }
83
95
  :host([checked]) label:hover .track { background: var(--color-action-primary-hover); }
84
- :host([checked]) .thumb { transform: translateX(var(--spacing-16)); }
96
+ :host([checked]) .thumb { transform: translateX(var(--bd-switch-travel)); }
85
97
 
86
98
  /* the one focus ring — see Foundations/Focus. On the row rather than the
87
99
  track, so the target and the indicator are the same shape here as on
88
100
  bd-checkbox and bd-radio. */
89
101
  label:has(input:focus-visible) {
90
102
  box-shadow: var(--focus-outline-primary);
103
+ /* forced-colors mode does not paint box-shadow, so the ring
104
+ vanishes there; it does paint an outline, in a system colour. This one
105
+ is transparent everywhere else, and the offset lands it on the ring's
106
+ outer band. */
107
+ outline: var(--border-width-2) solid transparent;
108
+ outline-offset: var(--focus-outline-offset);
91
109
  border-radius: var(--focus-outline-radius);
92
110
  }
93
111
 
@@ -31,7 +31,14 @@ export class BdSwitch extends LitElement {
31
31
  ?disabled=${this.disabled}
32
32
  @change=${e => {
33
33
  this.checked = e.target.checked;
34
- this.dispatchEvent(new CustomEvent('bd-change', { detail: this.checked }));
34
+ // bubbles and composed, as every sibling form atom's event is: with
35
+ // neither, it reached a listener on the switch and nothing above
36
+ // it, so a form wrapper delegating bd-change heard nothing
37
+ this.dispatchEvent(new CustomEvent('bd-change', {
38
+ detail : this.checked,
39
+ bubbles : true,
40
+ composed: true
41
+ }));
35
42
  }}
36
43
  />
37
44
  <span class="track"><span class="thumb"></span></span>
@@ -33,7 +33,8 @@ Content can also come from the default slot, which renders after \`label\` — s
33
33
  set one or the other, not both.
34
34
 
35
35
  ### Events
36
- \`bd-change\` — fires on toggle with \`detail\` set to the new boolean.
36
+ \`bd-change\` — fires on toggle with \`detail\` set to the new boolean. It bubbles and
37
+ is composed, so a form wrapper can listen for it on itself.
37
38
 
38
39
  ### Accessibility
39
40
  It is a real \`<input type="checkbox">\` with \`role="switch"\`, inside a
@@ -111,7 +111,12 @@ export const tabListCSS = css`
111
111
 
112
112
  .bd-tab-button:focus-visible {
113
113
  /* the one focus ring — see Foundations/Focus */
114
- outline: none;
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
  box-shadow: var(--focus-outline-primary);
116
121
  border-radius: var(--focus-outline-radius);
117
122
  }
@@ -32,6 +32,17 @@ class TerminalCursor extends LitElement {
32
32
  opacity: 0;
33
33
  }
34
34
  }
35
+
36
+ /* A cursor that blinks for as long as the page is open is exactly the
37
+ motion the setting exists for, and this one had no guard: every
38
+ other animated element in the package stops or softens under
39
+ prefers-reduced-motion. Here it stops, and the cursor stays shown --
40
+ a steady block still says "this is where the terminal is". */
41
+ @media (prefers-reduced-motion: reduce) {
42
+ .bd-terminal-cursor {
43
+ animation: none;
44
+ }
45
+ }
35
46
  `
36
47
  ];
37
48
 
@@ -160,7 +160,7 @@ class TerminalMessage extends LitElement {
160
160
  ${this.items && this.items.length
161
161
  ? html`
162
162
  <bd-list class="bd-terminalmsg-list">
163
- ${this.items.map((item) => html`<bd-li variant="bullet" size="sm" color="var(--color-neutral-0)">${item}</bd-li>`)}
163
+ ${this.items.map((item) => html`<bd-li variant="bullet" size="sm" color="inverse">${item}</bd-li>`)}
164
164
  </bd-list>
165
165
  `
166
166
  : nothing}
@@ -12,7 +12,12 @@ export default css`
12
12
 
13
13
  .bd-terms-text-a:focus-visible{
14
14
  /* the one focus ring — see Foundations/Focus */
15
- outline: none;
15
+ /* forced-colors mode does not paint box-shadow, so the ring
16
+ vanishes there; it does paint an outline, in a system colour. This one
17
+ is transparent everywhere else, and the offset lands it on the ring's
18
+ outer band. */
19
+ outline: var(--border-width-2) solid transparent;
20
+ outline-offset: var(--focus-outline-offset);
16
21
  box-shadow: var(--focus-outline-primary);
17
22
  border-radius: var(--focus-outline-radius);
18
23
  }
@@ -119,6 +119,11 @@ The vertical resize handle is left enabled, so a user can grow the field beyond
119
119
  control : 'boolean',
120
120
  description: 'Blocks editing.',
121
121
  table : { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'State' }
122
+ },
123
+ name: {
124
+ control : 'text',
125
+ description: 'Carried on the bd-change detail as { value, name }, for your own submit handling. NOT submitted with a form — the <textarea> is in a shadow root.',
126
+ table : { type: { summary: 'string' }, defaultValue: { summary: "''" }, category: 'Content' }
122
127
  }
123
128
  }
124
129
  };
@@ -127,6 +132,24 @@ export const Default = () => html`
127
132
  <bd-textarea label="What happened?" placeholder="Tell us what happened"></bd-textarea>
128
133
  `;
129
134
 
135
+ /**
136
+ * `name` reaches no form -- the <textarea> is in this element's shadow root --
137
+ * so it is carried on the event instead. Type, then click away: the line
138
+ * below shows the bd-change detail as it arrives.
139
+ */
140
+ export const NameOnTheEvent = {
141
+ name : 'name on the event',
142
+ render: () => html`
143
+ <bd-textarea
144
+ label="What happened?"
145
+ name="incident"
146
+ rows="2"
147
+ @bd-change=${e => { e.currentTarget.nextElementSibling.textContent = `bd-change detail: ${JSON.stringify(e.detail)}`; }}
148
+ ></bd-textarea>
149
+ <p style="font-family: var(--typography-fontFamily-mono); font-size: var(--typography-fontSize-sm)">bd-change detail: (type, then click away)</p>
150
+ `
151
+ };
152
+
130
153
  export const Sizes = {
131
154
  name : 'All Sizes',
132
155
  render: () => html`
@@ -64,8 +64,31 @@ export const tooltipCSS = css`
64
64
  transition: opacity var(--transition-duration-fast) var(--transition-easing-smooth);
65
65
  }
66
66
 
67
+ /* Shown means reachable. WCAG 1.4.13 asks that the pointer can move onto
68
+ content that appeared on hover without it disappearing. With
69
+ pointer-events: none left on, the pointer passed through the bubble to
70
+ whatever lay beneath it, the host lost :hover, and the bubble hid. The
71
+ bubble is in this shadow tree, so hovering it keeps :host(:hover). */
67
72
  :host(:hover) .bubble,
68
- :host(:focus-within) .bubble { opacity: 1; }
73
+ :host(:focus-within) .bubble { opacity: 1; pointer-events: auto; }
74
+
75
+ /* Escape hides it while the pointer or focus is still there; see the note
76
+ on _showing in tooltip.js. Same specificity as the rule above and later,
77
+ so it wins, and it takes pointer events away with the opacity. */
78
+ :host(:hover) .bubble--dismissed,
79
+ :host(:focus-within) .bubble--dismissed { opacity: 0; pointer-events: none; }
80
+
81
+ /* The bubble floats --spacing-8 above the trigger, and the pointer has to
82
+ cross that gap to reach it: this fills it, so the host stays hovered
83
+ across the way. It inherits pointer-events from the bubble, so it catches
84
+ nothing while the bubble is hidden. */
85
+ .bubble::after {
86
+ content: "";
87
+ position: absolute;
88
+ inset-inline: 0;
89
+ top: 100%;
90
+ bottom: calc(-1 * var(--spacing-8));
91
+ }
69
92
 
70
93
  @media (prefers-reduced-motion: reduce) {
71
94
  .bubble { transition: none; }