@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
@@ -3,7 +3,10 @@ import { css } from "lit";
3
3
  export const dividerHorizontal = css`
4
4
  .hr-line {
5
5
  border: none;
6
- border-top: var(--border-width-1) solid var(--color-border-default);
6
+ /* --color-border-container, the rule every bare divider already rendered:
7
+ it used to be --color-border-default dimmed to 0.25, which composites to
8
+ the same colour. See divider-horizontal.js. */
9
+ border-top: var(--border-width-1) solid var(--color-border-container);
7
10
  margin: 0;
8
11
  width: 100%;
9
12
  margin-left: auto;
@@ -33,12 +33,12 @@ Two divider components are available:
33
33
  control : { type: 'select' },
34
34
  options : ['primary', 'strong', 'muted', 'subtle', 'inverse', 'success', 'warning', 'error'],
35
35
  description: '(Horizontal) A semantic colour role, not a CSS colour.',
36
- table : { type: { summary: 'string' }, defaultValue: { summary: 'subtle' }, category: 'Horizontal' }
36
+ table : { type: { summary: 'string' }, defaultValue: { summary: "'' (--color-border-container)" }, category: 'Horizontal' }
37
37
  },
38
38
  hOpacity: {
39
39
  control : 'text',
40
- description: '(Horizontal) Opacity value between 0 and 1',
41
- table : { type: { summary: 'string' }, defaultValue: { summary: '0.25' }, category: 'Horizontal' }
40
+ description: '(Horizontal) Opacity from 0 to 1. Anything else is ignored. None by default.',
41
+ table : { type: { summary: 'string' }, defaultValue: { summary: "''" }, category: 'Horizontal' }
42
42
  },
43
43
  height: {
44
44
  control: { type: 'select' },
@@ -49,8 +49,8 @@ Two divider components are available:
49
49
  },
50
50
  thickness: {
51
51
  control : 'text',
52
- description: '(Vertical) Width/thickness of the line e.g. "1px"',
53
- table : { type: { summary: 'string' }, defaultValue: { summary: '1px' }, category: 'Vertical' }
52
+ description: '(Vertical) A border-width step — 0, 1, 2, 3, 4 or 8, also written "1px" etc. Resolves to var(--border-width-N); anything else is the 1px step.',
53
+ table : { type: { summary: 'string' }, defaultValue: { summary: '1' }, category: 'Vertical' }
54
54
  },
55
55
  vColor: {
56
56
  control : { type: 'select' },
@@ -71,7 +71,7 @@ Two divider components are available:
71
71
  export const HorizontalDefault = {
72
72
  name : 'Horizontal — Default',
73
73
  render : () => html`<bd-divider-horizontal></bd-divider-horizontal>`,
74
- parameters: { docs: { description: { story: 'Default horizontal divider with default color and 0.25 opacity.' } } }
74
+ parameters: { docs: { description: { story: 'Default horizontal divider: --color-border-container at full strength, no opacity.' } } }
75
75
  };
76
76
 
77
77
  export const HorizontalFullWidth = {
@@ -7,10 +7,26 @@ import dropdownCSS from "../dropdown/dropdown.css.js";
7
7
  import { resolveSize } from '../shared/prop-vocabulary.js';
8
8
 
9
9
 
10
+ /* options is a JSON array, as a property or as an attribute. Anything else --
11
+ malformed JSON, a comma list, a JSON object, a removed attribute -- is no
12
+ options rather than a crash. type: Array could not say that: Lit's Array
13
+ converter turns unparseable text into null before the setter sees it, and
14
+ render() then threw on null.map, so the field drew nothing at all. */
15
+ const toOptions = value => {
16
+ if (Array.isArray(value)) return value;
17
+ if (typeof value !== "string") return [];
18
+ try {
19
+ const parsed = JSON.parse(value);
20
+ return Array.isArray(parsed) ? parsed : [];
21
+ } catch {
22
+ return [];
23
+ }
24
+ };
25
+
10
26
  class CustomDropdown extends LitElement {
11
27
  static properties = {
12
28
  label : { type: String },
13
- options : { type: Array },
29
+ options : { converter: { fromAttribute: toOptions } },
14
30
  size : { type: String, reflect: true },
15
31
  /** @deprecated pass `size`. It named the size, not the kind. */
16
32
  kind : { type: String },
@@ -19,6 +35,12 @@ class CustomDropdown extends LitElement {
19
35
  invalid : { type: Boolean },
20
36
  validated: { type: Boolean },
21
37
  disabled : { type: Boolean },
38
+ /* The chosen option, and what the events carry, so a form handler written
39
+ for bd-custom-input reads this one unchanged. It always names an option
40
+ that exists: unset, or set to one that is not there, it is the first. */
41
+ value : { type: String },
42
+ /** Carried on the event detail, as on bd-custom-input. */
43
+ name : { type: String },
22
44
  /* Whether the native picker is showing. A best effort, not a fact: a
23
45
  <select> renders its popup in the OS, so the page is never told when it
24
46
  opens or closes. See _onSelectMouseDown. */
@@ -37,6 +59,8 @@ class CustomDropdown extends LitElement {
37
59
  this.invalid = false;
38
60
  this.validated = false;
39
61
  this.disabled = false;
62
+ this.value = "";
63
+ this.name = "";
40
64
  this._open = false;
41
65
  }
42
66
 
@@ -97,15 +121,7 @@ class CustomDropdown extends LitElement {
97
121
  }
98
122
 
99
123
  set options(val) {
100
- if (typeof val === "string") {
101
- try {
102
- this._options = JSON.parse(val);
103
- } catch {
104
- this._options = [];
105
- }
106
- } else {
107
- this._options = val;
108
- }
124
+ this._options = toOptions(val);
109
125
  }
110
126
 
111
127
  get options() {
@@ -116,6 +132,37 @@ class CustomDropdown extends LitElement {
116
132
  // so it moved; see shared/prop-vocabulary.js
117
133
  if (changed.has("kind") && this.kind) this.size = this.kind;
118
134
  if (changed.has("size") || changed.has("kind")) this.size = resolveSize(this.size);
135
+ if (!this.options.map(String).includes(this.value)) this.value = this.options.length ? String(this.options[0]) : "";
136
+ }
137
+
138
+ /* The select is told its value after its options exist. A binding on the
139
+ <select> would be committed before the options inside it rendered, and
140
+ an option's selected attribute stops counting once a visitor has chosen,
141
+ so neither could move the selection when value is set from outside. */
142
+ updated() {
143
+ const select = this.shadowRoot.querySelector("select");
144
+ if (select && select.value !== this.value) select.value = this.value;
145
+ }
146
+
147
+ /* WHY TWO EVENTS FOR ONE CHOICE.
148
+ This dispatched nothing: choosing an option changed nothing observable
149
+ from outside, and the chosen value could only be read by reaching into
150
+ the shadow root. The native change and input do not help -- change is not
151
+ composed, so it never leaves the shadow root, and input arrives
152
+ retargeted to the host with no value on it.
153
+
154
+ A select commits the moment an option is chosen, so its input and change
155
+ are the same moment. Both are sent anyway, with bd-custom-input's names,
156
+ detail and flags, so a handler written for a form of inputs -- live
157
+ validation on bd-input, saving on bd-change -- needs nothing special for
158
+ this field. */
159
+ _onChange(e) {
160
+ this._closePicker();
161
+ this.value = e.target.value;
162
+ const detail = { value: this.value, name: this.name };
163
+ for (const type of ["bd-input", "bd-change"]) {
164
+ this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
165
+ }
119
166
  }
120
167
 
121
168
  render() {
@@ -145,6 +192,10 @@ class CustomDropdown extends LitElement {
145
192
  ? "form-help-text--success"
146
193
  : "form-help-text--default";
147
194
 
195
+ // The hint is the select's description, and the state is announced, not
196
+ // only drawn: invalid used to be a red border alone and the hint was never
197
+ // pointed at, so the reason a field was invalid reached a screen reader
198
+ // not at all. Both as bd-custom-input does them.
148
199
  return html`
