refactor: unify settings into tabbed modal with UI polish

Split monolithic UserSettings into AccountPanel, VoicePanel, ConnectionsPanel,
and InstancePanel tabs. Remove standalone InstanceSettings modal. Add reusable
Toggle component fixing size deformation, color inconsistency (green→purple),
and flex-shrink issues. Fix custom status clearing by always sending the field
to the server. Wrap Log Out button in glass bubble. Add subtle card depth with
borders.
This commit is contained in:
Jannis Braun
2026-03-10 03:57:01 +01:00
parent 3d37c70e50
commit 4d9454382c
13 changed files with 344 additions and 285 deletions
@@ -128,11 +128,9 @@ export function ChannelSidebar() {
isDeafened={isDeafened}
isServerMuted={isServerMuted}
isServerDeafened={isServerDeafened}
isAdmin={!!user.isAdmin}
onMicToggle={handleMicToggle}
onDeafenToggle={handleDeafenToggle}
onSettingsClick={() => openModal('userSettings')}
onAdminClick={() => openModal('instanceSettings')}
onSettingsClick={(tab) => openModal('userSettings', tab ? { tab } : {})}
/>
</div>
</div>
@@ -459,22 +457,18 @@ function UserAreaPanel({
isDeafened,
isServerMuted,
isServerDeafened,
isAdmin,
onMicToggle,
onDeafenToggle,
onSettingsClick,
onAdminClick,
}: {
user: any;
isMuted: boolean;
isDeafened: boolean;
isServerMuted: boolean;
isServerDeafened: boolean;
isAdmin: boolean;
onMicToggle: () => void;
onDeafenToggle: () => void;
onSettingsClick: () => void;
onAdminClick: () => void;
onSettingsClick: (tab?: string) => void;
}) {
const [openPanel, setOpenPanel] = useState<'input' | 'output' | null>(null);
const [inputDevices, setInputDevices] = useState<MediaDeviceInfo[]>([]);
@@ -684,7 +678,7 @@ function UserAreaPanel({
{/* Voice Settings link */}
<button
onClick={onSettingsClick}
onClick={() => onSettingsClick('voice')}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-interactive-hover transition-colors"
>
<span className="text-[15px] font-semibold text-txt-primary">Voice Settings</span>
@@ -758,7 +752,7 @@ function UserAreaPanel({
{/* Voice Settings link */}
<button
onClick={onSettingsClick}
onClick={() => onSettingsClick('voice')}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-interactive-hover transition-colors"
>
<span className="text-[15px] font-semibold text-txt-primary">Voice Settings</span>
@@ -837,24 +831,11 @@ function UserAreaPanel({
</svg>
</button>
{/* Admin — Instance Settings */}
{isAdmin && (
<button
onClick={onAdminClick}
className="w-8 h-8 flex items-center justify-center text-txt-tertiary hover:text-accent-amber hover:bg-interactive-hover rounded-[4px] transition-colors"
title="Instance Settings"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z" />
</svg>
</button>
)}
{/* Settings */}
<button
onClick={onSettingsClick}
onClick={() => onSettingsClick()}
className="w-8 h-8 flex items-center justify-center text-txt-tertiary hover:text-txt-primary hover:bg-interactive-hover rounded-[4px] transition-colors"
title="User Settings"
title="Settings"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" />