Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/components/Bi.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
// A piece of text in the page's language. On a page that picks its language in
// the browser (the 404 page: GitHub Pages serves one for every language), both
// are rendered and the page's script shows the visitor's.
import { getLang } from "~/i18n";

interface Props {
en: string;
zh: string;
/** Render both languages for the page's script to choose from */
both?: boolean;
}

const { en, zh, both = false } = Astro.props;
const lang = getLang(Astro);
---

{both ? (
<Fragment><span data-i18n="en">{en}</span><span data-i18n="zh-CN" hidden>{zh}</span></Fragment>
) : lang === "zh-CN" ? zh : en}
53 changes: 40 additions & 13 deletions src/components/SearchModal.astro
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
---
import { getLang } from "~/i18n";
import Bi from "./Bi.astro";

interface Props {
/** Render the text in both languages for the page's script to choose from (the 404 page; see Bi.astro) */
bilingual?: boolean;
}

const { bilingual = false } = Astro.props;
const lang = getLang(Astro);
const placeholder = lang === "zh-CN" ? "搜索文档…" : "Search docs…";
const buttonLabel = lang === "zh-CN" ? "搜索" : "Search";
const emptyHint = lang === "zh-CN" ? "开始输入以搜索文档。" : "Start typing to search the docs.";
const noResults = lang === "zh-CN" ? "未找到结果" : "No results";
// Used when the page is a product's docs page (DocsLayout sets data-docs-product).
const scopedPlaceholder = lang === "zh-CN" ? "搜索 {product} 文档…" : "Search {product} docs…";
const scopeLabel = lang === "zh-CN" ? "范围" : "Scope";
const scopeAll = lang === "zh-CN" ? "全部文档" : "All docs";
const scopeProduct = lang === "zh-CN" ? "{product} 文档" : "{product} docs";
const text = {
placeholder: { en: "Search docs…", zh: "搜索文档…" },
button: { en: "Search", zh: "搜索" },
emptyHint: { en: "Start typing to search the docs.", zh: "开始输入以搜索文档。" },
noResults: { en: "No results", zh: "未找到结果" },
// Used when the page is a product's docs page (DocsLayout sets data-docs-product).
scopedPlaceholder: { en: "Search {product} docs…", zh: "搜索 {product} 文档…" },
scopeLabel: { en: "Scope", zh: "范围" },
scopeAll: { en: "All docs", zh: "全部文档" },
scopeProduct: { en: "{product} docs", zh: "{product} 文档" },
};
const pick = (k: keyof typeof text) => (lang === "zh-CN" ? text[k].zh : text[k].en);
const placeholder = pick("placeholder");
const buttonLabel = pick("button");
const emptyHint = pick("emptyHint");
const noResults = pick("noResults");
const scopedPlaceholder = pick("scopedPlaceholder");
const scopeProduct = pick("scopeProduct");
/** On a bilingual page, attributes the page's script swaps in for Chinese visitors */
const zhAttrs = (attrs: Record<string, string>) => (bilingual ? JSON.stringify(attrs) : undefined);
---

<!--
Expand All @@ -27,13 +45,14 @@ const scopeProduct = lang === "zh-CN" ? "{product} 文档" : "{product} docs";
type="button"
data-search-trigger
aria-label={buttonLabel}
data-zh-attrs={zhAttrs({ "aria-label": text.button.zh })}
class="flex items-center justify-center gap-2 text-xs text-[var(--color-muted)] hover:text-white border border-white/10 hover:border-white/25 rounded-md w-9 h-9 md:w-auto md:h-9 md:px-2.5 transition-colors"
>
<svg class="w-4 h-4 md:w-3.5 md:h-3.5" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<circle cx="7" cy="7" r="5" stroke="currentColor" stroke-width="1.4"/>
<line x1="11" y1="11" x2="14" y2="14" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
</svg>
<span class="hidden md:inline">{buttonLabel}</span>
<span class="hidden md:inline"><Bi {...text.button} both={bilingual} /></span>
<kbd class="hidden lg:inline ml-1 px-1.5 py-0.5 text-[9px] font-mono rounded border border-white/10 bg-white/[0.04] text-[var(--color-dim)]">⌘K</kbd>
</button>

