Colour clean ups

This commit is contained in:
Jackson Harper 2024-02-08 15:01:40 +08:00
parent 88a4dc6c28
commit 11463ba6f7
11 changed files with 842 additions and 114 deletions

View file

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

View file

@ -18,6 +18,21 @@ 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',
'&:hover': {
opacity: '0.6',
border: '0px solid $ctaBlue',
},
},
ctaDarkYellow: {
border: '1px solid transparent',
fontSize: '14px',
@ -215,6 +230,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: '$thBorderSubtle',
cursor: 'pointer',
borderRadius: '5px',
'&:hover': {
color: '$thTextContrast',
},
},
tabSelected: {
px: '15px',
py: '6px',
border: 'none',
bg: '$thBorderSubtle',
fontSize: '12px',
fontWeight: '500',
fontFamily: '$inter',
color: '$textContrast',
cursor: 'pointer',
borderRadius: '5px',
'&:hover': {
color: '$thTextContrast',
},
},
squareIcon: {
mx: '$1',
display: 'flex',

View file

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

View file

@ -5,8 +5,11 @@ import { Button } from './Button'
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 => {
@ -41,35 +44,39 @@ const CaretButton = (): JSX.Element => {
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',
// minWidth: '70px',
bg: '$ctaBlue',
color: '#EDEDED',
fontSize: '12px',
fontSize: '14px',
fontFamily: '$inter',
border: '0px solid transparent',
borderTopLeftRadius: '5px',
borderBottomLeftRadius: '5px',
borderTopRightRadius: '0px',
borderBottomRightRadius: '0px',
borderTopRightRadius: '5px',
borderBottomRightRadius: '5px',
'&:hover': {
opacity: 1.0,
color: 'white',
opacity: 0.6,
border: '0px solid transparent',
},
'&:focus': {
outline: 'none',
border: '0px solid transparent',
},
}}
onClick={(event) => {
props.setShowLinkMode('link')
event.preventDefault()
}}
>
{props.title}
</Button>
<Dropdown triggerElement={<CaretButton />}>
{/* <Dropdown triggerElement={<CaretButton />}>
<DropdownOption onSelect={() => console.log()} title="Archive (e)" />
</Dropdown>
</Dropdown> */}
</HStack>
)
}

View file

