├── .prettierignore ├── public ├── images │ ├── cc-by.png │ ├── cc-by-nc.png │ ├── cc-by-nd.png │ ├── cc-by-sa.png │ ├── cc-by-nc-nd.png │ └── cc-by-nc-sa.png └── favicon.svg ├── src ├── content │ ├── posts │ │ ├── helloguys.jpg │ │ ├── Hello World.jpg │ │ ├── helloworld.jpeg │ │ ├── markdown-showcase.md │ │ ├── YuiToast document.md │ │ └── HelloWorldDoc.md │ └── snippets │ │ ├── Test Code Features.md │ │ └── Enlace Starter.md ├── lib │ └── utils.ts ├── utils │ ├── string.ts │ ├── theme.ts │ └── lastModified.ts ├── pages │ ├── index.astro │ ├── snippets │ │ ├── [...slug].astro │ │ └── index.astro │ ├── posts │ │ ├── [...slug].astro │ │ └── index.astro │ └── about.mdx ├── siteConfig.ts ├── components │ ├── ui │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── input.tsx │ │ ├── switch.tsx │ │ ├── avatar.tsx │ │ ├── checkbox.tsx │ │ ├── badge.tsx │ │ ├── popover.tsx │ │ ├── card.tsx │ │ ├── tooltip.tsx │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── command.tsx │ │ └── navigation-menu.tsx │ ├── CommonHeader.astro │ ├── site-nav.tsx │ ├── theme-switcher.tsx │ ├── TopLoaderVT.astro │ ├── Prose.astro │ ├── footer.tsx │ ├── indicator-text.tsx │ ├── mobile-header.tsx │ ├── sidebar-nav.tsx │ └── content-filter-button.tsx ├── layouts │ ├── Pages.astro │ ├── Main.astro │ └── Article.astro ├── content.config.ts └── styles │ └── global.css ├── .prettierrc.mjs ├── vercel.json ├── .cursor ├── mcp.json └── rules │ ├── astro-development.mdc │ ├── content-authoring.mdc │ ├── overview.mdc │ ├── shadcn-components.mdc │ └── site-config.mdc ├── .rulesync ├── .mcp.json └── rules │ ├── astro-development.md │ ├── content-authoring.md │ ├── overview.md │ ├── shadcn-components.md │ └── site-config.md ├── tsconfig.json ├── rulesync.jsonc ├── .gitignore ├── components.json ├── .codex └── memories │ ├── astro-development.md │ ├── content-authoring.md │ ├── shadcn-components.md │ └── site-config.md ├── astro.config.mjs ├── plugin ├── remark-reading-time.ts └── remark-modified-time.ts ├── package.json ├── AGENTS.md └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | stella-old/ -------------------------------------------------------------------------------- /public/images/cc-by.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jctaoo/stella/HEAD/public/images/cc-by.png -------------------------------------------------------------------------------- /public/images/cc-by-nc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jctaoo/stella/HEAD/public/images/cc-by-nc.png -------------------------------------------------------------------------------- /public/images/cc-by-nd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jctaoo/stella/HEAD/public/images/cc-by-nd.png -------------------------------------------------------------------------------- /public/images/cc-by-sa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jctaoo/stella/HEAD/public/images/cc-by-sa.png -------------------------------------------------------------------------------- /public/images/cc-by-nc-nd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jctaoo/stella/HEAD/public/images/cc-by-nc-nd.png -------------------------------------------------------------------------------- /public/images/cc-by-nc-sa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jctaoo/stella/HEAD/public/images/cc-by-nc-sa.png -------------------------------------------------------------------------------- /src/content/posts/helloguys.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jctaoo/stella/HEAD/src/content/posts/helloguys.jpg -------------------------------------------------------------------------------- /src/content/posts/Hello World.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jctaoo/stella/HEAD/src/content/posts/Hello World.jpg -------------------------------------------------------------------------------- /src/content/posts/helloworld.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jctaoo/stella/HEAD/src/content/posts/helloworld.jpeg -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | export default { 3 | printWidth: 120, 4 | plugins: ["prettier-plugin-astro"], 5 | }; 6 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "buildCommand": "pnpm build", 4 | "installCommand": "pnpm install", 5 | "framework": "astro" 6 | } -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/string.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | export function isLooseEqual(a?: string, b?: string) { 4 | const normalize = _.flow([_.trim, _.toLower, (str: string) => _.replace(str, /[\s\-]/g, "")]); 5 | return normalize(a) === normalize(b); 6 | } 7 | -------------------------------------------------------------------------------- /.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "Astro docs": { 4 | "url": "https://mcp.docs.astro.build/mcp", 5 | "headers": {} 6 | }, 7 | "shadcn": { 8 | "command": "npx", 9 | "args": [ 10 | "shadcn@latest", 11 | "mcp" 12 | ] 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /.rulesync/.mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "Astro docs": { 4 | "url": "https://mcp.docs.astro.build/mcp", 5 | "headers": {} 6 | }, 7 | "shadcn": { 8 | "command": "npx", 9 | "args": [ 10 | "shadcn@latest", 11 | "mcp" 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/content/snippets/Test Code Features.md: -------------------------------------------------------------------------------- 1 | --- 2 | category: Test 3 | title: "Test Code Features" 4 | tags: 5 | - code 6 | description: "Test Code Features" 7 | --- 8 | 9 | Show stella features for showing code. 10 | 11 | ```typescript 12 | // adding codes 13 | console.log("🍔"); 14 | // removing codes 15 | console.log("💩"); 16 | ``` 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "include": [".astro/types.d.ts", "**/*"], 4 | "exclude": ["dist"], 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | "jsxImportSource": "react", 8 | 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": ["./src/*"], 12 | "@plugins/*": ["./plugin/*"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /rulesync.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | "cursor", 4 | "codexcli" 5 | ], 6 | "features": [ 7 | "rules", 8 | "ignore", 9 | "mcp", 10 | "commands", 11 | "subagents" 12 | ], 13 | "baseDirs": [ 14 | "." 15 | ], 16 | "delete": true, 17 | "verbose": false, 18 | "experimentalSimulateCommands": false, 19 | "experimentalSimulateSubagents": false 20 | } -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import MainLayout from "@/layouts/Main.astro"; 3 | const pageTitle = "Home"; 4 | --- 5 | 6 | 7 |
8 | 9 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/utils/theme.ts: -------------------------------------------------------------------------------- 1 | export type Theme = "light" | "dark"; 2 | 3 | export function getNormalizedTheme(): Theme { 4 | const localStorageTheme = localStorage?.getItem("theme") ?? ""; 5 | if (["dark", "light"].includes(localStorageTheme)) { 6 | return localStorageTheme as Theme; 7 | } 8 | if (window.matchMedia("(prefers-color-scheme: dark)").matches) { 9 | return "dark"; 10 | } 11 | return "light"; 12 | } 13 | -------------------------------------------------------------------------------- /src/siteConfig.ts: -------------------------------------------------------------------------------- 1 | import { z } from "astro:schema"; 2 | 3 | const siteConfigSchema = z.object({ 4 | siteName: z.string().describe("The name of the site").default("Jctaoo."), 5 | bannerText: z.string().describe("The text of the banner").optional(), 6 | }); 7 | 8 | export type SiteConfig = z.infer; 9 | 10 | export const siteConfig = siteConfigSchema.parse({ 11 | bannerText: "This is a demo site.", 12 | }); 13 | -------------------------------------------------------------------------------- /src/content/snippets/Enlace Starter.md: -------------------------------------------------------------------------------- 1 | --- 2 | category: Enlace 3 | title: "[Enlace] Enlace Get Started" 4 | tags: 5 | - code 6 | description: "Enlace Get Started" 7 | --- 8 | 9 | description here ..... 10 | 11 | ```typescript 12 | @MainApplication 13 | class DemoApplication extends Application { 14 | @AddAdaptor(HttpAdaptor) 15 | onAddHttpAdaptor(router: Router) { 16 | router.useEndpointOn("/", () => "HelloWorld"); 17 | } 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | # jetbrains setting folder 24 | .idea/ 25 | 26 | # vercel 27 | .vercel/ 28 | 29 | # sonda 30 | .sonda/ 31 | .vercel 32 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/styles/global.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "iconLibrary": "lucide", 14 | "aliases": { 15 | "components": "@/components", 16 | "utils": "@/lib/utils", 17 | "ui": "@/components/ui", 18 | "lib": "@/lib", 19 | "hooks": "@/hooks" 20 | }, 21 | "registries": {} 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Label({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ) 20 | } 21 | 22 | export { Label } 23 | -------------------------------------------------------------------------------- /src/layouts/Pages.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import ArticleLayout from "@/layouts/Article.astro"; 3 | import { formatLastModified } from "@/utils/lastModified"; 4 | import type { GitTime } from "@plugins/remark-modified-time"; 5 | import type { ReadingTime } from "@plugins/remark-reading-time"; 6 | 7 | type Props = { 8 | frontmatter: { 9 | title: string; 10 | updateDates: string[]; 11 | gitTimes: GitTime; 12 | readingTime: ReadingTime; 13 | }; 14 | }; 15 | 16 | const { frontmatter } = Astro.props; 17 | const pageTitle = frontmatter.title; 18 | 19 | const gitTimes = frontmatter.gitTimes; 20 | const readingTime = frontmatter.readingTime; 21 | --- 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /src/content.config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection, z } from "astro:content"; 2 | 3 | const posts = defineCollection({ 4 | type: "content", 5 | schema: z.object({ 6 | category: z.string().optional(), 7 | tags: z.array(z.string()).optional(), 8 | title: z.string(), 9 | identifier: z.string().optional(), 10 | abbr: z.string().optional(), 11 | topImage: z.string().optional(), 12 | topImageAlt: z.string().optional(), 13 | }), 14 | }); 15 | 16 | const snippets = defineCollection({ 17 | type: "content", 18 | schema: z.object({ 19 | category: z.string().optional(), 20 | title: z.string(), 21 | description: z.string().optional(), 22 | tags: z.array(z.string()).optional(), 23 | }), 24 | }); 25 | 26 | export const collections = { posts, snippets }; 27 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Separator({ 7 | className, 8 | orientation = "horizontal", 9 | decorative = true, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 23 | ) 24 | } 25 | 26 | export { Separator } 27 | -------------------------------------------------------------------------------- /src/utils/lastModified.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionEntry, CollectionKey } from "astro:content"; 2 | import type { GitTime } from "@plugins/remark-modified-time"; 3 | import dayjs from "dayjs"; 4 | 5 | export function formatLastModified(gitTimes: GitTime) { 6 | return dayjs(gitTimes.updatedAt).format("YYYY/MM/DD HH:mm"); 7 | } 8 | 9 | export function getLastModified(post: CollectionEntry) { 10 | // @ts-expect-error 11 | const gitTimes = post.rendered.metadata.frontmatter.gitTimes as GitTime; 12 | return formatLastModified(gitTimes); 13 | } 14 | 15 | export function getLastModifiedDate(post: CollectionEntry) { 16 | // @ts-expect-error 17 | const gitTimes = post.rendered.metadata.frontmatter.gitTimes as GitTime; 18 | return dayjs(gitTimes.updatedAt); 19 | } -------------------------------------------------------------------------------- /src/pages/snippets/[...slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import ArticleLayout from "@/layouts/Article.astro"; 3 | import { getCollection, render, type CollectionEntry } from "astro:content"; 4 | import type { GitTime } from "@plugins/remark-modified-time"; 5 | import dayjs from "dayjs"; 6 | 7 | export async function getStaticPaths() { 8 | const snippets = await getCollection("snippets"); 9 | return snippets.map((snip) => ({ 10 | params: { slug: snip.slug }, 11 | props: snip, 12 | })); 13 | } 14 | type Props = CollectionEntry<"snippets">; 15 | 16 | const snippet = Astro.props; 17 | 18 | const { Content, remarkPluginFrontmatter } = await snippet.render(); 19 | const gitTimes = remarkPluginFrontmatter.gitTimes as GitTime; 20 | 21 | const { title: pageTitle, tags, category } = snippet.data; 22 | --- 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/pages/posts/[...slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import ArticleLayout from "@/layouts/Article.astro"; 3 | import { getCollection, type CollectionEntry } from "astro:content"; 4 | import type { GitTime } from "@plugins/remark-modified-time"; 5 | import type { ReadingTime } from "@plugins/remark-reading-time"; 6 | 7 | export async function getStaticPaths() { 8 | const posts = await getCollection("posts"); 9 | return posts.map((post) => ({ 10 | params: { slug: post.slug }, 11 | props: post, 12 | })); 13 | } 14 | type Props = CollectionEntry<"posts">; 15 | 16 | const post = Astro.props; 17 | 18 | const { title: pageTitle, tags, category } = post.data; 19 | const { Content, remarkPluginFrontmatter } = await post.render(); 20 | 21 | const gitTimes = remarkPluginFrontmatter.gitTimes as GitTime; 22 | const readingTime = remarkPluginFrontmatter.readingTime as ReadingTime; 23 | --- 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /.codex/memories/astro-development.md: -------------------------------------------------------------------------------- 1 | # Astro 开发与调研规则 2 | 3 | - 当用户提出新增或修改 Astro 相关功能时,优先通过已配置的 Astro MCP 能力进行检索与方案生成;若不可用或结果不足,请在线搜索并参考最新的 Astro 官方文档、发布说明与社区最佳实践后再实施。 4 | 5 | - 重点场景(包括但不限于): 6 | - Server Islands / Islands 架构与部分水合策略 7 | - Prerender / 预渲染与静态生成(含按路由/页面粒度的设置) 8 | - View Transitions / 视图过渡 9 | 10 | - 执行准则: 11 | - 在变更前先确认项目 Astro 版本(查看 `package.json` 与 `astro.config.mjs`),避免使用与版本不兼容的 API 或配置。 12 | - 以官方文档为准,优先参考最新版本文档与 Release Notes;必要时对比版本差异与迁移指南。 13 | - 在提交实现前,简要写明方案与影响面:涉及路由、构建目标、适配器或运行时行为的变更需特别说明。 14 | - 在 PR/提交信息中附上所依据的官方文档或权威来源链接以便复核。 15 | 16 | - 建议的检索方式与关键词: 17 | - 使用 Astro MCP:查询目标功能的官方说明、API、示例与限制项。 18 | - 在线搜索关键词:`Astro docs`, `Astro islands best practices`, `Astro prerender static generation`, `Astro view transitions`, `Astro breaking changes`。 19 | 20 | - 校验清单: 21 | - 本地 `dev` 与 `build` 通过,关键路由/页面按预期渲染(含预渲染页面与动态路由)。 22 | - 如改动 `astro.config.mjs`,验证适配器、输出目标与集成插件均工作正常。 23 | - 无新增的类型报错与 Lint 问题,站点导航/过渡交互在主流浏览器正常。 24 | 25 | - 项目内相关位置参考: 26 | - 配置:`astro.config.mjs` 27 | - 页面与路由:`src/pages` 28 | - 布局:`src/layouts` 29 | - 组件:`src/components` 30 | - 静态资源:`public` 31 | 32 | - 禁止: 33 | - 未经调研直接拍板实现;直接复制过时教程代码;忽略当前版本兼容性与迁移指引。 -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import tailwindcss from "@tailwindcss/vite"; 4 | import { defineConfig } from "astro/config"; 5 | import remarkMath from 'remark-math'; 6 | import rehypeKatex from 'rehype-katex'; 7 | 8 | import react from "@astrojs/react"; 9 | 10 | import mdx from "@astrojs/mdx"; 11 | 12 | import { remarkModifiedTime } from "./plugin/remark-modified-time"; 13 | import { remarkReadingTime } from "./plugin/remark-reading-time"; 14 | 15 | import vercel from "@astrojs/vercel/serverless"; 16 | 17 | import Sonda from 'sonda/astro'; 18 | 19 | // https://astro.build/config 20 | export default defineConfig({ 21 | 22 | 23 | vite: { 24 | plugins: [tailwindcss()], 25 | build: { 26 | sourcemap: true, 27 | }, 28 | }, 29 | 30 | markdown: { 31 | remarkPlugins: [ 32 | // @ts-expect-error 33 | [remarkModifiedTime, { fallbackToFs: true }], 34 | // @ts-expect-error 35 | remarkReadingTime, 36 | remarkMath, 37 | ], 38 | rehypePlugins: [rehypeKatex], 39 | }, 40 | 41 | integrations: [react(), mdx(), Sonda({ server: true })], 42 | 43 | adapter: vercel({ 44 | imageService: true, 45 | devImageService: "sharp", 46 | }), 47 | }); -------------------------------------------------------------------------------- /plugin/remark-reading-time.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "unified"; 2 | import getReadingTime from 'reading-time'; 3 | import { toString } from 'mdast-util-to-string'; 4 | import dayjs from "dayjs"; 5 | import duration from "dayjs/plugin/duration"; 6 | import relativeTime from "dayjs/plugin/relativeTime"; 7 | 8 | // config dayjs 9 | dayjs.extend(duration); 10 | dayjs.extend(relativeTime) 11 | 12 | export interface ReadingTimeStats { 13 | minutes: number; // rounded up minutes 14 | words: number; // computed words (latin words + CJK chars) 15 | text: string; // human text, e.g. "3 min read" 16 | } 17 | 18 | export const remarkReadingTime: Plugin<[]> = function () { 19 | return (tree, file) => { 20 | const textOnPage = toString(tree); 21 | const readingTime = getReadingTime(textOnPage); 22 | 23 | const duration = dayjs.duration(readingTime.minutes, "minutes"); 24 | 25 | const stats = { 26 | minutes: readingTime.minutes, 27 | words: readingTime.words, 28 | text: duration.humanize(), 29 | } 30 | 31 | if (file.data?.astro?.frontmatter) { 32 | file.data.astro.frontmatter.readingTime = stats; 33 | } 34 | }; 35 | }; 36 | 37 | export type { ReadingTimeStats as ReadingTime }; 38 | 39 | -------------------------------------------------------------------------------- /.cursor/rules/astro-development.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | alwaysApply: true 3 | description: 4 | globs: **/* 5 | --- 6 | 7 | # Astro 开发与调研规则 8 | 9 | - 当用户提出新增或修改 Astro 相关功能时,优先通过已配置的 Astro MCP 能力进行检索与方案生成;若不可用或结果不足,请在线搜索并参考最新的 Astro 官方文档、发布说明与社区最佳实践后再实施。 10 | 11 | - 重点场景(包括但不限于): 12 | - Server Islands / Islands 架构与部分水合策略 13 | - Prerender / 预渲染与静态生成(含按路由/页面粒度的设置) 14 | - View Transitions / 视图过渡 15 | 16 | - 执行准则: 17 | - 在变更前先确认项目 Astro 版本(查看 `package.json` 与 `astro.config.mjs`),避免使用与版本不兼容的 API 或配置。 18 | - 以官方文档为准,优先参考最新版本文档与 Release Notes;必要时对比版本差异与迁移指南。 19 | - 在提交实现前,简要写明方案与影响面:涉及路由、构建目标、适配器或运行时行为的变更需特别说明。 20 | - 在 PR/提交信息中附上所依据的官方文档或权威来源链接以便复核。 21 | 22 | - 建议的检索方式与关键词: 23 | - 使用 Astro MCP:查询目标功能的官方说明、API、示例与限制项。 24 | - 在线搜索关键词:`Astro docs`, `Astro islands best practices`, `Astro prerender static generation`, `Astro view transitions`, `Astro breaking changes`。 25 | 26 | - 校验清单: 27 | - 本地 `dev` 与 `build` 通过,关键路由/页面按预期渲染(含预渲染页面与动态路由)。 28 | - 如改动 `astro.config.mjs`,验证适配器、输出目标与集成插件均工作正常。 29 | - 无新增的类型报错与 Lint 问题,站点导航/过渡交互在主流浏览器正常。 30 | 31 | - 项目内相关位置参考: 32 | - 配置:`astro.config.mjs` 33 | - 页面与路由:`src/pages` 34 | - 布局:`src/layouts` 35 | - 组件:`src/components` 36 | - 静态资源:`public` 37 | 38 | - 禁止: 39 | - 未经调研直接拍板实现;直接复制过时教程代码;忽略当前版本兼容性与迁移指引。 -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SwitchPrimitive from "@radix-ui/react-switch" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Switch({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | 25 | 26 | ) 27 | } 28 | 29 | export { Switch } 30 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Avatar({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ) 20 | } 21 | 22 | function AvatarImage({ 23 | className, 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 32 | ) 33 | } 34 | 35 | function AvatarFallback({ 36 | className, 37 | ...props 38 | }: React.ComponentProps) { 39 | return ( 40 | 48 | ) 49 | } 50 | 51 | export { Avatar, AvatarImage, AvatarFallback } 52 | -------------------------------------------------------------------------------- /.rulesync/rules/astro-development.md: -------------------------------------------------------------------------------- 1 | --- 2 | targets: 3 | - '*' 4 | root: false 5 | globs: 6 | - '**/*' 7 | cursor: 8 | alwaysApply: true 9 | globs: 10 | - '**/*' 11 | --- 12 | # Astro 开发与调研规则 13 | 14 | - 当用户提出新增或修改 Astro 相关功能时,优先通过已配置的 Astro MCP 能力进行检索与方案生成;若不可用或结果不足,请在线搜索并参考最新的 Astro 官方文档、发布说明与社区最佳实践后再实施。 15 | 16 | - 重点场景(包括但不限于): 17 | - Server Islands / Islands 架构与部分水合策略 18 | - Prerender / 预渲染与静态生成(含按路由/页面粒度的设置) 19 | - View Transitions / 视图过渡 20 | 21 | - 执行准则: 22 | - 在变更前先确认项目 Astro 版本(查看 `package.json` 与 `astro.config.mjs`),避免使用与版本不兼容的 API 或配置。 23 | - 以官方文档为准,优先参考最新版本文档与 Release Notes;必要时对比版本差异与迁移指南。 24 | - 在提交实现前,简要写明方案与影响面:涉及路由、构建目标、适配器或运行时行为的变更需特别说明。 25 | - 在 PR/提交信息中附上所依据的官方文档或权威来源链接以便复核。 26 | 27 | - 建议的检索方式与关键词: 28 | - 使用 Astro MCP:查询目标功能的官方说明、API、示例与限制项。 29 | - 在线搜索关键词:`Astro docs`, `Astro islands best practices`, `Astro prerender static generation`, `Astro view transitions`, `Astro breaking changes`。 30 | 31 | - 校验清单: 32 | - 本地 `dev` 与 `build` 通过,关键路由/页面按预期渲染(含预渲染页面与动态路由)。 33 | - 如改动 `astro.config.mjs`,验证适配器、输出目标与集成插件均工作正常。 34 | - 无新增的类型报错与 Lint 问题,站点导航/过渡交互在主流浏览器正常。 35 | 36 | - 项目内相关位置参考: 37 | - 配置:`astro.config.mjs` 38 | - 页面与路由:`src/pages` 39 | - 布局:`src/layouts` 40 | - 组件:`src/components` 41 | - 静态资源:`public` 42 | 43 | - 禁止: 44 | - 未经调研直接拍板实现;直接复制过时教程代码;忽略当前版本兼容性与迁移指引。 45 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { CheckIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Checkbox({ 10 | className, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 22 | 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export { Checkbox } 33 | -------------------------------------------------------------------------------- /.codex/memories/content-authoring.md: -------------------------------------------------------------------------------- 1 | # Content 编写规范(对齐 content.config.ts) 2 | 3 | - 新增或修改 `src/content` 下内容时,必须对照 [src/content.config.ts](mdc:src/content.config.ts) 中集合与字段定义进行 Frontmatter 编写与校验。 4 | - 严禁添加 schema 未定义的字段;字段类型必须吻合(如 `tags` 为字符串数组)。 5 | - 运行时校验:在提交前确保 `pnpm dev` 或 `pnpm build` 无内容校验报错。 6 | 7 | ## Frontmatter 基本要求 8 | - 使用 YAML Frontmatter;推荐按 schema 顺序书写键。 9 | - 文件命名使用 kebab-case;与页面 slug 对应。 10 | - 图片路径:优先与内容共置(相对路径),或使用 `public/` 下绝对路径。 11 | - 自动元数据:阅读时长与最后修改时间由 Remark 插件注入(见 `plugin/remark-reading-time.ts` 与 `plugin/remark-modified-time.ts`),不要手动写入。 12 | 13 | ## posts 集合模板(必填:title) 14 | ```yaml 15 | --- 16 | title: Post Title 17 | category: Optional Category 18 | tags: [tag1, tag2] 19 | identifier: optional-stable-id 20 | abbr: OPT-ABBR 21 | topImage: ./HelloWorld.jpg 22 | topImageAlt: A short accessible description 23 | --- 24 | ``` 25 | 26 | 字段说明: 27 | - `title` 必填;`category`、`tags`、`identifier`、`abbr`、`topImage`、`topImageAlt` 可选。 28 | - `tags` 为字符串数组。 29 | - 存在 `topImage` 时应提供 `topImageAlt`。 30 | 31 | ## snippets 集合模板(必填:title) 32 | ```yaml 33 | --- 34 | title: Snippet Title 35 | description: Optional short description 36 | category: Optional Category 37 | tags: [tag1, tag2] 38 | --- 39 | ``` 40 | 41 | 字段说明: 42 | - `title` 必填;`description`、`category`、`tags` 可选。 43 | - `tags` 为字符串数组。 44 | 45 | ## 校验流程 46 | - 本地预览:`pnpm dev` 确认无 schema 报错与页面渲染异常。 47 | - 构建验证:`pnpm build` 通过后再提交。 48 | - 格式化:`pnpm run fix:prettier` 统一风格。 49 | 50 | ## 禁止项 51 | - 未对齐 [src/content.config.ts](mdc:src/content.config.ts) 的字段名/类型。 52 | - 在 Frontmatter 中写入自动元数据(阅读时长、修改时间)。 53 | - 将体积过大的图片直接置于 Frontmatter(仅放路径)。 -------------------------------------------------------------------------------- /src/components/CommonHeader.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { ClientRouter } from "astro:transitions"; 3 | import Analytics from '@vercel/analytics/astro'; 4 | 5 | export type Props = { 6 | title: string; 7 | description?: string; 8 | }; 9 | 10 | const { title, description } = Astro.props; 11 | --- 12 | 13 | 14 | 15 | 16 | 17 | 18 | {title} 19 | 20 | {description && } 21 | 22 | 23 | 24 | 54 | 55 | -------------------------------------------------------------------------------- /src/components/site-nav.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | NavigationMenu, 4 | NavigationMenuList, 5 | NavigationMenuItem, 6 | NavigationMenuLink, 7 | navigationMenuTriggerStyle, 8 | } from "@/components/ui/navigation-menu"; 9 | 10 | export function SiteNav() { 11 | return ( 12 | 40 | ); 41 | } 42 | 43 | export default SiteNav; 44 | -------------------------------------------------------------------------------- /src/components/theme-switcher.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import { Moon, Sun } from "lucide-react"; 3 | import { Switch } from "@/components/ui/switch"; 4 | import { getNormalizedTheme } from "@/utils/theme"; 5 | import { cn } from "@/lib/utils"; 6 | 7 | interface ThemeSwitcherProps { 8 | className?: string; 9 | labelClassName?: string; 10 | switchClassName?: string; 11 | ariaLabel?: string; 12 | } 13 | 14 | export default function ThemeSwitcher({ className = "", labelClassName = "", switchClassName = "", ariaLabel = "切换暗黑模式" }: ThemeSwitcherProps) { 15 | const [isDark, setIsDark] = useState(false); 16 | 17 | useEffect(() => { 18 | const isDarkNow = getNormalizedTheme() === "dark"; 19 | setIsDark(isDarkNow); 20 | }, []); 21 | 22 | const setTheme = useCallback((nextIsDark: boolean) => { 23 | const root = document.documentElement; 24 | setIsDark(nextIsDark); 25 | if (nextIsDark) { 26 | localStorage.setItem("theme", "dark"); 27 | root.classList.add("dark"); 28 | } else { 29 | localStorage.setItem("theme", "light"); 30 | root.classList.remove("dark"); 31 | } 32 | }, []); 33 | 34 | return ( 35 |
36 |
37 | {isDark ? : } 38 | {isDark ? "暗黑模式" : "亮色模式"} 39 |
40 | 41 |
42 | ); 43 | } 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const badgeVariants = cva( 8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", 9 | { 10 | variants: { 11 | variant: { 12 | default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", 13 | secondary: "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", 14 | destructive: 15 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", 17 | }, 18 | }, 19 | defaultVariants: { 20 | variant: "default", 21 | }, 22 | }, 23 | ); 24 | 25 | function Badge({ 26 | className, 27 | variant, 28 | asChild = false, 29 | ...props 30 | }: React.ComponentProps<"span"> & VariantProps & { asChild?: boolean }) { 31 | const Comp = asChild ? Slot : "span"; 32 | 33 | return ; 34 | } 35 | 36 | export { Badge, badgeVariants }; 37 | -------------------------------------------------------------------------------- /.cursor/rules/content-authoring.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Content authoring rules aligned with src/content.config.ts schemas 3 | globs: src/content/**/*.md,src/content/**/*.mdx 4 | --- 5 | 6 | # Content 编写规范(对齐 content.config.ts) 7 | 8 | - 新增或修改 `src/content` 下内容时,必须对照 [src/content.config.ts](mdc:src/content.config.ts) 中集合与字段定义进行 Frontmatter 编写与校验。 9 | - 严禁添加 schema 未定义的字段;字段类型必须吻合(如 `tags` 为字符串数组)。 10 | - 运行时校验:在提交前确保 `pnpm dev` 或 `pnpm build` 无内容校验报错。 11 | 12 | ## Frontmatter 基本要求 13 | - 使用 YAML Frontmatter;推荐按 schema 顺序书写键。 14 | - 文件命名使用 kebab-case;与页面 slug 对应。 15 | - 图片路径:优先与内容共置(相对路径),或使用 `public/` 下绝对路径。 16 | - 自动元数据:阅读时长与最后修改时间由 Remark 插件注入(见 `plugin/remark-reading-time.ts` 与 `plugin/remark-modified-time.ts`),不要手动写入。 17 | 18 | ## posts 集合模板(必填:title) 19 | ```yaml 20 | --- 21 | title: Post Title 22 | category: Optional Category 23 | tags: [tag1, tag2] 24 | identifier: optional-stable-id 25 | abbr: OPT-ABBR 26 | topImage: ./HelloWorld.jpg 27 | topImageAlt: A short accessible description 28 | --- 29 | ``` 30 | 31 | 字段说明: 32 | - `title` 必填;`category`、`tags`、`identifier`、`abbr`、`topImage`、`topImageAlt` 可选。 33 | - `tags` 为字符串数组。 34 | - 存在 `topImage` 时应提供 `topImageAlt`。 35 | 36 | ## snippets 集合模板(必填:title) 37 | ```yaml 38 | --- 39 | title: Snippet Title 40 | description: Optional short description 41 | category: Optional Category 42 | tags: [tag1, tag2] 43 | --- 44 | ``` 45 | 46 | 字段说明: 47 | - `title` 必填;`description`、`category`、`tags` 可选。 48 | - `tags` 为字符串数组。 49 | 50 | ## 校验流程 51 | - 本地预览:`pnpm dev` 确认无 schema 报错与页面渲染异常。 52 | - 构建验证:`pnpm build` 通过后再提交。 53 | - 格式化:`pnpm run fix:prettier` 统一风格。 54 | 55 | ## 禁止项 56 | - 未对齐 [src/content.config.ts](mdc:src/content.config.ts) 的字段名/类型。 57 | - 在 Frontmatter 中写入自动元数据(阅读时长、修改时间)。 58 | - 将体积过大的图片直接置于 Frontmatter(仅放路径)。 -------------------------------------------------------------------------------- /src/layouts/Main.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import CommonHeader, { type Props as CommonHeaderProps } from "@/components/CommonHeader.astro"; 3 | import SidebarNav from "@/components/sidebar-nav"; 4 | import MobileHeader from "@/components/mobile-header"; 5 | import "@/styles/global.css"; 6 | import TopLoaderVT from "@/components/TopLoaderVT.astro"; 7 | import { siteConfig } from "@/siteConfig"; 8 | 9 | type Props = { 10 | header: CommonHeaderProps; 11 | }; 12 | const { header } = Astro.props; 13 | 14 | // Determine if we're on the home page 15 | const isHomePage = Astro.url.pathname === "/"; 16 | 17 | // get current route 18 | const currentRoute = Astro.url.pathname; 19 | 20 | const { bannerText } = siteConfig; 21 | --- 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 38 | 47 |
52 |
53 | 54 |
55 |
56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as PopoverPrimitive from "@radix-ui/react-popover" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Popover({ 7 | ...props 8 | }: React.ComponentProps) { 9 | return 10 | } 11 | 12 | function PopoverTrigger({ 13 | ...props 14 | }: React.ComponentProps) { 15 | return 16 | } 17 | 18 | function PopoverContent({ 19 | className, 20 | align = "center", 21 | sideOffset = 4, 22 | ...props 23 | }: React.ComponentProps) { 24 | return ( 25 | 26 | 36 | 37 | ) 38 | } 39 | 40 | function PopoverAnchor({ 41 | ...props 42 | }: React.ComponentProps) { 43 | return 44 | } 45 | 46 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 47 | -------------------------------------------------------------------------------- /src/components/TopLoaderVT.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import "nprogress/nprogress.css"; 3 | --- 4 | 5 | 15 | 16 | 62 | -------------------------------------------------------------------------------- /src/components/Prose.astro: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | --- 4 | 5 |
ul]:pl-0 prose-ul:list-none", 14 | // Custom link 15 | "prose-a:text-blue-500", 16 | // Custom blockquote border 17 | "prose-blockquote:border-l-[6px] prose-blockquote:text-muted-foreground", 18 | // Disable inline code quote 19 | "prose-code:before:content-none prose-code:after:content-none", 20 | ]} 21 | > 22 | 23 |
24 | 25 | 70 | -------------------------------------------------------------------------------- /.cursor/rules/overview.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | alwaysApply: true 3 | description: 4 | globs: **/* 5 | --- 6 | 7 | # 项目结构与开发约定(Stella / Astro) 8 | 9 | - 包管理器:必须使用 pnpm(见 [package.json](mdc:package.json) 中 `packageManager` 字段)。 10 | - 安装依赖:`pnpm install` 11 | - 本地开发:`pnpm dev` 12 | - 构建产物:`pnpm build` 13 | - 本地预览:`pnpm preview` 14 | 15 | ## 关键配置与入口 16 | - Astro 配置:[astro.config.mjs](mdc:astro.config.mjs) 17 | - 包与脚本:[package.json](mdc:package.json) 18 | - TypeScript 配置:[tsconfig.json](mdc:tsconfig.json) 19 | 20 | ## 目录总览(./src) 21 | - 组件:`src/components`(通用组件与 `ui/` 下的 shadcn 组件) 22 | - 内容集合:`src/content`(Markdown/资产内容) 23 | - 内容模型定义:[src/content.config.ts](mdc:src/content.config.ts) 24 | - 布局:`src/layouts`(`main.astro`、`pages.astro`、`article.astro`) 25 | - 路由页面:`src/pages`(基于文件系统路由) 26 | - 样式:`src/styles`(`global.css`) 27 | - 工具库:`src/utils`、`src/lib` 28 | - 自定义插件:`plugin/`(`remark-modified-time.ts`、`remark-reading-time.ts`) 29 | - 静态资源:`public/` 30 | 31 | ## 基础路由(基于 src/pages) 32 | - `/` → [src/pages/index.astro](mdc:src/pages/index.astro) 33 | - `/about` → [src/pages/about.mdx](mdc:src/pages/about.mdx) 34 | - `/posts` 列表 → [src/pages/posts/index.astro](mdc:src/pages/posts/index.astro) 35 | - `/posts/:slug...` 详情(动态) → [src/pages/posts/[...slug].astro](mdc:src/pages/posts/[...slug].astro) 36 | - `/snippets` 列表 → [src/pages/snippets/index.astro](mdc:src/pages/snippets/index.astro) 37 | - `/snippets/:slug...` 详情(动态) → [src/pages/snippets/[...slug].astro](mdc:src/pages/snippets/[...slug].astro) 38 | 39 | ## 内容组织(src/content) 40 | - 文章集合:`src/content/posts`(含 `.md` 与配图等资产) 41 | - 片段集合:`src/content/snippets` 42 | - 集合/字段等规范在 [src/content.config.ts](mdc:src/content.config.ts) 中定义与校验。 43 | 44 | ## shadcn 组件使用 45 | - 严格遵循规则见:[shadcn-components.mdc](mdc:shadcn-components.mdc) 46 | 47 | ## Astro 研发流程 48 | - 新增 Astro 能力时遵循:[astro-development.mdc](mdc:astro-development.mdc) 49 | 50 | ## 其他约定 51 | - 禁止使用 npm/yarn 进行安装与脚本执行;统一使用 pnpm。 52 | - 代码格式:使用 Prettier;修复命令:`pnpm run fix:prettier`。 -------------------------------------------------------------------------------- /.rulesync/rules/content-authoring.md: -------------------------------------------------------------------------------- 1 | --- 2 | targets: 3 | - '*' 4 | root: false 5 | description: Content authoring rules aligned with src/content.config.ts schemas 6 | globs: 7 | - src/content/**/*.md 8 | - src/content/**/*.mdx 9 | cursor: 10 | description: Content authoring rules aligned with src/content.config.ts schemas 11 | globs: 12 | - src/content/**/*.md 13 | - src/content/**/*.mdx 14 | --- 15 | # Content 编写规范(对齐 content.config.ts) 16 | 17 | - 新增或修改 `src/content` 下内容时,必须对照 [src/content.config.ts](mdc:src/content.config.ts) 中集合与字段定义进行 Frontmatter 编写与校验。 18 | - 严禁添加 schema 未定义的字段;字段类型必须吻合(如 `tags` 为字符串数组)。 19 | - 运行时校验:在提交前确保 `pnpm dev` 或 `pnpm build` 无内容校验报错。 20 | 21 | ## Frontmatter 基本要求 22 | - 使用 YAML Frontmatter;推荐按 schema 顺序书写键。 23 | - 文件命名使用 kebab-case;与页面 slug 对应。 24 | - 图片路径:优先与内容共置(相对路径),或使用 `public/` 下绝对路径。 25 | - 自动元数据:阅读时长与最后修改时间由 Remark 插件注入(见 `plugin/remark-reading-time.ts` 与 `plugin/remark-modified-time.ts`),不要手动写入。 26 | 27 | ## posts 集合模板(必填:title) 28 | ```yaml 29 | --- 30 | title: Post Title 31 | category: Optional Category 32 | tags: [tag1, tag2] 33 | identifier: optional-stable-id 34 | abbr: OPT-ABBR 35 | topImage: ./HelloWorld.jpg 36 | topImageAlt: A short accessible description 37 | --- 38 | ``` 39 | 40 | 字段说明: 41 | - `title` 必填;`category`、`tags`、`identifier`、`abbr`、`topImage`、`topImageAlt` 可选。 42 | - `tags` 为字符串数组。 43 | - 存在 `topImage` 时应提供 `topImageAlt`。 44 | 45 | ## snippets 集合模板(必填:title) 46 | ```yaml 47 | --- 48 | title: Snippet Title 49 | description: Optional short description 50 | category: Optional Category 51 | tags: [tag1, tag2] 52 | --- 53 | ``` 54 | 55 | 字段说明: 56 | - `title` 必填;`description`、`category`、`tags` 可选。 57 | - `tags` 为字符串数组。 58 | 59 | ## 校验流程 60 | - 本地预览:`pnpm dev` 确认无 schema 报错与页面渲染异常。 61 | - 构建验证:`pnpm build` 通过后再提交。 62 | - 格式化:`pnpm run fix:prettier` 统一风格。 63 | 64 | ## 禁止项 65 | - 未对齐 [src/content.config.ts](mdc:src/content.config.ts) 的字段名/类型。 66 | - 在 Frontmatter 中写入自动元数据(阅读时长、修改时间)。 67 | - 将体积过大的图片直接置于 Frontmatter(仅放路径)。 68 | -------------------------------------------------------------------------------- /.rulesync/rules/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | targets: 3 | - '*' 4 | root: true 5 | globs: 6 | - '**/*' 7 | cursor: 8 | alwaysApply: true 9 | globs: 10 | - '**/*' 11 | --- 12 | # 项目结构与开发约定(Stella / Astro) 13 | 14 | - 包管理器:必须使用 pnpm(见 [package.json](mdc:package.json) 中 `packageManager` 字段)。 15 | - 安装依赖:`pnpm install` 16 | - 本地开发:`pnpm dev` 17 | - 构建产物:`pnpm build` 18 | - 本地预览:`pnpm preview` 19 | 20 | ## 关键配置与入口 21 | - Astro 配置:[astro.config.mjs](mdc:astro.config.mjs) 22 | - 包与脚本:[package.json](mdc:package.json) 23 | - TypeScript 配置:[tsconfig.json](mdc:tsconfig.json) 24 | 25 | ## 目录总览(./src) 26 | - 组件:`src/components`(通用组件与 `ui/` 下的 shadcn 组件) 27 | - 内容集合:`src/content`(Markdown/资产内容) 28 | - 内容模型定义:[src/content.config.ts](mdc:src/content.config.ts) 29 | - 布局:`src/layouts`(`main.astro`、`pages.astro`、`article.astro`) 30 | - 路由页面:`src/pages`(基于文件系统路由) 31 | - 样式:`src/styles`(`global.css`) 32 | - 工具库:`src/utils`、`src/lib` 33 | - 自定义插件:`plugin/`(`remark-modified-time.ts`、`remark-reading-time.ts`) 34 | - 静态资源:`public/` 35 | 36 | ## 基础路由(基于 src/pages) 37 | - `/` → [src/pages/index.astro](mdc:src/pages/index.astro) 38 | - `/about` → [src/pages/about.mdx](mdc:src/pages/about.mdx) 39 | - `/posts` 列表 → [src/pages/posts/index.astro](mdc:src/pages/posts/index.astro) 40 | - `/posts/:slug...` 详情(动态) → [src/pages/posts/[...slug].astro](mdc:src/pages/posts/[...slug].astro) 41 | - `/snippets` 列表 → [src/pages/snippets/index.astro](mdc:src/pages/snippets/index.astro) 42 | - `/snippets/:slug...` 详情(动态) → [src/pages/snippets/[...slug].astro](mdc:src/pages/snippets/[...slug].astro) 43 | 44 | ## 内容组织(src/content) 45 | - 文章集合:`src/content/posts`(含 `.md` 与配图等资产) 46 | - 片段集合:`src/content/snippets` 47 | - 集合/字段等规范在 [src/content.config.ts](mdc:src/content.config.ts) 中定义与校验。 48 | 49 | ## shadcn 组件使用 50 | - 严格遵循规则见:[shadcn-components.mdc](mdc:shadcn-components.mdc) 51 | 52 | ## Astro 研发流程 53 | - 新增 Astro 能力时遵循:[astro-development.mdc](mdc:astro-development.mdc) 54 | 55 | ## 其他约定 56 | - 禁止使用 npm/yarn 进行安装与脚本执行;统一使用 pnpm。 57 | - 代码格式:使用 Prettier;修复命令:`pnpm run fix:prettier`。 58 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
12 | ); 13 | } 14 | 15 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 16 | return ( 17 |
25 | ); 26 | } 27 | 28 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 29 | return
; 30 | } 31 | 32 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 33 | return
; 34 | } 35 | 36 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 37 | return ( 38 |
43 | ); 44 | } 45 | 46 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 47 | return
; 48 | } 49 | 50 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 51 | return ( 52 |
53 | ); 54 | } 55 | 56 | export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; 57 | -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function TooltipProvider({ 7 | delayDuration = 0, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 16 | ) 17 | } 18 | 19 | function Tooltip({ 20 | ...props 21 | }: React.ComponentProps) { 22 | return ( 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | function TooltipTrigger({ 30 | ...props 31 | }: React.ComponentProps) { 32 | return 33 | } 34 | 35 | function TooltipContent({ 36 | className, 37 | sideOffset = 0, 38 | children, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 43 | 52 | {children} 53 | 54 | 55 | 56 | ) 57 | } 58 | 59 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 60 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 15 | outline: 16 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 17 | secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 18 | ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 19 | link: "text-primary underline-offset-4 hover:underline", 20 | }, 21 | size: { 22 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 23 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 24 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 25 | icon: "size-9", 26 | }, 27 | }, 28 | defaultVariants: { 29 | variant: "default", 30 | size: "default", 31 | }, 32 | }, 33 | ); 34 | 35 | function Button({ 36 | className, 37 | variant, 38 | size, 39 | asChild = false, 40 | ...props 41 | }: React.ComponentProps<"button"> & 42 | VariantProps & { 43 | asChild?: boolean; 44 | }) { 45 | const Comp = asChild ? Slot : "button"; 46 | 47 | return ; 48 | } 49 | 50 | export { Button, buttonVariants }; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stella", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "build": "astro build", 8 | "preview": "astro preview", 9 | "astro": "astro", 10 | "fix:prettier": "prettier --write .", 11 | "syncrules": "rulesync generate" 12 | }, 13 | "dependencies": { 14 | "@astrojs/mdx": "^4.3.5", 15 | "@astrojs/react": "^4.3.1", 16 | "@astrojs/vercel": "^8.2.7", 17 | "@radix-ui/react-avatar": "^1.1.10", 18 | "@radix-ui/react-checkbox": "^1.3.3", 19 | "@radix-ui/react-dialog": "^1.1.15", 20 | "@radix-ui/react-label": "^2.1.7", 21 | "@radix-ui/react-navigation-menu": "^1.2.14", 22 | "@radix-ui/react-popover": "^1.1.15", 23 | "@radix-ui/react-separator": "^1.1.7", 24 | "@radix-ui/react-slot": "^1.2.3", 25 | "@radix-ui/react-switch": "^1.2.6", 26 | "@radix-ui/react-tooltip": "^1.2.8", 27 | "@tailwindcss/vite": "^4.1.12", 28 | "@types/canvas-confetti": "^1.9.0", 29 | "@types/react": "^19.1.12", 30 | "@types/react-dom": "^19.1.9", 31 | "@vercel/analytics": "^1.5.0", 32 | "astro": "^5.13.7", 33 | "canvas-confetti": "^1.9.3", 34 | "class-variance-authority": "^0.7.1", 35 | "clsx": "^2.1.1", 36 | "cmdk": "^1.1.1", 37 | "dayjs": "^1.11.18", 38 | "katex": "^0.16.22", 39 | "lodash": "^4.17.21", 40 | "lucide-react": "^0.469.0", 41 | "mdast-util-to-string": "^4.0.0", 42 | "nprogress": "^0.2.0", 43 | "react": "^19.1.1", 44 | "react-dom": "^19.1.1", 45 | "react-icons": "^5.5.0", 46 | "reading-time": "^1.5.0", 47 | "rehype-katex": "^7.0.1", 48 | "remark-math": "^6.0.0", 49 | "sharp": "^0.34.4", 50 | "tailwind-merge": "^3.3.1", 51 | "tailwindcss": "^4.1.12", 52 | "vaul": "^1.1.2" 53 | }, 54 | "devDependencies": { 55 | "@tailwindcss/typography": "^0.5.18", 56 | "@types/lodash": "^4.17.20", 57 | "@types/nprogress": "^0.2.3", 58 | "prettier": "^3.6.2", 59 | "prettier-plugin-astro": "^0.14.1", 60 | "rulesync": "^1.2.3", 61 | "shadcn": "^3.3.1", 62 | "sonda": "^0.9.0", 63 | "tw-animate-css": "^1.3.8", 64 | "unified": "^11.0.5" 65 | }, 66 | "packageManager": "pnpm@10.15.1+sha512.34e538c329b5553014ca8e8f4535997f96180a1d0f614339357449935350d924e22f8614682191264ec33d1462ac21561aff97f6bb18065351c162c7e8f6de67" 67 | } 68 | -------------------------------------------------------------------------------- /.codex/memories/shadcn-components.md: -------------------------------------------------------------------------------- 1 | # Shadcn UI 组件使用与来源规则(MCP 增强) 2 | 3 | 本项目对 shadcn UI 的使用有强约束,且要求在涉及 shadcn 组件的任何任务时,**在必要的时候总是优先尝试使用 shadcn MCP 工具** 进行检索、命令生成与校验,再执行命令或实现代码。 4 | 5 | ## 基本规范 6 | 7 | - **包管理器统一使用 pnpm**。 8 | - **强制使用 CLI 管理组件**:`pnpm dlx shadcn@latest` 9 | - 添加:`pnpm dlx shadcn@latest add button input` 10 | - 覆盖更新:`pnpm dlx shadcn@latest add --overwrite button` 11 | - **禁止**: 12 | - 手动在 `src/components/ui` 下创建、拷贝、粘贴、重命名组件文件。 13 | - 从文档/第三方仓库直接复制 shadcn 组件源码。 14 | - 通过脚手架/代码生成器绕过 `shadcn` CLI 生成组件。 15 | - 已生成组件的修改: 16 | - 原则上不直接改动 `src/components/ui/*`。如需变更,优先修改 [components.json](mdc:components.json) 或通过 `add --overwrite` 重新生成。 17 | - 若必须本地化小范围样式,不能改变组件 API,并在提交中说明原因与范围。 18 | - 目录与约定: 19 | - 组件根目录:`src/components/ui` 20 | - 不在其他目录重复放置 shadcn 组件。 21 | - 代码评审检查: 22 | - PR 若新增/修改 `src/components/ui/*` 且非由 `pnpm dlx shadcn` 生成,应退回并改为通过 CLI 处理。 23 | - 确认组件清单与 [components.json](mdc:components.json) 一致。 24 | - 提交信息建议: 25 | - `chore(shadcn): add button via shadcn CLI` 26 | - `chore(shadcn): update button via shadcn --overwrite` 27 | 28 | ## MCP 工具使用(务必优先尝试) 29 | 30 | 当需要查找、添加、更新、核对 shadcn 组件时,**先使用 shadcn MCP 工具** 获取信息与生成命令,再执行 CLI: 31 | 32 | - 可用工具(按常见流程): 33 | - `get_project_registries`:读取项目已配置的 shadcn registry。 34 | - `list_items_in_registries`:列出可用组件项(支持分页)。 35 | - `search_items_in_registries`:按名称/描述模糊搜索组件。 36 | - `view_items_in_registries`:查看组件的详细信息、文件清单与说明。 37 | - `get_item_examples_from_registries`:获取组件示例与完整用法代码。 38 | - `get_add_command_for_items`:根据所选组件生成准确的 `shadcn add` 命令。 39 | - `get_audit_checklist`:生成变更后的核对清单,确保新增/更新后无遗漏。 40 | 41 | ### 典型操作流程 42 | 43 | - 添加组件: 44 | 1) 使用 `search_items_in_registries` 定位组件 → 2) 用 `view_items_in_registries` 确认内容与依赖 → 3) 用 `get_add_command_for_items` 生成命令 → 4) 执行生成的 `pnpm dlx shadcn@latest add `。 45 | 46 | - 更新组件: 47 | 1) 按上步骤检索 → 2) 使用 `get_add_command_for_items` 生成命令并添加 `--overwrite` → 3) 执行 `pnpm dlx shadcn@latest add --overwrite `。 48 | 49 | - 变更校验: 50 | - 运行 `get_audit_checklist`,并确保: 51 | - [components.json](mdc:components.json) 与实际组件文件一致; 52 | - 未直接手改 `src/components/ui/*`; 53 | - 依赖、样式、构建均正常。 54 | 55 | ### 执行约束与命令规范 56 | 57 | - 运行命令统一使用 `pnpm`,禁止 `npm/yarn`。 58 | - 执行时如需非交互,请添加对应的非交互参数(在自动化任务中务必考虑)。 59 | - 组件覆盖更新必须使用 `--overwrite`,避免手工同步差异。 60 | 61 | ## 参考与对齐 62 | 63 | - 组件清单与生成策略以 [components.json](mdc:components.json) 为准。 64 | - 组件目录:`src/components/ui`(请勿在其他目录重复放置)。 65 | - 出现与规则冲突的变更,应回滚并按 MCP + CLI 流程重新执行。 66 | 67 | > 提示:本规则为“总是应用”。当任务与 shadcn 组件相关时,应首先尝试使用上列 **shadcn MCP** 工具完成检索和命令生成,再执行具体变更。 -------------------------------------------------------------------------------- /src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | export const CreativeCommons = { 2 | by: "cc-by", 3 | byNc: "cc-by-nc", 4 | byNcNd: "cc-by-nc-nd", 5 | byNcSa: "cc-by-nc-sa", 6 | byNd: "cc-by-nd", 7 | bySa: "cc-by-sa", 8 | } as const; 9 | 10 | export type CreativeCommonsType = keyof typeof CreativeCommons; 11 | 12 | function CreativeCommonsView({ ccLink, ccImage }: { ccLink: string; ccImage: string }) { 13 | return ( 14 |
15 |
16 | 17 | 知识共享许可协议 18 | 19 |
20 |