Expand All @@ -44,6 +63,13 @@ const scopeProduct = lang === "zh-CN" ? "{product} 文档" : "{product} docs";
data-placeholder={placeholder}
data-scoped-placeholder={scopedPlaceholder}
data-scope-product={scopeProduct}
data-zh-attrs={zhAttrs({
"data-empty-hint": text.emptyHint.zh,
"data-no-results": text.noResults.zh,
"data-placeholder": text.placeholder.zh,
"data-scoped-placeholder": text.scopedPlaceholder.zh,
"data-scope-product": text.scopeProduct.zh,
})}
class="search-modal fixed top-4 sm:top-[15vh] left-1/2 -translate-x-1/2 m-0 w-[calc(100vw-1rem)] sm:w-full max-w-2xl rounded-2xl border border-white/10 bg-[var(--color-surface)]/95 backdrop-blur-xl shadow-2xl shadow-black/60 text-[var(--color-text)] p-0 outline-none"
>
<form method="dialog">
Expand All @@ -56,6 +82,7 @@ const scopeProduct = lang === "zh-CN" ? "{product} 文档" : "{product} docs";
id="search-input"
type="search"
placeholder={placeholder}
data-zh-attrs={zhAttrs({ placeholder: text.placeholder.zh })}
autocomplete="off"
spellcheck="false"
class="flex-1 bg-transparent border-0 outline-none text-base text-white placeholder:text-[var(--color-dim)]"
Expand All @@ -66,13 +93,13 @@ const scopeProduct = lang === "zh-CN" ? "{product} 文档" : "{product} docs";
</div>

<div id="search-scope" hidden class="flex items-center gap-2 overflow-x-auto px-5 py-2.5 border-b border-white/10 text-xs">
<span class="text-[var(--color-dim)] mr-1 shrink-0">{scopeLabel}</span>
<span class="text-[var(--color-dim)] mr-1 shrink-0"><Bi {...text.scopeLabel} both={bilingual} /></span>
<button type="button" data-scope="product" aria-pressed="true" class="search-scope-btn"></button>
<button type="button" data-scope="all" aria-pressed="false" class="search-scope-btn">{scopeAll}</button>
<button type="button" data-scope="all" aria-pressed="false" class="search-scope-btn"><Bi {...text.scopeAll} both={bilingual} /></button>
</div>

<div id="search-results" class="max-h-[70vh] sm:max-h-[50vh] overflow-y-auto px-2 py-2 text-sm">
<div class="px-3 py-6 text-center text-[var(--color-dim)]">{emptyHint}</div>
<div class="px-3 py-6 text-center text-[var(--color-dim)]"><Bi {...text.emptyHint} both={bilingual} /></div>
</div>
</form>
</dialog>
Expand Down
35 changes: 22 additions & 13 deletions src/components/SiteFooter.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@
import { getLang, t, localePath } from "~/i18n";

import TwMark from "./TwMark.astro";
import Bi from "./Bi.astro";

interface Props {
/** Render the text in both languages, with links the page's script localizes (the 404 page; see Bi.astro) */
bilingual?: boolean;
}

const { bilingual = false } = Astro.props;
const lang = getLang(Astro);
const s = t(lang);
const en = t("en");
const zh = t("zh-CN");
const year = new Date().getFullYear();

/** A link to a page of the site, and on a bilingual page the path its script localizes */
const link = (path: string) => ({ href: localePath(lang, path), "data-localize": bilingual ? path : undefined });
---

<footer class="border-t border-white/5 mt-20 sm:mt-32">
Expand All @@ -17,33 +26,33 @@ const year = new Date().getFullYear();
<span class="text-white"><span class="font-medium">Think</span><span class="font-bold">Watch</span></span>
</div>
<p class="text-[var(--color-muted)] max-w-sm">
{s.footer.tagline}
<Bi en={en.footer.tagline} zh={zh.footer.tagline} both={bilingual} />
</p>
</div>

