Files
rootandClaude Opus 5.5 09a2adb637 feat(web): 设计规范与 uhd_web 同步(截至 2026-10-08)
- 材质统一边光: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
2026-10-08 02:29:19 +08:00

138 lines
5.2 KiB
TypeScript

import { useTheme } from "next-themes";
import { LiquidGlass, type LiquidGlassProps } from "simple-liquid-glass";
import { useTouchInput } from "~/lib/input-modality.ts";
import { cn } from "~/lib/utils.ts";
/**
* 液态玻璃面:simple-liquid-glass 的薄封装,材质按 meniscus 的尺寸分档。
* - lens:小控件(图标键、头像),几乎不糊,只在边带折射;
* - regular:栏(顶栏胶囊、播放器底栏);
* - frost:大块、字多的悬浮层(菜单)。只有真模糊,不做折射(见下方 effectMode)。
* 折射只有 Chromium 桌面有(SVG 位移进 backdrop-filter);Safari / Firefox / 触屏设备走包自带的磨砂。
* 只放在压着图片、视频、海报的层上;背后没细节的内容卡继续用 pg-slab。
*/
export type LiquidTier = "lens" | "regular" | "frost";
const TIERS: Record<
LiquidTier,
{
blur: number;
displacement: number;
fill: { dark: string; light: string };
}
> = {
lens: { blur: 1, displacement: 6, fill: { dark: "rgba(0,0,0,0.14)", light: "rgba(255,255,255,0.08)" } },
regular: { blur: 5, displacement: 10, fill: { dark: "rgba(10,11,14,0.30)", light: "rgba(255,255,255,0.16)" } },
frost: { blur: 20, displacement: 8, fill: { dark: "rgba(14,15,19,0.58)", light: "rgba(255,255,255,0.5)" } },
};
const FROST_BLUR = "blur(24px) saturate(160%)";
// 模糊之上再压一层底色,浅色字(次要说明、退出登录)压在暖色画面上也读得清
const FROST_FILL = { dark: "rgba(14,15,19,0.55)", light: "rgba(255,255,255,0.6)" };
interface Props extends Omit<LiquidGlassProps, "radius"> {
tier?: LiquidTier;
radius: number;
/** 播放器等恒暗区域固定用夜间参数 */
forceDark?: boolean;
/** 不画包自带的边光(主按钮的棱镜高光就是它的边光) */
rimless?: boolean;
/** 折射边缘带色散:背后画面在棱边被拆成彩色(只在 Chromium 桌面、背后有画面时看得到) */
dispersive?: boolean;
/** 只要玻璃外观与模糊,不做折射(测速页:实时曲线在重绘,不加 SVG 滤镜开销) */
noRefraction?: boolean;
}
export function LiquidSurface({
tier = "regular",
radius,
forceDark,
rimless,
dispersive,
noRefraction,
className,
style,
children,
...rest
}: Props) {
const { resolvedTheme } = useTheme();
const dark = forceDark || resolvedTheme !== "light";
// 触屏设备(手机、平板、车机)不跑 SVG 折射,退回同参数的磨砂:车机这类 Linux 触屏会被包当成桌面
const touch = useTouchInput();
// 不折射、只模糊的情况:包在这个模式下不铺 glassColor,还会在顶部画一道 50% 白的光泽(夜间压住第一行字),
// 底色由这里铺,光泽在 app.css 里按档收淡 / 去掉
const blurOnly = touch || noRefraction || tier === "frost";
const t = TIERS[tier];
return (
<LiquidGlass
mode="custom"
refraction="lens"
lensProfile="material"
quality={dispersive ? "high" : "standard"}
radius={radius}
blur={t.blur}
displacementScale={dispersive ? t.displacement + 4 : t.displacement}
saturation={150}
dispersion={50}
aberrationIntensity={dispersive ? 0.4 : 0.06}
frost={0}
// frost 档(字多的悬浮层)不做折射:Chromium 里同一个 backdrop-filter 同时有 blur 和 SVG 位移时,
// 模糊不生效,菜单底下的画面原样透出来、字看不清。frost 只要真模糊,折射留给栏和按钮
effectMode={blurOnly ? "blur" : "auto"}
glassColor={dark ? t.fill.dark : t.fill.light}
borderColor={rimless ? "rgba(255,255,255,0)" : dark ? "rgba(255,255,255,0.22)" : "rgba(255,255,255,0.65)"}
className={cn("liquid-surface", `liquid-surface-${tier}`, blurOnly && "liquid-surface-blur", className)}
style={{
// frost 档(菜单、搜索结果等字多的悬浮层)自己垫一层固定模糊:包退回磨砂时只给 8~11px,
// 菜单压在画面上看不清,这里统一保证 24px
...(tier === "frost"
? {
backdropFilter: FROST_BLUR,
WebkitBackdropFilter: FROST_BLUR,
backgroundColor: dark ? FROST_FILL.dark : FROST_FILL.light,
}
: blurOnly
? { backgroundColor: dark ? t.fill.dark : t.fill.light }
: null),
// 1px 暗边线在亮棱外侧,投影只留贴边一层
boxShadow: dark
? "0 0 0 1px rgba(0,0,0,0.28), 0 8px 24px -14px rgba(0,0,0,0.5)"
: "0 0 0 1px rgba(20,22,28,0.08), 0 8px 24px -16px rgba(20,22,28,0.22)",
...style,
}}
{...rest}
>
{children}
</LiquidGlass>
);
}
/**
* 给压在大图上的按钮加真折射:外面套一层 lens 玻璃,按钮本身挂 .btn-liquid 去掉自己的底和模糊,
* 只留字、图标和主按钮的彩虹环。radius 取按钮高度的一半。
*/
export function LiquidLens({
radius,
primary,
className,
children,
}: {
radius: number;
primary?: boolean;
className?: string;
children: React.ReactNode;
}) {
return (
<LiquidSurface
tier="lens"
radius={radius}
rimless={primary}
dispersive={primary}
className={className}
style={{ width: "auto", height: "auto" }}
>
{children}
</LiquidSurface>
);
}