149
200
  <div class="input-container">
150
201
  ${this.label
@@ -161,13 +212,16 @@ class CustomDropdown extends LitElement {
161
212
  class="form-input-bd ${inputClass} ${inputStateClass}"
162
213
  ?required="${this.required}"
163
214
  ?disabled="${this.disabled}"
215
+ aria-invalid="${this.invalid ? "true" : nothing}"
216
+ aria-describedby="${this.tooltip ? "custom-dropdown-help" : nothing}"
164
217
  @mousedown="${this._onSelectMouseDown}"
165
218
  @keydown="${this._onSelectKeyDown}"
166
- @change="${this._closePicker}"
219
+ name="${this.name || nothing}"
220
+ @change="${this._onChange}"
167
221
  @blur="${this._closePicker}"
168
222
  >
169
223
  ${this.options.map(
170
- (option, index) => html`<option value="${option}" ?selected="${index === 0}">
224
+ option => html`<option value="${option}" ?selected="${String(option) === this.value}">
171
225
  ${option}
172
226
  </option>`
173
227
  )}
@@ -178,7 +232,7 @@ class CustomDropdown extends LitElement {
178
232
  ></bd-arrow-down-icon>
179
233
  </div>
180
234
  ${this.tooltip
181
- ? html`<div class="form-help-text ${helpTextClass}">
235
+ ? html`<div id="custom-dropdown-help" class="form-help-text ${helpTextClass}">
182
236
  ${this.tooltip}
183
237
  </div>`
184
238
  : ""}
@@ -55,20 +55,21 @@ descender.
55
55
  stories under **Atoms/Input**. They are one component; the Atoms/Input page
56
56
  carries the fuller write-up.
57
57
 
58
- ### Known gaps
59
- Three things the sibling \`bd-custom-input\` has and this does not. They are
60
- listed here because the states above imply a parity that is not there yet:
61
-
62
- - **No \`value\` and no event.** Nothing is emitted when a selection changes, so
63
- the chosen option cannot be read without reaching into the shadow root. The
64
- input's \`value\` property plus \`bd-input\` / \`bd-change\` is the pattern to
65
- copy.
66
- - **\`invalid\` is colour only.** It sets a red border but no
67
- \`aria-invalid="true"\`, so a screen reader is told nothing. The input sets it.
68
- - **\`tooltip\` is not announced.** It renders as help text but the select does
69
- not point at it with \`aria-describedby\`, so when it carries the reason a
70
- field is invalid, that reason is visual only.
71
-
58
+ ### Value and events
59
+ \`value\` is the chosen option — read it, or set it to choose one; unset, or
60
+ set to an option that is not there, it is the first. Choosing an option fires
61
+ \`bd-input\` and then \`bd-change\`, both with \`detail\` \`{ value, name }\`,
62
+ bubbling and composed — the same names, shape and flags as
63
+ \`bd-custom-input\`, so one form handler reads both. A select commits on choice,
64
+ so the two arrive together.
65
+
66
+ ### Accessibility
67
+ \`invalid\` sets \`aria-invalid="true"\` on the select as well as the red
68
+ border, and \`tooltip\` is the select's description through
69
+ \`aria-describedby\`, so the reason a field is invalid is announced, not only
70
+ drawn — as on \`bd-custom-input\`.
71
+
72
+ ### Known gap
72
73
  Being in a shadow root, the select also does not participate in an enclosing
73
74
  \`<form>\` — a \`<form>\` containing it yields no \`FormData\` entry.
74
75
  `
@@ -1,8 +1,7 @@
1
1
  import { html } from 'lit';
2
2
  import './fieldset.js';
3
3
  import '../switch/switch.js';
4
- import '../label/label.js';
5
- import '../help-text/help-text.js';
4
+ import '../radio/radio.js';
6
5
  import '../input/input.js';
7
6
 
8
7
  export default {
@@ -37,6 +36,14 @@ they are choosing between. The \`legend\` is what supplies that.
37
36
  Because slotted controls sit inside the \`<fieldset>\` in the flattened tree, the
38
37
  native grouping does apply here — unlike a \`for\` reference, which cannot cross
39
38
  the shadow boundary.
39
+
40
+ ### Labelling the fields inside it
41
+ Give each field its own label and hint through its own props —
42
+ \`label\`, \`required\` and \`tooltip\` on \`bd-custom-input\` — not a
43
+ \`<bd-label for>\` and a \`<bd-help-text>\` beside it. The field keeps its
44
+ \`<input>\` in its own shadow root, and a \`for\` or an \`aria-describedby\`
45
+ in another tree resolves to nothing there: the field is then named only by its
46
+ placeholder, which disappears as soon as anyone types.
40
47
  `
41
48
  }
42
49
  }
@@ -50,6 +57,22 @@ the shadow boundary.
50
57
  }
51
58
  };