<div>
<div class="text-white/80 font-medium mb-3">{s.footer.product}</div>
<div class="text-white/80 font-medium mb-3"><Bi en={en.footer.product} zh={zh.footer.product} both={bilingual} /></div>
<ul class="space-y-1 sm:space-y-2 text-[var(--color-muted)]">
<li><a href={localePath(lang, "/thinkwatch")} class="inline-block py-1.5 hover:text-white">{lang === "zh-CN" ? "ThinkWatch 企业版" : "ThinkWatch Enterprise"}</a></li>
<li><a href={localePath(lang, "/lite")} class="inline-block py-1.5 hover:text-white">ThinkWatch Lite</a></li>
<li><a href={localePath(lang, "/core")} class="inline-block py-1.5 hover:text-white">ThinkWatch Core</a></li>
<li><a {...link("/thinkwatch")} class="inline-block py-1.5 hover:text-white"><Bi en="ThinkWatch Enterprise" zh="ThinkWatch 企业版" both={bilingual} /></a></li>
<li><a {...link("/lite")} class="inline-block py-1.5 hover:text-white">ThinkWatch Lite</a></li>
<li><a {...link("/core")} class="inline-block py-1.5 hover:text-white">ThinkWatch Core</a></li>
</ul>
</div>

<div>
<div class="text-white/80 font-medium mb-3">{s.footer.resources}</div>
<div class="text-white/80 font-medium mb-3"><Bi en={en.footer.resources} zh={zh.footer.resources} both={bilingual} /></div>
<ul class="space-y-1 sm:space-y-2 text-[var(--color-muted)]">
<li><a href={localePath(lang, "/docs")} class="inline-block py-1.5 hover:text-white">{s.nav.docs}</a></li>
<li><a href={localePath(lang, "/changelog")} class="inline-block py-1.5 hover:text-white">{s.nav.changelog}</a></li>
<li><a href={localePath(lang, "/pricing")} class="inline-block py-1.5 hover:text-white">{s.nav.pricing}</a></li>
<li><a {...link("/docs")} class="inline-block py-1.5 hover:text-white"><Bi en={en.nav.docs} zh={zh.nav.docs} both={bilingual} /></a></li>
<li><a {...link("/changelog")} class="inline-block py-1.5 hover:text-white"><Bi en={en.nav.changelog} zh={zh.nav.changelog} both={bilingual} /></a></li>
<li><a {...link("/pricing")} class="inline-block py-1.5 hover:text-white"><Bi en={en.nav.pricing} zh={zh.nav.pricing} both={bilingual} /></a></li>
<li><a href="https://github.com/ThinkWatchProject" class="inline-block py-1.5 hover:text-white">GitHub</a></li>
<li><a href="/changelog.xml" class="inline-block py-1.5 hover:text-white">RSS</a></li>
</ul>
</div>
</div>

<div class="container-page py-6 border-t border-white/5 flex flex-col sm:flex-row items-center justify-between gap-2 text-xs text-[var(--color-dim)]">
<span>© {year} ThinkWatch. {s.footer.licensing}</span>
<span>{s.footer.builtWith}</span>
<span>© {year} ThinkWatch. <Bi en={en.footer.licensing} zh={zh.footer.licensing} both={bilingual} /></span>
<span><Bi en={en.footer.builtWith} zh={zh.footer.builtWith} both={bilingual} /></span>
</div>
</footer>
56 changes: 36 additions & 20 deletions src/components/SiteHeader.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,22 @@ import { getStarCount, formatStars } from "~/lib/github";
import { findDoc, getProduct } from "~/content/docs/_meta";
import SearchModal from "./SearchModal.astro";
import TwMark from "./TwMark.astro";
import Bi from "./Bi.astro";

interface Props {
/**
* Render the text in both languages, with links that the page's script
* points at the visitor's (the 404 page; see Bi.astro).
*/
bilingual?: boolean;
}

