@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.
- package/CHANGELOG.md +81 -0
- package/README.md +43 -12
- package/package.json +4 -2
- package/schemas/component-manifest.json +207 -49
- package/schemas/manifest-omissions.json +2 -0
- package/src/components/accordion/accordion.css.js +12 -1
- package/src/components/agent-endpoints/agent-endpoints.js +1 -1
- package/src/components/alert/alert.css.js +22 -1
- package/src/components/anchor/anchor-nav.css.js +6 -1
- package/src/components/annotation/annotation.css.js +6 -1
- package/src/components/architecture.stories.js +6 -4
- package/src/components/avatar/avatar.js +37 -11
- package/src/components/avatar/avatar.stories.js +10 -6
- package/src/components/awards/awards-flex.css.js +31 -129
- package/src/components/awards/awards-flex.js +66 -116
- package/src/components/awards/awards-flex.stories.js +8 -6
- package/src/components/back/back.css.js +6 -1
- package/src/components/back/back.js +24 -12
- package/src/components/back/back.stories.js +3 -3
- package/src/components/badge/badge.js +6 -3
- package/src/components/badge/badge.stories.js +13 -9
- package/src/components/bento-grid/bento-grid.js +1 -1
- package/src/components/blockquote/blockquote.css.js +12 -3
- package/src/components/blockquote/blockquote.js +12 -5
- package/src/components/blockquote/blockquote.stories.js +6 -4
- package/src/components/button/button.css.js +46 -25
- package/src/components/button/button.js +44 -43
- package/src/components/button/button.stories.js +2 -2
- package/src/components/caption/caption.css.js +10 -0
- package/src/components/caption/caption.stories.js +4 -4
- package/src/components/carousel/carousel.css.js +19 -116
- package/src/components/carousel/carousel.js +70 -74
- package/src/components/carousel/carousel.stories.js +9 -8
- package/src/components/checkbox/checkbox.css.js +6 -1
- package/src/components/code/code.css.js +6 -1
- package/src/components/code/code.js +48 -7
- package/src/components/code/code.stories.js +7 -4
- package/src/components/divider/divider-horizontal.js +11 -3
- package/src/components/divider/divider-vertical.js +12 -5
- package/src/components/divider/divider.css.js +4 -1
- package/src/components/divider/divider.stories.js +6 -6
- package/src/components/dropdown/dropdown.js +67 -13
- package/src/components/dropdown/dropdown.stories.js +15 -14
- package/src/components/fieldset/fieldset.stories.js +41 -16
- package/src/components/footer/footer-nav-menu.css.js +6 -1
- package/src/components/footer/footer.css.js +6 -1
- package/src/components/header/header-code.js +1 -1
- package/src/components/header/header.css.js +9 -4
- package/src/components/heading/heading.stories.js +64 -31
- package/src/components/highlight/highlight.js +74 -34
- package/src/components/highlight/highlight.stories.js +5 -4
- package/src/components/icons/icon-element.js +49 -6
- package/src/components/icons/icons.stories.js +7 -0
- package/src/components/image/image.css.js +9 -98
- package/src/components/image/image.js +7 -2
- package/src/components/input/custom-form.stories.js +14 -19
- package/src/components/input/input-clipboard.css.js +6 -1
- package/src/components/input/input-clipboard.js +56 -8
- package/src/components/input/input.js +19 -2
- package/src/components/label/label.js +87 -1
- package/src/components/label/label.stories.js +21 -12
- package/src/components/link/link.css.js +52 -3
- package/src/components/link/link.js +52 -17
- package/src/components/link/link.stories.js +114 -3
- package/src/components/list/list-item/list-item.css.js +4 -6
- package/src/components/list/list-item/list-item.js +53 -5
- package/src/components/list/list-item/list-item.stories.js +22 -0
- package/src/components/list/list.js +27 -0
- package/src/components/modal/modal.css.js +6 -1
- package/src/components/pagination-dots/pagination-dots.css.js +161 -0
- package/src/components/pagination-dots/pagination-dots.js +202 -0
- package/src/components/pagination-dots/pagination-dots.stories.js +171 -0
- package/src/components/paragraph/paragraph.css.js +37 -1
- package/src/components/paragraph/paragraph.js +19 -2
- package/src/components/paragraph/paragraph.stories.js +40 -6
- package/src/components/picture/picture.css.js +8 -67
- package/src/components/picture/picture.js +15 -8
- package/src/components/pricing-cards/pricing-card.css.js +12 -2
- package/src/components/pricing-cards/pricing-card.js +4 -4
- package/src/components/progress/progress.js +24 -4
- package/src/components/progress/progress.stories.js +8 -6
- package/src/components/radio/radio.css.js +6 -1
- package/src/components/radio/radio.js +48 -3
- package/src/components/radio/radio.stories.js +1 -1
- package/src/components/shared/form-field.css.js +30 -4
- package/src/components/shared/token-scales.js +16 -0
- package/src/components/spinner/spinner.css.js +31 -4
- package/src/components/spinner/spinner.js +26 -4
- package/src/components/spinner/spinner.stories.js +10 -7
- package/src/components/switch/switch.css.js +21 -3
- package/src/components/switch/switch.js +8 -1
- package/src/components/switch/switch.stories.js +2 -1
- package/src/components/tab-list/tab-list.css.js +6 -1
- package/src/components/terminal/terminal-cursor.js +11 -0
- package/src/components/terminal/terminal-message.js +1 -1
- package/src/components/termsOfUse/terms.css.js +6 -1
- package/src/components/textarea/textarea.stories.js +23 -0
- package/src/components/tooltip/tooltip.css.js +24 -1
- package/src/components/tooltip/tooltip.js +135 -5
- package/src/components/tooltip/tooltip.stories.js +20 -14
- package/src/components/trust-panel/trust-panel.js +2 -2
- package/src/components/video/video.js +38 -3
- package/src/components/video/video.stories.js +10 -5
- package/src/components/video-player/video-player.css.js +6 -1
- package/src/tokens/elevation.stories.js +2 -3
- package/src/tokens/focus.stories.js +81 -15
- package/src/tokens/motion.stories.js +0 -6
- package/src/tokens/tokens.css +174 -97
- package/src/tokens/tokens.js +174 -97
- package/src/tokens/vertical-rhythm.stories.js +1 -1
- package/src/stories/Button.js +0 -20
- package/src/stories/Button.stories.js +0 -46
- package/src/stories/Configure.mdx +0 -364
- package/src/stories/Header.js +0 -45
- package/src/stories/Header.stories.js +0 -24
- package/src/stories/Page.js +0 -62
- package/src/stories/Page.stories.js +0 -20
- package/src/stories/assets/accessibility.png +0 -0
- package/src/stories/assets/accessibility.svg +0 -1
- package/src/stories/assets/addon-library.png +0 -0
- package/src/stories/assets/assets.png +0 -0
- package/src/stories/assets/avif-test-image.avif +0 -0
- package/src/stories/assets/context.png +0 -0
- package/src/stories/assets/discord.svg +0 -1
- package/src/stories/assets/docs.png +0 -0
- package/src/stories/assets/figma-plugin.png +0 -0
- package/src/stories/assets/github.svg +0 -1
- package/src/stories/assets/share.png +0 -0
- package/src/stories/assets/styling.png +0 -0
- package/src/stories/assets/testing.png +0 -0
- package/src/stories/assets/theming.png +0 -0
- package/src/stories/assets/tutorials.svg +0 -1
- package/src/stories/assets/youtube.svg +0 -1
- package/src/stories/button.css +0 -30
- package/src/stories/header.css +0 -32
- 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
|
-
/**
|
|
10
|
-
|
|
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
|
|