@repobit/dex-system-design 0.23.95 → 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 +7 -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,401 @@
1
+ import { css, html } from 'lit';
2
+ import { fbiCryptoScamArticle } from '../articles/fbi-crypto-scam.js';
3
+ import '../atoms/blog-authors.js';
4
+ import '../atoms/blog-avatar.js';
5
+ import '../atoms/blog-blockquote.js';
6
+ import '../atoms/blog-caption.js';
7
+ import '../atoms/blog-code-block.js';
8
+ import '../atoms/blog-dek.js';
9
+ import '../atoms/blog-featured-image.js';
10
+ import '../atoms/blog-image.js';
11
+ import '../atoms/blog-key-takeaways.js';
12
+ import '../atoms/blog-link.js';
13
+ import '../atoms/blog-meta.js';
14
+ import '../atoms/blog-newsletter-signup.js';
15
+ import '../atoms/blog-paragraph.js';
16
+ import '../atoms/blog-product-upsell.js';
17
+ import '../atoms/blog-related-articles.js';
18
+ import '../atoms/blog-related-label.js';
19
+ import '../atoms/blog-report-promo.js';
20
+ import '../atoms/blog-standout.js';
21
+ import '../atoms/blog-subhead.js';
22
+ import '../atoms/blog-table-of-contents.js';
23
+ import '../atoms/blog-tag.js';
24
+ import '../atoms/blog-title.js';
25
+ import { BlogElement } from '../base/blog-element.js';
26
+ import {
27
+ relatedArticles,
28
+ relatedArticlesThree
29
+ } from '../data/related-articles.js';
30
+ import { assignSubheadAnchors, buildTocItems } from '../lib/article-toc.js';
31
+
32
+ const sample = fbiCryptoScamArticle;
33
+
34
+ /**
35
+ * Token → atom exercise showcase.
36
+ * Each row shows the atom rendered with existing tokens only.
37
+ */
38
+ export class BlogAtomsShowcase extends BlogElement {
39
+ static styles = [
40
+ ...BlogElement.styles,
41
+ css`
42
+ :host {
43
+ display: block;
44
+ background: var(--color-surface-subtle);
45
+ border-bottom: var(--border-width-1) solid var(--color-border-subtle);
46
+ }
47
+
48
+ .showcase {
49
+ max-width: var(--layout-article-max-width);
50
+ margin: 0 auto;
51
+ padding: var(--spacing-48) var(--spacing-24);
52
+ }
53
+
54
+ .showcase-header {
55
+ margin-bottom: var(--spacing-48);
56
+ }
57
+
58
+ .showcase-header h2 {
59
+ margin: 0 0 var(--spacing-8);
60
+ font-family: var(--typography-heading-h2-fontFamily);
61
+ font-size: var(--typography-heading-h2-fontSize);
62
+ font-weight: var(--typography-heading-h2-fontWeight);
63
+ color: var(--color-text-primary);
64
+ }
65
+
66
+ .showcase-header p {
67
+ margin: 0;
68
+ font-size: var(--typography-fontSize-sm);
69
+ color: var(--color-text-secondary);
70
+ max-width: var(--blog-prose-max-width);
71
+ }
72
+
73
+ .grid {
74
+ display: grid;
75
+ gap: var(--spacing-32);
76
+ }
77
+
78
+ .specimen {
79
+ padding: var(--spacing-24);
80
+ background: var(--color-surface);
81
+ border: var(--border-width-1) solid var(--color-border-subtle);
82
+ border-radius: var(--radius-lg);
83
+ }
84
+
85
+ .specimen__label {
86
+ margin: 0 0 var(--spacing-4);
87
+ font-family: var(--typography-fontFamily-mono);
88
+ font-size: var(--typography-fontSize-xs);
89
+ font-weight: var(--typography-fontWeight-semibold);
90
+ color: var(--color-orange-700);
91
+ text-transform: uppercase;
92
+ letter-spacing: var(--typography-letterSpacing-wider);
93
+ }
94
+
95
+ .specimen__tokens {
96
+ margin: 0 0 var(--spacing-16);
97
+ font-family: var(--typography-fontFamily-mono);
98
+ font-size: var(--typography-fontSize-xxs);
99
+ color: var(--color-text-muted);
100
+ line-height: var(--typography-lineHeight-relaxed);
101
+ }
102
+
103
+ .specimen__preview {
104
+ padding: var(--spacing-16);
105
+ background: var(--color-surface-subtle);
106
+ border-radius: var(--radius-md);
107
+ }
108
+
109
+ .specimen__preview--row {
110
+ display: flex;
111
+ flex-wrap: wrap;
112
+ align-items: center;
113
+ gap: var(--blog-byline-gap);
114
+ }
115
+
116
+ .specimen__preview--tags {
117
+ display: flex;
118
+ flex-wrap: wrap;
119
+ gap: var(--blog-tag-gap);
120
+ }
121
+
122
+ .specimen__preview--related {
123
+ padding: 0;
124
+ background: transparent;
125
+ }
126
+
127
+ .specimen__preview--sidebar {
128
+ // max-width: 24.5rem;
129
+ padding: 0;
130
+ background: transparent;
131
+ }
132
+ `
133
+ ];
134
+
135
+ _specimen(label, tokens, preview) {
136
+ return html`
137
+ <div class="specimen">
138
+ <p class="specimen__label">${label}</p>
139
+ <p class="specimen__tokens">${tokens}</p>
140
+ <div class="specimen__preview">${preview}</div>
141
+ </div>
142
+ `;
143
+ }
144
+
145
+ render() {
146
+ const { meta, document: doc } = sample;
147
+ const { title, abstract, blocks } = doc;
148
+ const subhead = blocks.find(
149
+ (b) => b.type === 'subhead' && b.text === 'How the scam works'
150
+ );
151
+ const relatedLabel = blocks.find((b) => b.type === 'related-label') ?? {
152
+ text: 'You may also want to read:'
153
+ };
154
+ const takeaways = blocks.find((b) => b.type === 'takeaways');
155
+ const relatedStandout = blocks.find((b) => b.type === 'standout') ?? {
156
+ text: 'Related article headline example',
157
+ href: 'https://example.com/related-article'
158
+ };
159
+ const quote = blocks.find((b) => b.type === 'quote');
160
+ const tocItems = buildTocItems(assignSubheadAnchors(blocks));
161
+
162
+ return html`
163
+ <section class="showcase">
164
+ <header class="showcase-header">
165
+ <h2>Atoms × existing tokens</h2>
166
+ <p>
167
+ Each specimen uses only tokens already defined in
168
+ <code>tokens/core.js</code> and <code>tokens/blog.js</code> — no
169
+ new values added for this exercise.
170
+ </p>
171
+ </header>
172
+
173
+ <div class="grid">
174
+ ${this._specimen(
175
+ 'blog-tag (brand — article + authors)',
176
+ '--blog-tag-brand-background · --color-blue-100 · --blog-tag-brand-color · --color-blue-800 · --typography-label-default-* · --dimension-20px · --spacing-8 gap',
177
+ html`<div class="specimen__preview--tags">
178
+ ${meta.tags.map(
179
+ (tag) => html`
180
+ <blog-tag label=${tag.label} variant="brand"></blog-tag>
181
+ `
182
+ )}
183
+ </div>`
184
+ )}
185
+
186
+ ${this._specimen(
187
+ 'blog-avatar',
188
+ '--blog-byline-avatar-size · --radius-full · --color-blue-50 · --color-blue-700 · --blog-byline-fontSize · --typography-fontWeight-semibold',
189
+ html`<blog-avatar initials=${meta.byline.initials}></blog-avatar>`
190
+ )}
191
+
192
+ ${this._specimen(
193
+ 'blog-meta (author)',
194
+ '--blog-byline-fontSize · --typography-fontWeight-medium · --color-text-primary',
195
+ html`<blog-meta type="author" value=${meta.byline.author}></blog-meta>`
196
+ )}
197
+
198
+ ${this._specimen(
199
+ 'blog-meta (date)',
200
+ '--blog-byline-fontSize · --blog-meta-fontWeight · --blog-date-color',
201
+ html`<blog-meta
202
+ type="date"
203
+ value=${meta.byline.date}
204
+ datetime=${meta.byline.datetime}
205
+ ></blog-meta>`
206
+ )}
207
+
208
+ ${this._specimen(
209
+ 'blog-meta (reading-time)',
210
+ '--blog-byline-fontSize · --blog-meta-fontWeight · --blog-reading-time-color',
211
+ html`<blog-meta
212
+ type="reading-time"
213
+ value=${meta.byline.readingTime}
214
+ ></blog-meta>`
215
+ )}
216
+
217
+ ${this._specimen(
218
+ 'blog-meta (updated)',
219
+ '--blog-byline-fontSize · --blog-meta-fontWeight · --blog-date-color · "Updated" prefix',
220
+ html`<blog-meta
221
+ type="updated"
222
+ value=${meta.byline.updated}
223
+ datetime=${meta.byline.updatedDatetime}
224
+ ></blog-meta>`
225
+ )}
226
+
227
+ ${this._specimen(
228
+ 'article-meta row',
229
+ 'Publication date · reading time · last update — hero first row',
230
+ html`<div class="specimen__preview--row">
231
+ <blog-meta
232
+ type="date"
233
+ value=${meta.byline.date}
234
+ datetime=${meta.byline.datetime}
235
+ ></blog-meta>
236
+ <blog-meta type="separator"></blog-meta>
237
+ <blog-meta
238
+ type="reading-time"
239
+ value=${meta.byline.readingTime}
240
+ ></blog-meta>
241
+ <blog-meta type="separator"></blog-meta>
242
+ <blog-meta
243
+ type="updated"
244
+ value=${meta.byline.updated}
245
+ datetime=${meta.byline.updatedDatetime}
246
+ ></blog-meta>
247
+ </div>`
248
+ )}
249
+
250
+ ${this._specimen(
251
+ 'blog-meta (separator)',
252
+ '--blog-meta-separator-fontSize · 16px · --blog-meta-separator-color · --color-text-muted · semibold',
253
+ html`<blog-meta type="separator"></blog-meta>`
254
+ )}
255
+
256
+ ${this._specimen(
257
+ 'blog-title',
258
+ '--blog-hero-title-fontFamily · --blog-hero-title-fontSize · --blog-hero-title-fontWeight · --blog-hero-title-lineHeight · --blog-hero-title-color · --typography-display-md-letterSpacing',
259
+ html`<blog-title>${title}</blog-title>`
260
+ )}
261
+
262
+ ${this._specimen(
263
+ 'blog-authors',
264
+ '--blog-authors-label-* · --blog-tag-brand-* · flex-wrap pills',
265
+ html`<blog-authors .authors=${meta.authors}></blog-authors>`
266
+ )}
267
+
268
+ ${this._specimen(
269
+ 'blog-abstract',
270
+ '--blog-hero-abstract-border-* · --blog-prose-color · --typography-body-lead-* · regular (not italic)',
271
+ html`<blog-abstract .paragraphs=${abstract}></blog-abstract>`
272
+ )}
273
+
274
+ ${this._specimen(
275
+ 'blog-subhead (Word section labels)',
276
+ '--blog-prose-subhead-* · 24px condensed semibold · parser: bold lines in body (not H2)',
277
+ html`<blog-subhead>${subhead.text}</blog-subhead>`
278
+ )}
279
+
280
+ ${this._specimen(
281
+ 'blog-key-takeaways',
282
+ '--blog-takeaways-padding-inline · --spacing-24 · header --spacing-20/--spacing-12 · body --spacing-16/--spacing-24 · item-gap --spacing-12 · list-indent --spacing-16',
283
+ html`<blog-key-takeaways
284
+ title=${takeaways.title}
285
+ .items=${takeaways.items}
286
+ ></blog-key-takeaways>`
287
+ )}
288
+
289
+ ${this._specimen(
290
+ 'blog-table-of-contents',
291
+ '--blog-takeaways-title-* · --blog-takeaways-header/divider padding · title: Contents · auto-built from subheads',
292
+ html`<blog-table-of-contents .items=${tocItems}></blog-table-of-contents>`
293
+ )}
294
+
295
+ ${this._specimen(
296
+ 'blog-table-of-contents (collapsible)',
297
+ 'mobile inline TOC · collapsed by default · + expands anchor list',
298
+ html`<blog-table-of-contents collapsible .items=${tocItems}></blog-table-of-contents>`
299
+ )}
300
+
301
+ ${this._specimen(
302
+ 'blog-product-upsell',
303
+ '--blog-card-* · --blog-newsletter-* · --blog-takeaways-title-* · --button-size-small-* · --button-primary-* · --button-shared-*',
304
+ html`<blog-product-upsell></blog-product-upsell>`
305
+ )}
306
+
307
+ ${this._specimen(
308
+ 'blog-report-promo',
309
+ '--blog-card-* · --blog-newsletter-* · --blog-takeaways-title-* · --button-cta-* · --button-size-small-*',
310
+ html`<div class="specimen__preview--sidebar"><blog-report-promo></blog-report-promo></div>`
311
+ )}
312
+
313
+ ${this._specimen(
314
+ 'blog-related-label',
315
+ '--blog-related-label-fontSize · italic · colon-ended intro before related links',
316
+ html`<blog-related-label>${relatedLabel.text}</blog-related-label>`
317
+ )}
318
+
319
+ ${this._specimen(
320
+ 'blog-standout (related links)',
321
+ '--typography-fontSize-xl · --typography-fontWeight-bold · italic — Word bold+italic paragraphs',
322
+ html`<blog-standout href=${relatedStandout.href}
323
+ >${relatedStandout.text}</blog-standout
324
+ >`
325
+ )}
326
+
327
+ ${this._specimen(
328
+ 'blog-featured-image (hero cover)',
329
+ '--blog-hero-featured-borderRadius · --radius-2xl · --blog-hero-featured-aspect-ratio · 16/9',
330
+ html`<blog-featured-image
331
+ src=${meta.featuredImage}
332
+ alt=${meta.featuredImageAlt}
333
+ ></blog-featured-image>`
334
+ )}
335
+
336
+ ${this._specimen(
337
+ 'blog-image (inline content)',
338
+ '--blog-content-image-borderRadius · --blog-content-image-background · natural aspect ratio',
339
+ html`<figure>
340
+ <blog-image
341
+ src="../assets/articles/fbi-crypto-scam-1.jpeg"
342
+ alt="Social media scam statistics"
343
+ ></blog-image>
344
+ <blog-caption>Screenshot or supporting visual in article body</blog-caption>
345
+ </figure>`
346
+ )}
347
+
348
+ ${this._specimen(
349
+ 'blog-code-block (PrismJS)',
350
+ '--blog-code-block-background · --color-neutral-50 · --blog-code-block-fontFamily · mono · --radius-2xl · vendor/prism/prism.js',
351
+ html`<blog-code-block
352
+ language="javascript"
353
+ caption="Example: flag a suspicious investment URL before opening it"
354
+ code=${`const suspiciousUrl = 'https://fake-crypto-platform.example/invest';
355
+
356
+ if (/^(https?:\\/\\/)?[\\w.-]+\\.(xyz|top|click)$/i.test(suspiciousUrl)) {
357
+ console.warn('Unverified domain — treat as a potential scam.');
358
+ }`}
359
+ ></blog-code-block>`
360
+ )}
361
+
362
+ ${this._specimen(
363
+ 'blog-blockquote (Word quote pattern)',
364
+ '--blog-quote-* tokens · intro paragraph + blockquote · parser merges “…” lines after labels ending with :',
365
+ html`${quote.intro
366
+ ? html`<blog-paragraph>${quote.intro}</blog-paragraph>`
367
+ : null}<blog-blockquote>${quote.text}</blog-blockquote>`
368
+ )}
369
+
370
+ ${this._specimen(
371
+ 'blog-related-articles (4 cards — full width)',
372
+ 'desktop: meta → title → excerpt · mobile: 2:1 image, no excerpt, 2 cards + See all',
373
+ html`<div class="specimen__preview--related">
374
+ <blog-related-articles
375
+ .items=${relatedArticles}
376
+ ></blog-related-articles>
377
+ </div>`
378
+ )}
379
+
380
+ ${this._specimen(
381
+ 'blog-related-articles (3 cards — page default)',
382
+ 'page default · lg+ 12-col grid · mobile: compact cards with dividers',
383
+ html`<div class="specimen__preview--related">
384
+ <blog-related-articles
385
+ .items=${relatedArticlesThree}
386
+ ></blog-related-articles>
387
+ </div>`
388
+ )}
389
+
390
+ ${this._specimen(
391
+ 'blog-newsletter-signup',
392
+ '--blog-newsletter-* · italic heading emphasis · description · privacy disclaimer · envelope icon · presentational form',
393
+ html`<blog-newsletter-signup></blog-newsletter-signup>`
394
+ )}
395
+ </div>
396
+ </section>
397
+ `;
398
+ }
399
+ }
400
+
401
+ customElements.define('blog-atoms-showcase', BlogAtomsShowcase);
@@ -0,0 +1,73 @@
1
+ import { css, html, unsafeCSS } from 'lit';
2
+ import { BlogElement } from '../base/blog-element.js';
3
+ import '../atoms/blog-related-articles.js';
4
+ import { relatedArticlesThree } from '../data/related-articles.js';
5
+ import { minWidth } from '../breakpoints.js';
6
+
7
+ /**
8
+ * Page shell — article (3/4 + 1/4 layout inside blog-article) + Related articles below.
9
+ */
10
+ export class BlogPageLayout extends BlogElement {
11
+ static properties = {
12
+ relatedItems: { type: Array },
13
+ };
14
+
15
+ constructor() {
16
+ super();
17
+ this.relatedItems = relatedArticlesThree;
18
+ }
19
+
20
+ static styles = [
21
+ ...BlogElement.styles,
22
+ css`
23
+ :host {
24
+ display: block;
25
+ }
26
+
27
+ .page-layout {
28
+ max-width: var(--layout-page-max-width);
29
+ margin: 0 auto;
30
+ padding: var(--blog-hero-padding);
31
+ padding-top: var(--spacing-48);
32
+ }
33
+
34
+ .page-layout__main {
35
+ min-width: 0;
36
+ }
37
+
38
+ .page-layout__related {
39
+ margin-top: var(--spacing-48);
40
+ }
41
+
42
+ ${unsafeCSS(minWidth('lg'))} {
43
+ .page-layout__related {
44
+ margin-top: var(--spacing-64);
45
+ }
46
+ }
47
+ `,
48
+ ];
49
+
50
+ _relatedLayoutClass() {
51
+ const count = Math.min(this.relatedItems.length, 4);
52
+ return count >= 3 ? 'page-layout__related--full' : 'page-layout__related--main';
53
+ }
54
+
55
+ render() {
56
+ return html`
57
+ <div class="page-layout">
58
+ <div class="page-layout__main">
59
+ <slot></slot>
60
+ </div>
61
+ <div
62
+ class="page-layout__related ${this._relatedLayoutClass()}"
63
+ >
64
+ <blog-related-articles
65
+ .items=${this.relatedItems}
66
+ ></blog-related-articles>
67
+ </div>
68
+ </div>
69
+ `;
70
+ }
71
+ }
72
+
73
+ customElements.define('blog-page-layout', BlogPageLayout);
@@ -0,0 +1,101 @@
1
+ import { html } from 'lit';
2
+ import '../atoms/blog-abstract.js';
3
+ import '../atoms/blog-authors.js';
4
+ import '../atoms/blog-avatar.js';
5
+ import '../atoms/blog-blockquote.js';
6
+ import '../atoms/blog-caption.js';
7
+ import '../atoms/blog-code-block.js';
8
+ import '../atoms/blog-dek.js';
9
+ import '../atoms/blog-featured-image.js';
10
+ import '../atoms/blog-heading.js';
11
+ import '../atoms/blog-image.js';
12
+ import '../atoms/blog-key-takeaways.js';
13
+ import '../atoms/blog-link.js';
14
+ import '../atoms/blog-meta.js';
15
+ import '../atoms/blog-newsletter-signup.js';
16
+ import '../atoms/blog-paragraph.js';
17
+ import '../atoms/blog-product-upsell.js';
18
+ import '../atoms/blog-related-articles.js';
19
+ import '../atoms/blog-related-label.js';
20
+ import '../atoms/blog-report-promo.js';
21
+ import '../atoms/blog-standout.js';
22
+ import '../atoms/blog-subhead.js';
23
+ import '../atoms/blog-table-of-contents.js';
24
+ import '../atoms/blog-tag.js';
25
+ import '../atoms/blog-title.js';
26
+ import '../shell/blog-shell.js';
27
+ import './blog-article.js';
28
+ import './blog-atoms-showcase.js';
29
+ import './blog-page-layout.js';
30
+
31
+ export default {
32
+ title : 'Blog/Pages',
33
+ tags : ['autodocs'],
34
+ parameters: {
35
+ layout: 'fullscreen',
36
+ docs : {
37
+ description: {
38
+ component: 'Full blog page compositions — article layout, atoms showcase, and page shell.'
39
+ }
40
+ }
41
+ }
42
+ };
43
+
44
+ // ─── Article Page ──────────────────────────────────────────────────────────────
45
+
46
+ export const ArticlePage = {
47
+ name : 'Article Page',
48
+ render: () => html`
49
+ <blog-shell>
50
+ <blog-page-layout>
51
+ <blog-article></blog-article>
52
+ </blog-page-layout>
53
+ </blog-shell>
54
+ `,
55
+ parameters: {
56
+ docs: {
57
+ description: {
58
+ story: 'Full article page — `blog-shell` wraps `blog-page-layout` which contains `blog-article`. Includes related articles below the article body.'
59
+ }
60
+ }
61
+ }
62
+ };
63
+
64
+ // ─── Atoms Showcase ───────────────────────────────────────────────────────────
65
+
66
+ export const AtomsShowcase = {
67
+ name : 'Atoms Showcase',
68
+ render: () => html`
69
+ <blog-shell>
70
+ <blog-atoms-showcase></blog-atoms-showcase>
71
+ </blog-shell>
72
+ `,
73
+ parameters: {
74
+ docs: {
75
+ description: {
76
+ story: 'Full atoms showcase page — all blog components rendered in a single scrollable page inside `blog-shell`.'
77
+ }
78
+ }
79
+ }
80
+ };
81
+
82
+ // ─── Full Page (original index.html) ──────────────────────────────────────────
83
+
84
+ export const FullPage = {
85
+ name : 'Full Page — index.html',
86
+ render: () => html`
87
+ <blog-shell>
88
+ <blog-atoms-showcase></blog-atoms-showcase>
89
+ <blog-page-layout>
90
+ <blog-article></blog-article>
91
+ </blog-page-layout>
92
+ </blog-shell>
93
+ `,
94
+ parameters: {
95
+ docs: {
96
+ description: {
97
+ story: 'Replica exactă a `index.html` original — atoms showcase urmat de article page layout.'
98
+ }
99
+ }
100
+ }
101
+ };
@@ -0,0 +1,18 @@
1
+ import { html } from 'lit';
2
+ import '../atoms/blog-link.js';
3
+
4
+ /** @param {Array<{ type: 'text' | 'link', text: string, href?: string }>} content */
5
+ export function renderInlineContent(content) {
6
+ if (!content?.length) {
7
+ return null;
8
+ }
9
+
10
+ return content.map((segment) => {
11
+ if (segment.type === 'link' && segment.href) {
12
+ return html`<blog-link inline href=${segment.href}
13
+ >${segment.text}</blog-link
14
+ >`;
15
+ }
16
+ return segment.text;
17
+ });
18
+ }
@@ -0,0 +1,43 @@
1
+ /** Sample “Most Popular” sidebar items — replace with CMS data later. */
2
+ export const popularArticles = [
3
+ {
4
+ title : 'How bail bond scams are using AI to target families',
5
+ href : '#',
6
+ date : '6 Jun 2024',
7
+ datetime: '2024-06-06',
8
+ image : '/articles/fbi-crypto-scam-cover.png',
9
+ imageAlt: 'Handcuffs on a dark surface'
10
+ },
11
+ {
12
+ title : 'How to spot phishing emails before they reach your inbox',
13
+ href : '#',
14
+ date : '12 May 2024',
15
+ datetime: '2024-05-12',
16
+ image : '/articles/fbi-crypto-scam-1.jpeg',
17
+ imageAlt: 'Phishing scam illustration'
18
+ },
19
+ {
20
+ title : 'Ransomware in 2026: what changed and what stayed the same',
21
+ href : '#',
22
+ date : '28 Apr 2024',
23
+ datetime: '2024-04-28',
24
+ image : '/articles/fbi-crypto-scam-cover.png',
25
+ imageAlt: 'Ransomware security concept'
26
+ },
27
+ {
28
+ title : 'Why your router is the weakest link in home security',
29
+ href : '#',
30
+ date : '15 Mar 2024',
31
+ datetime: '2024-03-15',
32
+ image : '/articles/fbi-crypto-scam-1.jpeg',
33
+ imageAlt: 'Home router on a desk'
34
+ },
35
+ {
36
+ title : 'Deepfake scams are targeting video calls — here is what to watch for',
37
+ href : '#',
38
+ date : '2 Feb 2024',
39
+ datetime: '2024-02-02',
40
+ image : '/articles/fbi-crypto-scam-cover.png',
41
+ imageAlt: 'Video call on a laptop screen'
42
+ }
43
+ ];
@@ -0,0 +1,58 @@
1
+ /** Sample related articles — replace with CMS / recommendation data later. */
2
+
3
+ const relatedArticleBase = [
4
+ {
5
+ title : 'How bail bond scams are using AI to target families',
6
+ href : '#',
7
+ principle: 'Scams',
8
+ excerpt:
9
+ 'Scammers are impersonating relatives in distress with voice-cloned calls and urgent wire requests.',
10
+ date : '6 Jun 2024',
11
+ datetime : '2024-06-06',
12
+ readingTime: '5 min read',
13
+ image : '/articles/fbi-crypto-scam-cover.png',
14
+ imageAlt : 'Handcuffs on a dark surface'
15
+ },
16
+ {
17
+ title : 'How to spot phishing emails before they reach your inbox',
18
+ href : '#',
19
+ principle: 'Phishing',
20
+ excerpt:
21
+ 'Red flags in sender addresses, links, and attachments that security teams watch for every day.',
22
+ date : '12 May 2024',
23
+ datetime : '2024-05-12',
24
+ readingTime: '4 min read',
25
+ image : '/articles/fbi-crypto-scam-1.jpeg',
26
+ imageAlt : 'Phishing scam illustration'
27
+ },
28
+ {
29
+ title : 'Ransomware in 2026: what changed and what stayed the same',
30
+ href : '#',
31
+ principle: 'Ransomware',
32
+ excerpt:
33
+ 'Double extortion, faster dwell times, and why backups alone are no longer enough.',
34
+ date : '28 Apr 2024',
35
+ datetime : '2024-04-28',
36
+ readingTime: '6 min read',
37
+ image : '/articles/fbi-crypto-scam-cover.png',
38
+ imageAlt : 'Ransomware security concept'
39
+ },
40
+ {
41
+ title : 'Why your router is the weakest link in home security',
42
+ href : '#',
43
+ principle: 'Digital Privacy',
44
+ excerpt:
45
+ 'Default credentials, outdated firmware, and simple steps to harden the gateway to your network.',
46
+ date : '15 Mar 2024',
47
+ datetime : '2024-03-15',
48
+ readingTime: '3 min read',
49
+ image : '/articles/fbi-crypto-scam-1.jpeg',
50
+ imageAlt : 'Home router on a desk'
51
+ }
52
+ ];
53
+
54
+ /** Default page dataset — four related cards, full-width grid on desktop. */
55
+ export const relatedArticles = relatedArticleBase.slice(0, 4);
56
+
57
+ /** Three-item fallback — main-column width only when sidebar is present. */
58
+ export const relatedArticlesThree = relatedArticleBase.slice(0, 3);