52
59
 
60
+ /**
61
+ * The case the docs are about: a choice among radios, named by the legend.
62
+ * Each radio's own label says only "Basic" or "Premium"; the legend is what
63
+ * says they are plans. It renders the usage example above, so the page shows
64
+ * the grouping it describes rather than only asserting it.
65
+ */
66
+ export const ChooseOne = {
67
+ name : 'Choose one — radios under a legend',
68
+ render: () => html`
69
+ <bd-fieldset legend="Choose your plan">
70
+ <bd-radio name="plan" value="basic" label="Basic"></bd-radio>
71
+ <bd-radio name="plan" value="premium" label="Premium"></bd-radio>
72
+ </bd-fieldset>
73
+ `
74
+ };
75
+
53
76
  export const Default = () => html`
54
77
  <bd-fieldset legend="Notifications">
55
78
  <bd-switch label="Email"></bd-switch>
@@ -63,22 +86,24 @@ export const Default = () => html`
63
86
  * `--form-help-text-default-marginTop`. None of those three is visible in a
64
87
  * fieldset of bare switches, so this story is the one that shows them — it is
65
88
  * where a change to the form spacing tokens actually appears.
89
+ *
90
+ * Each label and hint is the field's own, through its label / required /
91
+ * tooltip props. This story used to pair <bd-label for="line1"> and a
92
+ * <bd-help-text> with an unlabelled field, and no for in it resolved: the
93
+ * three fields were named by their placeholders and the hints described
94
+ * nothing. The field's own props wire the label and aria-describedby inside
95
+ * its shadow root, where the <input> is.
66
96
  */
