feat: Discord Dark theme overhaul — darker palette, floating controls, UI polish

Shift entire color palette from Discord's Ash theme to the darker Dark theme.
Redesign VoiceControlBar as floating auto-show pill, add gradient Join Voice
screen, move invite icon to server header, add voice channel user status
badges, update auth pages with subtle gradient backgrounds, and sweep all
hardcoded hex colors across 14 files to match the new darker palette.
This commit is contained in:
Jannis Braun
2026-02-19 06:49:50 +01:00
parent cd48261f54
commit 1f813395e2
28 changed files with 311 additions and 277 deletions
File diff suppressed because one or more lines are too long
@@ -208,15 +208,26 @@ export function ChannelSidebar() {
return (
<div className="w-60 bg-discord-bg-secondary flex flex-col flex-shrink-0 select-none">
{/* Server header */}
<button
onClick={() => openModal('serverSettings')}
className="h-12 px-4 flex items-center justify-between shadow-header z-10 hover:bg-discord-modifier-hover transition-colors group"
>
<span className="font-bold text-[16px] text-discord-text-primary truncate leading-tight">{server.name}</span>
<svg width="18" height="18" viewBox="0 0 18 18" fill="currentColor" className="text-discord-text-muted flex-shrink-0 group-hover:text-discord-text-secondary">
<path d="M5.293 7.293a1 1 0 011.414 0L9 9.586l2.293-2.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" />
</svg>
</button>
<div className="h-12 flex items-center shadow-header z-10 group/header">
<button
onClick={() => openModal('serverSettings')}
className="flex-1 h-full px-4 flex items-center justify-between hover:bg-discord-modifier-hover transition-colors min-w-0"
>
<span className="font-bold text-[16px] text-discord-text-primary truncate leading-tight">{server.name}</span>
<svg width="18" height="18" viewBox="0 0 18 18" fill="currentColor" className="text-discord-text-muted flex-shrink-0">
<path d="M5.293 7.293a1 1 0 011.414 0L9 9.586l2.293-2.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" />
</svg>
</button>
<button
onClick={() => openModal('invite')}
className="w-10 h-full flex items-center justify-center text-discord-text-muted hover:text-discord-text-primary hover:bg-discord-modifier-hover transition-all flex-shrink-0"
title="Invite People"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 3H24V5H21V8H19V5H16V3H19V0H21V3ZM10 12C12.21 12 14 10.21 14 8C14 5.79 12.21 4 10 4C7.79 4 6 5.79 6 8C6 10.21 7.79 12 10 12ZM10 13C6.69 13 1 14.66 1 18V20H19V18C19 14.66 13.31 13 10 13Z" />
</svg>
</button>
</div>
{/* Channels */}
<div className="flex-1 overflow-y-auto pt-3 px-2 space-y-[21px] no-scrollbar">
@@ -308,18 +319,6 @@ export function ChannelSidebar() {
</div>
</div>
{/* Restore Invite Button */}
<div className="pt-2">
<button
onClick={() => openModal('invite')}
className="w-full flex items-center gap-2 px-2 h-8 rounded-[4px] text-[15px] font-medium text-discord-text-muted hover:text-discord-text-secondary hover:bg-discord-modifier-hover transition-colors"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" className="opacity-60">
<path d="M13 13v5h-2v-5H6v-2h5V6h2v5h5v2h-5z" />
</svg>
Invite People
</button>
</div>
</div>
{/* Voice controls — VoiceControls reads state and calls LiveKit SDK directly */}
@@ -473,7 +472,7 @@ function UserAreaPanel({
<div className="relative" ref={panelRef}>
{/* Input settings panel */}
{openPanel === 'input' && (
<div className="absolute bottom-full left-0 right-0 mb-0 bg-[#2b2d31] rounded-t-lg shadow-lg z-50 border-t border-x border-discord-bg-tertiary">
<div className="absolute bottom-full left-0 right-0 mb-0 bg-[#1e1f22] rounded-t-lg shadow-lg z-50 border-t border-x border-discord-bg-tertiary">
{/* Input Device */}
<div className="relative">
<button
@@ -510,7 +509,7 @@ function UserAreaPanel({
)}
</div>
<div className="mx-4 border-t border-[#3f4147]" />
<div className="mx-4 border-t border-[#2b2d31]" />
{/* Input Volume */}
<div className="px-4 py-3">
@@ -545,14 +544,14 @@ function UserAreaPanel({
<div
key={i}
className={`flex-1 h-[6px] rounded-[1px] transition-colors duration-75 ${
i < activeBars ? 'bg-discord-text-muted' : 'bg-[#3f4147]'
i < activeBars ? 'bg-discord-text-muted' : 'bg-[#313338]'
}`}
/>
))}
</div>
</div>
<div className="mx-4 border-t border-[#3f4147]" />
<div className="mx-4 border-t border-[#2b2d31]" />
{/* Voice Settings link */}
<button
@@ -569,7 +568,7 @@ function UserAreaPanel({
{/* Output settings panel */}
{openPanel === 'output' && (
<div className="absolute bottom-full left-0 right-0 mb-0 bg-[#2b2d31] rounded-t-lg shadow-lg z-50 border-t border-x border-discord-bg-tertiary">
<div className="absolute bottom-full left-0 right-0 mb-0 bg-[#1e1f22] rounded-t-lg shadow-lg z-50 border-t border-x border-discord-bg-tertiary">
{/* Output Device */}
<div className="relative">
<button
@@ -606,7 +605,7 @@ function UserAreaPanel({
)}
</div>
<div className="mx-4 border-t border-[#3f4147]" />
<div className="mx-4 border-t border-[#2b2d31]" />
{/* Output Volume */}
<div className="px-4 py-3">
@@ -635,7 +634,7 @@ function UserAreaPanel({
/>
</div>
<div className="mx-4 border-t border-[#3f4147]" />
<div className="mx-4 border-t border-[#2b2d31]" />
{/* Voice Settings link */}
<button
@@ -67,15 +67,15 @@ export function MainContent() {
// Voice/Video channel view
if (isVoiceChannel) {
const isInThisChannel = currentVoiceChannelId === currentChannelId;
// Not connected — show "Join Voice" prompt
// Not connected — show "Join Voice" prompt with gradient
if (!isInThisChannel) {
return (_jsxs("div", { className: "flex-1 flex flex-col bg-discord-bg-primary", children: [_jsx("div", { className: "h-12 px-4 flex items-center justify-between shadow-header", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", className: "text-discord-text-muted", children: _jsx("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" }) }), _jsx("span", { className: "font-bold text-discord-text-primary", children: channel.name })] }) }), _jsxs("div", { className: "flex-1 flex flex-col items-center justify-center gap-6", children: [_jsxs("div", { className: "text-center", children: [_jsx("svg", { width: "80", height: "80", viewBox: "0 0 24 24", fill: "currentColor", className: "text-discord-text-muted mx-auto mb-4 opacity-40", children: _jsx("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.46ZM19.07 4.93C20.91 6.77 22 9.28 22 12C22 14.72 20.91 17.23 19.07 19.07L17.66 17.66C19.11 16.21 20 14.21 20 12C20 9.79 19.11 7.79 17.66 6.34L19.07 4.93Z" }) }), _jsx("h2", { className: "text-[24px] font-bold text-discord-text-header mb-2", children: channel.name }), _jsx("p", { className: "text-discord-text-muted text-[14px]", children: "No one is currently in this voice channel." })] }), _jsx("button", { onClick: () => {
return (_jsxs("div", { className: "flex-1 flex flex-col bg-[#0b0c0e]", children: [_jsx("div", { className: "h-12 px-4 flex items-center justify-between shadow-header flex-shrink-0 bg-[#0b0c0e]", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", className: "text-discord-text-muted", children: _jsx("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" }) }), _jsx("span", { className: "font-bold text-discord-text-primary", children: channel.name })] }) }), _jsxs("div", { className: "flex-1 flex flex-col items-center justify-center gap-8 relative", children: [_jsx("div", { className: "absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(88,101,242,0.12)_0%,transparent_70%)] animate-gradient-pulse pointer-events-none" }), _jsxs("div", { className: "text-center relative z-10", children: [_jsx("h2", { className: "text-[28px] font-bold text-white mb-3", children: channel.name }), _jsx("p", { className: "text-discord-text-muted text-[15px]", children: "No one is currently in this voice channel." })] }), _jsx("button", { onClick: () => {
useVoiceStore.getState().setCurrentVoiceChannel(currentChannelId);
wsSend({ type: 'voice_join', channelId: currentChannelId });
}, className: "px-8 py-3 bg-discord-green hover:bg-discord-green/80 text-white font-medium rounded-[3px] transition-colors text-[14px]", children: "Join Voice" })] })] }));
}, className: "relative z-10 px-8 py-3 bg-gradient-to-r from-[#5865f2] to-[#7b6cf6] hover:brightness-110 text-white font-semibold rounded-full transition-all text-[15px] shadow-[0_4px_20px_rgba(88,101,242,0.3)]", children: "Join Voice" })] })] }));
}
// Connected — full voice view with grid + chat panel + control bar
const voiceView = (_jsxs("div", { className: `flex-1 flex flex-col bg-[#111214] min-w-0 ${voiceFullscreen ? 'fixed inset-0 z-50' : ''}`, children: [_jsx("div", { className: "h-12 px-4 flex items-center justify-between shadow-header flex-shrink-0 bg-[#111214]", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", className: "text-discord-text-muted", children: _jsx("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" }) }), _jsx("span", { className: "font-bold text-discord-text-primary", children: channel.name }), _jsx("span", { className: "text-xs text-discord-green font-medium ml-2", children: "Connected" })] }) }), _jsxs("div", { className: "flex-1 flex overflow-hidden", children: [_jsx(VoiceGrid, { participants: participants }), voiceChatOpen && (_jsx(VoiceChatPanel, { channelId: currentChannelId, channelName: channel.name }))] }), _jsx(VoiceControlBar, {})] }));
// Connected — full voice view with grid + floating control bar
const voiceView = (_jsxs("div", { className: `flex-1 flex flex-col bg-[#0b0c0e] min-w-0 ${voiceFullscreen ? 'fixed inset-0 z-50' : ''} group/voice relative`, children: [_jsx("div", { className: "h-12 px-4 flex items-center justify-between shadow-header flex-shrink-0 bg-[#0b0c0e]", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", className: "text-discord-text-muted", children: _jsx("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" }) }), _jsx("span", { className: "font-bold text-discord-text-primary", children: channel.name }), _jsx("span", { className: "text-xs text-discord-green font-medium ml-2", children: "Connected" }), _jsxs("span", { className: "text-xs text-discord-text-muted ml-1", children: [participants.length, " connected"] })] }) }), _jsxs("div", { className: "flex-1 flex overflow-hidden pb-20", children: [_jsx(VoiceGrid, { participants: participants }), voiceChatOpen && (_jsx(VoiceChatPanel, { channelId: currentChannelId, channelName: channel.name }))] }), _jsx(VoiceControlBar, {})] }));
return voiceView;
}
// Text channel view
@@ -181,11 +181,11 @@ export function MainContent() {
if (isVoiceChannel) {
const isInThisChannel = currentVoiceChannelId === currentChannelId;
// Not connected — show "Join Voice" prompt
// Not connected — show "Join Voice" prompt with gradient
if (!isInThisChannel) {
return (
<div className="flex-1 flex flex-col bg-discord-bg-primary">
<div className="h-12 px-4 flex items-center justify-between shadow-header">
<div className="flex-1 flex flex-col bg-[#0b0c0e]">
<div className="h-12 px-4 flex items-center justify-between shadow-header flex-shrink-0 bg-[#0b0c0e]">
<div className="flex items-center gap-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" className="text-discord-text-muted">
<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" />
@@ -193,20 +193,19 @@ export function MainContent() {
<span className="font-bold text-discord-text-primary">{channel.name}</span>
</div>
</div>
<div className="flex-1 flex flex-col items-center justify-center gap-6">
<div className="text-center">
<svg width="80" height="80" viewBox="0 0 24 24" fill="currentColor" className="text-discord-text-muted mx-auto mb-4 opacity-40">
<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.46ZM19.07 4.93C20.91 6.77 22 9.28 22 12C22 14.72 20.91 17.23 19.07 19.07L17.66 17.66C19.11 16.21 20 14.21 20 12C20 9.79 19.11 7.79 17.66 6.34L19.07 4.93Z" />
</svg>
<h2 className="text-[24px] font-bold text-discord-text-header mb-2">{channel.name}</h2>
<p className="text-discord-text-muted text-[14px]">No one is currently in this voice channel.</p>
<div className="flex-1 flex flex-col items-center justify-center gap-8 relative">
{/* Radial gradient glow */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(88,101,242,0.12)_0%,transparent_70%)] animate-gradient-pulse pointer-events-none" />
<div className="text-center relative z-10">
<h2 className="text-[28px] font-bold text-white mb-3">{channel.name}</h2>
<p className="text-discord-text-muted text-[15px]">No one is currently in this voice channel.</p>
</div>
<button
onClick={() => {
useVoiceStore.getState().setCurrentVoiceChannel(currentChannelId);
wsSend({ type: 'voice_join', channelId: currentChannelId });
}}
className="px-8 py-3 bg-discord-green hover:bg-discord-green/80 text-white font-medium rounded-[3px] transition-colors text-[14px]"
className="relative z-10 px-8 py-3 bg-gradient-to-r from-[#5865f2] to-[#7b6cf6] hover:brightness-110 text-white font-semibold rounded-full transition-all text-[15px] shadow-[0_4px_20px_rgba(88,101,242,0.3)]"
>
Join Voice
</button>
@@ -215,29 +214,30 @@ export function MainContent() {
);
}
// Connected — full voice view with grid + chat panel + control bar
// Connected — full voice view with grid + floating control bar
const voiceView = (
<div className={`flex-1 flex flex-col bg-[#111214] min-w-0 ${voiceFullscreen ? 'fixed inset-0 z-50' : ''}`}>
<div className={`flex-1 flex flex-col bg-[#0b0c0e] min-w-0 ${voiceFullscreen ? 'fixed inset-0 z-50' : ''} group/voice relative`}>
{/* Voice header */}
<div className="h-12 px-4 flex items-center justify-between shadow-header flex-shrink-0 bg-[#111214]">
<div className="h-12 px-4 flex items-center justify-between shadow-header flex-shrink-0 bg-[#0b0c0e]">
<div className="flex items-center gap-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" className="text-discord-text-muted">
<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>
<span className="font-bold text-discord-text-primary">{channel.name}</span>
<span className="text-xs text-discord-green font-medium ml-2">Connected</span>
<span className="text-xs text-discord-text-muted ml-1">{participants.length} connected</span>
</div>
</div>
{/* Main content: grid + optional chat */}
<div className="flex-1 flex overflow-hidden">
<div className="flex-1 flex overflow-hidden pb-20">
<VoiceGrid participants={participants} />
{voiceChatOpen && (
<VoiceChatPanel channelId={currentChannelId} channelName={channel.name} />
)}
</div>
{/* Control bar at bottom */}
{/* Floating control bar */}
<VoiceControlBar />
</div>
);