Files
omnivore/packages/web/components/patterns/LibraryCards/LibraryListCard.tsx
Lily Mara 01856c3099 Fix hover behavior of library list hover actions
In the library list view, there is a toolbar of actions that appears over each
entry when you hover over it. This change fixes the hover behavior of this
toolbar so that you do not need to move the mouse cursor out and back into the
library list item after archiving/deleting a previous item in order to
re-activate the toolbar.
2023-10-26 23:59:36 -07:00

364 lines
9.2 KiB
TypeScript

import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives'
import { LabelChip } from '../../elements/LabelChip'
import type { LinkedItemCardProps } from './CardTypes'
import { useCallback, useState } from 'react'
import Link from 'next/link'
import {
AuthorInfoStyle,
CardCheckbox,
LibraryItemMetadata,
MetaStyle,
siteName,
TitleStyle,
MenuStyle,
FLAIR_ICON_NAMES,
} from './LibraryCardStyles'
import { sortedLabels } from '../../../lib/labelsSort'
import { LIBRARY_LEFT_MENU_WIDTH } from '../../templates/homeFeed/LibraryFilterMenu'
import { LibraryHoverActions } from './LibraryHoverActions'
import {
useHover,
useFloating,
useInteractions,
size,
offset,
autoUpdate,
} from '@floating-ui/react'
import { CardMenu } from '../CardMenu'
import { DotsThree } from 'phosphor-react'
import { isTouchScreenDevice } from '../../../lib/deviceType'
import { CoverImage } from '../../elements/CoverImage'
import { ProgressBar } from '../../elements/ProgressBar'
import { theme } from '../../tokens/stitches.config'
import { FallbackImage } from './FallbackImage'
import { useRouter } from 'next/router'
import { LoadingBar } from '../../elements/LoadingBar'
export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
const router = useRouter()
const [isOpen, setIsOpen] = useState(false)
const { refs, floatingStyles, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
middleware: [
offset({
mainAxis: -25,
}),
size(),
],
placement: 'top-end',
whileElementsMounted: autoUpdate,
})
const hover = useHover(context)
const { getReferenceProps, getFloatingProps } = useInteractions([hover])
return (
<VStack
ref={refs.setReference}
{...getReferenceProps()}
css={{
px: '20px',
pl: '10px',
py: '15px',
height: '100%',
cursor: 'pointer',
gap: '10px',
border: '1px solid $grayBorder',
borderBottom: 'none',
borderRadius: '6px',
width: '100vw',
'@media (min-width: 768px)': {
width: `calc(100vw - ${LIBRARY_LEFT_MENU_WIDTH})`,
},
'@media (min-width: 930px)': {
width: '640px',
},
'@media (min-width: 1280px)': {
width: '1000px',
},
'@media (min-width: 1600px)': {
width: '1340px',
},
boxShadow:
'0 1px 3px 0 rgba(0, 0, 0, 0.1),0 1px 2px 0 rgba(0, 0, 0, 0.06);',
'@media (max-width: 930px)': {
boxShadow: 'unset',
borderRadius: 'unset',
},
}}
alignment="start"
distribution="start"
onClick={(event) => {
if (event.metaKey || event.ctrlKey) {
window.open(
`/${props.viewer.profile.username}/${props.item.slug}`,
'_blank'
)
} else {
router.push(`/${props.viewer.profile.username}/${props.item.slug}`)
}
}}
>
{!isTouchScreenDevice() && (
<Box
ref={refs.setFloating}
style={{ ...floatingStyles, zIndex: 3 }}
{...getFloatingProps()}
>
<LibraryHoverActions
item={props.item}
viewer={props.viewer}
handleAction={props.handleAction}
isHovered={isOpen ?? false}
/>
</Box>
)}
<LibraryListCardContent {...props} isHovered={isOpen} />
</VStack>
)
}
type LoadingBarOverlayProps = {
top: number
width: string
bottomRadius: string
fillColor?: string
percentFill?: number
}
type ProgressBarOverlayProps = {
top: number
width: string
value: number
bottomRadius: string
}
export const LoadingBarOverlay = (
props: LoadingBarOverlayProps
): JSX.Element => {
return (
<Box
css={{
position: 'absolute',
width: props.width,
top: props.top,
borderBottomLeftRadius: props.bottomRadius,
borderBottomRightRadius: props.bottomRadius,
overflow: 'clip',
zIndex: 2,
}}
>
<LoadingBar
fillColor={props.fillColor ?? theme.colors.thProgressFg.toString()}
backgroundColor="rgba(217, 217, 217, 0.65)"
borderRadius={'2px'}
percentFill={props.percentFill}
/>
</Box>
)
}
export const ProgressBarOverlay = (
props: ProgressBarOverlayProps
): JSX.Element => {
return (
<Box
css={{
position: 'absolute',
width: props.width,
top: props.top,
borderBottomLeftRadius: props.bottomRadius,
borderBottomRightRadius: props.bottomRadius,
overflow: 'clip',
zIndex: 2,
}}
>
<ProgressBar
fillPercentage={props.value}
fillColor={theme.colors.thProgressFg.toString()}
backgroundColor="rgba(217, 217, 217, 0.65)"
borderRadius={'2px'}
/>
</Box>
)
}
type ListImageProps = {
src?: string
title?: string
readingProgress?: number
isLoading?: boolean
}
const ListImage = (props: ListImageProps): JSX.Element => {
const [displayFallback, setDisplayFallback] = useState(props.src == undefined)
return (
<>
{props.isLoading && (
<LoadingBarOverlay
width="55px"
top={50}
bottomRadius="4px"
fillColor={'rgba(60, 179, 113, 1)'}
percentFill={30}
/>
)}
{(props.readingProgress ?? 0) > 0 && !props.isLoading && (
<ProgressBarOverlay
width="55px"
top={50}
bottomRadius="4px"
value={props.readingProgress ?? 0}
/>
)}
{displayFallback ? (
<FallbackImage
title={props.title ?? 'Omnivore Fallback'}
width="55px"
height="55px"
fontSize="36pt"
/>
) : (
<CoverImage
src={props.src}
width={55}
height={55}
css={{
bg: '$thBackground',
borderRadius: '4px',
}}
onError={() => {
setDisplayFallback(true)
}}
/>
)}
</>
)
}
export function LibraryListCardContent(
props: LinkedItemCardProps
): JSX.Element {
const [menuOpen, setMenuOpen] = useState(false)
const { isChecked, setIsChecked, item } = props
const originText = siteName(props.item.originalArticleUrl, props.item.url)
const handleCheckChanged = useCallback(() => {
setIsChecked(item.id, !isChecked)
}, [isChecked, setIsChecked, item])
return (
<HStack css={{ gap: '15px', width: '100%' }}>
<SpanBox
css={{
display: 'flex',
m: '0px',
mt: '0px',
p: '0px',
ml: '4px',
lineHeight: '1',
'> input': {
p: '0px',
m: '0px',
},
}}
>
<CardCheckbox
isChecked={props.isChecked}
handleChanged={handleCheckChanged}
/>
</SpanBox>
<Box css={{ position: 'relative', width: '55px' }}>
<ListImage
src={props.item.image}
title={props.item.title}
readingProgress={item.readingProgressPercent}
isLoading={props.isLoading}
/>
</Box>
<VStack
alignment="start"
distribution="start"
css={{
height: '100%',
width: '100%',
lineHeight: 1,
gap: '3px',
position: 'relative',
}}
>
<Box
css={{
...MenuStyle,
position: 'absolute',
top: -10,
right: -10,
m: '5px',
visibility: menuOpen ? 'visible' : 'hidden',
'@media (hover: none)': {
visibility: 'unset',
},
}}
>
<CardMenu
item={props.item}
viewer={props.viewer}
onOpenChange={(open) => setMenuOpen(open)}
actionHandler={props.handleAction}
triggerElement={
<DotsThree size={25} weight="bold" color="#ADADAD" />
}
/>
</Box>
<HStack
css={{
...MetaStyle,
}}
distribution="start"
>
<LibraryItemMetadata item={props.item} showProgress={true} />
</HStack>
<Box css={{ ...TitleStyle }}>{props.item.title}</Box>
{(props.item.author?.length ?? 0 + originText.length) > 0 && (
<SpanBox
css={{
...AuthorInfoStyle,
}}
>
{props.item.author}
{props.item.author && originText && ' | '}
{originText}
</SpanBox>
)}
<HStack
distribution="start"
alignment="start"
// The two pixels here are to account for the label margin
css={{ width: '100%', ml: '-2px', mt: '5px' }}
>
<HStack
css={{
display: 'block',
}}
>
{sortedLabels(props.item.labels)
.filter(
({ name }) =>
FLAIR_ICON_NAMES.indexOf(name.toLocaleLowerCase()) == -1
)
.map(({ name, color }, index) => (
<LabelChip key={index} text={name || ''} color={color} />
))}
</HStack>
</HStack>
</VStack>
</HStack>
)
}