@repobit/dex-system-design 0.25.1 → 0.25.3

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 CHANGED
@@ -3,6 +3,21 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.25.3](https://github.com/bitdefender/dex-core/compare/@repobit/dex-system-design@0.25.2...@repobit/dex-system-design@0.25.3) (2026-10-01)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **DEX-1014:** button component - new variant
11
+
12
+
13
+ ## [0.25.2](https://github.com/bitdefender/dex-core/compare/@repobit/dex-system-design@0.25.1...@repobit/dex-system-design@0.25.2) (2026-10-01)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **DEX-1014:** anchor nav component adjustments
18
+ * **DEX-1014:** anchor nav component adjustments
19
+
20
+
6
21
  ## [0.25.1](https://github.com/bitdefender/dex-core/compare/@repobit/dex-system-design@0.25.0...@repobit/dex-system-design@0.25.1) (2026-09-25)
7
22
 
8
23
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@repobit/dex-system-design",
3
- "version": "0.25.1",
3
+ "version": "0.25.3",
4
4
  "description": "Design system based on Web Components.",
5
5
  "author": "Iordache Matei Cezar <miordache@bitdefender.com>",
6
6
  "homepage": "https://github.com/bitdefender/dex-core#readme",
@@ -76,8 +76,8 @@
76
76
  "url": "https://github.com/bitdefender/dex-core/issues"
77
77
  },
78
78
  "dependencies": {
79
- "@repobit/dex-store": "1.6.38",
80
- "@repobit/dex-store-elements": "2.1.8",
79
+ "@repobit/dex-store": "1.6.40",
80
+ "@repobit/dex-store-elements": "2.1.10",
81
81
  "lit": "^3.3.2"
82
82
  },
83
83
  "devDependencies": {
@@ -94,5 +94,5 @@
94
94
  "volta": {
95
95
  "node": "24.14.0"
96
96
  },
97
- "gitHead": "252088eff23e9de9125bd60243283eba2f86a909"
97
+ "gitHead": "bf65491b1abdfd2228891d548bcf46bd59643551"
98
98
  }
