chore(docs): remove redundant workspace files
This commit is contained in:
@@ -1,264 +0,0 @@
|
||||
import { Icon } from "@icones/react"
|
||||
import { usePreference } from "@workspace/preferences"
|
||||
import { useEffect, useId, useRef, useState } from "react"
|
||||
import { createPortal } from "react-dom"
|
||||
|
||||
import { LocaleFlag } from "./locale-flag"
|
||||
import { getLocalizedHref } from "../content/registry"
|
||||
import type { DocLocale } from "../content/types"
|
||||
import { useLocale, useTranslate } from "../lib/locale"
|
||||
import { useIsMobile } from "../lib/use-mobile"
|
||||
|
||||
type LocaleOption = {
|
||||
label: string
|
||||
locale: DocLocale
|
||||
}
|
||||
|
||||
const localeOptions: readonly LocaleOption[] = [
|
||||
{ label: "简体中文", locale: "zh-Hans" },
|
||||
{ label: "English", locale: "en-US" },
|
||||
]
|
||||
|
||||
export function LanguageSwitcher() {
|
||||
const locale = useLocale()
|
||||
const t = useTranslate()
|
||||
const isMobile = useIsMobile()
|
||||
const [, setLocaleMode] = usePreference("locale-mode")
|
||||
const [, setPreferredLocale] = usePreference("locale")
|
||||
const [open, setOpen] = useState(false)
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const triggerRef = useRef<HTMLButtonElement>(null)
|
||||
const menuId = useId()
|
||||
const selectedLocale = getLocaleOption(locale)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
|
||||
function isInsideSwitcher(target: EventTarget | null) {
|
||||
return (
|
||||
target instanceof Node &&
|
||||
(rootRef.current?.contains(target) || menuRef.current?.contains(target))
|
||||
)
|
||||
}
|
||||
|
||||
function closeAndRestoreFocus() {
|
||||
setOpen(false)
|
||||
window.requestAnimationFrame(() => triggerRef.current?.focus())
|
||||
}
|
||||
|
||||
function handlePointerDown(event: PointerEvent) {
|
||||
if (isInsideSwitcher(event.target)) return
|
||||
|
||||
if (isMobile) {
|
||||
closeAndRestoreFocus()
|
||||
} else {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocusIn(event: FocusEvent) {
|
||||
if (!isMobile && !isInsideSwitcher(event.target)) setOpen(false)
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
closeAndRestoreFocus()
|
||||
return
|
||||
}
|
||||
|
||||
if (!isMobile || event.key !== "Tab") return
|
||||
|
||||
const items = getMenuItems(menuRef.current)
|
||||
const firstItem = items[0]
|
||||
const lastItem = items.at(-1)
|
||||
if (!firstItem || !lastItem) return
|
||||
|
||||
if (event.shiftKey && document.activeElement === firstItem) {
|
||||
event.preventDefault()
|
||||
lastItem.focus()
|
||||
} else if (!event.shiftKey && document.activeElement === lastItem) {
|
||||
event.preventDefault()
|
||||
firstItem.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const previousOverflow = document.body.style.overflow
|
||||
if (isMobile) {
|
||||
document.body.style.overflow = "hidden"
|
||||
window.requestAnimationFrame(() => {
|
||||
menuRef.current
|
||||
?.querySelector<HTMLElement>('[aria-checked="true"]')
|
||||
?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
document.addEventListener("pointerdown", handlePointerDown)
|
||||
document.addEventListener("focusin", handleFocusIn)
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", handlePointerDown)
|
||||
document.removeEventListener("focusin", handleFocusIn)
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
if (isMobile) document.body.style.overflow = previousOverflow
|
||||
}
|
||||
}, [isMobile, open])
|
||||
|
||||
function focusMenuItem(position: "first" | "last") {
|
||||
setOpen(true)
|
||||
window.requestAnimationFrame(() => {
|
||||
const items = getMenuItems(menuRef.current)
|
||||
const item = position === "first" ? items[0] : items.at(-1)
|
||||
item?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
function selectLocale(nextLocale: DocLocale) {
|
||||
setPreferredLocale(nextLocale)
|
||||
setLocaleMode("manual")
|
||||
setOpen(false)
|
||||
window.requestAnimationFrame(() => triggerRef.current?.focus())
|
||||
}
|
||||
|
||||
const menu = (
|
||||
<div
|
||||
ref={menuRef}
|
||||
id={menuId}
|
||||
role={isMobile ? "dialog" : undefined}
|
||||
aria-label={t({ zh: "语言", us: "Language" })}
|
||||
aria-modal={isMobile || undefined}
|
||||
className="w-[min(80vw,20rem)] rounded-xl border border-slate-200 bg-white/95 p-4 shadow-2xl backdrop-blur-md md:w-auto md:min-w-40 md:p-1 dark:border-slate-800 dark:bg-slate-950/95 dark:shadow-black/60"
|
||||
>
|
||||
<p className="px-2 py-2 text-sm text-slate-500 md:text-xs dark:text-slate-400">
|
||||
{t({ zh: "语言", us: "Language" })}
|
||||
</p>
|
||||
<ul
|
||||
role="menu"
|
||||
aria-label={t({ zh: "语言", us: "Languages" })}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key !== "ArrowDown" &&
|
||||
event.key !== "ArrowUp" &&
|
||||
event.key !== "Home" &&
|
||||
event.key !== "End"
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
const items = getMenuItems(event.currentTarget)
|
||||
const currentIndex = items.indexOf(
|
||||
document.activeElement as HTMLButtonElement
|
||||
)
|
||||
let nextIndex = currentIndex
|
||||
|
||||
if (event.key === "Home") nextIndex = 0
|
||||
if (event.key === "End") nextIndex = items.length - 1
|
||||
if (event.key === "ArrowDown") {
|
||||
nextIndex = (currentIndex + 1) % items.length
|
||||
}
|
||||
if (event.key === "ArrowUp") {
|
||||
nextIndex = (currentIndex - 1 + items.length) % items.length
|
||||
}
|
||||
|
||||
items[nextIndex]?.focus()
|
||||
}}
|
||||
>
|
||||
{localeOptions.map((option) => {
|
||||
const selected = option.locale === locale
|
||||
|
||||
return (
|
||||
<li role="none" key={option.locale}>
|
||||
<a
|
||||
role="menuitemradio"
|
||||
aria-checked={selected}
|
||||
lang={option.locale}
|
||||
href={getLocalizedHref(window.location.pathname, option.locale)}
|
||||
className="flex h-11 w-full cursor-pointer items-center gap-2 rounded-lg px-2 text-left whitespace-nowrap transition-colors hover:bg-slate-100 focus:bg-slate-100 focus:outline-none aria-checked:text-indigo-600 dark:hover:bg-slate-900 dark:focus:bg-slate-900 dark:aria-checked:text-indigo-400"
|
||||
onClick={() => selectLocale(option.locale)}
|
||||
>
|
||||
<LocaleFlag
|
||||
locale={option.locale}
|
||||
className="size-5 shrink-0"
|
||||
/>
|
||||
<span>{option.label}</span>
|
||||
{selected && (
|
||||
<Icon
|
||||
name="tabler:check"
|
||||
className="ml-auto size-4 shrink-0"
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className="flex items-center md:relative">
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
aria-controls={open ? menuId : undefined}
|
||||
aria-expanded={open}
|
||||
aria-haspopup={isMobile ? "dialog" : "menu"}
|
||||
aria-label={t({ zh: "选择语言", us: "Choose language" })}
|
||||
title={t({ zh: "选择语言", us: "Choose language" })}
|
||||
className="inline-flex size-8 cursor-pointer items-center justify-center rounded-full transition-colors outline-none hover:bg-slate-100 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 dark:hover:bg-slate-800 dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-slate-950"
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault()
|
||||
focusMenuItem("first")
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault()
|
||||
focusMenuItem("last")
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LocaleFlag locale={selectedLocale.locale} className="size-7" />
|
||||
</button>
|
||||
|
||||
{open &&
|
||||
(isMobile ? (
|
||||
createPortal(
|
||||
<div className="fixed inset-0 z-100 flex items-center justify-center p-4">
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
aria-label={t({
|
||||
zh: "关闭语言菜单",
|
||||
us: "Close language menu",
|
||||
})}
|
||||
className="absolute inset-0 cursor-default bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
window.requestAnimationFrame(() =>
|
||||
triggerRef.current?.focus()
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div className="relative">{menu}</div>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
) : (
|
||||
<div className="absolute top-full right-0 z-100 pt-2">{menu}</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getMenuItems(root: ParentNode | null) {
|
||||
return Array.from(
|
||||
root?.querySelectorAll<HTMLElement>('[role="menuitemradio"]') ?? []
|
||||
)
|
||||
}
|
||||
|
||||
function getLocaleOption(locale: DocLocale) {
|
||||
return localeOptions.find((option) => option.locale === locale)!
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import React from "react"
|
||||
import { SearchTrigger } from "./search-trigger"
|
||||
import { SettingsLink } from "./settings-link"
|
||||
|
||||
// oxlint-disable-next-line react/only-export-components -- Shared layout tokens are consumed by App.
|
||||
export const rootCSSVariables = {
|
||||
"--cw": "60rem",
|
||||
"--max-lw": "18rem",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { MdxComponents } from "../content/types"
|
||||
|
||||
/* oxlint-disable react/only-export-components -- This module exports an MDX component registry. */
|
||||
export const mdxComponents: MdxComponents = {
|
||||
a: MdxAnchor,
|
||||
blockquote: MdxBlockquote,
|
||||
@@ -90,4 +89,3 @@ function MdxPre({ className, ...props }: React.ComponentProps<"pre">) {
|
||||
/>
|
||||
)
|
||||
}
|
||||
/* oxlint-enable react/only-export-components */
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Icon } from "@icones/react"
|
||||
import { usePreference } from "@workspace/preferences"
|
||||
|
||||
import { useResolvedTheme } from "../lib/appearance"
|
||||
import { useTranslate } from "../lib/locale"
|
||||
|
||||
export function ThemeSwitch({ className }: { className?: string }) {
|
||||
const t = useTranslate()
|
||||
const [, setThemeMode] = usePreference("theme-mode")
|
||||
const resolvedTheme = useResolvedTheme()
|
||||
const dark = resolvedTheme === "dark"
|
||||
const label = dark
|
||||
? t({ zh: "切换到浅色主题", us: "Switch to light theme" })
|
||||
: t({ zh: "切换到深色主题", us: "Switch to dark theme" })
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={dark}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
className={[
|
||||
"relative inline-flex h-7 w-13 shrink-0 cursor-pointer items-center rounded-full border transition-colors duration-200",
|
||||
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 motion-reduce:transition-none",
|
||||
dark
|
||||
? "border-zinc-700 bg-zinc-900 shadow-inner hover:border-zinc-600"
|
||||
: "border-slate-300 bg-slate-100 shadow-inner hover:border-slate-400",
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
onClick={() => {
|
||||
setThemeMode(dark ? "light" : "dark")
|
||||
}}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={[
|
||||
"absolute top-px left-px inline-flex size-6 items-center justify-center rounded-full ring-1 shadow-sm transition-transform duration-200 ease-out motion-reduce:transition-none",
|
||||
dark
|
||||
? "translate-x-6 bg-black text-zinc-100 ring-zinc-700"
|
||||
: "translate-x-0 bg-white text-slate-500 ring-slate-200",
|
||||
].join(" ")}
|
||||
>
|
||||
<Icon
|
||||
name="tabler:sun-high"
|
||||
altName="tabler:moon"
|
||||
showAlt={dark}
|
||||
className="z-10 size-4"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user