@repobit/dex-system-design 0.23.94 → 0.23.96

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 (84) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +4 -4
  3. package/src/blog/assets/articles/fbi-crypto-scam-1.jpeg +0 -0
  4. package/src/blog/assets/articles/fbi-crypto-scam-cover.png +0 -0
  5. package/src/blog/assets/fonts/IBMPlexSans-Bold.woff2 +0 -0
  6. package/src/blog/assets/fonts/IBMPlexSans-BoldItalic.woff2 +0 -0
  7. package/src/blog/assets/fonts/IBMPlexSans-Italic.woff2 +0 -0
  8. package/src/blog/assets/fonts/IBMPlexSans-Light.woff2 +0 -0
  9. package/src/blog/assets/fonts/IBMPlexSans-LightItalic.woff2 +0 -0
  10. package/src/blog/assets/fonts/IBMPlexSans-Medium.woff2 +0 -0
  11. package/src/blog/assets/fonts/IBMPlexSans-MediumItalic.woff2 +0 -0
  12. package/src/blog/assets/fonts/IBMPlexSans-Regular.woff2 +0 -0
  13. package/src/blog/assets/fonts/IBMPlexSans-SemiBold.woff2 +0 -0
  14. package/src/blog/assets/fonts/IBMPlexSans-SemiBoldItalic.woff2 +0 -0
  15. package/src/blog/assets/fonts/IBMPlexSansCondensed-Bold.woff2 +0 -0
  16. package/src/blog/assets/fonts/IBMPlexSansCondensed-BoldItalic.woff2 +0 -0
  17. package/src/blog/assets/fonts/IBMPlexSansCondensed-Italic.woff2 +0 -0
  18. package/src/blog/assets/fonts/IBMPlexSansCondensed-Light.woff2 +0 -0
  19. package/src/blog/assets/fonts/IBMPlexSansCondensed-LightItalic.woff2 +0 -0
  20. package/src/blog/assets/fonts/IBMPlexSansCondensed-Medium.woff2 +0 -0
  21. package/src/blog/assets/fonts/IBMPlexSansCondensed-MediumItalic.woff2 +0 -0
  22. package/src/blog/assets/fonts/IBMPlexSansCondensed-Regular.woff2 +0 -0
  23. package/src/blog/assets/fonts/IBMPlexSansCondensed-SemiBold.woff2 +0 -0
  24. package/src/blog/assets/fonts/IBMPlexSansCondensed-SemiBoldItalic.woff2 +0 -0
  25. package/src/blog/assets/global-scam-intelligence-report-2026.png +0 -0
  26. package/src/blog/assets/psd-4.jpg +0 -0
  27. package/src/blog/src/articles/FBI warns of crypto investment scam that sends couriers to collect victims' cash-editedAB.docx +0 -0
  28. package/src/blog/src/articles/FBI-crypto-scam-courier-cash.png +0 -0
  29. package/src/blog/src/articles/fbi-crypto-scam.js +324 -0
  30. package/src/blog/src/articles/~$I warns of crypto investment scam that sends couriers to collect victims' cash-editedAB.docx +0 -0
  31. package/src/blog/src/atoms/blog-abstract.js +51 -0
  32. package/src/blog/src/atoms/blog-atoms.stories.js +519 -0
  33. package/src/blog/src/atoms/blog-authors.js +70 -0
  34. package/src/blog/src/atoms/blog-avatar.js +44 -0
  35. package/src/blog/src/atoms/blog-blockquote.js +92 -0
  36. package/src/blog/src/atoms/blog-caption.js +31 -0
  37. package/src/blog/src/atoms/blog-code-block.js +89 -0
  38. package/src/blog/src/atoms/blog-dek.js +30 -0
  39. package/src/blog/src/atoms/blog-featured-image.js +65 -0
  40. package/src/blog/src/atoms/blog-heading.js +63 -0
  41. package/src/blog/src/atoms/blog-image.js +50 -0
  42. package/src/blog/src/atoms/blog-key-takeaways.js +101 -0
  43. package/src/blog/src/atoms/blog-link.js +68 -0
  44. package/src/blog/src/atoms/blog-meta.js +87 -0
  45. package/src/blog/src/atoms/blog-most-popular.js +146 -0
  46. package/src/blog/src/atoms/blog-newsletter-signup.js +342 -0
  47. package/src/blog/src/atoms/blog-paragraph.js +61 -0
  48. package/src/blog/src/atoms/blog-product-upsell.js +168 -0
  49. package/src/blog/src/atoms/blog-related-articles.js +326 -0
  50. package/src/blog/src/atoms/blog-related-label.js +30 -0
  51. package/src/blog/src/atoms/blog-report-promo.js +250 -0
  52. package/src/blog/src/atoms/blog-standout.js +78 -0
  53. package/src/blog/src/atoms/blog-subhead.js +32 -0
  54. package/src/blog/src/atoms/blog-table-of-contents.js +447 -0
  55. package/src/blog/src/atoms/blog-tag.js +86 -0
  56. package/src/blog/src/atoms/blog-title.js +31 -0
  57. package/src/blog/src/base/blog-element.js +10 -0
  58. package/src/blog/src/breakpoints.js +23 -0
  59. package/src/blog/src/content/blog-article.js +736 -0
  60. package/src/blog/src/content/blog-atoms-showcase.js +401 -0
  61. package/src/blog/src/content/blog-page-layout.js +73 -0
  62. package/src/blog/src/content/blog-pages.stories.js +101 -0
  63. package/src/blog/src/content/render-inline-content.js +18 -0
  64. package/src/blog/src/data/popular-articles.js +43 -0
  65. package/src/blog/src/data/related-articles.js +58 -0
  66. package/src/blog/src/index.js +104 -0
  67. package/src/blog/src/lib/article-toc.js +206 -0
  68. package/src/blog/src/lib/load-prism.js +23 -0
  69. package/src/blog/src/shell/blog-shell.js +31 -0
  70. package/src/blog/src/styles/prism-theme.js +65 -0
  71. package/src/blog/src/tokens/blog.js +406 -0
  72. package/src/blog/src/tokens/core.js +811 -0
  73. package/src/blog/src/tokens/fonts.css +241 -0
  74. package/src/blog/src/tokens/index.js +27 -0
  75. package/src/blog/src/tokens/legacy.js +58 -0
  76. package/src/blog/tokens/blog.js +406 -0
  77. package/src/blog/tokens/core.js +811 -0
  78. package/src/blog/tokens/fonts.css +241 -0
  79. package/src/blog/tokens/index.js +27 -0
  80. package/src/blog/tokens/legacy.js +58 -0
  81. package/src/blog/tokens.css +1512 -0
  82. package/src/blog/tokens.js +1 -0
  83. package/src/blog/vendor/prism/prism.css +120 -0
  84. package/src/blog/vendor/prism/prism.js +300 -0
