diff --git a/packages/api/src/apollo.ts b/packages/api/src/apollo.ts index fa869a3c3..53c94ec24 100644 --- a/packages/api/src/apollo.ts +++ b/packages/api/src/apollo.ts @@ -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') diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index b3ccce06d..ccd3b0131 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -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', diff --git a/packages/web/components/elements/DropdownElements.tsx b/packages/web/components/elements/DropdownElements.tsx index 818898f06..9ed0299c5 100644 --- a/packages/web/components/elements/DropdownElements.tsx +++ b/packages/web/components/elements/DropdownElements.tsx @@ -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 ( - + {triggerElement} {labelText && {labelText}} {children} - {showArrow && } + {showArrow && } ) diff --git a/packages/web/components/elements/LabelChip.tsx b/packages/web/components/elements/LabelChip.tsx index b368825e9..86dd3c3ba 100644 --- a/packages/web/components/elements/LabelChip.tsx +++ b/packages/web/components/elements/LabelChip.tsx @@ -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 ( - - {props.text} - + + + {props.text} + + ) } diff --git a/packages/web/components/elements/LabelColorDropdown.tsx b/packages/web/components/elements/LabelColorDropdown.tsx index 6e105beed..5d3ff2979 100644 --- a/packages/web/components/elements/LabelColorDropdown.tsx +++ b/packages/web/components/elements/LabelColorDropdown.tsx @@ -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, diff --git a/packages/web/components/elements/ModalPrimitives.tsx b/packages/web/components/elements/ModalPrimitives.tsx index db9aec2e2..3827e020b 100644 --- a/packages/web/components/elements/ModalPrimitives.tsx +++ b/packages/web/components/elements/ModalPrimitives.tsx @@ -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' }, }) diff --git a/packages/web/components/elements/TickedRangeSlider.tsx b/packages/web/components/elements/TickedRangeSlider.tsx new file mode 100644 index 000000000..c112d8d73 --- /dev/null +++ b/packages/web/components/elements/TickedRangeSlider.tsx @@ -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 ( + + onChange(e.target.value as any)} value={value} type="range" min={min} max={max} step={step} className='slider'/> + + {[...Array(ticks)].map((val, idx) => )} + + + ) +} \ No newline at end of file diff --git a/packages/web/components/elements/Tooltip.tsx b/packages/web/components/elements/Tooltip.tsx index 241c20e8e..b1139061a 100644 --- a/packages/web/components/elements/Tooltip.tsx +++ b/packages/web/components/elements/Tooltip.tsx @@ -62,6 +62,15 @@ type TooltipWrappedProps = { style?: TooltipPrimitive.TooltipContentProps['style'] } +const DefaultTooltipStyle = { + backgroundColor: '#F9D354', + color: '#0A0806', +} + +const DefaultArrowStyle = { + fill: '#F9D354' +} + export const TooltipWrapped: FC = ({ children, active, @@ -73,9 +82,14 @@ export const TooltipWrapped: FC = ({ return ( {children} - + {tooltipContent} - + ) diff --git a/packages/web/components/elements/images/AIcon.tsx b/packages/web/components/elements/images/AIcon.tsx new file mode 100644 index 000000000..f7972a2f0 --- /dev/null +++ b/packages/web/components/elements/images/AIcon.tsx @@ -0,0 +1,14 @@ +type AIconProps = { + size: number + color: string + style?: React.CSSProperties +} + +export function AIcon(props: AIconProps): JSX.Element { + return ( + + + + + ) +} \ No newline at end of file diff --git a/packages/web/components/elements/images/OmnivoreNameLogo.tsx b/packages/web/components/elements/images/OmnivoreNameLogo.tsx index 482845d0d..0f3e25328 100644 --- a/packages/web/components/elements/images/OmnivoreNameLogo.tsx +++ b/packages/web/components/elements/images/OmnivoreNameLogo.tsx @@ -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 { - Omnivore + {/* Omnivore */} ) diff --git a/packages/web/components/patterns/DropdownMenu.tsx b/packages/web/components/patterns/DropdownMenu.tsx index c63dca2ee..317af1ade 100644 --- a/packages/web/components/patterns/DropdownMenu.tsx +++ b/packages/web/components/patterns/DropdownMenu.tsx @@ -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 ? '✓' : '' } - - {props.displayFontStepper && ( - <> - - - - - - - )} props.actionHandler('navigate-to-emails')} title="Emails" /> + props.actionHandler('navigate-to-labels')} + title="Labels" + /> window.Intercom('show')} title="Feedback" diff --git a/packages/web/components/patterns/HighlightBar.tsx b/packages/web/components/patterns/HighlightBar.tsx index 444fb0f56..ead951168 100644 --- a/packages/web/components/patterns/HighlightBar.tsx +++ b/packages/web/components/patterns/HighlightBar.tsx @@ -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%', diff --git a/packages/web/components/patterns/LibraryCards/GridLinkedItemCard.tsx b/packages/web/components/patterns/LibraryCards/GridLinkedItemCard.tsx index bb60b4c37..319bc6fdb 100644 --- a/packages/web/components/patterns/LibraryCards/GridLinkedItemCard.tsx +++ b/packages/web/components/patterns/LibraryCards/GridLinkedItemCard.tsx @@ -154,11 +154,11 @@ export function GridLinkedItemCard(props: LinkedItemCardProps): JSX.Element { /> )} - - {props.item.labels?.map(({ description, color }, index) => ( - + {/* + {props.item.labels?.map(({ name, color }, index) => ( + ))} - + */} ) } diff --git a/packages/web/components/patterns/PrimaryHeader.tsx b/packages/web/components/patterns/PrimaryHeader.tsx index a74c39999..79294d02f 100644 --- a/packages/web/components/patterns/PrimaryHeader.tsx +++ b/packages/web/components/patterns/PrimaryHeader.tsx @@ -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 - 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', + }, }} > @@ -183,26 +182,26 @@ function NavHeader(props: NavHeaderProps): JSX.Element { + + {props.toolbarControl && ( + + {props.toolbarControl} + + )} + {props.username ? ( - - - - - } actionHandler={props.actionHandler} - displayFontStepper={props.displayFontStepper} /> ) : ( diff --git a/packages/web/components/templates/PrimaryLayout.tsx b/packages/web/components/templates/PrimaryLayout.tsx index 17757cacd..56e85a419 100644 --- a/packages/web/components/templates/PrimaryLayout.tsx +++ b/packages/web/components/templates/PrimaryLayout.tsx @@ -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 - 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 ? ( ) : null} - + + {props.children} {showLogoutConfirmation ? ( 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' ? : <>) +} + +type ActionDropdownProps = { + layout: ArticleActionsMenuLayout + triggerElement: JSX.Element + children: JSX.Element +} + +const ActionDropdown = (props: ActionDropdownProps): JSX.Element => { + return + {props.children} + +} + +export function ArticleActionsMenu(props: ArticleActionsMenuProps): JSX.Element { + return ( + <> + + + + + + } + > + + + + + + + + + + } + > + + + + + + + + + + + + {/* + + */} + + + ) +} \ No newline at end of file diff --git a/packages/web/components/templates/article/ArticleContainer.tsx b/packages/web/components/templates/article/ArticleContainer.tsx index b4efcb97d..46a846637 100644 --- a/packages/web/components/templates/article/ArticleContainer.tsx +++ b/packages/web/components/templates/article/ArticleContainer.tsx @@ -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 isAppleAppEmbed: boolean @@ -24,21 +27,22 @@ type ArticleContainerProps = { margin?: number fontSize?: number fontFamily?: string + lineHeight?: number + showHighlightsModal: boolean + setShowHighlightsModal: React.Dispatch> } 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} /> - 0} - /> + {props.labels ? ( + + {props.labels?.map((label) => + + )} + + ) : null} + {props.isAppleAppEmbed && ( + 0} + /> + )}
{showReportIssuesModal ? ( diff --git a/packages/web/components/templates/article/ArticleHeaderToolbar.tsx b/packages/web/components/templates/article/ArticleHeaderToolbar.tsx index c870a7682..10b0e993b 100644 --- a/packages/web/components/templates/article/ArticleHeaderToolbar.tsx +++ b/packages/web/components/templates/article/ArticleHeaderToolbar.tsx @@ -14,7 +14,7 @@ type ArticleHeaderToolbarProps = { articleTitle: string articleShareURL: string hasHighlights: boolean - setShowNotesSidebar: (showNotesSidebar: boolean) => void + setShowHighlightsModal: React.Dispatch> setShowShareArticleModal: (showShareModal: boolean) => void } @@ -46,7 +46,12 @@ export function ArticleHeaderToolbar( return ( {props.hasHighlights && ( - + + + + + + ) +} diff --git a/packages/web/components/templates/article/EditLabelsModal.tsx b/packages/web/components/templates/article/EditLabelsModal.tsx deleted file mode 100644 index 6b3e043d5..000000000 --- a/packages/web/components/templates/article/EditLabelsModal.tsx +++ /dev/null @@ -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) => { - // const label = event.target.value - // if (event.target.checked) { - // setSelectedLabels([...selectedLabels, label]) - // } else { - // setSelectedLabels(selectedLabels.filter((l) => l !== label)) - // } - }, - [selectedLabels] - ) - - return ( - - - { - event.preventDefault() - }} - css={{ overflow: 'auto', p: '0' }} - > - - - - Edit Labels - - - - {labels && - labels.map((label) => ( - { - // if (selectedLabels.includes(label.id)) { - // setSelectedLabels( - // selectedLabels.filter((id) => id !== label.id) - // ) - // } else { - // setSelectedLabels([...selectedLabels, label.id]) - // } - }} - > - - - - ))} - - - - - - - ) -} diff --git a/packages/web/components/templates/article/HighlightsLayer.tsx b/packages/web/components/templates/article/HighlightsLayer.tsx index 3d19c33ff..5d00a58e5 100644 --- a/packages/web/components/templates/article/HighlightsLayer.tsx +++ b/packages/web/components/templates/article/HighlightsLayer.tsx @@ -25,9 +25,9 @@ type HighlightsLayerProps = { articleAuthor: string isAppleAppEmbed: boolean highlightBarDisabled: boolean - showNotesSidebar: boolean + showHighlightsModal: boolean highlightsBaseURL: string - setShowNotesSidebar: React.Dispatch> + setShowHighlightsModal: React.Dispatch> articleMutations: ArticleMutations } @@ -466,11 +466,11 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element { ) } - if (props.showNotesSidebar) { + if (props.showHighlightsModal) { return ( props.setShowNotesSidebar(false)} + onOpenChange={() => props.setShowHighlightsModal(false)} deleteHighlightAction={(highlightId: string) => { removeHighlightCallback(highlightId) }} diff --git a/packages/web/components/templates/article/HighlightsModal.tsx b/packages/web/components/templates/article/HighlightsModal.tsx index 750ef1636..92358b057 100644 --- a/packages/web/components/templates/article/HighlightsModal.tsx +++ b/packages/web/components/templates/article/HighlightsModal.tsx @@ -30,6 +30,7 @@ export function HighlightsModal(props: HighlightsModalProps): JSX.Element { { event.preventDefault() + props.onOpenChange(false) }} css={{ overflow: 'auto', p: '0' }} > diff --git a/packages/web/components/templates/article/ReaderSettingsControl.tsx b/packages/web/components/templates/article/ReaderSettingsControl.tsx new file mode 100644 index 000000000..4e5f731aa --- /dev/null +++ b/packages/web/components/templates/article/ReaderSettingsControl.tsx @@ -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 ( + + + + + + + + Margin: + + + { + setMarginWidth(value) + props.articleActionHandler('setMarginWidth', value) + }} /> + + + + + + Line Spacing: + + + { + setLineHeight(value) + props.articleActionHandler('setLineHeight', value) + }} /> + + + + + + + ) +} diff --git a/packages/web/components/templates/article/SetLabelsControl.tsx b/packages/web/components/templates/article/SetLabelsControl.tsx new file mode 100644 index 000000000..1302369d8 --- /dev/null +++ b/packages/web/components/templates/article/SetLabelsControl.tsx @@ -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(null) + + useEffect(() => { + if (!isTouchScreenDevice() && props.focused && inputRef.current) { + inputRef.current.focus() + } + }, [props.focused]) + + return ( + + + { + 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)', + }, + }} + /> + + ) +} + +type LabelListItemProps = { + label: Label + focused: boolean + selected: boolean + toggleLabel: (label: Label) => void +} + +function LabelListItem(props: LabelListItemProps): JSX.Element { + const ref = useRef(null) + const { label, focused, selected } = props + + useEffect(() => { + if (props.focused && ref.current) { + ref.current.focus() + } + }, [props.focused]) + + return ( + { + event.preventDefault() + props.toggleLabel(label) + ref.current?.blur() + }} + > + + + {selected && } + + + + + + {label.name} + + + {selected && } + + + ) +} + +type FooterProps = { + focused: boolean +} + +function Footer(props: FooterProps): JSX.Element { + const ref = useRef(null) + + useEffect(() => { + if (props.focused && ref.current) { + ref.current.focus() + } + }, [props.focused]) + + return ( + + + + Edit labels + + + ) +} + +export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element { + const router = useRouter() + const [filterText, setFilterText] = useState('') + const { labels, revalidate } = useGetLabelsQuery() + const [selectedLabels, setSelectedLabels] = useState(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(undefined) + const handleKeyDown = useCallback(async (event: React.KeyboardEvent) => { + 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 ( + +
setFocusedIndex(undefined)} + setFilterText={setFilterText} filterText={filterText} + /> + + {filteredLabels && + filteredLabels.map((label, idx) => ( + + ))} + + {filterText && ( + + )} + +