Merge pull request #39 from ClovertaTheTrilobita/dev

refactor overall design
This commit is contained in:
ClovertaTheTrilobita 2026-09-17 16:18:24 +08:00 committed by GitHub
commit 62a1917790
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1512 additions and 628 deletions

View file

@ -5,6 +5,7 @@ import svelte from "@astrojs/svelte";
import rehypeMermaid from "rehype-mermaid";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import rehypeImageCaptions from "./src/plugins/rehype-image-captions.mjs";
// https://astro.build/config
import { fileURLToPath } from 'node:url'
@ -27,6 +28,7 @@ export default defineConfig({
excludeLangs: ['mermaid', 'math'],
},
rehypePlugins: [
rehypeImageCaptions,
rehypeMermaid,
rehypeKatex,
],

View file

@ -62,14 +62,13 @@
border-right: var(--deep-blue) 2.5px solid;
border-bottom: var(--deep-blue) 2.5px solid;
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
background: color-mix(in srgb, var(--background-color) 94%, transparent);
color: #222;
/* box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); */
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(6px);
}
#theme-toggle-fab {
@ -141,10 +140,16 @@
:global(.dark) #theme-toggle-fab,
:global(.dark) #back-to-top {
background: rgba(34, 34, 34, 0.92);
background: color-mix(
in srgb,
var(--background-color-dark) 94%,
transparent
);
color: #f5f5f5;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32);
border: #515151 2px solid;
border-top-color: #515151;
border-left-color: #515151;
border-right-color: #515151;
border-bottom-color: #515151;
}
@media (max-width: 640px) {

View file

@ -8,7 +8,9 @@ const t = getTranslations(lang);
---
<footer class="footer">
<div class="footer-divider" data-footer-divider>
<Divider />
</div>
<a
href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog"
class="github"

View file

@ -20,7 +20,7 @@ const t = getTranslations(lang);
{t.friends.content.map((line: string) => <p set:html={line} />)}
</div>
<div class="friend-code-block">
<pre><code>---
<pre data-language="yaml"><code class="language-yaml">---
name: 'CLoverta的博客'
description: "欢迎光临,请进门左转"
url: "https://blog.cloverta.top"
@ -59,27 +59,35 @@ avatar: 'https://s2.loli.net/2025/11/22/tiDKuzdqycx1v9B.png'
}
.friend-code-block {
background: #fbf5f2;
border: 1px solid #8fa1ad;
border-radius: 3px;
padding: 0rem;
overflow-x: auto;
/* color: var(--text-color); */
min-width: 0;
margin-top: 1rem;
}
:global(.dark) .friend-code-block {
background: rgba(127, 127, 127, 0.12);
.friend-code-block :global(.code-block-wrapper) {
margin: 0;
overflow: hidden;
border: 1px solid #8fa1ad;
border-radius: 3px;
}
.friend-code-block pre {
margin: 0;
background: var(--comment-background);
}
.friend-code-block code {
font-family: monospace;
font-family: "Maple Mono", monospace;
white-space: pre;
}
:global(.dark) .friend-code-block :global(.code-block-wrapper) {
border-color: rgba(170, 183, 196, 0.45);
}
:global(.dark) .friend-code-block pre {
background: rgba(127, 127, 127, 0.12);
}
.image-wrap {
width: fit-content;
margin: 0 auto;

View file

@ -8,7 +8,9 @@ const lang = getLangFromUrl(Astro.url);
const t = getTranslations(lang);
---
<div class="header-slot">
<header class="site-header">
<div class="header-content">
<div class="theme-icon-wrap">
<ThemeIcon />
</div>
@ -26,23 +28,35 @@ const t = getTranslations(lang);
</div>
<Navigation />
</nav>
</div>
<div class="divider">
<Divider />
</div>
</header>
</div>
<style>
.site-header {
position: relative;
z-index: 30;
padding-top: 0.5rem;
}
.header-content {
position: relative;
}
.banner-link {
display: inline-block;
display: block;
}
.banner-logo {
display: block;
height: 55px; /* 按你的 banner 比例改 */
height: 55px;
background-color: #1f2328;
-webkit-mask: url("/images/banner-black.svg") no-repeat left / contain;
mask: url("/images/banner-black.svg") no-repeat left / contain;
transform-origin: left center;
}
:global(html.dark .banner-logo) {
@ -55,11 +69,6 @@ const t = getTranslations(lang);
display: block;
}
.site-header {
position: relative;
padding-top: 0.5rem;
}
.theme-icon-wrap {
position: absolute;
top: 0;
@ -71,17 +80,14 @@ const t = getTranslations(lang);
display: flex;
flex-direction: column;
gap: 1rem;
max-height: 12rem;
}
.header-nav .banner {
margin: 0;
font-style: italic;
line-height: 1.1;
/* 给右上角 ThemeIcon 留空间 */
max-width: calc(100% - 7rem);
/* 桌面正常,手机自动缩小 */
font-size: clamp(1.6rem, 6vw, 2.5rem);
}
@ -90,8 +96,106 @@ const t = getTranslations(lang);
margin-bottom: 2rem;
}
@media (max-width: 768px) {
@media (min-width: 769px) {
.header-slot {
/* Keep document height independent of scroll-driven header height. */
display: flow-root;
position: sticky;
top: 0;
z-index: 30;
height: var(--header-slot-height, auto);
pointer-events: none;
}
.site-header {
pointer-events: auto;
width: 100%;
max-width: 90ch;
margin-inline: auto;
padding: 0.5rem 0 0;
background-color: color-mix(
in srgb,
var(--background-color) var(--header-surface-weight, 0%),
transparent
);
transform: translateY(0);
transition: transform 0.3s ease;
}
.header-content {
width: 100%;
}
.header-slot.is-scroll-hidden .site-header,
.header-slot.is-footer-hidden .site-header {
transform: translateY(calc(-100% - 0.5rem));
}
:global(html.dark) .site-header {
background-color: color-mix(
in srgb,
#1e1e1e var(--header-surface-weight, 0%),
transparent
);
}
.theme-icon-wrap {
top: var(--theme-top, 0);
right: 0;
}
.header-nav {
gap: var(--header-gap, 1rem);
height: var(--header-nav-height, auto);
max-height: var(--header-nav-height, 12rem);
transform: translateY(var(--header-nav-y, 0));
}
.header-nav .banner {
flex-shrink: 0;
width: var(--banner-width, auto);
max-width: var(--banner-width, calc(100% - 7rem));
}
.banner-logo {
height: var(--logo-height, 55px);
}
.site-header :global(.site-nav) {
width: var(--site-nav-width, auto);
margin: var(--site-nav-margin, 1rem) 0;
padding-bottom: var(--site-nav-padding, 1rem);
transform: translate(var(--site-nav-x, 0), var(--site-nav-y, 0));
}
.site-header :global(.site-nav-desktop) {
gap: var(--site-nav-gap, 1.5rem);
}
.site-header :global(.site-nav a) {
font-size: var(--site-nav-font-size, 1.05rem);
}
.site-header > .divider {
width: 100%;
margin-top: var(--divider-margin-top, 0.2rem);
margin-bottom: var(--divider-margin-bottom, 2rem);
transform: translateY(var(--divider-y, 0));
}
.site-header > .divider :global(.divider) {
width: 100%;
margin-bottom: var(--divider-inner-margin-bottom, 1rem);
}
}
@media (max-width: 768px) {
.header-slot {
display: contents;
}
.site-header {
position: relative;
padding-top: 0.25rem;
}
@ -101,3 +205,268 @@ const t = getTranslations(lang);
}
}
</style>
<script>
const desktopHeaderQuery = window.matchMedia("(min-width: 769px)");
const HEADER_SCROLL_DISTANCE = 120;
const DIRECTION_TRIGGER_DISTANCE = 6;
const headerStyleProperties = [
"--header-surface-weight",
"--theme-top",
"--header-gap",
"--header-nav-height",
"--header-nav-y",
"--banner-width",
"--logo-height",
"--site-nav-width",
"--site-nav-margin",
"--site-nav-padding",
"--site-nav-x",
"--site-nav-y",
"--site-nav-gap",
"--site-nav-font-size",
"--divider-margin-top",
"--divider-margin-bottom",
"--divider-inner-margin-bottom",
"--divider-y",
];
type HeaderMetrics = {
header: HTMLElement;
expandedWidth: number;
expandedNavHeight: number;
expandedBannerWidth: number;
expandedSiteNavWidth: number;
rootFontSize: number;
};
let headerFrame = 0;
let headerMetrics: HeaderMetrics | null = null;
let footerDividerObserver: IntersectionObserver | null = null;
let isFooterDividerVisible = false;
let lastScrollY = window.scrollY;
let scrollDirection = 0;
let directionDistance = 0;
const lerp = (start: number, end: number, progress: number) =>
start + (end - start) * progress;
function clearHeaderStyles(header: HTMLElement) {
headerStyleProperties.forEach((property) => {
header.style.removeProperty(property);
});
}
function measureHeader(header: HTMLElement): HeaderMetrics | null {
clearHeaderStyles(header);
const slot = header.parentElement;
if (!slot) return null;
slot.style.removeProperty("--header-slot-height");
const headerNav = header.querySelector<HTMLElement>(".header-nav");
const banner = header.querySelector<HTMLElement>(".banner");
const siteNav = header.querySelector<HTMLElement>(".site-nav");
if (!headerNav || !banner || !siteNav) return null;
// Measure the expanded footprint, including its bottom spacing, once.
// Only the contents animate; the following page content never reflows.
slot.style.setProperty(
"--header-slot-height",
`${slot.getBoundingClientRect().height}px`,
);
return {
header,
expandedWidth: header.getBoundingClientRect().width,
expandedNavHeight: headerNav.getBoundingClientRect().height,
expandedBannerWidth: banner.getBoundingClientRect().width,
expandedSiteNavWidth: siteNav.getBoundingClientRect().width,
rootFontSize: Number.parseFloat(
getComputedStyle(document.documentElement).fontSize,
),
};
}
function updateHeaderState() {
const header = document.querySelector<HTMLElement>(".site-header");
if (!header) return;
updateScrollVisibility(header.parentElement, window.scrollY);
updateFooterVisibility(header.parentElement, window.scrollY);
if (!desktopHeaderQuery.matches) {
clearHeaderStyles(header);
header.parentElement?.style.removeProperty("--header-slot-height");
headerMetrics = null;
return;
}
if (!headerMetrics || headerMetrics.header !== header) {
headerMetrics = measureHeader(header);
}
if (!headerMetrics) return;
const {
expandedWidth,
expandedNavHeight,
expandedBannerWidth,
expandedSiteNavWidth,
rootFontSize,
} = headerMetrics;
const rawProgress = Math.min(
Math.max(window.scrollY / HEADER_SCROLL_DISTANCE, 0),
1,
);
const progress = rawProgress;
const viewportWidth = document.documentElement.clientWidth;
const compactLogoWidth = Math.min(
Math.max(9 * rootFontSize, viewportWidth * 0.22),
12 * rootFontSize,
);
const compactSiteNavWidth = Math.max(
expandedWidth - compactLogoWidth - 7.5 * rootFontSize,
0,
);
const compactNavGap = Math.min(
Math.max(0.75 * rootFontSize, viewportWidth * 0.02),
1.5 * rootFontSize,
);
const values: Record<string, string> = {
"--header-surface-weight": `${lerp(0, 98, progress)}%`,
"--theme-top": `${lerp(0, 0.85 * rootFontSize, progress)}px`,
"--header-gap": `${lerp(rootFontSize, 0, progress)}px`,
"--header-nav-height": `${lerp(expandedNavHeight, 3 * rootFontSize, progress)}px`,
"--header-nav-y": `${lerp(0, 0.5 * rootFontSize, progress)}px`,
"--banner-width": `${lerp(expandedBannerWidth, compactLogoWidth, progress)}px`,
"--logo-height": `${lerp(55, 36, progress)}px`,
"--site-nav-width": `${lerp(expandedSiteNavWidth, compactSiteNavWidth, progress)}px`,
"--site-nav-margin": `${lerp(rootFontSize, 0, progress)}px`,
"--site-nav-padding": `${lerp(rootFontSize, 0, progress)}px`,
"--site-nav-x": `${lerp(0, compactLogoWidth + 1.5 * rootFontSize, progress)}px`,
"--site-nav-y": `${lerp(0, -36, progress)}px`,
"--site-nav-gap": `${lerp(1.5 * rootFontSize, compactNavGap, progress)}px`,
"--site-nav-font-size": `${lerp(1.05 * rootFontSize, 0.95 * rootFontSize, progress)}px`,
"--divider-margin-top": `${lerp(0.2 * rootFontSize, 0.45 * rootFontSize, progress)}px`,
"--divider-margin-bottom": `${lerp(2 * rootFontSize, 0, progress)}px`,
"--divider-inner-margin-bottom": `${lerp(rootFontSize, 0, progress)}px`,
"--divider-y": `${lerp(0, -0.1 * rootFontSize, progress)}px`,
};
Object.entries(values).forEach(([property, value]) => {
header.style.setProperty(property, value);
});
}
function updateScrollVisibility(
slot: HTMLElement | null,
currentScrollY: number,
) {
if (!slot) return;
const delta = currentScrollY - lastScrollY;
lastScrollY = currentScrollY;
if (
!desktopHeaderQuery.matches ||
currentScrollY <= HEADER_SCROLL_DISTANCE
) {
slot.classList.remove("is-scroll-hidden");
scrollDirection = 0;
directionDistance = 0;
return;
}
if (Math.abs(delta) < 0.5) return;
const nextDirection = Math.sign(delta);
if (nextDirection !== scrollDirection) {
scrollDirection = nextDirection;
directionDistance = 0;
}
directionDistance += Math.abs(delta);
if (directionDistance < DIRECTION_TRIGGER_DISTANCE) return;
slot.classList.toggle("is-scroll-hidden", scrollDirection > 0);
directionDistance = 0;
}
function updateFooterVisibility(
slot: HTMLElement | null,
currentScrollY: number,
) {
if (!slot) return;
const headerIsFullyExpanded = currentScrollY >= HEADER_SCROLL_DISTANCE;
slot.classList.toggle(
"is-footer-hidden",
desktopHeaderQuery.matches &&
headerIsFullyExpanded &&
isFooterDividerVisible,
);
}
function requestHeaderUpdate() {
if (headerFrame) return;
headerFrame = window.requestAnimationFrame(() => {
headerFrame = 0;
updateHeaderState();
});
}
function bindFooterDividerObserver() {
footerDividerObserver?.disconnect();
footerDividerObserver = null;
isFooterDividerVisible = false;
const slot = document.querySelector<HTMLElement>(".header-slot");
const footerDivider = document.querySelector<HTMLElement>(
"[data-footer-divider]",
);
if (!slot) return;
slot.classList.remove("is-footer-hidden");
if (!desktopHeaderQuery.matches || !footerDivider) return;
footerDividerObserver = new IntersectionObserver(([entry]) => {
isFooterDividerVisible = entry.isIntersecting;
updateFooterVisibility(slot, window.scrollY);
});
footerDividerObserver.observe(footerDivider);
}
window.addEventListener("scroll", requestHeaderUpdate, { passive: true });
window.addEventListener("resize", () => {
headerMetrics = null;
requestHeaderUpdate();
});
desktopHeaderQuery.addEventListener("change", () => {
requestHeaderUpdate();
bindFooterDividerObserver();
});
document.addEventListener("astro:page-load", () => {
headerMetrics = null;
lastScrollY = window.scrollY;
scrollDirection = 0;
directionDistance = 0;
isFooterDividerVisible = false;
document
.querySelector<HTMLElement>(".header-slot")
?.classList.remove("is-scroll-hidden");
updateHeaderState();
bindFooterDividerObserver();
});
document.fonts.ready.then(() => {
headerMetrics = null;
requestHeaderUpdate();
});
updateHeaderState();
bindFooterDividerObserver();
</script>

View file

@ -2,7 +2,7 @@
const { lang = "zh" } = Astro.props;
const host = import.meta.env.PUBLIC_REMARK42_HOST;
const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
const max = 10;
const max = 5;
const heading = lang === "zh" ? "最新评论" : "Latest comments";
---
@ -65,6 +65,13 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
}
}
function getDiscussionUrl(url, lang) {
const localizedUrl = rewritePostUrl(url, lang);
if (!localizedUrl || localizedUrl === "#") return "#";
return `${localizedUrl.split("#")[0]}#comments`;
}
function getAvatar(item) {
return (
item?.user?.picture || item?.user?.photo || item?.user?.avatar || ""
@ -75,6 +82,22 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
return item?.user?.name || (lang === "zh" ? "匿名用户" : "Anonymous");
}
function getCommentPreview(html, lang) {
const template = document.createElement("template");
template.innerHTML = String(html || "");
const text = (template.content.textContent || "")
.replace(/\s+/g, " ")
.trim();
if (text) return escapeHtml(text);
if (template.content.querySelector("img")) {
return lang === "zh" ? "[图片]" : "[Image]";
}
return lang === "zh" ? "[无文字内容]" : "[No text content]";
}
function normalizePath(url) {
if (!url) return "";
@ -207,10 +230,14 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
item?.title ||
(lang === "zh" ? "未命名文章" : "Untitled post"),
);
const href = rewritePostUrl(item?.locator?.url || "", lang);
const href = escapeHtml(
rewritePostUrl(item?.locator?.url || "", lang),
);
const discussionHref = escapeHtml(
getDiscussionUrl(item?.locator?.url || "", lang),
);
// Remark42 返回的 text 是处理后的 HTML,适合直接渲染
const html = item?.text || "";
const preview = getCommentPreview(item?.text, lang);
const time = formatTime(item?.time, lang);
const replyTo = escapeHtml(item?.replyToName || "");
@ -227,7 +254,7 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
return `
<article class="comment-card">
<div class="comment-card-body">
<div class="comment-card-info">
<header class="comment-card-info">
<div class="comment-avatar">
${avatarHtml}
</div>
@ -238,19 +265,37 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
${time ? `<span class="comment-time">${escapeHtml(time)}</span>` : ""}
</div>
</div>
</div>
</header>
<div class="comment-card-title">
<div class="comment-card-text">${preview}</div>
<footer class="comment-source">
<div class="comment-source-context">
<span>${lang === "zh" ? "评论于" : "On"}</span>
<a class="comment-title-link" href="${href}">${title}</a>
</div>
<div class="comment-card-text">${html}</div>
<a class="comment-discussion-link" href="${discussionHref}">${
lang === "zh" ? "查看完整讨论" : "View discussion"
}</a>
</footer>
</div>
</article>
`;
})
.join("");
requestAnimationFrame(() => {
list.querySelectorAll(".comment-card").forEach((card) => {
const preview = card.querySelector(".comment-card-text");
if (!preview) return;
card.classList.toggle(
"is-truncated",
preview.scrollHeight > preview.clientHeight + 1,
);
});
});
loading.style.display = "none";
}
@ -259,7 +304,7 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
const host = section.dataset.host;
const siteId = section.dataset.siteId;
const max = section.dataset.max || "10";
const max = section.dataset.max || "5";
const lang = section.dataset.lang || "zh";
const list = section.querySelector(".latest-comments-list");

