- 材质统一边光:slab / float / pill / pill-selected / seg-thumb / chrome-side 改为左上亮右下暗的 1px 边光 + 外侧暗线;新增 lens / field / chip 参数与 pg-float-modal - 主按钮:转动光谱环改为静态左上光谱高光,悬停沿边扫过一小段;次按钮同为 lens 玻璃 - Input / Textarea / SelectTrigger 挂 .field-glass,原生下拉与实体选择器同步;对话框按钮行 gap-3 - 提示条改 pg-float(状态色只在图标、展开不叠放);新增 GlassTooltip 接管 title 提示 - 触屏判断 input-modality.ts、touch / hover 自定义变体;animate-pg-pop 改 backwards;scroll-fade-end - 控制台外壳:侧栏无硬分隔线、折叠开关进标志行;顶栏去磨砂,右侧控件收进玻璃胶囊 - 登录页居中单卡 auth-card;详情头图「订阅」改回普通主按钮,删除 pill-solid;发现页海报角标改 glass-chip-media - 引入 simple-liquid-glass(liquid-surface.tsx);更新 docs/design 三份规范与 CHANGELOG Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
59 lines
2.4 KiB
TypeScript
59 lines
2.4 KiB
TypeScript
import { Moon, Sun } from "lucide-react";
|
|
import { useTheme } from "next-themes";
|
|
import { useEffect, useState } from "react";
|
|
import { flushSync } from "react-dom";
|
|
|
|
import { Button } from "~/components/ui/button.tsx";
|
|
import { cn } from "~/lib/utils.ts";
|
|
|
|
/** bare:放在顶栏的玻璃胶囊里时不自带玻璃,只留图标与悬停淡底 */
|
|
export function ThemeToggle({ className, bare = false }: { className?: string; bare?: boolean }) {
|
|
const [mounted, setMounted] = useState(false);
|
|
const { resolvedTheme, setTheme } = useTheme();
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
}, []);
|
|
|
|
const isDark = mounted && resolvedTheme === "dark";
|
|
const nextTheme = isDark ? "light" : "dark";
|
|
|
|
// 渐进式主题过渡:整页新旧快照交叉淡化(View Transitions 默认行为,
|
|
// plus-lighter 混合,中途无灰闪)。.theme-vt 只在本次过渡期间挂在 html
|
|
// 上,给 app.css 一个作用域去放缓时长/缓动,不影响其它 view transition
|
|
// (如 play-card)。不支持或用户偏好减弱动效时,退回直接切换。
|
|
function handleToggle() {
|
|
const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
if (!document.startViewTransition || prefersReduced) {
|
|
setTheme(nextTheme);
|
|
return;
|
|
}
|
|
|
|
const root = document.documentElement;
|
|
root.classList.add("theme-vt");
|
|
// flushSync:startViewTransition 的回调返回后浏览器就拍新快照,
|
|
// 必须让 React 同步把主题类落到 DOM,不能等下一轮调度
|
|
const vt = document.startViewTransition(() => {
|
|
flushSync(() => setTheme(nextTheme));
|
|
});
|
|
void vt.finished.finally(() => root.classList.remove("theme-vt"));
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
type="button"
|
|
variant={bare ? "ghost" : "outline"}
|
|
size="icon-sm"
|
|
aria-label={isDark ? "切换到亮色主题" : "切换到暗色主题"}
|
|
title={isDark ? "切换到亮色主题" : "切换到暗色主题"}
|
|
onClick={handleToggle}
|
|
className={cn("relative", className)}
|
|
>
|
|
{/* 两个图标常驻,靠 dark: 变体做旋转缩放交接 —— 比条件渲染多一层
|
|
"转出去/转进来"的连贯感,也天然无水合闪烁 */}
|
|
<Sun className="h-4 w-4 rotate-0 scale-100 transition-transform duration-slow ease-out dark:-rotate-90 dark:scale-0" />
|
|
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-transform duration-slow ease-out dark:rotate-0 dark:scale-100" />
|
|
</Button>
|
|
);
|
|
}
|