refactor overall style

This commit is contained in:
ClovertaTheTrilobita 2026-09-17 16:16:27 +08:00
parent 0c39b1c120
commit 5ecebe00ec
19 changed files with 626 additions and 161 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

@ -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

@ -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 || ""
@ -226,6 +233,9 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
const href = escapeHtml( const href = escapeHtml(
rewritePostUrl(item?.locator?.url || "", lang), rewritePostUrl(item?.locator?.url || "", lang),
); );
const discussionHref = escapeHtml(
getDiscussionUrl(item?.locator?.url || "", lang),
);
const preview = getCommentPreview(item?.text, lang); const preview = getCommentPreview(item?.text, lang);
const time = formatTime(item?.time, lang); const time = formatTime(item?.time, lang);
@ -260,8 +270,13 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
<div class="comment-card-text">${preview}</div> <div class="comment-card-text">${preview}</div>
<footer class="comment-source"> <footer class="comment-source">
<span>${lang === "zh" ? "评论于" : "On"}</span> <div class="comment-source-context">
<a class="comment-title-link" href="${href}">${title}</a> <span>${lang === "zh" ? "评论于" : "On"}</span>
<a class="comment-title-link" href="${href}">${title}</a>
</div>
<a class="comment-discussion-link" href="${discussionHref}">${
lang === "zh" ? "查看完整讨论" : "View discussion"
}</a>
</footer> </footer>
</div> </div>
</article> </article>
@ -269,6 +284,18 @@ const heading = lang === "zh" ? "最新评论" : "Latest comments";
}) })
.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";
} }

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

