feat: upgrade Liquid Glass design system to full spec (v4.6.0)

- Upgrade CSS variables with proper glass transparency, fixed gradient
  backgrounds, cubic-bezier(0.2, 0.8, 0.2, 1) fluid transitions, and
  enhanced shadow/border values matching the Liquid Glass touchstone
- Add full backdrop-filter: blur(25px) saturate(180%) to glass-card,
  glass-input, glass-popover, and glass-backdrop CSS classes
- Add lensing inner-glow hover effects and toast-in animation
- Update all UI components (Button, Card, Input, Badge, Switch,
  SegmentedControl, ConfirmDialog, ModalBackdrop, ModalHeader,
  BackToTop, Navbar) for strict Liquid Glass compliance
- Enforce rounded-2xl / rounded-full consistency across all components
- Bump version to 4.6.0, update @vercel/analytics to ^2.0.1

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
kuekhaoyang
2026-03-15 15:47:16 +08:00
co-authored by Claude Opus 4.6
parent b424047c5b
commit df137c97f3
18 changed files with 292 additions and 258 deletions
+7 -5
View File
@@ -10,6 +10,7 @@
**在线体验:[https://kvideo.pages.dev/](https://kvideo.pages.dev/)**
[![Version](https://img.shields.io/badge/Version-4.6.0-blue?style=for-the-badge)](https://github.com/KuekHaoYang/KVideo)
[![Next.js](https://img.shields.io/badge/Next.js-16.1-black?style=for-the-badge&logo=next.js)](https://nextjs.org/)
[![React](https://img.shields.io/badge/React-19.2-blue?style=for-the-badge&logo=react)](https://react.dev/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind-4.0-38B2AC?style=for-the-badge&logo=tailwind-css)](https://tailwindcss.com/)
@@ -24,11 +25,12 @@
项目的视觉设计基于 **"Liquid Glass"** 设计系统,这是一套融合了以下特性的现代化 UI 设计语言:
- **玻璃拟态效果**:通过 `backdrop-filter` 实现的磨砂半透明效果,让 UI 元素如同真实的玻璃材质
- **通用柔和度**统一使用 `rounded-2xl` `rounded-full` 两种圆角半径,创造和谐的视觉体验
- **光影交互**:悬停和聚焦状态下的内发光效果,模拟光线被"捕获"的物理现象
- **流畅动画**:基于物理的 `cubic-bezier` 曲线,实现自然的加速和减速过渡
- **深度层级**:清晰的 z-axis 层次结构,增强空间感和交互反馈
- **玻璃拟态效果**:通过 `backdrop-filter: blur(25px) saturate(180%)` 实现的磨砂半透明效果,让 UI 元素如同真实的玻璃材质,动态反射和折射背景内容
- **通用柔和度**严格使用 `rounded-2xl`1.5rem`rounded-full`9999px两种圆角半径,创造和谐统一的视觉体验
- **光影交互**:悬停和聚焦状态下的内发光效果lensing,模拟光线被"捕获"的物理现象,配合动态阴影响应用户交互
- **流畅动画**:基于物理的 `cubic-bezier(0.2, 0.8, 0.2, 1)` 曲线,实现自然的加速和减速过渡
- **深度层级**:清晰的 z-axis 层次结构,主交互层始终在视觉最前方,背景元素通过更多模糊和更低饱和度自然后退
- **固定渐变背景**`background-attachment: fixed` 的渐变背景,让玻璃效果在滚动时保持一致的视觉效果
## 核心功能
+27 -54
View File
@@ -1,71 +1,38 @@
/* Effects - Liquid Glass Design System */
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes spin {
to {
transform: rotate(360deg);
}
to { transform: rotate(360deg); }
}
@keyframes spin-slow {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes spin-reverse {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
from { transform: rotate(360deg); }
to { transform: rotate(0deg); }
}
@keyframes bounce-subtle {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
@keyframes float {
0%,
100% {
0%, 100% {
transform: translateY(0) translateX(0);
opacity: 0.3;
}
50% {
transform: translateY(-20px) translateX(10px);
opacity: 0.8;
@@ -73,13 +40,19 @@
}
@keyframes gradient-x {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
0%,
100% {
background-position: 0% 50%;
}
/* Lensing inner-glow effect for interactive elements */
.glass-glow:hover {
box-shadow:
0 8px 24px var(--shadow-color),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
50% {
background-position: 100% 50%;
}
}
.glass-glow:focus-visible {
box-shadow:
0 0 0 3px color-mix(in srgb, var(--accent-color) 30%, transparent),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
+50 -12
View File
@@ -1,36 +1,42 @@
/* Glass Components */
/* Glass Components - Liquid Glass Design System */
.glass-card {
background: var(--bg-color);
opacity: 1;
background: var(--glass-bg);
backdrop-filter: blur(25px) saturate(180%);
-webkit-backdrop-filter: blur(25px) saturate(180%);
border-radius: var(--radius-2xl);
box-shadow: var(--shadow-sm);
box-shadow: var(--shadow-md);
border: 1px solid var(--glass-border);
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
transform: translateZ(0);
will-change: transform;
transition: all var(--transition-fluid);
position: relative;
}
.glass-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
transform: translateY(-5px) scale(1.02);
box-shadow: 0 8px 24px var(--shadow-color);
z-index: 10;
}
.glass-input {
width: 100%;
background: var(--glass-bg);
backdrop-filter: blur(10px) saturate(150%);
-webkit-backdrop-filter: blur(10px) saturate(150%);
border: 1px solid var(--glass-border);
border-radius: var(--radius-2xl);
color: var(--text-color);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
transition: all var(--transition-fluid);
}
.glass-input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.3);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-color) 30%, transparent);
}
.glass-button {
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--accent-color);
color: white;
border: none;
@@ -45,7 +51,7 @@
.glass-button:hover {
transform: translateY(-2px);
filter: brightness(1.1);
box-shadow: var(--shadow-md);
box-shadow: 0 4px 8px var(--shadow-color);
}
.glass-button:active {
@@ -53,6 +59,15 @@
filter: brightness(0.95);
}
.glass-button:disabled {
background-color: var(--text-color-secondary);
opacity: 0.5;
cursor: not-allowed;
transform: none;
filter: none;
box-shadow: none;
}
.glass-badge {
display: inline-flex;
align-items: center;
@@ -64,3 +79,26 @@
background-color: var(--accent-color);
color: white;
}
.glass-badge-secondary {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
color: var(--text-color);
}
/* Glass backdrop for modals/drawers */
.glass-backdrop {
background-color: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
/* Glass popover/dropdown */
.glass-popover {
background: var(--glass-bg);
backdrop-filter: blur(25px) saturate(180%);
-webkit-backdrop-filter: blur(25px) saturate(180%);
border-radius: var(--radius-2xl);
box-shadow: var(--shadow-md);
border: 1px solid var(--glass-border);
}
+36 -23
View File
@@ -1,9 +1,9 @@
/* Transitions - Liquid Glass Design System */
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
@@ -15,7 +15,6 @@
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
@@ -25,9 +24,8 @@
@keyframes scale-in {
0% {
opacity: 0;
transform: scale(0.9) translateY(10px);
transform: scale(0.95) translateY(10px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
@@ -39,10 +37,9 @@
opacity: 1;
transform: scale(1) translateY(0);
}
100% {
opacity: 0;
transform: scale(0.9) translateY(-10px);
transform: scale(0.95) translateY(-10px);
}
}
@@ -51,7 +48,6 @@
opacity: 0;
transform: translateY(-10px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
@@ -59,23 +55,13 @@
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideInRight {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
@keyframes fadeInUp {
@@ -83,9 +69,36 @@
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Utility animation classes */
.animate-fade-in {
animation: fade-in 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.animate-slide-up {
animation: slide-up 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.animate-scale-in {
animation: scale-in 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.animate-toast-in {
animation: toast-in 0.5s cubic-bezier(0.2, 0.8, 0.2, 1);
}
+19 -17
View File
@@ -1,35 +1,35 @@
/* CSS Custom Properties */
/* CSS Custom Properties - Liquid Glass Design System */
:root {
--font-family-system: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif;
/* Light Mode Palette */
--bg-color-light: #f2f4f7;
--bg-image-light: none;
--bg-color-light: #f0f2f5;
--bg-image-light: linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%);
--text-color-light: #1d1d1f;
--text-color-secondary-light: #6e6e73;
--accent-color-light: #0056b3;
--glass-bg-light: rgba(255, 255, 255, 0.95);
--glass-border-light: rgba(0, 0, 0, 0.05);
--shadow-color-light: rgba(0, 0, 0, 0.05);
--accent-color-light: #007aff;
--glass-bg-light: rgba(242, 242, 247, 0.8);
--glass-border-light: rgba(255, 255, 255, 0.5);
--shadow-color-light: rgba(0, 0, 0, 0.1);
/* Dark Mode Palette */
--bg-color-dark: #121212;
--bg-image-dark: linear-gradient(120deg, #1a1a1a 0%, #121212 100%);
--bg-image-dark: linear-gradient(120deg, #272B30 0%, #121212 100%);
--text-color-dark: #f5f5f7;
--text-color-secondary-dark: #8e8e93;
--accent-color-dark: #1A6DBF;
--glass-bg-dark: rgba(30, 30, 30, 0.9);
--glass-border-dark: rgba(255, 255, 255, 0.1);
--accent-color-dark: #0a84ff;
--glass-bg-dark: rgba(28, 28, 30, 0.75);
--glass-border-dark: rgba(60, 60, 60, 0.7);
--shadow-color-dark: rgba(0, 0, 0, 0.3);
/* Universal Variables */
--radius-2xl: 1.5rem;
--radius-full: 9999px;
--shadow-sm: 0 1px 2px var(--shadow-color);
--shadow-md: 0 4px 6px var(--shadow-color);
--transition-fluid: 0.3s ease;
--shadow-sm: 0 2px 4px var(--shadow-color);
--shadow-md: 0 4px 12px var(--shadow-color);
--transition-fluid: 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
/* Active Theme Variables */
/* Active Theme Variables (default: light) */
--bg-color: var(--bg-color-light);
--bg-image: var(--bg-image-light);
--text-color: var(--text-color-light);
@@ -39,7 +39,7 @@
--glass-border: var(--glass-border-light);
--shadow-color: var(--shadow-color-light);
--background: #f2f4f7;
--background: #f0f2f5;
--foreground: #1d1d1f;
}
@@ -55,8 +55,10 @@ body {
background-color: var(--bg-color);
background-image: var(--bg-image);
background-attachment: fixed;
color: var(--text-color);
font-family: var(--font-family-system);
transition: background 0.3s ease;
-webkit-overflow-scrolling: touch;
overscroll-behavior-y: none;
@@ -75,4 +77,4 @@ body.dark,
--background: #121212;
--foreground: #f5f5f7;
}
}
+30 -27
View File
@@ -24,19 +24,35 @@ export function Navbar({ onReset, isPremiumMode = false }: NavbarProps) {
const handleLogout = () => {
clearSession();
// Navigate to root to clear search query params
window.location.href = '/';
};
const iconBtnClass = `
w-8 h-8 sm:w-10 sm:h-10
flex items-center justify-center
rounded-[var(--radius-full)]
bg-[var(--glass-bg)]
border border-[var(--glass-border)]
text-[var(--text-color)]
hover:bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)]
transition-all duration-200
cursor-pointer
`;
return (
<nav className="sticky top-0 z-[2000] pt-4 pb-2" style={{
transform: 'translate3d(0, 0, 0)',
willChange: 'transform'
}}>
<nav className="sticky top-0 z-[2000] pt-4 pb-2">
<div className="max-w-7xl mx-auto px-4">
<div className="bg-[var(--glass-bg)] border border-[var(--glass-border)] shadow-[var(--shadow-sm)] px-3 sm:px-6 py-2 sm:py-4 rounded-[var(--radius-2xl)]" style={{
transform: 'translate3d(0, 0, 0)'
}}>
<div className="
bg-[var(--glass-bg)]
backdrop-blur-[25px] saturate-[180%]
[-webkit-backdrop-filter:blur(25px)_saturate(180%)]
border border-[var(--glass-border)]
shadow-[var(--shadow-md)]
px-3 sm:px-6 py-2 sm:py-4
rounded-[var(--radius-2xl)]
transition-all duration-[0.4s]
[transition-timing-function:cubic-bezier(0.2,0.8,0.2,1)]
">
<div className="flex items-center justify-between gap-2 sm:gap-4">
<Link
href={isPremiumMode ? '/premium' : '/'}
@@ -60,20 +76,12 @@ export function Navbar({ onReset, isPremiumMode = false }: NavbarProps) {
</Link>
<div className="flex items-center gap-2 sm:gap-3 flex-shrink-0">
{/* IPTV Link - only show if user has iptv_access or no auth configured */}
{hasPermission('iptv_access') && (
<Link
href="/iptv"
className="w-8 h-8 sm:w-10 sm:h-10 flex items-center justify-center rounded-[var(--radius-full)] bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)] transition-all duration-200 cursor-pointer"
aria-label="直播"
title="直播"
data-focusable
>
<Icons.TV size={16} className="sm:w-5 sm:h-5" />
</Link>
<Link href="/iptv" className={iconBtnClass} aria-label="直播" title="直播" data-focusable>
<Icons.TV size={16} className="sm:w-5 sm:h-5" />
</Link>
)}
{/* User Info */}
{session && (
<div className="hidden sm:flex items-center gap-2">
<div className="flex items-center gap-1.5 px-2.5 py-1 bg-[var(--glass-bg)] border border-[var(--glass-border)] rounded-[var(--radius-full)] text-xs">
@@ -89,7 +97,7 @@ export function Navbar({ onReset, isPremiumMode = false }: NavbarProps) {
</div>
<button
onClick={handleLogout}
className="w-8 h-8 sm:w-8 sm:h-8 flex items-center justify-center rounded-[var(--radius-full)] bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color-secondary)] hover:text-red-500 hover:border-red-500/30 transition-all duration-200 cursor-pointer"
className="w-8 h-8 flex items-center justify-center rounded-[var(--radius-full)] bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color-secondary)] hover:text-red-500 hover:border-red-500/30 transition-all duration-200 cursor-pointer"
aria-label="退出登录"
title="退出登录"
>
@@ -101,17 +109,12 @@ export function Navbar({ onReset, isPremiumMode = false }: NavbarProps) {
href="https://github.com/KuekHaoYang/KVideo"
target="_blank"
rel="noopener noreferrer"
className="w-8 h-8 sm:w-10 sm:h-10 flex items-center justify-center rounded-[var(--radius-full)] bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)] transition-all duration-200 cursor-pointer hidden sm:flex"
className={`${iconBtnClass} hidden sm:flex`}
aria-label="GitHub 仓库"
>
<Icons.Github size={20} />
</a>
<Link
href={settingsHref}
className="w-8 h-8 sm:w-10 sm:h-10 flex items-center justify-center rounded-[var(--radius-full)] bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)] transition-all duration-200 cursor-pointer"
aria-label="设置"
data-focusable
>
<Link href={settingsHref} className={iconBtnClass} aria-label="设置" data-focusable>
<svg className="w-4 h-4 sm:w-5 sm:h-5" viewBox="0 -960 960 960" fill="currentColor">
<path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z" />
</svg>
+11 -5
View File
@@ -38,11 +38,17 @@ export function BackToTop() {
return (
<button
onClick={scrollToTop}
className={`fixed bottom-8 right-8 z-[9999] p-3 rounded-full
bg-[var(--glass-bg)] border border-[var(--glass-border)]
shadow-[var(--shadow-md)] backdrop-blur-xl
text-[var(--text-color)] transition-all duration-300 ease-out
hover:bg-[color-mix(in_srgb,var(--accent-color)_15%,transparent)]
className={`fixed bottom-8 right-8 z-[9999] p-3
rounded-[var(--radius-full)]
bg-[var(--glass-bg)]
backdrop-blur-[25px] saturate-[180%]
[-webkit-backdrop-filter:blur(25px)_saturate(180%)]
border border-[var(--glass-border)]
shadow-[var(--shadow-md)]
text-[var(--text-color)]
transition-all duration-[0.4s]
[transition-timing-function:cubic-bezier(0.2,0.8,0.2,1)]
hover:bg-[color-mix(in_srgb,var(--accent-color)_15%,transparent)]
hover:scale-110 active:scale-95
${isVisible
? 'opacity-100 translate-y-0 scale-100'
+2 -15
View File
@@ -22,14 +22,8 @@ const BadgeComponent = memo(function Badge({
const iconElement = icon && (
<span
className={`inline-flex items-center justify-center ${iconPosition === 'left' ? 'mr-1' : 'ml-1'
}`}
style={{
width: '0.875em',
height: '0.875em',
transform: 'translateZ(0)',
willChange: 'auto',
}}
className={`inline-flex items-center justify-center ${iconPosition === 'left' ? 'mr-1' : 'ml-1'}`}
style={{ width: '0.875em', height: '0.875em' }}
>
{icon}
</span>
@@ -45,10 +39,6 @@ const BadgeComponent = memo(function Badge({
${variants[variant]}
${className}
`}
style={{
transform: 'translateZ(0)',
willChange: 'auto',
}}
>
{icon && iconPosition === 'left' && iconElement}
{children}
@@ -57,7 +47,4 @@ const BadgeComponent = memo(function Badge({
);
});
// Export both named and default for compatibility
export const Badge = BadgeComponent;
+23 -19
View File
@@ -11,31 +11,37 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
className = '',
...props
}, ref) => {
const baseStyles = "inline-flex items-center justify-center px-4 py-2.5 md:px-6 md:py-3 font-semibold text-sm md:text-base transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed min-h-[44px] touch-manipulation cursor-pointer";
const baseStyles = `
inline-flex items-center justify-center
px-4 py-2.5 md:px-6 md:py-3
font-semibold text-sm md:text-base
rounded-[var(--radius-2xl)]
transition-all duration-200
disabled:opacity-50 disabled:cursor-not-allowed
disabled:transform-none disabled:filter-none disabled:shadow-none
min-h-[44px] touch-manipulation cursor-pointer
`;
const variants = {
primary: `
bg-[var(--accent-color)]
text-white
border-none
rounded-[var(--radius-2xl)]
shadow-[0_2px_8px_color-mix(in_srgb,var(--shadow-color)_50%,transparent)]
hover:brightness-110
hover:shadow-[0_4px_12px_color-mix(in_srgb,var(--shadow-color)_70%,transparent)]
active:scale-[0.98]
bg-[var(--accent-color)]
text-white
border-none
shadow-[var(--shadow-sm)]
hover:translate-y-[-2px]
hover:brightness-110
hover:shadow-[0_4px_8px_var(--shadow-color)]
active:translate-y-0 active:scale-[0.98]
active:brightness-95
`,
secondary: `
bg-[var(--glass-bg)]
backdrop-blur-xl
bg-[var(--glass-bg)]
backdrop-blur-[25px] saturate-[180%]
[-webkit-backdrop-filter:blur(25px)_saturate(180%)]
saturate-[180%]
border
border-[var(--glass-border)]
rounded-[var(--radius-2xl)]
border border-[var(--glass-border)]
text-[var(--text-color)]
shadow-[0_2px_8px_color-mix(in_srgb,var(--shadow-color)_50%,transparent)]
hover:shadow-[0_4px_12px_color-mix(in_srgb,var(--shadow-color)_70%,transparent)]
shadow-[var(--shadow-sm)]
hover:shadow-[0_4px_8px_var(--shadow-color)]
active:scale-[0.98]
`,
ghost: `
@@ -58,5 +64,3 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
});
Button.displayName = 'Button';
+5 -9
View File
@@ -11,27 +11,26 @@ interface CardProps {
export function Card({ children, className = '', hover = true, blur = true, onClick, style }: CardProps) {
const hoverStyles = hover
? "hover:translate-y-[-2px] hover:shadow-[0_8px_24px_var(--shadow-color)] cursor-pointer transition-transform duration-200 ease-out"
? "hover:translate-y-[-5px] hover:scale-[1.02] hover:shadow-[0_8px_24px_var(--shadow-color)] hover:z-10 cursor-pointer"
: "";
// Conditionally apply blur classes - NOW DISABLED GLOBALLY via CSS override, but logic kept for structure
const blurClasses = blur
? "bg-[var(--glass-bg)]" // Removed backdrop-blur classes here as they are expensive
: "bg-[var(--bg-color)]/90"; // More opaque fallback
? "bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] [-webkit-backdrop-filter:blur(25px)_saturate(180%)]"
: "bg-[var(--bg-color)]/90";
const baseClasses = `
${blurClasses}
rounded-[var(--radius-2xl)]
shadow-[0_2px_8px_var(--shadow-color)] md:shadow-[var(--shadow-md)]
shadow-[var(--shadow-md)]
border
border-[var(--glass-border)]
p-4 md:p-6
relative
transition-all duration-[0.4s] [transition-timing-function:cubic-bezier(0.2,0.8,0.2,1)]
${hoverStyles}
${className}
`;
// Use semantic button when interactive
if (onClick) {
return (
<button
@@ -45,12 +44,9 @@ export function Card({ children, className = '', hover = true, blur = true, onCl
);
}
// Use div for non-interactive cards
return (
<div className={baseClasses} style={style}>
{children}
</div>
);
}
+5 -22
View File
@@ -27,36 +27,26 @@ export function ConfirmDialog({
variant = 'warning',
dangerous = false,
}: ConfirmDialogProps) {
const dialogRef = useRef<HTMLDivElement>(null);
const cancelButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) {
// Focus the cancel button when dialog opens
cancelButtonRef.current?.focus();
// Prevent body scroll
document.body.style.overflow = 'hidden';
} else {
// Restore body scroll
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
return () => { document.body.style.overflow = ''; };
}, [isOpen]);
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
onCancel();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onCancel]);
@@ -68,46 +58,39 @@ export function ConfirmDialog({
warning: 'bg-[var(--accent-color)] hover:brightness-110',
info: 'bg-blue-500 hover:bg-blue-600',
};
// Use dangerous prop to override variant
const finalVariant = dangerous ? 'danger' : variant;
return (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-[9998] bg-black/30 backdrop-blur-sm animate-fade-in"
className="fixed inset-0 z-[9998] bg-black/30 backdrop-blur-[8px] [-webkit-backdrop-filter:blur(8px)] animate-fade-in"
onClick={onCancel}
aria-hidden="true"
/>
{/* Dialog */}
<div
ref={dialogRef}
role="alertdialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
className="fixed top-1/2 left-1/2 z-[9999] w-[90%] max-w-md -translate-x-1/2 -translate-y-1/2 animate-slide-up"
className="fixed top-1/2 left-1/2 z-[9999] w-[90%] max-w-md -translate-x-1/2 -translate-y-1/2 animate-scale-in"
>
<Card className="p-6">
{/* Header */}
<Card hover={false} className="p-6">
<h2
id="dialog-title"
className="text-xl font-semibold text-[var(--text-color)] mb-3"
>
{title}
</h2>
{/* Message */}
<p
id="dialog-description"
className="text-[var(--text-color-secondary)] mb-6 leading-relaxed"
>
{message}
</p>
{/* Actions */}
<div className="flex gap-3 justify-end">
<Button
ref={cancelButtonRef}
+5 -8
View File
@@ -18,21 +18,19 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
ref={ref}
className={`
w-full px-4 py-3 md:px-6 md:py-4
text-base md:text-[var(--text-color)]
text-base
bg-[var(--glass-bg)]
backdrop-blur-[10px]
saturate-[150%]
backdrop-blur-[10px] saturate-[150%]
[-webkit-backdrop-filter:blur(10px)_saturate(150%)]
border
border-[var(--glass-border)]
border border-[var(--glass-border)]
rounded-[var(--radius-2xl)]
text-[var(--text-color)]
placeholder:text-[var(--text-color-secondary)]
focus:outline-none
focus:border-[var(--accent-color)]
focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--accent-color)_30%,transparent)]
transition-all
duration-[var(--transition-fluid)]
transition-all duration-[0.4s]
[transition-timing-function:cubic-bezier(0.2,0.8,0.2,1)]
touch-manipulation
${error ? 'border-red-500' : ''}
${className}
@@ -48,4 +46,3 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
);
Input.displayName = 'Input';
+10 -3
View File
@@ -1,5 +1,5 @@
/**
* Reusable modal backdrop component
* Reusable modal backdrop component - Liquid Glass design
*/
interface ModalBackdropProps {
@@ -10,9 +10,16 @@ interface ModalBackdropProps {
export function ModalBackdrop({ isOpen, onClose }: ModalBackdropProps) {
return (
<div
className={`fixed inset-0 z-[9998] bg-black/30 backdrop-blur-md transition-opacity duration-300 ${isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
}`}
className={`
fixed inset-0 z-[9998]
bg-black/30
backdrop-blur-[8px]
[-webkit-backdrop-filter:blur(8px)]
transition-opacity duration-300
${isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}
`}
onClick={onClose}
aria-hidden="true"
/>
);
}
+11 -2
View File
@@ -1,5 +1,5 @@
/**
* Reusable modal header component
* Reusable modal header component - Liquid Glass design
*/
interface ModalHeaderProps {
@@ -15,7 +15,16 @@ export function ModalHeader({ title, onClose }: ModalHeaderProps) {
</h3>
<button
onClick={onClose}
className="w-8 h-8 flex items-center justify-center rounded-[var(--radius-full)] bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)] transition-all duration-200 cursor-pointer"
className="
w-8 h-8 flex items-center justify-center
rounded-[var(--radius-full)]
bg-[var(--glass-bg)]
border border-[var(--glass-border)]
text-[var(--text-color)]
hover:bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)]
transition-all duration-200
cursor-pointer
"
aria-label="关闭"
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+16 -7
View File
@@ -38,7 +38,6 @@ export function SegmentedControl<T extends string>({
};
updateIndicator();
// Update on window resize as well
window.addEventListener('resize', updateIndicator);
return () => window.removeEventListener('resize', updateIndicator);
}, [value, options]);
@@ -47,14 +46,19 @@ export function SegmentedControl<T extends string>({
<div
ref={containerRef}
className={`
relative flex p-1 bg-[var(--glass-bg)] backdrop-blur-xl
border border-[var(--glass-border)] rounded-[var(--radius-2xl)]
shadow-[var(--shadow-sm)] ${className}
relative flex p-1
bg-[var(--glass-bg)]
backdrop-blur-[25px] saturate-[180%]
[-webkit-backdrop-filter:blur(25px)_saturate(180%)]
border border-[var(--glass-border)]
rounded-[var(--radius-2xl)]
shadow-[var(--shadow-sm)]
${className}
`}
>
{/* Sliding Indicator */}
<div
className="absolute top-1 bottom-1 bg-[var(--accent-color)] rounded-[calc(var(--radius-2xl)-4px)] shadow-[0_2px_8px_rgba(0,122,255,0.3)] transition-all duration-300 cubic-bezier(0.2, 0.8, 0.2, 1)"
className="absolute top-1 bottom-1 bg-[var(--accent-color)] rounded-[calc(var(--radius-2xl)-4px)] shadow-[0_2px_8px_color-mix(in_srgb,var(--accent-color)_30%,transparent)] transition-all duration-300 [transition-timing-function:cubic-bezier(0.2,0.8,0.2,1)]"
style={{
left: `${indicatorStyle.left}px`,
width: `${indicatorStyle.width}px`,
@@ -68,8 +72,13 @@ export function SegmentedControl<T extends string>({
data-value={option.value}
onClick={() => onChange(option.value)}
className={`
relative z-10 flex-1 py-2 px-4 text-sm font-semibold transition-colors duration-200
${value === option.value ? 'text-white' : 'text-[var(--text-color-secondary)] hover:text-[var(--text-color)]'}
relative z-10 flex-1 py-2 px-4
text-sm font-semibold
transition-colors duration-200
${value === option.value
? 'text-white'
: 'text-[var(--text-color-secondary)] hover:text-[var(--text-color)]'
}
`}
>
{option.label}
+21 -19
View File
@@ -25,11 +25,11 @@ export function Switch({
return (
<label
className={`
switch relative inline-flex items-center cursor-pointer
h-[30px] w-[50px] shrink-0
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
${className}
`}
relative inline-flex items-center cursor-pointer
h-[30px] w-[50px] shrink-0
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
${className}
`}
>
<input
type="checkbox"
@@ -41,20 +41,22 @@ export function Switch({
/>
<div
className={`
switch-slider w-full h-full rounded-[var(--radius-full)]
bg-[color-mix(in_srgb,var(--text-color)_20%,transparent)]
peer-checked:bg-[var(--accent-color)]
transition-colors duration-[0.4s] cubic-bezier(0.2,0.8,0.2,1)
before:content-[''] before:absolute before:h-[26px] before:w-[26px]
before:left-[2px] before:bottom-[2px]
before:bg-white before:rounded-[var(--radius-full)]
before:transition-transform before:duration-[0.4s]
before:cubic-bezier(0.2,0.8,0.2,1)
before:shadow-[0_1px_3px_rgba(0,0,0,0.2)]
peer-checked:before:translate-x-[20px]
active:before:scale-95
`}
></div>
w-full h-full rounded-[var(--radius-full)]
bg-[color-mix(in_srgb,var(--text-color)_20%,transparent)]
peer-checked:bg-[var(--accent-color)]
transition-[background-color] duration-[0.4s]
[transition-timing-function:cubic-bezier(0.2,0.8,0.2,1)]
before:content-[''] before:absolute
before:h-[26px] before:w-[26px]
before:left-[2px] before:bottom-[2px]
before:bg-white before:rounded-[var(--radius-full)]
before:transition-transform before:duration-[0.4s]
before:[transition-timing-function:cubic-bezier(0.2,0.8,0.2,1)]
before:shadow-[0_1px_3px_rgba(0,0,0,0.2)]
peer-checked:before:translate-x-[20px]
active:before:scale-95
`}
/>
</label>
);
}
+12 -9
View File
@@ -1,18 +1,18 @@
{
"name": "kvideo",
"version": "4.5.0",
"version": "4.6.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "kvideo",
"version": "4.5.0",
"version": "4.6.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@upstash/redis": "^1.37.0",
"@vercel/analytics": "^2.0.0",
"@vercel/analytics": "^2.0.1",
"hls.js": "^1.6.15",
"lucide-react": "^0.577.0",
"next": "16.1.6",
@@ -3507,9 +3507,9 @@
}
},
"node_modules/@vercel/analytics": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-2.0.0.tgz",
"integrity": "sha512-fP/ASXXz+1K/C2vWTnocd8RsGnkO9f1qOIDrhgQ3DagJtnea1EsM9AV9fDzjXlPIPb2vBQapxOIMCjtGIW8PZw==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-2.0.1.tgz",
"integrity": "sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g==",
"license": "MIT",
"peerDependencies": {
"@remix-run/react": "^2",
@@ -3531,6 +3531,9 @@
"next": {
"optional": true
},
"nuxt": {
"optional": true
},
"react": {
"optional": true
},
@@ -11063,9 +11066,9 @@
}
},
"@vercel/analytics": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-2.0.0.tgz",
"integrity": "sha512-fP/ASXXz+1K/C2vWTnocd8RsGnkO9f1qOIDrhgQ3DagJtnea1EsM9AV9fDzjXlPIPb2vBQapxOIMCjtGIW8PZw=="
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-2.0.1.tgz",
"integrity": "sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g=="
},
"acorn": {
"version": "8.16.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "kvideo",
"version": "4.5.0",
"version": "4.6.0",
"private": true,
"scripts": {
"dev": "next dev --port ${PORT:-3000}",
@@ -14,7 +14,7 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@upstash/redis": "^1.37.0",
"@vercel/analytics": "^2.0.0",
"@vercel/analytics": "^2.0.1",
"hls.js": "^1.6.15",
"lucide-react": "^0.577.0",
"next": "16.1.6",