@repobit/dex-system-design 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +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
@@ -12,7 +12,10 @@ export default css`
12
12
  display: block;
13
13
  align-items: center;
14
14
  position: relative;
15
- margin-bottom: 100px;
15
+ /* 100px used to stand here, room for two arrows positioned absolutely
16
+ 65px below the track. The dots are in the flow now, so the space below
17
+ the track is their margin, their 24px and this: 96. */
18
+ margin-bottom: var(--spacing-48);
16
19
  max-width: 100%;
17
20
  flex-direction: column;
18
21
  }
@@ -27,17 +30,16 @@ export default css`
27
30
  hand while leaving its line-height unset, which is how the title came to
28
31
  render at the browser's default leading and with the browser's default h2
29
32
  margins. LightCarousel and CardFeatures declare no such rule either. */
30
- div.bd-arrow {
31
- padding: var(--spacing-2);
32
- visibility: visible;
33
- }
34
-
35
33
  .bd-box:focus-visible,
36
- .bd-arrow:focus-visible,
37
34
  .bd-plus-button:focus-visible,
38
35
  .bd-modal-content button:focus-visible {
39
36
  /* the one focus ring — see Foundations/Focus */
40
- outline: none;
37
+ /* forced-colors mode does not paint box-shadow, so the ring
38
+ vanishes there; it does paint an outline, in a system colour. This one
39
+ is transparent everywhere else, and the offset lands it on the ring's
40
+ outer band. */
41
+ outline: var(--border-width-2) solid transparent;
42
+ outline-offset: var(--focus-outline-offset);
41
43
  box-shadow: var(--focus-outline-primary);
42
44
  }
43
45
 
