mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-17 01:33:43 +08:00
feat: Standardize border radius usage across components and implement View Transition API for theme changes
This commit is contained in:
+5
-11
@@ -793,12 +793,12 @@ export const announceToScreenReader = (message: string) => {
|
||||
### 🔵 Low Priority (低优先级,时间允许时完成)
|
||||
|
||||
#### 13. **统一圆角使用方式** (预计 1 小时)
|
||||
- [ ] 全局搜索 `style={{ borderRadius: 'var(--radius-`
|
||||
- [ ] 统一改为 `className="rounded-[var(--radius-2xl)]"`
|
||||
- [ ] 或者相反,全部统一使用 `style`
|
||||
- [✅] 全局搜索 `style={{ borderRadius: 'var(--radius-`
|
||||
- [✅] 统一改为 `className="rounded-[var(--radius-2xl)]"`
|
||||
- [✅] 或者相反,全部统一使用 `style`
|
||||
|
||||
#### 14. **添加暗色模式动画过渡** (预计 1 小时)
|
||||
- [ ] 在 `ThemeProvider.tsx` 中添加 View Transition API
|
||||
- [✅] 在 `ThemeProvider.tsx` 中添加 View Transition API
|
||||
```tsx
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(() => {
|
||||
@@ -824,13 +824,7 @@ export const announceToScreenReader = (message: string) => {
|
||||
```
|
||||
- [ ] 更新文档
|
||||
|
||||
#### 17. **添加组件使用文档** (预计 4 小时)
|
||||
- [ ] 创建 `docs/COMPONENTS.md`
|
||||
- [ ] 为每个组件编写使用示例
|
||||
- [ ] 添加 Props API 文档
|
||||
- [ ] 添加可访问性指南
|
||||
|
||||
#### 18. **创建 Storybook** (预计 8 小时)
|
||||
#### 17. **创建 Storybook** (预计 8 小时)
|
||||
- [ ] 安装 Storybook 7.x
|
||||
- [ ] 为所有 UI 组件创建 stories
|
||||
- [ ] 添加 A11y addon
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ function HomePage() {
|
||||
{/* Glass Navbar */}
|
||||
<nav className="sticky top-0 z-50 pt-4 pb-2" style={{ transform: 'translateZ(0)' }}>
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<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-6 py-4 transition-all duration-[var(--transition-fluid)]" style={{ borderRadius: 'var(--radius-2xl)' }}>
|
||||
<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-6 py-4 transition-all duration-[var(--transition-fluid)] rounded-[var(--radius-2xl)]">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link
|
||||
href="/"
|
||||
|
||||
@@ -52,14 +52,12 @@ export function SearchLoadingAnimation({
|
||||
{/* Progress Bar - Unified 0-100% */}
|
||||
<div className="w-full">
|
||||
<div
|
||||
className="h-1 bg-[color-mix(in_srgb,var(--glass-bg)_50%,transparent)] overflow-hidden"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
className="h-1 bg-[color-mix(in_srgb,var(--glass-bg)_50%,transparent)] overflow-hidden rounded-[var(--radius-full)]"
|
||||
>
|
||||
<div
|
||||
className="h-full bg-[var(--accent-color)] transition-all duration-500 ease-out relative will-change-[width]"
|
||||
className="h-full bg-[var(--accent-color)] transition-all duration-500 ease-out relative will-change-[width] rounded-[var(--radius-full)]"
|
||||
style={{
|
||||
width: `${progress}%`,
|
||||
borderRadius: 'var(--radius-full)',
|
||||
transform: 'translateZ(0)'
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -25,24 +25,42 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const applyTheme = () => {
|
||||
if (theme === 'system') {
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
setActualTheme(systemPrefersDark ? 'dark' : 'light');
|
||||
document.documentElement.classList.toggle('dark', systemPrefersDark);
|
||||
const applyTheme = (newTheme?: 'light' | 'dark') => {
|
||||
const themeToApply = newTheme || (theme === 'system'
|
||||
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
||||
: theme);
|
||||
|
||||
setActualTheme(themeToApply);
|
||||
document.documentElement.classList.toggle('dark', themeToApply === 'dark');
|
||||
};
|
||||
|
||||
const applyThemeWithTransition = () => {
|
||||
// Check if View Transition API is supported
|
||||
// @ts-ignore - View Transition API is experimental
|
||||
if (typeof document.startViewTransition === 'function') {
|
||||
// @ts-ignore
|
||||
document.startViewTransition(() => {
|
||||
applyTheme();
|
||||
});
|
||||
} else {
|
||||
setActualTheme(theme);
|
||||
document.documentElement.classList.toggle('dark', theme === 'dark');
|
||||
// Fallback for browsers that don't support View Transition API
|
||||
applyTheme();
|
||||
}
|
||||
};
|
||||
|
||||
applyTheme();
|
||||
applyThemeWithTransition();
|
||||
localStorage.setItem('theme', theme);
|
||||
|
||||
// Listen for system theme changes
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mediaQuery.addEventListener('change', applyTheme);
|
||||
return () => mediaQuery.removeEventListener('change', applyTheme);
|
||||
const handleSystemThemeChange = () => {
|
||||
if (theme === 'system') {
|
||||
applyThemeWithTransition();
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleSystemThemeChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleSystemThemeChange);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -32,13 +32,12 @@ export function MovieCard({ movie, onMovieClick }: MovieCardProps) {
|
||||
className="group cursor-pointer"
|
||||
>
|
||||
<Card hover className="overflow-hidden p-0 h-full">
|
||||
<div className="relative aspect-[2/3] overflow-hidden bg-[var(--glass-bg)]" style={{ borderRadius: 'var(--radius-2xl)' }}>
|
||||
<div className="relative aspect-[2/3] overflow-hidden bg-[var(--glass-bg)] rounded-[var(--radius-2xl)]">
|
||||
<Image
|
||||
src={movie.cover}
|
||||
alt={movie.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-110 will-change-transform"
|
||||
style={{ borderRadius: 'var(--radius-2xl)' }}
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-110 will-change-transform rounded-[var(--radius-2xl)]"
|
||||
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 20vw"
|
||||
onError={(e) => {
|
||||
const target = e.currentTarget as HTMLImageElement;
|
||||
@@ -55,8 +54,7 @@ export function MovieCard({ movie, onMovieClick }: MovieCardProps) {
|
||||
/>
|
||||
{movie.rate && parseFloat(movie.rate) > 0 && (
|
||||
<div
|
||||
className="absolute top-2 right-2 bg-black/70 backdrop-blur-sm px-2.5 py-1.5 flex items-center gap-1.5"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
className="absolute top-2 right-2 bg-black/70 backdrop-blur-sm px-2.5 py-1.5 flex items-center gap-1.5 rounded-[var(--radius-full)]"
|
||||
>
|
||||
<Icons.Star size={12} className="text-yellow-400 fill-yellow-400" />
|
||||
<span className="text-xs font-bold text-white">
|
||||
|
||||
@@ -71,13 +71,11 @@ export function TagManager({
|
||||
onChange={(e) => onNewTagInputChange(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onAddTag()}
|
||||
placeholder="添加自定义标签..."
|
||||
className="flex-1 bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)] text-[var(--text-color)] px-4 py-2 focus:outline-none focus:border-[var(--accent-color)] transition-colors"
|
||||
style={{ borderRadius: 'var(--radius-2xl)' }}
|
||||
className="flex-1 bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)] text-[var(--text-color)] px-4 py-2 focus:outline-none focus:border-[var(--accent-color)] transition-colors rounded-[var(--radius-2xl)]"
|
||||
/>
|
||||
<button
|
||||
onClick={onAddTag}
|
||||
className="px-6 py-2 bg-[var(--accent-color)] text-white font-semibold hover:opacity-90 transition-opacity"
|
||||
style={{ borderRadius: 'var(--radius-2xl)' }}
|
||||
className="px-6 py-2 bg-[var(--accent-color)] text-white font-semibold hover:opacity-90 transition-opacity rounded-[var(--radius-2xl)]"
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
@@ -91,13 +89,12 @@ export function TagManager({
|
||||
<button
|
||||
onClick={() => onTagSelect(tag.id)}
|
||||
className={`
|
||||
px-6 py-2.5 text-sm font-semibold transition-all whitespace-nowrap will-change-transform
|
||||
px-6 py-2.5 text-sm font-semibold transition-all whitespace-nowrap will-change-transform rounded-[var(--radius-full)]
|
||||
${selectedTag === tag.id
|
||||
? 'bg-[var(--accent-color)] text-white shadow-md scale-105'
|
||||
: 'bg-[var(--glass-bg)] backdrop-blur-xl text-[var(--text-color)] border border-[var(--glass-border)] hover:border-[var(--accent-color)] hover:scale-105'
|
||||
}
|
||||
`}
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
>
|
||||
{tag.label}
|
||||
</button>
|
||||
@@ -107,8 +104,7 @@ export function TagManager({
|
||||
e.stopPropagation();
|
||||
onTagDelete(tag.id);
|
||||
}}
|
||||
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white flex items-center justify-center hover:bg-red-600 transition-colors"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white flex items-center justify-center hover:bg-red-600 transition-colors rounded-[var(--radius-full)]"
|
||||
>
|
||||
<Icons.X size={14} />
|
||||
</button>
|
||||
|
||||
@@ -8,8 +8,7 @@ export function EmptyState() {
|
||||
<div className="text-center py-20 animate-fade-in">
|
||||
<div className="mb-8">
|
||||
<div
|
||||
className="inline-flex items-center justify-center w-32 h-32 bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)] mb-6"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
className="inline-flex items-center justify-center w-32 h-32 bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)] mb-6 rounded-[var(--radius-full)]"
|
||||
>
|
||||
<Icons.Film size={64} className="text-[var(--text-color-secondary)]" />
|
||||
</div>
|
||||
|
||||
@@ -11,8 +11,7 @@ export function NoResults({ onReset }: NoResultsProps) {
|
||||
return (
|
||||
<div className="text-center py-20 animate-fade-in">
|
||||
<div
|
||||
className="inline-flex items-center justify-center w-32 h-32 bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)] mb-6"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
className="inline-flex items-center justify-center w-32 h-32 bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)] mb-6 rounded-[var(--radius-full)]"
|
||||
>
|
||||
<Icons.Search size={64} className="text-[var(--text-color-secondary)]" />
|
||||
</div>
|
||||
|
||||
@@ -51,8 +51,8 @@ export function TypeBadgeItem({
|
||||
? 'ring-2 ring-[var(--accent-color)] ring-offset-2'
|
||||
: ''
|
||||
}
|
||||
rounded-[var(--radius-full)]
|
||||
`}
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
>
|
||||
<span>{type}</span>
|
||||
<span className={`
|
||||
|
||||
@@ -122,13 +122,12 @@ export function VideoGrid({ videos, className = '' }: VideoGridProps) {
|
||||
}`}
|
||||
>
|
||||
{/* Poster */}
|
||||
<div className="relative aspect-[2/3] bg-[color-mix(in_srgb,var(--glass-bg)_50%,transparent)] overflow-hidden" style={{ borderRadius: 'var(--radius-2xl)' }}>
|
||||
<div className="relative aspect-[2/3] bg-[color-mix(in_srgb,var(--glass-bg)_50%,transparent)] overflow-hidden rounded-[var(--radius-2xl)]">
|
||||
{video.vod_pic ? (
|
||||
<img
|
||||
src={video.vod_pic}
|
||||
alt={video.vod_name}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500 will-change-transform"
|
||||
style={{ borderRadius: 'var(--radius-2xl)' }}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500 will-change-transform rounded-[var(--radius-2xl)]"
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = '/placeholder-poster.svg';
|
||||
|
||||
Reference in New Issue
Block a user