@repobit/dex-system-design 1.0.1 → 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 +81 -0
  2. package/README.md +43 -12
  3. package/package.json +4 -2
  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
@@ -0,0 +1,161 @@
1
+ import { css } from "lit";
2
+
3
+ /**
4
+ * Styled entirely from src/tokens/tokens.js -- see CLAUDE.md, "The foundation
5
+ * rule".
6
+ *
7
+ * The geometry is dimension, not spacing, on purpose: a dot is a fixed mark
8
+ * and should not grow when a reader enlarges their text. The gap around each
9
+ * dot is spacing, because it is padding, and padding is allowed to grow. So a
10
+ * larger root font makes the targets wider and leaves the dots as drawn.
11
+ */
12
+ export default css`
13
+ :host {
14
+ display: inline-flex;
15
+ }
16
+
17
+ .dots {
18
+ display: flex;
19
+ align-items: center;
20
+ /* Each button carries its own target, so the space between two dots comes
21
+ from the boxes. A gap on top would push them apart without making
22
+ either easier to hit. */
23
+ }
24
+
25
+ /* ---- dots ---------------------------------------------------------------
26
+ The target. 24px tall for WCAG 2.5.8, and the dot plus 8px either side
27
+ wide: 24 for a resting dot, 56 for the current pill. The dot is drawn
28
+ inside it rather than being it. */
29
+ .dot {
30
+ display: grid;
31
+ place-items: center;
32
+ height: var(--dimension-24);
33
+ margin: 0;
34
+ padding: 0 var(--spacing-8);
35
+ border: none;
36
+ background: transparent;
37
+ cursor: pointer;
38
+ }
39
+
40
+ /* The mark is a real element rather than a ::before, because the current
41
+ one holds the progress fill and a Web Animation needs an element to run
42
+ on. */
43
+ .mark {
44
+ position: relative;
45
+ display: block;
46
+ overflow: hidden;
47
+ width: var(--dimension-8);
48
+ height: var(--dimension-8);
49
+ border-radius: var(--radius-full);
50
+ /* no role: a resting slide mark. No semantic role names an inactive
51
+ indicator, and it has to clear the 3:1 WCAG 1.4.11 asks of a control's
52
+ only visual: neutral-500 measures 4.81 on the page and 4.46 on the
53
+ blue-50 info tint the homepage puts it on. The design's 30% neutral-900
54
+ measures 1.94 there; neutral-300, which bd-awards-flex used to draw,
55
+ 2.10; and neutral-450 would clear it but exists for the dark theme only. */
56
+ background: var(--color-neutral-500);
57
+ transition: width var(--transition-duration-normal) var(--transition-easing-ease),
58
+ background var(--transition-duration-normal) var(--transition-easing-ease);
59
+ }
60
+
61
+ .dot:hover .mark {
62
+ /* no role: the same mark under the pointer, one step darker */
63
+ background: var(--color-neutral-600);
64
+ }
65
+
66
+ /* The progress. Scaled rather than sized, so the fill is drawn by the
67
+ compositor and never re-lays the row out. Without autoplay the current
68
+ fill is whole, which is the pill as it always was; with it, the atom's
69
+ Web Animation runs this from 0 to 1 over the delay and the end of the
70
+ animation is the change of slide. */
71
+ .fill {
72
+ position: absolute;
73
+ inset: 0;
74
+ background: var(--indicator-color);
75
+ transform: scaleX(0);
76
+ transform-origin: left center;
77
+ }
78
+
79
+ :host(:dir(rtl)) .fill {
80
+ transform-origin: right center;
81
+ }
82
+
83
+ /* The current slide. The pill is the design's shape. The colour is the
84
+ indicator role, the one every current-item mark in the package shares --
85
+ the tab and nav underlines -- so they cannot drift apart. The design draws
86
+ it neutral-900 as an unbound value; using that would make this the one
87
+ current mark in the system that is not the indicator. */
88
+ .dot[aria-current="true"] {
89
+ cursor: default;
90
+ }
91
+
92
+ /* The current pill is also the autoplay track: the indicator colour at a
93
+ fifth, laid over whatever surface the dots sit on, inside an indicator
94
+ edge. Translucent rather than a colour of its own, so it belongs to every
95
+ ground -- a white track read as a hole on the awards tint and on a band.
96
+ It was the resting grey before that, and the blue fill could not be seen
97
+ on it: blue-500 on neutral-500 is 1.06:1.
98
+ .
99
+ The edge is 2px, drawn as an outline pulled inside the box rather than as
100
+ a border. At 1px it was faint at 1x density, and that is the moment it
101
+ matters most: the start of each slide, when nothing has filled yet and
102
+ the edge alone marks the pill. A 2px border would have taken the 2px out
103
+ of the box and left the fill a 4px sliver; an outline takes no room, and
104
+ it paints above the fill in the fill's own colour, so the fill keeps the
105
+ full 8px and the two merge where they meet. Under a forced palette the
106
+ outline is repainted in a system colour and still draws.
107
+ .
108
+ Measured: the fill against the track is 3.10:1 or better on the page,
109
+ the container, the subtle band and the awards tint; the edge against the
110
+ ground 4.05:1 or better. When the fill is whole the edge and fill are one
111
+ colour, which is the solid pill a dots row without autoplay shows. */
112
+ .dot[aria-current="true"] .mark {
113
+ width: var(--dimension-40);
114
+ background: color-mix(in srgb, var(--indicator-color) 20%, transparent);
115
+ outline: var(--border-width-2) solid var(--indicator-color);
116
+ outline-offset: calc(-1 * var(--border-width-2));
117
+ }
118
+
119
+ .dot[aria-current="true"] .fill {
120
+ transform: scaleX(1);
121
+ }
122
+
123
+ /* ---- focus ------------------------------------------------------------
124
+ the one focus ring -- see Foundations/Focus. It follows the target, not
125
+ the mark, so keyboard focus shows the size the button really is. */
126
+ .dot:focus {
127
+ outline: none;
128
+ }
129
+
130
+ .dot:focus-visible {
131
+ box-shadow: var(--focus-outline-primary);
132
+ /* forced-colors mode does not paint box-shadow, so the ring
133
+ vanishes there; it does paint an outline, in a system colour. This one
134
+ is transparent everywhere else, and the offset lands it on the ring's
135
+ outer band. */
136
+ outline: var(--border-width-2) solid transparent;
137
+ outline-offset: var(--focus-outline-offset);
138
+ border-radius: var(--radius-full);
139
+ }
140
+
141
+ @media (prefers-reduced-motion: reduce) {
142
+ .mark {
143
+ transition: none;
144
+ }
145
+ }
146
+
147
+ /* A forced palette repaints backgrounds, which would erase every dot.
148
+ Borders survive it, so a resting dot is drawn as a ring, and the fill is
149
+ kept out of the repaint and drawn in the highlight colour -- so the
150
+ current pill still shows, and so does the time it has left. */
151
+ @media (forced-colors: active) {
152
+ .mark {
153
+ box-sizing: border-box;
154
+ border: var(--border-width-1) solid currentColor;
155
+ }
156
+ .fill {
157
+ forced-color-adjust: none;
158
+ background: Highlight;
159
+ }
160
+ }
161
+ `;
@@ -0,0 +1,202 @@
1
+ import { LitElement, html, nothing } from "lit";
2
+ // Installs the design tokens into document scope; see src/tokens/tokens.js
3
+ import "../../tokens/tokens.js";
4
+ import paginationDotsCSS from "./pagination-dots.css.js";
5
+
6
+ /**
7
+ * Which of a run of slides is showing, and a way to go to another: the
8
+ * package's one set of slide dots. Drawn in the foundation Figma file as
9
+ * Pagination dot and Pagination dots.
10
+ *
11
+ * bd-awards-flex and bd-carousel-section each used to draw their own -- a dot
12
+ * row that claimed to be a tablist, and a pair of <img role="button"> arrows --
13
+ * and the two had drifted apart on contrast, semantics and names. The carousel
14
+ * that owns the slides stays the owner of the slides: it sets current, and
15
+ * listens for bd-change.
16
+ *
17
+ * SEMANTICS. A group of buttons, the current one marked aria-current. Not a
18
+ * tablist: a tab needs a tabpanel it controls, and this atom does not own or
19
+ * know the panels. Each button is named for what it does ("Go to slide 2"),
20
+ * and the group for what it moves through. Both names are attributes, so a
21
+ * page in another language can say them.
22
+ *
23
+ * TARGET SIZE. A dot is 8px and the design spaces them 8px apart. Clickable at
24
+ * that size they fail WCAG 2.5.8, so each button is a 24px target with the dot
25
+ * drawn inside it. The dots end up 16px apart instead of 8; the part you can
26
+ * hit grows and the dots do not.
27
+ *
28
+ * AUTOPLAY. With autoplay set, the atom advances itself every delay ms and the
29
+ * current pill fills as the time runs: the progress IS the clock, so the fill
30
+ * and the change of slide cannot drift apart. That is why the timer lives here
31
+ * and not in the carousel. Every advance fires bd-change, exactly as a click.
32
+ *
33
+ * Stopping it. WCAG 2.2.2 wants a way to stop anything that moves on its own,
34
+ * and there is no pause button, so the dots are the mechanism: picking one
35
+ * stops autoplay for good (paused), because a visitor who chose a slide has
36
+ * said which one they want to read. The clock also holds -- transiently --
37
+ * while the pointer or focus is on the dots, or while the owner sets held for
38
+ * its own slides. Under prefers-reduced-motion it never starts.
39
+ *
40
+ * count below 2 renders nothing: one page has nothing to move to.
41
+ */
42
+ const MIN_DELAY = 1000;
43
+
44
+ class BdPaginationDots extends LitElement {
45
+ static styles = [paginationDotsCSS];
46
+
47
+ static properties = {
48
+ count : { type: Number },
49
+ current : { type: Number, reflect: true },
50
+ label : { type: String },
51
+ itemLabel: { type: String, attribute: "item-label" },
52
+ autoplay : { type: Boolean, reflect: true },
53
+ delay : { type: Number },
54
+ paused : { type: Boolean, reflect: true },
55
+ held : { type: Boolean },
56
+ _selfHeld: { state: true }
57
+ };
58
+
59
+ constructor() {
60
+ super();
61
+ this.count = 0;
62
+ // 1-based, like the names the buttons announce
63
+ this.current = 1;
64
+ this.label = "Slides";
65
+ this.itemLabel = "Go to slide";
66
+ this.autoplay = false;
67
+ this.delay = 4000;
68
+ this.paused = false;
69
+ this.held = false;
70
+ this._selfHeld = false;
71
+ this._progress = null;
72
+ this._announced = undefined;
73
+ this._onMotionChange = () => { if (this._motionQuery.matches) this.paused = true; };
74
+ }
75
+
76
+ connectedCallback() {
77
+ super.connectedCallback();
78
+ // Watched rather than read once, so turning the setting on mid-visit stops it.
79
+ if (typeof window !== "undefined" && window.matchMedia) {
80
+ this._motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
81
+ if (this._motionQuery.matches) this.paused = true;
82
+ this._motionQuery.addEventListener("change", this._onMotionChange);
83
+ }
84
+ }
85
+
86
+ disconnectedCallback() {
87
+ super.disconnectedCallback();
88
+ this._motionQuery?.removeEventListener("change", this._onMotionChange);
89
+ this._stopProgress();
90
+ }
91
+
92
+ /** count is a whole number of dots, and current always names one of them. */
93
+ willUpdate(changed) {
94
+ if (!changed.has("count") && !changed.has("current")) return;
95
+ const count = Math.max(0, Math.floor(Number(this.count) || 0));
96
+ if (count !== this.count) this.count = count;
97
+ const current = Math.min(Math.max(1, Math.floor(Number(this.current) || 1)), Math.max(1, count));
98
+ if (current !== this.current) this.current = current;
99
+ }
100
+
101
+ updated(changed) {
102
+ // A new slide, a new clock or autoplay switched: the fill starts again.
103
+ if (["count", "current", "autoplay", "delay"].some(k => changed.has(k))) this._startProgress();
104
+ this._syncProgress();
105
+ this._announcePlayState();
106
+ }
107
+
108
+ get _running() {
109
+ return this.autoplay && this.count > 1;
110
+ }
111
+
112
+ get _moving() {
113
+ return this._running && !this.paused && !this.held && !this._selfHeld;
114
+ }
115
+
116
+ /* One Web Animation per slide, on the current pill's fill. Its finish is the
117
+ advance; pausing it is pausing the clock, and the fill holds where it
118
+ stopped rather than snapping back. */
119
+ _startProgress() {
120
+ this._stopProgress();
121
+ if (!this._running) return;
122
+ const fill = this.renderRoot.querySelector('.dot[aria-current="true"] .fill');
123
+ if (!fill?.animate) return;
124
+ const delay = Math.max(MIN_DELAY, Number(this.delay) || 0);
125
+ const progress = fill.animate(
126
+ [{ transform: "scaleX(0)" }, { transform: "scaleX(1)" }],
127
+ { duration: delay, easing: "linear", fill: "forwards" }
128
+ );
129
+ this._progress = progress;
130
+ progress.finished.then(() => {
131
+ if (this._progress !== progress) return;
132
+ this._select(this.current >= this.count ? 1 : this.current + 1);
133
+ }, () => {});
134
+ }
135
+
136
+ _stopProgress() {
137
+ const progress = this._progress;
138
+ this._progress = null;
139
+ progress?.cancel();
140
+ }
141
+
142
+ _syncProgress() {
143
+ const progress = this._progress;
144
+ if (!progress) return;
145
+ if (this._moving && progress.playState === "paused") progress.play();
146
+ else if (!this._moving && progress.playState === "running") progress.pause();
147
+ }
148
+
149
+ /* bd-toggle whenever the slides start or stop moving, for whatever reason.
150
+ An owner needs it for its live region -- slides that change on their own
151
+ must not be announced, and slides that change because the visitor asked
152
+ must be -- and only this atom knows about the holds it places itself.
153
+ Fired once on first render too, so the owner starts in step. */
154
+ _announcePlayState() {
155
+ const state = `${this.paused}|${this._moving}`;
156
+ if (state === this._announced) return;
157
+ this._announced = state;
158
+ this.dispatchEvent(new CustomEvent("bd-toggle", {
159
+ detail : { paused: this.paused, moving: this._moving },
160
+ bubbles : true,
161
+ composed: true
162
+ }));
163
+ }
164
+
165
+ _select(n) {
166
+ if (n === this.current) return;
167
+ this.current = n;
168
+ this.dispatchEvent(new CustomEvent("bd-change", {
169
+ detail : { current: n },
170
+ bubbles : true,
171
+ composed: true
172
+ }));
173
+ }
174
+
175
+ // The visitor's pick: it moves, and it stops autoplay for good.
176
+ _pick(n) {
177
+ if (this._running) this.paused = true;
178
+ this._select(n);
179
+ }
180
+
181
+ render() {
182
+ if (this.count < 2) return nothing;
183
+ const pages = Array.from({ length: this.count }, (_, i) => i + 1);
184
+ return html`<div
185
+ class="dots"
186
+ role="group"
187
+ aria-label="${this.label}"
188
+ @pointerenter=${() => { this._selfHeld = true; }}
189
+ @pointerleave=${() => { this._selfHeld = this.renderRoot.contains(this.renderRoot.activeElement); }}
190
+ @focusin=${() => { this._selfHeld = true; }}
191
+ @focusout=${e => { this._selfHeld = this.renderRoot.contains(e.relatedTarget) || this.matches(":hover"); }}
192
+ >${pages.map(n => html`<button
193
+ type="button"
194
+ class="dot"
195
+ aria-label="${this.itemLabel} ${n}"
196
+ aria-current="${n === this.current ? "true" : nothing}"
197
+ @click=${() => this._pick(n)}
198
+ ><span class="mark"><span class="fill"></span></span></button>`)}</div>`;
199
+ }
200
+ }
201
+
202
+ customElements.define("bd-pagination-dots", BdPaginationDots);
@@ -0,0 +1,171 @@
1
+ import { html } from 'lit';
2
+ import './pagination-dots.js';
3
+
4
+ export default {
5
+ title : 'Atoms/Pagination Dots',
6
+ tags : ['autodocs'],
7
+ parameters: {
8
+ docs: {
9
+ description: {
10
+ component: `
11
+ \`<bd-pagination-dots>\` is the package's one set of slide dots: which slide
12
+ is showing, and a way to go to another. \`bd-awards-flex\` and
13
+ \`bd-carousel-section\` both use it.
14
+
15
+ \`\`\`html
16
+ <bd-pagination-dots count="5" current="1"></bd-pagination-dots>
17
+ <bd-pagination-dots count="5" current="1" autoplay delay="4000"></bd-pagination-dots>
18
+ \`\`\`
19
+
20
+ The carousel that owns the slides stays their owner. It sets \`current\`, and
21
+ listens for \`bd-change\`, whose \`detail.current\` is the slide to show --
22
+ picked by a dot or by the autoplay clock.
23
+
24
+ - **A group of buttons**, the current one marked \`aria-current\`. Not a
25
+ tablist: a tab needs a panel it controls, and this atom does not own the
26
+ panels.
27
+ - **Two names, both attributes.** \`label\` names the group (default
28
+ "Slides") and \`item-label\` starts each button's name (default "Go to
29
+ slide", so "Go to slide 2"). A page in another language sets both.
30
+ - **Every dot is a 24px target.** The dot is drawn inside it, so the dots sit
31
+ 16px apart rather than the design's 8. At 8 they would fail WCAG 2.5.8.
32
+ - **The current dot is a 40px pill** in the indicator colour, the same
33
+ colour as every other current-item mark in the package. The design draws it
34
+ neutral-900; that would make it the only current mark that is not the
35
+ indicator.
36
+ - **A resting dot is neutral-500**, the lightest step that clears 3:1 on
37
+ every light ground it can sit on. The design's faint grey measures 1.94:1.
38
+ - **\`autoplay\` puts the clock in the pill.**- **\`autoplay\` puts the clock in the pill.** The atom advances itself every
39
+ \`delay\` ms, wrapping after the last slide. The current pill is the
40
+ progress: a white track inside an indicator-colour edge, which the
41
+ indicator fills as the time runs, so the fill and the change of slide are
42
+ one event and cannot drift apart. Without autoplay the pill is filled whole.
43
+ - **The dots are how it stops** (WCAG 2.2.2). Picking one stops autoplay for
44
+ good and sets \`paused\`. The clock also holds while the pointer or focus is
45
+ on the dots, or while the owner sets \`held\` -- the carousel does that for
46
+ its own slides. Under \`prefers-reduced-motion\` it never starts.
47
+ - **\`bd-toggle\`** fires when the slides start or stop moving, with
48
+ \`detail.paused\` and \`detail.moving\`. An owner uses it for its live
49
+ region: off while slides change by themselves, polite once they are still.
50
+ - **\`count\` below 2 renders nothing.** One page has nowhere to go.
51
+ `
52
+ }
53
+ }
54
+ },
55
+ argTypes: {
56
+ count: {
57
+ control : { type: 'number', min: 0, max: 12, step: 1 },
58
+ description: 'How many dots. Below 2 nothing renders.',
59
+ table : { type: { summary: 'number' }, defaultValue: { summary: '0' } }
60
+ },
61
+ current: {
62
+ control : { type: 'number', min: 1, max: 12, step: 1 },
63
+ description: 'Which dot is current, 1-based. Held between 1 and `count`.',
64
+ table : { type: { summary: 'number' }, defaultValue: { summary: '1' } }
65
+ },
66
+ label: {
67
+ control : 'text',
68
+ description: 'The group\'s accessible name: what the dots move through.',
69
+ table : { type: { summary: 'string' }, defaultValue: { summary: 'Slides' } }
70
+ },
71
+ itemLabel: {
72
+ control : 'text',
73
+ description: 'Starts each button\'s accessible name; the dot\'s number follows it.',
74
+ table : { type: { summary: 'string' }, defaultValue: { summary: 'Go to slide' } }
75
+ },
76
+ autoplay: {
77
+ control : 'boolean',
78
+ description: 'Advance every `delay` ms, with the progress in the current pill. Picking a dot stops it.',
79
+ table : { type: { summary: 'boolean' }, defaultValue: { summary: 'false' } }
80
+ },
81
+ delay: {
82
+ control : { type: 'number', min: 1000, step: 500 },
83
+ description: 'Milliseconds per slide under autoplay. Held at 1000 or more.',
84
+ table : { type: { summary: 'number' }, defaultValue: { summary: '4000' } }
85
+ },
86
+ paused: {
87
+ control : 'boolean',
88
+ description: 'Autoplay stopped for good. Set when the visitor picks a dot.',
89
+ table : { type: { summary: 'boolean' }, defaultValue: { summary: 'false' } }
90
+ }
91
+ }
92
+ };
93
+
94
+ const caption = text => html`
95
+ <p style="margin:var(--spacing-24) 0 var(--spacing-8);font:var(--typography-fontSize-xs)/1.4 var(--typography-fontFamily-sans);color:var(--color-text-muted)">${text}</p>
96
+ `;
97
+
98
+ export const Default = {
99
+ name : 'Default — as the homepage uses it',
100
+ render: () => html`
101
+ <div style="background:var(--color-surface-info);padding:var(--spacing-32);border-radius:var(--radius-3xl);display:flex;justify-content:center;">
102
+ <bd-pagination-dots count="5" current="1" label="Awards"></bd-pagination-dots>
103
+ </div>
104
+ `,
105
+ parameters: { docs: { description: { story: 'Five slides on the awards carousel\'s info tint. Each dot is a 24px target; tab through them to see the ring follow the target, not the dot.' } } }
106
+ };
107
+
108
+ export const Counts = {
109
+ name : 'Counts',
110
+ render: () => html`
111
+ ${[2, 3, 4, 5, 8].map(count => html`
112
+ ${caption(`count="${count}"`)}
113
+ <bd-pagination-dots count="${count}" current="2"></bd-pagination-dots>
114
+ `)}
115
+ ${caption('count="1" — renders nothing, because one page has nowhere to go')}
116
+ <bd-pagination-dots count="1"></bd-pagination-dots>
117
+ `,
118
+ parameters: { docs: { description: { story: 'The same atom at every count the homepage could need, current on the second dot.' } } }
119
+ };
120
+
121
+ export const OnGrounds = {
122
+ name : 'On every light ground',
123
+ render: () => html`
124
+ ${[['page — surface/default', 'var(--color-surface-default)'], ['card — surface/container', 'var(--color-surface-container)'], ['band — surface/subtle', 'var(--color-surface-subtle)'], ['awards — surface/info', 'var(--color-surface-info)']].map(([label, bg]) => html`
125
+ ${caption(label)}
126
+ <div style="background:${bg};padding:var(--spacing-16) var(--spacing-24);border-radius:var(--radius-md);display:inline-flex;">
127
+ <bd-pagination-dots count="5" current="3"></bd-pagination-dots>
128
+ </div>
129
+ `)}
130
+ `,
131
+ parameters: { docs: { description: { story: 'The resting dot clears 3:1 on all four (WCAG 1.4.11), at 4.29:1 or better; the current pill, the indicator role, at 4.05:1 or better.' } } }
132
+ };
133
+
134
+ export const Interactive = {
135
+ name : 'Picking a dot',
136
+ render: () => html`
137
+ <bd-pagination-dots
138
+ count="5"
139
+ current="1"
140
+ @bd-change=${e => { e.currentTarget.nextElementSibling.textContent = `bd-change — detail.current is ${e.detail.current}`; }}
141
+ ></bd-pagination-dots>
142
+ <p style="margin:var(--spacing-12) 0 0;font:var(--typography-fontSize-sm)/1.5 var(--typography-fontFamily-sans);color:var(--color-text-secondary)" aria-live="polite">Pick a dot.</p>
143
+ `,
144
+ parameters: { docs: { description: { story: 'The atom moves its own current dot and fires `bd-change`. Picking the current dot does nothing and fires nothing.' } } }
145
+ };
146
+
147
+ export const Autoplay = {
148
+ name : 'Autoplay — the pill is the clock',
149
+ render: () => html`
150
+ <div style="background:var(--color-surface-info);padding:var(--spacing-32);border-radius:var(--radius-3xl);display:flex;justify-content:center;">
151
+ <bd-pagination-dots count="3" current="1" label="Awards" autoplay delay="4000"></bd-pagination-dots>
152
+ </div>
153
+ `,
154
+ parameters: { docs: { description: { story: 'As bd-awards-flex uses it. The current pill fills over four seconds and the slide changes when it is full. Point at the dots or tab into them and the fill holds where it is; pick one and autoplay stops for good.' } } }
155
+ };
156
+
157
+ export const Playground = {
158
+ name : '🛝 Playground',
159
+ args : { count: 5, current: 1, label: 'Slides', itemLabel: 'Go to slide', autoplay: false, delay: 4000, paused: false },
160
+ render: args => html`
161
+ <bd-pagination-dots
162
+ count="${args.count}"
163
+ current="${args.current}"
164
+ label="${args.label}"
165
+ item-label="${args.itemLabel}"
166
+ ?autoplay=${args.autoplay}
167
+ delay="${args.delay}"
168
+ ?paused=${args.paused}
169
+ ></bd-pagination-dots>
170
+ `
171
+ };
@@ -1,4 +1,5 @@
1
- import { css } from "lit";
1
+ import { css, unsafeCSS } from "lit";
2
+ import { below } from "../shared/token-scales.js";
2
3
 
