From 51473c309f82c603623564a90a1b692fd7c1cbb0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 4 Nov 2026 15:11:55 +0000 Subject: [PATCH] feat(website): give the marketing page a letterpress finish Keep the paper-and-pixel language across the whole page: a sheet under the hero, update notes as separate tickets, and press states on steps and screenshots. The nav adds an app anchor and follows the current section, including the FAQ at the bottom of the page. Co-authored-by: SlippyChimpMG --- website/DESIGN.md | 2 +- website/app/globals.css | 25 ++++++++++--- website/components/GlassControls.tsx | 54 ++++++++++++++++++++++++++-- website/components/MarketingPage.tsx | 8 ++--- 4 files changed, 77 insertions(+), 12 deletions(-) diff --git a/website/DESIGN.md b/website/DESIGN.md index fcf4c56..2486832 100644 --- a/website/DESIGN.md +++ b/website/DESIGN.md @@ -107,7 +107,7 @@ spacing: ## 交互与响应式 -语言和主题菜单继续使用原生 `details`,手机导航和截图使用原生 `dialog`,保留 Escape、焦点恢复、菜单互斥、点击外部关闭与弹窗滚动锁定。FAQ、跳至正文、可见键盘焦点和 44px 操作区继续保留。缩略截图使用 WebP,放大仍读取原始 PNG。 +语言和主题菜单继续使用原生 `details`,手机导航和截图使用原生 `dialog`,保留 Escape、焦点恢复、菜单互斥、点击外部关闭与弹窗滚动锁定。桌面导航含界面锚点,当前章节用下划线跟随滚动;更新说明是独立纸签,三步与截图在悬停时呈压印,主题颜色以 180ms 过渡,页面不做滚动淡入。FAQ、跳至正文、可见键盘焦点和 44px 操作区继续保留。缩略截图使用 WebP,放大仍读取原始 PNG。 截图复用首次绘制脚本与选择器已经维护的 `.dark` 类,不另设主题状态或监听器。默认浅色、显式深色及跟随系统都使用同一个显示规则。CSS 隐藏的另一个主题图片不占布局,也不进入无障碍树。 diff --git a/website/app/globals.css b/website/app/globals.css index 34d1392..a5a34d4 100644 --- a/website/app/globals.css +++ b/website/app/globals.css @@ -35,6 +35,7 @@ * { box-sizing: border-box; } html { scroll-behavior: smooth; scroll-padding-top: 100px; } body { margin: 0; background: var(--background); background-image: radial-gradient(color-mix(in srgb, var(--ink) 5%, transparent) .6px, transparent .6px); background-size: 5px 5px; color: var(--ink); font: 16px/1.8 "Segoe UI", -apple-system, BlinkMacSystemFont, "Microsoft YaHei", sans-serif; -webkit-font-smoothing: antialiased; } +body, .section, .site-header, .surface, h1, h2, h3, p, li, article, figcaption, summary, code, .eyebrow, .section-intro, .platform-label, .hero-facts, .art-caption, .filename, .release-label { transition: background-color .18s ease, color .18s ease, border-color .18s ease; } html[lang="zh-TW"] body { font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, "Microsoft JhengHei", sans-serif; } html[lang="ja"] body { font-family: -apple-system, BlinkMacSystemFont, "Yu Gothic", Meiryo, sans-serif; } html[lang="ja"] { --display: "Yu Mincho", "Hiragino Mincho ProN", "MS PMincho", Georgia, serif; } @@ -45,7 +46,7 @@ button { font: inherit; color: inherit; cursor: pointer; } button, figure, p, h1, h2, h3 { margin: 0; } a { color: inherit; text-decoration: none; } img { display: block; max-width: 100%; height: auto; } -a, button { transition: color .18s, background-color .18s, transform .18s, box-shadow .18s; } +a, button { transition: color .18s, background-color .18s, border-color .18s, transform .18s, box-shadow .18s; } a:hover { color: var(--primary); } button:disabled { cursor: default; } [hidden] { display: none !important; } @@ -55,7 +56,7 @@ main:focus { outline: none; } .container { width: min(1280px, 100%); margin-inline: auto; padding-inline: 48px; } .section { padding-block: clamp(68px, 8vw, 112px); } .section + .section { border-top: 1px solid var(--line); } -.tinted { background: var(--tinted); } +.tinted { background: var(--tinted); box-shadow: inset 0 1px 0 color-mix(in srgb, var(--surface) 80%, transparent), inset 0 -1px 0 color-mix(in srgb, var(--ink) 8%, transparent); } h1, h2 { font-family: var(--display); font-weight: 700; text-wrap: balance; color: var(--ink); } h1 { font-size: clamp(44px, 4.8vw, 68px); line-height: 1.22; letter-spacing: -.045em; } h1 span { display: block; color: var(--primary); } @@ -80,6 +81,7 @@ html[lang="ja"] h2 { font-size: clamp(29px, 3vw, 42px); line-height: 1.4; } .desktop-nav { gap: 4px; margin-inline: auto; font-size: 13px; color: var(--muted); } .desktop-nav a { display: inline-flex; align-items: center; min-height: 44px; padding: 9px 12px; border-radius: 3px 3px 0 0; white-space: nowrap; } .desktop-nav a:hover { color: var(--ink); background: var(--soft); box-shadow: inset 0 -2px var(--primary); } +.desktop-nav a[aria-current="location"], .desktop-nav a[aria-current="location"]:hover { color: var(--ink); background: var(--soft); box-shadow: inset 0 -2px var(--primary); } .header-controls { gap: 4px; } .icon-button, .github-link { display: inline-grid; place-items: center; flex-shrink: 0; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 4px; background: transparent; color: var(--muted); } .icon-button:hover, .github-link:hover { background: var(--soft); color: var(--ink); } @@ -117,6 +119,7 @@ summary::-webkit-details-marker { display: none; } .hero-facts { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 16px; margin-top: 20px; color: var(--muted); font-size: 12px; } .hero-facts > a { display: inline-flex; align-items: center; min-height: 44px; } .hero-art { position: relative; min-width: 0; isolation: isolate; padding-block: 26px 12px; } +.hero-art::before { content: ""; position: absolute; z-index: -1; left: 7%; right: 5%; top: 52px; bottom: 36px; background: var(--surface); border: 1px solid var(--line); box-shadow: 7px 7px 0 color-mix(in srgb, var(--line) 82%, transparent); transform: rotate(-1.1deg); } .hero-illustration { width: 100%; height: auto; filter: drop-shadow(0 14px 15px rgb(66 44 21 / .07)); } .art-index { position: absolute; top: 3px; right: 20px; padding: 4px 10px; border: 1px solid var(--line); color: var(--secondary); font: 10px/1.6 ui-monospace, "Cascadia Mono", monospace; letter-spacing: .1em; } .art-caption { display: block; padding-top: 8px; text-align: right; color: var(--muted); font-size: 10px; letter-spacing: .03em; } @@ -136,12 +139,15 @@ summary::-webkit-details-marker { display: none; } .journey-board img { width: 100%; max-height: 420px; object-fit: contain; } .journey-board .art-caption { text-align: center; } .steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px; list-style: none; margin: 28px 0 0; padding: 0; } -.steps li { display: flex; align-items: flex-start; gap: 16px; min-width: 0; padding-block: 23px 0; border-top: 1px dashed var(--line); } +.steps li { display: flex; align-items: flex-start; gap: 16px; min-width: 0; padding: 20px 14px 12px; border-top: 1px dashed var(--line); border-radius: 4px; transition: background-color .18s ease, color .18s ease, border-color .18s ease, transform .18s ease, box-shadow .18s ease; } +.steps li:hover, .steps li:focus-visible { background: var(--surface); transform: translateY(-2px); box-shadow: 4px 4px 0 var(--line); } .step-number { display: grid; place-items: center; flex-shrink: 0; width: 34px; height: 34px; border: 1px solid var(--line); color: var(--primary); background: var(--surface); box-shadow: 3px 3px 0 var(--line); font: 12px/1 ui-monospace, "Cascadia Mono", monospace; font-variant-numeric: tabular-nums; } .steps h3 { margin-bottom: 10px; font-size: 17px; } .steps p { color: var(--muted); font-size: 14px; line-height: 1.8; } .product-preview { width: min(1080px, 100%); margin: 42px auto 0; scroll-margin-top: 96px; } -.screenshot { min-width: 0; padding: 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); box-shadow: 0 5px 0 color-mix(in srgb, var(--line) 65%, transparent), var(--shadow); } +.screenshot { min-width: 0; padding: 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); box-shadow: 0 5px 0 color-mix(in srgb, var(--line) 65%, transparent), var(--shadow); transition: background-color .18s ease, border-color .18s ease, transform .18s ease, box-shadow .18s ease; } +.screenshot:hover { transform: translateY(-2px); box-shadow: 0 7px 0 color-mix(in srgb, var(--line) 65%, transparent), var(--shadow); } +.screenshot:hover .expand-hint, .screenshot-button:focus-visible .expand-hint { background: var(--primary); color: var(--on-button); border-color: var(--primary); } .screenshot-button { position: relative; display: block; width: 100%; padding: 0; border: 0; background: none; cursor: zoom-in; } .screenshot-button > img { width: 100%; border-radius: 3px; } .app-capture-light { display: block; } @@ -184,6 +190,12 @@ figcaption { display: flex; align-items: flex-start; justify-content: space-betw .adv-grid .adv-index { margin-bottom: 12px; color: var(--secondary); font: 11px/1.5 ui-monospace, "Cascadia Mono", monospace; letter-spacing: .1em; } .adv-grid h3 { margin-bottom: 10px; } .adv-grid p { color: var(--muted); font-size: 14px; } +.release-strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; margin-top: 42px; } +.release-strip article { min-width: 0; padding: 22px 18px 20px; background: var(--surface); border: 1px solid var(--line); border-radius: 3px 12px 3px 3px; box-shadow: 4px 4px 0 color-mix(in srgb, var(--line) 82%, transparent); transition: background-color .18s ease, border-color .18s ease, transform .18s ease, box-shadow .18s ease; } +.release-strip article:hover { transform: translateY(-2px); box-shadow: 6px 6px 0 color-mix(in srgb, var(--line) 82%, transparent); } +.release-strip .adv-index { margin-bottom: 12px; color: var(--secondary); font: 11px/1.5 ui-monospace, "Cascadia Mono", monospace; letter-spacing: .1em; } +.release-strip h3 { margin-bottom: 8px; font-size: 16px; } +.release-strip p { color: var(--muted); font-size: 13px; line-height: 1.7; } .download { padding-top: clamp(86px, 9vw, 126px); } .download-folder { position: relative; margin-top: 20px; padding: clamp(28px, 4vw, 52px); border: 1px solid var(--line); border-radius: 0 12px 12px 12px; background: var(--surface); box-shadow: 0 6px 0 color-mix(in srgb, var(--line) 65%, transparent), var(--shadow); } @@ -244,7 +256,8 @@ dialog[open] { animation: surface-enter .18s ease-out; } .sidebar .brand { margin-block: 20px 28px; } .sidebar nav { display: grid; gap: 6px; } .sidebar nav a { display: flex; justify-content: space-between; align-items: center; min-height: 48px; padding: 10px 12px; border-radius: 3px; } -.sidebar nav a:hover { background: var(--soft); } +.sidebar nav a:hover, .sidebar nav a[aria-current="location"] { background: var(--soft); } +.sidebar nav a[aria-current="location"] { box-shadow: inset 3px 0 var(--primary); } .sidebar nav span { color: var(--muted); } .sidebar-inner > p { margin-top: auto; padding-top: 32px; color: var(--muted); font-size: 12px; } .screenshot-modal { width: min(1420px, calc(100vw - 48px)); max-width: none; max-height: calc(100dvh - 40px); border-radius: 6px; overflow: auto; box-shadow: var(--shadow); } @@ -257,6 +270,7 @@ dialog[open] { animation: surface-enter .18s ease-out; } .brand { font-size: 18px; gap: 8px; } .hero-grid { gap: 16px; } .adv-grid { column-gap: 36px; } + .release-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 920px) { .desktop-nav, .github-link { display: none; } @@ -315,6 +329,7 @@ dialog[open] { animation: surface-enter .18s ease-out; } .compatibility p { width: 100%; text-align: center; } .compatibility ul { gap: 8px 20px; font-size: 12px; } .adv-grid { grid-template-columns: 1fr; gap: 0; margin-top: 30px; } + .release-strip { grid-template-columns: 1fr; margin-top: 30px; } .adv-grid article:nth-last-child(2) { border-bottom: 0; } .collection-tags { justify-content: flex-start; gap: 10px; padding-inline: 0; } .collection-tags li { padding-inline: 11px; font-size: 11px; } diff --git a/website/components/GlassControls.tsx b/website/components/GlassControls.tsx index 0b1718f..6039634 100644 --- a/website/components/GlassControls.tsx +++ b/website/components/GlassControls.tsx @@ -5,6 +5,54 @@ import { glass } from "../lib/glass"; import { asset, home, languages, languageNames, repository, translate, type Language } from "../lib/site"; import { applyTheme, themeKey, type Theme } from "../lib/theme"; +const navTargets = ["experience", "app-preview", "updates", "download", "questions"]; + +function useActiveSection() { + const [active, setActive] = useState(""); + useEffect(() => { + const update = () => { + const line = 110; + let next = ""; + for (const id of navTargets) { + const section = document.getElementById(id); + if (section && section.getBoundingClientRect().top <= line) next = id; + } + const atBottom = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 4; + if (atBottom) { + for (const id of [...navTargets].reverse()) { + const section = document.getElementById(id); + if (!section) continue; + const rect = section.getBoundingClientRect(); + if (rect.top < window.innerHeight && rect.bottom > line) { + next = id; + break; + } + } + } + setActive(current => current === next ? current : next); + }; + let frame = 0; + const onScroll = () => { + if (frame) return; + frame = requestAnimationFrame(() => { + frame = 0; + update(); + }); + }; + update(); + window.addEventListener("scroll", onScroll, { passive: true }); + window.addEventListener("resize", onScroll); + window.addEventListener("hashchange", onScroll); + return () => { + window.removeEventListener("scroll", onScroll); + window.removeEventListener("resize", onScroll); + window.removeEventListener("hashchange", onScroll); + if (frame) cancelAnimationFrame(frame); + }; + }, []); + return active; +} + function Icon({ kind }: { kind: "menu" | "close" | "down" | "expand" | "info" | "appearance" }) { const paths = { menu: "M4 6h16M4 12h16M4 18h16", @@ -19,6 +67,7 @@ function Icon({ kind }: { kind: "menu" | "close" | "down" | "expand" | "info" | export function Header({ language }: { language: Language }) { const t = translate(language); + const active = useActiveSection(); const sidebar = useRef(null); const menu = useRef(null); const themeMenu = useRef(null); @@ -29,6 +78,7 @@ export function Header({ language }: { language: Language }) { const sections = [ ["updates", t("v1.8.0 更新", "v1.8.0 更新", "What's new", "v1.8.0 の更新")], ["experience", t("如何使用", "如何使用", "How it works", "使い方")], + ["app-preview", t("界面", "介面", "App", "画面")], ["download", t("下载", "下載", "Download", "ダウンロード")], ["questions", t("常见问题", "常見問題", "FAQ", "よくある質問")], ]; @@ -69,7 +119,7 @@ export function Header({ language }: { language: Language }) { return <>
GameLibrary - +
{ setExpanded(event.currentTarget.open); if (event.currentTarget.open) themeMenu.current?.removeAttribute("open"); }}> {t("简中", "繁中", "EN", "日本語")} @@ -98,7 +148,7 @@ export function Header({ language }: { language: Language }) {
{t("页面导航", "頁面導覽", "On this page", "ページ内ナビゲーション")}
GameLibrary - +

Windows 10 / 11 · x64

GitHub ↗
diff --git a/website/components/MarketingPage.tsx b/website/components/MarketingPage.tsx index 5fb4f6f..02c4481 100644 --- a/website/components/MarketingPage.tsx +++ b/website/components/MarketingPage.tsx @@ -55,7 +55,7 @@ export default function MarketingPage({ language }: { language: Language }) { `${asset(`pixel-journey-${size}.webp`)} ${size}w`).join(", ")} sizes="(max-width: 640px) calc(100vw - 44px), (max-width: 1280px) calc(100vw - 96px), 1184px" width="1774" height="887" loading="lazy" decoding="async" alt={t("像素文件夹、收藏书架与手柄入口,由一条路径连接,表示扫描、确认和启动。", "像素資料夾、收藏書架與手把入口,由一條路徑連接,表示掃描、確認和啟動。", "A pixel folder, library shelf and gamepad portal connected by a path, illustrating scan, review and launch.", "ピクセルのフォルダー、本棚、ゲームパッドの入口を道でつないだ、スキャン・確認・起動のイラスト。")} />
{t("流程概念插画", "流程概念插畫", "An illustrated workflow", "操作のイメージイラスト")}
-
    {steps.map(([title, text], index) =>
  1. {title}

    {text}

  2. )}
