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:
@@ -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">
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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]!;
|
||||
}
|
||||
Reference in New Issue
Block a user