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
14 changes: 14 additions & 0 deletions public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/components/contributors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export function Contributors() {
<p className="mt-3 text-neutral-500 dark:text-neutral-400">
感谢这些为 Mix Space 开源社区做出贡献的开发者
</p>
<p className="mt-1.5 text-xs text-neutral-400 dark:text-neutral-500 tabular-nums">
共 {humans.length + bots.length} 位贡献者
</p>
</div>

{/* Human contributors */}
Expand Down
4 changes: 2 additions & 2 deletions src/components/docs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
ViewOptionsPopover,
} from 'fumadocs-ui/layouts/docs/page';
import {
Rocket,
Compass,
Server,
Settings,
PaintRoller,
Expand All @@ -27,7 +27,7 @@ import { baseOptions } from '@/lib/layout.shared';
import { RootProvider } from '@/components/root-provider';

const iconMap: Record<string, ReactNode> = {
'新手入门': <Rocket className="size-4" />,
'新手入门': <Compass className="size-4" />,
'部署': <Server className="size-4" />,
'配置': <Settings className="size-4" />,
'前端主题': <PaintRoller className="size-4" />,
Expand Down
6 changes: 3 additions & 3 deletions src/components/features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
Bot,
Cpu,
FileEdit,
Palette,
Settings,
Sparkles,
} from 'lucide-react';

const p = 'text-sm leading-relaxed text-neutral-600 dark:text-neutral-400';
Expand Down Expand Up @@ -57,8 +57,8 @@ export function Features() {
<p className={p}>后端 API 和前端界面分开部署,可以各自升级、各自扩容。</p>
</SpotlightCard>

<SpotlightCard className="lg:col-span-2" watermark={Sparkles}>
<CardHeader icon={Sparkles} title="现代化 UI" />
<SpotlightCard className="lg:col-span-2" watermark={Palette}>
<CardHeader icon={Palette} title="现代化 UI" />
<p className={p}>
提供 Yohaku、Shiro 等官方主题,界面简洁现代,支持深色模式。
</p>
Expand Down
14 changes: 13 additions & 1 deletion src/components/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,19 @@ export function Footer() {
>
GitHub
</a>
<span>&copy; 2021-{new Date().getFullYear()} Mix Space Team</span>
<a
href="/privacy"
className="hover:text-neutral-900 dark:hover:text-neutral-100 transition-colors"
>
隐私政策
</a>
<a
href="/terms"
className="hover:text-neutral-900 dark:hover:text-neutral-100 transition-colors"
>
服务条款
</a>
<span className="tabular-nums">&copy; 2021-{new Date().getFullYear()} Mix Space Team</span>
</div>
</div>
</div>
Expand Down
55 changes: 50 additions & 5 deletions src/components/preview-images.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Image } from '@/components/image';
import type { ResponsiveImage } from '@/lib/images';
import { cn } from '@/lib/cn';
Expand All @@ -9,18 +9,32 @@ interface PreviewItem extends ResponsiveImage {

export function PreviewImages({ previews }: { previews: PreviewItem[] }) {
const [active, setActive] = useState(0);
const [paused, setPaused] = useState(false);
const [reducedMotion, setReducedMotion] = useState(false);
const btnRefs = useRef<(HTMLButtonElement | null)[]>([]);
const tablistRef = useRef<HTMLDivElement | null>(null);
const [indicator, setIndicator] = useState({ left: 0, width: 0 });

// Auto-rotate every 4s
// Auto-rotate every 4s;reduced-motion 用户不自动播放,悬停时暂停
useEffect(() => {
if (paused || reducedMotion) return;
const timer = setInterval(() => {
setActive((prev) => (prev + 1) % previews.length);
}, 4000);
return () => clearInterval(timer);
}, []);
}, [paused, reducedMotion, previews.length]);

// prefers-reduced-motion:响应式监听系统设置变化
useEffect(() => {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
const onChange = () => setReducedMotion(mq.matches);
onChange();
mq.addEventListener('change', onChange);
return () => mq.removeEventListener('change', onChange);
}, []);

// 指示条位置以 tab 相对容器的偏移计算,切换或尺寸变化时重算
const updateIndicator = useCallback(() => {
const btn = btnRefs.current[active];
if (!btn) return;
const parent = btn.parentElement;
Expand All @@ -33,8 +47,25 @@ export function PreviewImages({ previews }: { previews: PreviewItem[] }) {
});
}, [active]);

useEffect(() => {
updateIndicator();
}, [updateIndicator]);

// 容器尺寸变化(窗口 resize、字体加载等)时重算指示条位置
useEffect(() => {
const el = tablistRef.current;
if (!el) return;
const ro = new ResizeObserver(updateIndicator);
ro.observe(el);
return () => ro.disconnect();
}, [updateIndicator]);

return (
<section className="relative w-full max-w-6xl mx-auto px-6 pb-14 md:pb-20">
<section
className="relative w-full max-w-6xl mx-auto px-6 pb-14 md:pb-20"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
{/* 环境辉光:品牌色弥散阴影;内收一圈,避免模糊溢出到卡片外被误认为图片漏底 */}
<div
aria-hidden
Expand All @@ -53,6 +84,10 @@ export function PreviewImages({ previews }: { previews: PreviewItem[] }) {
{previews.map((item, i) => (
<div
key={i}
role="tabpanel"
id={`preview-panel-${i}`}
aria-labelledby={`preview-tab-${i}`}
aria-hidden={i !== active}
className={cn(
'absolute inset-0 transition-all duration-700',
i === active
Expand All @@ -75,8 +110,14 @@ export function PreviewImages({ previews }: { previews: PreviewItem[] }) {

{/* Tab bar:悬浮在图片上(绝对定位),图片一直铺到卡片底缘,不在下方留背景条 */}
<div className="absolute inset-x-0 bottom-4 z-10 flex justify-center px-3">
<div className="relative flex items-center gap-0.5 p-1 rounded-full bg-white dark:bg-neutral-800 border shadow-lg max-w-full overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<div
ref={tablistRef}
role="tablist"
aria-label="预览图切换"
className="relative flex items-center gap-0.5 p-1 rounded-full bg-white dark:bg-neutral-800 border shadow-lg max-w-full overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
>
<div
aria-hidden
className="absolute top-1 h-8 rounded-full bg-teal-700 transition-all duration-300 ease-out"
style={{
left: indicator.left,
Expand All @@ -87,6 +128,10 @@ export function PreviewImages({ previews }: { previews: PreviewItem[] }) {
<button
key={i}
ref={(el) => { btnRefs.current[i] = el; }}
role="tab"
id={`preview-tab-${i}`}
aria-selected={active === i}
aria-controls={`preview-panel-${i}`}
onClick={() => setActive(i)}
className={cn(
'relative z-10 h-8 px-3 sm:px-5 text-[13px] sm:text-sm font-medium rounded-full transition-colors whitespace-nowrap',
Expand Down
1 change: 1 addition & 0 deletions src/components/theme-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export function ThemeCard({
width={24}
height={24}
className="rounded-full bg-neutral-200 dark:bg-neutral-800"
loading="lazy"
/>
<span className="text-sm text-neutral-600 dark:text-neutral-400">
{author}
Expand Down
19 changes: 17 additions & 2 deletions src/layouts/base.astro
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,15 @@ const ogImage = new URL(image ?? '/og.png', Astro.site).href;
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{pageTitle}</title>
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0a0a0a" />
<meta name="description" content={pageDescription} />
<meta name="keywords" content={keywords.join(',')} />
<link rel="canonical" href={canonicalHref} />
{markdownUrl && <link rel="alternate" type="text/markdown" href={markdownUrl} />}
<link rel="icon" href="/images/logo.png" />
<!-- favicon:SVG 为主(品牌渐变 + M 标记),logo.png 作 PNG 兜底与 apple-touch-icon -->
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/images/logo.png" sizes="any" type="image/png" />
<link rel="shortcut icon" href="/images/logo.png" />
<link rel="apple-touch-icon" href="/images/logo.png" />
<meta
Expand Down Expand Up @@ -120,6 +124,17 @@ const ogImage = new URL(image ?? '/og.png', Astro.site).href;
<ClientRouter />
</head>
<body class="flex flex-col min-h-screen">
<slot />
<!-- skip-to-content:视觉隐藏,键盘 Tab 首焦点即出现(焦点环沿用 global.css 的 :focus-visible) -->
<a
href="#main"
class="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:rounded-md focus:bg-neutral-900 focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-white dark:focus:bg-white dark:focus:text-neutral-900"
>
跳到主要内容
</a>
<!-- fumadocs 的 HomeLayout/DocsLayout 内部的 <main> 无法从外部加 id,
故在此包一层作为 skip link 的落点;类与原 body 一致,保持 flex 布局语义 -->
<div id="main" tabindex="-1" class="flex flex-col min-h-screen w-full outline-none">
<slot />
</div>
</body>
</html>
27 changes: 26 additions & 1 deletion src/pages/404.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Base from '@/layouts/base.astro';

<Base title="页面未找到" description="你访问的页面不存在或已被移动。">
<main class="flex flex-1 flex-col items-center justify-center gap-4 px-6 py-32 text-center">
<p class="text-6xl font-bold tracking-tight">404</p>
<p aria-hidden="true" class="text-6xl font-bold tracking-tight">404</p>
<h1 class="text-2xl font-semibold">页面未找到</h1>
<p class="text-fd-muted-foreground max-w-md text-sm">
你访问的页面不存在或已被移动,请检查地址是否正确。
Expand All @@ -23,5 +23,30 @@ import Base from '@/layouts/base.astro';
阅读文档
</a>
</div>
<div
class="mt-5 flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-xs text-fd-muted-foreground"
>
<span class="text-[11px] uppercase tracking-wider opacity-70" aria-hidden="true"
>常用页面:</span
>
<a
href="/docs/deploy"
class="underline-offset-4 transition-colors hover:text-fd-foreground hover:underline"
>
部署指南
</a>
<a
href="/docs/configure/environment"
class="underline-offset-4 transition-colors hover:text-fd-foreground hover:underline"
>
环境变量参考
</a>
<a
href="/docs/configure"
class="underline-offset-4 transition-colors hover:text-fd-foreground hover:underline"
>
配置总览
</a>
</div>
</main>
</Base>
84 changes: 84 additions & 0 deletions src/pages/privacy.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
import Base from '@/layouts/base.astro';
---

<Base title="隐私政策" description="本站如何处理访问过程中产生的信息,以及自托管实例的数据归属说明。">
<main class="mx-auto w-full max-w-2xl flex-1 px-6 py-16 md:py-24">
<h1 class="text-3xl font-semibold tracking-tight">隐私政策</h1>
<p class="mt-2 text-sm text-fd-muted-foreground">最近更新:2026 年 10 月</p>
<div class="mt-8 flex flex-col gap-8 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
<p>
本页说明 Mix Space 文档站(下称「本站」)在提供服务的过程中处理哪些信息,以及
Mix Space 自托管实例的数据归属。本站是 Mix Space 开源项目的官方文档站点,
不设注册、登录或评论功能。
</p>

<section class="flex flex-col gap-4">
<h2 class="text-base font-semibold tracking-tight text-neutral-900 dark:text-neutral-100">
我们收集哪些信息
</h2>
<p>
本站为静态文档站点,不会主动向你收集姓名、电子邮箱、电话号码等个人身份信息。
你可以匿名浏览全部页面,使用本站不要求提供任何个人信息。
</p>
<p>
本站不建立可识别个人身份的档案,也不主动留存除访问日志之外的数据。
</p>
</section>

<section class="flex flex-col gap-4">
<h2 class="text-base font-semibold tracking-tight text-neutral-900 dark:text-neutral-100">
访问日志
</h2>
<p>
与大多数网站一样,托管服务提供方可能记录访问日志,包括 IP 地址、浏览器与操作系统
类型、访问时间及所请求的页面地址。此类日志仅用于保障站点可用性、排查故障与进行
必要的流量统计,不会用于建立用户画像或对外出售。如需了解日志的留存期限与删除方式,
请参考托管服务提供方的说明。
</p>
</section>

<section class="flex flex-col gap-4">
<h2 class="text-base font-semibold tracking-tight text-neutral-900 dark:text-neutral-100">
Cookie 与本地存储
</h2>
<p>
本站不使用用于跨站跟踪的 Cookie。主题偏好(如深色模式)等设置仅保存在你的浏览器
本地存储中,随浏览器数据一并由你控制,不会上传到本站的服务器。
</p>
</section>

<section class="flex flex-col gap-4">
<h2 class="text-base font-semibold tracking-tight text-neutral-900 dark:text-neutral-100">
第三方资源
</h2>
<p>
页面中的部分资源(如贡献者头像)由第三方服务提供。加载这些资源时,你的浏览器会
与对应服务方建立连接,相关请求由该服务方按其自身的隐私政策处理。
</p>
</section>

<section class="flex flex-col gap-4">
<h2 class="text-base font-semibold tracking-tight text-neutral-900 dark:text-neutral-100">
自托管实例的数据归属
</h2>
<p>
Mix Space 是自托管的开源内容管理系统。当你自行部署 Mix Space
时,实例中产生的一切数据(包括你与访客的内容、日志与备份)均由实例持有者持有并
负责,与本站无关。实例持有者应自行确定隐私政策、保障数据安全,并遵守所在司法辖区
适用法律的要求。
</p>
</section>

<section class="flex flex-col gap-4">
<h2 class="text-base font-semibold tracking-tight text-neutral-900 dark:text-neutral-100">
政策更新与联系方式
</h2>
<p>
本页内容可能随站点结构调整而更新,更新后在本页直接生效。对本页所述内容如有疑问,
可通过开源仓库的 Issue 或社区讨论渠道与我们联系。
</p>
</section>
</div>
</main>
</Base>
Loading
Loading