@repobit/dex-system-design 0.23.15 → 0.23.16
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 +7 -0
- package/package.json +2 -2
- package/src/components/Button/Button.js +60 -32
- package/src/components/Button/button.css.js +59 -75
- package/src/components/anchor/anchor-nav.css.js +3 -1
- package/src/components/anchor/anchor-nav.js +88 -119
- package/src/components/anchor/anchor.stories.js +190 -83
- package/src/components/badge/badge.css.js +15 -38
- package/src/components/badge/badge.js +15 -35
- package/src/components/compare/compare.stories.js +6 -3
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import { LitElement, html, nothing } from "lit";
|
|
1
|
+
import { LitElement, html, nothing } from "lit";
|
|
2
2
|
import { tokens } from "../../tokens/tokens.js";
|
|
3
3
|
import { anchorNavStyles } from "./anchor-nav.css.js";
|
|
4
4
|
|
|
5
|
-
/** Parse token length from computed style (e.g. `8rem` → px). */
|
|
6
5
|
function parseLengthToPx(value) {
|
|
7
6
|
const s = (value || "").trim();
|
|
8
7
|
if (!s) return 0;
|
|
9
8
|
if (s.endsWith("px")) return parseFloat(s);
|
|
10
9
|
if (s.endsWith("rem")) {
|
|
11
|
-
const root =
|
|
12
|
-
parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
10
|
+
const root = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
13
11
|
return parseFloat(s) * root;
|
|
14
12
|
}
|
|
15
13
|
return parseFloat(s) || 0;
|
|
@@ -20,45 +18,41 @@ class BdAnchorNavItem extends HTMLElement {
|
|
|
20
18
|
super();
|
|
21
19
|
this.attachShadow({ mode: "open" });
|
|
22
20
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
this.render();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
render() {
|
|
29
|
-
this.shadowRoot.innerHTML = `<slot></slot>`;
|
|
30
|
-
}
|
|
21
|
+
connectedCallback() { this.render(); }
|
|
22
|
+
render() { this.shadowRoot.innerHTML = `<slot></slot>`; }
|
|
31
23
|
}
|
|
32
24
|
|
|
33
25
|
class BdAnchorNav extends LitElement {
|
|
34
26
|
static properties = {
|
|
35
27
|
activeId : { type: String, state: true },
|
|
36
|
-
/** Landmark label for the outer `<nav>` (screen readers). */
|
|
37
28
|
navLabel : { type: String, attribute: "aria-label" },
|
|
38
|
-
/** Mobile dropdown open state */
|
|
39
29
|
_dropdownOpen : { type: Boolean, state: true },
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
_mobileCtaRevealed: { type: Boolean, state: true },
|
|
31
|
+
ctaLabel : { type: String, attribute: "cta-label" },
|
|
32
|
+
ctaHref : { type: String, attribute: "cta-href" },
|
|
33
|
+
items : { type: Array }
|
|
42
34
|
};
|
|
43
35
|
|
|
44
36
|
static styles = [tokens, anchorNavStyles];
|
|
45
37
|
|
|
46
38
|
constructor() {
|
|
47
39
|
super();
|
|
48
|
-
this.activeId
|
|
49
|
-
this.navLabel
|
|
50
|
-
this._dropdownOpen
|
|
51
|
-
this._mobileCtaRevealed
|
|
52
|
-
this.
|
|
40
|
+
this.activeId = "";
|
|
41
|
+
this.navLabel = "Section navigation";
|
|
42
|
+
this._dropdownOpen = false;
|
|
43
|
+
this._mobileCtaRevealed = false;
|
|
44
|
+
this.ctaLabel = "Buy now";
|
|
45
|
+
this.ctaHref = "";
|
|
46
|
+
this.items = null;
|
|
47
|
+
this._panelId = `bd-anchor-nav-panel-${Math.random().toString(36)
|
|
53
48
|
.slice(2, 11)}`;
|
|
54
|
-
this._onScrollSpy
|
|
55
|
-
this._onDocumentPointerDown
|
|
56
|
-
this._onKeydown
|
|
57
|
-
this._scrollPending
|
|
58
|
-
/** While set, scrollspy must not overwrite activeId during smooth scroll to a clicked section */
|
|
49
|
+
this._onScrollSpy = this._onScrollSpy.bind(this);
|
|
50
|
+
this._onDocumentPointerDown = this._onDocumentPointerDown.bind(this);
|
|
51
|
+
this._onKeydown = this._onKeydown.bind(this);
|
|
52
|
+
this._scrollPending = false;
|
|
59
53
|
this._programmaticScrollTargetId = null;
|
|
60
|
-
this._unlockScrollSpyTimer
|
|
61
|
-
this._scrollEndUnlockHandler
|
|
54
|
+
this._unlockScrollSpyTimer = null;
|
|
55
|
+
this._scrollEndUnlockHandler = null;
|
|
62
56
|
}
|
|
63
57
|
|
|
64
58
|
connectedCallback() {
|
|
@@ -90,7 +84,6 @@ class BdAnchorNav extends LitElement {
|
|
|
90
84
|
}
|
|
91
85
|
}
|
|
92
86
|
|
|
93
|
-
/** After in-nav navigation, ignore scrollspy until scroll finishes (avoids flashing 2→3→4). */
|
|
94
87
|
_scheduleScrollSpyUnlock() {
|
|
95
88
|
this._cancelScrollSpyUnlock();
|
|
96
89
|
const onScrollEnd = () => {
|
|
@@ -99,7 +92,7 @@ class BdAnchorNav extends LitElement {
|
|
|
99
92
|
this._unlockScrollSpyTimer = null;
|
|
100
93
|
}
|
|
101
94
|
window.removeEventListener("scrollend", onScrollEnd);
|
|
102
|
-
this._scrollEndUnlockHandler
|
|
95
|
+
this._scrollEndUnlockHandler = null;
|
|
103
96
|
this._programmaticScrollTargetId = null;
|
|
104
97
|
this._runScrollSpy();
|
|
105
98
|
};
|
|
@@ -142,48 +135,58 @@ class BdAnchorNav extends LitElement {
|
|
|
142
135
|
});
|
|
143
136
|
}
|
|
144
137
|
|
|
138
|
+
_resolvedItems() {
|
|
139
|
+
if (this.items && this.items.length) {
|
|
140
|
+
return this.items.map((item, i) => ({
|
|
141
|
+
title: item.title,
|
|
142
|
+
href : item.href || `#anchor-${i + 1}-section`,
|
|
143
|
+
id : `anchor-${i + 1}`
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
return Array.from(this.children)
|
|
147
|
+
.filter((el) => el.tagName === "BD-ANCHOR-NAV-ITEM")
|
|
148
|
+
.map((el, i) => ({
|
|
149
|
+
title: el.getAttribute("title"),
|
|
150
|
+
href : el.getAttribute("href") || `#anchor-${i + 1}-section`,
|
|
151
|
+
id : `anchor-${i + 1}`
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
|
|
145
155
|
_runScrollSpy() {
|
|
146
|
-
const
|
|
147
|
-
|
|
156
|
+
const resolved = this._resolvedItems();
|
|
157
|
+
const count = resolved.length;
|
|
158
|
+
let next = this.activeId;
|
|
159
|
+
|
|
148
160
|
if (count && !this._programmaticScrollTargetId) {
|
|
149
161
|
const y = window.scrollY + this._stickyOffsetPx() + 2;
|
|
150
162
|
next = "anchor-1";
|
|
151
163
|
for (let i = 0; i < count; i++) {
|
|
152
|
-
const
|
|
164
|
+
const sectionId = resolved[i].href.replace(/^#/, "") || `anchor-${i + 1}-section`;
|
|
165
|
+
const el = document.getElementById(sectionId);
|
|
153
166
|
if (!el) continue;
|
|
154
167
|
const top = el.getBoundingClientRect().top + window.scrollY;
|
|
155
168
|
if (top <= y) next = `anchor-${i + 1}`;
|
|
156
169
|
}
|
|
157
170
|
}
|
|
158
171
|
|
|
159
|
-
const isMobile
|
|
160
|
-
const
|
|
172
|
+
const isMobile = window.matchMedia("(max-width: 768px)").matches;
|
|
173
|
+
const firstHref = resolved[0]?.href.replace(/^#/, "") || "anchor-1-section";
|
|
174
|
+
const first = document.getElementById(firstHref);
|
|
161
175
|
let nextCta;
|
|
162
|
-
if (!isMobile)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
nextCta = true;
|
|
166
|
-
} else {
|
|
167
|
-
/* Show only while first section is fully scrolled past; hide again when it scrolls back into view */
|
|
168
|
-
nextCta = first.getBoundingClientRect().bottom <= 0;
|
|
169
|
-
}
|
|
176
|
+
if (!isMobile) nextCta = true;
|
|
177
|
+
else if (!first) nextCta = true;
|
|
178
|
+
else nextCta = first.getBoundingClientRect().bottom <= 0;
|
|
170
179
|
|
|
171
180
|
if (this.activeId !== next || this._mobileCtaRevealed !== nextCta) {
|
|
172
|
-
this.activeId
|
|
181
|
+
this.activeId = next;
|
|
173
182
|
this._mobileCtaRevealed = nextCta;
|
|
174
183
|
this.requestUpdate();
|
|
175
184
|
}
|
|
176
185
|
}
|
|
177
186
|
|
|
178
|
-
_navItemCount() {
|
|
179
|
-
return Array.from(this.children).filter(
|
|
180
|
-
(el) => el.tagName === "BD-ANCHOR-NAV-ITEM"
|
|
181
|
-
).length;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
187
|
firstUpdated() {
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
188
|
+
const resolved = this._resolvedItems();
|
|
189
|
+
if (resolved.length) {
|
|
187
190
|
this.activeId = "anchor-1";
|
|
188
191
|
this.requestUpdate();
|
|
189
192
|
}
|
|
@@ -199,7 +202,6 @@ class BdAnchorNav extends LitElement {
|
|
|
199
202
|
queueMicrotask(() => this._playMobileDropdownLabelSwap());
|
|
200
203
|
}
|
|
201
204
|
|
|
202
|
-
/** Mobile dropdown title: subtle fade + slide when scrollspy (or tap) changes section. */
|
|
203
205
|
_playMobileDropdownLabelSwap() {
|
|
204
206
|
if (typeof window === "undefined") return;
|
|
205
207
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
@@ -216,15 +218,11 @@ class BdAnchorNav extends LitElement {
|
|
|
216
218
|
);
|
|
217
219
|
}
|
|
218
220
|
|
|
219
|
-
/**
|
|
220
|
-
* Scroll clearance = actual sticky bar height + small inset (not a fixed 128px token).
|
|
221
|
-
* Matches best practice: target headings sit just under the bar, not with extra dead space.
|
|
222
|
-
*/
|
|
223
221
|
_stickyOffsetPx() {
|
|
224
|
-
const barPx
|
|
222
|
+
const barPx = this.offsetHeight;
|
|
225
223
|
const insetRaw = getComputedStyle(this).getPropertyValue("--spacing-8")
|
|
226
224
|
.trim();
|
|
227
|
-
const insetPx
|
|
225
|
+
const insetPx = parseLengthToPx(insetRaw) || 8;
|
|
228
226
|
if (barPx > 0) return barPx + insetPx;
|
|
229
227
|
const fallback = getComputedStyle(this).getPropertyValue("--spacing-128")
|
|
230
228
|
.trim();
|
|
@@ -235,33 +233,28 @@ class BdAnchorNav extends LitElement {
|
|
|
235
233
|
if (event?.preventDefault) event.preventDefault();
|
|
236
234
|
const el = getTarget();
|
|
237
235
|
if (!el) return;
|
|
238
|
-
const offset
|
|
239
|
-
const rect
|
|
240
|
-
const scrollTop
|
|
241
|
-
const elementY
|
|
242
|
-
const prefersReduced =
|
|
243
|
-
typeof window !== "undefined" &&
|
|
236
|
+
const offset = this._stickyOffsetPx();
|
|
237
|
+
const rect = el.getBoundingClientRect();
|
|
238
|
+
const scrollTop = window.scrollY ?? window.pageYOffset;
|
|
239
|
+
const elementY = rect.top + scrollTop;
|
|
240
|
+
const prefersReduced = typeof window !== "undefined" &&
|
|
244
241
|
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
245
|
-
window.scrollTo({
|
|
246
|
-
top : elementY - offset,
|
|
247
|
-
behavior: prefersReduced ? "auto" : "smooth"
|
|
248
|
-
});
|
|
242
|
+
window.scrollTo({ top: elementY - offset, behavior: prefersReduced ? "auto" : "smooth" });
|
|
249
243
|
}
|
|
250
244
|
|
|
251
|
-
handleClick(event, id) {
|
|
245
|
+
handleClick(event, id, href) {
|
|
252
246
|
event.preventDefault();
|
|
253
247
|
this._cancelScrollSpyUnlock();
|
|
254
248
|
this._programmaticScrollTargetId = id;
|
|
255
|
-
this.activeId
|
|
256
|
-
|
|
257
|
-
const sectionEl = document.getElementById(
|
|
249
|
+
this.activeId = id;
|
|
250
|
+
const sectionId = (href || "").replace(/^#/, "") || `${id}-section`;
|
|
251
|
+
const sectionEl = document.getElementById(sectionId);
|
|
258
252
|
if (sectionEl) {
|
|
259
253
|
this._scrollToY(event, () => sectionEl);
|
|
260
254
|
this._scheduleScrollSpyUnlock();
|
|
261
255
|
} else {
|
|
262
256
|
this._programmaticScrollTargetId = null;
|
|
263
257
|
}
|
|
264
|
-
|
|
265
258
|
this.requestUpdate();
|
|
266
259
|
}
|
|
267
260
|
|
|
@@ -272,49 +265,39 @@ class BdAnchorNav extends LitElement {
|
|
|
272
265
|
this.requestUpdate();
|
|
273
266
|
}
|
|
274
267
|
|
|
275
|
-
_selectFromDropdown(e, id) {
|
|
276
|
-
this.handleClick(e, id);
|
|
268
|
+
_selectFromDropdown(e, id, href) {
|
|
269
|
+
this.handleClick(e, id, href);
|
|
277
270
|
this._dropdownOpen = false;
|
|
278
271
|
this.requestUpdate();
|
|
279
272
|
}
|
|
280
273
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const m = /^anchor-(\d+)$/.exec(this.activeId || "");
|
|
287
|
-
const n = m ? parseInt(m[1], 10) : 1;
|
|
288
|
-
const idx = Math.min(Math.max(n - 1, 0), Math.max(0, items.length - 1));
|
|
289
|
-
return items[idx]?.getAttribute("title") || items[0]?.getAttribute("title") || "Section";
|
|
274
|
+
_activeLabel(resolved) {
|
|
275
|
+
const m = /^anchor-(\d+)$/.exec(this.activeId || "");
|
|
276
|
+
const n = m ? parseInt(m[1], 10) : 1;
|
|
277
|
+
const idx = Math.min(Math.max(n - 1, 0), Math.max(0, resolved.length - 1));
|
|
278
|
+
return resolved[idx]?.title || resolved[0]?.title || "Section";
|
|
290
279
|
}
|
|
291
280
|
|
|
292
281
|
render() {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
);
|
|
282
|
+
const resolved = this._resolvedItems();
|
|
283
|
+
const dropdownLabel = this._activeLabel(resolved);
|
|
296
284
|
|
|
297
285
|
const linkRow = (cls) => html`
|
|
298
286
|
<div class="${cls}">
|
|
299
|
-
${
|
|
300
|
-
const id = `anchor-${index + 1}`;
|
|
287
|
+
${resolved.map(({ id, title, href }) => {
|
|
301
288
|
const isActive = this.activeId === id;
|
|
302
289
|
return html`
|
|
303
290
|
<a
|
|
304
|
-
href="
|
|
291
|
+
href="${href}"
|
|
305
292
|
class="${isActive ? "active" : ""}"
|
|
306
293
|
aria-current=${isActive ? "true" : nothing}
|
|
307
|
-
@click=${(e) => this.handleClick(e, id)}
|
|
308
|
-
>
|
|
309
|
-
${item.getAttribute("title")}
|
|
310
|
-
</a>
|
|
294
|
+
@click=${(e) => this.handleClick(e, id, href)}
|
|
295
|
+
>${title}</a>
|
|
311
296
|
`;
|
|
312
297
|
})}
|
|
313
298
|
</div>
|
|
314
299
|
`;
|
|
315
300
|
|
|
316
|
-
const dropdownLabel = this._activeLabel(items);
|
|
317
|
-
|
|
318
301
|
return html`
|
|
319
302
|
<nav aria-label=${this.navLabel}>
|
|
320
303
|
<div
|
|
@@ -336,13 +319,7 @@ class BdAnchorNav extends LitElement {
|
|
|
336
319
|
<span class="bd-anchor-nav__dropdown-label">${dropdownLabel}</span>
|
|
337
320
|
<span class="bd-anchor-nav__dropdown-chevron" aria-hidden="true">
|
|
338
321
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
339
|
-
<path
|
|
340
|
-
d="M5 8l5 5 5-5"
|
|
341
|
-
stroke="currentColor"
|
|
342
|
-
stroke-width="2"
|
|
343
|
-
stroke-linecap="round"
|
|
344
|
-
stroke-linejoin="round"
|
|
345
|
-
/>
|
|
322
|
+
<path d="M5 8l5 5 5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
346
323
|
</svg>
|
|
347
324
|
</span>
|
|
348
325
|
</button>
|
|
@@ -354,22 +331,17 @@ class BdAnchorNav extends LitElement {
|
|
|
354
331
|
?hidden=${!this._dropdownOpen}
|
|
355
332
|
>
|
|
356
333
|
<ul class="bd-anchor-nav__dropdown-list" role="presentation">
|
|
357
|
-
${
|
|
358
|
-
const id = `anchor-${index + 1}`;
|
|
334
|
+
${resolved.map(({ id, title, href }) => {
|
|
359
335
|
const isActive = this.activeId === id;
|
|
360
336
|
return html`
|
|
361
337
|
<li role="presentation">
|
|
362
338
|
<button
|
|
363
339
|
type="button"
|
|
364
340
|
role="option"
|
|
365
|
-
class="bd-anchor-nav__dropdown-option ${isActive
|
|
366
|
-
? "bd-anchor-nav__dropdown-option--active"
|
|
367
|
-
: ""}"
|
|
341
|
+
class="bd-anchor-nav__dropdown-option ${isActive ? "bd-anchor-nav__dropdown-option--active" : ""}"
|
|
368
342
|
aria-selected=${isActive ? "true" : "false"}
|
|
369
|
-
@click=${(e) => this._selectFromDropdown(e, id)}
|
|
370
|
-
>
|
|
371
|
-
${item.getAttribute("title")}
|
|
372
|
-
</button>
|
|
343
|
+
@click=${(e) => this._selectFromDropdown(e, id, href)}
|
|
344
|
+
>${title}</button>
|
|
373
345
|
</li>
|
|
374
346
|
`;
|
|
375
347
|
})}
|
|
@@ -378,14 +350,11 @@ class BdAnchorNav extends LitElement {
|
|
|
378
350
|
</div>
|
|
379
351
|
|
|
380
352
|
<div class="bd-cta">
|
|
381
|
-
<
|
|
382
|
-
type="button"
|
|
353
|
+
<a
|
|
383
354
|
class="bd-anchor-nav__cta"
|
|
355
|
+
href="${this.ctaHref || "#section-pricing"}"
|
|
384
356
|
part="buy-button"
|
|
385
|
-
|
|
386
|
-
>
|
|
387
|
-
Buy now
|
|
388
|
-
</button>
|
|
357
|
+
>${this.ctaLabel}</a>
|
|
389
358
|
</div>
|
|
390
359
|
</div>
|
|
391
360
|
</nav>
|