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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,9 @@ const lang = getLangFromUrl(Astro.url);
const t = getTranslations(lang); const t = getTranslations(lang);
--- ---
<header class="site-header"> <div class="header-slot">
<header class="site-header">
<div class="header-content">
<div class="theme-icon-wrap"> <div class="theme-icon-wrap">
<ThemeIcon /> <ThemeIcon />
</div> </div>
@ -26,23 +28,35 @@ const t = getTranslations(lang);
</div> </div>
<Navigation /> <Navigation />
</nav> </nav>
</div>
<div class="divider"> <div class="divider">
<Divider /> <Divider />
</div> </div>
</header> </header>
</div>
<style> <style>
.site-header {
position: relative;
z-index: 30;
padding-top: 0.5rem;
}
.header-content {
position: relative;
}
.banner-link { .banner-link {
display: inline-block; display: block;
} }
.banner-logo { .banner-logo {
display: block; display: block;
height: 55px; /* 按你的 banner 比例改 */ height: 55px;
background-color: #1f2328; background-color: #1f2328;
-webkit-mask: url("/images/banner-black.svg") no-repeat left / contain; -webkit-mask: url("/images/banner-black.svg") no-repeat left / contain;
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) { :global(html.dark .banner-logo) {
@ -55,11 +69,6 @@ const t = getTranslations(lang);
display: block; display: block;
} }
.site-header {
position: relative;
padding-top: 0.5rem;
}
.theme-icon-wrap { .theme-icon-wrap {
position: absolute; position: absolute;
top: 0; top: 0;
@ -71,17 +80,14 @@ const t = getTranslations(lang);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
max-height: 12rem;
} }
.header-nav .banner { .header-nav .banner {
margin: 0; margin: 0;
font-style: italic; font-style: italic;
line-height: 1.1; line-height: 1.1;
/* 给右上角 ThemeIcon 留空间 */
max-width: calc(100% - 7rem); max-width: calc(100% - 7rem);
/* 桌面正常,手机自动缩小 */
font-size: clamp(1.6rem, 6vw, 2.5rem); font-size: clamp(1.6rem, 6vw, 2.5rem);
} }
@ -90,8 +96,106 @@ const t = getTranslations(lang);
margin-bottom: 2rem; 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 { .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; padding-top: 0.25rem;
} }
@ -101,3 +205,268 @@ const t = getTranslations(lang);
} }
} }
</style> </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 { lang = "zh" } = Astro.props;
const host = import.meta.env.PUBLIC_REMARK42_HOST; const host = import.meta.env.PUBLIC_REMARK42_HOST;
const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID; const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
const max = 10; const max = 5;
const heading = lang === "zh" ? "最新评论" : "Latest comments"; 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) { function getAvatar(item) {
return ( return (
item?.user?.picture || item?.user?.photo || item?.user?.avatar || "" 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"); 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) { function normalizePath(url) {
if (!url) return ""; if (!url) return "";
@ -207,10 +230,14 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
item?.title || item?.title ||
(lang === "zh" ? "未命名文章" : "Untitled post"), (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 preview = getCommentPreview(item?.text, lang);
const html = item?.text || "";
const time = formatTime(item?.time, lang); const time = formatTime(item?.time, lang);
const replyTo = escapeHtml(item?.replyToName || ""); const replyTo = escapeHtml(item?.replyToName || "");
@ -227,7 +254,7 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
return ` return `
<article class="comment-card"> <article class="comment-card">
<div class="comment-card-body"> <div class="comment-card-body">
<div class="comment-card-info"> <header class="comment-card-info">
<div class="comment-avatar"> <div class="comment-avatar">
${avatarHtml} ${avatarHtml}
</div> </div>
@ -238,19 +265,37 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
${time ? `<span class="comment-time">${escapeHtml(time)}</span>` : ""} ${time ? `<span class="comment-time">${escapeHtml(time)}</span>` : ""}
</div> </div>
</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> <a class="comment-title-link" href="${href}">${title}</a>
</div> </div>
<a class="comment-discussion-link" href="${discussionHref}">${
<div class="comment-card-text">${html}</div> lang === "zh" ? "查看完整讨论" : "View discussion"
}</a>
</footer>
</div> </div>
</article> </article>
`; `;
}) })
.join(""); .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"; loading.style.display = "none";
} }
@ -259,7 +304,7 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
const host = section.dataset.host; const host = section.dataset.host;
const siteId = section.dataset.siteId; const siteId = section.dataset.siteId;
const max = section.dataset.max || "10"; const max = section.dataset.max || "5";
const lang = section.dataset.lang || "zh"; const lang = section.dataset.lang || "zh";
const list = section.querySelector(".latest-comments-list"); const list = section.querySelector(".latest-comments-list");