21 | 本作品采用 22 | 28 | 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 29 | 30 | 进行许可。 31 |

32 |
33 | ); 34 | } 35 | 36 | function CopyrightView({ author }: { author: string }) { 37 | const currentYear = new Date().getFullYear(); 38 | return ( 39 | <> 40 |

41 | Copyright © {currentYear} {author}. 保留所有权利。 42 |

43 |

44 | Powered by Stella 45 |

46 | 47 | ); 48 | } 49 | 50 | export default function Footer({ author, creativeCommons }: { author: string; creativeCommons: CreativeCommonsType }) { 51 | // Get the Creative Commons license type 52 | const creativeCommonsKey = creativeCommons as CreativeCommonsType | undefined; 53 | const creativeCommonsValue = creativeCommonsKey ? CreativeCommons[creativeCommonsKey] : undefined; 54 | 55 | // Build CC link and image path 56 | const ccName = creativeCommonsValue?.slice(3); // Remove 'cc-' prefix 57 | const ccLink = ccName ? `http://creativecommons.org/licenses/${ccName}/4.0/` : ""; 58 | const ccImage = creativeCommonsValue ? `/images/${creativeCommonsValue}.png` : ""; 59 | 60 | return ( 61 |
62 | {creativeCommons && ccImage ? : null} 63 | {author ? : null} 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/layouts/Article.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import MainLayout from "@/layouts/Main.astro"; 3 | import { formatLastModified } from "@/utils/lastModified"; 4 | import type { GitTime } from "@plugins/remark-modified-time"; 5 | import type { ReadingTime } from "@plugins/remark-reading-time"; 6 | import { Badge } from "@/components/ui/badge"; 7 | import Prose from "@/components/Prose.astro"; 8 | import { CalendarClock, Timer, Tag } from "lucide-react"; 9 | import IndicatorText from "@/components/indicator-text"; 10 | import Footer from "@/components/footer"; 11 | 12 | type Props = { 13 | frontmatter: { 14 | title: string; 15 | gitTimes: GitTime; 16 | readingTime?: ReadingTime; 17 | tags?: string[]; 18 | badge?: string; 19 | }; 20 | }; 21 | 22 | const { frontmatter } = Astro.props; 23 | const pageTitle = frontmatter.title; 24 | 25 | const { gitTimes, readingTime, tags, badge } = frontmatter; 26 | const lastModified = formatLastModified(gitTimes); 27 | 28 | // TODO: support config 29 | const author = "jctaoo"; 30 | const creativeCommons = "byNcSa"; 31 | --- 32 | 33 | 34 |
35 |
36 |
37 | {badge && {badge}} 38 |
39 |

40 | {pageTitle} 41 |

42 |
43 | 44 | 45 | 46 | {lastModified} 47 | 48 | 49 | 50 | { 51 | readingTime?.text && ( 52 | 53 | 54 | {readingTime.text} 55 | 56 | ) 57 | } 58 | 59 | 60 | { 61 | tags?.map((tag) => ( 62 | 63 | 64 | {tag} 65 | 66 | )) 67 | } 68 |
69 |
70 | 71 | 72 | 73 | 74 | 75 |
76 |
77 |
78 |
79 |
80 | -------------------------------------------------------------------------------- /src/components/indicator-text.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "../lib/utils"; 2 | 3 | interface IndicatorTextProps { 4 | children: string; 5 | active?: boolean; 6 | className?: string; 7 | size?: "sm" | "md" | "lg" | "xl"; 8 | 9 | interactiveIndicator?: boolean; 10 | } 11 | 12 | function IndicatorText({ 13 | children, 14 | active = false, 15 | className, 16 | size = "lg", 17 | interactiveIndicator: interactive = false, 18 | }: IndicatorTextProps) { 19 | const sizeClasses = { 20 | sm: "text-base", 21 | md: "text-lg", 22 | lg: "text-xl", 23 | xl: "text-2xl", 24 | }; 25 | 26 | const indicatorNormalHeightClasses = { 27 | sm: "h-[3px] max-sm:h-[3px]", 28 | md: "h-[5px] max-sm:h-[3px]", 29 | lg: "h-[5px] max-sm:h-[3px]", 30 | xl: "h-[6px] max-sm:h-[4px]", 31 | }; 32 | 33 | const indicatorActiveHeightClasses = { 34 | sm: [interactive ? "h-[6px]" : "h-[11px]", "group-hover:h-[11px]"], 35 | md: [interactive ? "h-[8px]" : "h-[13px]", "group-hover:h-[13px]"], 36 | lg: [interactive ? "h-[10px]" : "h-[15px]", "group-hover:h-[15px]"], 37 | xl: [interactive ? "h-[12px]" : "h-[19px]", "group-hover:h-[19px]"], 38 | }; 39 | 40 | return ( 41 |
48 | 65 | {children} 66 | 67 | 68 | 82 |
83 | ); 84 | } 85 | 86 | export default IndicatorText; 87 | -------------------------------------------------------------------------------- /.cursor/rules/shadcn-components.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | alwaysApply: true 3 | description: Shadcn UI 组件规范与 MCP 使用指引(优先使用 shadcn MCP 获取信息与命令) 4 | globs: **/* 5 | --- 6 | 7 | # Shadcn UI 组件使用与来源规则(MCP 增强) 8 | 9 | 本项目对 shadcn UI 的使用有强约束,且要求在涉及 shadcn 组件的任何任务时,**在必要的时候总是优先尝试使用 shadcn MCP 工具** 进行检索、命令生成与校验,再执行命令或实现代码。 10 | 11 | ## 基本规范 12 | 13 | - **包管理器统一使用 pnpm**。 14 | - **强制使用 CLI 管理组件**:`pnpm dlx shadcn@latest` 15 | - 添加:`pnpm dlx shadcn@latest add button input` 16 | - 覆盖更新:`pnpm dlx shadcn@latest add --overwrite button` 17 | - **禁止**: 18 | - 手动在 `src/components/ui` 下创建、拷贝、粘贴、重命名组件文件。 19 | - 从文档/第三方仓库直接复制 shadcn 组件源码。 20 | - 通过脚手架/代码生成器绕过 `shadcn` CLI 生成组件。 21 | - 已生成组件的修改: 22 | - 原则上不直接改动 `src/components/ui/*`。如需变更,优先修改 [components.json](mdc:components.json) 或通过 `add --overwrite` 重新生成。 23 | - 若必须本地化小范围样式,不能改变组件 API,并在提交中说明原因与范围。 24 | - 目录与约定: 25 | - 组件根目录:`src/components/ui` 26 | - 不在其他目录重复放置 shadcn 组件。 27 | - 代码评审检查: 28 | - PR 若新增/修改 `src/components/ui/*` 且非由 `pnpm dlx shadcn` 生成,应退回并改为通过 CLI 处理。 29 | - 确认组件清单与 [components.json](mdc:components.json) 一致。 30 | - 提交信息建议: 31 | - `chore(shadcn): add button via shadcn CLI` 32 | - `chore(shadcn): update button via shadcn --overwrite` 33 | 34 | ## MCP 工具使用(务必优先尝试) 35 | 36 | 当需要查找、添加、更新、核对 shadcn 组件时,**先使用 shadcn MCP 工具** 获取信息与生成命令,再执行 CLI: 37 | 38 | - 可用工具(按常见流程): 39 | - `get_project_registries`:读取项目已配置的 shadcn registry。 40 | - `list_items_in_registries`:列出可用组件项(支持分页)。 41 | - `search_items_in_registries`:按名称/描述模糊搜索组件。 42 | - `view_items_in_registries`:查看组件的详细信息、文件清单与说明。 43 | - `get_item_examples_from_registries`:获取组件示例与完整用法代码。 44 | - `get_add_command_for_items`:根据所选组件生成准确的 `shadcn add` 命令。 45 | - `get_audit_checklist`:生成变更后的核对清单,确保新增/更新后无遗漏。 46 | 47 | ### 典型操作流程 48 | 49 | - 添加组件: 50 | 1) 使用 `search_items_in_registries` 定位组件 → 2) 用 `view_items_in_registries` 确认内容与依赖 → 3) 用 `get_add_command_for_items` 生成命令 → 4) 执行生成的 `pnpm dlx shadcn@latest add `。 51 | 52 | - 更新组件: 53 | 1) 按上步骤检索 → 2) 使用 `get_add_command_for_items` 生成命令并添加 `--overwrite` → 3) 执行 `pnpm dlx shadcn@latest add --overwrite `。 54 | 55 | - 变更校验: 56 | - 运行 `get_audit_checklist`,并确保: 57 | - [components.json](mdc:components.json) 与实际组件文件一致; 58 | - 未直接手改 `src/components/ui/*`; 59 | - 依赖、样式、构建均正常。 60 | 61 | ### 执行约束与命令规范 62 | 63 | - 运行命令统一使用 `pnpm`,禁止 `npm/yarn`。 64 | - 执行时如需非交互,请添加对应的非交互参数(在自动化任务中务必考虑)。 65 | - 组件覆盖更新必须使用 `--overwrite`,避免手工同步差异。 66 | 67 | ## 参考与对齐 68 | 69 | - 组件清单与生成策略以 [components.json](mdc:components.json) 为准。 70 | - 组件目录:`src/components/ui`(请勿在其他目录重复放置)。 71 | - 出现与规则冲突的变更,应回滚并按 MCP + CLI 流程重新执行。 72 | 73 | > 提示:本规则为“总是应用”。当任务与 shadcn 组件相关时,应首先尝试使用上列 **shadcn MCP** 工具完成检索和命令生成,再执行具体变更。 -------------------------------------------------------------------------------- /src/content/posts/markdown-showcase.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown & LaTeX Showcase 3 | category: Demo 4 | tags: [markdown, latex, astro] 5 | abbr: A compact tour of Markdown features plus inline and block LaTeX. 6 | --- 7 | 8 | Welcome! This page demonstrates common Markdown features used in this site, plus LaTeX math. Use it as a reference when writing new content.[^credits] 9 | 10 | ## Headings 11 | 12 | ### H3 Heading 13 | 14 | #### H4 Heading 15 | 16 | ## Text styles 17 | 18 | You can write regular paragraphs with inline styles like **bold**, *italic*, and `inline code`. Links also work: [Astro](https://astro.build). 19 | 20 | > Blockquotes highlight important remarks or notes. 21 | 22 | 23 | --- 24 | 25 | ## Lists 26 | 27 | - Unordered item one 28 | - Unordered item two 29 | - Nested item 30 | 31 | 1. Ordered one 32 | 2. Ordered two 33 | 34 | - [x] Task done 35 | - [ ] Task pending 36 | 37 | --- 38 | 39 | ## Code blocks 40 | 41 | ```ts 42 | // TypeScript example 43 | type User = { 44 | id: string; 45 | name: string; 46 | }; 47 | 48 | export function greet(user: User): string { 49 | return `Hello, ${user.name}!`; 50 | } 51 | ``` 52 | 53 | ```bash 54 | # Shell example 55 | pnpm install 56 | pnpm run dev 57 | ``` 58 | 59 | --- 60 | 61 | ## Table 62 | 63 | | Feature | Supported | Notes | 64 | |----------|-----------|------------------------| 65 | | Headings | Yes | H2–H4 styled by Prose | 66 | | Lists | Yes | Ordered and unordered | 67 | | Code | Yes | Fenced with language | 68 | | Tables | Yes | Markdown tables | 69 | 70 | --- 71 | 72 | ## Image 73 | 74 | Local content assets can be referenced relative to this file: 75 | 76 | ![Sample image](./Hello%20World.jpg) 77 | 78 | --- 79 | 80 | ## LaTeX math 81 | 82 | Inline math uses dollar signs, for example: $E = mc^2$, or conditional probability \(P(A\mid B) = \frac{P(A \cap B)}{P(B)}\). 83 | 84 | Block math uses double dollar signs: 85 | 86 | $$ 87 | \int_{0}^{\infty} e^{-x^2} \, dx = \frac{\sqrt{\pi}}{2} 88 | $$ 89 | 90 | You can also align multiple equations: 91 | 92 | $$ 93 | \begin{aligned} 94 | \sum_{i=1}^{n} i &= \frac{n(n+1)}{2} \\ 95 | \int_{-\infty}^{\infty} e^{-x^2} \, dx &= \sqrt{\pi} 96 | \end{aligned} 97 | $$ 98 | 99 | Matrices are supported by many renderers: 100 | 101 | $$ 102 | \mathbf{A} = \begin{bmatrix} 103 | 1 & 2 \\ 104 | 3 & 4 105 | \end{bmatrix} 106 | $$ 107 | 108 | --- 109 | 110 | ## Footnotes 111 | 112 | Footnotes help with references and asides.[^aside] 113 | 114 | [^aside]: This is an example footnote. 115 | [^credits]: Content crafted for a quick feature tour. 116 | 117 | 118 | -------------------------------------------------------------------------------- /.rulesync/rules/shadcn-components.md: -------------------------------------------------------------------------------- 1 | --- 2 | targets: 3 | - '*' 4 | root: false 5 | description: Shadcn UI 组件规范与 MCP 使用指引(优先使用 shadcn MCP 获取信息与命令) 6 | globs: 7 | - '**/*' 8 | cursor: 9 | alwaysApply: true 10 | description: Shadcn UI 组件规范与 MCP 使用指引(优先使用 shadcn MCP 获取信息与命令) 11 | globs: 12 | - '**/*' 13 | --- 14 | # Shadcn UI 组件使用与来源规则(MCP 增强) 15 | 16 | 本项目对 shadcn UI 的使用有强约束,且要求在涉及 shadcn 组件的任何任务时,**在必要的时候总是优先尝试使用 shadcn MCP 工具** 进行检索、命令生成与校验,再执行命令或实现代码。 17 | 18 | ## 基本规范 19 | 20 | - **包管理器统一使用 pnpm**。 21 | - **强制使用 CLI 管理组件**:`pnpm dlx shadcn@latest` 22 | - 添加:`pnpm dlx shadcn@latest add button input` 23 | - 覆盖更新:`pnpm dlx shadcn@latest add --overwrite button` 24 | - **禁止**: 25 | - 手动在 `src/components/ui` 下创建、拷贝、粘贴、重命名组件文件。 26 | - 从文档/第三方仓库直接复制 shadcn 组件源码。 27 | - 通过脚手架/代码生成器绕过 `shadcn` CLI 生成组件。 28 | - 已生成组件的修改: 29 | - 原则上不直接改动 `src/components/ui/*`。如需变更,优先修改 [components.json](mdc:components.json) 或通过 `add --overwrite` 重新生成。 30 | - 若必须本地化小范围样式,不能改变组件 API,并在提交中说明原因与范围。 31 | - 目录与约定: 32 | - 组件根目录:`src/components/ui` 33 | - 不在其他目录重复放置 shadcn 组件。 34 | - 代码评审检查: 35 | - PR 若新增/修改 `src/components/ui/*` 且非由 `pnpm dlx shadcn` 生成,应退回并改为通过 CLI 处理。 36 | - 确认组件清单与 [components.json](mdc:components.json) 一致。 37 | - 提交信息建议: 38 | - `chore(shadcn): add button via shadcn CLI` 39 | - `chore(shadcn): update button via shadcn --overwrite` 40 | 41 | ## MCP 工具使用(务必优先尝试) 42 | 43 | 当需要查找、添加、更新、核对 shadcn 组件时,**先使用 shadcn MCP 工具** 获取信息与生成命令,再执行 CLI: 44 | 45 | - 可用工具(按常见流程): 46 | - `get_project_registries`:读取项目已配置的 shadcn registry。 47 | - `list_items_in_registries`:列出可用组件项(支持分页)。 48 | - `search_items_in_registries`:按名称/描述模糊搜索组件。 49 | - `view_items_in_registries`:查看组件的详细信息、文件清单与说明。 50 | - `get_item_examples_from_registries`:获取组件示例与完整用法代码。 51 | - `get_add_command_for_items`:根据所选组件生成准确的 `shadcn add` 命令。 52 | - `get_audit_checklist`:生成变更后的核对清单,确保新增/更新后无遗漏。 53 | 54 | ### 典型操作流程 55 | 56 | - 添加组件: 57 | 1) 使用 `search_items_in_registries` 定位组件 → 2) 用 `view_items_in_registries` 确认内容与依赖 → 3) 用 `get_add_command_for_items` 生成命令 → 4) 执行生成的 `pnpm dlx shadcn@latest add `。 58 | 59 | - 更新组件: 60 | 1) 按上步骤检索 → 2) 使用 `get_add_command_for_items` 生成命令并添加 `--overwrite` → 3) 执行 `pnpm dlx shadcn@latest add --overwrite `。 61 | 62 | - 变更校验: 63 | - 运行 `get_audit_checklist`,并确保: 64 | - [components.json](mdc:components.json) 与实际组件文件一致; 65 | - 未直接手改 `src/components/ui/*`; 66 | - 依赖、样式、构建均正常。 67 | 68 | ### 执行约束与命令规范 69 | 70 | - 运行命令统一使用 `pnpm`,禁止 `npm/yarn`。 71 | - 执行时如需非交互,请添加对应的非交互参数(在自动化任务中务必考虑)。 72 | - 组件覆盖更新必须使用 `--overwrite`,避免手工同步差异。 73 | 74 | ## 参考与对齐 75 | 76 | - 组件清单与生成策略以 [components.json](mdc:components.json) 为准。 77 | - 组件目录:`src/components/ui`(请勿在其他目录重复放置)。 78 | - 出现与规则冲突的变更,应回滚并按 MCP + CLI 流程重新执行。 79 | 80 | > 提示:本规则为“总是应用”。当任务与 shadcn 组件相关时,应首先尝试使用上列 **shadcn MCP** 工具完成检索和命令生成,再执行具体变更。 81 | -------------------------------------------------------------------------------- /plugin/remark-modified-time.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | import path from "path"; 3 | import type { Plugin } from "unified"; 4 | import { execFile } from "node:child_process"; 5 | import { promisify } from "node:util"; 6 | import fs from "node:fs/promises"; 7 | 8 | const pExecFile = promisify(execFile); 9 | 10 | export interface GitTimeOptions { 11 | /** 12 | * Git 仓库根目录(默认 process.cwd()) 13 | */ 14 | repoRoot?: string; 15 | /** 16 | * 当无法从 git 获得时间时,是否回退到 fs.stat 的 mtime/ctime 17 | */ 18 | fallbackToFs?: boolean; 19 | } 20 | 21 | export type GitTime = { 22 | createdAt?: string; 23 | updatedAt?: string; 24 | }; 25 | 26 | const cache = new Map>(); 27 | 28 | async function getGitTime(absPath: string, options: Required): Promise { 29 | const { repoRoot, fallbackToFs } = options; 30 | const rel = path.relative(repoRoot, absPath); 31 | 32 | // 并行两次 git log: 33 | // 1) 最近一次提交时间 34 | const lastArgs = [ 35 | "-C", 36 | repoRoot, 37 | "log", 38 | "-1", 39 | "--date=iso-strict", 40 | "--format=%ad", 41 | "--follow", 42 | "--", 43 | rel, 44 | ]; 45 | 46 | // 2) 首次(A=added)提交时间 47 | const firstArgs = [ 48 | "-C", 49 | repoRoot, 50 | "log", 51 | "--diff-filter=A", 52 | "--follow", 53 | "--date=iso-strict", 54 | "--format=%ad", 55 | "-1", 56 | "--", 57 | rel, 58 | ]; 59 | 60 | const [lastResult, firstResult] = await Promise.all([ 61 | pExecFile("git", lastArgs), 62 | pExecFile("git", firstArgs), 63 | ]); 64 | 65 | const createdAt = firstResult.stdout.trim(); 66 | const updatedAt = lastResult.stdout.trim(); 67 | 68 | if (createdAt || updatedAt) { 69 | return { 70 | createdAt, 71 | updatedAt, 72 | }; 73 | } 74 | 75 | if (fallbackToFs) { 76 | const stats = await fs.stat(absPath); 77 | return { 78 | createdAt: stats.ctime.toISOString(), 79 | updatedAt: stats.mtime.toISOString(), 80 | }; 81 | } 82 | 83 | return {}; 84 | } 85 | 86 | export const remarkModifiedTime: Plugin<[GitTimeOptions]> = function (options) { 87 | const { repoRoot = process.cwd(), fallbackToFs = true } = options; 88 | 89 | return async (tree, file) => { 90 | const absPath = file.path ? path.resolve(file.path) : undefined; 91 | if (!absPath) return; 92 | 93 | const key = `${absPath}|${repoRoot}|${fallbackToFs}`; 94 | const timesPromise = cache.get(key) ?? getGitTime(absPath, { repoRoot, fallbackToFs }); 95 | cache.set(key, timesPromise); 96 | 97 | const times = await timesPromise; 98 | 99 | if (file.data.astro && file.data.astro.frontmatter) { 100 | file.data.astro.frontmatter.gitTimes = times; 101 | } 102 | }; 103 | }; 104 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | Please also reference the following documents as needed. In this case, `@` stands for the project root directory. 2 | 3 | 4 | 5 | @.codex\memories\astro-development.md 6 | **/* 7 | 8 | 9 | @.codex\memories\content-authoring.md 10 | Content authoring rules aligned with src/content.config.ts schemas 11 | src/content/**/*.md, src/content/**/*.mdx 12 | 13 | 14 | @.codex\memories\shadcn-components.md 15 | Shadcn UI 组件规范与 MCP 使用指引(优先使用 shadcn MCP 获取信息与命令) 16 | **/* 17 | 18 | 19 | @.codex\memories\site-config.md 20 | 站点配置中心化(src/siteConfig.ts)与避免硬编码的约定 21 | **/* 22 | 23 | 24 | 25 | # Additional Conventions Beyond the Built-in Functions 26 | 27 | As this project's AI coding tool, you must follow the additional conventions below, in addition to the built-in functions.# 项目结构与开发约定(Stella / Astro) 28 | 29 | - 包管理器:必须使用 pnpm(见 [package.json](mdc:package.json) 中 `packageManager` 字段)。 30 | - 安装依赖:`pnpm install` 31 | - 本地开发:`pnpm dev` 32 | - 构建产物:`pnpm build` 33 | - 本地预览:`pnpm preview` 34 | 35 | ## 关键配置与入口 36 | - Astro 配置:[astro.config.mjs](mdc:astro.config.mjs) 37 | - 包与脚本:[package.json](mdc:package.json) 38 | - TypeScript 配置:[tsconfig.json](mdc:tsconfig.json) 39 | 40 | ## 目录总览(./src) 41 | - 组件:`src/components`(通用组件与 `ui/` 下的 shadcn 组件) 42 | - 内容集合:`src/content`(Markdown/资产内容) 43 | - 内容模型定义:[src/content.config.ts](mdc:src/content.config.ts) 44 | - 布局:`src/layouts`(`main.astro`、`pages.astro`、`article.astro`) 45 | - 路由页面:`src/pages`(基于文件系统路由) 46 | - 样式:`src/styles`(`global.css`) 47 | - 工具库:`src/utils`、`src/lib` 48 | - 自定义插件:`plugin/`(`remark-modified-time.ts`、`remark-reading-time.ts`) 49 | - 静态资源:`public/` 50 | 51 | ## 基础路由(基于 src/pages) 52 | - `/` → [src/pages/index.astro](mdc:src/pages/index.astro) 53 | - `/about` → [src/pages/about.mdx](mdc:src/pages/about.mdx) 54 | - `/posts` 列表 → [src/pages/posts/index.astro](mdc:src/pages/posts/index.astro) 55 | - `/posts/:slug...` 详情(动态) → [src/pages/posts/[...slug].astro](mdc:src/pages/posts/[...slug].astro) 56 | - `/snippets` 列表 → [src/pages/snippets/index.astro](mdc:src/pages/snippets/index.astro) 57 | - `/snippets/:slug...` 详情(动态) → [src/pages/snippets/[...slug].astro](mdc:src/pages/snippets/[...slug].astro) 58 | 59 | ## 内容组织(src/content) 60 | - 文章集合:`src/content/posts`(含 `.md` 与配图等资产) 61 | - 片段集合:`src/content/snippets` 62 | - 集合/字段等规范在 [src/content.config.ts](mdc:src/content.config.ts) 中定义与校验。 63 | 64 | ## shadcn 组件使用 65 | - 严格遵循规则见:[shadcn-components.mdc](mdc:shadcn-components.mdc) 66 | 67 | ## Astro 研发流程 68 | - 新增 Astro 能力时遵循:[astro-development.mdc](mdc:astro-development.mdc) 69 | 70 | ## 其他约定 71 | - 禁止使用 npm/yarn 进行安装与脚本执行;统一使用 pnpm。 72 | - 代码格式:使用 Prettier;修复命令:`pnpm run fix:prettier`。 -------------------------------------------------------------------------------- /.codex/memories/site-config.md: -------------------------------------------------------------------------------- 1 | ## 目的 2 | 3 | 统一在 [src/siteConfig.ts](mdc:src/siteConfig.ts) 中集中定义站点层级的所有可配置项(展示文案、开关、元信息、链接等),通过类型与校验保障一致性,避免在页面/布局/组件中出现硬编码,提升全站可配置性与可维护性。 4 | 5 | ## 强制要求(必须遵守) 6 | 7 | - 所有站点级配置项必须定义在 `siteConfigSchema` 中,并通过 `siteConfig` 导出后被消费。 8 | - 在 `.astro`、`.tsx`、`.ts` 中引用配置时,统一使用路径别名导入: 9 | 10 | ```ts 11 | import { siteConfig, type SiteConfig } from "@/siteConfig"; 12 | ``` 13 | 14 | - 禁止在 `src/pages`、`src/layouts`、`src/components` 等目录中直接硬编码站点名称、描述、社交链接、默认标题/描述、主题缺省、版权/许可、分析 ID、RSS/Feed 开关等。 15 | - 新增/变更配置项时: 16 | - 同步更新 `siteConfigSchema`(使用 `z` 添加类型与 `.describe()` 描述); 17 | - 给出合理默认值(`.default(...)`)或明确 `optional()` 并在使用端处理兜底; 18 | - 仅从 `siteConfig` 使用,不绕过类型直接访问未声明字段。 19 | - 仅在需要修改/扩展配置模型时才导入 `siteConfigSchema`;业务使用端不得依赖或修改 schema。 20 | 21 | ## 新增配置项规范 22 | 23 | 在 [src/siteConfig.ts](mdc:src/siteConfig.ts) 中: 24 | 25 | ```ts 26 | import { z } from "astro:schema"; 27 | 28 | const siteConfigSchema = z.object({ 29 | siteName: z.string().describe("站点名称").default("Jctaoo."), 30 | bannerText: z.string().describe("横幅文案").optional(), 31 | 32 | // 示例:新增字段请提供类型、描述与默认值或 optional 33 | description: z.string().describe("默认站点描述").default("A modern Astro site"), 34 | baseUrl: z.string().url().describe("站点基础 URL(不含末尾斜杠)").optional(), 35 | defaultTheme: z.enum(["system", "light", "dark"]).describe("默认主题").default("system"), 36 | social: z 37 | .object({ github: z.string().url().optional(), twitter: z.string().url().optional() }) 38 | .default({}), 39 | }); 40 | 41 | export type SiteConfig = z.infer; 42 | 43 | export const siteConfig = siteConfigSchema.parse({ 44 | // 在此集中覆写实际值;缺省项走默认值 45 | bannerText: "This is a demo site.", 46 | description: "A modern Astro site", 47 | defaultTheme: "system", 48 | social: { github: "https://github.com/your/repo" }, 49 | }); 50 | ``` 51 | 52 | 规范要点: 53 | 54 | - 为每个新字段写清 `.describe()`,便于后续维护与自动文档化。 55 | - 能给默认值就给默认值;不可给出合理默认的字段才标记为 `optional()`。 56 | - 需要从环境变量读取的敏感信息(如 API Key)仍应置于 `import.meta.env.*`,但“是否启用”之类的站点级开关可进入 `siteConfig`。 57 | 58 | ## 使用示例 59 | 60 | - 在 `.astro` 中使用: 61 | 62 | ```astro 63 | --- 64 | import { siteConfig } from "@/siteConfig"; 65 | const title = siteConfig.siteName; 66 | const banner = siteConfig.bannerText ?? ""; 67 | --- 68 |

{title}

69 | {banner &&

{banner}

} 70 | ``` 71 | 72 | - 在 `tsx` 组件中使用: 73 | 74 | ```tsx 75 | import { siteConfig } from "@/siteConfig"; 76 | 77 | export function HeaderTitle() { 78 | return {siteConfig.siteName}; 79 | } 80 | ``` 81 | 82 | ## 典型适用范围(必须走配置) 83 | 84 | - 站点名称、描述、副标题、版权与许可、社交链接、页脚/页眉文案 85 | - 默认 SEO 元信息与分享图、标题模板 86 | - 主题默认值、是否展示 banner、UI 显示/隐藏开关 87 | - RSS/Feed 是否开启、条数限制等 88 | - 第三方分析/脚本开关与标识(非密钥) 89 | 90 | ## 违例与迁移指引 91 | 92 | - 若发现硬编码的站点文案/链接/标题模板/开关: 93 | 1) 在 `siteConfigSchema` 中新增字段并赋默认值; 94 | 2) 在 `siteConfig.parse({...})` 中填充项目实际值; 95 | 3) 将使用处替换为 `siteConfig.` 引用。 96 | 97 | ## 评审检查清单(PR Reviewer) 98 | 99 | - 新增或更改站点级文案/开关是否进入了 [src/siteConfig.ts](mdc:src/siteConfig.ts)? 100 | - 是否为新字段提供了类型、`.describe()` 与默认值/可选性? 101 | - 业务代码是否统一通过 `@/siteConfig` 读取,避免相对路径? 102 | - 是否移除了页面/组件中的站点级硬编码? 103 | - 是否为可选字段在使用端做了兜底处理(如 `??`、显式分支)? 104 | 105 | ## 备注 106 | 107 | - 路径别名见 [tsconfig.json](mdc:tsconfig.json) 中 `baseUrl` 与 `paths`(`@/*` → `src/*`)。 108 | - 若配置变更影响运行时/构建行为(如适配器、输出目标),需同步评估与验证 [astro.config.mjs](mdc:astro.config.mjs) 与构建产物。 -------------------------------------------------------------------------------- /.cursor/rules/site-config.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | alwaysApply: true 3 | description: 站点配置中心化(src/siteConfig.ts)与避免硬编码的约定 4 | globs: **/* 5 | --- 6 | 7 | ## 目的 8 | 9 | 统一在 [src/siteConfig.ts](mdc:src/siteConfig.ts) 中集中定义站点层级的所有可配置项(展示文案、开关、元信息、链接等),通过类型与校验保障一致性,避免在页面/布局/组件中出现硬编码,提升全站可配置性与可维护性。 10 | 11 | ## 强制要求(必须遵守) 12 | 13 | - 所有站点级配置项必须定义在 `siteConfigSchema` 中,并通过 `siteConfig` 导出后被消费。 14 | - 在 `.astro`、`.tsx`、`.ts` 中引用配置时,统一使用路径别名导入: 15 | 16 | ```ts 17 | import { siteConfig, type SiteConfig } from "@/siteConfig"; 18 | ``` 19 | 20 | - 禁止在 `src/pages`、`src/layouts`、`src/components` 等目录中直接硬编码站点名称、描述、社交链接、默认标题/描述、主题缺省、版权/许可、分析 ID、RSS/Feed 开关等。 21 | - 新增/变更配置项时: 22 | - 同步更新 `siteConfigSchema`(使用 `z` 添加类型与 `.describe()` 描述); 23 | - 给出合理默认值(`.default(...)`)或明确 `optional()` 并在使用端处理兜底; 24 | - 仅从 `siteConfig` 使用,不绕过类型直接访问未声明字段。 25 | - 仅在需要修改/扩展配置模型时才导入 `siteConfigSchema`;业务使用端不得依赖或修改 schema。 26 | 27 | ## 新增配置项规范 28 | 29 | 在 [src/siteConfig.ts](mdc:src/siteConfig.ts) 中: 30 | 31 | ```ts 32 | import { z } from "astro:schema"; 33 | 34 | const siteConfigSchema = z.object({ 35 | siteName: z.string().describe("站点名称").default("Jctaoo."), 36 | bannerText: z.string().describe("横幅文案").optional(), 37 | 38 | // 示例:新增字段请提供类型、描述与默认值或 optional 39 | description: z.string().describe("默认站点描述").default("A modern Astro site"), 40 | baseUrl: z.string().url().describe("站点基础 URL(不含末尾斜杠)").optional(), 41 | defaultTheme: z.enum(["system", "light", "dark"]).describe("默认主题").default("system"), 42 | social: z 43 | .object({ github: z.string().url().optional(), twitter: z.string().url().optional() }) 44 | .default({}), 45 | }); 46 | 47 | export type SiteConfig = z.infer; 48 | 49 | export const siteConfig = siteConfigSchema.parse({ 50 | // 在此集中覆写实际值;缺省项走默认值 51 | bannerText: "This is a demo site.", 52 | description: "A modern Astro site", 53 | defaultTheme: "system", 54 | social: { github: "https://github.com/your/repo" }, 55 | }); 56 | ``` 57 | 58 | 规范要点: 59 | 60 | - 为每个新字段写清 `.describe()`,便于后续维护与自动文档化。 61 | - 能给默认值就给默认值;不可给出合理默认的字段才标记为 `optional()`。 62 | - 需要从环境变量读取的敏感信息(如 API Key)仍应置于 `import.meta.env.*`,但“是否启用”之类的站点级开关可进入 `siteConfig`。 63 | 64 | ## 使用示例 65 | 66 | - 在 `.astro` 中使用: 67 | 68 | ```astro 69 | --- 70 | import { siteConfig } from "@/siteConfig"; 71 | const title = siteConfig.siteName; 72 | const banner = siteConfig.bannerText ?? ""; 73 | --- 74 |

{title}

75 | {banner &&

{banner}

} 76 | ``` 77 | 78 | - 在 `tsx` 组件中使用: 79 | 80 | ```tsx 81 | import { siteConfig } from "@/siteConfig"; 82 | 83 | export function HeaderTitle() { 84 | return {siteConfig.siteName}; 85 | } 86 | ``` 87 | 88 | ## 典型适用范围(必须走配置) 89 | 90 | - 站点名称、描述、副标题、版权与许可、社交链接、页脚/页眉文案 91 | - 默认 SEO 元信息与分享图、标题模板 92 | - 主题默认值、是否展示 banner、UI 显示/隐藏开关 93 | - RSS/Feed 是否开启、条数限制等 94 | - 第三方分析/脚本开关与标识(非密钥) 95 | 96 | ## 违例与迁移指引 97 | 98 | - 若发现硬编码的站点文案/链接/标题模板/开关: 99 | 1) 在 `siteConfigSchema` 中新增字段并赋默认值; 100 | 2) 在 `siteConfig.parse({...})` 中填充项目实际值; 101 | 3) 将使用处替换为 `siteConfig.` 引用。 102 | 103 | ## 评审检查清单(PR Reviewer) 104 | 105 | - 新增或更改站点级文案/开关是否进入了 [src/siteConfig.ts](mdc:src/siteConfig.ts)? 106 | - 是否为新字段提供了类型、`.describe()` 与默认值/可选性? 107 | - 业务代码是否统一通过 `@/siteConfig` 读取,避免相对路径? 108 | - 是否移除了页面/组件中的站点级硬编码? 109 | - 是否为可选字段在使用端做了兜底处理(如 `??`、显式分支)? 110 | 111 | ## 备注 112 | 113 | - 路径别名见 [tsconfig.json](mdc:tsconfig.json) 中 `baseUrl` 与 `paths`(`@/*` → `src/*`)。 114 | - 若配置变更影响运行时/构建行为(如适配器、输出目标),需同步评估与验证 [astro.config.mjs](mdc:astro.config.mjs) 与构建产物。 -------------------------------------------------------------------------------- /src/content/posts/YuiToast document.md: -------------------------------------------------------------------------------- 1 | --- 2 | category: open-source 3 | title: YuiToast, a Lightweight iOS toast framework 4 | tags: 5 | - iOS 6 | abbr: "Features: Display toast (Can contain small pictures) in one;line code. Modify the toast content appeared on the screen easily. " 7 | --- 8 | 9 | # YuiToast 10 | 11 | A lightweight toast library for iOS. 12 | 13 | ## Requirements 14 | 15 | - iOS 11 or any higher version. 16 | - Preferably Swift 5.0 or any higher version. 17 | - The library has not been tested with Swift 4.x or any lower version. 18 | 19 | ## Features: 20 | 21 | - Display toast (Can contain small pictures) in one line code. 22 | - Modify the toast content appeared on the screen easily. 23 | 24 | ## Example Project 25 | 26 | The example project contains some usages of YuiToast. You can use and modify as your like. 27 | 28 | ### Example Project Installation 29 | 30 | Clone or download YuiToast first and run the following command: 31 | 32 | ```shell 33 | cd YuiToast/Example 34 | pod install 35 | ``` 36 | 37 | Then, open Example.xcworkspace. 38 | 39 | ## Installation 40 | 41 | ### CocoaPods 42 | 43 | [CocoaPods](https://cocoapods.org/) is a dependency manager for Cocoa projects. You can install it with the following command: 44 | 45 | ```shell 46 | gem install cocoapods 47 | ``` 48 | 49 | To integrate YuiToast into your Xcode project using CocoaPods, specify it in your Podfile: 50 | 51 | ```Ruby 52 | platform :ios, '11.0' 53 | 54 | target 'Your Project Name' do 55 | use_frameworks! 56 | # Pods for Your Project Name 57 | pod 'YuiToast', '0.1.0' 58 | end 59 | ``` 60 | 61 | Then, run the following command: 62 | 63 | ```shell 64 | pod install 65 | ``` 66 | 67 | ### Accio (Also SwiftPM) 68 | 69 | [Accio](https://github.com/JamitLabs/Accio) is a decentralized dependency manager driven by SwiftPM that works for iOS/tvOS/watchOS/macOS projects. 70 | You can install Accio with [Homebrew](http://brew.sh/) using the following command: (If you use SwiftPM, it's not necessary) 71 | 72 | ```shell 73 | brew tap JamitLabs/Accio https://github.com/JamitLabs/Accio.git 74 | brew install accio 75 | ``` 76 | 77 | To integrate YuiToast into your Xcode project using Accio or SwiftPM, specify the following in your Package.swift manifest: 78 | 79 | ```swift 80 | .package(url: "https://github.com/jctaoo/YuiToast", .exact("0.1.0")) 81 | ``` 82 | 83 | If you use Accio, run: 84 | 85 | ```shell 86 | accio install 87 | ``` 88 | 89 | ## Usage 90 | 91 | ### Show text toast: 92 | 93 | ```swift 94 | import YuiToast 95 | 96 | Toast.default.show(message: "Hello World") 97 | ``` 98 | 99 | ### Show toast with text and image: 100 | 101 | ```swift 102 | import YuiToast 103 | 104 | Toast.default.show(message: "Toast With Image", image: UIImage(named: "DemoImage")) 105 | ``` 106 | 107 | ### Set the timing of disappearance 108 | 109 | ```swift 110 | import YuiToast 111 | 112 | Toast.default.show(message: "Hello World", duration: .never) // never disappear 113 | Toast.default.show(message: "Hello World", duration: .timeInterval(duration: 3)) // disappears after 3 seconds 114 | ``` 115 | 116 | ### Show any custom ToastItem 117 | 118 | ```swift 119 | func show(_ toastItem: ToastItem) 120 | ``` 121 | 122 | ### Modify appeared Toast 123 | 124 | ```swift 125 | import YuiToast 126 | 127 | let item = Toast.defaut.show(...) 128 | Toast.default.update(item: item) { item in 129 | item.title = "updated" 130 | } 131 | ``` 132 | 133 | ## License 134 | 135 | Copyright (c) 2020 Tao Juncheng 136 | 137 | [LICENSE](/LICENSE) file 138 | -------------------------------------------------------------------------------- /.rulesync/rules/site-config.md: -------------------------------------------------------------------------------- 1 | --- 2 | targets: 3 | - '*' 4 | root: false 5 | description: 站点配置中心化(src/siteConfig.ts)与避免硬编码的约定 6 | globs: 7 | - '**/*' 8 | cursor: 9 | alwaysApply: true 10 | description: 站点配置中心化(src/siteConfig.ts)与避免硬编码的约定 11 | globs: 12 | - '**/*' 13 | --- 14 | ## 目的 15 | 16 | 统一在 [src/siteConfig.ts](mdc:src/siteConfig.ts) 中集中定义站点层级的所有可配置项(展示文案、开关、元信息、链接等),通过类型与校验保障一致性,避免在页面/布局/组件中出现硬编码,提升全站可配置性与可维护性。 17 | 18 | ## 强制要求(必须遵守) 19 | 20 | - 所有站点级配置项必须定义在 `siteConfigSchema` 中,并通过 `siteConfig` 导出后被消费。 21 | - 在 `.astro`、`.tsx`、`.ts` 中引用配置时,统一使用路径别名导入: 22 | 23 | ```ts 24 | import { siteConfig, type SiteConfig } from "@/siteConfig"; 25 | ``` 26 | 27 | - 禁止在 `src/pages`、`src/layouts`、`src/components` 等目录中直接硬编码站点名称、描述、社交链接、默认标题/描述、主题缺省、版权/许可、分析 ID、RSS/Feed 开关等。 28 | - 新增/变更配置项时: 29 | - 同步更新 `siteConfigSchema`(使用 `z` 添加类型与 `.describe()` 描述); 30 | - 给出合理默认值(`.default(...)`)或明确 `optional()` 并在使用端处理兜底; 31 | - 仅从 `siteConfig` 使用,不绕过类型直接访问未声明字段。 32 | - 仅在需要修改/扩展配置模型时才导入 `siteConfigSchema`;业务使用端不得依赖或修改 schema。 33 | 34 | ## 新增配置项规范 35 | 36 | 在 [src/siteConfig.ts](mdc:src/siteConfig.ts) 中: 37 | 38 | ```ts 39 | import { z } from "astro:schema"; 40 | 41 | const siteConfigSchema = z.object({ 42 | siteName: z.string().describe("站点名称").default("Jctaoo."), 43 | bannerText: z.string().describe("横幅文案").optional(), 44 | 45 | // 示例:新增字段请提供类型、描述与默认值或 optional 46 | description: z.string().describe("默认站点描述").default("A modern Astro site"), 47 | baseUrl: z.string().url().describe("站点基础 URL(不含末尾斜杠)").optional(), 48 | defaultTheme: z.enum(["system", "light", "dark"]).describe("默认主题").default("system"), 49 | social: z 50 | .object({ github: z.string().url().optional(), twitter: z.string().url().optional() }) 51 | .default({}), 52 | }); 53 | 54 | export type SiteConfig = z.infer; 55 | 56 | export const siteConfig = siteConfigSchema.parse({ 57 | // 在此集中覆写实际值;缺省项走默认值 58 | bannerText: "This is a demo site.", 59 | description: "A modern Astro site", 60 | defaultTheme: "system", 61 | social: { github: "https://github.com/your/repo" }, 62 | }); 63 | ``` 64 | 65 | 规范要点: 66 | 67 | - 为每个新字段写清 `.describe()`,便于后续维护与自动文档化。 68 | - 能给默认值就给默认值;不可给出合理默认的字段才标记为 `optional()`。 69 | - 需要从环境变量读取的敏感信息(如 API Key)仍应置于 `import.meta.env.*`,但“是否启用”之类的站点级开关可进入 `siteConfig`。 70 | 71 | ## 使用示例 72 | 73 | - 在 `.astro` 中使用: 74 | 75 | ```astro 76 | --- 77 | import { siteConfig } from "@/siteConfig"; 78 | const title = siteConfig.siteName; 79 | const banner = siteConfig.bannerText ?? ""; 80 | --- 81 |

{title}

82 | {banner &&

{banner}

} 83 | ``` 84 | 85 | - 在 `tsx` 组件中使用: 86 | 87 | ```tsx 88 | import { siteConfig } from "@/siteConfig"; 89 | 90 | export function HeaderTitle() { 91 | return {siteConfig.siteName}; 92 | } 93 | ``` 94 | 95 | ## 典型适用范围(必须走配置) 96 | 97 | - 站点名称、描述、副标题、版权与许可、社交链接、页脚/页眉文案 98 | - 默认 SEO 元信息与分享图、标题模板 99 | - 主题默认值、是否展示 banner、UI 显示/隐藏开关 100 | - RSS/Feed 是否开启、条数限制等 101 | - 第三方分析/脚本开关与标识(非密钥) 102 | 103 | ## 违例与迁移指引 104 | 105 | - 若发现硬编码的站点文案/链接/标题模板/开关: 106 | 1) 在 `siteConfigSchema` 中新增字段并赋默认值; 107 | 2) 在 `siteConfig.parse({...})` 中填充项目实际值; 108 | 3) 将使用处替换为 `siteConfig.` 引用。 109 | 110 | ## 评审检查清单(PR Reviewer) 111 | 112 | - 新增或更改站点级文案/开关是否进入了 [src/siteConfig.ts](mdc:src/siteConfig.ts)? 113 | - 是否为新字段提供了类型、`.describe()` 与默认值/可选性? 114 | - 业务代码是否统一通过 `@/siteConfig` 读取,避免相对路径? 115 | - 是否移除了页面/组件中的站点级硬编码? 116 | - 是否为可选字段在使用端做了兜底处理(如 `??`、显式分支)? 117 | 118 | ## 备注 119 | 120 | - 路径别名见 [tsconfig.json](mdc:tsconfig.json) 中 `baseUrl` 与 `paths`(`@/*` → `src/*`)。 121 | - 若配置变更影响运行时/构建行为(如适配器、输出目标),需同步评估与验证 [astro.config.mjs](mdc:astro.config.mjs) 与构建产物。 122 | -------------------------------------------------------------------------------- /src/pages/posts/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import MainLayout from "@/layouts/Main.astro"; 3 | import { getCollection } from "astro:content"; 4 | import { z } from "astro:schema"; 5 | import { Badge } from "@/components/ui/badge"; 6 | import ContentFilterButton from "@/components/content-filter-button"; 7 | import { getLastModified } from "@/utils/lastModified"; 8 | import { isLooseEqual } from "@/utils/string"; 9 | import { Tag } from "lucide-react"; 10 | import IndicatorText from "@/components/indicator-text"; 11 | 12 | export const prerender = false; 13 | 14 | const paramsSchema = z.object({ 15 | category: z.string().optional(), 16 | tags: z 17 | .string() 18 | .transform((val) => val.split(",")) 19 | .pipe(z.array(z.string())) 20 | .optional(), 21 | }); 22 | const params = paramsSchema.parse(Object.fromEntries(Astro.url.searchParams)); 23 | 24 | const posts = await getCollection("posts", ({ data }) => { 25 | const categoryOk = params.category ? isLooseEqual(data.category, params.category) : true; 26 | const tagsOk = params.tags 27 | ? params.tags.every((tag) => { 28 | return data.tags?.some((t) => isLooseEqual(t, tag)); 29 | }) 30 | : true; 31 | return categoryOk && tagsOk; 32 | }); 33 | const pageTitle = "Posts"; 34 | 35 | // Build filter options from all posts (unfiltered) 36 | const allPosts = await getCollection("posts"); 37 | const allCategories = Array.from(new Set(allPosts.map((p) => p.data.category).filter(Boolean) as string[])); 38 | const allTags = Array.from( 39 | new Set(allPosts.flatMap((p) => (p.data.tags ? (p.data.tags as string[]) : [])).filter(Boolean) as string[]), 40 | ); 41 | --- 42 | 43 | 44 | 98 | 99 | -------------------------------------------------------------------------------- /src/pages/snippets/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import MainLayout from "@/layouts/Main.astro"; 3 | import { getCollection, type CollectionEntry, type InferEntrySchema, type Render, type RenderedContent } from "astro:content"; 4 | import { Card, CardHeader, CardTitle, CardContent, CardDescription, CardFooter } from "@/components/ui/card"; 5 | import { Badge } from "@/components/ui/badge"; 6 | import { getLastModified } from "@/utils/lastModified"; 7 | import { z } from "astro:schema"; 8 | import { isLooseEqual } from "@/utils/string"; 9 | import ContentFilterButton from "@/components/content-filter-button"; 10 | import IndicatorText from "@/components/indicator-text"; 11 | import { Tag } from "lucide-react"; 12 | 13 | export const prerender = false; 14 | const paramsSchema = z.object({ 15 | category: z.string().optional(), 16 | tags: z.string().transform((val) => val.split(",")).pipe(z.array(z.string())).optional(), 17 | }); 18 | const params = paramsSchema.parse(Object.fromEntries(Astro.url.searchParams)); 19 | 20 | const snippets = await getCollection("snippets", ({ data }) => { 21 | const categoryOk = params.category ? isLooseEqual(data.category, params.category) : true; 22 | const tagsOk = params.tags 23 | ? params.tags.every((tag) => { 24 | return data.tags?.some((t) => isLooseEqual(t, tag)); 25 | }) 26 | : true; 27 | return categoryOk && tagsOk; 28 | }); 29 | const pageTitle = "Snippets"; 30 | 31 | // Build filter options from all snippets 32 | const allSnippets = await getCollection("snippets"); 33 | const allCategories = Array.from( 34 | new Set(allSnippets.map((s) => s.data.category).filter(Boolean) as string[]) 35 | ); 36 | const allTags = Array.from( 37 | new Set( 38 | allSnippets 39 | .flatMap((s) => (s.data.tags ? (s.data.tags as string[]) : [])) 40 | .filter(Boolean) as string[] 41 | ) 42 | ); 43 | --- 44 | 45 | 46 | 99 | 100 | -------------------------------------------------------------------------------- /src/pages/about.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | layout: "@/layouts/Pages.astro" 3 | title: about me 4 | updateDates: 5 | - 2020-06-15 6 | --- 7 | 8 | ```swift 9 | struct Jctaoo: Boy, Student, Developer { 10 | var email: String { 11 | "jctaoo@outlook.com" 12 | } 13 | 14 | static let unique = Jctaoo() 15 | private init() {} 16 | 17 | var mostInterestLangs: [Language] { 18 | [.swift, .rust] 19 | } 20 | 21 | func sayHi() { 22 | """ 23 | 👋 Hi there, I'm jctaoo. 24 | A high school student from China with a passion for developing some 25 | interesting programs and fascinated by everything that's CS related. 26 | """.announce() 27 | focusing(on: "Swift Student Challenage").say() 28 | work(in: "🚀fusée code lab", to: "make fantastic open-source programs").say() 29 | recently { 30 | learning { 31 | "Algorithms" 32 | "Compilation Principles" 33 | } 34 | }.say() 35 | bigFans(to: "Detective Conan").say() 36 | "✨ See my pined project below, you may lik these.".happyToSay() 37 | } 38 | } 39 | 40 | mail(to: Jctaoo.unique) 41 | 42 | ``` 43 | 44 | [run result](./result.md) 45 | 46 |

47 | 48 |

49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |

76 |

77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stella 2 | 3 | Welcome to Stella, a modern blog project built with Astro. This README provides a guide to help you get started quickly. 4 | 5 | ## 🚀 Get Started 6 | 7 | To get started with this project, you can either create a new repository from this template or clone it directly. 8 | 9 | - **Use this template**: Click the `Use this template` button on the GitHub page to create a new repository with the same structure and files. 10 | - **Clone the repository**: If you prefer to work with a local copy, you can clone the repository to your machine. 11 | 12 | After setting up the repository, follow these steps to run the project locally. This project uses `pnpm` as the package manager. 13 | 14 | 1. **Install dependencies**: 15 | ```bash 16 | pnpm install 17 | ``` 18 | 19 | 2. **Start the development server**: 20 | ```bash 21 | pnpm dev 22 | ``` 23 | 24 | 3. **Open in browser**: 25 | Once the server is running, you can view the site at `http://localhost:4321`. 26 | 27 | ## ✍️ Add Content 28 | 29 | This project uses [Astro's Content Collections](https://docs.astro.build/en/guides/content-collections/) to manage and validate your content. All content is stored in the `src/content/` directory. 30 | 31 | ### Posts 32 | 33 | - **Location**: Add new blog posts to `src/content/posts/`. 34 | - **Format**: Use Markdown (`.md`) or MDX (`.mdx`). 35 | - **Frontmatter**: 36 | ```yaml 37 | --- 38 | title: "My New Blog Post" # Required 39 | category: "Technology" 40 | tags: ["astro", "example", "blog"] 41 | topImage: "/images/post-banner.jpg" 42 | topImageAlt: "A descriptive alt text for the image" 43 | --- 44 | 45 | Your content starts here... 46 | ``` 47 | 48 | ### Snippets 49 | 50 | - **Location**: Add short code snippets or notes to `src/content/snippets/`. 51 | - **Format**: Also uses Markdown or MDX. 52 | - **Frontmatter**: 53 | ```yaml 54 | --- 55 | title: "My Awesome Snippet" # Required 56 | description: "A short description of what this snippet does." 57 | tags: ["javascript", "react"] 58 | --- 59 | 60 | // Your code snippet here 61 | ``` 62 | 63 | ## 📄 Custom Pages 64 | 65 | You can easily add new static pages (like an "About" or "Contact" page). 66 | 67 | - **Location**: Create a new file in the `src/pages/` directory. The filename will become the page's URL. For example, `src/pages/about.mdx` will be available at `/about`. 68 | - **Format**: You can use `.astro`, `.md`, or `.mdx` files. 69 | - **Example**: To create an "About" page, you can create a `src/pages/about.mdx` file with the following content: 70 | ```mdx 71 | --- 72 | layout: "@/layouts/Pages.astro" # Use a pre-defined layout 73 | title: "About Me" 74 | --- 75 | 76 | This is my about page. Here I can write about myself. 77 | ``` 78 | 79 | ## ⚙️ Configuration 80 | 81 | All global site configurations are centralized in a single file for easy management. 82 | 83 | - **Location**: `src/siteConfig.ts` 84 | - **What to change**: You can modify the site's name, banner text, and other site-wide settings in this file. The configuration is type-safe to prevent errors. 85 | 86 | ## 🌐 Deployment 87 | 88 | This project is configured for seamless deployment on **Vercel**. 89 | 90 | 1. **Push to GitHub**: Make sure your project is on a GitHub repository. 91 | 2. **Import Project on Vercel**: Log in to your Vercel account and import the GitHub repository. 92 | 3. **Deploy**: Vercel will automatically detect that this is an Astro project and configure the build settings. No extra configuration is needed. Your site will be deployed. 93 | 94 | ## 🤖 About RuleSync 95 | 96 | This project uses [RuleSync](https://github.com/dyoshikawa/rulesync) to keep development conventions and guidelines synchronized for both human developers and AI coding assistants. This ensures consistency and adherence to best practices throughout the project's lifecycle. 97 | 98 | The rules are defined in the `.rulesync/` directory and can be customized to fit the project's needs. 99 | 100 | --- 101 | 102 | 本项目前身为一个 Gatsby 博客,现已迁移至 Astro。你可以在 [这里](https://github.com/jctaoo-archive/stella) 查看原始项目。 103 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { XIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Dialog({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function DialogTrigger({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return 19 | } 20 | 21 | function DialogPortal({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return 25 | } 26 | 27 | function DialogClose({ 28 | ...props 29 | }: React.ComponentProps) { 30 | return 31 | } 32 | 33 | function DialogOverlay({ 34 | className, 35 | ...props 36 | }: React.ComponentProps) { 37 | return ( 38 | 46 | ) 47 | } 48 | 49 | function DialogContent({ 50 | className, 51 | children, 52 | showCloseButton = true, 53 | ...props 54 | }: React.ComponentProps & { 55 | showCloseButton?: boolean 56 | }) { 57 | return ( 58 | 59 | 60 | 68 | {children} 69 | {showCloseButton && ( 70 | 74 | 75 | Close 76 | 77 | )} 78 | 79 | 80 | ) 81 | } 82 | 83 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { 84 | return ( 85 |
90 | ) 91 | } 92 | 93 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { 94 | return ( 95 |
103 | ) 104 | } 105 | 106 | function DialogTitle({ 107 | className, 108 | ...props 109 | }: React.ComponentProps) { 110 | return ( 111 | 116 | ) 117 | } 118 | 119 | function DialogDescription({ 120 | className, 121 | ...props 122 | }: React.ComponentProps) { 123 | return ( 124 | 129 | ) 130 | } 131 | 132 | export { 133 | Dialog, 134 | DialogClose, 135 | DialogContent, 136 | DialogDescription, 137 | DialogFooter, 138 | DialogHeader, 139 | DialogOverlay, 140 | DialogPortal, 141 | DialogTitle, 142 | DialogTrigger, 143 | } 144 | -------------------------------------------------------------------------------- /src/components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Drawer as DrawerPrimitive } from "vaul" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Drawer({ 7 | ...props 8 | }: React.ComponentProps) { 9 | return 10 | } 11 | 12 | function DrawerTrigger({ 13 | ...props 14 | }: React.ComponentProps) { 15 | return 16 | } 17 | 18 | function DrawerPortal({ 19 | ...props 20 | }: React.ComponentProps) { 21 | return 22 | } 23 | 24 | function DrawerClose({ 25 | ...props 26 | }: React.ComponentProps) { 27 | return 28 | } 29 | 30 | function DrawerOverlay({ 31 | className, 32 | ...props 33 | }: React.ComponentProps) { 34 | return ( 35 | 43 | ) 44 | } 45 | 46 | function DrawerContent({ 47 | className, 48 | children, 49 | ...props 50 | }: React.ComponentProps) { 51 | return ( 52 | 53 | 54 | 66 |
67 | {children} 68 | 69 | 70 | ) 71 | } 72 | 73 | function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { 74 | return ( 75 |
83 | ) 84 | } 85 | 86 | function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { 87 | return ( 88 |
93 | ) 94 | } 95 | 96 | function DrawerTitle({ 97 | className, 98 | ...props 99 | }: React.ComponentProps) { 100 | return ( 101 | 106 | ) 107 | } 108 | 109 | function DrawerDescription({ 110 | className, 111 | ...props 112 | }: React.ComponentProps) { 113 | return ( 114 | 119 | ) 120 | } 121 | 122 | export { 123 | Drawer, 124 | DrawerPortal, 125 | DrawerOverlay, 126 | DrawerTrigger, 127 | DrawerClose, 128 | DrawerContent, 129 | DrawerHeader, 130 | DrawerFooter, 131 | DrawerTitle, 132 | DrawerDescription, 133 | } 134 | -------------------------------------------------------------------------------- /src/components/mobile-header.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; 3 | import { Mail, Menu, X } from "lucide-react"; 4 | import { SiGithub, SiX, SiRss } from "react-icons/si"; 5 | import IndicatorText from "@/components/indicator-text"; 6 | import { useState } from "react"; 7 | import { Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTitle } from "@/components/ui/drawer"; 8 | import ThemeSwitcher from "@/components/theme-switcher"; 9 | import { siteConfig } from "@/siteConfig"; 10 | 11 | interface MobileHeaderProps { 12 | isHome?: boolean; 13 | currentRoute?: string; 14 | } 15 | 16 | export default function MobileHeader({ isHome = false, currentRoute = "" }: MobileHeaderProps) { 17 | const [isMenuOpen, setIsMenuOpen] = useState(false); 18 | 19 | const routeTitle = (() => { 20 | if (currentRoute.startsWith("/posts")) return "文章"; 21 | if (currentRoute.startsWith("/snippets")) return "片段"; 22 | if (currentRoute.startsWith("/about")) return "关于"; 23 | return null; 24 | })(); 25 | 26 | const socialLinks = [ 27 | { icon: SiGithub, href: "https://github.com", label: "GitHub" }, 28 | { icon: Mail, href: "mailto:example@email.com", label: "Email" }, 29 | { icon: SiX, href: "https://twitter.com", label: "Twitter" }, 30 | { icon: SiRss, href: "/rss.xml", label: "RSS" }, 31 | ]; 32 | 33 | const { siteName } = siteConfig; 34 | 35 | if (isHome) return <>; 36 | 37 | return ( 38 | <> 39 |
40 |
41 | 42 | {siteName} 43 | {routeTitle ? / {routeTitle} : null} 44 | 45 | 48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 | setIsMenuOpen(false)} className="text-base font-semibold text-foreground"> 56 | {siteName} 57 | 58 | 59 | 60 | 63 | 64 | 65 |
66 | 77 | 78 |
79 | 80 |
81 | {socialLinks.map((link, index) => ( 82 | 83 | 84 | 85 | 93 | 94 | 95 | 96 |

{link.label}

97 |
98 |
99 | ))} 100 |
101 |
102 |
103 |
104 | 105 | 106 | 107 | 108 |
109 |
110 | 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /src/components/sidebar-nav.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Card, CardContent } from "@/components/ui/card"; 3 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; 4 | import { Mail } from "lucide-react"; 5 | import { SiGithub, SiX, SiRss } from "react-icons/si"; 6 | import IndicatorText from "@/components/indicator-text"; 7 | import { cn } from "@/lib/utils"; 8 | import { useEffect, useRef } from "react"; 9 | import confetti from "canvas-confetti"; 10 | import ThemeSwitcher from "@/components/theme-switcher"; 11 | import { siteConfig } from "@/siteConfig"; 12 | 13 | interface SidebarNavProps { 14 | isHome?: boolean; 15 | className?: string; 16 | currentRoute?: string; 17 | bannerText?: string; 18 | } 19 | 20 | export default function SidebarNav({ isHome = false, className = "", currentRoute = "", bannerText }: SidebarNavProps) { 21 | const titleRef = useRef(null); 22 | const socialLinks = [ 23 | { icon: SiGithub, href: "https://github.com", label: "GitHub" }, 24 | { icon: Mail, href: "mailto:example@email.com", label: "Email" }, 25 | { icon: SiX, href: "https://twitter.com", label: "Twitter" }, 26 | { icon: SiRss, href: "/rss.xml", label: "RSS" }, 27 | ]; 28 | 29 | useEffect(() => { 30 | const hasShownConfetti = localStorage.getItem("hasShownConfetti"); 31 | 32 | if (!hasShownConfetti && titleRef.current) { 33 | const titleRect = titleRef.current.getBoundingClientRect(); 34 | const x = titleRect.left + titleRect.width / 2; 35 | const y = titleRect.top + titleRect.height / 2; 36 | const origin = { x: x / window.innerWidth, y: y / window.innerHeight }; 37 | 38 | confetti({ 39 | particleCount: 50, 40 | angle: 90, 41 | spread: 100, 42 | origin: origin, 43 | }); 44 | 45 | localStorage.setItem("hasShownConfetti", "true"); 46 | } 47 | }, []); 48 | 49 | const showBanner = !!bannerText; 50 | 51 | const { siteName } = siteConfig; 52 | 53 | return ( 54 |
65 | {/* spacer */} 66 |
67 | 68 |
69 | {/* Blog Title */} 70 |
71 | 72 |

