@repobit/dex-system-design 1.0.1 → 2.0.1
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
|
@@ -21,52 +21,92 @@ class BdHighlightSection extends LitElement {
|
|
|
21
21
|
this.as = "h3";
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/* The headline is built from the light DOM, so a change there -- text set
|
|
25
|
+
after upgrade, children a framework adds later, an edit -- has to render
|
|
26
|
+
again. Rendering never touches the light DOM, so this cannot loop. */
|
|
27
|
+
connectedCallback() {
|
|
28
|
+
super.connectedCallback();
|
|
29
|
+
this._observer = new MutationObserver(() => this.requestUpdate());
|
|
30
|
+
this._observer.observe(this, { childList: true, characterData: true, subtree: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
disconnectedCallback() {
|
|
34
|
+
this._observer?.disconnect();
|
|
35
|
+
super.disconnectedCallback();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* The headline with the phrase marked, or null when it cannot be.
|
|
39
|
+
|
|
40
|
+
This used to split this.textContent and render the halves as plain
|
|
41
|
+
strings, so the moment the phrase matched, every element in the heading
|
|
42
|
+
was gone from the page: an <em>, a <strong>, a <br>, a link -- measured,
|
|
43
|
+
a slotted <a href> stopped existing anywhere in the rendered tree. Now
|
|
44
|
+
the light DOM is copied node for node, markup and all, and only the
|
|
45
|
+
text node holding the first occurrence is split around the mark.
|
|
46
|
+
|
|
47
|
+
A copy, not the consumer's own nodes, so the page's DOM is never
|
|
48
|
+
rearranged under it. The cost is that what renders is a clone: markup,
|
|
49
|
+
attributes and links survive, but a listener a script attached to one of
|
|
50
|
+
those elements does not. A phrase that crosses an element boundary --
|
|
51
|
+
"family <em>member</em>" -- is in no single text node, so it is not
|
|
52
|
+
marked and the content renders through the slot as it is. */
|
|
53
|
+
_markedHeadline(phrase) {
|
|
54
|
+
const copy = document.createDocumentFragment();
|
|
55
|
+
for (const node of this.childNodes) copy.append(node.cloneNode(true));
|
|
56
|
+
|
|
57
|
+
const walker = document.createTreeWalker(copy, NodeFilter.SHOW_TEXT);
|
|
58
|
+
for (let text = walker.nextNode(); text; text = walker.nextNode()) {
|
|
59
|
+
const at = text.data.indexOf(phrase);
|
|
60
|
+
if (at === -1) continue;
|
|
61
|
+
const match = text.splitText(at);
|
|
62
|
+
match.splitText(phrase.length);
|
|
63
|
+
const mark = markTemplate().content.cloneNode(true);
|
|
64
|
+
mark.querySelector(".highlight-text").textContent = phrase;
|
|
65
|
+
match.replaceWith(mark);
|
|
66
|
+
return copy;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
24
71
|
render() {
|
|
25
|
-
const fullText = this.textContent || "";
|
|
26
72
|
const highlightText = this.highlightText || "";
|
|
73
|
+
const headline = highlightText ? this._markedHeadline(highlightText) : null;
|
|
27
74
|
|
|
28
75
|
const subtitleTemplate = this.subTitle
|
|
29
76
|
? html`<bd-p variant="small" class="sub-title">${this.subTitle}</bd-p>`
|
|
30
77
|
: null;
|
|
31
78
|
|
|
32
|
-
if (!highlightText || !fullText.includes(highlightText)) {
|
|
33
|
-
return html`
|
|
34
|
-
${subtitleTemplate}
|
|
35
|
-
<bd-h as="${this.as}" class="headline">
|
|
36
|
-
<slot></slot>
|
|
37
|
-
</bd-h>
|
|
38
|
-
`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const [before, after] = fullText.split(highlightText);
|
|
42
|
-
|
|
43
79
|
return html`
|
|
44
80
|
${subtitleTemplate}
|
|
45
|
-
<bd-h as="${this.as}" class="headline">
|
|
46
|
-
${before}
|
|
47
|
-
<span class="highlight"><span class="highlight-anchor"
|
|
48
|
-
><span class="highlight-deco left"
|
|
49
|
-
><svg class="highlight-svg-ellipse-top" viewBox="0 0 8 8">
|
|
50
|
-
<ellipse cx="4" cy="4" rx="4" ry="4" />
|
|
51
|
-
</svg>
|
|
52
|
-
<svg class="highlight-svg-rect" viewBox="0 0 2 42" preserveAspectRatio="none">
|
|
53
|
-
<rect x="0" y="0" width="2" height="42" />
|
|
54
|
-
</svg></span
|
|
55
|
-
></span
|
|
56
|
-
><span class="highlight-text">${highlightText}</span><span class="highlight-anchor"
|
|
57
|
-
><span class="highlight-deco right"
|
|
58
|
-
><svg class="highlight-svg-rect" viewBox="0 0 2 42" preserveAspectRatio="none">
|
|
59
|
-
<rect x="0" y="0" width="2" height="42" />
|
|
60
|
-
</svg>
|
|
61
|
-
<svg class="highlight-svg-ellipse-bottom" viewBox="0 0 8 8">
|
|
62
|
-
<ellipse cx="4" cy="4" rx="4" ry="4" />
|
|
63
|
-
</svg></span
|
|
64
|
-
></span></span>
|
|
65
|
-
${after}
|
|
66
|
-
</bd-h>
|
|
81
|
+
<bd-h as="${this.as}" class="headline">${headline ?? html`<slot></slot>`}</bd-h>
|
|
67
82
|
`;
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
}
|
|
71
86
|
|
|
87
|
+
/* The mark's own markup: the tint, the phrase, and the bar-and-dot at each
|
|
88
|
+
end. Built once, on first use, and cloned for every headline. No
|
|
89
|
+
whitespace between the pieces, because inline whitespace renders as a
|
|
90
|
+
space beside the phrase. */
|
|
91
|
+
let template;
|
|
92
|
+
const markTemplate = () => {
|
|
93
|
+
if (!template) {
|
|
94
|
+
template = document.createElement("template");
|
|
95
|
+
template.innerHTML = [
|
|
96
|
+
'<span class="highlight">',
|
|
97
|
+
'<span class="highlight-anchor"><span class="highlight-deco left">',
|
|
98
|
+
'<svg class="highlight-svg-ellipse-top" viewBox="0 0 8 8"><ellipse cx="4" cy="4" rx="4" ry="4"/></svg>',
|
|
99
|
+
'<svg class="highlight-svg-rect" viewBox="0 0 2 42" preserveAspectRatio="none"><rect x="0" y="0" width="2" height="42"/></svg>',
|
|
100
|
+
'</span></span>',
|
|
101
|
+
'<span class="highlight-text"></span>',
|
|
102
|
+
'<span class="highlight-anchor"><span class="highlight-deco right">',
|
|
103
|
+
'<svg class="highlight-svg-rect" viewBox="0 0 2 42" preserveAspectRatio="none"><rect x="0" y="0" width="2" height="42"/></svg>',
|
|
104
|
+
'<svg class="highlight-svg-ellipse-bottom" viewBox="0 0 8 8"><ellipse cx="4" cy="4" rx="4" ry="4"/></svg>',
|
|
105
|
+
'</span></span>',
|
|
106
|
+
'</span>'
|
|
107
|
+
].join("");
|
|
108
|
+
}
|
|
109
|
+
return template;
|
|
110
|
+
};
|
|
111
|
+
|
|
72
112
|
customElements.define("bd-highlight", BdHighlightSection);
|
|
@@ -22,10 +22,11 @@ export default {
|
|
|
22
22
|
\`\`\`
|
|
23
23
|
|
|
24
24
|
### Behavior
|
|
25
|
-
- The
|
|
26
|
-
-
|
|
27
|
-
- If found,
|
|
28
|
-
- The
|
|
25
|
+
- The headline keeps its markup: \`<em>\`, \`<strong>\`, \`<br>\` and links render as written, and the first occurrence of \`highlight-text\` is marked where it sits; a later occurrence renders as plain text
|
|
26
|
+
- The headline is a copy of the content, so a script listener attached to an element inside it does not carry over; links still work, because the \`href\` does
|
|
27
|
+
- If \`highlight-text\` is not found, or crosses an element boundary (\`family <em>member</em>\`), the content renders through the slot as it is, without decoration
|
|
28
|
+
- The mark is a \`<span class="highlight">\` with a bar and dot drawn at each end
|
|
29
|
+
- Editing the content after it renders re-renders the headline
|
|
29
30
|
|
|
30
31
|
### Notes
|
|
31
32
|
- \`sub-title\` renders a \`bd-p kind="small"\` above the headline, **not** a heading
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { LitElement, css } from "lit";
|
|
2
2
|
import { iconSize } from "../shared/token-scales.js";
|
|
3
3
|
|
|
4
|
+
/* One sheet per distinct size and colour, shared by every icon that asks for
|
|
5
|
+
the same pair: a page of forty check icons adopts one sheet, not forty. */
|
|
6
|
+
const instanceSheets = new Map();
|
|
7
|
+
|
|
8
|
+
const instanceSheet = (size, color) => {
|
|
9
|
+
const key = `${size}|${color}`;
|
|
10
|
+
let sheet = instanceSheets.get(key);
|
|
11
|
+
if (!sheet) {
|
|
12
|
+
sheet = new CSSStyleSheet();
|
|
13
|
+
sheet.replaceSync(
|
|
14
|
+
`:host { --bd-icon-size: ${size}; }` + (color ? ` svg { color: ${color}; }` : "")
|
|
15
|
+
);
|
|
16
|
+
instanceSheets.set(key, sheet);
|
|
17
|
+
}
|
|
18
|
+
return sheet;
|
|
19
|
+
};
|
|
20
|
+
|
|
4
21
|
/**
|
|
5
22
|
* Base for the tintable interface icons.
|
|
6
23
|
*
|
|
@@ -47,8 +64,9 @@ export class BdIconElement extends LitElement {
|
|
|
47
64
|
static styles = css`
|
|
48
65
|
:host {
|
|
49
66
|
display: inline-flex;
|
|
50
|
-
/* --bd-icon-size is component-local and always set
|
|
51
|
-
fallback is only reached if a subclass forgets to
|
|
67
|
+
/* --bd-icon-size is component-local and always set by the instance
|
|
68
|
+
sheet below, so the fallback is only reached if a subclass forgets to
|
|
69
|
+
call super. */
|
|
52
70
|
height: var(--bd-icon-size, var(--icon-md-size));
|
|
53
71
|
}
|
|
54
72
|
|
|
@@ -83,17 +101,42 @@ export class BdIconElement extends LitElement {
|
|
|
83
101
|
this.height = null;
|
|
84
102
|
}
|
|
85
103
|
|
|
104
|
+
/* WHY NOT THE HOST'S STYLE ATTRIBUTE.
|
|
105
|
+
This used to write --bd-icon-size and color into this.style on every
|
|
106
|
+
update. The host's style attribute belongs to the page, and with no color
|
|
107
|
+
prop the update removed the colour outright, so
|
|
108
|
+
<bd-check-icon style="color: green"> rendered in the inherited text
|
|
109
|
+
colour, and so did any icon a script coloured after its first render.
|
|
110
|
+
The inline size meant no stylesheet could set --bd-icon-size either.
|
|
111
|
+
|
|
112
|
+
Both now live in a sheet adopted into this shadow root, and the host's
|
|
113
|
+
style attribute is never read or written:
|
|
114
|
+
|
|
115
|
+
size a :host rule. The page's CSS on the element wins over it, as it
|
|
116
|
+
already did for height -- header.css.js sets one on
|
|
117
|
+
.bd-header-signature -- and now does for --bd-icon-size too.
|
|
118
|
+
colour a rule on the svg, so the color prop still beats the page's CSS
|
|
119
|
+
on the host, as the inline colour did. Unset, nothing is
|
|
120
|
+
written and the artwork's currentColor follows the host,
|
|
121
|
+
wherever its colour came from. */
|
|
86
122
|
willUpdate() {
|
|
87
123
|
// An explicit pixel size wins, for the callers that still pass one.
|
|
88
124
|
const px = this.height ?? this.width;
|
|
89
125
|
const { defaultSize } = this.constructor;
|
|
90
126
|
// a step first, then an off-scale default, then the caller's pixels
|
|
91
127
|
const step = iconSize(this.size) || iconSize(defaultSize) || defaultSize;
|
|
92
|
-
|
|
128
|
+
const size = Number.isFinite(px) ? `${px}px` : step;
|
|
93
129
|
|
|
94
130
|
// No colour of its own by default: the artwork is currentColor, so it
|
|
95
|
-
// follows whatever it sits in.
|
|
96
|
-
|
|
97
|
-
|
|
131
|
+
// follows whatever it sits in. A value CSS would not accept as a colour is
|
|
132
|
+
// dropped, as an inline one was -- and it never reaches the sheet's text.
|
|
133
|
+
const color = this.color && CSS.supports("color", this.color) ? this.color : "";
|
|
134
|
+
|
|
135
|
+
this._baseSheets ??= this.renderRoot.adoptedStyleSheets;
|
|
136
|
+
const sheet = instanceSheet(size, color);
|
|
137
|
+
if (this._instanceSheet !== sheet) {
|
|
138
|
+
this._instanceSheet = sheet;
|
|
139
|
+
this.renderRoot.adoptedStyleSheets = [...this._baseSheets, sheet];
|
|
140
|
+
}
|
|
98
141
|
}
|
|
99
142
|
}
|
|
@@ -81,6 +81,13 @@ per the foundation rule it should be a token reference such as
|
|
|
81
81
|
text it sits beside. A raw hex value here is the same drift the tokens exist to
|
|
82
82
|
prevent.
|
|
83
83
|
|
|
84
|
+
### Styling an icon from the page
|
|
85
|
+
An icon never writes its own \`style\` attribute, so whatever the page puts
|
|
86
|
+
there stays. With no \`color\` prop, \`style="color: …"\` or any stylesheet
|
|
87
|
+
colour on the icon reaches the artwork; the \`color\` prop, when set, beats both.
|
|
88
|
+
A stylesheet can size an icon with \`height\` or \`--bd-icon-size\` on the
|
|
89
|
+
element, and either beats the \`size\` prop.
|
|
90
|
+
|
|
84
91
|
### Known inconsistencies
|
|
85
92
|
- **\`bd-laurel-icon\` keeps an 80px default**, off the scale, which stops at 64.
|
|
86
93
|
Shrinking the awards wreath by a fifth is a design decision rather than a
|
|
@@ -84,112 +84,23 @@ export default css`
|
|
|
84
84
|
box-shadow: var(--shadow-xl);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
/*
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.bd-img.bordered-thick {
|
|
97
|
-
border: var(--border-width-4) solid var(--color-border-disabled);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/* Responsive Behavior */
|
|
101
|
-
.bd-img.responsive {
|
|
102
|
-
width: 100%;
|
|
103
|
-
height: auto;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/* Aspect Ratio Containers */
|
|
107
|
-
.bd-img.aspect-16-9 {
|
|
108
|
-
aspect-ratio: 16 / 9;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.bd-img.aspect-4-3 {
|
|
112
|
-
aspect-ratio: 4 / 3;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.bd-img.aspect-1-1 {
|
|
116
|
-
aspect-ratio: 1 / 1;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/* Hover Effects */
|
|
120
|
-
.bd-img.hover-zoom:hover {
|
|
121
|
-
transform: scale(1.05);
|
|
122
|
-
transition: transform var(--transition-duration-slow) var(--transition-easing-smooth);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
.bd-img.hover-shadow:hover {
|
|
126
|
-
box-shadow: var(--shadow-lg);
|
|
127
|
-
transition: box-shadow var(--transition-duration-slow) var(--transition-easing-smooth);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.bd-img:focus-visible {
|
|
131
|
-
/* the one focus ring — see Foundations/Focus */
|
|
132
|
-
outline: none;
|
|
133
|
-
box-shadow: var(--focus-outline-primary);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/* Loading state */
|
|
137
|
-
.bd-img:not([src]) {
|
|
138
|
-
background-color: var(--color-surface-muted);
|
|
139
|
-
/* no token: the dimension scale stops at 64px */
|
|
140
|
-
min-height: 100px;
|
|
141
|
-
display: flex;
|
|
142
|
-
align-items: center;
|
|
143
|
-
justify-content: center;
|
|
144
|
-
/* --color-text-muted, not --color-text-disabled. Loading is not disabled,
|
|
145
|
-
and the difference is not academic: the disabled text role carries a
|
|
146
|
-
contrast exemption -- WCAG 1.4.3 excuses disabled controls from the
|
|
147
|
-
4.5:1 minimum -- and this label claims that exemption without being
|
|
148
|
-
entitled to it. It is a sentence a reader is meant to read while they
|
|
149
|
-
wait.
|
|
150
|
-
|
|
151
|
-
Measured on this ground: neutral-400 on neutral-100 is 2.51:1, which
|
|
152
|
-
misses even the 3:1 floor for non-text. --color-text-muted is
|
|
153
|
-
neutral-600 at 5.12:1 and is the role this package already uses for
|
|
154
|
-
low-emphasis readable text -- caption and form help text both resolve
|
|
155
|
-
to it.
|
|
156
|
-
|
|
157
|
-
The ground stays --color-surface-muted. Loading is a state, muted is
|
|
158
|
-
the state surface, and bd-skeleton -- the package's other loading
|
|
159
|
-
affordance -- already sits on it. Only the ink was choosing by value.
|
|
160
|
-
See "Choosing a semantic role" in the package README. */
|
|
161
|
-
color: var(--color-text-muted);
|
|
162
|
-
/* no role: alt text shown when the image has no src -- a glyph/marker/placeholder box, not running
|
|
163
|
-
prose. See list-item.css.js's .bd-list-item-marker::before for the
|
|
164
|
-
argument. */
|
|
165
|
-
font-size: var(--typography-fontSize-sm);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/* The placeholder copy is a local property rather than a string in the
|
|
169
|
-
stylesheet: content: "Image loading..." cannot be translated, and this
|
|
170
|
-
atom and bd-picture used two different sentences for the same state. */
|
|
171
|
-
.bd-img:not([src])::before {
|
|
172
|
-
content: var(--bd-img-loading-label, "Loading image");
|
|
173
|
-
}
|
|
87
|
+
/* Gone from here, because nothing could reach them: bordered,
|
|
88
|
+
bordered-primary and bordered-thick, responsive, aspect-16-9 / 4-3 /
|
|
89
|
+
1-1, hover-zoom and hover-shadow had no prop, and the only way to set
|
|
90
|
+
one was to pass its name as fit, which now accepts only the five values
|
|
91
|
+
above. :focus-visible styled an <img>, which never takes focus. And the
|
|
92
|
+
:not([src]) loading placeholder could not appear: the template always
|
|
93
|
+
writes a src attribute, and an <img> renders no ::before even without
|
|
94
|
+
one. bd-picture keeps its own placeholder, which does work. */
|
|
174
95
|
|
|
175
96
|
/* Smooth transitions for radius and shadow changes */
|
|
176
97
|
.bd-img {
|
|
177
98
|
transition: border-radius var(--transition-duration-normal) var(--transition-easing-smooth), box-shadow var(--transition-duration-normal) var(--transition-easing-smooth);
|
|
178
99
|
}
|
|
179
100
|
|
|
180
|
-
/* Performance optimization for transforms */
|
|
181
|
-
.bd-img.hover-zoom {
|
|
182
|
-
will-change: transform;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/* A hover that scales the image is the category of motion the setting
|
|
186
|
-
exists for, and this atom had no guard at all. */
|
|
187
101
|
@media (prefers-reduced-motion: reduce) {
|
|
188
|
-
.bd-img
|
|
189
|
-
.bd-img.hover-zoom:hover,
|
|
190
|
-
.bd-img.hover-shadow:hover {
|
|
102
|
+
.bd-img {
|
|
191
103
|
transition: none;
|
|
192
|
-
transform: none;
|
|
193
104
|
}
|
|
194
105
|
}
|
|
195
106
|
}
|
|
@@ -5,6 +5,11 @@ import imgCSS from "./image.css.js";
|
|
|
5
5
|
|
|
6
6
|
// the radius steps image.css.js implements — the full --radius-* scale
|
|
7
7
|
const RADIUS_STEPS = ['none', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', 'full'];
|
|
8
|
+
/* fit and shadow become class names, so an unknown value used to become a
|
|
9
|
+
class too -- fit="bordered" reached a rule nothing documented. Only the
|
|
10
|
+
values image.css.js implements get through; anything else is the default. */
|
|
11
|
+
const FITS = ['cover', 'contain', 'fill', 'none', 'scale-down'];
|
|
12
|
+
const SHADOWS = ['none', 'sm', 'md', 'lg', 'xl'];
|
|
8
13
|
|
|
9
14
|
export class BdImg extends LitElement {
|
|
10
15
|
static properties = {
|
|
@@ -32,10 +37,10 @@ export class BdImg extends LitElement {
|
|
|
32
37
|
|
|
33
38
|
render() {
|
|
34
39
|
const baseClass = 'bd-img';
|
|
35
|
-
const fitClass = this.fit
|
|
40
|
+
const fitClass = FITS.includes(this.fit) ? this.fit : 'cover';
|
|
36
41
|
const radius = RADIUS_STEPS.includes(this.radius) ? this.radius : 'none';
|
|
37
42
|
const radiusClass = `radius-${radius}`;
|
|
38
|
-
const shadowClass = `shadow-${this.shadow
|
|
43
|
+
const shadowClass = `shadow-${SHADOWS.includes(this.shadow) ? this.shadow : 'none'}`;
|
|
39
44
|
|
|
40
45
|
const finalClass = `${baseClass} ${fitClass} ${radiusClass} ${shadowClass}`;
|
|
41
46
|
|
|
@@ -23,7 +23,7 @@ It includes:
|
|
|
23
23
|
| State | Description |
|
|
24
24
|
|---|---|
|
|
25
25
|
| \`default\` | Idle, no interaction |
|
|
26
|
-
| \`focused\` |
|
|
26
|
+
| \`focused\` | Has keyboard focus — set it to move focus into the field; it reflects whether the field is focused |
|
|
27
27
|
| \`invalid\` | Failed validation — red border + helper text |
|
|
28
28
|
| \`validated\` | Passed validation — green border + check icon |
|
|
29
29
|
| \`disabled\` | Non-interactive, visually muted |
|
|
@@ -86,7 +86,8 @@ the events for your own use.
|
|
|
86
86
|
\`invalid\` sets \`aria-invalid="true"\`, and when \`tooltip\` is present the input
|
|
87
87
|
points at it with \`aria-describedby\` — so a screen reader announces both that
|
|
88
88
|
the field is wrong and why. Put the reason in \`tooltip\`; it doubles as the
|
|
89
|
-
help text and the error message.
|
|
89
|
+
help text and the error message. \`bd-custom-dropdown\` and
|
|
90
|
+
\`bd-custom-input-clipboard\` do the same.
|
|
90
91
|
|
|
91
92
|
The required asterisk is \`aria-hidden\`, because the native \`required\`
|
|
92
93
|
attribute is what actually conveys it.
|
|
@@ -129,26 +130,19 @@ everything else is not:
|
|
|
129
130
|
| | \`bd-custom-input\` | \`bd-custom-dropdown\` | \`bd-custom-input-clipboard\` |
|
|
130
131
|
|---|---|---|---|
|
|
131
132
|
| \`label\`, \`kind\`, \`required\`, \`invalid\`, \`validated\`, \`disabled\`, \`tooltip\` | yes | yes | yes |
|
|
132
|
-
| \`value\` | yes |
|
|
133
|
+
| \`value\` | yes | yes | yes |
|
|
133
134
|
| \`type\`, \`placeholder\`, \`focused\` | yes | **no** | yes |
|
|
134
|
-
| \`name
|
|
135
|
+
| \`name\` | yes | yes | yes |
|
|
136
|
+
| \`inputmode\`, \`autocomplete\`, \`enterkeyhint\` | yes | **no** | **no** |
|
|
135
137
|
| \`options\` | **no** | yes | **no** |
|
|
136
|
-
| emits | \`bd-input\`, \`bd-change\` |
|
|
138
|
+
| emits | \`bd-input\`, \`bd-change\` | \`bd-input\`, \`bd-change\` | \`bd-input\`, \`bd-change\` |
|
|
137
139
|
|
|
138
140
|
Storybook shows one shared control panel for all three, so a knob that does
|
|
139
141
|
nothing on the story you are looking at is expected — check this table.
|
|
140
142
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
nothing observable from outside the component, so it cannot currently be read
|
|
145
|
-
without reaching into its shadow root. It needs the same \`value\` +
|
|
146
|
-
\`bd-input\`/\`bd-change\` treatment the input has.
|
|
147
|
-
- **The clipboard field's event is not the input's.** It fires \`input\` — the
|
|
148
|
-
native event's own name — with the string as \`detail\` rather than
|
|
149
|
-
\`{ value, name }\`, and it is neither \`bubbles\` nor \`composed\`, so it reaches
|
|
150
|
-
a listener on the element itself and nothing further up. Renaming it to
|
|
151
|
-
\`bd-input\` would align the three, and is a breaking change.
|
|
143
|
+
All three emit the same events with the same \`detail\`, so one form handler
|
|
144
|
+
reads them. The clipboard field used to fire \`input\` — the native event's
|
|
145
|
+
name — with a bare string; a paste fires \`bd-input\` and then \`bd-change\`.
|
|
152
146
|
|
|
153
147
|
### The clipboard field pastes, it does not copy
|
|
154
148
|
The button reads the system clipboard with \`navigator.clipboard.readText()\` and
|
|
@@ -222,7 +216,7 @@ misses 2.5.5 (AAA, 44px) at every size.
|
|
|
222
216
|
},
|
|
223
217
|
focused: {
|
|
224
218
|
control : 'boolean',
|
|
225
|
-
description: '
|
|
219
|
+
description: 'Moves keyboard focus into the field, and reflects whether it has focus. The ring is the browser\'s real focus ring.',
|
|
226
220
|
table : {
|
|
227
221
|
type : { summary: 'boolean' },
|
|
228
222
|
defaultValue: { summary: false },
|
|
@@ -405,7 +399,8 @@ export const InputDefault = {
|
|
|
405
399
|
};
|
|
406
400
|
|
|
407
401
|
/**
|
|
408
|
-
*
|
|
402
|
+
* An input that takes focus when it renders, through the focused prop -- the
|
|
403
|
+
* ring is the real one, because the field really is focused.
|
|
409
404
|
*/
|
|
410
405
|
export const InputFocused = {
|
|
411
406
|
name : 'Input — Focused',
|
|
@@ -415,7 +410,7 @@ export const InputFocused = {
|
|
|
415
410
|
docs: {
|
|
416
411
|
description: {
|
|
417
412
|
story:
|
|
418
|
-
'
|
|
413
|
+
'`focused` moves keyboard focus into the field when the story renders, so the ring shown is the browser\'s own. Clicking elsewhere clears it, and the prop follows.'
|
|
419
414
|
}
|
|
420
415
|
}
|
|
421
416
|
}
|
|
@@ -50,7 +50,12 @@ export default css`
|
|
|
50
50
|
shared one; the radius is the default for an element with no radius of its
|
|
51
51
|
own, so the ring is not a hard rectangle. */
|
|
52
52
|
.paste-button:focus-visible {
|
|
53
|
-
|
|
53
|
+
/* forced-colors mode does not paint box-shadow, so the ring
|
|
54
|
+
vanishes there; it does paint an outline, in a system colour. This one
|
|
55
|
+
is transparent everywhere else, and the offset lands it on the ring's
|
|
56
|
+
outer band. */
|
|
57
|
+
outline: var(--border-width-2) solid transparent;
|
|
58
|
+
outline-offset: var(--focus-outline-offset);
|
|
54
59
|
box-shadow: var(--focus-outline-primary);
|
|
55
60
|
border-radius: var(--focus-outline-radius);
|
|
56
61
|
}
|
|
@@ -14,12 +14,14 @@ class CustomInputClipboard extends LitElement {
|
|
|
14
14
|
kind : { type: String },
|
|
15
15
|
placeholder: { type: String },
|
|
16
16
|
required : { type: Boolean },
|
|
17
|
-
focused : { type: Boolean },
|
|
17
|
+
focused : { type: Boolean, reflect: true },
|
|
18
18
|
tooltip : { type: String },
|
|
19
19
|
invalid : { type: Boolean },
|
|
20
20
|
validated : { type: Boolean, reflect: true },
|
|
21
21
|
disabled : { type: Boolean, reflect: true },
|
|
22
|
-
value : { type: String }
|
|
22
|
+
value : { type: String },
|
|
23
|
+
/** Carried on the event detail, as on bd-custom-input. */
|
|
24
|
+
name : { type: String }
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
constructor() {
|
|
@@ -37,13 +39,32 @@ class CustomInputClipboard extends LitElement {
|
|
|
37
39
|
this.validated = false;
|
|
38
40
|
this.disabled = false;
|
|
39
41
|
this.value = "";
|
|
42
|
+
this.name = "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* THE EVENTS ARE bd-custom-input's.
|
|
46
|
+
This fired an event named input -- the native event's own name -- with
|
|
47
|
+
the bare string as detail, neither bubbling nor composed. The native
|
|
48
|
+
input from the inner <input> is composed and already leaves the shadow
|
|
49
|
+
root, so a listener on the host heard every keystroke twice, once with a
|
|
50
|
+
detail and once without, and a paste once. Now it is bd-input on every
|
|
51
|
+
change and bd-change on commit, each { value, name }, bubbling and
|
|
52
|
+
composed, as on bd-custom-input: one form handler reads all three fields.
|
|
53
|
+
A paste is a commit as well as a change, so it fires both. */
|
|
54
|
+
_emit(type) {
|
|
55
|
+
this.dispatchEvent(new CustomEvent(type, {
|
|
56
|
+
detail : { value: this.value, name: this.name },
|
|
57
|
+
bubbles : true,
|
|
58
|
+
composed: true
|
|
59
|
+
}));
|
|
40
60
|
}
|
|
41
61
|
|
|
42
62
|
async pasteFromClipboard() {
|
|
43
63
|
try {
|
|
44
64
|
const text = await navigator.clipboard.readText();
|
|
45
65
|
this.value = text;
|
|
46
|
-
this.
|
|
66
|
+
this._emit("bd-input");
|
|
67
|
+
this._emit("bd-change");
|
|
47
68
|
} catch (err) {
|
|
48
69
|
console.error("Clipboard paste failed:", err);
|
|
49
70
|
}
|
|
@@ -51,7 +72,11 @@ class CustomInputClipboard extends LitElement {
|
|
|
51
72
|
|
|
52
73
|
handleInput(e) {
|
|
53
74
|
this.value = e.target.value;
|
|
54
|
-
this.
|
|
75
|
+
this._emit("bd-input");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
handleChange() {
|
|
79
|
+
this._emit("bd-change");
|
|
55
80
|
}
|
|
56
81
|
|
|
57
82
|
willUpdate(changed) {
|
|
@@ -61,6 +86,21 @@ class CustomInputClipboard extends LitElement {
|
|
|
61
86
|
if (changed.has("size") || changed.has("kind")) this.size = resolveSize(this.size);
|
|
62
87
|
}
|
|
63
88
|
|
|
89
|
+
/* focused is the field's real focus, both ways. Setting it moves keyboard
|
|
90
|
+
focus into the <input> and clearing it blurs the field; focusing or
|
|
91
|
+
leaving the field by hand sets it, and it reflects, so the attribute says
|
|
92
|
+
whether the field has focus. It used to add a .focused class that painted
|
|
93
|
+
the focus ring on a field that did not have focus -- a second, false
|
|
94
|
+
indicator on any page that set it. The ring is now only the browser's
|
|
95
|
+
:focus-visible, which a text field shows whenever it is focused. */
|
|
96
|
+
updated(changed) {
|
|
97
|
+
if (!changed.has('focused')) return;
|
|
98
|
+
const input = this.shadowRoot.querySelector('input');
|
|
99
|
+
const has = this.shadowRoot.activeElement === input;
|
|
100
|
+
if (this.focused && !has) input?.focus();
|
|
101
|
+
if (!this.focused && has) input?.blur();
|
|
102
|
+
}
|
|
103
|
+
|
|
64
104
|
render() {
|
|
65
105
|
const inputClass = {
|
|
66
106
|
sm: "input-sm",
|
|
@@ -88,6 +128,10 @@ class CustomInputClipboard extends LitElement {
|
|
|
88
128
|
? "form-help-text--success"
|
|
89
129
|
: "form-help-text--default";
|
|
90
130
|
|
|
131
|
+
// The hint is the field's description, and the state is announced, not
|
|
132
|
+
// only drawn: invalid used to be a red border alone and the hint was never
|
|
133
|
+
// pointed at, so the reason a field was invalid reached a screen reader
|
|
134
|
+
// not at all. Both as bd-custom-input does them.
|
|
91
135
|
return html`
|
|
92
136
|
<div class="input-container">
|
|
93
137
|
${this.label
|
|
@@ -101,15 +145,19 @@ class CustomInputClipboard extends LitElement {
|
|
|
101
145
|
<div class="input-with-button">
|
|
102
146
|
<input
|
|
103
147
|
id="clipboard-input"
|
|
104
|
-
class="form-input-bd ${inputClass} ${
|
|
105
|
-
? "focused"
|
|
106
|
-
: ""} ${inputStateClass}"
|
|
148
|
+
class="form-input-bd ${inputClass} ${inputStateClass}"
|
|
107
149
|
type="${this.type}"
|
|
108
150
|
placeholder="${this.placeholder}"
|
|
109
151
|
.value="${this.value}"
|
|
110
152
|
?required="${this.required}"
|
|
111
153
|
?disabled="${this.disabled}"
|
|
154
|
+
name="${this.name || nothing}"
|
|
155
|
+
aria-invalid="${this.invalid ? "true" : nothing}"
|
|
156
|
+
aria-describedby="${this.tooltip ? "clipboard-input-help" : nothing}"
|
|
157
|
+
@focus="${() => { this.focused = true; }}"
|
|
158
|
+
@blur="${() => { this.focused = false; }}"
|
|
112
159
|
@input="${this.handleInput}"
|
|
160
|
+
@change="${this.handleChange}"
|
|
113
161
|
/>
|
|
114
162
|
<button
|
|
115
163
|
class="paste-button"
|
|
@@ -139,7 +187,7 @@ class CustomInputClipboard extends LitElement {
|
|
|
139
187
|
</div>
|
|
140
188
|
|
|
141
189
|
${this.tooltip
|
|
142
|
-
? html`<div class="form-help-text ${helpTextClass}">
|
|
190
|
+
? html`<div id="clipboard-input-help" class="form-help-text ${helpTextClass}">
|
|
143
191
|
${this.tooltip}
|
|
144
192
|
</div>`
|
|
145
193
|
: ""}
|
|
@@ -18,7 +18,7 @@ class CustomInput extends LitElement {
|
|
|
18
18
|
kind : { type: String },
|
|
19
19
|
placeholder : { type: String },
|
|
20
20
|
required : { type: Boolean },
|
|
21
|
-
focused : { type: Boolean },
|
|
21
|
+
focused : { type: Boolean, reflect: true },
|
|
22
22
|
tooltip : { type: String },
|
|
23
23
|
invalid : { type: Boolean },
|
|
24
24
|
validated : { type: Boolean, reflect: true },
|
|
@@ -86,6 +86,21 @@ class CustomInput extends LitElement {
|
|
|
86
86
|
if (changed.has("size") || changed.has("kind")) this.size = resolveSize(this.size);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/* focused is the field's real focus, both ways. Setting it moves keyboard
|
|
90
|
+
focus into the <input> and clearing it blurs the field; focusing or
|
|
91
|
+
leaving the field by hand sets it, and it reflects, so the attribute says
|
|
92
|
+
whether the field has focus. It used to add a .focused class that painted
|
|
93
|
+
the focus ring on a field that did not have focus -- a second, false
|
|
94
|
+
indicator on any page that set it. The ring is now only the browser's
|
|
95
|
+
:focus-visible, which a text field shows whenever it is focused. */
|
|
96
|
+
updated(changed) {
|
|
97
|
+
if (!changed.has('focused')) return;
|
|
98
|
+
const input = this.shadowRoot.querySelector('input');
|
|
99
|
+
const has = this.shadowRoot.activeElement === input;
|
|
100
|
+
if (this.focused && !has) input?.focus();
|
|
101
|
+
if (!this.focused && has) input?.blur();
|
|
102
|
+
}
|
|
103
|
+
|
|
89
104
|
render() {
|
|
90
105
|
const inputClass = { sm: "input-sm", md: "input-md", lg: "input-lg" }[this.size];
|
|
91
106
|
const labelClass = { sm: "label-sm", md: "label-md", lg: "label-lg" }[this.size];
|
|
@@ -116,7 +131,7 @@ class CustomInput extends LitElement {
|
|
|
116
131
|
: nothing}
|
|
117
132
|
<input
|
|
118
133
|
id="${INPUT_ID}"
|
|
119
|
-
class="form-input-bd ${inputClass} ${
|
|
134
|
+
class="form-input-bd ${inputClass} ${inputStateClass}"
|
|
120
135
|
type="${this.type}"
|
|
121
136
|
name="${this.name || nothing}"
|
|
122
137
|
placeholder="${this.placeholder}"
|
|
@@ -128,6 +143,8 @@ class CustomInput extends LitElement {
|
|
|
128
143
|
?disabled="${this.disabled}"
|
|
129
144
|
aria-invalid="${this.invalid ? "true" : nothing}"
|
|
130
145
|
aria-describedby="${this.tooltip ? HELP_ID : nothing}"
|
|
146
|
+
@focus="${() => { this.focused = true; }}"
|
|
147
|
+
@blur="${() => { this.focused = false; }}"
|
|
131
148
|
@input="${this._onInput}"
|
|
132
149
|
@change="${this._onChange}"
|
|
133
150
|
/>
|