mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
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:
co-authored by
Claude Opus 4.6
parent
b424047c5b
commit
df137c97f3
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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';
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user