-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(auth/logo): Add dynamic sizing and conditional twenty logo #8587
base: main
Are you sure you want to change the base?
Changes from all commits
c3024f7
c62ef0e
9cbbe51
bbe4baf
68580bb
68ec09a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,67 +1,79 @@ | ||
import { useTheme } from '@emotion/react'; | ||
import styled from '@emotion/styled'; | ||
|
||
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI'; | ||
import { Spacing } from 'twenty-ui'; | ||
|
||
type LogoProps = { | ||
workspaceLogo?: string | null; | ||
primaryLogo?: string | null; | ||
secondaryLogo?: string | null; | ||
size?: Spacing | null; | ||
}; | ||
|
||
const StyledContainer = styled.div` | ||
height: 48px; | ||
const StyledContainer = styled.div<{ size: number }>` | ||
height: ${({ size }) => size}px; | ||
margin-bottom: ${({ theme }) => theme.spacing(4)}; | ||
margin-top: ${({ theme }) => theme.spacing(4)}; | ||
|
||
position: relative; | ||
width: 48px; | ||
width: ${({ size }) => size}px; | ||
`; | ||
|
||
const StyledTwentyLogo = styled.img` | ||
const StyledSecondaryLogo = styled.img<{ size: number }>` | ||
border-radius: ${({ theme }) => theme.border.radius.xs}; | ||
height: 24px; | ||
width: 24px; | ||
height: calc(${({ size }) => size}px / 2); | ||
width: calc(${({ size }) => size}px / 2); | ||
`; | ||
|
||
const StyledTwentyLogoContainer = styled.div` | ||
const StyledSecondaryLogoContainer = styled.div<{ size: number }>` | ||
align-items: center; | ||
background-color: ${({ theme }) => theme.background.primary}; | ||
border-radius: ${({ theme }) => theme.border.radius.sm}; | ||
bottom: ${({ theme }) => `-${theme.spacing(3)}`}; | ||
bottom: calc( | ||
(${({ theme }) => theme.spacing(3)} * -1) * ${({ size }) => size} / 48 | ||
); | ||
display: flex; | ||
height: 28px; | ||
height: calc( | ||
(${({ theme }) => theme.spacing(7)} * ${({ size }) => size}) / 48 | ||
); | ||
justify-content: center; | ||
|
||
position: absolute; | ||
right: ${({ theme }) => `-${theme.spacing(3)}`}; | ||
width: 28px; | ||
right: calc( | ||
(${({ theme }) => theme.spacing(3)} * -1) * ${({ size }) => size} / 48 | ||
); | ||
width: calc(${({ theme }) => theme.spacing(7)} * ${({ size }) => size} / 48); | ||
`; | ||
|
||
type StyledMainLogoProps = { | ||
logo?: string | null; | ||
}; | ||
|
||
const StyledMainLogo = styled.div<StyledMainLogoProps>` | ||
background: url(${(props) => props.logo}); | ||
const StyledPrimaryLogo = styled.div<{ src: string }>` | ||
background: url(${(props) => props.src}); | ||
background-size: cover; | ||
height: 100%; | ||
|
||
width: 100%; | ||
`; | ||
|
||
export const Logo = ({ workspaceLogo }: LogoProps) => { | ||
if (!workspaceLogo) { | ||
return ( | ||
<StyledContainer> | ||
<StyledMainLogo logo="/icons/android/android-launchericon-192-192.png" /> | ||
</StyledContainer> | ||
); | ||
} | ||
export const Logo = (props: LogoProps) => { | ||
const theme = useTheme(); | ||
|
||
const size = props.size ?? theme.spacing(12); | ||
|
||
const defaultPrimaryLogoUrl = `${window.location.origin}/icons/android/android-launchericon-192-192.png`; | ||
|
||
const primaryLogoUrl = getImageAbsoluteURI( | ||
props.primaryLogo ?? defaultPrimaryLogoUrl, | ||
); | ||
const secondaryLogoUrl = getImageAbsoluteURI(props.secondaryLogo); | ||
|
||
const sizeInNumber = parseInt(size, 10); | ||
|
||
return ( | ||
<StyledContainer> | ||
<StyledMainLogo logo={getImageAbsoluteURI(workspaceLogo)} /> | ||
<StyledTwentyLogoContainer> | ||
<StyledTwentyLogo src="/icons/android/android-launchericon-192-192.png" /> | ||
</StyledTwentyLogoContainer> | ||
<StyledContainer size={sizeInNumber}> | ||
<StyledPrimaryLogo src={primaryLogoUrl} /> | ||
{secondaryLogoUrl && ( | ||
<StyledSecondaryLogoContainer size={sizeInNumber}> | ||
<StyledSecondaryLogo size={sizeInNumber} src={secondaryLogoUrl} /> | ||
</StyledSecondaryLogoContainer> | ||
)} | ||
</StyledContainer> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,26 @@ | ||
import { REACT_APP_SERVER_BASE_URL } from '~/config'; | ||
|
||
export const getImageAbsoluteURI = (imageUrl?: string | null) => { | ||
type ImageAbsoluteURI<T extends string | null | undefined> = T extends string | ||
? string | ||
: null; | ||
|
||
export const getImageAbsoluteURI = <T extends string | null | undefined>( | ||
imageUrl: T, | ||
): ImageAbsoluteURI<T> => { | ||
if (!imageUrl) { | ||
return null; | ||
return null as ImageAbsoluteURI<T>; | ||
} | ||
|
||
if (imageUrl?.startsWith('https:') || imageUrl?.startsWith('http:')) { | ||
return imageUrl; | ||
return imageUrl as ImageAbsoluteURI<T>; | ||
} | ||
|
||
const serverFilesUrl = REACT_APP_SERVER_BASE_URL; | ||
const serverFilesUrl = new URL(REACT_APP_SERVER_BASE_URL); | ||
|
||
serverFilesUrl.pathname = `/files/`; | ||
serverFilesUrl.pathname += imageUrl.startsWith('/') | ||
? imageUrl.slice(1) | ||
: imageUrl; | ||
|
||
return `${serverFilesUrl}/files/${imageUrl}`; | ||
return serverFilesUrl.toString() as ImageAbsoluteURI<T>; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ import { GRAY_SCALE } from './GrayScale'; | |
import { ICON } from './Icon'; | ||
import { MODAL } from './Modal'; | ||
import { TEXT } from './Text'; | ||
import { Spacing } from '@ui/theme'; | ||
|
||
export const THEME_COMMON = { | ||
color: COLOR, | ||
|
@@ -15,14 +16,14 @@ export const THEME_COMMON = { | |
animation: ANIMATION, | ||
spacingMultiplicator: 4, | ||
spacing: (...args: number[]) => | ||
args.map((multiplicator) => `${multiplicator * 4}px`).join(' '), | ||
betweenSiblingsGap: `2px`, | ||
args.map((multiplicator) => `${multiplicator * 4}px`).join(' ') as Spacing, | ||
betweenSiblingsGap: `2px` as Spacing, | ||
table: { | ||
horizontalCellMargin: '8px', | ||
checkboxColumnWidth: '32px', | ||
horizontalCellPadding: '8px', | ||
horizontalCellMargin: '8px' as Spacing, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think I've ever seen us doing this. Why do we need it here? Seems like a pattern we want to be careful introducing, if we do it we probably have to do it everywhere There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's beneficial to use this type because it ensures that a prop is a Spacing. This doesn't affect the current usage since Spacing is a string. Therefore, if you use a prop of type string, you can provide a Spacing. However, by opting to use a Spacing, as I did in the components, you ensure that the prop will be a spacing. For example:
This is valid because:
Only one parameter returns a unit, so CSS recognizes it as a px spacing. With a string type instead of a Spacing type, developers might make mistakes, such as using 2rem. So this kind of improvement should increase the DX and the coherence of the global codebase. |
||
checkboxColumnWidth: '32px' as Spacing, | ||
horizontalCellPadding: '8px' as Spacing, | ||
}, | ||
rightDrawerWidth: '500px', | ||
rightDrawerWidth: '500px' as Spacing, | ||
clickableElementBackgroundTransition: 'background 0.1s ease', | ||
lastLayerZIndex: 2147483647, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
import { THEME_LIGHT } from '../constants/ThemeLight'; | ||
|
||
export type ThemeType = typeof THEME_LIGHT; | ||
|
||
export type Spacing = `${number}px`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this feels odd, not sure what issue it was trying to solve but we can probably do better no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It enables the inference of the type. When you use the function with a specified parameter, you can be confident that the return type will be determined. As a result, you won't need to add a condition to verify the return type. For example:
Before:
Now: