From 723522b90d3dfc30f3fc35478404d77452e407ed Mon Sep 17 00:00:00 2001 From: devsyncwrld Date: Thu, 3 Sep 2026 09:27:38 -0300 Subject: [PATCH] =?UTF-8?q?fix(profile):=20give=20the=20editor=20room=20?= =?UTF-8?q?=E2=80=94=20three=20panels=20instead=20of=20a=20cramped=20colum?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner's verdict on the first editor was "imprensado", and it was: the settings shell caps its content at 640px, and the editor put a 360px preview next to six-column tile grids inside that. Everything was legible and nothing was usable. Now it follows the shape of the reference (Discord's profile editor): - left, 320px: the list of sections, each showing its current value as a mini preview (the nameplate strip, the decorated avatar, the styled name, the theme swatch, the effect thumbnail). Picking one replaces the list with that section's editor, with a back arrow — a drill-down instead of sub-modals, one level deep. - centre: the live card, plus two in-context samples — how the name and tag read on a chat line, and the row in the member list with its nameplate. - right: the Board as others see it, with an Edit shortcut. Tiles went from 5–6 per row to 2–3, with real size (Gg samples at 26px, decorations on a 56px avatar, effect thumbnails 64px tall). The settings shell drops its width cap and lets the panel fill the height for this tab only; every other panel keeps the single reading column. Stacks to one column below the lg breakpoint. Verified: web build, 517 web tests. Not verified: pixels — there is no browser here, so the proportions are from the reference and the numbers, and a screenshot decides. Co-Authored-By: Claude Fable 5.1 --- .../src/components/modals/UserSettings.tsx | 5 +- .../modals/settingsPanels/ProfilePanel.tsx | 512 +++++++++++------- packages/web/src/i18n/locales/en.ts | 15 + packages/web/src/i18n/locales/pt-BR.ts | 15 + 4 files changed, 348 insertions(+), 199 deletions(-) diff --git a/packages/web/src/components/modals/UserSettings.tsx b/packages/web/src/components/modals/UserSettings.tsx index 80695bef..f02ab0da 100644 --- a/packages/web/src/components/modals/UserSettings.tsx +++ b/packages/web/src/components/modals/UserSettings.tsx @@ -238,7 +238,10 @@ export function UserSettingsModal() { {/* Content area (desktop always, mobile only when viewing content) */} {(!isMobile || mobileView === 'content') && ( -
+ {/* The profile editor is a three-panel layout and needs the whole + width and height of the content area; every other panel is a + single reading column. */} +
{/* Mobile back button */} {isMobile && ( ))} {q.trim() && !busy && ( - )} @@ -93,22 +114,24 @@ function GameSearch({ onPick, placeholder }: { onPick: (g: GameEntry) => void; p function GameEditor({ game, onChange, onRemove, withNote }: { game: GameEntry; onChange: (g: GameEntry) => void; onRemove: () => void; withNote?: boolean }) { const t = useT(); return ( -
- {game.cover ? :
} -
-
{game.name}
+
+ {game.cover ? :
} +
+
{game.name}
{withNote && ( onChange({ ...game, note: e.target.value })} - placeholder={t('profile.editor.gameNote')} className="input-standard w-full text-xs py-1" /> + placeholder={t('profile.editor.gameNote')} className="input-standard w-full text-xs py-1.5" /> )} onChange({ ...game, tags: e.target.value.split(',').map((x) => x.trim()).filter(Boolean).slice(0, PROFILE_LIMITS.gameTags) })} - placeholder={t('profile.editor.gameTags')} className="input-standard w-full text-xs py-1" /> + placeholder={t('profile.editor.gameTags')} className="input-standard w-full text-xs py-1.5" />
- +
); } +// ─── Panel ────────────────────────────────────────────────────────────────── + export function ProfilePanel() { const t = useT(); const user = useAuthStore((s) => s.user); @@ -117,6 +140,7 @@ export function ProfilePanel() { const saved = useMemo(() => getProfileStyle(user), [user]); const [draft, setDraft] = useState(saved); + const [section, setSection] = useState(null); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); useEffect(() => { setDraft(saved); }, [saved]); @@ -138,196 +162,288 @@ export function ProfilePanel() { } catch (e) { setError(e instanceof Error ? e.message : t('profile.editor.saveFailed')); } finally { setSaving(false); } }; - const handleReset = () => setDraft(DEFAULT_PROFILE_STYLE); - const chip = (active: boolean) => `px-2.5 py-1.5 rounded-md 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]'}`; - const tile = (active: boolean) => `relative rounded-lg border-2 p-2 flex flex-col items-center gap-1 transition-colors ${active ? 'border-accent-primary bg-accent-primary/10' : 'border-white/[0.06] bg-white/[0.03] hover:bg-white/[0.06]'}`; + const plate = nameplateProps(draft.nameplate, draft.nameplateColors); + const nameP = nameStyleProps(draft.name); + const boardCount = (draft.widgets.favoriteGame ? 1 : 0) + draft.widgets.rotation.length + draft.widgets.wishlist.length + (draft.widgets.quote ? 1 : 0); + + // ── Left column: section list, or the open section's editor ── + const sectionPreview: Record = { + nameplate: ( +
+ + {displayName} +
+ ), + avatar: ( +
+ + + + {t(`profile.decoration.${draft.decoration}` as TranslationKey)} +
+ ), + name:
, + theme: ( +
+ ), + effect: ( +
+ + {t(`profile.profileEffect.${draft.effect}` as TranslationKey)} +
+ ), + identity: ( +
+ {draft.tag ? : —} + {draft.pronouns && {draft.pronouns}} +
+ ), + links:
{draft.links.length} / {PROFILE_LIMITS.links}
, + board:
{boardCount > 0 ? `${boardCount} ${t('profile.editor.items')}` : '—'}
, + }; + + const sectionEditor: Record = { + nameplate: ( + <> +

{t('profile.editor.nameplate.hint')}

+
+ {NAMEPLATES.map((n) => { + const p = nameplateProps(n, draft.nameplateColors); + return ( + + ); + })} +
+ {draft.nameplate === 'custom' && <> set('nameplateColors', c)} />} + + ), + avatar: ( +
+ {AVATAR_DECORATIONS.map((d) => ( + + ))} +
+ ), + name: ( + <> + +
+ {NAME_FONTS.map((f) => ( + + ))} +
+ +
+ {NAME_EFFECTS.map((e) => { + const p = nameStyleProps({ font: draft.name.font, effect: e, colors: draft.name.colors }); + return ( + + ); + })} +
+ + set('name', { ...draft.name, colors: c })} /> + + ), + theme: ( + <> +

{t('profile.editor.theme.hint')}

+ + set('theme', { colors: c })} /> + + ), + effect: ( +
+ {PROFILE_EFFECTS.map((e) => ( + + ))} +
+ ), + identity: ( +
+ +
+ +
+ set('tag', { text: draft.tag?.text ?? '', emoji: e.target.value, color: draft.tag?.color ?? '#7c6cf6' })} placeholder="🔥" className="input-standard w-14 text-center" /> + set('tag', { text: e.target.value.toUpperCase(), emoji: draft.tag?.emoji ?? '', color: draft.tag?.color ?? '#7c6cf6' })} placeholder="STRT" className="input-standard flex-1 uppercase tracking-wider" /> + +
+
+

