Merge branch 'saicaca:main' into feat/gitlab-card

This commit is contained in:
noeFly
2025-11-29 23:26:39 +08:00
committed by GitHub
35 changed files with 2570 additions and 2160 deletions
+2 -2
View File
@@ -75,8 +75,8 @@ onMount(async () => {
);
const groupedPostsArray = Object.keys(grouped).map((yearStr) => ({
year: Number.parseInt(yearStr),
posts: grouped[Number.parseInt(yearStr)],
year: Number.parseInt(yearStr, 10),
posts: grouped[Number.parseInt(yearStr, 10)],
}));
groupedPostsArray.sort((a, b) => b.year - a.year);
+1 -1
View File
@@ -33,7 +33,7 @@ const { badge, url, label } = Astro.props;
{ badge !== undefined && badge !== null && badge !== '' &&
<div class="transition px-2 h-7 ml-4 min-w-[2rem] rounded-lg text-sm font-bold
text-[var(--btn-content)] dark:text-[var(--deep-text)]
bg-[oklch(0.95_0.025_var(--hue))] dark:bg-[var(--primary)]
bg-[var(--btn-regular-bg)] dark:bg-[var(--primary)]
flex items-center justify-center">
{ badge }
</div>
+1 -1
View File
@@ -23,7 +23,7 @@ $: if (hue || hue === 0) {
before:absolute before:-left-3 before:top-[0.33rem]"
>
{i18n(I18nKey.themeColor)}
<button aria-label="Reset to Default" class="btn-regular w-7 h-7 rounded-md active:scale-90"
<button aria-label="Reset to Default" class="btn-regular w-7 h-7 rounded-md active:scale-90 will-change-transform"
class:opacity-0={hue === defaultHue} class:pointer-events-none={hue === defaultHue} on:click={resetHue}>
<div class="text-[var(--btn-content)]">
<Icon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"></Icon>
+1 -1
View File
@@ -22,7 +22,7 @@ const config = profileConfig;
<div class="font-bold text-xl text-center mb-1 dark:text-neutral-50 transition">{config.name}</div>
<div class="h-1 w-5 bg-[var(--primary)] mx-auto rounded-full mb-2 transition"></div>
<div class="text-center text-neutral-400 mb-2.5 transition">{config.bio}</div>
<div class="flex gap-2 justify-center mb-1">
<div class="flex flex-wrap gap-2 justify-center mb-1">
{config.links.length > 1 && config.links.map(item =>
<a rel="me" aria-label={item.name} href={item.url} target="_blank" class="btn-regular rounded-lg h-10 w-10 active:scale-90">
<Icon name={item.icon} class="text-[1.5rem]"></Icon>
+13 -9
View File
@@ -1,6 +1,7 @@
---
import type { MarkdownHeading } from "astro";
import { siteConfig } from "../../config";
import { url } from "../../utils/url-utils";
interface Props {
class?: string;
@@ -15,8 +16,7 @@ for (const heading of headings) {
}
const className = Astro.props.class;
const isPostsRoute = Astro.url.pathname.startsWith("/posts/");
const isPostsRoute = Astro.url.pathname.startsWith(url("/posts/"));
const removeTailingHash = (text: string) => {
let lastIndexOfHash = text.lastIndexOf("#");
@@ -55,7 +55,7 @@ const maxLevel = siteConfig.toc.depth;
}]}>{removeTailingHash(heading.text)}</div>
</a>
)}
<div id="active-indicator" class:list={[{'hidden': headings.length == 0}, "-z-10 absolute bg-[var(--toc-btn-hover)] left-0 right-0 rounded-xl transition-all " +
<div id="active-indicator" style="opacity: 0" class:list={[{'hidden': headings.length == 0}, "-z-10 absolute bg-[var(--toc-btn-hover)] left-0 right-0 rounded-xl transition-all " +
"group-hover:bg-transparent border-2 border-[var(--toc-btn-hover)] group-hover:border-[var(--toc-btn-active)] border-dashed"]}></div>
</table-of-contents>}
@@ -97,7 +97,7 @@ class TableOfContents extends HTMLElement {
toggleActiveHeading = () => {
let i = this.active.length - 1;
let min = this.active.length - 1, max = 0;
let min = this.active.length - 1, max = -1;
while (i >= 0 && !this.active[i]) {
this.tocEntries[i].classList.remove(this.visibleClass);
i--;
@@ -112,11 +112,15 @@ class TableOfContents extends HTMLElement {
this.tocEntries[i].classList.remove(this.visibleClass);
i--;
}
let parentOffset = this.tocEl?.getBoundingClientRect().top || 0;
let scrollOffset = this.tocEl?.scrollTop || 0;
let top = this.tocEntries[min].getBoundingClientRect().top - parentOffset + scrollOffset;
let bottom = this.tocEntries[max].getBoundingClientRect().bottom - parentOffset + scrollOffset;
this.activeIndicator?.setAttribute("style", `top: ${top}px; height: ${bottom - top}px`);
if (min > max) {
this.activeIndicator?.setAttribute("style", `opacity: 0`);
} else {
let parentOffset = this.tocEl?.getBoundingClientRect().top || 0;
let scrollOffset = this.tocEl?.scrollTop || 0;
let top = this.tocEntries[min].getBoundingClientRect().top - parentOffset + scrollOffset;
let bottom = this.tocEntries[max].getBoundingClientRect().bottom - parentOffset + scrollOffset;
this.activeIndicator?.setAttribute("style", `top: ${top}px; height: ${bottom - top}px`);
}
};
scrollToActiveHeading = () => {
+1 -1
View File
@@ -10,7 +10,7 @@ import { LinkPreset } from "./types/config";
export const siteConfig: SiteConfig = {
title: "Fuwari",
subtitle: "Demo Site",
lang: "en", // 'en', 'zh_CN', 'zh_TW', 'ja', 'ko', 'es', 'th'
lang: "en", // Language code, e.g. 'en', 'zh_CN', 'ja', etc.
themeColor: {
hue: 250, // Default hue for the theme color, from 0 to 360. e.g. red: 0, teal: 200, cyan: 250, pink: 345
fixed: false, // Hide the theme color picker for visitors
+4
View File
@@ -19,6 +19,10 @@ const postsCollection = defineCollection({
nextSlug: z.string().default(""),
}),
});
const specCollection = defineCollection({
schema: z.object({}),
});
export const collections = {
posts: postsCollection,
spec: specCollection,
};
+11
View File
@@ -93,4 +93,15 @@ This is a note with a custom title.
> [!TIP]
> The GitHub syntax is also supported.
```
### Spoiler
You can add spoilers to your text. The text also supports **Markdown** syntax.
The content :spoiler[is hidden **ayyy**]!
```markdown
The content :spoiler[is hidden **ayyy**]!
```
+38
View File
@@ -0,0 +1,38 @@
import Key from "../i18nKey";
import type { Translation } from "../translation";
export const id: Translation = {
[Key.home]: "Beranda",
[Key.about]: "Tentang",
[Key.archive]: "Arsip",
[Key.search]: "Cari",
[Key.tags]: "Tag",
[Key.categories]: "Kategori",
[Key.recentPosts]: "Postingan Terbaru",
[Key.comments]: "Komentar",
[Key.untitled]: "Tanpa Judul",
[Key.uncategorized]: "Tanpa Kategori",
[Key.noTags]: "Tanpa Tag",
[Key.wordCount]: "kata",
[Key.wordsCount]: "kata",
[Key.minuteCount]: "menit",
[Key.minutesCount]: "menit",
[Key.postCount]: "postingan",
[Key.postsCount]: "postingan",
[Key.themeColor]: "Warna Tema",
[Key.lightMode]: "Terang",
[Key.darkMode]: "Gelap",
[Key.systemMode]: "Sistem",
[Key.more]: "Lainnya",
[Key.author]: "Penulis",
[Key.publishedAt]: "Diterbitkan pada",
[Key.license]: "Lisensi",
};
+38
View File
@@ -0,0 +1,38 @@
import Key from "../i18nKey";
import type { Translation } from "../translation";
export const tr: Translation = {
[Key.home]: "Anasayfa",
[Key.about]: "Hakkında",
[Key.archive]: "Arşiv",
[Key.search]: "Ara",
[Key.tags]: "Taglar",
[Key.categories]: "Katagoriler",
[Key.recentPosts]: "Son Paylaşımlar",
[Key.comments]: "Yorumlar",
[Key.untitled]: "Başlıksız",
[Key.uncategorized]: "Katagorisiz",
[Key.noTags]: "Tag Bulunamadı",
[Key.wordCount]: "kelime",
[Key.wordsCount]: "kelime",
[Key.minuteCount]: "dakika",
[Key.minutesCount]: "dakika",
[Key.postCount]: "gönderi",
[Key.postsCount]: "gönderiler",
[Key.themeColor]: "Tema Rengi",
[Key.lightMode]: "Aydınlık",
[Key.darkMode]: "Koyu",
[Key.systemMode]: "Sistem",
[Key.more]: "Daha Fazla",
[Key.author]: "Yazar",
[Key.publishedAt]: "Yayınlanma:",
[Key.license]: "Lisans",
};
+38
View File
@@ -0,0 +1,38 @@
import Key from "../i18nKey";
import type { Translation } from "../translation";
export const vi: Translation = {
[Key.home]: "Trang chủ",
[Key.about]: "Giới thiệu",
[Key.archive]: "Kho bài",
[Key.search]: "Tìm kiếm",
[Key.tags]: "Thẻ",
[Key.categories]: "Danh mục",
[Key.recentPosts]: "Bài viết mới nhất",
[Key.comments]: "Bình luận",
[Key.untitled]: "Không tiêu đề",
[Key.uncategorized]: "Chưa phân loại",
[Key.noTags]: "Chưa có thẻ",
[Key.wordCount]: "từ",
[Key.wordsCount]: "từ",
[Key.minuteCount]: "phút đọc",
[Key.minutesCount]: "phút đọc",
[Key.postCount]: "bài viết",
[Key.postsCount]: "bài viết",
[Key.themeColor]: "Màu giao diện",
[Key.lightMode]: "Sáng",
[Key.darkMode]: "Tối",
[Key.systemMode]: "Hệ thống",
[Key.more]: "Thêm",
[Key.author]: "Tác giả",
[Key.publishedAt]: "Đăng vào lúc",
[Key.license]: "Giấy phép bản quyền",
};
+8
View File
@@ -2,9 +2,12 @@ import { siteConfig } from "../config";
import type I18nKey from "./i18nKey";
import { en } from "./languages/en";
import { es } from "./languages/es";
import { id } from "./languages/id";
import { ja } from "./languages/ja";
import { ko } from "./languages/ko";
import { th } from "./languages/th";
import { tr } from "./languages/tr";
import { vi } from "./languages/vi";
import { zh_CN } from "./languages/zh_CN";
import { zh_TW } from "./languages/zh_TW";
@@ -28,6 +31,11 @@ const map: { [key: string]: Translation } = {
ko_kr: ko,
th: th,
th_th: th,
vi: vi,
vi_vn: vi,
id: id,
tr: tr,
tr_tr: tr,
};
export function getTranslation(lang: string): Translation {
+3 -3
View File
@@ -3,12 +3,12 @@ import ArchivePanel from "@components/ArchivePanel.svelte";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro";
import { getSortedPosts } from "../utils/content-utils";
import { getSortedPostsList } from "../utils/content-utils";
const sortedPosts = await getSortedPosts();
const sortedPostsList = await getSortedPostsList();
---
<MainGridLayout title={i18n(I18nKey.archive)}>
<ArchivePanel sortedPosts={sortedPosts} client:only="svelte"></ArchivePanel>
<ArchivePanel sortedPosts={sortedPostsList} client:only="svelte"></ArchivePanel>
</MainGridLayout>
+2 -1
View File
@@ -1,5 +1,6 @@
import rss from "@astrojs/rss";
import { getSortedPosts } from "@utils/content-utils";
import { url } from "@utils/url-utils";
import type { APIContext } from "astro";
import MarkdownIt from "markdown-it";
import sanitizeHtml from "sanitize-html";
@@ -30,7 +31,7 @@ export async function GET(context: APIContext) {
title: post.data.title,
pubDate: post.data.published,
description: post.data.description || "",
link: `/posts/${post.slug}/`,
link: url(`/posts/${post.slug}/`),
content: sanitizeHtml(parser.render(cleanedContent), {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
}),
@@ -6,7 +6,7 @@ import { definePlugin } from "@expressive-code/core";
export function pluginLanguageBadge() {
return definePlugin({
name: "Language Badge",
// @ts-ignore
// @ts-expect-error
baseStyles: ({ _cssVar }) => `
[data-language]::before {
position: absolute;
@@ -15,6 +15,7 @@ export function pluginLanguageBadge() {
top: 0.5rem;
padding: 0.1rem 0.5rem;
content: attr(data-language);
font-family: "JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.75rem;
font-weight: bold;
text-transform: uppercase;
+8 -2
View File
@@ -1,3 +1,9 @@
.expressive-code .frame {
@apply !shadow-none;
.expressive-code {
.frame {
@apply !shadow-none;
}
.title {
font-family: "JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
}
+11
View File
@@ -139,3 +139,14 @@
.collapsed {
height: var(--collapsedHeight);
}
.custom-md spoiler {
@apply bg-[var(--codeblock-bg)] hover:bg-transparent px-1 py-0.5 overflow-hidden rounded-md transition-all duration-150;
&:not(:hover) {
color: var(--codeblock-bg);
* {
color: var(--codeblock-bg);
}
}
}
+1
View File
@@ -24,6 +24,7 @@
underline decoration-[var(--link-underline)] decoration-1 decoration-dashed underline-offset-4;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
display: inline-block;
&:hover, &:active {
@apply decoration-transparent;
+2 -2
View File
@@ -90,8 +90,8 @@ define({
--admonitions-color-warning: oklch(0.7 0.14 60) oklch(0.75 0.14 60)
--admonitions-color-caution: oklch(0.6 0.2 25) oklch(0.65 0.2 25)
--toc-badge-bg: oklch(0.9 0.045 var(--hue)) var(--btn-regular-bg)
--toc-btn-hover: oklch(0.92 0.015 var(--hue)) oklch(0.22 0.02 var(--hue))
--toc-badge-bg: oklch(0.89 0.050 var(--hue)) var(--btn-regular-bg)
--toc-btn-hover: oklch(0.926 0.015 var(--hue)) oklch(0.22 0.02 var(--hue))
--toc-btn-active: oklch(0.90 0.015 var(--hue)) oklch(0.25 0.02 var(--hue))
--toc-width: calc((100vw - var(--page-width)) / 2 - 1rem)
--toc-item-active: oklch(0.70 0.13 var(--hue)) oklch(0.35 0.07 var(--hue))
+11 -1
View File
@@ -4,7 +4,17 @@ export type SiteConfig = {
title: string;
subtitle: string;
lang: string;
lang:
| "en"
| "zh_CN"
| "zh_TW"
| "ja"
| "ko"
| "es"
| "th"
| "vi"
| "tr"
| "id";
themeColor: {
hue: number;
+25 -5
View File
@@ -1,9 +1,10 @@
import { getCollection } from "astro:content";
import { type CollectionEntry, getCollection } from "astro:content";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import { getCategoryUrl } from "@utils/url-utils.ts";
export async function getSortedPosts() {
// // Retrieve posts and sort them by publication date
async function getRawSortedPosts() {
const allBlogPosts = await getCollection("posts", ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true;
});
@@ -13,6 +14,11 @@ export async function getSortedPosts() {
const dateB = new Date(b.data.published);
return dateA > dateB ? -1 : 1;
});
return sorted;
}
export async function getSortedPosts() {
const sorted = await getRawSortedPosts();
for (let i = 1; i < sorted.length; i++) {
sorted[i].data.nextSlug = sorted[i - 1].slug;
@@ -25,7 +31,21 @@ export async function getSortedPosts() {
return sorted;
}
export type PostForList = {
slug: string;
data: CollectionEntry<"posts">["data"];
};
export async function getSortedPostsList(): Promise<PostForList[]> {
const sortedFullPosts = await getRawSortedPosts();
// delete post.body
const sortedPostsList = sortedFullPosts.map((post) => ({
slug: post.slug,
data: post.data,
}));
return sortedPostsList;
}
export type Tag = {
name: string;
count: number;
@@ -37,8 +57,8 @@ export async function getTagList(): Promise<Tag[]> {
});
const countMap: { [key: string]: number } = {};
allBlogPosts.map((post: { data: { tags: string[] } }) => {
post.data.tags.map((tag: string) => {
allBlogPosts.forEach((post: { data: { tags: string[] } }) => {
post.data.tags.forEach((tag: string) => {
if (!countMap[tag]) countMap[tag] = 0;
countMap[tag]++;
});
@@ -63,7 +83,7 @@ export async function getCategoryList(): Promise<Category[]> {
return import.meta.env.PROD ? data.draft !== true : true;
});
const count: { [key: string]: number } = {};
allBlogPosts.map((post: { data: { category: string | null } }) => {
allBlogPosts.forEach((post: { data: { category: string | null } }) => {
if (!post.data.category) {
const ucKey = i18n(I18nKey.uncategorized);
count[ucKey] = count[ucKey] ? count[ucKey] + 1 : 1;
+2 -2
View File
@@ -10,12 +10,12 @@ import type { LIGHT_DARK_MODE } from "@/types/config";
export function getDefaultHue(): number {
const fallback = "250";
const configCarrier = document.getElementById("config-carrier");
return Number.parseInt(configCarrier?.dataset.hue || fallback);
return Number.parseInt(configCarrier?.dataset.hue || fallback, 10);
}
export function getHue(): number {
const stored = localStorage.getItem("hue");
return stored ? Number.parseInt(stored) : getDefaultHue();
return stored ? Number.parseInt(stored, 10) : getDefaultHue();
}
export function setHue(hue: number): void {