diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/LinkIconWithLinkOverlay.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/LinkIconWithLinkOverlay.tsx new file mode 100644 index 00000000000..3a7c0da6a11 --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/LinkIconWithLinkOverlay.tsx @@ -0,0 +1,115 @@ +import { styled } from '@linaria/react'; +import { useContext, useState } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import type { IconComponent } from 'twenty-ui/display'; +import { ThemeContext } from 'twenty-ui/theme'; + +import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorLink'; +import { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/utils/get-navigation-menu-item-icon-style-from-color'; +import { getLinkFaviconUrl } from '@/navigation-menu-item/utils/getLinkFaviconUrl'; + +const StyledCompositeContainer = styled.div` + align-items: center; + border-radius: 4px; + box-sizing: border-box; + display: flex; + flex-shrink: 0; + height: 16px; + justify-content: center; + position: relative; + width: 16px; +`; + +const StyledMainIconWrapper = styled.div<{ + $backgroundColor: string; + $borderColor?: string; + $noBackgroundOrBorder?: boolean; +}>` + position: absolute; + inset: 0; + border-radius: 4px; + box-sizing: border-box; + background-color: ${({ $backgroundColor, $noBackgroundOrBorder }) => + $noBackgroundOrBorder ? 'transparent' : $backgroundColor}; + display: flex; + align-items: center; + justify-content: center; + border: ${({ $borderColor, $noBackgroundOrBorder }) => + $noBackgroundOrBorder || !$borderColor + ? 'none' + : `1px solid ${$borderColor}`}; + overflow: hidden; +`; + +const StyledFaviconImage = styled.img` + height: 100%; + object-fit: contain; + width: 100%; +`; + +const StyledLinkOverlay = styled.div<{ $backgroundColor: string }>` + align-items: center; + background-color: ${({ $backgroundColor }) => $backgroundColor}; + border-radius: 4px; + bottom: -7px; + display: flex; + height: 14px; + justify-content: center; + position: absolute; + right: -7px; + width: 14px; +`; + +export type LinkIconWithLinkOverlayProps = { + link: string | null | undefined; + LinkIcon: IconComponent; + DefaultIcon: IconComponent; + color?: string | null; +}; + +export const LinkIconWithLinkOverlay = ({ + link, + LinkIcon, + DefaultIcon, + color: navItemColor, +}: LinkIconWithLinkOverlayProps) => { + const { theme } = useContext(ThemeContext); + const [faviconFailed, setFaviconFailed] = useState(false); + const faviconUrl = getLinkFaviconUrl(link); + const showFavicon = isDefined(faviconUrl) && !faviconFailed; + const linkStyle = getNavigationMenuItemIconStyleFromColor( + theme, + navItemColor ?? DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK, + ); + + return ( + + + {showFavicon ? ( + setFaviconFailed(true)} + /> + ) : ( + + )} + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemIcon.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemIcon.tsx index 82b27d19536..eeb1f011e19 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemIcon.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/components/NavigationMenuItemIcon.tsx @@ -1,10 +1,11 @@ import { isNonEmptyString } from '@sniptt/guards'; import { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; -import { Avatar, useIcons } from 'twenty-ui/display'; +import { Avatar, IconLink, IconWorld, useIcons } from 'twenty-ui/display'; import { ThemeContext } from 'twenty-ui/theme'; import { FeatureFlagKey } from '~/generated-metadata/graphql'; +import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/components/LinkIconWithLinkOverlay'; import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer'; import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay'; import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType'; @@ -65,6 +66,17 @@ export const NavigationMenuItemIcon = ({ ); } + if (navigationMenuItem.itemType === NavigationMenuItemType.LINK) { + return ( + + ); + } + const iconToUse = StandardIcon ?? (navigationMenuItem.Icon ? getIcon(navigationMenuItem.Icon) : undefined); diff --git a/packages/twenty-front/src/modules/navigation-menu-item/utils/getLinkFaviconUrl.ts b/packages/twenty-front/src/modules/navigation-menu-item/utils/getLinkFaviconUrl.ts new file mode 100644 index 00000000000..10f39310d3c --- /dev/null +++ b/packages/twenty-front/src/modules/navigation-menu-item/utils/getLinkFaviconUrl.ts @@ -0,0 +1,20 @@ +import { getLogoUrlFromDomainName } from 'twenty-shared/utils'; + +export const getLinkFaviconUrl = ( + link: string | null | undefined, +): string | undefined => { + const trimmed = (link ?? '').trim(); + if (!trimmed) { + return undefined; + } + const normalized = + trimmed.startsWith('http://') || trimmed.startsWith('https://') + ? trimmed + : `https://${trimmed}`; + try { + const hostname = new URL(normalized).hostname; + return getLogoUrlFromDomainName(hostname); + } catch { + return undefined; + } +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent.tsx index 787685d3143..9dc337eb3f5 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent.tsx @@ -1,8 +1,7 @@ import { lazy, Suspense } from 'react'; -import { IconLink } from 'twenty-ui/display'; +import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon'; import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState'; -import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor'; import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem'; import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems'; import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem'; @@ -101,8 +100,7 @@ export const WorkspaceSectionItemContent = ({ onClick={ isNavigationMenuInEditMode ? editModeProps.onEditModeClick : undefined } - Icon={IconLink} - iconColor={getEffectiveNavigationMenuItemColor(linkItem) ?? undefined} + Icon={() => } active={false} isSelectedInEditMode={editModeProps.isSelectedInEditMode} isDragging={isDragging}