From ea9acdaa2e5218a5f0e062996f519b21a635f456 Mon Sep 17 00:00:00 2001 From: Abdul Rahman Date: Thu, 5 Mar 2026 17:34:34 +0530 Subject: [PATCH] feat: add LinkIconWithLinkOverlay component for enhanced link representation in navigation menu Introduced the LinkIconWithLinkOverlay component to display a link icon with an optional favicon overlay in the navigation menu. This component utilizes the getLinkFaviconUrl utility to fetch favicons based on provided links and integrates with the NavigationMenuItemIcon to enhance link item rendering. Additionally, updated NavigationMenuItemIcon to utilize the new component for link items, improving visual consistency and user experience. --- .../components/LinkIconWithLinkOverlay.tsx | 115 ++++++++++++++++++ .../components/NavigationMenuItemIcon.tsx | 14 ++- .../utils/getLinkFaviconUrl.ts | 20 +++ ...onDrawerSectionForWorkspaceItemContent.tsx | 6 +- 4 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/components/LinkIconWithLinkOverlay.tsx create mode 100644 packages/twenty-front/src/modules/navigation-menu-item/utils/getLinkFaviconUrl.ts 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}