Add multiple highlight colors

This commit is contained in:
Jackson Harper
2023-08-24 12:00:35 +08:00
parent 37e994084e
commit 03e95a029b
14 changed files with 301 additions and 133 deletions

View File

@ -102,6 +102,8 @@ export function makeApolloServer(): ApolloServer {
const apollo = new ApolloServer({
schema: schema,
context: contextFunc,
cache: 'bounded',
persistedQueries: false,
formatError: (err) => {
logger.info('server error', err)
Sentry.captureException(err)

View File

@ -246,6 +246,18 @@ export const Button = styled('button', {
},
'&:focus': { outline: 'none' },
},
highlightBarIcon: {
p: '0px',
lineHeight: '0px',
bg: 'transparent',
border: 'none',
cursor: 'pointer',
'&:hover': {
opacity: 0.5,
},
'&:focus': { outline: 'none' },
},
articleActionIcon: {
bg: 'transparent',
border: 'none',

View File

@ -6,9 +6,20 @@ import { StyledText } from '../elements/StyledText'
import { Button } from '../elements/Button'
import { HStack, Box } from '../elements/LayoutPrimitives'
import { PenWithColorIcon } from '../elements/images/PenWithColorIcon'
import { Note, Tag, Trash, Copy } from 'phosphor-react'
import {
Note,
Tag,
Trash,
Copy,
Circle,
CopySimple,
CheckCircle,
} from 'phosphor-react'
import { TrashIcon } from '../elements/icons/TrashIcon'
import { LabelIcon } from '../elements/icons/LabelIcon'
import { NotebookIcon } from '../elements/icons/NotebookIcon'
import { highlightColor, highlightColors } from '../../lib/themeUpdater'
import { useState } from 'react'
type PageCoordinates = {
pageX: number
@ -24,27 +35,29 @@ export type HighlightAction =
| 'unshare'
| 'setHighlightLabels'
| 'copy'
| 'updateColor'
type HighlightBarProps = {
anchorCoordinates: PageCoordinates
isNewHighlight: boolean
isSharedToFeed: boolean
displayAtBottom: boolean
handleButtonClick: (action: HighlightAction) => void
highlightColor?: string
handleButtonClick: (action: HighlightAction, param?: string) => void
}
export function HighlightBar(props: HighlightBarProps): JSX.Element {
return (
<Box
css={{
width: '100%',
maxWidth: props.isNewHighlight ? '330px' : '380px',
height: '48px',
// width: '295px',
// height: '50px',
position: props.displayAtBottom ? 'fixed' : 'absolute',
background: '$grayBg',
borderRadius: '4px',
border: '1px solid $grayBorder',
boxShadow: theme.shadows.cardBoxShadow.toString(),
background: '$thBackground2',
borderRadius: '5px',
border: '1px solid $thHighlightBar',
boxShadow: `0px 4px 4px 0px rgba(0, 0, 0, 0.15)`,
...(props.displayAtBottom && {
bottom: 'calc(38px + env(safe-area-inset-bottom, 40px))',
}),
@ -65,113 +78,139 @@ export function HighlightBar(props: HighlightBarProps): JSX.Element {
)
}
type BarButtonProps = {
title: string
onClick: VoidFunction
iconElement: JSX.Element
text: string
}
function BarButton({ text, title, iconElement, onClick }: BarButtonProps) {
return (
<Button
style="plainIcon"
title={title}
onClick={onClick}
css={{
flexDirection: 'column',
height: '100%',
m: 0,
p: 0,
alignItems: 'baseline',
}}
>
<HStack css={{ height: '100%', alignItems: 'center' }}>
{iconElement}
<StyledText
style="body"
css={{
pl: '4px',
m: '0px',
color: '$readerFont',
fontWeight: '400',
fontSize: '16px',
}}
>
{text}
</StyledText>
</HStack>
</Button>
)
}
const Separator = styled('div', {
width: '1px',
height: '20px',
mx: '5px',
background: '$thHighlightBar',
})
function BarContent(props: HighlightBarProps): JSX.Element {
const Separator = styled('div', {
width: '1px',
maxWidth: '1px',
height: '100%',
background: '$grayBorder',
})
const [hovered, setHovered] = useState<string | undefined>(undefined)
return (
<HStack
distribution="evenly"
distribution="start"
alignment="center"
css={{
height: '100%',
alignItems: 'center',
display: 'flex',
padding: '5px 10px',
gap: '5px',
width: props.displayAtBottom ? '100%' : 'auto',
}}
>
{props.isNewHighlight ? (
<BarButton
text="Highlight"
title="Create Highlight"
iconElement={<PenWithColorIcon />}
onClick={() => props.handleButtonClick('create')}
/>
) : (
<>
<BarButton
text="Delete"
title="Remove Highlight"
iconElement={
<TrashIcon
size={20}
color={theme.colors.omnivoreRed.toString()}
{highlightColors.map((color) => {
return (
<Button
key={`color-${color}`}
style="highlightBarIcon"
title={`Create Highlight (${color})`}
onClick={() => {
if (!props.isNewHighlight && props.highlightColor != color) {
props.handleButtonClick('updateColor', color)
} else if (
!props.isNewHighlight &&
props.highlightColor == color
) {
props.handleButtonClick('delete')
} else {
props.handleButtonClick('create', color)
}
}}
onMouseEnter={() => {
setHovered(color)
}}
onMouseLeave={() => {
setHovered(undefined)
}}
>
{props.isNewHighlight || props.highlightColor != color ? (
<Circle
key={color}
width={25}
height={25}
color={highlightColor(color)}
weight="fill"
/>
}
onClick={() => props.handleButtonClick('delete')}
/>
<Separator />
<BarButton
text="Labels"
title="Set Labels"
iconElement={
<LabelIcon size={20} color={theme.colors.readerFont.toString()} />
}
) : (
<CheckCircle
key={color}
width={25}
height={25}
color={highlightColor(color)}
weight="fill"
/>
)}
</Button>
)
})}
<Separator />
{!props.isNewHighlight && (
<>
<Button
title={`Set Labels`}
style="highlightBarIcon"
onClick={() => props.handleButtonClick('setHighlightLabels')}
/>
onMouseEnter={() => {
setHovered('labels')
}}
onMouseLeave={() => {
setHovered(undefined)
}}
>
<LabelIcon
size={25}
color={
hovered == 'labels'
? theme.colors.thTextContrast.toString()
: theme.colors.thHighlightBar.toString()
}
/>
</Button>
</>
)}
<Separator />
<BarButton
text="Note"
title="Add Note to Highlight"
iconElement={
<Note size={20} color={theme.colors.readerFont.toString()} />
}
<Button
title={props.isNewHighlight ? `Create Highlight w/note` : 'Add Note'}
style="highlightBarIcon"
onClick={() => props.handleButtonClick('comment')}
/>
<Separator />
<BarButton
text="Copy"
title="Copy Text to Clipboard"
iconElement={
<Copy size={20} color={theme.colors.readerFont.toString()} />
}
onMouseEnter={() => {
setHovered('note')
}}
onMouseLeave={() => {
setHovered(undefined)
}}
>
<NotebookIcon
size={25}
color={
hovered == 'note'
? theme.colors.thTextContrast.toString()
: theme.colors.thHighlightBar.toString()
}
/>
</Button>
<Button
title={`Copy`}
style="highlightBarIcon"
onClick={() => props.handleButtonClick('copy')}
/>
onMouseEnter={() => {
setHovered('copy')
}}
onMouseLeave={() => {
setHovered(undefined)
}}
>
<CopySimple
width={25}
height={25}
color={
hovered == 'copy'
? theme.colors.thTextContrast.toString()
: theme.colors.thHighlightBar.toString()
}
/>
</Button>
</HStack>
)
}

View File

@ -13,7 +13,11 @@ import { styled, theme } from '../tokens/stitches.config'
import { HighlightViewNote } from './HighlightNotes'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { isDarkTheme } from '../../lib/themeUpdater'
import {
highlightColor,
highlightColorVar,
isDarkTheme,
} from '../../lib/themeUpdater'
import { ReadableItem } from '../../lib/networking/queries/useGetLibraryItemsQuery'
import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery'
import {
@ -70,7 +74,7 @@ export function HighlightView(props: HighlightViewProps): JSX.Element {
const hover = useHover(context)
const { getReferenceProps, getFloatingProps } = useInteractions([hover])
const highlightAlpha = isDark ? 0.5 : 0.35
const highlightColor = highlightColorVar(props.highlight.color)
return (
<VStack
@ -111,9 +115,8 @@ export function HighlightView(props: HighlightViewProps): JSX.Element {
css={{
'> *': {
display: 'inline',
padding: '2px',
backgroundColor: `rgba(var(--colors-highlightBackground), ${highlightAlpha})`,
boxShadow: `3px 0 0 rgba(var(--colors-highlightBackground), ${highlightAlpha}), -3px 0 0 rgba(var(--colors-highlightBackground), ${highlightAlpha})`,
padding: '3px',
backgroundColor: `rgba(${highlightColor}, var(--colors-highlightBackgroundAlpha))`,
boxDecorationBreak: 'clone',
borderRadius: '2px',
},

View File

@ -30,6 +30,7 @@ import 'react-sliding-pane/dist/react-sliding-pane.css'
import { NotebookContent } from './Notebook'
import { NotebookHeader } from './NotebookHeader'
import useGetWindowDimensions from '../../../lib/hooks/useGetWindowDimensions'
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
type HighlightsLayerProps = {
viewer: UserBasicData
@ -88,20 +89,26 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
undefined
)
const [
confirmDeleteHighlightWithNoteId,
setConfirmDeleteHighlightWithNoteId,
] = useState<string | undefined>(undefined)
const windowDimensions = useGetWindowDimensions()
const createHighlightFromSelection = useCallback(
async (
selection: SelectionAttributes,
note?: string
options: { annotation?: string; color?: string } | undefined
): Promise<Highlight | undefined> => {
const result = await createHighlight(
{
selection: selection,
articleId: props.articleId,
existingHighlights: highlights,
color: options?.color,
highlightStartEndOffsets: highlightLocations,
annotation: note,
annotation: options?.annotation,
highlightPositionPercent: selectionPercentPos(selection.selection),
highlightPositionAnchorIndex: selectionAnchorIndex(
selection.selection
@ -190,8 +197,10 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
setHighlights(highlights.filter(($0) => $0.id !== highlightId))
setFocusedHighlight(undefined)
document.dispatchEvent(new Event('highlightsUpdated'))
showSuccessToast('Highlight removed')
} else {
console.error('Failed to delete highlight')
showErrorToast('Error removing highlight')
}
},
[focusedHighlight, highlights, highlightLocations, props.articleMutations]
@ -271,14 +280,14 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
}
const createHighlightCallback = useCallback(
async (annotation?: string) => {
async (options: { annotation?: string; color?: string } | undefined) => {
if (!selectionData) {
return
}
try {
const result = await createHighlightFromSelection(
selectionData,
annotation
options
)
if (!result) {
showErrorToast('Error saving highlight', { position: 'bottom-right' })
@ -436,13 +445,25 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
}, [handleSingleClick, handleDoubleClick])
const handleAction = useCallback(
async (action: HighlightAction) => {
async (action: HighlightAction, param?: string) => {
switch (action) {
case 'delete':
await removeHighlightCallback()
if (focusedHighlight?.annotation == undefined) {
await removeHighlightCallback()
} else {
setConfirmDeleteHighlightWithNoteId(focusedHighlight?.id)
}
break
case 'create':
await createHighlightCallback()
await createHighlightCallback({
color: param,
})
break
case 'updateColor':
if (focusedHighlight) {
focusedHighlight.color = param
await updateHighlightsCallback(focusedHighlight)
}
break
case 'comment':
if (props.highlightBarDisabled || focusedHighlight) {
@ -517,6 +538,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
removeHighlightCallback,
selectionData,
setSelectionData,
confirmDeleteHighlightWithNoteId,
]
)
@ -751,6 +773,20 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
onOpenChange={() => setLabelsTarget(undefined)}
/>
)}
{confirmDeleteHighlightWithNoteId && (
<ConfirmationModal
message="Are you sure you want to delete this highlight? The note associated with it will also be deleted."
onAccept={() => {
;(async () => {
await removeHighlightCallback(confirmDeleteHighlightWithNoteId)
setConfirmDeleteHighlightWithNoteId(undefined)
})()
}}
onOpenChange={() => {
setConfirmDeleteHighlightWithNoteId(undefined)
}}
/>
)}
{/* // Display the button bar if we are not in the native app and there // is
a focused highlight or selection data */}
{!props.highlightBarDisabled && (focusedHighlight || selectionData) && (
@ -761,6 +797,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
handleButtonClick={handleAction}
isSharedToFeed={focusedHighlight?.sharedAt != undefined}
displayAtBottom={isTouchScreenDevice()}
highlightColor={focusedHighlight?.color ?? 'yellow'}
/>
</>
)}

View File

@ -1,5 +1,6 @@
import type * as Stitches from '@stitches/react'
import { createStitches, createTheme } from '@stitches/react'
import { highlightColor } from '../../lib/themeUpdater'
export enum ThemeId {
Light = 'Light',
@ -198,6 +199,16 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
thProgressFg: '#FFD234',
thHighContrast: '#3D3D3D',
thHighlightBar: '#D9D9D9',
highlightBackgroundGreen: '85, 198, 137',
highlightBackgroundBlue: '106, 177, 255',
highlightBackgroundOrange: '254, 181, 109',
highlightBackgroundYellow: '255, 210, 52',
highlightBackgroundRed: '251, 154, 154',
highlightBackgroundAlpha: '0.2',
highlightUnderlineAlpha: '1',
},
},
media: {
@ -298,6 +309,10 @@ const darkThemeSpec = {
thProgressFg: '#FFD234',
thHighContrast: '#D9D9D9',
thHighlightBar: '#6A6968',
highlightUnderlineAlpha: '0.5',
highlightBackgroundAlpha: '0.35',
},
shadows: {
cardBoxShadow:

View File

@ -17,6 +17,7 @@ type CreateHighlightInput = {
selection: SelectionAttributes
articleId: string
annotation?: string
color?: string
existingHighlights: Highlight[]
highlightStartEndOffsets: HighlightLocation[]
highlightPositionPercent?: number
@ -94,7 +95,8 @@ export async function createHighlight(
const highlightAttributes = makeHighlightNodeAttributes(
patch,
id,
annotations.length > 0
annotations.length > 0,
input.color
)
const newHighlightAttributes = {
@ -102,6 +104,7 @@ export async function createHighlight(
shortId: nanoid(8),
patch,
color: input.color,
prefix: highlightAttributes.prefix,
suffix: highlightAttributes.suffix,
quote: htmlToMarkdown(container.innerHTML),

View File

@ -68,9 +68,7 @@ function nodeAttributesFromHighlight(
const id = highlight.id
const withNote = !!highlight.annotation
const tooltip = undefined
const customColor = highlight.createdByMe
? undefined
: 'var(--colors-recommendedHighlightBackground)'
const customColor = highlight.color
return makeHighlightNodeAttributes(patch, id, withNote, customColor, tooltip)
}
@ -142,15 +140,17 @@ export function makeHighlightNodeAttributes(
}
const newHighlightSpan = document.createElement('span')
newHighlightSpan.className = withNote
? highlightWithNoteClassName
: highlightClassname
newHighlightSpan.className = highlightClassname
if (withNote) {
newHighlightSpan.className = `${newHighlightSpan.className} ${highlightWithNoteClassName}`
}
if (customColor) {
newHighlightSpan.className = `${newHighlightSpan.className} highlight_${customColor}`
}
newHighlightSpan.setAttribute(highlightIdAttribute, id)
customColor &&
newHighlightSpan.setAttribute(
'style',
`background-color: ${customColor} !important`
)
tooltip && newHighlightSpan.setAttribute('title', tooltip)
newHighlightSpan.appendChild(newTextNode)
lastElement = newHighlightSpan
@ -162,7 +162,7 @@ export function makeHighlightNodeAttributes(
if (withNote && lastElement) {
lastElement.classList.add('last_element')
const svg = noteImage()
const svg = noteImage(customColor)
svg.setAttribute(highlightNoteIdAttribute, id)
const ctr = document.createElement('div')

View File

@ -1,3 +1,5 @@
import { highlightColor, highlightColorVar } from '../themeUpdater'
export type SelectionAttributes = {
selection: Selection
range: Range
@ -25,7 +27,7 @@ export function getHighlightNoteButton(highlightId: string): Element[] {
)
}
export function noteImage(): SVGSVGElement {
export function noteImage(color: string | undefined): SVGSVGElement {
const svgURI = 'http://www.w3.org/2000/svg'
const svg = document.createElementNS(svgURI, 'svg')
svg.setAttribute('viewBox', '0 0 14 14')
@ -38,7 +40,7 @@ export function noteImage(): SVGSVGElement {
'd',
'M1 5.66602C1 3.7804 1 2.83759 1.58579 2.2518C2.17157 1.66602 3.11438 1.66602 5 1.66602H9C10.8856 1.66602 11.8284 1.66602 12.4142 2.2518C13 2.83759 13 3.7804 13 5.66602V7.66601C13 9.55163 13 10.4944 12.4142 11.0802C11.8284 11.666 10.8856 11.666 9 11.666H4.63014C4.49742 11.666 4.43106 11.666 4.36715 11.6701C3.92582 11.6984 3.50632 11.8722 3.17425 12.1642C3.12616 12.2065 3.07924 12.2534 2.98539 12.3473V12.3473C2.75446 12.5782 2.639 12.6937 2.55914 12.7475C1.96522 13.1481 1.15512 12.8125 1.01838 12.1093C1 12.0148 1 11.8515 1 11.5249V5.66602Z'
)
path.setAttribute('stroke', 'rgba(255, 210, 52, 0.8)')
path.setAttribute('stroke', `rgba(${highlightColorVar(color)}, 0.8)`)
path.setAttribute('stroke-width', '1.8')
path.setAttribute('stroke-linejoin', 'round')
svg.appendChild(path)

View File

@ -10,6 +10,7 @@ export const highlightFragment = gql`
prefix
suffix
patch
color
annotation
createdByMe
createdAt
@ -41,6 +42,7 @@ export type Highlight = {
updatedAt: string
sharedAt: string
labels?: Label[]
color?: string
highlightPositionPercent?: number
highlightPositionAnchorIndex?: number
}

View File

@ -15,6 +15,7 @@ export type CreateHighlightInput = {
suffix?: string
quote?: string
html?: string
color?: string
annotation?: string
patch?: string

View File

@ -40,6 +40,7 @@ export async function mergeHighlightMutation(
prefix
suffix
patch
color
createdAt
updatedAt
annotation

View File

@ -111,3 +111,37 @@ export function isDarkTheme(): boolean {
currentTheme == 'Black'
)
}
export const highlightColors = ['yellow', 'red', 'green', 'blue']
export const highlightColor = (name: string | undefined) => {
switch (name) {
case 'green':
return '#55C689'
case 'blue':
return '#6AB1FF'
case 'yellow':
return '#FFD234'
case 'orange':
return '#FEB56D'
case 'red':
return '#FB9A9A'
}
return '#FFD234'
}
export const highlightColorVar = (name: string | undefined) => {
switch (name) {
case 'green':
return 'var(--colors-highlightBackgroundGreen)'
case 'blue':
return 'var(--colors-highlightBackgroundBlue)'
case 'yellow':
return 'var(--colors-highlightBackgroundYellow)'
case 'orange':
return 'var(--colors-highlightBackgroundOrange)'
case 'red':
return 'var(--colors-highlightBackgroundRed)'
}
return 'var(--colors-highlightBackground)'
}

View File

@ -19,12 +19,29 @@
cursor: pointer;
}
.highlight_with_note {
color: var(--colors-highlightText);
background-color: rgba(var(--colors-highlightBackground), 0.35);
border-bottom: 2px rgb(var(--colors-highlightBackground)) solid;
border-radius: 2px;
cursor: pointer;
.highlight_green {
background-color: rgba(var(--colors-highlightBackgroundGreen), var(--colors-highlightBackgroundAlpha));
border-bottom: 2px rgba(var(--colors-highlightBackgroundGreen), var(--colors-highlightUnderlineAlpha)) solid;
}
.highlight_red {
background-color: rgba(var(--colors-highlightBackgroundRed), var(--colors-highlightBackgroundAlpha));
border-bottom: 2px rgba(var(--colors-highlightBackgroundRed), var(--colors-highlightUnderlineAlpha)) solid;
}
.highlight_blue {
background-color: rgba(var(--colors-highlightBackgroundBlue), var(--colors-highlightBackgroundAlpha));
border-bottom: 2px rgba(var(--colors-highlightBackgroundBlue), var(--colors-highlightUnderlineAlpha)) solid;
}
.highlight_yellow {
background-color: rgba(var(--colors-highlightBackgroundYellow), var(--colors-highlightBackgroundAlpha));
border-bottom: 2px rgba(var(--colors-highlightBackgroundYellow), var(--colors-highlightUnderlineAlpha)) solid;
}
.highlight_orange {
background-color: rgba(var(--colors-highlightBackgroundOrange), var(--colors-highlightBackgroundAlpha));
border-bottom: 2px rgba(var(--colors-highlightBackgroundOrange), var(--colors-highlightUnderlineAlpha)) solid;
}
.article-inner-css {