Merge pull request #394 from omnivore-app/header-and-reader-improvements

Add margins on wider screens, keyboard commands to adjust
This commit is contained in:
Jackson Harper
2022-04-13 21:26:24 -07:00
committed by GitHub
43 changed files with 1440 additions and 351 deletions

View File

@ -115,6 +115,7 @@ export function makeApolloServer(): ApolloServer {
schema: schema,
context: contextFunc,
formatError: (err) => {
console.log('server error', err)
Sentry.captureException(err)
// hide error messages from frontend on prod
return new Error('Unexpected server error')

View File

@ -153,6 +153,14 @@ export const Button = styled('button', {
opacity: 0.8,
},
},
articleActionIcon: {
bg: 'transparent',
border: 'none',
cursor: 'pointer',
'&:hover': {
opacity: 0.8,
},
},
ghost: {
color: 'transparent',
border: 'none',

View File

@ -8,6 +8,7 @@ import {
Arrow,
Label,
} from '@radix-ui/react-dropdown-menu'
import { PopperContentProps } from '@radix-ui/react-popover';
import { CSS } from '@stitches/react';
import { styled } from './../tokens/stitches.config'
@ -49,12 +50,12 @@ export const DropdownContent = styled(Content, {
backgroundColor: '$grayBg',
borderRadius: '0.5em',
padding: 5,
border: '1px solid $grayBorder',
outline: '1px solid $grayBorder',
boxShadow: '$cardBoxShadow',
})
const StyledArrow = styled(Arrow, {
fill: '$grayBase',
fill: '$grayBg',
})
const StyledLabel = styled(Label, {
@ -65,6 +66,7 @@ const StyledLabel = styled(Label, {
})
export type DropdownAlignment = 'start' | 'end' | 'center'
export type DropdownSide = 'top' | 'right' | 'bottom' | 'left'
type DropdownProps = {
labelText?: string
@ -73,8 +75,11 @@ type DropdownProps = {
children: React.ReactNode
styledArrow?: boolean
align?: DropdownAlignment
side?: DropdownSide
sideOffset?: number
disabled?: boolean
css?: CSS
modal?: boolean
}
export const DropdownSeparator = styled(Separator, {
@ -101,17 +106,22 @@ export function DropdownOption(props: DropdownOptionProps): JSX.Element {
)
}
export function Dropdown({
children,
align,
triggerElement,
labelText,
showArrow = true,
disabled = false,
css
}: DropdownProps): JSX.Element {
export function Dropdown(props: DropdownProps & PopperContentProps): JSX.Element {
const {
children,
align,
triggerElement,
labelText,
showArrow = true,
disabled = false,
side = 'bottom',
sideOffset = 0,
alignOffset = 0,
css,
modal
} = props
return (
<Root modal={false}>
<Root modal={modal}>
<DropdownTrigger disabled={disabled}>{triggerElement}</DropdownTrigger>
<DropdownContent
css={css}
@ -119,11 +129,14 @@ export function Dropdown({
// remove focus from dropdown
;(document.activeElement as HTMLElement).blur()
}}
side={side}
sideOffset={sideOffset}
align={align ? align : 'center'}
alignOffset={alignOffset}
>
{labelText && <StyledLabel>{labelText}</StyledLabel>}
{children}
{showArrow && <StyledArrow offset={20} />}
{showArrow && <StyledArrow offset={20} width={20} height={10} />}
</DropdownContent>
</Root>
)

View File

@ -1,3 +1,5 @@
import Link from 'next/link'
import { SpanBox } from './LayoutPrimitives'
import { StyledText } from './StyledText'
type LabelChipProps = {
@ -16,19 +18,25 @@ export function LabelChip(props: LabelChipProps): JSX.Element {
}
const color = hexToRgb(props.color)
return (
<StyledText
css={{
margin: '4px',
borderRadius: '32px',
color: props.color,
fontSize: '12px',
fontWeight: 'bold',
padding: '4px 8px 4px 8px',
border: `1px solid rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.7)`,
backgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.08)`,
}}
>
{props.text}
</StyledText>
<Link href={`/home?q=label:"${props.text}"`}>
<SpanBox
css={{
display: 'inline-table',
margin: '4px',
borderRadius: '32px',
color: props.color,
fontSize: '12px',
fontWeight: 'bold',
padding: '4px 8px 4px 8px',
whiteSpace: 'nowrap',
cursor: 'pointer',
backgroundClip: 'padding-box',
border: `1px solid rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.7)`,
backgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.08)`,
}}
>
{props.text}
</SpanBox>
</Link>
)
}

View File

@ -2,21 +2,18 @@ import React, { useState } from 'react'
import { styled } from '../tokens/stitches.config'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { HexColorPicker } from 'react-colorful'
import { Button } from './Button'
import { HStack, SpanBox } from './LayoutPrimitives'
import { CaretDown } from 'phosphor-react'
import { StyledText } from './StyledText'
import {
ColorDetailsProps,
LabelColor,
LabelColorDropdownProps,
LabelColorHex,
LabelColorObject,
LabelOptionProps,
} from '../../utils/settings-page/labels/types'
import { labelColorObjects } from '../../utils/settings-page/labels/labelColorObjects'
import { DropdownOption } from './DropdownElements'
import { isDarkTheme } from '../../lib/themeUpdater'
import { LabelColor } from '../../lib/networking/fragments/labelFragment'
const DropdownMenuContent = styled(DropdownMenuPrimitive.Content, {
maxWidth: 190,

View File

@ -1,5 +1,5 @@
import { Root, Overlay, Content } from '@radix-ui/react-dialog'
import { styled, keyframes } from '../tokens/stitches.config'
import { styled, keyframes, theme } from '../tokens/stitches.config'
export const ModalRoot = styled(Root, {})
@ -25,8 +25,7 @@ const contentShow = keyframes({
const Modal = styled(Content, {
backgroundColor: '$grayBg',
borderRadius: 6,
boxShadow:
'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
boxShadow: theme.shadows.cardBoxShadow.toString(),
position: 'fixed',
'&:focus': { outline: 'none' },
})

View File

@ -0,0 +1,39 @@
import { Box, HStack } from './LayoutPrimitives'
import { styled, theme } from '../tokens/stitches.config'
type TickedRangeSliderProps = {
ticks?: number,
value: number,
onChange: (value: number) => void,
min?: number,
max?: number,
step?: number,
}
const Tick = styled(Box, {
background: theme.colors.grayBorderHover,
width: 2,
height: 8,
})
export function TickedRangeSlider({
ticks = 8,
min = 10,
max = 28,
step = 1,
value,
onChange,
} : TickedRangeSliderProps
): JSX.Element {
return (
<Box css={{zIndex: 2}}>
<input onChange={(e) => onChange(e.target.value as any)} value={value} type="range" min={min} max={max} step={step} className='slider'/>
<HStack distribution='between' css={{position: 'relative', bottom: 12.2, left: 2, zIndex: -1}}>
{[...Array(ticks)].map((val, idx) => <Tick key={`ticks-${idx}`} />)}
</HStack>
</Box>
)
}

View File

@ -62,6 +62,15 @@ type TooltipWrappedProps = {
style?: TooltipPrimitive.TooltipContentProps['style']
}
const DefaultTooltipStyle = {
backgroundColor: '#F9D354',
color: '#0A0806',
}
const DefaultArrowStyle = {
fill: '#F9D354'
}
export const TooltipWrapped: FC<TooltipWrappedProps> = ({
children,
active,
@ -73,9 +82,14 @@ export const TooltipWrapped: FC<TooltipWrappedProps> = ({
return (
<Tooltip open={active}>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent sideOffset={5} side={tooltipSide} {...props}>
<TooltipContent
sideOffset={5}
side={tooltipSide}
style={DefaultTooltipStyle}
{...props}
>
{tooltipContent}
<TooltipArrow style={arrowStyles} />
<TooltipArrow style={arrowStyles ?? DefaultArrowStyle} />
</TooltipContent>
</Tooltip>
)

View File

@ -0,0 +1,14 @@
type AIconProps = {
size: number
color: string
style?: React.CSSProperties
}
export function AIcon(props: AIconProps): JSX.Element {
return (
<svg style={props.style} width={props.size} height={props.size} viewBox={`0 0 20 20`} fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.5 18L11.75 5.25L5 18" stroke={props.color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M16.5133 14.25H6.98828" stroke={props.color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)
}

View File

@ -2,6 +2,7 @@ import { config } from '../../tokens/stitches.config'
import Image from 'next/image'
import { StyledText } from '../../elements/StyledText'
import Link from 'next/link'
import { SpanBox } from '../LayoutPrimitives'
export function OmnivoreNameLogoImage(): JSX.Element {
return (
@ -51,7 +52,7 @@ export function OmnivoreNameLogo(props: OmnivoreNameLogoProps): JSX.Element {
<Link passHref href={href}>
<a style={{ textDecoration: 'none', display: 'flex', alignItems: 'center' }}>
<OmnivoreLogoIcon size={27} strokeColor={fillColor}></OmnivoreLogoIcon>
<StyledText style="logoTitle" css={{ color: fillColor, paddingLeft: '12px' }}>Omnivore</StyledText>
{/* <StyledText style="logoTitle" css={{ color: fillColor, paddingLeft: '12px' }}>Omnivore</StyledText> */}
</a>
</Link>
)

View File

@ -16,6 +16,7 @@ export type HeaderDropdownAction =
| 'apply-lighter-theme'
| 'navigate-to-install'
| 'navigate-to-emails'
| 'navigate-to-labels'
| 'navigate-to-profile'
| 'increaseFontSize'
| 'decreaseFontSize'
@ -24,7 +25,6 @@ export type HeaderDropdownAction =
type DropdownMenuProps = {
username?: string
triggerElement: ReactNode
displayFontStepper?: boolean
actionHandler: (action: HeaderDropdownAction) => void
}
@ -53,24 +53,6 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element {
{ isDark ? '✓' : '' }
</Button>
</HStack>
{props.displayFontStepper && (
<>
<HStack css={{ mt: '8px', width: '100%', height: '26px', gap: '8px', borderRadius: '6px', border: '1px solid $grayTextContrast', }}>
<Button style='plainIcon' css={{ display: 'inline-block', verticalAlign: 'baseline', width: '50%', height: '100%', bg: 'unset' }} onClick={() => {
props.actionHandler('decreaseFontSize')
}}>
<StyledText css={{ fontSize: '14px', m: '0px' }}>A</StyledText>
</Button>
<Box css={{ width: '1px', height: '100%', bg: '$grayTextContrast' }} />
<Button style='plainIcon' css={{ display: 'inline-block', verticalAlign: 'baseline', width: '50%', height: '100%', bg: 'unset' }} onClick={() => {
props.actionHandler('increaseFontSize')
}}>
<StyledText css={{ fontSize: '18px', m: '0px' }}>A</StyledText>
</Button>
</HStack>
</>
)}
</VStack>
<DropdownSeparator />
<DropdownOption
@ -81,6 +63,10 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element {
onSelect={() => props.actionHandler('navigate-to-emails')}
title="Emails"
/>
<DropdownOption
onSelect={() => props.actionHandler('navigate-to-labels')}
title="Labels"
/>
<DropdownOption
onSelect={() => window.Intercom('show')}
title="Feedback"

View File

@ -44,7 +44,7 @@ export function HighlightBar(props: HighlightBarProps): JSX.Element {
background: '$grayBg',
borderRadius: '4px',
border: '1px solid $grayBorder',
boxShadow: '0px 0px 9px -2px rgba(32, 31, 29, 0.09), 0px 7px 12px rgba(32, 31, 29, 0.07)',
boxShadow: theme.shadows.cardBoxShadow.toString(),
bottom: 'calc(38px + env(safe-area-inset-bottom, 40px))',
'@smDown': {
maxWidth: '80%',

View File

@ -154,11 +154,11 @@ export function GridLinkedItemCard(props: LinkedItemCardProps): JSX.Element {
/>
)}
</HStack>
<HStack css={{ mt: '8px' }}>
{props.item.labels?.map(({ description, color }, index) => (
<LabelChip key={index} text={description || ''} color={color} />
{/* <HStack css={{ mt: '8px' }}>
{props.item.labels?.map(({ name, color }, index) => (
<LabelChip key={index} text={name || ''} color={color} />
))}
</HStack>
</HStack> */}
</VStack>
)
}

View File

@ -17,7 +17,6 @@ import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery'
import { setupAnalytics } from '../../lib/analytics'
import { Button } from '../elements/Button'
import Link from 'next/link'
import { ArrowSquareOut } from 'phosphor-react'
type HeaderProps = {
user?: UserBasicData
@ -26,7 +25,7 @@ type HeaderProps = {
profileImageURL?: string
isFixedPosition: boolean
scrollElementRef?: React.RefObject<HTMLDivElement>
displayFontStepper?: boolean
toolbarControl?: JSX.Element
setShowLogoutConfirmation: (showShareModal: boolean) => void
setShowKeyboardCommandsModal: (showShareModal: boolean) => void
}
@ -55,9 +54,6 @@ export function PrimaryHeader(props: HeaderProps): JSX.Element {
(changeset: ScrollOffsetChangeset) => {
const isScrolledBeyondMinThreshold = changeset.current.y >= 50
const isScrollingDown = changeset.current.y > changeset.previous.y
// setIsScrolled(isScrolledBeyondMinThreshold)
// setShowHeader(!(isScrollingDown && isScrolledBeyondMinThreshold))
},
0
)
@ -106,6 +102,9 @@ export function PrimaryHeader(props: HeaderProps): JSX.Element {
case 'navigate-to-emails':
router.push('/settings/emails')
break
case 'navigate-to-labels':
router.push('/settings/labels')
break
case 'navigate-to-profile':
if (props.user) {
router.push(`/${props.user.profile.username}`)
@ -128,7 +127,7 @@ export function PrimaryHeader(props: HeaderProps): JSX.Element {
isDisplayingShadow={isScrolled}
isVisible={true}
isFixedPosition={true}
displayFontStepper={props.displayFontStepper}
toolbarControl={props.toolbarControl}
/>
</>
)
@ -143,7 +142,7 @@ type NavHeaderProps = {
isDisplayingShadow?: boolean
isVisible?: boolean
isFixedPosition: boolean
displayFontStepper?: boolean
toolbarControl?: JSX.Element
}
function NavHeader(props: NavHeaderProps): JSX.Element {
@ -159,16 +158,16 @@ function NavHeader(props: NavHeaderProps): JSX.Element {
zIndex: 5,
width: '100%',
boxShadow: props.isDisplayingShadow ? '$panelShadow' : 'unset',
bg: '$grayBase',
p: '0px $3 0px $3',
height: '68px',
height: '48px',
position: 'fixed',
minHeight: '68px',
bg: 'transparent',
'@smDown': {
height: '48px',
minHeight: '48px',
p: '0px 18px 0px 16px',
},
'@lgDown': {
bg: '$grayBase',
},
}}
>
<HStack alignment="center" distribution="start">
@ -183,26 +182,26 @@ function NavHeader(props: NavHeaderProps): JSX.Element {
</Box>
<NavLinks currentPath={currentPath} isLoggedIn={!!props.username} />
</HStack>
{props.toolbarControl && (
<HStack distribution="end" alignment="center" css={{
height: '100%', width: '100%',
mr: '16px',
display: 'none',
'@lgDown': {
display: 'flex',
},
}}
>
{props.toolbarControl}
</HStack>
)}
{props.username ? (
<HStack
alignment="center"
css={{ display: 'flex', alignItems: 'center' }}
>
<Box css={{ '@smDown': { visibility: 'collapse' } }}>
<a href="https://github.com/omnivore-app/omnivore" target='_blank' rel="noreferrer">
<Button style="ctaLightGray" css={{ background: 'unset', mr: '32px' }}>
<HStack css={{ height: '100%' }}>
<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
<path fill={theme.colors.grayTextContrast.toString()} fillRule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
</svg>
<SpanBox css={{ pl: '8px', color: '$grayTextContrast' }}>Follow us on GitHub</SpanBox>
<Box className='ctaButtonIcon' css={{ ml: '4px' }}>
<ArrowSquareOut size={16} />
</Box>
</HStack>
</Button>
</a>
</Box>
<DropdownMenu
username={props.username}
triggerElement={
@ -212,7 +211,6 @@ function NavHeader(props: NavHeaderProps): JSX.Element {
/>
}
actionHandler={props.actionHandler}
displayFontStepper={props.displayFontStepper}
/>
</HStack>
) : (

View File

@ -4,7 +4,6 @@ import {
ReactNode,
MutableRefObject,
useEffect,
useContext,
useState,
} from 'react'
import { PrimaryHeader } from './../patterns/PrimaryHeader'
@ -23,7 +22,7 @@ type PrimaryLayoutProps = {
hideHeader?: boolean
pageMetaDataProps?: PageMetaDataProps
scrollElementRef?: MutableRefObject<HTMLDivElement | null>
displayFontStepper?: boolean
headerToolbarControl?: JSX.Element
}
export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
@ -62,23 +61,28 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
{props.pageMetaDataProps ? (
<PageMetaData {...props.pageMetaDataProps} />
) : null}
<Box css={{ bg: '$grayBase', height: '100vh', width: '100vw' }}>
<Box css={{
height: '100vh',
width: '100vw',
bg: 'transparent',
'@smDown': {
bg: '$grayBase',
}
}}>
<PrimaryHeader
user={viewerData?.me}
hideHeader={props.hideHeader}
userInitials={viewerData?.me?.name.charAt(0) ?? ''}
profileImageURL={viewerData?.me?.profile.pictureUrl}
isFixedPosition={true}
toolbarControl={props.headerToolbarControl}
scrollElementRef={props.scrollElementRef}
displayFontStepper={props.displayFontStepper}
setShowLogoutConfirmation={setShowLogoutConfirmation}
setShowKeyboardCommandsModal={setShowKeyboardCommandsModal}
/>
<Box
ref={props.scrollElementRef}
css={{
top: '68px',
'@smDown': { top: '48px' },
position: 'fixed',
overflowY: 'auto',
height: '100%',
@ -86,6 +90,13 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
bg: '$grayBase',
}}
>
<Box
ref={props.scrollElementRef}
css={{
height: '48px',
bg: '$grayBase',
}}
></Box>
{props.children}
{showLogoutConfirmation ? (
<ConfirmationModal

View File

@ -54,8 +54,7 @@ export function SettingsLayout(props: SettingsLayoutProps): JSX.Element {
/>
<Box
css={{
top: '68px',
'@smDown': { top: '48px' },
top: '48px',
position: 'fixed',
overflowY: 'auto',
height: '100%',

View File

@ -0,0 +1,154 @@
import { Separator } from "@radix-ui/react-separator"
import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa } from "phosphor-react"
import { ArticleAttributes } from "../../../lib/networking/queries/useGetArticleQuery"
import { useGetUserPreferences } from "../../../lib/networking/queries/useGetUserPreferences"
import { Button } from "../../elements/Button"
import { Dropdown } from "../../elements/DropdownElements"
import { Box, SpanBox } from "../../elements/LayoutPrimitives"
import { TooltipWrapped } from "../../elements/Tooltip"
import { styled, theme } from "../../tokens/stitches.config"
import { SetLabelsControl } from "./SetLabelsControl"
import { ReaderSettingsControl } from "./ReaderSettingsControl"
import { usePersistedState } from "../../../lib/hooks/usePersistedState"
export type ArticleActionsMenuLayout = 'horizontal' | 'vertical'
type ArticleActionsMenuProps = {
article: ArticleAttributes
layout: ArticleActionsMenuLayout
lineHeight: number
marginWidth: number
articleActionHandler: (action: string, arg?: unknown) => void
}
type MenuSeparatorProps = {
layout: ArticleActionsMenuLayout
}
const MenuSeparator = (props: MenuSeparatorProps): JSX.Element => {
const LineSeparator = styled(Separator, {
width: '100%',
margin: 0,
borderBottom: `1px solid ${theme.colors.grayLine.toString()}`,
my: '8px',
})
return (props.layout == 'vertical' ? <LineSeparator /> : <></>)
}
type ActionDropdownProps = {
layout: ArticleActionsMenuLayout
triggerElement: JSX.Element
children: JSX.Element
}
const ActionDropdown = (props: ActionDropdownProps): JSX.Element => {
return <Dropdown
showArrow={true}
css={{ m: '0px', p: '0px', overflow: 'hidden', width: '265px', maxWidth: '265px', '@smDown': { width: '230px' } }}
side={props.layout == 'vertical' ? 'right' : 'bottom'}
sideOffset={props.layout == 'vertical' ? 8 : 0}
align={props.layout == 'vertical' ? 'start' : 'center'}
alignOffset={props.layout == 'vertical' ? -18 : undefined}
triggerElement={props.triggerElement}
>
{props.children}
</Dropdown>
}
export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element {
return (
<>
<Box
css={{
display: 'flex',
alignItems: 'center',
flexDirection: props.layout == 'vertical' ? 'column' : 'row',
justifyContent: props.layout == 'vertical' ? 'center' : 'flex-end',
gap: props.layout == 'vertical' ? '8px' : '24px',
paddingTop: '6px',
}}
>
<ActionDropdown
layout={props.layout}
triggerElement={
<TooltipWrapped
tooltipContent="Adjust Display Settings"
tooltipSide={props.layout == 'vertical' ? 'right' : 'bottom'}
>
<TextAa size={24} color={theme.colors.readerFont.toString()} />
</TooltipWrapped>
}
>
<ReaderSettingsControl
lineHeight={props.lineHeight}
marginWidth={props.marginWidth}
articleActionHandler={props.articleActionHandler}
/>
</ActionDropdown>
<MenuSeparator layout={props.layout} />
<SpanBox css={{
'display': 'flex',
'@smDown': {
display: 'none',
}}}
>
<ActionDropdown
layout={props.layout}
triggerElement={
<TooltipWrapped
tooltipContent="Edit labels"
tooltipSide={props.layout == 'vertical' ? 'right' : 'bottom'}
>
<TagSimple size={24} color={theme.colors.readerFont.toString()} />
</TooltipWrapped>
}
>
<SetLabelsControl
article={props.article}
articleActionHandler={props.articleActionHandler}
/>
</ActionDropdown>
</SpanBox>
<Button style='articleActionIcon'
onClick={() => props.articleActionHandler('setLabels')}
css={{
'display': 'none',
'@smDown': {
display: 'flex',
},
}}>
<TagSimple size={24} color={theme.colors.readerFont.toString()} />
</Button>
<Button style='articleActionIcon' onClick={() => props.articleActionHandler('showHighlights')}>
<TooltipWrapped
tooltipContent="View Highlights"
tooltipSide={props.layout == 'vertical' ? 'right' : 'bottom'}
>
<HighlighterCircle size={24} color={theme.colors.readerFont.toString()} />
</TooltipWrapped>
</Button>
<MenuSeparator layout={props.layout} />
<Button style='articleActionIcon' onClick={() => props.articleActionHandler('archive')}>
<TooltipWrapped
tooltipContent="Archive"
tooltipSide={props.layout == 'vertical' ? 'right' : 'bottom'}
>
<ArchiveBox size={24} color={theme.colors.readerFont.toString()} />
</TooltipWrapped>
</Button>
{/* <MenuSeparator layout={props.layout} />
<Button style='articleActionIcon'>
<DotsThree size={24} color={theme.colors.readerFont.toString()} />
</Button> */}
</Box>
</>
)
}

View File

@ -1,6 +1,6 @@
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
import { Article } from './../../../components/templates/article/Article'
import { Box, VStack } from './../../elements/LayoutPrimitives'
import { Box, SpanBox, VStack } from './../../elements/LayoutPrimitives'
import { StyledText } from './../../elements/StyledText'
import { ArticleSubtitle } from './../../patterns/ArticleSubtitle'
import { theme, ThemeId } from './../../tokens/stitches.config'
@ -13,9 +13,12 @@ import { ArticleHeaderToolbar } from './ArticleHeaderToolbar'
import { userPersonalizationMutation } from '../../../lib/networking/mutations/userPersonalizationMutation'
import { updateThemeLocally } from '../../../lib/themeUpdater'
import { ArticleMutations } from '../../../lib/articleActions'
import { LabelChip } from '../../elements/LabelChip'
import { Label } from '../../../lib/networking/fragments/labelFragment'
type ArticleContainerProps = {
article: ArticleAttributes
labels: Label[]
articleMutations: ArticleMutations
scrollElementRef: MutableRefObject<HTMLDivElement | null>
isAppleAppEmbed: boolean
@ -24,21 +27,22 @@ type ArticleContainerProps = {
margin?: number
fontSize?: number
fontFamily?: string
lineHeight?: number
showHighlightsModal: boolean
setShowHighlightsModal: React.Dispatch<React.SetStateAction<boolean>>
}
export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
const [showShareModal, setShowShareModal] = useState(false)
const [showLabelsModal, setShowLabelsModal] = useState(false)
const [showNotesSidebar, setShowNotesSidebar] = useState(false)
const [showReportIssuesModal, setShowReportIssuesModal] = useState(false)
const [showHighlightsModal, setShowHighlightsModal] = useState(props.showHighlightsModal)
const [fontSize, setFontSize] = useState(props.fontSize ?? 20)
const [labels, setLabels] = useState(
props.article.labels?.map((l) => l.id) || []
)
const updateFontSize = async (newFontSize: number) => {
setFontSize(newFontSize)
await userPersonalizationMutation({ fontSize: newFontSize })
if (fontSize !== newFontSize) {
setFontSize(newFontSize)
await userPersonalizationMutation({ fontSize: newFontSize })
}
}
useEffect(() => {
@ -85,8 +89,9 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
}, [props.article])
const styles = {
margin: props.margin ?? 140,
fontSize,
margin: props.margin ?? 360,
lineHeight: props.lineHeight ?? 150,
fontFamily: props.fontFamily ?? 'inter',
readerFontColor: theme.colors.readerFont.toString(),
readerFontColorTransparent: theme.colors.readerFontTransparent.toString(),
@ -100,10 +105,11 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
id="article-container"
css={{
padding: '16px',
maxWidth: '94%',
maxWidth: '100%',
background: props.isAppleAppEmbed ? 'unset' : theme.colors.grayBg.toString(),
'--text-font-family': styles.fontFamily,
'--text-font-size': `${styles.fontSize}px`,
'--line-height': `150%`,
'--line-height': `${styles.lineHeight}%`,
'--blockquote-padding': '0.5em 1em',
'--blockquote-icon-font-size': '1.3rem',
'--figure-margin': '1.6rem auto',
@ -117,15 +123,15 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
'--blockquote-icon-font-size': '1.7rem',
'--figure-margin': '2.6875rem auto',
'--hr-margin': '2em',
margin: `30px ${styles.margin / 2}px`,
margin: `30px 0px`,
},
'@md': {
maxWidth: '92%',
maxWidth: 1024 - (styles.margin),
},
'@lg': {
margin: `30px 0`,
width: 'auto',
maxWidth: 1024 - styles.margin,
maxWidth: 1024 - (styles.margin),
},
}}
>
@ -143,13 +149,22 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
author={props.article.author}
href={props.article.url}
/>
<ArticleHeaderToolbar
articleTitle={props.article.title}
articleShareURL={props.highlightsBaseURL}
setShowNotesSidebar={setShowNotesSidebar}
setShowShareArticleModal={setShowShareModal}
hasHighlights={props.article.highlights?.length > 0}
/>
{props.labels ? (
<SpanBox css={{ pb: '16px', width: '100%', '&:empty': { display: 'none' } }}>
{props.labels?.map((label) =>
<LabelChip key={label.id} text={label.name} color={label.color} />
)}
</SpanBox>
) : null}
{props.isAppleAppEmbed && (
<ArticleHeaderToolbar
articleTitle={props.article.title}
articleShareURL={props.highlightsBaseURL}
setShowShareArticleModal={setShowShareModal}
setShowHighlightsModal={props.setShowHighlightsModal}
hasHighlights={props.article.highlights?.length > 0}
/>
)}
</VStack>
<Article
articleId={props.article.id}
@ -181,10 +196,10 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
articleAuthor={props.article.author ?? ''}
articleId={props.article.id}
isAppleAppEmbed={props.isAppleAppEmbed}
highlightBarDisabled={props.highlightBarDisabled}
showNotesSidebar={showNotesSidebar}
highlightsBaseURL={props.highlightsBaseURL}
setShowNotesSidebar={setShowNotesSidebar}
highlightBarDisabled={props.highlightBarDisabled}
showHighlightsModal={props.showHighlightsModal}
setShowHighlightsModal={props.setShowHighlightsModal}
articleMutations={props.articleMutations}
/>
{showReportIssuesModal ? (

View File

@ -14,7 +14,7 @@ type ArticleHeaderToolbarProps = {
articleTitle: string
articleShareURL: string
hasHighlights: boolean
setShowNotesSidebar: (showNotesSidebar: boolean) => void
setShowHighlightsModal: React.Dispatch<React.SetStateAction<boolean>>
setShowShareArticleModal: (showShareModal: boolean) => void
}
@ -46,7 +46,12 @@ export function ArticleHeaderToolbar(
return (
<HStack distribution="between" alignment="center" css={{ gap: '$2' }}>
{props.hasHighlights && (
<Button style="plainIcon" onClick={() => props.setShowNotesSidebar(true)} title="View all your highlights and notes">
<Button style="plainIcon" onClick={() => {
if (props.setShowHighlightsModal) {
props.setShowHighlightsModal(true)
}
}}
title="View all your highlights and notes">
<CommentIcon
size={24}
strokeColor={theme.colors.grayTextContrast.toString()}

View File

@ -0,0 +1,63 @@
import { X } from 'phosphor-react'
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
import { UserPreferences } from '../../../lib/networking/queries/useGetUserPreferences'
import { Button } from '../../elements/Button'
import { CrossIcon } from '../../elements/images/CrossIcon'
import { Box, HStack, VStack } from '../../elements/LayoutPrimitives'
import {
ModalRoot,
ModalOverlay,
ModalContent,
} from '../../elements/ModalPrimitives'
import { StyledText } from '../../elements/StyledText'
import { theme } from '../../tokens/stitches.config'
import { ReaderSettingsControl } from './ReaderSettingsControl'
type DisplaySettingsModalProps = {
onOpenChange: (open: boolean) => void
lineHeight: number
marginWidth: number
articleActionHandler: (action: string, arg?: number) => void
}
export function DisplaySettingsModal(props: DisplaySettingsModalProps): JSX.Element {
return (
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
<ModalOverlay />
<ModalContent
css={{ overflow: 'auto' }}
onPointerDownOutside={(event) => {
event.preventDefault()
props.onOpenChange(false)
}}
>
<VStack css={{ width: '100%' }}>
<HStack
distribution="between"
alignment="center"
css={{ width: '100%' }}
>
<StyledText style="modalHeadline" css={{ pl: '16px' }}>Labels</StyledText>
<Button
css={{ pt: '16px', pr: '16px' }}
style="ghost"
onClick={() => {
props.onOpenChange(false)
}}
>
<CrossIcon
size={14}
strokeColor={theme.colors.grayText.toString()}
/>
</Button>
</HStack>
<ReaderSettingsControl
lineHeight={props.lineHeight}
marginWidth={props.marginWidth}
articleActionHandler={props.articleActionHandler}
/>
</VStack>
</ModalContent>
</ModalRoot>
)
}

View File

@ -1,111 +0,0 @@
import {
ModalContent,
ModalOverlay,
ModalRoot,
} from '../../elements/ModalPrimitives'
import { HStack, VStack } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { StyledText } from '../../elements/StyledText'
import { CrossIcon } from '../../elements/images/CrossIcon'
import { theme } from '../../tokens/stitches.config'
import { Label, useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
import { ChangeEvent, useCallback, useState } from 'react'
import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation'
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
import { LabelChip } from '../../elements/LabelChip'
type EditLabelsModalProps = {
labels: Label[]
article: ArticleAttributes
onOpenChange: (open: boolean) => void
setLabels: (labels: Label[]) => void
}
export function EditLabelsModal(props: EditLabelsModalProps): JSX.Element {
const [selectedLabels, setSelectedLabels] = useState(props.labels)
const { labels } = useGetLabelsQuery()
const saveAndExit = useCallback(async () => {
const result = await setLabelsMutation(props.article.id, selectedLabels.map((l) => l.id))
console.log('result of setting labels', result)
props.onOpenChange(false)
props.setLabels(selectedLabels)
}, [props, selectedLabels])
const handleChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
// const label = event.target.value
// if (event.target.checked) {
// setSelectedLabels([...selectedLabels, label])
// } else {
// setSelectedLabels(selectedLabels.filter((l) => l !== label))
// }
},
[selectedLabels]
)
return (
<ModalRoot defaultOpen onOpenChange={saveAndExit}>
<ModalOverlay />
<ModalContent
onPointerDownOutside={(event) => {
event.preventDefault()
}}
css={{ overflow: 'auto', p: '0' }}
>
<VStack distribution="start" css={{ p: '0' }}>
<HStack
distribution="between"
alignment="center"
css={{ width: '100%' }}
>
<StyledText style="modalHeadline" css={{ p: '16px' }}>
Edit Labels
</StyledText>
<Button
css={{ pt: '16px', pr: '16px' }}
style="ghost"
onClick={() => {
props.onOpenChange(false)
}}
>
<CrossIcon
size={20}
strokeColor={theme.colors.grayText.toString()}
/>
</Button>
</HStack>
{labels &&
labels.map((label) => (
<HStack
key={label.id}
css={{ height: '50px', verticalAlign: 'middle' }}
onClick={() => {
// if (selectedLabels.includes(label.id)) {
// setSelectedLabels(
// selectedLabels.filter((id) => id !== label.id)
// )
// } else {
// setSelectedLabels([...selectedLabels, label.id])
// }
}}
>
<LabelChip color={label.color} text={label.name} />
<input
type="checkbox"
value={label.id}
onChange={handleChange}
checked={selectedLabels.includes(label)}
/>
</HStack>
))}
<HStack css={{ width: '100%', mb: '16px' }} alignment="center">
<Button style="ctaDarkYellow" onClick={saveAndExit}>
Save
</Button>
</HStack>
</VStack>
</ModalContent>
</ModalRoot>
)
}

View File

@ -25,9 +25,9 @@ type HighlightsLayerProps = {
articleAuthor: string
isAppleAppEmbed: boolean
highlightBarDisabled: boolean
showNotesSidebar: boolean
showHighlightsModal: boolean
highlightsBaseURL: string
setShowNotesSidebar: React.Dispatch<React.SetStateAction<boolean>>
setShowHighlightsModal: React.Dispatch<React.SetStateAction<boolean>>
articleMutations: ArticleMutations
}
@ -466,11 +466,11 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
)
}
if (props.showNotesSidebar) {
if (props.showHighlightsModal) {
return (
<HighlightsModal
highlights={highlights}
onOpenChange={() => props.setShowNotesSidebar(false)}
onOpenChange={() => props.setShowHighlightsModal(false)}
deleteHighlightAction={(highlightId: string) => {
removeHighlightCallback(highlightId)
}}

View File

@ -30,6 +30,7 @@ export function HighlightsModal(props: HighlightsModalProps): JSX.Element {
<ModalContent
onPointerDownOutside={(event) => {
event.preventDefault()
props.onOpenChange(false)
}}
css={{ overflow: 'auto', p: '0' }}
>

View File

@ -0,0 +1,130 @@
import { HStack, VStack, SpanBox } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { StyledText } from '../../elements/StyledText'
import { styled, theme } from '../../tokens/stitches.config'
import { useEffect, useState } from 'react'
import { AlignCenterHorizontalSimple, ArrowsInLineHorizontal, ArrowsOutLineHorizontal, Minus, Pen, Plus, Trash, X } from 'phosphor-react'
import { AIcon } from '../../elements/images/AIcon'
import { TickedRangeSlider } from '../../elements/TickedRangeSlider'
import { showSuccessToast } from '../../../lib/toastHelpers'
type ReaderSettingsProps = {
marginWidth: number
lineHeight: number
articleActionHandler: (action: string, arg?: number) => void
}
const VerticalDivider = styled(SpanBox, {
width: '1px',
height: '100%',
background: `${theme.colors.grayLine.toString()}`,
})
export function ReaderSettingsControl(props: ReaderSettingsProps): JSX.Element {
const [lineHeight, setLineHeight] = useState(props.lineHeight)
const [marginWidth, setMarginWidth] = useState(props.marginWidth)
useEffect(() => {
setLineHeight(props.lineHeight)
setMarginWidth(props.marginWidth)
}, [props.lineHeight, props.marginWidth, setLineHeight, setMarginWidth])
return (
<VStack>
<HStack
alignment='center'
css={{
width: '100%',
height: '70px',
borderBottom: `1px solid ${theme.colors.grayLine.toString()}`,
}}
>
<Button style='plainIcon' onClick={() => props.articleActionHandler('decrementFontSize')}>
<AIcon size={28} color={theme.colors.readerFont.toString()} />
<Minus size={28} color={theme.colors.readerFont.toString()}/>
</Button>
<VerticalDivider />
<Button style='plainIcon' onClick={() => props.articleActionHandler('incrementFontSize')}>
<AIcon size={44} color={theme.colors.readerFont.toString()} />
<Plus size={28} color={theme.colors.readerFont.toString()} />
</Button>
</HStack>
<VStack
css={{
p: '0px',
m: '0px',
pb: '14px',
width: '100%',
height: '100%',
'@mdDown': {
display: 'none',
},
}}
>
<StyledText color={theme.colors.readerFontTransparent.toString()} css={{ pl: '8px', m: '0px', pt: '14px' }}>Margin:</StyledText>
<HStack distribution='between' css={{ gap: '16px', alignItems: 'center', alignSelf: 'center' }}>
<Button style='plainIcon' css={{ pt: '10px', px: '4px' }} onClick={() => {
const newMarginWith = Math.max(marginWidth - 45, 200)
setMarginWidth(newMarginWith)
props.articleActionHandler('setMarginWidth', newMarginWith)
}}>
<ArrowsOutLineHorizontal size={24} color={theme.colors.readerFont.toString()} />
</Button>
<TickedRangeSlider min={200} max={560} step={45} value={marginWidth} onChange={(value) => {
setMarginWidth(value)
props.articleActionHandler('setMarginWidth', value)
}} />
<Button style='plainIcon' css={{ pt: '10px', px: '4px' }} onClick={() => {
const newMarginWith = Math.min(marginWidth + 45, 560)
setMarginWidth(newMarginWith)
props.articleActionHandler('setMarginWidth', newMarginWith)
}}>
<ArrowsInLineHorizontal size={24} color={theme.colors.readerFont.toString()} />
</Button>
</HStack>
</VStack>
<VStack css={{
p: '0px',
m: '0px',
pb: '12px',
width: '100%',
height: '100%',
}}>
<StyledText color={theme.colors.readerFontTransparent.toString()} css={{ pl: '12px', m: '0px', pt: '14px' }}>Line Spacing:</StyledText>
<HStack distribution='between' css={{ gap: '16px', alignItems: 'center', alignSelf: 'center' }}>
<Button style='plainIcon' css={{ pt: '10px', px: '4px' }} onClick={() => {
const newLineHeight = Math.max(lineHeight - 25, 100)
setLineHeight(newLineHeight)
props.articleActionHandler('setLineHeight', newLineHeight)
}}>
<AlignCenterHorizontalSimple size={25} color={theme.colors.readerFont.toString()} />
</Button>
<TickedRangeSlider min={100} max={300} step={25} value={lineHeight} onChange={(value) => {
setLineHeight(value)
props.articleActionHandler('setLineHeight', value)
}} />
<Button style='plainIcon' css={{ pt: '10px', px: '4px' }} onClick={() => {
const newLineHeight = Math.min(lineHeight + 25, 300)
setLineHeight(newLineHeight)
props.articleActionHandler('setLineHeight', newLineHeight)
}}>
<AlignCenterHorizontalSimple size={25} color={theme.colors.readerFont.toString()} />
</Button>
</HStack>
<Button style='plainIcon' css={{ justifyContent: 'center', textDecoration: 'underline', display: 'flex', gap: '4px', width: '100%', fontSize: '12px', p: '8px', pb: '0px', pt: '16px', height: '42px', alignItems: 'center' }}
onClick={() => {
setMarginWidth(290)
setLineHeight(150)
props.articleActionHandler('resetReaderSettings')
showSuccessToast('Display settings reset', { position: 'bottom-right' })
}}
>
Reset to default
</Button>
</VStack>
</VStack>
)
}

View File

@ -0,0 +1,345 @@
import { useCallback, useRef, useState, useMemo, useEffect } from 'react'
import Link from 'next/link'
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { StyledText } from '../../elements/StyledText'
import { CrossIcon } from '../../elements/images/CrossIcon'
import { styled, theme } from '../../tokens/stitches.config'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
import { Check, Circle, PencilSimple, Plus } from 'phosphor-react'
import { isTouchScreenDevice } from '../../../lib/deviceType'
import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation'
import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
import { useRouter } from 'next/router'
type SetLabelsControlProps = {
article: ArticleAttributes
articleActionHandler: (action: string, arg?: unknown) => void
}
type HeaderProps = {
filterText: string
focused: boolean
resetFocusedIndex: () => void
setFilterText: (text: string) => void
}
const FormInput = styled('input', {
width: '100%',
fontSize: '16px',
fontFamily: 'inter',
fontWeight: 'normal',
lineHeight: '1.8',
color: '$grayTextContrast',
'&:focus': {
outline: 'none',
},
})
const StyledLabel = styled('label', {
display: 'flex',
justifyContent: 'flex-start',
})
function Header(props: HeaderProps): JSX.Element {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (!isTouchScreenDevice() && props.focused && inputRef.current) {
inputRef.current.focus()
}
}, [props.focused])
return (
<VStack css={{ width: '100%', my: '0px', borderBottom: '1px solid $grayBorder'}}>
<Box css={{
width: '100%',
my: '14px',
px: '14px',
}}>
<FormInput
ref={inputRef}
type="text"
tabIndex={props.focused && !isTouchScreenDevice() ? 0 : -1}
autoFocus={!isTouchScreenDevice()}
value={props.filterText}
placeholder="Filter for label"
onChange={(event) => {
props.setFilterText(event.target.value)
}}
onFocus={() => {
props.resetFocusedIndex()
}}
css={{
border: '1px solid $grayBorder',
borderRadius: '8px',
width: '100%',
bg: 'transparent',
fontSize: '16px',
textIndent: '8px',
marginBottom: '2px',
color: '$grayTextContrast',
'&:focus': {
outline: 'none',
boxShadow: '0px 0px 2px 2px rgba(255, 234, 159, 0.56)',
},
}}
/>
</Box>
</VStack>)
}
type LabelListItemProps = {
label: Label
focused: boolean
selected: boolean
toggleLabel: (label: Label) => void
}
function LabelListItem(props: LabelListItemProps): JSX.Element {
const ref = useRef<HTMLLabelElement>(null)
const { label, focused, selected } = props
useEffect(() => {
if (props.focused && ref.current) {
ref.current.focus()
}
}, [props.focused])
return (
<StyledLabel
ref={ref}
css={{
width: '100%',
height: '42px',
borderBottom: '1px solid $grayBorder',
bg: props.focused ? '$grayBgActive' : 'unset',
}}
tabIndex={props.focused ? 0 : -1}
onClick={(event) => {
event.preventDefault()
props.toggleLabel(label)
ref.current?.blur()
}}
>
<input autoFocus={focused} hidden={true} type="checkbox" checked={selected} readOnly />
<Box css={{ pl: '10px', width: '32px', display: 'flex', alignItems: 'center' }}>
{selected && <Check size={15} color={theme.colors.grayText.toString()} weight='bold' />}
</Box>
<Box css={{ width: '30px', height: '100%', display: 'flex', alignItems: 'center' }}>
<Circle width={22} height={22} color={label.color} weight='fill' />
</Box>
<Box css={{ overflow: 'clip', height: '100%', display: 'flex', alignItems: 'center' }}>
<StyledText style="caption">{label.name}</StyledText>
</Box>
<Box css={{ pl: '10px', width: '40px', marginLeft: 'auto', display: 'flex', alignItems: 'center' }}>
{selected && <CrossIcon
size={14}
strokeColor={theme.colors.grayText.toString()}
/>}
</Box>
</StyledLabel>
)
}
type FooterProps = {
focused: boolean
}
function Footer(props: FooterProps): JSX.Element {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (props.focused && ref.current) {
ref.current.focus()
}
}, [props.focused])
return (
<HStack
ref={ref}
distribution="start" alignment="center"
css={{
width: '100%', height: '42px',
bg: props.focused ? '$grayBgActive' : 'unset',
color: theme.colors.grayText.toString(),
'a:link': {
textDecoration: 'none',
},
'a:visited': {
color: theme.colors.grayText.toString(),
},
}}
>
<SpanBox css={{ display: 'flex', fontSize: '12px', padding: '33px', gap: '8px' }}>
<PencilSimple size={18} color={theme.colors.grayText.toString()} />
<Link href="/settings/labels">Edit labels</Link>
</SpanBox>
</HStack>
)
}
export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
const router = useRouter()
const [filterText, setFilterText] = useState('')
const { labels, revalidate } = useGetLabelsQuery()
const [selectedLabels, setSelectedLabels] = useState<Label[]>(props.article.labels || [])
useEffect(() => {
setFocusedIndex(undefined)
}, [filterText])
const isSelected = useCallback((label: Label): boolean => {
return selectedLabels.some((other) => {
return other.id === label.id
})
}, [selectedLabels])
const toggleLabel = useCallback(async (label: Label) => {
let newSelectedLabels = [...selectedLabels]
if (isSelected(label)) {
newSelectedLabels = selectedLabels.filter((other) => {
return other.id !== label.id
})
} else {
newSelectedLabels = [...selectedLabels, label]
}
setSelectedLabels(newSelectedLabels)
const result = await setLabelsMutation(
props.article.linkId,
newSelectedLabels.map((label) => label.id)
)
props.article.labels = result
props.articleActionHandler('refreshLabels', result)
revalidate()
}, [isSelected, selectedLabels, setSelectedLabels])
const filteredLabels = useMemo(() => {
if (!labels) {
return []
}
return labels.filter((label) => {
return label.name.toLowerCase().includes(filterText.toLowerCase())
})
}, [labels, filterText])
// Move focus through the labels list on tab or arrow up/down keys
const [focusedIndex, setFocusedIndex] = useState<number | undefined>(undefined)
const handleKeyDown = useCallback(async (event: React.KeyboardEvent<HTMLInputElement>) => {
const maxIndex = filteredLabels.length + 1
if (event.key === 'ArrowUp') {
event.preventDefault()
let newIndex = focusedIndex
if (focusedIndex) {
newIndex = Math.max(0, focusedIndex - 1)
} else {
newIndex = undefined
}
// If the `Create New label` button isn't visible we skip it
// when navigating with the arrow keys
if (focusedIndex === maxIndex && !filterText) {
newIndex = maxIndex - 2
}
setFocusedIndex(newIndex)
}
if (event.key === 'ArrowDown' || event.key === 'Tab') {
event.preventDefault()
let newIndex = focusedIndex
if (focusedIndex === undefined) {
newIndex = 0
} else {
newIndex = Math.min(maxIndex, focusedIndex + 1)
}
// If the `Create New label` button isn't visible we skip it
// when navigating with the arrow keys
if (focusedIndex === maxIndex - 2 && !filterText) {
newIndex = maxIndex
}
setFocusedIndex(newIndex)
}
if (event.key === 'Enter') {
event.preventDefault()
if (focusedIndex === maxIndex) {
router.push('/settings/labels')
return
}
if (focusedIndex === maxIndex - 1) {
await createLabelFromFilterText()
return
}
if (focusedIndex !== undefined) {
const label = filteredLabels[focusedIndex]
if (label) {
toggleLabel(label)
}
}
}
}, [filterText, filteredLabels, focusedIndex, isSelected, selectedLabels, setSelectedLabels])
const createLabelFromFilterText = useCallback(async () => {
const label = await createLabelMutation(filterText, randomLabelColorHex(), '')
if (label) {
showSuccessToast(`Created label ${label.name}`, { position: 'bottom-right' })
toggleLabel(label)
} else {
showErrorToast('Failed to create label', { position: 'bottom-right' })
}
}, [filterText, selectedLabels, setSelectedLabels, toggleLabel])
return (
<VStack
distribution="start"
onKeyDown={handleKeyDown}
css={{
p: '0',
width: '100%',
}}>
<Header
focused={focusedIndex === undefined}
resetFocusedIndex={() => setFocusedIndex(undefined)}
setFilterText={setFilterText} filterText={filterText}
/>
<VStack
distribution="start"
alignment="start"
css={{ flexGrow: '1', overflow: 'scroll', width: '100%', height: '100%', maxHeight: '294px'
}}>
{filteredLabels &&
filteredLabels.map((label, idx) => (
<LabelListItem
key={label.id}
label={label}
focused={idx === focusedIndex}
selected={isSelected(label)}
toggleLabel={toggleLabel}
/>
))}
</VStack>
{filterText && (
<Button style='modalOption' css={{
pl: '26px',
color: theme.colors.grayText.toString(),
height: '42px',
borderBottom: '1px solid $grayBorder',
bg: focusedIndex === filteredLabels.length ? '$grayBgActive' : 'unset',
}}
onClick={createLabelFromFilterText}
>
<HStack alignment='center' distribution='start' css={{ gap: '8px' }}>
<Plus size={18} color={theme.colors.grayText.toString()} />
<SpanBox css={{ fontSize: '12px' }}>{`Create new label "${filterText}"`}</SpanBox>
</HStack>
</Button>
)}
<Footer focused={focusedIndex === (filteredLabels.length + 1)} />
</VStack>
)
}

View File

@ -0,0 +1,56 @@
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
import { Button } from '../../elements/Button'
import { CrossIcon } from '../../elements/images/CrossIcon'
import { HStack, VStack } from '../../elements/LayoutPrimitives'
import {
ModalRoot,
ModalOverlay,
ModalContent,
} from '../../elements/ModalPrimitives'
import { StyledText } from '../../elements/StyledText'
import { theme } from '../../tokens/stitches.config'
import { SetLabelsControl } from './SetLabelsControl'
type SetLabelsModalProps = {
article: ArticleAttributes
onOpenChange: (open: boolean) => void
articleActionHandler: (action: string, arg?: unknown) => void
}
export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
return (
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
<ModalOverlay />
<ModalContent
css={{ border: '1px solid $grayBorder' }}
onPointerDownOutside={(event) => {
event.preventDefault()
props.onOpenChange(false)
}}
>
<VStack css={{ width: '100%' }}>
<HStack
distribution="between"
alignment="center"
css={{ width: '100%' }}
>
<StyledText style="modalHeadline" css={{ pl: '16px' }}>Labels</StyledText>
<Button
css={{ pt: '16px', pr: '16px' }}
style="ghost"
onClick={() => {
props.onOpenChange(false)
}}
>
<CrossIcon
size={14}
strokeColor={theme.colors.grayText.toString()}
/>
</Button>
</HStack>
<SetLabelsControl {...props} />
</VStack>
</ModalContent>
</ModalRoot>
)
}

View File

@ -102,8 +102,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
borderStyles: {},
shadows: {
panelShadow: '0px 4px 18px rgba(120, 123, 134, 0.12)',
cardBoxShadow:
'0px 0px 9px -2px rgba(32, 31, 29, 0.09), 0px 7px 12px rgba(32, 31, 29, 0.07)',
cardBoxShadow: '0px 0px 4px 0px rgba(0, 0, 0, 0.1)',
},
zIndices: {},
transitions: {},
@ -156,6 +155,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
xsmDown: '(max-width: 375px)',
smDown: '(max-width: 575px)',
mdDown: '(max-width: 768px)',
lgDown: '(max-width: 992px)',
sm: '(min-width: 576px)',
md: '(min-width: 768px)',
lg: '(min-width: 992px)',
@ -199,7 +199,7 @@ const darkThemeSpec = {
},
shadows: {
cardBoxShadow:
'0px 0px 9px -2px rgba(32, 31, 29, 0.09), 0px 7px 12px rgba(32, 31, 29, 0.07)',
'0px 0px 9px -2px rgba(255, 255, 255, 0.09), 0px 7px 12px rgba(255, 255, 255, 0.07)',
},
}

View File

@ -207,7 +207,9 @@ type ArticleKeyboardAction =
| 'openOriginalArticle'
| 'incrementFontSize'
| 'decrementFontSize'
| 'editLabels'
| 'incrementMarginWidth'
| 'decrementMarginWidth'
| 'setLabels'
export function articleKeyboardCommands(
router: NextRouter | undefined,
@ -238,11 +240,23 @@ export function articleKeyboardCommands(
shortcutKeyDescription: '-',
callback: () => actionHandler('decrementFontSize'),
},
{
shortcutKeys: [']'],
actionDescription: 'Increase margin width',
shortcutKeyDescription: ']',
callback: () => actionHandler('incrementMarginWidth'),
},
{
shortcutKeys: ['['],
actionDescription: 'Decrease margin width',
shortcutKeyDescription: '[',
callback: () => actionHandler('decrementMarginWidth'),
},
{
shortcutKeys: ['l'],
actionDescription: 'Edit labels',
shortcutKeyDescription: 'l',
callback: () => actionHandler('editLabels'),
callback: () => actionHandler('setLabels'),
},
]
}

View File

@ -1,5 +1,14 @@
import { gql } from 'graphql-request'
export type LabelColor =
| '#FF5D99'
| '#7CFF7B'
| '#FFD234'
| '#7BE4FF'
| '#CE88EF'
| '#EF8C43'
| 'custom color';
export const labelFragment = gql`
fragment LabelFields on Label {
id
@ -13,7 +22,7 @@ export const labelFragment = gql`
export type Label = {
id: string
name: string
color: string
color: LabelColor
description?: string
createdAt: string
}
createdAt: Date
}

View File

@ -1,6 +1,16 @@
import { gql } from 'graphql-request'
import { Label } from '../fragments/labelFragment'
import { gqlFetcher } from '../networkHelpers'
type CreateLabelResult = {
createLabel: CreateLabel
errorCodes?: unknown[]
}
type CreateLabel = {
label: Label
}
export async function createLabelMutation(
name: string,
color: string,
@ -32,9 +42,9 @@ export async function createLabelMutation(
`
try {
const data = await gqlFetcher(mutation)
const data = await gqlFetcher(mutation) as CreateLabelResult
console.log('created label', data)
return data
return data.errorCodes ? undefined : data.createLabel.label
} catch (error) {
console.log('createLabelMutation error', error)
return undefined

View File

@ -1,16 +1,26 @@
import { gql } from 'graphql-request'
import { Label, labelFragment } from '../fragments/labelFragment'
import { gqlFetcher } from '../networkHelpers'
type SetLabelsResult = {
setLabels: SetLabels
errorCodes?: unknown[]
}
type SetLabels = {
labels: Label[]
}
export async function setLabelsMutation(
pageId: string,
labelIds: string[]
): Promise<any | undefined> {
): Promise<Label[] | undefined> {
const mutation = gql`
mutation SetLabels($input: SetLabelsInput!) {
setLabels(input: $input) {
... on SetLabelsSuccess {
labels {
id
...LabelFields
}
}
... on SetLabelsError {
@ -18,12 +28,12 @@ export async function setLabelsMutation(
}
}
}
${labelFragment}
`
try {
const data = await gqlFetcher(mutation, { input: { pageId, labelIds } })
console.log(data)
return data
const data = await gqlFetcher(mutation, { input: { pageId, labelIds } }) as SetLabelsResult
return data.errorCodes ? undefined : data.setLabels.labels
} catch (error) {
console.log('SetLabelsOutput error', error)
return undefined

View File

@ -1,10 +1,11 @@
import { gql } from 'graphql-request'
import useSWRImmutable from 'swr'
import useSWRImmutable, { Cache } from 'swr'
import { makeGqlFetcher, RequestContext, ssrFetcher } from '../networkHelpers'
import { articleFragment, ContentReader } from '../fragments/articleFragment'
import { Highlight, highlightFragment } from '../fragments/highlightFragment'
import { ScopedMutator } from 'swr/dist/types'
import { Label, labelFragment } from '../fragments/labelFragment'
import { LibraryItems } from './useGetLibraryItemsQuery'
type ArticleQueryInput = {
username?: string
@ -77,16 +78,7 @@ const query = gql`
${highlightFragment}
${labelFragment}
`
export const cacheArticle = (
mutate: ScopedMutator,
username: string,
article: ArticleAttributes,
includeFriendsHighlights = false
) => {
mutate([query, username, article.slug, includeFriendsHighlights], {
article: { article: { ...article, cached: true } },
})
}
export function useGetArticleQuery({
username,
@ -99,7 +91,7 @@ export function useGetArticleQuery({
includeFriendsHighlights,
}
const { data, error } = useSWRImmutable(
const { data, error, mutate } = useSWRImmutable(
slug ? [query, username, slug, includeFriendsHighlights] : null,
makeGqlFetcher(variables)
)
@ -133,3 +125,46 @@ export async function articleQuery(
return Promise.reject()
}
export const cacheArticle = (
mutate: ScopedMutator,
username: string,
article: ArticleAttributes,
includeFriendsHighlights = false
) => {
mutate([query, username, article.slug, includeFriendsHighlights], {
article: { article: { ...article, cached: true } },
})
}
export const removeItemFromCache = (
cache: Cache<unknown>,
mutate: ScopedMutator,
itemId: string,
) => {
try {
const mappedCache = cache as Map<string, unknown>
mappedCache.forEach((value: any, key) => {
if (typeof value == 'object' && 'articles' in value) {
const articles = value.articles as LibraryItems
const idx = articles.edges.findIndex((edge) => edge.node.id == itemId)
if (idx > -1) {
value.articles.edges.splice(idx, 1)
mutate(key, value, false)
}
}
})
mappedCache.forEach((value: any, key) => {
if (Array.isArray(value)) {
const idx = value.findIndex((item) => 'articles' in item)
if (idx > -1) {
mutate(key, value, false)
}
}
})
} catch (error) {
console.log('error removing item from cache', error)
}
}

View File

@ -1,7 +1,6 @@
import { gql } from 'graphql-request'
import useSWR from 'swr'
import { LabelColor } from '../../../utils/settings-page/labels/types';
import { labelFragment } from '../fragments/labelFragment'
import { Label, labelFragment } from '../fragments/labelFragment'
import { publicGqlFetcher } from '../networkHelpers'
type LabelsQueryResponse = {
@ -18,14 +17,6 @@ type LabelsData = {
labels?: unknown
}
export type Label = {
id: string
name: string
color: LabelColor
description?: string
createdAt: Date
}
export function useGetLabelsQuery(): LabelsQueryResponse {
const query = gql`
query GetLabels {

View File

@ -6,7 +6,7 @@ import { articleFragment } from '../fragments/articleFragment'
import { setLinkArchivedMutation } from '../mutations/setLinkArchivedMutation'
import { deleteLinkMutation } from '../mutations/deleteLinkMutation'
import { articleReadingProgressMutation } from '../mutations/articleReadingProgressMutation'
import { Label } from './../fragments/labelFragment'
import { Label, labelFragment } from './../fragments/labelFragment'
import { showErrorToast, showSuccessToast } from '../../toastHelpers'
export type LibraryItemsQueryInput = {
@ -40,7 +40,7 @@ export type LibraryItemsData = {
articles: LibraryItems
}
type LibraryItems = {
export type LibraryItems = {
edges: LibraryItem[]
pageInfo: PageInfo
errorCodes?: string[]
@ -67,6 +67,30 @@ export type PageInfo = {
totalCount: number
}
const libraryItemFragment = gql`
fragment ArticleFields on Article {
id
title
url
author
image
savedAt
createdAt
publishedAt
contentReader
originalArticleUrl
readingProgressPercent
readingProgressAnchorIndex
slug
isArchived
description
linkId
labels {
...LabelFields
}
}
`
export function useGetLibraryItemsQuery({
limit,
sortDescending,
@ -109,7 +133,8 @@ export function useGetLibraryItemsQuery({
}
}
}
${articleFragment}
${libraryItemFragment}
${labelFragment}
`
const variables = {

View File

@ -51,7 +51,7 @@ export function useGetNewsletterEmailsQuery(): NewsletterEmailsQueryResponse {
isValidating,
emailAddresses,
revalidate: () => {
mutate()
mutate(undefined, true)
}
}
}

View File

@ -25,6 +25,7 @@ export type UserPreferences = {
fontSize: number
fontFamily: string
margin: number
lineHeight?: number
libraryLayoutType: string
librarySortOrder?: SortOrder
}

View File

@ -1,12 +1,12 @@
import { PrimaryLayout } from '../../../components/templates/PrimaryLayout'
import { LoadingView } from '../../../components/patterns/LoadingView'
import { useGetViewerQuery } from '../../../lib/networking/queries/useGetViewerQuery'
import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
import { removeItemFromCache, useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
import { useRouter } from 'next/router'
import { VStack } from './../../../components/elements/LayoutPrimitives'
import { ArticleContainer } from './../../../components/templates/article/ArticleContainer'
import { PdfArticleContainerProps } from './../../../components/templates/article/PdfArticleContainer'
import { useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts'
import { articleKeyboardCommands, navigationCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts'
import dynamic from 'next/dynamic'
@ -20,9 +20,16 @@ import { articleReadingProgressMutation } from '../../../lib/networking/mutation
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
import { userPersonalizationMutation } from '../../../lib/networking/mutations/userPersonalizationMutation'
import Script from 'next/script'
import { EditLabelsModal } from '../../../components/templates/article/EditLabelsModal'
import { theme } from '../../../components/tokens/stitches.config'
import { ArticleActionsMenu } from '../../../components/templates/article/ArticleActionsMenu'
import { setLinkArchivedMutation } from '../../../lib/networking/mutations/setLinkArchivedMutation'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { isVipUser } from '../../../lib/featureFlag'
import { useSWRConfig } from 'swr'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import { SetLabelsModal } from '../../../components/templates/article/SetLabelsModal'
import { DisplaySettingsModal } from '../../../components/templates/article/DisplaySettingsModal'
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
const PdfArticleContainerNoSSR = dynamic<PdfArticleContainerProps>(
() => import('./../../../components/templates/article/PdfArticleContainer'),
@ -31,49 +38,121 @@ const PdfArticleContainerNoSSR = dynamic<PdfArticleContainerProps>(
export default function Home(): JSX.Element {
const router = useRouter()
const { cache, mutate } = useSWRConfig()
const scrollRef = useRef<HTMLDivElement | null>(null)
const { slug } = router.query
const [showLabelsModal, setShowLabelsModal] = useState(false)
const [showHighlightsModal, setShowHighlightsModal] = useState(false)
// Populate data cache
const { viewerData } = useGetViewerQuery()
const { preferencesData } = useGetUserPreferences()
const [fontSize, setFontSize] = useState(preferencesData?.fontSize ?? 20)
const [lineHeight, setLineHeight] = usePersistedState({ key: 'lineHeight', initialValue: 150 })
const [marginWidth, setMarginWidth] = usePersistedState({ key: 'marginWidth', initialValue: 200 })
const [showSetLabelsModal, setShowSetLabelsModal] = useState(false)
const [showEditDisplaySettingsModal, setShowEditDisplaySettingsModal] = useState(false)
const { articleData } = useGetArticleQuery({
username: router.query.username as string,
slug: router.query.slug as string,
includeFriendsHighlights: false,
})
const { preferencesData } = useGetUserPreferences()
const article = articleData?.article.article
const [fontSize, setFontSize] = useState(preferencesData?.fontSize ?? 20)
const [labels, setLabels] = useState<Label[]>([])
useEffect(() => {
if (article?.labels) {
setLabels(article.labels)
}
}, [article])
useKeyboardShortcuts(navigationCommands(router))
const updateFontSize = async (newFontSize: number) => {
setFontSize(newFontSize)
await userPersonalizationMutation({ fontSize: newFontSize })
}
const actionHandler = useCallback(async(action: string, arg?: unknown) => {
const updateFontSize = async(newFontSize: number) => {
setFontSize(newFontSize)
await userPersonalizationMutation({ fontSize: newFontSize })
}
switch (action) {
case 'archive':
if (article) {
removeItemFromCache(cache, mutate, article.id)
await setLinkArchivedMutation({
linkId: article.id,
archived: true,
}).then((res) => {
if (res) {
showSuccessToast('Link archived', { position: 'bottom-right' })
} else {
// todo: revalidate or put back in cache?
showErrorToast('Error archiving link', { position: 'bottom-right' })
}
})
router.push(`/home`)
}
break
case 'openOriginalArticle':
const url = article?.url
if (url) {
window.open(url, '_blank')
}
break
case 'refreshLabels':
setLabels(arg as Label[])
break
case 'showHighlights':
setShowHighlightsModal(true)
break
case 'incrementFontSize':
await updateFontSize(Math.min(fontSize + 2, 28))
break
case 'decrementFontSize':
await updateFontSize(Math.max(fontSize - 2, 10))
break
case 'setMarginWidth': {
const value = Number(arg)
if (value >= 200 && value <= 560) {
setMarginWidth(value)
}
break
}
case 'incrementMarginWidth':
setMarginWidth(Math.min(marginWidth + 45, 560))
break
case 'decrementMarginWidth':
setMarginWidth(Math.max(marginWidth - 45, 200))
break
case 'setLineHeight': {
const value = Number(arg)
if (value >= 100 && value <= 300) {
setLineHeight(arg as number)
}
break
}
case 'editDisplaySettings': {
setShowEditDisplaySettingsModal(true)
break
}
case 'setLabels': {
setShowSetLabelsModal(true)
break
}
case 'resetReaderSettings': {
updateFontSize(20)
setMarginWidth(290)
setLineHeight(150)
break
}
}
}, [article, cache, mutate, router,
fontSize, setFontSize, lineHeight,
setLineHeight, marginWidth, setMarginWidth])
useKeyboardShortcuts(
articleKeyboardCommands(router, async (action) => {
switch (action) {
case 'openOriginalArticle':
const url = article?.url
if (url) {
window.open(url, '_blank')
}
break
case 'incrementFontSize':
await updateFontSize(Math.min(fontSize + 2, 28))
break
case 'decrementFontSize':
await updateFontSize(Math.max(fontSize - 2, 10))
break
case 'editLabels':
if (viewerData?.me && isVipUser(viewerData?.me)) {
setShowLabelsModal(true)
}
break
}
actionHandler(action)
})
)
@ -82,7 +161,15 @@ export default function Home(): JSX.Element {
<PrimaryLayout
pageTestId="home-page-tag"
scrollElementRef={scrollRef}
displayFontStepper={true}
headerToolbarControl={
<ArticleActionsMenu
article={article}
layout='horizontal'
lineHeight={lineHeight}
marginWidth={marginWidth}
articleActionHandler={actionHandler}
/>
}
pageMetaDataProps={{
title: article.title,
path: router.pathname,
@ -97,6 +184,28 @@ export default function Home(): JSX.Element {
/>
<Toaster />
<VStack distribution="between" alignment="center" css={{
position: 'fixed',
flexDirection: 'row-reverse',
top: '-120px',
left: 8,
height: '100%',
width: '48px',
'@lgDown': {
display: 'none',
},
}}
>
{article.contentReader !== 'PDF' ? (
<ArticleActionsMenu
article={article}
layout='vertical'
lineHeight={lineHeight}
marginWidth={marginWidth}
articleActionHandler={actionHandler}
/>
) : null}
</VStack>
{article.contentReader == 'PDF' ? (
<PdfArticleContainerNoSSR
article={article}
@ -104,10 +213,15 @@ export default function Home(): JSX.Element {
/>
) : (
<VStack
alignment="center"
distribution="center"
ref={scrollRef}
className="disable-webkit-callout"
alignment="center"
distribution="center"
ref={scrollRef}
className="disable-webkit-callout"
css={{
'@smDown': {
background: theme.colors.grayBg.toString(),
}
}}
>
<ArticleContainer
article={article}
@ -116,6 +230,11 @@ export default function Home(): JSX.Element {
highlightBarDisabled={false}
highlightsBaseURL={`${webBaseURL}/${viewerData.me?.profile?.username}/${slug}/highlights`}
fontSize={fontSize}
margin={marginWidth}
lineHeight={lineHeight}
labels={labels}
showHighlightsModal={showHighlightsModal}
setShowHighlightsModal={setShowHighlightsModal}
articleMutations={{
createHighlightMutation,
deleteHighlightMutation,
@ -124,20 +243,25 @@ export default function Home(): JSX.Element {
articleReadingProgressMutation,
}}
/>
{/* {showLabelsModal && (
<EditLabelsModal
labels={article.labels || []}
article={article}
onOpenChange={() => {
setShowLabelsModal(false)
}}
setLabels={(labels: Label[]) => {
// setLabels(labels)
}}
/>
)} */}
</VStack>
)}
</VStack>
)}
{showSetLabelsModal && (
<SetLabelsModal
article={article}
articleActionHandler={actionHandler}
onOpenChange={() => setShowSetLabelsModal(false)}
/>
)}
{showEditDisplaySettingsModal && (
<DisplaySettingsModal
lineHeight={lineHeight}
marginWidth={marginWidth}
articleActionHandler={actionHandler}
onOpenChange={() => setShowEditDisplaySettingsModal(false)}
/>
)}
</PrimaryLayout>
)
}

View File

@ -63,6 +63,7 @@ function AppArticleEmbedContent(
props: AppArticleEmbedContentProps
): JSX.Element {
const scrollRef = useRef<HTMLDivElement | null>(null)
const [showHighlightsModal, setShowHighlightsModal] = useState(false)
const { articleData } = useGetArticleQuery({
username: props.username,
@ -99,6 +100,9 @@ function AppArticleEmbedContent(
fontSize={props.fontSize}
margin={props.margin}
fontFamily={props.fontFamily}
labels={[]}
showHighlightsModal={showHighlightsModal}
setShowHighlightsModal={setShowHighlightsModal}
articleMutations={{
createHighlightMutation,
deleteHighlightMutation,

View File

@ -0,0 +1,89 @@
/* eslint-disable @next/next/no-img-element */
import { Box, HStack } from '../../components/elements/LayoutPrimitives'
import { PrimaryLayout } from '../../components/templates/PrimaryLayout'
import Link from 'next/link'
export default function Labels(): JSX.Element {
return (
<PrimaryLayout
pageMetaDataProps={{
title: 'Labels',
path: '/help/labels',
}}
pageTestId="help-labels-tag"
>
<Box
css={{
m: '42px',
maxWidth: '640px',
color: '$grayText',
img: {
maxWidth: '85%',
},
'@smDown': {
m: '16px',
maxWidth: '85%',
alignSelf: 'center',
},
}}
>
<h1>Organize your Omnivore library with labels</h1>
<hr />
<h2>Introduction</h2>
<p>
Labels allow you to group and search for content in Omnivore. A saved page
can have multiple labels and search results can be filtered by label.
</p>
<h2>Adding labels to a page on iOS</h2>
<p>
On iOS you add and remove labels from a page using the Assign Labels modal.
</p>
<p>
You can open the Assign Labels modal from the home view or the reader view.
In the home view long press on an item and choose Edit Labels from the dropdowm
menu. In the reader view use the top right menu button.
</p>
<h2>Adding labels to a page on the web</h2>
<p>
On the web you add and remove labels from a page using the Assign Labels dropdown
or modal depending on your screen size. For larger monitors you will see the Labels
button on the left hand side of the reader view. For smaller monitors you will see
the labels dropdown at the top of your screen.
</p>
<p>
You can also use keyboard commands to open the assign labels modal. On the reader
view tap the <code>l</code> key. Once open you can use the up/down arrow keys, or the
tab key to navigate the available labels, and the Enter key to toggle a label.
</p>
<h2>Searching by label on iOS</h2>
<p>
On iOS you can use the Labels search chip to search for labels. This will open a modal
allowing you to assign multiple labels to your search. This will become an <code>OR </code>
search, meaning if you add multiple labels to your search, pages that have any of the
labels will be returned.
</p>
<h2>Searching by label with Advanced Search</h2>
<p>
Omnivore&apos;s advanced search syntax supports searching for multiple labels using
<code>AND</code> and <code>OR</code> clauses. You can also negate a label search
to find all pages that do not have a certain label.
</p>
<p>Some examples:</p>
<ul>
<li><code>-label:Newsletter</code> finds all pages that do not have the label <code>Newsletter</code></li>
<li><code>label:Cooking,Fitness</code> finds all your pages with either the <code>Cooking</code> or <code>Fitness</code> labels</li>
<li><code>label:Newsletter label:Surfing</code> finds all pages with both the <code>Newsletter</code> and <code>Surfing</code> labels</li>
<li><code>label:Coding -label:Newsletter</code> finds all pages with the <code>Coding</code> label that do not have the <code>Newsletter</code> label</li>
</ul>
<h2>Editing your list of labels</h2>
<p>
The <Link href="/settings/labels"><a>labels</a></Link> page allows you to
edit all of your labels. From here you can create new labels, delete existing
labels, or modify the color and description of a label.
</p>
</Box>
<Box css={{ height: '120px' }} />
</PrimaryLayout>
)
}

View File

@ -15,8 +15,7 @@ import { updateLabelMutation } from '../../lib/networking/mutations/updateLabelM
import { deleteLabelMutation } from '../../lib/networking/mutations/deleteLabelMutation'
import { applyStoredTheme, isDarkTheme } from '../../lib/themeUpdater'
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
import { Label } from '../../lib/networking/queries/useGetLabelsQuery'
import { Label, LabelColor } from '../../lib/networking/fragments/labelFragment'
import { StyledText } from '../../components/elements/StyledText'
import {
ArrowClockwise,
@ -26,7 +25,6 @@ import {
Plus,
} from 'phosphor-react'
import {
LabelColor,
GenericTableCardProps,
LabelColorHex,
} from '../../utils/settings-page/labels/types'
@ -185,18 +183,14 @@ export default function LabelsPage(): JSX.Element {
async function createLabel(): Promise<void> {
const res = await createLabelMutation(
nameInputText,
nameInputText.trim(),
labelColorHex.value,
descriptionInputText
)
if (res) {
if (res.createLabel.errorCodes && res.createLabel.errorCodes.length > 0) {
showErrorToast(res.createLabel.errorCodes[0])
} else {
showSuccessToast('Label created', { position: 'bottom-right' })
resetLabelState()
revalidate()
}
showSuccessToast('Label created', { position: 'bottom-right' })
resetLabelState()
revalidate()
} else {
showErrorToast('Failed to create label')
}

View File

@ -99,3 +99,44 @@ div#appleid-signin {
border-bottom-color: transparent;
border-left-color: transparent;
}
.slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 1px;
background: var(--colors-grayBorderHover);
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px !important;
height: 16px;
border-radius: 50%;
background: var(--colors-utilityTextContrast);
cursor: pointer;
}
.slider::-moz-range-thumb {
-webkit-appearance: none;
width: 16px !important;
height: 16px;
border-radius: 50%;
background: var(--colors-utilityTextContrast);
cursor: pointer;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
border: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: var(--colors-grayTextContrast);
}
button {
padding: 0px;
margin: 0px;
}

View File

@ -44,3 +44,9 @@ export const labelColorObjects: LabelColorObjects = {
background: '#D8D7D50D',
},
}
export const randomLabelColorHex = () => {
const colorHexes = Object.keys(labelColorObjects).slice(0, -1)
const randomColorHex = colorHexes[Math.floor(Math.random() * colorHexes.length)]
return randomColorHex
}

View File

@ -1,14 +1,4 @@
import React from "react";
import { Label } from "../../../lib/networking/queries/useGetLabelsQuery";
export type LabelColor =
| '#FF5D99'
| '#7CFF7B'
| '#FFD234'
| '#7BE4FF'
| '#CE88EF'
| '#EF8C43'
| 'custom color';
import { Label, LabelColor } from "../../../lib/networking/fragments/labelFragment";
export type LabelOptionProps = {
color: string;