fix: hover card styling

This commit is contained in:
Apoorv Mishra
2023-07-25 19:31:27 +05:30
parent 03ebca2f0c
commit 31f8a3fb44
5 changed files with 164 additions and 132 deletions

View File

@@ -1,9 +1,16 @@
import { transparentize } from "polished";
import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { s } from "@shared/styles";
import Text from "~/components/Text";
export const CARD_PADDING = 16;
export const CARD_WIDTH = 375;
export const THUMBNAIL_HEIGHT = 200;
const StyledText = styled(Text)`
margin-bottom: 0;
padding-top: 0.125em;
@@ -11,7 +18,11 @@ const StyledText = styled(Text)`
export const Preview = styled(Link)`
cursor: var(--pointer);
margin-bottom: 0;
border-radius: 4px;
box-shadow: 0 30px 90px -20px rgba(0, 0, 0, 0.3),
0 0 1px 1px rgba(0, 0, 0, 0.05);
overflow: hidden;
position: absolute;
${(props) => (!props.to ? "pointer-events: none;" : "")}
`;
@@ -21,7 +32,7 @@ export const Title = styled.h2`
color: ${s("text")};
`;
export const Info: React.FC = ({ children }) => (
export const Info: React.FC = ({ children }: { children: string }) => (
<StyledText type="tertiary" size="xsmall">
{children}
</StyledText>
@@ -34,3 +45,57 @@ export const Description: React.FC = styled(StyledText)`
export const DescriptionContainer = styled.div`
margin-top: 0.5em;
`;
export const CardContent = styled.div`
overflow: hidden;
max-height: 20.5em;
user-select: none;
`;
// &:after — gradient mask for overflow text
export const Card = styled.div<{ fadeOut?: boolean; $borderRadius?: string }>`
backdrop-filter: blur(10px);
background: ${(props) => props.theme.menuBackground};
padding: ${CARD_PADDING}px;
width: ${CARD_WIDTH}px;
font-size: 0.9em;
position: relative;
.placeholder,
.heading-anchor {
display: none;
}
// fills the gap between the card and pointer to avoid a dead zone
&::before {
content: "";
position: absolute;
top: -10px;
left: 0;
right: 0;
height: 10px;
}
${(props) =>
props.fadeOut !== false
? `&:after {
content: "";
display: block;
position: absolute;
pointer-events: none;
background: linear-gradient(
90deg,
${transparentize(1, props.theme.menuBackground)} 0%,
${transparentize(1, props.theme.menuBackground)} 75%,
${props.theme.menuBackground} 90%
);
bottom: 0;
left: 0;
right: 0;
height: 1.7em;
border-bottom: 16px solid ${props.theme.menuBackground};
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}`
: ""}
`;

View File

@@ -1,5 +1,4 @@
import { m } from "framer-motion";
import { transparentize } from "polished";
import * as React from "react";
import { Portal } from "react-portal";
import styled from "styled-components";
@@ -12,14 +11,13 @@ import useOnClickOutside from "~/hooks/useOnClickOutside";
import useRequest from "~/hooks/useRequest";
import useStores from "~/hooks/useStores";
import { client } from "~/utils/ApiClient";
import { CARD_WIDTH, CARD_PADDING } from "./Components";
import HoverPreviewDocument from "./HoverPreviewDocument";
import HoverPreviewLink from "./HoverPreviewLink";
import HoverPreviewMention from "./HoverPreviewMention";
const DELAY_OPEN = 300;
const DELAY_CLOSE = 600;
const CARD_PADDING = 16;
const CARD_MAX_WIDTH = 375;
type Props = {
/* The HTML element that is being hovered over */
@@ -123,10 +121,7 @@ function HoverPreviewInternal({ element, onClose }: Props) {
const elemBounds = element.getBoundingClientRect();
const cardBounds = cardRef.current?.getBoundingClientRect();
const left = cardBounds
? Math.min(
elemBounds.left,
window.innerWidth - CARD_PADDING - CARD_MAX_WIDTH
)
? Math.min(elemBounds.left, window.innerWidth - CARD_PADDING - CARD_WIDTH)
: elemBounds.left;
const leftOffset = elemBounds.left - left;
@@ -151,33 +146,29 @@ function HoverPreviewInternal({ element, onClose }: Props) {
initial={{ opacity: 0, y: -20, pointerEvents: "none" }}
animate={{ opacity: 1, y: 0, pointerEvents: "auto" }}
>
<Card fadeOut={data.type !== UnfurlType.Mention}>
<CardContent>
{data.type === UnfurlType.Mention ? (
<HoverPreviewMention
url={data.thumbnailUrl}
title={data.title}
info={data.meta.info}
color={data.meta.color}
/>
) : data.type === UnfurlType.Document ? (
<HoverPreviewDocument
id={data.meta.id}
url={data.url}
title={data.title}
description={data.description}
info={data.meta.info}
/>
) : (
<HoverPreviewLink
url={data.url}
thumbnailUrl={data.thumbnailUrl}
title={data.title}
description={data.description}
/>
)}
</CardContent>
</Card>
{data.type === UnfurlType.Mention ? (
<HoverPreviewMention
url={data.thumbnailUrl}
title={data.title}
info={data.meta.info}
color={data.meta.color}
/>
) : data.type === UnfurlType.Document ? (
<HoverPreviewDocument
id={data.meta.id}
url={data.url}
title={data.title}
description={data.description}
info={data.meta.info}
/>
) : (
<HoverPreviewLink
url={data.url}
thumbnailUrl={data.thumbnailUrl}
title={data.title}
description={data.description}
/>
)}
<Pointer offset={leftOffset + elemBounds.width / 2} />
</Animate>
) : null}
@@ -202,64 +193,6 @@ const Animate = styled(m.div)`
}
`;
const CardContent = styled.div`
overflow: hidden;
max-height: 20.5em;
user-select: none;
`;
// &:after — gradient mask for overflow text
const Card = styled.div<{ fadeOut?: boolean }>`
backdrop-filter: blur(10px);
background: ${(props) => props.theme.menuBackground};
border-radius: 4px;
box-shadow: 0 30px 90px -20px rgba(0, 0, 0, 0.3),
0 0 1px 1px rgba(0, 0, 0, 0.05);
padding: ${CARD_PADDING}px;
min-width: 350px;
max-width: ${CARD_MAX_WIDTH}px;
font-size: 0.9em;
position: relative;
.placeholder,
.heading-anchor {
display: none;
}
// fills the gap between the card and pointer to avoid a dead zone
&::before {
content: "";
position: absolute;
top: -10px;
left: 0;
right: 0;
height: 10px;
}
${(props) =>
props.fadeOut !== false
? `&:after {
content: "";
display: block;
position: absolute;
pointer-events: none;
background: linear-gradient(
90deg,
${transparentize(1, props.theme.menuBackground)} 0%,
${transparentize(1, props.theme.menuBackground)} 75%,
${props.theme.menuBackground} 90%
);
bottom: 0;
left: 0;
right: 0;
height: 1.7em;
border-bottom: 16px solid ${props.theme.menuBackground};
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}`
: ""}
`;
const Position = styled.div<{ fixed?: boolean; top?: number; left?: number }>`
margin-top: 10px;
position: ${({ fixed }) => (fixed ? "fixed" : "absolute")};

View File

@@ -1,7 +1,14 @@
import * as React from "react";
import Editor from "~/components/Editor";
import Flex from "~/components/Flex";
import { Preview, Title, Info, DescriptionContainer } from "./Components";
import {
Preview,
Title,
Info,
DescriptionContainer,
Card,
CardContent,
} from "./Components";
type Props = {
/** Document id associated with the editor, if any */
@@ -19,20 +26,24 @@ type Props = {
function HoverPreviewDocument({ id, url, title, info, description }: Props) {
return (
<Preview to={url}>
<Flex column>
<Title>{title}</Title>
<Info>{info}</Info>
<DescriptionContainer>
<React.Suspense fallback={<div />}>
<Editor
key={id}
defaultValue={description}
embedsDisabled
readOnly
/>
</React.Suspense>
</DescriptionContainer>
</Flex>
<Card>
<CardContent>
<Flex column>
<Title>{title}</Title>
<Info>{info}</Info>
<DescriptionContainer>
<React.Suspense fallback={<div />}>
<Editor
key={id}
defaultValue={description}
embedsDisabled
readOnly
/>
</React.Suspense>
</DescriptionContainer>
</Flex>
</CardContent>
</Card>
</Preview>
);
}

View File

@@ -1,7 +1,16 @@
import * as React from "react";
import styled from "styled-components";
import Img from "@shared/editor/components/Img";
import Flex from "~/components/Flex";
import { Preview, Title, Description } from "./Components";
import {
Preview,
Title,
Description,
Card,
CardContent,
CARD_WIDTH,
THUMBNAIL_HEIGHT,
} from "./Components";
type Props = {
/** Link url */
@@ -16,16 +25,26 @@ type Props = {
function HoverPreviewLink({ url, thumbnailUrl, title, description }: Props) {
return (
<Preview to={url}>
<Flex gap={12} column>
<Img src={thumbnailUrl} alt={""} />
<Flex column>
<Title>{title}</Title>
<Description>{description}</Description>
</Flex>
<Preview to={{ pathname: url }} target="_blank" rel="noopener noreferrer">
<Flex column>
{thumbnailUrl ? <Thumbnail src={thumbnailUrl} alt={""} /> : null}
<Card>
<CardContent>
<Flex column>
<Title>{title}</Title>
<Description>{description}</Description>
</Flex>
</CardContent>
</Card>
</Flex>
</Preview>
);
}
const Thumbnail = styled(Img)`
object-fit: cover;
max-width: ${CARD_WIDTH}px;
height: ${THUMBNAIL_HEIGHT}px;
`;
export default HoverPreviewLink;

View File

@@ -2,7 +2,7 @@ import * as React from "react";
import Avatar from "~/components/Avatar";
import { AvatarSize } from "~/components/Avatar/Avatar";
import Flex from "~/components/Flex";
import { Preview, Title, Info } from "./Components";
import { Preview, Title, Info, Card, CardContent } from "./Components";
type Props = {
/** Resource url, avatar url in case of user mention */
@@ -18,20 +18,24 @@ type Props = {
function HoverPreviewMention({ url, title, info, color }: Props) {
return (
<Preview to="">
<Flex gap={12}>
<Avatar
model={{
avatarUrl: url,
initial: title ? title[0] : "?",
color,
}}
size={AvatarSize.XLarge}
/>
<Flex column>
<Title>{title}</Title>
<Info>{info}</Info>
</Flex>
</Flex>
<Card fadeOut={false}>
<CardContent>
<Flex gap={12}>
<Avatar
model={{
avatarUrl: url,
initial: title ? title[0] : "?",
color,
}}
size={AvatarSize.XLarge}
/>
<Flex column>
<Title>{title}</Title>
<Info>{info}</Info>
</Flex>
</Flex>
</CardContent>
</Card>
</Preview>
);
}