summaryrefslogtreecommitdiff
path: root/website/.vitepress
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-04-05 00:02:58 +0900
committerSho Sakuma <me@m1sk9.dev>2026-04-05 00:02:58 +0900
commit842bf9df75a5e23ab31bdfb5717b76449264e999 (patch)
tree3acd09750c7fb40c32d8d0f8fc998a23be98aff1 /website/.vitepress
parent278b6c9a65b93af61146a11575b32814624a3e40 (diff)
downloadLunaticChat-842bf9df75a5e23ab31bdfb5717b76449264e999.tar.gz
LunaticChat-842bf9df75a5e23ab31bdfb5717b76449264e999.tar.bz2
LunaticChat-842bf9df75a5e23ab31bdfb5717b76449264e999.zip
docs: add download page and rename docs to website
- Add download page with GitHub API data loader for latest releases - Add DownloadCard Vue component with JA/EN i18n support - Include Modrinth and CI build links - Rename docs/ directory to website/ Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'website/.vitepress')
-rw-r--r--website/.vitepress/config.mts50
-rw-r--r--website/.vitepress/config/en.ts133
-rw-r--r--website/.vitepress/config/ja.ts124
-rw-r--r--website/.vitepress/public/favicon.icobin0 -> 4286 bytes
-rw-r--r--website/.vitepress/theme/components/DownloadCard.vue338
-rw-r--r--website/.vitepress/theme/components/download.data.ts105
-rw-r--r--website/.vitepress/theme/custom.css188
-rw-r--r--website/.vitepress/theme/index.ts11
8 files changed, 949 insertions, 0 deletions
diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts
new file mode 100644
index 0000000..1cb5f83
--- /dev/null
+++ b/website/.vitepress/config.mts
@@ -0,0 +1,50 @@
+import { execSync } from 'node:child_process';
+import { defineConfig } from 'vitepress';
+import { en } from './config/en';
+import { ja } from './config/ja';
+
+const gitRoot = execSync('git rev-parse --show-toplevel').toString().trim();
+const commitHash = execSync(`git log -1 --format=%H -- ${gitRoot}/docs/`)
+ .toString()
+ .trim()
+ .slice(0, 7);
+
+export default defineConfig({
+ cleanUrls: true,
+ description: 'Next-generation channel chat plugin for Paper/Velocity',
+ head: [['link', { href: '/favicon.ico', rel: 'icon' }]],
+ locales: {
+ en: {
+ label: 'English',
+ lang: 'en-US',
+ link: '/en/',
+ themeConfig: {
+ ...en,
+ footer: {
+ copyright: 'Copyright © 2026 m1sk9',
+ message: `<a href="https://github.com/m1sk9/LunaticChat/commit/${commitHash}">LunaticChat/docs@${commitHash}</a>`,
+ },
+ },
+ },
+ root: {
+ label: '日本語',
+ lang: 'ja-JP',
+ themeConfig: {
+ ...ja,
+ footer: {
+ copyright: 'Copyright © 2026 m1sk9',
+ message: `<a href="https://github.com/m1sk9/LunaticChat/commit/${commitHash}">LunaticChat/docs@${commitHash}</a>`,
+ },
+ },
+ },
+ },
+ outDir: './dist',
+ srcDir: 'src',
+ themeConfig: {
+ socialLinks: [
+ { icon: 'github', link: 'https://github.com/m1sk9/LunaticChat' },
+ ],
+ },
+ title: 'LunaticChat',
+ titleTemplate: 'LunaticChat',
+});
diff --git a/website/.vitepress/config/en.ts b/website/.vitepress/config/en.ts
new file mode 100644
index 0000000..b42faeb
--- /dev/null
+++ b/website/.vitepress/config/en.ts
@@ -0,0 +1,133 @@
+import type { DefaultTheme } from 'vitepress';
+
+export const en: DefaultTheme.Config = {
+ editLink: {
+ pattern: 'https://github.com/m1sk9/LunaticChat/edit/main/docs/src/:path',
+ text: 'Edit this page on GitHub',
+ },
+ nav: [
+ { link: '/en/player-guide/getting-started', text: 'Player Guide' },
+ { link: '/en/admin-guide/getting-started', text: 'Admin Guide' },
+ { link: '/download', text: 'Download' },
+ ],
+ sidebar: {
+ '/en/admin-guide/': [
+ {
+ link: '/en/admin-guide/getting-started',
+ text: 'Getting Started',
+ },
+ {
+ link: '/en/admin-guide/configuration',
+ text: 'Configuration',
+ },
+ {
+ link: '/en/admin-guide/permissions',
+ text: 'Permissions',
+ },
+ {
+ items: [
+ {
+ link: '/en/admin-guide/channel-chat/introduction',
+ text: 'Deployment Guide',
+ },
+ {
+ link: '/en/admin-guide/channel-chat/logs',
+ text: 'Logs',
+ },
+ ],
+ text: 'Channel Chat',
+ },
+ {
+ link: '/en/admin-guide/velocity',
+ text: 'Velocity Integration',
+ },
+ {
+ link: '/en/admin-guide/cache',
+ text: 'Cache System',
+ },
+ {
+ link: '/en/admin-guide/management-data',
+ text: 'Data Management',
+ },
+ ],
+ '/en/player-guide/': [
+ {
+ link: '/en/player-guide/getting-started',
+ text: 'Getting Started',
+ },
+ {
+ link: '/en/player-guide/about',
+ text: 'About LunaticChat',
+ },
+ {
+ items: [
+ {
+ link: '/en/player-guide/channel-chat/private-channel',
+ text: 'Private Channel',
+ },
+ {
+ link: '/en/player-guide/channel-chat/moderation',
+ text: 'Moderation',
+ },
+ ],
+ link: '/en/player-guide/channel-chat/about',
+ text: 'Channel Chat',
+ },
+ {
+ link: '/en/player-guide/direct-message',
+ text: 'Direct Messages',
+ },
+ {
+ link: '/en/player-guide/japanese-romanization',
+ text: 'Romanization Conversion',
+ },
+ {
+ items: [
+ {
+ link: '/en/player-guide/commands/tell',
+ text: '/tell',
+ },
+ {
+ link: '/en/player-guide/commands/reply',
+ text: '/reply',
+ },
+ {
+ link: '/en/player-guide/commands/jp',
+ text: '/jp',
+ },
+ {
+ link: '/en/player-guide/commands/notice',
+ text: '/notice',
+ },
+ {
+ items: [
+ {
+ link: '/en/player-guide/commands/lc/settings',
+ text: '/lc settings',
+ },
+ {
+ link: '/en/player-guide/commands/lc/status',
+ text: '/lc status',
+ },
+ {
+ link: '/en/player-guide/commands/lc/channel',
+ text: '/lc channel',
+ },
+ ],
+ text: '/lc',
+ },
+ {
+ items: [
+ {
+ link: '/en/player-guide/commands/lcv/status',
+ text: '/lcv status',
+ },
+ ],
+ text: '/lcv',
+ },
+ ],
+ text: 'Commands',
+ },
+ ],
+ },
+};
diff --git a/website/.vitepress/config/ja.ts b/website/.vitepress/config/ja.ts
new file mode 100644
index 0000000..af2349b
--- /dev/null
+++ b/website/.vitepress/config/ja.ts
@@ -0,0 +1,124 @@
+import type { DefaultTheme } from 'vitepress';
+
+export const ja: DefaultTheme.Config = {
+ editLink: {
+ pattern: 'https://github.com/m1sk9/LunaticChat/edit/main/docs/src/:path',
+ text: 'GitHub で編集',
+ },
+ nav: [
+ { link: '/download', text: 'ダウンロード' },
+ { link: '/docs', text: 'ドキュメント' }
+ ],
+ sidebar: {
+ '/admin-guide/': [
+ {
+ link: '/admin-guide/getting-started',
+ text: 'はじめる',
+ },
+ {
+ link: '/admin-guide/configuration',
+ text: '設定',
+ },
+ {
+ link: '/admin-guide/permissions',
+ text: 'パーミッション',
+ },
+ {
+ items: [
+ {
+ link: '/admin-guide/channel-chat/introduction',
+ text: '展開ガイド',
+ },
+ {
+ link: '/admin-guide/channel-chat/logs',
+ text: 'ログ',
+ },
+ ],
+ text: 'チャンネルチャット',
+ },
+ {
+ link: '/admin-guide/velocity',
+ text: 'Velocity 連携',
+ },
+ {
+ link: '/admin-guide/cache',
+ text: 'キャッシュシステム',
+ },
+ {
+ link: '/admin-guide/management-data',
+ text: 'データの管理',
+ },
+ ],
+ '/player-guide/': [
+ {
+ link: '/player-guide/getting-started',
+ text: 'はじめる',
+ },
+ {
+ link: '/player-guide/about',
+ text: 'LunaticChat について',
+ },
+ {
+ items: [
+ {
+ link: '/player-guide/channel-chat/private-channel',
+ text: 'プライベートチャンネル',
+ },
+ {
+ link: '/player-guide/channel-chat/moderation',
+ text: 'モデレーション',
+ },
+ ],
+ link: '/player-guide/channel-chat/about',
+ text: 'チャンネルチャット',
+ },
+ {
+ link: '/player-guide/direct-message',
+ text: 'ダイレクトメッセージ',
+ },
+ {
+ link: '/player-guide/japanese-romanization',
+ text: 'ローマ字変換',
+ },
+ {
+ items: [
+ {
+ link: '/player-guide/commands/tell',
+ text: '/tell',
+ },
+ {
+ link: '/player-guide/commands/reply',
+ text: '/reply',
+ },
+ {
+ items: [
+ {
+ link: '/player-guide/commands/lc/settings',
+ text: '/lc settings',
+ },
+ {
+ link: '/player-guide/commands/lc/status',
+ text: '/lc status',
+ },
+ {
+ link: '/player-guide/commands/lc/channel',
+ text: '/lc channel',
+ },
+ ],
+ text: '/lc',
+ },
+ {
+ items: [
+ {
+ link: '/player-guide/commands/lcv/status',
+ text: '/lcv status',
+ },
+ ],
+ text: '/lcv',
+ },
+ ],
+ text: 'コマンド',
+ },
+ ],
+ },
+};
diff --git a/website/.vitepress/public/favicon.ico b/website/.vitepress/public/favicon.ico
new file mode 100644
index 0000000..46c0cad
--- /dev/null
+++ b/website/.vitepress/public/favicon.ico
Binary files differ
diff --git a/website/.vitepress/theme/components/DownloadCard.vue b/website/.vitepress/theme/components/DownloadCard.vue
new file mode 100644
index 0000000..d14b9d0
--- /dev/null
+++ b/website/.vitepress/theme/components/DownloadCard.vue
@@ -0,0 +1,338 @@
+<script setup lang="ts">
+import { computed } from 'vue';
+import { useData } from 'vitepress';
+import { data } from './download.data';
+
+const { lang } = useData();
+const isEn = computed(() => lang.value === 'en-US');
+
+const t = computed(() =>
+ isEn.value
+ ? {
+ title: 'Download',
+ description: 'Download the latest releases of LunaticChat.',
+ requirements: 'Requirements',
+ javaReq: '25 or later',
+ paperReq: '26.1.x or later',
+ velocityReq: '3.4.x or later',
+ noRelease: 'No release',
+ releaseDate: 'Release date',
+ fileSize: 'File size',
+ download: 'Download',
+ releaseNotes: 'Release notes',
+ modrinthDesc: 'LunaticChat is also available on Modrinth.',
+ notYetReleased: 'Not yet released.',
+ devBuilds: 'Development Builds',
+ devBuildsDesc:
+ 'The latest build from the main branch is available from CI. Development builds are not guaranteed to be stable.',
+ viewCiBuilds: 'View latest CI builds',
+ }
+ : {
+ title: 'ダウンロード',
+ description: 'LunaticChat の最新リリースをダウンロードできます。',
+ requirements: '動作要件',
+ javaReq: '25 以降',
+ paperReq: '26.1.x 以降',
+ velocityReq: '3.4.x 以降',
+ noRelease: 'リリースなし',
+ releaseDate: 'リリース日',
+ fileSize: 'ファイルサイズ',
+ download: 'ダウンロード',
+ releaseNotes: 'リリースノート',
+ modrinthDesc: 'LunaticChat は Modrinth でも公開しています。',
+ notYetReleased: 'まだリリースされていません。',
+ devBuilds: '開発ビルド',
+ devBuildsDesc:
+ '最新の main ブランチのビルドは CI から取得できます。開発ビルドは安定性が保証されていません。',
+ viewCiBuilds: '最新の CI ビルドを確認',
+ },
+);
+
+function formatSize(bytes: number | null): string {
+ if (!bytes) return '-';
+ const mb = bytes / (1024 * 1024);
+ return `${mb.toFixed(1)} MB`;
+}
+
+function formatDate(dateStr: string | null): string {
+ if (!dateStr) return '-';
+ const locale = isEn.value ? 'en-US' : 'ja-JP';
+ return new Date(dateStr).toLocaleDateString(locale, {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+}
+</script>
+
+<template>
+ <div class="download-page">
+ <h1>{{ t.title }}</h1>
+ <p>{{ t.description }}</p>
+
+ <div class="download-requirements">
+ <p class="download-requirements-title">{{ t.requirements }}</p>
+ <ul>
+ <li><strong>Java</strong>: {{ t.javaReq }}</li>
+ <li><strong>Paper / Folia</strong>: {{ t.paperReq }}</li>
+ <li><strong>Velocity</strong>: {{ t.velocityReq }}</li>
+ </ul>
+ </div>
+
+ <div class="download-grid">
+ <!-- Paper / Folia -->
+ <div class="download-card">
+ <div class="download-card-header">
+ <img src="/assets/brand/paper.svg" alt="Paper" class="download-icon" />
+ <div>
+ <h2>Paper / Folia</h2>
+ <p class="download-version" v-if="data.paper">v{{ data.paper.version }}</p>
+ <p class="download-version" v-else>{{ t.noRelease }}</p>
+ </div>
+ </div>
+ <div class="download-card-body" v-if="data.paper">
+ <dl class="download-meta">
+ <div>
+ <dt>{{ t.releaseDate }}</dt>
+ <dd>{{ formatDate(data.paper.publishedAt) }}</dd>
+ </div>
+ <div>
+ <dt>{{ t.fileSize }}</dt>
+ <dd>{{ formatSize(data.paper.fileSize) }}</dd>
+ </div>
+ <div>
+ <dd><code>{{ data.paper.fileName ?? '-' }}</code></dd>
+ </div>
+ </dl>
+ <div class="download-actions">
+ <a v-if="data.paper.downloadUrl" :href="data.paper.downloadUrl" class="download-btn primary">{{ t.download }}</a>
+ <a :href="data.paper.releaseUrl" class="download-btn" target="_blank" rel="noopener">{{ t.releaseNotes }}</a>
+ </div>
+ </div>
+ <div class="download-card-body" v-else>
+ <p class="download-empty">{{ t.notYetReleased }}</p>
+ </div>
+ </div>
+
+ <!-- Velocity -->
+ <div class="download-card">
+ <div class="download-card-header">
+ <img src="/assets/brand/velocity.svg" alt="Velocity" class="download-icon" />
+ <div>
+ <h2>Velocity</h2>
+ <p class="download-version" v-if="data.velocity">v{{ data.velocity.version }}</p>
+ <p class="download-version" v-else>{{ t.noRelease }}</p>
+ </div>
+ </div>
+ <div class="download-card-body" v-if="data.velocity">
+ <dl class="download-meta">
+ <div>
+ <dt>{{ t.releaseDate }}</dt>
+ <dd>{{ formatDate(data.velocity.publishedAt) }}</dd>
+ </div>
+ <div>
+ <dt>{{ t.fileSize }}</dt>
+ <dd>{{ formatSize(data.velocity.fileSize) }}</dd>
+ </div>
+ <div>
+ <dd><code>{{ data.velocity.fileName ?? '-' }}</code></dd>
+ </div>
+ </dl>
+ <div class="download-actions">
+ <a v-if="data.velocity.downloadUrl" :href="data.velocity.downloadUrl" class="download-btn primary">{{ t.download }}</a>
+ <a :href="data.velocity.releaseUrl" class="download-btn" target="_blank" rel="noopener">{{ t.releaseNotes }}</a>
+ </div>
+ </div>
+ <div class="download-card-body" v-else>
+ <p class="download-empty">{{ t.notYetReleased }}</p>
+ </div>
+ </div>
+ </div>
+
+ <h2>Modrinth</h2>
+ <p>{{ t.modrinthDesc }}</p>
+ <div class="download-ci">
+ <a href="https://modrinth.com/plugin/lunaticchat" class="download-btn" target="_blank" rel="noopener">Modrinth</a>
+ </div>
+
+ <h2>{{ t.devBuilds }}</h2>
+ <p>{{ t.devBuildsDesc }}</p>
+ <div class="download-ci">
+ <a :href="data.ci.url" class="download-btn" target="_blank" rel="noopener">{{ t.viewCiBuilds }}</a>
+ </div>
+ </div>
+</template>
+
+<style scoped>
+.download-page {
+ max-width: 768px;
+ margin: 0 auto;
+ padding: 48px 24px;
+}
+
+.download-page h1 {
+ font-size: 2rem;
+ font-weight: 700;
+ margin-bottom: 8px;
+}
+
+.download-page > p {
+ color: var(--vp-c-text-2);
+ margin-bottom: 24px;
+}
+
+.download-page h2 {
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin-top: 40px;
+ margin-bottom: 8px;
+ border: none;
+ padding: 0;
+}
+
+.download-requirements {
+ border: 1px solid var(--vp-c-brand-soft);
+ background: var(--vp-c-brand-soft);
+ border-radius: 8px;
+ padding: 16px 20px;
+ margin-bottom: 24px;
+}
+
+.download-requirements-title {
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+
+.download-requirements ul {
+ margin: 0;
+ padding-left: 20px;
+}
+
+.download-requirements li {
+ font-size: 0.9rem;
+ line-height: 1.7;
+}
+
+.download-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 20px;
+ margin: 24px 0;
+}
+
+@media (max-width: 640px) {
+ .download-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.download-card {
+ border: 1px solid var(--vp-c-divider);
+ border-radius: 12px;
+ overflow: hidden;
+ background: var(--vp-c-bg-soft);
+}
+
+.download-card-header {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 24px 24px 0;
+}
+
+.download-card-header h2 {
+ margin: 0;
+ font-size: 1.25rem;
+}
+
+.download-icon {
+ width: 40px;
+ height: 40px;
+ object-fit: contain;
+ filter: brightness(0) saturate(100%);
+}
+
+:global(.dark) .download-icon {
+ filter: brightness(0) saturate(100%) invert(1);
+}
+
+.download-version {
+ margin: 2px 0 0;
+ font-size: 0.875rem;
+ color: var(--vp-c-text-2);
+}
+
+.download-card-body {
+ padding: 20px 24px 24px;
+}
+
+.download-meta {
+ margin: 0 0 20px;
+ padding: 0;
+}
+
+.download-meta div {
+ display: flex;
+ justify-content: space-between;
+ padding: 6px 0;
+ border-bottom: 1px solid var(--vp-c-divider);
+ font-size: 0.875rem;
+}
+
+.download-meta div:last-child {
+ border-bottom: none;
+}
+
+.download-meta dt {
+ color: var(--vp-c-text-2);
+}
+
+.download-meta dd {
+ margin: 0;
+ color: var(--vp-c-text-1);
+}
+
+.download-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.download-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px 20px;
+ border-radius: 8px;
+ font-size: 0.875rem;
+ font-weight: 500;
+ text-decoration: none !important;
+ color: var(--vp-c-text-1);
+ border: 1px solid var(--vp-c-divider);
+ background: var(--vp-c-bg);
+ transition: border-color 0.25s, background 0.25s;
+}
+
+.download-btn:hover {
+ border-color: var(--vp-c-brand-1);
+}
+
+.download-btn.primary {
+ background: var(--vp-c-brand-1);
+ color: var(--vp-c-white);
+ border-color: var(--vp-c-brand-1);
+}
+
+.download-btn.primary:hover {
+ background: var(--vp-c-brand-2);
+ border-color: var(--vp-c-brand-2);
+}
+
+.download-empty {
+ color: var(--vp-c-text-3);
+ font-size: 0.875rem;
+}
+
+.download-ci {
+ margin: 16px 0;
+}
+</style>
diff --git a/website/.vitepress/theme/components/download.data.ts b/website/.vitepress/theme/components/download.data.ts
new file mode 100644
index 0000000..11e0489
--- /dev/null
+++ b/website/.vitepress/theme/components/download.data.ts
@@ -0,0 +1,105 @@
+const REPO = 'm1sk9/LunaticChat';
+
+interface ReleaseAsset {
+ name: string;
+ size: number;
+ browser_download_url: string;
+}
+
+interface GitHubRelease {
+ tag_name: string;
+ published_at: string;
+ html_url: string;
+ assets: ReleaseAsset[];
+}
+
+interface PlatformRelease {
+ version: string;
+ publishedAt: string;
+ releaseUrl: string;
+ downloadUrl: string | null;
+ fileName: string | null;
+ fileSize: number | null;
+}
+
+interface CIBuild {
+ url: string;
+}
+
+export interface DownloadData {
+ paper: PlatformRelease | null;
+ velocity: PlatformRelease | null;
+ ci: CIBuild;
+}
+
+async function fetchLatestRelease(
+ tagPrefix: string,
+): Promise<GitHubRelease | null> {
+ const res = await fetch(
+ `https://api.github.com/repos/${REPO}/releases?per_page=20`,
+ );
+ if (!res.ok) return null;
+
+ const releases: GitHubRelease[] = await res.json();
+ return (
+ releases.find(
+ (r) =>
+ r.tag_name.startsWith(tagPrefix) || r.tag_name.startsWith('v'),
+ ) ?? null
+ );
+}
+
+function parsePlatformRelease(
+ release: GitHubRelease | null,
+ jarPattern: RegExp,
+): PlatformRelease | null {
+ if (!release) return null;
+
+ const asset = release.assets.find((a) => jarPattern.test(a.name));
+ const version = release.tag_name.replace(/^(paper\/|velocity\/)?v/, '');
+
+ return {
+ version,
+ publishedAt: release.published_at,
+ releaseUrl: release.html_url,
+ downloadUrl: asset?.browser_download_url ?? null,
+ fileName: asset?.name ?? null,
+ fileSize: asset?.size ?? null,
+ };
+}
+
+export default {
+ async load(): Promise<DownloadData> {
+ // Fetch all recent releases and find the latest for each platform
+ const res = await fetch(
+ `https://api.github.com/repos/${REPO}/releases?per_page=30`,
+ );
+ const releases: GitHubRelease[] = res.ok ? await res.json() : [];
+
+ // Find latest Paper release (paper/v* or v*)
+ const paperRelease =
+ releases.find((r) => r.tag_name.startsWith('paper/v')) ??
+ releases.find((r) => /^v\d/.test(r.tag_name)) ??
+ null;
+
+ // Find latest Velocity release (velocity/v* or v*)
+ const velocityRelease =
+ releases.find((r) => r.tag_name.startsWith('velocity/v')) ??
+ releases.find((r) => /^v\d/.test(r.tag_name)) ??
+ null;
+
+ return {
+ paper: parsePlatformRelease(
+ paperRelease,
+ /^LunaticChat-[\d.]+\.jar$/,
+ ),
+ velocity: parsePlatformRelease(
+ velocityRelease,
+ /^LunaticChat-[\d.]+-velocity\.jar$/,
+ ),
+ ci: {
+ url: `https://github.com/${REPO}/actions/workflows/ci.yaml?query=branch%3Amain`,
+ },
+ };
+ },
+};
diff --git a/website/.vitepress/theme/custom.css b/website/.vitepress/theme/custom.css
new file mode 100644
index 0000000..1f928a1
--- /dev/null
+++ b/website/.vitepress/theme/custom.css
@@ -0,0 +1,188 @@
+/* ── Home page custom styles ── */
+
+/* Hero tagline emphasis */
+.VPHero .tagline {
+ font-size: 1.2rem !important;
+ max-width: 560px;
+}
+
+/* ── Feature showcase: alternating text + image ── */
+.feature-showcase {
+ max-width: 1152px;
+ margin: 0 auto;
+ padding: 64px 24px;
+ display: flex;
+ align-items: center;
+ gap: 48px;
+}
+
+.feature-showcase.reverse {
+ flex-direction: row-reverse;
+}
+
+.feature-showcase-text {
+ flex: 3;
+ min-width: 0;
+}
+
+.feature-showcase-text h2 {
+ font-size: 1.75rem;
+ font-weight: 700;
+ line-height: 1.3;
+ margin-bottom: 16px;
+}
+
+.feature-showcase-text p {
+ color: var(--vp-c-text-2);
+ font-size: 1rem;
+ line-height: 1.7;
+ margin-bottom: 12px;
+}
+
+.feature-showcase-text ul {
+ list-style: none;
+ padding: 0;
+ margin: 16px 0 0;
+}
+
+.feature-showcase-text ul li {
+ position: relative;
+ padding-left: 24px;
+ color: var(--vp-c-text-2);
+ font-size: 0.95rem;
+ line-height: 1.7;
+ margin-bottom: 8px;
+}
+
+.feature-showcase-text ul li::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 10px;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--vp-c-brand-1);
+}
+
+.feature-showcase-image {
+ flex: 3;
+ min-width: 0;
+}
+
+.feature-showcase-image img {
+ width: 100%;
+ border-radius: 12px;
+ border: 1px solid var(--vp-c-divider);
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
+}
+
+.feature-showcase-image .image-placeholder {
+ width: 100%;
+ aspect-ratio: 16 / 10;
+ border-radius: 12px;
+ border: 2px dashed var(--vp-c-divider);
+ background: var(--vp-c-bg-soft);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--vp-c-text-3);
+ font-size: 0.875rem;
+}
+
+@media (max-width: 768px) {
+ .feature-showcase,
+ .feature-showcase.reverse {
+ flex-direction: column;
+ padding: 40px 24px;
+ gap: 32px;
+ }
+}
+
+/* ── Platform cards ── */
+.platform-section {
+ max-width: 1024px;
+ margin: 0 auto;
+ padding: 64px 24px;
+ text-align: center;
+}
+
+.platform-section h2 {
+ font-size: 1.75rem;
+ font-weight: 700;
+ margin-bottom: 8px;
+}
+
+.platform-section .section-desc {
+ color: var(--vp-c-text-2);
+ margin-bottom: 32px;
+ font-size: 1rem;
+}
+
+.platform-cards {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 16px;
+}
+
+@media (max-width: 640px) {
+ .platform-cards {
+ grid-template-columns: 1fr;
+ }
+}
+
+.platform-card {
+ display: block;
+ border: 1px solid var(--vp-c-divider);
+ border-radius: 12px;
+ padding: 24px 16px;
+ background: var(--vp-c-bg-soft);
+ transition: border-color 0.25s, box-shadow 0.25s;
+ text-decoration: none !important;
+ color: inherit;
+}
+
+.platform-card:hover {
+ border-color: var(--vp-c-brand-1);
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+}
+
+.platform-icon {
+ margin-bottom: 12px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.platform-icon img {
+ width: 40px;
+ height: 40px;
+ object-fit: contain;
+ filter: brightness(0) saturate(100%);
+}
+
+.dark .platform-icon img {
+ filter: brightness(0) saturate(100%) invert(1);
+}
+
+.platform-card .name {
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin-bottom: 8px;
+ color: var(--vp-c-text-1);
+}
+
+.platform-card .desc {
+ font-size: 0.875rem;
+ color: var(--vp-c-text-2);
+ line-height: 1.6;
+}
+
+/* ── Divider ── */
+.home-divider {
+ max-width: 1024px;
+ margin: 0 auto;
+ border: none;
+ border-top: 1px solid var(--vp-c-divider);
+}
diff --git a/website/.vitepress/theme/index.ts b/website/.vitepress/theme/index.ts
new file mode 100644
index 0000000..bc82522
--- /dev/null
+++ b/website/.vitepress/theme/index.ts
@@ -0,0 +1,11 @@
+import type { Theme } from 'vitepress';
+import DefaultTheme from 'vitepress/theme';
+import DownloadCard from './components/DownloadCard.vue';
+import './custom.css';
+
+export default {
+ extends: DefaultTheme,
+ enhanceApp({ app }) {
+ app.component('DownloadCard', DownloadCard);
+ },
+} satisfies Theme;