diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 54a7695..65cab28 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,90 +1,62 @@ name: Deploy Pages -# 触发条件,main分支被修改后 on: push: branches: [main] - # pull_request: - # branches: [main] - - # 支持手动在工作流上触发 + pull_request: + branches: [main] workflow_dispatch: -# 设置时区 env: TZ: Asia/Shanghai + NEXT_TELEMETRY_DISABLED: 1 -# 权限设置 permissions: - # 允许读取仓库内容的权限。 contents: read - # 允许写入 GitHub Pages 的权限。 - pages: write - # 允许写入 id-token 的权限。 - id-token: write -# 并发控制配置 concurrency: - group: pages - cancel-in-progress: false + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} -# 定义执行任务 jobs: - # 构建任务 build: - runs-on: ubuntu-latest - - # node v24 运行 - strategy: - matrix: - node-version: [24] - steps: - # 拉取代码 - - name: Checkout - uses: actions/checkout@v3 + - name: Checkout with complete page history + uses: actions/checkout@v4 with: - # 保留 Git 信息 fetch-depth: 0 - - # 设置使用 Node.js 版本 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - - uses: pnpm/action-setup@v4 - name: Install pnpm + - name: Set up pnpm + uses: pnpm/action-setup@v4 with: - version: 11 + version: 11.19.0 run_install: false - - # 安装依赖 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile - - # 构建项目 - - name: Build blog project - run: | - echo ${{ github.workspace }} - pnpm build - - # 上传 VitePress 构建产物,用于后续部署 - # 不经 Jekyll 处理(dist/.nojekyll 已禁用),避免下划线开头的文件被丢弃 - - name: Upload artifact + - name: Check types, search targets and static output + run: pnpm check + - name: Upload GitHub Pages artifact + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' uses: actions/upload-pages-artifact@v3 with: - path: ./docs/.vitepress/dist + path: ./out - # 部署任务 deploy: + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + needs: build + permissions: + pages: write + id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest - needs: build steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index ce7ca6d..6f47bc5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ -node_modules -dist -cache -.temp -.DS_Store \ No newline at end of file +node_modules/ +dist/ +cache/ +.temp/ +.DS_Store +.next/ +.source/ +out/ +*.tsbuildinfo +next-env.d.ts +.env*.local diff --git a/AGENTS.md b/AGENTS.md index 692fbf7..c2ad802 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,56 +1,33 @@ # AGENTS.md -面向 AI agent 的仓库维护说明。人类投稿者请看站点[贡献指南](https://whudays.org/maintainer/contributing)。 +武汉大学动漫协会(WHUDAYS)历史存档站。Fumadocs + Next.js + React;完整静态导出部署至 GitHub Pages,正式域名 `whudays.org`。 -## 项目 +## 硬性约束 -武汉大学动漫协会(武大漫协 / WHUDAYS)社团历史存档站。[VitePress](https://vitepress.dev/) 静态站,GitHub Pages 部署,正式域名 `whudays.org`。 +1. 站内链接用绝对路径,例如 `/group/ani-key/`;不要使用相对链接。构建的死链检查失败时不能提交。 +2. 新增页面需同步 `lib/navigation.json` 对应分区侧栏;子分区按最长路径匹配,不能覆盖丢失。 +3. 改动后运行 `pnpm check`,确保类型检查和完整构建通过。 +4. Commit 不要带 AI 的 Co-Authors。Commit message 使用中文并说明原因。 +5. 不能删除或改名历史附件、中文 URL、旧锚点或独立 HTML 存档。变更时必须先解释兼容方案。 -- 内容源在 `docs/`,VitePress 配置在 `docs/.vitepress/config.js` -- 本地预览 `pnpm dev`,构建 `pnpm build` +## 内容与资源 -## 硬性约束(最易违反,务必遵守) +- 内容在 `docs/<区域>/**/*.mdx`;区域为 activity、about、department、group、message-box、maintainer +- 图片、附件和独立 HTML 存档直接维护在根目录 `public/`,用根绝对路径引用;例如 `public/activity/2024/x.jpg` → `/activity/2024/x.jpg` +- 年份为学年,例如 2024 指 2024.6–2025.6,与干部任期一致 +- 内容页面使用无 `.html` 后缀的地址;独立 HTML 存档保持原地址 +- 标题锚点沿用旧规则;添加显式锚点使用 Fumadocs 支持的 `[#anchor]` -1. **站内链接用绝对路径**(`/group/...`),禁用相对路径。构建期有死链检查,失效链接会让 `pnpm build` 失败、阻断部署。 -2. **新增页面必须同步侧边栏**:在 `docs/.vitepress/config.js` 的 `themeConfig.sidebar` 对应区域手动登记,否则页面不出现在导航。 -3. **改动后跑 `pnpm build` 验证**能通过再提交。 -4. **commit 不要带 AI 的 Co-Authors**——这是社区硬性规矩,带了会被拒。 -5. commit message 用中文,描述清楚"为什么"。 +## React / MDX -## 目录与资源约定 +- `ChatMessage`、`MemberCard`、`TeamMembers`、`TeamPage`、`TeamPageTitle`、`TeamPageSection`、`Badge`、`Callout` 已全局注册 +- 组件支持服务器渲染,不再依赖 Vue `ClientOnly`;保留的同名兼容组件会直接输出子内容 +- JavaScript 变量用 `export const`;动态属性用 `avatar={avatarOf('名字')}`;不能使用 Vue `:prop`、` + + ; +} diff --git a/app/markdown/[...slug]/route.ts b/app/markdown/[...slug]/route.ts new file mode 100644 index 0000000..cda5176 --- /dev/null +++ b/app/markdown/[...slug]/route.ts @@ -0,0 +1,20 @@ +import { source, getMarkdownPath } from '@/lib/source'; + +export const dynamic = 'force-static'; +export const dynamicParams = false; + +export function generateStaticParams() { + return source.getPages().map(page => ({ + slug: getMarkdownPath(page.path).slice('/markdown/'.length).split('/').map(decodeURIComponent), + })); +} + +export async function GET(_request: Request, { params }: { params: Promise<{ slug: string[] }> }) { + const { slug } = await params; + const requestedPath = `/markdown/${slug.join('/')}`; + const page = source.getPages().find(page => getMarkdownPath(page.path) === requestedPath); + if (!page) return new Response('Not found', { status: 404 }); + return new Response(await page.data.getText('raw'), { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }); +} diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..b4672e9 --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,5 @@ +import { HomeLayout } from '@fumadocs/base-ui/layouts/home'; +import { baseOptions } from '@/lib/layout.shared'; +export default function NotFound() { + return

