@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.
- package/CHANGELOG.md +15 -0
- package/package.json +4 -4
- package/src/blog/assets/articles/fbi-crypto-scam-1.jpeg +0 -0
- package/src/blog/assets/articles/fbi-crypto-scam-cover.png +0 -0
- package/src/blog/assets/fonts/IBMPlexSans-Bold.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSans-BoldItalic.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSans-Italic.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSans-Light.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSans-LightItalic.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSans-Medium.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSans-MediumItalic.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSans-Regular.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSans-SemiBold.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSans-SemiBoldItalic.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSansCondensed-Bold.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSansCondensed-BoldItalic.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSansCondensed-Italic.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSansCondensed-Light.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSansCondensed-LightItalic.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSansCondensed-Medium.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSansCondensed-MediumItalic.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSansCondensed-Regular.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSansCondensed-SemiBold.woff2 +0 -0
- package/src/blog/assets/fonts/IBMPlexSansCondensed-SemiBoldItalic.woff2 +0 -0
- package/src/blog/assets/global-scam-intelligence-report-2026.png +0 -0
- package/src/blog/assets/psd-4.jpg +0 -0
- package/src/blog/src/articles/FBI warns of crypto investment scam that sends couriers to collect victims' cash-editedAB.docx +0 -0
- package/src/blog/src/articles/FBI-crypto-scam-courier-cash.png +0 -0
- package/src/blog/src/articles/fbi-crypto-scam.js +324 -0
- package/src/blog/src/articles/~$I warns of crypto investment scam that sends couriers to collect victims' cash-editedAB.docx +0 -0
- package/src/blog/src/atoms/blog-abstract.js +51 -0
- package/src/blog/src/atoms/blog-atoms.stories.js +519 -0
- package/src/blog/src/atoms/blog-authors.js +70 -0
- package/src/blog/src/atoms/blog-avatar.js +44 -0
- package/src/blog/src/atoms/blog-blockquote.js +92 -0
- package/src/blog/src/atoms/blog-caption.js +31 -0
- package/src/blog/src/atoms/blog-code-block.js +89 -0
- package/src/blog/src/atoms/blog-dek.js +30 -0
- package/src/blog/src/atoms/blog-featured-image.js +65 -0
- package/src/blog/src/atoms/blog-heading.js +63 -0
- package/src/blog/src/atoms/blog-image.js +50 -0
- package/src/blog/src/atoms/blog-key-takeaways.js +101 -0
- package/src/blog/src/atoms/blog-link.js +68 -0
- package/src/blog/src/atoms/blog-meta.js +87 -0
- package/src/blog/src/atoms/blog-most-popular.js +146 -0
- package/src/blog/src/atoms/blog-newsletter-signup.js +342 -0
- package/src/blog/src/atoms/blog-paragraph.js +61 -0
- package/src/blog/src/atoms/blog-product-upsell.js +168 -0
- package/src/blog/src/atoms/blog-related-articles.js +326 -0
- package/src/blog/src/atoms/blog-related-label.js +30 -0
- package/src/blog/src/atoms/blog-report-promo.js +250 -0
- package/src/blog/src/atoms/blog-standout.js +78 -0
- package/src/blog/src/atoms/blog-subhead.js +32 -0
- package/src/blog/src/atoms/blog-table-of-contents.js +447 -0
- package/src/blog/src/atoms/blog-tag.js +86 -0
- package/src/blog/src/atoms/blog-title.js +31 -0
- package/src/blog/src/base/blog-element.js +10 -0
- package/src/blog/src/breakpoints.js +23 -0
- package/src/blog/src/content/blog-article.js +736 -0
- package/src/blog/src/content/blog-atoms-showcase.js +401 -0
- package/src/blog/src/content/blog-page-layout.js +73 -0
- package/src/blog/src/content/blog-pages.stories.js +101 -0
- package/src/blog/src/content/render-inline-content.js +18 -0
- package/src/blog/src/data/popular-articles.js +43 -0
- package/src/blog/src/data/related-articles.js +58 -0
- package/src/blog/src/index.js +104 -0
- package/src/blog/src/lib/article-toc.js +206 -0
- package/src/blog/src/lib/load-prism.js +23 -0
- package/src/blog/src/shell/blog-shell.js +31 -0
- package/src/blog/src/styles/prism-theme.js +65 -0
- package/src/blog/src/tokens/blog.js +406 -0
- package/src/blog/src/tokens/core.js +811 -0
- package/src/blog/src/tokens/fonts.css +241 -0
- package/src/blog/src/tokens/index.js +27 -0
- package/src/blog/src/tokens/legacy.js +58 -0
- package/src/blog/tokens/blog.js +406 -0
- package/src/blog/tokens/core.js +811 -0
- package/src/blog/tokens/fonts.css +241 -0
- package/src/blog/tokens/index.js +27 -0
- package/src/blog/tokens/legacy.js +58 -0
- package/src/blog/tokens.css +1512 -0
- package/src/blog/tokens.js +1 -0
- package/src/blog/vendor/prism/prism.css +120 -0
- 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
|
+
}
|