@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.
@@ -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
- connectedCallback() {
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
- /** Mobile: show Buy now only after first anchor section has been scrolled past */
41
- _mobileCtaRevealed: { type: Boolean, state: true }
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 = "Section navigation";
50
- this._dropdownOpen = false;
51
- this._mobileCtaRevealed = false;
52
- this._panelId = `bd-anchor-nav-panel-${Math.random().toString(36)
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 = this._onScrollSpy.bind(this);
55
- this._onDocumentPointerDown = this._onDocumentPointerDown.bind(this);
56
- this._onKeydown = this._onKeydown.bind(this);
57
- this._scrollPending = false;
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 = null;
61
- this._scrollEndUnlockHandler = null;
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 = null;
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 count = this._navItemCount();
147
- let next = this.activeId;
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 el = document.getElementById(`anchor-${i + 1}-section`);
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 = window.matchMedia("(max-width: 768px)").matches;
160
- const first = document.getElementById("anchor-1-section");
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
- nextCta = true;
164
- } else if (!first) {
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 = next;
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 n = this._navItemCount();
186
- if (n) {
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 = this.offsetHeight;
222
+ const barPx = this.offsetHeight;
225
223
  const insetRaw = getComputedStyle(this).getPropertyValue("--spacing-8")
226
224
  .trim();
227
- const insetPx = parseLengthToPx(insetRaw) || 8;
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 = this._stickyOffsetPx();
239
- const rect = el.getBoundingClientRect();
240
- const scrollTop = window.scrollY ?? window.pageYOffset;
241
- const elementY = rect.top + scrollTop;
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 = id;
256
-
257
- const sectionEl = document.getElementById(`${id}-section`);
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
- scrollToPricing(event) {
282
- this._scrollToY(event, () => document.getElementById("section-pricing"));
283
- }
284
-
285
- _activeLabel(items) {
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 items = Array.from(this.children).filter(
294
- (el) => el.tagName === "BD-ANCHOR-NAV-ITEM"
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
- ${items.map((item, index) => {
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="#${id}-section"
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
- ${items.map((item, index) => {
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
- <button
382
- type="button"
353
+ <a
383
354
  class="bd-anchor-nav__cta"
355
+ href="${this.ctaHref || "#section-pricing"}"
384
356
  part="buy-button"
385
- @click=${(e) => this.scrollToPricing(e)}
386
- >
387
- Buy now
388
- </button>
357
+ >${this.ctaLabel}</a>
389
358
  </div>
390
359
  </div>
391
360
  </nav>