Compare commits

...

48 commits

Author SHA1 Message Date
ClovertaTheTrilobita
f47501d79d
Merge pull request #27 from ClovertaTheTrilobita/dev
Some checks failed
Sync and Deploy Astro Blog / sync-blog-content (push) Has been cancelled
Sync and Deploy Astro Blog / manual-deploy (push) Has been cancelled
Sync and Deploy Astro Blog / build-and-deploy (push) Has been cancelled
Seperate zh-cn and en-us rss
2026-06-18 15:46:07 +08:00
e6fb5a4519 Seperate zh-cn and en-us rss 2026-06-18 15:45:32 +08:00
ClovertaTheTrilobita
715d6c9b7b
Merge pull request #26 from ClovertaTheTrilobita/dev
Some checks failed
Sync and Deploy Astro Blog / sync-blog-content (push) Has been cancelled
Sync and Deploy Astro Blog / manual-deploy (push) Has been cancelled
Sync and Deploy Astro Blog / build-and-deploy (push) Has been cancelled
fix css
2026-06-02 02:46:38 +08:00
ClovertaTheTrilobita
1bb9d56e03
Update global.css 2026-06-02 02:43:54 +08:00
ClovertaTheTrilobita
5673fc7ddb
Update global.css 2026-06-02 02:42:42 +08:00
ClovertaTheTrilobita
7067e43cb4
Merge pull request #25 from ClovertaTheTrilobita/dev
fix css
2026-06-02 02:35:44 +08:00
ClovertaTheTrilobita
1901b50062
Update MarkdownPostLayout.astro 2026-06-02 02:35:05 +08:00
ClovertaTheTrilobita
94cbaf7707
Update global.css 2026-06-02 02:33:56 +08:00
c9d1bacec4 Merge branch 'dev' 2026-06-02 01:56:52 +08:00
6e84ef4884 added comments desc 2026-06-02 01:55:28 +08:00
06999dae80 Merge branch 'dev' 2026-06-02 01:37:43 +08:00
eeff9fa4f6 update dependencies 2026-06-02 01:37:14 +08:00
9d4c8d9f76 Merge branch 'dev' 2026-06-02 01:11:29 +08:00
b60b8d8e61 update github cd 2026-06-02 01:10:24 +08:00
664fa20b0a Merge branch 'dev' 2026-06-02 01:00:37 +08:00
04aca48a3b added css for quote and dropdown 2026-06-02 00:57:49 +08:00
ClovertaTheTrilobita
9b8f3794c8
Merge pull request #24 from ClovertaTheTrilobita/dev
Some checks are pending
Sync and Deploy Astro Blog / sync-blog-content (push) Waiting to run
Sync and Deploy Astro Blog / build-and-deploy (push) Blocked by required conditions
Sync and Deploy Astro Blog / manual-deploy (push) Waiting to run
Added inline latex support
2026-06-01 12:53:34 +03:00
f54acb3ac3 Added inline latex support 2026-06-01 17:50:06 +08:00
eabd15fc74 Merge branch 'dev'
Some checks failed
Sync and Deploy Astro Blog / sync-blog-content (push) Has been cancelled
Sync and Deploy Astro Blog / manual-deploy (push) Has been cancelled
Sync and Deploy Astro Blog / build-and-deploy (push) Has been cancelled
2026-04-25 01:22:22 +03:00
db6deb3205 update preview radius 2026-04-25 01:22:07 +03:00
ClovertaTheTrilobita
c69e72cd2f
Merge pull request #23 from ClovertaTheTrilobita/dev
Add image preview
2026-04-25 01:17:26 +03:00
ae259e84e6 Refactor dialog as a component 2026-04-25 01:15:55 +03:00
f86ac62e04 Added image preview 2026-04-25 01:15:55 +03:00
d30148c021 Merge branch 'dev' 2026-04-25 00:56:21 +03:00
9c53ea922e Added runtime darkmode to remark42 2026-04-25 00:56:06 +03:00
ClovertaTheTrilobita
9b017117d8
Merge pull request #22 from ClovertaTheTrilobita/dev
Added menu feature in post
2026-04-25 00:24:25 +03:00
7292affc04 fix menu dark colors 2026-04-25 00:23:39 +03:00
18ddbb82f1 update pc menu behavior 2026-04-25 00:13:55 +03:00
0541250822 update mobile menu color 2026-04-25 00:05:17 +03:00
5b4ba442ed update menu style, refactor css 2026-04-25 00:01:08 +03:00
b10aec8d15 update menu actions 2026-04-19 02:20:01 +03:00
2f48907035 added menu 2026-04-18 22:29:51 +03:00
ClovertaTheTrilobita
0936ea2c58
Merge pull request #21 from ClovertaTheTrilobita/dev
Some checks failed
Sync and Deploy Astro Blog / sync-blog-content (push) Has been cancelled
Sync and Deploy Astro Blog / manual-deploy (push) Has been cancelled
Sync and Deploy Astro Blog / build-and-deploy (push) Has been cancelled
Added Prev & Next post in post layout
2026-04-18 22:29:45 +03:00
fabe058bfe added animation to postnav 2026-04-18 21:52:28 +03:00
d9ae38197c update post nav format 2026-04-18 21:46:01 +03:00
804bfbd080 Added navigation in posts 2026-04-18 21:38:42 +03:00
1e389aeb44 Merge branch 'dev'
Some checks are pending
Sync and Deploy Astro Blog / sync-blog-content (push) Waiting to run
Sync and Deploy Astro Blog / build-and-deploy (push) Blocked by required conditions
Sync and Deploy Astro Blog / manual-deploy (push) Waiting to run
2026-04-17 21:56:46 +03:00
7dd21ea510 update font link 2026-04-17 21:56:28 +03:00
8fe898a281 Merge branch 'dev' 2026-04-17 21:04:48 +03:00
9db3347970 update abouot 2026-04-17 21:04:33 +03:00
ClovertaTheTrilobita
55c446a4e4
Merge pull request #20 from ClovertaTheTrilobita/dev
Update friends
2026-04-17 19:16:38 +03:00
27badbee83 Update friends 2026-04-17 19:16:02 +03:00
1160e375d4 Merge branch 'dev'
Some checks are pending
Sync and Deploy Astro Blog / sync-blog-content (push) Waiting to run
Sync and Deploy Astro Blog / build-and-deploy (push) Blocked by required conditions
Sync and Deploy Astro Blog / manual-deploy (push) Waiting to run
2026-04-17 14:26:36 +03:00
9973a78d4a update title 2026-04-17 14:26:16 +03:00
965bde4bed Merge branch 'dev' 2026-04-17 14:02:55 +03:00
8c4eae8a96 update font settings 2026-04-17 14:02:45 +03:00
e96ccb6841 Merge branch 'dev' 2026-04-17 13:42:39 +03:00
cce78260c3 fix html lang and font 2026-04-17 13:42:26 +03:00
26 changed files with 2320 additions and 1284 deletions

