@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
|
@@ -20,7 +20,11 @@ class BdRadio extends LitElement {
|
|
|
20
20
|
static styles = [radioCSS];
|
|
21
21
|
|
|
22
22
|
static properties = {
|
|
23
|
-
|
|
23
|
+
/* Reflected because the group is found by it: _group selects same-name
|
|
24
|
+
radios by attribute, so a name set as a property -- radio.name = "plan",
|
|
25
|
+
which is what a framework binding does -- left the radio in no group at
|
|
26
|
+
all, with no exclusivity, no arrow keys and its own tab stop. */
|
|
27
|
+
name : { type: String, reflect: true },
|
|
24
28
|
value : { type: String },
|
|
25
29
|
checked : { type: Boolean, reflect: true },
|
|
26
30
|
disabled: { type: Boolean, reflect: true },
|
|
@@ -43,7 +47,7 @@ class BdRadio extends LitElement {
|
|
|
43
47
|
if (!this.name) return [this];
|
|
44
48
|
const root = this.getRootNode();
|
|
45
49
|
const scope = root instanceof Document || root instanceof ShadowRoot ? root : document;
|
|
46
|
-
return [...scope.querySelectorAll(`bd-radio[name="${this.name}"]`)];
|
|
50
|
+
return [...scope.querySelectorAll(`bd-radio[name="${CSS.escape(this.name)}"]`)];
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
/**
|
|
@@ -57,6 +61,45 @@ class BdRadio extends LitElement {
|
|
|
57
61
|
return group.find(r => !r.disabled) === this;
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
/* The tab stop is a group decision, but each radio renders only when its own
|
|
65
|
+
properties change. So when one radio's checked or disabled flips, or one
|
|
66
|
+
joins or leaves, the rest have to be told to re-render: clicking radio 3
|
|
67
|
+
in a group with nothing selected used to leave radio 1 at tabindex 0
|
|
68
|
+
beside it -- two tab stops in one group. The peers' own checked and
|
|
69
|
+
disabled do not change, so this does not cascade. */
|
|
70
|
+
_refreshPeers(group) {
|
|
71
|
+
for (const radio of group) {
|
|
72
|
+
if (radio !== this) radio.requestUpdate?.();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
connectedCallback() {
|
|
77
|
+
super.connectedCallback();
|
|
78
|
+
this._refreshPeers(this._group);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
disconnectedCallback() {
|
|
82
|
+
super.disconnectedCallback();
|
|
83
|
+
// detached, this radio can no longer find its root, so it asks the one it
|
|
84
|
+
// was connected in
|
|
85
|
+
const root = this._root;
|
|
86
|
+
if (this.name && root) {
|
|
87
|
+
this._refreshPeers(root.querySelectorAll(`bd-radio[name="${CSS.escape(this.name)}"]`));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
updated(changed) {
|
|
92
|
+
this._root = this.isConnected ? this.getRootNode() : this._root;
|
|
93
|
+
if (changed.has("checked") || changed.has("disabled") || changed.has("name")) {
|
|
94
|
+
this._refreshPeers(this._group);
|
|
95
|
+
}
|
|
96
|
+
// a radio renamed out of a group leaves its old peers to re-decide too
|
|
97
|
+
const oldName = changed.get("name");
|
|
98
|
+
if (oldName && this._root) {
|
|
99
|
+
this._refreshPeers(this._root.querySelectorAll(`bd-radio[name="${CSS.escape(oldName)}"]`));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
60
103
|
_select() {
|
|
61
104
|
if (this.disabled || this.checked) return;
|
|
62
105
|
|
|
@@ -153,8 +196,10 @@ class BdToggleRadio extends LitElement {
|
|
|
153
196
|
this.checked = !this.checked;
|
|
154
197
|
|
|
155
198
|
this.dispatchEvent(
|
|
199
|
+
// name was declared and read by nothing; it is carried here, as
|
|
200
|
+
// bd-radio carries it, so a handler can tell which control changed
|
|
156
201
|
new CustomEvent("change", {
|
|
157
|
-
detail : { checked: this.checked, value: this.checked ? this.value : null },
|
|
202
|
+
detail : { checked: this.checked, value: this.checked ? this.value : null, name: this.name },
|
|
158
203
|
bubbles : true,
|
|
159
204
|
composed: true
|
|
160
205
|
})
|
|
@@ -69,7 +69,7 @@ Two deviations from the design, both deliberate:
|
|
|
69
69
|
|
|
70
70
|
### Events
|
|
71
71
|
- \`change\` — \`detail: { name, value }\` on \`bd-radio\`;
|
|
72
|
-
\`detail: { checked, value }\` on \`bd-toggle-radio\`.
|
|
72
|
+
\`detail: { checked, value, name }\` on \`bd-toggle-radio\`.
|
|
73
73
|
`
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -106,12 +106,17 @@ export const formFieldCSS = css`
|
|
|
106
106
|
shows the indicator for a pointer click there too, because typing is
|
|
107
107
|
expected — but the select is a control you click and walk away from, and
|
|
108
108
|
it was drawing the full ring on every mouse press where bd-button drew
|
|
109
|
-
nothing.
|
|
110
|
-
|
|
111
|
-
.form-input-bd:focus-visible
|
|
112
|
-
.form-input-bd.focused {
|
|
109
|
+
nothing. There is no .focused class any more: a field's focused prop now
|
|
110
|
+
moves real focus, so :focus-visible is the only thing that draws this. */
|
|
111
|
+
.form-input-bd:focus-visible {
|
|
113
112
|
border-color: var(--form-input-focus-borderColor);
|
|
114
113
|
box-shadow: var(--focus-outline-primary);
|
|
114
|
+
/* forced-colors mode does not paint box-shadow, so the ring
|
|
115
|
+
vanishes there; it does paint an outline, in a system colour. This one
|
|
116
|
+
is transparent everywhere else, and the offset lands it on the ring's
|
|
117
|
+
outer band. */
|
|
118
|
+
outline: var(--border-width-2) solid transparent;
|
|
119
|
+
outline-offset: var(--focus-outline-offset);
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
/* ---- sizes ------------------------------------------------------------
|
|
@@ -139,6 +144,27 @@ export const formFieldCSS = css`
|
|
|
139
144
|
border-radius: var(--form-input-large-borderRadius);
|
|
140
145
|
}
|
|
141
146
|
|
|
147
|
+
/* The text's own room. --form-input-*-height is padding plus the line box,
|
|
148
|
+
and the box is border-box, so the 1px border on each edge came out of the
|
|
149
|
+
content: the line box stood 2px taller than the space left for it. An
|
|
150
|
+
input centres its line and paints into the padding, so it never showed;
|
|
151
|
+
a select does neither -- it clips at the content box -- and at md the
|
|
152
|
+
descenders of "Italy gjpqy ș ț" lost their bottoms. Taking the border out
|
|
153
|
+
of the block padding instead gives the line all of its box and leaves
|
|
154
|
+
every height where it was, on its button. The textarea is left out: its
|
|
155
|
+
height comes from its rows, so this would only make it 2px shorter. */
|
|
156
|
+
.input-sm:not(textarea) {
|
|
157
|
+
padding-block: calc(var(--spacing-8) - var(--form-input-default-borderWidth));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.input-md:not(textarea) {
|
|
161
|
+
padding-block: calc(var(--spacing-12) - var(--form-input-default-borderWidth));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.input-lg:not(textarea) {
|
|
165
|
+
padding-block: calc(var(--spacing-16) - var(--form-input-default-borderWidth));
|
|
166
|
+
}
|
|
167
|
+
|
|
142
168
|
::placeholder {
|
|
143
169
|
color: var(--color-text-subtle);
|
|
144
170
|
opacity: 1;
|
|
@@ -141,6 +141,22 @@ export const fontFamily = onScale(FONT_FAMILY_STEPS, '--typography-fontFamily-')
|
|
|
141
141
|
export const letterSpacing = onScale(LETTER_SPACING_STEPS, '--typography-letterSpacing-');
|
|
142
142
|
export const radius = onScale(RADIUS_STEPS, '--radius-');
|
|
143
143
|
|
|
144
|
+
/* The border-width steps. A px spelling of a step -- "2px" -- is read as the
|
|
145
|
+
step, because that is what callers wrote before there was a scale here. */
|
|
146
|
+
export const BORDER_WIDTH_STEPS = ['0', '1', '2', '3', '4', '8'];
|
|
147
|
+
export const borderWidth = value => {
|
|
148
|
+
const step = String(value ?? '').replace(/px$/, '');
|
|
149
|
+
return BORDER_WIDTH_STEPS.includes(step) ? `var(--border-width-${step})` : '';
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/* Not a scale: a fraction. An opacity given as a prop is kept only when it is
|
|
153
|
+
a number from 0 to 1, so a stray string never reaches an inline style. */
|
|
154
|
+
export const opacity = value => {
|
|
155
|
+
if (value === '' || value == null) return '';
|
|
156
|
+
const n = Number(value);
|
|
157
|
+
return Number.isFinite(n) && n >= 0 && n <= 1 ? String(n) : '';
|
|
158
|
+
};
|
|
159
|
+
|
|
144
160
|
export const space = name =>
|
|
145
161
|
(Object.hasOwn(SPACE_STEPS, name) ? `var(--spacing-${SPACE_STEPS[name]})` : '');
|
|
146
162
|
|
|
@@ -33,6 +33,19 @@ export const spinnerCSS = css`
|
|
|
33
33
|
--bd-spinner-pulse-fill: color-mix(in srgb, var(--color-neutral-0) 30%, transparent);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/* The status region: text for assistive technology, nothing on screen. */
|
|
37
|
+
.announce {
|
|
38
|
+
position: absolute;
|
|
39
|
+
/* no token: 1px is the clip idiom's own measure, not a border -- the same
|
|
40
|
+
sanctioned literal as visually-hidden.css.js. */
|
|
41
|
+
width: 1px;
|
|
42
|
+
height: 1px;
|
|
43
|
+
margin: -1px;
|
|
44
|
+
overflow: hidden;
|
|
45
|
+
clip-path: inset(50%);
|
|
46
|
+
white-space: nowrap;
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
.spinner {
|
|
37
50
|
width: var(--dimension-24);
|
|
38
51
|
height: var(--dimension-24);
|
|
@@ -47,13 +60,27 @@ export const spinnerCSS = css`
|
|
|
47
60
|
|
|
48
61
|
@keyframes bd-spin { to { transform: rotate(360deg); } }
|
|
49
62
|
|
|
50
|
-
/*
|
|
51
|
-
|
|
63
|
+
/* A spinner that cannot spin should still say something, so it pulses
|
|
64
|
+
rather than freezing solid -- but only the disc inside it. This used to
|
|
65
|
+
repaint the arc in the track colour and pulse the whole ring, so what was
|
|
66
|
+
left was the track alone: 1.66:1 against the page, 1.21:1 at the pulse's
|
|
67
|
+
low point, under the 3:1 WCAG 1.4.11 asks of a graphic that conveys
|
|
68
|
+
state. The arc now stays, still and at full strength (4.53:1 on the page,
|
|
69
|
+
3.64:1 on the disc), and the disc behind it carries the motion. Pulsing
|
|
70
|
+
the whole spinner instead would take the arc to 1.79:1 at the bottom of
|
|
71
|
+
each pulse. */
|
|
52
72
|
@media (prefers-reduced-motion: reduce) {
|
|
53
73
|
.spinner {
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
position: relative;
|
|
75
|
+
animation: none;
|
|
76
|
+
}
|
|
77
|
+
.spinner::before {
|
|
78
|
+
content: "";
|
|
79
|
+
position: absolute;
|
|
80
|
+
inset: var(--spacing-0);
|
|
81
|
+
border-radius: var(--radius-full);
|
|
56
82
|
background: var(--bd-spinner-pulse-fill);
|
|
83
|
+
animation: bd-pulse var(--transition-duration-slower) var(--transition-easing-smooth) infinite alternate;
|
|
57
84
|
}
|
|
58
85
|
@keyframes bd-pulse { to { opacity: var(--opacity-pulse); } }
|
|
59
86
|
}
|
|
@@ -8,12 +8,13 @@ export class BdSpinner extends LitElement {
|
|
|
8
8
|
static styles = [spinnerCSS];
|
|
9
9
|
|
|
10
10
|
static properties = {
|
|
11
|
-
size
|
|
12
|
-
label
|
|
11
|
+
size : { type: String, reflect: true },
|
|
12
|
+
label : { type: String },
|
|
13
13
|
/* The surface the spinner sits on. Its default arc and track were both
|
|
14
14
|
chosen against a white page, so on a filled panel it all but disappears.
|
|
15
15
|
Same attribute, same meaning, as on bd-button. */
|
|
16
|
-
surface: { type: String, reflect: true }
|
|
16
|
+
surface : { type: String, reflect: true },
|
|
17
|
+
_announce: { type: Boolean, state: true }
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
constructor() {
|
|
@@ -21,6 +22,26 @@ export class BdSpinner extends LitElement {
|
|
|
21
22
|
this.size = 'md';
|
|
22
23
|
this.label = 'Loading';
|
|
23
24
|
this.surface = 'light';
|
|
25
|
+
this._announce = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* WHY THE TEXT ARRIVES LATE.
|
|
29
|
+
This was an empty role="status" named by aria-label. A live region is
|
|
30
|
+
announced when its CONTENT changes, and a name is not content, so it
|
|
31
|
+
never had anything to say: nothing announced the wait at all.
|
|
32
|
+
|
|
33
|
+
The label is now text inside the region. It is rendered empty first and
|
|
34
|
+
filled a moment later, because the region has to be in the accessibility
|
|
35
|
+
tree before its text arrives -- a region inserted already holding its
|
|
36
|
+
text is, to many screen readers, not a change. A later label change is
|
|
37
|
+
a content change too, so it is announced as well. */
|
|
38
|
+
firstUpdated() {
|
|
39
|
+
this._announceTimer = setTimeout(() => { this._announce = true; }, 100);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
disconnectedCallback() {
|
|
43
|
+
clearTimeout(this._announceTimer);
|
|
44
|
+
super.disconnectedCallback();
|
|
24
45
|
}
|
|
25
46
|
|
|
26
47
|
willUpdate(changed) {
|
|
@@ -30,7 +51,8 @@ export class BdSpinner extends LitElement {
|
|
|
30
51
|
|
|
31
52
|
render() {
|
|
32
53
|
return html`
|
|
33
|
-
<span class="spinner"
|
|
54
|
+
<span class="spinner" aria-hidden="true"></span>
|
|
55
|
+
<span class="announce" role="status">${this._announce ? this.label : ''}</span>
|
|
34
56
|
`;
|
|
35
57
|
}
|
|
36
58
|
}
|
|
@@ -22,16 +22,19 @@ unknown. When you know the proportion done, use \`<bd-progress>\` instead.
|
|
|
22
22
|
### Props
|
|
23
23
|
| Prop | Values | Default |
|
|
24
24
|
|---|---|---|
|
|
25
|
-
| \`size\` | \`
|
|
25
|
+
| \`size\` | \`sm\`, \`md\`, \`lg\` — \`small\` / \`default\` / \`large\` still resolve | \`md\` |
|
|
26
26
|
| \`label\` | the accessible name | \`Loading\` |
|
|
27
27
|
|
|
28
28
|
### Accessibility
|
|
29
|
-
|
|
30
|
-
announced politely rather than interrupting.
|
|
29
|
+
\`label\` is the text of a visually hidden \`role="status"\` region, so the wait
|
|
30
|
+
is announced politely rather than interrupting. The text arrives just after
|
|
31
|
+
the region does, which is what lets a screen reader treat it as news, and a
|
|
32
|
+
later change to \`label\` is announced too. Give it a specific \`label\` where
|
|
31
33
|
you can — "Checking your licence" is more use than the default "Loading".
|
|
32
34
|
|
|
33
|
-
Under \`prefers-reduced-motion\` it stops spinning
|
|
34
|
-
still reads as active
|
|
35
|
+
Under \`prefers-reduced-motion\` it stops spinning: the arc stays, still and
|
|
36
|
+
at full contrast, and the disc behind it pulses, so it still reads as active
|
|
37
|
+
without rotating and never fades below 3:1.
|
|
35
38
|
|
|
36
39
|
### Note on colour
|
|
37
40
|
The spinner's track and head are fixed at \`--color-neutral-200\` and
|
|
@@ -47,9 +50,9 @@ let the two converge.
|
|
|
47
50
|
argTypes: {
|
|
48
51
|
size: {
|
|
49
52
|
control : { type: 'select' },
|
|
50
|
-
options : ['
|
|
53
|
+
options : ['sm', 'md', 'lg'],
|
|
51
54
|
description: 'Diameter of the spinner.',
|
|
52
|
-
table : { type: { summary: 'string' }, defaultValue: { summary:
|
|
55
|
+
table : { type: { summary: 'string' }, defaultValue: { summary: 'md' }, category: 'Appearance' }
|
|
53
56
|
},
|
|
54
57
|
label: {
|
|
55
58
|
control : 'text',
|
|
@@ -58,10 +58,22 @@ export const switchCSS = css`
|
|
|
58
58
|
flex: none;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/* The thumb's inset and travel are derived from the two boxes they sit
|
|
62
|
+
between, in px like those boxes. They were --spacing-2 and --spacing-16,
|
|
63
|
+
which are rem, inside a track that is --dimension-* and so is not: at a
|
|
64
|
+
24px root the thumb rode 3px in and travelled 24px, and finished at 47px
|
|
65
|
+
in a 40px track. A knob has to stay on the track it belongs to at any text
|
|
66
|
+
size, so all four lengths are now one unit. Inset is half the height left
|
|
67
|
+
over; travel is the width left over after both insets: 2px and 16px. */
|
|
68
|
+
.track {
|
|
69
|
+
--bd-switch-inset: calc((var(--dimension-24) - var(--dimension-20)) / 2);
|
|
70
|
+
--bd-switch-travel: calc(var(--dimension-40) - var(--dimension-20) - 2 * var(--bd-switch-inset));
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
.thumb {
|
|
62
74
|
position: absolute;
|
|
63
|
-
top: var(--
|
|
64
|
-
left: var(--
|
|
75
|
+
top: var(--bd-switch-inset);
|
|
76
|
+
left: var(--bd-switch-inset);
|
|
65
77
|
width: var(--dimension-20);
|
|
66
78
|
height: var(--dimension-20);
|
|
67
79
|
/* the knob on the track, white on both tracks: --color-action-on-fill,
|
|
@@ -81,13 +93,19 @@ export const switchCSS = css`
|
|
|
81
93
|
If a second component ever needs it, that is when it earns a name. */
|
|
82
94
|
label:hover .track { background: var(--color-neutral-500); }
|
|
83
95
|
:host([checked]) label:hover .track { background: var(--color-action-primary-hover); }
|
|
84
|
-
:host([checked]) .thumb { transform: translateX(var(--
|
|
96
|
+
:host([checked]) .thumb { transform: translateX(var(--bd-switch-travel)); }
|
|
85
97
|
|
|
86
98
|
/* the one focus ring — see Foundations/Focus. On the row rather than the
|
|
87
99
|
track, so the target and the indicator are the same shape here as on
|
|
88
100
|
bd-checkbox and bd-radio. */
|
|
89
101
|
label:has(input:focus-visible) {
|
|
90
102
|
box-shadow: var(--focus-outline-primary);
|
|
103
|
+
/* forced-colors mode does not paint box-shadow, so the ring
|
|
104
|
+
vanishes there; it does paint an outline, in a system colour. This one
|
|
105
|
+
is transparent everywhere else, and the offset lands it on the ring's
|
|
106
|
+
outer band. */
|
|
107
|
+
outline: var(--border-width-2) solid transparent;
|
|
108
|
+
outline-offset: var(--focus-outline-offset);
|
|
91
109
|
border-radius: var(--focus-outline-radius);
|
|
92
110
|
}
|
|
93
111
|
|
|
@@ -31,7 +31,14 @@ export class BdSwitch extends LitElement {
|
|
|
31
31
|
?disabled=${this.disabled}
|
|
32
32
|
@change=${e => {
|
|
33
33
|
this.checked = e.target.checked;
|
|
34
|
-
|
|
34
|
+
// bubbles and composed, as every sibling form atom's event is: with
|
|
35
|
+
// neither, it reached a listener on the switch and nothing above
|
|
36
|
+
// it, so a form wrapper delegating bd-change heard nothing
|
|
37
|
+
this.dispatchEvent(new CustomEvent('bd-change', {
|
|
38
|
+
detail : this.checked,
|
|
39
|
+
bubbles : true,
|
|
40
|
+
composed: true
|
|
41
|
+
}));
|
|
35
42
|
}}
|
|
36
43
|
/>
|
|
37
44
|
<span class="track"><span class="thumb"></span></span>
|
|
@@ -33,7 +33,8 @@ Content can also come from the default slot, which renders after \`label\` — s
|
|
|
33
33
|
set one or the other, not both.
|
|
34
34
|
|
|
35
35
|
### Events
|
|
36
|
-
\`bd-change\` — fires on toggle with \`detail\` set to the new boolean.
|
|
36
|
+
\`bd-change\` — fires on toggle with \`detail\` set to the new boolean. It bubbles and
|
|
37
|
+
is composed, so a form wrapper can listen for it on itself.
|
|
37
38
|
|
|
38
39
|
### Accessibility
|
|
39
40
|
It is a real \`<input type="checkbox">\` with \`role="switch"\`, inside a
|
|
@@ -111,7 +111,12 @@ export const tabListCSS = css`
|
|
|
111
111
|
|
|
112
112
|
.bd-tab-button:focus-visible {
|
|
113
113
|
/* the one focus ring — see Foundations/Focus */
|
|
114
|
-
|
|
114
|
+
/* forced-colors mode does not paint box-shadow, so the ring
|
|
115
|
+
vanishes there; it does paint an outline, in a system colour. This one
|
|
116
|
+
is transparent everywhere else, and the offset lands it on the ring's
|
|
117
|
+
outer band. */
|
|
118
|
+
outline: var(--border-width-2) solid transparent;
|
|
119
|
+
outline-offset: var(--focus-outline-offset);
|
|
115
120
|
box-shadow: var(--focus-outline-primary);
|
|
116
121
|
border-radius: var(--focus-outline-radius);
|
|
117
122
|
}
|
|
@@ -32,6 +32,17 @@ class TerminalCursor extends LitElement {
|
|
|
32
32
|
opacity: 0;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
/* A cursor that blinks for as long as the page is open is exactly the
|
|
37
|
+
motion the setting exists for, and this one had no guard: every
|
|
38
|
+
other animated element in the package stops or softens under
|
|
39
|
+
prefers-reduced-motion. Here it stops, and the cursor stays shown --
|
|
40
|
+
a steady block still says "this is where the terminal is". */
|
|
41
|
+
@media (prefers-reduced-motion: reduce) {
|
|
42
|
+
.bd-terminal-cursor {
|
|
43
|
+
animation: none;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
35
46
|
`
|
|
36
47
|
];
|
|
37
48
|
|
|
@@ -160,7 +160,7 @@ class TerminalMessage extends LitElement {
|
|
|
160
160
|
${this.items && this.items.length
|
|
161
161
|
? html`
|
|
162
162
|
<bd-list class="bd-terminalmsg-list">
|
|
163
|
-
${this.items.map((item) => html`<bd-li variant="bullet" size="sm" color="
|
|
163
|
+
${this.items.map((item) => html`<bd-li variant="bullet" size="sm" color="inverse">${item}</bd-li>`)}
|
|
164
164
|
</bd-list>
|
|
165
165
|
`
|
|
166
166
|
: nothing}
|
|
@@ -12,7 +12,12 @@ export default css`
|
|
|
12
12
|
|
|
13
13
|
.bd-terms-text-a:focus-visible{
|
|
14
14
|
/* the one focus ring — see Foundations/Focus */
|
|
15
|
-
|
|
15
|
+
/* forced-colors mode does not paint box-shadow, so the ring
|
|
16
|
+
vanishes there; it does paint an outline, in a system colour. This one
|
|
17
|
+
is transparent everywhere else, and the offset lands it on the ring's
|
|
18
|
+
outer band. */
|
|
19
|
+
outline: var(--border-width-2) solid transparent;
|
|
20
|
+
outline-offset: var(--focus-outline-offset);
|
|
16
21
|
box-shadow: var(--focus-outline-primary);
|
|
17
22
|
border-radius: var(--focus-outline-radius);
|
|
18
23
|
}
|
|
@@ -119,6 +119,11 @@ The vertical resize handle is left enabled, so a user can grow the field beyond
|
|
|
119
119
|
control : 'boolean',
|
|
120
120
|
description: 'Blocks editing.',
|
|
121
121
|
table : { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'State' }
|
|
122
|
+
},
|
|
123
|
+
name: {
|
|
124
|
+
control : 'text',
|
|
125
|
+
description: 'Carried on the bd-change detail as { value, name }, for your own submit handling. NOT submitted with a form — the <textarea> is in a shadow root.',
|
|
126
|
+
table : { type: { summary: 'string' }, defaultValue: { summary: "''" }, category: 'Content' }
|
|
122
127
|
}
|
|
123
128
|
}
|
|
124
129
|
};
|
|
@@ -127,6 +132,24 @@ export const Default = () => html`
|
|
|
127
132
|
<bd-textarea label="What happened?" placeholder="Tell us what happened"></bd-textarea>
|
|
128
133
|
`;
|
|
129
134
|
|
|
135
|
+
/**
|
|
136
|
+
* `name` reaches no form -- the <textarea> is in this element's shadow root --
|
|
137
|
+
* so it is carried on the event instead. Type, then click away: the line
|
|
138
|
+
* below shows the bd-change detail as it arrives.
|
|
139
|
+
*/
|
|
140
|
+
export const NameOnTheEvent = {
|
|
141
|
+
name : 'name on the event',
|
|
142
|
+
render: () => html`
|
|
143
|
+
<bd-textarea
|
|
144
|
+
label="What happened?"
|
|
145
|
+
name="incident"
|
|
146
|
+
rows="2"
|
|
147
|
+
@bd-change=${e => { e.currentTarget.nextElementSibling.textContent = `bd-change detail: ${JSON.stringify(e.detail)}`; }}
|
|
148
|
+
></bd-textarea>
|
|
149
|
+
<p style="font-family: var(--typography-fontFamily-mono); font-size: var(--typography-fontSize-sm)">bd-change detail: (type, then click away)</p>
|
|
150
|
+
`
|
|
151
|
+
};
|
|
152
|
+
|
|
130
153
|
export const Sizes = {
|
|
131
154
|
name : 'All Sizes',
|
|
132
155
|
render: () => html`
|
|
@@ -64,8 +64,31 @@ export const tooltipCSS = css`
|
|
|
64
64
|
transition: opacity var(--transition-duration-fast) var(--transition-easing-smooth);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/* Shown means reachable. WCAG 1.4.13 asks that the pointer can move onto
|
|
68
|
+
content that appeared on hover without it disappearing. With
|
|
69
|
+
pointer-events: none left on, the pointer passed through the bubble to
|
|
70
|
+
whatever lay beneath it, the host lost :hover, and the bubble hid. The
|
|
71
|
+
bubble is in this shadow tree, so hovering it keeps :host(:hover). */
|
|
67
72
|
:host(:hover) .bubble,
|
|
68
|
-
:host(:focus-within) .bubble { opacity: 1; }
|
|
73
|
+
:host(:focus-within) .bubble { opacity: 1; pointer-events: auto; }
|
|
74
|
+
|
|
75
|
+
/* Escape hides it while the pointer or focus is still there; see the note
|
|
76
|
+
on _showing in tooltip.js. Same specificity as the rule above and later,
|
|
77
|
+
so it wins, and it takes pointer events away with the opacity. */
|
|
78
|
+
:host(:hover) .bubble--dismissed,
|
|
79
|
+
:host(:focus-within) .bubble--dismissed { opacity: 0; pointer-events: none; }
|
|
80
|
+
|
|
81
|
+
/* The bubble floats --spacing-8 above the trigger, and the pointer has to
|
|
82
|
+
cross that gap to reach it: this fills it, so the host stays hovered
|
|
83
|
+
across the way. It inherits pointer-events from the bubble, so it catches
|
|
84
|
+
nothing while the bubble is hidden. */
|
|
85
|
+
.bubble::after {
|
|
86
|
+
content: "";
|
|
87
|
+
position: absolute;
|
|
88
|
+
inset-inline: 0;
|
|
89
|
+
top: 100%;
|
|
90
|
+
bottom: calc(-1 * var(--spacing-8));
|
|
91
|
+
}
|
|
69
92
|
|
|
70
93
|
@media (prefers-reduced-motion: reduce) {
|
|
71
94
|
.bubble { transition: none; }
|