feat: unified glass material system for all floating surfaces
Add .glass-modal CSS class (0.82 opacity) for dialogs and apply consistent glass tiers across all floating UI: modals, context menus, tooltips, popovers, and call cards. Replaces ad-hoc bg-surface-elevated and inline styles with the documented 5-tier glass hierarchy.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void | Promise<void>;
|
||||
title: string;
|
||||
description: string | React.ReactNode;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
variant?: 'danger' | 'warning';
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function ConfirmDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
title,
|
||||
description,
|
||||
confirmLabel = 'Confirm',
|
||||
cancelLabel = 'Cancel',
|
||||
variant = 'danger',
|
||||
loading = false,
|
||||
}: ConfirmDialogProps) {
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && !loading) onClose();
|
||||
}, [onClose, loading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
}, [isOpen, handleKeyDown]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const isDanger = variant === 'danger';
|
||||
const accentBg = isDanger ? 'bg-accent-rose/10' : 'bg-accent-amber/10';
|
||||
const accentBorder = isDanger ? 'border-accent-rose/20' : 'border-accent-amber/20';
|
||||
const confirmBg = isDanger
|
||||
? 'bg-accent-rose hover:bg-accent-rose/80'
|
||||
: 'bg-accent-amber hover:bg-accent-amber/80';
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center animate-fade-in">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50"
|
||||
onClick={() => { if (!loading) onClose(); }}
|
||||
/>
|
||||
<div className="relative max-w-sm w-full mx-4 glass-modal rounded-lg animate-slide-up">
|
||||
<div className="p-4">
|
||||
<h3 className="text-base font-semibold text-txt-primary mb-3">{title}</h3>
|
||||
<div className={`p-3 rounded-lg ${accentBg} border ${accentBorder} mb-4`}>
|
||||
<div className="text-sm text-txt-secondary">{description}</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
className="px-3 py-1.5 text-sm text-txt-secondary hover:text-txt-primary transition-colors disabled:opacity-50"
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
disabled={loading}
|
||||
className={`px-3 py-1.5 ${confirmBg} text-white text-sm font-medium rounded transition-colors disabled:opacity-50`}
|
||||
>
|
||||
{loading ? 'Please wait...' : confirmLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
@@ -64,7 +64,7 @@ export function ContextMenu({ items, children }: ContextMenuProps) {
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="fixed z-[200] min-w-[180px] py-1.5 bg-surface-elevated rounded-md shadow-elevation-high animate-fade-in max-h-[calc(100vh-16px)] overflow-y-auto scrollbar-thin"
|
||||
className="fixed z-[200] min-w-[180px] py-1.5 glass rounded-md animate-fade-in max-h-[calc(100vh-16px)] overflow-y-auto scrollbar-thin"
|
||||
style={{ left: position.x, top: position.y }}
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
|
||||
@@ -27,10 +27,10 @@ export function Modal({ isOpen, onClose, title, children, maxWidth = 'max-w-md'
|
||||
return (
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center animate-fade-in">
|
||||
<div
|
||||
className="absolute inset-0 bg-surface-overlay"
|
||||
className="absolute inset-0 bg-black/50"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className={`relative ${maxWidth} w-full mx-4 max-h-[calc(100vh-2rem)] flex flex-col bg-surface-elevated rounded-lg shadow-xl animate-slide-up`}>
|
||||
<div className={`relative ${maxWidth} w-full mx-4 max-h-[calc(100vh-2rem)] flex flex-col glass-modal rounded-lg animate-slide-up`}>
|
||||
{title && (
|
||||
<div className="flex items-center justify-between px-4 pt-4 flex-shrink-0">
|
||||
<h2 className="text-xl font-bold text-txt-primary">{title}</h2>
|
||||
|
||||
@@ -43,7 +43,7 @@ export function Tooltip({ content, children, position = 'right', delay = 200 }:
|
||||
<div
|
||||
ref={floatingRef}
|
||||
style={style}
|
||||
className="px-3 py-1.5 text-sm font-medium text-txt-primary bg-surface-elevated rounded-md shadow-elevation-high whitespace-nowrap pointer-events-none"
|
||||
className="px-3 py-1.5 text-sm font-medium text-txt-primary glass rounded-md whitespace-nowrap pointer-events-none"
|
||||
>
|
||||
{content}
|
||||
</div>,
|
||||
|
||||
@@ -79,16 +79,10 @@ export function UserProfilePopout({ user, onClose, position }: UserProfilePopout
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed z-[200] w-[340px] rounded-[12px] overflow-hidden animate-fade-in select-none border border-white/[0.07]"
|
||||
style={{
|
||||
...(position
|
||||
? { top, left }
|
||||
: { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }),
|
||||
backdropFilter: 'blur(20px) saturate(1.2)',
|
||||
WebkitBackdropFilter: 'blur(20px) saturate(1.2)',
|
||||
backgroundColor: 'rgba(20,20,26,0.85)',
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.45), 0 2px 8px rgba(0,0,0,0.25)',
|
||||
}}
|
||||
className="fixed z-[200] w-[340px] rounded-[12px] overflow-hidden animate-fade-in select-none glass-modal"
|
||||
style={position
|
||||
? { top, left }
|
||||
: { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
|
||||
>
|
||||
{/* Banner */}
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user