Merge pull request #3524 from omnivore-app/feat/library-navigation-updates
Updated library layout
This commit is contained in:
@ -532,6 +532,7 @@ const processSubscription = async (
|
||||
if (itemCount == 100) {
|
||||
logger.info(`Max limit reached for feed ${feedUrl}`)
|
||||
}
|
||||
itemCount = itemCount + 1
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ const StyledFallback = styled(Fallback, {
|
||||
justifyContent: 'center',
|
||||
fontSize: '15px',
|
||||
fontWeight: 600,
|
||||
fontFamily: 'Inter',
|
||||
fontFamily: '$inter',
|
||||
color: '$avatarFont',
|
||||
backgroundColor: '$avatarBg',
|
||||
})
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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}`,
|
||||
|
||||
@ -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}`,
|
||||
|
||||
@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -573,7 +573,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (props.highlightOnRelease && selectionData?.wasDragEvent) {
|
||||
if (props.highlightOnRelease) {
|
||||
handleAction('create')
|
||||
setSelectionData(null)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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()} />
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
// // )}
|
||||
// </>
|
||||
// )
|
||||
// }
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user