@@ -0,0 +1,736 @@
1
+ import { css, html, nothing, unsafeCSS } from 'lit';
2
+ import { fbiCryptoScamArticle } from '../articles/fbi-crypto-scam.js';
3
+ import '../atoms/blog-abstract.js';
4
+ import '../atoms/blog-authors.js';
5
+ import '../atoms/blog-blockquote.js';
6
+ import '../atoms/blog-caption.js';
7
+ import '../atoms/blog-code-block.js';
8
+ import '../atoms/blog-featured-image.js';
9
+ import '../atoms/blog-heading.js';
10
+ import '../atoms/blog-image.js';
11
+ import '../atoms/blog-key-takeaways.js';
12
+ import '../atoms/blog-meta.js';
13
+ import '../atoms/blog-newsletter-signup.js';
14
+ import '../atoms/blog-paragraph.js';
15
+ import '../atoms/blog-product-upsell.js';
16
+ import '../atoms/blog-related-label.js';
17
+ import '../atoms/blog-report-promo.js';
18
+ import '../atoms/blog-standout.js';
19
+ import '../atoms/blog-subhead.js';
20
+ import '../atoms/blog-table-of-contents.js';
21
+ import '../atoms/blog-tag.js';
22
+ import '../atoms/blog-title.js';
23
+ import { BlogElement } from '../base/blog-element.js';
24
+ import { matchesBreakpoint, minWidth } from '../breakpoints.js';
25
+ import {
26
+ assignSubheadAnchors,
27
+ buildTocItems,
28
+ splitBodyAtFirstSubhead
29
+ } from '../lib/article-toc.js';
30
+ import { renderInlineContent } from './render-inline-content.js';
31
+
32
+ /** @param {string | { text: string, content?: Array<{ type: string, text: string, href?: string }> }} item */
33
+ function renderListItem(item) {
34
+ if (typeof item === 'string') {
35
+ return item;
36
+ }
37
+
38
+ if (item.content?.some((segment) => segment.type === 'link')) {
39
+ return renderInlineContent(item.content);
40
+ }
41
+
42
+ return item.text;
43
+ }
44
+
45
+ /** @param {Array<Record<string, unknown>>} blocks */
46
+ function splitArticleBlocks(blocks) {
47
+ const relatedIndex = blocks.findIndex((block) => block.type === 'related-label');
48
+
49
+ if (relatedIndex === -1) {
50
+ return { bodyBlocks: blocks, relatedBlocks: [] };
51
+ }
52
+
53
+ return {
54
+ bodyBlocks : blocks.slice(0, relatedIndex),
55
+ relatedBlocks: blocks.slice(relatedIndex)
56
+ };
57
+ }
58
+
59
+ /** @param {Record<string, unknown>} block */
60
+ function renderBlock(block) {
61
+ switch (block.type) {
62
+ case 'paragraph':
63
+ return html`<blog-paragraph
64
+ emphasis=${block.style ?? ''}
65
+ .content=${block.content ?? []}
66
+ >${block.content ? '' : block.text}</blog-paragraph
67
+ >`;
68
+ case 'heading':
69
+ return html`<blog-heading level="2">${block.text}</blog-heading>`;
70
+ case 'subhead':
71
+ return html`<blog-subhead id=${block.anchorId || nothing}
72
+ >${block.text}</blog-subhead
73
+ >`;
74
+ case 'related-label':
75
+ return html`<blog-related-label>${block.text}</blog-related-label>`;
76
+ case 'quote':
77
+ return html`${block.intro
78
+ ? html`<blog-paragraph>${block.intro}</blog-paragraph>`
79
+ : null}<blog-blockquote>${block.text}</blog-blockquote>`;
80
+ case 'code':
81
+ return html`<blog-code-block
82
+ language=${block.language ?? 'javascript'}
83
+ code=${block.code ?? ''}
84
+ caption=${block.caption ?? ''}
85
+ ></blog-code-block>`;
86
+ case 'takeaways':
87
+ return html`<blog-key-takeaways
88
+ title=${block.title}
89
+ .items=${block.items}
90
+ ></blog-key-takeaways>`;
91
+ case 'list':
92
+ return html`<ul>
93
+ ${block.items.map((item) => html`<li>${renderListItem(item)}</li>`)}
94
+ </ul>`;
95
+ case 'standout':
96
+ return html`<blog-standout
97
+ href=${block.href ?? ''}
98
+ .content=${block.content ?? []}
99
+ >${block.content ? '' : block.text}</blog-standout
100
+ >`;
101
+ case 'image':
102
+ return html`<figure class="article-figure">
103
+ <blog-image src=${block.src} alt=${block.alt ?? ''}></blog-image>
104
+ ${block.caption
105
+ ? html`<blog-caption>${block.caption}</blog-caption>`
106
+ : null}
107
+ </figure>`;
108
+ default:
109
+ return null;
110
+ }
111
+ }
112
+
113
+ /** Full article — content structure parsed from Word, styled with atoms. */
114
+ export class BlogArticle extends BlogElement {
115
+ static properties = {
116
+ article: { type: Object }
117
+ };
118
+
119
+ constructor() {
120
+ super();
121
+ this.article = fbiCryptoScamArticle;
122
+ /** @type {(() => void) | null} */
123
+ this._onWindowResize = null;
124
+ /** @type {ResizeObserver | null} */
125
+ this._sidebarLayoutObserver = null;
126
+ }
127
+
128
+ static styles = [
129
+ ...BlogElement.styles,
130
+ css`
131
+ :host {
132
+ display: block;
133
+ }
134
+
135
+ .article {
136
+ max-width: none;
137
+ margin: 0;
138
+ padding: 0;
139
+ }
140
+
141
+ .article-body,
142
+ .article-related {
143
+ width: 100%;
144
+ }
145
+
146
+ .article-toc--inline {
147
+ margin: 0 0 var(--blog-prose-paragraph-spacing);
148
+ }
149
+
150
+ .article-layout {
151
+ display: block;
152
+ }
153
+
154
+ .article-sidebar {
155
+ display: none;
156
+ }
157
+
158
+ .article-hero__authors--mobile {
159
+ display: block;
160
+ }
161
+
162
+ ${unsafeCSS(minWidth('lg'))} {
163
+ .article-layout {
164
+ display: grid;
165
+ grid-template-columns:
166
+ minmax(0, var(--layout-page-main-ratio))
167
+ minmax(0, var(--layout-page-sidebar-ratio));
168
+ gap: var(--layout-page-gap);
169
+ align-items: start;
170
+ }
171
+
172
+ .article-layout__hero-main {
173
+ grid-column: 1;
174
+ grid-row: 1;
175
+ min-width: 0;
176
+ }
177
+
178
+ .article-layout__preamble {
179
+ grid-column: 1;
180
+ grid-row: 2;
181
+ min-width: 0;
182
+ }
183
+
184
+ .article-layout__sections-main,
185
+ .article-layout__body-fallback {
186
+ grid-column: 1;
187
+ grid-row: 3;
188
+ min-width: 0;
189
+ }
190
+
191
+ .article-layout__newsletter {
192
+ grid-column: 1;
193
+ grid-row: 4;
194
+ min-width: 0;
195
+ }
196
+
197
+ .article-sidebar {
198
+ display: flex;
199
+ flex-direction: column;
200
+ grid-column: 2;
201
+ grid-row: 1 / 4;
202
+ align-self: stretch;
203
+ min-width: 0;
204
+ }
205
+
206
+ .article-sidebar__authors-zone {
207
+ flex: none;
208
+ height: var(--sidebar-authors-zone-height, auto);
209
+ }
210
+
211
+ .article-sidebar__authors {
212
+ position: sticky;
213
+ top: var(--blog-toc-sticky-offset);
214
+ }
215
+
216
+ .article-sidebar__toc-gap {
217
+ flex: none;
218
+ height: var(--sidebar-toc-gap-height, 0px);
219
+ }
220
+
221
+ .article-sidebar__toc {
222
+ position: sticky;
223
+ top: var(--blog-toc-sticky-offset);
224
+ flex: none;
225
+ }
226
+
227
+ .article-toc--inline {
228
+ display: none;
229
+ }
230
+
231
+ .article-hero__authors--mobile {
232
+ display: none;
233
+ }
234
+ }
235
+
236
+ .article-hero {
237
+ margin-bottom: var(--spacing-24);
238
+ }
239
+
240
+ .article-meta {
241
+ display: flex;
242
+ flex-wrap: wrap;
243
+ align-items: center;
244
+ gap: var(--blog-byline-gap);
245
+ margin-bottom: var(--spacing-16);
246
+ }
247
+
248
+ .article-tags {
249
+ display: flex;
250
+ flex-wrap: wrap;
251
+ gap: var(--blog-tag-gap);
252
+ margin-top: var(--spacing-32);
253
+ }
254
+
255
+ .article-divider,
256
+ .article-hero-divider {
257
+ margin: var(--spacing-24) 0;
258
+ border: 0;
259
+ border-top: var(--blog-hero-divider-width) solid
260
+ var(--blog-hero-divider-color);
261
+ }
262
+
263
+ .article-hero-divider {
264
+ margin: 0 0 var(--spacing-24);
265
+ }
266
+
267
+ .article-hero blog-title {
268
+ margin-bottom: var(--spacing-24);
269
+ }
270
+
271
+ .article-hero blog-authors,
272
+ .article-hero__authors--mobile {
273
+ margin-bottom: var(--spacing-24);
274
+ }
275
+
276
+ .article-sidebar blog-authors {
277
+ margin-bottom: 0;
278
+ }
279
+
280
+ .article-hero blog-abstract {
281
+ --blog-abstract-spacing-after-last: 0;
282
+ }
283
+
284
+ .article-hero__upsell,
285
+ .article-hero__featured {
286
+ margin: var(--blog-hero-abstract-to-featured-gap) 0 0;
287
+ }
288
+
289
+ .article-body,
290
+ .article-layout__preamble,
291
+ .article-layout__sections-main,
292
+ .article-related {
293
+ width: 100%;
294
+ }
295
+
296
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main, .article-related)
297
+ :is(
298
+ blog-paragraph,
299
+ blog-heading,
300
+ blog-subhead,
301
+ blog-related-label,
302
+ blog-standout,
303
+ ul,
304
+ ol
305
+ ) {
306
+ max-width: var(--blog-prose-max-width);
307
+ }
308
+
309
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main, .article-related)
310
+ :is(
311
+ blog-paragraph,
312
+ blog-standout,
313
+ ul,
314
+ ol,
315
+ blog-heading,
316
+ blog-subhead,
317
+ blog-related-label,
318
+ blog-blockquote,
319
+ blog-code-block,
320
+ .article-figure
321
+ ) {
322
+ margin: 0 0 var(--blog-prose-paragraph-spacing);
323
+ }
324
+
325
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main)
326
+ blog-key-takeaways,
327
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main)
328
+ .article-promo {
329
+ margin: 0 0 var(--blog-prose-paragraph-spacing);
330
+ }
331
+
332
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main)
333
+ blog-key-takeaways,
334
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main)
335
+ .article-figure,
336
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main)
337
+ blog-code-block,
338
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main)
339
+ .article-promo {
340
+ display: block;
341
+ width: 100%;
342
+ }
343
+
344
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main)
345
+ blog-key-takeaways {
346
+ margin-top: 0;
347
+ }
348
+
349
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main)
350
+ .article-promo {
351
+ margin-top: 0;
352
+ }
353
+
354
+ .article-newsletter {
355
+ margin: var(--spacing-32) 0 var(--blog-newsletter-spacing-bottom);
356
+ }
357
+
358
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main)
359
+ blog-key-takeaways
360
+ + :is(blog-heading, blog-subhead) {
361
+ margin-top: var(--blog-prose-subhead-spacing-top);
362
+ }
363
+
364
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main, .article-related)
365
+ blog-heading {
366
+ margin-top: var(--blog-prose-heading-spacing-top);
367
+ margin-bottom: var(--blog-prose-heading-spacing-bottom);
368
+ }
369
+
370
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main, .article-related)
371
+ blog-subhead {
372
+ margin-top: var(--blog-prose-subhead-spacing-top);
373
+ margin-bottom: var(--blog-prose-subhead-spacing-bottom);
374
+ }
375
+
376
+ .article-layout__sections-main blog-subhead:first-child,
377
+ .article-body blog-subhead:first-child {
378
+ margin-top: 0;
379
+ }
380
+
381
+ .article-related blog-related-label {
382
+ margin-top: 0;
383
+ margin-bottom: var(--spacing-8);
384
+ }
385
+
386
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main, .article-related) ul,
387
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main, .article-related) ol {
388
+ padding-left: var(--blog-prose-list-indent);
389
+ font-family: var(--typography-fontFamily-sans);
390
+ font-size: var(--blog-prose-fontSize);
391
+ line-height: var(--blog-prose-lineHeight);
392
+ color: var(--blog-prose-color);
393
+ }
394
+
395
+ :is(.article-body, .article-layout__preamble, .article-layout__sections-main, .article-related) li + li {
396
+ margin-top: var(--blog-prose-list-spacing);
397
+ }
398
+
399
+ ${unsafeCSS(minWidth('lg'))} {
400
+ .article-hero {
401
+ margin-bottom: 0;
402
+ }
403
+
404
+ :is(.article-layout__preamble) blog-key-takeaways {
405
+ margin-bottom: 0;
406
+ }
407
+
408
+ .article-newsletter {
409
+ margin-top: 0;
410
+ }
411
+ }
412
+ `
413
+ ];
414
+
415
+ getTocItems() {
416
+ const { blocks } = this.article.document;
417
+ const { bodyBlocks } = splitArticleBlocks(blocks);
418
+
419
+ return buildTocItems(assignSubheadAnchors(bodyBlocks));
420
+ }
421
+
422
+ connectedCallback() {
423
+ super.connectedCallback();
424
+ this.dispatchEvent(
425
+ new CustomEvent('toc-items-changed', {
426
+ detail : { items: this.getTocItems() },
427
+ bubbles : true,
428
+ composed: true
429
+ })
430
+ );
431
+ this._onWindowResize = () => this._syncSidebarLayout();
432
+ window.addEventListener('resize', this._onWindowResize);
433
+ this._sidebarLayoutObserver = new ResizeObserver(() => this._syncSidebarLayout());
434
+ requestAnimationFrame(() => {
435
+ this._observeSidebarLayout();
436
+ this._syncSidebarLayout();
437
+ });
438
+ }
439
+
440
+ disconnectedCallback() {
441
+ super.disconnectedCallback();
442
+ if (this._onWindowResize) {
443
+ window.removeEventListener('resize', this._onWindowResize);
444
+ this._onWindowResize = null;
445
+ }
446
+ this._sidebarLayoutObserver?.disconnect();
447
+ this._sidebarLayoutObserver = null;
448
+ }
449
+
450
+ updated(changed) {
451
+ if (changed.has('article')) {
452
+ this.dispatchEvent(
453
+ new CustomEvent('toc-items-changed', {
454
+ detail : { items: this.getTocItems() },
455
+ bubbles : true,
456
+ composed: true
457
+ })
458
+ );
459
+ }
460
+
461
+ this._observeSidebarLayout();
462
+ this._syncSidebarLayout();
463
+ }
464
+
465
+ _observeSidebarLayout() {
466
+ if (!this._sidebarLayoutObserver || !matchesBreakpoint('lg')) {
467
+ return;
468
+ }
469
+
470
+ const layoutTargets = [
471
+ this.shadowRoot?.querySelector('.article-layout'),
472
+ this.shadowRoot?.querySelector('.article-layout__hero-main'),
473
+ this.shadowRoot?.querySelector('.article-layout__preamble'),
474
+ this.shadowRoot?.querySelector('.article-layout__sections-main')
475
+ ].filter(Boolean);
476
+
477
+ for (const target of layoutTargets) {
478
+ this._sidebarLayoutObserver.observe(target);
479
+ }
480
+ }
481
+
482
+ /** Size sidebar zones so authors scroll away at Key Takeaways and TOC aligns with the first subhead. */
483
+ _syncSidebarLayout() {
484
+ if (!matchesBreakpoint('lg')) {
485
+ return;
486
+ }
487
+
488
+ const sidebar = this.shadowRoot?.querySelector('.article-sidebar');
489
+ const takeaways = this.shadowRoot?.querySelector(
490
+ '.article-layout__preamble blog-key-takeaways'
491
+ );
492
+ const firstSubhead = this.shadowRoot?.querySelector(
493
+ '.article-layout__sections-main blog-subhead'
494
+ );
495
+
496
+ if (!sidebar) {
497
+ return;
498
+ }
499
+
500
+ const scrollY = window.scrollY;
501
+ const sidebarTop = sidebar.getBoundingClientRect().top + scrollY;
502
+
503
+ if (!firstSubhead) {
504
+ sidebar.style.removeProperty('--sidebar-authors-zone-height');
505
+ sidebar.style.removeProperty('--sidebar-toc-gap-height');
506
+ return;
507
+ }
508
+
509
+ const subheadTop = firstSubhead.getBoundingClientRect().top + scrollY;
510
+
511
+ if (takeaways) {
512
+ const takeawaysBottom = takeaways.getBoundingClientRect().bottom + scrollY;
513
+ sidebar.style.setProperty(
514
+ '--sidebar-authors-zone-height',
515
+ `${Math.max(0, takeawaysBottom - sidebarTop)}px`
516
+ );
517
+ sidebar.style.setProperty(
518
+ '--sidebar-toc-gap-height',
519
+ `${Math.max(0, subheadTop - takeawaysBottom)}px`
520
+ );
521
+ return;
522
+ }
523
+
524
+ sidebar.style.setProperty('--sidebar-authors-zone-height', '0px');
525
+ sidebar.style.setProperty(
526
+ '--sidebar-toc-gap-height',
527
+ `${Math.max(0, subheadTop - sidebarTop)}px`
528
+ );
529
+ }
530
+
531
+ _renderSidebar(meta, tocItems, hasSections) {
532
+ const hasAuthors = meta.authors.length > 0;
533
+ const hasToc = tocItems.length > 0 && hasSections;
534
+
535
+ if (!hasAuthors && !hasToc) {
536
+ return null;
537
+ }
538
+
539
+ return html`
540
+ <aside class="article-sidebar" aria-label="Article sidebar">
541
+ ${hasAuthors
542
+ ? html`
543
+ <div class="article-sidebar__authors-zone">
544
+ <div class="article-sidebar__authors" aria-label="Authors">
545
+ <blog-authors .authors=${meta.authors}></blog-authors>
546
+ </div>
547
+ </div>
548
+ `
549
+ : nothing}
550
+ ${hasToc
551
+ ? html`
552
+ <div class="article-sidebar__toc-gap" aria-hidden="true"></div>
553
+ <div class="article-sidebar__toc" aria-label="Table of contents">
554
+ <blog-table-of-contents .items=${tocItems}></blog-table-of-contents>
555
+ </div>
556
+ `
557
+ : nothing}
558
+ </aside>
559
+ `;
560
+ }
561
+
562
+ _renderToc(items, { collapsible = false } = {}) {
563
+ if (!items.length) {
564
+ return null;
565
+ }
566
+
567
+ return html`
568
+ <div class="article-toc article-toc--inline">
569
+ <blog-table-of-contents
570
+ ?collapsible=${collapsible}
571
+ .items=${items}
572
+ ></blog-table-of-contents>
573
+ </div>
574
+ `;
575
+ }
576
+
577
+ _renderAuthors(authors, className = '') {
578
+ if (!authors.length) {
579
+ return null;
580
+ }
581
+
582
+ return html`
583
+ <div class=${className || nothing}>
584
+ <blog-authors .authors=${authors}></blog-authors>
585
+ </div>
586
+ `;
587
+ }
588
+
589
+ _renderBodyBlocks(blocks) {
590
+ return blocks.map((block) => renderBlock(block));
591
+ }
592
+
593
+ _renderSectionsWithPromo(sections) {
594
+ const blocks = this._renderBodyBlocks(sections);
595
+
596
+ // Inject the promo card after the second subhead
597
+ let insertIndex = -1;
598
+ let subheadCount = 0;
599
+
600
+ for (let i = 0; i < sections.length; i++) {
601
+ if (sections[i].type === 'subhead') {
602
+ subheadCount++;
603
+ if (subheadCount === 2) {
604
+ // Insert just before the second subhead
605
+ insertIndex = i;
606
+ break;
607
+ }
608
+ }
609
+ }
610
+
611
+ const promoHtml = html`
612
+ <div class="article-promo">
613
+ <blog-report-promo></blog-report-promo>
614
+ </div>
615
+ `;
616
+
617
+ if (insertIndex !== -1) {
618
+ // Insert inline before the target block
619
+ return [
620
+ ...blocks.slice(0, insertIndex),
621
+ promoHtml,
622
+ ...blocks.slice(insertIndex)
623
+ ];
624
+ } else {
625
+ // Fallback if there are fewer than 2 subheads
626
+ return [...blocks, promoHtml];
627
+ }
628
+ }
629
+
630
+ render() {
631
+ const { meta, document: doc } = this.article;
632
+ const { title, abstract, blocks } = doc;
633
+ const { bodyBlocks, relatedBlocks } = splitArticleBlocks(blocks);
634
+ const anchoredBlocks = assignSubheadAnchors(bodyBlocks);
635
+ const tocItems = buildTocItems(anchoredBlocks);
636
+ const { preamble, sections } = splitBodyAtFirstSubhead(anchoredBlocks);
637
+
638
+ return html`
639
+ <article class="article">
640
+ <div class="article-layout">
641
+ <header class="article-hero article-layout__hero-main">
642
+ <div class="article-meta">
643
+ <blog-meta
644
+ type="date"
645
+ value=${meta.byline.date}
646
+ datetime=${meta.byline.datetime}
647
+ ></blog-meta>
648
+ <blog-meta type="separator"></blog-meta>
649
+ <blog-meta
650
+ type="reading-time"
651
+ value=${meta.byline.readingTime}
652
+ ></blog-meta>
653
+ <blog-meta type="separator"></blog-meta>
654
+ <blog-meta
655
+ type="updated"
656
+ value=${meta.byline.updated}
657
+ datetime=${meta.byline.updatedDatetime}
658
+ ></blog-meta>
659
+ </div>
660
+
661
+ <blog-title>${title}</blog-title>
662
+
663
+ ${this._renderAuthors(meta.authors, 'article-hero__authors--mobile')}
664
+
665
+ <hr class="article-hero-divider" />
666
+
667
+ <blog-abstract .paragraphs=${abstract}></blog-abstract>
668
+
669
+ <div class="article-hero__upsell">
670
+ <blog-product-upsell></blog-product-upsell>
671
+ </div>
672
+
673
+ <figure class="article-hero__featured">
674
+ <blog-featured-image
675
+ src=${meta.featuredImage || ''}
676
+ alt=${meta.featuredImageAlt || ''}
677
+ ></blog-featured-image>
678
+ </figure>
679
+ </header>
680
+
681
+ ${preamble.length
682
+ ? html`
683
+ <div class="article-layout__preamble">
684
+ ${this._renderBodyBlocks(preamble)}
685
+ </div>
686
+ `
687
+ : nothing}
688
+
689
+ ${sections.length && tocItems.length
690
+ ? this._renderToc(tocItems, { collapsible: true })
691
+ : nothing}
692
+
693
+ ${sections.length
694
+ ? html`
695
+ <div class="article-layout__sections-main">
696
+ ${this._renderSectionsWithPromo(sections)}
697
+ </div>
698
+ `
699
+ : html`
700
+ <div class="article-body article-layout__body-fallback">
701
+ ${this._renderBodyBlocks(anchoredBlocks)}
702
+ ${!sections.length && tocItems.length
703
+ ? this._renderToc(tocItems, { collapsible: true })
704
+ : nothing}
705
+ </div>
706
+ `}
707
+
708
+ ${this._renderSidebar(meta, tocItems, sections.length > 0)}
709
+
710
+ <div class="article-newsletter article-layout__newsletter">
711
+ <blog-newsletter-signup></blog-newsletter-signup>
712
+ </div>
713
+ </div>
714
+
715
+ <footer class="article-tags">
716
+ ${meta.tags.map(
717
+ (tag) => html`
718
+ <blog-tag label=${tag.label} variant="brand"></blog-tag>
719
+ `
720
+ )}
721
+ </footer>
722
+
723
+ ${relatedBlocks.length
724
+ ? html`
725
+ <hr class="article-divider" />
726
+ <aside class="article-related">
727
+ ${relatedBlocks.map((block) => renderBlock(block))}
728
+ </aside>
729
+ `
730
+ : null}
731
+ </article>
732
+ `;
733
+ }
734
+ }
735
+
736
+ customElements.define('blog-article', BlogArticle);