@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { html } from 'lit';
|
|
2
2
|
import './paragraph.js';
|
|
3
|
+
import '../heading/heading.js';
|
|
3
4
|
|
|
4
5
|
export default {
|
|
5
6
|
title : 'Atoms/Paragraph',
|
|
@@ -51,9 +52,15 @@ layout around them, not to the paragraph itself.
|
|
|
51
52
|
argTypes: {
|
|
52
53
|
variant: {
|
|
53
54
|
control : { type: 'select' },
|
|
54
|
-
options : ['large', 'regular', 'small'],
|
|
55
|
-
description: 'Typography
|
|
56
|
-
table : { type: { summary: "'large'|'regular'|'small'" }, defaultValue: { summary: 'regular' }, category: 'Appearance' }
|
|
55
|
+
options : ['lead', 'large', 'regular', 'small', 'xsmall', 'eyebrow'],
|
|
56
|
+
description: 'Typography role, reflected as an attribute for CSS targeting. `eyebrow` is the short brand-ink label above a section title.',
|
|
57
|
+
table : { type: { summary: "'lead'|'large'|'regular'|'small'|'xsmall'|'eyebrow'" }, defaultValue: { summary: 'regular' }, category: 'Appearance' }
|
|
58
|
+
},
|
|
59
|
+
color: {
|
|
60
|
+
control : { type: 'select' },
|
|
61
|
+
options : ['', 'primary', 'strong', 'muted', 'subtle', 'inverse', 'success', 'warning', 'error'],
|
|
62
|
+
description: 'A semantic colour role, the same set `bd-h` and `bd-display` take. `primary` is brand blue; `inverse` is white on a dark or brand band.',
|
|
63
|
+
table : { type: { summary: 'string' }, defaultValue: { summary: "''" }, category: 'Appearance' }
|
|
57
64
|
},
|
|
58
65
|
padding: {
|
|
59
66
|
control : 'text',
|
|
@@ -145,9 +152,36 @@ export const MultiParagraph = {
|
|
|
145
152
|
parameters: { docs: { description: { story: 'Three paragraphs of different kinds used together as a typical content block.' } } }
|
|
146
153
|
};
|
|
147
154
|
|
|
155
|
+
export const Eyebrow = {
|
|
156
|
+
name : 'Eyebrow',
|
|
157
|
+
render: () => html`
|
|
158
|
+
<div style="display:flex; flex-direction:column; gap:var(--spacing-16); max-width:588px;">
|
|
159
|
+
<bd-p variant="eyebrow">15+ years of AI-driven security innovation</bd-p>
|
|
160
|
+
<bd-h as="h2" visual-level="h2">AI Across Bitdefender</bd-h>
|
|
161
|
+
</div>
|
|
162
|
+
<div style="margin-top:var(--spacing-32); background:var(--color-surface-brand); padding:var(--spacing-24); border-radius:var(--radius-md); max-width:588px;">
|
|
163
|
+
<bd-p variant="eyebrow" color="inverse">On a brand band, color="inverse"</bd-p>
|
|
164
|
+
</div>
|
|
165
|
+
`,
|
|
166
|
+
parameters: { docs: { description: { story: 'The label above a section title, in text-brand: the label-large role (16px Medium 500 at 1.5) from lg up, and label-default (14px, same weight and leading) below lg, as the homepage draws it on tablet and mobile. It is how all five eyebrows in the package are spelled now (bento grid, header code, trust panel twice, agent endpoints), and what the homepage draws. `color` overrides the ink.' } } }
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const Color = {
|
|
170
|
+
name : 'Color',
|
|
171
|
+
render: () => html`
|
|
172
|
+
<div style="display:flex; flex-direction:column; gap:var(--spacing-12); max-width:588px;">
|
|
173
|
+
${['strong', 'primary', 'muted', 'subtle', 'success', 'warning', 'error'].map(c => html`<bd-p color="${c}">color="${c}" — ${sampleText}</bd-p>`)}
|
|
174
|
+
</div>
|
|
175
|
+
<div style="margin-top:var(--spacing-24); background:var(--color-surface-brand); padding:var(--spacing-24); border-radius:var(--radius-md); max-width:588px;">
|
|
176
|
+
<bd-p variant="large" color="inverse">color="inverse" — Bitdefender protects consumers, small businesses, and enterprises with cybersecurity powered by world-class threat intelligence.</bd-p>
|
|
177
|
+
</div>
|
|
178
|
+
`,
|
|
179
|
+
parameters: { docs: { description: { story: 'A semantic role, resolved to a token. Without it a paragraph\'s ink could only come from the composing component\'s CSS.' } } }
|
|
180
|
+
};
|
|
181
|
+
|
|
148
182
|
export const Playground = {
|
|
149
183
|
name : '🛝 Playground',
|
|
150
|
-
args : { kind: 'regular' },
|
|
151
|
-
render : (args) => html`<bd-p variant="${args.kind}">${sampleText}</bd-p>`,
|
|
152
|
-
parameters: { docs: { description: { story: 'Interactive playground. Switch
|
|
184
|
+
args : { kind: 'regular', color: '' },
|
|
185
|
+
render : (args) => html`<bd-p variant="${args.kind}" color="${args.color}">${sampleText}</bd-p>`,
|
|
186
|
+
parameters: { docs: { description: { story: 'Interactive playground. Switch the variant and the colour role via Controls.' } } }
|
|
153
187
|
};
|
|
@@ -68,53 +68,11 @@ export default css`
|
|
|
68
68
|
border-radius: var(--radius-full);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/*
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
.bd-picture.responsive img {
|
|
77
|
-
width: 100%;
|
|
78
|
-
height: auto;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/* Aspect Ratio Containers */
|
|
82
|
-
.bd-picture.aspect-16-9 {
|
|
83
|
-
aspect-ratio: 16 / 9;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.bd-picture.aspect-16-9 img {
|
|
87
|
-
width: 100%;
|
|
88
|
-
height: 100%;
|
|
89
|
-
object-fit: cover;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.bd-picture.aspect-4-3 {
|
|
93
|
-
aspect-ratio: 4 / 3;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
.bd-picture.aspect-4-3 img {
|
|
97
|
-
width: 100%;
|
|
98
|
-
height: 100%;
|
|
99
|
-
object-fit: cover;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.bd-picture.aspect-1-1 {
|
|
103
|
-
aspect-ratio: 1 / 1;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.bd-picture.aspect-1-1 img {
|
|
107
|
-
width: 100%;
|
|
108
|
-
height: 100%;
|
|
109
|
-
object-fit: cover;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.bd-picture:focus-within {
|
|
113
|
-
/* the one focus ring — see Foundations/Focus */
|
|
114
|
-
outline: none;
|
|
115
|
-
box-shadow: var(--focus-outline-primary);
|
|
116
|
-
border-radius: var(--focus-outline-radius);
|
|
117
|
-
}
|
|
71
|
+
/* Gone from here, because nothing could reach them: responsive,
|
|
72
|
+
aspect-16-9 / 4-3 / 1-1 and the hover-zoom and hover-lift effects had
|
|
73
|
+
no prop that sets them -- fit is written as fit-*, so
|
|
74
|
+
not even a misspelt fit could produce one. :focus-within ringed a
|
|
75
|
+
<picture> holding nothing focusable. */
|
|
118
76
|
|
|
119
77
|
/* Loading state.
|
|
120
78
|
|
|
@@ -169,8 +127,9 @@ export default css`
|
|
|
169
127
|
color: var(--color-text-muted);
|
|
170
128
|
}
|
|
171
129
|
|
|
172
|
-
/*
|
|
173
|
-
|
|
130
|
+
/* A local property rather than a string in the stylesheet, so it can be
|
|
131
|
+
translated. It keeps the --bd-img- name it always had, which is the one
|
|
132
|
+
pages already set. */
|
|
174
133
|
.bd-picture:not([loaded])::before {
|
|
175
134
|
content: var(--bd-img-loading-label, "Loading image");
|
|
176
135
|
/* no role: the pre-load placeholder -- a glyph/marker/placeholder box, not running
|
|
@@ -199,16 +158,6 @@ export default css`
|
|
|
199
158
|
transform: scale(1);
|
|
200
159
|
}
|
|
201
160
|
|
|
202
|
-
/* Hover effects */
|
|
203
|
-
.bd-picture.hover-zoom:hover img {
|
|
204
|
-
transform: scale(1.05);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
.bd-picture.hover-lift:hover {
|
|
208
|
-
transform: translateY(-2px);
|
|
209
|
-
transition: transform var(--transition-duration-slow) var(--transition-easing-smooth);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
161
|
/* Performance optimizations */
|
|
213
162
|
.bd-picture img {
|
|
214
163
|
will-change: transform, opacity;
|
|
@@ -248,14 +197,6 @@ export default css`
|
|
|
248
197
|
.bd-picture img {
|
|
249
198
|
transition: none;
|
|
250
199
|
}
|
|
251
|
-
|
|
252
|
-
.bd-picture.hover-zoom:hover img {
|
|
253
|
-
transform: none;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
.bd-picture.hover-lift:hover {
|
|
257
|
-
transform: none;
|
|
258
|
-
}
|
|
259
200
|
}
|
|
260
201
|
|
|
261
202
|
/* No @media (prefers-color-scheme: dark) block. This was the one atom that
|
|
@@ -5,6 +5,7 @@ import pictureCSS from "./picture.css.js";
|
|
|
5
5
|
|
|
6
6
|
// the radius steps picture.css.js implements — the full --radius-* scale
|
|
7
7
|
const RADIUS_STEPS = ['none', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', 'full'];
|
|
8
|
+
const FITS = ['cover', 'contain', 'fill', 'none', 'scale-down'];
|
|
8
9
|
|
|
9
10
|
export class BdPicture extends LitElement {
|
|
10
11
|
static properties = {
|
|
@@ -50,20 +51,26 @@ export class BdPicture extends LitElement {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
render() {
|
|
54
|
+
// fit becomes a class, so only the values picture.css.js implements get
|
|
55
|
+
// through; an unknown one used to leave the image with no object-fit
|
|
53
56
|
const classes = [
|
|
54
57
|
'bd-picture',
|
|
55
|
-
`fit-${this.fit}`,
|
|
58
|
+
`fit-${FITS.includes(this.fit) ? this.fit : 'cover'}`,
|
|
56
59
|
`radius-${RADIUS_STEPS.includes(this.radius) ? this.radius : 'none'}`
|
|
57
60
|
].filter(Boolean).join(' ');
|
|
58
61
|
|
|
62
|
+
// Each source's optional attributes are left off when not given. type
|
|
63
|
+
// used to be written as-is, so a source without one rendered
|
|
64
|
+
// type="undefined" -- a MIME type no browser supports, so it skipped the
|
|
65
|
+
// source and fell through to the next one or the <img>.
|
|
59
66
|
return html`
|
|
60
67
|
<picture class="${classes}">
|
|
61
68
|
${this.sources.map(source => html`
|
|
62
|
-
<source
|
|
63
|
-
type="${source.type}"
|
|
69
|
+
<source
|
|
70
|
+
type="${source.type || nothing}"
|
|
64
71
|
srcset="${source.srcset}"
|
|
65
|
-
media="${source.media ||
|
|
66
|
-
sizes="${source.sizes ||
|
|
72
|
+
media="${source.media || nothing}"
|
|
73
|
+
sizes="${source.sizes || nothing}"
|
|
67
74
|
>
|
|
68
75
|
`)}
|
|
69
76
|
<img
|
|
@@ -102,9 +109,9 @@ export class BdPicture extends LitElement {
|
|
|
102
109
|
and a default-configured bd-picture then renders a fully loaded image at
|
|
103
110
|
opacity 0. The two defects had to be fixed together or neither is visible.
|
|
104
111
|
|
|
105
|
-
bd-img
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
bd-img never hides its image at all, which is why it has no equivalent
|
|
113
|
+
bug -- and no placeholder either, since an <img> cannot draw one. The
|
|
114
|
+
minimal
|
|
108
115
|
change here is to make this component tell the truth it already asks CSS
|
|
109
116
|
about, rather than to restyle it: the fade on load is a deliberate
|
|
110
117
|
nicety, and it is kept.
|
|
@@ -20,12 +20,22 @@ export default css`
|
|
|
20
20
|
|
|
21
21
|
button:focus-visible {
|
|
22
22
|
/* the one focus ring — see Design Tokens/Focus */
|
|
23
|
-
|
|
23
|
+
/* forced-colors mode does not paint box-shadow, so the ring
|
|
24
|
+
vanishes there; it does paint an outline, in a system colour. This one
|
|
25
|
+
is transparent everywhere else, and the offset lands it on the ring's
|
|
26
|
+
outer band. */
|
|
27
|
+
outline: var(--border-width-2) solid transparent;
|
|
28
|
+
outline-offset: var(--focus-outline-offset);
|
|
24
29
|
box-shadow: var(--focus-outline-primary);
|
|
25
30
|
}
|
|
26
31
|
a:focus-visible {
|
|
27
32
|
/* the one focus ring — see Design Tokens/Focus */
|
|
28
|
-
|
|
33
|
+
/* forced-colors mode does not paint box-shadow, so the ring
|
|
34
|
+
vanishes there; it does paint an outline, in a system colour. This one
|
|
35
|
+
is transparent everywhere else, and the offset lands it on the ring's
|
|
36
|
+
outer band. */
|
|
37
|
+
outline: var(--border-width-2) solid transparent;
|
|
38
|
+
outline-offset: var(--focus-outline-offset);
|
|
29
39
|
box-shadow: var(--focus-outline-primary);
|
|
30
40
|
border-radius: var(--focus-outline-radius);
|
|
31
41
|
}
|
|
@@ -204,7 +204,7 @@ class PricingCard extends LitElement {
|
|
|
204
204
|
</bd-badge>
|
|
205
205
|
|
|
206
206
|
<bd-p variant="small" class="badge-subtitle">1 account & 5 devices</bd-p>
|
|
207
|
-
<bd-divider-horizontal color="subtle"></bd-divider-horizontal>
|
|
207
|
+
<bd-divider-horizontal color="subtle" opacity="0.25"></bd-divider-horizontal>
|
|
208
208
|
</div>
|
|
209
209
|
|
|
210
210
|
<!-- Pricing section with store integration -->
|
|
@@ -249,7 +249,7 @@ class PricingCard extends LitElement {
|
|
|
249
249
|
</bd-button>
|
|
250
250
|
</div>
|
|
251
251
|
|
|
252
|
-
<bd-divider-horizontal color="subtle"></bd-divider-horizontal>
|
|
252
|
+
<bd-divider-horizontal color="subtle" opacity="0.25"></bd-divider-horizontal>
|
|
253
253
|
|
|
254
254
|
<!-- Features sections -->
|
|
255
255
|
${this.features ? html`<pricing-card-features .shadow=${this.shadow}></pricing-card-features>` : ""}
|
|
@@ -316,7 +316,7 @@ class PricingCardFamily extends LitElement {
|
|
|
316
316
|
</bd-badge>
|
|
317
317
|
|
|
318
318
|
<bd-p variant="small" class="badge-subtitle">5 accounts & 25 devices</bd-p>
|
|
319
|
-
<bd-divider-horizontal color="subtle"></bd-divider-horizontal>
|
|
319
|
+
<bd-divider-horizontal color="subtle" opacity="0.25"></bd-divider-horizontal>
|
|
320
320
|
</div>
|
|
321
321
|
|
|
322
322
|
<!-- Pricing section with store integration -->
|
|
@@ -361,7 +361,7 @@ class PricingCardFamily extends LitElement {
|
|
|
361
361
|
</bd-button>
|
|
362
362
|
</div>
|
|
363
363
|
|
|
364
|
-
<bd-divider-horizontal color="subtle"></bd-divider-horizontal>
|
|
364
|
+
<bd-divider-horizontal color="subtle" opacity="0.25"></bd-divider-horizontal>
|
|
365
365
|
|
|
366
366
|
<!-- Features sections -->
|
|
367
367
|
${this.features
|
|
@@ -3,6 +3,9 @@ import { LitElement, html, nothing } from 'lit';
|
|
|
3
3
|
import '../../tokens/tokens.js';
|
|
4
4
|
import { progressCSS } from './progress.css.js';
|
|
5
5
|
|
|
6
|
+
/* The tones progress.css.js styles. No tone is the default fill. */
|
|
7
|
+
const TONES = ['info', 'success', 'error'];
|
|
8
|
+
|
|
6
9
|
export class BdProgress extends LitElement {
|
|
7
10
|
static styles = [progressCSS];
|
|
8
11
|
|
|
@@ -20,20 +23,37 @@ export class BdProgress extends LitElement {
|
|
|
20
23
|
this.label = '';
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
/* tone reflects, so an unknown one used to sit on the element as if it
|
|
27
|
+
meant something -- tone="warning" or the retired "default" -- while the
|
|
28
|
+
bar drew the default fill. It is cleared instead, so the attribute says
|
|
29
|
+
what is drawn. */
|
|
30
|
+
willUpdate(changed) {
|
|
31
|
+
if (changed.has('tone') && this.tone != null && !TONES.includes(this.tone)) this.tone = undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
render() {
|
|
35
|
+
/* One clamped value for all three readers. Only the fill used to be
|
|
36
|
+
clamped: value 150 read "150%" beside a full bar and reported an
|
|
37
|
+
aria-valuenow above aria-valuemax, a negative value drew nothing and
|
|
38
|
+
read "-20%", and max 0 read "NaN%". A max that is not a positive number
|
|
39
|
+
falls back to the default, as an unknown variant does elsewhere. */
|
|
40
|
+
const max = Number.isFinite(this.max) && this.max > 0 ? this.max : 100;
|
|
41
|
+
const value = Number.isFinite(this.value) ? Math.min(Math.max(this.value, 0), max) : 0;
|
|
42
|
+
const percent = (value / max) * 100;
|
|
43
|
+
|
|
24
44
|
return html`
|
|
25
45
|
${this.label
|
|
26
|
-
? html`<span class="label"><span>${this.label}</span><span>${Math.round(
|
|
46
|
+
? html`<span class="label"><span>${this.label}</span><span>${Math.round(percent)}%</span></span>`
|
|
27
47
|
: nothing}
|
|
28
48
|
<div
|
|
29
49
|
class="track"
|
|
30
50
|
role="progressbar"
|
|
31
|
-
aria-valuenow=${
|
|
51
|
+
aria-valuenow=${value}
|
|
32
52
|
aria-valuemin="0"
|
|
33
|
-
aria-valuemax=${
|
|
53
|
+
aria-valuemax=${max}
|
|
34
54
|
aria-label=${this.label || 'Progress'}
|
|
35
55
|
>
|
|
36
|
-
<div class="fill" style="width: ${
|
|
56
|
+
<div class="fill" style="width: ${percent}%"></div>
|
|
37
57
|
</div>
|
|
38
58
|
`;
|
|
39
59
|
}
|
|
@@ -24,15 +24,17 @@ quantity something is. For work of unknown length use \`<bd-spinner>\`.
|
|
|
24
24
|
| \`value\` | number, current progress | \`0\` |
|
|
25
25
|
| \`max\` | number, the total | \`100\` |
|
|
26
26
|
| \`label\` | caption shown above the bar, with a percentage | \`''\` |
|
|
27
|
-
| \`tone\` | \`
|
|
27
|
+
| \`tone\` | \`info\`, \`success\`, \`error\` — anything else is cleared | unset: the default fill |
|
|
28
28
|
|
|
29
29
|
### Notes
|
|
30
30
|
When \`label\` is set, a row above the bar shows the label on the left and the
|
|
31
31
|
rounded percentage on the right, so the number is visible and not only
|
|
32
32
|
announced.
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
\`value\` is clamped to 0…\`max\`, and the fill, the percentage and
|
|
35
|
+
\`aria-valuenow\` all read the clamped number, so a value above \`max\` shows a
|
|
36
|
+
full bar, "100%" and a valuenow equal to the max. A \`max\` that is not a
|
|
37
|
+
positive number falls back to \`100\`.
|
|
36
38
|
|
|
37
39
|
### Accessibility
|
|
38
40
|
The track carries \`role="progressbar"\` with \`aria-valuenow\`, \`aria-valuemin\`
|
|
@@ -61,9 +63,9 @@ and \`aria-valuemax\`, so assistive tech reports real progress rather than just
|
|
|
61
63
|
},
|
|
62
64
|
tone: {
|
|
63
65
|
control : { type: 'select' },
|
|
64
|
-
options : ['
|
|
65
|
-
description: 'Colour of the fill.',
|
|
66
|
-
table : { type: { summary: '
|
|
66
|
+
options : ['', 'info', 'success', 'error'],
|
|
67
|
+
description: 'Colour of the fill. Unset is the default fill; a value that is not a tone is cleared.',
|
|
68
|
+
table : { type: { summary: "'info' | 'success' | 'error'" }, defaultValue: { summary: 'unset' }, category: 'Appearance' }
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
};
|
|
@@ -39,7 +39,12 @@ export default css`
|
|
|
39
39
|
/* the one focus ring — see Foundations/Focus. A two-colour ring whose
|
|
40
40
|
white inner band keeps it visible on a coloured or dark surface, which a
|
|
41
41
|
single-colour outline cannot do. */
|
|
42
|
-
|
|
42
|
+
/* forced-colors mode does not paint box-shadow, so the ring
|
|
43
|
+
vanishes there; it does paint an outline, in a system colour. This one
|
|
44
|
+
is transparent everywhere else, and the offset lands it on the ring's
|
|
45
|
+
outer band. */
|
|
46
|
+
outline: var(--border-width-2) solid transparent;
|
|
47
|
+
outline-offset: var(--focus-outline-offset);
|
|
43
48
|
box-shadow: var(--focus-outline-primary);
|
|
44
49
|
border-radius: var(--focus-outline-radius);
|
|
45
50
|
}
|
|
@@ -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
|
}
|