feat(site): rewrite the landing page around verified facts and accessible controls

- Replace claims that contradicted the app: the default hotkey is Right Alt
  (hold to talk), local features are free with no daily cap, there is no
  14-day trial, Team/SSO/SCIM/ZDR are not offered, and the repository is not
  public. Remove invented metrics, status badges, the competitor table, the
  ad-mediation changelog, the hash calculator and the duplicate demo.
- Seven sections: hero with one labelled example, features, how it works,
  privacy, pricing (Free / Pro 2,900 / Pro+ 8,900 KRW a month), download, FAQ.
  Footer links the privacy policy, terms and account deletion pages.
- All copy moves into i18n and every one of the 10 locales is translated.
  Pricing and cloud quotas come from site/src/pricing.ts.
- Accessibility: skip link, labelled sections, aria-expanded with Esc and
  focus return for the menu, language list and FAQ, live status for the
  example, reduced-motion support, 44px targets, AA contrast on the primary
  button. Korean keep-all line breaking is scoped to :lang(ko) because
  Tailwind's break-keep blocked wrapping in Japanese and Chinese.
- JS 111 -> 98 KB gzip, CSS 7.8 -> 5.2 KB gzip.
This commit is contained in:
Yun Chan 2026-09-26 15:04:43 +09:00
parent ad6bb70c20
commit e689683b72
44 changed files with 2198 additions and 3782 deletions

View file

@ -1,110 +1,28 @@
import { useState, useEffect } from 'react'
import {
DESKTOP_VERSION,
DESKTOP_WINDOWS_INSTALLER_FILENAME,
DESKTOP_WINDOWS_INSTALLER_URL,
} from '../release'
export type ClientOSType = 'android' | 'ios' | 'windows' | 'macos' | 'linux'
export type ClientOSType = 'windows' | 'macos' | 'linux' | 'android' | 'ios'
export interface ClientOSInfo {
os: ClientOSType
osName: string
isMobile: boolean
downloadUrl: string
downloadFilename: string
badgeText: string
buttonLabelKo: string
buttonLabelEn: string
subtextKo: string
subtextEn: string
interface NavigatorWithUAData extends Navigator {
userAgentData?: { platform?: string }
}
export const OS_CONFIGS: Record<ClientOSType, ClientOSInfo> = {
android: {
os: 'android',
osName: 'Android',
isMobile: true,
downloadUrl: '#download',
downloadFilename: '',
badgeText: 'ANDROID RELEASE PENDING',
buttonLabelKo: 'Android 공식 배포 준비 중',
buttonLabelEn: 'Android release unavailable',
subtextKo: '서명된 배포 증거가 확인될 때까지 다운로드를 제공하지 않습니다.',
subtextEn: 'Download stays unavailable until signed release evidence is verified.',
},
ios: {
os: 'ios',
osName: 'iOS',
isMobile: true,
downloadUrl: '#download',
downloadFilename: '',
badgeText: 'iOS DISTRIBUTION UNAVAILABLE',
buttonLabelKo: 'iOS 배포 미지원',
buttonLabelEn: 'iOS distribution unavailable',
subtextKo: '현재 검증된 App Store 또는 TestFlight 배포가 없습니다.',
subtextEn: 'No verified App Store or TestFlight distribution is available.',
},
windows: {
os: 'windows',
osName: 'Windows',
isMobile: false,
downloadUrl: DESKTOP_WINDOWS_INSTALLER_URL,
downloadFilename: DESKTOP_WINDOWS_INSTALLER_FILENAME,
badgeText: `DESKTOP ${DESKTOP_VERSION} OFFICIAL RELEASE`,
buttonLabelKo: `Windows용 다운로드 (x64) - v${DESKTOP_VERSION}`,
buttonLabelEn: `Download for Windows (x64) - v${DESKTOP_VERSION}`,
subtextKo: `서명된 공식 릴리스 v${DESKTOP_VERSION}이며, 설치 후 자동 업데이트 피드로 갱신됩니다.`,
subtextEn: `Signed official release v${DESKTOP_VERSION}, updated through the auto-update feed after install.`,
},
macos: {
os: 'macos',
osName: 'macOS',
isMobile: false,
downloadUrl: '#download',
downloadFilename: '',
badgeText: 'MACOS APPLE SILICON PREPARATION',
buttonLabelKo: 'macOS 릴리스 준비 중',
buttonLabelEn: 'macOS release in preparation',
subtextKo: 'Apple Silicon용 빌드는 서명과 설치 검증을 마친 뒤 제공합니다.',
subtextEn: 'The Apple Silicon build will be published after signing and installation verification.',
},
linux: {
os: 'linux',
osName: 'Linux',
isMobile: false,
downloadUrl: '#download',
downloadFilename: '',
badgeText: 'LINUX PACKAGE UNAVAILABLE',
buttonLabelKo: 'Linux 배포 미지원',
buttonLabelEn: 'Linux package unavailable',
subtextKo: '현재 검증된 AppImage 또는 DEB 패키지가 없습니다.',
subtextEn: 'No verified AppImage or DEB package is available.',
},
function detectOS(): ClientOSType {
const nav = navigator as NavigatorWithUAData
const ua = nav.userAgent || ''
const platform = nav.userAgentData?.platform || nav.platform || ''
if (/android/i.test(ua)) return 'android'
if (/iphone|ipad|ipod/i.test(ua) || (platform === 'MacIntel' && nav.maxTouchPoints > 1)) return 'ios'
if (/macintosh|mac os x/i.test(ua) || /mac/i.test(platform)) return 'macos'
if (/linux/i.test(ua) || /linux/i.test(platform)) return 'linux'
return 'windows'
}
export function useClientOS(): ClientOSInfo {
const [osInfo, setOsInfo] = useState<ClientOSInfo>(OS_CONFIGS.windows)
/** 방문자의 OS. 첫 렌더는 'windows'로 시작해 SSR·첫 페인트 차이를 없앤다. */
export function useClientOS(): ClientOSType {
const [os, setOs] = useState<ClientOSType>('windows')
useEffect(() => {
if (typeof window === 'undefined' || !navigator) return
const ua = navigator.userAgent || ''
const platform = (navigator as any).userAgentData?.platform || navigator.platform || ''
if (/android/i.test(ua)) {
setOsInfo(OS_CONFIGS.android)
} else if (/iphone|ipad|ipod/i.test(ua) || (platform === 'MacIntel' && navigator.maxTouchPoints > 1)) {
setOsInfo(OS_CONFIGS.ios)
} else if (/macintosh|mac os x/i.test(ua) || /mac/i.test(platform)) {
setOsInfo(OS_CONFIGS.macos)
} else if (/linux/i.test(ua) || /linux/i.test(platform)) {
setOsInfo(OS_CONFIGS.linux)
} else if (/windows|win32|win64/i.test(ua) || /win/i.test(platform)) {
setOsInfo(OS_CONFIGS.windows)
} else {
setOsInfo(OS_CONFIGS.windows)
}
setOs(detectOS())
}, [])
return osInfo
return os
}

View file

@ -0,0 +1,17 @@
import { useEffect, useState } from 'react'
const QUERY = '(prefers-reduced-motion: reduce)'
/** 사용자의 감소 모션 설정을 따라간다. 설정이 바뀌면 다시 렌더한다. */
export function usePrefersReducedMotion(): boolean {
const [reduced, setReduced] = useState(() => window.matchMedia(QUERY).matches)
useEffect(() => {
const media = window.matchMedia(QUERY)
const onChange = () => setReduced(media.matches)
media.addEventListener('change', onChange)
return () => media.removeEventListener('change', onChange)
}, [])
return reduced
}