feat: add landing page and docs website (#537)

* feat: add landing page and docs website package

Add packages/website — a Vue 3 + Naive UI static site with landing page
and documentation, sharing the Pure Ink monochrome design with the main
app. Features: particle network hero animation, screenshot carousel,
feature grid, install guide tabs, GitHub star history, scroll reveal
animations, and Chinese/English bilingual support.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: add favicon to website package

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: use dynamic theme param for star history chart

Switch from CSS media query to JS-based dark mode detection so the
star-history SVG matches the current theme toggle state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: resolve TypeScript strict mode errors in website components

- Remove unused isDark import in HeroSection
- Add null check for canvas parent element
- Rename unused img loop variable in ScreenshotsSection
- Remove unused NIcon import in SiteHeader

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: resolve TS narrowing errors in canvas resize closure

Use canvasRef.value directly inside resize() with local null check
instead of relying on outer closure narrowing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-05-08 13:36:11 +08:00
committed by GitHub
parent b0e03ae838
commit 9edb76ac64
34 changed files with 2672 additions and 1 deletions
@@ -0,0 +1,27 @@
import { onMounted, onUnmounted } from 'vue'
export function useScrollReveal() {
let observer: IntersectionObserver | null = null
onMounted(() => {
observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.classList.add('revealed')
observer!.unobserve(entry.target)
}
}
},
{ threshold: 0.1, rootMargin: '0px 0px -40px 0px' },
)
document.querySelectorAll('.reveal').forEach((el) => {
observer!.observe(el)
})
})
onUnmounted(() => {
observer?.disconnect()
})
}
@@ -0,0 +1,54 @@
import { ref, watch } from 'vue'
export type ThemeMode = 'light' | 'dark' | 'system'
const STORAGE_KEY = 'hermes_website_theme'
const mode = ref<ThemeMode>(
(localStorage.getItem(STORAGE_KEY) as ThemeMode) || 'system',
)
const isDark = ref(false)
function applyTheme(dark: boolean) {
isDark.value = dark
document.documentElement.classList.toggle('dark', dark)
}
function resolveDark(m: ThemeMode): boolean {
if (m === 'system') {
return window.matchMedia('(prefers-color-scheme: dark)').matches
}
return m === 'dark'
}
applyTheme(resolveDark(mode.value))
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addEventListener('change', () => {
if (mode.value === 'system') {
applyTheme(resolveDark('system'))
}
})
watch(mode, (newMode) => {
localStorage.setItem(STORAGE_KEY, newMode)
applyTheme(resolveDark(newMode))
})
export function useTheme() {
function setMode(m: ThemeMode) {
mode.value = m
}
function toggleTheme() {
mode.value = isDark.value ? 'light' : 'dark'
}
return {
mode,
isDark,
setMode,
toggleTheme,
}
}