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:
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user