@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.
Files changed (136) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +43 -12
  3. package/package.json +6 -4
  4. package/schemas/component-manifest.json +207 -49
  5. package/schemas/manifest-omissions.json +2 -0
  6. package/src/components/accordion/accordion.css.js +12 -1
  7. package/src/components/agent-endpoints/agent-endpoints.js +1 -1
  8. package/src/components/alert/alert.css.js +22 -1
  9. package/src/components/anchor/anchor-nav.css.js +6 -1
  10. package/src/components/annotation/annotation.css.js +6 -1
  11. package/src/components/architecture.stories.js +6 -4
  12. package/src/components/avatar/avatar.js +37 -11
  13. package/src/components/avatar/avatar.stories.js +10 -6
  14. package/src/components/awards/awards-flex.css.js +31 -129
  15. package/src/components/awards/awards-flex.js +66 -116
  16. package/src/components/awards/awards-flex.stories.js +8 -6
  17. package/src/components/back/back.css.js +6 -1
  18. package/src/components/back/back.js +24 -12
  19. package/src/components/back/back.stories.js +3 -3
  20. package/src/components/badge/badge.js +6 -3
  21. package/src/components/badge/badge.stories.js +13 -9
  22. package/src/components/bento-grid/bento-grid.js +1 -1
  23. package/src/components/blockquote/blockquote.css.js +12 -3
  24. package/src/components/blockquote/blockquote.js +12 -5
  25. package/src/components/blockquote/blockquote.stories.js +6 -4
  26. package/src/components/button/button.css.js +46 -25
  27. package/src/components/button/button.js +44 -43
  28. package/src/components/button/button.stories.js +2 -2
  29. package/src/components/caption/caption.css.js +10 -0
  30. package/src/components/caption/caption.stories.js +4 -4
  31. package/src/components/carousel/carousel.css.js +19 -116
  32. package/src/components/carousel/carousel.js +70 -74
  33. package/src/components/carousel/carousel.stories.js +9 -8
  34. package/src/components/checkbox/checkbox.css.js +6 -1
  35. package/src/components/code/code.css.js +6 -1
  36. package/src/components/code/code.js +48 -7
  37. package/src/components/code/code.stories.js +7 -4
  38. package/src/components/divider/divider-horizontal.js +11 -3
  39. package/src/components/divider/divider-vertical.js +12 -5
  40. package/src/components/divider/divider.css.js +4 -1
  41. package/src/components/divider/divider.stories.js +6 -6
  42. package/src/components/dropdown/dropdown.js +67 -13
  43. package/src/components/dropdown/dropdown.stories.js +15 -14
  44. package/src/components/fieldset/fieldset.stories.js +41 -16
  45. package/src/components/footer/footer-nav-menu.css.js +6 -1
  46. package/src/components/footer/footer.css.js +6 -1
  47. package/src/components/header/header-code.js +1 -1
  48. package/src/components/header/header.css.js +9 -4
  49. package/src/components/heading/heading.stories.js +64 -31
  50. package/src/components/highlight/highlight.js +74 -34
  51. package/src/components/highlight/highlight.stories.js +5 -4
  52. package/src/components/icons/icon-element.js +49 -6
  53. package/src/components/icons/icons.stories.js +7 -0
  54. package/src/components/image/image.css.js +9 -98
  55. package/src/components/image/image.js +7 -2
  56. package/src/components/input/custom-form.stories.js +14 -19
  57. package/src/components/input/input-clipboard.css.js +6 -1
  58. package/src/components/input/input-clipboard.js +56 -8
  59. package/src/components/input/input.js +19 -2
  60. package/src/components/label/label.js +87 -1
  61. package/src/components/label/label.stories.js +21 -12
  62. package/src/components/link/link.css.js +52 -3
  63. package/src/components/link/link.js +52 -17
  64. package/src/components/link/link.stories.js +114 -3
  65. package/src/components/list/list-item/list-item.css.js +4 -6
  66. package/src/components/list/list-item/list-item.js +53 -5
  67. package/src/components/list/list-item/list-item.stories.js +22 -0
  68. package/src/components/list/list.js +27 -0
  69. package/src/components/modal/modal.css.js +6 -1
  70. package/src/components/pagination-dots/pagination-dots.css.js +161 -0
  71. package/src/components/pagination-dots/pagination-dots.js +202 -0
  72. package/src/components/pagination-dots/pagination-dots.stories.js +171 -0
  73. package/src/components/paragraph/paragraph.css.js +37 -1
  74. package/src/components/paragraph/paragraph.js +19 -2
  75. package/src/components/paragraph/paragraph.stories.js +40 -6
  76. package/src/components/picture/picture.css.js +8 -67
  77. package/src/components/picture/picture.js +15 -8
  78. package/src/components/pricing-cards/pricing-card.css.js +12 -2
  79. package/src/components/pricing-cards/pricing-card.js +4 -4
  80. package/src/components/progress/progress.js +24 -4
  81. package/src/components/progress/progress.stories.js +8 -6
  82. package/src/components/radio/radio.css.js +6 -1
  83. package/src/components/radio/radio.js +48 -3
  84. package/src/components/radio/radio.stories.js +1 -1
  85. package/src/components/shared/form-field.css.js +30 -4
  86. package/src/components/shared/token-scales.js +16 -0
  87. package/src/components/spinner/spinner.css.js +31 -4
  88. package/src/components/spinner/spinner.js +26 -4
  89. package/src/components/spinner/spinner.stories.js +10 -7
  90. package/src/components/switch/switch.css.js +21 -3
  91. package/src/components/switch/switch.js +8 -1
  92. package/src/components/switch/switch.stories.js +2 -1
  93. package/src/components/tab-list/tab-list.css.js +6 -1
  94. package/src/components/terminal/terminal-cursor.js +11 -0
  95. package/src/components/terminal/terminal-message.js +1 -1
  96. package/src/components/termsOfUse/terms.css.js +6 -1
  97. package/src/components/textarea/textarea.stories.js +23 -0
  98. package/src/components/tooltip/tooltip.css.js +24 -1
  99. package/src/components/tooltip/tooltip.js +135 -5
  100. package/src/components/tooltip/tooltip.stories.js +20 -14
  101. package/src/components/trust-panel/trust-panel.js +2 -2
  102. package/src/components/video/video.js +38 -3
  103. package/src/components/video/video.stories.js +10 -5
  104. package/src/components/video-player/video-player.css.js +6 -1
  105. package/src/tokens/elevation.stories.js +2 -3
  106. package/src/tokens/focus.stories.js +81 -15
  107. package/src/tokens/motion.stories.js +0 -6
  108. package/src/tokens/tokens.css +174 -97
  109. package/src/tokens/tokens.js +174 -97
  110. package/src/tokens/vertical-rhythm.stories.js +1 -1
  111. package/src/stories/Button.js +0 -20
  112. package/src/stories/Button.stories.js +0 -46
  113. package/src/stories/Configure.mdx +0 -364
  114. package/src/stories/Header.js +0 -45
  115. package/src/stories/Header.stories.js +0 -24
  116. package/src/stories/Page.js +0 -62
  117. package/src/stories/Page.stories.js +0 -20
  118. package/src/stories/assets/accessibility.png +0 -0
  119. package/src/stories/assets/accessibility.svg +0 -1
  120. package/src/stories/assets/addon-library.png +0 -0
  121. package/src/stories/assets/assets.png +0 -0
  122. package/src/stories/assets/avif-test-image.avif +0 -0
  123. package/src/stories/assets/context.png +0 -0
  124. package/src/stories/assets/discord.svg +0 -1
  125. package/src/stories/assets/docs.png +0 -0
  126. package/src/stories/assets/figma-plugin.png +0 -0
  127. package/src/stories/assets/github.svg +0 -1
  128. package/src/stories/assets/share.png +0 -0
  129. package/src/stories/assets/styling.png +0 -0
  130. package/src/stories/assets/testing.png +0 -0
  131. package/src/stories/assets/theming.png +0 -0
  132. package/src/stories/assets/tutorials.svg +0 -1
  133. package/src/stories/assets/youtube.svg +0 -1
  134. package/src/stories/button.css +0 -30
  135. package/src/stories/header.css +0 -32
  136. 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 for=${this.for || nothing}>
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\` | \`small\`, \`default\`, \`large\` | \`default\` |
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 not cross the shadow boundary
29
- The \`for\` attribute is applied to a \`<label>\` inside this element's shadow
30
- root, and a \`for\` reference can only resolve within the same tree. **It will
31
- not associate with an \`<input id="…">\` in your page.**
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
- Until that is addressed, associate the control by nesting or by
34
- \`aria-labelledby\`:
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 id="email-label">Email address</bd-label>
38
- <input aria-labelledby="email-label">
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 : ['small', 'default', 'large'],
62
+ options : ['sm', 'md', 'lg'],
54
63
  description: 'Type size of the label.',
55
- table : { type: { summary: 'string' }, defaultValue: { summary: "default" }, category: 'Appearance' }
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 labelled control. Note it cannot cross the shadow boundary — see the notes.',
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-default-fontWeight);
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
- outline: none;
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
- this.href = "#";
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
- <slot></slot>
144
- </a>
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: 'The anchor href URL',
71
- table : { type: { summary: 'string' }, defaultValue: { summary: '#' }, category: 'Navigation' }
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
- .bd-list-item.number {
81
- counter-increment: bd-list-counter;
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: counter(bd-list-counter) ".";
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