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:
Jannis Braun
2026-03-01 23:36:10 +01:00
parent 662eb8abc5
commit 7172091159
10 changed files with 114 additions and 114 deletions
@@ -13,21 +13,21 @@ function formatBitrate(kbps: number): string {
}
function pingColor(ms: number): string {
if (ms <= 80) return 'text-discord-green';
if (ms <= 200) return 'text-discord-yellow';
return 'text-discord-red';
if (ms <= 80) return 'text-status-online';
if (ms <= 200) return 'text-status-idle';
return 'text-txt-danger';
}
function lossColor(pct: number): string {
if (pct <= 1) return 'text-discord-green';
if (pct <= 5) return 'text-discord-yellow';
return 'text-discord-red';
if (pct <= 1) return 'text-status-online';
if (pct <= 5) return 'text-status-idle';
return 'text-txt-danger';
}
function jitterColor(ms: number): string {
if (ms <= 30) return 'text-discord-green';
if (ms <= 80) return 'text-discord-yellow';
return 'text-discord-red';
if (ms <= 30) return 'text-status-online';
if (ms <= 80) return 'text-status-idle';
return 'text-txt-danger';
}
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 }) => (
<div className="flex items-center justify-between py-[3px]">
<span className="text-[12px] text-discord-text-muted">{label}</span>
<span className={`text-[12px] font-medium ${colorClass ?? 'text-discord-text-secondary'}`}>{value}</span>
<span className="text-[12px] text-txt-tertiary">{label}</span>
<span className={`text-[12px] font-medium ${colorClass ?? 'text-txt-secondary'}`}>{value}</span>
</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 }) => (
<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}
</div>
);
@@ -68,10 +68,10 @@ function AudioTrackRow({ track }: { track: AudioTrackStat }) {
const label = trackLabel(track.direction, track.source, track.participantName);
return (
<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] font-medium text-discord-text-secondary whitespace-nowrap">
<span className="text-[12px] text-txt-tertiary truncate mr-2">{label}</span>
<span className="text-[12px] font-medium text-txt-secondary whitespace-nowrap">
{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>
</div>
);
@@ -84,19 +84,19 @@ function VideoTrackRow({ track }: { track: VideoTrackStat }) {
return (
<div className="py-[3px]">
<div className="flex items-center justify-between">
<span className="text-[12px] text-discord-text-muted truncate mr-2">{label}</span>
<span className="text-[12px] font-medium text-discord-text-secondary whitespace-nowrap">
<span className="text-[12px] text-txt-tertiary truncate mr-2">{label}</span>
<span className="text-[12px] font-medium text-txt-secondary whitespace-nowrap">
{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>
</div>
{(resolution || track.fps !== null || track.qualityLimitation || track.simulcastLayer) && (
<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}`}
{track.fps !== null && ` @${track.fps}`}
</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.qualityLimitation
: ''}
@@ -133,17 +133,17 @@ export function ConnectionInfoPopover({ open, onClose }: ConnectionInfoPopoverPr
return (
<div
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]">
<span className="text-[14px] font-bold text-discord-text-primary">Connection Info</span>
<div className="px-3 py-2 border-b border-border-hard">
<span className="text-[14px] font-bold text-txt-primary">Connection Info</span>
</div>
<div className="px-3 py-2">
{!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 ? (
<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 */}
@@ -43,22 +43,22 @@ export function IncomingCallModal() {
<div className="absolute inset-0 bg-black/60" />
{/* 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 */}
<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-[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-[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-status-online/10 animate-ping" style={{ animationDuration: '2s', animationDelay: '0.5s' }} />
</div>
{/* Content */}
<div className="relative p-8 flex flex-col items-center gap-4">
{/* Caller avatar */}
<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()}
</div>
{/* 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">
<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>
@@ -67,8 +67,8 @@ export function IncomingCallModal() {
{/* Caller info */}
<div className="text-center">
<h3 className="text-[20px] font-bold text-discord-text-header">{incomingCall.callerName}</h3>
<p className="text-[14px] text-discord-text-muted mt-1">Incoming Voice Call...</p>
<h3 className="text-[20px] font-bold text-txt-primary">{incomingCall.callerName}</h3>
<p className="text-[14px] text-txt-tertiary mt-1">Incoming Voice Call...</p>
</div>
{/* Action buttons */}
@@ -76,7 +76,7 @@ export function IncomingCallModal() {
{/* Decline */}
<button
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"
>
<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 */}
<button
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"
>
<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 (
<div
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'
}`}
style={{
@@ -263,7 +263,7 @@ export function PictureInPicture() {
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 ? (
<div className="relative">
<Avatar
@@ -271,11 +271,11 @@ export function PictureInPicture() {
size={64}
/>
{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 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">
<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>
@@ -287,7 +287,7 @@ export function PictureInPicture() {
{/* LIVE badge */}
{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
</div>
)}
@@ -308,7 +308,7 @@ export function PictureInPicture() {
<div className="flex items-center gap-1.5">
<span className="text-white text-xs font-semibold truncate">{displayName}</span>
{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 className="text-white/50 text-[10px] truncate">{channelName}</div>
@@ -101,22 +101,22 @@ export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSetting
const result = buildScreenShareOptions(config);
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 pillUnselected = 'bg-[#2b2d31] text-discord-text-secondary hover:bg-[#35373c]';
const pillSelected = 'bg-accent-primary text-white';
const pillUnselected = 'bg-surface-elevated text-txt-secondary hover:bg-interactive-hover';
return (
<div
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]">
<span className="text-[14px] font-bold text-discord-text-primary">Stream Settings</span>
<div className="px-3 py-2 border-b border-border-hard">
<span className="text-[14px] font-bold text-txt-primary">Stream Settings</span>
</div>
<div className="px-3 py-3 flex flex-col gap-3">
{/* Resolution */}
<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
</div>
<div className="flex gap-1.5">
@@ -134,7 +134,7 @@ export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSetting
{/* Frame Rate */}
<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
</div>
<div className="flex gap-1.5">
@@ -152,7 +152,7 @@ export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSetting
{/* Content Mode */}
<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
</div>
<div className="flex gap-1.5">
@@ -171,13 +171,13 @@ export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSetting
{/* Bitrate Override */}
<div>
<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
</div>
{config.customBitrateKbps != null && (
<button
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
</button>
@@ -191,7 +191,7 @@ export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSetting
step={BITRATE_STEP}
value={config.customBitrateKbps ?? Math.round(result.publish.videoEncoding.maxBitrate / 1000)}
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]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-md
[&::-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"
/>
<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
? formatKbps(config.customBitrateKbps)
@@ -210,8 +210,8 @@ export function ScreenShareSettingsPopover({ open, onClose }: ScreenShareSetting
</div>
{/* Footer — computed stats */}
<div className="px-3 py-2 border-t border-[#111214]">
<span className="text-[12px] text-discord-text-muted">
<div className="px-3 py-2 border-t border-border-hard">
<span className="text-[12px] text-txt-tertiary">
{formatBitrate(result.publish.videoEncoding.maxBitrate)} · {formatDegradation(result.overdrive.degradationPreference)}
</span>
</div>
@@ -130,7 +130,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
return (
<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'
}`}
onContextMenu={handleContextMenu}
@@ -144,18 +144,18 @@ export function StreamTile({ tile, large }: StreamTileProps) {
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">
<Avatar src={null} name={participant.username} size={large ? 80 : 48} />
</div>
<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
</p>
{!isLocal && (
<button
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
</button>
@@ -165,7 +165,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
)}
{/* 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
</div>
@@ -203,7 +203,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
{/* Context Menu */}
{contextMenu && (
<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 }}
onClick={(e) => e.stopPropagation()}
>
@@ -215,7 +215,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
handleStopStreaming();
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
width="16"
@@ -240,7 +240,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
handleChangeStream();
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
width="16"
@@ -254,13 +254,13 @@ export function StreamTile({ tile, large }: StreamTileProps) {
</button>
<div className="border-t border-white/[0.06] my-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
</div>
<div className="relative">
<button
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>
<svg
@@ -290,7 +290,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
handleUnwatch();
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
width="16"
@@ -308,7 +308,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
handleWatch();
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
width="16"
@@ -327,14 +327,14 @@ export function StreamTile({ tile, large }: StreamTileProps) {
onClick={() => {
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>
<div
className={`w-4 h-4 rounded border flex items-center justify-center transition-colors ${
isStreamMuted
? 'bg-discord-blurple border-discord-blurple'
: 'border-discord-text-muted'
? 'bg-accent-primary border-accent-primary'
: 'border-txt-tertiary'
}`}
>
{isStreamMuted && (
@@ -351,7 +351,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
</button>
{/* Stream Volume slider */}
<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
</div>
<div className="flex items-center gap-2">
@@ -360,7 +360,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
height="16"
viewBox="0 0 24 24"
fill="currentColor"
className="text-discord-text-muted flex-shrink-0"
className="text-txt-tertiary flex-shrink-0"
>
<path d="M3 9v6h4l5 5V4L7 9H3z" />
</svg>
@@ -372,9 +372,9 @@ export function StreamTile({ tile, large }: StreamTileProps) {
onChange={(e) =>
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}%
</span>
</div>
@@ -385,14 +385,14 @@ export function StreamTile({ tile, large }: StreamTileProps) {
onClick={() =>
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>
<div
className={`w-4 h-4 rounded border flex items-center justify-center transition-colors ${
streamAttenuationEnabled
? 'bg-discord-blurple border-discord-blurple'
: 'border-discord-text-muted'
? 'bg-accent-primary border-accent-primary'
: 'border-txt-tertiary'
}`}
>
{streamAttenuationEnabled && (
@@ -410,7 +410,7 @@ export function StreamTile({ tile, large }: StreamTileProps) {
{/* Attenuation Strength slider */}
{streamAttenuationEnabled && (
<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
</div>
<div className="flex items-center gap-2">
@@ -422,9 +422,9 @@ export function StreamTile({ tile, large }: StreamTileProps) {
onChange={(e) =>
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}%
</span>
</div>
@@ -29,8 +29,8 @@ export function VoiceChannel({ channelId, channelName, onClick }: VoiceChannelPr
onClick={onClick}
className={`w-full flex items-center gap-1.5 px-2 h-8 rounded-[4px] group transition-colors ${
isActive
? 'bg-discord-modifier-selected text-white'
: 'text-discord-text-muted hover:text-discord-text-secondary hover:bg-discord-modifier-hover'
? 'bg-interactive-selected text-white'
: '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">
@@ -61,36 +61,36 @@ export function VoiceChannel({ channelId, channelName, onClick }: VoiceChannelPr
const isScreenSharing = participant?.isScreenSharing ?? wsStatus?.isScreenSharing ?? false;
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
src={avatar}
name={displayName}
size={20}
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 */}
<div className="flex items-center gap-1 flex-shrink-0">
{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="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" />
</svg>
)}
{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" />
<line x1="3" y1="3" x2="21" y2="21" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
</svg>
)}
{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" />
</svg>
)}
{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>
@@ -13,13 +13,13 @@ export function VoiceChatPanel({ channelId, channelName }: VoiceChatPanelProps)
const toggleVoiceChat = useUIStore((s) => s.toggleVoiceChat);
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 */}
<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
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"
>
<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';
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 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() {
const isMuted = useVoiceStore((s) => s.isMuted);
@@ -141,12 +141,12 @@ export function VoiceControlBar() {
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="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 */}
<button
onClick={handleMute}
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
}
title={isMuted ? 'Unmute (M)' : 'Mute (M)'}
@@ -162,7 +162,7 @@ export function VoiceControlBar() {
<button
onClick={handleDeafen}
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
}
title={isDeafened ? 'Undeafen (D)' : 'Deafen (D)'}
@@ -208,7 +208,7 @@ export function VoiceControlBar() {
<button
onClick={() => setQualityOpen(!qualityOpen)}
className={qualityOpen
? `${btnBase} bg-[#1e1f22] text-discord-text-primary`
? `${btnBase} bg-surface-channel text-txt-primary`
: btnDefault
}
title="Video Quality"
@@ -227,7 +227,7 @@ export function VoiceControlBar() {
<button
onClick={toggleVoiceChat}
className={voiceChatOpen
? `${btnBase} bg-[#1e1f22] text-discord-text-primary`
? `${btnBase} bg-surface-channel text-txt-primary`
: btnDefault
}
title="Toggle Chat"
@@ -241,7 +241,7 @@ export function VoiceControlBar() {
<button
onClick={handleFullscreen}
className={voiceFullscreen
? `${btnBase} bg-[#1e1f22] text-discord-text-primary`
? `${btnBase} bg-surface-channel text-txt-primary`
: btnDefault
}
title={voiceFullscreen ? 'Exit Fullscreen (Esc)' : 'Fullscreen'}
@@ -263,7 +263,7 @@ export function VoiceControlBar() {
{/* Disconnect */}
<button
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"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
@@ -50,11 +50,11 @@ export function VoiceGrid({ participants }: VoiceGridProps) {
height="48"
viewBox="0 0 24 24"
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" />
</svg>
<p className="text-discord-text-muted text-sm">
<p className="text-txt-tertiary text-sm">
Waiting for others to join...
</p>
</div>
@@ -134,7 +134,7 @@ export function VoiceGrid({ participants }: VoiceGridProps) {
{/* Bottom strip of other tiles */}
{!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) => (
<div
key={t.key}
+11 -11
View File
@@ -73,9 +73,9 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
return (
<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
? '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'
} ${large ? 'h-full w-full' : 'h-full aspect-video'}`}
onContextMenu={handleContextMenu}
@@ -89,7 +89,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
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">
<Avatar
src={null}
@@ -97,7 +97,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
size={large ? 100 : 64}
/>
{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>
@@ -119,7 +119,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
</div>
<div className="flex items-center gap-1 flex-shrink-0">
{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">
<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
@@ -134,7 +134,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
</div>
)}
{(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">
<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
@@ -154,11 +154,11 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
{volumeMenu && !isLocal && (
<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 }}
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
</div>
<div className="flex items-center gap-2">
@@ -167,7 +167,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
height="16"
viewBox="0 0 24 24"
fill="currentColor"
className="text-discord-text-muted flex-shrink-0"
className="text-txt-tertiary flex-shrink-0"
>
<path d="M3 9v6h4l5 5V4L7 9H3z" />
</svg>
@@ -182,9 +182,9 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
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}%
</span>
</div>