@@ -59,90 +61,16 @@ export default css`
59
61
  gap: 1rem;
60
62
  padding: 1rem;
61
63
  }
62
- .bd-arrow {
63
- border-radius: 50%;
64
- width: var(--spacing-44);
65
- height: var(--spacing-44);
64
+ /* The dots: bd-pagination-dots, the package's one set of slide controls.
65
+ Two <img role="button"> arrows stood here, painted by eight data-URI SVGs
66
+ with the blues and the disabled grey baked in as hex -- invisible to every
67
+ colour check -- and positioned absolutely 65px under the track. Where they
68
+ sat, the dots sit now: under the track, at its end. */
69
+ .bd-carousel-controls {
66
70
  display: flex;
67
- align-items: center;
68
- justify-content: center;
69
- cursor: pointer;
70
- box-shadow: var(--spacing-0) var(--spacing-2) var(--spacing-4) rgba(0, 0, 0, 0.1);
71
- position: absolute;
72
- bottom: -65px;
73
- background: var(--color-surface-brand);
74
- color: var(--color-text-inverse);
75
- /* no role: .bd-arrow is an <img> -- left-arrow.svg / right-arrow.svg in
76
- carousel.js. It renders no text, so this styles nothing but the alt
77
- text a browser shows when the SVG fails to load. Fallback typography
78
- is not a role decision. */
79
- font-size: var(--typography-fontSize-xl);
80
- /* no role: .bd-arrow is an <img> -- left-arrow.svg / right-arrow.svg in
81
- carousel.js. It renders no text, so this styles nothing but the alt
82
- text a browser shows when the SVG fails to load. Fallback typography
83
- is not a role decision. */
84
- line-height: var(--typography-lineHeight-none);
85
- text-align: center;
86
- }
87
-
88
- .bd-left-arrow {
89
- content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" transform="matrix(-1 0 0 1 40 0)" fill="%23006DFF"/><path d="M23.3431 14.4142L21.9289 13L14.8579 20.071L16.2721 21.4853L23.3431 14.4142Z" fill="white"/><path d="M16.2721 18.6568L14.8579 20.071L21.9289 27.1421L23.3431 25.7279L16.2721 18.6568Z" fill="white"/></svg>');
90
- }
91
-
92
-
93
- .bd-left-arrow:hover {
94
- content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" transform="matrix(-1 0 0 1 40 0)" fill="%230059D6"/><path d="M23.3431 14.4142L21.9289 13L14.8579 20.071L16.2721 21.4853L23.3431 14.4142Z" fill="white"/><path d="M16.2721 18.6568L14.8579 20.071L21.9289 27.1421L23.3431 25.7279L16.2721 18.6568Z" fill="white"/></svg>');
95
- }
96
-
97
- .bd-left-arrow:active {
98
- content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" transform="matrix(-1 0 0 1 40 0)" fill="%230045AD"/><path d="M23.3431 14.4142L21.9289 13L14.8579 20.071L16.2721 21.4853L23.3431 14.4142Z" fill="white"/><path d="M16.2721 18.6568L14.8579 20.071L21.9289 27.1421L23.3431 25.7279L16.2721 18.6568Z" fill="white"/></svg>');
99
- }
100
-
101
-
102
- .bd-left-arrow:disabled {
103
- content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="%23F1F2F3"/><path d="M23.3431 14.4142L21.9289 13L14.8579 20.071L16.2721 21.4853L23.3431 14.4142Z" fill="%23A6ADB4"/><path d="M16.2721 18.6568L14.8579 20.071L21.9289 27.1421L23.3431 25.7279L16.2721 18.6568Z" fill="%23A6ADB4"/></svg>');
104
- }
105
-
106
- .bd-right-arrow {
107
- content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="%23006DFF"/><path d="M16.6569 14.4142L18.0711 13L25.1421 20.071L23.7279 21.4853L16.6569 14.4142Z" fill="white"/><path d="M23.7279 18.6568L25.1421 20.071L18.0711 27.1421L16.6569 25.7279L23.7279 18.6568Z" fill="white"/></svg>');
108
- }
109
-
110
-
111
- .bd-right-arrow:hover {
112
- content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="%230059D6"/><path d="M16.6569 14.4142L18.0711 13L25.1421 20.071L23.7279 21.4853L16.6569 14.4142Z" fill="white"/><path d="M23.7279 18.6568L25.1421 20.071L18.0711 27.1421L16.6569 25.7279L23.7279 18.6568Z" fill="white"/></svg>');
113
- }
114
-
115
-
116
- .bd-right-arrow:active {
117
- content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="%230045AD"/><path d="M16.6569 14.4142L18.0711 13L25.1421 20.071L23.7279 21.4853L16.6569 14.4142Z" fill="white"/><path d="M23.7279 18.6568L25.1421 20.071L18.0711 27.1421L16.6569 25.7279L23.7279 18.6568Z" fill="white"/></svg>');
118
- }
119
-
120
-
121
- .bd-right-arrow:disabled {
122
- content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="%23F1F2F3"/><path d="M16.6569 14.4142L18.0711 13L25.1421 20.071L23.7279 21.4853L16.6569 14.4142Z" fill="%23A6ADB4"/><path d="M23.7279 18.6568L25.1421 20.071L18.0711 27.1421L16.6569 25.7279L23.7279 18.6568Z" fill="%23A6ADB4"/></svg>');
123
- }
124
- .bd-right-arrow.bd-disabled {
125
- content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="%23F1F2F3"/><path d="M16.6569 14.4142L18.0711 13L25.1421 20.071L23.7279 21.4853L16.6569 14.4142Z" fill="%23A6ADB4"/><path d="M23.7279 18.6568L25.1421 20.071L18.0711 27.1421L16.6569 25.7279L23.7279 18.6568Z" fill="%23A6ADB4"/></svg>');
126
- cursor: not-allowed;
127
- }
128
-
129
- .bd-left-arrow.bd-disabled {
130
- content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="%23F1F2F3"/><path d="M23.3431 14.4142L21.9289 13L14.8579 20.071L16.2721 21.4853L23.3431 14.4142Z" fill="%23A6ADB4"/><path d="M16.2721 18.6568L14.8579 20.071L21.9289 27.1421L23.3431 25.7279L16.2721 18.6568Z" fill="%23A6ADB4"/></svg>');
131
- cursor: not-allowed;
132
- }
133
-
134
- .bd-disabled {
135
- background: var(--color-neutral-50);
136
- color: var(--color-text-muted-inverse);
137
- cursor: not-allowed;
138
- }
139
-
140
- .bd-arrow:first-of-type {
141
- right: 60px;
142
- }
143
-
144
- .bd-arrow:last-of-type {
145
- right: var(--spacing-10);
71
+ justify-content: flex-end;
72
+ margin-top: var(--spacing-24);
73
+ padding-inline: var(--spacing-10);
146
74
  }
147
75
 
148
76
  .bd-carousel-track {
@@ -259,14 +187,6 @@ export default css`
259
187
  content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none"><circle cx="20" cy="20" r="19" fill="%23006DFF" stroke="%23006DFF" stroke-width="2"/><path d="M19 11H21V29H19V11Z" fill="white"/><path d="M29 19V21L11 21L11 19L29 19Z" fill="white"/></svg>');