@ -49,12 +49,12 @@ const TriggerButton = (props: TriggerButtonProps): JSX.Element => {
return (
<HStack
css={{
mx: '10px',
gap: '10px',
alignItems: 'center',
borderRadius: '5px',
height: '27px',
paddingLeft: '5px',
paddingRight: '5px',
height: '32px',
padding: '5px',
'&:hover': {
bg: '#6A6968',
opacity: '0.7px',

View file

@ -1,11 +1,13 @@
import { useCallback, useState } from 'react'
import { useCallback, useRef, useState } from 'react'
import * as Progress from '@radix-ui/react-progress'
import { File } 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,577 @@ 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: '$thLibrarySearchbox',
}}
/>
<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
description="Drag PDFs here to add to your library"
accept={{
'application/pdf': ['.pdf'],
}}
/>
<SpanBox css={{ fontSize: '12px', color: '$thTextSubtle' }}>
* PDFs have a maximum size of 8MB
</SpanBox>
</VStack>
)
}
const UploadImportTab = (props: AddLinkModalProps): JSX.Element => {
return (
<VStack
alignment="start"
distribution="start"
css={{ height: '180px', width: '100%' }}
>
<UploadPad
description="Drop import files here"
accept={{
'text/csv': ['.csv'],
'application/zip': ['.zip'],
}}
/>
<SpanBox css={{ fontSize: '12px', color: '$thTextSubtle' }}>
* Imports must be in a supported format{' '}
<a
href="https://docs.omnivore.app/using/importing.html"
target="_blank"
rel="noreferrer"
>
read more
</a>
.
</SpanBox>
</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 = {
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={{ width: '100%', height: '100%' }}>
<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.thTextSubtle2.toString()}
/>
{inDragOperation ? (
<>
<Box
css={{
p: '0px',
fontSize: '14px',
}}
>
Drop to upload your file
</Box>
</>
) : (
<>
<Box
css={{
fontSize: '14px',
textAlign: 'center',
}}
>
{props.description}
<br /> or{' '}
<a href="" onClick={openDialog}>
choose your files
</a>
</Box>
</>
)}
</VStack>
</DragnDropIndicator>
</DragnDropStyle>
<VStack css={{ width: '100%', mt: '25px', gap: '5px' }}>
{uploadFiles.map((file) => {
return (
<HStack
key={file.id}
css={{
width: '100%',
height: '54px',
border: '1px dashed $grayBorder',
borderRadius: '5px',
padding: '15px',
gap: '10px',
color: '$thTextContrast',
}}
alignment="center"
distribution="start"
>
<Box
css={{
width: '280px',
maxLines: '1',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: '200px',
overflow: 'hidden',
fontSize: '14px',
fontWeight: 'bold',
}}
>
{file.name}
</Box>
{file.status != 'inprogress' ? (
<HStack
alignment="center"
css={{ marginLeft: 'auto', fontSize: '14px' }}
>
{file.status == 'success' && file.openUrl && (
<a href={file.openUrl}>Read Now</a>
)}
{file.status == 'success' && !file.openUrl && (
<span>
{file.message || 'Your import has started'}
</span>
)}
{file.status == 'error' && (
<SpanBox css={{ color: 'red' }}>
Error Uploading
</SpanBox>
)}
</HStack>
) : (
<ProgressRoot value={file.progress} max={100}>
<ProgressIndicator
style={{
transform: `translateX(-${100 - file.progress}%)`,
}}
/>{' '}
</ProgressRoot>
)}
</HStack>
)
})}
</VStack>
</DragnDropContainer>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
</VStack>
)
}
type TabBarProps = {
selectedTab: string
setSelectedTab: (selected: TabName) => void
onOpenChange: (open: boolean) => void
}
const TabBar = (props: TabBarProps) => {
return (
<HStack
distribution="between"
alignment="center"
css={{ width: '100%', gap: '4px' }}
>
<Button
style={props.selectedTab == 'link' ? 'tabSelected' : 'tab'}
onClick={(event) => {
props.setSelectedTab('link')
event.preventDefault()
}}
>
Link
</Button>
<Button
style={props.selectedTab == 'pdf' ? 'tabSelected' : 'tab'}
onClick={(event) => {
props.setSelectedTab('pdf')
event.preventDefault()
}}
>
PDF
</Button>
<Button
style={props.selectedTab == 'feed' ? 'tabSelected' : 'tab'}
onClick={(event) => {
props.setSelectedTab('feed')
event.preventDefault()
}}
>
Feed
</Button>
{/* <Button
style={props.selectedTab == 'opml' ? 'tabSelected' : 'tab'}
onClick={(event) => {
props.setSelectedTab('opml')
event.preventDefault()
}}
>
OPML
</Button> */}
<Button
style={props.selectedTab == 'import' ? 'tabSelected' : 'tab'}
onClick={(event) => {
props.setSelectedTab('import')
event.preventDefault()
}}
>
Import
</Button>
<SpanBox css={{ ml: 'auto' }}>
<CloseButton close={() => props.onOpenChange(false)} />
</SpanBox>
</HStack>
)
}

View file

@ -963,9 +963,6 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
applySearchQuery={(searchQuery: string) => {
props.applySearchQuery(searchQuery)
}}
handleLinkSubmission={props.handleLinkSubmission}
allowSelectMultiple={props.mode !== 'highlights'}
alwaysShowHeader={props.mode == 'highlights'}
showFilterMenu={showFilterMenu}
setShowFilterMenu={setShowFilterMenu}
multiSelectMode={props.multiSelectMode}
@ -1234,7 +1231,8 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element {
outline: 'none',
},
'&> div': {
bg: '$thBackground3',
bg: '$thLeftMenuBackground',
// bg: '$thLibraryBackground',
},
'&:focus': {
outline: 'none',
@ -1246,6 +1244,7 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element {
'&:hover': {
'> div': {
bg: '$thBackgroundActive',
boxShadow: '$cardBoxShadow',
},
'> a': {
bg: '$thBackgroundActive',

View file

@ -122,7 +122,7 @@ export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element {
<SavedSearches {...props} savedSearches={savedSearches} />
<Subscriptions {...props} subscriptions={subscriptions} />
<Labels {...props} labels={labels} />
<Footer />
<Footer {...props} />
<Box css={{ height: '250px ' }} />
</Box>
{/* This spacer pushes library content to the right of
@ -611,16 +611,18 @@ function EditButton(props: EditButtonProps): JSX.Element {
)
}
const Footer = (): JSX.Element => {
const Footer = (props: LibraryFilterMenuProps): JSX.Element => {
return (
<HStack
css={{
gap: '10px',
height: '55px',
height: '65px',
position: 'fixed',
bottom: '0%',
alignItems: 'center',
padding: '5px',
paddingX: '5px',
paddingY: '5px',
backgroundColor: '$thBackground2',
width: LIBRARY_LEFT_MENU_WIDTH,
overflowY: 'auto',
@ -638,10 +640,12 @@ const Footer = (): JSX.Element => {
css={{
marginLeft: 'auto',
marginRight: '5px',
'&:hover': { opacity: 1.0, color: 'white' },
}}
>
<SplitButton title="Add Link" />
<SplitButton
title="Add"
setShowLinkMode={() => props.setShowAddLinkModal(true)}
/>
</SpanBox>
</HStack>
)

View file

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { theme } from '../../tokens/stitches.config'
import { FormInput } from '../../elements/FormElements'
@ -41,6 +41,7 @@ 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'
@ -83,7 +84,6 @@ const controlWidths = (
}
export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
const headerHeight = useGetHeaderHeight()
const [small, setSmall] = useState(false)
useEffect(() => {
@ -105,7 +105,7 @@ export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
position: 'fixed',
left: LIBRARY_LEFT_MENU_WIDTH,
height: small ? '60px' : DEFAULT_HEADER_HEIGHT,
transition: '0.5s',
transition: 'height 0.5s',
'@mdDown': {
left: '0px',
right: '0',
@ -123,6 +123,7 @@ 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
@ -132,6 +133,10 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
isSessionStorage: false,
})
const isWideWindow = useMemo(() => {
return dimensions.width >= 480
}, [dimensions])
return (
<HStack
alignment="center"
@ -148,25 +153,29 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
</HStack>
) : (
<>
<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 || 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} />
@ -308,7 +317,7 @@ export function SearchBox(props: SearchBoxProps): JSX.Element {
bg: '$thLibrarySearchbox',
borderRadius: '100px',
border: focused
? '2px solid $omnivoreCtaYellow'
? '2px solid $searchActiveOutline'
: '2px solid transparent',
boxShadow: focused
? 'none'

View file

@ -103,7 +103,8 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
borderWidths: {},
borderStyles: {},
shadows: {
cardBoxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.05);',
// cardBoxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.05);',
cardBoxShadow: '0px 4px 4px rgba(0, 0, 0, 0.20);',
},
zIndices: {},
transitions: {},
@ -130,6 +131,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
grayText: '#6A6968',
ctaBlue: '#007AFF',
modalBackground: '#FFFFFF',
highlightBackground: '255, 210, 52',
recommendedHighlightBackground: '#E5FFE5',
@ -143,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: '#866D15',
// Reader Colors
readerBg: 'white',
@ -234,6 +237,9 @@ const darkThemeSpec = {
colorScheme: {
colorScheme: 'dark',
},
shadows: {
cardBoxShadow: '0px 4px 8px rgba(0, 0, 0, 0.50);',
},
colors: {
grayBase: '#252525',
grayBg: '#3B3938',
@ -250,6 +256,8 @@ const darkThemeSpec = {
grayBorderHover: 'hsl(0 0% 31.2%)',
grayText: '#CDCDCD',
modalBackground: '#2A2A2A',
// Semantic Colors
highlightBackground: '134, 109, 21',
recommendedHighlightBackground: '#1F4315',

View file

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