@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,447 @@
1
+ import { css, html, nothing } from 'lit';
2
+ import { BlogElement } from '../base/blog-element.js';
3
+ import {
4
+ findArticleSubhead,
5
+ getTocScrollLock,
6
+ resolveActiveTocId,
7
+ scrollToHeading,
8
+ setTocScrollLock,
9
+ watchTocScrollEnd
10
+ } from '../lib/article-toc.js';
11
+
12
+ /**
13
+ * Article table of contents — anchor links to in-body subheads.
14
+ * Highlights the active chapter with a blue marker while scrolling.
15
+ *
16
+ * @property {string} title
17
+ * @property {Array<{ id: string, label: string }>} items
18
+ * @property {boolean} collapsible — collapsed by default; mobile inline TOC
19
+ */
20
+ export class BlogTableOfContents extends BlogElement {
21
+ static properties = {
22
+ title : { type: String },
23
+ items : { type: Array },
24
+ collapsible: { type: Boolean, reflect: true },
25
+ activeId : { type: String, state: true },
26
+ isOpen : { type: Boolean, state: true }
27
+ };
28
+
29
+ constructor() {
30
+ super();
31
+ this.title = 'Contents';
32
+ this.items = [];
33
+ this.collapsible = false;
34
+ this.isOpen = false;
35
+ this.activeId = '';
36
+ /** @type {Array<{ id: string, el: Element }>} */
37
+ this._headings = [];
38
+ /** @type {(() => void) | null} */
39
+ this._onScroll = null;
40
+ /** @type {number} */
41
+ this._scrollRaf = 0;
42
+ /** @type {number} */
43
+ this._spyRetry = 0;
44
+ }
45
+
46
+ static styles = [
47
+ ...BlogElement.styles,
48
+ css`
49
+ :host {
50
+ display: block;
51
+ }
52
+
53
+ .toc {
54
+ border: var(--blog-takeaways-border-width) solid
55
+ var(--blog-takeaways-border-color);
56
+ border-radius: var(--blog-takeaways-borderRadius);
57
+ background: var(--blog-takeaways-background);
58
+ overflow: hidden;
59
+ }
60
+
61
+ .toc__header {
62
+ padding: var(--blog-takeaways-header-padding-block)
63
+ var(--blog-takeaways-padding-inline);
64
+ padding-bottom: var(--blog-takeaways-header-padding-bottom);
65
+ }
66
+
67
+ :host([collapsible]) .toc__header {
68
+ padding-bottom: var(--blog-takeaways-header-padding-block);
69
+ }
70
+
71
+ .toc__title {
72
+ margin: 0;
73
+ font-family: var(--blog-takeaways-title-fontFamily);
74
+ font-size: var(--blog-takeaways-title-fontSize);
75
+ font-weight: var(--blog-takeaways-title-fontWeight);
76
+ line-height: var(--blog-takeaways-title-lineHeight);
77
+ color: var(--blog-takeaways-title-color);
78
+ }
79
+
80
+ .toc__toggle {
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: space-between;
84
+ gap: var(--spacing-12);
85
+ width: 100%;
86
+ margin: 0;
87
+ padding: 0;
88
+ border: 0;
89
+ background: transparent;
90
+ cursor: pointer;
91
+ text-align: left;
92
+ font: inherit;
93
+ color: inherit;
94
+ }
95
+
96
+ .toc__toggle:focus-visible {
97
+ outline: var(--focus-outline-traditional);
98
+ outline-offset: var(--focus-outline-offset);
99
+ }
100
+
101
+ .toc__toggle-icon {
102
+ flex-shrink: 0;
103
+ display: flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ width: var(--blog-toc-toggle-size);
107
+ height: var(--blog-toc-toggle-size);
108
+ font-family: var(--typography-fontFamily-sans);
109
+ font-size: var(--blog-toc-toggle-fontSize);
110
+ font-weight: var(--typography-fontWeight-normal);
111
+ line-height: var(--typography-lineHeight-none);
112
+ color: var(--blog-toc-toggle-color);
113
+ transition: color var(--transition-duration-fast)
114
+ var(--transition-easing-ease-out);
115
+ }
116
+
117
+ .toc__toggle:hover .toc__toggle-icon,
118
+ .toc__toggle:focus-visible .toc__toggle-icon {
119
+ color: var(--blog-toc-toggle-hover-color);
120
+ }
121
+
122
+ .toc__divider {
123
+ margin: 0;
124
+ border: 0;
125
+ border-top: var(--blog-takeaways-divider-width) solid
126
+ var(--blog-takeaways-divider-color);
127
+ }
128
+
129
+ :host([collapsible]) .toc--collapsed .toc__divider,
130
+ :host([collapsible]) .toc--collapsed .toc__list {
131
+ display: none;
132
+ }
133
+
134
+ .toc__list {
135
+ margin: 0;
136
+ padding: var(--blog-takeaways-body-padding-top)
137
+ var(--blog-takeaways-padding-inline)
138
+ var(--blog-takeaways-body-padding-bottom);
139
+ list-style: none;
140
+ }
141
+
142
+ .toc__item {
143
+ position: relative;
144
+ }
145
+
146
+ .toc__item::before {
147
+ content: '';
148
+ position: absolute;
149
+ left: calc(
150
+ -1 * (var(--blog-toc-marker-size) + var(--blog-toc-marker-gap))
151
+ );
152
+ top: var(--blog-toc-marker-offset-top);
153
+ width: var(--blog-toc-marker-size);
154
+ height: var(--blog-toc-marker-size);
155
+ border-radius: var(--radius-full);
156
+ background: transparent;
157
+ transform: translateY(-50%);
158
+ transition: background-color var(--transition-duration-fast)
159
+ var(--transition-easing-ease-out);
160
+ }
161
+
162
+ .toc__item[data-active]::before {
163
+ background: var(--blog-toc-marker-color);
164
+ }
165
+
166
+ .toc__list li + li {
167
+ margin-top: var(--blog-toc-item-gap);
168
+ }
169
+
170
+ .toc__link {
171
+ display: block;
172
+ font-family: var(--typography-fontFamily-sans);
173
+ font-size: var(--blog-toc-link-fontSize);
174
+ font-weight: var(--blog-toc-link-fontWeight);
175
+ line-height: var(--blog-toc-link-lineHeight);
176
+ color: var(--blog-toc-link-color);
177
+ text-decoration: none;
178
+ transition: color var(--transition-duration-fast)
179
+ var(--transition-easing-ease-out);
180
+ }
181
+
182
+ .toc__item[data-active] .toc__link {
183
+ color: var(--blog-toc-link-active-color);
184
+ font-weight: var(--typography-fontWeight-medium);
185
+ }
186
+
187
+ .toc__link:hover {
188
+ color: var(--blog-toc-link-hover-color);
189
+ text-decoration: underline;
190
+ }
191
+
192
+ .toc__link:focus-visible {
193
+ outline: var(--focus-outline-traditional);
194
+ outline-offset: var(--focus-outline-offset);
195
+ }
196
+ `
197
+ ];
198
+
199
+ connectedCallback() {
200
+ super.connectedCallback();
201
+ this._spyRetry = requestAnimationFrame(() => this._attachScrollSpy());
202
+ }
203
+
204
+ updated(changed) {
205
+ if (changed.has('items')) {
206
+ this._detachScrollSpy();
207
+ this._spyRetry = requestAnimationFrame(() => this._attachScrollSpy());
208
+ }
209
+ }
210
+
211
+ disconnectedCallback() {
212
+ super.disconnectedCallback();
213
+ cancelAnimationFrame(this._spyRetry);
214
+ this._detachScrollSpy();
215
+ }
216
+
217
+ _findHeading(id) {
218
+ const root = this.getRootNode();
219
+
220
+ if (root && root !== this) {
221
+ const inRoot =
222
+ root instanceof Document
223
+ ? document.getElementById(id)
224
+ : (root.getElementById?.(id) ??
225
+ root.querySelector?.(`blog-subhead[id="${CSS.escape(id)}"]`));
226
+
227
+ if (inRoot) {
228
+ return inRoot;
229
+ }
230
+ }
231
+
232
+ return findArticleSubhead(id);
233
+ }
234
+
235
+ _attachScrollSpy() {
236
+ const root = this.getRootNode();
237
+
238
+ if (!root || root === this) {
239
+ return;
240
+ }
241
+
242
+ this._headings = this.items
243
+ .map(({ id }) => {
244
+ const el = this._findHeading(id);
245
+ return el ? { id, el } : null;
246
+ })
247
+ .filter(Boolean);
248
+
249
+ if (!this._headings.length) {
250
+ return;
251
+ }
252
+
253
+ this._onScroll = () => {
254
+ if (this._scrollRaf) {
255
+ return;
256
+ }
257
+
258
+ this._scrollRaf = requestAnimationFrame(() => {
259
+ this._scrollRaf = 0;
260
+ this._updateActiveId();
261
+ });
262
+ };
263
+
264
+ window.addEventListener('scroll', this._onScroll, { passive: true });
265
+ this._updateActiveId();
266
+ this._scrollToInitialHash();
267
+ }
268
+
269
+ _scrollToInitialHash() {
270
+ const hashId = window.location.hash.slice(1);
271
+
272
+ if (!hashId || !this._headings.some(({ id }) => id === hashId)) {
273
+ return;
274
+ }
275
+
276
+ requestAnimationFrame(() => {
277
+ const target = this._findHeading(hashId);
278
+
279
+ if (!target) {
280
+ return;
281
+ }
282
+
283
+ setTocScrollLock(hashId);
284
+ scrollToHeading(target, this._scrollOffset(), 'auto');
285
+ this.activeId = hashId;
286
+ watchTocScrollEnd((lockedId) => {
287
+ if (lockedId) {
288
+ this.activeId = lockedId;
289
+ }
290
+ });
291
+ });
292
+ }
293
+
294
+ _detachScrollSpy() {
295
+ if (this._onScroll) {
296
+ window.removeEventListener('scroll', this._onScroll);
297
+ this._onScroll = null;
298
+ }
299
+
300
+ if (this._scrollRaf) {
301
+ cancelAnimationFrame(this._scrollRaf);
302
+ this._scrollRaf = 0;
303
+ }
304
+
305
+ this._headings = [];
306
+ }
307
+
308
+ _scrollOffset() {
309
+ const heading = this._headings[0]?.el;
310
+
311
+ if (heading) {
312
+ const margin = getComputedStyle(heading).scrollMarginTop.trim();
313
+ const parsed = Number.parseFloat(margin);
314
+
315
+ if (Number.isFinite(parsed)) {
316
+ return parsed;
317
+ }
318
+ }
319
+
320
+ return this._readTokenPx('--blog-toc-scroll-offset', 48);
321
+ }
322
+
323
+ _readingLineOffset() {
324
+ return this._readTokenPx('--blog-toc-reading-line-offset', 128);
325
+ }
326
+
327
+ _readTokenPx(token, fallback) {
328
+ const value = getComputedStyle(this).getPropertyValue(token)
329
+ .trim();
330
+ const parsed = Number.parseFloat(value);
331
+
332
+ return Number.isFinite(parsed) ? parsed : fallback;
333
+ }
334
+
335
+ _updateActiveId() {
336
+ if (!this._headings.length) {
337
+ return;
338
+ }
339
+
340
+ const lockedId = getTocScrollLock();
341
+
342
+ if (lockedId) {
343
+ if (this.activeId !== lockedId) {
344
+ this.activeId = lockedId;
345
+ }
346
+ return;
347
+ }
348
+
349
+ const active = resolveActiveTocId(this._headings, this._readingLineOffset());
350
+
351
+ if (this.activeId !== active) {
352
+ this.activeId = active;
353
+ }
354
+ }
355
+
356
+ _toggleExpanded() {
357
+ this.isOpen = !this.isOpen;
358
+ }
359
+
360
+ /** @param {Event} event @param {string} id */
361
+ _onLinkClick(event, id) {
362
+ event.preventDefault();
363
+
364
+ const target = this._findHeading(id);
365
+
366
+ if (!target) {
367
+ return;
368
+ }
369
+
370
+ setTocScrollLock(id);
371
+ this.activeId = id;
372
+ scrollToHeading(target, this._scrollOffset());
373
+ history.pushState(null, '', `#${id}`);
374
+ watchTocScrollEnd((lockedId) => {
375
+ if (lockedId) {
376
+ this.activeId = lockedId;
377
+ }
378
+ });
379
+ }
380
+
381
+ _renderHeader() {
382
+ if (this.collapsible) {
383
+ return html`
384
+ <header class="toc__header">
385
+ <button
386
+ type="button"
387
+ class="toc__toggle"
388
+ part="toggle"
389
+ aria-expanded=${this.isOpen ? 'true' : 'false'}
390
+ aria-controls="toc-list"
391
+ @click=${this._toggleExpanded}
392
+ >
393
+ <span class="toc__title" part="title">${this.title}</span>
394
+ <span class="toc__toggle-icon" part="toggle-icon" aria-hidden="true"
395
+ >${this.isOpen ? '−' : '+'}</span
396
+ >
397
+ </button>
398
+ </header>
399
+ `;
400
+ }
401
+
402
+ return html`
403
+ <header class="toc__header">
404
+ <h2 class="toc__title" part="title">${this.title}</h2>
405
+ </header>
406
+ `;
407
+ }
408
+
409
+ render() {
410
+ if (!this.items.length) {
411
+ return null;
412
+ }
413
+
414
+ return html`
415
+ <nav
416
+ class="toc ${this.collapsible && !this.isOpen ? 'toc--collapsed' : ''}"
417
+ part="toc"
418
+ aria-label=${this.title}
419
+ >
420
+ ${this._renderHeader()}
421
+ <hr class="toc__divider" />
422
+ <ol id="toc-list" class="toc__list" part="list">
423
+ ${this.items.map(
424
+ (item) => html`
425
+ <li
426
+ class="toc__item"
427
+ part="item"
428
+ data-active=${item.id === this.activeId || nothing}
429
+ >
430
+ <a
431
+ class="toc__link"
432
+ part="link"
433
+ href="#${item.id}"
434
+ aria-current=${item.id === this.activeId ? 'location' : nothing}
435
+ @click=${(event) => this._onLinkClick(event, item.id)}
436
+ >${item.label}</a
437
+ >
438
+ </li>
439
+ `
440
+ )}
441
+ </ol>
442
+ </nav>
443
+ `;
444
+ }
445
+ }
446
+
447
+ customElements.define('blog-table-of-contents', BlogTableOfContents);
@@ -0,0 +1,86 @@
1
+ import { css, html } from 'lit';
2
+ import { BlogElement } from '../base/blog-element.js';
3
+
4
+ /**
5
+ * Blog category/tag chip.
6
+ *
7
+ * @attr {string} label - Visible text
8
+ * @attr {string} href - Optional link URL
9
+ * @attr {'default'|'brand'} variant - Visual style
10
+ */
11
+ export class BlogTag extends BlogElement {
12
+ static properties = {
13
+ label : { type: String },
14
+ href : { type: String },
15
+ variant: { type: String, reflect: true }
16
+ };
17
+
18
+ constructor() {
19
+ super();
20
+ this.label = '';
21
+ this.href = '';
22
+ this.variant = 'default';
23
+ }
24
+
25
+ static styles = [
26
+ ...BlogElement.styles,
27
+ css`
28
+ :host {
29
+ display: inline-block;
30
+ }
31
+
32
+ .tag {
33
+ display: inline-flex;
34
+ align-items: center;
35
+ gap: var(--blog-tag-gap);
36
+ padding: var(--blog-tag-padding);
37
+ font-family: var(--typography-fontFamily-sans);
38
+ font-size: var(--blog-tag-fontSize);
39
+ font-weight: var(--blog-tag-fontWeight);
40
+ line-height: var(--blog-tag-lineHeight);
41
+ border-radius: var(--blog-tag-borderRadius);
42
+ border: var(--blog-tag-border-width) solid
43
+ var(--blog-tag-default-border-color);
44
+ background: var(--blog-tag-default-background);
45
+ color: var(--blog-tag-default-color);
46
+ text-decoration: none;
47
+ transition: background-color var(--transition-duration-fast)
48
+ var(--transition-easing-smooth),
49
+ color var(--transition-duration-fast) var(--transition-easing-smooth),
50
+ border-color var(--transition-duration-fast)
51
+ var(--transition-easing-smooth);
52
+ }
53
+
54
+ :host([variant='brand']) .tag {
55
+ background: var(--blog-tag-brand-background);
56
+ color: var(--blog-tag-brand-color);
57
+ border-color: var(--blog-tag-brand-border-color);
58
+ }
59
+
60
+ :host([variant='brand']) .tag:hover {
61
+ background: var(--blog-tag-brand-hover-background);
62
+ color: var(--blog-tag-brand-hover-color);
63
+ }
64
+
65
+ :host(:not([variant='brand'])) .tag:hover {
66
+ background: var(--blog-tag-hover-background);
67
+ color: var(--blog-tag-hover-color);
68
+ }
69
+
70
+ .tag:focus-visible {
71
+ outline: var(--focus-outline-traditional);
72
+ outline-offset: var(--focus-outline-offset);
73
+ }
74
+ `
75
+ ];
76
+
77
+ render() {
78
+ if (this.href) {
79
+ return html`<a class="tag" href=${this.href} part="tag">${this.label}</a>`;
80
+ }
81
+
82
+ return html`<span class="tag" part="tag">${this.label}</span>`;
83
+ }
84
+ }
85
+
86
+ customElements.define('blog-tag', BlogTag);
@@ -0,0 +1,31 @@
1
+ import { css, html } from 'lit';
2
+ import { BlogElement } from '../base/blog-element.js';
3
+
4
+ /** Article H1 — hero display title */
5
+ export class BlogTitle extends BlogElement {
6
+ static styles = [
7
+ ...BlogElement.styles,
8
+ css`
9
+ :host {
10
+ display: block;
11
+ }
12
+
13
+ h1 {
14
+ margin: 0;
15
+ font-family: var(--blog-hero-title-fontFamily);
16
+ font-size: var(--blog-hero-title-fontSize);
17
+ font-weight: var(--blog-hero-title-fontWeight);
18
+ font-style: normal;
19
+ line-height: var(--blog-hero-title-lineHeight);
20
+ letter-spacing: var(--typography-display-md-letterSpacing);
21
+ color: var(--blog-hero-title-color);
22
+ }
23
+ `
24
+ ];
25
+
26
+ render() {
27
+ return html`<h1 part="title"><slot></slot></h1>`;
28
+ }
29
+ }
30
+
31
+ customElements.define('blog-title', BlogTitle);
@@ -0,0 +1,10 @@
1
+ import { LitElement } from 'lit';
2
+ import { blogTokens, tokens } from '../tokens/index.js';
3
+
4
+ /**
5
+ * Base class for blog web components.
6
+ * Applies core + blog tokens to :host for shadow DOM.
7
+ */
8
+ export class BlogElement extends LitElement {
9
+ static styles = [tokens, blogTokens];
10
+ }
@@ -0,0 +1,23 @@
1
+ /** Breakpoint values in px — use in JS; CSS media queries cannot use var() */
2
+ export const breakpoints = {
3
+ xs: 0,
4
+ sm: 576,
5
+ md: 768,
6
+ lg: 992,
7
+ xl: 1200,
8
+ xxl: 1400,
9
+ };
10
+
11
+ /** Returns a min-width media query string for use in Lit css templates */
12
+ export function minWidth(bp) {
13
+ const value = breakpoints[bp];
14
+ if (value === undefined) {
15
+ throw new Error(`Unknown breakpoint: ${bp}`);
16
+ }
17
+ return `@media (min-width: ${value}px)`;
18
+ }
19
+
20
+ /** Match media query in JS */
21
+ export function matchesBreakpoint(bp) {
22
+ return window.matchMedia(`(min-width: ${breakpoints[bp]}px)`).matches;
23
+ }