260
188
  }
261
189
 
262
- .bd-left-arrow {
263
- margin-right: var(--spacing-6);
264
- }
265
-
266
- .bd-right-arrow {
267
- margin-left: var(--spacing-6);
268
- }
269
-
270
190
  @media ${unsafeCSS(below('md'))} {
271
191
  .bd-carousel-track {
272
192
  flex-wrap: nowrap;
@@ -284,23 +204,6 @@ export default css`
284
204
  max-width: 90%;
285
205
  padding: 1.5em;
286
206
  }
287
-
288
- .bd-arrow {
289
- bottom: -75px;
290
- /* no role: .bd-arrow is an <img> -- left-arrow.svg / right-arrow.svg in
291
- carousel.js. It renders no text, so this styles nothing but the alt
292
- text a browser shows when the SVG fails to load. Fallback typography
293
- is not a role decision. */
294
- font-size: var(--typography-fontSize-base);
295
- }
296
-
297
- .bd-arrow:first-of-type {
298
- right: 50px;
299
- }
300
-
301
- .bd-arrow:last-of-type {
302
- right: var(--spacing-6);
303
- }
304
207
  }
305
208
 
306
209
  @media (max-width: 480px) {
@@ -4,6 +4,7 @@ import "../../tokens/tokens.js";
4
4
  import carouselCSS from "../carousel/carousel.css.js";
5
5
  import "../heading/heading.js";
6
6
  import "../modal/modal.js";
7
+ import "../pagination-dots/pagination-dots.js";
7
8
  export class CarouselItem extends LitElement {
8
9
  static properties = {
9
10
  icon : { type: String },
@@ -61,7 +62,9 @@ class CustomCarousel extends LitElement {
61
62
  static properties = {
62
63
  title : { type: String },
63
64
  headingLevel: { type: Number, attribute: "heading-level" },
64
- selectedItem: { type: Object }
65
+ selectedItem: { type: Object },
66
+ _pages : { state: true },
67
+ _page : { state: true }
65
68
  };
66
69
 
67
70
  constructor() {
@@ -69,6 +72,10 @@ class CustomCarousel extends LitElement {
69
72
  this.selectedItem = null;
70
73
  this.title = "";
71
74
  this.headingLevel = 2;
75
+ // scroll stops, 1-based, read from the track; see _measure
76
+ this._pages = 1;
77
+ this._page = 1;
78
+ this._seeking = 0;
72
79
  }
73
80
 
74
81
  /**
@@ -98,36 +105,22 @@ class CustomCarousel extends LitElement {
98
105
  firstUpdated() {
99
106
  this.track = this.shadowRoot.querySelector(".bd-carousel-track");
100
107
  this.slotElement = this.shadowRoot.querySelector("slot");
101
- this.leftArrow = this.shadowRoot.querySelector(".bd-left-arrow");
102
- this.rightArrow = this.shadowRoot.querySelector(".bd-right-arrow");
103
108
 
104
- this.track.addEventListener("scroll", () => this.updateArrowStates());
109
+ this.track.addEventListener("scroll", () => this._measure(), { passive: true });
110
+ this.track.addEventListener("scrollend", () => { this._seeking = 0; this._measure(); });
111
+ // a visitor taking hold of the track ends any scroll a control started
112
+ for (const type of ["pointerdown", "touchstart", "wheel"]) {
113
+ this.track.addEventListener(type, () => { this._seeking = 0; }, { passive: true });
114
+ }
105
115
 
106
116
  this.slotElement.addEventListener("slotchange", () => {
107
- this.waitForImagesToLoad().then(() => {
108
- this.updateArrowStates();
109
- });
117
+ this.waitForImagesToLoad().then(() => this._measure());
110
118
  });
111
119
 
112
- setTimeout(() => {
113
- this.waitForImagesToLoad().then(() => {
114
- this.updateArrowStates();
115
- });
116
- }, 0);
117
-
118
- const arrows = this.shadowRoot.querySelectorAll(".bd-arrow");
119
- arrows.forEach((arrow) => {
120
- arrow.addEventListener("mousedown", () =>
121
- arrow.classList.add("bd-active")
122
- );
123
- arrow.addEventListener("mouseup", () =>
124
- arrow.classList.remove("bd-active")
125
- );
126
- arrow.addEventListener("mouseleave", () =>
127
- arrow.classList.remove("bd-active")
128
- );
129
- arrow.addEventListener("keydown", (e) => this._onArrowKeydown(e, arrow));
130
- });
120
+ this._resizeObserver = new ResizeObserver(() => this._measure());
121
+ this._resizeObserver.observe(this.track);
122
+
123
+ this.waitForImagesToLoad().then(() => this._measure());
131
124
 
132
125
  this.addEventListener("open-modal", (e) => {
133
126
  this.openModal(e.detail);
@@ -135,6 +128,11 @@ class CustomCarousel extends LitElement {
135
128
  this.enableDragAndTouchScroll(this.track);
136
129
  }
137
130
 
131
+ disconnectedCallback() {
132
+ super.disconnectedCallback();
133
+ this._resizeObserver?.disconnect();
134
+ }
135
+
138
136
  waitForImagesToLoad() {
139
137
  const assignedElements = this.slotElement.assignedElements({
140
138
  flatten: true
@@ -155,40 +153,47 @@ class CustomCarousel extends LitElement {
155
153
  return Promise.all(promises);
156
154
  }
157
155
 
158
- scrollCarousel(direction) {
159
- const assignedElements = this.slotElement.assignedElements({
160
- flatten: true
161
- });
162
- if (!assignedElements.length) return;
163
- const itemWidth = assignedElements[0].offsetWidth || 300;
164
- if (!this.track) return;
165
-
166
- this.track.scrollBy({
167
- left : direction === "left" ? -itemWidth : itemWidth,
168
- behavior: "smooth"
169
- });
170
-
171
- setTimeout(() => this.updateArrowStates(), 300);
156
+ /* The distance from one item to the next: an item's width and the gap. A
157
+ scroll stop is one of these, and the controls count stops, not items --
158
+ the last stop is wherever the track ends, which is short of a full step
159
+ when the items do not divide the width evenly. */
160
+ _step() {
161
+ const first = this.slotElement?.assignedElements({ flatten: true })[0];
162
+ if (!first) return 0;
163
+ const gap = parseFloat(getComputedStyle(this.track).columnGap) || 0;
164
+ return first.offsetWidth + gap;
172
165
  }
173
166
 
174
- updateArrowStates() {
175
- const trackWidth = this.track.offsetWidth;
176
- const totalWidth = this.track.scrollWidth;
177
- const scrollPosition = this.track.scrollLeft;
178
- if (!this.leftArrow || !this.rightArrow) return;
179
-
180
- this.leftArrow.classList.toggle("bd-disabled", scrollPosition <= 0);
181
- this.rightArrow.classList.toggle(
182
- "bd-disabled",
183
- scrollPosition + trackWidth >= totalWidth - 1
184
- );
167
+ /* How many stops the track has and which one it is at, from where it has
168
+ scrolled to -- by a control, a drag, a wheel or a keyboard. While a
169
+ control's smooth scroll is on its way, the dot it asked for stays current
170
+ rather than stepping through every stop it passes. */
171
+ _measure() {
172
+ if (!this.track) return;
173
+ const max = this.track.scrollWidth - this.track.clientWidth;
174
+ const step = this._step();
175
+ if (max <= 1 || step <= 0) {
176
+ this._pages = 1;
177
+ this._page = 1;
178
+ return;
179
+ }
180
+ this._pages = Math.ceil((max - 1) / step) + 1;
181
+ const left = this.track.scrollLeft;
182
+ const page = left >= max - 1 ? this._pages : Math.min(this._pages, Math.round(left / step) + 1);
183
+ if (this._seeking && page !== this._seeking) return;
184
+ this._seeking = 0;
185
+ this._page = page;
185
186
  }
186
187
 
187
- _onArrowKeydown(e, arrow) {
188
- if (e.key === "Enter" || e.key === " ") {
189
- e.preventDefault();
190
- arrow.click();
191
- }
188
+ _goToPage(n) {
189
+ const max = this.track.scrollWidth - this.track.clientWidth;
190
+ const reduce = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
191
+ this._seeking = n;
192
+ this._page = n;
193
+ this.track.scrollTo({
194
+ left : Math.min((n - 1) * this._step(), max),
195
+ behavior: reduce ? "auto" : "smooth"
196
+ });
192
197
  }
193
198
 
194
199
  openModal(item) {
@@ -256,27 +261,18 @@ class CustomCarousel extends LitElement {
256
261
  </div>`
257
262
  : ""}
258
263
 
259
- <img
260
- src="left-arrow.svg"
261
- alt="Left Arrow"
262
- class="bd-arrow bd-left-arrow"
263
- role="button"
264
- aria-label="Scroll left"
265
- @click="${() => this.scrollCarousel("left")}"
266
- tabindex="0"
267
- />
268
264
  <div class="bd-carousel-track">
269
265
  <slot></slot>
270
266
  </div>
271
- <img
272
- src="right-arrow.svg"
273
- alt="Right Arrow"
274
- class="bd-arrow bd-right-arrow"
275
- role="button"
276
- aria-label="Scroll right"
277
- @click="${() => this.scrollCarousel("right")}"
278
- tabindex="0"
279
- />
267
+ <div class="bd-carousel-controls">
268
+ <bd-pagination-dots
269
+ count=${this._pages}
270
+ current=${this._page}
271
+ label=${this.title || "Slides"}
272
+ item-label="Go to item"
273
+ @bd-change=${e => this._goToPage(e.detail.current)}
274
+ ></bd-pagination-dots>
275
+ </div>
280
276
  </div>
281
277
 
282
278
  ${this.selectedItem
@@ -10,7 +10,7 @@ export default {
10
10
  component: `
11
11
  **CustomCarousel** and **CarouselItem** are Lit components for a scrollable, draggable card carousel with modal support.
12
12
 
13
- - \`<bd-carousel-section>\` — scrollable container with left/right arrows and optional title
13
+ - \`<bd-carousel-section>\` — scrollable container with optional title, and the package's slide dots (\`bd-pagination-dots\`) under the track
14
14
  - \`<bd-carousel-item>\` — individual card with icon, slotted content, and a \`+\` button to open a modal
15
15
 
16
16
  ### Usage
@@ -23,14 +23,15 @@ export default {
23
23
  \`\`\`
24
24
 
25
25
  ### Behavior
26
- - Left/right arrows scroll by one item width with smooth scroll
27
- - Arrows disable automatically at scroll boundaries
28
- - Click and drag (mouse + touch) supported for scrolling
26
+ - One dot per scroll stop: a stop is one item's width plus the gap, and the last is wherever the track ends
27
+ - A dot scrolls there smoothly (instantly under \`prefers-reduced-motion\`)
28
+ - When every item fits, there is nowhere to go and no controls render
29
+ - Click and drag (mouse + touch) supported for scrolling; the dots follow any scroll
29
30
  - Clicking \`+\` on an item fires \`open-modal\` custom event, caught by the carousel to show \`<bd-modal>\`
30
31
  - Modal closes via \`close-modal\` event
31
32
 
32
33
  ### Keyboard Support
33
- - Arrow buttons respond to Enter/Space
34
+ - The dots are real buttons
34
35
  - \`+\` button on items responds to Enter/Space
35
36
  `
36
37
  }
@@ -84,7 +85,7 @@ export const Default = {
84
85
  ${sampleItems.map(renderItem)}
85
86
  </bd-carousel-section>
86
87
  `,
87
- parameters: { docs: { description: { story: '6-item carousel with title. Scroll with arrows or drag.' } } }
88
+ parameters: { docs: { description: { story: '6-item carousel with title. Move with the dots under the track, or drag.' } } }
88
89
  };
89
90
 
90
91
  export const NoTitle = {
@@ -104,7 +105,7 @@ export const FewItems = {
104
105
  ${sampleItems.slice(0, 2).map(renderItem)}
105
106
  </bd-carousel-section>
106
107
  `,
107
- parameters: { docs: { description: { story: 'Only 2 items — right arrow should be disabled if all items fit in view.' } } }
108
+ parameters: { docs: { description: { story: 'Only 2 items. When every item fits there is nowhere to scroll, so no dots render.' } } }
108
109
  };
109
110
 
110
111
  export const SingleItem = {
@@ -114,7 +115,7 @@ export const SingleItem = {
114
115
  ${renderItem(sampleItems[0])}
115
116
  </bd-carousel-section>
116
117
  `,
117
- parameters: { docs: { description: { story: 'Edge case with a single item. Both arrows should be disabled.' } } }
118
+ parameters: { docs: { description: { story: 'Edge case with a single item: nothing to scroll, so no dots render.' } } }
118
119
  };
119
120
 
120
121
  export const NoIcon = {
@@ -40,7 +40,12 @@ export default css`
40
40
  /* the one focus ring — see Foundations/Focus. A two-colour ring whose
41
41
  white inner band keeps it visible on a coloured or dark surface, which a
42
42
  single-colour outline cannot do. */
43
- outline: none;
43
+ /* forced-colors mode does not paint box-shadow, so the ring
44
+ vanishes there; it does paint an outline, in a system colour. This one
45
+ is transparent everywhere else, and the offset lands it on the ring's
46
+ outer band. */
47
+ outline: var(--border-width-2) solid transparent;
48
+ outline-offset: var(--focus-outline-offset);
44
49
  box-shadow: var(--focus-outline-primary);
45
50
  border-radius: var(--focus-outline-radius);
46
51
  }
@@ -62,7 +62,12 @@ export const codeCSS = css`
62
62
  /* the one focus ring — see Foundations/Focus. A two-colour ring whose
63
63
  white inner band keeps it visible on a coloured or dark surface, which a
64
64
  single-colour outline cannot do. */
65
- outline: none;
65
+ /* forced-colors mode does not paint box-shadow, so the ring
66
+ vanishes there; it does paint an outline, in a system colour. This one
67
+ is transparent everywhere else, and the offset lands it on the ring's
68
+ outer band. */
69
+ outline: var(--border-width-2) solid transparent;
70
+ outline-offset: var(--focus-outline-offset);
66
71
  box-shadow: var(--focus-outline-primary);
67
72
  }
68
73
  `;
@@ -1,4 +1,4 @@
1
- import { LitElement, html } from 'lit';
1
+ import { LitElement, html, nothing } from 'lit';
2
2
  // Installs the design tokens into document scope; see src/tokens/tokens.js
3
3
  import '../../tokens/tokens.js';
4
4
  import { codeCSS } from './code.css.js';
@@ -17,20 +17,61 @@ export class BdCode extends LitElement {
17
17
  this.label = '';
18
18
  }
19
19
 
20
+ /* Whether the block is wider than its box. Only then is there anything to
21
+ scroll, and only then does it need to be a tab stop. The box changes
22
+ size with the page, and the content with the light DOM, so both are
23
+ watched. */
24
+ _measure() {
25
+ const pre = this.shadowRoot?.querySelector('pre');
26
+ const scrollable = !!pre && pre.scrollWidth > pre.clientWidth;
27
+ if (scrollable !== this._scrollable) {
28
+ this._scrollable = scrollable;
29
+ this.requestUpdate();
30
+ }
31
+ }
32
+
33
+ updated() {
34
+ const pre = this.shadowRoot.querySelector('pre');
35
+ if (pre && pre !== this._observed) {
36
+ this._resize?.disconnect();
37
+ this._resize = new ResizeObserver(() => this._measure());
38
+ this._resize.observe(pre);
39
+ this._observed = pre;
40
+ }
41
+ if (!pre) { this._resize?.disconnect(); this._observed = null; }
42
+ this._measure();
43
+ }
44
+
45
+ connectedCallback() {
46
+ super.connectedCallback();
47
+ this._content = new MutationObserver(() => this._measure());
48
+ this._content.observe(this, { childList: true, characterData: true, subtree: true });
49
+ }
50
+
51
+ disconnectedCallback() {
52
+ this._content?.disconnect();
53
+ this._resize?.disconnect();
54
+ this._observed = null;
55
+ super.disconnectedCallback();
56
+ }
57
+
20
58
  render() {
21
59
  // Block mode nests <code> inside <pre>: <pre> carries the preformatted
22
60
  // whitespace, <code> carries "this is code". A bare <pre> is only
23
61
  // preformatted text, so assistive tech got no signal it was code at all,
24
62
  // even though the inline variant gave one.
25
63
  //
26
- // The <pre> scrolls horizontally, and a scrollable region that cannot be
27
- // focused cannot be scrolled from the keyboard (WCAG 2.1.1). tabindex
28
- // makes it reachable; role and label give it something to announce, since
29
- // a focusable element with no accessible name says nothing.
64
+ // A <pre> that scrolls horizontally has to be focusable, or it cannot be
65
+ // scrolled from the keyboard (WCAG 2.1.1), and a focusable element needs
66
+ // a name. It is a group, not a region: a named region is a landmark, so
67
+ // every code block on a page joined the landmark list a screen reader
68
+ // navigates by, beside header, main and footer. And it is a tab stop only
69
+ // while it overflows; a snippet that fits has nothing to scroll and was a
70
+ // stop that did nothing.
30
71
  return this.block
31
72
  ? html`<pre
32
- tabindex="0"
33
- role="region"
73
+ tabindex="${this._scrollable ? '0' : nothing}"
74
+ role="group"
34
75
  aria-label="${this.label || 'Code'}"
35
76
  ><code><slot></slot></code></pre>`
36
77
  : html`<code><slot></slot></code>`;
@@ -33,10 +33,13 @@ Inline renders a single \`<code>\` with a tinted background. Block nests
33
33
  \`<code>\` carries "this is code", which is the spec's pairing for a code block.
34
34
 
35
35
  ### The block is keyboard-scrollable
36
- A long line scrolls horizontally, and a scrollable region that cannot be
37
- focused cannot be scrolled from the keyboard. The block therefore takes
38
- \`tabindex="0"\` with \`role="region"\` and an accessible name from \`label\`,
39
- defaulting to "Code", and shows a focus ring when reached by Tab.
36
+ A long line scrolls horizontally, and a scrollable box that cannot be focused
37
+ cannot be scrolled from the keyboard. So while the block is wider than its box
38
+ it takes \`tabindex="0"\` and shows a focus ring when reached by Tab; a block
39
+ that fits has nothing to scroll and is not a tab stop. It is a
40
+ \`role="group"\` named by \`label\`, defaulting to "Code" — a group rather
41
+ than a region, because a named region is a landmark, and a page of code
42
+ samples would otherwise fill the landmark list a screen reader navigates by.
40
43
 
41
44
  ### Notes
42
45
  The inline variant sets its colour explicitly rather than inheriting, so it
@@ -2,7 +2,7 @@ import { LitElement, html } from "lit";
2
2
  // Installs the design tokens into document scope; see src/tokens/tokens.js
3
3
  import "../../tokens/tokens.js";
4
4
  import { dividerHorizontal } from "./divider.css.js";
5
- import { borderColor } from "../shared/token-scales.js";
5
+ import { borderColor, opacity } from "../shared/token-scales.js";
6
6
 
7
7
  export class BdDividerHorizontal extends LitElement {
8
8
  static styles = [dividerHorizontal];
@@ -27,7 +27,14 @@ export class BdDividerHorizontal extends LitElement {
27
27
  ask for `brand-subtle`. The ink vocabulary is still accepted as a
28
28
  fallback for the names already in the markup. */
29
29
  this.color = "";
30
- this.opacity = "0.25";
30
+ /* No opacity by default. It was 0.25, laid over --color-border-default,
31
+ and that composite -- rgb(225,227,230), 1.286:1 on the page -- is
32
+ --color-border-container to within one unit (225,227,229, 1.287:1). So
33
+ every bare divider was spelling a role that exists by dimming a
34
+ different one. The stylesheet now draws that role at full strength,
35
+ which renders the same rule, and opacity is only what a caller asks
36
+ for. */
37
+ this.opacity = "";
31
38
  }
32
39
 
33
40
  render() {
@@ -35,13 +42,14 @@ export class BdDividerHorizontal extends LitElement {
35
42
  // stylesheet's --color-border-default stands rather than being overridden
36
43
  // with an empty value.
37
44
  const ink = borderColor(this.color);
45
+ const fade = opacity(this.opacity);
38
46
  return html`
39
47
  <hr
40
48
  class="hr-line"
41
49
  style="
42
50
  width: ${this.width};
43
51
  ${ink ? `border-top-color: ${ink};` : ''}
44
- opacity: ${this.opacity};
52
+ ${fade ? `opacity: ${fade};` : ''}
45
53
  "
46
54
  />
47
55
  `;
@@ -2,7 +2,7 @@ import { LitElement, html } from "lit";
2
2
  // Installs the design tokens into document scope; see src/tokens/tokens.js
3
3
  import "../../tokens/tokens.js";
4
4
  import { dividerVertical } from "./divider.css.js";
5
- import { borderColor, spacing } from "../shared/token-scales.js";
5
+ import { borderColor, spacing, opacity, borderWidth } from "../shared/token-scales.js";
6
6
 
7
7
  export class BdDividerVertical extends LitElement {
8
8
  static styles = [dividerVertical];
@@ -18,13 +18,19 @@ export class BdDividerVertical extends LitElement {
18
18
  constructor() {
19
19
  super();
20
20
  this.height = "16"; // a spacing step; was a 15px literal, off every scale
21
- this.thickness = "1px";
21
+ /* A border-width step, resolved to var(--border-width-N); "1px" and the
22
+ other px spellings are read as their step. It was the literal "1px",
23
+ written into the style as given -- out of lint:tokens' reach, so any
24
+ length, or any string at all, could stand in for a hairline. An
25
+ unknown value falls back to the 1px step. */
26
+ this.thickness = "1";
22
27
  /* No colour by default — see the note in divider-horizontal.js. The
23
28
  stylesheet's --color-border-default is the role for a hairline; "subtle"
24
29
  was a text role that happened to resolve to the same step. The prop
25
30
  resolves through BORDER_ROLES first; see token-scales.js. */
26
31
  this.color = "";
27
- this.opacity = "1.00";
32
+ // none by default, so none is written; a value is kept only from 0 to 1
33
+ this.opacity = "";
28
34
 
29
35
  }
30
36
 
@@ -32,14 +38,15 @@ export class BdDividerVertical extends LitElement {
32
38
  // See divider-horizontal.js: no colour means no declaration, so the
33
39
  // stylesheet's --color-border-default stands.
34
40
  const ink = borderColor(this.color);
41
+ const fade = opacity(this.opacity);
35
42
  return html`
36
43
  <div
37
44
  class="divider"
38
45
  style="
39
- width: ${this.thickness};
46
+ width: ${borderWidth(this.thickness) || borderWidth('1')};
40
47
  height: ${spacing(this.height)};
41
48
  ${ink ? `background-color: ${ink};` : ''}
42
- opacity: ${this.opacity};
49
+ ${fade ? `opacity: ${fade};` : ''}
43
50
  "
44
51
  ></div>
45
52
  `;