404

页面不存在

可以使用站内搜索查找活动或资料

返回首页
; +} diff --git a/app/search-index.json/route.ts b/app/search-index.json/route.ts new file mode 100644 index 0000000..7ea734d --- /dev/null +++ b/app/search-index.json/route.ts @@ -0,0 +1,26 @@ +import { createSearchTokenizer } from '@/lib/search-tokenizer'; +import { source } from '@/lib/source'; +import { createFromSource } from 'fumadocs-core/search/server'; +import { collectComponentText } from '@/lib/search-component-text.mjs'; + +export const dynamic = 'force-static'; +export const revalidate = false; +export async function GET() { + const api = createFromSource(source, { + tokenizer: createSearchTokenizer(), + buildIndex(page) { + const text = collectComponentText(page.data._exports); + return { + id: page.url, + url: page.url, + title: page.data.title, + description: page.data.description, + structuredData: { + ...page.data.structuredData, + contents: [...page.data.structuredData.contents, ...text], + }, + }; + }, + }); + return api.staticGET(); +} diff --git a/app/sitemap.ts b/app/sitemap.ts new file mode 100644 index 0000000..933d03a --- /dev/null +++ b/app/sitemap.ts @@ -0,0 +1,16 @@ +import type { MetadataRoute } from 'next'; +import { source, getCanonicalPath } from '@/lib/source'; +import { readPageHistory } from '@/lib/git-history.mjs'; +import { site } from '@/lib/site'; + +export const dynamic = 'force-static'; + +export default function sitemap(): MetadataRoute.Sitemap { + return source.getPages().map(page => { + const lastModified = readPageHistory(`docs/${page.path}`)[0]?.date; + return { + url: `${site.url}${getCanonicalPath(page.path)}`, + ...(lastModified ? { lastModified } : {}), + }; + }); +} diff --git a/components/archive-cards.css b/components/archive-cards.css new file mode 100644 index 0000000..f1275d5 --- /dev/null +++ b/components/archive-cards.css @@ -0,0 +1,51 @@ +.archive-card-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr)); + align-items: stretch; + gap: 16px; +} +.archive-card { + min-width: 0; + overflow: hidden; + border-radius: 20px; + background: #eeeef0; + transition: box-shadow .2s; +} +.dark .archive-card { background: #202020; } +.archive-card:hover, .archive-card:focus-within { + box-shadow: inset 0 0 0 1px var(--color-fd-primary); +} +.archive-card-avatar { + width: 80px; + height: 80px; + margin: 0 auto 12px; + border-radius: 50%; + object-fit: cover; +} +.archive-card-title { + margin: 0; + border: 0; + padding: 0; + font-size: 18px; + font-weight: 650; + line-height: 26px; + overflow-wrap: anywhere; +} +.archive-card-subtitle { + margin: 4px 0 0; + color: var(--color-fd-muted-foreground); + font-size: 15px; + font-weight: 500; + line-height: 22px; + overflow-wrap: anywhere; +} +.archive-card-term { + margin: 8px 0 0; + color: var(--color-fd-muted-foreground); + font-size: 14px; + line-height: 22px; + white-space: nowrap; +} +@media (prefers-reduced-motion: reduce) { + .archive-card { transition: none; } +} diff --git a/components/canonical-url.tsx b/components/canonical-url.tsx new file mode 100644 index 0000000..c3367a6 --- /dev/null +++ b/components/canonical-url.tsx @@ -0,0 +1,15 @@ +'use client'; +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +/** Legacy HTML files are real SSR pages; move their address to the clean route + * after hydration so active navigation and subsequent Back/Forward stay in sync. */ +export function CanonicalUrl({ path }: { path: string }) { + const router = useRouter(); + useEffect(() => { + if (window.location.pathname.endsWith('.html')) { + router.replace(`${path}${window.location.search}${window.location.hash}`, { scroll: false }); + } + }, [path, router]); + return null; +} diff --git a/components/content.css b/components/content.css new file mode 100644 index 0000000..759bec7 --- /dev/null +++ b/components/content.css @@ -0,0 +1,238 @@ +/*! + * Team and badge styles adapted from VitePress 2.0.0-alpha.5 (MIT). + * Copyright (c) 2019-present, Yuxi (Evan) You + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* Content components remain server-rendered; the root .dark class handles theme changes. */ +:where(.VPTeamPage, .VPTeamPageTitle, .VPTeamPageSection, .VPTeamMembers) { + --whu-team-bg: var(--color-fd-muted, #f6f6f7); + --whu-team-page-bg: var(--color-fd-background, #fff); + --whu-team-muted: var(--color-fd-muted-foreground, #67676c); + --whu-team-border: var(--color-fd-border, #e2e2e3); + --whu-team-brand: var(--color-fd-primary, #3451b2); + --whu-team-sponsor: #db2777; +} + +.dark :where(.VPTeamPage, .VPTeamPageTitle, .VPTeamPageSection, .VPTeamMembers) { + --whu-team-bg: var(--color-fd-muted, #202127); + --whu-team-page-bg: var(--color-fd-background, #1b1b1f); + --whu-team-muted: var(--color-fd-muted-foreground, #98989f); + --whu-team-border: var(--color-fd-border, #3c3f44); + --whu-team-brand: var(--color-fd-primary, #a8b1ff); +} + +.ChatMessage { + display: flex; + align-items: flex-end; + gap: 5px; + margin: 10px 0; + text-align: left; +} + +.ChatMessage[data-position='right'] { flex-direction: row-reverse; } +.ChatMessage .chat-avatar { + flex: 0 0 50px; + width: 50px; + height: 50px; + margin: -5px 0 0; + border-radius: 50%; + object-fit: cover; +} +.ChatMessage .chat-content { min-width: 0; max-width: 70%; margin-right: auto; } +.ChatMessage[data-position='right'] .chat-content { margin-right: 0; margin-left: auto; } +.ChatMessage .chat-nickname { margin-bottom: 5px; font-weight: 700; } +.ChatMessage[data-position='right'] .chat-nickname { text-align: right; } +.ChatMessage .message-bubble { + border-radius: 10px; + padding: 8px 12px; + color: #000; + background: #f0f0f0; + overflow-wrap: anywhere; +} +.dark .ChatMessage .message-bubble { color: #fff; background: #333; } +.ChatMessage .message-bubble img { display: block; max-width: 100%; height: auto; margin: 0; } +.ChatMessage .message-bubble a { color: var(--color-fd-primary, #3451b2); text-decoration: underline; } + +.VPBadge { + display: inline-block; + margin-left: 2px; + border: 1px solid transparent; + border-radius: 12px; + padding: 0 10px; + font-size: 12px; + font-weight: 500; + line-height: 22px; + vertical-align: middle; + transform: translateY(-2px); +} +.VPBadge.small { padding: 0 6px; line-height: 18px; font-size: 10px; transform: translateY(-8px); } +.VPBadge.info { color: #67676c; background: #8e96aa24; } +.VPBadge.tip { color: #3451b2; background: #646cff24; } +.VPBadge.warning { color: #915930; background: #eab30824; } +.VPBadge.danger { color: #b8272c; background: #f43f5e24; } +.dark .VPBadge.info { color: #c2c2c4; background: #65758529; } +.dark .VPBadge.tip { color: #a8b1ff; background: #646cff29; } +.dark .VPBadge.warning { color: #f9b44e; background: #eab30829; } +.dark .VPBadge.danger { color: #f66f81; background: #f43f5e29; } + +.member-grid { + column-count: 2; + column-gap: 24px; + max-width: 900px; + margin: 30px auto; + padding: 10px; +} +.member-card-link { + display: block; + break-inside: avoid; + margin-bottom: 24px; + text-decoration: none; + color: inherit; +} +.member-card { + position: relative; + display: flex; + flex-direction: column; + min-height: 120px; + border-radius: 12px; + padding: 20px; + background: #eff2f3; + box-shadow: 0 2px 12px #39c5bb33; + transform: scale(1); + transition: box-shadow 0.3s ease, transform 0.3s ease; +} +.dark .member-card { background: #1c1d1d; box-shadow: 0 2px 12px #39c5bb55; } +.member-card:hover, .member-card:focus-within { box-shadow: 0 8px 32px #e6188055; transform: scale(1.05); } +.member-card-target { + position: absolute; + inset: 0; + z-index: 1; + border-radius: inherit; +} +.member-card-target:focus-visible { outline: 2px solid #39c5bb; outline-offset: 4px; } +.member-card-top { display: flex; align-items: flex-start; gap: 15px; margin-bottom: 15px; } +.member-card .member-avatar { + flex: 0 0 70px; + width: 70px; + height: 70px; + margin: 0; + border-radius: 50%; + object-fit: cover; + box-shadow: 0 0 8px #39c5bb88; +} +.member-info { flex: 1; min-width: 0; text-align: left; overflow-wrap: anywhere; } +.member-info .member-name { margin-bottom: 8px; font-size: 18px; font-weight: 700; line-height: 1.2; } +.member-info .member-description { margin-top: 8px; color: #666; font-size: 14px; } +.dark .member-info .member-description { color: #b6b6bc; } +.member-sns { display: flex; justify-content: center; gap: 8px; margin-top: auto; } +.member-sns a { position: relative; z-index: 2; display: inline-flex; border-radius: 4px; } +.member-sns a:focus-visible { outline: 2px solid #39c5bb; outline-offset: 4px; } +.member-sns .member-sns-logo { width: 32px; height: 32px; margin: 0; transition: transform 0.2s; } +.member-sns a:hover .member-sns-logo, .member-sns a:focus-visible .member-sns-logo { transform: scale(1.1); } + +.VPTeamMembers { width: 100%; margin: 24px 0; } +.VPTeamMembers .team-container { margin: 0 auto; } +.VPTeamMembers.count-1 .team-container { max-width: 368px; } +.VPTeamMembers.count-2 .team-container { max-width: 760px; } +.VPTeamMembers .team-item { min-width: 0; } +.VPTeamMembersItem { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; +} +.VPTeamMembersItem .profile { flex-grow: 1; padding: 20px; } +.VPTeamMembersItem .data { text-align: center; overflow-wrap: anywhere; } +.VPTeamMembersItem .avatar { position: relative; flex-shrink: 0; } +.VPTeamMembersItem .avatar-img { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; border-radius: 50%; object-fit: cover; } +.VPTeamMembersItem.year-group .name { min-height: 22px; } +.VPTeamMembersItem .org.link { color: inherit; text-decoration: none; transition: color .2s; } +.VPTeamMembersItem .org.link:hover { color: var(--whu-team-brand); } +.VPTeamMembersItem .desc { margin: 12px auto 0; font-size: 14px; line-height: 22px; } +.VPTeamMembersItem .desc a { color: var(--whu-team-brand); font-weight: 500; text-decoration: underline dotted; transition: color .2s; } +.VPTeamMembersItem .links { display: flex; justify-content: center; margin-top: 12px; } +.VPTeamMembersItem .VPSocialLinks { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; } +.VPTeamMembersItem .VPSocialLink { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 6px; + color: var(--whu-team-muted); + text-decoration: none; + transition: color 0.25s; +} +.VPTeamMembersItem .VPSocialLink:hover { color: var(--whu-team-brand); } +.VPTeamMembersItem .VPSocialLink:focus-visible { outline: 2px solid var(--whu-team-brand); outline-offset: 2px; } +.VPTeamMembersItem .team-social-icon, .VPTeamMembersItem .team-social-icon svg { display: block; width: 20px; height: 20px; fill: currentColor; } +.VPTeamMembersItem .sp-link { + display: flex; + justify-content: center; + align-items: center; + padding: 16px; + text-align: center; + font-size: 14px; + font-weight: 500; + color: var(--whu-team-sponsor); + background: var(--whu-team-bg); + text-decoration: none; + transition: color 0.25s, background-color 0.25s; +} +.VPTeamMembersItem .sp-link:hover, .VPTeamMembersItem .sp-link:focus-visible { color: #fff; background: var(--whu-team-sponsor); } +.VPTeamMembersItem .sp-link:focus-visible { outline: 2px solid var(--whu-team-brand); outline-offset: -3px; } +.VPTeamMembersItem .sp-icon { flex: 0 0 16px; width: 16px; height: 16px; margin-right: 8px; } + +.VPTeamPage { width: 100%; margin: 48px 0 64px; } +.VPTeamPage > .VPTeamPageSection + .VPTeamPageSection, +.VPTeamPage > .VPTeamMembers + .VPTeamPageSection { margin-top: 64px; } +.VPTeamPage > .VPTeamMembers + .VPTeamMembers { margin-top: 24px; } +.VPTeamPage > .VPTeamMembers { padding: 0; } +.VPTeamPageTitle { padding: 48px 32px; text-align: center; } +.VPTeamPageTitle > .title { margin: 0; letter-spacing: 0; line-height: 44px; font-size: 36px; font-weight: 500; } +.VPTeamPageTitle > .lead { margin: 0 auto; max-width: 512px; padding-top: 12px; line-height: 24px; font-size: 16px; font-weight: 500; color: var(--whu-team-muted); } +.VPTeamPageSection { padding: 0; } +.VPTeamPageSection > .title { position: relative; margin: 0 auto; max-width: 1152px; text-align: center; color: var(--whu-team-muted); } +.VPTeamPageSection .title-line { position: absolute; top: 16px; left: 0; width: 100%; height: 1px; background: var(--whu-team-border); } +.VPTeamPageSection .title-text { position: relative; display: inline-block; margin: 0; border: 0; padding: 0 24px; letter-spacing: 0; line-height: 32px; font-size: 20px; font-weight: 500; background: var(--whu-team-page-bg); } +.VPTeamPageSection > .lead { margin: 0 auto; max-width: 480px; padding-top: 12px; text-align: center; line-height: 24px; font-size: 16px; font-weight: 500; color: var(--whu-team-muted); } +.VPTeamPageSection > .members { margin-top: 24px; } +.VPTeamPageSection > .members:first-child { margin-top: 0; } +.VPTeamPageSection > .lead img, .VPTeamPageTitle > .lead img { max-width: 100%; } + +@media (max-width: 700px) { + .member-grid { column-count: 1; } +} +@media (min-width: 768px) { + .VPTeamPage > .VPTeamPageTitle + .VPTeamPageSection { margin-top: 16px; } + .VPTeamPage > .VPTeamPageSection + .VPTeamPageSection, + .VPTeamPage > .VPTeamMembers + .VPTeamPageSection { margin-top: 96px; } + .VPTeamPageTitle { padding: 64px 48px 48px; } + .VPTeamPageTitle > .title { letter-spacing: -0.5px; line-height: 56px; font-size: 48px; } + .VPTeamPageTitle > .lead { max-width: 592px; letter-spacing: 0.15px; line-height: 28px; font-size: 20px; } +} +@media (min-width: 960px) { + .VPTeamPageTitle { padding: 80px 64px 48px; } +} +@media (prefers-reduced-motion: reduce) { + .member-card, .member-sns-logo { transition: none !important; } + .member-card:hover, .member-card:focus-within, .member-sns a:hover .member-sns-logo, .member-sns a:focus-visible .member-sns-logo { transform: none; } +} diff --git a/components/content.tsx b/components/content.tsx new file mode 100644 index 0000000..1baafb9 --- /dev/null +++ b/components/content.tsx @@ -0,0 +1,262 @@ +import type { HTMLAttributes, ReactNode } from 'react'; +import { PLACEHOLDER_AVATAR } from '../lib/people.js'; +import * as icons from '../lib/icons.js'; +import './archive-cards.css'; +import './content.css'; + +const classes = (...values: Array) => + values.filter(Boolean).join(' '); + +/** The former ClientOnly content is now safe to render on the server. */ +export function ClientOnly({ children }: { children?: ReactNode }) { + return <>{children}; +} + +export interface ChatMessageProps extends HTMLAttributes { + avatar?: string; + nickname?: string; + /** Trusted, repository-authored HTML, matching the original message format. */ + message?: string; + position?: 'left' | 'right'; +} + +export function ChatMessage({ + avatar = PLACEHOLDER_AVATAR, + nickname, + message, + position = 'left', + className, + children, + ...props +}: ChatMessageProps) { + return ( +
+ {nickname +
+ {nickname &&
{nickname}
} + {message === undefined ? ( +
{children}
+ ) : ( +
+ )} +
+
+ ); +} + +export interface BadgeProps extends HTMLAttributes { + text?: string; + type?: 'info' | 'tip' | 'warning' | 'danger'; +} + +export function Badge({ text, type = 'tip', className, children, ...props }: BadgeProps) { + return {children ?? text}; +} + +export interface MemberSocial { + platform: string; + url: string; + icon: string; +} + +export interface MemberCardProps extends HTMLAttributes { + name: string; + avatar?: string; + description?: string; + link?: string; + badges?: Array>; + socials?: MemberSocial[]; +} + +export function MemberCard({ + name, + avatar = PLACEHOLDER_AVATAR, + description, + link, + badges = [], + socials = [], + className, + ...props +}: MemberCardProps) { + return ( + + ); +} + +export interface TeamSocialLink { + icon: string | { svg?: string }; + link: string; + ariaLabel?: string; +} + +export interface TeamMember { + avatar?: string; + name: string; + title?: string; + org?: string; + orgLink?: string; + /** Trusted HTML from the checked-in member registry or MDX. */ + desc?: string; + links?: TeamSocialLink[]; + sponsor?: string; + actionText?: string; +} + +const githubIcon = 'GitHub'; +const genericLinkIcon = ''; + +function socialIcon(link: TeamSocialLink) { + return typeof link.icon === 'string' + ? (link.icon === 'github' ? githubIcon : (icons as Record)[link.icon]) || genericLinkIcon + : link.icon.svg || genericLinkIcon; +} + +function socialLabel(link: TeamSocialLink) { + if (link.ariaLabel) return link.ariaLabel; + if (typeof link.icon === 'string') return link.icon; + return link.icon.svg?.match(/]*>([^<]+)<\/title>/i)?.[1] || link.link.trim(); +} + +function externalLink(href?: string) { + return href && /^(?:https?:)?\/\//i.test(href.trim()) + ? { target: '_blank', rel: 'noopener noreferrer' } + : {}; +} + +export function TeamMembers({ + members, + size = 'medium', + variant = 'person', + className, + id, + ...props +}: HTMLAttributes & { + members: TeamMember[]; + size?: 'small' | 'medium'; + /** Year groups display their academic year above the group name. */ + variant?: 'person' | 'year'; +}) { + return ( +
+
+ {members.map((member, index) => ( +
+
+
+
+ {member.name} +
+
+ {variant === 'year' &&

{member.title}学年

} +

{member.name}

+ {variant === 'year' &&

{member.title}.6–{Number(member.title) + 1}.6

} + {variant === 'person' && (member.title || member.org) && ( +

+ {member.title && {member.title}} + {member.title && member.org && @ } + {member.org && (member.orgLink + ? {member.org} + : {member.org})} +

+ )} + {member.desc &&
} + {member.links && member.links.length > 0 && ( +
+
+ {member.links.map((link, linkIndex) => ( + + + ))} +
+
+ )} +
+
+ {member.sponsor && ( + + )} +
+
+ ))} +
+
+ ); +} + +export function TeamPage({ className, children, ...props }: HTMLAttributes) { + return
{children}
; +} + +export interface TeamPageTitleProps extends Omit, 'title'> { + title?: ReactNode; + lead?: ReactNode; +} + +export function TeamPageTitle({ title, lead, className, children, ...props }: TeamPageTitleProps) { + return ( +
+ {title &&

{title}

} + {lead &&
{lead}
} + {children} +
+ ); +} + +export interface TeamPageSectionProps extends Omit, 'title'> { + title?: ReactNode; + lead?: ReactNode; + members?: ReactNode; +} + +export function TeamPageSection({ title, lead, members, className, children, ...props }: TeamPageSectionProps) { + return ( +
+ {title &&
+ } + {lead &&
{lead}
} + {(members || children) &&
{members ?? children}
} +
+ ); +} + +// Friendly to older MDX snippets while all page sources use the React names. +export { + TeamMembers as VPTeamMembers, + TeamPage as VPTeamPage, + TeamPageTitle as VPTeamPageTitle, + TeamPageSection as VPTeamPageSection, +}; diff --git a/components/git-changelog.css b/components/git-changelog.css new file mode 100644 index 0000000..e76349f --- /dev/null +++ b/components/git-changelog.css @@ -0,0 +1,87 @@ +/* Nolebase 2.17.1 component styles and UnoCSS utilities, scoped to this React port. + * See git-history-view.tsx for the upstream MIT licence and original-site patch. + */ +.git-changelog { + --history-bg: #8e96aa24; + --history-code-bg: #8e96aa24; + color: var(--vp-c-text-1); + font-size: 16px; + line-height: 24px; +} +.dark .git-changelog { --history-bg: #65758529; --history-code-bg: #65758529; } +.git-changelog h2 { + margin: 48px 0 16px; + border-top: 1px solid var(--vp-c-divider); + padding-top: 24px; + letter-spacing: -.02em; + line-height: 32px; + font-size: 24px; + font-weight: 600; + position: relative; +} +.git-changelog .git-changelog-anchor { + text-decoration: none; + position: absolute; + left: -.87em; + padding-right: .23em; + font-weight: 500; + opacity: 0; + transition: color .25s, opacity .25s; +} +.git-changelog h2:hover .git-changelog-anchor, +.git-changelog-anchor:focus-visible { opacity: 1; } +.git-changelog a { color: var(--vp-c-brand-1); font-weight: 500; text-decoration: underline; text-underline-offset: 2px; transition: color .25s; } +.git-changelog a:hover { color: var(--vp-c-brand-2); } +.git-changelog a:focus-visible, +.git-changelog button:focus-visible { outline: 2px solid var(--vp-c-brand-1); outline-offset: 3px; border-radius: 4px; } +.git-changelog code { + border-radius: 4px; + padding: 3px 6px; + background: var(--history-code-bg); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + font-size: .875em; + transition: color .25s, background-color .5s; +} +.git-changelog-empty { font-style: italic; opacity: .7; } +.vp-nolebase-git-changelog-history-container { border-radius: 8px; padding: 16px; background: var(--history-bg); } +.vp-nolebase-git-changelog-history-container[data-fresh] { background: #4ade8029; } +.vp-nolebase-git-changelog-title { position: relative; color: var(--vp-c-text-1); font-size: 14px; user-select: none; transition: color .2s ease-in-out; } +.vp-nolebase-git-changelog-title:hover { color: var(--vp-c-brand-1); } +.git-changelog button { border: 0; padding: 0; color: inherit; background: transparent; font: inherit; line-height: inherit; cursor: pointer; } +.git-changelog-toggle { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 12px; text-align: left; } +.vp-nolebase-git-changelog-last-edited-title { display: inline-flex; align-items: center; gap: 12px; font-weight: 800; } +.vp-nolebase-git-changelog-view-full-history-title { display: inline-flex; align-items: center; gap: 12px; padding-left: 36px; font-weight: 400; white-space: nowrap; } +.git-changelog-icon { display: inline-block; flex-shrink: 0; width: 1.2em; min-width: 1.2rem; height: 1.2em; vertical-align: middle; } +.git-changelog .git-changelog-sort { position: absolute; right: calc(7.2em + 28px); top: 0; height: 100%; display: inline-flex; align-items: center; } +.git-changelog-chevron { transition: transform .2s ease-in-out; } +[data-expanded='true'] .git-changelog-chevron { transform: rotate(180deg); } +.git-changelog-collapse { display: grid; grid-template-rows: 0fr; visibility: hidden; transition: grid-template-rows .2s ease-in-out, visibility .2s; } +[data-expanded='true'] .git-changelog-collapse { grid-template-rows: 1fr; visibility: visible; } +.git-changelog-collapse-content { min-height: 0; overflow: hidden; } +.git-changelog-list { display: grid; grid-template-columns: 30px minmax(0, 1fr); margin: 12px 0 0 -6px; gap: 6px; } +.git-changelog-list > * { margin-top: auto; margin-bottom: auto; } +.git-changelog-commit-icon { margin: auto; transform: rotate(90deg); opacity: .3; } +.git-changelog-entry { display: flex; align-items: baseline; gap: 4px; min-width: 0; } +.git-changelog-sha { flex-shrink: 0; } +.git-changelog-sha code { color: var(--vp-c-brand-1); font-size: 14px; line-height: 16px; } +.git-changelog-content { min-width: 0; overflow-wrap: anywhere; } +.git-changelog-message { font-size: 14px; line-height: 20px; } +.vp-nolebase-git-changelog-authors { margin: 4px 0 4px 12px; } +.vp-nolebase-git-changelog-author { display: inline; margin-right: .5em; white-space: nowrap; } +.vp-nolebase-git-changelog-author img { display: inline-block; width: 24px; height: 24px; margin: 0; border: 2px solid var(--vp-c-bg-soft); border-radius: 50%; object-fit: cover; vertical-align: middle; } +.vp-nolebase-git-changelog-author-name { margin-left: .25em; font-size: 14px; line-height: 20px; vertical-align: middle; } +.git-changelog-content time, .git-changelog-release time { font-size: 12px; line-height: 16px; opacity: .5; } +.git-changelog-release-icon { display: inline-flex; width: 1.75em; height: 1.75em; margin: auto; border-radius: 50%; background: #9ca3af1a; opacity: .9; } +.git-changelog-release-icon svg { width: 50%; min-width: 50%; height: 50%; margin: auto; } +.git-changelog-release { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; } +.git-changelog-release code { font-weight: 700; } +@media (max-width: 639px) { + .git-changelog-sha, .git-changelog-sha + span { display: none; } + .vp-nolebase-git-changelog-view-full-history-title > span { display: none; } + .git-changelog .git-changelog-sort { right: calc(1.2em + 16px); } + .git-changelog-list { font-size: 12px; } + .git-changelog-message, .vp-nolebase-git-changelog-author-name { font-size: 12px; line-height: 16px; } +} +@media (prefers-reduced-motion: reduce) { + .git-changelog-collapse, .git-changelog-chevron { transition: none; } +} diff --git a/components/git-changelog.tsx b/components/git-changelog.tsx new file mode 100644 index 0000000..398e516 --- /dev/null +++ b/components/git-changelog.tsx @@ -0,0 +1,8 @@ +import { getPageHistory, shouldShowChangelog, type ChangelogFrontmatter } from '../lib/changelog'; +import { GitHistoryView } from './git-history-view'; + +/** Read history only on the server and preserve all original per-page exclusions. */ +export function GitChangelog({ sourcePath, frontmatter = {} }: { sourcePath: string; frontmatter?: ChangelogFrontmatter }) { + if (!shouldShowChangelog(frontmatter)) return null; + return ; +} diff --git a/components/git-history-view.tsx b/components/git-history-view.tsx new file mode 100644 index 0000000..8769682 --- /dev/null +++ b/components/git-history-view.tsx @@ -0,0 +1,150 @@ +'use client'; + +/*! + * Adapted from @nolebase/vitepress-plugin-git-changelog 2.17.1 (MIT). + * https://github.com/nolebase/integrations/tree/v2.17.1/packages/vitepress-plugin-git-changelog + * + * MIT License + * + * Copyright (c) 2023-PRESENT All the contributors of Nólëbase + * Copyright (c) GitHub, Inc. (Octicons) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { Fragment, useEffect, useId, useState, type ReactNode } from 'react'; +import { differenceInDays, formatDistanceToNow } from 'date-fns'; +import { zhCN } from 'date-fns/locale'; +import type { HistoryAuthor, HistoryCommit } from '../lib/changelog'; +import './git-changelog.css'; + +const repository = 'https://github.com/WHUDAYS/WHUDAYS.github.io'; +const absoluteDate = (date: string) => new Date(date).toLocaleDateString('zh-CN'); +const fullDate = (date: string) => new Date(date).toLocaleString('zh-CN', { dateStyle: 'full', timeStyle: 'long' }); + +// The same Octicons used by Nolebase's UnoCSS build, without its global CSS reset. +const iconPaths = { + "history": "m.427 1.927l1.215 1.215a8.002 8.002 0 1 1-1.6 5.685a.75.75 0 1 1 1.493-.154a6.5 6.5 0 1 0 1.18-4.458l1.358 1.358A.25.25 0 0 1 3.896 6H.25A.25.25 0 0 1 0 5.75V2.104a.25.25 0 0 1 .427-.177M7.75 4a.75.75 0 0 1 .75.75v2.992l2.028.812a.75.75 0 0 1-.557 1.392l-2.5-1A.75.75 0 0 1 7 8.25v-3.5A.75.75 0 0 1 7.75 4", + "sort-desc": "M0 4.25a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 4.25m0 4a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 8.25m0 4a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1-.75-.75M13.5 10h2.25a.25.25 0 0 1 .177.427l-3 3a.25.25 0 0 1-.354 0l-3-3A.25.25 0 0 1 9.75 10H12V3.75a.75.75 0 0 1 1.5 0z", + "sort-asc": "m12.927 2.573l3 3A.25.25 0 0 1 15.75 6H13.5v6.75a.75.75 0 0 1-1.5 0V6H9.75a.25.25 0 0 1-.177-.427l3-3a.25.25 0 0 1 .354 0M0 12.25a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1-.75-.75m0-4a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 8.25m0-4a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 4.25", + "chevron-down": "M12.78 5.22a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L3.22 6.28a.749.749 0 1 1 1.06-1.06L8 8.939l3.72-3.719a.75.75 0 0 1 1.06 0", + "git-commit": "M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0a2.5 2.5 0 0 0 5 0", + "rocket": "M14.064 0h.186C15.216 0 16 .784 16 1.75v.186a8.75 8.75 0 0 1-2.564 6.186l-.458.459q-.472.471-.979.904v3.207c0 .608-.315 1.172-.833 1.49l-2.774 1.707a.75.75 0 0 1-1.11-.418l-.954-3.102a1 1 0 0 1-.145-.125L3.754 9.816a1 1 0 0 1-.124-.145L.528 8.717a.75.75 0 0 1-.418-1.11l1.71-2.774A1.75 1.75 0 0 1 3.31 4h3.204q.433-.508.904-.979l.459-.458A8.75 8.75 0 0 1 14.064 0M8.938 3.623h-.002l-.458.458c-.76.76-1.437 1.598-2.02 2.5l-1.5 2.317l2.143 2.143l2.317-1.5c.902-.583 1.74-1.26 2.499-2.02l.459-.458a7.25 7.25 0 0 0 2.123-5.127V1.75a.25.25 0 0 0-.25-.25h-.186a7.25 7.25 0 0 0-5.125 2.123M3.56 14.56c-.732.732-2.334 1.045-3.005 1.148a.23.23 0 0 1-.201-.064a.23.23 0 0 1-.064-.201c.103-.671.416-2.273 1.15-3.003a1.502 1.502 0 1 1 2.12 2.12m6.94-3.935q-.132.09-.266.175l-2.35 1.521l.548 1.783l1.949-1.2a.25.25 0 0 0 .119-.213ZM3.678 8.116L5.2 5.766q.087-.135.176-.266H3.309a.25.25 0 0 0-.213.119l-1.2 1.95ZM12 5a1 1 0 1 1-2 0a1 1 0 0 1 2 0" +} as const; +function Icon({ name, className = '' }: { name: keyof typeof iconPaths; className?: string }) { + return ; +} + +function safeLink(href: string) { + return /^(?:https?:\/\/|\/|#)/i.test(href) ? href : undefined; +} + +/** Nolebase's inline Markdown and issue links, rendered as escaped React nodes. */ +function CommitMessage({ message }: { message: string }) { + const parts: ReactNode[] = []; + const tokens = /`([^`]+)`|\*\*([^*]+)\*\*|\*([^*]+)\*|\[([^\]]+)\]\(([^)]+)\)|#(\d+)/g; + let cursor = 0; + for (const match of message.matchAll(tokens)) { + parts.push(message.slice(cursor, match.index)); + const [, code, bold, italic, label, href, issue] = match; + parts.push({code !== undefined ? {code} + : bold !== undefined ? {bold} + : italic !== undefined ? {italic} + : issue !== undefined ? #{issue} + : {label}}); + cursor = match.index + match[0].length; + } + parts.push(message.slice(cursor)); + return <>{parts}; +} + +function Author({ author }: { author: HistoryAuthor }) { + const content = <>{`${author.name}{author.name}; + return author.url + ? {content} + : {content}; +} + +function CommittedOn({ commit }: { commit: HistoryCommit }) { + return ; +} + +function CommitList({ commits }: { commits: HistoryCommit[] }) { + return
{commits.map(commit => + {commit.tags?.length ? <> + +
+ {commit.tags.map(tag => {tag.name})} + +
+ : <> + +
+ {commit.shortSha} + + + + {commit.authors.map((author, index) => )} + + +
+ } +
)}
; +} + +/** React port of Nolebase 2.17.1 Changelog/CommitRegularLine/CommitTagLine. + * The original site's author-name patch is preserved. Contributors were disabled. + * All records stay in the exported HTML; collapsing only changes their visibility. + */ +export function GitHistoryView({ commits }: { commits: HistoryCommit[] }) { + const [descending, setDescending] = useState(true); + const [expanded, setExpanded] = useState(false); + const [now, setNow] = useState(null); + const historyId = useId(); + useEffect(() => { + setNow(Date.now()); + const timer = window.setInterval(() => setNow(Date.now()), 60_000); + return () => window.clearInterval(timer); + }, []); + const latest = commits[0]; + const fresh = latest && now !== null && differenceInDays(now, new Date(latest.date)) < 1; + const ordered = descending ? commits : [...commits].reverse(); + return
+

页面历史#

+ {!latest ?
暂无最近变更历史
:
+
+ + +
+
+
+
+
} +
; +} diff --git a/components/home.tsx b/components/home.tsx new file mode 100644 index 0000000..fa5c744 --- /dev/null +++ b/components/home.tsx @@ -0,0 +1,25 @@ +import { HomeLayout } from '@fumadocs/base-ui/layouts/home'; +import { baseOptions } from '@/lib/layout.shared'; +import { SiteFooter } from './site-footer'; + +type Hero = { name?: string; text?: string; tagline?: string; image?: { src: string; alt?: string }; actions?: { text: string; link: string; theme?: string }[] }; +type Feature = { title: string; details?: string; link?: string }; +export function Home({ hero, features = [] }: { hero: Hero; features?: Feature[] }) { + return +
+
+
+

{hero.name}{hero.text}

+

{hero.tagline}

+
{hero.actions?.map((action) => {action.text})}
+
+ {hero.image &&
{hero.image.alt
} +
+
{features.map((feature) => +

{feature.title}

{feature.details}

+
)}
+ +
+ +
; +} diff --git a/components/hq-years.css b/components/hq-years.css new file mode 100644 index 0000000..b9993ae --- /dev/null +++ b/components/hq-years.css @@ -0,0 +1,23 @@ +.hq-year-grid { margin: 24px 0; } +.hq-year-card .hq-year-link { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + padding: 20px; + color: inherit; + text-align: center; + text-decoration: none; +} +.hq-year-card .hq-year-link:focus-visible { + outline: 2px solid var(--color-fd-primary); + outline-offset: -4px; + border-radius: 20px; +} +.hq-year-name { min-height: 22px; } +.hq-year-arrow { position: absolute; top: 16px; right: 16px; color: var(--color-fd-muted-foreground); transition: color .2s; } +.hq-year-link:hover .hq-year-arrow { color: var(--color-fd-primary); } +@media (prefers-reduced-motion: reduce) { + .hq-year-arrow { transition: none; } +} diff --git a/components/hq-years.tsx b/components/hq-years.tsx new file mode 100644 index 0000000..05d2006 --- /dev/null +++ b/components/hq-years.tsx @@ -0,0 +1,25 @@ +import type { ReactNode } from 'react'; +import { ArrowUpRight } from 'lucide-react'; +import { PLACEHOLDER_AVATAR } from '@/lib/people'; +import './archive-cards.css'; +import './hq-years.css'; + +export function HQYearGrid({ children }: { children: ReactNode }) { + return
{children}
; +} + +export function HQYearCard({ year, name, avatar }: { + year: number; + name?: string; + avatar?: string; +}) { + return ; +} diff --git a/components/mdx.tsx b/components/mdx.tsx new file mode 100644 index 0000000..4903c70 --- /dev/null +++ b/components/mdx.tsx @@ -0,0 +1,9 @@ +import defaultMdxComponents from '@fumadocs/base-ui/mdx'; +import { Callout } from '@fumadocs/base-ui/components/callout'; +import type { MDXComponents } from 'mdx/types'; +import * as content from './content'; + +export function getMDXComponents(components?: MDXComponents): MDXComponents { + return { ...defaultMdxComponents, ...content, Callout, img: (props) => , ...components }; +} +export const useMDXComponents = getMDXComponents; diff --git a/components/page-view-options.tsx b/components/page-view-options.tsx new file mode 100644 index 0000000..d5b7a54 --- /dev/null +++ b/components/page-view-options.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { ChevronDown, ExternalLink, TextIcon } from 'lucide-react'; +import { Popover, PopoverContent, PopoverTrigger } from '@fumadocs/base-ui/components/ui/popover'; +import { buttonVariants } from '@fumadocs/base-ui/components/ui/button'; + +/* + * Brand icon paths from @fumadocs/base-ui. + * MIT License + * + * Copyright (c) 2023 Fuma + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +const brandPaths = { + "GitHub": "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12", + "OpenAI": "M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z", + "Anthropic": "M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z" +} as const; + +function BrandIcon({ brand }: { brand: keyof typeof brandPaths }) { + return ; +} + +export function PageViewOptions({ markdownUrl, githubUrl, pageUrl }: { + markdownUrl: string; + githubUrl: string; + pageUrl: string; +}) { + const prompt = `请阅读 ${pageUrl},我想询问有关页面内容的问题。`; + const items = [ + { title: '在 GitHub 查看', href: githubUrl, icon: }, + { title: '查看 Markdown', href: markdownUrl, icon: