Skip to content

Commit 8328c9b

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(branding): refresh Google logo
1 parent d152fad commit 8328c9b

8 files changed

Lines changed: 462 additions & 39 deletions

File tree

‎apps/docs/components/icons.tsx‎

Lines changed: 156 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,25 +1365,164 @@ export const S3Icon = (props: SVGProps<SVGSVGElement>) => (
13651365
</svg>
13661366
)
13671367

1368+
const GOOGLE_ICON_FILTERS = [
1369+
{
1370+
index: 0,
1371+
x: '5.25922',
1372+
y: '4.90668',
1373+
width: '29.7381',
1374+
height: '29.772',
1375+
},
1376+
{
1377+
index: 1,
1378+
x: '12.9977',
1379+
y: '14.828',
1380+
width: '14.1038',
1381+
height: '10.8265',
1382+
},
1383+
{
1384+
index: 2,
1385+
x: '23.9146',
1386+
y: '13.1219',
1387+
width: '18.8784',
1388+
height: '10.1871',
1389+
},
1390+
{
1391+
index: 3,
1392+
x: '15.8659',
1393+
y: '11.8415',
1394+
width: '18.8171',
1395+
height: '8.7561',
1396+
},
1397+
{
1398+
index: 4,
1399+
x: '20.1341',
1400+
y: '8.54878',
1401+
width: '18.8171',
1402+
height: '8.7561',
1403+
},
1404+
{
1405+
index: 5,
1406+
x: '13.9756',
1407+
y: '14.7683',
1408+
width: '21.0122',
1409+
height: '10.2195',
1410+
},
1411+
{
1412+
index: 6,
1413+
x: '19.0404',
1414+
y: '9.00419',
1415+
width: '12.2878',
1416+
height: '10.0309',
1417+
},
1418+
] as const
1419+
1420+
/**
1421+
* Google "G" artwork from Google's official Identity sign-in asset bundle.
1422+
*
1423+
* @see https://developers.google.com/static/identity/images/signin-assets.zip
1424+
*/
13681425
export function GoogleIcon(props: SVGProps<SVGSVGElement>) {
1426+
const idPrefix = `google_${useId()}`
1427+
const maskId = `${idPrefix}_mask`
1428+
const clipId = `${idPrefix}_angular_clip`
1429+
const getFilterId = (index: number) => `${idPrefix}_filter_${index}`
1430+
13691431
return (
1370-
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48' width='24' height='24'>
1371-
<path
1372-
fill='#fbc02d'
1373-
d='M43.61,20.08H42V20H24v8h11.3c-1.65,4.66-6.08,8-11.3,8c-6.63,0-12-5.37-12-12 s5.37-12,12-12c3.06,0,5.84,1.15,7.96,3.04l5.66-5.66C34.05,6.05,29.27,4,24,4C12.96,4,4,12.96,4,24s8.96,20,20,20 s20-8.95,20-20C44,22.66,43.86,21.35,43.61,20.08z'
1374-
/>
1375-
<path
1376-
fill='#e53935'
1377-
d='M6.31,14.69l6.57,4.82C14.66,15.11,18.96,12,24,12c3.06,0,5.84,1.15,7.96,3.04 l5.66-5.66C34.05,6.05,29.27,4,24,4C16.32,4,9.66,8.34,6.31,14.69z'
1378-
/>
1379-
<path
1380-
fill='#4caf50'
1381-
d='M24,44c5.17,0,9.86-1.98,13.41-5.19l-6.19-5.24C29.21,35.09,26.72,36,24,36 c-5.2,0-9.62-3.32-11.28-7.95l-6.52,5.03C9.51,39.56,16.23,44,24,44z'
1382-
/>
1383-
<path
1384-
fill='#1565c0'
1385-
d='M43.61,20.08L43.6,20L42,20H24v8h11.3c-0.79,2.24-2.23,4.17-4.09,5.57 c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.24C36.97,39.21,44,34,44,24C44,22.66,43.86,21.35,43.61,20.08z'
1386-
/>
1432+
<svg
1433+
{...props}
1434+
xmlns='http://www.w3.org/2000/svg'
1435+
viewBox='10 10 20 20'
1436+
width='24'
1437+
height='24'
1438+
fill='none'
1439+
>
1440+
<mask
1441+
id={maskId}
1442+
style={{ maskType: 'alpha' }}
1443+
maskUnits='userSpaceOnUse'
1444+
x='10'
1445+
y='10'
1446+
width='20'
1447+
height='20'
1448+
>
1449+
<path
1450+
d='M29.3987 18.1814H19.9849V22.0445H25.3598C25.1286 23.294 24.4294 24.3596 23.3676 25.0712C22.4746 25.6716 21.3266 26.0211 19.9849 26.0211C17.3864 26.0211 15.1823 24.2666 14.3947 21.9004C14.1952 21.2989 14.0853 20.6599 14.0853 19.9983C14.0853 19.3367 14.1952 18.6966 14.3947 18.0962C15.1823 15.7311 17.3864 13.9755 19.9849 13.9755C21.4524 13.9755 22.767 14.4816 23.8039 15.4713L26.6653 12.6057C24.936 10.9908 22.6786 10 19.9849 10C16.0832 10 12.705 12.2414 11.0618 15.5076C10.383 16.8592 10 18.3834 10 19.9994C10 21.6155 10.383 23.1396 11.0618 24.4913C12.705 27.7597 16.0832 30 19.9849 30C22.6797 30 24.9485 29.1137 26.6018 27.5861C28.4887 25.8452 29.5732 23.2702 29.5732 20.2275C29.5732 19.5182 29.5131 18.835 29.3987 18.1825V18.1814Z'
1451+
fill='#E94FFF'
1452+
/>
1453+
</mask>
1454+
<g mask={`url(#${maskId})`}>
1455+
<g filter={`url(#${getFilterId(0)})`}>
1456+
<g clipPath={`url(#${clipId})`}>
1457+
<g transform='matrix(0.00804129 -0.00805186 0.00804128 0.00805186 19.6819 19.7927)'>
1458+
<foreignObject x='-2105.64' y='-2105.64' width='4211.29' height='4211.29'>
1459+
<div
1460+
style={{
1461+
background:
1462+
'conic-gradient(from 90deg, rgba(255, 70, 65, 1) 0deg, rgba(255, 70, 65, 1) 4.14555deg, rgba(49, 134, 255, 1) 39.154deg, rgba(49, 134, 255, 1) 72.0044deg, rgba(0, 165, 183, 1) 96.7463deg, rgba(14, 188, 95, 1) 120.897deg, rgba(14, 188, 95, 1) 154.722deg, rgba(108, 196, 0, 1) 179.136deg, rgba(255, 204, 0, 1) 203.588deg, rgba(255, 211, 20, 1) 226.915deg, rgba(255, 204, 0, 1) 251.688deg, rgba(255, 106, 43, 1) 273.129deg, rgba(253, 70, 65, 1) 289.305deg, rgba(255, 70, 65, 1) 359.593deg, rgba(255, 70, 65, 1) 360deg)',
1463+
height: '100%',
1464+
width: '100%',
1465+
opacity: 1,
1466+
}}
1467+
/>
1468+
</foreignObject>
1469+
</g>
1470+
</g>
1471+
</g>
1472+
<g filter={`url(#${getFilterId(1)})`}>
1473+
<ellipse
1474+
cx='20.0496'
1475+
cy='20.2413'
1476+
rx='5.39634'
1477+
ry='2.83537'
1478+
transform='rotate(24.4473 20.0496 20.2413)'
1479+
fill='#3186FF'
1480+
/>
1481+
</g>
1482+
<g filter={`url(#${getFilterId(2)})`}>
1483+
<ellipse cx='33.3538' cy='18.2155' rx='7.43918' ry='3.09357' fill='#3186FF' />
1484+
</g>
1485+
<g filter={`url(#${getFilterId(3)})`}>
1486+
<ellipse cx='25.2744' cy='16.2195' rx='7.40854' ry='2.37805' fill='#FF4641' />
1487+
</g>
1488+
<g filter={`url(#${getFilterId(4)})`}>
1489+
<ellipse cx='29.5427' cy='12.9268' rx='7.40854' ry='2.37805' fill='#FF5B8B' />
1490+
</g>
1491+
<g filter={`url(#${getFilterId(5)})`}>
1492+
<ellipse cx='24.4817' cy='19.878' rx='8.5061' ry='3.10976' fill='#3186FF' />
1493+
</g>
1494+
<g filter={`url(#${getFilterId(6)})`}>
1495+
<ellipse
1496+
cx='25.1842'
1497+
cy='14.0197'
1498+
rx='4.53882'
1499+
ry='2.37805'
1500+
transform='rotate(-28.6599 25.1842 14.0197)'
1501+
fill='#FF4641'
1502+
/>
1503+
</g>
1504+
</g>
1505+
<defs>
1506+
{GOOGLE_ICON_FILTERS.map(({ index, x, y, width, height }) => (
1507+
<filter
1508+
key={index}
1509+
id={getFilterId(index)}
1510+
x={x}
1511+
y={y}
1512+
width={width}
1513+
height={height}
1514+
filterUnits='userSpaceOnUse'
1515+
colorInterpolationFilters='sRGB'
1516+
>
1517+
<feFlood floodOpacity='0' result='BackgroundImageFix' />
1518+
<feBlend mode='normal' in='SourceGraphic' in2='BackgroundImageFix' result='shape' />
1519+
<feGaussianBlur stdDeviation='1' result='effect1_foregroundBlur_1298_12516' />
1520+
</filter>
1521+
))}
1522+
<clipPath id={clipId}>
1523+
<path d='M7.25922 19.7927C7.25922 12.6759 13.0209 6.90668 20.1283 6.90668C27.2357 6.90668 32.9973 12.6759 32.9973 19.7927C32.9973 26.9094 27.2357 32.6786 20.1283 32.6786C13.0209 32.6786 7.25921 26.9094 7.25922 19.7927Z' />
1524+
</clipPath>
1525+
</defs>
13871526
</svg>
13881527
)
13891528
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act } from 'react'
5+
import { createRoot, type Root } from 'react-dom/client'
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7+
8+
const { mockSocialSignIn } = vi.hoisted(() => ({ mockSocialSignIn: vi.fn() }))
9+
10+
vi.mock('@/lib/auth/auth-client', () => ({
11+
client: { signIn: { social: mockSocialSignIn } },
12+
}))
13+
14+
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
15+
16+
let container: HTMLDivElement
17+
let root: Root
18+
19+
function renderGoogleButton() {
20+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
21+
container = document.createElement('div')
22+
document.body.appendChild(container)
23+
root = createRoot(container)
24+
act(() => {
25+
root.render(
26+
<SocialLoginButtons
27+
githubAvailable={false}
28+
googleAvailable
29+
microsoftAvailable={false}
30+
callbackURL='/after-login'
31+
isProduction
32+
/>
33+
)
34+
})
35+
}
36+
37+
beforeEach(() => {
38+
vi.clearAllMocks()
39+
})
40+
41+
afterEach(() => {
42+
act(() => root.unmount())
43+
container.remove()
44+
})
45+
46+
describe('SocialLoginButtons Google sign-in', () => {
47+
it('renders the compliant CTA and 20px logo while preserving the loading state', async () => {
48+
let finishSignIn: (() => void) | undefined
49+
mockSocialSignIn.mockReturnValue(
50+
new Promise<void>((resolve) => {
51+
finishSignIn = resolve
52+
})
53+
)
54+
renderGoogleButton()
55+
56+
const button = container.querySelector('button') as HTMLButtonElement
57+
const icon = button.querySelector('svg')
58+
59+
expect(button).toBeEnabled()
60+
expect(button).toHaveTextContent('Continue with Google')
61+
expect(icon).toHaveClass('size-[20px]')
62+
63+
await act(async () => {
64+
button.click()
65+
})
66+
67+
expect(mockSocialSignIn).toHaveBeenCalledWith({
68+
provider: 'google',
69+
callbackURL: '/after-login',
70+
})
71+
expect(button).toBeDisabled()
72+
expect(button).toHaveTextContent('Connecting…')
73+
74+
await act(async () => {
75+
finishSignIn?.()
76+
})
77+
78+
expect(button).toBeEnabled()
79+
expect(button).toHaveTextContent('Continue with Google')
80+
})
81+
})