{t('profile.editor.tag.hint')}

+ {draft.tag && } +
+
+
+ ), + links: ( + <> +

{t('profile.editor.links.hint')}

+
+ {draft.links.map((l, i) => ( +
+
+ set('links', draft.links.map((x, j) => j === i ? { ...x, label: e.target.value } : x))} placeholder={t('profile.editor.linkLabel')} className="input-standard flex-1 text-sm" /> + +
+ set('links', draft.links.map((x, j) => j === i ? { ...x, url: e.target.value } : x))} placeholder="https://" className="input-standard w-full text-sm" /> +
+ ))} + {draft.links.length < PROFILE_LIMITS.links && ( + + )} +
+ + ), + board: ( +
+
+ + {draft.widgets.favoriteGame + ? setW('favoriteGame', g)} onRemove={() => setW('favoriteGame', null)} /> + : setW('favoriteGame', g)} />} +
+
+ +
+ {draft.widgets.rotation.map((g, i) => setW('rotation', draft.widgets.rotation.map((x, j) => j === i ? n : x))} onRemove={() => setW('rotation', draft.widgets.rotation.filter((_, j) => j !== i))} />)} + {draft.widgets.rotation.length < PROFILE_LIMITS.rotation && setW('rotation', [...draft.widgets.rotation, g])} />} +
+
+
+ +
+ {draft.widgets.wishlist.map((g, i) => ( +
+ {g.cover ? :
{g.name}
} + +
+ ))} +
+ {draft.widgets.wishlist.length < PROFILE_LIMITS.wishlist && setW('wishlist', [...draft.widgets.wishlist, g])} />} +
+
+ +