feat: Phase 5+6 Aether Drift — legacy token removal, colorful avatars & Backspace branding

Phase 5: Remove all 54 legacy Discord token definitions from tailwind.config.js,
migrate body classes in index.html to Aether Drift tokens, update ARCHITECTURE_AUDIT.md.

Phase 6: Replace flat purple fallback avatars with deterministic colorful gradients
(mint, sky, lavender, coral, rose, teal, amber) based on user/server ID hash.
Replace Discord logo SVG with Backspace "B" in server strip. Add per-server gradient
icons with hover glow. Update profile popout banner and incoming call modal to match.
This commit is contained in:
Jannis Braun
2026-03-02 00:56:34 +01:00
parent d0e696e03c
commit 7f7656ed24
7 changed files with 106 additions and 75 deletions
@@ -4,6 +4,7 @@ import { useServerStore } from '../../stores/serverStore';
import { useChatStore } from '../../stores/chatStore';
import { useUIStore } from '../../stores/uiStore';
import { Tooltip } from '../ui/Tooltip';
import { getServerGradient, HOME_GRADIENT } from '../../utils/gradients';
interface SidebarItemProps {
id: string;
@@ -16,7 +17,7 @@ interface SidebarItemProps {
hasUnread?: boolean;
}
function SidebarItem({ name, icon, active, onClick, type = 'server', actionType, hasUnread }: SidebarItemProps) {
function SidebarItem({ id, name, icon, active, onClick, type = 'server', actionType, hasUnread }: SidebarItemProps) {
const [isHovered, setIsHovered] = useState(false);
const firstLetter = name.charAt(0).toUpperCase();
@@ -27,22 +28,46 @@ function SidebarItem({ name, icon, active, onClick, type = 'server', actionType,
return 'h-2 scale-0';
};
const backgroundStyle = useMemo((): React.CSSProperties | undefined => {
if (type === 'action') return undefined;
if (type === 'dm') {
return {
background: HOME_GRADIENT.gradient,
...(isHovered ? { boxShadow: `0 0 12px ${HOME_GRADIENT.glow}40` } : {}),
};
}
// Server type — if it has a custom icon image, no gradient needed
if (icon) return undefined;
const serverGrad = getServerGradient(id, name);
return {
background: serverGrad.gradient,
...(isHovered ? { boxShadow: `0 0 12px ${serverGrad.glow}40` } : {}),
};
}, [type, id, name, icon, active, isHovered]);
const getButtonClasses = () => {
const base = 'w-12 h-12 flex items-center justify-center transition-all duration-200 overflow-hidden relative group';
if (type === 'dm') {
return `${base} ${active ? 'bg-accent-primary rounded-[16px] text-white' : 'bg-surface-chat rounded-[24px] hover:rounded-[16px] text-txt-primary hover:bg-accent-primary-hover hover:text-white'}`;
return `${base} text-white ${active ? 'rounded-[16px]' : 'rounded-[24px] hover:rounded-[16px]'}`;
}
if (type === 'action') {
return `${base} bg-surface-chat rounded-[24px] hover:rounded-[16px] text-status-online hover:bg-status-online hover:text-white`;
}
return `${base} ${active ? 'bg-accent-primary rounded-[16px] text-white' : 'bg-surface-chat rounded-[24px] hover:rounded-[16px] text-txt-primary hover:bg-accent-primary-hover hover:text-white'}`;
if (icon) {
return `${base} ${active ? 'rounded-[16px]' : 'rounded-[24px] hover:rounded-[16px]'}`;
}
return `${base} text-white ${active ? 'rounded-[16px]' : 'rounded-[24px] hover:rounded-[16px]'}`;
};
return (
<div
<div
className="relative flex items-center mb-2 w-full justify-center"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
@@ -50,18 +75,16 @@ function SidebarItem({ name, icon, active, onClick, type = 'server', actionType,
{/* Pill Indicator */}
{(type === 'server' || type === 'dm') && (
<div className="absolute -left-0 w-2 h-12 flex items-center">
<div
<div
className={`bg-white rounded-r-full transition-all duration-200 origin-left ${getPillHeight()} w-1`}
/>
</div>
)}
<Tooltip content={name} position="right">
<button onClick={onClick} className={getButtonClasses()}>
<button onClick={onClick} className={getButtonClasses()} style={backgroundStyle}>
{type === 'dm' ? (
<svg width="28" height="20" viewBox="0 0 28 20" fill="currentColor">
<path d="M23.0212 1.67671C21.3107 0.879656 19.5079 0.318797 17.6584 0C17.4062 0.461742 17.1749 0.934541 16.9708 1.4184C15.003 1.12145 12.9974 1.12145 11.0292 1.4184C10.8251 0.934541 10.5938 0.461742 10.3416 0C8.49215 0.318797 6.68934 0.879656 4.97882 1.67671C0.665804 8.44726 -0.364554 15.0614 0.225316 21.5765C2.41849 23.2105 4.70543 24.3115 7.04773 25.043C7.60419 24.2941 8.09868 23.4944 8.52321 22.6521C7.71966 22.3602 6.9466 21.9905 6.21274 21.5543C6.39845 21.4212 6.58011 21.2838 6.75775 21.1429C12.7568 23.8968 19.2811 23.8968 25.2422 21.1429C25.4199 21.2838 25.6015 21.4212 25.7873 21.5543C25.0534 21.9905 24.2804 22.3602 23.4768 22.6521C23.9013 23.4944 24.3958 24.2941 24.9523 25.043C27.2946 24.3115 29.5815 23.2105 31.7747 21.5765C32.4517 14.0051 30.5663 7.45459 26.0212 1.67671H23.0212Z" transform="scale(0.85) translate(0, 0)" />
</svg>
<span className="text-[20px] font-bold">B</span>
) : type === 'action' ? (
actionType === 'add' ? (
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
+4 -2
View File
@@ -1,6 +1,7 @@
import React from 'react';
import type { User } from '@backspace/shared';
import { useUIStore } from '../../stores/uiStore';
import { getAvatarGradient } from '../../utils/gradients';
interface AvatarProps {
src?: string | null;
@@ -23,6 +24,7 @@ export function Avatar({ src, name, size = 40, status, className = '', onClick,
const openUserProfile = useUIStore((s) => s.openUserProfile);
const initials = name.charAt(0).toUpperCase();
const fontSize = size < 32 ? 'text-xs' : size < 48 ? 'text-sm' : 'text-lg';
const gradient = getAvatarGradient(user?.id, name);
const handleClick = (e: React.MouseEvent) => {
if (onClick) {
@@ -59,8 +61,8 @@ export function Avatar({ src, name, size = 40, status, className = '', onClick,
/>
) : null}
<div
className={`avatar-fallback w-full h-full rounded-full bg-accent-primary flex items-center justify-center ${fontSize} font-semibold text-white ${src ? 'hidden' : 'flex'}`}
style={src ? { display: 'none' } : undefined}
className={`avatar-fallback w-full h-full rounded-full flex items-center justify-center ${fontSize} font-semibold text-white ${src ? 'hidden' : 'flex'}`}
style={src ? { display: 'none' } : { background: gradient.gradient }}
>
{initials}
</div>
@@ -5,6 +5,7 @@ import { Avatar } from '../ui/Avatar';
import { api } from '../../api/client';
import { useServerStore } from '../../stores/serverStore';
import { useUIStore } from '../../stores/uiStore';
import { getAvatarGradient } from '../../utils/gradients';
interface UserProfilePopoutProps {
user: User;
@@ -35,16 +36,17 @@ export function UserProfilePopout({ user, onClose, position }: UserProfilePopout
style={position ? { top: position.top, left: position.left } : { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
>
{/* Banner */}
<div className="h-[60px] bg-accent-primary" />
<div className="h-[60px]" style={{ background: getAvatarGradient(user.id, displayName).gradient }} />
{/* Avatar Container */}
<div className="px-4 pb-4 relative">
<div className="absolute -top-8 left-4 rounded-full border-[6px] border-surface-elevated bg-surface-elevated">
<Avatar
src={user.avatar}
name={displayName}
size={80}
status={user.status as any}
<Avatar
src={user.avatar}
name={displayName}
size={80}
status={user.status as any}
user={user}
/>
</div>
@@ -1,6 +1,7 @@
import React, { useEffect, useRef } from 'react';
import { useVoiceStore } from '../../stores/voiceStore';
import { wsSend } from '../../hooks/useWebSocket';
import { getAvatarGradient } from '../../utils/gradients';
export function IncomingCallModal() {
const incomingCall = useVoiceStore((s) => s.incomingCall);
@@ -54,7 +55,7 @@ export function IncomingCallModal() {
<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-accent-primary flex items-center justify-center text-white text-3xl font-bold">
<div className="w-20 h-20 rounded-full flex items-center justify-center text-white text-3xl font-bold" style={{ background: getAvatarGradient(incomingCall.callerId, incomingCall.callerName).gradient }}>
{incomingCall.callerName.charAt(0).toUpperCase()}
</div>
{/* Ringing phone icon */}