View file

@ -3,25 +3,48 @@ import { getLangFromUrl, getTranslations } from "@/i18n";
const lang = getLangFromUrl(Astro.url);
const t = getTranslations(lang);
const pathname = Astro.url.pathname;
const items = [
{ href: `/${lang}/`, label: t.nav.home, exact: true },
{ href: `/${lang}/about/`, label: t.nav.about },
{ href: `/${lang}/tags/`, label: t.nav.tags },
{ href: `/${lang}/timeline/`, label: t.nav.timeline },
{ href: `/${lang}/friends/`, label: t.nav.friends },
];
const isCurrent = (href: string, exact = false) =>
exact
? pathname === href || pathname === href.slice(0, -1)
: pathname.startsWith(href);
---
<nav class="site-nav" aria-label="Site navigation">
<div class="site-nav-desktop">
<a href={`/${lang}/`}>{t.nav.home}</a>
<a href={`/${lang}/about/`}>{t.nav.about}</a>
<a href={`/${lang}/tags/`}>{t.nav.tags}</a>
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a>
<a href={`/${lang}/friends/`}>{t.nav.friends}</a>
{
items.map(({ href, label, exact }) => (
<a
href={href}
aria-current={isCurrent(href, exact) ? "page" : undefined}
>
{label}
</a>
))
}
</div>
<details class="site-nav-mobile">
<summary>☰ {lang === "zh" ? "导航栏" : "Menu"}</summary>
<div class="dropdown-menu">
<a href={`/${lang}/`}>{t.nav.home}</a>
<a href={`/${lang}/about/`}>{t.nav.about}</a>
<a href={`/${lang}/tags/`}>{t.nav.tags}</a>
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a>
<a href={`/${lang}/friends/`}>{t.nav.friends}</a>
{
items.map(({ href, label, exact }) => (
<a
href={href}
aria-current={isCurrent(href, exact) ? "page" : undefined}
>
{label}
</a>
))
}
</div>
</details>
</nav>
@ -54,22 +77,45 @@ const t = getTranslations(lang);
}
.site-nav a {
color: black;
text-decoration: underline;
font-weight: 700;
font-size: 1.2rem;
position: relative;
color: var(--text-color);
text-decoration: none;
font-weight: 500;
font-size: 1.05rem;
line-height: 1.4;
}
.site-nav a:hover {
text-decoration: underline;
.site-nav-desktop a::after {
content: "";
position: absolute;
left: 50%;
bottom: -0.28rem;
width: 100%;
height: 2px;
background: var(--deep-red);
transform: translateX(-50%) scaleX(0);
transform-origin: center;
transition: transform 0.18s ease;
}
.site-nav-desktop a:hover::after,
.site-nav-desktop a:focus-visible::after,
.site-nav-desktop a[aria-current="page"]::after {
transform: translateX(-50%) scaleX(1);
}
.site-nav a:focus-visible {
outline: 2px solid var(--url-color);
outline-offset: 0.35rem;
text-decoration: none;
}
:global(.dark) .site-nav a {
color: white;
color: var(--text-color-dark);
}
:global(.dark) .site-nav a:hover {
color: #ddd;
color: #fff;
}
.site-nav-mobile {
@ -81,10 +127,10 @@ const t = getTranslations(lang);
list-style: none;
cursor: pointer;
display: inline-block;
font-weight: 700;
font-size: 1.1rem;
color: black;
text-decoration: underline;
font-weight: 500;
font-size: 1rem;
color: var(--text-color);
text-decoration: none;
user-select: none;
}
@ -121,6 +167,11 @@ const t = getTranslations(lang);
text-decoration: underline;
}
.dropdown-menu a[aria-current="page"] {
color: var(--deep-red);
font-weight: 600;
}
:global(.dark) .dropdown-menu {
background: #1f1b18;
border-color: #c8a98a;

View file

@ -1,6 +1,7 @@
---
import "@/styles/global.css";
import Remark42Counter from "@/components/Remark42Counter.astro";
import TagLink from "@/components/TagLink.astro";
import { getLangFromUrl } from "@/i18n";
const lang = getLangFromUrl(Astro.url);
@ -22,9 +23,7 @@ const tags = data.tags;
<div class="tags" transition:name={`post-tags-${data.postId}`}>
{
tags.map((tag: string) => (
<p class="tag">
<a href={`/${lang}/tags/${tag}`}>{tag}</a>
</p>
<TagLink tag={tag} lang={lang} />
))
}
</div>
@ -51,81 +50,31 @@ const tags = data.tags;
</div>
</li>
<div class="section-divider"></div>
<style>
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.tag {
font-family:
"Noto Sans SC",
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
margin: 0;
padding: 0.22em 0.65em;
font-size: 1rem;
line-height: 1.2;
border: 1px dashed #8b6b4a;
border-radius: 0;
background-color: transparent;
}
.tag a {
color: #6f4e37;
text-decoration: none;
font-size: 0.8rem;
font-weight: 500;
}
.tag a:hover {
text-decoration: underline;
}
:global(.dark) .tag {
border-color: #d8c7a1;
}
:global(.dark) .tag a {
color: #e6d8b8;
}
:global(.dark) .tag {
border-color: #d8c7a1;
}
:global(.dark) .tag a {
color: #e6d8b8;
gap: 0.25rem 0.9rem;
margin-top: 0.75rem;
min-width: 0;
font-size: 0.85rem;
}
.post-card {
list-style: none;
margin: 0 0 1rem 0;
padding-left: 0;
padding-bottom: 0.9rem;
padding-top: 0.9rem;
margin: 0;
padding: 0;
}
.post-card + .post-card {
margin-top: 2rem;
}
.post-link {
display: flex;
justify-content: space-between;
align-items: stretch;
gap: 1rem;
align-items: flex-start;
gap: 1.5rem;
width: 100%;
text-decoration: none;
}
@ -136,14 +85,13 @@ const tags = data.tags;
max-width: 40rem;
display: flex;
flex-direction: column;
justify-content: space-between;
line-height: 1.6;
min-height: calc(1.6em * 4);
}
.post-title-link {
text-decoration: none;
display: block;
min-width: 0;
}
.post-title {
@ -163,7 +111,8 @@ const tags = data.tags;
sans-serif;
font-weight: 500;
font-size: 1.34rem;
line-height: 1.6;
line-height: 1.5;
overflow-wrap: anywhere;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
@ -171,11 +120,13 @@ const tags = data.tags;
}
.post-description {
color: black;
margin-top: 0.5rem;
color: #4e5e6e;
text-decoration: none;
font-weight: 400;
font-size: 0.92rem;
/* font-style: italic; */
font-size: 0.95rem;
line-height: 1.7;
overflow-wrap: anywhere;
}
.post-meta-row {
@ -183,11 +134,13 @@ const tags = data.tags;
align-items: center;
gap: 0.9rem;
flex-wrap: wrap;
margin-top: 0.75rem;
font-size: 0.82rem;
font-weight: 400;
color: #66717c;
}
.post-date {
color: gray;
font-size: 0.95rem;
white-space: nowrap;
line-height: 1.6;
}
@ -196,8 +149,6 @@ const tags = data.tags;
display: inline-flex;
align-items: center;
gap: 0.7rem;
color: #6f8090;
font-size: 0.92rem;
line-height: 1.6;
white-space: nowrap;
}
@ -208,22 +159,28 @@ const tags = data.tags;
gap: 0.2rem;
}
.post-image-link {
display: block;
flex: 0 0 11rem;
width: 11rem;
aspect-ratio: 16 / 10;
overflow: hidden;
}
.post-image {
width: calc((1.6em * 4) * 16 / 10);
height: calc(1.6em * 4);
width: 100%;
height: 100%;
aspect-ratio: 16 / 10;
object-fit: cover;
object-position: center;
border: 1.5px #94a0ab dashed;
flex-shrink: 0;
display: block;
}
:global(.dark) .post-description {
color: #e6e6e6;
color: #c4ccd5;
}
:global(.dark) .post-stats {
:global(.dark) .post-meta-row {
color: #aab7c4;
}
@ -264,7 +221,6 @@ const tags = data.tags;
}
.post-image {
/* border: none; */
grid-area: image;
width: 100%;
height: calc(145px * 10 / 16);
@ -274,8 +230,9 @@ const tags = data.tags;
.post-image-link {
grid-area: image;
align-self: center;
display: block;
width: 145px;
aspect-ratio: auto;
overflow: visible;
text-decoration: none;
}

View file

@ -59,15 +59,15 @@ const latestPosts = filteredPosts.slice(0, 7);
}
.more-link-wrap {
margin-top: 1rem;
margin-top: 2rem;
text-align: right;
}
.more-link {
color: #6f8090;
font-weight: 700;
font-weight: 400;
text-decoration: none;
font-size: 1.14rem;
font-size: 0.9rem;
}
.more-link:hover {

View file

@ -83,6 +83,9 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
<style>
.post-menu {
box-sizing: border-box;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.post-menu-title {
@ -125,6 +128,7 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
.post-menu-item {
position: relative;
min-width: 0;
margin: 0.42rem 0;
line-height: 1.45;
}
@ -136,27 +140,41 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
}
.post-menu a {
display: block;
min-width: 0;
max-width: 100%;
padding: 0.08rem 0.35rem 0.08rem 0.55rem;
border-left: 2px solid transparent;
color: inherit;
text-decoration: none;
transition:
border-color 0.16s ease,
color 0.16s ease;
}
.post-menu a:hover {
text-decoration: underline;
}
.post-menu a.is-current {
border-left-color: var(--deep-red);
color: var(--url-color);
font-weight: 600;
}
.post-menu-mobile-toggle {
display: none;
}
@media (min-width: 1200px) {
.post-menu {
--content-width: 90ch;
--content-width: var(--page-content-width);
--menu-width: clamp(
140px,
calc((100vw - 1200px) * 0.42 + 140px),
260px
);
--menu-gap: 0rem;
--menu-gap: clamp(1.5rem, 2vw, 2rem);
position: fixed;
top: 8.5rem;
@ -168,7 +186,9 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
);
max-height: calc(100vh - 10rem);
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
padding: 0.2rem 0.2rem 0.2rem 0;
opacity: 0.9;
}
@ -187,8 +207,11 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
padding: 0.6rem 0.75rem;
border: 1.5px solid var(--deep-blue);
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(6px);
background: color-mix(
in srgb,
var(--background-color) 94%,
transparent
);
color: inherit;
font-size: 0.85rem;
cursor: pointer;
@ -232,11 +255,16 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
z-index: 29;
width: min(88vw, 24rem);
max-height: min(65vh, 32rem);
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
padding: 0.9rem 1rem;
border: 1.5px solid var(--deep-blue);
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(8px);
background: color-mix(
in srgb,
var(--background-color) 97%,
transparent
);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.08);
}
@ -329,8 +357,6 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
};
const updateCurrentHeading = () => {
if (window.innerWidth >= 1200) return;
headingItems = getHeadingElements();
let current = null;
@ -347,10 +373,16 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
preview.textContent = current ? current.text : defaultText;
headingLinks.forEach((link) =>
link.classList.remove("is-current"),
);
if (current) current.link.classList.add("is-current");
headingLinks.forEach((link) => {
const isCurrent = link === current?.link;
link.classList.toggle("is-current", isCurrent);
if (isCurrent) {
link.setAttribute("aria-current", "location");
} else {
link.removeAttribute("aria-current");
}
});
};
const toggleMenu = () => {
@ -377,7 +409,11 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
e.preventDefault();
el.scrollIntoView({
behavior: "smooth",
behavior: window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches
? "auto"
: "smooth",
block: "start",
});

View file

@ -0,0 +1,48 @@
---
interface Props {
tag: string;
lang: string;
}
const { tag, lang } = Astro.props;
---
<a class="tag-link" href={`/${lang}/tags/${encodeURIComponent(tag)}/`}>#{tag}</a>
<style>
.tag-link {
display: inline-block;
max-width: 100%;
padding-block: 0.2rem;
font-family: "Noto Sans SC", system-ui, sans-serif;
font-size: inherit;
font-weight: 400;
line-height: 1.6;
color: #536493;
overflow-wrap: anywhere;
text-decoration-line: underline;
text-decoration-style: dashed;
text-decoration-thickness: 1px;
text-underline-offset: 0.25em;
}
.tag-link:hover,
.tag-link:focus-visible {
color: var(--text-color);
text-decoration: underline dashed 1px;
}
.tag-link:focus-visible {
outline: 2px solid currentColor;
outline-offset: 3px;
}
:global(html.dark) .tag-link {
color: #aabbd8;
}
:global(html.dark) .tag-link:hover,
:global(html.dark) .tag-link:focus-visible {
color: #d9e5f6;
}
</style>

View file

@ -8,6 +8,7 @@ import "katex/dist/katex.min.css";
const lang = getLangFromUrl(Astro.url);
const htmlLang = lang === "zh" ? "zh-CN" : "en-US";
const isHomePage = new RegExp(`^/${lang}/?$`).test(Astro.url.pathname);
import SEO from "@/components/SEO.astro";
import FloatingActions from "@/components/FloatingActions.astro";
import Spinner from "@/components/Spinner.astro";
@ -44,6 +45,33 @@ const {
<FloatingActions />
<script is:inline>
function addCodeCopyButtons() {
const languageNames = {
js: "JavaScript",
javascript: "JavaScript",
ts: "TypeScript",
typescript: "TypeScript",
jsx: "JSX",
tsx: "TSX",
sh: "Shell",
bash: "Shell",
zsh: "Shell",
py: "Python",
python: "Python",
yml: "YAML",
yaml: "YAML",
json: "JSON",
astro: "Astro",
html: "HTML",
css: "CSS",
scss: "SCSS",
sql: "SQL",
rust: "Rust",
go: "Go",
java: "Java",
cpp: "C++",
csharp: "C#",
};
document.querySelectorAll("pre").forEach((pre) => {
const code = pre.querySelector("code");
if (!code) return;
@ -62,6 +90,29 @@ const {
pre.parentNode?.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const toolbar = document.createElement("div");
toolbar.className = "code-block-toolbar";
wrapper.insertBefore(toolbar, pre);
const languageClass = [...code.classList, ...pre.classList].find((name) =>
name.startsWith("language-"),
);
const language = (
pre.dataset.language ||
code.dataset.language ||
languageClass?.slice("language-".length) ||
""
).trim().toLowerCase();
if (language) {
const label = document.createElement("span");
label.className = "code-language-label";
label.textContent =
languageNames[language.toLowerCase()] ||
language.toUpperCase();
toolbar.appendChild(label);
}
/* 创建复制按钮 */
const button = document.createElement("button");
button.className = "code-copy-button";
@ -76,13 +127,15 @@ const {
icon.setAttribute("aria-hidden", "true");
button.appendChild(icon);
wrapper.appendChild(button);
toolbar.appendChild(button);
button.addEventListener("click", async () => {
const text = code.textContent ?? "";
let copied = false;
try {
await navigator.clipboard.writeText(text);
copied = true;
} catch {
const textarea = document.createElement("textarea");
textarea.value = text;
@ -90,16 +143,25 @@ const {
textarea.style.opacity = "0";
textarea.style.pointerEvents = "none";
try {
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
copied = document.execCommand("copy");
} catch {
copied = false;
} finally {
textarea.remove();
button.focus({ preventScroll: true });
}
}
icon.src = "/images/correct-signal-svgrepo-com.svg";
button.setAttribute("aria-label", "已复制");
button.title = "已复制";
button.classList.add("copied");
icon.src = copied
? "/images/correct-signal-svgrepo-com.svg"
: "/images/copy-svgrepo-com.svg";
const message = copied ? "已复制" : "复制失败,请手动选择代码复制";
button.setAttribute("aria-label", message);
button.title = message;
button.classList.toggle("copied", copied);
window.setTimeout(() => {
icon.src = "/images/copy-svgrepo-com.svg";
@ -115,7 +177,7 @@ const {
document.addEventListener("astro:page-load", addCodeCopyButtons);
</script>
<body>
<body class:list={{ "is-home": isHomePage }}>
<Header />
<Spinner />
<main
@ -129,10 +191,15 @@ const {
</html>
<style>
html,
html {
margin: 0;
width: 100%;
min-height: 100%;
}
body {
margin: 0;
width: 100%;
height: 100%;
min-height: 100%;
}
</style>

View file

@ -2,6 +2,7 @@
import BaseLayout from "./BaseLayout.astro";
import Remark42Embed from "@/components/Remark42Embed.astro";
import PostNav from "@/components/Posts/PostNav.astro";
import TagLink from "@/components/TagLink.astro";
import PostMenu from "@/components/Posts/PostMenu.astro";
import Dialog from "@/components/Posts/Dialog.astro";
import { fade } from "astro:transitions";
@ -11,6 +12,14 @@ import "@/styles/global.css";
const { post, frontmatter, lang, slug, postId, headings } = Astro.props;
const comments = lang === "zh" ? "评论区" : "comments";
const t = getTranslations(lang);
const publishedDate = new Intl.DateTimeFormat(
lang === "zh" ? "zh-CN" : "en-US",
{
year: "numeric",
month: "long",
day: "numeric",
},
).format(frontmatter.pubDate);
---
<BaseLayout
@ -18,6 +27,7 @@ const t = getTranslations(lang);
description={frontmatter.description}
image={frontmatter.image?.url}
>
<div class="post-layout page-shell">
<div id="reading-progress" aria-hidden="true"></div>
<a
href="/"
@ -50,19 +60,20 @@ const t = getTranslations(lang);
{frontmatter.title}
</h1>
<p class="description">
<em>{frontmatter.description}</em>
{frontmatter.description}
</p>
<p class="meta-line">
{t.post.publishedOn}: {frontmatter.pubDate.toLocaleDateString()}
<p class="post-byline">
<span>{t.post.writtenBy} {frontmatter.author}</span>
<span class="byline-separator" aria-hidden="true">·</span>
<time datetime={frontmatter.pubDate.toISOString()}>
{t.post.publishedOn} {publishedDate}
</time>
</p>
<p class="meta-line">{t.post.writtenBy}: {frontmatter.author}</p>
<div class="tags" transition:name={`post-tags-${postId}`}>
{
frontmatter.tags.map((tag: string) => (
<p class="tag">
<a href={`/${lang}/tags/${tag}`}>{tag}</a>
</p>
<TagLink tag={tag} lang={lang} />
))
}
</div>
@ -86,10 +97,17 @@ const t = getTranslations(lang);
<PostNav post={post} lang={lang} />
<h2>{comments}</h2>
<section
id="comments"
class="comments-section"
aria-labelledby="comments-heading"
>
<h2 id="comments-heading">{comments}</h2>
{t.post.commentsDesc.map((line: string) => <p set:html={line} />)}
<Remark42Embed slug={slug} />
</section>
</article>
</div>
</BaseLayout>
<script
@ -148,36 +166,68 @@ const t = getTranslations(lang);
backButton.dataset.bound = "true";
}
};
const updateProgress = () => {
const doc = document.documentElement;
const scrollTop = window.scrollY || doc.scrollTop;
const scrollHeight = doc.scrollHeight - window.innerHeight;
const progress = scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0;
const bar = document.getElementById("reading-progress");
if (bar) {
bar.style.width = `${Math.min(progress, 100)}%`;
}
const initReadingProgress = () => {
const readingWindow = window as typeof window & {
__postReadingProgressHandler?: () => void;
};
updateProgress();
if (readingWindow.__postReadingProgressHandler) {
window.removeEventListener(
"scroll",
readingWindow.__postReadingProgressHandler,
);
window.removeEventListener(
"resize",
readingWindow.__postReadingProgressHandler,
);
}
const bar = document.getElementById("reading-progress");
const content = document.querySelector<HTMLElement>(".post-content");
if (!bar || !content) return;
const updateProgress = () => {
const contentTop = content.getBoundingClientRect().top + window.scrollY;
const readableDistance = Math.max(
content.scrollHeight - window.innerHeight,
1,
);
const progress = Math.min(
Math.max((window.scrollY - contentTop) / readableDistance, 0),
1,
);
bar.style.transform = `scaleX(${progress})`;
};
readingWindow.__postReadingProgressHandler = updateProgress;
window.addEventListener("scroll", updateProgress, { passive: true });
window.addEventListener("resize", updateProgress);
updateProgress();
};
initReadingProgress();
initFloatingActions();
document.addEventListener("astro:page-load", initReadingProgress);
document.addEventListener("astro:page-load", initFloatingActions);
</script>
<style>
.post-layout {
min-width: 0;
}
#reading-progress {
position: fixed;
top: 0;
left: 0;
z-index: 9999;
width: 0;
width: 100%;
height: 3px;
background: #5a89ff;
transition: width 0.08s linear;
transform: scaleX(0);
transform-origin: left center;
transition: transform 0.08s linear;
pointer-events: none;
}
@ -192,15 +242,12 @@ const t = getTranslations(lang);
align-items: center;
justify-content: center;
text-decoration: none;
border: #536493 1.5px solid;
border-top: #536493 2.5px dotted;
border-left: #536493 2.5px dotted;
border-top: #536493 1.5px dotted;
border-left: #536493 1.5px dotted;
border-right: #536493 2.5px solid;
border-bottom: #536493 2.5px solid;
background: rgba(255, 255, 255, 0.92);
background: color-mix(in srgb, var(--background-color) 94%, transparent);
color: #222;
/* box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); */
backdrop-filter: blur(6px);
z-index: 1000;
visibility: hidden;
transform: translateY(-80px);
@ -230,10 +277,16 @@ const t = getTranslations(lang);
}
:global(.dark) .back-button {
background: rgba(34, 34, 34, 0.92);
background: color-mix(
in srgb,
var(--background-color-dark) 94%,
transparent
);
color: #f5f5f5;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32);
border: #2f406d 2px solid;
border-top-color: #515151;
border-left-color: #515151;
border-right-color: #515151;
border-bottom-color: #515151;
}
@media (max-width: 640px) {
@ -245,6 +298,9 @@ const t = getTranslations(lang);
}
}
.post-content {
--article-muted: #596672;
--article-rule: rgba(83, 100, 147, 0.25);
--article-tint: rgba(83, 100, 147, 0.045);
font-family:
system-ui,
-apple-system,
@ -257,6 +313,130 @@ const t = getTranslations(lang);
"Source Han Sans SC",
sans-serif;
font-weight: 400;
line-height: 1.85;
overflow-wrap: break-word;
}
.post-content :global(> :first-child) {
margin-top: 0;
}
.post-content :global(p) {
margin-block: 1.05em;
}
.post-content :global(h2) {
margin: 3.2rem 0 1rem;
font-family: "Noto Sans SC", system-ui, sans-serif;
font-size: clamp(1.35rem, 2vw, 1.55rem);
line-height: 1.4;
text-wrap: balance;
}
.post-content :global(h3) {
margin: 2.25rem 0 0.8rem;
font-family: "Noto Sans SC", system-ui, sans-serif;
font-size: clamp(1.12rem, 1.6vw, 1.28rem);
line-height: 1.45;
text-wrap: balance;
}
.post-content :global(h4) {
margin: 1.8rem 0 0.65rem;
font-family: "Noto Sans SC", system-ui, sans-serif;
line-height: 1.5;
font-size: 1.05em;
}
.post-content :global(:is(h2, h3, h4) + :is(h3, h4, p, ul, ol)) {
margin-top: 0.7rem;
}
.post-content :global(ul),
.post-content :global(ol) {
margin-block: 1.1em;
padding-inline-start: 1.6em;
}
.post-content :global(li::marker) {
color: var(--article-muted);
font-variant-numeric: tabular-nums;
}
.post-content :global(li > p) {
margin-block: 0.45em;
}
.post-content :global(li > :is(ul, ol)) {
margin-block: 0.5em;
}
.post-content :global(li + li) {
margin-top: 0.35rem;
}
.post-content :global(img:not(.code-copy-icon)) {
display: block;
max-width: 100%;
height: auto;
margin-inline: auto;
}
.post-content :global(figure) {
margin: 2rem 0;
}
.post-content :global(figcaption) {
margin-top: 0.55rem;
color: var(--article-muted);
font-family: "Noto Sans SC", system-ui, sans-serif;
font-size: 0.78rem;
line-height: 1.5;
text-align: center;
}
.post-content :global(table) {
display: block;
max-width: 100%;
overflow-x: auto;
margin-block: 1.75rem;
border-collapse: collapse;
font-size: 0.9em;
line-height: 1.65;
}
.post-content :global(th),
.post-content :global(td) {
padding: 0.65rem 0.85rem;
border-bottom: 1px solid var(--article-rule);
vertical-align: top;
}
.post-content :global(th) {
background: var(--article-tint);
font-weight: 600;
}
.post-content :global(a) {
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 0.2em;
text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
}
.post-content :global(a:hover) {
text-decoration-color: currentColor;
}
.post-content :global(:is(a, summary):focus-visible) {
outline: 2px solid var(--deep-blue);
outline-offset: 3px;
}
.post-content :global(hr) {
margin-block: 2.5rem;
border: 0;
border-top: 1px dashed var(--article-rule);
}
.post-content :global(code),
@ -267,7 +447,7 @@ const t = getTranslations(lang);
}
a {
color: var(--post-link-color);
color: var(--url-color);
}
.post-header {
@ -287,31 +467,64 @@ const t = getTranslations(lang);
line-height: 1.25;
overflow-wrap: anywhere;
word-break: break-word;
text-wrap: balance;
}
.description {
margin: 0 0 0.7rem 0;
opacity: 0.9;
color: #596672;
font-weight: 400;
line-height: 1.6;
overflow-wrap: anywhere;
word-break: break-word;
}
.meta-line {
.post-byline {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.3rem 0.55rem;
margin: 0.3rem 0;
line-height: 1.6;
color: #66717c;
font-family: "Noto Sans SC", system-ui, sans-serif;
font-size: 0.8rem;
font-weight: 400;
line-height: 1.5;
overflow-wrap: anywhere;
word-break: break-word;
}
.byline-separator {
opacity: 0.65;
}
:global(html.dark) .post-byline {
color: #aab7c4;
}
:global(html.dark) .description {
color: #b5bec7;
}
@media (min-width: 701px) {
.post-title {
margin-bottom: 0.85rem;
font-size: clamp(1.7rem, 2.2vw, 2rem);
line-height: 1.4;
}
.description {
margin-bottom: 1rem;
font-size: 1rem;
}
}
.post-cover {
width: 300px;
height: auto;
aspect-ratio: 16 / 10;
object-fit: cover;
object-position: center;
/* border-radius: 0.4rem; */
border: 1.5px #94a0ab dashed;
display: block;
}
@ -327,39 +540,10 @@ const t = getTranslations(lang);
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
gap: 0.25rem 0.9rem;
margin-top: 0.8rem;
min-width: 0;
}
.tag {
margin: 0;
padding: 0.22em 0.65em;
font-size: 0.88rem;
line-height: 1.2;
border: 1px dashed #8b6b4a;
border-radius: 0;
background-color: transparent;
max-width: 100%;
}
.tag a {
color: #6f4e37;
text-decoration: none;
overflow-wrap: anywhere;
word-break: break-word;
}
.tag a:hover {
text-decoration: underline;
}
:global(.dark) .tag {
border-color: #d8c7a1;
}
:global(.dark) .tag a {
color: #e6d8b8;
}
@media (max-width: 560px) {
@ -374,15 +558,16 @@ const t = getTranslations(lang);
}
}
details {
.post-content :global(details) {
margin: 1.5rem 0;
overflow: hidden;
border-top: 2px solid #1e3a5f;
border-radius: 0 0 0.5rem 0.5rem;
background: rgba(128, 128, 128, 0.14);
border-top: 1px solid var(--article-rule);
border-bottom: 1px solid var(--article-rule);
border-radius: 0;
background: var(--article-tint);
}
details summary {
.post-content :global(details summary) {
display: flex;
align-items: center;
gap: 0.5rem;
@ -393,11 +578,11 @@ const t = getTranslations(lang);
list-style: none;
}
details summary::-webkit-details-marker {
.post-content :global(details summary::-webkit-details-marker) {
display: none;
}
details summary::before {
.post-content :global(details summary::before) {
content: "›";
display: inline-block;
font-size: 1.25rem;
@ -405,46 +590,46 @@ const t = getTranslations(lang);
transition: transform 0.2s ease;
}
details[open] summary::before {
.post-content :global(details[open] summary::before) {
transform: rotate(90deg);
}
/* 展开内容 */
details > :not(summary) {
.post-content :global(details > :not(summary)) {
box-sizing: border-box;
margin-top: 0;
margin-bottom: 0;
padding-right: 1rem;
margin: 0.8rem 1rem;
padding: 0;
}
/* 为列表序号预留空间 */
details > ol,
details > ul {
padding: 0.6rem 1rem 1rem 3rem;
.post-content :global(details > ol),
.post-content :global(details > ul) {
padding-inline-start: 1.6em;
}
details li {
.post-content :global(details li) {
margin: 0.8rem 0;
}
blockquote {
margin: 1.25rem 0;
padding: 0.75rem 1rem;
border-left: 4px solid var(--deep-red);
background: rgba(128, 128, 128, 0.1);
border-radius: 0 0.4rem 0.4rem 0;
.post-content :global(blockquote) {
margin: 1.5rem 0;
padding: 0.15rem 0 0.15rem 1rem;
border-left: 3px solid var(--deep-blue);
color: var(--article-muted);
background: transparent;
border-radius: 0;
}
blockquote p {
.post-content :global(blockquote p) {
margin: 0;
}
blockquote p + p {
.post-content :global(blockquote p + p) {
margin-top: 0.75rem;
}
/* 块级公式:超出屏幕时只滚动公式,不撑宽整篇文章 */
.katex-display {
.post-content :global(.katex-display) {
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
@ -453,7 +638,27 @@ const t = getTranslations(lang);
}
/* 避免 KaTeX 内部元素强行把父容器撑宽 */
.katex-display > .katex {
.post-content :global(.katex-display > .katex) {
min-width: max-content;
}
.comments-section {
scroll-margin-top: 5rem;
}
:global(html.dark) .post-content {
--article-muted: #aab7c4;
--article-rule: rgba(170, 183, 196, 0.28);
--article-tint: rgba(170, 183, 196, 0.055);
}
:global(html.dark) .post-content :global(blockquote) {
border-left-color: #9ecbff;
}
@media (prefers-reduced-motion: reduce) {
#reading-progress {
transition: none;
}
}
</style>

View file

@ -7,7 +7,7 @@ import "@/styles/global.css";
pageTitle="404 Not Found"
description="Sorry, the page doesn't exists"
>
<div class="not-found">
<div class="not-found page-shell">
<img src="/images/marisa.png" alt="Marisa" class="marisa-404" />
<div class="hint">
<h3>对不起,您请求的页面不存在哦。</h3>

View file

@ -16,7 +16,7 @@ const pageTitle = t.about.title;
pageTitle=`${pageTitle} - ${t.banner.title}`
image={t.about.profilePicture}
>
<main class="about">
<div class="about page-shell">
<h1>{t.about.title}</h1>
<div class="intro">
@ -36,14 +36,12 @@ const pageTitle = t.about.title;
<div class="content">
{t.about.content.map((line: string) => <p set:html={line} />)}
</div>
</main>
</div>
</BaseLayout>
<style>
.about {
max-width: 800px;
margin: 0 auto;
padding: 2rem 1rem;
padding-block: 2rem;
}
.about h1 {

View file

@ -15,9 +15,11 @@ const headerTitle = lang === "zh" ? "友情链接" : "Friends";
---
<BaseLayout pageTitle=`${headerTitle} - ${t.banner.title}`>
<div class="page-shell">
<FriendsDescription />
<div class="section-divider"></div>
<FriendlyLinkList />
</div>
</BaseLayout>
<style>

View file

@ -14,10 +14,11 @@ const lang = getLangFromUrl(Astro.url);
const t = getTranslations(lang);
const headerTitle = lang === "zh" ? "Cloverta的博客" : "Cloverta's blog";
const pageTitle = t.home.title;
const rssLink = lang === "zh" ? "/rss.xml" : "/en/rss.xml"
const rssLink = lang === "zh" ? "/rss.xml" : "/en/rss.xml";
---
<BaseLayout pageTitle=`${headerTitle} - ${t.banner.subtitle}`>
<div class="index page-shell">
<h1 class="page-title">
<span>{pageTitle}</span>
<a
@ -48,17 +49,22 @@ const rssLink = lang === "zh" ? "/rss.xml" : "/en/rss.xml"
<div class="section-divider"></div>
<h2>{t.home.latestTitle}</h2>
<h2 class="section-title">{t.home.latestTitle}</h2>
<PostList />
<div class="section-divider"></div>
<!-- <Remark42LatestComments lang={lang} client:load /> -->
<LatestComments />
<LatestComments lang={lang} />
</div>
</BaseLayout>
<style>
.index {
min-width: 0;
}
.section-divider {
height: 1px;
background: #cdd2d8;
@ -73,4 +79,19 @@ const rssLink = lang === "zh" ? "/rss.xml" : "/en/rss.xml"
.page-title {
font-weight: 700;
}
.section-title,
.index :global(.latest-comments > h2) {
margin: 0 0 1.25rem;
font-family: "Noto Sans SC", system-ui, sans-serif;
font-size: 0.95rem;
font-weight: 500;
line-height: 1.5;
color: #66717c;
}
:global(html.dark) .section-title,
:global(html.dark) .index :global(.latest-comments > h2) {
color: #aab7c4;
}
</style>

View file

@ -32,6 +32,7 @@ const t = getTranslations(lang);
---
<BaseLayout pageTitle={String(tag)}>
<div class="page-shell">
<p class="tag-heading">
{lang === "zh" ? "带有标签" : "Posts tagged with"}{" "}
<span class="tag-chip" transition:name={`post-tags-${tag}`}>{tag}</span>
@ -60,6 +61,7 @@ const t = getTranslations(lang);
})
}
</ul>
</div>
</BaseLayout>
<style>

View file

@ -1,5 +1,6 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import TagLink from "@/components/TagLink.astro";
import { getCollection } from "astro:content";
import { getTranslations, type Lang } from "@/i18n";
import "@/styles/global.css";
@ -16,6 +17,7 @@ const pageTitle = lang === "zh" ? "标签索引" : "Tag Index";
---
<BaseLayout pageTitle=`${pageTitle} - ${t.banner.title}`>
<div class="tags-layout page-shell">
<h1>{t.tags.title}</h1>
<p>{t.tags.description}</p>
@ -23,60 +25,25 @@ const pageTitle = lang === "zh" ? "标签索引" : "Tag Index";
{
tags.map((tag) => (
<p class="tag" transition:name={`post-tags-${tag}`}>
<a href={`/${lang}/tags/${tag}`}>{tag}</a>
<TagLink tag={String(tag)} lang={lang} />
</p>
))
}
</div>
</div>
</BaseLayout>
<style>
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
gap: 0.4rem 1.25rem;
margin-top: 0.8rem;
}
.tag {
font-family:
"Noto Sans SC",
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Open Sans",
"Helvetica Neue",
sans-serif;
margin: 0;
padding: 0.22em 0.65em;
font-size: 1rem;
line-height: 1.2;
border: 1px dashed #8b6b4a;
border-radius: 0;
background-color: transparent;
}
.tag a {
color: #6f4e37;
text-decoration: none;
font-weight: 500;
}
.tag a:hover {
text-decoration: underline;
}
:global(.dark) .tag {
border-color: #d8c7a1;
}
:global(.dark) .tag a {
color: #e6d8b8;
max-width: 100%;
}
</style>

View file

@ -14,6 +14,8 @@ const headerTitle = lang === "zh" ? "时间轴" : "Timeline";
---
<BaseLayout pageTitle=`${headerTitle} - ${t.banner.title}` `>
<div class="page-shell">
<h1>{t.nav.timeline}</h1>
<PostTimeline />
</div>
</BaseLayout>

View file

@ -0,0 +1,48 @@
// Standalone Markdown images may use their optional title as a visible caption.
// Keep alt text unchanged: alternative text and captions serve different purposes.
export default function rehypeImageCaptions() {
return (tree) => {
function visit(parent) {
if (!Array.isArray(parent.children)) return;
parent.children = parent.children.map((node) => {
if (node.type === "element" && node.tagName === "p") {
const content = node.children.filter(
(child) => child.type !== "text" || child.value.trim() !== "",
);
const onlyChild = content.length === 1 ? content[0] : null;
const image = onlyChild?.tagName === "img"
? onlyChild
: onlyChild?.tagName === "a" && onlyChild.children?.length === 1
&& onlyChild.children[0].tagName === "img"
? onlyChild.children[0]
: null;
const caption = image?.properties?.title;
if (typeof caption === "string" && caption.trim()) {
return {
type: "element",
tagName: "figure",
properties: {},
children: [
onlyChild,
{
type: "element",
tagName: "figcaption",
properties: {},
children: [{ type: "text", value: caption.trim() }],
},
],
};
}
}
// Respect captions and markup authored explicitly in HTML.
if (node.tagName !== "figure") visit(node);
return node;
});
}
visit(tree);
};
}

View file

@ -1,21 +1,31 @@
.code-block-wrapper {
position: relative;
min-width: 0;
max-width: 100%;
margin: 1.5rem 0;
}
.code-block-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.4rem 0.5rem 0.4rem 1rem;
border-bottom: 1px solid rgba(127, 127, 127, 0.2);
background: rgba(127, 127, 127, 0.08);
border-radius: 3px 3px 0 0;
}
.code-block-wrapper pre {
margin: 0;
padding: 1rem;
border-radius: 3px;
max-width: 100%;
border-radius: 0 0 3px 3px;
overflow-x: auto;
line-height: 1.6;
margin: 1rem 0;
position: relative;
}
/* pre:has(> .code-copy-button) {
padding-top: 2.75rem;
} */
code {
font-family: "Maple Mono", monospace;
font-size: 0.96rem;
@ -31,12 +41,15 @@ td code {
background: rgba(127, 127, 127, 0.12);
}
.code-copy-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
z-index: 2;
.code-block-wrapper pre > code {
padding: 0;
border-radius: 0;
background: transparent;
}
.code-copy-button {
flex: 0 0 auto;
margin-left: auto;
display: grid;
place-items: center;
width: 2rem;
@ -58,6 +71,18 @@ td code {
box-shadow 0.15s ease;
}
.code-language-label {
min-width: 0;
overflow-wrap: anywhere;
color: #687583;
font-family: "Maple Mono", monospace;
font-size: 0.72rem;
font-weight: 700;
line-height: 1.5;
letter-spacing: 0.04em;
pointer-events: none;
}
.code-copy-icon {
display: block;
width: 100%;
@ -81,6 +106,11 @@ td code {
background: #dcebd5;
}
html.dark .code-language-label,
.dark .code-language-label {
color: #aeb9c4;
}
/* .dark .code-copy-button {
border-color: #9ecbff;
}

View file

@ -45,23 +45,48 @@ body {
font-size: 1.15rem;
}
.page-content,
.page-shell {
width: 100%;
min-width: 0;
}
.page-shell {
max-width: var(--page-content-width);
margin-inline: auto;
}
body::after {
content: "";
position: fixed;
right: 0;
bottom: 0;
width: 400px;
/* 按需调整大小 */
height: 400px;
/* 按需调整大小 */
width: clamp(300px, 31vw, 400px);
height: clamp(300px, 31vw, 400px);
background-image: url("https://files.seeusercontent.com/2026/04/16/1sXb/touhou___kirisame_marisa__2__by_.webp");
background-repeat: no-repeat;
background-position: right bottom;
background-size: contain;
opacity: 0.35;
opacity: 0;
-webkit-mask-image: radial-gradient(
ellipse at right bottom,
#000 0%,
#000 38%,
transparent 78%
);
mask-image: radial-gradient(
ellipse at right bottom,
#000 0%,
#000 38%,
transparent 78%
);
pointer-events: none;
z-index: -1;
transition: opacity 0.25s ease;
}
body.is-home::after {
opacity: 0.16;
}
html.dark body::after {
@ -69,7 +94,7 @@ html.dark body::after {
}
@media (max-width: 900px) {
body::after {
body.is-home::after {
opacity: 0;
}
}
@ -98,7 +123,7 @@ h1 {
a {
color: var(--url-color);
font-weight: 700;
font-weight: inherit;
text-decoration: none;
}
@ -119,7 +144,6 @@ html.dark {
.dark .nav-links a {
color: #9ecbff;
font-weight: 700;
}
.dark .nav-links a:hover,
@ -129,7 +153,6 @@ html.dark {
.dark a {
color: #9ecbff;
font-weight: 700;
}
.dark a:hover,

View file

@ -7,14 +7,15 @@
}
.loading-card {
height: 92px;
margin: 0 0 14px 0;
padding: 0.9rem 1rem;
border: 1px dashed #aeb8c2;
background: linear-gradient(90deg,
rgba(160, 175, 190, 0.06) 25%,
rgba(160, 175, 190, 0.16) 50%,
rgba(160, 175, 190, 0.06) 75%);
height: 82px;
padding: 1rem 0;
border-bottom: 1px solid rgba(110, 125, 140, 0.28);
background: linear-gradient(
90deg,
transparent 15%,
rgba(160, 175, 190, 0.13) 50%,
transparent 85%
);
background-size: 200% 100%;
animation: shimmer 1.2s infinite linear;
}
@ -34,65 +35,58 @@
}
.comment-card {
display: block;
margin: 0 0 14px 0;
padding: 0.6rem 1rem;
border: 2px dashed #aeb8c2;
background-color: var(--comment-background);
padding: 1rem 0;
border-bottom: 1px solid rgba(110, 125, 140, 0.28);
}
.comment-card:first-child {
padding-top: 0.35rem;
}
.comment-card:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: 0;
}
.comment-card-body {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
grid-template-areas:
"info title"
"text title";
gap: 0.2rem 1rem;
align-items: start;
min-width: 0;
}
.comment-card-info {
grid-area: info;
display: flex;
align-items: center;
gap: 0.7rem;
gap: 0.65rem;
min-width: 0;
}
.comment-avatar {
width: 35px;
height: 35px;
flex-shrink: 0;
width: 32px;
height: 32px;
flex: 0 0 32px;
}
.comment-avatar-img,
.comment-avatar-fallback {
width: 35px;
height: 35px;
width: 32px;
height: 32px;
display: block;
border-radius: 50%;
object-fit: cover;
}
.comment-avatar-img {
border: 1px dashed #aeb8c2;
background: #f3f5f7;
background: rgba(160, 175, 190, 0.1);
}
.comment-avatar-fallback {
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #aeb8c2;
background: rgba(160, 175, 190, 0.12);
background: rgba(160, 175, 190, 0.14);
color: #5e6b77;
font-weight: 700;
font-size: 1rem;
font-family: system-ui, sans-serif;
font-size: 0.85rem;
font-weight: 600;
user-select: none;
}
@ -105,90 +99,92 @@
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.6rem;
gap: 0.25rem 0.55rem;
min-width: 0;
line-height: 1.3;
font-style: italic;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"PingFang SC",
"Hiragino Sans GB",
"Microsoft YaHei",
"Noto Sans CJK SC",
"Source Han Sans SC",
sans-serif;
font-family: system-ui, sans-serif;
line-height: 1.35;
}
.comment-author {
.comment-author,
.comment-reply-to {
color: #66717c;
font-size: 0.82rem;
font-weight: 500;
overflow-wrap: anywhere;
}
.comment-reply-sep,
.comment-time {
color: #7a858f;
font-size: 0.72rem;
font-weight: 400;
font-size: 0.8rem;
color: gray;
}
.comment-card-text {
display: -webkit-box;
margin: 0.6rem 0 0.5rem;
max-height: 4.65em;
overflow: hidden;
color: #3f4a54;
font-family: system-ui, sans-serif;
font-size: 0.93rem;
font-weight: 400;
line-height: 1.55;
overflow-wrap: anywhere;
word-break: break-word;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.comment-time {
color: #7a7a7a;
font-size: 0.7rem;
.comment-source {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.55rem 1rem;
min-width: 0;
color: #89939c;
font-family: system-ui, sans-serif;
font-size: 0.75rem;
line-height: 1.45;
}
.comment-card-title {
grid-area: title;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
text-align: right;
.comment-source-context {
display: flex;
align-items: baseline;
gap: 0.4rem;
min-width: 0;
}
.comment-title-link {
min-width: 0;
overflow: hidden;
color: #6f8090;
font-size: 0.82rem;
font-weight: 400;
text-decoration: none;
font-size: 1.1rem;
font-weight: 700;
line-height: 1.45;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
text-overflow: ellipsis;
white-space: nowrap;
}
.comment-title-link:hover {
.comment-title-link:hover,
.comment-title-link:focus-visible {
text-decoration: underline;
}
.comment-card-text {
grid-area: text;
margin: 0;
color: #555;
line-height: 1.5;
font-size: 0.93rem;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
white-space: normal;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"PingFang SC",
"Hiragino Sans GB",
"Microsoft YaHei",
"Noto Sans CJK SC",
"Source Han Sans SC",
sans-serif;
.comment-discussion-link {
display: none;
flex: 0 0 auto;
color: var(--url-color);
font-size: 0.76rem;
font-weight: 500;
text-decoration-line: underline;
text-decoration-style: dashed;
text-underline-offset: 0.22em;
white-space: nowrap;
}
.comment-card-text :global(p),
.comment-card-text p {
margin: 0;
}
.comment-card-text :global(img),
.comment-card-text img {
max-width: 100%;
height: auto;
.comment-card.is-truncated .comment-discussion-link {
display: inline;
}
.latest-comments-empty {
@ -196,68 +192,67 @@
font-style: italic;
}
.comment-reply-sep {
font-size: 0.8rem;
opacity: 0.7;
html.dark .comment-card,
.dark .comment-card,
html.dark .loading-card,
.dark .loading-card {
border-color: rgba(170, 183, 196, 0.25);
}
.latest-comments .comment-reply-to {
font-weight: 400;
font-size: 0.8rem;
color: gray;
opacity: 0.9;
overflow-wrap: anywhere;
word-break: break-word;
}
html.dark .latest-comments .comment-card,
.dark .latest-comments .comment-card {
border-color: #7f8c97;
background-color: #252525;
}
html.dark .comment-title-link {
color: #c8d2dc;
}
html.dark .comment-card-text {
html.dark .comment-card-text,
.dark .comment-card-text {
color: #d3d7db;
}
html.dark .comment-time {
color: #a8b0b7;
html.dark .comment-author,
.dark .comment-author,
html.dark .comment-reply-to,
.dark .comment-reply-to {
color: #b9c3cc;
}
html.dark .latest-comments .comment-avatar-fallback,
.dark .latest-comments .comment-avatar-fallback {
border-color: #7f8c97;
background-color: #3a444d;
color: #dbe3ea;
html.dark .comment-time,
.dark .comment-time,
html.dark .comment-reply-sep,
.dark .comment-reply-sep,
html.dark .comment-source,
.dark .comment-source {
color: #929da7;
}
:global(.dark) .comment-avatar-fallback {
background: rgba(180, 190, 200, 0.12);
html.dark .comment-title-link,
.dark .comment-title-link {
color: #aabbd0;
}
html.dark .comment-discussion-link,
.dark .comment-discussion-link {
color: #9ecbff;
}
html.dark .comment-avatar-fallback,
.dark .comment-avatar-fallback {
background: rgba(180, 190, 200, 0.14);
color: #dbe3ea;
}
@media (max-width: 640px) {
.comment-card-body {
grid-template-columns: 1fr;
grid-template-areas:
"title"
"info"
"text";
.comment-card {
padding-block: 0.85rem;
}
.comment-card-title {
text-align: left;
margin-bottom: 0.2rem;
.comment-card-text {
margin-top: 0.5rem;
}
.comment-title-link {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.comment-source {
align-items: flex-start;
flex-wrap: wrap;
}
}
@media (prefers-reduced-motion: reduce) {
.loading-card {
animation: none;
}
}

View file

@ -1,5 +1,6 @@
/* 默认配色 */
:root {
--page-content-width: 850px;
--deep-blue: #536493;
--deep-red: #ef5a6f;
--background-color: #f9f2ed;