From e6a5e4656c0f4129e7b534a4a023d0b628dc68ed Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Thu, 29 Oct 2026 14:35:29 +0000 Subject: [PATCH 1/5] ui: quieter top bar with avatar, aligned filter chips and ghost actions The top bar becomes a white bar with a hairline border instead of the blue gradient. The brand sits over the sidebar column so the Provider / Account chips start on the page content's left edge; the account cluster on the right reads help links, then who is signed in (initials avatar + email, which still opens the profile dialog, and the role pill), then a ghost Log out with an icon. Every control is 32px tall. All ids and handlers stay on the same elements (#user-email-display, #user-role-display, #feedback-link, #logout-btn, the docs link); the initials ride on a data attribute drawn by CSS so the email button's text is unchanged. theme-color follows the bar to white. Co-Authored-By: Claude Opus 5.5 --- frontend/src/__tests__/auth.test.ts | 12 ++- frontend/src/auth.ts | 16 +++ frontend/src/index.html | 12 ++- frontend/src/styles/shell.css | 157 ++++++++++++++++++++++++++++ 4 files changed, 192 insertions(+), 5 deletions(-) diff --git a/frontend/src/__tests__/auth.test.ts b/frontend/src/__tests__/auth.test.ts index 1fb5a600c..70f5b4855 100644 --- a/frontend/src/__tests__/auth.test.ts +++ b/frontend/src/__tests__/auth.test.ts @@ -1,7 +1,7 @@ /** * Auth module tests */ -import { showLoginModal, showResetPasswordModal, updateUserUI, logout } from '../auth'; +import { showLoginModal, showResetPasswordModal, updateUserUI, logout, emailInitials } from '../auth'; import { ADMINISTRATORS_GROUP_ID } from '../permissions'; import { reloadPage } from '../utils'; @@ -460,6 +460,16 @@ describe('Auth Module', () => { const userEmail = document.getElementById('user-email-display'); expect(userEmail?.textContent).toBe('test@example.com'); + // The avatar initials ride on a data attribute (drawn by CSS), so the + // button's text stays exactly the address. + expect(userEmail?.dataset['initials']).toBe('TE'); + }); + + test('emailInitials takes one letter from each of the first two name parts', () => { + expect(emailInitials('dana.okafor@example.com')).toBe('DO'); + expect(emailInitials('priya_raman@example.com')).toBe('PR'); + expect(emailInitials('admin@example.com')).toBe('AD'); + expect(emailInitials('x@example.com')).toBe('X'); }); test('shows user info when logged in', () => { diff --git a/frontend/src/auth.ts b/frontend/src/auth.ts index 1c6735f63..19ca5d981 100644 --- a/frontend/src/auth.ts +++ b/frontend/src/auth.ts @@ -943,6 +943,20 @@ function showResetEmailConfirmation(): void { document.getElementById('reset-confirmation-close')?.focus(); } +/** + * Up to two letters for the topbar avatar: the first letters of the first + * two dot/underscore/dash separated parts of the address's local part + * ("dana.okafor@..." -> "DO"), else its first two characters. + */ +export function emailInitials(email: string): string { + const local = email.split('@')[0] ?? ''; + const parts = local.split(/[._\-+]+/).filter(Boolean); + const letters = parts.length >= 2 + ? `${parts[0]!.charAt(0)}${parts[1]!.charAt(0)}` + : local.slice(0, 2); + return letters.toUpperCase(); +} + /** * Update user UI after login */ @@ -958,6 +972,8 @@ export function updateUserUI(): void { // Update the user email display with click-to-edit functionality if (userEmailEl) { userEmailEl.textContent = currentUser.email; + // Drawn as an avatar by CSS (::before), so it stays out of the text. + userEmailEl.dataset['initials'] = emailInitials(currentUser.email); userEmailEl.removeAttribute('aria-label'); userEmailEl.hidden = false; userEmailEl.title = 'Click to edit your profile'; diff --git a/frontend/src/index.html b/frontend/src/index.html index 3c803e34d..7929a10b6 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -16,7 +16,7 @@ - + CUDly - Cloud Commitment Optimizer @@ -40,12 +40,16 @@

CUDly

+
- - API Docs - + + + +