View file

@ -46,6 +46,9 @@ jobs:
needs: sync-blog-content
if: github.event_name == 'push'
runs-on: ubuntu-latest
environment:
name: production
url: https://blog.cloverta.top
env:
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
@ -105,6 +108,9 @@ jobs:
manual-deploy:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
environment:
name: production
url: https://blog.cloverta.top
env:
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}

View file

@ -3,6 +3,8 @@ import { defineConfig } from 'astro/config';
import sitemap from "@astrojs/sitemap";
import svelte from "@astrojs/svelte";
import rehypeMermaid from "rehype-mermaid";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
// https://astro.build/config
import { fileURLToPath } from 'node:url'
@ -24,6 +26,10 @@ export default defineConfig({
type: 'shiki',
excludeLangs: ['mermaid', 'math'],
},
rehypePlugins: [rehypeMermaid],
rehypePlugins: [
rehypeMermaid,
rehypeKatex,
],
remarkPlugins: [remarkMath],
},
})

1808
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -16,10 +16,13 @@
"@astrojs/sitemap": "^3.7.1",
"@astrojs/svelte": "^8.0.4",
"astro": "^6.0.8",
"katex": "^0.17.0",
"playwright": "^1.59.1",
"rehype-mermaid": "^3.0.0"
"rehype-katex": "^7.0.1",
"rehype-mermaid": "^3.0.0",
"remark-math": "^6.0.0"
},
"devDependencies": {
"@types/node": "^25.5.0"
}
}
}

View file