@@ -93,7 +93,7 @@ function aiGuardianBody() {
93
93
  aria-label="Page sections"
94
94
  cta-label="Download BETA Today"
95
95
  cta-href="#download"
96
- sticky-top="65px"
96
+ sticky-top="76px"
97
97
  .items=${[
98
98
  { title: "Overview" },
99
99
  { title: "Compatibility" },
@@ -500,7 +500,7 @@ function skillScannerBody() {
500
500
  <div style="max-width: 1200px; margin: 0 auto;">
501
501
  <bd-anchor-nav
502
502
  aria-label="Skill Scanner sections"
503
- sticky-top="65px"
503
+ sticky-top="76px"
504
504
  .items=${[
505
505
  { title: "Install" },
506
506
  { title: "Overview" },
@@ -958,7 +958,7 @@ function vpnBody() {
958
958
  <div style="max-width: 1200px; margin: 0 auto;">
959
959
  <bd-anchor-nav
960
960
  aria-label="VPN for AI Agents sections"
961
- sticky-top="65px"
961
+ sticky-top="76px"
962
962
  .items=${[
963
963
  { title: "Overview" },
964
964
  { title: "Protection" },
@@ -1444,7 +1444,7 @@ Interactive demo: sticky top-level product nav (\`AI Guardian\` / \`Skill Scanne
1444
1444
  navigation — clicking a tab intercepts the anchor click and swaps local state instead
1445
1445
  of following the href (which has no route in Storybook and would 404).
1446
1446
 
1447
- Both/all anchor-nav bars use \`sticky-top="65px"\` so they pin themselves *below*
1447
+ Both/all anchor-nav bars use \`sticky-top="76px"\` so they pin themselves *below*
1448
1448
  the sticky page-switcher instead of overlapping it once scrolled. That value
1449
1449
  is an estimate — measure bd-page-switcher's real rendered height in DevTools
1450
1450
  and adjust if the bars still overlap or leave a gap.
@@ -7,6 +7,8 @@ export const anchorNavStyles = css`
7
7
  background: var(--color-neutral-0);
8
8
  --bd-accesibility-focus: var(--color-blue-500);
9
9
  --anchor-nav-max-width: 1290px;
10
+ border-top: var(--border-width-1) solid var(--color-neutral-100);
11
+ border-bottom: var(--border-width-1) solid var(--color-neutral-100);
10
12
  }
11
13
 
12
14
  :host(.is-sticky) {
@@ -28,8 +30,6 @@ export const anchorNavStyles = css`
28
30
  padding: var(--spacing-0);
29
31
  width: 100%;
30
32
  box-sizing: border-box;
31
- border-top: var(--border-width-1) solid var(--color-neutral-100);
32
- border-bottom: var(--border-width-1) solid var(--color-neutral-100);
33
33
  }
34
34
 
35
35
  .bd-anchor-nav__inner {
@@ -46,7 +46,6 @@ export const anchorNavStyles = css`
46
46
  box-sizing: border-box;
47
47
  }
48
48
 
49
-
50
49
  .anchor-link:focus-visible,
51
50
  .bd-anchor-nav__cta:focus-visible,
52
51
  .bd-anchor-nav__dropdown-toggle:focus-visible,
@@ -115,17 +114,17 @@ export const anchorNavStyles = css`
115
114
  text-decoration: none !important;
116
115
  }
117
116
 
118
-
119
- .anchor-indicator {
117
+ .anchor-indicator {
120
118
  position: absolute;
121
119
  left: 0;
122
- bottom: calc(-1 * var(--spacing-14));
120
+ top: 0;
123
121
  width: 0;
124
122
  height: var(--border-width-3);
125
123
  background-color: var(--color-blue-500);
126
124
  transform: translateX(0);
127
125
  transition:
128
126
  transform var(--transition-duration-slow) var(--transition-easing-ease-out),
127
+ top var(--transition-duration-slow) var(--transition-easing-ease-out),
129
128
  width var(--transition-duration-slow) var(--transition-easing-ease-out);
130
129
  pointer-events: none;
131
130
  }
@@ -34,10 +34,6 @@ class BdAnchorNav extends LitElement {
34
34
  ctaHref : { type: String, attribute: "cta-href" },
35
35
  items : { type: Array },
36
36
  maxWidth : { type: String, attribute: "max-width" },
37
- // How far from the viewport top this nav sticks once pinned — set this
38
- // to the rendered height of whatever sticky element sits above it (e.g.
39
- // a page-switcher), so the two don't stack on top of each other at
40
- // top:0. Accepts any CSS length, e.g. "64px", "4rem".
41
37
  stickyTop : { type: String, attribute: "sticky-top" }
42
38
  };
43
39
 
@@ -67,10 +63,8 @@ class BdAnchorNav extends LitElement {
67
63
  this._stickyPlaceholder = null;
68
64
  this._onStickyScroll = null;
69
65
  this._handleResize = this._handleResize.bind(this);
70
- // sliding underline indicator: false until the first successful
71
- // measurement + placement, so it doesn't visibly animate in from the
72
- // left edge on first paint.
73
66
  this._indicatorReady = false;
67
+ this._indicatorResizeObserver = null;
74
68
  }
75
69
 
76
70
  connectedCallback() {
@@ -93,6 +87,7 @@ class BdAnchorNav extends LitElement {
93
87
  document.removeEventListener("pointerdown", this._onDocumentPointerDown, true);
94
88
  document.removeEventListener("keydown", this._onKeydown, true);
95
89
  this._destroySticky();
90
+ this._indicatorResizeObserver?.disconnect();
96
91
  }
97
92
 
98
93
  updated(changedProperties) {
@@ -100,17 +95,11 @@ class BdAnchorNav extends LitElement {
100
95
 
101
96
  if (changedProperties.has("stickyTop")) {
102
97
  this.style.setProperty("--sticky-top", this.stickyTop || "0px");
103
- // the scroll-spy offset (how far to scroll past the sticky bar when
104
- // clicking a link) depends on this too, since the nav's own visible
105
- // top now sits lower than the viewport top while stuck.
106
98
  this._runScrollSpy();
107
99
  }
108
100
 
109
101
  if (changedProperties.has("activeId") || changedProperties.has("items")) {
110
- // wait a frame so the .active class has actually painted before
111
- // measuring its box — requestAnimationFrame is enough here, no need
112
- // for updateComplete since nothing else in render() depends on this.
113
- requestAnimationFrame(() => this._updateIndicator());
102
+ requestAnimationFrame(() => this._updateIndicator("updated"));
114
103
  }
115
104
 
116
105
  if (!changedProperties.has("activeId")) return;
@@ -128,13 +117,23 @@ class BdAnchorNav extends LitElement {
128
117
  }
129
118
  queueMicrotask(() => this._runScrollSpy());
130
119
  this._handleMobileFullWidth();
131
- requestAnimationFrame(() => this._updateIndicator());
120
+ requestAnimationFrame(() => this._updateIndicator("firstUpdated"));
121
+
122
+ const links = this.renderRoot?.querySelector(".anchor-links--desktop");
123
+ if (links && "ResizeObserver" in window) {
124
+ this._indicatorResizeObserver = new ResizeObserver(() => {
125
+ console.log("[anchor-nav] ResizeObserver fired", {
126
+ isSticky : this.classList.contains("is-sticky"),
127
+ navRect : this.getBoundingClientRect(),
128
+ linksRect: links.getBoundingClientRect()
129
+ });
130
+ this._updateIndicator("ResizeObserver");
131
+ });
132
+ this._indicatorResizeObserver.observe(links);
133
+ this._indicatorResizeObserver.observe(this);
134
+ }
132
135
  }
133
136
 
134
- // Positions the sliding underline under the currently active desktop
135
- // link. Reads offsetLeft/offsetWidth directly off the active bd-link —
136
- // as a flex item it's block-level regardless of its own :host display,
137
- // so these are reliable without any extra math against a bounding rect.
138
137
  _updateIndicator() {
139
138
  const indicator = this.renderRoot?.querySelector(".anchor-indicator");
140
139
  const activeLink = this.renderRoot?.querySelector(
@@ -144,14 +143,13 @@ class BdAnchorNav extends LitElement {
144
143
 
145
144
  const left = activeLink.offsetLeft;
146
145
  const width = activeLink.offsetWidth;
146
+ const top = activeLink.offsetTop + activeLink.offsetHeight + 14;
147
147
 
148
148
  if (!this._indicatorReady) {
149
- // first placement: skip the transition so it doesn't slide in from
150
- // the left edge on initial paint, then re-enable it next frame.
151
149
  indicator.style.transition = "none";
152
150
  indicator.style.transform = `translateX(${left}px)`;
151
+ indicator.style.top = `${top}px`;
153
152
  indicator.style.width = `${width}px`;
154
- // force a style flush before removing the transition override
155
153
  void indicator.offsetWidth;
156
154
  indicator.style.transition = "";
157
155
  this._indicatorReady = true;
@@ -159,12 +157,13 @@ class BdAnchorNav extends LitElement {
159
157
  }
160
158
 
161
159
  indicator.style.transform = `translateX(${left}px)`;
160
+ indicator.style.top = `${top}px`;
162
161
  indicator.style.width = `${width}px`;
163
162
  }
164
163
 
165
164
  _handleResize() {
166
165
  this._handleMobileFullWidth();
167
- this._updateIndicator();
166
+ this._updateIndicator("windowResize");
168
167
  }
169
168
 
170
169
  _handleMobileFullWidth() {
@@ -190,14 +189,16 @@ class BdAnchorNav extends LitElement {
190
189
  this.style.setProperty("--sticky-top", this.stickyTop || "0px");
191
190
 
192
191
  this._onStickyScroll = () => {
193
- const parent = this.parentElement;
192
+ const parent = this.parentElement;
194
193
  if (!parent) return;
195
194
 
196
195
  const parentRect = parent.getBoundingClientRect();
197
196
  const navHeight = this._stickyPlaceholder
198
197
  ? this._stickyPlaceholder.offsetHeight
199
198
  : this.offsetHeight;
200
- const stickyTopPx = parseLengthToPx(this.stickyTop) || 0;
199
+ const stickyTopPx = parseLengthToPx(
200
+ this.style.getPropertyValue("--sticky-top") || this.stickyTop
201
+ ) || 0;
201
202
 
202
203
  if (this._stickyOriginalTop === null) {
203
204
  const rect = this.getBoundingClientRect();
@@ -217,8 +218,8 @@ class BdAnchorNav extends LitElement {
217
218
  }
218
219
  this.classList.add("is-sticky");
219
220
  }
220
- this.style.setProperty("--sticky-left", `${parentRect.left}px`);
221
- this.style.setProperty("--sticky-width", `${parentRect.width}px`);
221
+ this.style.setProperty("--sticky-left", "0px");
222
+ this.style.setProperty("--sticky-width", `${document.documentElement.clientWidth}px`);
222
223
  } else {
223
224
  if (this.classList.contains("is-sticky")) {
224
225
  this.classList.remove("is-sticky");
@@ -232,6 +233,15 @@ class BdAnchorNav extends LitElement {
232
233
  }
233
234
  };
234
235
 
236
+ this._onStickyResize = () => {
237
+ this._stickyOriginalTop = null;
238
+ if (this.classList.contains("is-sticky")) {
239
+ this.style.setProperty("--sticky-left", "0px");
240
+ this.style.setProperty("--sticky-width", `${document.documentElement.clientWidth}px`);
241
+ }
242
+ this._onStickyScroll();
243
+ };
244
+
235
245
  this._onStickyResize = () => {
236
246
  this._stickyOriginalTop = null;
237
247
  if (this.classList.contains("is-sticky")) {
@@ -27,6 +27,11 @@ export default css`
27
27
  --border-button-background-primary-outline-hover: var(--color-blue-500);
28
28
  --button-background-primary-outline-focus: var(--color-blue-500);
29
29
  --bd-accesibility-focus: var(--color-blue-500);
30
+
31
+ --button-arrow-size: 1.1em;
32
+ --button-arrow-shift: 0.65em;
33
+ --button-arrow-inset: 0.65em;
34
+ --button-arrow-offset: -0.4em;
30
35
  }
31
36
 
32
37
  /* ============================================
@@ -103,6 +108,10 @@ export default css`
103
108
  box-sizing: border-box;
104
109
  }
105
110
 
111
+ .button__label {
112
+ display: inline-block;
113
+ }
114
+
106
115
  .button--primary {
107
116
  background-color: var(--button-background-primary);
108
117
  color: var(--color-neutral-0);
@@ -141,6 +150,62 @@ export default css`
141
150
  .button--danger:active {
142
151
  background-color: var(--button-background-danger-focus);
143
152
  }
153
+
154
+ /* ============================================
155
+ HOVER CU SĂGEATĂ (implicit pentru kind="danger")
156
+ Lățimea butonului rămâne fixă: textul se translatează
157
+ spre stânga, iar săgeata (poziționată absolut) apare în dreapta.
158
+ ============================================ */
159
+
160
+ .button--with-arrow {
161
+ position: relative;
162
+ overflow: hidden;
163
+ }
164
+
165
+ .button--with-arrow .button__label {
166
+ transition: transform var(--transition-duration-normal) var(--transition-easing-smooth);
167
+ }
168
+
169
+ .button--with-arrow .button__arrow {
170
+ position: absolute;
171
+ top: 50%;
172
+ right: var(--button-arrow-inset);
173
+ width: var(--button-arrow-size);
174
+ height: var(--button-arrow-size);
175
+ display: inline-flex;
176
+ opacity: 0;
177
+ transform: translate(var(--button-arrow-offset), -50%);
178
+ transition: opacity var(--transition-duration-normal) var(--transition-easing-smooth),
179
+ transform var(--transition-duration-normal) var(--transition-easing-smooth);
180
+ pointer-events: none;
181
+ }
182
+
183
+ .button--with-arrow .button__arrow svg {
184
+ width: 100%;
185
+ height: 100%;
186
+ display: block;
187
+ }
188
+
189
+ .button--with-arrow:hover .button__label,
190
+ .button--with-arrow:focus-visible .button__label {
191
+ transform: translateX(calc(var(--button-arrow-shift) * -1));
192
+ }
193
+
194
+ .button--with-arrow:hover .button__arrow,
195
+ .button--with-arrow:focus-visible .button__arrow {
196
+ opacity: 1;
197
+ transform: translate(0, -50%);
198
+ }
199
+
200
+ /* Pe touch nu există hover real — nu lăsăm săgeata „blocată” după tap */
201
+ @media (hover: none) {
202
+ .button--with-arrow:hover .button__label {
203
+ transform: none;
204
+ }
205
+ .button--with-arrow:hover .button__arrow {
206
+ opacity: 0;
207
+ }
208
+ }
144
209
 
145
210
  .button--outline {
146
211
  background-color: transparent;
@@ -208,41 +273,46 @@ export default css`
208
273
  font-weight: var(--typography-fontWeight-semibold);
209
274
  }
210
275
 
211
- .button > ::slotted(*) {
276
+ /* slot-ul nu mai e copil direct al .button (e în .button__label) */
277
+ .button ::slotted(*) {
212
278
  display: inline-block;
213
279
  width: 100%;
214
280
  text-align: center;
215
281
  }
216
282
 
217
283
  @media (prefers-reduced-motion: reduce) {
218
- .button, .bd-contact-btn {
284
+ .button,
285
+ .bd-contact-btn,
286
+ .button__label,
287
+ .button__arrow {
219
288
  transition: none !important;
220
289
  }
221
290
  }
222
- .button--white {
223
- background-color: var(--color-neutral-0);
224
- color: var(--color-blue-500);
225
- }
226
291
 
227
- .button--white:hover {
228
- background-color: var(--color-blue-50);
229
- }
292
+ .button--white {
293
+ background-color: var(--color-neutral-0);
294
+ color: var(--color-blue-500);
295
+ }
230
296
 
231
- .button--white:active {
232
- background-color: var(--color-blue-100);
233
- }
297
+ .button--white:hover {
298
+ background-color: var(--color-blue-50);
299
+ }
300
+
301
+ .button--white:active {
302
+ background-color: var(--color-blue-100);
303
+ }
234
304
 
235
- .button--outline-white {
236
- background-color: transparent;
237
- box-shadow: inset 0 0 0 1.5px #ffffff8c;
238
- color: var(--color-neutral-0);
239
- }
305
+ .button--outline-white {
306
+ background-color: transparent;
307
+ box-shadow: inset 0 0 0 1.5px #ffffff8c;
308
+ color: var(--color-neutral-0);
309
+ }
240
310
 
241
- .button--outline-white:hover {
242
- background-color: rgba(255, 255, 255, 0.1);
243
- }
311
+ .button--outline-white:hover {
312
+ background-color: rgba(255, 255, 255, 0.1);
313
+ }
244
314
 
245
- .button--outline-white:active {
246
- background-color: rgba(255, 255, 255, 0.2);
247
- }
315
+ .button--outline-white:active {
316
+ background-color: rgba(255, 255, 255, 0.2);
317
+ }
248
318
  `;
@@ -2,6 +2,24 @@ import { LitElement, html } from "lit";
2
2
  import { tokens } from "../../tokens/tokens.js";
3
3
  import buttonsCSS from "./button.css.js";
4
4
 
5
+ /**
6
+ * Săgeata afișată pe hover / focus pentru butoanele cu kind="danger".
7
+ * Folosește currentColor, deci preia culoarea textului butonului.
8
+ */
9
+ const arrowIcon = html`
10
+ <span class="button__arrow" aria-hidden="true">
11
+ <svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
12
+ <path
13
+ d="M2.5 8h11M9 3.5 13.5 8 9 12.5"
14
+ stroke="currentColor"
15
+ stroke-width="1.5"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ />
19
+ </svg>
20
+ </span>
21
+ `;
22
+
5
23
  class Button extends LitElement {
6
24
  static properties = {
7
25
  label : { type: String },
@@ -13,7 +31,8 @@ class Button extends LitElement {
13
31
  strong : { type: Boolean },
14
32
  fontSize : { type: String, attribute: "font-size" },
15
33
  fontWeight : { type: String, attribute: "font-weight" },
16
- align : { type: String }
34
+ align : { type: String },
35
+ noArrow : { type: Boolean, attribute: "no-arrow" }
17
36
  };
18
37
 
19
38
  constructor() {
@@ -24,6 +43,7 @@ class Button extends LitElement {
24
43
  this.fontSize = "";
25
44
  this.fontWeight = "";
26
45
  this.align = "left";
46
+ this.noArrow = false;
27
47
  }
28
48
 
29
49
  _onFocusin(e) {
@@ -87,20 +107,30 @@ class Button extends LitElement {
87
107
  return this.strong ? "button--bold" : "";
88
108
  }
89
109
 
110
+ /** Săgeata pe hover e activă implicit pentru danger; se poate dezactiva cu no-arrow. */
111
+ get hasArrow() {
112
+ return this.kind === "danger" && !this.noArrow;
113
+ }
114
+
115
+ get arrowClass() {
116
+ return this.hasArrow ? "button--with-arrow" : "";
117
+ }
118
+
90
119
  render() {
91
120
  return html`
92
121
  <button
93
122
  @click=${this._handleClick}
94
123
  @focusin=${this._onFocusin}
95
124
  @focusout=${this._onFocusout}
96
- class="button ${this.kindClass} ${this.sizeClass} ${this.fullWidthClass} ${this.strongClass}"
125
+ class="button ${this.kindClass} ${this.sizeClass} ${this.fullWidthClass} ${this.strongClass} ${this.arrowClass}"
97
126
  aria-label="${this.label}"
98
127
  style="
99
128
  ${this.fontSize ? `font-size: ${this.fontSize};` : ""}
100
129
  ${this.fontWeight ? `font-weight: ${this.fontWeight};` : ""}
101
130
  "
102
131
  >
103
- <slot></slot>
132
+ <span class="button__label"><slot></slot></span>
133
+ ${this.hasArrow ? arrowIcon : ""}
104
134
  </button>
105
135
  `;
106
136
  }
@@ -119,7 +149,8 @@ class ButtonLink extends LitElement {
119
149
  fontWeight : { type: String, attribute: "font-weight" },
120
150
  buyLink : { type: Boolean },
121
151
  shadow : { type: Boolean, reflect: true },
122
- align : { type: String }
152
+ align : { type: String },
153
+ noArrow : { type: Boolean, attribute: "no-arrow" }
123
154
  };
124
155
 
125
156
  constructor() {
@@ -135,6 +166,7 @@ class ButtonLink extends LitElement {
135
166
  this.buyLink = false;
136
167
  this.shadow = true;
137
168
  this.align = "left";
169
+ this.noArrow = false;
138
170
  }
139
171
 
140
172
  _onFocusin(e) {
@@ -194,11 +226,19 @@ class ButtonLink extends LitElement {
194
226
  return this.strong ? "button--bold" : "";
195
227
  }
196
228
 
229
+ get hasArrow() {
230
+ return this.kind === "danger" && !this.noArrow;
231
+ }
232
+
233
+ get arrowClass() {
234
+ return this.hasArrow ? "button--with-arrow" : "";
235
+ }
236
+
197
237
  render() {
198
238
  return html`
199
239
  <a
200
240
  href="${this.href}"
201
- class="button ${this.kindClass} ${this.sizeClass} ${this.fullWidthClass} ${this.strongClass}"
241
+ class="button ${this.kindClass} ${this.sizeClass} ${this.fullWidthClass} ${this.strongClass} ${this.arrowClass}"
202
242
  aria-label="${this.label}"
203
243
  @click=${this._handleClick}
204
244
  @focusin=${this._onFocusin}
@@ -210,7 +250,8 @@ class ButtonLink extends LitElement {
210
250
  ?data-store-render="${this.buyLink}"
211
251
  ?data-store-buy-link="${this.buyLink}"
212
252
  >
213
- <slot>${this.label}</slot>
253
+ <span class="button__label"><slot>${this.label}</slot></span>
254
+ ${this.hasArrow ? arrowIcon : ""}
214
255
  </a>
215
256
  `;
216
257
  }
@@ -17,6 +17,7 @@ Three button components are available:
17
17
  ### Usage
18
18
  \`\`\`html
19
19
  <bd-button kind="primary" size="md">Click me</bd-button>
20
+ <bd-button kind="danger" size="md">Activez votre protection</bd-button>
20
21
  <bd-button-link kind="danger" size="lg" href="/buy">Buy now</bd-button-link>
21
22
  <bd-contact-button label="Contact us" href="/contact"></bd-contact-button>
22
23
  \`\`\`
@@ -28,7 +29,22 @@ Three button components are available:
28
29
  | \`primary-outline\` | \`button--primary--outline\` |
29
30
  | \`outline\` | \`button--outline\` |
30
31
  | \`secondary\` | \`button--secondary\` |
31
- | \`danger\` | \`button--danger\` |
32
+ | \`danger\` | \`button--danger\` + \`button--with-arrow\` |
33
+ | \`white\` | \`button--white\` |
34
+ | \`outline-white\` | \`button--outline-white\` |
35
+
36
+ ### Danger hover arrow
37
+ Every \`kind="danger"\` button (both \`bd-button\` and \`bd-button-link\`) has an arrow hover effect by default:
38
+ on hover (and on keyboard \`:focus-visible\`) the label slides to the left and an arrow fades in on the right.
39
+ The button width does not change.
40
+
41
+ To disable it on a specific button, add the \`no-arrow\` attribute:
42
+ \`\`\`html
43
+ <bd-button kind="danger" no-arrow>No arrow</bd-button>
44
+ \`\`\`
45
+
46
+ The effect can be tuned from outside via CSS custom properties:
47
+ \`--button-arrow-size\`, \`--button-arrow-shift\`, \`--button-arrow-inset\`, \`--button-arrow-offset\`.
32
48
 
33
49
  ### Size Values
34
50
  | Size | Class |
@@ -58,8 +74,8 @@ Three button components are available:
58
74
  },
59
75
  kind: {
60
76
  control : { type: 'select' },
61
- options : ['primary', 'primary-outline', 'outline', 'secondary', 'danger'],
62
- description: 'Visual style variant of the button',
77
+ options : ['primary', 'primary-outline', 'outline', 'secondary', 'danger', 'white', 'outline-white'],
78
+ description: 'Visual style variant of the button. `danger` includes the hover arrow by default.',
63
79
  table : { type: { summary: 'string' }, defaultValue: { summary: 'primary' }, category: 'Appearance' }
64
80
  },
65
81
  size: {
@@ -68,6 +84,11 @@ Three button components are available:
68
84
  description: 'Size of the button',
69
85
  table : { type: { summary: "'sm' | 'md' | 'lg'" }, defaultValue: { summary: 'md' }, category: 'Appearance' }
70
86
  },
87
+ noArrow: {
88
+ control : 'boolean',
89
+ description: '(kind="danger" only) Disables the hover arrow. Attribute: `no-arrow`',
90
+ table : { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'Appearance' }
91
+ },
71
92
  fullWidth: {
72
93
  control : 'boolean',
73
94
  description: 'When true, button stretches to full container width',
@@ -116,6 +137,7 @@ const defaultArgs = {
116
137
  label : 'Buy now',
117
138
  kind : 'primary',
118
139
  size : 'md',
140
+ noArrow : false,
119
141
  fullWidth : false,
120
142
  strong : false,
121
143
  align : 'left',
@@ -142,7 +164,40 @@ export const ButtonAllKinds = {
142
164
  <bd-button kind="danger" size="md">Danger</bd-button>
143
165
  </div>
144
166
  `,
145
- parameters: { docs: { description: { story: 'All five `kind` variants side by side.' } } }
167
+ parameters: { docs: { description: { story: 'All five `kind` variants side by side. Hover the danger button to see the arrow.' } } }
168
+ };
169
+
170
+ export const ButtonDangerArrow = {
171
+ name : 'Button — Danger (Hover Arrow)',
172
+ render: () => html`
173
+ <div style="display:flex; flex-direction:column; gap:16px; align-items:flex-start;">
174
+ <p style="font-size:14px; margin:0;">Hover (or Tab to focus) any button to see the arrow.</p>
175
+ <bd-button kind="danger" size="md" strong>Activez votre protection</bd-button>
176
+ <div style="display:flex; gap:12px; align-items:center; flex-wrap:wrap;">
177
+ <bd-button kind="danger" size="sm" strong>Small</bd-button>
178
+ <bd-button kind="danger" size="md" strong>Medium</bd-button>
179
+ <bd-button kind="danger" size="lg" strong>Large</bd-button>
180
+ </div>
181
+ </div>
182
+ `,
183
+ parameters: {
184
+ docs: {
185
+ description: {
186
+ story: 'Default behaviour for every `kind="danger"` button: on hover / `:focus-visible` the label slides left and an arrow appears on the right, without changing the button width.'
187
+ }
188
+ }
189
+ }
190
+ };
191
+
192
+ export const ButtonDangerNoArrow = {
193
+ name : 'Button — Danger without Arrow',
194
+ render: () => html`
195
+ <div style="display:flex; gap:12px; align-items:center; flex-wrap:wrap;">
196
+ <bd-button kind="danger" size="md" strong>With arrow (default)</bd-button>
197
+ <bd-button kind="danger" size="md" strong no-arrow>Without arrow</bd-button>
198
+ </div>
199
+ `,
200
+ parameters: { docs: { description: { story: 'Use the `no-arrow` attribute to opt out of the hover arrow on a specific danger button.' } } }
146
201
  };
147
202
 
148
203
  export const ButtonAllSizes = {
@@ -160,11 +215,12 @@ export const ButtonAllSizes = {
160
215
  export const ButtonFullWidth = {
161
216
  name : 'Button — Full Width',
162
217
  render: () => html`
163
- <div style="width:400px;">
218
+ <div style="width:400px; display:flex; flex-direction:column; gap:12px;">
164
219
  <bd-button kind="primary" size="md" .fullWidth="${true}">Full Width Button</bd-button>
220
+ <bd-button kind="danger" size="md" strong .fullWidth="${true}">Full Width Danger</bd-button>
165
221
  </div>
166
222
  `,
167
- parameters: { docs: { description: { story: '`fullWidth` set to true — applies `max--width` class.' } } }
223
+ parameters: { docs: { description: { story: '`fullWidth` set to true — applies `max--width` class. The danger arrow also works at full width.' } } }
168
224
  };
169
225
 
170
226
  export const ButtonStrong = {
@@ -223,7 +279,7 @@ export const ButtonCustomTypography = {
223
279
  Buy now
224
280
  </bd-button>
225
281
  `,
226
- parameters: { docs: { description: { story: 'Custom `font-size` and `font-weight` applied via inline styles.' } } }
282
+ parameters: { docs: { description: { story: 'Custom `font-size` and `font-weight` applied via inline styles. The arrow scales with the font size.' } } }
227
283
  };
228
284
 
229
285
  export const ButtonKeyboard = {
@@ -268,7 +324,18 @@ export const ButtonLinkAllSizes = {
268
324
  <bd-button-link kind="danger" size="lg" href="#">Large</bd-button-link>
269
325
  </div>
270
326
  `,
271
- parameters: { docs: { description: { story: 'All three sizes for `bd-button-link`.' } } }
327
+ parameters: { docs: { description: { story: 'All three sizes for `bd-button-link` (danger, with hover arrow).' } } }
328
+ };
329
+
330
+ export const ButtonLinkDangerArrow = {
331
+ name : 'ButtonLink — Danger (Hover Arrow)',
332
+ render: () => html`
333
+ <div style="display:flex; gap:12px; align-items:center; flex-wrap:wrap;">
334
+ <bd-button-link kind="danger" size="md" href="#" strong>Activez votre protection</bd-button-link>
335
+ <bd-button-link kind="danger" size="md" href="#" strong no-arrow>Without arrow</bd-button-link>
336
+ </div>
337
+ `,
338
+ parameters: { docs: { description: { story: 'The hover arrow is also on by default for `bd-button-link` with `kind="danger"`; `no-arrow` disables it.' } } }
272
339
  };
273
340
 
274
341
  export const ButtonLinkBuyLink = {
@@ -397,15 +464,17 @@ export const Playground = {
397
464
  size="${args.size}"
398
465
  label="${args.label}"
399
466
  align="${args.align}"
400
- ?full-width="${args.fullWidth}"
467
+ ?fullwidth="${args.fullWidth}"
401
468
  ?strong="${args.strong}"
469
+ ?no-arrow="${args.noArrow}"
402
470
  font-size="${args.fontSize}"
403
471
  font-weight="${args.fontWeight}"
404
472
  >${args.label}</bd-button>
405
473
  </div>
406
474
  `,
407
- parameters: { docs: { description: { story: 'Interactive playground for `bd-button`. Use Controls to adjust all props including alignment.' } } }
475
+ parameters: { docs: { description: { story: 'Interactive playground for `bd-button`. Use Controls to adjust all props including alignment and the danger hover arrow.' } } }
408
476
  };
477
+
409
478
  // ─── White / Outline-White (on dark background) ───────────────────────────
410
479
 
411
480
  export const ButtonWhiteOnDark = {