WIP: article actoins and reader settings
This commit is contained in:
@ -65,6 +65,7 @@ const StyledLabel = styled(Label, {
|
||||
})
|
||||
|
||||
export type DropdownAlignment = 'start' | 'end' | 'center'
|
||||
export type DropdownSide = 'top' | 'right' | 'bottom' | 'left'
|
||||
|
||||
type DropdownProps = {
|
||||
labelText?: string
|
||||
@ -73,6 +74,8 @@ type DropdownProps = {
|
||||
children: React.ReactNode
|
||||
styledArrow?: boolean
|
||||
align?: DropdownAlignment
|
||||
side?: DropdownSide
|
||||
sideOffset?: number
|
||||
disabled?: boolean
|
||||
css?: CSS
|
||||
}
|
||||
@ -108,8 +111,11 @@ export function Dropdown({
|
||||
labelText,
|
||||
showArrow = true,
|
||||
disabled = false,
|
||||
side = 'bottom',
|
||||
sideOffset = 0,
|
||||
css
|
||||
}: DropdownProps): JSX.Element {
|
||||
console.log('side', side)
|
||||
return (
|
||||
<Root modal={false}>
|
||||
<DropdownTrigger disabled={disabled}>{triggerElement}</DropdownTrigger>
|
||||
@ -119,6 +125,8 @@ export function Dropdown({
|
||||
// remove focus from dropdown
|
||||
;(document.activeElement as HTMLElement).blur()
|
||||
}}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
align={align ? align : 'center'}
|
||||
>
|
||||
{labelText && <StyledLabel>{labelText}</StyledLabel>}
|
||||
|
||||
39
packages/web/components/elements/TickedRangeSlider.tsx
Normal file
39
packages/web/components/elements/TickedRangeSlider.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
14
packages/web/components/elements/images/AIcon.tsx
Normal file
14
packages/web/components/elements/images/AIcon.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -1,20 +1,26 @@
|
||||
import { Separator } from "@radix-ui/react-separator"
|
||||
import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa } from "phosphor-react"
|
||||
import { Button } from "../../elements/Button"
|
||||
import { Box } from "../../elements/LayoutPrimitives"
|
||||
import { Dropdown } from "../../elements/DropdownElements"
|
||||
import { Box, SpanBox } from "../../elements/LayoutPrimitives"
|
||||
import { styled, theme } from "../../tokens/stitches.config"
|
||||
import { ReaderSettings } from "./ReaderSettingsModal"
|
||||
|
||||
export type ArticleActionsMenuLayout = 'horizontal' | 'vertical'
|
||||
|
||||
type ArticleActionsMenuProps = {
|
||||
layout: ArticleActionsMenuLayout
|
||||
articleActionHandler: (action: string, arg?: number) => void
|
||||
}
|
||||
|
||||
export function MenuSeparator(props: ArticleActionsMenuProps) {
|
||||
type MenuSeparatorProps = {
|
||||
layout: ArticleActionsMenuLayout
|
||||
}
|
||||
|
||||
export function MenuSeparator(props: MenuSeparatorProps) {
|
||||
const LineSeparator = styled(Separator, {
|
||||
width: '100%',
|
||||
margin: 0,
|
||||
backgroundColor: 'red',
|
||||
borderBottom: `1px solid ${theme.colors.grayLine.toString()}`,
|
||||
my: '8px',
|
||||
})
|
||||
@ -23,12 +29,12 @@ export function MenuSeparator(props: ArticleActionsMenuProps) {
|
||||
|
||||
export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
|
||||
flexDirection: props.layout == 'vertical' ? 'column' : 'row',
|
||||
alignItems: props.layout == 'vertical' ? 'flex-start' : 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: props.layout == 'vertical' ? 'center' : 'flex-end',
|
||||
gap: props.layout == 'vertical' ? '8px' : '4px',
|
||||
paddingTop: '6px',
|
||||
@ -37,21 +43,40 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element
|
||||
m: '0px',
|
||||
}}
|
||||
>
|
||||
<Button style='articleActionIcon'>
|
||||
<TextAa size={24} color={theme.colors.readerFont.toString()} />
|
||||
</Button>
|
||||
<Dropdown
|
||||
side='right'
|
||||
sideOffset={18}
|
||||
align='start'
|
||||
triggerElement={
|
||||
<SpanBox css={{ width: '100%', marginLeft: 'auto', marginRight: 'auto' }}>
|
||||
<TextAa size={24} color={theme.colors.readerFont.toString()} />
|
||||
</SpanBox>
|
||||
}
|
||||
css={{ m: '0px', p: '0px', outlineStyle: 'solid', outlineWidth: '1px', outlineColor: theme.colors.grayLine.toString() }}
|
||||
>
|
||||
<ReaderSettings />
|
||||
</Dropdown>
|
||||
|
||||
<MenuSeparator layout={props.layout} />
|
||||
|
||||
<Button style='articleActionIcon'>
|
||||
<TagSimple size={24} color={theme.colors.readerFont.toString()} />
|
||||
</Button>
|
||||
<Dropdown
|
||||
side='right'
|
||||
triggerElement={
|
||||
<SpanBox css={{ width: '100%', marginLeft: 'auto', marginRight: 'auto' }}>
|
||||
<TagSimple size={24} color={theme.colors.readerFont.toString()} />
|
||||
</SpanBox>
|
||||
}
|
||||
css={{ background: 'red' }}
|
||||
>
|
||||
{/* <EditLabelsModal> */}
|
||||
</Dropdown>
|
||||
|
||||
<Button style='articleActionIcon'>
|
||||
<Button style='articleActionIcon' onClick={() => props.articleActionHandler('showHighlights')}>
|
||||
<HighlighterCircle size={24} color={theme.colors.readerFont.toString()} />
|
||||
</Button>
|
||||
<MenuSeparator layout={props.layout} />
|
||||
|
||||
<Button style='articleActionIcon'>
|
||||
<Button style='articleActionIcon' onClick={() => props.articleActionHandler('archive')}>
|
||||
<ArchiveBox size={24} color={theme.colors.readerFont.toString()} />
|
||||
</Button>
|
||||
{/* <MenuSeparator layout={props.layout} />
|
||||
@ -60,5 +85,6 @@ export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element
|
||||
<DotsThree size={24} color={theme.colors.readerFont.toString()} />
|
||||
</Button> */}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -15,7 +15,6 @@ import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticle
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
|
||||
type EditLabelsModalProps = {
|
||||
labels: Label[]
|
||||
article: ArticleAttributes
|
||||
onOpenChange: (open: boolean) => void
|
||||
setLabels: (labels: Label[]) => void
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
import {
|
||||
ModalRoot,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { Box, HStack, VStack, Separator, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { CrossIcon } from '../../elements/images/CrossIcon'
|
||||
import { CommentIcon } from '../../elements/images/CommentIcon'
|
||||
import { TrashIcon } from '../../elements/images/TrashIcon'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
import type { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { HighlightView } from '../../patterns/HighlightView'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { StyledTextArea } from '../../elements/StyledTextArea'
|
||||
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
|
||||
import { AlignCenterHorizontalSimple, ArrowsInLineHorizontal, ArrowsOutLineHorizontal, Minus, Pen, Plus, Trash } from 'phosphor-react'
|
||||
import { AIcon } from '../../elements/images/AIcon'
|
||||
import { TickedRangeSlider } from '../../elements/TickedRangeSlider'
|
||||
|
||||
type ReaderSettingsProps = {
|
||||
|
||||
}
|
||||
|
||||
export function ReaderSettings(props: ReaderSettingsProps): JSX.Element {
|
||||
const VerticalDivider = styled(SpanBox, {
|
||||
width: '1px',
|
||||
height: '100%',
|
||||
background: `${theme.colors.grayLine.toString()}`,
|
||||
})
|
||||
return (
|
||||
<VStack>
|
||||
<HStack
|
||||
alignment='center'
|
||||
css={{
|
||||
width: '265px',
|
||||
height: '70px',
|
||||
borderBottom: `1px solid ${theme.colors.grayLine.toString()}`,
|
||||
}}>
|
||||
<Button style='plainIcon' onClick={() => {}}>
|
||||
<AIcon size={28} color={theme.colors.readerFont.toString()} />
|
||||
<Minus size={28} color={theme.colors.readerFont.toString()}/>
|
||||
</Button>
|
||||
<VerticalDivider />
|
||||
<Button style='plainIcon' onClick={() => {}}>
|
||||
<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: '8px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderBottom: `1px solid ${theme.colors.grayLine.toString()}`,
|
||||
}}>
|
||||
<StyledText color={theme.colors.readerFontTransparent.toString()} css={{ pl: '12px', m: '0px', pt: '14px' }}>Margin:</StyledText>
|
||||
<HStack distribution='between' css={{ gap: '16px', alignItems: 'center', alignSelf: 'center' }}>
|
||||
<Button style='plainIcon' css={{ pt: '10px', px: '4px' }}>
|
||||
<ArrowsInLineHorizontal size={24} color={theme.colors.readerFont.toString()} />
|
||||
</Button>
|
||||
<TickedRangeSlider value={14} onChange={() => {}} />
|
||||
<Button style='plainIcon' css={{ pt: '10px', px: '4px' }}>
|
||||
<ArrowsOutLineHorizontal 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 Height:</StyledText>
|
||||
<HStack distribution='between' css={{ gap: '16px', alignItems: 'center', alignSelf: 'center' }}>
|
||||
<Button style='plainIcon' css={{ pt: '10px', px: '4px' }}>
|
||||
<AlignCenterHorizontalSimple size={25} color={theme.colors.readerFont.toString()} />
|
||||
</Button>
|
||||
<TickedRangeSlider value={14} onChange={() => {}} />
|
||||
<Button style='plainIcon' css={{ pt: '10px', px: '4px' }}>
|
||||
<AlignCenterHorizontalSimple size={25} color={theme.colors.readerFont.toString()} />
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
@ -29,6 +29,8 @@ import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa } from 'pho
|
||||
import { Separator } from '@radix-ui/react-separator'
|
||||
import { Article } from '../../../components/templates/article/Article'
|
||||
import { ArticleActionsMenu } from '../../../components/templates/article/ArticleActionsMenu'
|
||||
import { HighlightsModal } from '../../../components/templates/article/HighlightsModal'
|
||||
import { setLinkArchivedMutation } from '../../../lib/networking/mutations/setLinkArchivedMutation'
|
||||
|
||||
const MenuSeparator = styled(Separator, {
|
||||
width: '100%',
|
||||
@ -48,6 +50,7 @@ export default function Home(): JSX.Element {
|
||||
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()
|
||||
@ -73,33 +76,51 @@ export default function Home(): JSX.Element {
|
||||
setMarginWidth(newMargin)
|
||||
}
|
||||
|
||||
const actionHandler = async (action: string, arg?: number) => {
|
||||
switch (action) {
|
||||
case 'archive':
|
||||
if (article) {
|
||||
await setLinkArchivedMutation({
|
||||
linkId: article.id,
|
||||
archived: true,
|
||||
})
|
||||
// TODO: merge from article actions PR
|
||||
// removeItemFromCache(cache, mutate, props.article.id)
|
||||
router.push(`/home`)
|
||||
}
|
||||
break
|
||||
case 'openOriginalArticle':
|
||||
const url = article?.url
|
||||
if (url) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
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 'incrementMarginWidth':
|
||||
updateMarginWidth(Math.min(marginWidth + 50, 560))
|
||||
break
|
||||
case 'decrementMarginWidth':
|
||||
updateMarginWidth(Math.max(marginWidth - 50, 200))
|
||||
break
|
||||
case 'editLabels':
|
||||
if (viewerData?.me && isVipUser(viewerData?.me)) {
|
||||
setShowLabelsModal(true)
|
||||
}
|
||||
break
|
||||
}
|
||||
};
|
||||
|
||||
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 'incrementMarginWidth':
|
||||
updateMarginWidth(Math.min(marginWidth + 50, 560))
|
||||
break
|
||||
case 'decrementMarginWidth':
|
||||
updateMarginWidth(Math.max(marginWidth - 50, 200))
|
||||
break
|
||||
case 'editLabels':
|
||||
if (viewerData?.me && isVipUser(viewerData?.me)) {
|
||||
setShowLabelsModal(true)
|
||||
}
|
||||
break
|
||||
}
|
||||
actionHandler(action)
|
||||
})
|
||||
)
|
||||
|
||||
@ -124,19 +145,22 @@ 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',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArticleActionsMenu layout='vertical' />
|
||||
</VStack>
|
||||
position: 'fixed',
|
||||
flexDirection: 'row-reverse',
|
||||
top: '-120px',
|
||||
left: 8,
|
||||
height: '100%',
|
||||
width: '48px',
|
||||
'@lgDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ArticleActionsMenu
|
||||
layout='vertical'
|
||||
articleActionHandler={actionHandler}
|
||||
/>
|
||||
</VStack>
|
||||
{article.contentReader == 'PDF' ? (
|
||||
<PdfArticleContainerNoSSR
|
||||
article={article}
|
||||
@ -170,6 +194,17 @@ export default function Home(): JSX.Element {
|
||||
articleReadingProgressMutation,
|
||||
}}
|
||||
/>
|
||||
</VStack>
|
||||
)}
|
||||
{showHighlightsModal && (
|
||||
<HighlightsModal
|
||||
highlights={article.highlights}
|
||||
onOpenChange={() => setShowHighlightsModal(false)}
|
||||
deleteHighlightAction={(highlightId: string) => {
|
||||
// removeHighlightCallback(highlightId)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* {showLabelsModal && (
|
||||
<EditLabelsModal
|
||||
labels={article.labels || []}
|
||||
@ -182,8 +217,6 @@ export default function Home(): JSX.Element {
|
||||
}}
|
||||
/>
|
||||
)} */}
|
||||
</VStack>
|
||||
)}
|
||||
</PrimaryLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@ -99,3 +99,39 @@ 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);
|
||||
}
|
||||
Reference in New Issue
Block a user