fix: banner/avatar overlap in settings preview and refine profile color system
Move bg-surface-channel from info section to card wrapper in AccountPanel so the avatar's negative margin overlaps into the banner correctly. Replace accent color presets with 7×3 banner color palette, remove accent color tinting from display names in popout/modal, and refactor gradient entries to expose from/to hex stops.
This commit is contained in:
@@ -251,7 +251,7 @@ export function UserProfileModal() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Header (avatar + name) */}
|
{/* Header (avatar + name) */}
|
||||||
<div className="px-5 flex-shrink-0">
|
<div className="px-5 flex-shrink-0 relative">
|
||||||
<div
|
<div
|
||||||
className="mt-[-48px] mb-2 w-fit rounded-full"
|
className="mt-[-48px] mb-2 w-fit rounded-full"
|
||||||
style={{ border: '4px solid var(--color-surface-elevated, #1e1e2a)' }}
|
style={{ border: '4px solid var(--color-surface-elevated, #1e1e2a)' }}
|
||||||
@@ -269,7 +269,6 @@ export function UserProfileModal() {
|
|||||||
<Username
|
<Username
|
||||||
username={displayName}
|
username={displayName}
|
||||||
className="text-[20px] font-bold leading-tight"
|
className="text-[20px] font-bold leading-tight"
|
||||||
style={user.accentColor ? { color: user.accentColor } : undefined}
|
|
||||||
/>
|
/>
|
||||||
<div className="text-[14px] text-txt-tertiary mt-0.5">
|
<div className="text-[14px] text-txt-tertiary mt-0.5">
|
||||||
{domain ? (
|
{domain ? (
|
||||||
@@ -346,23 +345,6 @@ export function UserProfileModal() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Accent color */}
|
|
||||||
{user.accentColor && (
|
|
||||||
<div>
|
|
||||||
<span className="text-[11px] uppercase tracking-wide font-semibold text-txt-tertiary">
|
|
||||||
Accent Color
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center gap-2 mt-1">
|
|
||||||
<div
|
|
||||||
className="w-5 h-5 rounded-full border border-white/10"
|
|
||||||
style={{ backgroundColor: user.accentColor }}
|
|
||||||
/>
|
|
||||||
<span className="text-[12px] text-txt-tertiary font-mono">
|
|
||||||
{user.accentColor}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useAuthStore } from '../../../stores/authStore';
|
|||||||
import { Avatar } from '../../ui/Avatar';
|
import { Avatar } from '../../ui/Avatar';
|
||||||
import { ImageCropModal } from '../../ui/ImageCropModal';
|
import { ImageCropModal } from '../../ui/ImageCropModal';
|
||||||
import { api } from '../../../api/client';
|
import { api } from '../../../api/client';
|
||||||
import { getAvatarGradient, adjustColor, AVATAR_GRADIENT_MAP, ACCENT_PRESETS } from '../../../utils/gradients';
|
import { getAvatarGradient, adjustColor, AVATAR_GRADIENT_MAP, BANNER_COLOR_PRESETS } from '../../../utils/gradients';
|
||||||
import { AVATAR_COLORS } from '@backspace/shared';
|
import { AVATAR_COLORS } from '@backspace/shared';
|
||||||
import type { User, UserStatus, AvatarColor } from '@backspace/shared';
|
import type { User, UserStatus, AvatarColor } from '@backspace/shared';
|
||||||
|
|
||||||
@@ -212,17 +212,17 @@ export function AccountPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Live Preview Card */}
|
{/* Live Preview Card */}
|
||||||
<div className="rounded-lg overflow-hidden border border-white/[0.06] mb-4">
|
<div className="rounded-lg overflow-hidden border border-white/[0.06] mb-4 bg-surface-channel">
|
||||||
{/* Banner area */}
|
{/* Banner area */}
|
||||||
<div
|
<div
|
||||||
className="h-[80px] relative"
|
className="h-[80px]"
|
||||||
style={displayBannerSrc
|
style={displayBannerSrc
|
||||||
? { backgroundImage: `url(${displayBannerSrc})`, backgroundSize: 'cover', backgroundPosition: 'center' }
|
? { backgroundImage: `url(${displayBannerSrc})`, backgroundSize: 'cover', backgroundPosition: 'center' }
|
||||||
: { background: bannerFallback, opacity: 0.6 }
|
: { background: bannerFallback, opacity: 0.6 }
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{/* Avatar + info */}
|
{/* Avatar + info */}
|
||||||
<div className="px-4 pb-3 bg-surface-channel">
|
<div className="px-4 pb-3">
|
||||||
<div
|
<div
|
||||||
className="mt-[-28px] mb-2 w-fit rounded-full"
|
className="mt-[-28px] mb-2 w-fit rounded-full"
|
||||||
style={{ border: '4px solid var(--color-surface-channel, #1e1e2a)' }}
|
style={{ border: '4px solid var(--color-surface-channel, #1e1e2a)' }}
|
||||||
@@ -244,8 +244,7 @@ export function AccountPanel() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="font-semibold text-[15px] leading-tight"
|
className="font-semibold text-[15px] leading-tight text-txt-primary"
|
||||||
style={{ color: effectiveAccent ?? 'var(--color-txt-primary)' }}
|
|
||||||
>
|
>
|
||||||
{effectiveDisplayName}
|
{effectiveDisplayName}
|
||||||
</div>
|
</div>
|
||||||
@@ -410,11 +409,14 @@ export function AccountPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Accent Color */}
|
{/* Banner Color */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-txt-secondary mb-1.5">Accent Color</label>
|
<label className="block text-xs text-txt-secondary mb-1.5">Banner Color</label>
|
||||||
<div className="grid grid-cols-7 gap-1.5 mb-2">
|
<div className="grid grid-cols-7 gap-1.5 mb-2">
|
||||||
{ACCENT_PRESETS.map((color) => (
|
{[0, 1, 2].map((row) =>
|
||||||
|
BANNER_COLOR_PRESETS.map((family) => {
|
||||||
|
const color = family[row]!;
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={color}
|
key={color}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -427,7 +429,9 @@ export function AccountPanel() {
|
|||||||
}}
|
}}
|
||||||
title={color}
|
title={color}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export function UserProfilePopout({ user, onClose, position }: UserProfilePopout
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div className="px-4 pb-4">
|
<div className="px-4 pb-4 relative">
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<div
|
<div
|
||||||
className="mt-[-40px] mb-3 w-fit rounded-full"
|
className="mt-[-40px] mb-3 w-fit rounded-full"
|
||||||
@@ -117,7 +117,6 @@ export function UserProfilePopout({ user, onClose, position }: UserProfilePopout
|
|||||||
<Username
|
<Username
|
||||||
username={user.displayName ?? baseName}
|
username={user.displayName ?? baseName}
|
||||||
className="text-[16px] font-semibold leading-tight"
|
className="text-[16px] font-semibold leading-tight"
|
||||||
style={user.accentColor ? { color: user.accentColor } : undefined}
|
|
||||||
/>
|
/>
|
||||||
<div className="text-[13px] text-txt-tertiary">
|
<div className="text-[13px] text-txt-tertiary">
|
||||||
{domain ? (
|
{domain ? (
|
||||||
|
|||||||
@@ -6,19 +6,25 @@
|
|||||||
import type { AvatarColor } from '@backspace/shared';
|
import type { AvatarColor } from '@backspace/shared';
|
||||||
|
|
||||||
export interface GradientEntry {
|
export interface GradientEntry {
|
||||||
gradient: string;
|
from: string; // hex start color
|
||||||
glow: string;
|
to: string; // hex end color
|
||||||
|
gradient: string; // CSS linear-gradient (derived from from/to)
|
||||||
|
glow: string; // hex glow color (hand-curated most prominent color)
|
||||||
|
}
|
||||||
|
|
||||||
|
function grad(from: string, to: string, glow: string): GradientEntry {
|
||||||
|
return { from, to, gradient: `linear-gradient(135deg, ${from}, ${to})`, glow };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Avatar gradients (user fallbacks) ──
|
// ── Avatar gradients (user fallbacks) ──
|
||||||
const AVATAR_GRADIENTS: GradientEntry[] = [
|
const AVATAR_GRADIENTS: GradientEntry[] = [
|
||||||
{ gradient: 'linear-gradient(135deg, #059669, #10b981)', glow: '#10b981' }, // mint
|
grad('#059669', '#10b981', '#10b981'), // mint
|
||||||
{ gradient: 'linear-gradient(135deg, #3b82f6, #6366f1)', glow: '#6366f1' }, // sky
|
grad('#3b82f6', '#6366f1', '#6366f1'), // sky
|
||||||
{ gradient: 'linear-gradient(135deg, #a78bfa, #c084fc)', glow: '#c084fc' }, // lavender
|
grad('#a78bfa', '#c084fc', '#c084fc'), // lavender
|
||||||
{ gradient: 'linear-gradient(135deg, #f97316, #ef4444)', glow: '#f97316' }, // coral
|
grad('#f97316', '#ef4444', '#f97316'), // coral
|
||||||
{ gradient: 'linear-gradient(135deg, #f43f5e, #ec4899)', glow: '#ec4899' }, // rose
|
grad('#f43f5e', '#ec4899', '#ec4899'), // rose
|
||||||
{ gradient: 'linear-gradient(135deg, #14b8a6, #06b6d4)', glow: '#06b6d4' }, // teal
|
grad('#14b8a6', '#06b6d4', '#06b6d4'), // teal
|
||||||
{ gradient: 'linear-gradient(135deg, #f59e0b, #eab308)', glow: '#f59e0b' }, // amber
|
grad('#f59e0b', '#eab308', '#f59e0b'), // amber
|
||||||
];
|
];
|
||||||
|
|
||||||
export const AVATAR_GRADIENT_MAP: Record<AvatarColor, GradientEntry> = {
|
export const AVATAR_GRADIENT_MAP: Record<AvatarColor, GradientEntry> = {
|
||||||
@@ -33,20 +39,17 @@ export const AVATAR_GRADIENT_MAP: Record<AvatarColor, GradientEntry> = {
|
|||||||
|
|
||||||
// ── Space icon gradients (space fallbacks) ──
|
// ── Space icon gradients (space fallbacks) ──
|
||||||
const SPACE_GRADIENTS: GradientEntry[] = [
|
const SPACE_GRADIENTS: GradientEntry[] = [
|
||||||
{ gradient: 'linear-gradient(135deg, #ef4444, #f97316)', glow: '#f97316' }, // red-orange
|
grad('#ef4444', '#f97316', '#f97316'), // red-orange
|
||||||
{ gradient: 'linear-gradient(135deg, #ec4899, #f472b6)', glow: '#ec4899' }, // pink
|
grad('#ec4899', '#f472b6', '#ec4899'), // pink
|
||||||
{ gradient: 'linear-gradient(135deg, #14b8a6, #06b6d4)', glow: '#06b6d4' }, // teal-cyan
|
grad('#14b8a6', '#06b6d4', '#06b6d4'), // teal-cyan
|
||||||
{ gradient: 'linear-gradient(135deg, #f59e0b, #eab308)', glow: '#f59e0b' }, // amber-yellow
|
grad('#f59e0b', '#eab308', '#f59e0b'), // amber-yellow
|
||||||
{ gradient: 'linear-gradient(135deg, #3b82f6, #6366f1)', glow: '#6366f1' }, // blue-indigo
|
grad('#3b82f6', '#6366f1', '#6366f1'), // blue-indigo
|
||||||
{ gradient: 'linear-gradient(135deg, #a78bfa, #c084fc)', glow: '#c084fc' }, // lavender
|
grad('#a78bfa', '#c084fc', '#c084fc'), // lavender
|
||||||
{ gradient: 'linear-gradient(135deg, #059669, #10b981)', glow: '#10b981' }, // mint
|
grad('#059669', '#10b981', '#10b981'), // mint
|
||||||
];
|
];
|
||||||
|
|
||||||
// ── Home button (DM / Backspace) — fixed indigo-purple gradient ──
|
// ── Home button (DM / Backspace) — fixed indigo-purple gradient ──
|
||||||
export const HOME_GRADIENT: GradientEntry = {
|
export const HOME_GRADIENT: GradientEntry = grad('#6366f1', '#8b5cf6', '#8b5cf6');
|
||||||
gradient: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
|
||||||
glow: '#8b5cf6',
|
|
||||||
};
|
|
||||||
|
|
||||||
/** djb2 hash → stable unsigned 32-bit integer. */
|
/** djb2 hash → stable unsigned 32-bit integer. */
|
||||||
function hashString(str: string): number {
|
function hashString(str: string): number {
|
||||||
@@ -72,11 +75,19 @@ export function getSpaceGradient(id?: string | null, name?: string): GradientEnt
|
|||||||
return SPACE_GRADIENTS[hashString(key) % SPACE_GRADIENTS.length]!;
|
return SPACE_GRADIENTS[hashString(key) % SPACE_GRADIENTS.length]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Flat accent-color presets derived from the avatar gradient palette. Both stops per gradient. */
|
/** Banner-color presets: 7 families × 3 shades (deep, vibrant, soft). */
|
||||||
export const ACCENT_PRESETS: string[] = AVATAR_GRADIENTS.flatMap(g => {
|
export const BANNER_COLOR_PRESETS: string[][] = [
|
||||||
const matches = g.gradient.match(/#[0-9a-fA-F]{6}/g);
|
['#047857', '#10b981', '#6ee7b7'], // mint
|
||||||
return matches ?? [];
|
['#4338ca', '#6366f1', '#a5b4fc'], // sky
|
||||||
});
|
['#7c3aed', '#c084fc', '#e9d5ff'], // lavender
|
||||||
|
['#c2410c', '#f97316', '#fdba74'], // coral
|
||||||
|
['#be185d', '#ec4899', '#f9a8d4'], // rose
|
||||||
|
['#0e7490', '#06b6d4', '#67e8f9'], // teal
|
||||||
|
['#b45309', '#f59e0b', '#fcd34d'], // amber
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @deprecated Use BANNER_COLOR_PRESETS instead. */
|
||||||
|
export const ACCENT_PRESETS: string[] = BANNER_COLOR_PRESETS.map(f => f[1]!);
|
||||||
|
|
||||||
/** Shift each RGB component of a hex color by `amount` (positive = lighter, negative = darker). */
|
/** Shift each RGB component of a hex color by `amount` (positive = lighter, negative = darker). */
|
||||||
export function adjustColor(hex: string, amount: number): string {
|
export function adjustColor(hex: string, amount: number): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user