feat: Phase 3 Aether Drift — voice & video component token migration
Migrate all 10 voice/video components from Discord visual language to Aether Drift design system. Zero discord-* classes or hardcoded hex values remain in components/voice/. Key changes: - Status indicators use pastel accents: mint (connected/speaking), amber (connecting/idle), rose (muted/deafened/error) - VoiceControlBar converted to frosted glass-bubble material - ConnectionInfoPopover and ScreenShareSettingsPopover use glass popovers - Speaking ring glow changed from Discord green to mint (#86efac) - Slider accents use lavender instead of blurple - All bg/text/border tokens mapped to surface-*/txt-*/interactive-* system
This commit is contained in:
@@ -13,21 +13,21 @@ function formatBitrate(kbps: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pingColor(ms: number): string {
|
function pingColor(ms: number): string {
|
||||||
if (ms <= 80) return 'text-discord-green';
|
if (ms <= 80) return 'text-status-online';
|
||||||
if (ms <= 200) return 'text-discord-yellow';
|
if (ms <= 200) return 'text-status-idle';
|
||||||
return 'text-discord-red';
|
return 'text-txt-danger';
|
||||||
}
|
}
|
||||||
|
|
||||||
function lossColor(pct: number): string {
|
function lossColor(pct: number): string {
|
||||||
if (pct <= 1) return 'text-discord-green';
|
if (pct <= 1) return 'text-status-online';
|
||||||
if (pct <= 5) return 'text-discord-yellow';
|
if (pct <= 5) return 'text-status-idle';
|
||||||
return 'text-discord-red';
|
return 'text-txt-danger';
|
||||||
}
|
}
|
||||||
|
|
||||||
function jitterColor(ms: number): string {
|
function jitterColor(ms: number): string {
|
||||||
if (ms <= 30) return 'text-discord-green';
|
if (ms <= 30) return 'text-status-online';
|
||||||
if (ms <= 80) return 'text-discord-yellow';
|
if (ms <= 80) return 'text-status-idle';
|
||||||
return 'text-discord-red';
|
return 'text-txt-danger';
|
||||||
}
|
}
|
||||||
|
|
||||||
function sourceLabel(source: string): string {
|
function sourceLabel(source: string): string {
|
||||||
@@ -51,15 +51,15 @@ function trackLabel(direction: 'send' | 'recv', source: string, participantName:
|
|||||||
|
|
||||||
const Row = ({ label, value, colorClass }: { label: string; value: string; colorClass?: string }) => (
|
const Row = ({ label, value, colorClass }: { label: string; value: string; colorClass?: string }) => (
|
||||||
<div className="flex items-center justify-between py-[3px]">
|
<div className="flex items-center justify-between py-[3px]">
|
||||||
<span className="text-[12px] text-discord-text-muted">{label}</span>
|
<span className="text-[12px] text-txt-tertiary">{label}</span>
|
||||||
<span className={`text-[12px] font-medium ${colorClass ?? 'text-discord-text-secondary'}`}>{value}</span>
|
<span className={`text-[12px] font-medium ${colorClass ?? 'text-txt-secondary'}`}>{value}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Divider = () => <div className="border-t border-[#2b2d31] my-1" />;
|
const Divider = () => <div className="border-t border-border-soft my-1" />;
|
||||||
|
|
||||||
const SectionHeader = ({ title }: { title: string }) => (
|
const SectionHeader = ({ title }: { title: string }) => (
|
||||||
<div className="text-[10px] font-bold text-discord-text-muted uppercase tracking-wider pt-1 pb-[2px]">
|
<div className="text-[10px] font-bold text-txt-tertiary uppercase tracking-wider pt-1 pb-[2px]">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -68,10 +68,10 @@ function AudioTrackRow({ track }: { track: AudioTrackStat }) {
|
|||||||
const label = trackLabel(track.direction, track.source, track.participantName);
|
const label = trackLabel(track.direction, track.source, track.participantName);
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between py-[3px]">
|
<div className="flex items-center justify-between py-[3px]">
|
||||||
<span className="text-[12px] text-discord-text-muted truncate mr-2">{label}</span>
|
<span className="text-[12px] text-txt-tertiary truncate mr-2">{label}</span>
|
||||||
<span className="text-[12px] font-medium text-discord-text-secondary whitespace-nowrap">
|
<span className="text-[12px] font-medium text-txt-secondary whitespace-nowrap">
|
||||||
{formatBitrate(track.bitrate)}
|
{formatBitrate(track.bitrate)}
|
||||||
{track.codec && <span className="text-discord-text-muted ml-2">{track.codec}</span>}
|
{track.codec && <span className="text-txt-tertiary ml-2">{track.codec}</span>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -84,19 +84,19 @@ function VideoTrackRow({ track }: { track: VideoTrackStat }) {
|
|||||||
return (
|
return (
|
||||||
<div className="py-[3px]">
|
<div className="py-[3px]">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-[12px] text-discord-text-muted truncate mr-2">{label}</span>
|
<span className="text-[12px] text-txt-tertiary truncate mr-2">{label}</span>
|
||||||
<span className="text-[12px] font-medium text-discord-text-secondary whitespace-nowrap">
|
<span className="text-[12px] font-medium text-txt-secondary whitespace-nowrap">
|
||||||
{formatBitrate(track.bitrate)}
|
{formatBitrate(track.bitrate)}
|
||||||
{track.codec && <span className="text-discord-text-muted ml-2">{track.codec}</span>}
|
{track.codec && <span className="text-txt-tertiary ml-2">{track.codec}</span>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{(resolution || track.fps !== null || track.qualityLimitation || track.simulcastLayer) && (
|
{(resolution || track.fps !== null || track.qualityLimitation || track.simulcastLayer) && (
|
||||||
<div className="flex items-center justify-between pl-3">
|
<div className="flex items-center justify-between pl-3">
|
||||||
<span className="text-[11px] text-discord-text-muted">
|
<span className="text-[11px] text-txt-tertiary">
|
||||||
{resolution && `${resolution}`}
|
{resolution && `${resolution}`}
|
||||||
{track.fps !== null && ` @${track.fps}`}
|
{track.fps !== null && ` @${track.fps}`}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[11px] text-discord-text-muted">
|
<span className="text-[11px] text-txt-tertiary">
|
||||||
{track.direction === 'send' && track.qualityLimitation && track.qualityLimitation !== 'none'
|
{track.direction === 'send' && track.qualityLimitation && track.qualityLimitation !== 'none'
|
||||||
? track.qualityLimitation
|
? track.qualityLimitation
|
||||||
: ''}
|
: ''}
|
||||||
@@ -133,17 +133,17 @@ export function ConnectionInfoPopover({ open, onClose }: ConnectionInfoPopoverPr
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={popoverRef}
|
ref={popoverRef}
|
||||||
className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 w-[300px] bg-[#1e1f22] rounded-lg shadow-lg border border-[#111214] z-50 overflow-hidden"
|
className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 w-[300px] glass rounded-lg z-50 overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="px-3 py-2 border-b border-[#111214]">
|
<div className="px-3 py-2 border-b border-border-hard">
|
||||||
<span className="text-[14px] font-bold text-discord-text-primary">Connection Info</span>
|
<span className="text-[14px] font-bold text-txt-primary">Connection Info</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-3 py-2">
|
<div className="px-3 py-2">
|
||||||
{!room ? (
|
{!room ? (
|
||||||
<div className="text-[12px] text-discord-text-muted py-2 text-center">Not connected</div>
|
<div className="text-[12px] text-txt-tertiary py-2 text-center">Not connected</div>
|
||||||
) : !stats ? (
|
) : !stats ? (
|
||||||
<div className="text-[12px] text-discord-text-muted py-2 text-center">Gathering stats...</div>
|
<div className="text-[12px] text-txt-tertiary py-2 text-center">Gathering stats...</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Network */}
|
{/* Network */}
|
||||||
|
|||||||
@@ -43,22 +43,22 @@ export function IncomingCallModal() {
|
|||||||
<div className="absolute inset-0 bg-black/60" />
|
<div className="absolute inset-0 bg-black/60" />
|
||||||
|
|
||||||
{/* Call card */}
|
{/* Call card */}
|
||||||
<div className="relative bg-[#1e1f22] rounded-lg shadow-2xl w-[340px] overflow-hidden">
|
<div className="relative bg-surface-channel rounded-lg shadow-2xl w-[340px] overflow-hidden">
|
||||||
{/* Ring animation background */}
|
{/* Ring animation background */}
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[200px] h-[200px] rounded-full bg-discord-green/5 animate-ping" style={{ animationDuration: '2s' }} />
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[200px] h-[200px] rounded-full bg-status-online/5 animate-ping" style={{ animationDuration: '2s' }} />
|
||||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[150px] h-[150px] rounded-full bg-discord-green/10 animate-ping" style={{ animationDuration: '2s', animationDelay: '0.5s' }} />
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[150px] h-[150px] rounded-full bg-status-online/10 animate-ping" style={{ animationDuration: '2s', animationDelay: '0.5s' }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="relative p-8 flex flex-col items-center gap-4">
|
<div className="relative p-8 flex flex-col items-center gap-4">
|
||||||
{/* Caller avatar */}
|
{/* Caller avatar */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="w-20 h-20 rounded-full bg-discord-blurple flex items-center justify-center text-white text-3xl font-bold">
|
<div className="w-20 h-20 rounded-full bg-accent-primary flex items-center justify-center text-white text-3xl font-bold">
|
||||||
{incomingCall.callerName.charAt(0).toUpperCase()}
|
{incomingCall.callerName.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
{/* Ringing phone icon */}
|
{/* Ringing phone icon */}
|
||||||
<div className="absolute -bottom-1 -right-1 w-7 h-7 rounded-full bg-discord-green flex items-center justify-center">
|
<div className="absolute -bottom-1 -right-1 w-7 h-7 rounded-full bg-status-online flex items-center justify-center">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="white">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="white">
|
||||||
<path d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z" />
|
<path d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -67,8 +67,8 @@ export function IncomingCallModal() {
|
|||||||
|
|
||||||
{/* Caller info */}
|
{/* Caller info */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-[20px] font-bold text-discord-text-header">{incomingCall.callerName}</h3>
|
<h3 className="text-[20px] font-bold text-txt-primary">{incomingCall.callerName}</h3>
|
||||||
<p className="text-[14px] text-discord-text-muted mt-1">Incoming Voice Call...</p>
|
<p className="text-[14px] text-txt-tertiary mt-1">Incoming Voice Call...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
@@ -76,7 +76,7 @@ export function IncomingCallModal() {
|
|||||||
{/* Decline */}
|
{/* Decline */}
|
||||||
<button
|
<button
|
||||||
onClick={handleDecline}
|
onClick={handleDecline}
|
||||||
className="w-14 h-14 rounded-full bg-discord-red hover:bg-discord-red/80 flex items-center justify-center transition-colors group"
|
className="w-14 h-14 rounded-full bg-accent-rose hover:bg-accent-rose/80 flex items-center justify-center transition-colors group"
|
||||||
title="Decline"
|
title="Decline"
|
||||||
>
|
>
|
||||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="white" className="group-hover:scale-110 transition-transform">
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="white" className="group-hover:scale-110 transition-transform">
|
||||||
@@ -87,7 +87,7 @@ export function IncomingCallModal() {
|
|||||||
{/* Accept */}
|
{/* Accept */}
|
||||||
<button
|
<button
|
||||||
onClick={handleAccept}
|
onClick={handleAccept}
|
||||||
className="w-14 h-14 rounded-full bg-discord-green hover:bg-discord-green/80 flex items-center justify-center transition-colors group"
|
className="w-14 h-14 rounded-full bg-status-online hover:bg-status-online/80 flex items-center justify-center transition-colors group"
|
||||||
title="Accept"
|
title="Accept"
|
||||||
>
|
>
|
||||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="white" className="group-hover:scale-110 transition-transform">
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="white" className="group-hover:scale-110 transition-transform">
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ export function PictureInPicture() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={`fixed z-[40] overflow-hidden rounded-lg shadow-2xl ring-1 ring-white/10 bg-[#080a0b] select-none ${
|
className={`fixed z-[40] overflow-hidden rounded-lg shadow-2xl ring-1 ring-white/10 bg-surface-base select-none ${
|
||||||
isDragging ? 'cursor-grabbing' : 'cursor-grab'
|
isDragging ? 'cursor-grabbing' : 'cursor-grab'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
@@ -263,7 +263,7 @@ export function PictureInPicture() {
|
|||||||
style={{ imageRendering: 'auto' }}
|
style={{ imageRendering: 'auto' }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex items-center justify-center bg-[#1e1f22]">
|
<div className="w-full h-full flex items-center justify-center bg-surface-channel">
|
||||||
{displayParticipant ? (
|
{displayParticipant ? (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -271,11 +271,11 @@ export function PictureInPicture() {
|
|||||||
size={64}
|
size={64}
|
||||||
/>
|
/>
|
||||||
{speakingParticipantIds.has(displayParticipant.identity) && (
|
{speakingParticipantIds.has(displayParticipant.identity) && (
|
||||||
<div className="absolute -inset-1 rounded-full ring-2 ring-discord-green animate-pulse" />
|
<div className="absolute -inset-1 rounded-full ring-2 ring-status-online animate-pulse" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2 text-discord-text-muted">
|
<div className="flex items-center gap-2 text-txt-tertiary">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||||
<path d="M11 5L6 9H2V15H6L11 19V5ZM15.54 8.46C16.48 9.4 17 10.67 17 12S16.48 14.6 15.54 15.54L14.12 14.12C14.69 13.55 15 12.79 15 12S14.69 10.45 14.12 9.88L15.54 8.46Z" />
|
<path d="M11 5L6 9H2V15H6L11 19V5ZM15.54 8.46C16.48 9.4 17 10.67 17 12S16.48 14.6 15.54 15.54L14.12 14.12C14.69 13.55 15 12.79 15 12S14.69 10.45 14.12 9.88L15.54 8.46Z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -287,7 +287,7 @@ export function PictureInPicture() {
|
|||||||
|
|
||||||
{/* LIVE badge */}
|
{/* LIVE badge */}
|
||||||
{isScreen && (
|
{isScreen && (
|
||||||
<div className="absolute top-2 left-2 px-1.5 py-0.5 bg-discord-red rounded text-[11px] font-bold text-white uppercase tracking-wide">
|
<div className="absolute top-2 left-2 px-1.5 py-0.5 bg-accent-rose rounded text-[11px] font-bold text-white uppercase tracking-wide">
|
||||||
LIVE
|
LIVE
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -308,7 +308,7 @@ export function PictureInPicture() {
|
|||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="text-white text-xs font-semibold truncate">{displayName}</span>
|
<span className="text-white text-xs font-semibold truncate">{displayName}</span>
|
||||||
{displayParticipant && speakingParticipantIds.has(displayParticipant.identity) && (
|
{displayParticipant && speakingParticipantIds.has(displayParticipant.identity) && (
|
||||||
<div className="w-2 h-2 rounded-full bg-discord-green flex-shrink-0 animate-pulse" />
|
<div className="w-2 h-2 rounded-full bg-status-online flex-shrink-0 animate-pulse" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-white/50 text-[10px] truncate">{channelName}</div>
|
<div className="text-white/50 text-[10px] truncate">{channelName}</div>
|
||||||
|
|||||||
@@ -101,22 +101,22 @@ export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSetting
|
|||||||
const result = buildScreenShareOptions(config);
|
const result = buildScreenShareOptions(config);
|
||||||
|
|
||||||
const pillBase = 'px-3 py-1.5 rounded-full text-[13px] font-medium transition-colors cursor-pointer select-none';
|
const pillBase = 'px-3 py-1.5 rounded-full text-[13px] font-medium transition-colors cursor-pointer select-none';
|
||||||
const pillSelected = 'bg-discord-blurple text-white';
|
const pillSelected = 'bg-accent-primary text-white';
|
||||||
const pillUnselected = 'bg-[#2b2d31] text-discord-text-secondary hover:bg-[#35373c]';
|
const pillUnselected = 'bg-surface-elevated text-txt-secondary hover:bg-interactive-hover';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={popoverRef}
|
ref={popoverRef}
|
||||||
className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 w-[260px] bg-[#1e1f22] rounded-lg shadow-lg border border-[#111214] z-50 overflow-hidden"
|
className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 w-[260px] glass rounded-lg z-50 overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="px-3 py-2 border-b border-[#111214]">
|
<div className="px-3 py-2 border-b border-border-hard">
|
||||||
<span className="text-[14px] font-bold text-discord-text-primary">Stream Settings</span>
|
<span className="text-[14px] font-bold text-txt-primary">Stream Settings</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-3 py-3 flex flex-col gap-3">
|
<div className="px-3 py-3 flex flex-col gap-3">
|
||||||
{/* Resolution */}
|
{/* Resolution */}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[11px] text-discord-text-muted font-semibold uppercase tracking-wider mb-1.5">
|
<div className="text-[11px] text-txt-tertiary font-semibold uppercase tracking-wider mb-1.5">
|
||||||
Resolution
|
Resolution
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
@@ -134,7 +134,7 @@ export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSetting
|
|||||||
|
|
||||||
{/* Frame Rate */}
|
{/* Frame Rate */}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[11px] text-discord-text-muted font-semibold uppercase tracking-wider mb-1.5">
|
<div className="text-[11px] text-txt-tertiary font-semibold uppercase tracking-wider mb-1.5">
|
||||||
Frame Rate
|
Frame Rate
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
@@ -152,7 +152,7 @@ export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSetting
|
|||||||
|
|
||||||
{/* Content Mode */}
|
{/* Content Mode */}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[11px] text-discord-text-muted font-semibold uppercase tracking-wider mb-1.5">
|
<div className="text-[11px] text-txt-tertiary font-semibold uppercase tracking-wider mb-1.5">
|
||||||
Content Mode
|
Content Mode
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
@@ -171,13 +171,13 @@ export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSetting
|
|||||||
{/* Bitrate Override */}
|
{/* Bitrate Override */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
<div className="text-[11px] text-discord-text-muted font-semibold uppercase tracking-wider">
|
<div className="text-[11px] text-txt-tertiary font-semibold uppercase tracking-wider">
|
||||||
Bitrate
|
Bitrate
|
||||||
</div>
|
</div>
|
||||||
{config.customBitrateKbps != null && (
|
{config.customBitrateKbps != null && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfig({ customBitrateKbps: null })}
|
onClick={() => setConfig({ customBitrateKbps: null })}
|
||||||
className="text-[11px] text-discord-blurple hover:text-[#7983f5] font-medium transition-colors"
|
className="text-[11px] text-accent-primary hover:text-accent-lavender font-medium transition-colors"
|
||||||
>
|
>
|
||||||
Reset to Auto
|
Reset to Auto
|
||||||
</button>
|
</button>
|
||||||
@@ -191,7 +191,7 @@ export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSetting
|
|||||||
step={BITRATE_STEP}
|
step={BITRATE_STEP}
|
||||||
value={config.customBitrateKbps ?? Math.round(result.publish.videoEncoding.maxBitrate / 1000)}
|
value={config.customBitrateKbps ?? Math.round(result.publish.videoEncoding.maxBitrate / 1000)}
|
||||||
onChange={(e) => setConfig({ customBitrateKbps: Number(e.target.value) })}
|
onChange={(e) => setConfig({ customBitrateKbps: Number(e.target.value) })}
|
||||||
className="flex-1 h-1.5 accent-discord-blurple cursor-pointer appearance-none bg-[#4e5058] rounded-full
|
className="flex-1 h-1.5 accent-accent-primary cursor-pointer appearance-none bg-interactive-muted rounded-full
|
||||||
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5
|
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5
|
||||||
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-md
|
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-md
|
||||||
[&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:border-0
|
[&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:border-0
|
||||||
@@ -199,7 +199,7 @@ export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSetting
|
|||||||
[&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:cursor-pointer"
|
[&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<span className={`text-[12px] font-medium min-w-[64px] text-right ${
|
<span className={`text-[12px] font-medium min-w-[64px] text-right ${
|
||||||
config.customBitrateKbps != null ? 'text-discord-text-primary' : 'text-discord-text-muted'
|
config.customBitrateKbps != null ? 'text-txt-primary' : 'text-txt-tertiary'
|
||||||
}`}>
|
}`}>
|
||||||
{config.customBitrateKbps != null
|
{config.customBitrateKbps != null
|
||||||
? formatKbps(config.customBitrateKbps)
|
? formatKbps(config.customBitrateKbps)
|
||||||
@@ -210,8 +210,8 @@ export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSetting
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer — computed stats */}
|
{/* Footer — computed stats */}
|
||||||
<div className="px-3 py-2 border-t border-[#111214]">
|
<div className="px-3 py-2 border-t border-border-hard">
|
||||||
<span className="text-[12px] text-discord-text-muted">
|
<span className="text-[12px] text-txt-tertiary">
|
||||||
{formatBitrate(result.publish.videoEncoding.maxBitrate)} · {formatDegradation(result.overdrive.degradationPreference)}
|
{formatBitrate(result.publish.videoEncoding.maxBitrate)} · {formatDegradation(result.overdrive.degradationPreference)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative bg-[#111214] rounded-xl overflow-hidden flex items-center justify-center group transition-all duration-200 ring-1 ring-white/[0.06] hover:ring-white/10 ${
|
className={`relative bg-surface-base rounded-xl overflow-hidden flex items-center justify-center group transition-all duration-200 ring-1 ring-white/[0.06] hover:ring-white/10 ${
|
||||||
large ? 'h-full w-full' : 'h-full aspect-video'
|
large ? 'h-full w-full' : 'h-full aspect-video'
|
||||||
}`}
|
}`}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
@@ -144,18 +144,18 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
className="w-full h-full object-contain bg-black"
|
className="w-full h-full object-contain bg-black"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex flex-col items-center justify-center gap-3 bg-[#1e1f22]">
|
<div className="w-full h-full flex flex-col items-center justify-center gap-3 bg-surface-channel">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar src={null} name={participant.username} size={large ? 80 : 48} />
|
<Avatar src={null} name={participant.username} size={large ? 80 : 48} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center px-4">
|
<div className="text-center px-4">
|
||||||
<p className="text-discord-text-primary text-sm font-semibold">
|
<p className="text-txt-primary text-sm font-semibold">
|
||||||
{participant.username} is streaming
|
{participant.username} is streaming
|
||||||
</p>
|
</p>
|
||||||
{!isLocal && (
|
{!isLocal && (
|
||||||
<button
|
<button
|
||||||
onClick={handleWatch}
|
onClick={handleWatch}
|
||||||
className="mt-2 px-4 py-1.5 bg-discord-blurple hover:bg-discord-blurple/80 rounded text-white text-xs font-semibold transition-colors"
|
className="mt-2 px-4 py-1.5 bg-accent-primary hover:bg-accent-primary/80 rounded text-white text-xs font-semibold transition-colors"
|
||||||
>
|
>
|
||||||
Watch Stream
|
Watch Stream
|
||||||
</button>
|
</button>
|
||||||
@@ -165,7 +165,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* LIVE badge — top left */}
|
{/* LIVE badge — top left */}
|
||||||
<div className="absolute top-2 left-2 px-1.5 py-0.5 bg-discord-red rounded text-[11px] font-bold text-white uppercase tracking-wide">
|
<div className="absolute top-2 left-2 px-1.5 py-0.5 bg-accent-rose rounded text-[11px] font-bold text-white uppercase tracking-wide">
|
||||||
LIVE
|
LIVE
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
{/* Context Menu */}
|
{/* Context Menu */}
|
||||||
{contextMenu && (
|
{contextMenu && (
|
||||||
<div
|
<div
|
||||||
className="fixed z-[60] bg-[#111214] rounded-lg shadow-2xl p-2 min-w-[220px] border border-white/[0.06]"
|
className="fixed z-[60] bg-surface-base rounded-lg shadow-2xl p-2 min-w-[220px] border border-white/[0.06]"
|
||||||
style={{ left: contextMenu.x, top: contextMenu.y }}
|
style={{ left: contextMenu.x, top: contextMenu.y }}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
@@ -215,7 +215,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
handleStopStreaming();
|
handleStopStreaming();
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center gap-2 px-3 py-2 text-discord-red hover:bg-discord-red/10 rounded text-sm transition-colors"
|
className="w-full flex items-center gap-2 px-3 py-2 text-txt-danger hover:bg-accent-rose/10 rounded text-sm transition-colors"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="16"
|
width="16"
|
||||||
@@ -240,7 +240,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
handleChangeStream();
|
handleChangeStream();
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center gap-2 px-3 py-2 text-discord-text-secondary hover:bg-discord-modifier-hover rounded text-sm transition-colors"
|
className="w-full flex items-center gap-2 px-3 py-2 text-txt-secondary hover:bg-interactive-hover rounded text-sm transition-colors"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="16"
|
width="16"
|
||||||
@@ -254,13 +254,13 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
</button>
|
</button>
|
||||||
<div className="border-t border-white/[0.06] my-1" />
|
<div className="border-t border-white/[0.06] my-1" />
|
||||||
<div className="px-3 py-1">
|
<div className="px-3 py-1">
|
||||||
<div className="text-xs text-discord-text-muted mb-1 font-medium uppercase tracking-wider">
|
<div className="text-xs text-txt-tertiary mb-1 font-medium uppercase tracking-wider">
|
||||||
Stream Quality
|
Stream Quality
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => setQualityPopoverOpen(!qualityPopoverOpen)}
|
onClick={() => setQualityPopoverOpen(!qualityPopoverOpen)}
|
||||||
className="w-full flex items-center justify-between px-2 py-1.5 text-sm text-discord-text-secondary hover:bg-discord-modifier-hover rounded transition-colors"
|
className="w-full flex items-center justify-between px-2 py-1.5 text-sm text-txt-secondary hover:bg-interactive-hover rounded transition-colors"
|
||||||
>
|
>
|
||||||
<span>{`${useVoiceStore.getState().screenShareConfig.height}p ${useVoiceStore.getState().screenShareConfig.fps}fps`}</span>
|
<span>{`${useVoiceStore.getState().screenShareConfig.height}p ${useVoiceStore.getState().screenShareConfig.fps}fps`}</span>
|
||||||
<svg
|
<svg
|
||||||
@@ -290,7 +290,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
handleUnwatch();
|
handleUnwatch();
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center gap-2 px-3 py-2 text-discord-text-secondary hover:bg-discord-modifier-hover rounded text-sm transition-colors"
|
className="w-full flex items-center gap-2 px-3 py-2 text-txt-secondary hover:bg-interactive-hover rounded text-sm transition-colors"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="16"
|
width="16"
|
||||||
@@ -308,7 +308,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
handleWatch();
|
handleWatch();
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center gap-2 px-3 py-2 text-discord-text-secondary hover:bg-discord-modifier-hover rounded text-sm transition-colors"
|
className="w-full flex items-center gap-2 px-3 py-2 text-txt-secondary hover:bg-interactive-hover rounded text-sm transition-colors"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="16"
|
width="16"
|
||||||
@@ -327,14 +327,14 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setStreamMuteAction(userId, !isStreamMuted);
|
setStreamMuteAction(userId, !isStreamMuted);
|
||||||
}}
|
}}
|
||||||
className="w-full flex items-center justify-between px-3 py-2 text-discord-text-secondary hover:bg-discord-modifier-hover rounded text-sm transition-colors"
|
className="w-full flex items-center justify-between px-3 py-2 text-txt-secondary hover:bg-interactive-hover rounded text-sm transition-colors"
|
||||||
>
|
>
|
||||||
<span>Mute Stream</span>
|
<span>Mute Stream</span>
|
||||||
<div
|
<div
|
||||||
className={`w-4 h-4 rounded border flex items-center justify-center transition-colors ${
|
className={`w-4 h-4 rounded border flex items-center justify-center transition-colors ${
|
||||||
isStreamMuted
|
isStreamMuted
|
||||||
? 'bg-discord-blurple border-discord-blurple'
|
? 'bg-accent-primary border-accent-primary'
|
||||||
: 'border-discord-text-muted'
|
: 'border-txt-tertiary'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isStreamMuted && (
|
{isStreamMuted && (
|
||||||
@@ -351,7 +351,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
</button>
|
</button>
|
||||||
{/* Stream Volume slider */}
|
{/* Stream Volume slider */}
|
||||||
<div className="px-3 py-2">
|
<div className="px-3 py-2">
|
||||||
<div className="text-xs text-discord-text-muted mb-2 font-medium uppercase tracking-wider">
|
<div className="text-xs text-txt-tertiary mb-2 font-medium uppercase tracking-wider">
|
||||||
Stream Volume
|
Stream Volume
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -360,7 +360,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className="text-discord-text-muted flex-shrink-0"
|
className="text-txt-tertiary flex-shrink-0"
|
||||||
>
|
>
|
||||||
<path d="M3 9v6h4l5 5V4L7 9H3z" />
|
<path d="M3 9v6h4l5 5V4L7 9H3z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -372,9 +372,9 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setStreamVolumeAction(userId, parseInt(e.target.value))
|
setStreamVolumeAction(userId, parseInt(e.target.value))
|
||||||
}
|
}
|
||||||
className="flex-1 accent-discord-blurple h-1"
|
className="flex-1 accent-accent-primary h-1"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-discord-text-secondary min-w-[32px] text-right">
|
<span className="text-xs text-txt-secondary min-w-[32px] text-right">
|
||||||
{streamVolume}%
|
{streamVolume}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -385,14 +385,14 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
setAttenuationEnabled(!streamAttenuationEnabled)
|
setAttenuationEnabled(!streamAttenuationEnabled)
|
||||||
}
|
}
|
||||||
className="w-full flex items-center justify-between px-3 py-2 text-discord-text-secondary hover:bg-discord-modifier-hover rounded text-sm transition-colors"
|
className="w-full flex items-center justify-between px-3 py-2 text-txt-secondary hover:bg-interactive-hover rounded text-sm transition-colors"
|
||||||
>
|
>
|
||||||
<span>Stream Attenuation</span>
|
<span>Stream Attenuation</span>
|
||||||
<div
|
<div
|
||||||
className={`w-4 h-4 rounded border flex items-center justify-center transition-colors ${
|
className={`w-4 h-4 rounded border flex items-center justify-center transition-colors ${
|
||||||
streamAttenuationEnabled
|
streamAttenuationEnabled
|
||||||
? 'bg-discord-blurple border-discord-blurple'
|
? 'bg-accent-primary border-accent-primary'
|
||||||
: 'border-discord-text-muted'
|
: 'border-txt-tertiary'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{streamAttenuationEnabled && (
|
{streamAttenuationEnabled && (
|
||||||
@@ -410,7 +410,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
{/* Attenuation Strength slider */}
|
{/* Attenuation Strength slider */}
|
||||||
{streamAttenuationEnabled && (
|
{streamAttenuationEnabled && (
|
||||||
<div className="px-3 py-2">
|
<div className="px-3 py-2">
|
||||||
<div className="text-xs text-discord-text-muted mb-2 font-medium uppercase tracking-wider">
|
<div className="text-xs text-txt-tertiary mb-2 font-medium uppercase tracking-wider">
|
||||||
Attenuation Strength
|
Attenuation Strength
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -422,9 +422,9 @@ export function StreamTile({ tile, large }: StreamTileProps) {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setAttenuationStrength(parseInt(e.target.value))
|
setAttenuationStrength(parseInt(e.target.value))
|
||||||
}
|
}
|
||||||
className="flex-1 accent-discord-blurple h-1"
|
className="flex-1 accent-accent-primary h-1"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-discord-text-secondary min-w-[32px] text-right">
|
<span className="text-xs text-txt-secondary min-w-[32px] text-right">
|
||||||
{streamAttenuationStrength}%
|
{streamAttenuationStrength}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ export function VoiceChannel({ channelId, channelName, onClick }: VoiceChannelPr
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`w-full flex items-center gap-1.5 px-2 h-8 rounded-[4px] group transition-colors ${
|
className={`w-full flex items-center gap-1.5 px-2 h-8 rounded-[4px] group transition-colors ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-discord-modifier-selected text-white'
|
? 'bg-interactive-selected text-white'
|
||||||
: 'text-discord-text-muted hover:text-discord-text-secondary hover:bg-discord-modifier-hover'
|
: 'text-txt-tertiary hover:text-txt-secondary hover:bg-interactive-hover'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" className="flex-shrink-0 opacity-60">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" className="flex-shrink-0 opacity-60">
|
||||||
@@ -61,36 +61,36 @@ export function VoiceChannel({ channelId, channelName, onClick }: VoiceChannelPr
|
|||||||
const isScreenSharing = participant?.isScreenSharing ?? wsStatus?.isScreenSharing ?? false;
|
const isScreenSharing = participant?.isScreenSharing ?? wsStatus?.isScreenSharing ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={userId} className="flex items-center gap-2 px-2 py-0.5 rounded hover:bg-discord-modifier-hover transition-colors">
|
<div key={userId} className="flex items-center gap-2 px-2 py-0.5 rounded hover:bg-interactive-hover transition-colors">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={avatar}
|
src={avatar}
|
||||||
name={displayName}
|
name={displayName}
|
||||||
size={20}
|
size={20}
|
||||||
status={status}
|
status={status}
|
||||||
/>
|
/>
|
||||||
<span className="text-[13px] text-discord-text-secondary truncate flex-1 min-w-0">{displayName}</span>
|
<span className="text-[13px] text-txt-secondary truncate flex-1 min-w-0">{displayName}</span>
|
||||||
{/* Status badges */}
|
{/* Status badges */}
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
{isMuted && (
|
{isMuted && (
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" className="text-discord-red">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" className="text-txt-danger">
|
||||||
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" />
|
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" />
|
||||||
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" />
|
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" />
|
||||||
<line x1="3" y1="3" x2="21" y2="21" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
|
<line x1="3" y1="3" x2="21" y2="21" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
{isParticipantDeafened && (
|
{isParticipantDeafened && (
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" className="text-discord-red">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" className="text-txt-danger">
|
||||||
<path d="M12 3c-4.97 0-9 4.03-9 9v7c0 1.1.9 2 2 2h2v-7H5v-2c0-3.87 3.13-7 7-7s7 3.13 7 7v2h-2v7h2c1.1 0 2-.9 2-2v-7c0-4.97-4.03-9-9-9z" />
|
<path d="M12 3c-4.97 0-9 4.03-9 9v7c0 1.1.9 2 2 2h2v-7H5v-2c0-3.87 3.13-7 7-7s7 3.13 7 7v2h-2v7h2c1.1 0 2-.9 2-2v-7c0-4.97-4.03-9-9-9z" />
|
||||||
<line x1="3" y1="3" x2="21" y2="21" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
|
<line x1="3" y1="3" x2="21" y2="21" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
{hasCamera && (
|
{hasCamera && (
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" className="text-discord-text-muted">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" className="text-txt-tertiary">
|
||||||
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" />
|
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" />
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
{isScreenSharing && (
|
{isScreenSharing && (
|
||||||
<span className="bg-discord-red text-white text-[9px] font-bold px-1 rounded leading-[14px]">LIVE</span>
|
<span className="bg-accent-rose text-white text-[9px] font-bold px-1 rounded leading-[14px]">LIVE</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ export function VoiceChatPanel({ channelId, channelName }: VoiceChatPanelProps)
|
|||||||
const toggleVoiceChat = useUIStore((s) => s.toggleVoiceChat);
|
const toggleVoiceChat = useUIStore((s) => s.toggleVoiceChat);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[340px] flex-shrink-0 bg-discord-bg-primary flex flex-col border-l border-[#2b2d31]">
|
<div className="w-[340px] flex-shrink-0 bg-surface-chat flex flex-col border-l border-border-soft">
|
||||||
{/* Chat header */}
|
{/* Chat header */}
|
||||||
<div className="h-12 px-4 flex items-center justify-between shadow-header flex-shrink-0">
|
<div className="h-12 px-4 flex items-center justify-between shadow-header flex-shrink-0">
|
||||||
<span className="font-bold text-discord-text-primary text-[16px]">Chat</span>
|
<span className="font-bold text-txt-primary text-[16px]">Chat</span>
|
||||||
<button
|
<button
|
||||||
onClick={toggleVoiceChat}
|
onClick={toggleVoiceChat}
|
||||||
className="w-7 h-7 flex items-center justify-center text-discord-text-muted hover:text-discord-text-primary transition-colors rounded"
|
className="w-7 h-7 flex items-center justify-center text-txt-tertiary hover:text-txt-primary transition-colors rounded"
|
||||||
title="Close Chat"
|
title="Close Chat"
|
||||||
>
|
>
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { ScreenShareSettingsPopover } from './ScreenShareSettingsPopover';
|
|||||||
import { CAMERA_PRESET, startScreenShare, stopScreenShare } from '../../utils/screenShare';
|
import { CAMERA_PRESET, startScreenShare, stopScreenShare } from '../../utils/screenShare';
|
||||||
|
|
||||||
const btnBase = 'w-10 h-10 flex items-center justify-center rounded-full transition-colors';
|
const btnBase = 'w-10 h-10 flex items-center justify-center rounded-full transition-colors';
|
||||||
const btnDefault = `${btnBase} bg-[#1e1f22] text-discord-text-secondary hover:bg-[#2b2d31] hover:text-discord-text-primary`;
|
const btnDefault = `${btnBase} bg-surface-channel text-txt-secondary hover:bg-surface-elevated hover:text-txt-primary`;
|
||||||
const btnActive = (color: string) => `${btnBase} bg-${color}/20 text-${color} hover:bg-${color}/30`;
|
const btnActive = (color: string) => `${btnBase} bg-${color}/20 text-${color} hover:bg-${color}/30`;
|
||||||
const btnGreen = `${btnBase} bg-[#1e1f22] text-discord-green hover:bg-[#2b2d31]`;
|
const btnGreen = `${btnBase} bg-surface-channel text-status-online hover:bg-surface-elevated`;
|
||||||
|
|
||||||
export function VoiceControlBar() {
|
export function VoiceControlBar() {
|
||||||
const isMuted = useVoiceStore((s) => s.isMuted);
|
const isMuted = useVoiceStore((s) => s.isMuted);
|
||||||
@@ -141,12 +141,12 @@ export function VoiceControlBar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-20 opacity-0 translate-y-4 group-hover/voice:opacity-100 group-hover/voice:translate-y-0 transition-all duration-300 ease-out">
|
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-20 opacity-0 translate-y-4 group-hover/voice:opacity-100 group-hover/voice:translate-y-0 transition-all duration-300 ease-out">
|
||||||
<div className="flex items-center gap-1.5 rounded-full px-3 py-2 bg-[#111214]/90 backdrop-blur-md ring-1 ring-white/[0.06] shadow-[0_8px_32px_rgba(0,0,0,0.5)]">
|
<div className="flex items-center gap-1.5 rounded-full px-3 py-2 glass-bubble">
|
||||||
{/* Mute */}
|
{/* Mute */}
|
||||||
<button
|
<button
|
||||||
onClick={handleMute}
|
onClick={handleMute}
|
||||||
className={isMuted || isDeafened
|
className={isMuted || isDeafened
|
||||||
? `${btnBase} bg-discord-red/20 text-discord-red hover:bg-discord-red/30`
|
? `${btnBase} bg-accent-rose/20 text-txt-danger hover:bg-accent-rose/30`
|
||||||
: btnDefault
|
: btnDefault
|
||||||
}
|
}
|
||||||
title={isMuted ? 'Unmute (M)' : 'Mute (M)'}
|
title={isMuted ? 'Unmute (M)' : 'Mute (M)'}
|
||||||
@@ -162,7 +162,7 @@ export function VoiceControlBar() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleDeafen}
|
onClick={handleDeafen}
|
||||||
className={isDeafened
|
className={isDeafened
|
||||||
? `${btnBase} bg-discord-red/20 text-discord-red hover:bg-discord-red/30`
|
? `${btnBase} bg-accent-rose/20 text-txt-danger hover:bg-accent-rose/30`
|
||||||
: btnDefault
|
: btnDefault
|
||||||
}
|
}
|
||||||
title={isDeafened ? 'Undeafen (D)' : 'Deafen (D)'}
|
title={isDeafened ? 'Undeafen (D)' : 'Deafen (D)'}
|
||||||
@@ -208,7 +208,7 @@ export function VoiceControlBar() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setQualityOpen(!qualityOpen)}
|
onClick={() => setQualityOpen(!qualityOpen)}
|
||||||
className={qualityOpen
|
className={qualityOpen
|
||||||
? `${btnBase} bg-[#1e1f22] text-discord-text-primary`
|
? `${btnBase} bg-surface-channel text-txt-primary`
|
||||||
: btnDefault
|
: btnDefault
|
||||||
}
|
}
|
||||||
title="Video Quality"
|
title="Video Quality"
|
||||||
@@ -227,7 +227,7 @@ export function VoiceControlBar() {
|
|||||||
<button
|
<button
|
||||||
onClick={toggleVoiceChat}
|
onClick={toggleVoiceChat}
|
||||||
className={voiceChatOpen
|
className={voiceChatOpen
|
||||||
? `${btnBase} bg-[#1e1f22] text-discord-text-primary`
|
? `${btnBase} bg-surface-channel text-txt-primary`
|
||||||
: btnDefault
|
: btnDefault
|
||||||
}
|
}
|
||||||
title="Toggle Chat"
|
title="Toggle Chat"
|
||||||
@@ -241,7 +241,7 @@ export function VoiceControlBar() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleFullscreen}
|
onClick={handleFullscreen}
|
||||||
className={voiceFullscreen
|
className={voiceFullscreen
|
||||||
? `${btnBase} bg-[#1e1f22] text-discord-text-primary`
|
? `${btnBase} bg-surface-channel text-txt-primary`
|
||||||
: btnDefault
|
: btnDefault
|
||||||
}
|
}
|
||||||
title={voiceFullscreen ? 'Exit Fullscreen (Esc)' : 'Fullscreen'}
|
title={voiceFullscreen ? 'Exit Fullscreen (Esc)' : 'Fullscreen'}
|
||||||
@@ -263,7 +263,7 @@ export function VoiceControlBar() {
|
|||||||
{/* Disconnect */}
|
{/* Disconnect */}
|
||||||
<button
|
<button
|
||||||
onClick={handleDisconnect}
|
onClick={handleDisconnect}
|
||||||
className={`${btnBase} bg-discord-red hover:bg-discord-red-hover text-white`}
|
className={`${btnBase} bg-accent-rose hover:bg-accent-rose/80 text-white`}
|
||||||
title="Disconnect"
|
title="Disconnect"
|
||||||
>
|
>
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ export function VoiceGrid({ participants }: VoiceGridProps) {
|
|||||||
height="48"
|
height="48"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className="text-discord-text-muted/40 mx-auto mb-3"
|
className="text-txt-tertiary/40 mx-auto mb-3"
|
||||||
>
|
>
|
||||||
<path d="M11 5L6 9H2V15H6L11 19V5ZM15.54 8.46C16.48 9.4 17 10.67 17 12S16.48 14.6 15.54 15.54L14.12 14.12C14.69 13.55 15 12.79 15 12S14.69 10.45 14.12 9.88L15.54 8.46Z" />
|
<path d="M11 5L6 9H2V15H6L11 19V5ZM15.54 8.46C16.48 9.4 17 10.67 17 12S16.48 14.6 15.54 15.54L14.12 14.12C14.69 13.55 15 12.79 15 12S14.69 10.45 14.12 9.88L15.54 8.46Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p className="text-discord-text-muted text-sm">
|
<p className="text-txt-tertiary text-sm">
|
||||||
Waiting for others to join...
|
Waiting for others to join...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,7 +134,7 @@ export function VoiceGrid({ participants }: VoiceGridProps) {
|
|||||||
|
|
||||||
{/* Bottom strip of other tiles */}
|
{/* Bottom strip of other tiles */}
|
||||||
{!stripHidden && otherTiles.length > 0 && (
|
{!stripHidden && otherTiles.length > 0 && (
|
||||||
<div className="h-[120px] flex-shrink-0 flex items-center justify-center gap-2 p-2 bg-[#111214]/50 overflow-x-auto no-scrollbar">
|
<div className="h-[120px] flex-shrink-0 flex items-center justify-center gap-2 p-2 bg-surface-base/50 overflow-x-auto no-scrollbar">
|
||||||
{otherTiles.map((t) => (
|
{otherTiles.map((t) => (
|
||||||
<div
|
<div
|
||||||
key={t.key}
|
key={t.key}
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative bg-[#111214] rounded-xl overflow-hidden flex items-center justify-center group transition-all duration-200 ${
|
className={`relative bg-surface-base rounded-xl overflow-hidden flex items-center justify-center group transition-all duration-200 ${
|
||||||
isSpeaking
|
isSpeaking
|
||||||
? 'ring-[3px] ring-discord-green shadow-[0_0_12px_rgba(35,165,90,0.25)]'
|
? 'ring-[3px] ring-status-online shadow-[0_0_12px_rgba(134,239,172,0.25)]'
|
||||||
: 'ring-1 ring-white/[0.06] hover:ring-white/10'
|
: 'ring-1 ring-white/[0.06] hover:ring-white/10'
|
||||||
} ${large ? 'h-full w-full' : 'h-full aspect-video'}`}
|
} ${large ? 'h-full w-full' : 'h-full aspect-video'}`}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
@@ -89,7 +89,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
|||||||
className={`w-full h-full ${large ? 'object-contain bg-black' : 'object-cover'}`}
|
className={`w-full h-full ${large ? 'object-contain bg-black' : 'object-cover'}`}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex flex-col items-center justify-center gap-3 bg-[#1e1f22]">
|
<div className="w-full h-full flex flex-col items-center justify-center gap-3 bg-surface-channel">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={null}
|
src={null}
|
||||||
@@ -97,7 +97,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
|||||||
size={large ? 100 : 64}
|
size={large ? 100 : 64}
|
||||||
/>
|
/>
|
||||||
{isSpeaking && (
|
{isSpeaking && (
|
||||||
<div className="absolute -inset-1.5 rounded-full ring-[3px] ring-discord-green animate-pulse" />
|
<div className="absolute -inset-1.5 rounded-full ring-[3px] ring-status-online animate-pulse" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,7 +119,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
{participant.isMuted && (
|
{participant.isMuted && (
|
||||||
<div className="w-5 h-5 bg-discord-red/90 rounded-full flex items-center justify-center">
|
<div className="w-5 h-5 bg-accent-rose/90 rounded-full flex items-center justify-center">
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="white">
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="white">
|
||||||
<path d="M12 2C10.9 2 10 2.9 10 4V12C10 13.1 10.9 14 12 14C13.1 14 14 13.1 14 12V4C14 2.9 13.1 2 12 2Z" />
|
<path d="M12 2C10.9 2 10 2.9 10 4V12C10 13.1 10.9 14 12 14C13.1 14 14 13.1 14 12V4C14 2.9 13.1 2 12 2Z" />
|
||||||
<line
|
<line
|
||||||
@@ -134,7 +134,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(isLocal ? isDeafened : participant.isDeafened) && (
|
{(isLocal ? isDeafened : participant.isDeafened) && (
|
||||||
<div className="w-5 h-5 bg-discord-red/90 rounded-full flex items-center justify-center">
|
<div className="w-5 h-5 bg-accent-rose/90 rounded-full flex items-center justify-center">
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="white">
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="white">
|
||||||
<path d="M12 3c-4.97 0-9 4.03-9 9v7c0 1.1.9 2 2 2h2v-7H5v-2c0-3.87 3.13-7 7-7s7 3.13 7 7v2h-2v7h2c1.1 0 2-.9 2-2v-7c0-4.97-4.03-9-9-9z" />
|
<path d="M12 3c-4.97 0-9 4.03-9 9v7c0 1.1.9 2 2 2h2v-7H5v-2c0-3.87 3.13-7 7-7s7 3.13 7 7v2h-2v7h2c1.1 0 2-.9 2-2v-7c0-4.97-4.03-9-9-9z" />
|
||||||
<line
|
<line
|
||||||
@@ -154,11 +154,11 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
|||||||
|
|
||||||
{volumeMenu && !isLocal && (
|
{volumeMenu && !isLocal && (
|
||||||
<div
|
<div
|
||||||
className="fixed z-[60] bg-[#111214] rounded-lg shadow-2xl p-3 min-w-[200px] border border-white/[0.06]"
|
className="fixed z-[60] bg-surface-base rounded-lg shadow-2xl p-3 min-w-[200px] border border-white/[0.06]"
|
||||||
style={{ left: volumeMenu.x, top: volumeMenu.y }}
|
style={{ left: volumeMenu.x, top: volumeMenu.y }}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div className="text-xs text-discord-text-muted mb-2 font-medium uppercase tracking-wider">
|
<div className="text-xs text-txt-tertiary mb-2 font-medium uppercase tracking-wider">
|
||||||
User Volume
|
User Volume
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -167,7 +167,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
|||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className="text-discord-text-muted flex-shrink-0"
|
className="text-txt-tertiary flex-shrink-0"
|
||||||
>
|
>
|
||||||
<path d="M3 9v6h4l5 5V4L7 9H3z" />
|
<path d="M3 9v6h4l5 5V4L7 9H3z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -182,9 +182,9 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
|||||||
parseInt(e.target.value),
|
parseInt(e.target.value),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="flex-1 accent-discord-blurple h-1"
|
className="flex-1 accent-accent-primary h-1"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-discord-text-secondary min-w-[32px] text-right">
|
<span className="text-xs text-txt-secondary min-w-[32px] text-right">
|
||||||
{perUserVolume}%
|
{perUserVolume}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user