From e6eba7a2b78829fabb33cee4919f447f9673bcda Mon Sep 17 00:00:00 2001 From: devsyncwrld Date: Thu, 3 Sep 2026 09:46:20 -0300 Subject: [PATCH] fix(profile): align the decorations, and make the effects actually look like something MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three complaints, three causes. Decorations sat off-centre. 's box is `size + 2 × ringWidth`, but the overlay was positioned with a percentage inset of that box — so it drifted by exactly the ring whenever one was present. That is why the crown floated above the head on the profile card while looking fine on the editor tiles, which have no ring. DecoratedAvatar now takes `size` and `ringWidth` and computes the inset in pixels, so the picture's edge always lands at a known radius in the SVG viewBox and every decoration is drawn against it. The ugly ones were ugly. Flames were a blob with a black circle punched out of it; they are now fourteen tongues of alternating height on a gradient. The crown's band sits ON the picture's top edge instead of hovering above it. Frost, stars and hearts were placed by eye and are now on a computed ring. Nine more: holo disc (a conic gradient — the honest way to draw one, and CSS has it where SVG does not), chain, cyber brackets, lightning, spikes, butterflies, sakura, bubbles, flower. Twenty in total. Name effects were thin. Gradient was a two-stop ramp; it now has a lit middle. Neon was one shadow, now five layered with a buzz. Glitch was a keyframed text-shadow, now a real RGB split duplicated through ::before/::after from data-text. Added chrome and gold (banded metal with a specular line — the y2k look the reference is after), holo, rainbow, ice, 3D extrude, and outline. Sixteen effects, thirteen fonts with Audiowide, Bungee and Monoton added. The editor tiles now sample the user's own name rather than "Gg" or the effect label: an effect reads differently on a short word than on the name it will carry. Two per row instead of three, and section rows gained an icon. One test failed on the catalog growth and was right to: it used 'rainbow' as an example of an unknown id, and rainbow is now a real effect. It uses an absurd id instead, and a new test walks every catalog id through the normaliser so a catalog and the normaliser can never drift apart silently. Verified: shared build, web build, server typecheck, 518 web tests (1 new). Not verified: pixels — no browser here; the geometry is now computed rather than guessed, but a screenshot decides. Co-Authored-By: Claude Opus 5 --- packages/shared/src/profile.ts | 7 +- packages/web/public/fonts/Audiowide-400.woff2 | Bin 0 -> 7176 bytes packages/web/public/fonts/Bungee-400.woff2 | Bin 0 -> 4120 bytes packages/web/public/fonts/Monoton-400.woff2 | Bin 0 -> 7780 bytes .../components/modals/UserProfileModal.tsx | 2 +- .../modals/settingsPanels/ProfilePanel.tsx | 43 ++- .../components/profile/DecoratedAvatar.tsx | 340 ++++++++++++++---- .../src/components/profile/ProfileCard.tsx | 2 +- packages/web/src/i18n/locales/en.ts | 19 + packages/web/src/i18n/locales/pt-BR.ts | 19 + packages/web/src/styles/globals.css | 230 +++++++++--- packages/web/src/utils/profileStyle.test.ts | 20 +- 12 files changed, 551 insertions(+), 131 deletions(-) create mode 100644 packages/web/public/fonts/Audiowide-400.woff2 create mode 100644 packages/web/public/fonts/Bungee-400.woff2 create mode 100644 packages/web/public/fonts/Monoton-400.woff2 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 0000000000000000000000000000000000000000..7d644b61dab091d639023cf2198fda9f1bc88e2c GIT binary patch literal 7176 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 KT=v5S00019WS{{6 literal 0 HcmV?d00001 diff --git a/packages/web/public/fonts/Bungee-400.woff2 b/packages/web/public/fonts/Bungee-400.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..2f5fed64f26af10cc84642894fc86cce652169f0 GIT binary patch literal 4120 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
- + - `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' && ( -