View file

@ -3,25 +3,48 @@ import { getLangFromUrl, getTranslations } from "@/i18n";
const lang = getLangFromUrl(Astro.url); const lang = getLangFromUrl(Astro.url);
const t = getTranslations(lang); 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"> <nav class="site-nav" aria-label="Site navigation">
<div class="site-nav-desktop"> <div class="site-nav-desktop">
<a href={`/${lang}/`}>{t.nav.home}</a> {
<a href={`/${lang}/about/`}>{t.nav.about}</a> items.map(({ href, label, exact }) => (
<a href={`/${lang}/tags/`}>{t.nav.tags}</a> <a
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a> href={href}
<a href={`/${lang}/friends/`}>{t.nav.friends}</a> aria-current={isCurrent(href, exact) ? "page" : undefined}
>
{label}
</a>
))
}
</div> </div>
<details class="site-nav-mobile"> <details class="site-nav-mobile">
<summary>☰ {lang === "zh" ? "导航栏" : "Menu"}</summary> <summary>☰ {lang === "zh" ? "导航栏" : "Menu"}</summary>
<div class="dropdown-menu"> <div class="dropdown-menu">
<a href={`/${lang}/`}>{t.nav.home}</a> {
<a href={`/${lang}/about/`}>{t.nav.about}</a> items.map(({ href, label, exact }) => (
<a href={`/${lang}/tags/`}>{t.nav.tags}</a> <a
<a href={`/${lang}/timeline/`}>{t.nav.timeline}</a> href={href}
<a href={`/${lang}/friends/`}>{t.nav.friends}</a> aria-current={isCurrent(href, exact) ? "page" : undefined}
>
{label}
</a>
))
}
</div> </div>
</details> </details>
</nav> </nav>
@ -54,22 +77,45 @@ const t = getTranslations(lang);
} }
.site-nav a { .site-nav a {
color: black; position: relative;
text-decoration: underline; color: var(--text-color);
font-weight: 700; text-decoration: none;
font-size: 1.2rem; font-weight: 500;
font-size: 1.05rem;
line-height: 1.4;
} }
.site-nav a:hover { .site-nav-desktop a::after {
text-decoration: underline; 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 { :global(.dark) .site-nav a {
color: white; color: var(--text-color-dark);
} }
:global(.dark) .site-nav a:hover { :global(.dark) .site-nav a:hover {
color: #ddd; color: #fff;
} }
.site-nav-mobile { .site-nav-mobile {
@ -81,10 +127,10 @@ const t = getTranslations(lang);
list-style: none; list-style: none;
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
font-weight: 700; font-weight: 500;
font-size: 1.1rem; font-size: 1rem;
color: black; color: var(--text-color);
text-decoration: underline; text-decoration: none;
user-select: none; user-select: none;
} }
@ -121,6 +167,11 @@ const t = getTranslations(lang);
text-decoration: underline; text-decoration: underline;
} }
.dropdown-menu a[aria-current="page"] {
color: var(--deep-red);
font-weight: 600;
}
:global(.dark) .dropdown-menu { :global(.dark) .dropdown-menu {
background: #1f1b18; background: #1f1b18;
border-color: #c8a98a; border-color: #c8a98a;

View file

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

View file

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

View file

@ -83,6 +83,9 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
<style> <style>
.post-menu { .post-menu {
box-sizing: border-box; box-sizing: border-box;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
} }
.post-menu-title { .post-menu-title {
@ -125,6 +128,7 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
.post-menu-item { .post-menu-item {
position: relative; position: relative;
min-width: 0;
margin: 0.42rem 0; margin: 0.42rem 0;
line-height: 1.45; line-height: 1.45;
} }
@ -136,27 +140,41 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
} }
.post-menu a { .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; color: inherit;
text-decoration: none; text-decoration: none;
transition:
border-color 0.16s ease,
color 0.16s ease;
} }
.post-menu a:hover { .post-menu a:hover {
text-decoration: underline; 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 { .post-menu-mobile-toggle {
display: none; display: none;
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
.post-menu { .post-menu {
--content-width: 90ch; --content-width: var(--page-content-width);
--menu-width: clamp( --menu-width: clamp(
140px, 140px,
calc((100vw - 1200px) * 0.42 + 140px), calc((100vw - 1200px) * 0.42 + 140px),
260px 260px
); );
--menu-gap: 0rem; --menu-gap: clamp(1.5rem, 2vw, 2rem);
position: fixed; position: fixed;
top: 8.5rem; top: 8.5rem;
@ -168,7 +186,9 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
); );
max-height: calc(100vh - 10rem); max-height: calc(100vh - 10rem);
overflow: auto; overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
padding: 0.2rem 0.2rem 0.2rem 0; padding: 0.2rem 0.2rem 0.2rem 0;
opacity: 0.9; opacity: 0.9;
} }
@ -187,8 +207,11 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
padding: 0.6rem 0.75rem; padding: 0.6rem 0.75rem;
border: 1.5px solid var(--deep-blue); border: 1.5px solid var(--deep-blue);
border-radius: 999px; border-radius: 999px;
background: rgba(255, 255, 255, 0.92); background: color-mix(
backdrop-filter: blur(6px); in srgb,
var(--background-color) 94%,
transparent
);
color: inherit; color: inherit;
font-size: 0.85rem; font-size: 0.85rem;
cursor: pointer; cursor: pointer;
@ -232,11 +255,16 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
z-index: 29; z-index: 29;
width: min(88vw, 24rem); width: min(88vw, 24rem);
max-height: min(65vh, 32rem); max-height: min(65vh, 32rem);
overflow: auto; overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
padding: 0.9rem 1rem; padding: 0.9rem 1rem;
border: 1.5px solid var(--deep-blue); border: 1.5px solid var(--deep-blue);
background: rgba(255, 255, 255, 0.96); background: color-mix(
backdrop-filter: blur(8px); in srgb,
var(--background-color) 97%,
transparent
);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.08); 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 = () => { const updateCurrentHeading = () => {
if (window.innerWidth >= 1200) return;
headingItems = getHeadingElements(); headingItems = getHeadingElements();
let current = null; let current = null;
@ -347,10 +373,16 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
preview.textContent = current ? current.text : defaultText; preview.textContent = current ? current.text : defaultText;
headingLinks.forEach((link) => headingLinks.forEach((link) => {
link.classList.remove("is-current"), const isCurrent = link === current?.link;
); link.classList.toggle("is-current", isCurrent);
if (current) current.link.classList.add("is-current");
if (isCurrent) {
link.setAttribute("aria-current", "location");
} else {
link.removeAttribute("aria-current");
}
});
}; };
const toggleMenu = () => { const toggleMenu = () => {
@ -377,7 +409,11 @@ const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
e.preventDefault(); e.preventDefault();
el.scrollIntoView({ el.scrollIntoView({
behavior: "smooth", behavior: window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches
? "auto"
: "smooth",
block: "start", 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 lang = getLangFromUrl(Astro.url);
const htmlLang = lang === "zh" ? "zh-CN" : "en-US"; const htmlLang = lang === "zh" ? "zh-CN" : "en-US";
const isHomePage = new RegExp(`^/${lang}/?$`).test(Astro.url.pathname);
import SEO from "@/components/SEO.astro"; import SEO from "@/components/SEO.astro";
import FloatingActions from "@/components/FloatingActions.astro"; import FloatingActions from "@/components/FloatingActions.astro";
import Spinner from "@/components/Spinner.astro"; import Spinner from "@/components/Spinner.astro";
@ -44,6 +45,33 @@ const {
<FloatingActions /> <FloatingActions />
<script is:inline> <script is:inline>
function addCodeCopyButtons() { 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) => { document.querySelectorAll("pre").forEach((pre) => {
const code = pre.querySelector("code"); const code = pre.querySelector("code");
if (!code) return; if (!code) return;
@ -62,6 +90,29 @@ const {
pre.parentNode?.insertBefore(wrapper, pre); pre.parentNode?.insertBefore(wrapper, pre);
wrapper.appendChild(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"); const button = document.createElement("button");
button.className = "code-copy-button"; button.className = "code-copy-button";
@ -76,13 +127,15 @@ const {
icon.setAttribute("aria-hidden", "true"); icon.setAttribute("aria-hidden", "true");
button.appendChild(icon); button.appendChild(icon);
wrapper.appendChild(button); toolbar.appendChild(button);
button.addEventListener("click", async () => { button.addEventListener("click", async () => {
const text = code.textContent ?? ""; const text = code.textContent ?? "";
let copied = false;
try { try {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
copied = true;
} catch { } catch {
const textarea = document.createElement("textarea"); const textarea = document.createElement("textarea");
textarea.value = text; textarea.value = text;
@ -90,16 +143,25 @@ const {
textarea.style.opacity = "0"; textarea.style.opacity = "0";
textarea.style.pointerEvents = "none"; textarea.style.pointerEvents = "none";
try {
document.body.appendChild(textarea); document.body.appendChild(textarea);
textarea.select(); textarea.select();
document.execCommand("copy"); copied = document.execCommand("copy");
} catch {
copied = false;
} finally {
textarea.remove(); textarea.remove();
button.focus({ preventScroll: true });
}
} }
icon.src = "/images/correct-signal-svgrepo-com.svg"; icon.src = copied
button.setAttribute("aria-label", "已复制"); ? "/images/correct-signal-svgrepo-com.svg"
button.title = "已复制"; : "/images/copy-svgrepo-com.svg";
button.classList.add("copied"); const message = copied ? "已复制" : "复制失败,请手动选择代码复制";
button.setAttribute("aria-label", message);
button.title = message;
button.classList.toggle("copied", copied);
window.setTimeout(() => { window.setTimeout(() => {
icon.src = "/images/copy-svgrepo-com.svg"; icon.src = "/images/copy-svgrepo-com.svg";
@ -115,7 +177,7 @@ const {
document.addEventListener("astro:page-load", addCodeCopyButtons); document.addEventListener("astro:page-load", addCodeCopyButtons);
</script> </script>
<body> <body class:list={{ "is-home": isHomePage }}>
<Header /> <Header />
<Spinner /> <Spinner />
<main <main
@ -129,10 +191,15 @@ const {
</html> </html>
<style> <style>
html, html {
margin: 0;
width: 100%;
min-height: 100%;
}
body { body {
margin: 0; margin: 0;
width: 100%; width: 100%;
height: 100%; min-height: 100%;
} }
</style> </style>

View file

@ -2,6 +2,7 @@
import BaseLayout from "./BaseLayout.astro"; import BaseLayout from "./BaseLayout.astro";
import Remark42Embed from "@/components/Remark42Embed.astro"; import Remark42Embed from "@/components/Remark42Embed.astro";
import PostNav from "@/components/Posts/PostNav.astro"; import PostNav from "@/components/Posts/PostNav.astro";
import TagLink from "@/components/TagLink.astro";
import PostMenu from "@/components/Posts/PostMenu.astro"; import PostMenu from "@/components/Posts/PostMenu.astro";
import Dialog from "@/components/Posts/Dialog.astro"; import Dialog from "@/components/Posts/Dialog.astro";
import { fade } from "astro:transitions"; import { fade } from "astro:transitions";
@ -11,6 +12,14 @@ import "@/styles/global.css";
const { post, frontmatter, lang, slug, postId, headings } = Astro.props; const { post, frontmatter, lang, slug, postId, headings } = Astro.props;
const comments = lang === "zh" ? "评论区" : "comments"; const comments = lang === "zh" ? "评论区" : "comments";
const t = getTranslations(lang); 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 <BaseLayout
@ -18,6 +27,7 @@ const t = getTranslations(lang);
description={frontmatter.description} description={frontmatter.description}
image={frontmatter.image?.url} image={frontmatter.image?.url}
> >
<div class="post-layout page-shell">
<div id="reading-progress" aria-hidden="true"></div> <div id="reading-progress" aria-hidden="true"></div>
<a <a
href="/" href="/"
@ -50,19 +60,20 @@ const t = getTranslations(lang);
{frontmatter.title} {frontmatter.title}
</h1> </h1>
<p class="description"> <p class="description">
<em>{frontmatter.description}</em> {frontmatter.description}
</p> </p>
<p class="meta-line"> <p class="post-byline">
{t.post.publishedOn}: {frontmatter.pubDate.toLocaleDateString()} <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>
<p class="meta-line">{t.post.writtenBy}: {frontmatter.author}</p>
<div class="tags" transition:name={`post-tags-${postId}`}> <div class="tags" transition:name={`post-tags-${postId}`}>
{ {
frontmatter.tags.map((tag: string) => ( frontmatter.tags.map((tag: string) => (
<p class="tag"> <TagLink tag={tag} lang={lang} />
<a href={`/${lang}/tags/${tag}`}>{tag}</a>
</p>
)) ))
} }
</div> </div>
@ -86,10 +97,17 @@ const t = getTranslations(lang);
<PostNav post={post} lang={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} />)} {t.post.commentsDesc.map((line: string) => <p set:html={line} />)}
<Remark42Embed slug={slug} /> <Remark42Embed slug={slug} />
</section>
</article> </article>
</div>
</BaseLayout> </BaseLayout>
<script <script
@ -148,36 +166,68 @@ const t = getTranslations(lang);
backButton.dataset.bound = "true"; backButton.dataset.bound = "true";
} }
}; };
const updateProgress = () => { const initReadingProgress = () => {
const doc = document.documentElement; const readingWindow = window as typeof window & {
const scrollTop = window.scrollY || doc.scrollTop; __postReadingProgressHandler?: () => void;
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)}%`;
}
}; };
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("scroll", updateProgress, { passive: true });
window.addEventListener("resize", updateProgress); window.addEventListener("resize", updateProgress);
updateProgress();
};
initReadingProgress();
initFloatingActions(); initFloatingActions();
document.addEventListener("astro:page-load", initReadingProgress);
document.addEventListener("astro:page-load", initFloatingActions); document.addEventListener("astro:page-load", initFloatingActions);
</script> </script>
<style> <style>
.post-layout {
min-width: 0;
}
#reading-progress { #reading-progress {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
z-index: 9999; z-index: 9999;
width: 0; width: 100%;
height: 3px; height: 3px;
background: #5a89ff; background: #5a89ff;
transition: width 0.08s linear; transform: scaleX(0);
transform-origin: left center;
transition: transform 0.08s linear;
pointer-events: none; pointer-events: none;
} }
@ -192,15 +242,12 @@ const t = getTranslations(lang);
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-decoration: none; text-decoration: none;
border: #536493 1.5px solid; border-top: #536493 1.5px dotted;
border-top: #536493 2.5px dotted; border-left: #536493 1.5px dotted;
border-left: #536493 2.5px dotted;
border-right: #536493 2.5px solid; border-right: #536493 2.5px solid;
border-bottom: #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; color: #222;
/* box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); */
backdrop-filter: blur(6px);
z-index: 1000; z-index: 1000;
visibility: hidden; visibility: hidden;
transform: translateY(-80px); transform: translateY(-80px);
@ -230,10 +277,16 @@ const t = getTranslations(lang);
} }
:global(.dark) .back-button { :global(.dark) .back-button {
background: rgba(34, 34, 34, 0.92); background: color-mix(
in srgb,
var(--background-color-dark) 94%,
transparent
);
color: #f5f5f5; color: #f5f5f5;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32); border-top-color: #515151;
border: #2f406d 2px solid; border-left-color: #515151;
border-right-color: #515151;
border-bottom-color: #515151;
} }
@media (max-width: 640px) { @media (max-width: 640px) {
@ -245,6 +298,9 @@ const t = getTranslations(lang);
} }
} }
.post-content { .post-content {
--article-muted: #596672;
--article-rule: rgba(83, 100, 147, 0.25);
--article-tint: rgba(83, 100, 147, 0.045);
font-family: font-family:
system-ui, system-ui,
-apple-system, -apple-system,
@ -257,6 +313,130 @@ const t = getTranslations(lang);
"Source Han Sans SC", "Source Han Sans SC",
sans-serif; sans-serif;
font-weight: 400; 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), .post-content :global(code),
@ -267,7 +447,7 @@ const t = getTranslations(lang);
} }
a { a {
color: var(--post-link-color); color: var(--url-color);
} }
.post-header { .post-header {
@ -287,31 +467,64 @@ const t = getTranslations(lang);
line-height: 1.25; line-height: 1.25;
overflow-wrap: anywhere; overflow-wrap: anywhere;
word-break: break-word; word-break: break-word;
text-wrap: balance;
} }
.description { .description {
margin: 0 0 0.7rem 0; margin: 0 0 0.7rem 0;
opacity: 0.9; color: #596672;
font-weight: 400;
line-height: 1.6; line-height: 1.6;
overflow-wrap: anywhere; overflow-wrap: anywhere;
word-break: break-word; 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; 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; overflow-wrap: anywhere;
word-break: break-word; 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 { .post-cover {
width: 300px; width: 300px;
height: auto; height: auto;
aspect-ratio: 16 / 10; aspect-ratio: 16 / 10;
object-fit: cover; object-fit: cover;
object-position: center; object-position: center;
/* border-radius: 0.4rem; */
border: 1.5px #94a0ab dashed;
display: block; display: block;
} }
@ -327,39 +540,10 @@ const t = getTranslations(lang);
.tags { .tags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.4rem; gap: 0.25rem 0.9rem;
margin-top: 0.8rem; margin-top: 0.8rem;
min-width: 0; min-width: 0;
}
.tag {
margin: 0;
padding: 0.22em 0.65em;
font-size: 0.88rem; 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) { @media (max-width: 560px) {
@ -374,15 +558,16 @@ const t = getTranslations(lang);
} }
} }
details { .post-content :global(details) {
margin: 1.5rem 0; margin: 1.5rem 0;
overflow: hidden; overflow: hidden;
border-top: 2px solid #1e3a5f; border-top: 1px solid var(--article-rule);
border-radius: 0 0 0.5rem 0.5rem; border-bottom: 1px solid var(--article-rule);
background: rgba(128, 128, 128, 0.14); border-radius: 0;
background: var(--article-tint);
} }
details summary { .post-content :global(details summary) {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
@ -393,11 +578,11 @@ const t = getTranslations(lang);
list-style: none; list-style: none;
} }
details summary::-webkit-details-marker { .post-content :global(details summary::-webkit-details-marker) {
display: none; display: none;
} }
details summary::before { .post-content :global(details summary::before) {
content: "›"; content: "›";
display: inline-block; display: inline-block;
font-size: 1.25rem; font-size: 1.25rem;
@ -405,46 +590,46 @@ const t = getTranslations(lang);
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
details[open] summary::before { .post-content :global(details[open] summary::before) {
transform: rotate(90deg); transform: rotate(90deg);
} }
/* 展开内容 */ /* 展开内容 */
details > :not(summary) { .post-content :global(details > :not(summary)) {
box-sizing: border-box; box-sizing: border-box;
margin-top: 0; margin: 0.8rem 1rem;
margin-bottom: 0; padding: 0;
padding-right: 1rem;
} }
/* 为列表序号预留空间 */ /* 为列表序号预留空间 */
details > ol, .post-content :global(details > ol),
details > ul { .post-content :global(details > ul) {
padding: 0.6rem 1rem 1rem 3rem; padding-inline-start: 1.6em;
} }
details li { .post-content :global(details li) {
margin: 0.8rem 0; margin: 0.8rem 0;
} }
blockquote { .post-content :global(blockquote) {
margin: 1.25rem 0; margin: 1.5rem 0;
padding: 0.75rem 1rem; padding: 0.15rem 0 0.15rem 1rem;
border-left: 4px solid var(--deep-red); border-left: 3px solid var(--deep-blue);
background: rgba(128, 128, 128, 0.1); color: var(--article-muted);
border-radius: 0 0.4rem 0.4rem 0; background: transparent;
border-radius: 0;
} }
blockquote p { .post-content :global(blockquote p) {
margin: 0; margin: 0;
} }
blockquote p + p { .post-content :global(blockquote p + p) {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
/* 块级公式:超出屏幕时只滚动公式,不撑宽整篇文章 */ /* 块级公式:超出屏幕时只滚动公式,不撑宽整篇文章 */
.katex-display { .post-content :global(.katex-display) {
max-width: 100%; max-width: 100%;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
@ -453,7 +638,27 @@ const t = getTranslations(lang);
} }
/* 避免 KaTeX 内部元素强行把父容器撑宽 */ /* 避免 KaTeX 内部元素强行把父容器撑宽 */
.katex-display > .katex { .post-content :global(.katex-display > .katex) {
min-width: max-content; 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> </style>

View file

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

View file

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

View file

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

View file

@ -14,10 +14,11 @@ const lang = getLangFromUrl(Astro.url);
const t = getTranslations(lang); const t = getTranslations(lang);
const headerTitle = lang === "zh" ? "Cloverta的博客" : "Cloverta's blog"; const headerTitle = lang === "zh" ? "Cloverta的博客" : "Cloverta's blog";
const pageTitle = t.home.title; 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}`> <BaseLayout pageTitle=`${headerTitle} - ${t.banner.subtitle}`>
<div class="index page-shell">
<h1 class="page-title"> <h1 class="page-title">
<span>{pageTitle}</span> <span>{pageTitle}</span>
<a <a
@ -48,17 +49,22 @@ const rssLink = lang === "zh" ? "/rss.xml" : "/en/rss.xml"
<div class="section-divider"></div> <div class="section-divider"></div>
<h2>{t.home.latestTitle}</h2> <h2 class="section-title">{t.home.latestTitle}</h2>
<PostList /> <PostList />
<div class="section-divider"></div> <div class="section-divider"></div>
<!-- <Remark42LatestComments lang={lang} client:load /> --> <!-- <Remark42LatestComments lang={lang} client:load /> -->
<LatestComments /> <LatestComments lang={lang} />
</div>
</BaseLayout> </BaseLayout>
<style> <style>
.index {
min-width: 0;
}
.section-divider { .section-divider {
height: 1px; height: 1px;
background: #cdd2d8; background: #cdd2d8;
@ -73,4 +79,19 @@ const rssLink = lang === "zh" ? "/rss.xml" : "/en/rss.xml"
.page-title { .page-title {
font-weight: 700; 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> </style>

View file

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

View file

@ -1,5 +1,6 @@
--- ---
import BaseLayout from "@/layouts/BaseLayout.astro"; import BaseLayout from "@/layouts/BaseLayout.astro";
import TagLink from "@/components/TagLink.astro";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import { getTranslations, type Lang } from "@/i18n"; import { getTranslations, type Lang } from "@/i18n";
import "@/styles/global.css"; import "@/styles/global.css";
@ -16,6 +17,7 @@ const pageTitle = lang === "zh" ? "标签索引" : "Tag Index";
--- ---
<BaseLayout pageTitle=`${pageTitle} - ${t.banner.title}`> <BaseLayout pageTitle=`${pageTitle} - ${t.banner.title}`>
<div class="tags-layout page-shell">
<h1>{t.tags.title}</h1> <h1>{t.tags.title}</h1>
<p>{t.tags.description}</p> <p>{t.tags.description}</p>
@ -23,60 +25,25 @@ const pageTitle = lang === "zh" ? "标签索引" : "Tag Index";
{ {
tags.map((tag) => ( tags.map((tag) => (
<p class="tag" transition:name={`post-tags-${tag}`}> <p class="tag" transition:name={`post-tags-${tag}`}>
<a href={`/${lang}/tags/${tag}`}>{tag}</a> <TagLink tag={String(tag)} lang={lang} />
</p> </p>
)) ))
} }
</div> </div>
</div>
</BaseLayout> </BaseLayout>
<style> <style>
.tags { .tags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.4rem; gap: 0.4rem 1.25rem;
margin-top: 0.8rem; margin-top: 0.8rem;
} }
.tag { .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; margin: 0;
padding: 0.22em 0.65em;
font-size: 1rem; font-size: 1rem;
line-height: 1.2; max-width: 100%;
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;
} }
</style> </style>

View file

@ -14,6 +14,8 @@ const headerTitle = lang === "zh" ? "时间轴" : "Timeline";
--- ---
<BaseLayout pageTitle=`${headerTitle} - ${t.banner.title}` `> <BaseLayout pageTitle=`${headerTitle} - ${t.banner.title}` `>
<div class="page-shell">
<h1>{t.nav.timeline}</h1> <h1>{t.nav.timeline}</h1>
<PostTimeline /> <PostTimeline />
</div>
</BaseLayout> </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 { .code-block-wrapper {
position: relative; 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 { .code-block-wrapper pre {
margin: 0;
padding: 1rem; padding: 1rem;
border-radius: 3px; max-width: 100%;
border-radius: 0 0 3px 3px;
overflow-x: auto; overflow-x: auto;
line-height: 1.6; line-height: 1.6;
margin: 1rem 0;
position: relative; position: relative;
} }
/* pre:has(> .code-copy-button) {
padding-top: 2.75rem;
} */
code { code {
font-family: "Maple Mono", monospace; font-family: "Maple Mono", monospace;
font-size: 0.96rem; font-size: 0.96rem;
@ -31,12 +41,15 @@ td code {
background: rgba(127, 127, 127, 0.12); background: rgba(127, 127, 127, 0.12);
} }
.code-copy-button { .code-block-wrapper pre > code {
position: absolute; padding: 0;
top: 0.5rem; border-radius: 0;
right: 0.5rem; background: transparent;
z-index: 2; }
.code-copy-button {
flex: 0 0 auto;
margin-left: auto;
display: grid; display: grid;
place-items: center; place-items: center;
width: 2rem; width: 2rem;
@ -58,6 +71,18 @@ td code {
box-shadow 0.15s ease; 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 { .code-copy-icon {
display: block; display: block;
width: 100%; width: 100%;
@ -81,6 +106,11 @@ td code {
background: #dcebd5; background: #dcebd5;
} }
html.dark .code-language-label,
.dark .code-language-label {
color: #aeb9c4;
}
/* .dark .code-copy-button { /* .dark .code-copy-button {
border-color: #9ecbff; border-color: #9ecbff;
} }

View file

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

View file

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

View file

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