const { bilingual = false } = Astro.props;
const lang = getLang(Astro);
const s = t(lang);
const sEn = t("en");
const sZh = t("zh-CN");
/** data-localize for a link on a bilingual page: the 404 page's script prefixes /zh-CN for Chinese visitors */
const localize = (path: string) => (bilingual ? path : undefined);
const stars = await getStarCount();
const starLabel = formatStars(stars);

Expand All @@ -20,14 +33,9 @@ const isHome =
const homeAnchor = (hash: string) => (isHome ? hash : `${homePath}${hash}`);

const currentPath = Astro.url.pathname.replace(/^\/zh-CN/, "").replace(/\/$/, "") || "/";
const links = [
{ href: localePath(lang, "/lite"), label: s.nav.lite, match: "/lite" },
{ href: localePath(lang, "/thinkwatch"), label: s.nav.thinkwatch, match: "/thinkwatch" },
{ href: localePath(lang, "/core"), label: s.nav.core, match: "/core" },
{ href: localePath(lang, "/docs"), label: s.nav.docs, match: "/docs" },
{ href: localePath(lang, "/changelog"), label: s.nav.changelog, match: "/changelog" },
{ href: localePath(lang, "/pricing"), label: s.nav.pricing, match: "/pricing" },
].map((l) => ({ ...l, active: currentPath === l.match || currentPath.startsWith(l.match + "/") }));
const links = (["lite", "thinkwatch", "core", "docs", "changelog", "pricing"] as const)
.map((key) => ({ path: `/${key}`, href: localePath(lang, `/${key}`), en: sEn.nav[key], zh: sZh.nav[key] }))
.map((l) => ({ ...l, active: currentPath === l.path || currentPath.startsWith(l.path + "/") }));

const cleanPath = Astro.url.pathname.replace(/^\/zh-CN/, "") || "/";

Expand All @@ -54,15 +62,15 @@ const langOptions: { code: Lang; label: string; native: string }[] = [
{ code: "en", label: "English", native: "EN" },
{ code: "zh-CN", label: "简体中文", native: "中文" },
];
const currentLangOption = langOptions.find((l) => l.code === lang)!;

const menuLabel = lang === "zh-CN" ? "菜单" : "Menu";
const closeLabel = lang === "zh-CN" ? "关闭" : "Close";
/** On a bilingual page, attributes the page's script swaps in for Chinese visitors */
const zhAttrs = (attrs: Record<string, string>) => (bilingual ? JSON.stringify(attrs) : undefined);
---

<header class="sticky top-0 z-50 backdrop-blur-md backdrop-saturate-150 bg-[var(--color-bg)]/70 border-b border-white/[0.07]">
<div class="container-page flex items-center justify-between h-16 gap-3">
<a href={localePath(lang, "/")} class="flex items-center gap-2 sm:gap-2.5 group shrink-0">
<a href={localePath(lang, "/")} data-localize={localize("/")} class="flex items-center gap-2 sm:gap-2.5 group shrink-0">
<TwMark id="m-header" size={26} />
<span class="text-base sm:text-lg tracking-tight text-white">
<span class="font-medium">Think</span><span class="font-bold">Watch</span>
Expand All @@ -74,17 +82,18 @@ const closeLabel = lang === "zh-CN" ? "关闭" : "Close";
{links.map((l) => (
<a
href={l.href}
data-localize={localize(l.path)}
aria-current={l.active ? "page" : undefined}
class:list={["transition-colors", l.active ? "text-white" : "hover:text-white"]}
>
{l.label}
<Bi en={l.en} zh={l.zh} both={bilingual} />
</a>
))}
</nav>

<!-- Right cluster: search + lang + GitHub (mobile compact) + CTA + burger -->
<div class="flex items-center gap-1.5 sm:gap-2 shrink-0">
<SearchModal />
<SearchModal bilingual={bilingual} />