‎apps/sim/app/(auth)/components/social-login-buttons.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,12 @@ export function SocialLoginButtons({
8585
const googleButton = (
8686
<Chip
8787
fullWidth
88-
leftIcon={GoogleIcon}
88+
leftAdornment={<GoogleIcon className='size-[20px] flex-shrink-0' />}
8989
className={cn(AUTH_BUTTON_CLASS, 'border border-[var(--border-1)]')}
9090
disabled={!googleAvailable || isGoogleLoading}
9191
onClick={signInWithGoogle}
9292
>
93-
{isGoogleLoading ? 'Connecting…' : 'Google'}
93+
{isGoogleLoading ? 'Connecting…' : 'Continue with Google'}
9494
</Chip>
9595
)
9696

‎apps/sim/app/(landing)/components/auth-modal/auth-modal.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ export function AuthModal({ children, defaultView = 'login', source }: AuthModal
202202
disabled={!!socialLoading}
203203
className={SOCIAL_BTN}
204204
>
205-
<GoogleIcon className='absolute left-4 size-[18px] shrink-0' />
205+
<GoogleIcon className='absolute left-4 size-[20px] shrink-0' />
206206
<span>
207207
{socialLoading === 'google' ? 'Connecting...' : 'Continue with Google'}
208208
</span>

‎apps/sim/components/icons.test.tsx‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { renderToStaticMarkup } from 'react-dom/server'
5+
import { describe, expect, it } from 'vitest'
6+
import { GoogleIcon } from '@/components/icons'
7+
8+
const ID_PATTERN = /\sid="([^"]+)"/g
9+
const URL_REFERENCE_PATTERN = /url\(#([^)]+)\)/g
10+
11+
describe('GoogleIcon', () => {
12+
it('renders the official gradient artwork and forwards SVG props', () => {
13+
const markup = renderToStaticMarkup(
14+
<GoogleIcon className='google-icon' data-testid='google-icon' aria-label='Google' />
15+
)
16+
17+
expect(markup).toContain('viewBox="10 10 20 20"')
18+
expect(markup).toContain('width="24"')
19+
expect(markup).toContain('height="24"')
20+
expect(markup).toContain('class="google-icon"')
21+
expect(markup).toContain('data-testid="google-icon"')
22+
expect(markup).toContain('aria-label="Google"')
23+
expect(markup).toContain('conic-gradient(from 90deg')
24+
expect(markup.match(/<filter\b/g)).toHaveLength(7)
25+
expect(markup.match(/<ellipse\b/g)).toHaveLength(6)
26+
expect(markup).not.toContain('data-figma')
27+
})
28+
29+
it('namespaces every definition and resolves every URL reference', () => {
30+
const markup = renderToStaticMarkup(
31+
<>
32+
<GoogleIcon />
33+
<GoogleIcon />
34+
</>
35+
)
36+
const iconMarkup = [...markup.matchAll(/<svg\b[^>]*>.*?<\/svg>/gs)].map(([svg]) => svg)
37+
38+
expect(iconMarkup).toHaveLength(2)
39+
40+
const allIds = iconMarkup.flatMap((svg) => [...svg.matchAll(ID_PATTERN)].map(([, id]) => id))
41+
expect(allIds).toHaveLength(18)
42+
expect(new Set(allIds).size).toBe(allIds.length)
43+
44+
for (const svg of iconMarkup) {
45+
const ids = new Set([...svg.matchAll(ID_PATTERN)].map(([, id]) => id))
46+
const references = [...svg.matchAll(URL_REFERENCE_PATTERN)].map(([, id]) => id)
47+
48+
expect(ids.size).toBe(9)
49+
expect(references).toHaveLength(9)
50+
expect(references.every((id) => ids.has(id))).toBe(true)
51+
}
52+
})
53+
})

0 commit comments

Comments
 (0)