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.
This commit is contained in:
+115
@@ -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 (
|
||||
<StyledCompositeContainer>
|
||||
<StyledMainIconWrapper
|
||||
$backgroundColor={linkStyle.backgroundColor}
|
||||
$borderColor={linkStyle.borderColor}
|
||||
$noBackgroundOrBorder={showFavicon}
|
||||
>
|
||||
{showFavicon ? (
|
||||
<StyledFaviconImage
|
||||
src={faviconUrl}
|
||||
alt=""
|
||||
onError={() => setFaviconFailed(true)}
|
||||
/>
|
||||
) : (
|
||||
<DefaultIcon
|
||||
size="14px"
|
||||
stroke={theme.icon.stroke.md}
|
||||
color={linkStyle.iconColor}
|
||||
/>
|
||||
)}
|
||||
</StyledMainIconWrapper>
|
||||
<StyledLinkOverlay $backgroundColor={theme.grayScale.gray4}>
|
||||
<LinkIcon
|
||||
size="12px"
|
||||
stroke={theme.icon.stroke.md}
|
||||
color={theme.grayScale.gray10}
|
||||
/>
|
||||
</StyledLinkOverlay>
|
||||
</StyledCompositeContainer>
|
||||
);
|
||||
};
|
||||
+13
-1
@@ -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 (
|
||||
<LinkIconWithLinkOverlay
|
||||
link={navigationMenuItem.link}
|
||||
LinkIcon={IconLink}
|
||||
DefaultIcon={IconWorld}
|
||||
color={getEffectiveNavigationMenuItemColor(navigationMenuItem)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const iconToUse =
|
||||
StandardIcon ??
|
||||
(navigationMenuItem.Icon ? getIcon(navigationMenuItem.Icon) : undefined);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
+2
-4
@@ -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={() => <NavigationMenuItemIcon navigationMenuItem={linkItem} />}
|
||||
active={false}
|
||||
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
|
||||
isDragging={isDragging}
|
||||
|
||||
Reference in New Issue
Block a user