<!-- Language dropdown — pure HTML <details>, no JS -->
<details class="lang-menu relative">
Expand All @@ -96,15 +105,16 @@ const closeLabel = lang === "zh-CN" ? "关闭" : "Close";
<circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.2"/>
<path d="M1.5 8h13M8 1.5c2 2 2 11 0 13M8 1.5c-2 2-2 11 0 13" stroke="currentColor" stroke-width="1.2"/>
</svg>
<span class="hidden xs:inline">{currentLangOption.native}</span>
<span class="hidden xs:inline"><Bi en="EN" zh="中文" both={bilingual} /></span>
<svg class="w-3 h-3 transition-transform hidden sm:block" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</summary>
<div class="absolute right-0 mt-2 w-44 rounded-lg border border-white/10 bg-[var(--color-surface)] shadow-2xl shadow-black/40 backdrop-blur-lg overflow-hidden z-50">
{langOptions.map((opt) => {
const href = localePath(opt.code, targetPath(opt.code));
const isActive = opt.code === lang;
// On a bilingual page the script marks the visitor's language instead
const isActive = !bilingual && opt.code === lang;
return (
<a
href={href}
Expand Down Expand Up @@ -145,16 +155,18 @@ const closeLabel = lang === "zh-CN" ? "关闭" : "Close";
<!-- Desktop CTA -->
<a
href={localePath(lang, "/thinkwatch#quickstart")}
data-localize={localize("/thinkwatch#quickstart")}
class="hidden sm:inline-flex items-center text-sm font-medium px-4 h-9 rounded-full bg-[#f7f8f8] text-[#08090a] hover:bg-white hover:shadow-[0_8px_30px_-8px_rgba(34,229,242,0.5)] transition"
>
{s.common.getStarted}
<Bi en={sEn.common.getStarted} zh={sZh.common.getStarted} both={bilingual} />
</a>

<!-- Mobile burger -->
<button
type="button"
data-mobile-nav-toggle
aria-label={menuLabel}
data-zh-attrs={zhAttrs({ "aria-label": "菜单" })}
aria-expanded="false"
aria-controls="mobile-nav-panel"
class="md:hidden flex items-center justify-center w-9 h-9 rounded-md border border-white/10 text-[var(--color-muted)] hover:text-white hover:border-white/25 transition-colors"
Expand All @@ -178,28 +190,30 @@ const closeLabel = lang === "zh-CN" ? "关闭" : "Close";
hidden
class="md:hidden border-t border-white/10 bg-[var(--color-bg)]/95 backdrop-blur-md"
>
<nav aria-label={menuLabel}>
<nav aria-label={menuLabel} data-zh-attrs={zhAttrs({ "aria-label": "菜单" })}>
<ul class="container-page py-3 flex flex-col">
{links.map((l) => (
<li>
<a
href={l.href}
data-localize={localize(l.path)}
aria-current={l.active ? "page" : undefined}
class:list={[
"block py-3 text-base transition-colors border-b border-white/5",
l.active ? "text-white" : "text-[var(--color-muted)] hover:text-white",
]}
>
{l.label}
<Bi en={l.en} zh={l.zh} both={bilingual} />
</a>
</li>
))}
<li>
<a
href={localePath(lang, "/thinkwatch#quickstart")}
data-localize={localize("/thinkwatch#quickstart")}
class="mt-4 inline-flex items-center justify-center w-full px-4 py-3 rounded-full bg-[#f7f8f8] text-[#08090a] font-medium hover:bg-white transition"
>
{s.common.getStarted}
<Bi en={sEn.common.getStarted} zh={sZh.common.getStarted} both={bilingual} />
</a>
</li>
</ul>
Expand All @@ -211,6 +225,8 @@ const closeLabel = lang === "zh-CN" ? "关闭" : "Close";
.lang-menu summary::-webkit-details-marker { display: none; }
.lang-menu[open] summary svg:last-child { transform: rotate(180deg); }
.lang-menu[open] summary { color: white; border-color: rgba(255, 255, 255, 0.25); }
/* The visitor's language, marked by the bilingual page's script */
.lang-menu [data-lang-choice][aria-current] { color: white; background: rgba(255, 255, 255, 0.06); }

/* Extra-small breakpoint: 400px — hide the lang code text on very narrow phones */
@media (min-width: 400px) {
Expand Down
Loading
Loading