67
97
  export const FieldStack = () => html`
68
98
  <bd-fieldset legend="Billing address">
69
- <div>
70
- <bd-label for="line1" required>Street</bd-label>
71
- <bd-custom-input label="" size="md" placeholder="221B Baker Street"></bd-custom-input>
72
- <bd-help-text>Include the building number.</bd-help-text>
73
- </div>
74
- <div>
75
- <bd-label for="city" required>City</bd-label>
76
- <bd-custom-input label="" size="md" placeholder="London"></bd-custom-input>
77
- </div>
78
- <div>
79
- <bd-label for="post">Postcode</bd-label>
80
- <bd-custom-input label="" size="md" placeholder="NW1 6XE"></bd-custom-input>
81
- <bd-help-text tone="error">We could not find that postcode.</bd-help-text>
82
- </div>
99
+ <bd-custom-input
100
+ label="Street" required size="md" placeholder="221B Baker Street"
101
+ tooltip="Include the building number."
102
+ ></bd-custom-input>
103
+ <bd-custom-input label="City" required size="md" placeholder="London"></bd-custom-input>
104
+ <bd-custom-input
105
+ label="Postcode" size="md" placeholder="NW1 6XE"
106
+ invalid tooltip="We could not find that postcode."
107
+ ></bd-custom-input>
83
108
  </bd-fieldset>
84
109
  `;
