@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.
- package/CHANGELOG.md +89 -0
- package/README.md +43 -12
- package/package.json +6 -4
- 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
|
@@ -2,6 +2,7 @@ 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 listItemCSS from "./list-item.css.js";
|
|
5
|
+
import { color } from "../../shared/token-scales.js";
|
|
5
6
|
import "../../icons/check-icon.js";
|
|
6
7
|
|
|
7
8
|
// the size steps list-item.css.js actually implements
|
|
@@ -10,6 +11,11 @@ const SIZE_STEPS = ['sm', 'md', 'lg'];
|
|
|
10
11
|
/** The markers this item can draw. */
|
|
11
12
|
const MARKERS = ['bullet', 'number', 'check', 'none'];
|
|
12
13
|
|
|
14
|
+
/* Whether a sibling is a numbered item, read so it also answers for one
|
|
15
|
+
that has not upgraded yet: its variant is still only an attribute. */
|
|
16
|
+
const isNumbered = el => el.localName === 'bd-li'
|
|
17
|
+
&& (el.variant || el.getAttribute('variant') || el.getAttribute('kind')) === 'number';
|
|
18
|
+
|
|
13
19
|
export class BdListItem extends LitElement {
|
|
14
20
|
static properties = {
|
|
15
21
|
variant: { type: String, reflect: true },
|
|
@@ -17,7 +23,7 @@ export class BdListItem extends LitElement {
|
|
|
17
23
|
kind : { type: String },
|
|
18
24
|
size : { type: String }, // sm, md, lg
|
|
19
25
|
icon : { type: String }, // custom icon URL
|
|
20
|
-
color : { type: String } //
|
|
26
|
+
color : { type: String } // a text role -- see token-scales.js COLOR_ROLES
|
|
21
27
|
};
|
|
22
28
|
|
|
23
29
|
constructor() {
|
|
@@ -36,10 +42,52 @@ export class BdListItem extends LitElement {
|
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
/* WHY THE NUMBER IS WORKED OUT HERE.
|
|
46
|
+
The number used to be a CSS counter, incremented on the <li> inside this
|
|
47
|
+
element's shadow root. Every shadow root starts its own counter, so each
|
|
48
|
+
item counted only itself and every numbered list read 1. 1. 1. Now each
|
|
49
|
+
item counts the numbered bd-li before it among its siblings, starting at
|
|
50
|
+
the enclosing bd-list's start.
|
|
51
|
+
|
|
52
|
+
The count depends on the siblings, so an item joining, leaving or
|
|
53
|
+
changing variant asks the others to recount, and bd-list asks its items
|
|
54
|
+
when start changes. */
|
|
55
|
+
get _number() {
|
|
56
|
+
const parent = this.parentElement;
|
|
57
|
+
if (!parent) return 1;
|
|
58
|
+
const start = parent.localName === 'bd-list' && Number.isFinite(parent.start) ? parent.start : 1;
|
|
59
|
+
return start + [...parent.children].filter(isNumbered).indexOf(this);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_renumberPeers(parent = this.parentElement) {
|
|
63
|
+
if (!parent) return;
|
|
64
|
+
for (const el of parent.children) if (el !== this && el.localName === 'bd-li') el.requestUpdate?.();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
connectedCallback() {
|
|
68
|
+
super.connectedCallback();
|
|
69
|
+
this._parent = this.parentElement;
|
|
70
|
+
this._renumberPeers();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
disconnectedCallback() {
|
|
74
|
+
super.disconnectedCallback();
|
|
75
|
+
this._renumberPeers(this._parent);
|
|
76
|
+
this._parent = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
updated(changed) {
|
|
80
|
+
if (changed.has("variant")) this._renumberPeers();
|
|
81
|
+
}
|
|
82
|
+
|
|
39
83
|
render() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
84
|
+
/* color is a text role, resolved the way bd-h resolves it. It used to be
|
|
85
|
+
written into the style attribute as given, so any string at all -- a
|
|
86
|
+
hex, a raw palette var(), a value with a semicolon in it -- reached the
|
|
87
|
+
inline style unchecked, past every token rule. A name that is not a
|
|
88
|
+
role writes nothing, and the item keeps its own colours. */
|
|
89
|
+
const ink = color(this.color);
|
|
90
|
+
const style = ink ? `color: ${ink}; --bd-li-marker-color: ${ink};` : "";
|
|
43
91
|
|
|
44
92
|
return html`
|
|
45
93
|
<li class="bd-list-item ${this.variant} ${SIZE_STEPS.includes(this.size) ? this.size : 'md'}" style="${style}">
|
|
@@ -58,7 +106,7 @@ export class BdListItem extends LitElement {
|
|
|
58
106
|
return html`<img src="${this.icon}" alt="" class="bd-list-item-custom-icon" />`;
|
|
59
107
|
}
|
|
60
108
|
|
|
61
|
-
return html`<span class="bd-list-item-marker">${
|
|
109
|
+
return html`<span class="bd-list-item-marker" data-number="${this.variant === "number" ? `${this._number}.` : nothing}">${
|
|
62
110
|
this.variant === "check"
|
|
63
111
|
? html`<bd-check-icon size="xs" aria-hidden="true"></bd-check-icon>`
|
|
64
112
|
: nothing
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { html } from "lit";
|
|
2
2
|
import "./list-item.js";
|
|
3
|
+
import "../list.js";
|
|
3
4
|
|
|
4
5
|
export default {
|
|
5
6
|
title : "Atoms/List item",
|
|
@@ -88,6 +89,27 @@ export const Number = {
|
|
|
88
89
|
`
|
|
89
90
|
};
|
|
90
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Each item counts the numbered bd-li before it, starting at the enclosing
|
|
94
|
+
* bd-list's start. The number was a CSS counter once, and a counter cannot
|
|
95
|
+
* count across shadow roots, so every item read "1." -- a single-item story
|
|
96
|
+
* could not show that, which is why this one has several.
|
|
97
|
+
*/
|
|
98
|
+
export const NumberedList = {
|
|
99
|
+
name : "Number — a list of them",
|
|
100
|
+
render: () => html`
|
|
101
|
+
<bd-list type="ordered">
|
|
102
|
+
<bd-li variant="number" size="md">Download the installer</bd-li>
|
|
103
|
+
<bd-li variant="number" size="md">Run it and sign in</bd-li>
|
|
104
|
+
<bd-li variant="number" size="md">Start your first scan</bd-li>
|
|
105
|
+
</bd-list>
|
|
106
|
+
<bd-list type="ordered" start="4">
|
|
107
|
+
<bd-li variant="number" size="md">Continues from four</bd-li>
|
|
108
|
+
<bd-li variant="number" size="md">and five</bd-li>
|
|
109
|
+
</bd-list>
|
|
110
|
+
`
|
|
111
|
+
};
|
|
112
|
+
|
|
91
113
|
// ─── Check ────────────────────────────────────────────────────────────────────
|
|
92
114
|
|
|
93
115
|
export const Check = {
|
|
@@ -3,6 +3,14 @@ import { LitElement, html } from "lit";
|
|
|
3
3
|
import "../../tokens/tokens.js";
|
|
4
4
|
import listCSS from "./list.css.js";
|
|
5
5
|
|
|
6
|
+
/* The values list.css.js styles, each with the default it falls back to. */
|
|
7
|
+
const ALLOWED = {
|
|
8
|
+
type : [['ordered', 'unordered'], 'unordered'],
|
|
9
|
+
spacing : [['sm', 'md', 'lg'], 'md'],
|
|
10
|
+
variant : [['default', 'compact', 'spacious'], 'default'],
|
|
11
|
+
orientation: [['vertical', 'horizontal'], 'vertical']
|
|
12
|
+
};
|
|
13
|
+
|
|
6
14
|
export class BdList extends LitElement {
|
|
7
15
|
static properties = {
|
|
8
16
|
type : { type: String, reflect: true }, // 'ordered', 'unordered'
|
|
@@ -21,6 +29,25 @@ export class BdList extends LitElement {
|
|
|
21
29
|
this.orientation = "vertical";
|
|
22
30
|
}
|
|
23
31
|
|
|
32
|
+
/* Each of these becomes a class and reflects, so an unknown value used to
|
|
33
|
+
land on the element and in the class list as a name nothing styles --
|
|
34
|
+
orientation="row" laid the list out as nothing in particular, and still
|
|
35
|
+
said "row". It now falls back to the default, and the attribute shows
|
|
36
|
+
what is applied. start that is not a number goes back to 1. */
|
|
37
|
+
willUpdate(changed) {
|
|
38
|
+
for (const [prop, [values, fallback]] of Object.entries(ALLOWED)) {
|
|
39
|
+
if (changed.has(prop) && !values.includes(this[prop])) this[prop] = fallback;
|
|
40
|
+
}
|
|
41
|
+
if (changed.has('start') && !Number.isFinite(this.start)) this.start = 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* A numbered bd-li counts from start, so it has to recount when start does. */
|
|
45
|
+
updated(changed) {
|
|
46
|
+
if (changed.has("start")) {
|
|
47
|
+
for (const el of this.children) if (el.localName === "bd-li") el.requestUpdate?.();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
24
51
|
render() {
|
|
25
52
|
const classes = `bd-list ${this.spacing} ${this.variant} ${this.orientation}`;
|
|
26
53
|
|
|
@@ -130,7 +130,12 @@ export default css`
|
|
|
130
130
|
|
|
131
131
|
.bd-modal-content button:focus-visible {
|
|
132
132
|
/* the one focus ring — see Foundations/Focus */
|
|
133
|
-
|
|
133
|
+
/* forced-colors mode does not paint box-shadow, so the ring
|
|
134
|
+
vanishes there; it does paint an outline, in a system colour. This one
|
|
135
|
+
is transparent everywhere else, and the offset lands it on the ring's
|
|
136
|
+
outer band. */
|
|
137
|
+
outline: var(--border-width-2) solid transparent;
|
|
138
|
+
outline-offset: var(--focus-outline-offset);
|
|
134
139
|
box-shadow: var(--focus-outline-primary);
|
|
135
140
|
}
|
|
136
141
|
|
|
@@ -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);
|