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
+1 -1
View File
@@ -10,7 +10,7 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<title>Backspace</title> <title>Backspace</title>
</head> </head>
<body class="bg-discord-bg-primary text-discord-text-primary"> <body class="bg-surface-base text-txt-primary">
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
@@ -4,6 +4,7 @@ import { useServerStore } from '../../stores/serverStore';
import { useChatStore } from '../../stores/chatStore'; import { useChatStore } from '../../stores/chatStore';
import { useUIStore } from '../../stores/uiStore'; import { useUIStore } from '../../stores/uiStore';
import { Tooltip } from '../ui/Tooltip'; import { Tooltip } from '../ui/Tooltip';
import { getServerGradient, HOME_GRADIENT } from '../../utils/gradients';
interface SidebarItemProps { interface SidebarItemProps {
id: string; id: string;
@@ -16,7 +17,7 @@ interface SidebarItemProps {
hasUnread?: boolean; 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 [isHovered, setIsHovered] = useState(false);
const firstLetter = name.charAt(0).toUpperCase(); const firstLetter = name.charAt(0).toUpperCase();
@@ -27,18 +28,42 @@ function SidebarItem({ name, icon, active, onClick, type = 'server', actionType,
return 'h-2 scale-0'; 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 getButtonClasses = () => {
const base = 'w-12 h-12 flex items-center justify-center transition-all duration-200 overflow-hidden relative group'; const base = 'w-12 h-12 flex items-center justify-center transition-all duration-200 overflow-hidden relative group';
if (type === 'dm') { 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') { 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} 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 ( return (
@@ -57,11 +82,9 @@ function SidebarItem({ name, icon, active, onClick, type = 'server', actionType,
)} )}
<Tooltip content={name} position="right"> <Tooltip content={name} position="right">
<button onClick={onClick} className={getButtonClasses()}> <button onClick={onClick} className={getButtonClasses()} style={backgroundStyle}>
{type === 'dm' ? ( {type === 'dm' ? (
<svg width="28" height="20" viewBox="0 0 28 20" fill="currentColor"> <span className="text-[20px] font-bold">B</span>
<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>
) : type === 'action' ? ( ) : type === 'action' ? (
actionType === 'add' ? ( actionType === 'add' ? (
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> <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 React from 'react';
import type { User } from '@backspace/shared'; import type { User } from '@backspace/shared';
import { useUIStore } from '../../stores/uiStore'; import { useUIStore } from '../../stores/uiStore';
import { getAvatarGradient } from '../../utils/gradients';
interface AvatarProps { interface AvatarProps {
src?: string | null; src?: string | null;
@@ -23,6 +24,7 @@ export function Avatar({ src, name, size = 40, status, className = '', onClick,
const openUserProfile = useUIStore((s) => s.openUserProfile); const openUserProfile = useUIStore((s) => s.openUserProfile);
const initials = name.charAt(0).toUpperCase(); const initials = name.charAt(0).toUpperCase();
const fontSize = size < 32 ? 'text-xs' : size < 48 ? 'text-sm' : 'text-lg'; const fontSize = size < 32 ? 'text-xs' : size < 48 ? 'text-sm' : 'text-lg';
const gradient = getAvatarGradient(user?.id, name);
const handleClick = (e: React.MouseEvent) => { const handleClick = (e: React.MouseEvent) => {
if (onClick) { if (onClick) {
@@ -59,8 +61,8 @@ export function Avatar({ src, name, size = 40, status, className = '', onClick,
/> />
) : null} ) : null}
<div <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'}`} 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' } : undefined} style={src ? { display: 'none' } : { background: gradient.gradient }}
> >
{initials} {initials}
</div> </div>
@@ -5,6 +5,7 @@ import { Avatar } from '../ui/Avatar';
import { api } from '../../api/client'; import { api } from '../../api/client';
import { useServerStore } from '../../stores/serverStore'; import { useServerStore } from '../../stores/serverStore';
import { useUIStore } from '../../stores/uiStore'; import { useUIStore } from '../../stores/uiStore';
import { getAvatarGradient } from '../../utils/gradients';
interface UserProfilePopoutProps { interface UserProfilePopoutProps {
user: User; user: User;
@@ -35,7 +36,7 @@ export function UserProfilePopout({ user, onClose, position }: UserProfilePopout
style={position ? { top: position.top, left: position.left } : { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }} style={position ? { top: position.top, left: position.left } : { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
> >
{/* Banner */} {/* Banner */}
<div className="h-[60px] bg-accent-primary" /> <div className="h-[60px]" style={{ background: getAvatarGradient(user.id, displayName).gradient }} />
{/* Avatar Container */} {/* Avatar Container */}
<div className="px-4 pb-4 relative"> <div className="px-4 pb-4 relative">
@@ -45,6 +46,7 @@ export function UserProfilePopout({ user, onClose, position }: UserProfilePopout
name={displayName} name={displayName}
size={80} size={80}
status={user.status as any} status={user.status as any}
user={user}
/> />
</div> </div>
@@ -1,6 +1,7 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { useVoiceStore } from '../../stores/voiceStore'; import { useVoiceStore } from '../../stores/voiceStore';
import { wsSend } from '../../hooks/useWebSocket'; import { wsSend } from '../../hooks/useWebSocket';
import { getAvatarGradient } from '../../utils/gradients';
export function IncomingCallModal() { export function IncomingCallModal() {
const incomingCall = useVoiceStore((s) => s.incomingCall); 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"> <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-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()} {incomingCall.callerName.charAt(0).toUpperCase()}
</div> </div>
{/* Ringing phone icon */} {/* Ringing phone icon */}
+58
View File
@@ -0,0 +1,58 @@
/**
* Aether Drift gradient palettes for avatars and server icons.
* Deterministic color assignment based on entity ID (snowflake) or name.
*/
export interface GradientEntry {
gradient: string;
glow: string;
}
// ── Avatar gradients (user fallbacks) ──
const AVATAR_GRADIENTS: GradientEntry[] = [
{ gradient: 'linear-gradient(135deg, #059669, #10b981)', glow: '#10b981' }, // mint
{ gradient: 'linear-gradient(135deg, #3b82f6, #6366f1)', glow: '#6366f1' }, // sky
{ gradient: 'linear-gradient(135deg, #a78bfa, #c084fc)', glow: '#c084fc' }, // lavender
{ gradient: 'linear-gradient(135deg, #f97316, #ef4444)', glow: '#f97316' }, // coral
{ gradient: 'linear-gradient(135deg, #f43f5e, #ec4899)', glow: '#ec4899' }, // rose
{ gradient: 'linear-gradient(135deg, #14b8a6, #06b6d4)', glow: '#06b6d4' }, // teal
{ gradient: 'linear-gradient(135deg, #f59e0b, #eab308)', glow: '#f59e0b' }, // amber
];
// ── Server icon gradients (server fallbacks) ──
const SERVER_GRADIENTS: GradientEntry[] = [
{ gradient: 'linear-gradient(135deg, #ef4444, #f97316)', glow: '#f97316' }, // red-orange
{ gradient: 'linear-gradient(135deg, #ec4899, #f472b6)', glow: '#ec4899' }, // pink
{ gradient: 'linear-gradient(135deg, #14b8a6, #06b6d4)', glow: '#06b6d4' }, // teal-cyan
{ gradient: 'linear-gradient(135deg, #f59e0b, #eab308)', glow: '#f59e0b' }, // amber-yellow
{ gradient: 'linear-gradient(135deg, #3b82f6, #6366f1)', glow: '#6366f1' }, // blue-indigo
{ gradient: 'linear-gradient(135deg, #a78bfa, #c084fc)', glow: '#c084fc' }, // lavender
{ gradient: 'linear-gradient(135deg, #059669, #10b981)', glow: '#10b981' }, // mint
];
// ── Home button (DM / Backspace) — fixed indigo-purple gradient ──
export const HOME_GRADIENT: GradientEntry = {
gradient: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
glow: '#8b5cf6',
};
/** djb2 hash → stable unsigned 32-bit integer. */
function hashString(str: string): number {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0;
}
return hash;
}
/** Deterministic gradient for a user avatar. Prefers ID for stability; falls back to name. */
export function getAvatarGradient(id?: string | null, name?: string): GradientEntry {
const key = id || name || 'unknown';
return AVATAR_GRADIENTS[hashString(key) % AVATAR_GRADIENTS.length]!;
}
/** Deterministic gradient for a server icon. Prefers ID for stability; falls back to name. */
export function getServerGradient(id?: string | null, name?: string): GradientEntry {
const key = id || name || 'unknown';
return SERVER_GRADIENTS[hashString(key) % SERVER_GRADIENTS.length]!;
}
-55
View File
@@ -7,61 +7,6 @@ export default {
theme: { theme: {
extend: { extend: {
colors: { colors: {
// ── LEGACY (preserved until Phase 5 removal) ──
discord: {
// Backgrounds (darkest → lightest) — Discord "Dark" theme
'bg-floating': '#080a0b',
'bg-tertiary': '#111214',
'bg-server': '#111214',
'bg-user-area': '#1a1b1e',
'bg-secondary': '#1e1f22',
'bg-members': '#1e1f22',
'bg-home': '#1a1d20',
'bg-hover': '#232428',
'bg-primary': '#232428',
'bg-input': '#2b2d31',
'bg-surface': '#2b2d31',
'bg-surface-higher': '#313338',
'bg-active': '#313338',
'bg-accent': '#353840',
'bg-overlay': 'rgba(0, 0, 0, 0.90)',
// Text
'text-primary': '#ffffff',
'text-normal': '#dcdcdf',
'text-secondary': '#c5c6ca',
'text-muted': '#abacb2',
'text-header': '#ffffff',
'text-link': '#76aff6',
'text-positive': '#73c48b',
'text-warning': '#faa900',
'text-danger': '#ff938e',
// Channel/Interactive
'channels-default': '#999aa1',
'interactive-muted': '#4e5058',
// Brand
'blurple': '#5865f2',
'blurple-hover': '#4452bb',
'blurple-active': '#3a48a3',
// Status
'green': '#23a55a',
'yellow': '#f0b232',
'red': '#f23f43',
'red-hover': '#a9232e',
// Notification badge
'notification': '#da3e44',
// Modifiers (semi-transparent — works on any background)
'modifier-hover': 'rgba(255,255,255,0.08)',
'modifier-active': 'rgba(255,255,255,0.16)',
'modifier-selected': 'rgba(255,255,255,0.20)',
'modifier-accent': 'rgba(255,255,255,0.12)',
},
// ── AETHER DRIFT TOKENS ── // ── AETHER DRIFT TOKENS ──
surface: { surface: {
base: 'var(--bg-base)', base: 'var(--bg-base)',