@ -45,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;
@ -63,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";
@ -77,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;
@ -91,16 +143,25 @@ const {
textarea.style.opacity = "0"; textarea.style.opacity = "0";
textarea.style.pointerEvents = "none"; textarea.style.pointerEvents = "none";
document.body.appendChild(textarea); try {
textarea.select(); document.body.appendChild(textarea);
document.execCommand("copy"); textarea.select();
textarea.remove(); copied = document.execCommand("copy");
} catch {
copied = false;
} finally {
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";

View file

@ -27,7 +27,7 @@ const publishedDate = new Intl.DateTimeFormat(
description={frontmatter.description} description={frontmatter.description}
image={frontmatter.image?.url} image={frontmatter.image?.url}
> >
<main class="post-layout"> <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="/"
@ -60,7 +60,7 @@ const publishedDate = new Intl.DateTimeFormat(
{frontmatter.title} {frontmatter.title}
</h1> </h1>
<p class="description"> <p class="description">
<em>{frontmatter.description}</em> {frontmatter.description}
</p> </p>
<p class="post-byline"> <p class="post-byline">
<span>{t.post.writtenBy} {frontmatter.author}</span> <span>{t.post.writtenBy} {frontmatter.author}</span>
@ -97,11 +97,17 @@ const publishedDate = new Intl.DateTimeFormat(
<PostNav post={post} lang={lang} /> <PostNav post={post} lang={lang} />
<h2>{comments}</h2> <section
{t.post.commentsDesc.map((line: string) => <p set:html={line} />)} id="comments"
<Remark42Embed slug={slug} /> 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> </article>
</main> </div>
</BaseLayout> </BaseLayout>
<script <script
@ -160,30 +166,55 @@ const publishedDate = new Intl.DateTimeFormat(
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; if (readingWindow.__postReadingProgressHandler) {
window.removeEventListener(
"scroll",
readingWindow.__postReadingProgressHandler,
);
window.removeEventListener(
"resize",
readingWindow.__postReadingProgressHandler,
);
}
const bar = document.getElementById("reading-progress"); const bar = document.getElementById("reading-progress");
if (bar) { const content = document.querySelector<HTMLElement>(".post-content");
bar.style.width = `${Math.min(progress, 100)}%`; 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();
}; };
updateProgress(); initReadingProgress();
window.addEventListener("scroll", updateProgress, { passive: true });
window.addEventListener("resize", updateProgress);
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 { .post-layout {
max-width: 850px; min-width: 0;
margin: 0 auto;
} }
#reading-progress { #reading-progress {
@ -191,10 +222,12 @@ const publishedDate = new Intl.DateTimeFormat(
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;
} }
@ -209,15 +242,12 @@ const publishedDate = new Intl.DateTimeFormat(
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);
@ -247,10 +277,16 @@ const publishedDate = new Intl.DateTimeFormat(
} }
: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) {
@ -262,6 +298,9 @@ const publishedDate = new Intl.DateTimeFormat(
} }
} }
.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,
@ -274,6 +313,130 @@ const publishedDate = new Intl.DateTimeFormat(
"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),
@ -284,7 +447,7 @@ const publishedDate = new Intl.DateTimeFormat(
} }
a { a {
color: var(--post-link-color); color: var(--url-color);
} }
.post-header { .post-header {
@ -304,11 +467,13 @@ const publishedDate = new Intl.DateTimeFormat(
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;
@ -337,6 +502,23 @@ const publishedDate = new Intl.DateTimeFormat(
color: #aab7c4; 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;
@ -376,15 +558,16 @@ const publishedDate = new Intl.DateTimeFormat(
} }
} }
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;
@ -395,11 +578,11 @@ const publishedDate = new Intl.DateTimeFormat(
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;
@ -407,46 +590,46 @@ const publishedDate = new Intl.DateTimeFormat(
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;
@ -455,7 +638,27 @@ const publishedDate = new Intl.DateTimeFormat(
} }
/* 避免 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}`>
<FriendsDescription /> <div class="page-shell">
<div class="section-divider"></div> <FriendsDescription />
<FriendlyLinkList /> <div class="section-divider"></div>
<FriendlyLinkList />
</div>
</BaseLayout> </BaseLayout>
<style> <style>

View file

@ -18,7 +18,7 @@ const rssLink = lang === "zh" ? "/rss.xml" : "/en/rss.xml";
--- ---
<BaseLayout pageTitle=`${headerTitle} - ${t.banner.subtitle}`> <BaseLayout pageTitle=`${headerTitle} - ${t.banner.subtitle}`>
<main class="index"> <div class="index page-shell">
<h1 class="page-title"> <h1 class="page-title">
<span>{pageTitle}</span> <span>{pageTitle}</span>
<a <a
@ -57,13 +57,12 @@ const rssLink = lang === "zh" ? "/rss.xml" : "/en/rss.xml";
<!-- <Remark42LatestComments lang={lang} client:load /> --> <!-- <Remark42LatestComments lang={lang} client:load /> -->
<LatestComments lang={lang} /> <LatestComments lang={lang} />
</main> </div>
</BaseLayout> </BaseLayout>
<style> <style>
.index { .index {
max-width: 850px; min-width: 0;
margin: 0 auto;
} }
.section-divider { .section-divider {

View file

@ -32,34 +32,36 @@ const t = getTranslations(lang);
--- ---
<BaseLayout pageTitle={String(tag)}> <BaseLayout pageTitle={String(tag)}>
<p class="tag-heading"> <div class="page-shell">
{lang === "zh" ? "带有标签" : "Posts tagged with"}{" "} <p class="tag-heading">
<span class="tag-chip" transition:name={`post-tags-${tag}`}>{tag}</span> {lang === "zh" ? "带有标签" : "Posts tagged with"}{" "}
{lang === "zh" ? "的文章" : ""} <span class="tag-chip" transition:name={`post-tags-${tag}`}>{tag}</span>
</p> {lang === "zh" ? "的文章" : ""}
<ul> </p>
{ <ul>
posts.map((post: any) => { {
const formattedDate = new Date(post.data.pubDate) posts.map((post: any) => {
.toISOString() const formattedDate = new Date(post.data.pubDate)
.split("T")[0]; .toISOString()
.split("T")[0];
const [postLang, ...slugParts] = post.id.split("/"); const [postLang, ...slugParts] = post.id.split("/");
const slug = slugParts.join("/"); const slug = slugParts.join("/");
return ( return (
<PostItem <PostItem
url={`/${lang}/posts/${slug}/`} url={`/${lang}/posts/${slug}/`}
title={post.data.title} title={post.data.title}
date={formattedDate} date={formattedDate}
img={post.data.image.url} img={post.data.image.url}
tags={post.data.tags} tags={post.data.tags}
postId={post.id} postId={post.id}
/> />
); );
}) })
} }
</ul> </ul>
</div>
</BaseLayout> </BaseLayout>
<style> <style>

View file

@ -17,7 +17,7 @@ const pageTitle = lang === "zh" ? "标签索引" : "Tag Index";
--- ---
<BaseLayout pageTitle=`${pageTitle} - ${t.banner.title}`> <BaseLayout pageTitle=`${pageTitle} - ${t.banner.title}`>
<main class="tags-layout"> <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>
@ -30,14 +30,10 @@ const pageTitle = lang === "zh" ? "标签索引" : "Tag Index";
)) ))
} }
</div> </div>
</main> </div>
</BaseLayout> </BaseLayout>
<style> <style>
.tags-layout {
max-width: 850px;
margin: 0 auto;
}
.tags { .tags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View file

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

View file

@ -139,7 +139,8 @@
.comment-source { .comment-source {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 0.4rem; justify-content: space-between;
gap: 0.55rem 1rem;
min-width: 0; min-width: 0;
color: #89939c; color: #89939c;
font-family: system-ui, sans-serif; font-family: system-ui, sans-serif;
@ -147,6 +148,13 @@
line-height: 1.45; line-height: 1.45;
} }
.comment-source-context {
display: flex;
align-items: baseline;
gap: 0.4rem;
min-width: 0;
}
.comment-title-link { .comment-title-link {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
@ -163,6 +171,22 @@
text-decoration: underline; text-decoration: underline;
} }
.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.is-truncated .comment-discussion-link {
display: inline;
}
.latest-comments-empty { .latest-comments-empty {
color: #777; color: #777;
font-style: italic; font-style: italic;
@ -201,6 +225,11 @@ html.dark .comment-title-link,
color: #aabbd0; color: #aabbd0;
} }
html.dark .comment-discussion-link,
.dark .comment-discussion-link {
color: #9ecbff;
}
html.dark .comment-avatar-fallback, html.dark .comment-avatar-fallback,
.dark .comment-avatar-fallback { .dark .comment-avatar-fallback {
background: rgba(180, 190, 200, 0.14); background: rgba(180, 190, 200, 0.14);
@ -215,6 +244,11 @@ html.dark .comment-avatar-fallback,
.comment-card-text { .comment-card-text {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.comment-source {
align-items: flex-start;
flex-wrap: wrap;
}
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {

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;