78 | {siteName} 79 |

80 |
81 | {isHome &&

A minimal blog built with Astro

} 82 |
83 | 84 | {/* Navigation Links */} 85 | 96 | 97 | {/* Social Media Links */} 98 |
99 | 100 |
101 | {socialLinks.map((link, index) => ( 102 | 103 | 104 | 105 | 113 | 114 | 115 | 116 |

{link.label}

117 |
118 |
119 | ))} 120 |
121 |
122 |
123 |
124 | 125 | {/* spacer */} 126 |
127 | 128 | {/* Banner and Theme Toggle at Bottom */ 129 | } 130 |
131 | {bannerText ? ( 132 |
{bannerText}
133 | ) : null} 134 | 135 |
136 |
137 | ); 138 | } 139 | -------------------------------------------------------------------------------- /src/components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Command as CommandPrimitive } from "cmdk" 3 | import { SearchIcon } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | import { 7 | Dialog, 8 | DialogContent, 9 | DialogDescription, 10 | DialogHeader, 11 | DialogTitle, 12 | } from "@/components/ui/dialog" 13 | 14 | function Command({ 15 | className, 16 | ...props 17 | }: React.ComponentProps) { 18 | return ( 19 | 27 | ) 28 | } 29 | 30 | function CommandDialog({ 31 | title = "Command Palette", 32 | description = "Search for a command to run...", 33 | children, 34 | className, 35 | showCloseButton = true, 36 | ...props 37 | }: React.ComponentProps & { 38 | title?: string 39 | description?: string 40 | className?: string 41 | showCloseButton?: boolean 42 | }) { 43 | return ( 44 | 45 | 46 | {title} 47 | {description} 48 | 49 | 53 | 54 | {children} 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | function CommandInput({ 62 | className, 63 | ...props 64 | }: React.ComponentProps) { 65 | return ( 66 |
70 | 71 | 79 |
80 | ) 81 | } 82 | 83 | function CommandList({ 84 | className, 85 | ...props 86 | }: React.ComponentProps) { 87 | return ( 88 | 96 | ) 97 | } 98 | 99 | function CommandEmpty({ 100 | ...props 101 | }: React.ComponentProps) { 102 | return ( 103 | 108 | ) 109 | } 110 | 111 | function CommandGroup({ 112 | className, 113 | ...props 114 | }: React.ComponentProps) { 115 | return ( 116 | 124 | ) 125 | } 126 | 127 | function CommandSeparator({ 128 | className, 129 | ...props 130 | }: React.ComponentProps) { 131 | return ( 132 | 137 | ) 138 | } 139 | 140 | function CommandItem({ 141 | className, 142 | ...props 143 | }: React.ComponentProps) { 144 | return ( 145 | 153 | ) 154 | } 155 | 156 | function CommandShortcut({ 157 | className, 158 | ...props 159 | }: React.ComponentProps<"span">) { 160 | return ( 161 | 169 | ) 170 | } 171 | 172 | export { 173 | Command, 174 | CommandDialog, 175 | CommandInput, 176 | CommandList, 177 | CommandEmpty, 178 | CommandGroup, 179 | CommandItem, 180 | CommandShortcut, 181 | CommandSeparator, 182 | } 183 | -------------------------------------------------------------------------------- /src/components/content-filter-button.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from "react"; 2 | import { navigate } from "astro:transitions/client"; 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Popover, 6 | PopoverTrigger, 7 | PopoverContent, 8 | } from "@/components/ui/popover"; 9 | import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from "@/components/ui/command"; 10 | import { Checkbox } from "@/components/ui/checkbox"; 11 | import { Badge } from "@/components/ui/badge"; 12 | import { Tags, ListFilter } from "lucide-react"; 13 | 14 | type ContentFilterButtonProps = { 15 | categories: string[]; 16 | tags: string[]; 17 | selectedCategory?: string; 18 | selectedTags?: string[]; 19 | }; 20 | 21 | function uniqueSorted(list: string[]): string[] { 22 | return [...new Set(list.filter(Boolean))].sort((a, b) => a.localeCompare(b)); 23 | } 24 | 25 | export default function ContentFilterButton(props: ContentFilterButtonProps) { 26 | const allCategories = useMemo(() => uniqueSorted(props.categories), [props.categories]); 27 | const allTags = useMemo(() => uniqueSorted(props.tags), [props.tags]); 28 | 29 | const [categoryOpen, setCategoryOpen] = useState(false); 30 | const [tagsOpen, setTagsOpen] = useState(false); 31 | const [category, setCategory] = useState(props.selectedCategory); 32 | const [selectedTags, setSelectedTags] = useState>(new Set(props.selectedTags || [])); 33 | 34 | useEffect(() => { 35 | setCategory(props.selectedCategory); 36 | }, [props.selectedCategory]); 37 | useEffect(() => { 38 | setSelectedTags(new Set(props.selectedTags || [])); 39 | }, [props.selectedTags]); 40 | 41 | function navigateWith(nextCategory: string | undefined, nextTags: Set) { 42 | const url = new URL(window.location.href); 43 | const sp = url.searchParams; 44 | if (nextCategory) sp.set("category", nextCategory); 45 | else sp.delete("category"); 46 | if (nextTags.size > 0) sp.set("tags", Array.from(nextTags).join(",")); 47 | else sp.delete("tags"); 48 | const search = sp.toString(); 49 | navigate(url.pathname + (search ? `?${search}` : "")); 50 | } 51 | 52 | function toggleTag(tag: string) { 53 | setSelectedTags((prev) => { 54 | const next = new Set(prev); 55 | if (next.has(tag)) next.delete(tag); 56 | else next.add(tag); 57 | // apply immediately 58 | navigateWith(category, next); 59 | return next; 60 | }); 61 | } 62 | 63 | return ( 64 |
65 | 66 | 67 | 73 | 74 | 75 | 76 | 77 | 没有匹配的分类 78 | 79 | { 82 | setCategory(undefined); 83 | navigateWith(undefined, selectedTags); 84 | }} 85 | className={category ? "" : "bg-accent/60"} 86 | > 87 | 全部 88 | 89 | {allCategories.map((c) => ( 90 | { 93 | setCategory(c); 94 | navigateWith(c, selectedTags); 95 | }} 96 | className={category === c ? "bg-accent/60" : ""} 97 | > 98 | {c} 99 | 100 | ))} 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 115 | 116 | 117 | 118 | 119 | 没有匹配的标签 120 | 121 | {allTags.map((t) => { 122 | const checked = selectedTags.has(t); 123 | return ( 124 | toggleTag(t)} 127 | className="flex items-center gap-2" 128 | > 129 | toggleTag(t)} 132 | onClick={(e) => e.stopPropagation()} 133 | onPointerDown={(e) => e.stopPropagation()} 134 | onKeyDown={(e) => e.stopPropagation()} 135 | /> 136 | {t} 137 | 138 | ); 139 | })} 140 | 141 | 142 | 143 | 144 | 145 | {(category || selectedTags.size > 0) && ( 146 | 159 | )} 160 |
161 | ); 162 | } 163 | 164 | 165 | -------------------------------------------------------------------------------- /src/components/ui/navigation-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"; 3 | import { cva } from "class-variance-authority"; 4 | import { ChevronDownIcon } from "lucide-react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function NavigationMenu({ 9 | className, 10 | children, 11 | viewport = true, 12 | ...props 13 | }: React.ComponentProps & { 14 | viewport?: boolean; 15 | }) { 16 | return ( 17 | 23 | {children} 24 | {viewport && } 25 | 26 | ); 27 | } 28 | 29 | function NavigationMenuList({ className, ...props }: React.ComponentProps) { 30 | return ( 31 | 36 | ); 37 | } 38 | 39 | function NavigationMenuItem({ className, ...props }: React.ComponentProps) { 40 | return ( 41 | 42 | ); 43 | } 44 | 45 | const navigationMenuTriggerStyle = cva( 46 | "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1", 47 | ); 48 | 49 | function NavigationMenuTrigger({ 50 | className, 51 | children, 52 | ...props 53 | }: React.ComponentProps) { 54 | return ( 55 | 60 | {children}{" "} 61 | 66 | ); 67 | } 68 | 69 | function NavigationMenuContent({ className, ...props }: React.ComponentProps) { 70 | return ( 71 | 80 | ); 81 | } 82 | 83 | function NavigationMenuViewport({ 84 | className, 85 | ...props 86 | }: React.ComponentProps) { 87 | return ( 88 |
89 | 97 |
98 | ); 99 | } 100 | 101 | function NavigationMenuLink({ className, ...props }: React.ComponentProps) { 102 | return ( 103 | 111 | ); 112 | } 113 | 114 | function NavigationMenuIndicator({ 115 | className, 116 | ...props 117 | }: React.ComponentProps) { 118 | return ( 119 | 127 |
128 | 129 | ); 130 | } 131 | 132 | export { 133 | NavigationMenu, 134 | NavigationMenuList, 135 | NavigationMenuItem, 136 | NavigationMenuContent, 137 | NavigationMenuTrigger, 138 | NavigationMenuLink, 139 | NavigationMenuIndicator, 140 | NavigationMenuViewport, 141 | navigationMenuTriggerStyle, 142 | }; 143 | -------------------------------------------------------------------------------- /src/content/posts/HelloWorldDoc.md: -------------------------------------------------------------------------------- 1 | --- 2 | category: Text 3 | title: Hello World, stella 4 | abbr: Hello World, stella. 5 | topImage: "./Hello World.jpg" 6 | topImageAlt: "Hello World" 7 | --- 8 | 9 | # "Hello, World!" program update update 10 | 11 | $$ 12 | \begin{aligned} 13 | \dot{x} & = \sigma(y-x) \\ 14 | \dot{y} & = \rho x - y - xz \\ 15 | \dot{z} & = -\beta z + xy 16 | \end{aligned} 17 | $$ 18 | 19 | > from [wikipedia](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program) 20 | 21 | "Hello World" redirects here. For other uses, see Hello World (disambiguation). 22 | 23 | ![hello world](./helloguys.jpg) 24 | 25 | A "Hello, World!" message being displayed through long-exposure light painting with a moving strip of LEDs 26 | A "Hello, World!" program generally is a computer program that outputs or displays the message "Hello, World!". Such a program is very simple in most programming languages, and is often used to illustrate the basic syntax of a programming language. It is often the first program written by people learning to code.[1][2] It can also be used as a sanity test to make sure that a computer language is correctly installed, and that the operator understands how to use it. 27 | 28 | ## History 29 | 30 | While small test programs have existed since the development of programmable computers, the tradition of using the phrase "Hello, World!" as a test message was influenced by an example program in the seminal 1978 book The C Programming Language.[3] The example program in that book prints "hello, world", and was inherited from a 1974 Bell Laboratories internal memorandum by Brian Kernighan, Programming in C: A Tutorial:[4] 31 | 32 | ```c 33 | main( ) { 34 | printf("hello, world\n"); 35 | } 36 | ``` 37 | 38 | In the above example, the main( ) function defines where the program should start executing. The function body consists of a single statement, a call to the printf function, which stands for "print formatted". This function will cause the program to output whatever is passed to it as the parameter, in this case the string hello, world, followed by a newline character. 39 | 40 | The C language version was preceded by Kernighan's own 1972 A Tutorial Introduction to the Language B,[5] where the first known version of the program is found in an example used to illustrate external variables: 41 | 42 | ```c 43 | ain( ) { 44 | extern a, b, c; 45 | putchar(a); putchar(b); putchar(c); putchar('!*n'); 46 | } 47 | 48 | a 'hell'; 49 | b 'o, w'; 50 | c 'orld'; 51 | ``` 52 | 53 | The program also prints hello, world! on the terminal, including a newline character. The phrase is divided into multiple variables because in B, a character constant is limited to four ASCII characters. The previous example in the tutorial printed hi! on the terminal, and the phrase hello, world! was introduced as a slightly longer greeting that required several character constants for its expression. 54 | 55 | The Jargon File claims that "Hello, World!" originated instead with BCPL (1967).[6] This claim is supposedly supported by the archived notes of the inventors of BCPL, Brian Kernighan at Princeton and Martin Richards at Cambridge. The phrase predated by over a decade its usage in computing; as early as the 1950s, it was the catchphrase of radio disc jockey William B. Williams.[7] 56 | 57 | ## Variations 58 | 59 | "Hello, World!" programs vary in complexity between different languages. In some languages, particularly scripting languages, the "Hello, World!" program can be written as a single statement, while in others (particularly many low-level languages) there can be many more statements required. For example, in Python, to print the string Hello, World! followed by a newline, one need only write `print("Hello, World!")`. In contrast, the equivalent code in C++ [1] requires the import of the input/output software library, the manual declaration of an entry point, and the explicit instruction that the output string should be sent to the standard output stream. Generally, programming languages that give the programmer more control over the machine will result in more complex "Hello, World" programs.[8] 60 | 61 | The phrase "Hello World!" has seen various deviations in punctuation and casing, such as the presence of the comma and exclamation mark, and the capitalization of the leading H and W. Some devices limit the format to specific variations, such as all-capitalized versions on systems that support only capital letters, while some esoteric programming languages may have to print a slightly modified string. For example, the first non-trivial Malbolge program printed "HEllO WORld", this having been determined to be good enough.[9] Other human languages have been used as the output; for example, a tutorial for the Go programming language outputted both English and Chinese characters, demonstrating the programming language's built-in Unicode support.[10] 62 | 63 | Some languages change the functionality of the "Hello, World!" program while maintaining the spirit of demonstrating a simple example. Functional programming languages, such as Lisp, ML and Haskell, tend to substitute a factorial program for "Hello, World!", as functional programming emphasizes recursive techniques, whereas the original examples emphasize I/O, which violates the spirit of pure functional programming by producing side effects. Languages otherwise capable of printing "Hello, World!" (Assembly, C, VHDL) may also be used in embedded systems, where text output is either difficult (requiring additional components or communication with another computer) or nonexistent. For devices such as microcontrollers, field-programmable gate arrays, and CPLDs, "Hello, World!" may thus be substituted with a blinking LED, which demonstrates timing and interaction between components.[11][12][13][14][15] 64 | 65 | The Debian and Ubuntu Linux distributions provide the "Hello, World!" program through their software package manager systems, which can be invoked with the command hello. It serves as a sanity check and a simple example of installing a software package. For developers, it provides an example of creating a .deb package, either traditionally or using debhelper, and the version of hello used, GNU Hello, serves as an example of writing a GNU program.[16] 66 | 67 | Variations of the "Hello, World!" program that produce a graphical output (as opposed to text output) have also been shown. Sun demonstrated a "Hello, World!" program in Java based on scalable vector graphics,[17] and the XL programming language features a spinning Earth "Hello, World!" using 3D computer graphics.[18] Mark Guzdial and Elliot Soloway have suggested that the "hello, world" test message may be outdated now that graphics and sound can be manipulated as easily as text.[19] 68 | 69 | ## Time to Hello World 70 | 71 | "Time to hello world" (TTHW) is the time it takes to author a "Hello, World!" program in a given programming language. This is one measure of a programming language's ease-of-use; since the program is meant as an introduction for people unfamiliar with the language, a more complex "Hello, World!" program may indicate that the programming language is less approachable.[8] The concept has been extended beyond programming languages to APIs, as a measure of how simple it is for a new developer to get a basic example working; a faster time indicates an easier API for developers to adopt.[20][21] 72 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | @plugin '@tailwindcss/typography'; 4 | @import "katex/dist/katex.min.css"; 5 | 6 | @custom-variant dark (&:is(.dark *)); 7 | 8 | @theme inline { 9 | --color-background: var(--background); 10 | --color-foreground: var(--foreground); 11 | --color-card: var(--card); 12 | --color-card-foreground: var(--card-foreground); 13 | --color-popover: var(--popover); 14 | --color-popover-foreground: var(--popover-foreground); 15 | --color-primary: var(--primary); 16 | --color-primary-foreground: var(--primary-foreground); 17 | --color-secondary: var(--secondary); 18 | --color-secondary-foreground: var(--secondary-foreground); 19 | --color-muted: var(--muted); 20 | --color-muted-foreground: var(--muted-foreground); 21 | --color-accent: var(--accent); 22 | --color-accent-foreground: var(--accent-foreground); 23 | --color-destructive: var(--destructive); 24 | --color-destructive-foreground: var(--destructive-foreground); 25 | --color-border: var(--border); 26 | --color-input: var(--input); 27 | --color-ring: var(--ring); 28 | --color-chart-1: var(--chart-1); 29 | --color-chart-2: var(--chart-2); 30 | --color-chart-3: var(--chart-3); 31 | --color-chart-4: var(--chart-4); 32 | --color-chart-5: var(--chart-5); 33 | --color-sidebar: var(--sidebar); 34 | --color-sidebar-foreground: var(--sidebar-foreground); 35 | --color-sidebar-primary: var(--sidebar-primary); 36 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 37 | --color-sidebar-accent: var(--sidebar-accent); 38 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 39 | --color-sidebar-border: var(--sidebar-border); 40 | --color-sidebar-ring: var(--sidebar-ring); 41 | 42 | --font-sans: var(--font-sans); 43 | --font-mono: var(--font-mono); 44 | --font-serif: var(--font-serif); 45 | 46 | --radius-sm: calc(var(--radius) - 4px); 47 | --radius-md: calc(var(--radius) - 2px); 48 | --radius-lg: var(--radius); 49 | --radius-xl: calc(var(--radius) + 4px); 50 | 51 | --shadow-2xs: var(--shadow-2xs); 52 | --shadow-xs: var(--shadow-xs); 53 | --shadow-sm: var(--shadow-sm); 54 | --shadow: var(--shadow); 55 | --shadow-md: var(--shadow-md); 56 | --shadow-lg: var(--shadow-lg); 57 | --shadow-xl: var(--shadow-xl); 58 | --shadow-2xl: var(--shadow-2xl); 59 | } 60 | 61 | :root { 62 | --background: rgb(255, 255, 255); 63 | --foreground: rgb(38, 38, 38); 64 | --card: rgb(255, 255, 255); 65 | --card-foreground: rgb(38, 38, 38); 66 | --popover: rgb(255, 255, 255); 67 | --popover-foreground: rgb(38, 38, 38); 68 | --primary: rgb(255, 210, 48); 69 | --primary-foreground: rgb(70, 25, 1); 70 | --secondary: rgb(243, 244, 246); 71 | --secondary-foreground: rgb(75, 85, 99); 72 | --muted: rgb(249, 250, 251); 73 | --muted-foreground: rgb(107, 114, 128); 74 | --accent: rgb(254, 243, 198); 75 | --accent-foreground: rgb(146, 64, 14); 76 | --destructive: rgb(239, 68, 68); 77 | --destructive-foreground: rgb(255, 255, 255); 78 | --border: rgb(229, 231, 235); 79 | --input: rgb(229, 231, 235); 80 | --ring: rgb(245, 158, 11); 81 | --chart-1: rgb(245, 158, 11); 82 | --chart-2: rgb(217, 119, 6); 83 | --chart-3: rgb(180, 83, 9); 84 | --chart-4: rgb(146, 64, 14); 85 | --chart-5: rgb(120, 53, 15); 86 | --sidebar: rgb(249, 250, 251); 87 | --sidebar-foreground: rgb(38, 38, 38); 88 | --sidebar-primary: rgb(245, 158, 11); 89 | --sidebar-primary-foreground: rgb(255, 255, 255); 90 | --sidebar-accent: rgb(255, 251, 235); 91 | --sidebar-accent-foreground: rgb(146, 64, 14); 92 | --sidebar-border: rgb(229, 231, 235); 93 | --sidebar-ring: rgb(245, 158, 11); 94 | --font-sans: Inter, sans-serif; 95 | --font-serif: Source Serif 4, serif; 96 | --font-mono: JetBrains Mono, monospace; 97 | --radius: 0.375rem; 98 | --shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); 99 | --shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); 100 | --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1); 101 | --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1); 102 | --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 2px 4px -2px hsl(0 0% 0% / 0.1); 103 | --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 4px 6px -2px hsl(0 0% 0% / 0.1); 104 | --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 8px 10px -2px hsl(0 0% 0% / 0.1); 105 | --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25); 106 | --tracking-normal: 0em; 107 | --spacing: 0.25rem; 108 | } 109 | 110 | .dark { 111 | --background: rgb(28, 28, 28); 112 | --foreground: rgb(229, 229, 229); 113 | --card: rgb(38, 38, 38); 114 | --card-foreground: rgb(229, 229, 229); 115 | --popover: rgb(38, 38, 38); 116 | --popover-foreground: rgb(229, 229, 229); 117 | --primary: rgb(79, 57, 246); 118 | --primary-foreground: rgb(224, 231, 255); 119 | --secondary: rgb(38, 38, 38); 120 | --secondary-foreground: rgb(229, 229, 229); 121 | --muted: rgb(38, 38, 38); 122 | --muted-foreground: rgb(163, 163, 163); 123 | --accent: rgb(30, 26, 77); 124 | --accent-foreground: rgb(198, 210, 255); 125 | --destructive: rgb(239, 68, 68); 126 | --destructive-foreground: rgb(255, 255, 255); 127 | --border: rgb(64, 64, 64); 128 | --input: rgb(64, 64, 64); 129 | --ring: rgb(245, 158, 11); 130 | --chart-1: rgb(251, 191, 36); 131 | --chart-2: rgb(217, 119, 6); 132 | --chart-3: rgb(146, 64, 14); 133 | --chart-4: rgb(180, 83, 9); 134 | --chart-5: rgb(146, 64, 14); 135 | --sidebar: rgb(15, 15, 15); 136 | --sidebar-foreground: rgb(229, 229, 229); 137 | --sidebar-primary: rgb(245, 158, 11); 138 | --sidebar-primary-foreground: rgb(255, 255, 255); 139 | --sidebar-accent: rgb(146, 64, 14); 140 | --sidebar-accent-foreground: rgb(253, 230, 138); 141 | --sidebar-border: rgb(64, 64, 64); 142 | --sidebar-ring: rgb(245, 158, 11); 143 | --font-sans: Inter, sans-serif; 144 | --font-serif: Source Serif 4, serif; 145 | --font-mono: JetBrains Mono, monospace; 146 | --radius: 0.375rem; 147 | --shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); 148 | --shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05); 149 | --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1); 150 | --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 1px 2px -2px hsl(0 0% 0% / 0.1); 151 | --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 2px 4px -2px hsl(0 0% 0% / 0.1); 152 | --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 4px 6px -2px hsl(0 0% 0% / 0.1); 153 | --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.1), 0px 8px 10px -2px hsl(0 0% 0% / 0.1); 154 | --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25); 155 | } 156 | 157 | @layer base { 158 | * { 159 | @apply border-border outline-ring/50; 160 | } 161 | body { 162 | @apply bg-background text-foreground; 163 | } 164 | } 165 | 166 | @utility scrollbar { 167 | scrollbar-gutter: stable both-edges; 168 | 169 | &::-webkit-scrollbar { 170 | width: 8px; 171 | height: 8px; 172 | } 173 | 174 | &::-webkit-scrollbar-track { 175 | background: transparent; 176 | border-radius: 4px; 177 | } 178 | 179 | &::-webkit-scrollbar-thumb { 180 | @apply bg-neutral-300; 181 | border-radius: 4px; 182 | min-height: 60px; 183 | transition: background-color 0.2s ease; 184 | } 185 | 186 | &::-webkit-scrollbar-thumb:hover { 187 | @apply bg-neutral-400; 188 | cursor: pointer; 189 | } 190 | 191 | &::-webkit-scrollbar-thumb:active { 192 | @apply bg-neutral-500; 193 | cursor: pointer; 194 | } 195 | 196 | @variant dark { 197 | &::-webkit-scrollbar-thumb { 198 | @apply bg-neutral-800; 199 | } 200 | 201 | &::-webkit-scrollbar-thumb:hover { 202 | @apply bg-neutral-700; 203 | } 204 | 205 | &::-webkit-scrollbar-thumb:active { 206 | @apply bg-neutral-600; 207 | cursor: pointer; 208 | } 209 | } 210 | } 211 | --------------------------------------------------------------------------------