├── .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 |
13 |
14 |
15 |
16 | stella
17 |
18 |
19 |
20 |
21 |
22 | Posts
23 |
24 |
25 |
26 |
27 | Snippets
28 |
29 |
30 |
31 |
32 | About
33 |
34 |
35 |
36 |
37 |
38 |
39 |
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 |
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 | 
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 |
50 |
51 |
52 |
53 |
54 |
55 | setIsMenuOpen(false)} className="text-base font-semibold text-foreground">
56 | {siteName}
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
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 |
83 |
84 | {/* Navigation Links */}
85 |
86 |
87 | 文章
88 |
89 |
90 | 片段
91 |
92 |
93 | 关于
94 |
95 |
96 |
97 | {/* Social Media Links */}
98 |
99 |
100 |
101 | {socialLinks.map((link, index) => (
102 |
103 |
104 |
105 |
111 |
112 |
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 |
68 | 分类
69 | {category && (
70 | 1
71 | )}
72 |
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 |
110 | 标签
111 | {selectedTags.size > 0 && (
112 | {selectedTags.size}
113 | )}
114 |
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 |
{
151 | const next = new Set();
152 | setCategory(undefined);
153 | setSelectedTags(next);
154 | navigateWith(undefined, next);
155 | }}
156 | >
157 | 清空
158 |
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 |
65 |
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 | 
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 |
--------------------------------------------------------------------------------