@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
@@ -23,7 +23,7 @@ there is no image.
23
23
  |---|---|---|
24
24
  | \`src\` | image URL — omit to fall back to initials | \`''\` |
25
25
  | \`name\` | the person's name | \`''\` |
26
- | \`size\` | \`small\`, \`default\`, \`large\` | \`default\` |
26
+ | \`size\` | \`sm\`, \`md\`, \`lg\` — \`small\` / \`default\` / \`large\` still resolve | \`md\` |
27
27
 
28
28
  ### Always pass a name
29
29
  \`name\` does two jobs. It is the source of the initials when there is no \`src\`,
@@ -31,9 +31,13 @@ and it is the accessible name in both cases — the wrapper carries
31
31
  \`role="img"\` with \`aria-label\` from \`name\`, and the inner image is given an
32
32
  empty \`alt\` so it is not announced twice.
33
33
 
34
- Without a \`name\` the accessible name falls back to the literal string
35
- "Avatar", which tells a screen reader user nothing. Pass one even when you have
36
- an image.
34
+ Without a \`name\` the avatar has nothing to say, so it is hidden from assistive
35
+ technology rather than announced — it used to be an image named "Avatar", which
36
+ tells a screen reader user nothing. Pass one even when you have an image.
37
+
38
+ ### When the image does not load
39
+ A \`src\` that fails to load falls back to the initials, the same as no \`src\`
40
+ at all. A new \`src\` gets its own chance to load.
37
41
 
38
42
  ### How the initials are derived
39
43
  The first letter of each whitespace-separated word, capped at two characters
@@ -57,9 +61,9 @@ first and last.
57
61
  },
58
62
  size: {
59
63
  control : { type: 'select' },
60
- options : ['small', 'default', 'large'],
64
+ options : ['sm', 'md', 'lg'],
61
65
  description: 'Diameter of the avatar.',
62
- table : { type: { summary: 'string' }, defaultValue: { summary: "default" }, category: 'Appearance' }
66
+ table : { type: { summary: 'string' }, defaultValue: { summary: 'md' }, category: 'Appearance' }
63
67
  }
64
68
  }
65
69
  };
@@ -203,136 +203,21 @@ export const awardsFlexCSS = css`
203
203
  margin: 0;
204
204
  }
205
205
 
