chore: scaffold monorepo and migrate uhd_web design system (W0-A)
- pnpm workspace: apps/{web,server}, packages/{contracts,db,domain,adapters,tasks,agent,testkit}
- server: Hono + zod-openapi, API Key auth with scopes, redacting pino logger, SSE event bus with replay, worker heartbeat
- db: Kysely + pg, file migrator, 0001_init (settings/api_key/audit_event/outbox_event)
- web: React Router 7 SPA console shell with 12 workspace placeholders, uhd_web tokens/ui/guard scripts
- docs: ADR-001 (Node over Deno), dev runbook, design system rules
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
commit
b22c440139
138 files changed
+14455
No files matched your search
@@ -0,0 +1,57 @@
|
||||
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";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
export function ThemeToggle({ className }: { className?: string }) {
|
||||
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="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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user