@ -20,7 +20,7 @@ const blocks: string[] = Array.from({ length: blockCount }, (_, i) => {
title={`Build ${safeHash}`}
aria-label={`Build ${safeHash}`}
>
<div class="build-hint">Astro Build</div>
<div class="build-hint">$build.onCommit.hash =</div>
{
blocks.map((color: string) => (
<span class="build-block" style={`background:${color}`} />

View file

@ -90,9 +90,9 @@ const t = getTranslations(lang);
margin-bottom: 1rem;
background: repeating-linear-gradient(
-45deg,
#ef5a6f 0 14px,
var(--deep-red) 0 14px,
transparent 14px 28px,
#536493 28px 42px,
var(--deep-blue) 28px 42px,
transparent 42px 56px
);
pointer-events: none;

View file

@ -23,6 +23,8 @@ const links = await getCollection("friends");
<style>
.friendly-link-list {
max-width: 770px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;

View file

@ -0,0 +1,108 @@
---
import { getLangFromUrl, getTranslations } from "@/i18n";
import "@/styles/global.css";
const lang = getLangFromUrl(Astro.url);
const t = getTranslations(lang);
---
<div class="description">
<div class="image-wrap">
<img
src="https://files.seeusercontent.com/2026/04/17/nOg4/7tx5GS3nAtOXM577XE9VOe0LUU8.webp"
alt="nichijo.webp"
/>
<p class="caption">{t.friends.imgDesc}</p>
</div>
<h1>{t.friends.title}</h1>
<div class="content">
{t.friends.content.map((line: string) => <p set:html={line} />)}
</div>
<div class="friend-code-block">
<pre><code>---
name: 'CLoverta的博客'
description: "欢迎光临,请进门左转"
url: "https://blog.cloverta.top"
avatar: 'https://s2.loli.net/2025/11/22/tiDKuzdqycx1v9B.png'
---</code></pre>
</div>
<!-- <div class="player">
<iframe
data-testid="embed-iframe"
style="border-radius:2px"
src="https://open.spotify.com/embed/track/2vuSiplO7RnGkoyw2gH96M?utm_source=generator"
width="450px"
height="80"
frameborder="0"
allowfullscreen=""
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"></iframe>
</div> -->
</div>
<style>
.player {
margin-top: 1.5rem;
display: flex;
justify-content: center;
}
.content p {
line-height: 1.8;
margin: 1rem 0;
}
.description {
max-width: 800px;
margin: 0 auto;
padding: 2rem 1rem;
}
.friend-code-block {
background: #fbf5f2;
border: 1px solid #8fa1ad;
border-radius: 3px;
padding: 0rem;
overflow-x: auto;
}
.friend-code-block pre {
margin: 0;
}
.friend-code-block code {
font-family: monospace;
white-space: pre;
}
.image-wrap {
width: fit-content;
margin: 0 auto;
}
.image-wrap img {
width: calc((3.8em * 4) * 16 / 5);
height: calc(3.4em * 4);
aspect-ratio: 16 / 5;
object-fit: cover;
object-position: center;
display: block;
margin: 0 auto;
border: 2px dashed gray;
filter: sepia(0.3) saturate(1.08) hue-rotate(-12deg) brightness(0.97);
}
.image-wrap .caption {
margin-top: 0.3rem;
text-align: right;
color: #888;
font-style: italic;
font-size: 0.9rem;
}
@media (max-width: 600px) {
.description img {
height: calc(2.6em * 4);
}
}
</style>

View file

@ -26,6 +26,20 @@ const t = getTranslations(lang);
</details>
</nav>
<script>
document.addEventListener("click", (event) => {
const nav = document.querySelector(".site-nav-mobile");
if (!nav) return;
const target = event.target as Node;
if (!nav.contains(target)) {
nav.removeAttribute("open");
}
});
</script>
<style>
.site-nav {
margin: 1rem 0;
@ -42,9 +56,9 @@ const t = getTranslations(lang);
height: 12px;
background: repeating-linear-gradient(
-45deg,
#ef5a6f 0 14px,
var(--deep-red) 0 14px,
transparent 14px 28px,
#536493 28px 42px,
var(--deep-blue) 28px 42px,
transparent 42px 56px
);
pointer-events: none;

View file

@ -0,0 +1,118 @@
<dialog id="image-preview-dialog" class="image-preview-dialog">
<button class="image-preview-close" aria-label="关闭图片预览">×</button>
<img id="image-preview-img" alt="" />
<a
id="image-preview-download"
class="image-preview-download"
href="#"
aria-label="下载图片"
title="下载图片"
>
<svg
width="26"
height="26"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<path
d="M12 3v12m0 0 5-5m-5 5-5-5M5 21h14"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"></path>
</svg>
</a>
</dialog>
<script is:inline>
function initImagePreview() {
const dialog = document.getElementById("image-preview-dialog");
const previewImg = document.getElementById("image-preview-img");
const closeBtn = dialog?.querySelector(".image-preview-close");
const downloadBtn = document.getElementById("image-preview-download");
if (!dialog || !previewImg || !closeBtn || !downloadBtn) return;
let currentImageUrl = "";
function getFileNameFromUrl(url) {
try {
const pathname = new URL(url, window.location.href).pathname;
return pathname.split("/").pop() || "image";
} catch {
return "image";
}
}
async function downloadImage(url) {
const response = await fetch(url, {
mode: "cors",
credentials: "omit",
});
if (!response.ok) {
throw new Error("Failed to fetch image");
}
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = blobUrl;
a.download = getFileNameFromUrl(url);
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(blobUrl);
}
document.querySelectorAll("article img").forEach((img) => {
if (img.dataset.previewBound === "true") return;
img.dataset.previewBound = "true";
img.addEventListener("click", () => {
const imageUrl = img.currentSrc || img.src;
currentImageUrl = imageUrl;
previewImg.src = imageUrl;
previewImg.alt = img.alt || "";
dialog.showModal();
});
});
downloadBtn.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
if (!currentImageUrl) return;
try {
await downloadImage(currentImageUrl);
} catch (error) {
// 如果 fetch 因为跨域失败,就退回到直接打开图片
window.open(currentImageUrl, "_blank", "noopener,noreferrer");
}
});
function closePreview() {
dialog.close();
previewImg.removeAttribute("src");
currentImageUrl = "";
}
closeBtn.addEventListener("click", closePreview);
dialog.addEventListener("click", (event) => {
if (event.target === dialog) {
closePreview();
}
});
}
document.addEventListener("DOMContentLoaded", initImagePreview);
document.addEventListener("astro:page-load", initImagePreview);
</script>

View file

@ -0,0 +1,403 @@
---
interface Heading {
depth: number;
slug: string;
text: string;
}
interface Props {
headings: Heading[];
}
const { headings = [] } = Astro.props;
const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
---
{
tocHeadings.length > 0 && (
<>
<button
class="post-menu-mobile-toggle"
type="button"
aria-expanded="false"
aria-controls="post-menu-panel"
aria-label="目录"
>
<span
class="post-menu-mobile-preview"
data-post-menu-preview
data-default-text={tocHeadings[0]?.text ?? "本文目录"}
>
{tocHeadings[0]?.text ?? "本文目录"}
</span>
<span class="post-menu-mobile-icon" aria-hidden="true">
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 6.5H19M5 12H19M5 17.5H19"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
/>
<circle cx="3.5" cy="6.5" r="1" fill="currentColor" />
<circle cx="3.5" cy="12" r="1" fill="currentColor" />
<circle cx="3.5" cy="17.5" r="1" fill="currentColor" />
</svg>
</span>
</button>
<aside class="post-menu" id="post-menu-panel">
<nav aria-label="Table of contents">
<p class="post-menu-title">目录</p>
<ul class="post-menu-list">
{tocHeadings.map((heading) => (
<li class={`post-menu-item depth-${heading.depth}`}>
<a
href={`#${heading.slug}`}
data-heading-link
data-heading-text={heading.text}
>
{heading.text}
</a>
</li>
))}
</ul>
</nav>
</aside>
</>
)
}
<style is:global>
:where(h1, h2, h3, h4, h5, h6) {
scroll-margin-top: 4rem;
}
</style>
<style>
.post-menu {
box-sizing: border-box;
}
.post-menu-title {
margin: 0 0 0.7rem;
padding-top: 0.65rem;
background-image: linear-gradient(
to right,
rgba(65, 65, 65, 0.8) 0,
rgba(65, 65, 65, 0.8) 8px,
transparent 8px,
transparent 14px
);
background-repeat: repeat-x;
background-size: 14px 2px;
background-position: top left;
font-size: 1.1rem;
font-weight: 600;
opacity: 0.72;
}
.post-menu-list {
position: relative;
list-style: none;
margin: 0;
padding: 0 0 0 0.9rem;
font-size: 1rem;
}
.post-menu-list::before {
content: "";
position: absolute;
left: 0.15rem;
top: 0.25rem;
bottom: 0.25rem;
width: 1px;
background: rgba(128, 128, 128, 0.55);
}
.post-menu-item {
position: relative;
margin: 0.42rem 0;
line-height: 1.45;
}
.post-menu-item.depth-3 {
padding-left: 0.85rem;
opacity: 0.82;
font-size: 0.94em;
}
.post-menu a {
color: inherit;
text-decoration: none;
}
.post-menu a:hover {
text-decoration: underline;
}
.post-menu-mobile-toggle {
display: none;
}
@media (min-width: 1200px) {
.post-menu {
--content-width: 90ch;
--menu-width: clamp(
140px,
calc((100vw - 1200px) * 0.42 + 140px),
260px
);
--menu-gap: 0rem;
position: fixed;
top: 8.5rem;
width: var(--menu-width);
left: calc(
50vw - var(--content-width) / 2 - var(--menu-gap) -
var(--menu-width)
);
max-height: calc(100vh - 10rem);
overflow: auto;
padding: 0.2rem 0.2rem 0.2rem 0;
opacity: 0.9;
}
}
@media (max-width: 1200px) {
.post-menu-mobile-toggle {
display: inline-flex;
align-items: center;
gap: 0.25rem;
position: fixed;
top: 0.75rem;
right: 0.8rem;
z-index: 30;
max-width: min(78vw, 22rem);
padding: 0.6rem 0.75rem;
border: 1.5px solid var(--deep-blue);
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(6px);
color: inherit;
font-size: 0.85rem;
cursor: pointer;
opacity: 0;
transform: translateY(calc(-100% - 0.6rem));
pointer-events: none;
transition:
transform 0.28s ease,
opacity 0.22s ease;
}
.post-menu-mobile-toggle.is-visible {
opacity: 1;
transform: translateY(calc(env(safe-area-inset-top) + 0.4rem));
pointer-events: auto;
}
.post-menu-mobile-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
opacity: 0.78;
}
.post-menu-mobile-preview {
display: inline-block;
width: 10rem;
/* height: 1rem; */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
.post-menu {
display: none;
position: fixed;
top: calc(env(safe-area-inset-top) + 4.2rem);
right: 0.8rem;
z-index: 29;
width: min(88vw, 24rem);
max-height: min(65vh, 32rem);
overflow: auto;
padding: 0.9rem 1rem;
border: 1.5px solid var(--deep-blue);
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(8px);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.08);
}
.post-menu-title {
padding-top: 0;
background-image: none;
}
.post-menu.is-open {
display: block;
}
:global(html.dark) .post-menu-mobile-toggle,
:global(html.dark) .post-menu {
background: var(--background-color-dark);
color: var(--text-color-dark);
}
}
</style>
<script is:inline>
(() => {
const setupPostMenu = () => {
const btn = document.querySelector(".post-menu-mobile-toggle");
const panel = document.querySelector("#post-menu-panel");
const preview = document.querySelector("[data-post-menu-preview]");
const headingLinks = Array.from(
document.querySelectorAll("[data-heading-link]"),
);
if (!btn || !panel || !preview || headingLinks.length === 0) return;
if (btn.dataset.bound === "true") return;
btn.dataset.bound = "true";
const defaultText =
preview.getAttribute("data-default-text") || "本文目录";
const getHeadingElements = () =>
headingLinks
.map((link) => {
const href = link.getAttribute("href");
if (!href || !href.startsWith("#")) return null;
const el = document.getElementById(href.slice(1));
if (!el) return null;
return {
link,
el,
text:
link.getAttribute("data-heading-text") ||
el.textContent ||
defaultText,
};
})
.filter(Boolean);
let headingItems = getHeadingElements();
const closeMenu = () => {
panel.classList.remove("is-open");
btn.setAttribute("aria-expanded", "false");
};
document.addEventListener("click", (e) => {
const target = e.target;
if (!(target instanceof Node)) return;
const clickedInsidePanel = panel.contains(target);
const clickedButton = btn.contains(target);
if (!clickedInsidePanel && !clickedButton) {
closeMenu();
}
});
const updateVisibility = () => {
if (window.innerWidth >= 1200) {
btn.classList.remove("is-visible");
closeMenu();
return;
}
const shouldShow = window.scrollY > 80;
btn.classList.toggle("is-visible", shouldShow);
if (!shouldShow) {
closeMenu();
}
};
const updateCurrentHeading = () => {
if (window.innerWidth >= 1200) return;
headingItems = getHeadingElements();
let current = null;
const triggerLine = 140;
for (const item of headingItems) {
const rect = item.el.getBoundingClientRect();
if (rect.top <= triggerLine) {
current = item;
} else {
break;
}
}
preview.textContent = current ? current.text : defaultText;
headingLinks.forEach((link) =>
link.classList.remove("is-current"),
);
if (current) current.link.classList.add("is-current");
};
const toggleMenu = () => {
const expanded = btn.getAttribute("aria-expanded") === "true";
btn.setAttribute("aria-expanded", String(!expanded));
panel.classList.toggle("is-open", !expanded);
};
btn.addEventListener("click", toggleMenu);
panel.addEventListener("click", (e) => {
const target = e.target;
if (!(target instanceof Element)) return;
const link = target.closest("a[data-heading-link]");
if (!link) return;
const href = link.getAttribute("href");
if (!href || !href.startsWith("#")) return;
const el = document.getElementById(href.slice(1));
if (!el) return;
e.preventDefault();
el.scrollIntoView({
behavior: "smooth",
block: "start",
});
history.replaceState(null, "", href);
closeMenu();
});
const onScroll = () => {
updateVisibility();
updateCurrentHeading();
};
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
updateVisibility();
updateCurrentHeading();
};
setupPostMenu();
document.addEventListener("astro:page-load", setupPostMenu);
})();
</script>

View file

@ -0,0 +1,130 @@
---
import { getCollection } from "astro:content";
import type { CollectionEntry } from "astro:content";
import type { Lang } from "@/i18n";
interface Props {
post: CollectionEntry<"blog">;
lang: Lang;
}
const { post, lang } = Astro.props;
const allPosts = await getCollection("blog");
// 只保留当前语言
const sameLangPosts = allPosts.filter((p) => {
const [postLang] = p.id.split("/");
return postLang === lang;
});
// 按日期倒序:越新越靠前
const sortedPosts = [...sameLangPosts].sort(
(a, b) =>
new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime(),
);
// 找当前文章位置
const currentIndex = sortedPosts.findIndex((p) => p.id === post.id);
// 更旧的一篇
const prevPost =
currentIndex < sortedPosts.length - 1
? sortedPosts[currentIndex + 1]
: null;
// 更新的一篇
const nextPost = currentIndex > 0 ? sortedPosts[currentIndex - 1] : null;
const getSlugFromId = (id: string) => id.split("/").slice(1).join("/");
const prevSlug = prevPost ? getSlugFromId(prevPost.id) : null;
const nextSlug = nextPost ? getSlugFromId(nextPost.id) : null;
---
<nav class="post-nav" aria-label="Post navigation">
<div class="post-nav-item post-nav-prev">
{
nextPost && nextSlug && (
<a href={`/${lang}/posts/${nextSlug}/`}>
<span class="post-nav-label">
{lang === "zh" ? "上一篇" : "Previous"}
</span>
<span class="post-nav-title">{nextPost.data.title}</span>
</a>
)
}
</div>
<div class="post-nav-item post-nav-next">
{
prevPost && prevSlug && (
<a href={`/${lang}/posts/${prevSlug}/`}>
<span class="post-nav-label">
{lang === "zh" ? "下一篇" : "Next"}
</span>
<span class="post-nav-title">{prevPost.data.title}</span>
</a>
)
}
</div>
</nav>
<style>
.post-nav {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-top: 3rem;
padding-top: 1.5rem;
padding-bottom: 1.5rem;
border-top: 1px dashed rgba(128, 128, 128, 0.5);
border-bottom: 1px dashed rgba(128, 128, 128, 0.5);
}
.post-nav-item {
min-width: 0;
}
.post-nav-next {
text-align: right;
}
.post-nav-title {
display: inline-block;
transition: transform 0.18s ease;
}
.post-nav a:hover .post-nav-title {
transform: scale(1.04);
}
.post-nav a {
display: block;
text-decoration: none;
/* color: inherit; */
}
.post-nav-label {
display: block;
font-size: 0.9rem;
opacity: 0.65;
margin-bottom: 0.35rem;
color: black;
}
.post-nav-title {
display: block;
font-size: 1rem;
line-height: 1.5;
}
@media (max-width: 768px) {
.post-nav {
grid-template-columns: 1fr;
}
.post-nav-next {
text-align: right;
}
}
</style>

View file

@ -6,7 +6,6 @@ const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
---
<div id="remark42"></div>
<script define:vars={{ pagePath, host, siteId }} is:inline data-astro-rerun>
function getTheme() {
return document.documentElement.classList.contains("dark")
@ -35,6 +34,7 @@ const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
resolve(true);
return;
}
existing.addEventListener("load", () => resolve(true), {
once: true,
});
@ -67,16 +67,42 @@ const siteId = import.meta.env.PUBLIC_REMARK42_SITE_ID;
setRemarkConfig();
await ensureScript();
if (window.REMARK42) {
if (typeof window.REMARK42.destroy === "function") {
window.REMARK42.destroy();
}
if (typeof window.REMARK42.createInstance === "function") {
node.innerHTML = "";
window.REMARK42.createInstance(window.remark_config);
}
if (!window.REMARK42) return;
if (typeof window.REMARK42.destroy === "function") {
window.REMARK42.destroy();
}
if (typeof window.REMARK42.createInstance === "function") {
node.innerHTML = "";
window.REMARK42.createInstance(window.remark_config);
}
}
function setupRemark42ThemeObserver() {
if (window.__remark42ThemeObserver) {
window.__remark42ThemeObserver.disconnect();
}
let lastTheme = getTheme();
const observer = new MutationObserver(() => {
const currentTheme = getTheme();
if (currentTheme === lastTheme) return;
lastTheme = currentTheme;
mountRemark42();
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
window.__remark42ThemeObserver = observer;
}
mountRemark42();
setupRemark42ThemeObserver();
</script>

View file

@ -15,6 +15,10 @@ export default {
writtenBy: "Written by",
comments: "Comments",
publishedOn: "Published on",
commentsDesc: [
"Feel free to leave your thoughts here. 💭💡",
"After signing in, you can click the “Subscribe by Email” button at the bottom right of the text box to receive notifications of new interactions via email."
]
},
theme: {
toggle: "Toggle theme",
@ -25,7 +29,8 @@ export default {
content: [
"These few lines speak what the heart would say; Ink and paper end, but thoughts still stay",
"Welcome to Cloverta's blog.",
]
],
latestTitle: "Latest Posts"
},
about: {
title: "About Me, and This Blog",
@ -41,6 +46,7 @@ export default {
"The design of this blog was inspired by:",
'· <a href="https://ex-tasty.com/">極限風味</a>',
'· <a href="https://blog.cloudti.de/">Parsifal\'s Blog</a>',
'· <a href="https://www.kokosa.icu/">Kokosa\'s Notebook</a>',
"· And some other websites whose names I have unfortunately forgotten",
"Thank you for your ideas and passion!",
"In addition, this blog is open source under MIT License. You can find its source code through the link in the footer.",
@ -53,5 +59,15 @@ export default {
footer: {
githubIntro: 'See more on <a href="https://github.com/ClovertaTheTrilobita">Github</a>!',
repoIntro: 'This blog is fully open source at <a href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog">ClovertaTheTrilobita/SanYeCao-blog</a>'
},
friends: {
title: "Friends",
content: [
"Thank you for making it this far.",
"These are my friends, and you're very welcome to exchange links with me too!",
'You can send your link information to <a href="mailto:cloverta@petalmail.com">my email</a>.',
"The format is as follows:"
],
imgDesc: "Our everyday lives may, in fact, be a series of miracles."
}
};

View file

@ -15,6 +15,10 @@ export default {
writtenBy: "作者",
comments: "评论",
publishedOn: "发布于",
commentsDesc: [
"欢迎在这里留下你的想法。💭💡",
"你可以在登录评论区后点击文本框右下角的「Subscribe by Email」以通过邮件接收最新的互动通知。"
]
},
theme: {
toggle: "切换主题",
@ -25,7 +29,8 @@ export default {
content: [
"见字如晤,展信舒颜。楮墨有限,不尽欲言。",
"欢迎来到三叶的博客。",
]
],
latestTitle: "最新文章"
},
about: {
title: "关于我,和这个博客",
@ -41,6 +46,7 @@ export default {
"这个博客在设计理念上参考了:",
'· <a href="https://ex-tasty.com/">極限風味</a>',
'· <a href="https://blog.cloudti.de/">Parsifal\'s Blog</a>',
'· <a href="https://www.kokosa.icu/">Kokosa\'s Notebook</a>',
"· 还有一些已经忘记名字的网站",
"谢谢你们的想法和热情!",
"此外,这个博客的源码使用 MIT 协议开源,你可以从页脚的链接处获取它的源代码。",
@ -53,5 +59,15 @@ export default {
footer: {
githubIntro: '在 <a href="https://github.com/ClovertaTheTrilobita">Github</a> 查看更多!',
repoIntro: '这个博客完全开源于 <a href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog">ClovertaTheTrilobita/SanYeCao-blog</a>'
},
friends: {
title: "友链",
content: [
"感谢你能看到这里。",
"这里是我的朋友们,也非常欢迎你来一起交换友链!",
'你可以把友链信息发送到<a href="mailto:cloverta@petalmail.com">我的邮箱</a>。',
"友链的格式如下:"
],
imgDesc: "——我们日复一日度过的日常,也许就是接连发生的奇迹"
}
};

View file

@ -2,7 +2,12 @@
import Footer from "@/components/Footer.astro";
import Header from "@/components/Header.astro";
import { ClientRouter } from "astro:transitions";
import { slide, fade } from "astro:transitions";
import { fade } from "astro:transitions";
import { getLangFromUrl } from "@/i18n";
import "katex/dist/katex.min.css";
const lang = getLangFromUrl(Astro.url);
const htmlLang = lang === "zh" ? "zh-CN" : "en-US";
import SEO from "@/components/SEO.astro";
import FloatingActions from "@/components/FloatingActions.astro";
import Spinner from "@/components/Spinner.astro";
@ -15,7 +20,7 @@ const {
---
<!doctype html>
<html lang="en">
<html lang={htmlLang}>
<head>
<ClientRouter />
<meta charset="UTF-8" />
@ -23,6 +28,16 @@ const {
<SEO title={pageTitle} description={description} image={image} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;500;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap"
rel="stylesheet"
/>
<meta name="generator" content={Astro.generator} />
<title>{pageTitle}</title>
</head>

View file

@ -1,11 +1,14 @@
---
import BaseLayout from "./BaseLayout.astro";
import Remark42Embed from "@/components/Remark42Embed.astro";
import PostNav from "@/components/Posts/PostNav.astro";
import PostMenu from "@/components/Posts/PostMenu.astro";
import Dialog from "@/components/Posts/Dialog.astro";
import { fade } from "astro:transitions";
import { getLangFromUrl, getTranslations } from "@/i18n";
import { getTranslations } from "@/i18n";
import "@/styles/global.css";
const { frontmatter, lang, slug, postId } = Astro.props;
const { post, frontmatter, lang, slug, postId, headings } = Astro.props;
const comments = lang === "zh" ? "评论区" : "comments";
const t = getTranslations(lang);
---
@ -37,6 +40,9 @@ const t = getTranslations(lang);
<path d="M15 18l-6-6 6-6"></path>
</svg>
</a>
<PostMenu headings={headings} />
<article class="post-article">
<div class="post-header">
<div class="post-meta">
@ -76,7 +82,12 @@ const t = getTranslations(lang);
<slot />
</div>
<Dialog />
<PostNav post={post} lang={lang} />
<h2>{comments}</h2>
{t.post.commentsDesc.map((line: string) => <p set:html={line} />)}
<Remark42Embed slug={slug} />
</article>
</BaseLayout>
@ -181,7 +192,7 @@ const t = getTranslations(lang);
align-items: center;
justify-content: center;
text-decoration: none;
border: #285ee9 1.5px solid;
border: #536493 1.5px solid;
background: rgba(255, 255, 255, 0.92);
color: #222;
/* box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16); */
@ -357,4 +368,88 @@ const t = getTranslations(lang);
aspect-ratio: 16 / 9;
}
}
details {
margin: 1.5rem 0;
overflow: hidden;
border-top: 2px solid #1e3a5f;
border-radius: 0 0 0.5rem 0.5rem;
background: rgba(128, 128, 128, 0.14);
}
details summary {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.8rem 0.9rem;
cursor: pointer;
font-weight: 600;
user-select: none;
list-style: none;
}
details summary::-webkit-details-marker {
display: none;
}
details summary::before {
content: "";
display: inline-block;
font-size: 1.25rem;
line-height: 1;
transition: transform 0.2s ease;
}
details[open] summary::before {
transform: rotate(90deg);
}
/* 展开内容 */
details > :not(summary) {
box-sizing: border-box;
margin-top: 0;
margin-bottom: 0;
padding-right: 1rem;
}
/* 为列表序号预留空间 */
details > ol,
details > ul {
padding: 0.6rem 1rem 1rem 3rem;
}
details li {
margin: 0.8rem 0;
}
blockquote {
margin: 1.25rem 0;
padding: 0.75rem 1rem;
border-left: 4px solid var(--deep-red);
background: rgba(128, 128, 128, 0.1);
border-radius: 0 0.4rem 0.4rem 0;
}
blockquote p {
margin: 0;
}
blockquote p + p {
margin-top: 0.75rem;
}
/* 块级公式:超出屏幕时只滚动公式,不撑宽整篇文章 */
.katex-display {
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
padding: 0.25rem 0;
-webkit-overflow-scrolling: touch;
}
/* 避免 KaTeX 内部元素强行把父容器撑宽 */
.katex-display > .katex {
min-width: max-content;
}
</style>

View file

@ -1,6 +1,7 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import FriendlyLinkList from "@/components/FriendlyLinks/FriendlyLinkList.astro";
import FriendsDescription from "@/components/FriendlyLinks/FriendsDescription.astro";
import { getLangFromUrl, getTranslations } from "@/i18n";
import "@/styles/global.css";
@ -14,5 +15,16 @@ const headerTitle = lang === "zh" ? "友情链接" : "Friends";
---
<BaseLayout pageTitle=`${headerTitle} - ${t.banner.title}`>
<FriendsDescription />
<div class="section-divider"></div>
<FriendlyLinkList />
</BaseLayout>
<style>
.section-divider {
height: 1px;
background: #cdd2d8;
margin: 1.5rem 0;
opacity: 0.8;
}
</style>

View file

@ -14,13 +14,14 @@ const lang = getLangFromUrl(Astro.url);
const t = getTranslations(lang);
const headerTitle = lang === "zh" ? "Cloverta的博客" : "Cloverta's blog";
const pageTitle = t.home.title;
const rssLink = lang === "zh" ? "/rss.xml" : "/en/rss.xml"
---
<BaseLayout pageTitle=`${headerTitle} - ${t.banner.subtitle}`>
<h1 class="page-title">
<span>{pageTitle}</span>
<a
href={`/rss.xml`}
href={rssLink}
target="_blank"
rel="noopener noreferrer"
aria-label="RSS Feed"
@ -47,6 +48,8 @@ const pageTitle = t.home.title;
<div class="section-divider"></div>
<h2>{t.home.latestTitle}</h2>
<PostList />
<div class="section-divider"></div>

View file

@ -22,17 +22,19 @@ export async function getStaticPaths() {
}
const { post, lang } = Astro.props;
const { Content } = await render(post);
const { Content, headings } = await render(post);
const [postLang, ...slugParts] = post.id.split("/");
const slug = slugParts.join("/");
---
<MarkdownPostLayout
post={post}
frontmatter={post.data}
lang={lang}
slug={slug}
postId={post.id}
headings={headings}
>
<Content />
</MarkdownPostLayout>

35
src/pages/en/rss.xml.js Normal file
View file

@ -0,0 +1,35 @@
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection('blog');
const enPosts = posts
.filter((post) => post.id.startsWith('en/'))
.sort(
(a, b) =>
new Date(b.data.pubDate).getTime() -
new Date(a.data.pubDate).getTime()
);
return rss({
title: "Cloverta's Blog",
description:
"Discover more here. Welcome to Cloverta's blog 🥳",
site: context.site,
items: enPosts.map((post) => {
const [, ...slugParts] = post.id.split('/');
const slug = slugParts.join('/');
return {
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/en/posts/${slug}/`,
};
}),
customData: '<language>en-US</language>',
});
}

View file

@ -1,26 +1,34 @@
import rss from '@astrojs/rss';
import { pagesGlobToRssItems } from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection("blog");
const posts = await getCollection('blog');
const zhPosts = posts
.filter((post) => post.id.startsWith('zh/'))
.sort(
(a, b) =>
new Date(b.data.pubDate).getTime() -
new Date(a.data.pubDate).getTime()
);
return rss({
title: 'Cloverta的博客',
description: '在这里,发现更多(雾)欢迎来到三叶的博客🥳',
site: context.site,
items: await pagesGlobToRssItems(import.meta.glob('./**/*.md')),
items: posts.map((post) => {
const [postLang, ...slugParts] = post.id.split("/");
const slug = slugParts.join("/");
return ({
items: zhPosts.map((post) => {
const [, ...slugParts] = post.id.split('/');
const slug = slugParts.join('/');
return {
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/${postLang}/posts/${slug}/`,
})
link: `/zh/posts/${slug}/`,
};
}),
customData: `<language>en-us</language>`,
})
customData: '<language>zh-CN</language>',
});
}

95
src/styles/dialog.css Normal file
View file

@ -0,0 +1,95 @@
.image-preview-dialog {
padding: 0;
border: none;
background: transparent;
max-width: min(96vw, 1200px);
max-height: 96vh;
}
.image-preview-dialog::backdrop {
background: rgba(0, 0, 0, 0.72);
backdrop-filter: blur(6px);
}
.image-preview-dialog img {
display: block;
max-width: 96vw;
max-height: 90vh;
object-fit: contain;
box-shadow: 0 20px 80px rgba(0, 0, 0, 0.45);
}
.image-preview-close:focus {
outline: none;
}
.image-preview-close:focus-visible {
outline: 2px solid rgba(125, 162, 255, 0.9);
outline-offset: 4px;
}
.image-preview-close {
position: fixed;
top: 1.25rem;
right: 1.25rem;
z-index: 1;
width: 2.4rem;
height: 2.4rem;
border: none;
border-radius: 999px;
background: transparent;
color: var(--text-color-dark);
font-size: 2.6rem;
line-height: 1;
cursor: pointer;
}
/* .image-preview-close:hover {
background: rgba(0, 0, 0, 0.75);
} */
/* 给文章图片一个可点击提示 */
.prose img,
.markdown-body img,
article img {
cursor: zoom-in;
}
.image-preview-download {
position: fixed;
left: 50%;
bottom: max(1.5rem, env(safe-area-inset-bottom));
transform: translateX(-50%);
z-index: 2;
display: inline-flex;
align-items: center;
justify-content: center;
color: white;
background: transparent;
border: none;
padding: 0.4rem;
cursor: pointer;
text-decoration: none;
opacity: 0.88;
transition:
opacity 0.2s ease,
transform 0.2s ease;
}
.image-preview-download:hover {
opacity: 1;
transform: translateX(-50%) translateY(-2px);
}
.image-preview-download:focus {
outline: none;
}
.image-preview-download:focus-visible {
outline: 2px solid rgba(125, 162, 255, 0.9);
outline-offset: 4px;
border-radius: 8px;
}

View file

@ -1,6 +1,9 @@
@import url("https://unpkg.com/@fontsource/maple-mono@5.2.6/400.css");
@import url("https://unpkg.com/@fontsource/maple-mono@5.2.6/400-italic.css");
@import url("https://unpkg.com/@fontsource/maple-mono@5.2.6/700.css");
@import "./latest-comments.css";
@import "./variables.css";
@import "./dialog.css";
pre {
padding: 1rem;
@ -34,11 +37,17 @@ article svg[id^="mermaid-"] {
html {
/* font-family: "Maple Mono", "Maple Mono CN", monospace; */
font-family: 'Noto Serif SC', serif;
font-family:
"Noto Serif SC",
"Source Han Serif SC",
"Songti SC",
"STSong",
"SimSun",
serif;
font-weight: 500;
/* background-color: #ffffff; */
background-color: #F9F2ED;
color: #0E2F56;
background-color: var(--background-color);
color: var(--text-color);
}
html {
@ -154,266 +163,110 @@ img {
display: block;
}
.latest-comments {
margin-top: 1.5rem;
}
.latest-comments h2 {
margin-bottom: 0.8rem;
}
.loading-card {
height: 92px;
margin: 0 0 14px 0;
padding: 0.9rem 1rem;
border: 1px dashed #aeb8c2;
background: linear-gradient(90deg,
rgba(160, 175, 190, 0.06) 25%,
rgba(160, 175, 190, 0.16) 50%,
rgba(160, 175, 190, 0.06) 75%);
background-size: 200% 100%;
animation: shimmer 1.2s infinite linear;
}
@keyframes shimmer {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
.latest-comments-list {
display: block;
}
.comment-card {
display: block;
margin: 0 0 14px 0;
padding: 0.6rem 1rem;
border: 2px dashed #aeb8c2;
background-color: #fbf5f2;
}
.comment-card:last-child {
margin-bottom: 0;
}
.comment-card-body {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
grid-template-areas:
"info title"
"text title";
gap: 0.2rem 1rem;
align-items: start;
}
.comment-card-info {
grid-area: info;
display: flex;
align-items: center;
gap: 0.7rem;
/* 避免 Markdown 内容撑宽正文 */
.post-article,
.post-content {
min-width: 0;
}
.comment-avatar {
width: 35px;
height: 35px;
flex-shrink: 0;
}
.comment-avatar-img,
.comment-avatar-fallback {
width: 35px;
height: 35px;
display: block;
border-radius: 50%;
object-fit: cover;
}
.comment-avatar-img {
border: 1px dashed #aeb8c2;
background: #f3f5f7;
}
.comment-avatar-fallback {
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #aeb8c2;
background: rgba(160, 175, 190, 0.12);
color: #5e6b77;
font-weight: 700;
font-size: 1rem;
user-select: none;
}
.comment-meta {
min-width: 0;
flex: 1;
}
.comment-author-row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.6rem;
min-width: 0;
line-height: 1.3;
font-style: italic;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"PingFang SC",
"Hiragino Sans GB",
"Microsoft YaHei",
"Noto Sans CJK SC",
"Source Han Sans SC",
sans-serif;
}
.comment-author {
font-weight: 400;
font-size: 0.8rem;
color: gray;
overflow-wrap: anywhere;
word-break: break-word;
}
.comment-time {
color: #7a7a7a;
font-size: 0.7rem;
}
.comment-card-title {
grid-area: title;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
text-align: right;
min-width: 0;
}
.comment-title-link {
color: #6f8090;
text-decoration: none;
font-size: 1.1rem;
font-weight: 700;
line-height: 1.45;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.comment-title-link:hover {
text-decoration: underline;
}
.comment-card-text {
grid-area: text;
margin: 0;
color: #555;
line-height: 1.5;
font-size: 0.93rem;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
white-space: normal;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"PingFang SC",
"Hiragino Sans GB",
"Microsoft YaHei",
"Noto Sans CJK SC",
"Source Han Sans SC",
sans-serif;
}
.comment-card-text :global(p),
.comment-card-text p {
margin: 0;
}
.comment-card-text :global(img),
.comment-card-text img {
max-width: 100%;
height: auto;
}
.latest-comments-empty {
color: #777;
font-style: italic;
/* 块级公式过长时独立横向滚动 */
.post-content .katex-display {
display: block;
width: 100%;
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
box-sizing: border-box;
padding: 0.25rem 0;
-webkit-overflow-scrolling: touch;
}
.comment-reply-sep {
font-size: 0.8rem;
opacity: 0.7;
.post-content .katex-display > .katex {
display: inline-block;
min-width: max-content;
white-space: nowrap;
}
.latest-comments .comment-reply-to {
font-weight: 400;
font-size: 0.8rem;
color: gray;
opacity: 0.9;
overflow-wrap: anywhere;
word-break: break-word;
/* =========================
Markdown details 下拉栏
========================= */
.post-content details {
min-width: 0;
max-width: 100%;
box-sizing: border-box;
margin: 1.5rem 0;
overflow: hidden;
border-top: 2px solid #1e3a5f;
border-radius: 0 0 0.5rem 0.5rem;
background: rgba(128, 128, 128, 0.14);
}
html.dark .latest-comments .comment-card,
.dark .latest-comments .comment-card {
border-color: #7f8c97;
background-color: #252525;
.post-content details summary {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
padding: 0.8rem 0.9rem;
cursor: pointer;
font-weight: 600;
user-select: none;
list-style: none;
}
html.dark .comment-title-link {
color: #c8d2dc;
.post-content details summary::-webkit-details-marker {
display: none;
}
html.dark .comment-card-text {
color: #d3d7db;
.post-content details summary::before {
content: "";
display: inline-block;
flex: 0 0 auto;
font-size: 1.25rem;
line-height: 1;
transition: transform 0.2s ease;
}
html.dark .comment-time {
color: #a8b0b7;
.post-content details[open] summary::before {
transform: rotate(90deg);
}
html.dark .latest-comments .comment-avatar-fallback,
.dark .latest-comments .comment-avatar-fallback {
border-color: #7f8c97;
background-color: #3a444d;
color: #dbe3ea;
.post-content details > :not(summary) {
min-width: 0;
max-width: 100%;
box-sizing: border-box;
margin-top: 0;
margin-bottom: 0;
padding-right: 1rem;
}
:global(.dark) .comment-avatar-fallback {
background: rgba(180, 190, 200, 0.12);
color: #dbe3ea;
.post-content details > ol,
.post-content details > ul {
padding: 0.6rem 1rem 1rem 3rem;
}
@media (max-width: 640px) {
.comment-card-body {
grid-template-columns: 1fr;
grid-template-areas:
"title"
"info"
"text";
}
.post-content details li {
margin: 0.8rem 0;
}
.comment-card-title {
text-align: left;
margin-bottom: 0.2rem;
}
blockquote {
margin: 1.25rem 0;
padding: 0.75rem 1rem;
border-left: 4px solid var(--deep-red);
background: rgba(128, 128, 128, 0.1);
border-radius: 0 0.4rem 0.4rem 0;
}
.comment-title-link {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
blockquote p {
margin: 0;
}
blockquote p + p {
margin-top: 0.75rem;
}

View file

@ -0,0 +1,263 @@
.latest-comments {
margin-top: 1.5rem;
}
.latest-comments h2 {
margin-bottom: 0.8rem;
}
.loading-card {
height: 92px;
margin: 0 0 14px 0;
padding: 0.9rem 1rem;
border: 1px dashed #aeb8c2;
background: linear-gradient(90deg,
rgba(160, 175, 190, 0.06) 25%,
rgba(160, 175, 190, 0.16) 50%,
rgba(160, 175, 190, 0.06) 75%);
background-size: 200% 100%;
animation: shimmer 1.2s infinite linear;
}
@keyframes shimmer {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
.latest-comments-list {
display: block;
}
.comment-card {
display: block;
margin: 0 0 14px 0;
padding: 0.6rem 1rem;
border: 2px dashed #aeb8c2;
background-color: #fbf5f2;
}
.comment-card:last-child {
margin-bottom: 0;
}
.comment-card-body {
display: grid;
grid-template-columns: minmax(0, 1fr) 320px;
grid-template-areas:
"info title"
"text title";
gap: 0.2rem 1rem;
align-items: start;
}
.comment-card-info {
grid-area: info;
display: flex;
align-items: center;
gap: 0.7rem;
min-width: 0;
}
.comment-avatar {
width: 35px;
height: 35px;
flex-shrink: 0;
}
.comment-avatar-img,
.comment-avatar-fallback {
width: 35px;
height: 35px;
display: block;
border-radius: 50%;
object-fit: cover;
}
.comment-avatar-img {
border: 1px dashed #aeb8c2;
background: #f3f5f7;
}
.comment-avatar-fallback {
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #aeb8c2;
background: rgba(160, 175, 190, 0.12);
color: #5e6b77;
font-weight: 700;
font-size: 1rem;
user-select: none;
}
.comment-meta {
min-width: 0;
flex: 1;
}
.comment-author-row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.6rem;
min-width: 0;
line-height: 1.3;
font-style: italic;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"PingFang SC",
"Hiragino Sans GB",
"Microsoft YaHei",
"Noto Sans CJK SC",
"Source Han Sans SC",
sans-serif;
}
.comment-author {
font-weight: 400;
font-size: 0.8rem;
color: gray;
overflow-wrap: anywhere;
word-break: break-word;
}
.comment-time {
color: #7a7a7a;
font-size: 0.7rem;
}
.comment-card-title {
grid-area: title;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
text-align: right;
min-width: 0;
}
.comment-title-link {
color: #6f8090;
text-decoration: none;
font-size: 1.1rem;
font-weight: 700;
line-height: 1.45;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.comment-title-link:hover {
text-decoration: underline;
}
.comment-card-text {
grid-area: text;
margin: 0;
color: #555;
line-height: 1.5;
font-size: 0.93rem;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
white-space: normal;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"PingFang SC",
"Hiragino Sans GB",
"Microsoft YaHei",
"Noto Sans CJK SC",
"Source Han Sans SC",
sans-serif;
}
.comment-card-text :global(p),
.comment-card-text p {
margin: 0;
}
.comment-card-text :global(img),
.comment-card-text img {
max-width: 100%;
height: auto;
}
.latest-comments-empty {
color: #777;
font-style: italic;
}
.comment-reply-sep {
font-size: 0.8rem;
opacity: 0.7;
}
.latest-comments .comment-reply-to {
font-weight: 400;
font-size: 0.8rem;
color: gray;
opacity: 0.9;
overflow-wrap: anywhere;
word-break: break-word;
}
html.dark .latest-comments .comment-card,
.dark .latest-comments .comment-card {
border-color: #7f8c97;
background-color: #252525;
}
html.dark .comment-title-link {
color: #c8d2dc;
}
html.dark .comment-card-text {
color: #d3d7db;
}
html.dark .comment-time {
color: #a8b0b7;
}
html.dark .latest-comments .comment-avatar-fallback,
.dark .latest-comments .comment-avatar-fallback {
border-color: #7f8c97;
background-color: #3a444d;
color: #dbe3ea;
}
:global(.dark) .comment-avatar-fallback {
background: rgba(180, 190, 200, 0.12);
color: #dbe3ea;
}
@media (max-width: 640px) {
.comment-card-body {
grid-template-columns: 1fr;
grid-template-areas:
"title"
"info"
"text";
}
.comment-card-title {
text-align: left;
margin-bottom: 0.2rem;
}
.comment-title-link {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}

9
src/styles/variables.css Normal file
View file

@ -0,0 +1,9 @@
:root {
--deep-blue: #536493;
--deep-red: #ef5a6f;
--background-color: #f9f2ed;
--text-color: #0E2F56;
--background-color-dark: #1e1e1e;
--text-color-dark: #e6e6e6;
}