206
- /* === DOTS === */
207
- /* Dots and the pause control share one row; the dots keep their own spacing
208
- so the treatment is unchanged when no control is shown. */
209
- .bd-awardsflex-controls {
210
- display: flex;
211
- align-items: center;
212
- gap: var(--spacing-16);
213
- }
214
-
215
- .bd-awardsflex-dots {
216
- display: flex;
217
- /* The buttons below carry their own 24px of target, so the space between
218
- two dots now comes from the boxes rather than from a gap. Adding one on
219
- top would push them apart without making either easier to hit. */
220
- gap: 0;
206
+ /* === SLIDE CONTROLS ===
207
+ bd-pagination-dots, which also runs the autoplay clock. This file used to
208
+ draw its own dots and pause button -- a dot row that called itself a
209
+ tablist with no tabpanels, resting dots at neutral-300 (2.10:1 on the
210
+ tint, under the 3:1 a control needs), and a setInterval the dots knew
211
+ nothing about. The atom owns all of that now; the margin is all that is
212
+ left to decide here. */
213
+ bd-pagination-dots {
221
214
  margin-top: var(--spacing-8);
222
215
  }
223
216
 
224
- .bd-awardsflex-autoplay-toggle {
225
- margin-top: var(--spacing-8);
226
- padding: var(--spacing-2) var(--spacing-8);
227
- background: transparent;
228
- border: var(--border-width-1) solid var(--color-border-default);
229
- border-radius: var(--radius-full);
230
- color: var(--color-text-muted);
231
- font-family: var(--typography-fontFamily-sans);
232
- /* no role: four roles sit at this size and each fails differently, so the
233
- size stays a primitive. Note the weight is not declared here at all --
234
- it inherits 400 -- which is why two of these are about what adopting a
235
- role would ADD.
236
- .
237
- caption-default matches every value exactly, including the colour token:
238
- xs / 400 / 1.5 / tracking 0 / sans, and its own colour axis is
239
- --color-text-muted, which is what this rule already declares two lines
240
- up. It renders as the same pixels. It is declined anyway, because
241
- vertical-rhythm.stories.js documents it as "text under an image or a
242
- figure" and this is the visible label of a toggle button. A
243
- five-axis-plus-colour identity is the most seductive form of choosing a
244
- role by its value rather than its meaning -- not an exemption from
245
- CLAUDE.md's rule against that, but the case the rule is for.
246
- .
247
- caption-small carries the same wrong meaning and adds
248
- letterSpacing-wide, which has no job on a five-letter word.
249
- .
250
- label-small is the right family -- "a small label: a badge, a tag, a
251
- field hint" -- but the wrong result: it would declare a weight of 500
252
- where nothing is declared today, and add wide tracking, making the
253
- control bolder and 2.2px wider. This control is deliberately recessive:
254
- muted, 2px of vertical padding, sitting beside the carousel dots as
255
- chrome whose job is not to compete with the quote cards. Adopting a role
256
- should not invert that.
257
- .
258
- control-label-small is the right meaning -- built for the name of a
259
- control -- and fails on the leading. See the line below. */
260
- font-size: var(--typography-fontSize-xs);
261
- /* no role: and this leading in particular cannot move. The line box is
262
- 18px, which with 2px of padding and a 1px border makes the button
263
- exactly 24px -- the same 24px as the .bd-awardsflex-dot buttons it shares
264
- a row with, which are 24 square deliberately for the WCAG 2.5.8 target
265
- size the comment further down explains.
266
- .
267
- control-label-small would take the line box to 15px and the button to
268
- 21px, misaligning it from the dots. Its tight leading exists for controls
269
- whose height is DERIVED from padding + fontSize x lineHeight, the way
270
- form-field computes one. This button's height is not derived -- it is
271
- matched to a sibling. So the constraint is a pixel alignment between two
272
- controls in one row, and nothing else in this file says so: a later pass
273
- adopting control-label-small on meaning alone would quietly break the
274
- row. */
275
- line-height: var(--typography-lineHeight-normal);
276
- cursor: pointer;
277
- }
278
-
279
- .bd-awardsflex-autoplay-toggle:hover {
280
- color: var(--color-text-primary);
281
- border-color: var(--color-border-strong);
282
- }
283
-
284
- .bd-awardsflex-autoplay-toggle:focus-visible {
285
- /* the one focus ring — see Foundations/Focus */
286
- outline: none;
287
- box-shadow: var(--focus-outline-primary);
288
- border-radius: var(--focus-outline-radius);
289
- }
290
-
291
- /* An 8px dot was an 8px target, which is a third of what WCAG 2.5.8 asks and
292
- close enough to its neighbour that the 24px circles would have overlapped
293
- as well. The button is the target and the dot is only what you see, so the
294
- button is 24 square and the dot is drawn inside it at the size it always
295
- was. Nothing about the component looks bigger; the part you can hit does. */
296
- .bd-awardsflex-dot {
297
- width: var(--dimension-24);
298
- height: var(--dimension-24);
299
- display: grid;
300
- place-items: center;
301
- border: none;
302
- background: transparent;
303
- padding: 0;
304
- cursor: pointer;
305
- }
306
-
307
- .bd-awardsflex-dot::before {
308
- content: "";
309
- width: var(--spacing-8);
310
- height: var(--spacing-8);
311
- border-radius: 50%;
312
- background: var(--color-neutral-300);
313
- transition: background var(--transition-duration-normal) var(--transition-easing-ease),
314
- transform var(--transition-duration-normal) var(--transition-easing-ease);
315
- }
316
-
317
- .bd-awardsflex-dot.active::before {
318
- /* The current slide's mark, and it takes the indicator role now that the
319
- role is not named for a shape. --indicator-color IS blue-500, so nothing
320
- moves; what changes is that this dot and the tab/nav underlines can no
321
- longer drift apart, which is the reason the role exists.
322
- .
323
- The geometry stays local on purpose: an underline slides its width, a dot
324
- scales, so --indicator-underline-height and its transition are not this
325
- component's to borrow. */
326
- background: var(--indicator-color);
327
- transform: scale(1.2);
328
- }
329
-
330
- .bd-awardsflex-dot:focus-visible {
331
- /* the one focus ring — see Foundations/Focus. It follows the target, not
332
- the dot, so keyboard focus is visible at the size the button really is. */
333
- outline: none;
334
- box-shadow: var(--focus-outline-primary);
335
- border-radius: var(--radius-full);
217
+ @media (prefers-reduced-motion: reduce) {
218
+ .bd-awardsflex-carousel-track {
219
+ transition: none;
220
+ }
336
221
  }
337
222
 
338
223
  @media ${unsafeCSS(below('sm'))} {
@@ -342,12 +227,29 @@ export const awardsFlexCSS = css`
342
227
  min-height: var(--bd-awardsflex-min-height-mobile, 280px);
343
228
  }
344
229
 
345
- .bd-awardsflex-quotes-row,
346
- .bd-awardsflex-carousel-slide {
230
+ /* The static row stacks. The carousel slide does not: below sm it holds
231
+ one card, and in a column the card's 200px flex-basis became its
232
+ height. In the row it fills the width and takes its own height. */
233
+ .bd-awardsflex-quotes-row {
347
234
  flex-direction: column;
348
235
  align-items: center;
349
236
  }
350
237
 
238
+ /* A stacked card is as tall as what it holds. In the column, the card's
239
+ 200px flex-basis became its height and the 160px min-height had nothing
240
+ left to do -- it keeps cards that stand side by side the same height,
241
+ and stacked cards stand side by side with nothing. Measured at 390:
242
+ 200px cards whose content ended at 111 and 132. The width is the
243
+ column's, so a short quote no longer hugs its source line.
244
+ .
245
+ The carousel slide keeps the min-height: there it stops the section
246
+ jumping in height from one slide to the next. */
247
+ .bd-awardsflex-quotes-row .bd-awardsflex-quote-card {
248
+ flex: none;
249
+ width: 100%;
250
+ min-height: 0;
251
+ }
252
+
351
253
  .bd-awardsflex-quote-card {
352
254
  max-width: none;
353
255
  }
@@ -10,11 +10,19 @@ import "../icons/pc-mag-readers-icon.js";
10
10
  import "../icons/top-product-icon.js";
11
11
  import "../heading/heading.js";
12
12
  import "../paragraph/paragraph.js";
13
+ import "../pagination-dots/pagination-dots.js";
13
14
  import { awardsFlexCSS } from "./awards-flex.css.js";
14
15
  import { ensembleCSS } from "../shared/ensemble.css.js";
16
+ import { below } from "../shared/token-scales.js";
15
17
 
16
-
17
- const GROUP_SIZE = 3; // how many quote cards are visible per carousel page
18
+ /* How many quote cards one carousel slide holds: three side by side, and one
19
+ below sm. Three used to stack in a column on a phone, so a slide was about
20
+ 1065px tall with every card on screen at once, and the dots paged between
21
+ two such stacks. One card a slide gives the phone a carousel again -- six
22
+ awards are six slides and six dots -- at about half the height. Drawn in the
23
+ foundation Figma file as Awards carousel, breakpoint=mobile. */
24
+ const GROUP_SIZE = 3;
25
+ const GROUP_SIZE_NARROW = 1;
18
26
 
19
27
  export class AwardsFlex extends LitElement {
20
28
  static properties = {
@@ -27,9 +35,10 @@ export class AwardsFlex extends LitElement {
27
35
  subtext : { type: String },
28
36
  badges : { type: Array },
29
37
  _slide : { state: true },
30
- _paused : { state: true },
31
- _userStopped : { state: true },
32
- _reduceMotion: { state: true }
38
+ _groupSize : { state: true },
39
+ _hovered : { state: true },
40
+ _focusWithin : { state: true },
41
+ _live : { state: true }
33
42
  };
34
43
 
35
44
  static styles = [tokens, ensembleCSS, awardsFlexCSS];
@@ -45,33 +54,43 @@ export class AwardsFlex extends LitElement {
45
54
  this.subtext = "";
46
55
  this.badges = [];
47
56
  this._slide = 0;
48
- this._paused = false;
49
- /* Hover/focus pause is transient. A user who presses Stop means it, and
50
- must not have it undone by the pointer leaving the carousel — so the two
51
- are separate pieces of state. */
52
- this._userStopped = false;
53
- this._reduceMotion = false;
54
- this._timer = null;
57
+ this._groupSize = GROUP_SIZE;
58
+ /* The pointer or focus on the slides holds the autoplay -- a hold, which
59
+ lifts by itself. Stopping it for good is the atom's: a visitor who picks
60
+ a dot has chosen a slide, and it stays. */
61
+ this._hovered = false;
62
+ this._focusWithin = false;
63
+ /* The slides' live region. Off while they change on their own, so a
64
+ screen reader is not read a new set of quotes every few seconds; polite
65
+ once they are still, so a change the visitor asks for is announced.
66
+ bd-pagination-dots says which, through bd-toggle. */
67
+ this._live = "polite";
55
68
  }
56
69
 
57
70
  connectedCallback() {
58
71
  super.connectedCallback();
59
- /* WCAG 2.2.2: content that moves on its own must be pausable, and under
60
- prefers-reduced-motion it should not start at all. Watched rather than
61
- read once, so a reader who changes the setting is honoured live. */
72
+ /* Watched rather than read once: a slide is a layout, and a phone turned
73
+ sideways crosses the breakpoint. */
62
74
  if (typeof window !== "undefined" && window.matchMedia) {
63
- this._motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
64
- this._reduceMotion = this._motionQuery.matches;
65
- this._onMotionChange = () => { this._reduceMotion = this._motionQuery.matches; };
66
- this._motionQuery.addEventListener("change", this._onMotionChange);
75
+ this._narrowQuery = window.matchMedia(below("sm"));
76
+ this._onNarrowChange = () => this._setGroupSize(this._narrowQuery.matches ? GROUP_SIZE_NARROW : GROUP_SIZE);
77
+ this._narrowQuery.addEventListener("change", this._onNarrowChange);
78
+ this._onNarrowChange();
67
79
  }
68
- this._maybeStartAutoplay();
69
80
  }
70
81
 
71
82
  disconnectedCallback() {
72
83
  super.disconnectedCallback();
73
- this._motionQuery?.removeEventListener("change", this._onMotionChange);
74
- this._stopAutoplay();
84
+ this._narrowQuery?.removeEventListener("change", this._onNarrowChange);
85
+ }
86
+
87
+ /* Changing the slide size keeps the visitor on the slide that holds the card
88
+ they were looking at: its first card, re-paged. */
89
+ _setGroupSize(size) {
90
+ if (size === this._groupSize) return;
91
+ const firstCard = this._slide * this._groupSize;
92
+ this._groupSize = size;
93
+ this._slide = Math.floor(firstCard / size);
75
94
  }
76
95
 
77
96
  updated(changed) {
@@ -80,23 +99,20 @@ export class AwardsFlex extends LitElement {
80
99
  changed.has("carousel") ||
81
100
  changed.has("layout") ||
82
101
  changed.has("badges") ||
83
- changed.has("_paused") ||
84
- changed.has("_userStopped") ||
85
- changed.has("_reduceMotion")
102
+ changed.has("_groupSize")
86
103
  ) {
87
104
  // if the badge set shrank, make sure the current slide index is still valid
88
105
  if (this._slide >= this._slidesCount) {
89
106
  this._slide = 0;
90
107
  }
91
- this._maybeStartAutoplay();
92
108
  }
93
109
  }
94
110
 
95
111
  get _groups() {
96
- // chunk badges into pages of GROUP_SIZE for the carousel
112
+ // chunk badges into slides of _groupSize for the carousel
97
113
  const groups = [];
98
- for (let i = 0; i < this.badges.length; i += GROUP_SIZE) {
99
- groups.push(this.badges.slice(i, i + GROUP_SIZE));
114
+ for (let i = 0; i < this.badges.length; i += this._groupSize) {
115
+ groups.push(this.badges.slice(i, i + this._groupSize));
100
116
  }
101
117
  return groups;
102
118
  }
@@ -105,52 +121,11 @@ export class AwardsFlex extends LitElement {
105
121
  return this.layout === "quotes" ? this._groups.length : 0;
106
122
  }
107
123
 
108
- _maybeStartAutoplay() {
109
- this._stopAutoplay();
110
- const canAutoplay =
111
- this.autoplay &&
112
- this.carousel &&
113
- this.layout === "quotes" &&
114
- this._slidesCount > 1 &&
115
- !this._paused &&
116
- !this._userStopped &&
117
- !this._reduceMotion;
118
-
119
- if (!canAutoplay) return;
120
-
121
- this._timer = setInterval(() => {
122
- this._slide = (this._slide + 1) % this._slidesCount;
123
- }, Math.max(1000, this.autoplayDelay));
124
- }
125
-
126
- _stopAutoplay() {
127
- if (this._timer) {
128
- clearInterval(this._timer);
129
- this._timer = null;
130
- }
131
- }
132
-
133
- _onMouseEnter() {
134
- if (this.autoplay) this._paused = true;
135
- }
136
-
137
- _onMouseLeave() {
138
- if (this.autoplay) this._paused = false;
139
- }
140
-
141
- _toggleAutoplay() {
142
- this._userStopped = !this._userStopped;
143
- }
144
-
145
- /** True when the carousel is actually able to move on its own. */
124
+ /** True when the carousel is able to move on its own. */
146
125
  get _autoplayOffered() {
147
126
  return this.autoplay && this.carousel && this.layout === "quotes" && this._slidesCount > 1;
148
127
  }
149
128
 
150
- _goTo(i) {
151
- this._slide = i;
152
- }
153
-
154
129
  _iconHeight() {
155
130
  if (this.layout === "grid") return 64;
156
131
  if (this.layout === "quotes") return 56;
@@ -202,48 +177,22 @@ export class AwardsFlex extends LitElement {
202
177
  `;
203
178
  }
204
179
 
205
- /**
206
- * The pause/stop control WCAG 2.2.2 requires of anything that starts moving
207
- * on its own and keeps going. Hovering or focusing the carousel already
208
- * paused it, but that is not a mechanism a keyboard or touch user can rely
209
- * on, and it cannot express "leave it stopped".
210
- *
211
- * Its name states the action it performs, which is what a screen reader
212
- * announces on focus. It sits beside the dots rather than inside them: that
213
- * container is a tablist and a plain button is not a valid child of one.
214
- */
215
- _renderAutoplayToggle() {
216
- if (!this._autoplayOffered) return nothing;
217
- const stopped = this._userStopped || this._reduceMotion;
218
- return html`
219
- <button
220
- type="button"
221
- class="bd-awardsflex-autoplay-toggle"
222
- @click=${this._toggleAutoplay}
223
- aria-label=${stopped ? "Play slideshow" : "Pause slideshow"}
224
- >${stopped ? "Play" : "Pause"}</button>
225
- `;
226
- }
227
-
180
+ /* bd-pagination-dots, which runs the clock too, so the progress in the
181
+ current pill and the change of slide are one thing. WCAG 2.2.2's way to
182
+ stop it is the dots themselves: picking one stops autoplay for good. */
228
183
  _renderDots() {
229
184
  if (!this.carousel || this._slidesCount < 2) return nothing;
230
185
  return html`
231
- <div class="bd-awardsflex-controls">
232
- <div class="bd-awardsflex-dots" role="tablist" aria-label="Slide navigation">
233
- ${this._groups.map(
234
- (_, i) => html`
235
- <button
236
- class="bd-awardsflex-dot ${i === this._slide ? "active" : ""}"
237
- role="tab"
238
- aria-selected="${i === this._slide}"
239
- aria-label="Go to page ${i + 1}"
240
- @click=${() => this._goTo(i)}
241
- ></button>
242
- `
243
- )}
244
- </div>
245
- ${this._renderAutoplayToggle()}
246
- </div>
186
+ <bd-pagination-dots
187
+ count=${this._slidesCount}
188
+ current=${this._slide + 1}
189
+ label="Awards"
190
+ ?autoplay=${this._autoplayOffered}
191
+ delay=${this.autoplayDelay}
192
+ ?held=${this._hovered || this._focusWithin}
193
+ @bd-change=${e => { this._slide = e.detail.current - 1; }}
194
+ @bd-toggle=${e => { this._live = e.detail.moving ? "off" : "polite"; }}
195
+ ></bd-pagination-dots>
247
196
  `;
248
197
  }
249
198
 
@@ -252,18 +201,19 @@ export class AwardsFlex extends LitElement {
252
201
  return html`
253
202
  <div
254
203
  class="bd-awardsflex-carousel-viewport"
255
- @mouseenter=${this._onMouseEnter}
256
- @mouseleave=${this._onMouseLeave}
257
- @focusin=${this._onMouseEnter}
258
- @focusout=${this._onMouseLeave}
204
+ @mouseenter=${() => { this._hovered = true; }}
205
+ @mouseleave=${() => { this._hovered = false; }}
206
+ @focusin=${() => { this._focusWithin = true; }}
207
+ @focusout=${e => { this._focusWithin = e.currentTarget.contains(e.relatedTarget); }}
259
208
  >
260
209
  <div
261
210
  class="bd-awardsflex-carousel-track"
262
211
  style="transform: translateX(${offset}%);"
212
+ aria-live=${this._live}
263
213
  >
264
214
  ${this._groups.map(
265
- (group) => html`
266
- <div class="bd-awardsflex-carousel-slide">
215
+ (group, i) => html`
216
+ <div class="bd-awardsflex-carousel-slide" ?inert=${i !== this._slide}>
267
217
  ${group.map((a) => this._renderQuoteCard(a))}
268
218
  </div>
269
219
  `
@@ -22,9 +22,11 @@ Badges render real icon components from \`components/icons\` (e.g. \`<bd-pc-mag-
22
22
  \`<bd-av-comparatives-icon>\`) — pass the icon's slug via the \`icon\` field on each badge
23
23
  object, and \`AwardsFlex\` maps it to the matching \`<bd-*-icon>\` custom element internally.
24
24
 
25
- When \`layout="quotes"\` and \`carousel\` is set, badges are paginated 3 per page. With 6
26
- badges that's 2 pages, with 9 badges 3 pages, and so on — the carousel slides a full page
27
- of 3 cards at a time.
25
+ When \`layout="quotes"\` and \`carousel\` is set, badges are paginated 3 per slide, and
26
+ 1 per slide below the \`sm\` breakpoint (576px). With 6 badges that's 2 slides on a wide
27
+ screen and 6 on a phone — the dots follow the count. The carousel slides a full slide at
28
+ a time; crossing the breakpoint keeps the visitor on the slide that holds the card they
29
+ were looking at.
28
30
 
29
31
  ### Usage
30
32
  \`\`\`html
@@ -45,7 +47,7 @@ of 3 cards at a time.
45
47
  |-------------------|------------------------------------|-------------|----------------------------------------------------------------------|
46
48
  | \`background\` | \`"white" \\| "tint"\` | \`"white"\` | Section background |
47
49
  | \`layout\` | \`"compact" \\| "grid" \\| "quotes"\` | \`"compact"\` | Badge size/arrangement, or badges paired with quote cards |
48
- | \`carousel\` | Boolean | \`false\` | Only for \`layout="quotes"\` — paginate 3 cards at a time |
50
+ | \`carousel\` | Boolean | \`false\` | Only for \`layout="quotes"\` — 3 cards a slide, 1 below \`sm\` |
49
51
  | \`autoplay\` | Boolean | \`false\` | Auto-advance carousel pages. Requires \`carousel\` + \`layout="quotes"\` and 2+ pages (7+ badges is unnecessary — 2 pages start at 4 badges) |
50
52
  | \`autoplay-delay\` | Number (ms) | \`4000\` | Time between auto-advances. Minimum enforced: 1000ms |
51
53
  | \`headline\` | String | \`""\` | Optional heading |
@@ -86,7 +88,7 @@ Autoplay pauses automatically on mouse hover or keyboard focus within the carous
86
88
  },
87
89
  carousel: {
88
90
  control : "boolean",
89
- description: "Paginate quote cards 3 at a time (layout=\"quotes\" only)",
91
+ description: "Paginate quote cards 3 a slide, 1 below sm (layout=\"quotes\" only)",
90
92
  table : { category: "Appearance" }
91
93
  },
92
94
  autoplay: {
@@ -170,7 +172,7 @@ const threeBadgesWithQuotes = [
170
172
  }
171
173
  ];
172
174
 
173
- // Six badges with quotes — needed so the carousel has 2+ pages (3 cards per page)
175
+ // Six badges with quotes — needed so the carousel has 2+ slides (3 a slide, 1 below sm)
174
176
  const sixBadgesWithQuotes = [
175
177
  ...threeBadgesWithQuotes,
176
178
  {
@@ -47,7 +47,12 @@ export default css`
47
47
  4px for as long as it was focused. That token is the default for
48
48
  elements with no radius, which is what Foundations/Focus says. */
49
49
  .bd-back:focus-visible {
50
- outline: none;
50
+ /* forced-colors mode does not paint box-shadow, so the ring
51
+ vanishes there; it does paint an outline, in a system colour. This one
52
+ is transparent everywhere else, and the offset lands it on the ring's
53
+ outer band. */
54
+ outline: var(--border-width-2) solid transparent;
55
+ outline-offset: var(--focus-outline-offset);
51
56
  box-shadow: var(--focus-outline-primary);
52
57
  }
53
58
 
@@ -30,27 +30,39 @@ export class BdBack extends LitElement {
30
30
  this.ariaLabel = null;
31
31
  }
32
32
 
33
+ /** Without an href there is no destination to link to, only history. */
33
34
  handleClick() {
34
- if (this.href) {
35
- window.location.href = this.href;
36
- } else {
37
- window.history.back();
38
- }
35
+ window.history.back();
39
36
  }
40
37
 
38
+ /* A real link when there is somewhere to go. With href this used to be a
39
+ <button> that set window.location.href on click, so the destination was
40
+ invisible to everything that reads links: no middle-click or
41
+ Cmd/Ctrl-click into a new tab, no "copy link", no status-bar URL, and a
42
+ screen reader announced a button and listed it under none of the page's
43
+ links. Without href it stays a button, because history.back() is an
44
+ action, not a place. */
41
45
  render() {
42
- return html`
46
+ const content = html`<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" aria-hidden="true" class="mi-solid mi-arrow-back" viewBox="0 0 24 24">
47
+ <path d="M19 11H7.83l4.88-4.88c.39-.39.39-1.03 0-1.42a.996.996 0 0 0-1.41 0l-6.59 6.59a.996.996 0 0 0 0 1.41l6.59 6.59a.996.996 0 1 0 1.41-1.41L7.83 13H19c.55 0 1-.45 1-1s-.45-1-1-1"/>
48
+ </svg>
49
+ <span class="bd-back-label">${this.label}</span>`;
50
+
51
+ return this.href
52
+ ? html`
53
+ <a
54
+ class="bd-back"
55
+ href="${this.href}"
56
+ aria-label="${this.ariaLabel ?? nothing}"
57
+ >${content}</a>
58
+ `
59
+ : html`
43
60
  <button
44
61
  type="button"
45
62
  class="bd-back"
46
63
  aria-label="${this.ariaLabel ?? nothing}"
47
64
  @click="${this.handleClick}"
48
- >
49
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" aria-hidden="true" class="mi-solid mi-arrow-back" viewBox="0 0 24 24">
50
- <path d="M19 11H7.83l4.88-4.88c.39-.39.39-1.03 0-1.42a.996.996 0 0 0-1.41 0l-6.59 6.59a.996.996 0 0 0 0 1.41l6.59 6.59a.996.996 0 1 0 1.41-1.41L7.83 13H19c.55 0 1-.45 1-1s-.45-1-1-1"/>
51
- </svg>
52
- <span class="bd-back-label">${this.label}</span>
53
- </button>
65
+ >${content}</button>
54
66
  `;
55
67
  }
56
68
 
@@ -20,8 +20,8 @@ export default {
20
20
  \`\`\`
21
21
 
22
22
  ### Behavior
23
- - If \`href\` is provided, clicking navigates to that URL via \`window.location.href\`
24
- - If \`href\` is empty, clicking calls \`window.history.back()\`
23
+ - If \`href\` is provided, it renders a real link to that URL, so it opens in a new tab, can be copied, and is announced as a link
24
+ - If \`href\` is empty, it renders a button that calls \`window.history.back()\`
25
25
  - Always renders a left-pointing arrow SVG icon alongside the label
26
26
  `
27
27
  }
@@ -69,7 +69,7 @@ export const WithHref = {
69
69
  parameters: {
70
70
  docs: {
71
71
  description: {
72
- story: '`href` is set to `/`. Clicking navigates to the home page via `window.location.href`.'
72
+ story: '`href` is set to `/`, so it renders a link to the home page.'
73
73
  }
74
74
  }
75
75
  }
@@ -71,12 +71,15 @@ export class BdBadge extends LitElement {
71
71
  if (!this.icon || this.mode === 'dot') return '';
72
72
 
73
73
  // currentColor makes the icon follow the tone's text colour, so there is
74
- // no per-tone icon colour to keep in step
74
+ // no per-tone icon colour to keep in step.
75
+ // size is a step on the icon scale, not pixels. This said size="16", which
76
+ // is no step, so both icons fell back to the xl they were drawn at and a
77
+ // badge with an icon measured 40px tall beside 23px ones; sm is the 16.
75
78
  switch (this.icon) {
76
79
  case 'individual':
77
- return html`<bd-individual-icon size="16" color="currentColor"></bd-individual-icon>`;
80
+ return html`<bd-individual-icon size="sm" color="currentColor"></bd-individual-icon>`;
78
81
  case 'family':
79
- return html`<bd-family-icon size="16" color="currentColor"></bd-family-icon>`;
82
+ return html`<bd-family-icon size="sm" color="currentColor"></bd-family-icon>`;
80
83
  default:
81
84
  return '';
82
85
  }