diff --git a/llm-fe/src/llm-fe/components/SsoButtons/SsoButtons.test.tsx b/llm-fe/src/llm-fe/components/SsoButtons/SsoButtons.test.tsx
new file mode 100644
index 0000000..86b68eb
--- /dev/null
+++ b/llm-fe/src/llm-fe/components/SsoButtons/SsoButtons.test.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import SsoButtons from './SsoButtons';
+import { startOAuth } from '../../auth/sso';
+
+jest.mock('../../auth/sso', () => ({
+ startOAuth: jest.fn(),
+}));
+
+describe('SsoButtons', () => {
+ beforeEach(() => {
+ jest.mocked(startOAuth).mockReset();
+ });
+
+ it('renders nothing when no providers enabled', () => {
+ const { container } = render(
+
+ );
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('uses brand-compliant labels and logos for login', () => {
+ render();
+
+ expect(screen.getByRole('button', { name: 'Continue with Google' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Sign in with Microsoft' })).toBeInTheDocument();
+ expect(document.querySelectorAll('svg').length).toBeGreaterThanOrEqual(2);
+ });
+
+ it('uses Sign up with Google for signup intent', () => {
+ render();
+ expect(screen.getByRole('button', { name: 'Sign up with Google' })).toBeInTheDocument();
+ });
+
+ it('starts OAuth for the selected provider', async () => {
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByRole('button', { name: 'Continue with Google' }));
+ expect(startOAuth).toHaveBeenCalledWith('google', 'login');
+
+ await user.click(screen.getByRole('button', { name: 'Sign in with Microsoft' }));
+ expect(startOAuth).toHaveBeenCalledWith('microsoft', 'login');
+ });
+});
diff --git a/llm-fe/src/llm-fe/components/SsoButtons/SsoButtons.tsx b/llm-fe/src/llm-fe/components/SsoButtons/SsoButtons.tsx
index 2917a94..fc935e5 100644
--- a/llm-fe/src/llm-fe/components/SsoButtons/SsoButtons.tsx
+++ b/llm-fe/src/llm-fe/components/SsoButtons/SsoButtons.tsx
@@ -20,21 +20,32 @@ const Divider = styled.div`
}
`;
-const SsoButton = styled.button`
+const ButtonStack = styled.div`
+ display: flex;
+ flex-direction: column;
width: 100%;
- background: rgba(255, 255, 255, 0.06);
- border: 1px solid rgba(255, 255, 255, 0.18);
- border-radius: 0.5rem;
- color: #fff;
- padding: 0.85rem 1rem;
- font-size: 0.95rem;
- font-weight: 600;
- cursor: pointer;
- transition: background 0.2s ease, transform 0.2s ease;
+ gap: 0.6rem;
margin-top: 0.6rem;
+`;
- &:hover {
- background: rgba(255, 255, 255, 0.12);
+/** Shared layout for IdP buttons — equal size / visual weight. */
+const SsoButtonBase = styled.button`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.75rem;
+ width: 100%;
+ min-height: 2.75rem;
+ border-radius: 0.25rem;
+ padding: 0.65rem 1rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ font-family: 'Roboto', 'Google Sans', system-ui, sans-serif;
+ letter-spacing: 0.01em;
+ cursor: pointer;
+ transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
+
+ &:hover:not(:disabled) {
transform: translateY(-1px);
}
@@ -45,6 +56,93 @@ const SsoButton = styled.button`
}
`;
+/**
+ * Google dark-theme button per
+ * https://developers.google.com/identity/branding-guidelines
+ * Fill #131314, stroke #8E918F, text #E3E3E3; multicolor G on white.
+ */
+const GoogleButton = styled(SsoButtonBase)`
+ background: #131314;
+ border: 1px solid #8e918f;
+ color: #e3e3e3;
+
+ &:hover:not(:disabled) {
+ background: #1e1f20;
+ border-color: #a8aba9;
+ }
+`;
+
+/**
+ * Microsoft dark-theme button — logo + "Sign in with Microsoft"
+ * https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-branding-in-apps
+ */
+const MicrosoftButton = styled(SsoButtonBase)`
+ background: #2f2f2f;
+ border: 1px solid transparent;
+ color: #ffffff;
+
+ &:hover:not(:disabled) {
+ background: #3b3b3b;
+ }
+`;
+
+const GoogleLogoBadge = styled.span`
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ width: 1.25rem;
+ height: 1.25rem;
+ background: #ffffff;
+ border-radius: 0.125rem;
+`;
+
+const LogoMark = styled.span`
+ display: inline-flex;
+ flex-shrink: 0;
+ width: 1.25rem;
+ height: 1.25rem;
+
+ svg {
+ width: 100%;
+ height: 100%;
+ display: block;
+ }
+`;
+
+/** Official multicolor Google "G" (standard color Super G). */
+const GoogleGIcon = (): JSX.Element => (
+
+);
+
+/** Official Microsoft four-square logo — do not recolor. */
+const MicrosoftLogoIcon = (): JSX.Element => (
+
+);
+
export type OAuthProviderFlags = {
google?: boolean;
microsoft?: boolean;
@@ -56,6 +154,9 @@ type SsoButtonsProps = {
disabled?: boolean;
};
+const googleLabel = (intent: 'login' | 'signup'): string =>
+ intent === 'signup' ? 'Sign up with Google' : 'Continue with Google';
+
const SsoButtons = ({ intent, providers, disabled = false }: SsoButtonsProps): JSX.Element | null => {
const google = Boolean(providers.google);
const microsoft = Boolean(providers.microsoft);
@@ -66,24 +167,34 @@ const SsoButtons = ({ intent, providers, disabled = false }: SsoButtonsProps): J
return (
<>
or continue with
- {google && (
- startOAuth('google', intent)}
- >
- Continue with Google
-
- )}
- {microsoft && (
- startOAuth('microsoft', intent)}
- >
- Continue with Microsoft
-
- )}
+
+ {google && (
+ startOAuth('google', intent)}
+ >
+
+
+
+
+
+ {googleLabel(intent)}
+
+ )}
+ {microsoft && (
+ startOAuth('microsoft', intent)}
+ >
+
+
+
+ Sign in with Microsoft
+
+ )}
+
>
);
};
diff --git a/llm-fe/src/llm-fe/pages/SignIn/SignIn.test.tsx b/llm-fe/src/llm-fe/pages/SignIn/SignIn.test.tsx
index ba5fa14..8c978df 100644
--- a/llm-fe/src/llm-fe/pages/SignIn/SignIn.test.tsx
+++ b/llm-fe/src/llm-fe/pages/SignIn/SignIn.test.tsx
@@ -85,7 +85,7 @@ describe('SignIn', () => {
renderSignIn();
expect(await screen.findByRole('button', { name: 'Continue with Google' })).toBeInTheDocument();
- expect(screen.getByRole('button', { name: 'Continue with Microsoft' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Sign in with Microsoft' })).toBeInTheDocument();
});
it('hides SSO buttons when oauth not configured', async () => {
@@ -99,6 +99,7 @@ describe('SignIn', () => {
await waitFor(() => {
expect(screen.queryByRole('button', { name: 'Continue with Google' })).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Sign in with Microsoft' })).not.toBeInTheDocument();
});
});