diff --git a/packages/shared/src/profile.ts b/packages/shared/src/profile.ts index 99594959..ad91436b 100644 --- a/packages/shared/src/profile.ts +++ b/packages/shared/src/profile.ts @@ -15,16 +15,19 @@ export const NAME_FONTS = [ 'default', 'display', 'comic', 'script', 'pixel', 'future', 'serif', 'mono', 'round', 'marker', + 'techno', 'block', 'retro', ] as const; export type NameFont = (typeof NAME_FONTS)[number]; export const NAME_EFFECTS = [ - 'solid', 'gradient', 'neon', 'toon', 'pop', 'gummy', 'prism', 'glitch', 'fire', + 'solid', 'gradient', 'chrome', 'gold', 'holo', 'neon', 'toon', 'pop', 'gummy', + 'prism', 'glitch', 'fire', 'ice', 'shadow', 'outline', 'rainbow', ] as const; export type NameEffect = (typeof NAME_EFFECTS)[number]; export const AVATAR_DECORATIONS = [ - 'none', 'ring', 'neon', 'flames', 'stars', 'halo', 'vines', 'pixel', 'hearts', 'frost', 'crown', + 'none', 'ring', 'neon', 'cd', 'chain', 'cyber', 'pixel', 'flames', 'lightning', 'spikes', + 'frost', 'stars', 'hearts', 'butterfly', 'sakura', 'vines', 'bubbles', 'crown', 'halo', 'flower', ] as const; export type AvatarDecoration = (typeof AVATAR_DECORATIONS)[number]; diff --git a/packages/web/public/fonts/Audiowide-400.woff2 b/packages/web/public/fonts/Audiowide-400.woff2 new file mode 100644 index 00000000..7d644b61 Binary files /dev/null and b/packages/web/public/fonts/Audiowide-400.woff2 differ diff --git a/packages/web/public/fonts/Bungee-400.woff2 b/packages/web/public/fonts/Bungee-400.woff2 new file mode 100644 index 00000000..2f5fed64 Binary files /dev/null and b/packages/web/public/fonts/Bungee-400.woff2 differ diff --git a/packages/web/public/fonts/Monoton-400.woff2 b/packages/web/public/fonts/Monoton-400.woff2 new file mode 100644 index 00000000..6a77b3d8 Binary files /dev/null and b/packages/web/public/fonts/Monoton-400.woff2 differ diff --git a/packages/web/src/components/modals/UserProfileModal.tsx b/packages/web/src/components/modals/UserProfileModal.tsx index 3d566497..dc8f4ad9 100644 --- a/packages/web/src/components/modals/UserProfileModal.tsx +++ b/packages/web/src/components/modals/UserProfileModal.tsx @@ -311,7 +311,7 @@ export function UserProfileModal() { {/* Header (avatar + name) */}
- + - `relative rounded-xl border-2 p-3 flex flex-col items-center gap-2 transition-colors text-center ${active ? 'border-accent-primary bg-accent-primary/10' : 'border-white/[0.06] bg-white/[0.03] hover:bg-white/[0.06]'}`; + `group/tile relative rounded-xl border-2 p-3 flex flex-col items-center gap-2 transition-all text-center ${active ? 'border-accent-primary bg-accent-primary/10 shadow-[0_0_0_3px_rgba(124,108,246,0.15)]' : 'border-white/[0.06] bg-white/[0.03] hover:bg-white/[0.07] hover:border-white/[0.14]'}`; + +/** One glyph per section, so the list scans by shape rather than by reading. */ +const SECTION_ICON: Record = { + nameplate: 'M3 6h18v4H3V6zm0 6h12v2H3v-2zm0 4h8v2H3v-2z', + avatar: 'M12 12c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4zm0 2c-2.7 0-8 1.3-8 4v2h16v-2c0-2.7-5.3-4-8-4z', + name: 'M5 4v3h5.5v12h3V7H19V4H5z', + theme: 'M12 3a9 9 0 1 0 0 18c.8 0 1.5-.7 1.5-1.5 0-.4-.2-.8-.4-1-.3-.3-.4-.6-.4-1 0-.8.7-1.5 1.5-1.5H16a5 5 0 0 0 5-5c0-4.4-4-8-9-8zm-5.5 9a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm3-4a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm3 4a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z', + effect: 'M12 2l2.4 6.6L21 11l-6.6 2.4L12 20l-2.4-6.6L3 11l6.6-2.4L12 2z', + identity: 'M12 2a5 5 0 1 0 0 10 5 5 0 0 0 0-10zm0 12c-4.4 0-8 2.2-8 5v3h16v-3c0-2.8-3.6-5-8-5z', + links: 'M3.9 12a4.1 4.1 0 0 1 4.1-4.1h4V6H8a6 6 0 0 0 0 12h4v-1.9H8A4.1 4.1 0 0 1 3.9 12zM8 13h8v-2H8v2zm8-7h-4v1.9h4a4.1 4.1 0 0 1 0 8.2h-4V18h4a6 6 0 0 0 0-12z', + board: 'M4 4h7v7H4V4zm9 0h7v4h-7V4zm0 6h7v10h-7V10zM4 13h7v7H4v-7z', +}; const chipClass = (active: boolean) => `px-3 py-2 rounded-lg text-sm border transition-colors ${active ? 'border-accent-primary bg-accent-primary/15 text-txt-primary' : 'border-white/[0.06] bg-white/[0.03] text-txt-secondary hover:bg-white/[0.06]'}`; @@ -177,7 +189,7 @@ export function ProfilePanel() { ), avatar: (
- + {t(`profile.decoration.${draft.decoration}` as TranslationKey)} @@ -225,10 +237,10 @@ export function ProfilePanel() { ), avatar: ( -
+
{AVATAR_DECORATIONS.map((d) => ( ))}
-
+
{NAME_EFFECTS.map((e) => { const p = nameStyleProps({ font: draft.name.font, effect: e, colors: draft.name.colors }); + const label = t(`profile.effect.${e}` as TranslationKey); return ( - ); })} @@ -370,10 +386,13 @@ export function ProfilePanel() {
{SECTIONS.map((id) => ( diff --git a/packages/web/src/components/profile/DecoratedAvatar.tsx b/packages/web/src/components/profile/DecoratedAvatar.tsx index bbea9ee4..7d4756f3 100644 --- a/packages/web/src/components/profile/DecoratedAvatar.tsx +++ b/packages/web/src/components/profile/DecoratedAvatar.tsx @@ -1,24 +1,46 @@ import React from 'react'; import type { AvatarDecoration } from '@backspace/shared/src/profile'; +/** + * How much wider the decoration layer is than the avatar *picture*. + * Everything below is drawn in a 100×100 viewBox mapped onto that layer, so the + * picture's edge always lands at radius AVATAR_R — decorations hug it instead of + * guessing. + */ +const SCALE = 1.42; +const AVATAR_R = 50 / SCALE; // ≈ 35.2 + interface DecoratedAvatarProps { decoration: AvatarDecoration; - /** Avatar element to decorate; the overlay sizes itself to this box. */ + /** Diameter of the avatar picture in px — the `size` given to . */ + size: number; + /** Ring width given to , if any. Avatar's box grows by 2× this. */ + ringWidth?: number; children: React.ReactNode; className?: string; } /** - * Wraps an avatar with a decorative layer. Every decoration is inline SVG on a - * 100×100 box around the picture — no image assets, so a new one is a few - * lines of markup and scales to any avatar size. + * Wraps an avatar with a decorative frame. + * + * The overlay is placed from the avatar's real geometry rather than a + * percentage: 's box is `size + 2 × ringWidth`, so a percentage inset + * drifted whenever a ring was present — which is why decorations sat off-centre + * on the profile card while looking fine on the editor tiles. + * + * Every decoration is inline SVG (or, for `cd`, a CSS conic gradient), so adding + * one is markup rather than an image asset, and it stays sharp at any size. */ -export function DecoratedAvatar({ decoration, children, className = '' }: DecoratedAvatarProps) { +export function DecoratedAvatar({ decoration, size, ringWidth = 0, children, className = '' }: DecoratedAvatarProps) { + const box = size + ringWidth * 2; + const overlay = size * SCALE; + const inset = (box - overlay) / 2; // negative: the layer overhangs the box + return ( -
+
{children} {decoration !== 'none' && ( -