@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
|
@@ -4,6 +4,13 @@ import '../../tokens/tokens.js';
|
|
|
4
4
|
import { labelCSS } from './label.css.js';
|
|
5
5
|
import { resolveSize } from '../shared/prop-vocabulary.js';
|
|
6
6
|
|
|
7
|
+
let labelIds = 0;
|
|
8
|
+
|
|
9
|
+
/* Controls whose own <label for> would have worked: the ones a label can be
|
|
10
|
+
for. A custom element is not here -- see the note on _target. */
|
|
11
|
+
const LABELABLE = new Set(["input", "select", "textarea", "button", "meter", "output", "progress"]);
|
|
12
|
+
const ACTIVATES = new Set(["checkbox", "radio"]);
|
|
13
|
+
|
|
7
14
|
export class BdLabel extends LitElement {
|
|
8
15
|
static styles = [labelCSS];
|
|
9
16
|
|
|
@@ -17,6 +24,7 @@ export class BdLabel extends LitElement {
|
|
|
17
24
|
super();
|
|
18
25
|
this.size = 'md';
|
|
19
26
|
this.required = false;
|
|
27
|
+
this.addEventListener('click', e => this._activate(e));
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
willUpdate(changed) {
|
|
@@ -24,9 +32,87 @@ export class BdLabel extends LitElement {
|
|
|
24
32
|
if (changed.has('size')) this.size = resolveSize(this.size, 'md');
|
|
25
33
|
}
|
|
26
34
|
|
|
35
|
+
/* WHY for IS DONE BY HAND.
|
|
36
|
+
The <label> this renders is in its own shadow root, and a for reference
|
|
37
|
+
resolves only in the tree it is written in, so <bd-label for="email">
|
|
38
|
+
pointed at nothing: the control went unnamed and a click on the label did
|
|
39
|
+
nothing. It now does what a native label's for does, for a native control
|
|
40
|
+
in the same tree as this element:
|
|
41
|
+
|
|
42
|
+
name the control gets aria-labelledby pointing at this element,
|
|
43
|
+
added to whatever it already lists, and taken back when for
|
|
44
|
+
changes or this element leaves -- so the control is named by
|
|
45
|
+
the label's text
|
|
46
|
+
activate a click on the label focuses the control, and toggles it
|
|
47
|
+
when it is a checkbox or a radio
|
|
48
|
+
|
|
49
|
+
The control may not exist yet when this renders -- it can come later in
|
|
50
|
+
the page, or be rendered after -- so until it is found the root is
|
|
51
|
+
watched for it. */
|
|
52
|
+
connectedCallback() {
|
|
53
|
+
super.connectedCallback();
|
|
54
|
+
this._link();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
disconnectedCallback() {
|
|
58
|
+
this._unlink();
|
|
59
|
+
super.disconnectedCallback();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
updated(changed) {
|
|
63
|
+
if (changed.has('for')) this._link();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* The control, when for names one in this element's own tree. A custom
|
|
67
|
+
element is skipped: its real control is inside its own shadow root, where
|
|
68
|
+
aria-labelledby on the host does not reach -- label a bd-custom-input
|
|
69
|
+
with its own label prop. */
|
|
70
|
+
get _target() {
|
|
71
|
+
if (!this.for || !this.isConnected) return null;
|
|
72
|
+
const el = this.getRootNode().getElementById?.(this.for);
|
|
73
|
+
return el && LABELABLE.has(el.localName) ? el : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_link() {
|
|
77
|
+
this._unlink();
|
|
78
|
+
if (!this.for || !this.isConnected) return;
|
|
79
|
+
const target = this._target;
|
|
80
|
+
if (!target) {
|
|
81
|
+
// not there yet: watch for it, and stop as soon as it is
|
|
82
|
+
const root = this.getRootNode();
|
|
83
|
+
this._waiting = new MutationObserver(() => { if (this._target) this._link(); });
|
|
84
|
+
this._waiting.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['id'] });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!this.id) this.id = `bd-label-${++labelIds}`;
|
|
88
|
+
const ids = (target.getAttribute('aria-labelledby') || '').split(/\s+/).filter(Boolean);
|
|
89
|
+
if (!ids.includes(this.id)) target.setAttribute('aria-labelledby', [...ids, this.id].join(' '));
|
|
90
|
+
this._linked = { target, id: this.id };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_unlink() {
|
|
94
|
+
this._waiting?.disconnect();
|
|
95
|
+
this._waiting = null;
|
|
96
|
+
if (!this._linked) return;
|
|
97
|
+
const { target, id } = this._linked;
|
|
98
|
+
const ids = (target.getAttribute('aria-labelledby') || '').split(/\s+/).filter(t => t && t !== id);
|
|
99
|
+
if (ids.length) target.setAttribute('aria-labelledby', ids.join(' '));
|
|
100
|
+
else target.removeAttribute('aria-labelledby');
|
|
101
|
+
this._linked = null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_activate(e) {
|
|
105
|
+
const target = this._target;
|
|
106
|
+
if (!target || target.disabled) return;
|
|
107
|
+
// a link or a control slotted into the label keeps its own click
|
|
108
|
+
if (e.composedPath().some(n => n !== this && n.matches?.('a[href], button, input, select, textarea'))) return;
|
|
109
|
+
target.focus();
|
|
110
|
+
if (target.localName === 'input' && ACTIVATES.has(target.type)) target.click();
|
|
111
|
+
}
|
|
112
|
+
|
|
27
113
|
render() {
|
|
28
114
|
return html`
|
|
29
|
-
<label
|
|
115
|
+
<label>
|
|
30
116
|
<slot></slot>${this.required
|
|
31
117
|
? html`<span class="required" aria-hidden="true">*</span>`
|
|
32
118
|
: nothing}
|
|
@@ -21,23 +21,32 @@ marker.
|
|
|
21
21
|
### Props
|
|
22
22
|
| Prop | Values | Default |
|
|
23
23
|
|---|---|---|
|
|
24
|
-
| \`size\` | \`
|
|
24
|
+
| \`size\` | \`sm\`, \`md\`, \`lg\` — \`small\` / \`default\` / \`large\` still resolve | \`md\` |
|
|
25
25
|
| \`required\` | boolean — appends a \`*\` marker | \`false\` |
|
|
26
26
|
| \`for\` | id of the control being labelled | \`''\` |
|
|
27
27
|
|
|
28
|
-
### \`for\` does
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
### What \`for\` does
|
|
29
|
+
\`for\` names a native control in the same tree as the label — an
|
|
30
|
+
\`<input>\`, \`<select>\` or \`<textarea>\` with that \`id\` — and behaves like a
|
|
31
|
+
native label's \`for\`:
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
\`
|
|
33
|
+
- the control is **named by the label**: it gets \`aria-labelledby\` pointing at
|
|
34
|
+
this element, added to any it already has and removed again when \`for\`
|
|
35
|
+
changes or the label leaves the page. A label without an \`id\` is given one.
|
|
36
|
+
- a **click on the label** focuses the control, and toggles it when it is a
|
|
37
|
+
checkbox or a radio.
|
|
38
|
+
|
|
39
|
+
The control may come later in the page; the label waits for it.
|
|
35
40
|
|
|
36
41
|
\`\`\`html
|
|
37
|
-
<bd-label
|
|
38
|
-
<input
|
|
42
|
+
<bd-label for="email" required>Email address</bd-label>
|
|
43
|
+
<input id="email" type="email" required>
|
|
39
44
|
\`\`\`
|
|
40
45
|
|
|
46
|
+
It does **not** reach a design-system field such as \`bd-custom-input\`: that
|
|
47
|
+
field's \`<input>\` is in its own shadow root, where nothing outside can name
|
|
48
|
+
it. Give those fields their own \`label\` prop instead.
|
|
49
|
+
|
|
41
50
|
### The required marker is decorative
|
|
42
51
|
The \`*\` carries \`aria-hidden="true"\`, so it is not announced. That is correct
|
|
43
52
|
only if the control itself is marked \`required\` or \`aria-required\` — a screen
|
|
@@ -50,9 +59,9 @@ Set both.
|
|
|
50
59
|
argTypes: {
|
|
51
60
|
size: {
|
|
52
61
|
control : { type: 'select' },
|
|
53
|
-
options : ['
|
|
62
|
+
options : ['sm', 'md', 'lg'],
|
|
54
63
|
description: 'Type size of the label.',
|
|
55
|
-
table : { type: { summary: 'string' }, defaultValue: { summary:
|
|
64
|
+
table : { type: { summary: 'string' }, defaultValue: { summary: 'md' }, category: 'Appearance' }
|
|
56
65
|
},
|
|
57
66
|
required: {
|
|
58
67
|
control : 'boolean',
|
|
@@ -61,7 +70,7 @@ Set both.
|
|
|
61
70
|
},
|
|
62
71
|
for: {
|
|
63
72
|
control : 'text',
|
|
64
|
-
description: 'Id of the
|
|
73
|
+
description: 'Id of a native control in the same tree. It is named by this label and focused by a click on it. Does not reach inside bd-custom-input — see the notes.',
|
|
65
74
|
table : { type: { summary: 'string' }, defaultValue: { summary: "" }, category: 'Accessibility' }
|
|
66
75
|
}
|
|
67
76
|
}
|
|
@@ -14,13 +14,19 @@ import { css } from "lit";
|
|
|
14
14
|
* text, and the atom's own bold variant already avoided it.
|
|
15
15
|
*/
|
|
16
16
|
export default css`
|
|
17
|
+
/* Every axis here is the standalone role's. Weight used to be the one
|
|
18
|
+
exception -- it read --typography-link-default-fontWeight, the inline
|
|
19
|
+
role's 400 -- so a standalone link rendered 400 against the 500 its own
|
|
20
|
+
role, the Figma link/standalone style and the homepage all draw. The
|
|
21
|
+
inline role's weight now lives in the inline block below, with the rest
|
|
22
|
+
of that role. */
|
|
17
23
|
.bd-link {
|
|
18
24
|
display: inline-flex;
|
|
19
25
|
align-items: center;
|
|
20
26
|
gap: var(--spacing-4);
|
|
21
27
|
font-family: var(--typography-fontFamily-sans);
|
|
22
28
|
font-size: var(--typography-link-standalone-fontSize);
|
|
23
|
-
font-weight: var(--typography-link-
|
|
29
|
+
font-weight: var(--typography-link-standalone-fontWeight);
|
|
24
30
|
line-height: var(--typography-link-standalone-lineHeight);
|
|
25
31
|
color: var(--bd-link-color, var(--color-text-brand));
|
|
26
32
|
text-decoration: none;
|
|
@@ -48,6 +54,7 @@ export default css`
|
|
|
48
54
|
:host([inline]) .bd-link {
|
|
49
55
|
display: inline;
|
|
50
56
|
font-size: var(--typography-link-default-fontSize);
|
|
57
|
+
font-weight: var(--typography-link-default-fontWeight);
|
|
51
58
|
line-height: var(--typography-link-default-lineHeight);
|
|
52
59
|
letter-spacing: var(--typography-link-default-letterSpacing);
|
|
53
60
|
font-family: var(--typography-link-default-fontFamily);
|
|
@@ -142,6 +149,35 @@ export default css`
|
|
|
142
149
|
text-decoration: none;
|
|
143
150
|
}
|
|
144
151
|
|
|
152
|
+
/* ---- arrow ------------------------------------------------------------
|
|
153
|
+
spacing/8 between the label and the arrow, the icon-to-label gap bd-back
|
|
154
|
+
and the md and lg buttons already use. The base row's spacing/4 was set
|
|
155
|
+
for text alone, and the Figma set measures 8.
|
|
156
|
+
|
|
157
|
+
The underline needs no rule of its own. The anchor is a flex container,
|
|
158
|
+
so the decoration propagates to its flex items and is drawn only where
|
|
159
|
+
there is text: under the label, not under the arrow, which is what the
|
|
160
|
+
Link arrow set draws. That was the set's open question, and it is
|
|
161
|
+
answered by the layout this atom already had rather than by a choice. */
|
|
162
|
+
.bd-link--arrow {
|
|
163
|
+
gap: var(--spacing-8);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* a swapped icon is held to the step the default arrow is drawn at */
|
|
167
|
+
.bd-link--arrow ::slotted([slot="icon"]) {
|
|
168
|
+
height: var(--icon-sm-size);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* inline returns the link to a run of text, so there is no row and the gap
|
|
172
|
+
goes inert; a start margin keeps the arrow off the last letter instead.
|
|
173
|
+
The icon has no text, so its baseline is its bottom edge and it sat on
|
|
174
|
+
the line about 4px high; middle centres it on the x-height. */
|
|
175
|
+
:host([inline]) .bd-link--arrow bd-arrow-icon,
|
|
176
|
+
:host([inline]) .bd-link--arrow ::slotted([slot="icon"]) {
|
|
177
|
+
margin-inline-start: var(--spacing-4);
|
|
178
|
+
vertical-align: middle;
|
|
179
|
+
}
|
|
180
|
+
|
|
145
181
|
.bd-link--active {
|
|
146
182
|
/* no role: a link in its active state -- a state or modifier weight, not a text-content
|
|
147
183
|
type. See button.css.js's .button--bold for the argument. */
|
|
@@ -181,8 +217,15 @@ export default css`
|
|
|
181
217
|
.bd-link:focus-visible {
|
|
182
218
|
/* the one focus ring — see Foundations/Focus. A two-colour ring whose
|
|
183
219
|
white inner band keeps it visible on a coloured or dark surface, which a
|
|
184
|
-
single-colour outline cannot do.
|
|
185
|
-
|
|
220
|
+
single-colour outline cannot do. surface="dark" keeps it too: a link has
|
|
221
|
+
no fill or border, so its white band meets the hero directly, 4.53:1 on
|
|
222
|
+
blue-500 and more on every darker ground. */
|
|
223
|
+
/* forced-colors mode does not paint box-shadow, so the ring
|
|
224
|
+
vanishes there; it does paint an outline, in a system colour. This one
|
|
225
|
+
is transparent everywhere else, and the offset lands it on the ring's
|
|
226
|
+
outer band. */
|
|
227
|
+
outline: var(--border-width-2) solid transparent;
|
|
228
|
+
outline-offset: var(--focus-outline-offset);
|
|
186
229
|
box-shadow: var(--focus-outline-primary);
|
|
187
230
|
border-radius: var(--focus-outline-radius);
|
|
188
231
|
}
|
|
@@ -209,6 +252,12 @@ export default css`
|
|
|
209
252
|
cursor: default;
|
|
210
253
|
}
|
|
211
254
|
|
|
255
|
+
/* An <a> with no href is not a link, so it does not get the hand: the
|
|
256
|
+
pointer would promise a destination there is none of. */
|
|
257
|
+
a.bd-link:not([href]) {
|
|
258
|
+
cursor: default;
|
|
259
|
+
}
|
|
260
|
+
|
|
212
261
|
@media (prefers-reduced-motion: reduce) {
|
|
213
262
|
.bd-link {
|
|
214
263
|
transition: none;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { LitElement, html } from "lit";
|
|
1
|
+
import { LitElement, html, nothing } from "lit";
|
|
2
2
|
// Installs the design tokens into document scope; see src/tokens/tokens.js
|
|
3
3
|
import "../../tokens/tokens.js";
|
|
4
4
|
import linkCSS from "./link.css.js";
|
|
5
|
+
// the trailing mark when arrow is set; an atom, the same one bd-button uses
|
|
6
|
+
import "../icons/arrow-icon.js";
|
|
5
7
|
import { fontSize, color } from "../shared/token-scales.js";
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -47,12 +49,29 @@ class BdLink extends LitElement {
|
|
|
47
49
|
/* The surface the link sits on, the same attribute bd-button uses. It
|
|
48
50
|
replaces kind="light", which described the surface rather than the link
|
|
49
51
|
and so could not be combined with any of the real kinds. */
|
|
50
|
-
surface : { type: String, reflect: true }
|
|
52
|
+
surface : { type: String, reflect: true },
|
|
53
|
+
/* A trailing arrow after the label: "Read more ->" on the homepage's post
|
|
54
|
+
cards and the four solution-splitter links. Drawn in the foundation
|
|
55
|
+
Figma file as Link arrow and Link arrow / dark; until now three
|
|
56
|
+
components hand-rolled it, each with a 12px chevron where the design
|
|
57
|
+
has the 16px shafted arrow. A modifier like underline rather than a
|
|
58
|
+
sixth variant, because every variant, state and surface takes it. */
|
|
59
|
+
arrow : { type: Boolean, reflect: true },
|
|
60
|
+
/* Extra text a screen reader reads after the link's name, through
|
|
61
|
+
aria-describedby on the inner <a>, as on bd-button: the <a> is in this
|
|
62
|
+
shadow root, so a description from outside never reaches it. bd-tooltip
|
|
63
|
+
sets it on a bd-link trigger; a page can set it directly. */
|
|
64
|
+
description: { type: String }
|
|
51
65
|
};
|
|
52
66
|
|
|
53
67
|
constructor() {
|
|
54
68
|
super();
|
|
55
|
-
|
|
69
|
+
/* No destination unless one is given. This was "#", so a bd-link written
|
|
70
|
+
without an href was still a real link -- focusable, announced as a
|
|
71
|
+
link, and on activation a jump to the top of the page. Now it renders
|
|
72
|
+
an <a> with no href, which is not a link at all, and the missing
|
|
73
|
+
destination shows instead of hiding. */
|
|
74
|
+
this.href = "";
|
|
56
75
|
this.target = "_self";
|
|
57
76
|
this.variant = "primary";
|
|
58
77
|
this.kind = "";
|
|
@@ -64,6 +83,8 @@ class BdLink extends LitElement {
|
|
|
64
83
|
this.color = "";
|
|
65
84
|
this.active = false;
|
|
66
85
|
this.surface = "light";
|
|
86
|
+
this.arrow = false;
|
|
87
|
+
this.description = "";
|
|
67
88
|
}
|
|
68
89
|
|
|
69
90
|
willUpdate(changed) {
|
|
@@ -85,10 +106,24 @@ class BdLink extends LitElement {
|
|
|
85
106
|
this.underline ? "bd-link--underline" : "",
|
|
86
107
|
this.noUnderline ? "bd-link--no-underline" : "",
|
|
87
108
|
this.active ? "bd-link--active" : "",
|
|
88
|
-
this.disabled ? "bd-link--disabled" : ""
|
|
109
|
+
this.disabled ? "bd-link--disabled" : "",
|
|
110
|
+
this.arrow ? "bd-link--arrow" : ""
|
|
89
111
|
].filter(Boolean).join(" ");
|
|
90
112
|
}
|
|
91
113
|
|
|
114
|
+
/**
|
|
115
|
+
* The arrow is decorative: the label already says where the link goes, so
|
|
116
|
+
* the icon is hidden rather than named. It sits in a named slot so a page
|
|
117
|
+
* can swap the glyph, the way the Figma set swaps its icon property, and it
|
|
118
|
+
* is drawn in currentColor, so it takes the link's ink in every variant,
|
|
119
|
+
* state and surface without a rule of its own.
|
|
120
|
+
*/
|
|
121
|
+
get _arrow() {
|
|
122
|
+
return this.arrow
|
|
123
|
+
? html`<slot name="icon"><bd-arrow-icon size="sm" aria-hidden="true"></bd-arrow-icon></slot>`
|
|
124
|
+
: nothing;
|
|
125
|
+
}
|
|
126
|
+
|
|
92
127
|
/**
|
|
93
128
|
* font-size is a type-scale step and color is a semantic role; both are
|
|
94
129
|
* resolved to token references by shared/token-scales.js, and anything off
|
|
@@ -122,27 +157,27 @@ class BdLink extends LitElement {
|
|
|
122
157
|
.join("; ");
|
|
123
158
|
}
|
|
124
159
|
|
|
160
|
+
/* No whitespace anywhere in these templates: not between the tags and the
|
|
161
|
+
slot, and not around the element either. The default flex row never
|
|
162
|
+
rendered it, but inline drops the row, and there each newline became a
|
|
163
|
+
real space inside the host -- measured at 3.5-4.5px past the text on
|
|
164
|
+
every inline link, so a link before a comma read "countries ,". */
|
|
125
165
|
render() {
|
|
126
166
|
if (this.disabled) {
|
|
127
|
-
return html
|
|
128
|
-
<span part="link" class="${this._classes}" style="${this._inlineStyle}">
|
|
129
|
-
<slot></slot>
|
|
130
|
-
</span>
|
|
131
|
-
`;
|
|
167
|
+
return html`<span part="link" class="${this._classes}" style="${this._inlineStyle}"><slot></slot>${this._arrow}</span>`;
|
|
132
168
|
}
|
|
133
169
|
|
|
134
|
-
return html
|
|
135
|
-
<a
|
|
170
|
+
return html`<a
|
|
136
171
|
part="link"
|
|
137
172
|
class="${this._classes}"
|
|
138
173
|
style="${this._inlineStyle}"
|
|
139
|
-
href="${this.href}"
|
|
140
|
-
target="${this.target}"
|
|
174
|
+
href="${this.href || nothing}"
|
|
175
|
+
target="${this.href ? this.target : nothing}"
|
|
176
|
+
aria-describedby="${this.description ? "description" : nothing}"
|
|
141
177
|
@click="${this._handleClick}"
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
`;
|
|
178
|
+
><slot></slot>${this._arrow}</a>${this.description
|
|
179
|
+
? html`<span id="description" hidden>${this.description}</span>`
|
|
180
|
+
: nothing}`;
|
|
146
181
|
}
|
|
147
182
|
|
|
148
183
|
_handleClick(e) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { html } from 'lit';
|
|
2
2
|
import './link.js';
|
|
3
|
+
import '../icons/arrow-right-icon.js';
|
|
4
|
+
import '../visually-hidden/visually-hidden.js';
|
|
3
5
|
|
|
4
6
|
export default {
|
|
5
7
|
title : 'Atoms/Link',
|
|
@@ -53,6 +55,27 @@ They are two booleans for one axis, which is one more than the axis needs.
|
|
|
53
55
|
depends on CSS order rather than on intent. Prefer whichever one differs from
|
|
54
56
|
your \`kind\`'s default and leave the other alone.
|
|
55
57
|
|
|
58
|
+
### arrow
|
|
59
|
+
A trailing arrow after the label, for a standalone call to action — "Read
|
|
60
|
+
more" on a post card, the links in a solution splitter. It is a modifier like
|
|
61
|
+
\`underline\`, so every variant, state and \`surface\` takes it, and the arrow
|
|
62
|
+
follows the link's ink in all of them.
|
|
63
|
+
|
|
64
|
+
\`\`\`html
|
|
65
|
+
<bd-link href="/blog/post" arrow>Read more</bd-link>
|
|
66
|
+
<bd-link href="/business" variant="bold" arrow>For Small Business</bd-link>
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
- The arrow is \`bd-arrow-icon\`, the only true shafted arrow, at the icon
|
|
70
|
+
scale's \`sm\` step (16px), with \`spacing/8\` before it.
|
|
71
|
+
- It is decorative and hidden from assistive technology. The label has to say
|
|
72
|
+
where the link goes; "Read more" alone on a page of cards needs more context.
|
|
73
|
+
- Slot any \`bd-*-icon\` into \`slot="icon"\` to swap the glyph. It is held to
|
|
74
|
+
the same 16px.
|
|
75
|
+
- The hover underline runs under the label and not under the arrow. Nothing
|
|
76
|
+
sets that: the link is a flex row, and a decoration is only drawn where there
|
|
77
|
+
is text.
|
|
78
|
+
|
|
56
79
|
### active
|
|
57
80
|
Marks the link as the current destination, for a nav or breadcrumb. It is
|
|
58
81
|
styling only — pair it with \`aria-current="page"\` on the element for the state
|
|
@@ -67,8 +90,8 @@ to be announced.
|
|
|
67
90
|
argTypes: {
|
|
68
91
|
href: {
|
|
69
92
|
control : 'text',
|
|
70
|
-
description: '
|
|
71
|
-
table : { type: { summary: 'string' }, defaultValue: { summary: '
|
|
93
|
+
description: 'Where the link goes. Without one it renders an <a> with no href, which is not a link — not focusable, not announced as one.',
|
|
94
|
+
table : { type: { summary: 'string' }, defaultValue: { summary: "''" }, category: 'Navigation' }
|
|
72
95
|
},
|
|
73
96
|
target: {
|
|
74
97
|
control : { type: 'select' },
|
|
@@ -102,6 +125,11 @@ to be announced.
|
|
|
102
125
|
description: 'Marks the link as the current one, e.g. in a nav. Adds `bd-link--active`.',
|
|
103
126
|
table : { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'State' }
|
|
104
127
|
},
|
|
128
|
+
arrow: {
|
|
129
|
+
control : 'boolean',
|
|
130
|
+
description: 'A trailing 16px arrow after the label, for a standalone call to action. Decorative: the label must say where the link goes. Slot a `bd-*-icon` into `slot="icon"` to swap it.',
|
|
131
|
+
table : { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'Appearance' }
|
|
132
|
+
},
|
|
105
133
|
inline: {
|
|
106
134
|
control : 'boolean',
|
|
107
135
|
description: 'The link is text inside a sentence: size, leading, tracking and family come from the sentence (the `--typography-link-default-*` role, all `inherit`). Without it the link renders 1rem whatever surrounds it. Suppresses `fontSize`.',
|
|
@@ -136,6 +164,7 @@ const defaultArgs = {
|
|
|
136
164
|
active : false,
|
|
137
165
|
disabled : false,
|
|
138
166
|
inline : false,
|
|
167
|
+
arrow : false,
|
|
139
168
|
fontSize : '',
|
|
140
169
|
color : ''
|
|
141
170
|
};
|
|
@@ -152,6 +181,7 @@ const render = (args) => html`
|
|
|
152
181
|
?active="${args.active}"
|
|
153
182
|
?inline="${args.inline}"
|
|
154
183
|
?disabled="${args.disabled}"
|
|
184
|
+
?arrow="${args.arrow}"
|
|
155
185
|
>Click this link</bd-link>
|
|
156
186
|
`;
|
|
157
187
|
|
|
@@ -252,12 +282,93 @@ export const NavigationLinks = {
|
|
|
252
282
|
parameters: { docs: { description: { story: 'Light kind links used as navigation items on a dark background.' } } }
|
|
253
283
|
};
|
|
254
284
|
|
|
285
|
+
// ─── Arrow ─────────────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
const ARROW_VARIANTS = ['primary', 'secondary', 'subtle', 'bold', 'danger'];
|
|
288
|
+
const arrowRow = 'display:flex; gap:var(--spacing-32); flex-wrap:wrap; align-items:center;';
|
|
289
|
+
const caption = text => html`
|
|
290
|
+
<p style="margin:var(--spacing-24) 0 var(--spacing-8);font:var(--typography-fontSize-xs)/1.4 var(--typography-fontFamily-sans);color:var(--color-text-muted)">${text}</p>
|
|
291
|
+
`;
|
|
292
|
+
|
|
293
|
+
export const Arrow = {
|
|
294
|
+
name : 'Arrow — every variant',
|
|
295
|
+
render: () => html`
|
|
296
|
+
${caption('arrow — the arrow follows the link\'s ink in every variant')}
|
|
297
|
+
<div style="${arrowRow}">
|
|
298
|
+
${ARROW_VARIANTS.map(v => html`<bd-link href="#" variant="${v}" arrow>Read more</bd-link>`)}
|
|
299
|
+
</div>
|
|
300
|
+
${caption('arrow with underline at rest — the underline runs under the label, not the arrow, exactly as on hover')}
|
|
301
|
+
<div style="${arrowRow}">
|
|
302
|
+
${ARROW_VARIANTS.map(v => html`<bd-link href="#" variant="${v}" arrow underline>Read more</bd-link>`)}
|
|
303
|
+
</div>
|
|
304
|
+
${caption('disabled — the arrow takes the disabled ink with the label')}
|
|
305
|
+
<div style="${arrowRow}">
|
|
306
|
+
${ARROW_VARIANTS.map(v => html`<bd-link href="#" variant="${v}" arrow disabled>Read more</bd-link>`)}
|
|
307
|
+
</div>
|
|
308
|
+
`,
|
|
309
|
+
parameters: { docs: { description: { story: '16px `bd-arrow-icon` after the label with `spacing/8` before it. Hover any link to see the underline stop at the label.' } } }
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export const ArrowOnDark = {
|
|
313
|
+
name : 'Arrow — on a dark surface',
|
|
314
|
+
render: () => html`
|
|
315
|
+
${[['surface-brand', 'var(--color-surface-brand)'], ['neutral-950', 'var(--color-neutral-950)']].map(([label, bg]) => html`
|
|
316
|
+
${caption(`${label} — surface="dark"`)}
|
|
317
|
+
<div style="background:${bg};padding:var(--spacing-24);border-radius:var(--radius-md);${arrowRow}">
|
|
318
|
+
${ARROW_VARIANTS.slice(0, 4).map(v => html`<bd-link href="#" variant="${v}" surface="dark" arrow>Explore enterprise AI</bd-link>`)}
|
|
319
|
+
</div>
|
|
320
|
+
`)}
|
|
321
|
+
`,
|
|
322
|
+
parameters: { docs: { description: { story: 'The same as Link arrow / dark in the Figma file: on a dark surface every variant collapses onto text/inverse, and the arrow goes with it.' } } }
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export const ArrowOwnIconAndInline = {
|
|
326
|
+
name : 'Arrow — swapped icon, and inline',
|
|
327
|
+
render: () => html`
|
|
328
|
+
${caption('slot="icon" swaps the glyph and holds it at 16px — here the chevron, for a link that steps into a list rather than leaving the page')}
|
|
329
|
+
<div style="${arrowRow}">
|
|
330
|
+
<bd-link href="#" arrow>See all products<bd-arrow-right-icon slot="icon"></bd-arrow-right-icon></bd-link>
|
|
331
|
+
</div>
|
|
332
|
+
${caption('inline — there is no row in a sentence, so a start margin keeps the arrow off the last letter')}
|
|
333
|
+
<p style="max-width:480px;margin:0;font:var(--typography-body-regular-fontWeight) var(--typography-body-regular-fontSize)/var(--typography-body-regular-lineHeight) var(--typography-fontFamily-sans);color:var(--color-text-primary)">
|
|
334
|
+
Bitdefender protects millions <bd-link href="#" inline arrow>across 170+ countries</bd-link>, combining deep security expertise with AI innovation.
|
|
335
|
+
</p>
|
|
336
|
+
`,
|
|
337
|
+
parameters: { docs: { description: { story: 'A swapped icon is held to the default arrow\'s step. Inline is supported, but the arrow is made for a standalone call to action, not a link inside a sentence.' } } }
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
export const ArrowOnTheHomepage = {
|
|
341
|
+
name : 'Arrow — as the homepage uses it',
|
|
342
|
+
render: () => html`
|
|
343
|
+
${caption('post card — primary')}
|
|
344
|
+
<div style="max-width:384px;padding:var(--spacing-24);border:var(--border-width-1) solid var(--color-border-container);border-radius:var(--radius-2xl);display:flex;flex-direction:column;gap:var(--spacing-16);">
|
|
345
|
+
<p style="margin:0;font:var(--typography-heading-h5-fontWeight) var(--typography-heading-h5-fontSize)/var(--typography-heading-h5-lineHeight) var(--typography-fontFamily-sans);color:var(--color-text-primary)">
|
|
346
|
+
They Wear Our Faces: How Scammers Are Using AI to Swindle American Families
|
|
347
|
+
</p>
|
|
348
|
+
<bd-link href="#" arrow>Read more<bd-visually-hidden> about scammers using AI</bd-visually-hidden></bd-link>
|
|
349
|
+
</div>
|
|
350
|
+
${caption('solution splitter — bold')}
|
|
351
|
+
<div style="display:grid;grid-template-columns:repeat(4, minmax(0, 1fr));gap:var(--spacing-24);max-width:1200px;">
|
|
352
|
+
${[['For Individuals', 'AI-powered security, privacy, and scam protection for everyday digital life.'],
|
|
353
|
+
['For Small Business', 'Effective protection for users, devices, and business operations.'],
|
|
354
|
+
['For Enterprise', 'Advanced cybersecurity for prevention, detection, and response.'],
|
|
355
|
+
['For MSPs', 'Security built for managed service providers and the customers they protect.']].map(([title, body]) => html`
|
|
356
|
+
<div style="display:flex;flex-direction:column;gap:var(--spacing-12);">
|
|
357
|
+
<bd-link href="#" variant="bold" arrow>${title}</bd-link>
|
|
358
|
+
<p style="margin:0;font:var(--typography-body-small-fontWeight) var(--typography-body-small-fontSize)/var(--typography-body-small-lineHeight) var(--typography-fontFamily-sans);color:var(--color-text-primary)">${body}</p>
|
|
359
|
+
</div>
|
|
360
|
+
`)}
|
|
361
|
+
</div>
|
|
362
|
+
`,
|
|
363
|
+
parameters: { docs: { description: { story: 'Read more on a post card carries visually hidden context, because a page of cards would otherwise read out several identical “Read more” links. The splitter links name their destination already.' } } }
|
|
364
|
+
};
|
|
365
|
+
|
|
255
366
|
export const Playground = {
|
|
256
367
|
name : '🛝 Playground',
|
|
257
368
|
args : { ...defaultArgs },
|
|
258
369
|
render,
|
|
259
370
|
parameters: {
|
|
260
371
|
controls: { disable: false },
|
|
261
|
-
docs : { description: { story: 'Interactive playground, and the only story wired to args. Every control here reaches the element: href, target, kind, underline, no-underline, active, disabled, inline, font-size and color.' } }
|
|
372
|
+
docs : { description: { story: 'Interactive playground, and the only story wired to args. Every control here reaches the element: href, target, kind, underline, no-underline, active, disabled, inline, arrow, font-size and color.' } }
|
|
262
373
|
}
|
|
263
374
|
};
|
|
@@ -76,17 +76,15 @@ export default css`
|
|
|
76
76
|
border-radius: var(--radius-full);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/* Number List
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
79
|
+
/* Number List. The number comes from list-item.js, on the marker's
|
|
80
|
+
data-number: a CSS counter cannot count across shadow roots, so each
|
|
81
|
+
item's counter only ever reached 1. */
|
|
84
82
|
.bd-list-item.number .bd-list-item-marker {
|
|
85
83
|
margin-top: 0; /* text — se aliniază natural pe linia de bază, nu geometric */
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
.bd-list-item.number .bd-list-item-marker::before {
|
|
89
|
-
content:
|
|
87
|
+
content: attr(data-number);
|
|
90
88
|
/* no role: a marker glyph, not running prose. The number or bullet in
|
|
91
89
|
front of a list item is a symbol sized and weighted to sit against the
|
|
92
90
|
text beside it, the same category typography rule 7 already covers for
|