@@ -37,7 +37,12 @@ export default css`
37
37
  Spelled ::slotted, which is the only selector that crosses this boundary,
38
38
  and :focus-visible so a mouse click does not draw it. */
39
39
  ::slotted(a:focus-visible) {
40
- outline: none;
40
+ /* forced-colors mode does not paint box-shadow, so the ring
41
+ vanishes there; it does paint an outline, in a system colour. This one
42
+ is transparent everywhere else, and the offset lands it on the ring's
43
+ outer band. */
44
+ outline: var(--border-width-2) solid transparent;
45
+ outline-offset: var(--focus-outline-offset);
41
46
  box-shadow: var(--focus-outline-primary);
42
47
  border-radius: var(--focus-outline-radius);
43
48
  }
@@ -36,7 +36,12 @@ export default css`
36
36
  .country-toggle:focus,
37
37
  .close-countries-button:focus,
38
38
  .social-icons a:focus {
39
- outline: none;
39
+ /* forced-colors mode does not paint box-shadow, so the ring
40
+ vanishes there; it does paint an outline, in a system colour. This one
41
+ is transparent everywhere else, and the offset lands it on the ring's
42
+ outer band. */
43
+ outline: var(--border-width-2) solid transparent;
44
+ outline-offset: var(--focus-outline-offset);
40
45
  /* The ring is --focus-outline-primary alone, as in every other component.
41
46
  The 2px border that stood here was redundant and, since these elements
42
47
  have no border otherwise, it shifted the layout by 2px on focus. */
@@ -176,7 +176,7 @@ class HeaderCode extends LitElement {
176
176
  </div>
177
177
 
178
178
  ${this.eyebrow
179
- ? html`<bd-p variant="regular" class="bd-headercode-eyebrow">${this.eyebrow}</bd-p>`
179
+ ? html`<bd-p variant="eyebrow" class="bd-headercode-eyebrow">${this.eyebrow}</bd-p>`
180
180
  : nothing}
181
181
 
182
182
  <bd-h as="${this._headingTag}" visual-level="h1" class="bd-headercode-title">
@@ -36,7 +36,12 @@ export default css`
36
36
  failure mode as the bare descendant selector this replaced. */
37
37
  ::slotted(a:focus-visible) {
38
38
  /* the one focus ring — see Foundations/Focus */
39
- outline: none;
39
+ /* forced-colors mode does not paint box-shadow, so the ring
40
+ vanishes there; it does paint an outline, in a system colour. This one
41
+ is transparent everywhere else, and the offset lands it on the ring's
42
+ outer band. */
43
+ outline: var(--border-width-2) solid transparent;
44
+ outline-offset: var(--focus-outline-offset);
40
45
  box-shadow: var(--focus-outline-primary);
41
46
  border-radius: var(--focus-outline-radius);
42
47
  }
@@ -736,9 +741,9 @@ export default css`
736
741
  :host { height: var(--bd-icon-size, ...) }, and a consuming rule and a
737
742
  :host rule match the SAME element, where the outer tree wins -- the same
738
743
  mechanism scripts/atom-inert-properties.json records for colour on a
739
- bd-p host. --bd-icon-size, which the element sets inline from its size prop, is
740
- deliberately not what is overridden here: an inline custom property
741
- would beat this rule.
744
+ bd-p host. Setting --bd-icon-size here would work as well, since the
745
+ atom now sets it from a :host rule rather than inline; height is kept
746
+ because it was measured, and it says the job directly.
742
747
  .
743
748
  sm on the icon scale is 16px, so the mark renders 121 wide -- 47% of a
744
749
  256px panel. --bd-header-mark-height above is set to the same step, so
@@ -8,25 +8,42 @@ export default {
8
8
  docs: {
9
9
  description: {
10
10
  component: `
11
- **BdH** is a Lit heading component that renders semantic HTML heading elements (h1–h6) with configurable styling.
11
+ **BdH** renders a semantic heading, \`<h1>\`–\`<h6>\`, on the system's type scale.
12
12
 
13
13
  ### Usage
14
14
  \`\`\`html
15
15
  <bd-h as="h1">Page Title</bd-h>
16
+ <bd-h as="h2" visual-level="h3">A component section inside a page section</bd-h>
16
17
  <bd-h as="h3" color="primary" fontWeight="bold" paddingBottom="16">Section Title</bd-h>
17
18
  \`\`\`
18
19
 
19
- ### Valid Heading Tags
20
- \`h1\`, \`h2\`, \`h3\`, \`h4\`, \`h5\`, \`h6\` — falls back to \`h1\` for invalid values.
21
-
22
- ### Style Prop Validation
23
- - **fontWeight**: accepts numeric strings (\`"100"\`–\`"900"\`) and keyword values (\`"normal"\`, \`"bold"\`, \`"lighter"\`, \`"bolder"\`)
24
- - **color**: accepts \`var(--...)\`, \`#hex\`, \`rgb(...)\`, \`hsl(...)\` — rejects bare color names
25
- - **padding / paddingBottom**: any valid CSS value string
26
-
27
- ### Notes
28
- - \`ariaLevel\` can override the implicit ARIA level for accessibility
29
- - Styles are applied inline via \`_computeStyles()\`
20
+ ### Level and size are two questions
21
+ \`as\` is the document outline — which heading element is rendered — and
22
+ belongs to the composing page. \`visual-level\` is the type role — which step of
23
+ the heading scale it looks like — and follows \`as\` when omitted. Both take
24
+ \`h1\`–\`h6\` or \`1\`–\`6\`. An \`as\` that is not a heading level renders an
25
+ \`h2\`: still a heading, never the page's \`h1\`.
26
+
27
+ Change the size with \`visual-level\`, not with \`aria-level\`: an \`aria-level\`
28
+ that disagrees with the element tells assistive technology one level and the
29
+ outline another.
30
+
31
+ ### Style props take steps, not CSS
32
+ Each resolves to a token; a value that is not a step is dropped and the
33
+ heading keeps its own styling.
34
+
35
+ - **fontWeight** — \`light\`, \`normal\`, \`medium\`, \`semibold\`, \`bold\`, each
36
+ \`var(--typography-fontWeight-*)\`. The scale stops at \`bold\`; numbers are
37
+ not accepted.
38
+ - **color** — a text role, not a CSS colour: \`primary\` (the brand text role,
39
+ \`--color-text-brand\`), \`strong\` (\`--color-text-primary\`), \`muted\`,
40
+ \`subtle\`, \`inverse\`, \`success\`, \`warning\`, \`error\`. Hex, \`rgb()\`,
41
+ \`var()\` and colour keywords are all dropped.
42
+ - **padding** — one to four spacing steps, e.g. \`24 40\`, each
43
+ \`var(--spacing-N)\`; if any part is off the scale the whole value is dropped.
44
+ **paddingBottom** — one spacing step.
45
+
46
+ The resolved values are applied as an inline style on the rendered heading.
30
47
  `
31
48
  }
32
49
  }
@@ -35,12 +52,18 @@ export default {
35
52
  as: {
36
53
  control : { type: 'select' },
37
54
  options : ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
38
- description: 'Which heading tag to render. Falls back to `h1` if invalid.',
55
+ description: 'Which heading element to render — the document outline. Falls back to `h2` if it is not a heading level.',
39
56
  table : { type: { summary: "'h1'|'h2'|'h3'|'h4'|'h5'|'h6'" }, defaultValue: { summary: 'h1' }, category: 'Semantic' }
40
57
  },
58
+ visualLevel: {
59
+ control : { type: 'select' },
60
+ options : ['', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
61
+ description: 'Which step of the heading type scale to render. Follows `as` when empty.',
62
+ table : { type: { summary: "'h1'…'h6' | '1'…'6'" }, defaultValue: { summary: 'as' }, category: 'Semantic' }
63
+ },
41
64
  ariaLevel: {
42
65
  control : 'number',
43
- description: 'Overrides the implicit ARIA level (e.g. for accessibility when visual level differs from semantic level)',
66
+ description: 'Overrides the element\'s ARIA level. Rarely right: to make a heading look a different size, use `visual-level`; to change its place in the outline, change `as`.',
44
67
  table : { type: { summary: 'number' }, defaultValue: { summary: 'null' }, category: 'Accessibility' }
45
68
  },
46
69
  padding: {
@@ -73,6 +96,7 @@ export default {
73
96
  const render = (args) => html`
74
97
  <bd-h
75
98
  as="${args.as}"
99
+ visual-level="${args.visualLevel}"
76
100
  padding="${args.padding}"
77
101
  paddingBottom="${args.paddingBottom}"
78
102
  fontWeight="${args.fontWeight}"
@@ -82,7 +106,7 @@ const render = (args) => html`
82
106
  </bd-h>
83
107
  `;
84
108
 
85
- const defaultArgs = { as: 'h2', padding: '', paddingBottom: '', fontWeight: '', color: '' };
109
+ const defaultArgs = { as: 'h2', visualLevel: '', padding: '', paddingBottom: '', fontWeight: '', color: '' };
86
110
 
87
111
  // ─── Stories ───────────────────────────────────────────────────────────────
88
112
 
@@ -111,13 +135,13 @@ export const CustomColor = {
111
135
  name : 'Custom Color',
112
136
  render: () => html`
113
137
  <div style="display:flex; flex-direction:column; gap:12px;">
114
- <bd-h as="h2" color="primary">primary — var(--color-blue-600)</bd-h>
115
- <bd-h as="h2" color="error">error — var(--color-red-700)</bd-h>
116
- <bd-h as="h2" color="success">success — var(--color-green-800)</bd-h>
117
- <bd-h as="h2" color="strong">strong — var(--color-neutral-900)</bd-h>
138
+ <bd-h as="h2" color="primary">primary — --color-text-brand</bd-h>
139
+ <bd-h as="h2" color="error">error — --color-text-error</bd-h>
140
+ <bd-h as="h2" color="success">success — --color-text-success</bd-h>
141
+ <bd-h as="h2" color="strong">strong — --color-text-primary</bd-h>
118
142
  </div>
119
143
  `,
120
- parameters: { docs: { description: { story: 'Headings with hex colors and a CSS variable token. Bare color names like "blue" are rejected by `_getValidColor()`.' } } }
144
+ parameters: { docs: { description: { story: '`color` takes a text role and resolves it to that role\'s token. Each label names the role it reads, not today\'s value.' } } }
121
145
  };
122
146
 
123
147
  export const FontWeights = {
@@ -131,7 +155,7 @@ export const FontWeights = {
131
155
  <bd-h as="h3" fontWeight="bold">bold (700)</bd-h>
132
156
  </div>
133
157
  `,
134
- parameters: { docs: { description: { story: 'Various font weights from light (300) to black (900). Invalid weights like "purple" are silently ignored.' } } }
158
+ parameters: { docs: { description: { story: 'The five weight steps, light (300) to bold (700); the scale has nothing heavier. A value that is not a step is ignored.' } } }
135
159
  };
136
160
 
137
161
  export const CustomPadding = {
@@ -144,37 +168,46 @@ export const CustomPadding = {
144
168
  <bd-h as="h3" paddingBottom="32">With Padding Bottom Only</bd-h>
145
169
  </div>
146
170
  `,
147
- parameters: { docs: { description: { story: 'Demonstrates `padding` shorthand and `paddingBottom` applied inline.' } } }
171
+ parameters: { docs: { description: { story: '`padding` takes one to four spacing steps and `paddingBottom` one, applied inline.' } } }
172
+ };
173
+
174
+ export const VisualLevel = {
175
+ name : 'Level and size apart',
176
+ render: () => html`
177
+ <bd-h as="h2" visual-level="h4">An h2 in the outline, at the h4 size</bd-h>
178
+ <bd-h as="h4" visual-level="h2">An h4 in the outline, at the h2 size</bd-h>
179
+ `,
180
+ parameters: { docs: { description: { story: '`as` sets the element and `visual-level` the type step, so a heading keeps its place in the outline at whatever size the design needs. This is the tool for a level that has to look different — not `aria-level`.' } } }
148
181
  };
149
182
 
150
183
  export const AriaLevelOverride = {
151
184
  name : 'ARIA Level Override',
152
185
  render: () => html`
153
186
  <p style="font-size:14px; margin-bottom:8px;">
154
- Visually renders as <code>h4</code> but has <code>aria-level="2"</code>.
187
+ An <code>h4</code> element carrying <code>aria-level="2"</code>.
155
188
  </p>
156
189
  <bd-h as="h4" aria-level="2" color="primary">
157
- Visually h4, semantically level 2
190
+ An h4 element, announced as level 2
158
191
  </bd-h>
159
192
  `,
160
- parameters: { docs: { description: { story: '`ariaLevel` overrides the implicit ARIA heading level for accessibility without changing the visual rendering.' } } }
193
+ parameters: { docs: { description: { story: '`ariaLevel` overrides the level assistive technology announces without changing the element. The element and the announced level then disagree, so prefer `as` with `visual-level` (see "Level and size apart"); this is for the rare page that cannot change the element.' } } }
161
194
  };
162
195
 
163
196
  export const InvalidTag = {
164
- name : 'Invalid Tag (fallback to h1)',
197
+ name : 'Invalid Tag (fallback to h2)',
165
198
  render: () => html`
166
- <bd-h as="div">This falls back to h1</bd-h>
199
+ <bd-h as="div">This falls back to h2</bd-h>
167
200
  `,
168
- parameters: { docs: { description: { story: '`as="div"` is not a valid heading tag — component falls back to rendering `<h1>`.' } } }
201
+ parameters: { docs: { description: { story: '`as="div"` is not a heading level, so the component renders an `<h2>` — still a heading, never the page\'s `<h1>`.' } } }
169
202
  };
170
203
 
171
204
  export const InvalidColor = {
172
205
  name : 'Invalid Color (silently ignored)',
173
206
  render: () => html`
174
207
  <bd-h as="h3" color="blue">A bare CSS keyword is not a role, so it is dropped and the heading keeps its own colour</bd-h>
175
- <bd-h as="h3" color="primary">color="primary" resolves to var(--color-blue-600)</bd-h>
208
+ <bd-h as="h3" color="primary">color="primary" resolves to --color-text-brand</bd-h>
176
209
  `,
177
- parameters: { docs: { description: { story: 'Bare color name `"blue"` is rejected by `_getValidColor()` and falls back to the default color. `"#026DFF"` is accepted.' } } }
210
+ parameters: { docs: { description: { story: '`color="blue"` is not a role, so it is dropped and the heading keeps its own colour. Hex values and `var()` are dropped the same way.' } } }
178
211
  };
179
212
 
180
213
  export const Combined = {
@@ -191,5 +224,5 @@ export const Playground = {
191
224
  name : '🛝 Playground',
192
225
  args : { ...defaultArgs },
193
226
  render,
194
- parameters: { docs: { description: { story: 'Interactive playground. Adjust `as`, `color`, `fontWeight`, `padding`, and `paddingBottom` via Controls.' } } }
227
+ parameters: { docs: { description: { story: 'Interactive playground. Adjust `as`, `visual-level`, `color`, `fontWeight`, `padding`, and `paddingBottom` via Controls.' } } }
195
228
  };