Files
UHD_Media_Tool/apps/web/app/components/theme-toggle.tsx
T
rootandClaude Fable 5.1 304feea141 chore(deno): migrate toolchain from Node 24 + pnpm to Deno 2.9.7 + TypeScript 7.0.2 (W1.5)
- root deno.json workspace (nodeModulesDir auto), npm versions registered once in imports, deno.lock;
  remove pnpm-lock/pnpm-workspace/root package.json; members keep minimal package.json (name/type/exports)
- tasks: dev, typecheck (tsc 7.0.2 per member + deno check), lint/fmt (biome, useImportExtensions),
  test (vitest via deno), check:tokens, build (web + deno compile), ci, db:migrate/reset, gen:types
- tsconfig base moved to config/ (no root tsconfig.json); deno types -> types/deno.d.ts; web drops baseUrl
- server: Deno.serve entry with graceful shutdown, --role migrate|preflight|probe, WEB_ROOT static SPA hosting;
  drop @hono/node-server, tsx, esbuild build.mjs
- logger: redact child() bindings and serialize err by message/stack; sync stdout destination; tests
- relative imports get explicit extensions (sites/mp-sync paths left for follow-up)
- crash-recovery tests spawn `deno run -A`; tasks tests use unique per-file schemas
- single-container Dockerfile + entrypoint (migrate -> preflight -> api+worker watchdog), docker-compose.yml
- docs: README, runbook, ADR-001 superseded, plan §1 validation row

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-27 18:53:06 +08:00

58 lines
2.3 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";
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>
);
}