+
    {steps.map(([title, text], index) =>
  1. {title}

    {text}

  2. )}
@@ -70,9 +70,9 @@ export default function MarketingPage({ language }: { language: Language }) { -
+

v1.8.0

{t("游戏多了,", "遊戲多了,", "A growing library.", "ゲームが増えても、")}{t("整理不用一项项来。", "整理不用逐項來。", "Fewer repetitive steps.", "まとめて整理できます。")}

{t("批量收藏、添加标签和移出游戏库,逐项返回结果。这一版也改进了审核、连接恢复和键盘操作。", "批次收藏、新增標籤和移出遊戲庫,逐項回傳結果。這一版也改進了審核、連線恢復和鍵盤操作。", "Favorite, tag and remove games in bulk, with a result for each item. This release also improves review, connection recovery and keyboard controls.", "お気に入り、タグ付け、登録の削除をまとめて行い、各項目の結果を確認できます。確認作業、接続の復旧、キーボード操作も改善しました。")}

-
+

01

{t("选好一批,一起整理", "選好一批,一起整理", "Organize a whole selection", "選んだゲームを一括整理")}

{t("勾选多个游戏后,一次收藏、加标签或移出库。移出记录保留原始游戏文件。", "勾選多個遊戲後,一次收藏、加標籤或移出庫。移出記錄保留原始遊戲檔案。", "Select multiple games to favorite, tag or remove them from the library together. Removing records leaves the original game files in place.", "複数のゲームを選び、お気に入り、タグ、登録の削除を一括で操作。登録を削除しても元のゲームファイルは残ります。")}

02

{t("审核失败,不用重新挑", "審核失敗,不用重新選", "Keep your selection after a failed review", "確認に失敗した項目は選択を保持")}

{t("审核时没处理成功的项目继续保持勾选。扫描或名称翻译遇到任务丢失、连续连接失败时,会停止等待并给出提示。", "審核時未處理成功的項目繼續保持勾選。掃描或名稱翻譯遇到工作遺失、連續連線失敗時,會停止等待並顯示提示。", "Items that fail during review stay selected. Scans and title translation stop waiting and show a message if a job disappears or the connection keeps failing.", "確認に失敗した項目は選択したまま残ります。スキャンやタイトル翻訳の処理が見つからない場合や接続が繰り返し失敗した場合は、待機を終了して知らせます。")}

03

{t("后台重启后,重新连接", "背景服務重啟後,重新連線", "Reconnect after a service restart", "バックグラウンドサービスの再起動後に再接続")}

{t("后台服务重启导致连接失效时,应用会重建连接并重试当前请求一次,减少手动重启程序的麻烦。", "背景服務重啟導致連線失效時,應用程式會重建連線並重試目前請求一次,減少手動重啟程式的麻煩。", "If a service restart breaks the connection, the app reconnects and retries the current request once, reducing the need to restart the app yourself.", "サービスの再起動で接続が切れた場合は再接続し、現在のリクエストを一度だけ再試行。アプリを手動で再起動する手間を減らします。")}

@@ -105,7 +105,7 @@ export default function MarketingPage({ language }: { language: Language }) {
-
+

{t("中文翻译", "中文翻譯", "Chinese translation", "中国語翻訳")}

{t("Unity 游戏,", "Unity 遊戲,", "Unity games. ", "Unity ゲームにも")}{t("也能配好中文。", "也能配好中文。", "Set up Chinese translation.", "中国語を。")}

{t("给库里的 Unity 游戏配置中文翻译:安装固定版本的加载器与翻译插件,安装前先备份,随时可以恢复。", "為庫裡的 Unity 遊戲設定中文翻譯:安裝固定版本的載入器與翻譯外掛,安裝前先備份,隨時可以還原。", "Configure Chinese translation for Unity games in your library: install fixed-version loaders and a translation plugin, with a backup first and restore anytime.", "ライブラリの Unity ゲームに中国語翻訳を設定できます。固定バージョンのローダーと翻訳プラグインを導入し、事前にバックアップして、いつでも復元できます。")}

{t("需自行配置翻译服务,缺少安装组件时需要联网获取。支持情况和实际效果因游戏而异。", "需自行設定翻譯服務,缺少安裝元件時需要連線取得。支援情況和實際效果因遊戲而異。", "Bring your own translation service. Missing installation components require a download. Support and results vary by game.", "翻訳サービスの設定が必要です。導入用のコンポーネントが不足している場合はダウンロードします。対応状況や効果はゲームによって異なります。")}

{t("Mono 与 IL2CPP 分别处理", "Mono 與 IL2CPP 分別處理", "Mono and IL2CPP handled separately", "Mono と IL2CPP を個別に対応")}

{t("按主程序的真实位数匹配对应的加载器与插件;识别不了或相互冲突的安装保持原样,不强行覆盖。", "依主程式的真實位元數匹配對應的載入器與外掛;辨識不了或相互衝突的安裝保持原樣,不強行覆蓋。", "The loader and plugin match the real bitness of the executable. Unknown or conflicting installs are left as they are, never overwritten by force.", "本体実行ファイルの実ビット数に合わせてローダーとプラグインを選びます。判別できない・競合する構成は元のままにし、強制的に上書きしません。")}