fix: inline stack theme

This commit is contained in:
2026-07-31 15:38:57 +08:00
parent b59ffa7703
commit 3423112533
335 changed files with 16340 additions and 1 deletions
+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-archive" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<rect x="3" y="4" width="18" height="4" rx="2" />
<path d="M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-10" />
<line x1="10" y1="12" x2="14" y2="12" />
</svg>

After

Width:  |  Height:  |  Size: 432 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-back" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M9 11l-4 4l4 4m-4 -4h11a4 4 0 0 0 0 -8h-1" />
</svg>

After

Width:  |  Height:  |  Size: 338 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-chevron-left" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<polyline points="15 6 9 12 15 18" />
</svg>

After

Width:  |  Height:  |  Size: 323 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-github" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5" />
</svg>

After

Width:  |  Height:  |  Size: 603 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-twitter" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M22 4.01c-1 .49 -1.98 .689 -3 .99c-1.121 -1.265 -2.783 -1.335 -4.38 -.737s-2.643 2.06 -2.62 3.737v1c-3.245 .083 -6.135 -1.395 -8 -4c0 0 -4.182 7.433 4 11c-1.872 1.247 -3.739 2.088 -6 2c3.308 1.803 6.913 2.423 10.034 1.517c3.58 -1.04 6.522 -3.723 7.651 -7.742a13.84 13.84 0 0 0 .497 -3.753c-.002 -.249 1.51 -2.772 1.818 -4.013z" />
</svg>

After

Width:  |  Height:  |  Size: 638 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-hash" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<line x1="5" y1="9" x2="19" y2="9" />
<line x1="5" y1="15" x2="19" y2="15" />
<line x1="11" y1="4" x2="7" y2="20" />
<line x1="17" y1="4" x2="13" y2="20" />
</svg>

After

Width:  |  Height:  |  Size: 440 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-left"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 6l-6 6l6 6" /></svg>

After

Width:  |  Height:  |  Size: 336 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 6l6 6l-6 6" /></svg>

After

Width:  |  Height:  |  Size: 336 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-clock" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="12" cy="12" r="9" />
<polyline points="12 7 12 12 15 15" />
</svg>

After

Width:  |  Height:  |  Size: 352 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-copyright" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="12" cy="12" r="9" />
<path d="M14.5 9a3.5 4 0 1 0 0 6" />
</svg>

After

Width:  |  Height:  |  Size: 354 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-calendar-time" width="56" height="56" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M11.795 21h-6.795a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v4" />
<circle cx="18" cy="18" r="4" />
<path d="M15 3v4" />
<path d="M7 3v4" />
<path d="M3 11h16" />
<path d="M18 16.496v1.504l1 1" />
</svg>

After

Width:  |  Height:  |  Size: 508 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-dots"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M19 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg>

After

Width:  |  Height:  |  Size: 459 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-hash" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<line x1="5" y1="9" x2="19" y2="9" />
<line x1="5" y1="15" x2="19" y2="15" />
<line x1="11" y1="4" x2="7" y2="20" />
<line x1="17" y1="4" x2="13" y2="20" />
</svg>

After

Width:  |  Height:  |  Size: 440 B

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-home" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<polyline points="5 12 3 12 12 3 21 12 19 12" />
<path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7" />
<path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6" />
</svg>

After

Width:  |  Height:  |  Size: 441 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-infinity" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M9.828 9.172a4 4 0 1 0 0 5.656 a10 10 0 0 0 2.172 -2.828a10 10 0 0 1 2.172 -2.828 a4 4 0 1 1 0 5.656a10 10 0 0 1 -2.172 -2.828a10 10 0 0 0 -2.172 -2.828" />
</svg>

After

Width:  |  Height:  |  Size: 447 B

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-language" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M4 5h7" />
<path d="M9 3v2c0 4.418 -2.239 8 -5 8" />
<path d="M5 9c-.003 2.144 2.952 3.908 6.7 4" />
<path d="M12 20l4 -9l4 9" />
<path d="M19.1 18h-6.2" />
</svg>

After

Width:  |  Height:  |  Size: 467 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5" />
<path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5" />
</svg>

After

Width:  |  Height:  |  Size: 418 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-messages" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M21 14l-3 -3h-7a1 1 0 0 1 -1 -1v-6a1 1 0 0 1 1 -1h9a1 1 0 0 1 1 1v10" />
<path d="M14 15v2a1 1 0 0 1 -1 1h-7l-3 3v-10a1 1 0 0 1 1 -1h2" />
</svg>

After

Width:  |  Height:  |  Size: 431 B

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-rss" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="5" cy="19" r="1" />
<path d="M4 4a16 16 0 0 1 16 16" />
<path d="M4 11a9 9 0 0 1 9 9" />
</svg>

After

Width:  |  Height:  |  Size: 381 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="10" cy="10" r="7" />
<line x1="21" y1="21" x2="15" y2="15" />
</svg>

After

Width:  |  Height:  |  Size: 355 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-tag" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M11 3L20 12a1.5 1.5 0 0 1 0 2L14 20a1.5 1.5 0 0 1 -2 0L3 11v-4a4 4 0 0 1 4 -4h4" />
<circle cx="9" cy="9" r="2" />
</svg>

After

Width:  |  Height:  |  Size: 402 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-toggle-left" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="8" cy="12" r="2" />
<rect x="2" y="6" width="20" height="12" rx="6" />
</svg>

After

Width:  |  Height:  |  Size: 369 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-toggle-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="16" cy="12" r="2" />
<rect x="2" y="6" width="20" height="12" rx="6" />
</svg>

After

Width:  |  Height:  |  Size: 371 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-user" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="12" cy="7" r="4" />
<path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
</svg>

After

Width:  |  Height:  |  Size: 366 B

