Merge pull request #3524 from omnivore-app/feat/library-navigation-updates

Updated library layout
This commit is contained in:
Jackson Harper
2024-02-09 14:17:01 +08:00
committed by GitHub
30 changed files with 1956 additions and 936 deletions

View File

@ -532,6 +532,7 @@ const processSubscription = async (
if (itemCount == 100) {
logger.info(`Max limit reached for feed ${feedUrl}`)
}
itemCount = itemCount + 1
continue
}

View File

@ -40,7 +40,7 @@ const StyledFallback = styled(Fallback, {
justifyContent: 'center',
fontSize: '15px',
fontWeight: 600,
fontFamily: 'Inter',
fontFamily: '$inter',
color: '$avatarFont',
backgroundColor: '$avatarBg',
})

View File

@ -8,7 +8,7 @@ type AvatarDropdownProps = {
export function AvatarDropdown(props: AvatarDropdownProps): JSX.Element {
return (
<HStack alignment="center" css={{ gap: '6px' }}>
<Avatar height="32px" fallbackText={props.userInitials} />
<Avatar height="25px" fallbackText={props.userInitials} />
</HStack>
)
}

View File

@ -18,6 +18,22 @@ export const Button = styled('button', {
border: '1px solid $grayBorderHover',
},
},
ctaBlue: {
borderRadius: '5px',
px: '20px',
py: '8px',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
border: '1px solid $yellow3',
bg: '$ctaBlue',
color: 'white',
'&:hover': {
opacity: '0.6',
border: '0px solid $ctaBlue',
},
},
ctaDarkYellow: {
border: '1px solid transparent',
fontSize: '14px',
@ -180,11 +196,11 @@ export const Button = styled('button', {
py: '5px',
font: '$inter',
fontSize: '12px',
fontWeight: '700',
fontWeight: '500',
whiteSpace: 'nowrap',
color: '$thLibraryMenuPrimary',
border: '1px solid $thLeftMenuBackground',
backgroundColor: '$thLeftMenuBackground',
bg: '$thBackgroundActive',
'&:hover': {
bg: '$thBackgroundActive',
border: '1px solid $thBackgroundActive',
@ -195,12 +211,12 @@ export const Button = styled('button', {
borderRadius: '15px',
px: '12px',
py: '5px',
bg: 'transparent',
font: '$inter',
fontSize: '12px',
fontWeight: 'medium',
fontWeight: '500',
whiteSpace: 'nowrap',
border: '1px solid $thBackground4',
backgroundColor: '$thBackground4',
'&:hover': {
bg: '$thBackgroundActive',
border: '1px solid $thBackgroundActive',
@ -215,6 +231,36 @@ export const Button = styled('button', {
color: '$thLibraryMenuUnselected',
cursor: 'pointer',
},
tab: {
px: '15px',
py: '6px',
border: 'none',
bg: 'transparent',
fontSize: '12px',
fontWeight: '500',
fontFamily: '$inter',
color: '$tabTextUnselected',
cursor: 'pointer',
borderRadius: '5px',
'&:hover': {
color: '$thTextContrast',
},
},
tabSelected: {
px: '15px',
py: '6px',
border: 'none',
bg: '#6A6968',
fontSize: '12px',
fontWeight: '500',
fontFamily: '$inter',
color: 'white',
cursor: 'pointer',
borderRadius: '5px',
// '&:hover': {
// color: '$thTextContrast',
// },
},
squareIcon: {
mx: '$1',
display: 'flex',

View File

@ -14,8 +14,8 @@ export function CloseButton(props: CloseButtonProps): JSX.Element {
<Box
css={{
display: 'flex',
height: '20px',
width: '20px',
height: '25px',
width: '25px',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '1000px',
@ -44,8 +44,8 @@ export function CloseButton(props: CloseButtonProps): JSX.Element {
}}
>
<X
width={10}
height={10}
width={12}
height={12}
weight="bold"
color={hover ? '#EBEBEB' : '#898989'}
className="xMark"

View File

@ -173,7 +173,11 @@ export function Dropdown(
<Root modal={modal} onOpenChange={props.onOpenChange}>
<DropdownTrigger
disabled={disabled}
css={{ height: '100%', cursor: 'pointer' }}
css={{
height: '100%',
cursor: 'pointer',
'&:hover': { opacity: '1.0' },
}}
>
{triggerElement}
</DropdownTrigger>

View File

@ -2,13 +2,14 @@ import { ReactNode, useEffect, useMemo, useRef } from 'react'
import { styled } from '../tokens/stitches.config'
import { Box, HStack, VStack } from './LayoutPrimitives'
import { Button } from './Button'
import { DropdownMenu } from '@radix-ui/react-dropdown-menu'
import { ArrowDown } from 'phosphor-react'
import { Dropdown, DropdownOption } from './DropdownElements'
import { CaretDownIcon } from './icons/CaretDownIcon'
type ShowLinkMode = 'none' | 'link' | 'pdf'
type SplitButtonProps = {
title: string
setShowLinkMode: (mode: ShowLinkMode) => void
}
const CaretButton = (): JSX.Element => {
@ -18,36 +19,47 @@ const CaretButton = (): JSX.Element => {
width: '20px',
height: '100%',
alignItems: 'center',
bg: '#6A6968',
bg: '$ctaBlue',
border: '0px solid transparent',
borderTopRightRadius: '5px',
borderBottomRightRadius: '5px',
borderTopLeftRadius: '0px',
borderBottomLeftRadius: '0px',
'--caret-color': '#EDEDED',
'&:hover': {
opacity: 1.0,
color: 'white',
'--caret-color': 'white',
},
'&:focus': {
outline: 'none',
border: '0px solid transparent',
},
}}
>
<CaretDownIcon size={8} color="#EDEDED" />
<CaretDownIcon size={8} color="var(--caret-color)" />
</VStack>
)
}
export const SplitButton = (props: SplitButtonProps): JSX.Element => {
return (
<HStack css={{ height: '27px', gap: '1px' }}>
<HStack css={{ height: '32px', gap: '1px' }}>
<Button
css={{
display: 'flex',
minWidth: '70px',
bg: '#6A6968',
fontSize: '12px',
// minWidth: '70px',
bg: '$ctaBlue',
color: '#EDEDED',
fontSize: '14px',
fontFamily: '$inter',
border: '0px solid transparent',
borderTopLeftRadius: '5px',
borderBottomLeftRadius: '5px',
borderTopRightRadius: '0px',
borderBottomRightRadius: '0px',
borderTopRightRadius: '5px',
borderBottomRightRadius: '5px',
'&:hover': {
opacity: 0.7,
opacity: 0.6,
border: '0px solid transparent',
},
'&:focus': {
@ -55,13 +67,16 @@ export const SplitButton = (props: SplitButtonProps): JSX.Element => {
border: '0px solid transparent',
},
}}
onClick={(event) => {
props.setShowLinkMode('link')
event.preventDefault()
}}
>
{props.title}
</Button>
{/* <Divider></Divider> */}
<Dropdown triggerElement={<CaretButton />}>
{/* <Dropdown triggerElement={<CaretButton />}>
<DropdownOption onSelect={() => console.log()} title="Archive (e)" />
</Dropdown>
</Dropdown> */}
</HStack>
)
}

View File

@ -1,40 +1,56 @@
/* eslint-disable functional/no-class */
/* eslint-disable functional/no-this-expression */
import { IconProps } from './IconProps'
import { SpanBox } from '../LayoutPrimitives'
import React from 'react'
export class HeaderCheckboxIcon extends React.Component<IconProps> {
render() {
const size = (this.props.size || 26).toString()
const color = (this.props.color || '#2A2A2A').toString()
return (
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
<SpanBox
css={{
display: 'flex',
'--inner-color': 'var(--colors-thHeaderIconInner)',
'--ring-color': 'var(--colors-thHeaderIconRing)',
'&:hover': {
'--inner-color': 'white',
'--ring-fill': '#007AFF',
'--ring-color': '#007AFF',
},
}}
>
<rect
x="0.5"
y="0.5"
width="39"
height="39"
rx="19.5"
stroke="#3D3D3D"
/>
<g>
<path
d="M12.5 14.1667C12.5 13.7246 12.6756 13.3007 12.9882 12.9882C13.3007 12.6756 13.7246 12.5 14.1667 12.5H25.8333C26.2754 12.5 26.6993 12.6756 27.0118 12.9882C27.3244 13.3007 27.5 13.7246 27.5 14.1667V25.8333C27.5 26.2754 27.3244 26.6993 27.0118 27.0118C26.6993 27.3244 26.2754 27.5 25.8333 27.5H14.1667C13.7246 27.5 13.3007 27.3244 12.9882 27.0118C12.6756 26.6993 12.5 26.2754 12.5 25.8333V14.1667Z"
stroke="#D9D9D9"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="0.5"
width="39"
height="39"
rx="19.5"
style={{
fill: 'var(--ring-fill)',
stroke: 'var(--ring-color)',
}}
/>
</g>
</svg>
<g>
<path
d="M12.5 14.1667C12.5 13.7246 12.6756 13.3007 12.9882 12.9882C13.3007 12.6756 13.7246 12.5 14.1667 12.5H25.8333C26.2754 12.5 26.6993 12.6756 27.0118 12.9882C27.3244 13.3007 27.5 13.7246 27.5 14.1667V25.8333C27.5 26.2754 27.3244 26.6993 27.0118 27.0118C26.6993 27.3244 26.2754 27.5 25.8333 27.5H14.1667C13.7246 27.5 13.3007 27.3244 12.9882 27.0118C12.6756 26.6993 12.5 26.2754 12.5 25.8333V14.1667Z"
style={{
stroke: 'var(--inner-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
</SpanBox>
)
}
}

View File

@ -1,47 +1,65 @@
/* eslint-disable functional/no-class */
/* eslint-disable functional/no-this-expression */
import { SpanBox } from '../LayoutPrimitives'
import { IconProps } from './IconProps'
import React from 'react'
export class HeaderSearchIcon extends React.Component<IconProps> {
render() {
const size = (this.props.size || 26).toString()
const color = (this.props.color || '#2A2A2A').toString()
return (
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
<SpanBox
css={{
display: 'flex',
'--inner-color': 'var(--colors-thHeaderIconInner)',
'--ring-color': 'var(--colors-thHeaderIconRing)',
'&:hover': {
'--inner-color': 'white',
'--ring-fill': '#007AFF',
'--ring-color': '#007AFF',
},
}}
>
<rect
x="0.5"
y="0.5"
width="39"
height="39"
rx="19.5"
stroke="#3D3D3D"
/>
<g>
<path
d="M12.5 18.3333C12.5 19.0994 12.6509 19.8579 12.944 20.5657C13.2372 21.2734 13.6669 21.9164 14.2085 22.4581C14.7502 22.9998 15.3933 23.4295 16.101 23.7226C16.8087 24.0158 17.5673 24.1667 18.3333 24.1667C19.0994 24.1667 19.8579 24.0158 20.5657 23.7226C21.2734 23.4295 21.9164 22.9998 22.4581 22.4581C22.9998 21.9164 23.4295 21.2734 23.7226 20.5657C24.0158 19.8579 24.1667 19.0994 24.1667 18.3333C24.1667 17.5673 24.0158 16.8087 23.7226 16.101C23.4295 15.3933 22.9998 14.7502 22.4581 14.2085C21.9164 13.6669 21.2734 13.2372 20.5657 12.944C19.8579 12.6509 19.0994 12.5 18.3333 12.5C17.5673 12.5 16.8087 12.6509 16.101 12.944C15.3933 13.2372 14.7502 13.6669 14.2085 14.2085C13.6669 14.7502 13.2372 15.3933 12.944 16.101C12.6509 16.8087 12.5 17.5673 12.5 18.3333Z"
stroke="#D9D9D9"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="0.5"
width="39"
height="39"
rx="19.5"
style={{
fill: 'var(--ring-fill)',
stroke: 'var(--ring-color)',
}}
/>
<path
d="M27.5 27.5L22.5 22.5"
stroke="#D9D9D9"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
<g>
<path
d="M12.5 18.3333C12.5 19.0994 12.6509 19.8579 12.944 20.5657C13.2372 21.2734 13.6669 21.9164 14.2085 22.4581C14.7502 22.9998 15.3933 23.4295 16.101 23.7226C16.8087 24.0158 17.5673 24.1667 18.3333 24.1667C19.0994 24.1667 19.8579 24.0158 20.5657 23.7226C21.2734 23.4295 21.9164 22.9998 22.4581 22.4581C22.9998 21.9164 23.4295 21.2734 23.7226 20.5657C24.0158 19.8579 24.1667 19.0994 24.1667 18.3333C24.1667 17.5673 24.0158 16.8087 23.7226 16.101C23.4295 15.3933 22.9998 14.7502 22.4581 14.2085C21.9164 13.6669 21.2734 13.2372 20.5657 12.944C19.8579 12.6509 19.0994 12.5 18.3333 12.5C17.5673 12.5 16.8087 12.6509 16.101 12.944C15.3933 13.2372 14.7502 13.6669 14.2085 14.2085C13.6669 14.7502 13.2372 15.3933 12.944 16.101C12.6509 16.8087 12.5 17.5673 12.5 18.3333Z"
style={{
stroke: 'var(--inner-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M27.5 27.5L22.5 22.5"
style={{
stroke: 'var(--inner-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
</SpanBox>
)
}
}

View File

@ -1,61 +1,83 @@
/* eslint-disable functional/no-class */
/* eslint-disable functional/no-this-expression */
import { SpanBox } from '../LayoutPrimitives'
import { IconProps } from './IconProps'
import React from 'react'
export class HeaderToggleGridIcon extends React.Component<IconProps> {
render() {
const size = (this.props.size || 26).toString()
const color = (this.props.color || '#2A2A2A').toString()
return (
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
<SpanBox
css={{
display: 'flex',
'--inner-color': 'var(--colors-thHeaderIconInner)',
'--ring-color': 'var(--colors-thHeaderIconRing)',
'&:hover': {
'--inner-color': 'white',
'--ring-fill': '#007AFF',
'--ring-color': '#007AFF',
},
}}
>
<rect
x="0.5"
y="0.5"
width="39"
height="39"
rx="19.5"
stroke="#3D3D3D"
/>
<g>
<path
d="M13.3333 14.1654C13.3333 13.9444 13.4211 13.7324 13.5774 13.5761C13.7337 13.4198 13.9457 13.332 14.1667 13.332H17.5C17.721 13.332 17.933 13.4198 18.0893 13.5761C18.2455 13.7324 18.3333 13.9444 18.3333 14.1654V17.4987C18.3333 17.7197 18.2455 17.9317 18.0893 18.088C17.933 18.2442 17.721 18.332 17.5 18.332H14.1667C13.9457 18.332 13.7337 18.2442 13.5774 18.088C13.4211 17.9317 13.3333 17.7197 13.3333 17.4987V14.1654Z"
stroke="#D9D9D9"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="0.5"
width="39"
height="39"
rx="19.5"
style={{
fill: 'var(--ring-fill)',
stroke: 'var(--ring-color)',
}}
/>
<path
d="M21.6667 14.1654C21.6667 13.9444 21.7545 13.7324 21.9107 13.5761C22.067 13.4198 22.279 13.332 22.5 13.332H25.8333C26.0543 13.332 26.2663 13.4198 26.4226 13.5761C26.5789 13.7324 26.6667 13.9444 26.6667 14.1654V17.4987C26.6667 17.7197 26.5789 17.9317 26.4226 18.088C26.2663 18.2442 26.0543 18.332 25.8333 18.332H22.5C22.279 18.332 22.067 18.2442 21.9107 18.088C21.7545 17.9317 21.6667 17.7197 21.6667 17.4987V14.1654Z"
stroke="#D9D9D9"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13.3333 22.5013C13.3333 22.2803 13.4211 22.0683 13.5774 21.912C13.7337 21.7558 13.9457 21.668 14.1667 21.668H17.5C17.721 21.668 17.933 21.7558 18.0893 21.912C18.2455 22.0683 18.3333 22.2803 18.3333 22.5013V25.8346C18.3333 26.0556 18.2455 26.2676 18.0893 26.4239C17.933 26.5802 17.721 26.668 17.5 26.668H14.1667C13.9457 26.668 13.7337 26.5802 13.5774 26.4239C13.4211 26.2676 13.3333 26.0556 13.3333 25.8346V22.5013Z"
stroke="#D9D9D9"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M21.6667 22.5013C21.6667 22.2803 21.7545 22.0683 21.9107 21.912C22.067 21.7558 22.279 21.668 22.5 21.668H25.8333C26.0543 21.668 26.2663 21.7558 26.4226 21.912C26.5789 22.0683 26.6667 22.2803 26.6667 22.5013V25.8346C26.6667 26.0556 26.5789 26.2676 26.4226 26.4239C26.2663 26.5802 26.0543 26.668 25.8333 26.668H22.5C22.279 26.668 22.067 26.5802 21.9107 26.4239C21.7545 26.2676 21.6667 26.0556 21.6667 25.8346V22.5013Z"
stroke="#D9D9D9"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
<g>
<path
d="M13.3333 14.1654C13.3333 13.9444 13.4211 13.7324 13.5774 13.5761C13.7337 13.4198 13.9457 13.332 14.1667 13.332H17.5C17.721 13.332 17.933 13.4198 18.0893 13.5761C18.2455 13.7324 18.3333 13.9444 18.3333 14.1654V17.4987C18.3333 17.7197 18.2455 17.9317 18.0893 18.088C17.933 18.2442 17.721 18.332 17.5 18.332H14.1667C13.9457 18.332 13.7337 18.2442 13.5774 18.088C13.4211 17.9317 13.3333 17.7197 13.3333 17.4987V14.1654Z"
style={{
stroke: 'var(--inner-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M21.6667 14.1654C21.6667 13.9444 21.7545 13.7324 21.9107 13.5761C22.067 13.4198 22.279 13.332 22.5 13.332H25.8333C26.0543 13.332 26.2663 13.4198 26.4226 13.5761C26.5789 13.7324 26.6667 13.9444 26.6667 14.1654V17.4987C26.6667 17.7197 26.5789 17.9317 26.4226 18.088C26.2663 18.2442 26.0543 18.332 25.8333 18.332H22.5C22.279 18.332 22.067 18.2442 21.9107 18.088C21.7545 17.9317 21.6667 17.7197 21.6667 17.4987V14.1654Z"
style={{
stroke: 'var(--inner-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13.3333 22.5013C13.3333 22.2803 13.4211 22.0683 13.5774 21.912C13.7337 21.7558 13.9457 21.668 14.1667 21.668H17.5C17.721 21.668 17.933 21.7558 18.0893 21.912C18.2455 22.0683 18.3333 22.2803 18.3333 22.5013V25.8346C18.3333 26.0556 18.2455 26.2676 18.0893 26.4239C17.933 26.5802 17.721 26.668 17.5 26.668H14.1667C13.9457 26.668 13.7337 26.5802 13.5774 26.4239C13.4211 26.2676 13.3333 26.0556 13.3333 25.8346V22.5013Z"
style={{
stroke: 'var(--inner-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M21.6667 22.5013C21.6667 22.2803 21.7545 22.0683 21.9107 21.912C22.067 21.7558 22.279 21.668 22.5 21.668H25.8333C26.0543 21.668 26.2663 21.7558 26.4226 21.912C26.5789 22.0683 26.6667 22.2803 26.6667 22.5013V25.8346C26.6667 26.0556 26.5789 26.2676 26.4226 26.4239C26.2663 26.5802 26.0543 26.668 25.8333 26.668H22.5C22.279 26.668 22.067 26.5802 21.9107 26.4239C21.7545 26.2676 21.6667 26.0556 21.6667 25.8346V22.5013Z"
style={{
stroke: 'var(--inner-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
</SpanBox>
)
}
}

View File

@ -1,47 +1,65 @@
/* eslint-disable functional/no-class */
/* eslint-disable functional/no-this-expression */
import { SpanBox } from '../LayoutPrimitives'
import { IconProps } from './IconProps'
import React from 'react'
export class HeaderToggleListIcon extends React.Component<IconProps> {
render() {
const size = (this.props.size || 26).toString()
const color = (this.props.color || '#2A2A2A').toString()
return (
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
<SpanBox
css={{
display: 'flex',
'--inner-color': 'var(--colors-thHeaderIconInner)',
'--ring-color': 'var(--colors-thHeaderIconRing)',
'&:hover': {
'--inner-color': 'white',
'--ring-fill': '#007AFF',
'--ring-color': '#007AFF',
},
}}
>
<rect
x="0.5"
y="0.5"
width="39"
height="39"
rx="19.5"
stroke="#3D3D3D"
/>
<g>
<path
d="M13.3334 14.9987C13.3334 14.5567 13.509 14.1327 13.8215 13.8202C14.1341 13.5076 14.558 13.332 15 13.332H25C25.4421 13.332 25.866 13.5076 26.1786 13.8202C26.4911 14.1327 26.6667 14.5567 26.6667 14.9987V16.6654C26.6667 17.1074 26.4911 17.5313 26.1786 17.8439C25.866 18.1564 25.4421 18.332 25 18.332H15C14.558 18.332 14.1341 18.1564 13.8215 17.8439C13.509 17.5313 13.3334 17.1074 13.3334 16.6654V14.9987Z"
stroke="#D9D9D9"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="0.5"
width="39"
height="39"
rx="19.5"
style={{
fill: 'var(--ring-fill)',
stroke: 'var(--ring-color)',
}}
/>
<path
d="M13.3334 23.3346C13.3334 22.8926 13.509 22.4687 13.8215 22.1561C14.1341 21.8436 14.558 21.668 15 21.668H25C25.4421 21.668 25.866 21.8436 26.1786 22.1561C26.4911 22.4687 26.6667 22.8926 26.6667 23.3346V25.0013C26.6667 25.4433 26.4911 25.8673 26.1786 26.1798C25.866 26.4924 25.4421 26.668 25 26.668H15C14.558 26.668 14.1341 26.4924 13.8215 26.1798C13.509 25.8673 13.3334 25.4433 13.3334 25.0013V23.3346Z"
stroke="#D9D9D9"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
<g>
<path
d="M13.3334 14.9987C13.3334 14.5567 13.509 14.1327 13.8215 13.8202C14.1341 13.5076 14.558 13.332 15 13.332H25C25.4421 13.332 25.866 13.5076 26.1786 13.8202C26.4911 14.1327 26.6667 14.5567 26.6667 14.9987V16.6654C26.6667 17.1074 26.4911 17.5313 26.1786 17.8439C25.866 18.1564 25.4421 18.332 25 18.332H15C14.558 18.332 14.1341 18.1564 13.8215 17.8439C13.509 17.5313 13.3334 17.1074 13.3334 16.6654V14.9987Z"
style={{
stroke: 'var(--inner-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13.3334 23.3346C13.3334 22.8926 13.509 22.4687 13.8215 22.1561C14.1341 21.8436 14.558 21.668 15 21.668H25C25.4421 21.668 25.866 21.8436 26.1786 22.1561C26.4911 22.4687 26.6667 22.8926 26.6667 23.3346V25.0013C26.6667 25.4433 26.4911 25.8673 26.1786 26.1798C25.866 26.4924 25.4421 26.668 25 26.668H15C14.558 26.668 14.1341 26.4924 13.8215 26.1798C13.509 25.8673 13.3334 25.4433 13.3334 25.0013V23.3346Z"
style={{
stroke: 'var(--inner-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
</SpanBox>
)
}
}

View File

@ -45,7 +45,7 @@ export const TitleStyle = {
fontSize: '16px',
fontWeight: '700',
maxLines: 2,
lineHeight: 1.25,
lineHeight: 1.5,
fontFamily: '$display',
overflow: 'hidden',
textOverflow: 'ellipsis',

View File

@ -64,15 +64,14 @@ export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element {
css={{
pl: '0px',
padding: '0px',
width: '320px',
width: '293px',
height: '100%',
minHeight: '270px',
background: 'white',
borderRadius: '5px',
borderWidth: '1px',
borderStyle: 'solid',
borderStyle: 'none',
overflow: 'hidden',
borderColor: '$thBorderColor',
cursor: 'pointer',
'@media (max-width: 930px)': {
m: '15px',
@ -88,6 +87,10 @@ export function LibraryGridCard(props: LinkedItemCardProps): JSX.Element {
setIsHovered(false)
}}
onClick={(event) => {
if (props.multiSelectMode !== 'off') {
props.setIsChecked(props.item.id, !props.isChecked)
return
}
if (event.metaKey || event.ctrlKey) {
window.open(
`/${props.viewer.profile.username}/${props.item.slug}`,

View File

@ -66,7 +66,7 @@ export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
height: '100%',
cursor: 'pointer',
gap: '10px',
border: '1px solid $grayBorder',
borderStyle: 'none',
borderBottom: 'none',
borderRadius: '6px',
width: '100vw',
@ -74,24 +74,22 @@ export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
width: `calc(100vw - ${LIBRARY_LEFT_MENU_WIDTH})`,
},
'@media (min-width: 930px)': {
width: '640px',
width: '580px',
},
'@media (min-width: 1280px)': {
width: '1000px',
width: '890px',
},
'@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',
width: '1200px',
},
}}
alignment="start"
distribution="start"
onClick={(event) => {
if (props.multiSelectMode !== 'off') {
props.setIsChecked(props.item.id, !props.isChecked)
return
}
if (event.metaKey || event.ctrlKey) {
window.open(
`/${props.viewer.profile.username}/${props.item.slug}`,

View File

@ -1,352 +1,395 @@
import { useRouter } from 'next/router'
import { Moon, Sun } from 'phosphor-react'
import { ReactNode, useCallback } from 'react'
import { ReactNode, useCallback, useState } from 'react'
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
import { currentTheme, updateTheme } from '../../lib/themeUpdater'
import { Avatar } from '../elements/Avatar'
import { AvatarDropdown } from '../elements/AvatarDropdown'
import {
Dropdown,
DropdownOption,
DropdownSeparator,
Dropdown,
DropdownOption,
DropdownSeparator,
} from '../elements/DropdownElements'
import GridLayoutIcon from '../elements/images/GridLayoutIcon'
import ListLayoutIcon from '../elements/images/ListLayoutIcon'
import { Box, HStack, VStack } from '../elements/LayoutPrimitives'
import { Box, HStack, SpanBox, VStack } from '../elements/LayoutPrimitives'
import { StyledText } from '../elements/StyledText'
import { styled, theme, ThemeId } from '../tokens/stitches.config'
import { LayoutType } from './homeFeed/HomeFeedContainer'
import { DropdownMenu } from '@radix-ui/react-dropdown-menu'
type PrimaryDropdownProps = {
children?: ReactNode
showThemeSection: boolean
children?: ReactNode
showThemeSection: boolean
layout?: LayoutType
updateLayout?: (layout: LayoutType) => void
showAddLinkModal?: () => void
layout?: LayoutType
updateLayout?: (layout: LayoutType) => void
}
export type HeaderDropdownAction =
| 'navigate-to-install'
| 'navigate-to-feeds'
| 'navigate-to-emails'
| 'navigate-to-labels'
| 'navigate-to-rules'
| 'navigate-to-profile'
| 'navigate-to-subscriptions'
| 'navigate-to-api'
| 'navigate-to-integrations'
| 'navigate-to-saved-searches'
| 'increaseFontSize'
| 'decreaseFontSize'
| 'logout'
| 'navigate-to-install'
| 'navigate-to-feeds'
| 'navigate-to-emails'
| 'navigate-to-labels'
| 'navigate-to-rules'
| 'navigate-to-profile'
| 'navigate-to-subscriptions'
| 'navigate-to-api'
| 'navigate-to-integrations'
| 'navigate-to-saved-searches'
| 'increaseFontSize'
| 'decreaseFontSize'
| 'logout'
type TriggerButtonProps = {
name: string
}
const TriggerButton = (props: TriggerButtonProps): JSX.Element => {
return (
<HStack
css={{
mx: '10px',
gap: '10px',
alignItems: 'center',
borderRadius: '5px',
height: '32px',
padding: '5px',
'&:hover': {
bg: '$thLibraryMenuFooterHover',
opacity: '0.7px',
},
}}
>
<AvatarDropdown userInitials={props.name.charAt(0) ?? ''} />
<SpanBox
css={{
display: 'flex',
justifyContent: 'start',
fontFamily: '$inter',
fontSize: '12px',
maxWidth: '100px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{props.name}
</SpanBox>
</HStack>
)
}
export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element {
const { viewerData } = useGetViewerQuery()
const router = useRouter()
const { viewerData } = useGetViewerQuery()
const router = useRouter()
const headerDropdownActionHandler = useCallback(
(action: HeaderDropdownAction) => {
switch (action) {
case 'navigate-to-install':
router.push('/settings/installation')
break
case 'navigate-to-feeds':
router.push('/settings/feeds')
break
case 'navigate-to-emails':
router.push('/settings/emails')
break
case 'navigate-to-labels':
router.push('/settings/labels')
break
case 'navigate-to-rules':
router.push('/settings/rules')
break
case 'navigate-to-subscriptions':
router.push('/settings/subscriptions')
break
case 'navigate-to-api':
router.push('/settings/api')
break
case 'navigate-to-integrations':
router.push('/settings/integrations')
break
case 'navigate-to-saved-searches':
router.push('/settings/saved-searches')
break
case 'logout':
document.dispatchEvent(new Event('logout'))
break
default:
break
}
},
[router]
)
const headerDropdownActionHandler = useCallback(
(action: HeaderDropdownAction) => {
switch (action) {
case 'navigate-to-install':
router.push('/settings/installation')
break
case 'navigate-to-feeds':
router.push('/settings/feeds')
break
case 'navigate-to-emails':
router.push('/settings/emails')
break
case 'navigate-to-labels':
router.push('/settings/labels')
break
case 'navigate-to-rules':
router.push('/settings/rules')
break
case 'navigate-to-subscriptions':
router.push('/settings/subscriptions')
break
case 'navigate-to-api':
router.push('/settings/api')
break
case 'navigate-to-integrations':
router.push('/settings/integrations')
break
case 'navigate-to-saved-searches':
router.push('/settings/saved-searches')
break
case 'logout':
document.dispatchEvent(new Event('logout'))
break
default:
break
}
},
[router]
)
if (!viewerData?.me) {
return <></>
}
if (!viewerData?.me) {
return <></>
}
return (
<Dropdown
triggerElement={
props.children ?? (
<AvatarDropdown userInitials={viewerData?.me?.name.charAt(0) ?? ''} />
)
}
css={{ width: '240px' }}
return (
<Dropdown
triggerElement={
props.children ?? <TriggerButton name={viewerData?.me?.name ?? 'O'} />
}
css={{ width: '240px' }}
>
<HStack
alignment="center"
distribution="start"
css={{
width: '100%',
height: '64px',
p: '15px',
gap: '15px',
cursor: 'pointer',
mouseEvents: 'all',
}}
onClick={(event) => {
router.push('/settings/account')
event.preventDefault()
}}
>
<Avatar
imageURL={viewerData.me.profile.pictureUrl}
height="40px"
fallbackText={viewerData?.me?.name.charAt(0) ?? ''}
/>
<VStack
css={{ height: '40px', maxWidth: '240px' }}
alignment="start"
distribution="around"
>
<HStack
alignment="center"
distribution="start"
{viewerData.me && (
<>
<StyledText
css={{
width: '100%',
height: '64px',
p: '15px',
gap: '15px',
cursor: 'pointer',
mouseEvents: 'all',
fontSize: '14px',
fontWeight: '500',
color: '$thTextContrast2',
m: '0px',
p: '0px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
onClick={(event) => {
router.push('/settings/account')
event.preventDefault()
>
{viewerData.me.name}
</StyledText>
<StyledText
css={{
fontSize: '14px',
fontWeight: '400',
color: '#898989',
m: '0px',
p: '0px',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
<Avatar
imageURL={viewerData.me.profile.pictureUrl}
height="40px"
fallbackText={viewerData?.me?.name.charAt(0) ?? ''}
/>
<VStack
css={{ height: '40px', maxWidth: '240px' }}
alignment="start"
distribution="around"
>
{viewerData.me && (
<>
<StyledText
css={{
fontSize: '14px',
fontWeight: '500',
color: '$thTextContrast2',
m: '0px',
p: '0px',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{viewerData.me.name}
</StyledText>
<StyledText
css={{
fontSize: '14px',
fontWeight: '400',
color: '#898989',
m: '0px',
p: '0px',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{`@${viewerData.me.profile.username}`}
</StyledText>
</>
)}
</VStack>
</HStack>
<DropdownSeparator />
{props.showThemeSection && <ThemeSection {...props} />}
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-install')}
title="Install"
/>
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-feeds')}
title="Feeds"
/>
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-emails')}
title="Emails"
/>
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-labels')}
title="Labels"
/>
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-rules')}
title="Rules"
/>
{props.showAddLinkModal && (
<>
<DropdownSeparator />
>
{`@${viewerData.me.profile.username}`}
</StyledText>
</>
)}
</VStack>
</HStack>
<DropdownSeparator />
{props.showThemeSection && <ThemeSection {...props} />}
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-install')}
title="Install"
/>
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-feeds')}
title="Feeds"
/>
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-emails')}
title="Emails"
/>
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-labels')}
title="Labels"
/>
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-rules')}
title="Rules"
/>
<DropdownMenu>
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-api')}
title="API Keys"
/>
<DropdownOption
onSelect={() =>
headerDropdownActionHandler('navigate-to-integrations')
}
title="Integrations"
/>
</DropdownMenu>
<DropdownOption
onSelect={() => props.showAddLinkModal && props.showAddLinkModal()}
title="Add Link"
/>
</>
)}
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-api')}
title="API Keys"
/>
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-integrations')}
title="Integrations"
/>
<DropdownOption
onSelect={() => window.open('https://docs.omnivore.app', '_blank')}
title="Documentation"
/>
<DropdownOption
onSelect={() => window.Intercom('show')}
title="Feedback"
/>
<DropdownSeparator />
<DropdownOption
onSelect={() => headerDropdownActionHandler('logout')}
title="Logout"
/>
</Dropdown>
)
<DropdownOption
onSelect={() => window.open('https://docs.omnivore.app', '_blank')}
title="Documentation"
/>
<DropdownOption
onSelect={() => window.Intercom('show')}
title="Feedback"
/>
<DropdownSeparator />
<DropdownOption
onSelect={() => headerDropdownActionHandler('logout')}
title="Logout"
/>
</Dropdown>
)
}
export const StyledToggleButton = styled('button', {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '$thTextContrast2',
backgroundColor: 'transparent',
border: 'none',
cursor: 'pointer',
width: '70px',
height: '100%',
borderRadius: '5px',
fontSize: '12px',
fontFamily: '$inter',
gap: '5px',
m: '2px',
'&:hover': {
opacity: 0.8,
},
'&[data-state="on"]': {
bg: '$thBackground',
},
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '$thTextContrast2',
backgroundColor: 'transparent',
border: 'none',
cursor: 'pointer',
width: '70px',
height: '100%',
borderRadius: '5px',
fontSize: '12px',
fontFamily: '$inter',
gap: '5px',
m: '2px',
'&:hover': {
opacity: 0.8,
},
'&[data-state="on"]': {
bg: '$thBackground',
},
})
function ThemeSection(props: PrimaryDropdownProps): JSX.Element {
return (
<>
<VStack>
<HStack
alignment="center"
css={{
width: '100%',
px: '15px',
justifyContent: 'space-between',
}}
>
<StyledText
css={{
fontSize: '14px',
fontWeight: '400',
cursor: 'default',
color: '$utilityTextDefault',
}}
>
Mode
</StyledText>
<Box
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bg: '$thBackground4',
borderRadius: '5px',
height: '34px',
p: '3px',
px: '1px',
}}
>
<StyledToggleButton
data-state={currentTheme() != ThemeId.Dark ? 'on' : 'off'}
onClick={() => {
updateTheme(ThemeId.Light)
}}
>
Light
<Sun size={15} color={theme.colors.thTextContrast2.toString()} />
</StyledToggleButton>
<StyledToggleButton
data-state={currentTheme() == ThemeId.Dark ? 'on' : 'off'}
onClick={() => {
updateTheme(ThemeId.Dark)
}}
>
Dark
<Moon size={15} color={theme.colors.thTextContrast2.toString()} />
</StyledToggleButton>
</Box>
</HStack>
{props.layout && (
<HStack
alignment="center"
css={{
width: '100%',
px: '15px',
justifyContent: 'space-between',
}}
>
<StyledText
css={{
fontSize: '14px',
fontWeight: '400',
cursor: 'default',
color: '$utilityTextDefault',
}}
>
Layout
</StyledText>
<Box
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bg: '$thBackground4',
borderRadius: '5px',
height: '34px',
p: '3px',
px: '1px',
}}
>
<StyledToggleButton
data-state={props.layout == 'LIST_LAYOUT' ? 'on' : 'off'}
onClick={() => {
props.updateLayout && props.updateLayout('LIST_LAYOUT')
}}
>
<ListLayoutIcon
color={theme.colors.thTextContrast2.toString()}
/>
</StyledToggleButton>
<StyledToggleButton
data-state={props.layout == 'GRID_LAYOUT' ? 'on' : 'off'}
onClick={() => {
props.updateLayout && props.updateLayout('GRID_LAYOUT')
}}
>
<GridLayoutIcon
color={theme.colors.thTextContrast2.toString()}
/>
</StyledToggleButton>
</Box>
</HStack>
)}
</VStack>
<DropdownSeparator />
</>
)
const [displayTheme, setDisplayTheme] = useState(currentTheme())
const doUpdateTheme = useCallback(
(newTheme: ThemeId) => {
updateTheme(newTheme)
setDisplayTheme(newTheme)
},
[displayTheme, setDisplayTheme]
)
return (
<>
<VStack>
<HStack
alignment="center"
css={{
width: '100%',
px: '15px',
justifyContent: 'space-between',
}}
>
<StyledText
css={{
fontSize: '14px',
fontWeight: '400',
cursor: 'default',
color: '$utilityTextDefault',
}}
>
Mode
</StyledText>
<Box
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bg: '$thBackground4',
borderRadius: '5px',
height: '34px',
p: '3px',
px: '1px',
}}
>
<StyledToggleButton
data-state={currentTheme() != ThemeId.Dark ? 'on' : 'off'}
onClick={() => {
doUpdateTheme(ThemeId.Light)
}}
>
Light
<Sun size={15} color={theme.colors.thTextContrast2.toString()} />
</StyledToggleButton>
<StyledToggleButton
data-state={currentTheme() == ThemeId.Dark ? 'on' : 'off'}
onClick={() => {
doUpdateTheme(ThemeId.Dark)
}}
>
Dark
<Moon size={15} color={theme.colors.thTextContrast2.toString()} />
</StyledToggleButton>
</Box>
</HStack>
{props.layout && (
<HStack
alignment="center"
css={{
width: '100%',
px: '15px',
justifyContent: 'space-between',
}}
>
<StyledText
css={{
fontSize: '14px',
fontWeight: '400',
cursor: 'default',
color: '$utilityTextDefault',
}}
>
Layout
</StyledText>
<Box
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bg: '$thBackground4',
borderRadius: '5px',
height: '34px',
p: '3px',
px: '1px',
}}
>
<StyledToggleButton
data-state={props.layout == 'LIST_LAYOUT' ? 'on' : 'off'}
onClick={() => {
props.updateLayout && props.updateLayout('LIST_LAYOUT')
}}
>
<ListLayoutIcon
color={theme.colors.thTextContrast2.toString()}
/>
</StyledToggleButton>
<StyledToggleButton
data-state={props.layout == 'GRID_LAYOUT' ? 'on' : 'off'}
onClick={() => {
props.updateLayout && props.updateLayout('GRID_LAYOUT')
}}
>
<GridLayoutIcon
color={theme.colors.thTextContrast2.toString()}
/>
</StyledToggleButton>
</Box>
</HStack>
)}
</VStack>
<DropdownSeparator />
</>
)
}

View File

@ -573,7 +573,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
)
useEffect(() => {
if (props.highlightOnRelease && selectionData?.wasDragEvent) {
if (props.highlightOnRelease) {
handleAction('create')
setSelectionData(null)
}

View File

@ -167,6 +167,37 @@ function AdvancedSettings(props: SettingsProps): JSX.Element {
<SwitchThumb />
</SwitchRoot>
</HStack>
<HStack
css={{
width: '100%',
pr: '30px',
alignItems: 'center',
'&:hover': {
opacity: 0.8,
},
'&[data-state="on"]': {
bg: '$thBackground',
},
}}
alignment="start"
distribution="between"
>
<Label htmlFor="auto-highlight-mode" css={{ width: '100%' }}>
<StyledText style="displaySettingsLabel" css={{ pl: '20px' }}>
Auto highlight mode
</StyledText>
</Label>
<SwitchRoot
id="high-contrast-text"
checked={readerSettings.highlightOnRelease ?? false}
onCheckedChange={(checked) => {
readerSettings.setHighlightOnRelease(checked)
}}
>
<SwitchThumb />
</SwitchRoot>
</HStack>
</VStack>
)
}

View File

@ -1,11 +1,13 @@
import { useCallback, useState } from 'react'
import { useCallback, useRef, useState } from 'react'
import * as Progress from '@radix-ui/react-progress'
import { File, Info } from 'phosphor-react'
import toast from 'react-hot-toast'
import { locale, timeZone } from '../../../lib/dateFormatting'
import { saveUrlMutation } from '../../../lib/networking/mutations/saveUrlMutation'
import { showErrorToast } from '../../../lib/toastHelpers'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import { Button } from '../../elements/Button'
import { FormInput } from '../../elements/FormElements'
import { Box, VStack } from '../../elements/LayoutPrimitives'
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import {
ModalButtonBar,
ModalContent,
@ -13,6 +15,29 @@ import {
ModalRoot,
ModalTitleBar,
} from '../../elements/ModalPrimitives'
import { CloseButton } from '../../elements/CloseButton'
import { StyledText } from '../../elements/StyledText'
import { styled } from '@stitches/react'
import Dropzone, {
Accept,
DropEvent,
DropzoneRef,
FileRejection,
} from 'react-dropzone'
import { v4 as uuidv4 } from 'uuid'
import { validateCsvFile } from '../../../utils/csvValidator'
import {
uploadImportFileRequestMutation,
UploadImportFileType,
} from '../../../lib/networking/mutations/uploadImportFileMutation'
import { uploadFileRequestMutation } from '../../../lib/networking/mutations/uploadFileMutation'
import axios from 'axios'
import { theme } from '../../tokens/stitches.config'
import { formatMessage } from '../../../locales/en/messages'
import { subscribeMutation } from '../../../lib/networking/mutations/subscribeMutation'
import { SubscriptionType } from '../../../lib/networking/queries/useGetSubscriptionsQuery'
type TabName = 'link' | 'feed' | 'opml' | 'pdf' | 'import'
type AddLinkModalProps = {
onOpenChange: (open: boolean) => void
@ -24,9 +49,127 @@ type AddLinkModalProps = {
}
export function AddLinkModal(props: AddLinkModalProps): JSX.Element {
const [link, setLink] = useState('')
const [selectedTab, setSelectedTab] = useState('link')
const validateLink = useCallback(
return (
<ModalRoot defaultOpen onOpenChange={props.onOpenChange} css={{}}>
<ModalOverlay />
<ModalContent
css={{
p: '20px',
bg: '$modalBackground',
maxWidth: '600',
maxHeight: '300',
fontFamily: '$inter',
}}
onInteractOutside={() => {
// remove focus from modal
;(document.activeElement as HTMLElement).blur()
}}
>
<VStack distribution="start" css={{ gap: '20px' }}>
<TabBar
selectedTab={selectedTab}
setSelectedTab={setSelectedTab}
onOpenChange={props.onOpenChange}
/>
<Box css={{ width: '100%' }}>
{selectedTab == 'link' && <AddLinkTab {...props} />}
{selectedTab == 'feed' && <AddFeedTab {...props} />}
{selectedTab == 'opml' && <UploadOPMLTab {...props} />}
{selectedTab == 'pdf' && <UploadPDFTab {...props} />}
{selectedTab == 'import' && <UploadImportTab {...props} />}
</Box>
</VStack>
</ModalContent>
</ModalRoot>
)
}
const AddLinkTab = (props: AddLinkModalProps): JSX.Element => {
const [errorMessage, setErrorMessage] = useState<string | undefined>(
undefined
)
const addLink = useCallback(
async (link: string) => {
await props.handleLinkSubmission(link, timeZone, locale)
props.onOpenChange(false)
},
[errorMessage, setErrorMessage]
)
return (
<AddFromURL
placeholder="https://example.com/"
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
onSubmit={addLink}
/>
)
}
const AddFeedTab = (props: AddLinkModalProps): JSX.Element => {
const [errorMessage, setErrorMessage] = useState<string | undefined>(
undefined
)
const subscribe = useCallback(
async (feedUrl: string) => {
if (!feedUrl) {
setErrorMessage('Please enter a valid feed URL')
return
}
let normailizedUrl: string
// normalize the url
try {
normailizedUrl = new URL(feedUrl.trim()).toString()
} catch (e) {
setErrorMessage('Please enter a valid feed URL')
return
}
const result = await subscribeMutation({
url: normailizedUrl,
subscriptionType: SubscriptionType.RSS,
})
if (result.subscribe.errorCodes) {
const errorMessage = formatMessage({
id: `error.${result.subscribe.errorCodes[0]}`,
})
setErrorMessage(`There was an error adding new feed: ${errorMessage}`)
return
}
showSuccessToast('New feed has been added.')
},
[errorMessage, setErrorMessage]
)
return (
<AddFromURL
placeholder="https://example.com/feed.atom"
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
onSubmit={subscribe}
/>
)
}
type AddFromURLProps = {
placeholder: string
errorMessage: string | undefined
setErrorMessage: (message: string) => void
onSubmit: (url: string) => Promise<void>
}
const AddFromURL = (props: AddFromURLProps): JSX.Element => {
const [url, setURL] = useState('')
const [errorMessage, setErrorMessage] = useState(props.errorMessage)
const validateURL = useCallback(
(link: string) => {
try {
const url = new URL(link)
@ -38,66 +181,619 @@ export function AddLinkModal(props: AddLinkModalProps): JSX.Element {
}
return true
},
[link]
[url]
)
return (
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
<ModalOverlay />
<ModalContent
css={{ bg: '$grayBg', px: '24px' }}
onInteractOutside={() => {
// remove focus from modal
;(document.activeElement as HTMLElement).blur()
<VStack css={{ width: '100%', height: '180px' }}>
<form
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
paddingTop: '5px',
}}
onSubmit={async (event) => {
event.preventDefault()
if (!validateURL(url)) {
setErrorMessage('Invalid URL')
return
}
props.onSubmit(url)
}}
>
<VStack distribution="start">
<ModalTitleBar title="Add Link" onOpenChange={props.onOpenChange} />
<Box css={{ width: '100%', py: '16px' }}>
<form
onSubmit={async (event) => {
event.preventDefault()
let submitLink = link
if (!validateLink(link)) {
// If validation fails, attempting adding
// `https` to give the link a protocol.
const newLink = `https://${link}`
if (!validateLink(newLink)) {
showErrorToast('Invalid link', { position: 'bottom-right' })
return
}
setLink(newLink)
submitLink = newLink
}
await props.handleLinkSubmission(submitLink, timeZone, locale)
props.onOpenChange(false)
}}
>
<FormInput
type="url"
value={link}
autoFocus={true}
placeholder="https://example.com"
onChange={(event) => setLink(event.target.value)}
css={{
borderRadius: '8px',
border: '1px solid $textNonessential',
width: '100%',
height: '38px',
p: '6px',
mb: '13px',
fontSize: '14px',
}}
/>
<ModalButtonBar
onOpenChange={props.onOpenChange}
acceptButtonLabel="Add Link"
/>
</form>
</Box>
</VStack>
</ModalContent>
</ModalRoot>
<FormInput
type="url"
value={url}
autoFocus={true}
placeholder={props.placeholder}
onChange={(event) => setURL(event.target.value)}
css={{
borderRadius: '4px',
width: '100%',
height: '38px',
p: '6px',
mb: '13px',
fontSize: '14px',
color: '$thTextContrast',
bg: '$thFormInput',
}}
/>
<Button
style="ctaBlue"
type="submit"
css={{ marginLeft: 'auto', marginTop: 'auto' }}
>
Add
</Button>
</form>
</VStack>
)
}
const UploadOPMLTab = (props: AddLinkModalProps): JSX.Element => {
return (
<VStack
alignment="start"
distribution="start"
css={{ height: '180px', width: '100%' }}
>
<UploadPad
description="Drag OPML file to add feeds"
accept={{
'text/csv': ['.csv'],
'application/zip': ['.zip'],
'application/pdf': ['.pdf'],
'application/epub+zip': ['.epub'],
}}
/>
</VStack>
)
}
const UploadPDFTab = (props: AddLinkModalProps): JSX.Element => {
return (
<VStack
alignment="start"
distribution="start"
css={{ height: '180px', width: '100%' }}
>
<UploadPad
info={
<HStack
distribution="start"
alignment="center"
css={{ gap: '5px', whiteSpace: 'pre-line' }}
>
<Info size={14} color="#007AFF" />
PDFs have a maximum size of 8MB.{' '}
</HStack>
}
description="Drag PDFs here to add to your library"
accept={{
'application/pdf': ['.pdf'],
}}
/>
</VStack>
)
}
const UploadImportTab = (props: AddLinkModalProps): JSX.Element => {
return (
<VStack
alignment="start"
distribution="start"
css={{ height: '180px', width: '100%' }}
>
<UploadPad
info={
<HStack
distribution="start"
alignment="center"
css={{ gap: '5px', whiteSpace: 'pre-line' }}
>
<Info size={14} color="#007AFF" />
Imports must be in a supported format.{' '}
<a
href="https://docs.omnivore.app/using/importing.html"
target="_blank"
rel="noreferrer"
style={{ color: '#007AFF' }}
>
Read more
</a>
</HStack>
}
description="Drop import files here"
accept={{
'text/csv': ['.csv'],
'application/zip': ['.zip'],
}}
/>
</VStack>
)
}
const DragnDropContainer = styled('div', {
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: '1',
alignSelf: 'center',
left: 0,
flexDirection: 'column',
})
const DragnDropStyle = styled('div', {
border: '1px solid $grayBorder',
borderRadius: '5px',
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
color: '$thTextSubtle2',
padding: '10px',
})
const DragnDropIndicator = styled('div', {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
width: '100%',
height: '100%',
borderRadius: '5px',
})
const ProgressIndicator = styled(Progress.Indicator, {
backgroundColor: '$omnivoreCtaYellow',
width: '100%',
height: '100%',
})
const ProgressRoot = styled(Progress.Root, {
position: 'relative',
overflow: 'hidden',
background: '$omnivoreGray',
borderRadius: '99999px',
width: '100%',
height: '5px',
transform: 'translateZ(0)',
})
type UploadingFile = {
id: string
file: any
name: string
progress: number
status: 'inprogress' | 'success' | 'error'
openUrl: string | undefined
contentType: string
message?: string
}
type UploadInfo = {
uploadSignedUrl?: string
requestId?: string
message?: string
}
type UploadPadProps = {
info?: React.ReactNode
description: string
accept: Accept
}
const UploadPad = (props: UploadPadProps): JSX.Element => {
const [uploadFiles, setUploadFiles] = useState<UploadingFile[]>([])
const [inDragOperation, setInDragOperation] = useState(false)
const dropzoneRef = useRef<DropzoneRef | null>(null)
const openDialog = useCallback(
(event: React.MouseEvent) => {
if (dropzoneRef.current) {
dropzoneRef.current.open()
}
event?.preventDefault()
},
[dropzoneRef]
)
const uploadSignedUrlForFile = async (
file: UploadingFile
): Promise<UploadInfo> => {
let { contentType } = file
if (
contentType == 'application/vnd.ms-excel' &&
file.name.endsWith('.csv')
) {
contentType = 'text/csv'
}
switch (contentType) {
case 'text/csv': {
let urlCount = 0
try {
const csvData = await validateCsvFile(file.file)
urlCount = csvData.data.length
if (urlCount > 5000) {
return {
message:
'Due to an increase in traffic we are limiting CSV imports to 5000 items.',
}
}
if (csvData.inValidData.length > 0) {
return {
message: csvData.inValidData[0].message,
}
}
if (urlCount === 0) {
return {
message: 'No URLs found in CSV file.',
}
}
} catch (error) {
return {
message: 'Invalid CSV file.',
}
}
try {
const result = await uploadImportFileRequestMutation(
UploadImportFileType.URL_LIST,
contentType
)
return {
uploadSignedUrl: result?.uploadSignedUrl,
message: `Importing ${urlCount} URLs`,
}
} catch (error) {
console.log('caught error', error)
if (error == 'UPLOAD_DAILY_LIMIT_EXCEEDED') {
return {
message: 'You have exceeded your maximum daily upload limit.',
}
}
}
}
case 'application/zip': {
const result = await uploadImportFileRequestMutation(
UploadImportFileType.MATTER,
contentType
)
return {
uploadSignedUrl: result?.uploadSignedUrl,
}
}
case 'application/pdf':
case 'application/epub+zip': {
const request = await uploadFileRequestMutation({
// This will tell the backend not to save the URL
// and give it the local filename as the title.
url: `file://local/${file.id}/${file.file.path}`,
contentType: contentType,
createPageEntry: true,
})
return {
uploadSignedUrl: request?.uploadSignedUrl,
requestId: request?.createdPageId,
}
}
}
return {
message: `Invalid content type: ${contentType}`,
}
}
const handleAcceptedFiles = useCallback(
(acceptedFiles: any, event: DropEvent) => {
setInDragOperation(false)
const addedFiles = acceptedFiles.map(
(file: { name: any; type: string }) => {
return {
id: uuidv4(),
file: file,
name: file.name,
progress: 0,
status: 'inprogress',
contentType: file.type,
}
}
)
const allFiles = [...uploadFiles, ...addedFiles]
setUploadFiles(allFiles)
;(async () => {
for (const file of addedFiles) {
try {
const uploadInfo = await uploadSignedUrlForFile(file)
if (!uploadInfo.uploadSignedUrl) {
const message = uploadInfo.message || 'No upload URL available'
showErrorToast(message, { duration: 10000 })
file.status = 'error'
setUploadFiles([...allFiles])
return
}
const uploadResult = await axios.request({
method: 'PUT',
url: uploadInfo.uploadSignedUrl,
data: file.file,
withCredentials: false,
headers: {
'Content-Type': file.file.type,
},
onUploadProgress: (p) => {
if (!p.total) {
console.warn('No total available for upload progress')
return
}
const progress = (p.loaded / p.total) * 100
file.progress = progress
setUploadFiles([...allFiles])
},
})
file.progress = 100
file.status = 'success'
file.openUrl = uploadInfo.requestId
? `/article/sr/${uploadInfo.requestId}`
: undefined
file.message = uploadInfo.message
setUploadFiles([...allFiles])
} catch (error) {
file.status = 'error'
setUploadFiles([...allFiles])
}
}
})()
},
[uploadFiles]
)
return (
<VStack
distribution="start"
css={{ gap: '10px', width: '100%', height: '100%' }}
>
{props.info && (
<HStack
distribution="start"
alignment="start"
css={{
width: '100%',
bg: '#007AFF10',
p: '5px',
pl: '10px',
fontSize: '12px',
fontFamily: '$inter',
textAlign: 'center',
color: '$ctaBlue',
borderRadius: '5px',
}}
>
{props.info}
</HStack>
)}
<Dropzone
ref={dropzoneRef}
onDragEnter={() => {
setInDragOperation(true)
}}
onDragLeave={() => {
setInDragOperation(false)
}}
onDropAccepted={handleAcceptedFiles}
onDropRejected={(fileRejections: FileRejection[], event: DropEvent) => {
console.log('onDropRejected: ', fileRejections, event)
alert('You can only upload PDF files to your Omnivore Library.')
setInDragOperation(false)
event.preventDefault()
}}
preventDropOnDocument={true}
noClick={true}
accept={props.accept}
>
{({ getRootProps, getInputProps, acceptedFiles, fileRejections }) => (
<div
{...getRootProps({ className: 'dropzone' })}
style={{ width: '100%', height: '100%' }}
>
<DragnDropContainer>
<DragnDropStyle>
<DragnDropIndicator
css={{
border: inDragOperation ? '2px dashed blue' : 'unset',
}}
>
<VStack
alignment="center"
css={{ gap: '20px', height: '100%' }}
>
<File
size={40}
color={theme.colors.tabTextUnselected.toString()}
/>
{inDragOperation ? (
<>
<Box
css={{
p: '0px',
fontSize: '12px',
fontFamily: '$inter',
textAlign: 'center',
color: '$tabTextUnselected',
}}
>
Drop to upload your file
</Box>
</>
) : (
<>
<Box
css={{
fontSize: '12px',
fontFamily: '$inter',
textAlign: 'center',
color: '$tabTextUnselected',
}}
>
{props.description}
<br /> or{' '}
<a href="" onClick={openDialog}>
choose your files
</a>
</Box>
</>
)}
</VStack>
</DragnDropIndicator>
</DragnDropStyle>
<VStack css={{ width: '100%', mt: '25px', gap: '5px' }}>
{uploadFiles.map((file) => {
return (
<HStack
key={file.id}
css={{
width: '100%',
height: '54px',
border: '1px dashed $grayBorder',
borderRadius: '5px',
padding: '15px',
gap: '10px',
color: '$thTextContrast',
}}
alignment="center"
distribution="start"
>
<Box
css={{
width: '280px',
maxLines: '1',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: '200px',
overflow: 'hidden',
fontSize: '14px',
fontWeight: 'bold',
}}
>
{file.name}
</Box>
{file.status != 'inprogress' ? (
<HStack
alignment="center"
css={{ marginLeft: 'auto', fontSize: '14px' }}
>
{file.status == 'success' && file.openUrl && (
<a href={file.openUrl}>Read Now</a>
)}
{file.status == 'success' && !file.openUrl && (
<span>
{file.message || 'Your import has started'}
</span>
)}
{file.status == 'error' && (
<SpanBox css={{ color: 'red' }}>
Error Uploading
</SpanBox>
)}
</HStack>
) : (
<ProgressRoot value={file.progress} max={100}>
<ProgressIndicator
style={{
transform: `translateX(-${100 - file.progress}%)`,
}}
/>{' '}
</ProgressRoot>
)}
</HStack>
)
})}
</VStack>
</DragnDropContainer>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
</VStack>
)
}
type TabBarProps = {
selectedTab: string
setSelectedTab: (selected: TabName) => void
onOpenChange: (open: boolean) => void
}
const TabBar = (props: TabBarProps) => {
return (
<HStack
distribution="between"
alignment="center"
css={{ width: '100%', gap: '4px' }}
>
<Button
style={props.selectedTab == 'link' ? 'tabSelected' : 'tab'}
onClick={(event) => {
props.setSelectedTab('link')
event.preventDefault()
}}
>
Link
</Button>
<Button
style={props.selectedTab == 'pdf' ? 'tabSelected' : 'tab'}
onClick={(event) => {
props.setSelectedTab('pdf')
event.preventDefault()
}}
>
PDF
</Button>
<Button
style={props.selectedTab == 'feed' ? 'tabSelected' : 'tab'}
onClick={(event) => {
props.setSelectedTab('feed')
event.preventDefault()
}}
>
Feed
</Button>
{/* <Button
style={props.selectedTab == 'opml' ? 'tabSelected' : 'tab'}
onClick={(event) => {
props.setSelectedTab('opml')
event.preventDefault()
}}
>
OPML
</Button> */}
<Button
style={props.selectedTab == 'import' ? 'tabSelected' : 'tab'}
onClick={(event) => {
props.setSelectedTab('import')
event.preventDefault()
}}
>
Import
</Button>
<SpanBox css={{ ml: 'auto' }}>
<CloseButton close={() => props.onOpenChange(false)} />
</SpanBox>
</HStack>
)
}

View File

@ -2,10 +2,9 @@ import { Book } from 'phosphor-react'
import { VStack } from '../../elements/LayoutPrimitives'
import { StyledText } from '../../elements/StyledText'
import { theme } from '../../tokens/stitches.config'
import { useGetHeaderHeight } from './HeaderSpacer'
import { DEFAULT_HEADER_HEIGHT } from './HeaderSpacer'
export function EmptyHighlights(): JSX.Element {
const headerHeight = useGetHeaderHeight()
return (
<VStack
alignment="center"
@ -13,7 +12,7 @@ export function EmptyHighlights(): JSX.Element {
css={{
color: '$grayTextContrast',
textAlign: 'center',
marginTop: headerHeight,
marginTop: DEFAULT_HEADER_HEIGHT,
}}
>
<Book size={44} color={theme.colors.grayTextContrast.toString()} />

View File

@ -2,32 +2,32 @@ import { usePersistedState } from '../../../lib/hooks/usePersistedState'
import { PinnedSearch } from '../../../pages/settings/pinned-searches'
import { Box } from '../../elements/LayoutPrimitives'
export const DEFAULT_HEADER_HEIGHT = '70px'
export const DEFAULT_HEADER_HEIGHT = '85px'
export const useGetHeaderHeight = () => {
const [hidePinnedSearches] = usePersistedState({
key: '--library-hide-pinned-searches',
initialValue: false,
isSessionStorage: false,
})
const [pinnedSearches] = usePersistedState<PinnedSearch[] | null>({
key: `--library-pinned-searches`,
initialValue: [],
isSessionStorage: false,
})
// export const useGetHeaderHeight = () => {
// const [hidePinnedSearches] = usePersistedState({
// key: '--library-hide-pinned-searches',
// initialValue: false,
// isSessionStorage: false,
// })
// const [pinnedSearches] = usePersistedState<PinnedSearch[] | null>({
// key: `--library-pinned-searches`,
// initialValue: [],
// isSessionStorage: false,
// })
if (hidePinnedSearches || !pinnedSearches?.length) {
return '70px'
}
return '100px'
}
// if (hidePinnedSearches || !pinnedSearches?.length) {
// return '90px'
// }
// return '90px'
// }
export function HeaderSpacer(): JSX.Element {
const headerHeight = useGetHeaderHeight()
// const headerHeight = useGetHeaderHeight()
return (
<Box
css={{
height: headerHeight,
height: DEFAULT_HEADER_HEIGHT,
bg: '$grayBase',
'@mdDown': {
height: DEFAULT_HEADER_HEIGHT,

View File

@ -20,7 +20,7 @@ import {
import { LibraryHighlightGridCard } from '../../patterns/LibraryCards/LibraryHighlightGridCard'
import { NotebookContent } from '../article/Notebook'
import { EmptyHighlights } from './EmptyHighlights'
import { DEFAULT_HEADER_HEIGHT, useGetHeaderHeight } from './HeaderSpacer'
import { DEFAULT_HEADER_HEIGHT } from './HeaderSpacer'
import { highlightsAsMarkdown } from './HighlightItem'
type HighlightItemsLayoutProps = {
@ -33,7 +33,7 @@ type HighlightItemsLayoutProps = {
export function HighlightItemsLayout(
props: HighlightItemsLayoutProps
): JSX.Element {
const headerHeight = useGetHeaderHeight()
// const headerHeight = useGetHeaderHeight()
const [currentItem, setCurrentItem] = useState<LibraryItem | undefined>(
undefined
)
@ -106,7 +106,7 @@ export function HighlightItemsLayout(
<Box
css={{
width: '100%',
height: `calc(100vh - ${headerHeight})`,
height: `calc(100vh - ${DEFAULT_HEADER_HEIGHT})`,
'@mdDown': {
height: DEFAULT_HEADER_HEIGHT,
},
@ -122,7 +122,7 @@ export function HighlightItemsLayout(
<HStack
css={{
width: '100%',
height: `calc(100vh - ${headerHeight})`,
height: `calc(100vh - ${DEFAULT_HEADER_HEIGHT})`,
'@lgDown': {
overflowY: 'scroll',
},
@ -153,7 +153,7 @@ export function HighlightItemsLayout(
>
<VStack
css={{
minHeight: `calc(100vh - ${headerHeight})`,
minHeight: `calc(100vh - ${DEFAULT_HEADER_HEIGHT})`,
bg: '$thBackground',
}}
distribution="start"

View File

@ -31,13 +31,17 @@ import { StyledText } from '../../elements/StyledText'
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
import { LinkedItemCardAction } from '../../patterns/LibraryCards/CardTypes'
import { LinkedItemCard } from '../../patterns/LibraryCards/LinkedItemCard'
import { Box, HStack, VStack } from './../../elements/LayoutPrimitives'
import { Box, HStack, SpanBox, VStack } from './../../elements/LayoutPrimitives'
import { AddLinkModal } from './AddLinkModal'
import { EditLibraryItemModal } from './EditItemModals'
import { EmptyLibrary } from './EmptyLibrary'
import { HighlightItemsLayout } from './HighlightsLayout'
import { LibraryFilterMenu } from './LibraryFilterMenu'
import { LibraryHeader, MultiSelectMode } from './LibraryHeader'
import {
LibraryHeader,
MultiSelectMode,
headerControlWidths,
} from './LibraryHeader'
import { UploadModal } from '../UploadModal'
import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
import { bulkActionMutation } from '../../../lib/networking/mutations/bulkActionMutation'
@ -53,6 +57,8 @@ import { articleQuery } from '../../../lib/networking/queries/useGetArticleQuery
import { searchQuery } from '../../../lib/networking/queries/search'
import { MoreOptionsIcon } from '../../elements/images/MoreOptionsIcon'
import { theme } from '../../tokens/stitches.config'
import { PinnedSearch } from '../../../pages/settings/pinned-searches'
import { PinnedButtons } from './PinnedButtons'
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
export type LibraryMode = 'reads' | 'highlights'
@ -879,7 +885,7 @@ export function HomeFeedContainer(): JSX.Element {
)
}
type HomeFeedContentProps = {
export type HomeFeedContentProps = {
items: LibraryItem[]
searchTerm?: string
reloadItems: () => void
@ -956,24 +962,23 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
width: props.mode == 'highlights' ? '100%' : 'unset',
}}
>
<LibraryHeader
layout={layout}
updateLayout={updateLayout}
searchTerm={props.searchTerm}
applySearchQuery={(searchQuery: string) => {
props.applySearchQuery(searchQuery)
}}
handleLinkSubmission={props.handleLinkSubmission}
allowSelectMultiple={props.mode !== 'highlights'}
alwaysShowHeader={props.mode == 'highlights'}
showFilterMenu={showFilterMenu}
setShowFilterMenu={setShowFilterMenu}
multiSelectMode={props.multiSelectMode}
setMultiSelectMode={props.setMultiSelectMode}
numItemsSelected={props.numItemsSelected}
showAddLinkModal={() => props.setShowAddLinkModal(true)}
performMultiSelectAction={props.performMultiSelectAction}
/>
{props.mode != 'highlights' && (
<LibraryHeader
layout={layout}
updateLayout={updateLayout}
searchTerm={props.searchTerm}
applySearchQuery={(searchQuery: string) => {
props.applySearchQuery(searchQuery)
}}
showFilterMenu={showFilterMenu}
setShowFilterMenu={setShowFilterMenu}
multiSelectMode={props.multiSelectMode}
setMultiSelectMode={props.setMultiSelectMode}
numItemsSelected={props.numItemsSelected}
performMultiSelectAction={props.performMultiSelectAction}
/>
)}
<HStack css={{ width: '100%', height: '100%' }}>
<LibraryFilterMenu
setShowAddLinkModal={props.setShowAddLinkModal}
@ -1024,7 +1029,9 @@ type LibraryItemsLayoutProps = {
setIsChecked: (itemId: string, set: boolean) => void
} & HomeFeedContentProps
function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
export function LibraryItemsLayout(
props: LibraryItemsLayoutProps
): JSX.Element {
const [showUnsubscribeConfirmation, setShowUnsubscribeConfirmation] =
useState(false)
const [showUploadModal, setShowUploadModal] = useState(false)
@ -1039,6 +1046,14 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
setShowUnsubscribeConfirmation(false)
}
const [pinnedSearches, setPinnedSearches] = usePersistedState<
PinnedSearch[] | null
>({
key: `--library-pinned-searches`,
initialValue: [],
isSessionStorage: false,
})
return (
<>
<VStack
@ -1051,6 +1066,29 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
>
<Toaster />
<SpanBox
css={{
alignSelf: 'flex-start',
'-ms-overflow-style': 'none',
scrollbarWidth: 'none',
'::-webkit-scrollbar': {
display: 'none',
},
'@lgDown': {
display: 'none',
},
mb: '10px',
}}
>
<PinnedButtons
multiSelectMode={props.multiSelectMode}
layout={props.layout}
items={pinnedSearches ?? []}
searchTerm={props.searchTerm}
applySearchQuery={props.applySearchQuery}
/>
</SpanBox>
{props.isValidating && props.items.length == 0 && <TopBarProgress />}
<div
onDragEnter={(event) => {
@ -1234,7 +1272,8 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element {
outline: 'none',
},
'&> div': {
bg: '$thBackground3',
bg: '$thLeftMenuBackground',
// bg: '$thLibraryBackground',
},
'&:focus': {
outline: 'none',
@ -1246,6 +1285,7 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element {
'&:hover': {
'> div': {
bg: '$thBackgroundActive',
boxShadow: '$cardBoxShadow',
},
'> a': {
bg: '$thBackgroundActive',

View File

@ -19,8 +19,11 @@ import { SavedSearch } from '../../../lib/networking/fragments/savedSearchFragme
import { ToggleCaretDownIcon } from '../../elements/icons/ToggleCaretDownIcon'
import Link from 'next/link'
import { ToggleCaretRightIcon } from '../../elements/icons/ToggleCaretRightIcon'
import { SplitButton } from '../../elements/SplitButton'
import { AvatarDropdown } from '../../elements/AvatarDropdown'
import { PrimaryDropdown } from '../PrimaryDropdown'
export const LIBRARY_LEFT_MENU_WIDTH = '233px'
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
type LibraryFilterMenuProps = {
setShowAddLinkModal: (show: boolean) => void
@ -119,6 +122,7 @@ export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element {
<SavedSearches {...props} savedSearches={savedSearches} />
<Subscriptions {...props} subscriptions={subscriptions} />
<Labels {...props} labels={labels} />
<Footer {...props} />
<Box css={{ height: '250px ' }} />
</Box>
{/* This spacer pushes library content to the right of
@ -241,8 +245,16 @@ function Subscriptions(
>
{!collapsed ? (
<>
<FilterButton filterTerm="in:inbox has:subscriptions" text="All" {...props} />
<FilterButton filterTerm={`in:inbox label:RSS`} text="Feeds" {...props} />
<FilterButton
filterTerm="in:inbox has:subscriptions"
text="All"
{...props}
/>
<FilterButton
filterTerm={`in:inbox label:RSS`}
text="Feeds"
{...props}
/>
<FilterButton
filterTerm={`in:inbox label:Newsletter`}
text="Newsletters"
@ -598,3 +610,41 @@ function EditButton(props: EditButtonProps): JSX.Element {
</Link>
)
}
const Footer = (props: LibraryFilterMenuProps): JSX.Element => {
return (
<HStack
css={{
gap: '10px',
height: '65px',
position: 'fixed',
bottom: '0%',
alignItems: 'center',
backgroundColor: '$thBackground2',
width: LIBRARY_LEFT_MENU_WIDTH,
overflowY: 'auto',
overflowX: 'hidden',
'&::-webkit-scrollbar': {
display: 'none',
},
'@mdDown': {
width: '100%',
},
}}
>
<PrimaryDropdown showThemeSection={true} />
<SpanBox
css={{
marginLeft: 'auto',
marginRight: '15px',
}}
>
<SplitButton
title="Add"
setShowLinkMode={() => props.setShowAddLinkModal(true)}
/>
</SpanBox>
</HStack>
)
}

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { theme } from '../../tokens/stitches.config'
import { FormInput } from '../../elements/FormElements'
@ -14,13 +14,8 @@ import {
X,
} from 'phosphor-react'
import { LayoutType } from './HomeFeedContainer'
import { PrimaryDropdown } from '../PrimaryDropdown'
import { OmnivoreSmallLogo } from '../../elements/images/OmnivoreNameLogo'
import {
DEFAULT_HEADER_HEIGHT,
HeaderSpacer,
useGetHeaderHeight,
} from './HeaderSpacer'
import { DEFAULT_HEADER_HEIGHT, HeaderSpacer } from './HeaderSpacer'
import { LIBRARY_LEFT_MENU_WIDTH } from '../../templates/homeFeed/LibraryFilterMenu'
import { CardCheckbox } from '../../patterns/LibraryCards/LibraryCardStyles'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
@ -37,6 +32,11 @@ import { CaretDownIcon } from '../../elements/icons/CaretDownIcon'
import { PinnedButtons } from './PinnedButtons'
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
import { PinnedSearch } from '../../../pages/settings/pinned-searches'
import { HeaderCheckboxIcon } from '../../elements/icons/HeaderCheckboxIcon'
import { HeaderSearchIcon } from '../../elements/icons/HeaderSearchIcon'
import { HeaderToggleGridIcon } from '../../elements/icons/HeaderToggleGridIcon'
import { HeaderToggleListIcon } from '../../elements/icons/HeaderToggleListIcon'
import useWindowDimensions from '../../../lib/hooks/useGetWindowDimensions'
export type MultiSelectMode = 'off' | 'none' | 'some' | 'visible' | 'search'
@ -47,28 +47,17 @@ type LibraryHeaderProps = {
searchTerm: string | undefined
applySearchQuery: (searchQuery: string) => void
alwaysShowHeader: boolean
allowSelectMultiple: boolean
showFilterMenu: boolean
setShowFilterMenu: (show: boolean) => void
showAddLinkModal: () => void
numItemsSelected: number
multiSelectMode: MultiSelectMode
setMultiSelectMode: (mode: MultiSelectMode) => void
performMultiSelectAction: (action: BulkAction, labelIds?: string[]) => void
handleLinkSubmission: (
link: string,
timezone: string,
locale: string
) => Promise<void>
}
const controlWidths = (
export const headerControlWidths = (
layout: LayoutType,
multiSelectMode: MultiSelectMode
) => {
@ -76,22 +65,34 @@ const controlWidths = (
width: '95%',
'@mdDown': {
width: multiSelectMode !== 'off' ? '100%' : '95%',
display: multiSelectMode !== 'off' ? 'flex' : 'none',
},
'@media (min-width: 930px)': {
width: layout == 'GRID_LAYOUT' ? '660px' : '640px',
width: '620px',
},
'@media (min-width: 1280px)': {
width: '1000px',
width: '940px',
},
'@media (min-width: 1600px)': {
width: '1340px',
width: '1232px',
},
}
}
export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
const headerHeight = useGetHeaderHeight()
const [small, setSmall] = useState(false)
useEffect(() => {
const handleScroll = () => {
setSmall(window.scrollY > 40)
}
if (typeof window !== 'undefined') {
window.addEventListener('scroll', handleScroll)
}
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
return (
<>
<VStack
@ -100,21 +101,19 @@ export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
css={{
top: '0',
right: '0',
left: LIBRARY_LEFT_MENU_WIDTH,
zIndex: 5,
position: 'fixed',
height: headerHeight,
bg: '$thLibraryBackground',
position: 'fixed',
left: LIBRARY_LEFT_MENU_WIDTH,
height: small ? '60px' : DEFAULT_HEADER_HEIGHT,
transition: 'height 0.5s',
'@mdDown': {
left: '0px',
right: '0',
height: DEFAULT_HEADER_HEIGHT,
},
}}
>
{/* These will display/hide depending on breakpoints */}
<LargeHeaderLayout {...props} />
<SmallHeaderLayout {...props} />
</VStack>
{/* This spacer is put in to push library content down
@ -125,6 +124,8 @@ export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
}
function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
const dimensions = useWindowDimensions()
const [showSearchBar, setShowSearchBar] = useState(false)
const [pinnedSearches, setPinnedSearches] = usePersistedState<
PinnedSearch[] | null
>({
@ -133,101 +134,91 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
isSessionStorage: false,
})
return (
<HStack
alignment="center"
distribution="center"
css={{
width: '100%',
height: '100%',
'@mdDown': {
display: 'none',
},
}}
>
<VStack alignment="center" distribution="start">
<ControlButtonBox
layout={props.layout}
updateLayout={props.updateLayout}
numItemsSelected={props.numItemsSelected}
multiSelectMode={props.multiSelectMode}
setMultiSelectMode={props.setMultiSelectMode}
showAddLinkModal={props.showAddLinkModal}
performMultiSelectAction={props.performMultiSelectAction}
searchTerm={props.searchTerm}
applySearchQuery={props.applySearchQuery}
allowSelectMultiple={props.allowSelectMultiple}
handleLinkSubmission={props.handleLinkSubmission}
/>
<SpanBox
css={{
...controlWidths(props.layout, props.multiSelectMode),
maxWidth: '587px',
alignSelf: 'flex-start',
'-ms-overflow-style': 'none',
scrollbarWidth: 'none',
'::-webkit-scrollbar': {
display: 'none',
},
}}
>
<PinnedButtons
items={pinnedSearches ?? []}
searchTerm={props.searchTerm}
applySearchQuery={props.applySearchQuery}
/>
</SpanBox>
</VStack>
</HStack>
)
}
function SmallHeaderLayout(props: LibraryHeaderProps): JSX.Element {
const [showInlineSearch, setShowInlineSearch] = useState(false)
const isWideWindow = useMemo(() => {
return dimensions.width >= 480
}, [dimensions])
return (
<HStack
alignment="center"
distribution="start"
css={{
width: '100%',
gap: '10px',
height: '100%',
bg: '$thBackground3',
'@md': {
display: 'none',
},
...headerControlWidths(props.layout, props.multiSelectMode),
}}
>
{showInlineSearch ? (
<HStack css={{ pl: '10px', pr: '0px', width: '100%' }}>
<SearchBox {...props} compact={true} />
<Button
style="cancelGeneric"
onClick={(event) => {
setShowInlineSearch(false)
event.preventDefault()
}}
>
Close
</Button>
{props.multiSelectMode !== 'off' ? (
<HStack alignment="center" css={{ width: '100% ' }}>
<MultiSelectControls {...props} />
</HStack>
) : (
<>
{props.multiSelectMode === 'off' && <MenuHeaderButton {...props} />}
<ControlButtonBox
handleLinkSubmission={props.handleLinkSubmission}
layout={props.layout}
updateLayout={props.updateLayout}
setShowInlineSearch={setShowInlineSearch}
numItemsSelected={props.numItemsSelected}
multiSelectMode={props.multiSelectMode}
setMultiSelectMode={props.setMultiSelectMode}
showAddLinkModal={props.showAddLinkModal}
performMultiSelectAction={props.performMultiSelectAction}
searchTerm={props.searchTerm}
applySearchQuery={props.applySearchQuery}
allowSelectMultiple={props.allowSelectMultiple}
/>
{(!showSearchBar || isWideWindow) && (
<>
<SpanBox
css={{
display: 'none',
'@mdDown': { display: 'flex' },
}}
>
<MenuHeaderButton {...props} />
</SpanBox>
<Button
title="Select multiple"
style="plainIcon"
css={{ display: 'flex', '&:hover': { opacity: '1.0' } }}
onClick={(e) => {
props.setMultiSelectMode('visible')
e.preventDefault()
}}
>
<HeaderCheckboxIcon />
</Button>
</>
)}
{showSearchBar ? (
<SearchBox {...props} setShowSearchBar={setShowSearchBar} />
) : (
<Button
title="search"
style="plainIcon"
css={{ display: 'flex', '&:hover': { opacity: '1.0' } }}
onClick={(e) => {
setShowSearchBar(true)
e.preventDefault()
}}
>
<HeaderSearchIcon />
</Button>
)}
<Button
title={
props.layout == 'GRID_LAYOUT'
? 'Switch to list layout'
: 'Switch to grid layout'
}
style="plainIcon"
css={{
display: 'flex',
marginLeft: 'auto',
'&:hover': { opacity: '1.0' },
}}
onClick={(e) => {
props.updateLayout(
props.layout == 'GRID_LAYOUT' ? 'LIST_LAYOUT' : 'GRID_LAYOUT'
)
e.preventDefault()
}}
>
{props.layout == 'LIST_LAYOUT' ? (
<HeaderToggleGridIcon />
) : (
<HeaderToggleListIcon />
)}
</Button>
</>
)}
</HStack>
@ -243,7 +234,6 @@ export function MenuHeaderButton(props: MenuHeaderButtonProps): JSX.Element {
return (
<HStack
css={{
ml: '10px',
width: '67px',
height: '40px',
bg: props.showFilterMenu ? '$thTextContrast2' : '$thBackground2',
@ -280,14 +270,10 @@ export function MenuHeaderButton(props: MenuHeaderButtonProps): JSX.Element {
export type SearchBoxProps = {
searchTerm: string | undefined
applySearchQuery: (searchQuery: string) => void
setShowSearchBar: (show: boolean) => void
compact?: boolean
onClose?: () => void
handleLinkSubmission: (
link: string,
timezone: string,
locale: string
) => Promise<void>
}
export function SearchBox(props: SearchBoxProps): JSX.Element {
@ -325,9 +311,9 @@ export function SearchBox(props: SearchBoxProps): JSX.Element {
width: '100%',
maxWidth: '521px',
bg: '$thLibrarySearchbox',
borderRadius: '6px',
borderRadius: '100px',
border: focused
? '2px solid $omnivoreCtaYellow'
? '2px solid $searchActiveOutline'
: '2px solid transparent',
boxShadow: focused
? 'none'
@ -373,14 +359,7 @@ export function SearchBox(props: SearchBoxProps): JSX.Element {
<form
onSubmit={async (event) => {
event.preventDefault()
if (!isAddAction) {
props.applySearchQuery(searchTerm || '')
} else {
await props.handleLinkSubmission(searchTerm, timeZone, locale)
setSearchTerm(props.searchTerm ?? '')
props.applySearchQuery(props.searchTerm ?? '')
}
props.applySearchQuery(searchTerm || '')
inputRef.current?.blur()
if (props.onClose) {
props.onClose()
@ -392,7 +371,7 @@ export function SearchBox(props: SearchBoxProps): JSX.Element {
ref={inputRef}
type="text"
value={searchTerm}
autoFocus={!!props.compact}
autoFocus={true}
placeholder="Search keywords or labels"
onFocus={(event) => {
event.target.select()
@ -412,48 +391,48 @@ export function SearchBox(props: SearchBoxProps): JSX.Element {
}}
/>
</form>
{searchTerm && searchTerm.length ? (
<Box
css={{
py: '15px',
marginLeft: 'auto',
}}
>
<IconButton
style="searchButton"
onClick={(event) => {
<HStack
alignment="center"
css={{
py: '15px',
marginLeft: 'auto',
}}
>
{/* <Button
css={{ padding: '4px', borderRadius: '50px', fontSize: '10px' }}
onClick={(event) => {
if (searchTerm && searchTerm.length) {
event.preventDefault()
setSearchTerm('')
props.applySearchQuery('')
inputRef.current?.blur()
}}
tabIndex={-1}
>
<X
width={16}
height={16}
color={theme.colors.grayTextContrast.toString()}
/>
</IconButton>
</Box>
) : (
<Box
css={{
py: '15px',
marginLeft: 'auto',
}}
>
<IconButton
style="searchButton"
onClick={() =>
requestAnimationFrame(() => inputRef?.current?.focus())
} else {
props.setShowSearchBar(false)
}
tabIndex={-1}
>
<kbd aria-hidden>/</kbd>
</IconButton>
</Box>
)}
}}
tabIndex={-1}
>
clear
</Button> */}
<IconButton
style="searchButton"
onClick={(event) => {
if (searchTerm && searchTerm.length && searchTerm != 'in:inbox') {
event.preventDefault()
setSearchTerm('in:inbox')
props.applySearchQuery('')
} else {
props.setShowSearchBar(false)
}
}}
tabIndex={-1}
>
<X
width={16}
height={16}
color={theme.colors.grayTextContrast.toString()}
/>
</IconButton>
</HStack>
</HStack>
</Box>
)
@ -464,24 +443,11 @@ type ControlButtonBoxProps = {
updateLayout: (layout: LayoutType) => void
setShowInlineSearch?: (show: boolean) => void
showAddLinkModal: () => void
allowSelectMultiple: boolean
numItemsSelected: number
multiSelectMode: MultiSelectMode
setMultiSelectMode: (mode: MultiSelectMode) => void
performMultiSelectAction: (action: BulkAction, labelIds?: string[]) => void
searchTerm: string | undefined
applySearchQuery: (searchQuery: string) => void
handleLinkSubmission: (
link: string,
timezone: string,
locale: string
) => Promise<void>
}
function MultiSelectControls(props: ControlButtonBoxProps): JSX.Element {
@ -571,7 +537,6 @@ function SearchControlButtonBox(
): JSX.Element {
return (
<>
<SearchBox {...props} />
<Button
style="plainIcon"
css={{ display: 'flex', marginLeft: 'auto' }}
@ -588,10 +553,6 @@ function SearchControlButtonBox(
<GridViewIcon size={30} color={'#898989'} />
)}
</Button>
<PrimaryDropdown
showThemeSection={true}
showAddLinkModal={props.showAddLinkModal}
/>
</>
)
}
@ -667,97 +628,87 @@ const MuliSelectControl = (props: ControlButtonBoxProps): JSX.Element => {
)
}
function ControlButtonBox(props: ControlButtonBoxProps): JSX.Element {
return (
<>
<HStack
alignment="center"
distribution={props.multiSelectMode !== 'off' ? 'center' : 'start'}
css={{
gap: '10px',
...controlWidths(props.layout, props.multiSelectMode),
}}
>
<MuliSelectControl {...props} />
{props.multiSelectMode !== 'off' && (
<SpanBox
css={{
flex: 1,
display: 'flex',
gap: '2px',
alignItems: 'center',
}}
>
<SpanBox
css={{
color: '#55B938',
paddingLeft: '5px',
fontSize: '12px',
fontWeight: '600',
fontFamily: '$inter',
'@xlgDown': {
paddingLeft: '5px',
},
}}
>
{props.numItemsSelected}{' '}
<SpanBox
css={{
'@media (max-width: 1280px)': { display: 'none' },
}}
>
selected
</SpanBox>
</SpanBox>
</SpanBox>
)}
{props.multiSelectMode !== 'off' ? (
<>
<MultiSelectControls {...props} />
<SpanBox css={{ flex: 1 }}></SpanBox>
</>
) : (
<SearchControlButtonBox {...props} />
)}
</HStack>
// function ControlButtonBox(props: ControlButtonBoxProps): JSX.Element {
// return (
// <>
{props.setShowInlineSearch && props.multiSelectMode === 'off' && (
<HStack
alignment="center"
distribution="end"
css={{
marginLeft: 'auto',
marginRight: '20px',
width: '100px',
height: '100%',
gap: '20px',
'@md': {
display: 'none',
},
}}
>
<Button
style="ghost"
onClick={() => {
props.setShowInlineSearch && props.setShowInlineSearch(true)
}}
css={{
display: 'flex',
}}
>
<MagnifyingGlass
size={20}
color={theme.colors.graySolid.toString()}
/>
</Button>
<PrimaryDropdown
showThemeSection={true}
layout={props.layout}
updateLayout={props.updateLayout}
showAddLinkModal={props.showAddLinkModal}
/>
</HStack>
)}
</>
)
}
// <SpanBox
// css={{
// flex: 1,
// display: 'flex',
// gap: '2px',
// alignItems: 'center',
// }}
// >
// <SpanBox
// css={{
// color: '#55B938',
// paddingLeft: '5px',
// fontSize: '12px',
// fontWeight: '600',
// fontFamily: '$inter',
// '@xlgDown': {
// paddingLeft: '5px',
// },
// }}
// >
// {props.numItemsSelected}{' '}
// <SpanBox
// css={{
// '@media (max-width: 1280px)': { display: 'none' },
// }}
// >
// selected
// </SpanBox>
// </SpanBox>
// </SpanBox>
// )}
// {/* {props.multiSelectMode !== 'off' ? (
// <>
// <MultiSelectControls {...props} />
// <SpanBox css={{ flex: 1 }}></SpanBox>
// </>
// ) : (
// <SearchControlButtonBox {...props} />
// )} */}
// {/* </HStack> */}
// // {props.setShowInlineSearch && props.multiSelectMode === 'off' && (
// // <HStack
// // alignment="center"
// // distribution="end"
// // css={{
// // marginLeft: 'auto',
// // marginRight: '20px',
// // width: '100px',
// // height: '100%',
// // gap: '20px',
// // '@md': {
// // display: 'none',
// // },
// // }}
// // >
// // <Button
// // style="ghost"
// // onClick={() => {
// // props.setShowInlineSearch && props.setShowInlineSearch(true)
// // }}
// // css={{
// // display: 'flex',
// // }}
// // >
// // <MagnifyingGlass
// // size={20}
// // color={theme.colors.graySolid.toString()}
// // />
// // </Button>
// // <PrimaryDropdown
// // showThemeSection={true}
// // layout={props.layout}
// // updateLayout={props.updateLayout}
// // />
// // </HStack>
// // )}
// </>
// )
// }

View File

@ -8,11 +8,16 @@ import { MoreOptionsIcon } from '../../elements/images/MoreOptionsIcon'
import { PinnedSearch } from '../../../pages/settings/pinned-searches'
import { useRouter } from 'next/router'
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
import { LayoutType } from './HomeFeedContainer'
import { MultiSelectMode } from './LibraryHeader'
type PinnedButtonsProps = {
items: PinnedSearch[]
searchTerm: string | undefined
applySearchQuery: (searchQuery: string) => void
multiSelectMode: MultiSelectMode
layout: LayoutType
}
export const PinnedButtons = (props: PinnedButtonsProps): JSX.Element => {
@ -22,53 +27,82 @@ export const PinnedButtons = (props: PinnedButtonsProps): JSX.Element => {
initialValue: false,
isSessionStorage: false,
})
const [opacity, setOpacity] = useState(1.0)
useEffect(() => {
const handleScroll = () => {
const scrollTop = window.scrollY
const opacityValue = 1 - scrollTop / 15
setOpacity(opacityValue >= 0 ? opacityValue : 0)
}
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
if (hidePinnedSearches || !props.items.length) {
return <></>
}
return (
<HStack
alignment="center"
distribution="start"
css={{
width: '100%',
maxWidth: '100%',
pt: '10px',
pb: '0px',
gap: '10px',
bg: 'transparent',
overflowX: 'scroll',
}}
>
{props.items.map((item) => {
const style =
item.search == props.searchTerm ? 'ctaPill' : 'ctaPillUnselected'
return (
<Button
key={item.search}
style={style}
onClick={(event) => {
props.applySearchQuery(item.search)
event.preventDefault()
}}
>
{item.name}
</Button>
)
})}
<HStack alignment="center" distribution="start" css={{ maxWidth: '100%' }}>
<HStack
alignment="center"
distribution="start"
css={{
gap: '10px',
bg: 'transparent',
overflowX: 'scroll',
opacity: opacity,
'@lgDown': {
display: 'none',
},
'@media (min-width: 930px)': {
px: '0px',
maxWidth: '580px',
},
'@media (min-width: 1280px)': {
maxWidth: '890px',
},
'@media (min-width: 1600px)': {
maxWidth: '1200px',
},
}}
>
{props.items.map((item) => {
const style =
item.search == props.searchTerm ? 'ctaPill' : 'ctaPillUnselected'
return (
<Button
key={item.search}
style={style}
onClick={(event) => {
props.applySearchQuery(item.search)
event.preventDefault()
}}
>
{item.name}
</Button>
)
})}
</HStack>
<Dropdown
triggerElement={
<SpanBox
css={{
ml: '10px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
width: '24px',
height: '24px',
width: '30px',
height: '30px',
border: '1px solid $thBackground4',
backgroundColor: '$thBackground4',
'&:hover': {
bg: '$grayBgHover',
border: '1px solid $grayBgHover',

View File

@ -3,7 +3,7 @@ import { Button } from '../../elements/Button'
import { PrimaryDropdown } from '../PrimaryDropdown'
import { LogoBox } from '../../elements/LogoBox'
import { ReactNode } from 'react'
import { useGetHeaderHeight } from '../homeFeed/HeaderSpacer'
import { DEFAULT_HEADER_HEIGHT } from '../homeFeed/HeaderSpacer'
import { theme } from '../../tokens/stitches.config'
import { ReaderSettingsIcon } from '../../elements/icons/ReaderSettingsIcon'
import { CircleUtilityMenuIcon } from '../../elements/icons/CircleUtilityMenuIcon'
@ -16,7 +16,6 @@ type ReaderHeaderProps = {
}
export function ReaderHeader(props: ReaderHeaderProps): JSX.Element {
const headerHeight = useGetHeaderHeight()
return (
<>
<VStack
@ -29,7 +28,7 @@ export function ReaderHeader(props: ReaderHeaderProps): JSX.Element {
pt: '0px',
position: 'fixed',
width: '100%',
height: headerHeight,
height: DEFAULT_HEADER_HEIGHT,
display: props.alwaysDisplayToolbar ? 'flex' : 'transparent',
pointerEvents: props.alwaysDisplayToolbar ? 'unset' : 'none',
borderBottom: '1px solid transparent',

View File

@ -103,7 +103,8 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
borderWidths: {},
borderStyles: {},
shadows: {
cardBoxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.05);',
// cardBoxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.05);',
cardBoxShadow: '0px 4px 4px rgba(0, 0, 0, 0.20);',
},
zIndices: {},
transitions: {},
@ -129,7 +130,9 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
grayBorderHover: 'hsl(0 0% 78.0%)',
grayText: '#6A6968',
// Semantic Colors
ctaBlue: '#007AFF',
modalBackground: '#FFFFFF',
highlightBackground: '255, 210, 52',
recommendedHighlightBackground: '#E5FFE5',
highlight: '#FFD234',
@ -142,6 +145,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
omnivoreYellow: 'rgb(255, 234, 159)',
omnivoreLightGray: 'rgb(125, 125, 125)',
omnivoreCtaYellow: 'rgb(255, 210, 52)',
searchActiveOutline: 'rgb(255, 210, 52)',
// Reader Colors
readerBg: 'white',
@ -166,18 +170,24 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
// once all switch over, we will rename
thBackground: '#FFFFFF',
thBackground2: '#F3F3F3',
thBackground3: '#FFFFFF',
thBackground3: '#FCFCFC',
thBackground4: '#EBEBEB',
thBackground5: '#F5F5F5',
thBackgroundActive: '#FFEA9F',
thBackgroundContrast: '#FFFFFF',
thLeftMenuBackground: '#FCFCFC',
thLibraryBackground: '#F3F3F3',
thLibraryBackground: '#FFFFFF',
thLibrarySearchbox: '#FCFCFC',
thLibraryMenuPrimary: '#3D3D3D',
thLibraryMenuSecondary: '#3D3D3D',
thLibraryMenuUnselected: '#898989',
thLibrarySelectionColor: '#FFEA9F',
thLibraryNavigationMenuFooter: '#EFEADE',
thLibraryMenuFooterHover: '#FFFFFF',
thFormInput: '#EBEBEB',
thHeaderIconRing: '#D9D9D9',
thHeaderIconInner: '#898989',
thNotebookSubtle: '#6A6968',
thNotebookBorder: '#D9D9D9',
@ -193,6 +203,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
thBorderColor: '#E1E1E1',
thBorderSubtle: '#EEEEEE',
tabTextUnselected: '#898989',
thProgressFg: '#FFD234',
@ -230,6 +241,9 @@ const darkThemeSpec = {
colorScheme: {
colorScheme: 'dark',
},
shadows: {
cardBoxShadow: '0px 4px 8px rgba(0, 0, 0, 0.50);',
},
colors: {
grayBase: '#252525',
grayBg: '#3B3938',
@ -246,6 +260,8 @@ const darkThemeSpec = {
grayBorderHover: 'hsl(0 0% 31.2%)',
grayText: '#CDCDCD',
modalBackground: '#2A2A2A',
// Semantic Colors
highlightBackground: '134, 109, 21',
recommendedHighlightBackground: '#1F4315',
@ -283,13 +299,20 @@ const darkThemeSpec = {
thBackground5: '#3D3D3D',
thBackgroundActive: '#3D3D3D',
thBackgroundContrast: '#000000',
thLeftMenuBackground: '#1D1D1D',
thLibraryBackground: '#333333',
thLeftMenuBackground: '#343434',
thLibraryBackground: '#2A2A2A',
thLibrarySearchbox: '#3D3D3D',
thLibraryMenuPrimary: '#EBEBEB',
thLibraryMenuSecondary: '#EBEBEB',
thLibraryMenuUnselected: '#898989',
thLibrarySelectionColor: '#3D3D3D',
thLibraryNavigationMenuFooter: '#3D3D3D',
thLibraryMenuFooterHover: '#6A6968',
searchActiveOutline: '#866D15',
thFormInput: '#3D3D3D',
thHeaderIconRing: '#3D3D3D',
thHeaderIconInner: '#D9D9D9',
thNotebookSubtle: '#898989',
thNotebookBorder: '#3D3D3D',
@ -306,6 +329,7 @@ const darkThemeSpec = {
thBorderColor: '#4F4F4F',
thBorderSubtle: '#6A6968',
tabTextUnselected: '#6A6968',
thProgressFg: '#FFD234',
@ -319,10 +343,6 @@ const darkThemeSpec = {
highlight_underline_alpha: '0.5',
highlight_background_alpha: '0.35',
},
shadows: {
cardBoxShadow:
'0px 0px 9px -2px rgba(5, 5, 5, 0.16), 0px 7px 12px rgba(0, 0, 0, 0.13)',
},
}
// This is used by iOS

View File

@ -8,9 +8,10 @@ function getWindowDimensions() {
}
}
export default function useWindowDimensions() {
const [windowDimensions, setWindowDimensions] = useState(
getWindowDimensions()
)
const [windowDimensions, setWindowDimensions] = useState({
width: 0,
height: 0,
})
useEffect(() => {
function handleResize() {

View File

@ -31,6 +31,9 @@ export type ReaderSettings = {
setJustifyText: (set: boolean) => void
highContrastText: boolean | undefined
setHighContrastText: (set: boolean) => void
highlightOnRelease: boolean | undefined
setHighlightOnRelease: (set: boolean) => void
}
export const useReaderSettings = (): ReaderSettings => {
@ -61,6 +64,13 @@ export const useReaderSettings = (): ReaderSettings => {
initialValue: false,
})
const [highlightOnRelease, setHighlightOnRelease] = usePersistedState<
boolean | undefined
>({
key: `--display-highlight-on-release`,
initialValue: false,
})
const [justifyText, setJustifyText] = usePersistedState<boolean | undefined>({
key: `--display-justify-text`,
initialValue: false,
@ -216,5 +226,7 @@ export const useReaderSettings = (): ReaderSettings => {
setJustifyText,
highContrastText,
setHighContrastText,
highlightOnRelease,
setHighlightOnRelease,
}
}

View File

@ -563,6 +563,9 @@ export default function Home(): JSX.Element {
setShowHighlightsModal={setShowHighlightsModal}
justifyText={readerSettings.justifyText ?? undefined}
highContrastText={readerSettings.highContrastText ?? undefined}
highlightOnRelease={
readerSettings.highlightOnRelease ?? undefined
}
articleMutations={{
createHighlightMutation,
deleteHighlightMutation,