- 材质统一边光: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
138 lines
5.2 KiB
TypeScript
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>
|
|
);
|
|
}
|