3
4
  export default css`
4
5
  /* --color-text-primary, not --color-neutral-1000. Body copy was set in pure
@@ -52,4 +53,39 @@ export default css`
52
53
  line-height: var(--typography-body-lead-lineHeight);
53
54
  letter-spacing: var(--typography-body-lead-letterSpacing);
54
55
  }
56
+
57
+ /* The eyebrow: a short label above a section title. It takes the
58
+ label-large role -- the role whose job is a short label rather than
59
+ running text -- which is 16px at 1.5 like body-regular and Medium 500
60
+ where body-regular is 400. Brand ink is part of the pattern: all five
61
+ eyebrows in the package and every one on the homepage are text-brand.
62
+ A color attribute still wins, for an eyebrow on a dark band. */
63
+ :host([variant="eyebrow"]) p {
64
+ font-size: var(--typography-label-large-fontSize);
65
+ font-weight: var(--typography-label-large-fontWeight);
66
+ line-height: var(--typography-label-large-lineHeight);
67
+ letter-spacing: var(--typography-label-large-letterSpacing);
68
+ font-family: var(--typography-label-large-fontFamily);
69
+ color: var(--color-text-brand);
70
+ }
71
+
72
+ /* Below lg the eyebrow steps down to label-default: 14px, the same Medium
73
+ 500 at 1.5. The homepage draws it 14 on the 390 and 768 frames and 16 on
74
+ desktop, so the step sits at lg, which is also where every fluid role in
75
+ the system stops growing. A step between two declared roles rather than a
76
+ fluid size, because the label family has no fluid role, and one for a
77
+ single variant would be a new token where two existing roles already
78
+ answer both ends exactly. */
79
+ @media ${unsafeCSS(below("lg"))} {
80
+ :host([variant="eyebrow"]) p {
81
+ font-size: var(--typography-label-default-fontSize);
82
+ font-weight: var(--typography-label-default-fontWeight);
83
+ line-height: var(--typography-label-default-lineHeight);
84
+ letter-spacing: var(--typography-label-default-letterSpacing);
85
+ }
86
+ }
87
+
88
+
89
+
90
+
55
91
  `;
@@ -6,8 +6,21 @@ import {
6
6
  } from "../shared/token-scales.js";
7
7
  import paragraphCSS from "./paragraph.css.js";
8
8
 
9
- /** The five body roles the stylesheet carries. */
10
- const VARIANTS = ["xsmall", "small", "regular", "large", "lead"];
9
+ /**
10
+ * The five body roles the stylesheet carries, and the eyebrow.
11
+ *
12
+ * eyebrow is the short brand-ink label above a section title. It reads the
13
+ * label-large role -- 16px, Medium 500, 1.5 -- from lg up, and label-default
14
+ * (14px, the same weight and leading) below lg, rather than a role of its
15
+ * own, so it adds no token. It exists because five components drew the pattern
16
+ * and disagreed on weight: bento-grid and header-code at the body role's 400,
17
+ * trust-panel twice and agent-endpoints at a semibold 600 passed through
18
+ * font-weight, and the homepage design at 500. The phase-1 eyebrow
19
+ * investigation found no role was needed while every eyebrow resolved
20
+ * through body-regular; once three of them reached past it for a weight, one
21
+ * spelling became the thing that keeps them together.
22
+ */
23
+ const VARIANTS = ["xsmall", "small", "regular", "large", "lead", "eyebrow"];
11
24
 
12
25
  export class BdP extends LitElement {
13
26
  static properties = {
@@ -21,6 +34,10 @@ export class BdP extends LitElement {
21
34
  fontWeight : { type: String, attribute: "font-weight" },
22
35
  fontSize : { type: String, attribute: "font-size" },
23
36
  letterSpacing: { type: String, attribute: "letter-spacing" },
37
+ /* A semantic role, the same closed set bd-h and bd-display take. Without
38
+ it a paragraph's ink could only come from the composing component's
39
+ CSS, so a page could not say an eyebrow is brand-blue or a lead on a
40
+ brand band is white. */
24
41
  color : { type: String }
25
42
  };
26
43