mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Add checkboxes on library cards in multi select mode
This commit is contained in:
parent
e6eadf75f0
commit
d4aa715b5b
5 changed files with 283 additions and 257 deletions
|
|
@ -2,39 +2,21 @@ import React from 'react'
|
|||
import { styled } from '@stitches/react'
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
|
||||
import { CheckIcon } from './images/CheckIcon'
|
||||
import { Box } from './LayoutPrimitives'
|
||||
import { Check } from 'phosphor-react'
|
||||
|
||||
// const Checkbox = styled(CheckboxPrimitive.Root, {
|
||||
// all: 'unset',
|
||||
// width: 15,
|
||||
// height: 15,
|
||||
// borderRadius: 2,
|
||||
// display: 'flex',
|
||||
// alignItems: 'center',
|
||||
// justifyContent: 'center',
|
||||
// })
|
||||
|
||||
// const CheckboxIndicator = styled(CheckboxPrimitive.Indicator, {
|
||||
// color: '#FFFFFF',
|
||||
// })
|
||||
|
||||
const CheckboxRoot = styled(CheckboxPrimitive.Root, {
|
||||
const Checkbox = styled(CheckboxPrimitive.Root, {
|
||||
all: 'unset',
|
||||
backgroundColor: 'white',
|
||||
width: 25,
|
||||
height: 25,
|
||||
borderRadius: 4,
|
||||
width: 16,
|
||||
height: 16,
|
||||
minWidth: 16,
|
||||
minHeight: 16,
|
||||
borderRadius: 3,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: `0 2px 10px black`,
|
||||
'&:hover': { backgroundColor: 'violet' },
|
||||
'&:focus': { boxShadow: `0 0 0 2px black` },
|
||||
})
|
||||
|
||||
const CheckboxIndicator = styled(CheckboxPrimitive.Indicator, {
|
||||
color: 'violet',
|
||||
color: '#FFFFFF',
|
||||
})
|
||||
|
||||
export const CheckboxComponent: React.FC<{
|
||||
|
|
@ -44,15 +26,18 @@ export const CheckboxComponent: React.FC<{
|
|||
const toggleChecked = () => setChecked(!checked)
|
||||
|
||||
return (
|
||||
<form>
|
||||
<Box css={{ alignItems: 'center' }}>
|
||||
<CheckboxRoot defaultChecked id="c1">
|
||||
<CheckboxIndicator>
|
||||
<Check size={20} color="black" />
|
||||
</CheckboxIndicator>
|
||||
</CheckboxRoot>
|
||||
</Box>
|
||||
</form>
|
||||
<Checkbox
|
||||
css={{
|
||||
border: checked ? '2px solid #F9D354' : '2px solid #3F3E3C4D',
|
||||
backgroundColor: checked ? '#F9D354' : '#FFFFFF',
|
||||
}}
|
||||
checked={checked}
|
||||
onCheckedChange={toggleChecked}
|
||||
>
|
||||
<CheckboxIndicator>
|
||||
<CheckIcon />
|
||||
</CheckboxIndicator>
|
||||
</Checkbox>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,4 +21,6 @@ export type LinkedItemCardProps = {
|
|||
viewer: UserBasicData
|
||||
inMultiSelect: boolean
|
||||
handleAction: (action: LinkedItemCardAction) => void
|
||||
|
||||
isHovered?: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,3 +146,18 @@ export function LibraryItemMetadata(
|
|||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
type CardCheckBoxProps = {
|
||||
isChecked: boolean
|
||||
handleChanged: () => void
|
||||
}
|
||||
|
||||
export function CardCheckbox(props: CardCheckBoxProps): JSX.Element {
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.isChecked}
|
||||
onChange={props.handleChanged}
|
||||
></input>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,22 +4,21 @@ import type { LinkedItemCardProps } from './CardTypes'
|
|||
import { CoverImage } from '../../elements/CoverImage'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { DotsThreeVertical } from 'phosphor-react'
|
||||
import Link from 'next/link'
|
||||
import { CardMenu } from '../CardMenu'
|
||||
import {
|
||||
AuthorInfoStyle,
|
||||
CardCheckbox,
|
||||
DescriptionStyle,
|
||||
LibraryItemMetadata,
|
||||
MenuStyle,
|
||||
MetaStyle,
|
||||
siteName,
|
||||
timeAgo,
|
||||
TitleStyle,
|
||||
} from './LibraryCardStyles'
|
||||
import { sortedLabels } from '../../../lib/labelsSort'
|
||||
import CheckboxComponent from '../../elements/Checkbox'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
|
|
@ -53,12 +52,8 @@ export function ProgressBar(props: ProgressBarProps): JSX.Element {
|
|||
)
|
||||
}
|
||||
|
||||
// Component
|
||||
export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
|
||||
const originText = siteName(props.item.originalArticleUrl, props.item.url)
|
||||
|
||||
return (
|
||||
<VStack
|
||||
|
|
@ -88,145 +83,156 @@ export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element {
|
|||
setIsHovered(false)
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={`${props.viewer.profile.username}/${props.item.slug}`}
|
||||
passHref
|
||||
>
|
||||
<a
|
||||
{props.inMultiSelect ? (
|
||||
<LibraryGridCardContent {...props} isHovered={isHovered} />
|
||||
) : (
|
||||
<Link
|
||||
href={`${props.viewer.profile.username}/${props.item.slug}`}
|
||||
style={{ textDecoration: 'unset', width: '100%', height: '100%' }}
|
||||
tabIndex={-1}
|
||||
passHref
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
...MetaStyle,
|
||||
minHeight: '35px',
|
||||
}}
|
||||
distribution="start"
|
||||
<a
|
||||
href={`${props.viewer.profile.username}/${props.item.slug}`}
|
||||
style={{ textDecoration: 'unset', width: '100%', height: '100%' }}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<LibraryItemMetadata item={props.item} />
|
||||
{props.inMultiSelect ? (
|
||||
<SpanBox css={{ marginLeft: 'auto' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
}}
|
||||
></input>
|
||||
</SpanBox>
|
||||
) : (
|
||||
<Box
|
||||
css={{
|
||||
...MenuStyle,
|
||||
visibility: isHovered || menuOpen ? 'unset' : 'hidden',
|
||||
'@media (hover: none)': {
|
||||
visibility: 'unset',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CardMenu
|
||||
item={props.item}
|
||||
viewer={props.viewer}
|
||||
onOpenChange={(open) => setMenuOpen(open)}
|
||||
actionHandler={props.handleAction}
|
||||
triggerElement={
|
||||
<DotsThreeVertical
|
||||
size={25}
|
||||
weight="bold"
|
||||
color="#ADADAD"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</HStack>
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
...TitleStyle,
|
||||
height: '42px',
|
||||
}}
|
||||
>
|
||||
{props.item.title}
|
||||
</Box>
|
||||
<Box css={DescriptionStyle}>{props.item.description}</Box>
|
||||
<SpanBox
|
||||
css={{
|
||||
...AuthorInfoStyle,
|
||||
mt: '10px',
|
||||
}}
|
||||
>
|
||||
{props.item.author}
|
||||
{props.item.author && originText && ' | '}
|
||||
<SpanBox css={{ textDecoration: 'underline' }}>
|
||||
{originText}
|
||||
</SpanBox>
|
||||
</SpanBox>
|
||||
<SpanBox
|
||||
css={{
|
||||
pt: '20px',
|
||||
pr: '10px',
|
||||
pb: '20px',
|
||||
width: '100%',
|
||||
m: '0px',
|
||||
}}
|
||||
>
|
||||
<ProgressBar
|
||||
fillPercentage={props.item.readingProgressPercent}
|
||||
fillColor="$thProgressFg"
|
||||
backgroundColor="$thBorderSubtle"
|
||||
borderRadius="5px"
|
||||
/>
|
||||
</SpanBox>
|
||||
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
css={{ width: '100%', minHeight: '50px' }}
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
display: 'block',
|
||||
minHeight: '35px',
|
||||
}}
|
||||
>
|
||||
{sortedLabels(props.item.labels).map(
|
||||
({ name, color }, index) => (
|
||||
<LabelChip key={index} text={name || ''} color={color} />
|
||||
)
|
||||
)}
|
||||
</HStack>
|
||||
<VStack
|
||||
css={{
|
||||
width: '80px',
|
||||
height: '100%',
|
||||
marginLeft: 'auto',
|
||||
flexGrow: '1',
|
||||
}}
|
||||
alignment="end"
|
||||
distribution="end"
|
||||
>
|
||||
{props.item.image && (
|
||||
<CoverImage
|
||||
src={props.item.image}
|
||||
alt="Link Preview Image"
|
||||
width={50}
|
||||
height={50}
|
||||
css={{ borderRadius: '8px' }}
|
||||
onError={(e) => {
|
||||
;(e.target as HTMLElement).style.display = 'none'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</a>
|
||||
</Link>
|
||||
<LibraryGridCardContent {...props} isHovered={isHovered} />
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const LibraryGridCardContent = (props: LinkedItemCardProps): JSX.Element => {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const [isChecked, setIsChecked] = useState(false)
|
||||
|
||||
const originText = siteName(props.item.originalArticleUrl, props.item.url)
|
||||
|
||||
const handleCheckChanged = useCallback(() => {
|
||||
setIsChecked(!isChecked)
|
||||
}, [isChecked])
|
||||
|
||||
return (
|
||||
<>
|
||||
<HStack
|
||||
css={{
|
||||
...MetaStyle,
|
||||
minHeight: '35px',
|
||||
}}
|
||||
distribution="start"
|
||||
>
|
||||
<LibraryItemMetadata item={props.item} />
|
||||
{props.inMultiSelect ? (
|
||||
<SpanBox css={{ marginLeft: 'auto' }}>
|
||||
<CardCheckbox
|
||||
isChecked={isChecked}
|
||||
handleChanged={handleCheckChanged}
|
||||
/>
|
||||
</SpanBox>
|
||||
) : (
|
||||
<Box
|
||||
css={{
|
||||
...MenuStyle,
|
||||
visibility: props.isHovered || menuOpen ? 'unset' : 'hidden',
|
||||
'@media (hover: none)': {
|
||||
visibility: 'unset',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CardMenu
|
||||
item={props.item}
|
||||
viewer={props.viewer}
|
||||
onOpenChange={(open) => setMenuOpen(open)}
|
||||
actionHandler={props.handleAction}
|
||||
triggerElement={
|
||||
<DotsThreeVertical size={25} weight="bold" color="#ADADAD" />
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</HStack>
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
...TitleStyle,
|
||||
height: '42px',
|
||||
}}
|
||||
>
|
||||
{props.item.title}
|
||||
</Box>
|
||||
<Box css={DescriptionStyle}>{props.item.description}</Box>
|
||||
<SpanBox
|
||||
css={{
|
||||
...AuthorInfoStyle,
|
||||
mt: '10px',
|
||||
}}
|
||||
>
|
||||
{props.item.author}
|
||||
{props.item.author && originText && ' | '}
|
||||
<SpanBox css={{ textDecoration: 'underline' }}>{originText}</SpanBox>
|
||||
</SpanBox>
|
||||
<SpanBox
|
||||
css={{
|
||||
pt: '20px',
|
||||
pr: '10px',
|
||||
pb: '20px',
|
||||
width: '100%',
|
||||
m: '0px',
|
||||
}}
|
||||
>
|
||||
<ProgressBar
|
||||
fillPercentage={props.item.readingProgressPercent}
|
||||
fillColor="$thProgressFg"
|
||||
backgroundColor="$thBorderSubtle"
|
||||
borderRadius="5px"
|
||||
/>
|
||||
</SpanBox>
|
||||
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="start"
|
||||
css={{ width: '100%', minHeight: '50px' }}
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
display: 'block',
|
||||
minHeight: '35px',
|
||||
}}
|
||||
>
|
||||
{sortedLabels(props.item.labels).map(({ name, color }, index) => (
|
||||
<LabelChip key={index} text={name || ''} color={color} />
|
||||
))}
|
||||
</HStack>
|
||||
<VStack
|
||||
css={{
|
||||
width: '80px',
|
||||
height: '100%',
|
||||
marginLeft: 'auto',
|
||||
flexGrow: '1',
|
||||
}}
|
||||
alignment="end"
|
||||
distribution="end"
|
||||
>
|
||||
{props.item.image && (
|
||||
<CoverImage
|
||||
src={props.item.image}
|
||||
alt="Link Preview Image"
|
||||
width={50}
|
||||
height={50}
|
||||
css={{ borderRadius: '8px' }}
|
||||
onError={(e) => {
|
||||
;(e.target as HTMLElement).style.display = 'none'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,23 @@
|
|||
import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
import type { LinkedItemCardProps } from './CardTypes'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { DotsThree } from 'phosphor-react'
|
||||
import Link from 'next/link'
|
||||
import { CardMenu } from '../CardMenu'
|
||||
import {
|
||||
AuthorInfoStyle,
|
||||
CardCheckbox,
|
||||
LibraryItemMetadata,
|
||||
MenuStyle,
|
||||
MetaStyle,
|
||||
siteName,
|
||||
timeAgo,
|
||||
TitleStyle,
|
||||
} from './LibraryCardStyles'
|
||||
import { sortedLabels } from '../../../lib/labelsSort'
|
||||
import { Checkbox } from '@radix-ui/react-checkbox'
|
||||
|
||||
export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
|
||||
const originText = siteName(props.item.originalArticleUrl, props.item.url)
|
||||
|
||||
return (
|
||||
<VStack
|
||||
|
|
@ -54,83 +50,105 @@ export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
|
|||
setIsHovered(false)
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={`${props.viewer.profile.username}/${props.item.slug}`}
|
||||
passHref
|
||||
>
|
||||
<a
|
||||
{props.inMultiSelect ? (
|
||||
<LibraryListCardContent {...props} isHovered={isHovered} />
|
||||
) : (
|
||||
<Link
|
||||
href={`${props.viewer.profile.username}/${props.item.slug}`}
|
||||
style={{ textDecoration: 'unset', width: '100%', height: '100%' }}
|
||||
tabIndex={-1}
|
||||
passHref
|
||||
>
|
||||
<HStack css={MetaStyle} distribution="start">
|
||||
<LibraryItemMetadata item={props.item} showProgress={true} />
|
||||
{props.inMultiSelect ? (
|
||||
<SpanBox css={{ marginLeft: 'auto' }}>
|
||||
<input type="checkbox"></input>
|
||||
</SpanBox>
|
||||
) : (
|
||||
<Box
|
||||
css={{
|
||||
...MenuStyle,
|
||||
visibility: isHovered || menuOpen ? 'unset' : '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>
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ height: '100%', width: '100%' }}
|
||||
<a
|
||||
href={`${props.viewer.profile.username}/${props.item.slug}`}
|
||||
style={{ textDecoration: 'unset', width: '100%', height: '100%' }}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Box css={{ ...TitleStyle, width: '80%' }}>{props.item.title}</Box>
|
||||
<SpanBox
|
||||
css={{
|
||||
mt: '5px',
|
||||
...AuthorInfoStyle,
|
||||
}}
|
||||
>
|
||||
{props.item.author}
|
||||
{props.item.author && originText && ' | '}
|
||||
<SpanBox css={{ textDecoration: 'underline' }}>
|
||||
{originText}
|
||||
</SpanBox>
|
||||
</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).map(
|
||||
({ name, color }, index) => (
|
||||
<LabelChip key={index} text={name || ''} color={color} />
|
||||
)
|
||||
)}
|
||||
</HStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</a>
|
||||
</Link>
|
||||
<LibraryListCardContent {...props} isHovered={isHovered} />
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
export function LibraryListCardContent(
|
||||
props: LinkedItemCardProps
|
||||
): JSX.Element {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const [isChecked, setIsChecked] = useState(false)
|
||||
|
||||
const originText = siteName(props.item.originalArticleUrl, props.item.url)
|
||||
|
||||
const handleCheckChanged = useCallback(() => {
|
||||
setIsChecked(!isChecked)
|
||||
}, [isChecked])
|
||||
|
||||
return (
|
||||
<>
|
||||
<HStack css={MetaStyle} distribution="start">
|
||||
<LibraryItemMetadata item={props.item} showProgress={true} />
|
||||
{props.inMultiSelect ? (
|
||||
<SpanBox css={{ marginLeft: 'auto' }}>
|
||||
<CardCheckbox
|
||||
isChecked={isChecked}
|
||||
handleChanged={handleCheckChanged}
|
||||
/>
|
||||
</SpanBox>
|
||||
) : (
|
||||
<Box
|
||||
css={{
|
||||
...MenuStyle,
|
||||
visibility: props.isHovered || menuOpen ? 'unset' : '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>
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
<Box css={{ ...TitleStyle, width: '80%' }}>{props.item.title}</Box>
|
||||
<SpanBox
|
||||
css={{
|
||||
mt: '5px',
|
||||
...AuthorInfoStyle,
|
||||
}}
|
||||
>
|
||||
{props.item.author}
|
||||
{props.item.author && originText && ' | '}
|
||||
<SpanBox css={{ textDecoration: 'underline' }}>{originText}</SpanBox>
|
||||
</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).map(({ name, color }, index) => (
|
||||
<LabelChip key={index} text={name || ''} color={color} />
|
||||
))}
|
||||
</HStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue