Skip to content

Commit 7370f9b

Browse files
committed
feat(docs): redesign GitHub Pages with kimi-cli style and language switcher
- Replace minimal style.css with complete 654-line design system - Add custom homepage components (home-header, feature-map, quick-start) - Implement GitHub-style dark mode backgrounds - Add responsive breakpoints (960px, 640px) - Add LanguageSwitcher component for nav bar language toggle - Add CI docs verification workflow - Simplify root index.md to language selector - Restructure en/zh index pages with custom HTML layout - Set content max-width to 800px for C++ code blocks Based on kimi-cli project's GitHub Pages design.
1 parent e3b0242 commit 7370f9b

8 files changed

Lines changed: 1177 additions & 98 deletions

File tree

.github/workflows/ci-docs.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: CI (docs)
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- ".github/workflows/ci-docs.yml"
7+
- ".github/workflows/pages.yml"
8+
- "docs/**"
9+
push:
10+
branches:
11+
- master
12+
paths:
13+
- ".github/workflows/ci-docs.yml"
14+
- ".github/workflows/pages.yml"
15+
- "docs/**"
16+
17+
jobs:
18+
build:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@v4
23+
24+
- name: Set up Node.js
25+
uses: actions/setup-node@v4
26+
with:
27+
node-version: "22"
28+
cache: npm
29+
cache-dependency-path: docs/package-lock.json
30+
31+
- name: Install docs dependencies
32+
working-directory: docs
33+
run: npm ci
34+
35+
- name: Build docs
36+
working-directory: docs
37+
run: npm run build
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<script setup lang="ts">
2+
import { onMounted } from 'vue'
3+
import { useRouter, useRoute, useData } from 'vitepress'
4+
5+
const STORAGE_KEY = 'cpp-hpc-guide-lang-preference'
6+
const DEFAULT_LANG = 'en'
7+
8+
const SUPPORTED_LANGS = [
9+
{ code: 'en', path: '/en/', match: ['en', 'en-US', 'en-GB', 'en-CA', 'en-AU'] },
10+
{ code: 'zh', path: '/zh/', match: ['zh', 'zh-CN', 'zh-TW', 'zh-HK', 'zh-SG'] },
11+
]
12+
13+
function getBrowserLanguage(): string {
14+
if (typeof navigator === 'undefined') return DEFAULT_LANG
15+
return navigator.language || (navigator as any).userLanguage || DEFAULT_LANG
16+
}
17+
18+
function detectLanguage(): string {
19+
const stored = typeof localStorage !== 'undefined' ? localStorage.getItem(STORAGE_KEY) : null
20+
if (stored) return stored
21+
22+
const browserLang = getBrowserLanguage()
23+
for (const lang of SUPPORTED_LANGS) {
24+
if (lang.match.some(m => browserLang.toLowerCase().startsWith(m.toLowerCase()))) {
25+
return lang.code
26+
}
27+
}
28+
return DEFAULT_LANG
29+
}
30+
31+
function getLanguagePath(base: string, langCode: string): string {
32+
const lang = SUPPORTED_LANGS.find(l => l.code === langCode)
33+
const langPath = lang?.path || `/${langCode}/`
34+
// Combine base path with language path, avoiding double slashes
35+
if (base === '/') return langPath
36+
const cleanBase = base.endsWith('/') ? base.slice(0, -1) : base
37+
return `${cleanBase}${langPath}`
38+
}
39+
40+
onMounted(() => {
41+
// Only run on client side
42+
if (typeof window === 'undefined') return
43+
44+
const router = useRouter()
45+
const route = useRoute()
46+
const { site } = useData()
47+
48+
const base = site.value.base || '/'
49+
const currentPath = route.path
50+
51+
// Only redirect from root path (not already on a language-specific path)
52+
// Handle both `/` and `/cpp-high-performance-guide/` cases
53+
const isRoot = currentPath === '/' || currentPath === base || currentPath === base.replace(/\/$/, '')
54+
if (!isRoot) {
55+
return
56+
}
57+
58+
const targetLang = detectLanguage()
59+
const targetPath = getLanguagePath(base, targetLang)
60+
61+
// Avoid redirect loop - only redirect if we're not already at the target
62+
if (!currentPath.startsWith(targetPath.replace(/\/$/, ''))) {
63+
// Use replace to avoid adding a history entry
64+
router.go(targetPath)
65+
}
66+
})
67+
</script>
68+
69+
<template>
70+
<!-- This component has no visual representation -->
71+
</template>
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
<script setup lang="ts">
2+
import { ref, computed } from 'vue'
3+
import { useRouter, useRoute, useData } from 'vitepress'
4+
5+
const STORAGE_KEY = 'cpp-hpc-guide-lang-preference'
6+
7+
const SUPPORTED_LANGS = [
8+
{ code: 'en', label: 'English', path: '/en/' },
9+
{ code: 'zh', label: '中文', path: '/zh/' },
10+
]
11+
12+
const { site, theme } = useData()
13+
const router = useRouter()
14+
const route = useRoute()
15+
const isOpen = ref(false)
16+
17+
const currentLang = computed(() => {
18+
const path = route.path
19+
const lang = SUPPORTED_LANGS.find(l => path.startsWith(l.path))
20+
return lang || SUPPORTED_LANGS[0]
21+
})
22+
23+
function switchLang(lang: typeof SUPPORTED_LANGS[0]) {
24+
if (lang.code === currentLang.value.code) {
25+
isOpen.value = false
26+
return
27+
}
28+
29+
// Save preference
30+
if (typeof localStorage !== 'undefined') {
31+
localStorage.setItem(STORAGE_KEY, lang.code)
32+
}
33+
34+
// Calculate target path
35+
const base = site.value.base || '/'
36+
const currentPath = route.path
37+
38+
// Remove current language prefix
39+
let pathWithoutLang = currentPath
40+
for (const l of SUPPORTED_LANGS) {
41+
const langPath = base + l.path.substring(1)
42+
if (currentPath.startsWith(langPath)) {
43+
pathWithoutLang = currentPath.substring(langPath.length)
44+
break
45+
}
46+
}
47+
48+
// Build target path
49+
const targetPath = base + lang.path.substring(1) + pathWithoutLang
50+
51+
isOpen.value = false
52+
router.go(targetPath)
53+
}
54+
55+
function toggle() {
56+
isOpen.value = !isOpen.value
57+
}
58+
59+
function handleClickOutside(event: MouseEvent) {
60+
const target = event.target as HTMLElement
61+
if (!target.closest('.language-switcher')) {
62+
isOpen.value = false
63+
}
64+
}
65+
66+
// Close on outside click
67+
if (typeof document !== 'undefined') {
68+
document.addEventListener('click', handleClickOutside)
69+
}
70+
</script>
71+
72+
<template>
73+
<div class="language-switcher">
74+
<button class="language-button" @click="toggle" :title="theme.langMenuLabel || 'Switch Language'">
75+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
76+
<circle cx="12" cy="12" r="10"/>
77+
<path d="M2 12h20"/>
78+
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
79+
</svg>
80+
<span class="language-label">{{ currentLang.label }}</span>
81+
<svg class="chevron" :class="{ open: isOpen }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
82+
<polyline points="6 9 12 15 18 9"/>
83+
</svg>
84+
</button>
85+
86+
<Transition name="dropdown">
87+
<div v-if="isOpen" class="language-dropdown">
88+
<button
89+
v-for="lang in SUPPORTED_LANGS"
90+
:key="lang.code"
91+
class="language-option"
92+
:class="{ active: lang.code === currentLang.code }"
93+
@click="switchLang(lang)"
94+
>
95+
<span class="option-label">{{ lang.label }}</span>
96+
<svg v-if="lang.code === currentLang.code" class="check-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
97+
<polyline points="20 6 9 17 4 12"/>
98+
</svg>
99+
</button>
100+
</div>
101+
</Transition>
102+
</div>
103+
</template>
104+
105+
<style scoped>
106+
.language-switcher {
107+
position: relative;
108+
display: inline-flex;
109+
align-items: center;
110+
}
111+
112+
.language-button {
113+
display: inline-flex;
114+
align-items: center;
115+
gap: 6px;
116+
padding: 6px 10px;
117+
border: 1px solid var(--vp-c-border);
118+
border-radius: 6px;
119+
background: transparent;
120+
color: var(--vp-c-text-2);
121+
font-size: 13px;
122+
cursor: pointer;
123+
transition: all 0.15s ease;
124+
white-space: nowrap;
125+
}
126+
127+
.language-button:hover {
128+
border-color: var(--vp-c-brand-1);
129+
color: var(--vp-c-brand-1);
130+
}
131+
132+
.language-label {
133+
display: none;
134+
}
135+
136+
@media (min-width: 768px) {
137+
.language-label {
138+
display: inline;
139+
}
140+
}
141+
142+
.chevron {
143+
transition: transform 0.2s ease;
144+
}
145+
146+
.chevron.open {
147+
transform: rotate(180deg);
148+
}
149+
150+
.language-dropdown {
151+
position: absolute;
152+
top: 100%;
153+
right: 0;
154+
margin-top: 4px;
155+
min-width: 120px;
156+
background: var(--vp-c-bg);
157+
border: 1px solid var(--vp-c-border);
158+
border-radius: 8px;
159+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
160+
overflow: hidden;
161+
z-index: 100;
162+
}
163+
164+
.language-option {
165+
display: flex;
166+
align-items: center;
167+
justify-content: space-between;
168+
width: 100%;
169+
padding: 10px 14px;
170+
border: none;
171+
background: transparent;
172+
color: var(--vp-c-text-1);
173+
font-size: 14px;
174+
cursor: pointer;
175+
transition: background 0.15s ease;
176+
}
177+
178+
.language-option:hover {
179+
background: var(--vp-c-bg-soft);
180+
}
181+
182+
.language-option.active {
183+
color: var(--vp-c-brand-1);
184+
background: var(--vp-c-brand-soft);
185+
}
186+
187+
.check-icon {
188+
color: var(--vp-c-brand-1);
189+
}
190+
191+
/* Dropdown transition */
192+
.dropdown-enter-active,
193+
.dropdown-leave-active {
194+
transition: opacity 0.15s ease, transform 0.15s ease;
195+
}
196+
197+
.dropdown-enter-from,
198+
.dropdown-leave-to {
199+
opacity: 0;
200+
transform: translateY(-4px);
201+
}
202+
</style>

docs/.vitepress/theme/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1+
import { h } from 'vue'
2+
import type { Theme } from 'vitepress'
13
import DefaultTheme from 'vitepress/theme'
24
import './style.css'
5+
import LanguageRedirect from './LanguageRedirect.vue'
6+
import LanguageSwitcher from './LanguageSwitcher.vue'
37

4-
export default DefaultTheme
8+
export default {
9+
extends: DefaultTheme,
10+
Layout() {
11+
return h(DefaultTheme.Layout, null, {
12+
'layout-top': () => h(LanguageRedirect),
13+
'nav-bar-content-after': () => h(LanguageSwitcher),
14+
})
15+
},
16+
} satisfies Theme

0 commit comments

Comments
 (0)