+17
View File
@@ -0,0 +1,17 @@
$breakpoints: (
sm: 640px,
md: 768px,
lg: 1024px,
xl: 1280px,
2xl: 1536px,
);
@mixin respond($breakpoint) {
@if not map-has-key($breakpoints, $breakpoint) {
@warn "'#{$breakpoint}' is not a valid breakpoint";
} @else {
@media (min-width: map-get($breakpoints, $breakpoint)) {
@content;
}
}
}
+1
View File
@@ -0,0 +1 @@
/* Place your custom SCSS in HUGO_SITE_FOLDER/assets/scss/custom.scss */
+349
View File
@@ -0,0 +1,349 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}
+31
View File
@@ -0,0 +1,31 @@
a {
text-decoration: none;
color: var(--accent-color);
&:hover {
color: var(--accent-color-darker);
}
&.link {
box-shadow: 0px -2px 0px rgba(var(--link-background-color), var(--link-background-opacity)) inset;
transition: all 0.3s ease;
&:hover {
box-shadow: 0px calc(-1rem * var(--article-line-height)) 0px rgba(var(--link-background-color), var(--link-background-opacity-hover)) inset;
}
}
}
.section-title {
text-transform: uppercase;
margin-top: 0;
margin-bottom: var(--spacing-sm);
display: block;
font-size: 1.6rem;
font-weight: bold;
color: var(--body-text-color);
a {
color: var(--body-text-color);
}
}
+99
View File
@@ -0,0 +1,99 @@
.container {
margin-left: auto;
margin-right: auto;
.left-sidebar {
order: -3;
width: 100%;
max-width: var(--left-sidebar-max-width);
}
.right-sidebar {
order: -1;
width: 100%;
max-width: var(--right-sidebar-max-width);
/// Display right sidebar when min-width: lg
@include respond(lg) {
display: flex;
}
}
&.extended {
@include respond(md) {
max-width: 1024px;
--left-sidebar-max-width: 25%;
--right-sidebar-max-width: 30%;
}
@include respond(lg) {
max-width: 1280px;
--left-sidebar-max-width: 20%;
--right-sidebar-max-width: 30%;
}
@include respond(xl) {
max-width: 1536px;
--left-sidebar-max-width: 15%;
--right-sidebar-max-width: 25%;
}
}
&.compact {
@include respond(md) {
--left-sidebar-max-width: 25%;
max-width: 768px;
}
@include respond(lg) {
max-width: 1024px;
--left-sidebar-max-width: 20%;
}
@include respond(xl) {
max-width: 1280px;
}
}
}
.flex {
display: flex;
flex-direction: row;
&.on-phone--column {
flex-direction: column;
@include respond(md) {
flex-direction: row;
}
}
.full-width {
width: 100%;
}
}
main.main {
order: -2;
min-width: 0;
max-width: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
gap: var(--section-separation);
@include respond(md) {
padding-top: var(--main-top-padding);
}
}
.main-container {
min-height: 100vh;
align-items: flex-start;
padding: 0 var(--container-padding);
gap: var(--section-separation);
padding-top: var(--main-top-padding);
@include respond(md) {
padding-top: 0;
}
}
+5
View File
@@ -0,0 +1,5 @@
@mixin card-style {
background-color: var(--card-background);
box-shadow: var(--shadow-l1);
border-radius: var(--card-border-radius);
}
@@ -0,0 +1,400 @@
/* Default article style */
.article-list {
display: flex;
flex-direction: column;
gap: var(--section-separation);
article {
display: flex;
flex-direction: column;
@include card-style;
overflow: hidden;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: var(--shadow-l2);
}
.article-image {
img {
width: 100%;
height: var(--article-image-height);
object-fit: cover;
}
}
}
}
.article-details {
display: flex;
flex-direction: column;
justify-content: center;
padding: var(--card-padding);
gap: var(--spacing-md);
}
.article-title {
font-family: var(--article-font-family);
font-weight: 600;
margin: 0;
color: var(--card-text-color-main);
font-size: 2.2rem;
@include respond(xl) {
font-size: 2.4rem;
}
a {
color: var(--card-text-color-main);
&:hover {
opacity: 0.8;
}
}
}
.article-subtitle {
font-weight: normal;
color: var(--card-text-color-secondary);
line-height: 1.5;
margin: 0;
font-size: 1.75rem;
@include respond(xl) {
font-size: 2rem;
}
}
.article-title-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
}
.article-meta {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
font-size: 1.4rem;
svg {
vertical-align: middle;
width: 20px;
height: 20px;
stroke-width: 1.33;
flex-shrink: 0;
}
&,
time,
a,
span {
font-size: 1.4rem;
color: var(--card-text-color-tertiary);
}
& > .inline-meta {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: var(--spacing-md);
}
}
.article-category,
.article-tags {
display: flex;
gap: var(--spacing-sm);
flex-wrap: wrap;
a {
color: var(--accent-color-text);
background-color: var(--accent-color);
padding: 8px 16px;
border-radius: var(--tag-border-radius);
display: inline-block;
font-size: 1.4rem;
transition: background-color 0.5s ease;
&:hover {
color: var(--accent-color-text);
background-color: var(--accent-color-darker);
}
}
}
/* Compact style article list */
.article-list--compact {
@include card-style;
--image-size: var(--article-compact-image-size);
article {
& > a {
display: flex;
align-items: center;
padding: var(--small-card-padding);
gap: var(--spacing-md);
}
&:not(:last-of-type) {
border-bottom: 1.5px solid var(--card-separator-color);
}
.article-details {
flex-grow: 1;
padding: 0;
min-height: var(--image-size);
gap: var(--spacing-sm);
}
.article-title {
margin: 0;
font-size: 1.6rem;
@include respond(md) {
font-size: 1.8rem;
}
}
.article-image {
img {
width: var(--image-size);
height: var(--image-size);
object-fit: cover;
}
}
.article-preview {
font-size: 1.4rem;
color: var(--card-text-color-tertiary);
line-height: 1.5;
}
}
}
/* Tile style article list */
.article-list--tile {
article {
position: relative;
width: var(--article-tile-width);
height: var(--article-tile-height);
@include card-style;
overflow: hidden;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: var(--shadow-l2);
}
&.has-image {
.article-details {
background-color: var(--article-tile-overlay-bg);
}
.article-title {
color: var(--article-tile-text-color);
}
}
.article-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.article-details {
border-radius: var(--card-border-radius);
position: relative;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
z-index: 2;
padding: 15px;
@include respond(sm) {
padding: 20px;
}
}
.article-title {
font-size: 2rem;
font-weight: 500;
color: var(--card-text-color-main);
@include respond(sm) {
font-size: 2.2rem;
}
}
}
}
.mermaid:not([data-processed]) {
background: var(--card-background);
min-height: 150px;
border-radius: 8px;
position: relative;
overflow: hidden;
font-size: 0 !important;
line-height: 0;
color: transparent;
&::after {
visibility: visible;
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 28px;
height: 28px;
margin: -14px 0 0 -14px;
border: 3px solid var(--card-background);
border-top-color: var(--accent-color, #3273dc);
border-radius: 50%;
animation: mermaid-spinner 0.8s linear infinite;
}
}
@keyframes mermaid-spinner {
to {
transform: rotate(360deg);
}
}
// Mermaid diagram wrapper with expand toolbar
.mermaid-wrapper {
position: relative;
margin: 1rem 0;
.mermaid {
display: flex;
justify-content: center;
svg {
display: block;
max-width: 100%;
height: auto;
}
}
}
.mermaid-toolbar {
position: absolute;
top: 0.75rem;
right: 0.75rem;
display: flex;
gap: 0.5rem;
z-index: 10;
button {
padding: 0.5rem 1rem;
background: var(--mermaid-toolbar-btn-bg);
border: 2px solid var(--mermaid-toolbar-btn-text);
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
color: var(--mermaid-toolbar-btn-text);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
transition: all 0.2s;
&:hover {
background: var(--mermaid-toolbar-btn-text);
color: var(--card-background);
transform: scale(1.05);
}
}
}
// Mermaid fullscreen modal
.mermaid-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: var(--mermaid-modal-bg);
z-index: 9999;
&.active {
display: flex;
flex-direction: column;
}
}
.mermaid-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: rgba(255, 255, 255, 0.1);
}
.mermaid-modal-controls {
display: flex;
gap: 0.5rem;
}
.mermaid-modal-controls button,
.mermaid-modal-close {
padding: 0.5rem 1rem;
background: var(--mermaid-modal-btn-bg);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
color: var(--card-text-color-main);
&:hover {
background: var(--mermaid-modal-btn-hover);
}
}
.mermaid-modal-body {
flex: 1;
overflow: hidden;
position: relative;
}
.mermaid-modal-content {
position: absolute;
top: 1rem;
left: 1rem;
right: 1rem;
bottom: 1rem;
border-radius: 8px;
overflow: hidden;
background: var(--mermaid-modal-content-bg);
}
.mermaid-panzoom-container {
display: inline-block;
transform-origin: 0 0;
visibility: hidden;
&.ready {
visibility: visible;
}
svg {
display: block;
}
}
// Off-screen container for pre-rendering alternate theme
.mermaid-offscreen {
position: absolute;
left: -9999px;
visibility: hidden;
width: 800px;
}
@@ -0,0 +1,21 @@
html {
font-size: 62.5%;
overflow-y: scroll;
scrollbar-gutter: stable;
}
* {
box-sizing: border-box;
}
body {
background: var(--body-background);
margin: 0;
font-family: var(--base-font-family);
font-size: 1.6rem;
text-autospace: ideograph-alpha ideograph-numeric punctuation insert;
text-spacing-trim: trim-start allow-end;
-ms-text-autospace: ideograph-alpha;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@@ -0,0 +1,80 @@
@import "../../breakpoints";
@import "../../mixins";
.artalk-container {
@include card-style;
padding: 5px;
@include respond(md) {
padding: var(--card-padding);
}
}
.artalk {
--at-color-bg: var(--card-background);
--at-color-font: var(--card-text-color-main);
--at-color-sub: var(--card-text-color-secondary);
--at-color-main: var(--accent-color);
--at-color-border: var(--card-separator-color);
--at-border-radius: var(--card-border-radius);
--at-color-link: var(--accent-color);
}
.artalk a {
color: var(--at-color-link);
text-decoration: none;
transition: color 0.3s ease;
&:hover {
color: var(--accent-color-darker);
}
}
.artalk .atk-content a,
.artalk .atk-editor-plug-preview a {
color: var(--accent-color);
box-shadow: 0px -2px 0px rgba(var(--link-background-color), var(--link-background-opacity)) inset;
transition: all 0.3s ease;
&:hover {
color: var(--accent-color-darker);
box-shadow: 0px calc(-1rem * var(--article-line-height)) 0px rgba(var(--link-background-color), var(--link-background-opacity-hover)) inset;
}
}
[data-scheme="dark"] .artalk {
--at-color-main: var(--accent-color);
--at-color-link: var(--accent-color);
--at-color-bg: var(--card-background);
--at-color-bg-transl: rgba(66, 66, 66, 0.95);
--at-color-bg-grey: rgba(255, 255, 255, 0.08);
--at-color-bg-grey-transl: rgba(255, 255, 255, 0.06);
--at-color-bg-light: rgba(255, 255, 255, 0.08);
--at-color-border: var(--card-separator-color);
--at-color-font: var(--card-text-color-main);
--at-color-deep: var(--card-text-color-main);
--at-color-sub: var(--card-text-color-secondary);
--at-color-meta: var(--card-text-color-secondary);
--at-color-grey: var(--card-text-color-secondary);
--at-color-gradient: linear-gradient(
180deg,
transparent,
var(--card-background)
);
}
.artalk code {
font-family: var(--code-font-family);
}
.artalk .atk-avatar img {
border-radius: var(--card-border-radius);
}
[data-scheme="dark"] .artalk .atk-avatar img {
filter: brightness(0.75);
}
[data-scheme="dark"] .artalk .atk-send-btn {
color: var(--accent-color-text);
}
@@ -0,0 +1,394 @@
.disqus-container {
background-color: var(--card-background);
border-radius: var(--card-border-radius);
box-shadow: var(--shadow-l1);
padding: var(--card-padding);
}
#dsqjs * {
margin: 0;
padding: 0
}
#dsqjs a {
text-decoration: none;
color: #076dd0
}
#dsqjs .dsqjs-hide {
display: none!important
}
#dsqjs .dsqjs-disabled {
cursor: not-allowed;
opacity: .5
}
#dsqjs #dsqjs-msg {
text-align: center;
margin-top: 4px;
margin-bottom: 4px;
font-size: 14px
}
#dsqjs #dsqjs-msg .dsqjs-msg-btn {
cursor: pointer
}
#dsqjs .dsqjs-bullet {
line-height: 1.4;
margin: 0 2px
}
#dsqjs .dsqjs-bullet:after {
color: #c2c6cc;
content: "·";
font-weight: 700
}
#dsqjs .dsqjs-clearfix:after,#dsqjs .dsqjs-clearfix:before {
display: table;
content: "";
line-height: 0;
clear: both
}
#dsqjs .dsqjs-nav {
position: relative;
margin: 0 0 20px;
border-bottom: 2px solid #e7e9ee
}
#dsqjs ol,#dsqjs ul {
list-style: none;
list-style-type: none
}
#dsqjs .dsqjs-no-comment {
text-align: center;
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
overflow: hidden;
color: #2a2e2e;
margin-bottom: 6px
}
#dsqjs .dsqjs-nav-tab {
float: left;
text-transform: capitalize;
font-size: 15px;
padding: 12px 8px;
color: #656c7a;
display: block;
margin: 0 15px 0 0;
font-weight: 700;
line-height: 1;
position: relative;
transition: all .2s ease-in-out
}
#dsqjs .dsqjs-nav-tab:last-child {
margin: 0
}
#dsqjs .dsqjs-tab-active {
color: #2a2e2e
}
#dsqjs .dsqjs-tab-active>span:after {
content: " ";
display: block;
height: 2px;
background-color: #076dd0!important;
position: absolute;
bottom: -5px;
left: 0;
right: 0
}
#dsqjs .dsqjs-post-list .dsqjs-post-item {
position: relative;
margin-bottom: 16px
}
#dsqjs .dsqjs-post-list .dsqjs-post-avatar {
float: left;
margin-right: 10px;
position: relative;
background: #dbdfe4;
padding: 0;
display: block;
border-radius: 4px
}
#dsqjs .dsqjs-post-list .dsqjs-post-avatar img {
width: 44px;
height: 44px;
display: block;
border-radius: 4px
}
#dsqjs .dsqjs-post-list .dsqjs-post-header {
line-height: 1;
font-size: 14px;
margin-bottom: 3px
}
#dsqjs .dsqjs-post-list .dsqjs-post-header .dsqjs-post-author {
color: #656c7a;
font-weight: 700
}
#dsqjs .dsqjs-post-list .dsqjs-post-header .dsqjs-admin-badge {
color: #fff;
background: #687a86;
padding: 1px 3px;
margin-left: 4px;
font-size: 12px;
line-height: 1;
font-weight: 700;
border-radius: 3px;
display: inline-block;
position: relative;
top: -1px;
left: 1px
}
#dsqjs .dsqjs-post-list .dsqjs-post-header .dsqjs-meta {
display: inline-block;
font-size: 12px;
color: #656c7a
}
#dsqjs .dsqjs-post-body {
font-size: 15px;
line-height: 1.5;
word-wrap: break-word;
overflow: hidden;
color: #2a2e2e
}
#dsqjs .dsqjs-post-body code {
padding: .2em .4em;
margin: 0;
font-size: 85%;
background: #f5f5f5;
color: inherit;
border-radius: 3px
}
#dsqjs .dsqjs-post-body pre {
padding: .5em;
overflow: auto;
font-size: 85%;
line-height: 1.45;
border-radius: 3px;
background: #f5f5f5;
margin: .5em 0
}
#dsqjs .dsqjs-post-body blockquote {
padding: 0 .8em;
margin: .5em 0;
color: #6a737d;
border-left: .25em solid #dfe2e5
}
#dsqjs .dsqjs-post-body p:last-child {
margin: 0
}
#dsqjs .dsqjs-post-list.dsqjs-children>li {
margin-left: 30px
}
#dsqjs .dsqjs-post-list.dsqjs-children .dsqjs-post-avatar img {
width: 38px;
height: 38px
}
#dsqjs .dsqjs-load-more {
font-size: 14px;
font-weight: 400;
display: block;
text-align: center;
padding: 11px 14px;
margin: 0 0 24px;
background: #687a86;
color: #fff;
cursor: pointer
}
#dsqjs .dsqjs-load-more:hover {
opacity: .8
}
#dsqjs footer {
text-align: right;
line-height: 1.5;
padding-top: 10px;
padding-right: 10px;
border-top: 2px solid #e7e9ee;
margin-top: 12px;
font-weight: 700;
font-size: 16px;
color: #555
}
#dsqjs .dsqjs-disqus-logo {
background-image: url(https://c.disquscdn.com/next/embed/assets/img/sprite.654110a9206fd22f08cca0798e34a65e.png);
background-repeat: no-repeat;
display: inline-block;
background-size: 86px 40.5px;
height: 16.5px;
width: 86px;
}
#dsqjs .dsqjs-order {
display: flex;
float: right;
align-items: center;
margin-top: 10px;
margin-bottom: 12px
}
#dsqjs .dsqjs-order-radio {
display: none
}
#dsqjs .dsqjs-order-radio:checked+.dsqjs-order-label {
color: #fff;
background-color: #888
}
#dsqjs .dsqjs-order-label {
display: block;
height: 20px;
line-height: 20px;
margin-right: 10px;
font-size: 12px;
border-radius: 2px;
padding: 0 5px;
background-color: #dcdcdc;
cursor: pointer
}
#dsqjs p.dsqjs-has-more {
margin-bottom: 24px;
margin-left: 48px;
font-size: 13px;
line-height: 15px
}
#dsqjs p.dsqjs-has-more a.dsqjs-has-more-btn {
color: #656c7a;
text-decoration: underline;
cursor: pointer
}
@media (min-width: 768px) {
#dsqjs .dsqjs-post-list.dsqjs-children>li {
margin-left:48px
}
#dsqjs .dsqjs-post-list .dsqjs-post-avatar {
margin-right: 12px
}
#dsqjs .dsqjs-post-list .dsqjs-post-item {
margin-bottom: 20px
}
}
@media (min-width: 1024px) {
#dsqjs .dsqjs-post-list.dsqjs-children>li {
margin-left:60px
}
}
:root[data-scheme="light"] {
#dsqjs .dsqjs-disqus-logo {
background-position: 0 -7px;
}
}
:root[data-scheme="dark"] {
#dsqjs {
--t-s: rgba(255,255,255,0.9);
--alt: #3e4b5e;
--link-hover: #47a2e0;
--hover-bg: #3e4b5e;
--tag: #3e4b5e;
--border: #435266;
--pre: #3c495b;
--c-bg: #2f3947;
--code: #c3c7cb;
--kbd: #4e5f77;
--hl: #abb2bf;
--hlc: #808895;
--hlk: #c678dd;
--hln: #e06c75;
--hll: #56b6c2;
--hls: #98c379;
--hlt: #e6c07b;
--hlv: #d19a66;
--bg: #181c27;
--main: #252d38;
--t: rgba(255,255,255,0.86);
--t-l: rgba(255,255,255,0.66);
--logo: #fff;
--link: #38a3fd;
--title: rgba(255,255,255,0.92);
--fab: #364151;
--shadow: none;
}
#disqus_thread {
color: var(--body-text-color)
}
#dsqjs #dsqjs-msg {
color: var(--t)
}
#dsqjs a {
color:var(--link)
}
#dsqjs a:focus,#dsqjs a:hover {
color: var(--link-hover)
}
#dsqjs .dsqjs-disqus-logo {
background-position: 0 -24px;
}
#dsqjs .dsqjs-nav,#dsqjs footer {
border-color: var(--hlc)
}
#dsqjs .dsqjs-load-more,#dsqjs .dsqjs-load-more:hover,#dsqjs .dsqjs-nav-tab,#dsqjs .dsqjs-no-comment,#dsqjs .dsqjs-post-content {
color: var(--t)
}
#dsqjs .dsqjs-order-label {
background-color: var(--hlc)
}
#dsqjs .dsqjs-order-radio:checked+.dsqjs-order-label {
background-color: var(--kbd)
}
#dsqjs .dsqjs-tab-active>span:after {
background-color: #2e9fff
}
#dsqjs .dsqjs-footer,#dsqjs .dsqjs-meta {
color: var(--t-l)
}
#dsqjs .dsqjs-post-body blockquote {
border-color: var(--border)
}
}
@@ -0,0 +1,199 @@
.cookie-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
padding: var(--container-padding);
pointer-events: none;
&[aria-hidden="true"] {
display: none;
}
}
.cookie-banner__content {
max-width: 900px;
margin: 0 auto;
background: var(--card-background);
border-radius: var(--card-border-radius);
box-shadow: var(--shadow-l2);
padding: var(--card-padding);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
pointer-events: auto;
@include respond(md) {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: var(--spacing-lg);
}
}
.cookie-banner__text {
flex: 1;
min-width: 200px;
strong {
display: block;
color: var(--card-text-color-main);
font-size: 1.6rem;
margin-bottom: var(--spacing-xs);
}
p {
color: var(--card-text-color-secondary);
font-size: 1.4rem;
line-height: 1.6;
margin: 0;
}
}
.cookie-banner__actions {
display: flex;
gap: var(--spacing-sm);
flex-shrink: 0;
}
.cookie-banner__settings-link {
width: auto;
align-self: flex-start;
background: none;
border: none;
color: var(--accent-color);
font-size: 1.3rem;
cursor: pointer;
padding: 5px;
&:hover {
text-decoration: underline;
}
@include respond(md) {
margin-left: auto;
align-self: center;
}
}
.cookie-btn {
padding: 10px 20px;
border-radius: var(--tag-border-radius);
font-size: 1.4rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
&--primary {
background: var(--accent-color);
color: var(--accent-color-text);
&:hover {
background: var(--accent-color-darker);
}
}
&--secondary {
background: transparent;
color: var(--card-text-color-main);
border: 1.5px solid var(--card-separator-color);
&:hover {
background: var(--card-background-selected);
}
}
}
// Cookie settings panel
.cookie-settings {
padding-top: 20px;
border-top: 1.5px solid var(--card-separator-color);
&[aria-hidden="true"] {
display: none;
}
h3 {
color: var(--card-text-color-main);
font-size: 1.6rem;
margin: 0 0 var(--spacing-md) 0;
}
&__actions {
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
margin-top: var(--spacing-lg);
}
}
.cookie-category {
padding: var(--spacing-md) 0;
border-bottom: 1px solid var(--card-separator-color);
&:last-of-type {
border-bottom: none;
}
label {
display: flex;
align-items: center;
gap: var(--spacing-sm);
cursor: pointer;
input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--accent-color);
cursor: pointer;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
strong {
color: var(--card-text-color-main);
font-size: 1.5rem;
}
}
p {
color: var(--card-text-color-secondary);
font-size: 1.3rem;
margin: 8px 0 0 28px;
line-height: 1.5;
}
}
// Footer link for reopening settings
.cookie-settings-link {
display: inline-block;
color: var(--body-text-color);
font-size: 1.2rem;
cursor: pointer;
background: none;
border: none;
padding: 0;
&:hover {
color: var(--accent-color);
text-decoration: underline;
}
}
// Comments placeholder when consent not given
.consent-placeholder {
@include card-style;
padding: var(--card-padding);
text-align: center;
color: var(--card-text-color-secondary);
font-size: 1.4rem;
p {
margin: 0 0 var(--spacing-md) 0;
}
}
@@ -0,0 +1,30 @@
footer.site-footer {
padding: var(--spacing-lg) 0 var(--section-separation) 0;
font-size: 1.4rem;
line-height: 1.75;
&:before {
content: "";
display: block;
height: 3px;
width: 50px;
background: var(--body-text-color);
margin-bottom: var(--spacing-lg);
}
.copyright {
color: var(--accent-color);
font-weight: bold;
margin-bottom: var(--spacing-xs);
}
.powerby {
color: var(--body-text-color);
font-weight: normal;
font-size: 1.2rem;
a {
color: var(--body-text-color);
}
}
}
@@ -0,0 +1,434 @@
/* Background */
.chroma {
color: $color;
background-color: $background-color;
}
/* Other */
.chroma .x {
}
/* Error */
.chroma .err {
color: $error-color;
}
/* LineTableTD */
.chroma .lntd {
vertical-align: top;
padding: 0;
margin: 0;
border: 0;
}
/* LineTable */
.chroma .lntable {
border-spacing: 0;
padding: 0;
margin: 0;
border: 0;
width: 100%;
display: block;
> tbody {
display: block;
width: 100%;
> tr {
display: flex;
width: 100%;
> td:last-child {
overflow-x: auto;
}
}
}
}
/* LineHighlight */
.chroma .hl {
display: block;
width: 100%;
background-color: #ffffcc;
}
/* LineNumbersTable */
.chroma .lnt {
margin-right: 0;
user-select: none;
padding: 0 0.4em 0 0.4em;
color: #7f7f7f;
display: block;
}
/* Optimise code block selection experience */
span.line {
margin-left: 0.4em;
}
/* LineNumbers */
.chroma .ln {
margin-right: 0.4em;
padding: 0 0.4em 0 0.4em;
color: #7f7f7f;
}
/* Keyword */
.chroma .k {
color: $keyword-color;
}
/* KeywordConstant */
.chroma .kc {
color: $keyword-color;
}
/* KeywordDeclaration */
.chroma .kd {
color: $keyword-color;
}
/* KeywordNamespace */
.chroma .kn {
color: #f92672;
}
/* KeywordPseudo */
.chroma .kp {
color: $keyword-color;
}
/* KeywordReserved */
.chroma .kr {
color: $keyword-color;
}
/* KeywordType */
.chroma .kt {
color: $keyword-color;
}
/* Name */
.chroma .n {
color: $text-color;
}
/* NameAttribute */
.chroma .na {
color: $name-color;
}
/* NameBuiltin */
.chroma .nb {
color: $text-color;
}
/* NameBuiltinPseudo */
.chroma .bp {
color: $text-color;
}
/* NameClass */
.chroma .nc {
color: $name-color;
}
/* NameConstant */
.chroma .no {
color: $keyword-color;
}
/* NameDecorator */
.chroma .nd {
color: $name-color;
}
/* NameEntity */
.chroma .ni {
color: $text-color;
}
/* NameException */
.chroma .ne {
color: $name-color;
}
/* NameFunction */
.chroma .nf {
color: $name-color;
}
/* NameFunctionMagic */
.chroma .fm {
color: $text-color;
}
/* NameLabel */
.chroma .nl {
color: $text-color;
}
/* NameNamespace */
.chroma .nn {
color: $text-color;
}
/* NameOther */
.chroma .nx {
color: $name-color;
}
/* NameProperty */
.chroma .py {
color: $text-color;
}
/* NameTag */
.chroma .nt {
color: #f92672;
}
/* NameVariable */
.chroma .nv {
color: $text-color;
}
/* NameVariableClass */
.chroma .vc {
color: $text-color;
}
/* NameVariableGlobal */
.chroma .vg {
color: $text-color;
}
/* NameVariableInstance */
.chroma .vi {
color: $text-color;
}
/* NameVariableMagic */
.chroma .vm {
color: $text-color;
}
/* Literal */
.chroma .l {
color: #ae81ff;
}
/* LiteralDate */
.chroma .ld {
color: $literal-color;
}
/* LiteralString */
.chroma .s {
color: $literal-color;
}
/* LiteralStringAffix */
.chroma .sa {
color: $literal-color;
}
/* LiteralStringBacktick */
.chroma .sb {
color: $literal-color;
}
/* LiteralStringChar */
.chroma .sc {
color: $literal-color;
}
/* LiteralStringDelimiter */
.chroma .dl {
color: $literal-color;
}
/* LiteralStringDoc */
.chroma .sd {
color: $literal-color;
}
/* LiteralStringDouble */
.chroma .s2 {
color: $literal-color;
}
/* LiteralStringEscape */
.chroma .se {
color: #ae81ff;
}
/* LiteralStringHeredoc */
.chroma .sh {
color: $literal-color;
}
/* LiteralStringInterpol */
.chroma .si {
color: $literal-color;
}
/* LiteralStringOther */
.chroma .sx {
color: $literal-color;
}
/* LiteralStringRegex */
.chroma .sr {
color: $literal-color;
}
/* LiteralStringSingle */
.chroma .s1 {
color: $literal-color;
}
/* LiteralStringSymbol */
.chroma .ss {
color: $literal-color;
}
/* LiteralNumber */
.chroma .m {
color: #ae81ff;
}
/* LiteralNumberBin */
.chroma .mb {
color: #ae81ff;
}
/* LiteralNumberFloat */
.chroma .mf {
color: #ae81ff;
}
/* LiteralNumberHex */
.chroma .mh {
color: #ae81ff;
}
/* LiteralNumberInteger */
.chroma .mi {
color: #ae81ff;
}
/* LiteralNumberIntegerLong */
.chroma .il {
color: #ae81ff;
}
/* LiteralNumberOct */
.chroma .mo {
color: #ae81ff;
}
/* Operator */
.chroma .o {
color: #f92672;
}
/* OperatorWord */
.chroma .ow {
color: #f92672;
}
/* Punctuation */
.chroma .p {
color: $text-color;
}
/* Comment */
.chroma .c {
color: #75715e;
}
/* CommentHashbang */
.chroma .ch {
color: #75715e;
}
/* CommentMultiline */
.chroma .cm {
color: #75715e;
}
/* CommentSingle */
.chroma .c1 {
color: #75715e;
}
/* CommentSpecial */
.chroma .cs {
color: #75715e;
}
/* CommentPreproc */
.chroma .cp {
color: #75715e;
}
/* CommentPreprocFile */
.chroma .cpf {
color: #75715e;
}
/* Generic */
.chroma .g {
}
/* GenericDeleted */
.chroma .gd {
color: #f92672;
}
/* GenericEmph */
.chroma .ge {
font-style: italic;
}
/* GenericError */
.chroma .gr {
}
/* GenericHeading */
.chroma .gh {
}
/* GenericInserted */
.chroma .gi {
color: $name-color;
}
/* GenericOutput */
.chroma .go {
}
/* GenericPrompt */
.chroma .gp {
}
/* GenericStrong */
.chroma .gs {
font-weight: bold;
}
/* GenericSubheading */
.chroma .gu {
color: #75715e;
}
/* GenericTraceback */
.chroma .gt {
}
/* GenericUnderline */
.chroma .gl {
}
/* TextWhitespace */
.chroma .w {
}
@@ -0,0 +1,14 @@
/*
* Style: monokai
* https://xyproto.github.io/splash/docs/monokai.html
*/
$color: #f8f8f2;
$background-color: #272822;
$error-color: #bb0064;
$keyword-color: #66d9ef;
$text-color: $color;
$name-color: #a6e22e;
$literal-color: #e6db74;
@import "common.scss";
@@ -0,0 +1,14 @@
/*
* Style: monokailight
* https://xyproto.github.io/splash/docs/monokailight.html
*/
$color: #272822;
$background-color: #fafafa;
$error-color: #960050;
$keyword-color: #00a8c8;
$text-color: #111111;
$name-color: #75af00;
$literal-color: #d88200;
@import "common.scss";
@@ -0,0 +1,6 @@
.not-found-card {
background-color: var(--card-background);
box-shadow: var(--shadow-l1);
border-radius: var(--card-border-radius);
padding: var(--card-padding);
}
@@ -0,0 +1,486 @@
.article-page {
&.hide-sidebar-sm .left-sidebar {
display: none;
@include respond(md) {
display: inherit;
}
}
.main-article {
background: var(--card-background);
border-radius: var(--card-border-radius);
box-shadow: var(--shadow-l1);
overflow: hidden;
.article-header {
.article-image {
img {
height: auto;
width: 100%;
max-height: 50vh;
object-fit: cover;
}
}
.article-details {
padding: var(--card-padding);
padding-bottom: 0;
}
}
.article-content {
margin: var(--card-padding) 0;
color: var(--card-text-color-main);
.footnotes {
font-family: var(--base-font-family);
}
img {
max-width: 100%;
height: auto;
}
}
.article-footer {
margin: var(--card-padding);
gap: var(--spacing-2xl);
}
}
}
.toc-nav {
overflow-x: auto;
max-height: 75vh;
ol,
ul {
margin: 0;
padding: 0;
}
ol {
list-style-type: none;
counter-reset: item;
li a:first-of-type::before {
counter-increment: item;
content: counters(item, ".") ". ";
font-weight: bold;
margin-right: 5px;
}
}
&>ul {
padding: 0 1em;
}
li {
margin: var(--spacing-md) 0 var(--spacing-md) var(--spacing-lg);
padding: var(--spacing-xs);
&>ol,
&>ul {
margin-top: var(--spacing-sm);
padding-left: var(--spacing-sm);
margin-bottom: calc(var(--spacing-xs) * -1);
&>li:last-child {
margin-bottom: 0;
}
}
}
li.active-class>a {
border-left: var(--heading-border-size) solid var(--accent-color);
font-weight: bold;
}
ul li.active-class>a {
display: block;
}
@function repeat($str, $n) {
$result: "";
@for $_ from 0 to $n {
$result: $result + $str;
}
@return $result;
}
// Support up to 6 levels of indentation for lists in ToCs
@for $i from 0 to 5 {
&>ul #{repeat("> li > ul", $i)}>li.active-class>a {
$n: 25 + $i * 35;
margin-left: calc(-#{$n}px - 1em);
padding-left: calc(#{$n}px + 1em - var(--heading-border-size));
}
&>ol #{repeat("> li > ol", $i)}>li.active-class>a {
$n: 9 + $i * 35;
margin-left: calc(-#{$n}px - 1em);
padding-left: calc(#{$n}px + 1em - var(--heading-border-size));
display: block;
}
}
}
.widget--toc,
.article-toc {
display: flex;
flex-direction: column;
color: var(--card-text-color-main);
}
.widget--toc {
background-color: var(--card-background);
border-radius: var(--card-border-radius);
box-shadow: var(--shadow-l1);
overflow: hidden;
}
.article-toc {
padding: 0 var(--card-padding);
margin-top: var(--spacing-lg);
@include respond(lg) {
display: none;
}
details {
background-color: var(--card-background);
border-radius: var(--card-border-radius);
box-shadow: var(--shadow-l2);
&[open] {
.article-toc-title {
svg {
transform: rotate(90deg);
}
}
}
summary {
list-style: none;
padding: var(--spacing-md) var(--spacing-lg);
cursor: pointer;
&::-webkit-details-marker {
display: none;
}
.article-toc-title {
display: flex;
align-items: center;
gap: var(--spacing-sm);
text-transform: uppercase;
font-size: 1.4rem;
font-weight: bold;
color: var(--card-text-color-tertiary);
svg {
width: 20px;
height: 20px;
stroke-width: 1.33;
transition: transform 0.3s ease;
}
}
}
}
}
.article-content {
font-family: var(--article-font-family);
font-size: var(--article-font-size);
padding: 0 var(--card-padding);
line-height: var(--article-line-height);
&>p {
margin: 1.5em 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-inline-start: calc((var(--card-padding)) * -1);
padding-inline-start: calc(var(--card-padding) - var(--heading-border-size));
border-inline-start: var(--heading-border-size) solid var(--accent-color);
position: relative;
a.header-anchor {
transition: opacity 0.3s ease;
opacity: 0;
position: absolute;
left: 0;
width: var(--card-padding);
text-align: center;
color: var(--accent-color);
&:before {
content: "#";
}
}
&:hover,
&:focus {
a.header-anchor {
opacity: 1;
}
}
}
figure {
text-align: center;
figcaption {
font-size: 1.4rem;
color: var(--card-text-color-secondary);
}
}
blockquote {
position: relative;
margin: 1.5em 0;
border-inline-start: var(--blockquote-border-size) solid var(--card-separator-color);
padding: var(--spacing-md) calc(var(--card-padding) - var(--blockquote-border-size));
background-color: var(--blockquote-background-color);
.cite {
display: block;
text-align: right;
font-size: 0.75em;
a {
text-decoration: underline;
}
}
}
blockquote.alert {
.alert-header {
display: flex;
align-items: center;
gap: 0.5em;
font-weight: bold;
}
.alert-body {
> :last-child {
margin-bottom: 0;
}
}
$alert-types: note, tip, important, warning, caution;
@each $type in $alert-types {
&.alert-#{$type} {
border-color: var(--alert-#{$type}-color);
background-color: var(--alert-#{$type}-background);
.alert-title {
color: var(--alert-#{$type}-color);
}
}
}
}
hr {
width: 100px;
margin: 40px auto;
background: var(--card-text-color-tertiary);
height: 2px;
border: 0;
opacity: 0.55;
}
code {
color: var(--code-text-color);
background-color: var(--code-background-color);
padding: 2px 4px;
border-radius: var(--tag-border-radius);
font-family: var(--code-font-family);
}
a,
code {
word-break: break-word;
}
.gallery {
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
margin: 1.5em 0;
gap: var(--spacing-sm);
figure {
margin: 0;
}
}
pre {
overflow-x: auto;
display: block;
background-color: var(--pre-background-color);
color: var(--pre-text-color);
font-family: var(--code-font-family);
line-height: 1.428571429;
word-break: break-all;
padding: var(--card-padding);
// keep Codeblocks LTR
[dir="rtl"] & {
direction: ltr;
}
code {
color: unset;
border: none;
background: none;
padding: 0;
}
}
.highlight {
background-color: var(--pre-background-color);
padding: var(--card-padding);
position: relative;
pre {
background-color: transparent;
}
&:hover {
.copyCodeButton {
opacity: 1;
}
}
// keep Codeblocks LTR
[dir="rtl"] & {
direction: ltr;
}
pre {
padding: 0;
margin: 0;
width: auto;
}
}
.copyCodeButton {
position: absolute;
top: calc(var(--card-padding));
right: calc(var(--card-padding));
background: var(--card-background);
border: none;
box-shadow: var(--shadow-l2);
border-radius: var(--tag-border-radius);
padding: 8px 16px;
color: var(--card-text-color-main);
cursor: pointer;
font-size: 14px;
opacity: 0;
transition: opacity 0.3s ease;
}
.table-wrapper {
padding: 0 var(--card-padding);
overflow-x: auto;
display: block;
}
table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
margin-bottom: 1.5em;
font-size: 0.96em;
}
th,
td {
text-align: left;
padding: 4px 8px 4px 10px;
border: 1px solid var(--table-border-color);
}
td {
vertical-align: top;
}
tr:nth-child(even) {
background-color: var(--tr-even-background-color);
}
.twitter-tweet {
color: var(--card-text-color-main);
}
.video-wrapper {
position: relative;
width: 100%;
height: 0;
padding-bottom: 56.25%;
overflow: hidden;
&>iframe,
&>video {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
border: 0;
}
}
.gitlab-embed-snippets {
margin: 0 !important;
.file-holder.snippet-file-content {
margin-block-end: 0 !important;
margin-block-start: 0 !important;
margin-left: calc((var(--card-padding)) * -1) !important;
margin-right: calc((var(--card-padding)) * -1) !important;
padding: 0 var(--card-padding) !important;
}
}
/// Negative margins
blockquote,
figure,
.highlight,
pre,
.gallery,
.video-wrapper,
.table-wrapper,
.s_video_simple {
margin-left: calc((var(--card-padding)) * -1);
margin-right: calc((var(--card-padding)) * -1);
width: calc(100% + var(--card-padding) * 2);
}
/// Make long KaTeX equations scrollable in the x-axis
.katex-display>.katex {
overflow-x: auto;
overflow-y: hidden;
}
kbd {
border: 1px solid var(--kbd-border-color);
font-weight: bold;
font-size: 0.9em;
line-height: 1;
padding: 2px 4px;
border-radius: 4px;
display: inline-block;
}
}
@@ -0,0 +1,157 @@
.section-card {
@include card-style;
padding: var(--small-card-padding);
display: flex;
align-items: center;
gap: var(--spacing-lg);
--separation: 15px;
.section-term {
font-size: 2.2rem;
margin: 0;
color: var(--card-text-color-main);
}
.section-description {
font-weight: normal;
color: var(--card-text-color-secondary);
font-size: 1.6rem;
margin: 0;
}
.section-details {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.section-image {
img {
width: 60px;
height: 60px;
object-fit: cover;
}
}
.section-count {
color: var(--card-text-color-tertiary);
font-size: 1.4rem;
margin: 0;
font-weight: bold;
text-transform: uppercase;
}
}
@property --left-edge-fade-size {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}
@property --right-edge-fade-size {
syntax: "<length>";
inherits: false;
initial-value: 64px;
}
.subsection-list {
--shadow-overlap-x: calc(var(--shadow-l2-blur) + 4px);
--shadow-overlap-y: calc(var(--shadow-l2-blur) + var(--shadow-l2-y) + 4px);
--edge-fade-size: 64px;
--left-edge-fade-size: 0px;
--right-edge-fade-size: var(--edge-fade-size);
overflow-x: auto;
/// Limit the horizontal overlap to the container padding to prevent page overflow on mobile.
/// On desktop, it still allows the full shadow to be visible.
--overlap-x: min(var(--shadow-overlap-x), var(--container-padding));
/// These values are computed from the shadow of the article-list--tile (which uses shadow-l1 and shadow-l2)
/// so that the shadow is not cut off, while staying within the viewport on small screens.
/// Clamp both sides to prevent page-level horizontal overflow.
margin-left: calc(var(--overlap-x) * -1);
margin-right: calc(var(--overlap-x) * -1);
margin-bottom: calc(var(--shadow-overlap-y) * -1);
/// Smooth fade on both horizontal edges to avoid hard cuts where shadows are clamped.
--subsection-mask-image: linear-gradient(
to right,
transparent 0%,
rgb(0 0 0 / 0.06) calc(var(--left-edge-fade-size) * 0.12),
rgb(0 0 0 / 0.2) calc(var(--left-edge-fade-size) * 0.3),
rgb(0 0 0 / 0.45) calc(var(--left-edge-fade-size) * 0.52),
rgb(0 0 0 / 0.72) calc(var(--left-edge-fade-size) * 0.76),
black var(--left-edge-fade-size),
black calc(100% - var(--right-edge-fade-size)),
rgb(0 0 0 / 0.72) calc(100% - (var(--right-edge-fade-size) * 0.76)),
rgb(0 0 0 / 0.45) calc(100% - (var(--right-edge-fade-size) * 0.52)),
rgb(0 0 0 / 0.2) calc(100% - (var(--right-edge-fade-size) * 0.3)),
rgb(0 0 0 / 0.06) calc(100% - (var(--right-edge-fade-size) * 0.12)),
transparent 100%
);
-webkit-mask-image: var(--subsection-mask-image);
mask-image: var(--subsection-mask-image);
[dir="rtl"] & {
margin-left: calc(var(--overlap-x) * -1);
margin-right: calc(var(--overlap-x) * -1);
}
.article-list--tile {
display: flex;
padding: var(--spacing-xs) var(--overlap-x) var(--shadow-overlap-y) var(--overlap-x);
width: max-content;
gap: var(--spacing-lg);
[dir="rtl"] & {
padding: var(--spacing-xs) var(--overlap-x) var(--shadow-overlap-y) var(--overlap-x);
}
article {
flex-shrink: 0;
.article-title {
margin: 0;
font-size: 1.8rem;
}
.article-details {
padding: var(--spacing-lg);
}
}
}
}
// Only apply scroll-driven edge fades in browsers that support scroll timelines.
// Browsers without support (e.g. Firefox) fall back to the static right-edge fade
// defined by the explicit CSS variable declarations above.
@supports (animation-timeline: scroll(self inline)) {
.subsection-list {
animation: subsection-left-fade linear both, subsection-right-fade linear both;
animation-timeline: scroll(self inline);
animation-range: 1px 24px, 0% 100%;
animation-timing-function: ease-out, linear;
}
}
@keyframes subsection-left-fade {
from {
--left-edge-fade-size: 0px;
}
to {
--left-edge-fade-size: var(--edge-fade-size);
}
}
@keyframes subsection-right-fade {
0%,
92% {
--right-edge-fade-size: var(--edge-fade-size);
}
100% {
--right-edge-fade-size: 0px;
}
}
@@ -0,0 +1,87 @@
.search-form {
position: relative;
--button-size: 80px;
&.widget {
--button-size: 60px;
label {
font-size: 1.3rem;
top: var(--spacing-sm);
}
input {
font-size: 1.5rem;
padding: var(--spacing-2xl) var(--spacing-lg) var(--spacing-md) var(--spacing-lg);
}
}
p {
position: relative;
margin: 0;
}
label {
position: absolute;
top: var(--spacing-md);
inset-inline-start: var(--spacing-lg);
font-size: 1.4rem;
color: var(--card-text-color-tertiary);
}
input {
padding: 40px 20px 20px;
@include card-style;
color: var(--card-text-color-main);
width: 100%;
border: 0;
-webkit-appearance: none;
appearance: none;
transition: box-shadow 0.3s ease;
font-size: 1.8rem;
&:focus {
outline: 0;
box-shadow: var(--shadow-l2);
}
}
button {
position: absolute;
inset-inline-end: 0;
top: 0;
height: 100%;
width: var(--button-size);
cursor: pointer;
background-color: transparent;
border: 0;
padding: 0 10px;
&:focus {
outline: 0;
svg {
stroke-width: 2;
color: var(--accent-color);
}
}
svg {
color: var(--card-text-color-secondary);
stroke-width: 1.33;
transition: all 0.3s ease;
width: 20px;
height: 20px;
}
}
}
.search-result {
&.hidden {
display: none;
}
}
+231
View File
@@ -0,0 +1,231 @@
/*!
* Hamburgers
* @description Tasty CSS-animated hamburgers
* @author Jonathan Suh @jonsuh
* @site https://jonsuh.com/hamburgers
* @link https://github.com/jonsuh/hamburgers
*/
.hamburger {
padding-top: var(--spacing-sm);
display: inline-block;
cursor: pointer;
transition-property: opacity, filter;
transition-duration: 0.15s;
transition-timing-function: linear;
font: inherit;
color: inherit;
text-transform: none;
background-color: transparent;
border: 0;
margin: 0;
overflow: visible;
}
.hamburger:hover {
opacity: 0.7;
}
.hamburger-box {
width: 30px;
height: 24px;
display: inline-block;
position: relative;
}
.hamburger-inner {
display: block;
top: 50%;
margin-top: -2px;
}
.hamburger-inner,
.hamburger-inner::before,
.hamburger-inner::after {
width: 30px;
height: 2px;
background-color: var(--card-text-color-main);
border-radius: 4px;
position: absolute;
transition-property: transform;
transition-duration: 0.15s;
transition-timing-function: ease;
}
.hamburger-inner::before,
.hamburger-inner::after {
content: "";
display: block;
}
.hamburger-inner::before {
top: -10px;
}
.hamburger-inner::after {
bottom: -10px;
}
.hamburger--spin .hamburger-inner {
transition-duration: 0.22s;
transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
}
.hamburger--spin .hamburger-inner::before {
transition: top 0.1s 0.25s ease-in, opacity 0.1s ease-in;
}
.hamburger--spin .hamburger-inner::after {
transition: bottom 0.1s 0.25s ease-in, transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19);
}
.hamburger--spin.is-active .hamburger-inner {
transform: rotate(225deg);
transition-delay: 0.12s;
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
.hamburger--spin.is-active .hamburger-inner::before {
top: 0;
opacity: 0;
transition: top 0.1s ease-out, opacity 0.1s 0.12s ease-out;
}
.hamburger--spin.is-active .hamburger-inner::after {
bottom: 0;
transform: rotate(-90deg);
transition: bottom 0.1s ease-out, transform 0.22s 0.12s cubic-bezier(0.215, 0.61, 0.355, 1);
}
#toggle-menu {
background: none;
border: none;
position: absolute;
right: 0;
top: 0;
z-index: 2;
cursor: pointer;
[dir="rtl"] & {
left: 0;
right: auto;
}
@include respond(md) {
display: none;
}
outline: none;
&.is-active {
.hamburger-inner,
.hamburger-inner::before,
.hamburger-inner::after {
background-color: var(--accent-color);
}
}
}
/* Menu style */
#main-menu {
list-style: none;
overflow-y: auto;
flex-grow: 1;
font-size: 1.4rem;
background-color: var(--card-background);
box-shadow: var(--shadow-l1);
display: none;
margin: 0 calc(var(--container-padding) * -1);
padding: var(--spacing-2xl);
@include respond(xl) {
padding: 15px 0;
}
&,
.menu-bottom-section ol {
flex-direction: column;
gap: var(--spacing-2xl);
@include respond(xl) {
gap: var(--spacing-xl);
}
}
&.show {
display: flex;
}
@include respond(md) {
align-items: flex-end;
display: flex;
background-color: transparent;
padding: 0;
box-shadow: none;
margin: 0;
}
li {
position: relative;
vertical-align: middle;
padding: 0;
@include respond(md) {
width: 100%;
}
svg {
stroke: currentColor;
stroke-width: 1.33;
width: 20px;
height: 20px;
}
a {
height: 100%;
display: inline-flex;
align-items: center;
color: var(--body-text-color);
gap: var(--menu-icon-separation);
}
span {
flex: 1;
}
&.current {
a {
color: var(--accent-color);
font-weight: bold;
}
}
&.menu-bottom-section {
margin-top: auto;
ol {
display: flex;
padding-left: 0;
}
}
}
}
.menu-social {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: row;
gap: var(--spacing-sm);
svg {
width: 24px;
height: 24px;
stroke: var(--body-text-color);
stroke-width: 1.33;
}
}
@@ -0,0 +1,259 @@
.pagination {
display: flex;
@include card-style;
overflow: hidden;
.hide-on-mobile,
.pagination-link {
display: none;
@include respond(md) {
display: flex;
}
}
.page-link {
padding: var(--spacing-sm) 0;
margin: 0;
border: 0;
background: none;
font: inherit;
border-radius: 0;
display: flex;
align-items: center;
justify-content: center;
flex: 1 1 auto;
min-width: 36px;
color: var(--card-text-color-secondary);
text-decoration: none;
@include respond(md) {
min-width: 48px;
padding: var(--spacing-md) 0;
}
&:first-child,
&:last-child {
flex: 0 0 var(--spacing-3xl);
padding: var(--spacing-sm);
@include respond(md) {
flex: 0 0 48px;
padding: var(--spacing-md);
}
}
&:hover:not(.current):not(.disabled) {
background-color: var(--card-background-selected);
opacity: 0.8;
}
&.current {
background-color: var(--card-background-selected);
color: var(--card-text-color-main);
font-weight: bold;
cursor: default;
}
&.disabled {
opacity: 0.3;
cursor: not-allowed;
pointer-events: none;
color: var(--card-text-color-secondary);
}
svg {
width: 20px;
height: 20px;
}
&.pagination-jump-trigger {
cursor: pointer;
}
}
}
#pagination-jump-dialog {
border: none;
@include card-style;
box-shadow: var(--shadow-l2);
color: var(--body-text-color);
margin: auto; // Center dialog
width: calc(100% - (var(--container-padding) * 2));
max-width: 320px;
padding: var(--card-padding);
&::backdrop {
background-color: rgba(0, 0, 0, 0.4);
opacity: 0;
animation: backdrop-fade-in 0.3s ease forwards;
}
&[open] {
animation: scale-up 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
&.closing {
animation: scale-down 0.2s ease forwards;
&::backdrop {
animation: backdrop-fade-out 0.2s ease forwards;
}
}
.pagination-jump-form {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-xs);
label {
cursor: pointer;
}
.title {
font-size: 1.6rem;
font-weight: 600;
color: var(--card-text-color-main);
margin: 0;
}
.subtitle {
font-size: 1.3rem;
color: var(--card-text-color-tertiary);
}
}
.pagination-jump-input-group {
display: flex;
gap: var(--spacing-sm);
align-items: stretch;
input[type="number"] {
flex: 1;
width: 100%;
min-width: 0;
padding: 0 var(--spacing-md);
height: 44px; // Explicit height for perfect alignment
border: 1.5px solid var(--card-separator-color);
border-radius: 6px;
background-color: var(--body-background);
color: var(--card-text-color-main);
font-size: 1.6rem;
outline: none;
-webkit-appearance: none;
appearance: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
/* Hide spinner */
-moz-appearance: textfield;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
&:focus,
&:focus-visible {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-color) 20%, transparent);
}
}
.btn-primary {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 var(--spacing-xl);
height: 44px; // Match input height
background-color: var(--accent-color);
color: var(--accent-color-text);
border: none;
border-radius: 6px;
font-size: 1.5rem;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
&:hover {
background-color: var(--accent-color-darker);
box-shadow: var(--shadow-l1);
}
&:active {
box-shadow: none;
}
}
}
.pagination-jump-hint {
display: flex;
align-items: center;
justify-content: flex-start;
gap: var(--spacing-sm);
font-size: 1.3rem;
color: var(--card-text-color-tertiary);
margin-top: calc(var(--spacing-xs) * -1);
kbd {
background-color: var(--body-background);
border: 1px solid var(--card-separator-color);
border-radius: 4px;
padding: 3px 6px;
font-family: var(--base-font-family);
font-size: 1.1rem;
box-shadow: 0 2px 0 var(--card-separator-color);
color: var(--card-text-color-main);
font-weight: 600;
line-height: 1;
}
}
}
@keyframes scale-up {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes scale-down {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
@keyframes backdrop-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes backdrop-fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
@@ -0,0 +1,228 @@
.sidebar {
&.sticky {
@include respond(md) {
position: sticky;
}
}
}
.left-sidebar {
display: flex;
flex-direction: column;
flex-shrink: 0;
align-self: stretch;
@include respond(md) {
gap: var(--sidebar-element-separation);
}
margin-bottom: calc(-1 * var(--sidebar-element-separation));
@include respond(md) {
margin-bottom: 0;
}
&> :not(:last-child) {
margin-bottom: var(--sidebar-element-separation);
@include respond(md) {
margin-bottom: 0;
}
}
max-width: none;
width: 100%;
position: relative;
--sidebar-avatar-size: 100px;
--sidebar-element-separation: var(--spacing-lg);
--emoji-size: 40px;
--emoji-font-size: 20px;
@include respond(md) {
width: auto;
padding-top: var(--main-top-padding);
padding-bottom: var(--main-top-padding);
max-height: 100vh;
}
@include respond(2xl) {
--sidebar-avatar-size: 120px;
--sidebar-element-separation: var(--spacing-xl);
--emoji-size: 40px;
}
&.sticky {
top: 0;
}
&.compact {
--sidebar-avatar-size: 80px;
--emoji-size: 30px;
--emoji-font-size: 15px;
header {
@include respond(lg) {
flex-direction: row;
}
.site-meta {
gap: var(--spacing-xs);
}
.site-name {
font-size: 1.4rem;
@include respond(2xl) {
font-size: 1.75rem;
}
}
.site-description {
font-size: 1.4rem;
}
}
}
}
.right-sidebar {
width: 100%;
display: none;
flex-direction: column;
gap: var(--widget-separation);
&.sticky {
top: 0;
}
@include respond(lg) {
padding-top: var(--main-top-padding);
padding-bottom: var(--main-top-padding);
}
}
.sidebar header {
z-index: 1;
transition: box-shadow 0.5s ease;
display: flex;
flex-direction: column;
gap: var(--sidebar-element-separation);
@include respond(md) {
padding: 0;
}
.site-avatar {
position: relative;
margin: 0;
width: var(--sidebar-avatar-size);
height: var(--sidebar-avatar-size);
flex-shrink: 0;
.site-logo {
width: 100%;
height: 100%;
border-radius: 100%;
box-shadow: var(--shadow-l1);
}
.emoji {
position: absolute;
width: var(--emoji-size);
height: var(--emoji-size);
line-height: var(--emoji-size);
border-radius: 100%;
bottom: 0;
right: 0;
text-align: center;
font-size: var(--emoji-font-size);
background-color: var(--card-background);
box-shadow: var(--shadow-l2);
}
}
.site-meta {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
justify-content: center;
}
.site-name {
color: var(--accent-color);
margin: 0;
font-size: 1.6rem;
@include respond(2xl) {
font-size: 1.8rem;
}
}
.site-description {
color: var(--body-text-color);
font-weight: normal;
margin: 0;
font-size: 1.4rem;
@include respond(2xl) {
font-size: 1.6rem;
}
}
}
[data-scheme="dark"] {
#dark-mode-toggle {
color: var(--accent-color);
font-weight: 700;
.icon-tabler-toggle-left {
display: none;
}
.icon-tabler-toggle-right {
display: unset;
}
}
}
#dark-mode-toggle {
margin-top: auto;
color: var(--body-text-color);
display: flex;
align-items: center;
cursor: pointer;
gap: var(--menu-icon-separation);
.icon-tabler-toggle-right {
display: none;
}
}
#i18n-switch {
color: var(--body-text-color);
display: inline-flex;
align-content: center;
gap: var(--menu-icon-separation);
select {
border: 0;
background: none !important;
color: var(--body-text-color);
padding: 0 !important;
cursor: pointer;
font-family: inherit;
font-size: inherit;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
&::-ms-expand {
display: none;
}
option {
color: var(--card-text-color-main);
background-color: var(--card-background);
}
}
}
@@ -0,0 +1,64 @@
.widget {
display: flex;
flex-direction: column;
.widget-icon {
svg {
width: var(--widget-icon-size);
height: var(--widget-icon-size);
stroke-width: 1.6;
color: var(--body-text-color);
}
}
}
/* Tag cloud widget */
.tagCloud {
.tagCloud-tags {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
a {
@include card-style;
border-radius: var(--tag-border-radius);
padding: 8px 20px;
color: var(--card-text-color-main);
font-size: 1.4rem;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: var(--shadow-l2);
}
}
}
}
/* Archives widget */
.widget.archives {
.widget-archive--list {
@include card-style;
}
.archives-year {
&:not(:last-of-type) {
border-bottom: 1.5px solid var(--card-separator-color);
}
a {
font-size: 1.4rem;
padding: 18px 25px;
display: flex;
span.year {
flex: 1;
color: var(--card-text-color-main);
font-weight: bold;
}
span.count {
color: var(--card-text-color-tertiary);
}
}
}
}
+30
View File
@@ -0,0 +1,30 @@
/*!
* Hugo Theme Stack
*
* @author: Jimmy Cai
* @website: https://jimmycai.com
* @link: https://github.com/CaiJimmy/hugo-theme-stack
*/
@import "breakpoints.scss";
@import "variables.scss";
@import "mixins.scss";
@import "grid.scss";
@import "external/normalize.scss";
@import "partials/menu.scss";
@import "partials/article.scss";
@import "partials/widgets.scss";
@import "partials/footer.scss";
@import "partials/pagination.scss";
@import "partials/sidebar.scss";
@import "partials/base.scss";
@import "partials/layout/article.scss";
@import "partials/layout/list.scss";
@import "partials/layout/404.scss";
@import "partials/layout/search.scss";
@import "partials/cookies.scss";
@import "general.scss";
@import "custom.scss";
+228
View File
@@ -0,0 +1,228 @@
/*
* Global style
*/
:root {
--main-top-padding: 35px;
@include respond(xl) {
--main-top-padding: 50px;
}
--body-background: #f5f5fa;
--accent-color: #34495e;
--accent-color-darker: #2c3e50;
--accent-color-text: #fff;
--body-text-color: #707070;
--tag-border-radius: 4px;
--section-separation: 40px;
&[data-scheme="dark"] {
--body-background: #303030;
--accent-color: #ecf0f1;
--accent-color-darker: #bdc3c7;
--accent-color-text: #000;
--body-text-color: rgba(255, 255, 255, 0.7);
}
}
/**
* Global font family
*/
:root {
--sys-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Droid Sans", "Helvetica Neue";
--base-font-family: "Lato", var(--sys-font-family), sans-serif;
--code-font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}
/*
* Card style
*/
:root {
--card-background: #fff;
--card-background-selected: #eaeaea;
--card-text-color-main: #000;
--card-text-color-secondary: #747474;
--card-text-color-tertiary: #767676;
--card-separator-color: rgba(218, 218, 218, 0.5);
--card-border-radius: 10px;
--card-padding: 20px;
@include respond(md) {
--card-padding: 25px;
}
@include respond(xl) {
--card-padding: 30px;
}
--small-card-padding: 25px 20px;
@include respond(md) {
--small-card-padding: 25px;
}
&[data-scheme="dark"] {
--card-background: #424242;
--card-background-selected: rgba(255, 255, 255, 0.16);
--card-text-color-main: rgba(255, 255, 255, 0.9);
--card-text-color-secondary: rgba(255, 255, 255, 0.7);
--card-text-color-tertiary: rgba(255, 255, 255, 0.5);
--card-separator-color: rgba(255, 255, 255, 0.12);
}
}
/**
* Article content font settings
*/
:root {
--article-font-family: var(--base-font-family);
--article-font-size: 1.6rem;
@include respond(md) {
--article-font-size: 1.7rem;
}
--article-line-height: 1.85;
}
/*
* Article content style
*/
:root {
--blockquote-border-size: 4px;
--blockquote-background-color: rgb(248 248 248);
--heading-border-size: 4px;
--alert-note-color: #4da3ff;
--alert-note-background: rgba(77, 163, 255, 0.08);
--alert-tip-color: #3fb950;
--alert-tip-background: rgba(63, 185, 80, 0.08);
--alert-important-color: #8457d9;
--alert-important-background: rgba(130, 80, 223, 0.08);
--alert-warning-color: #d29922;
--alert-warning-background: rgba(210, 153, 34, 0.1);
--alert-caution-color: #f85149;
--alert-caution-background: rgba(248, 81, 73, 0.1);
--link-background-color: 189, 195, 199;
--link-background-opacity: 0.5;
--link-background-opacity-hover: 0.7;
--code-background-color: rgba(0, 0, 0, 0.12);
--code-text-color: #808080;
--table-border-color: #dadada;
--tr-even-background-color: #efefee;
--kbd-border-color: #dadada;
&[data-scheme="dark"] {
--code-background-color: #272822;
--code-text-color: rgba(255, 255, 255, 0.9);
--table-border-color: #717171;
--tr-even-background-color: #545454;
--blockquote-background-color: rgb(75 75 75);
}
}
/*
* Shadow style
* Thanks to https://www.figma.com/community/plugin/744987207861965946/Shadow-picker
*/
:root {
--shadow-l1-y: 4px;
--shadow-l1-blur: 8px;
--shadow-l2-y: 10px;
--shadow-l2-blur: 20px;
--shadow-l1: 0px var(--shadow-l1-y) var(--shadow-l1-blur) rgba(0, 0, 0, 0.04), 0px 0px 2px rgba(0, 0, 0, 0.06), 0px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-l2: 0px var(--shadow-l2-y) var(--shadow-l2-blur) rgba(0, 0, 0, 0.04), 0px 2px 6px rgba(0, 0, 0, 0.04), 0px 0px 1px rgba(0, 0, 0, 0.04);
}
[data-scheme="light"] {
--pre-text-color: #272822;
--pre-background-color: #fafafa;
@import "partials/highlight/light.scss";
}
[data-scheme="dark"] {
--pre-text-color: #f8f8f2;
--pre-background-color: #272822;
@import "partials/highlight/dark.scss";
}
:root {
--menu-icon-separation: 40px;
--container-padding: 15px;
@include respond(md) {
--container-padding: 20px;
}
--widget-separation: var(--section-separation);
/* Spacing tokens */
--spacing-xs: 5px;
--spacing-sm: 10px;
--spacing-md: 15px;
--spacing-lg: 20px;
--spacing-xl: 25px;
--spacing-2xl: 30px;
--spacing-3xl: 40px;
/* Article image height variables */
--article-image-height: 150px;
@include respond(md) {
--article-image-height: 200px;
}
@include respond(xl) {
--article-image-height: 250px;
}
/* Mermaid component variables */
--mermaid-toolbar-btn-bg: rgba(255, 255, 255, 0.95);
--mermaid-toolbar-btn-text: #333;
--mermaid-modal-bg: rgba(0, 0, 0, 0.95);
--mermaid-modal-content-bg: #fff;
--mermaid-modal-btn-bg: #fff;
--mermaid-modal-btn-hover: #ddd;
&[data-scheme="dark"] {
--mermaid-toolbar-btn-bg: rgba(0, 0, 0, 0.85);
--mermaid-toolbar-btn-text: #eee;
--mermaid-modal-content-bg: #1e1e1e;
--mermaid-modal-btn-bg: #333;
--mermaid-modal-btn-hover: #444;
}
/* Article compact image size */
--article-compact-image-size: 50px;
@include respond(md) {
--article-compact-image-size: 60px;
}
/* Widget icon size */
--widget-icon-size: 32px;
/* Article list tile variables */
--article-tile-width: 250px;
--article-tile-height: 150px;
--article-tile-overlay-bg: rgba(0, 0, 0, 0.25);
--article-tile-text-color: #fff;
&[data-scheme="dark"] {
--article-tile-overlay-bg: rgba(0, 0, 0, 0.45);
}
}
+41
View File
@@ -0,0 +1,41 @@
import * as params from '@params';
export function setupCodeCopy() {
/**
* Add copy button to code block
*/
const highlights = document.querySelectorAll('.article-content div.highlight');
const copyText = params.codeblock.copy,
copiedText = params.codeblock.copied;
if (!navigator.clipboard) {
/// Clipboard API is only supported in secure contexts (HTTPS)
console.warn('Clipboard API not supported, copy button will not work.');
return;
}
highlights.forEach(highlight => {
const copyButton = document.createElement('button');
copyButton.innerHTML = copyText;
copyButton.classList.add('copyCodeButton');
highlight.appendChild(copyButton);
const codeBlock = highlight.querySelector('code[data-lang]');
if (!codeBlock) return;
copyButton.addEventListener('click', () => {
navigator.clipboard.writeText(codeBlock.textContent)
.then(() => {
copyButton.textContent = copiedText;
setTimeout(() => {
copyButton.textContent = copyText;
}, 1000);
})
.catch(err => {
alert(err)
console.log('Something went wrong', err);
});
});
});
};
+92
View File
@@ -0,0 +1,92 @@
type colorScheme = 'light' | 'dark' | 'auto';
class StackColorScheme {
private localStorageKey = 'StackColorScheme';
private currentScheme: colorScheme;
private systemPreferScheme: colorScheme;
constructor(toggleEl: HTMLElement) {
this.bindMatchMedia();
this.currentScheme = this.getSavedScheme();
if (window.matchMedia('(prefers-color-scheme: dark)').matches === true)
this.systemPreferScheme = 'dark'
else
this.systemPreferScheme = 'light';
this.dispatchEvent(document.documentElement.dataset.scheme as colorScheme);
if (toggleEl)
this.bindClick(toggleEl);
if (document.body.style.transition == '')
document.body.style.setProperty('transition', 'background-color .3s ease');
}
private saveScheme() {
localStorage.setItem(this.localStorageKey, this.currentScheme);
}
private bindClick(toggleEl: HTMLElement) {
toggleEl.addEventListener('click', (e) => {
if (this.isDark()) {
/// Disable dark mode
this.currentScheme = 'light';
}
else {
this.currentScheme = 'dark';
}
this.setBodyClass();
if (this.currentScheme == this.systemPreferScheme) {
/// Set to auto
this.currentScheme = 'auto';
}
this.saveScheme();
})
}
private isDark() {
return (this.currentScheme == 'dark' || this.currentScheme == 'auto' && this.systemPreferScheme == 'dark');
}
private dispatchEvent(colorScheme: colorScheme) {
const event = new CustomEvent('onColorSchemeChange', {
detail: colorScheme
});
window.dispatchEvent(event);
}
private setBodyClass() {
if (this.isDark()) {
document.documentElement.dataset.scheme = 'dark';
}
else {
document.documentElement.dataset.scheme = 'light';
}
this.dispatchEvent(document.documentElement.dataset.scheme as colorScheme);
}
private getSavedScheme(): colorScheme {
const savedScheme = localStorage.getItem(this.localStorageKey);
if (savedScheme == 'light' || savedScheme == 'dark' || savedScheme == 'auto') return savedScheme;
else return 'auto';
}
private bindMatchMedia() {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (e.matches) {
this.systemPreferScheme = 'dark';
}
else {
this.systemPreferScheme = 'light';
}
this.setBodyClass();
});
}
}
export default StackColorScheme;
+141
View File
@@ -0,0 +1,141 @@
/*
* Consent-gated comments bootstrap.
*
* This module controls whether third-party comments are rendered based on
* functional cookie consent, and injects template scripts in sequence to
* avoid eager or duplicate script execution.
*/
/**
* Consent event payload used by the comments gate.
* It mirrors cookie consent state shape but keeps optional fields for safety.
*/
interface CommentsConsentState {
necessary?: boolean;
analytics?: boolean;
functional?: boolean;
timestamp?: number;
}
/**
* Resolve functional consent with layered fallback:
* 1) explicit event detail, 2) CookieConsent public API, 3) dataset mirror.
*/
const hasFunctionalConsent = (consentDetail?: CommentsConsentState | null): boolean => {
const cookieConsent = (window as Window & {
cookieConsent?: {
hasConsent?: (category: 'necessary' | 'analytics' | 'functional') => boolean;
};
}).cookieConsent;
return (
consentDetail?.functional ??
cookieConsent?.hasConsent?.('functional') ??
document.documentElement.dataset.consentFunctional === 'true'
);
};
interface DeferredScript {
placeholder: Comment;
script: HTMLScriptElement;
}
const prepareDeferredScripts = (fragment: DocumentFragment): DeferredScript[] => {
return Array.from(fragment.querySelectorAll('script')).map(script => {
const placeholder = document.createComment('comments-script-placeholder');
script.replaceWith(placeholder);
return { placeholder, script };
});
};
const activateDeferredScripts = async (scripts: DeferredScript[]): Promise<void> => {
// Inject scripts sequentially so third-party embeds can rely on execution order.
for (const { placeholder, script } of scripts) {
if (!placeholder.parentNode) {
continue;
}
const newScript = document.createElement('script');
Array.from(script.attributes).forEach(attr => {
newScript.setAttribute(attr.name, attr.value);
});
if (script.textContent) {
newScript.text = script.textContent;
}
let done: Promise<void> = Promise.resolve();
if (newScript.src) {
done = new Promise<void>(resolve => {
newScript.onload = newScript.onerror = () => resolve();
});
}
placeholder.replaceWith(newScript);
await done;
}
};
const initCommentsConsent = (): void => {
// Main entry: gate comments rendering by functional cookie consent.
const placeholder = document.getElementById('comments-consent-placeholder') as HTMLElement | null;
const container = document.getElementById('comments-container') as HTMLElement | null;
const template = document.getElementById('comments-template') as HTMLTemplateElement | null;
if (!placeholder || !container || !template) {
return;
}
let commentsLoaded = false;
let commentsLoading = false;
const showComments = async (): Promise<void> => {
placeholder.style.display = 'none';
container.style.display = 'block';
if (commentsLoaded || commentsLoading) {
return;
}
// Prevent duplicate template hydration while scripts are still loading.
commentsLoading = true;
try {
const clone = template.content.cloneNode(true) as DocumentFragment;
const scripts = prepareDeferredScripts(clone);
container.appendChild(clone);
await activateDeferredScripts(scripts);
commentsLoaded = true;
} finally {
commentsLoading = false;
}
};
const hideComments = (): void => {
placeholder.style.display = 'block';
container.style.display = 'none';
};
window.addEventListener('onCookieConsentChange', (event: Event) => {
// Cross-module contract: this event is dispatched by cookies.ts.
const customEvent = event as CustomEvent<CommentsConsentState | null>;
if (hasFunctionalConsent(customEvent.detail)) {
showComments();
} else {
hideComments();
}
});
if (hasFunctionalConsent()) {
showComments();
} else {
hideComments();
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCommentsConsent, { once: true });
} else {
initCommentsConsent();
}
+222
View File
@@ -0,0 +1,222 @@
interface ConsentState {
necessary: boolean;
analytics: boolean;
functional: boolean;
timestamp: number;
}
class CookieConsent {
private static COOKIE_NAME = 'cookie_consent';
private static COOKIE_DAYS = 365;
private state: ConsentState | null = null;
private banner: HTMLElement | null = null;
private settingsPanel: HTMLElement | null = null;
constructor() {
this.banner = document.getElementById('cookie-consent-banner');
this.settingsPanel = document.getElementById('cookie-settings-panel');
this.state = this.loadState();
if (!this.state && this.banner) {
this.showBanner();
}
this.bindEvents();
this.dispatchConsentEvent();
}
private loadState(): ConsentState | null {
const cookie = document.cookie
.split('; ')
.find(row => row.startsWith(CookieConsent.COOKIE_NAME + '='));
if (!cookie) return null;
try {
return JSON.parse(decodeURIComponent(cookie.split('=')[1]));
} catch {
return null;
}
}
private saveState(): void {
if (!this.state) return;
const expires = new Date();
expires.setDate(expires.getDate() + CookieConsent.COOKIE_DAYS);
document.cookie = `${CookieConsent.COOKIE_NAME}=${encodeURIComponent(JSON.stringify(this.state))}; expires=${expires.toUTCString()}; path=/; SameSite=Lax`;
}
private showBanner(): void {
if (this.banner) {
this.banner.removeAttribute('aria-hidden');
}
}
private hideBanner(): void {
if (this.banner) {
// Blur any focused element inside the banner before hiding
const activeElement = document.activeElement as HTMLElement;
if (activeElement && this.banner.contains(activeElement)) {
activeElement.blur();
}
this.banner.setAttribute('aria-hidden', 'true');
}
this.hideSettings();
}
private showSettings(): void {
if (this.settingsPanel) {
this.settingsPanel.removeAttribute('aria-hidden');
// Restore checkbox states from current state or defaults
const checkboxes = this.settingsPanel.querySelectorAll('input[data-cookie-category]');
checkboxes.forEach((cb) => {
const input = cb as HTMLInputElement;
const category = input.dataset.cookieCategory as keyof ConsentState;
if (category && this.state && typeof this.state[category] === 'boolean') {
input.checked = this.state[category] as boolean;
} else {
input.checked = false;
}
});
}
}
private hideSettings(): void {
if (this.settingsPanel) {
// Blur any focused element inside the settings panel before hiding
const activeElement = document.activeElement as HTMLElement;
if (activeElement && this.settingsPanel.contains(activeElement)) {
activeElement.blur();
}
this.settingsPanel.setAttribute('aria-hidden', 'true');
}
}
private bindEvents(): void {
document.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
const action = target.dataset.cookieAction;
if (!action) return;
switch (action) {
case 'accept':
this.acceptAll();
break;
case 'deny':
this.denyAll();
break;
case 'settings':
this.showSettings();
break;
case 'save':
this.saveSettings();
break;
case 'cancel':
this.hideSettings();
break;
case 'reopen':
this.showBanner();
break;
}
});
}
private acceptAll(): void {
this.state = {
necessary: true,
analytics: true,
functional: true,
timestamp: Date.now()
};
this.saveState();
this.hideBanner();
this.dispatchConsentEvent();
}
private denyAll(): void {
this.state = {
necessary: true,
analytics: false,
functional: false,
timestamp: Date.now()
};
this.saveState();
this.hideBanner();
this.dispatchConsentEvent();
}
private saveSettings(): void {
const checkboxes = document.querySelectorAll('input[data-cookie-category]');
this.state = {
necessary: true,
analytics: false,
functional: false,
timestamp: Date.now()
};
checkboxes.forEach((cb) => {
const input = cb as HTMLInputElement;
const category = input.dataset.cookieCategory as keyof ConsentState;
if (category && category in this.state!) {
(this.state as any)[category] = input.checked;
}
});
this.saveState();
this.hideBanner();
this.dispatchConsentEvent();
}
private dispatchConsentEvent(): void {
// Cross-module event consumed by consent-gated features (for example commentsConsent.ts).
const event = new CustomEvent('onCookieConsentChange', {
detail: this.state
});
window.dispatchEvent(event);
// Set data attributes on document for CSS-based control
if (this.state) {
document.documentElement.dataset.consentAnalytics = String(this.state.analytics);
document.documentElement.dataset.consentFunctional = String(this.state.functional);
}
}
// Public API
public hasConsent(category: keyof Omit<ConsentState, 'timestamp'>): boolean {
if (!this.state) return false;
return this.state[category] ?? false;
}
public getState(): ConsentState | null {
return this.state;
}
public reopenBanner(): void {
this.showBanner();
}
}
// Export for module usage
export default CookieConsent;
// Initialize when DOM is ready and expose globally
declare global {
interface Window {
cookieConsent: CookieConsent;
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
window.cookieConsent = new CookieConsent();
});
} else {
window.cookieConsent = new CookieConsent();
}
+34
View File
@@ -0,0 +1,34 @@
/**
* createElement
* Edited from:
* @link https://stackoverflow.com/a/42405694
*/
function createElement(tag, attrs, children) {
var element = document.createElement(tag);
for (let name in attrs) {
if (name && attrs.hasOwnProperty(name)) {
let value = attrs[name];
if (name == "dangerouslySetInnerHTML") {
element.innerHTML = value.__html;
}
else if (value === true) {
element.setAttribute(name, name);
} else if (value !== false && value != null) {
element.setAttribute(name, value.toString());
}
}
}
for (let i = 2; i < arguments.length; i++) {
let child = arguments[i];
if (child) {
element.appendChild(
child.nodeType == null ?
document.createTextNode(child.toString()) : child);
}
}
return element;
}
export default createElement;
+110
View File
@@ -0,0 +1,110 @@
const wrap = (figures: HTMLElement[]) => {
const galleryContainer = document.createElement('div');
galleryContainer.className = 'gallery';
const parentNode = figures[0].parentNode!,
first = figures[0];
parentNode.insertBefore(galleryContainer, first)
for (const figure of figures) {
galleryContainer.appendChild(figure);
}
}
const unescapeHtml = (html: string) => {
/// Replace special HTML entities with their corresponding characters
return html.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");
}
export default (container: HTMLElement) => {
/// The process of wrapping image with figure tag is done using JavaScript instead of only Hugo markdown render hook
/// because it can not detect whether image is being wrapped by a link or not
/// and it lead to a invalid HTML construction (<a><figure><img></figure></a>)
const images = container.querySelectorAll('img.gallery-image') as NodeListOf<HTMLImageElement>;
for (const img of Array.from(images)) {
/// Images are wrapped with figure tag if the paragraph has only images without texts
/// This is done to allow inline images within paragraphs
const paragraph = img.closest('p');
if (!paragraph || !container.contains(paragraph)) continue;
if (paragraph.textContent.trim() == '') {
/// Once we insert figcaption, this check no longer works
/// So we add a class to paragraph to mark it
paragraph.classList.add('no-text');
}
let isNewLineImage = paragraph.classList.contains('no-text');
if (!isNewLineImage) continue;
const hasLink = img.parentElement!.tagName == 'A';
let el: HTMLElement = img;
/// Wrap image with figure tag, with flex-grow and flex-basis values extracted from img's data attributes
const figure = document.createElement('figure');
figure.classList.add('gallery-image');
figure.style.setProperty('flex-grow', img.getAttribute('data-flex-grow') || '1');
figure.style.setProperty('flex-basis', img.getAttribute('data-flex-basis') || '0');
if (hasLink) {
/// Wrap <a> if it exists
el = img.parentElement!;
el.classList.add('image-link');
el.setAttribute('data-pswp-width', img.getAttribute('width')!);
el.setAttribute('data-pswp-height', img.getAttribute('height')!);
} else {
const a = document.createElement('a');
a.href = img.src;
a.setAttribute('class', 'image-link');
a.setAttribute('target', '_blank');
a.setAttribute('data-pswp-width', img.getAttribute('width')!);
a.setAttribute('data-pswp-height', img.getAttribute('height')!);
img.parentNode!.insertBefore(a, img);
a.appendChild(img);
el = a;
}
el.parentElement!.insertBefore(figure, el);
figure.appendChild(el);
/// Add figcaption if it exists
let caption = img.getAttribute('alt');
if (img.getAttribute('data-title-escaped')) {
caption = unescapeHtml(img.getAttribute('data-title-escaped')!);
}
if (caption) {
const figcaption = document.createElement('figcaption');
figcaption.innerHTML = caption;
figure.appendChild(figcaption);
}
}
const figuresEl = container.querySelectorAll('figure.gallery-image') as NodeListOf<HTMLElement>;
let currentGallery: HTMLElement[] = [];
for (const figure of Array.from(figuresEl)) {
if (!currentGallery.length) {
/// First iteration
currentGallery = [figure];
}
else if (figure.previousElementSibling === currentGallery[currentGallery.length - 1]) {
/// Adjacent figures
currentGallery.push(figure);
}
else if (currentGallery.length) {
/// End gallery
wrap(currentGallery);
currentGallery = [figure];
}
}
if (currentGallery.length > 0) {
wrap(currentGallery);
}
};
+50
View File
@@ -0,0 +1,50 @@
/*!
* Hugo Theme Stack
*
* @author: Jimmy Cai
* @website: https://jimmycai.com
* @link: https://github.com/CaiJimmy/hugo-theme-stack
*/
import menu from './menu';
import createElement from './createElement';
import StackColorScheme from './colorScheme';
import { setupScrollspy } from './scrollspy';
import { setupSmoothAnchors } from './smoothAnchors';
import { setupPaginationJump } from './pagination';
import { setupCodeCopy } from './code-copy';
let Stack = {
init: () => {
/**
* Bind menu event
*/
menu();
const articleContent = document.querySelector('.article-content') as HTMLElement;
if (articleContent) {
setupSmoothAnchors();
setupScrollspy();
setupCodeCopy();
}
setupPaginationJump();
new StackColorScheme(document.getElementById('dark-mode-toggle')!);
}
}
window.addEventListener('load', () => {
setTimeout(function () {
Stack.init();
}, 0);
})
declare global {
interface Window {
createElement: any;
Stack: any
}
}
window.Stack = Stack;
window.createElement = createElement;
+83
View File
@@ -0,0 +1,83 @@
/**
* Slide up/down
* Code from https://dev.to/bmsvieira/vanilla-js-slidedown-up-4dkn
* @param target
* @param duration
*/
let slideUp = (target: HTMLElement, duration = 500) => {
target.classList.add('transiting');
target.style.transitionProperty = 'height, margin, padding';
target.style.transitionDuration = duration + 'ms';
///target.style.boxSizing = 'border-box';
target.style.height = target.offsetHeight + 'px';
target.offsetHeight;
target.style.overflow = 'hidden';
target.style.height = "0";
target.style.paddingTop = "0";
target.style.paddingBottom = "0";
target.style.marginTop = "0";
target.style.marginBottom = "0";
window.setTimeout(() => {
target.classList.remove('show')
target.style.removeProperty('height');
target.style.removeProperty('padding-top');
target.style.removeProperty('padding-bottom');
target.style.removeProperty('margin-top');
target.style.removeProperty('margin-bottom');
target.style.removeProperty('overflow');
target.style.removeProperty('transition-duration');
target.style.removeProperty('transition-property');
target.classList.remove('transiting');
}, duration);
}
let slideDown = (target: HTMLElement, duration = 500) => {
target.classList.add('transiting');
target.style.removeProperty('display');
target.classList.add('show');
let height = target.offsetHeight;
target.style.overflow = 'hidden';
target.style.height = "0";
target.style.paddingTop = "0";
target.style.paddingBottom = "0";
target.style.marginTop = "0";
target.style.marginBottom = "0";
target.offsetHeight;
///target.style.boxSizing = 'border-box';
target.style.transitionProperty = "height, margin, padding";
target.style.transitionDuration = duration + 'ms';
target.style.height = height + 'px';
target.style.removeProperty('padding-top');
target.style.removeProperty('padding-bottom');
target.style.removeProperty('margin-top');
target.style.removeProperty('margin-bottom');
window.setTimeout(() => {
target.style.removeProperty('height');
target.style.removeProperty('overflow');
target.style.removeProperty('transition-duration');
target.style.removeProperty('transition-property');
target.classList.remove('transiting');
}, duration);
}
let slideToggle = (target, duration = 500) => {
if (window.getComputedStyle(target).display === 'none') {
return slideDown(target, duration);
} else {
return slideUp(target, duration);
}
}
export default function () {
const toggleMenu = document.getElementById('toggle-menu');
if (toggleMenu) {
toggleMenu.addEventListener('click', () => {
if (document.getElementById('main-menu').classList.contains('transiting')) return;
document.body.classList.toggle('show-menu');
slideToggle(document.getElementById('main-menu'), 300);
toggleMenu.classList.toggle('is-active');
});
}
}
+235
View File
@@ -0,0 +1,235 @@
declare const mermaid: {
initialize(config: Record<string, any>): void;
run(options: { nodes: HTMLElement[] }): Promise<void>;
};
interface MermaidConfig {
transparentBackground?: boolean;
lightTheme?: string;
darkTheme?: string;
lightThemeVariables?: Record<string, any>;
darkThemeVariables?: Record<string, any>;
securityLevel?: string;
look?: string;
htmlLabels?: boolean;
maxTextSize?: number;
maxEdges?: number;
fontSize?: number;
fontFamily?: string;
curve?: string;
logLevel?: number;
}
type Scheme = 'light' | 'dark';
const PANZOOM_CDN = 'https://cdn.jsdelivr.net/npm/panzoom@9.4.3/+esm';
function getScheme(): Scheme {
return document.documentElement.dataset.scheme === 'dark' ? 'dark' : 'light';
}
function buildThemeConfig(cfg: MermaidConfig, scheme: Scheme) {
const isLight = scheme === 'light';
const theme = isLight ? (cfg.lightTheme ?? 'default') : (cfg.darkTheme ?? 'dark');
const vars = isLight ? (cfg.lightThemeVariables ?? {}) : (cfg.darkThemeVariables ?? {});
return {
theme,
themeVariables: { ...vars, ...(cfg.transparentBackground ? { background: 'transparent' } : {}) },
};
}
function buildBaseConfig(cfg: MermaidConfig): Record<string, any> {
const base: Record<string, any> = {
startOnLoad: false,
securityLevel: cfg.securityLevel ?? 'strict',
look: cfg.look ?? 'classic',
flowchart: { htmlLabels: cfg.htmlLabels ?? true, useMaxWidth: true },
gantt: { useWidth: 800 },
};
const optional: (keyof MermaidConfig)[] = ['maxTextSize', 'maxEdges', 'fontSize', 'fontFamily', 'curve', 'logLevel'];
for (const key of optional) {
if (cfg[key] != null) base[key] = cfg[key];
}
return base;
}
function initWithTheme(
scheme: Scheme,
themes: Record<Scheme, ReturnType<typeof buildThemeConfig>>,
baseConfig: Record<string, any>,
) {
const { theme, themeVariables } = themes[scheme];
mermaid.initialize({
...baseConfig,
theme,
...(Object.keys(themeVariables).length && { themeVariables }),
});
}
async function renderOffscreen(sources: string[]): Promise<string[]> {
const container = document.createElement('div');
container.className = 'mermaid-offscreen';
document.body.appendChild(container);
const nodes = sources.map(src => {
const n = document.createElement('pre');
n.innerHTML = src;
container.appendChild(n);
return n;
});
await mermaid.run({ nodes });
const results = nodes.map(n => n.innerHTML);
container.remove();
return results;
}
function setupWrappers(elements: NodeListOf<HTMLElement>) {
elements.forEach((el, idx) => {
const wrapper = document.createElement('div');
wrapper.className = 'mermaid-wrapper';
el.parentNode!.insertBefore(wrapper, el);
wrapper.appendChild(el);
wrapper.insertAdjacentHTML(
'beforeend',
`<div class="mermaid-toolbar"><button data-idx="${idx}" title="Open fullscreen with pan/zoom">⛶ Expand</button></div>`,
);
});
}
function setupModal(elements: NodeListOf<HTMLElement>) {
const modal = document.getElementById('mermaid-modal')!;
const modalBody = document.getElementById('mermaid-modal-body')!;
const modalContent = document.getElementById('mermaid-modal-content')!;
let pzInstance: any = null;
let panzoom: any = null;
const loadPanzoom = async () => {
if (!panzoom) {
const url = PANZOOM_CDN;
panzoom = (await import(url)).default;
}
return panzoom;
};
const fitToScreen = () => {
const wrapper = modalContent.querySelector('.mermaid-panzoom-container') as HTMLElement | null;
if (!pzInstance || !wrapper) return;
const w = +(wrapper.dataset.nativeWidth ?? 0);
const h = +(wrapper.dataset.nativeHeight ?? 0);
const rect = modalContent.getBoundingClientRect();
const scale = Math.min((rect.width - 60) / w, (rect.height - 60) / h);
pzInstance.zoomAbs(0, 0, scale);
pzInstance.moveTo((rect.width - w * scale) / 2, (rect.height - h * scale) / 2);
};
const closeModal = () => {
modal.classList.remove('active');
document.body.style.overflow = '';
pzInstance?.dispose();
pzInstance = null;
modalContent.innerHTML = '';
};
const openModal = async (idx: number) => {
const svg = elements[idx].querySelector('svg');
if (!svg) return;
const svgClone = svg.cloneNode(true) as SVGElement;
const viewBox = svg.getAttribute('viewBox');
const [w, h] = viewBox
? viewBox.split(/[\s,]+/).slice(2).map(Number)
: [svg.getBoundingClientRect().width || 800, svg.getBoundingClientRect().height || 600];
svgClone.setAttribute('width', String(w));
svgClone.setAttribute('height', String(h));
const wrapper = document.createElement('div');
wrapper.className = 'mermaid-panzoom-container';
wrapper.dataset.nativeWidth = String(w);
wrapper.dataset.nativeHeight = String(h);
wrapper.appendChild(svgClone);
modalContent.innerHTML = '';
modalContent.appendChild(wrapper);
modal.classList.add('active');
document.body.style.overflow = 'hidden';
const pz = await loadPanzoom();
setTimeout(() => {
pzInstance = pz(wrapper, { maxZoom: 10, minZoom: 0.05, bounds: false });
fitToScreen();
wrapper.classList.add('ready');
}, 50);
};
// Event delegation
document.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
const toolbarBtn = target.closest('.mermaid-toolbar button') as HTMLElement | null;
if (toolbarBtn) return openModal(+(toolbarBtn.dataset.idx!));
const zoomBtn = target.closest('.mermaid-modal-controls button') as HTMLElement | null;
if (zoomBtn && pzInstance) {
const z = zoomBtn.dataset.zoom;
const rect = modalBody.getBoundingClientRect();
if (z === 'fit') fitToScreen();
else if (z === '0') { pzInstance.moveTo(0, 0); pzInstance.zoomAbs(0, 0, 1); }
else pzInstance.smoothZoom(rect.width / 2, rect.height / 2, z === '1' ? 1.5 : 0.67);
}
});
document.getElementById('mermaid-modal-close')!.addEventListener('click', closeModal);
modalBody.addEventListener('click', (e) => { if (e.target === modalBody) closeModal(); });
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal.classList.contains('active')) closeModal();
});
}
export async function initMermaidPage(config: MermaidConfig) {
const elements = document.querySelectorAll('.mermaid') as NodeListOf<HTMLElement>;
if (!elements.length) return;
const sources = Array.from(elements).map(el => el.innerHTML);
const perDiagramTransparent = sources.map(src => /%%\s*transparent\s*%%/i.test(src));
const cache: Record<Scheme, string[]> = { light: [], dark: [] };
const themes = {
light: buildThemeConfig(config, 'light'),
dark: buildThemeConfig(config, 'dark'),
};
const baseConfig = buildBaseConfig(config);
const applyTransparency = (el: HTMLElement, i: number) => {
if (perDiagramTransparent[i]) el.querySelector('svg')?.style.setProperty('background', 'transparent');
};
setupWrappers(elements);
setupModal(elements);
// Initial render
const scheme = getScheme();
initWithTheme(scheme, themes, baseConfig);
await mermaid.run({ nodes: Array.from(elements) });
elements.forEach((el, i) => {
el.style.visibility = '';
cache[scheme][i] = el.innerHTML;
applyTransparency(el, i);
});
// Pre-render alternate theme during idle time
const alt: Scheme = scheme === 'dark' ? 'light' : 'dark';
const idle = window.requestIdleCallback ?? ((fn: IdleRequestCallback) => setTimeout(fn, 1000));
idle(() => {
if (cache[alt].length) return;
initWithTheme(alt, themes, baseConfig);
renderOffscreen(sources).then(results => { cache[alt] = results; });
});
// Swap cached diagrams on theme change
window.addEventListener('onColorSchemeChange', async () => {
const newScheme = getScheme();
if (!cache[newScheme].length) {
initWithTheme(newScheme, themes, baseConfig);
cache[newScheme] = await renderOffscreen(sources);
}
elements.forEach((el, i) => { el.innerHTML = cache[newScheme][i]; applyTransparency(el, i); });
});
}
+104
View File
@@ -0,0 +1,104 @@
export function setupPaginationJump() {
const triggers = document.querySelectorAll<HTMLButtonElement>('.pagination-jump-trigger');
const dialog = document.getElementById('pagination-jump-dialog') as HTMLDialogElement;
if (!dialog || triggers.length === 0) return;
const nav = document.querySelector('.pagination') as HTMLElement;
const input = document.getElementById('pagination-jump-input') as HTMLInputElement;
const form = dialog.querySelector('.pagination-jump-form') as HTMLFormElement;
const supportsDialog = typeof dialog.showModal === 'function' && typeof dialog.close === 'function';
let lastFocusedElement: HTMLElement | null = null;
if (!supportsDialog || !nav || !input || !form) return;
const closeDialog = (): void => {
if (dialog.classList.contains('closing')) return;
dialog.classList.add('closing');
dialog.addEventListener(
'animationend',
() => {
dialog.classList.remove('closing');
dialog.close();
if (lastFocusedElement?.isConnected) {
lastFocusedElement.focus();
}
},
{ once: true }
);
};
// Open dialog when triggers are clicked
triggers.forEach((trigger) => {
trigger.addEventListener('click', () => {
const activeElement = document.activeElement;
lastFocusedElement = activeElement instanceof HTMLElement ? activeElement : trigger;
dialog.showModal();
input.value = '';
input.focus();
});
});
// Handle ESC key closing the dialog
dialog.addEventListener('cancel', (e) => {
e.preventDefault();
closeDialog();
});
// Close dialog when clicking backdrop
dialog.addEventListener('click', (e) => {
const rect = dialog.getBoundingClientRect();
const isInDialog =
rect.top <= e.clientY &&
e.clientY <= rect.top + rect.height &&
rect.left <= e.clientX &&
e.clientX <= rect.left + rect.width;
if (!isInDialog) {
closeDialog();
}
});
// Allow Enter key to trigger validation and submit
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (form.reportValidity()) {
form.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
}
}
});
// Handle form submission
form.addEventListener('submit', (e) => {
e.preventDefault();
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const targetPage = parseInt(input.value);
if (isNaN(targetPage) || targetPage < 1) return;
const totalPages = parseInt(nav.dataset.total || '0');
if (targetPage > totalPages) return;
const firstUrl = nav.dataset.firstUrl || '';
const formatUrl = nav.dataset.formatUrl || '';
let targetUrl = '';
if (targetPage === 1) {
targetUrl = firstUrl;
} else {
// formatUrl is the URL for page 2. E.g., /tags/page/2/ or /page/2/
// Replace the '2' before the trailing slash or .html with the target page number
targetUrl = formatUrl.replace(/2([^\d]*)$/, `${targetPage}$1`);
}
if (targetUrl) {
window.location.href = targetUrl;
}
closeDialog();
});
}
+140
View File
@@ -0,0 +1,140 @@
// Implements a scroll spy system for the ToC, displaying the current section with an indicator and scrolling to it when needed.
// Inspired from https://gomakethings.com/debouncing-your-javascript-events/
function debounced(func: Function) {
let timeout;
return () => {
if (timeout) {
window.cancelAnimationFrame(timeout);
}
timeout = window.requestAnimationFrame(() => func());
}
}
const headersQuery = ".article-content h1[id], .article-content h2[id], .article-content h3[id], .article-content h4[id], .article-content h5[id], .article-content h6[id]";
const tocQuery = "#TableOfContents";
const navigationQuery = "#TableOfContents li";
const activeClass = "active-class";
function scrollToTocElement(tocElement: HTMLElement, scrollableNavigation: HTMLElement) {
let textHeight = tocElement.querySelector("a").offsetHeight;
let scrollTop = tocElement.offsetTop - scrollableNavigation.offsetHeight / 2 + textHeight / 2 - scrollableNavigation.offsetTop;
if (scrollTop < 0) {
scrollTop = 0;
}
scrollableNavigation.scrollTo({ top: scrollTop, behavior: "smooth" });
}
type IdToElementMap = { [key: string]: HTMLElement };
function buildIdToNavigationElementMap(navigation: NodeListOf<Element>): IdToElementMap {
const sectionLinkRef: IdToElementMap = {};
navigation.forEach((navigationElement: HTMLElement) => {
const link = navigationElement.querySelector("a");
if (link) {
const href = link.getAttribute("href");
if (href.startsWith("#")) {
sectionLinkRef[href.slice(1)] = navigationElement;
}
}
});
return sectionLinkRef;
}
function computeOffsets(headers: NodeListOf<Element>) {
let sectionsOffsets = [];
headers.forEach((header: HTMLElement) => { sectionsOffsets.push({ id: header.id, offset: header.offsetTop }) });
sectionsOffsets.sort((a, b) => a.offset - b.offset);
return sectionsOffsets;
}
function setupScrollspy() {
let headers = document.querySelectorAll(headersQuery);
if (!headers) {
console.warn("No header matched query", headers);
return;
}
let scrollableNavigation = document.querySelector(tocQuery) as HTMLElement | undefined;
if (!scrollableNavigation) {
console.warn("No toc matched query", tocQuery);
return;
}
let navigation = document.querySelectorAll(navigationQuery);
if (!navigation) {
console.warn("No navigation matched query", navigationQuery);
return;
}
let sectionsOffsets = computeOffsets(headers);
// We need to avoid scrolling when the user is actively interacting with the ToC. Otherwise, if the user clicks on a link in the ToC,
// we would scroll their view, which is not optimal usability-wise.
let tocHovered: boolean = false;
scrollableNavigation.addEventListener("mouseenter", debounced(() => tocHovered = true));
scrollableNavigation.addEventListener("mouseleave", debounced(() => tocHovered = false));
let activeSectionLink: Element;
let idToNavigationElement: IdToElementMap = buildIdToNavigationElementMap(navigation);
function scrollHandler() {
let scrollPosition = document.documentElement.scrollTop || document.body.scrollTop;
let newActiveSection: HTMLElement | undefined;
// Find the section that is currently active.
// It is possible for no section to be active, so newActiveSection may be undefined.
sectionsOffsets.forEach((section) => {
if (scrollPosition >= section.offset - 20) {
newActiveSection = document.getElementById(section.id);
}
});
// Find the link for the active section. Once again, there are a few edge cases:
// - No active section = no link => undefined
// - No active section but the link does not exist in toc (e.g. because it is outside of the applicable ToC levels) => undefined
let newActiveSectionLink: HTMLElement | undefined
if (newActiveSection) {
newActiveSectionLink = idToNavigationElement[newActiveSection.id];
}
if (newActiveSection && !newActiveSectionLink) {
// The active section does not have a link in the ToC, so we can't scroll to it.
console.debug("No link found for section", newActiveSection);
} else if (newActiveSectionLink !== activeSectionLink) {
if (activeSectionLink)
activeSectionLink.classList.remove(activeClass);
if (newActiveSectionLink) {
newActiveSectionLink.classList.add(activeClass);
if (!tocHovered) {
// Scroll so that newActiveSectionLink is in the middle of scrollableNavigation, except when it's from a manual click (hence the tocHovered check)
scrollToTocElement(newActiveSectionLink, scrollableNavigation);
}
}
activeSectionLink = newActiveSectionLink;
}
}
window.addEventListener("scroll", debounced(scrollHandler));
// Resizing may cause the offset values to change: recompute them.
function resizeHandler() {
sectionsOffsets = computeOffsets(headers);
scrollHandler();
}
// Use ResizeObserver to detect changes in the size of .article-content
const articleContent = document.querySelector(".article-content");
if (articleContent) {
const resizeObserver = new ResizeObserver(debounced(resizeHandler));
resizeObserver.observe(articleContent);
}
window.addEventListener("resize", debounced(resizeHandler));
}
export { setupScrollspy };
+348
View File
@@ -0,0 +1,348 @@
interface pageData {
title: string,
date: string,
permalink: string,
content: string,
image?: string,
preview: string,
matchCount: number
}
interface match {
start: number,
end: number
}
/**
* Escape HTML tags as HTML entities
* Edited from:
* @link https://stackoverflow.com/a/5499821
*/
const tagsToReplace = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'…': '&hellip;'
};
function replaceTag(tag: string) {
return (tagsToReplace as Record<string, string>)[tag] || tag;
}
function replaceHTMLEnt(str: string) {
return str.replace(/[&<>"]/g, replaceTag);
}
function escapeRegExp(string: string) {
return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
}
class Search {
private data: pageData[];
private form: HTMLFormElement;
private input: HTMLInputElement;
private list: HTMLDivElement;
private resultTitle: HTMLHeadElement;
private resultTitleTemplate: string;
private container: HTMLDivElement;
constructor({ form, input, list, resultTitle, resultTitleTemplate }: {
form: HTMLFormElement,
input: HTMLInputElement,
list: HTMLDivElement,
resultTitle: HTMLHeadingElement,
resultTitleTemplate: string
}) {
this.form = form;
this.input = input;
this.list = list;
this.resultTitle = resultTitle;
this.resultTitleTemplate = resultTitleTemplate;
this.container = list.parentElement as HTMLDivElement;
/// Check if there's already value in the search input
if (this.input.value.trim() !== '') {
this.doSearch(this.input.value.split(' '));
}
else {
this.handleQueryString();
}
this.bindQueryStringChange();
this.bindSearchForm();
}
/**
* Processes search matches
* @param str original text
* @param matches array of matches
* @param ellipsis whether to add ellipsis to the end of each match
* @param charLimit max length of preview string
* @param offset how many characters before and after the match to include in preview
* @returns preview string
*/
private static processMatches(str: string, matches: match[], ellipsis: boolean = true, charLimit = 140, offset = 20): string {
matches.sort((a, b) => {
return a.start - b.start;
});
let i = 0,
lastIndex = 0,
charCount = 0;
const resultArray: string[] = [];
while (i < matches.length) {
const item = matches[i];
/// item.start >= lastIndex (equal only for the first iteration)
/// because of the while loop that comes after, iterating over variable j
if (ellipsis && item.start - offset > lastIndex) {
resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, lastIndex + offset))} [...] `);
resultArray.push(`${replaceHTMLEnt(str.substring(item.start - offset, item.start))}`);
charCount += offset * 2;
}
else {
/// If the match is too close to the end of last match, don't add ellipsis
resultArray.push(replaceHTMLEnt(str.substring(lastIndex, item.start)));
charCount += item.start - lastIndex;
}
let j = i + 1,
end = item.end;
/// Include as many matches as possible
/// [item.start, end] is the range of the match
while (j < matches.length && matches[j].start <= end) {
end = Math.max(matches[j].end, end);
++j;
}
resultArray.push(`<mark>${replaceHTMLEnt(str.substring(item.start, end))}</mark>`);
charCount += end - item.start;
i = j;
lastIndex = end;
if (ellipsis && charCount > charLimit) break;
}
/// Add the rest of the string
if (lastIndex < str.length) {
let end = str.length;
if (ellipsis) end = Math.min(end, lastIndex + offset);
resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, end))}`);
if (ellipsis && end != str.length) {
resultArray.push(` [...]`);
}
}
return resultArray.join('');
}
private async searchKeywords(keywords: string[]) {
const rawData = await this.getData();
const results: pageData[] = [];
const regex = new RegExp(keywords.filter((v, index, arr) => {
arr[index] = escapeRegExp(v);
return v.trim() !== '';
}).join('|'), 'gi');
for (const item of rawData) {
const titleMatches: match[] = [],
contentMatches: match[] = [];
let result = {
...item,
preview: '',
matchCount: 0
}
const contentMatchAll = item.content.matchAll(regex);
for (const match of Array.from(contentMatchAll)) {
contentMatches.push({
start: match.index,
end: match.index + match[0].length
});
}
const titleMatchAll = item.title.matchAll(regex);
for (const match of Array.from(titleMatchAll)) {
titleMatches.push({
start: match.index,
end: match.index + match[0].length
});
}
if (titleMatches.length > 0) result.title = Search.processMatches(result.title, titleMatches, false);
if (contentMatches.length > 0) {
result.preview = Search.processMatches(result.content, contentMatches);
}
else {
/// If there are no matches in the content, use the first 140 characters as preview
result.preview = replaceHTMLEnt(result.content.substring(0, 140));
}
result.matchCount = titleMatches.length + contentMatches.length;
if (result.matchCount > 0) results.push(result);
}
/// Result with more matches appears first
return results.sort((a, b) => {
return b.matchCount - a.matchCount;
});
}
private async doSearch(keywords: string[]) {
const startTime = performance.now();
const results = await this.searchKeywords(keywords);
this.clear();
for (const item of results) {
this.list.append(Search.render(item));
}
const endTime = performance.now();
this.resultTitle.innerText = this.generateResultTitle(results.length, ((endTime - startTime) / 1000).toPrecision(1));
this.container?.classList.remove('hidden');
}
private generateResultTitle(resultLen: number, time: string) {
return this.resultTitleTemplate.replace("#PAGES_COUNT", resultLen.toString()).replace("#TIME_SECONDS", time);
}
public async getData() {
if (!this.data) {
/// Not fetched yet
const jsonURL = this.form.dataset.json as string;
this.data = await fetch(jsonURL).then(res => res.json());
const parser = new DOMParser();
for (const item of this.data) {
item.content = parser.parseFromString(item.content, 'text/html').body.innerText;
}
}
return this.data;
}
private bindSearchForm() {
let lastSearch = '';
const eventHandler = (e: Event) => {
e.preventDefault();
const keywords = this.input.value.trim();
Search.updateQueryString(keywords, true);
if (keywords === '') {
lastSearch = '';
return this.clear();
}
if (lastSearch === keywords) return;
lastSearch = keywords;
this.doSearch(keywords.split(' '));
}
this.input.addEventListener('input', eventHandler);
this.input.addEventListener('compositionend', eventHandler);
}
private clear() {
this.list.innerHTML = '';
this.resultTitle.innerText = '';
this.container.classList.add('hidden');
}
private bindQueryStringChange() {
window.addEventListener('popstate', (e) => {
this.handleQueryString()
})
}
private handleQueryString() {
const pageURL = new URL(window.location.toString());
const keywords = pageURL.searchParams.get('keyword') || '';
this.input.value = keywords;
if (keywords) {
this.doSearch(keywords.split(' '));
}
else {
this.clear()
}
}
private static updateQueryString(keywords: string, replaceState = false) {
const pageURL = new URL(window.location.toString());
if (keywords === '') {
pageURL.searchParams.delete('keyword')
}
else {
pageURL.searchParams.set('keyword', keywords);
}
if (replaceState) {
window.history.replaceState('', '', pageURL.toString());
}
else {
window.history.pushState('', '', pageURL.toString());
}
}
public static render(item: pageData) {
return (
<article>
<a href={item.permalink}>
<div class="article-details">
<h2 class="article-title" dangerouslySetInnerHTML={{ __html: item.title }}></h2>
<section class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></section>
</div>
{item.image &&
<div class="article-image">
<img src={item.image} loading="lazy" />
</div>
}
</a>
</article>
);
}
}
declare global {
interface Window {
searchResultTitleTemplate: string;
}
}
window.addEventListener('load', () => {
setTimeout(function () {
const searchForm = document.querySelector('.search-form') as HTMLFormElement,
searchInput = searchForm?.querySelector('input') as HTMLInputElement,
searchResultList = document.querySelector('.search-result--list') as HTMLDivElement,
searchResultTitle = document.querySelector('.search-result--title') as HTMLHeadingElement;
if (!searchForm || !searchInput || !searchResultList || !searchResultTitle) return;
new Search({
form: searchForm,
input: searchInput,
list: searchResultList,
resultTitle: searchResultTitle,
resultTitleTemplate: window.searchResultTitleTemplate
});
}, 0);
})
export default Search;
+37
View File
@@ -0,0 +1,37 @@
// Implements smooth scrolling when clicking on an anchor link.
// This is required instead of using modern CSS because Chromium does not currently support scrolling
// one element with scrollTo while another element is scrolled because of a click on a link. This would
// thus not work with the ToC scrollspy and e.g. footnotes.
// Here are additional links about this issue:
// - https://stackoverflow.com/questions/49318497/google-chrome-simultaneously-smooth-scrollintoview-with-more-elements-doesn
// - https://stackoverflow.com/questions/57214373/scrollintoview-using-smooth-function-on-multiple-elements-in-chrome
// - https://bugs.chromium.org/p/chromium/issues/detail?id=833617
// - https://bugs.chromium.org/p/chromium/issues/detail?id=1043933
// - https://bugs.chromium.org/p/chromium/issues/detail?id=1121151
const anchorLinksQuery = "a[href]";
function setupSmoothAnchors() {
document.querySelectorAll(anchorLinksQuery).forEach(aElement => {
let href = aElement.getAttribute("href");
if (!href.startsWith("#")) {
return;
}
aElement.addEventListener("click", clickEvent => {
clickEvent.preventDefault();
const targetId = decodeURI(aElement.getAttribute("href").substring(1)),
target = document.getElementById(targetId) as HTMLElement,
offset = target.getBoundingClientRect().top - document.documentElement.getBoundingClientRect().top;
window.history.pushState({}, "", aElement.getAttribute("href"));
scrollTo({
top: offset,
behavior: "smooth"
});
});
});
}
export { setupSmoothAnchors };