Merge pull request #1944 from omnivore-app/feat/notebook-improvements

Notebook improvements
This commit is contained in:
Jackson Harper 2023-04-03 16:53:56 +08:00 committed by GitHub
commit e15119e808
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 2365 additions and 958 deletions

View file

@ -1,91 +0,0 @@
import { useCallback, useState } from 'react'
import { Highlight } from '../../lib/networking/fragments/highlightFragment'
import { updateHighlightMutation } from '../../lib/networking/mutations/updateHighlightMutation'
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
import { Button } from './Button'
import { HStack, VStack } from './LayoutPrimitives'
import { StyledTextArea } from './StyledTextArea'
type HighlightNoteTextEditAreaProps = {
setIsEditing: (editing: boolean) => void
highlight: Highlight
updateHighlight: (highlight: Highlight) => void
}
export const HighlightNoteTextEditArea = (
props: HighlightNoteTextEditAreaProps
): JSX.Element => {
const [noteContent, setNoteContent] = useState(
props.highlight.annotation ?? ''
)
const handleNoteContentChange = useCallback(
(event: React.ChangeEvent<HTMLTextAreaElement>): void => {
setNoteContent(event.target.value)
},
[setNoteContent]
)
return (
<VStack css={{ width: '100%' }} key="textEditor">
<StyledTextArea
css={{
my: '10px',
minHeight: '200px',
borderRadius: '5px',
p: '10px',
width: '100%',
marginTop: '16px',
resize: 'vertical',
bg: '#EBEBEB',
color: '#3D3D3D',
}}
autoFocus
maxLength={4000}
value={noteContent}
placeholder={'Add your notes...'}
onChange={handleNoteContentChange}
/>
<HStack alignment="center" distribution="end" css={{ width: '100%' }}>
<Button
style="cancelGeneric"
css={{ mr: '$2' }}
onClick={() => {
props.setIsEditing(false)
setNoteContent(props.highlight.annotation ?? '')
}}
>
Cancel
</Button>
<Button
style="ctaDarkYellow"
onClick={async (e) => {
e.preventDefault()
try {
const result = await updateHighlightMutation({
highlightId: props.highlight.id,
annotation: noteContent,
})
console.log('result: ' + result)
if (!result) {
showErrorToast('There was an error updating your highlight.')
} else {
showSuccessToast('Note saved')
props.highlight.annotation = noteContent
props.updateHighlight(props.highlight)
}
} catch (err) {
console.log('error updating annoation', err)
showErrorToast('There was an error updating your highlight.')
}
props.setIsEditing(false)
}}
>
Save
</Button>
</HStack>
</VStack>
)
}

View file

@ -0,0 +1,443 @@
/* eslint-disable react/no-children-prop */
import {
ChangeEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { formattedShortTime } from '../../lib/dateFormatting'
import { HStack, SpanBox, VStack } from '../elements/LayoutPrimitives'
import MarkdownIt from 'markdown-it'
import MdEditor from 'react-markdown-editor-lite'
import 'react-markdown-editor-lite/lib/index.css'
import ReactMarkdown from 'react-markdown'
import throttle from 'lodash/throttle'
import { updateHighlightMutation } from '../../lib/networking/mutations/updateHighlightMutation'
import { Highlight } from '../../lib/networking/fragments/highlightFragment'
import { Button } from '../elements/Button'
import {
ModalContent,
ModalOverlay,
ModalRoot,
} from '../elements/ModalPrimitives'
import { CloseButton } from '../elements/CloseButton'
import { StyledText } from '../elements/StyledText'
const mdParser = new MarkdownIt()
type NoteSectionProps = {
placeHolder: string
mode: 'edit' | 'preview'
sizeMode: 'normal' | 'maximized'
setEditMode: (set: 'edit' | 'preview') => void
text: string | undefined
saveText: (text: string, completed: (success: boolean) => void) => void
}
export function HighlightNoteBox(props: NoteSectionProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const saveText = useCallback(
(text, updateTime) => {
props.saveText(text, (success) => {
if (success) {
setLastSaved(updateTime)
}
})
},
[props]
)
return (
<MarkdownNote
placeHolder={props.placeHolder}
mode={props.mode}
sizeMode={props.sizeMode}
setEditMode={props.setEditMode}
text={props.text}
saveText={saveText}
lastSaved={lastSaved}
fillBackground={false}
/>
)
}
type HighlightViewNoteProps = {
placeHolder: string
mode: 'edit' | 'preview'
highlight: Highlight
sizeMode: 'normal' | 'maximized'
setEditMode: (set: 'edit' | 'preview') => void
text: string | undefined
updateHighlight: (highlight: Highlight) => void
}
export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const saveText = useCallback(
(text, updateTime) => {
;(async () => {
const success = await updateHighlightMutation({
annotation: text,
highlightId: props.highlight?.id,
})
if (success) {
setLastSaved(updateTime)
props.highlight.annotation = text
props.updateHighlight(props.highlight)
}
})()
},
[props]
)
return (
<MarkdownNote
placeHolder={props.placeHolder}
mode={props.mode}
sizeMode={props.sizeMode}
setEditMode={props.setEditMode}
text={props.text}
saveText={saveText}
lastSaved={lastSaved}
fillBackground={true}
/>
)
}
type MarkdownNote = {
placeHolder: string
mode: 'edit' | 'preview'
sizeMode: 'normal' | 'maximized'
setEditMode: (set: 'edit' | 'preview') => void
text: string | undefined
fillBackground: boolean | undefined
lastSaved: Date | undefined
saveText: (text: string, updateTime: Date) => void
}
export function MarkdownNote(props: MarkdownNote): JSX.Element {
const editorRef = useRef<MdEditor | null>(null)
const [lastChanged, setLastChanged] = useState<Date | undefined>(undefined)
const [errorSaving, setErrorSaving] = useState<string | undefined>(undefined)
const saveRef = useRef(props.saveText)
useEffect(() => {
saveRef.current = props.saveText
}, [props.lastSaved, lastChanged])
const debouncedSave = useMemo<
(text: string, updateTime: Date) => void
>(() => {
const func = (text: string, updateTime: Date) => {
saveRef.current?.(text, updateTime)
}
return throttle(func, 3000)
}, [])
const handleEditorChange = useCallback(
(
data: { text: string; html: string },
event?: ChangeEvent<HTMLTextAreaElement> | undefined
) => {
if (event) {
event.preventDefault()
}
const updateTime = new Date()
setLastChanged(updateTime)
debouncedSave(data.text, updateTime)
},
[props.lastSaved, lastChanged]
)
return (
<>
{props.mode == 'edit' ? (
<VStack
css={{
width: '100%',
mt: '15px',
'.rc-md-editor': {
borderRadius: '5px',
},
'.rc-md-navigation': {
borderRadius: '5px',
borderBottomLeftRadius: '0px',
borderBottomRightRadius: '0px',
},
'.rc-md-editor .editor-container >.section': {
borderRight: 'unset',
},
'.rc-md-editor .editor-container .sec-md .input': {
padding: '10px',
borderRadius: '5px',
fontSize: '16px',
},
}}
onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.code.toLowerCase() === 'escape') {
props.setEditMode('preview')
event.preventDefault()
}
}}
>
<MdEditor
key="note-editor"
ref={editorRef}
autoFocus={true}
defaultValue={props.text}
placeholder={props.placeHolder}
view={{ menu: true, md: true, html: false }}
canView={{
menu: props.mode == 'edit',
md: true,
html: true,
both: false,
fullScreen: false,
hideMenu: false,
}}
plugins={[
'header',
'font-bold',
'font-italic',
'font-underline',
'font-strikethrough',
'list-unordered',
'list-ordered',
'block-quote',
'link',
'auto-resize',
]}
style={{
width: '100%',
height: props.sizeMode == 'normal' ? '160px' : '320px',
}}
renderHTML={(text: string) => mdParser.render(text)}
onChange={handleEditorChange}
/>
<HStack
css={{
minHeight: '15px',
width: '100%',
fontSize: '9px',
mt: '1px',
color: '$thTextSubtle',
}}
alignment="start"
distribution="start"
>
{errorSaving && (
<SpanBox
css={{
width: '100%',
fontSize: '9px',
mt: '1px',
color: 'red',
}}
>
{errorSaving}
</SpanBox>
)}
{props.lastSaved !== undefined ? (
<>
{lastChanged === props.lastSaved
? 'Saved'
: `Last saved ${formattedShortTime(
props.lastSaved.toISOString()
)}`}
</>
) : null}
{lastChanged !== props.lastSaved && (
<SpanBox
css={{
fontSize: '9px',
mt: '1px',
color: 'green',
marginLeft: 'auto',
}}
>
<Button
css={{
textDecoration: 'underline',
border: 'unset',
background: 'unset',
'&:hover': {
border: 'unset',
background: 'unset',
},
}}
onClick={(event) => {
const value = editorRef.current?.getMdValue()
if (value) {
props.saveText(value, new Date())
}
event.preventDefault()
}}
>
Save
</Button>
</SpanBox>
)}
</HStack>
</VStack>
) : (
<>
<SpanBox
css={{
p: '5px',
width: '100%',
fontSize: '15px',
borderRadius: '2px',
marginTop: props.fillBackground || !props.text ? '10px' : '0px',
paddingLeft:
props.fillBackground && props.text
? '10px'
: !props.text
? '5px'
: '0px',
paddingRight:
props.fillBackground && props.text
? '10px'
: !props.text
? '5px'
: '0px',
color: props.text ? '$thHighContrast' : '#898989',
border: props.text ? 'unset' : '1px solid $thBorderColor',
background:
props.text && props.fillBackground ? '$thBackground5' : 'unset',
'> *': {
m: '0px',
},
}}
onClick={() => props.setEditMode('edit')}
>
<ReactMarkdown children={props.text ?? props.placeHolder} />
</SpanBox>
</>
)}
</>
)
}
type MarkdownModalProps = {
placeHolder: string
mode: 'edit' | 'preview'
sizeMode: 'normal' | 'maximized'
setEditMode: (set: 'edit' | 'preview') => void
text: string | undefined
saveText: (text: string, completed: (success: boolean) => void) => void
}
export function MarkdownModal(props: MarkdownModalProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const saveText = useCallback(
(text, updateTime) => {
props.saveText(text, (success) => {
if (success) {
setLastSaved(updateTime)
}
})
},
[props]
)
const handleClose = useCallback(() => {
console.log('onOpenChange')
}, [])
return (
<ModalRoot
defaultOpen
onOpenChange={handleClose}
css={{ width: '100%', height: '100%' }}
>
<ModalOverlay css={{ width: '100%', height: '100%' }} />
<ModalContent
css={{
bg: '$grayBg',
zIndex: '30',
width: '100%',
height: '100%',
maxHeight: 'unset',
maxWidth: 'unset',
}}
>
<VStack>
<HStack
distribution="between"
alignment="center"
css={{
width: '100%',
position: 'sticky',
top: '0px',
height: '50px',
p: '20px',
bg: '$grayBg',
zIndex: 10,
}}
>
<StyledText style="modalHeadline" css={{ color: '$thTextSubtle2' }}>
Edit Note
</StyledText>
<HStack
css={{
ml: 'auto',
cursor: 'pointer',
gap: '15px',
mr: '-5px',
}}
distribution="center"
alignment="center"
>
{/* <Dropdown triggerElement={<MenuTrigger />}>
<DropdownOption
onSelect={() => {
exportHighlights()
}}
title="Export Notebook"
/>
<DropdownOption
onSelect={() => {
setShowConfirmDeleteNote(true)
}}
title="Delete Document Note"
/>
</Dropdown> */}
<CloseButton close={handleClose} />
</HStack>
</HStack>
<SpanBox css={{ padding: '20px', width: '100%', height: '100%' }}>
<MarkdownNote
placeHolder={props.placeHolder}
mode={props.mode}
sizeMode={props.sizeMode}
setEditMode={props.setEditMode}
text={props.text}
saveText={saveText}
lastSaved={lastSaved}
fillBackground={false}
/>
</SpanBox>
</VStack>
</ModalContent>
</ModalRoot>
)
}

View file

@ -1,60 +1,134 @@
import { Fragment, useMemo } from 'react'
/* eslint-disable react/no-children-prop */
import { BookOpen, PencilLine } from 'phosphor-react'
import { useState } from 'react'
import type { Highlight } from '../../lib/networking/fragments/highlightFragment'
import { LabelChip } from '../elements/LabelChip'
import { Box, VStack, Blockquote, SpanBox } from '../elements/LayoutPrimitives'
import { StyledText } from '../elements/StyledText'
import {
Box,
VStack,
Blockquote,
SpanBox,
HStack,
} from '../elements/LayoutPrimitives'
import { styled } from '../tokens/stitches.config'
import { HighlightViewNote, MarkdownModal } from './HighlightNotes'
import ReactMarkdown from 'react-markdown'
type HighlightViewProps = {
highlight: Highlight
author?: string
title?: string
scrollToHighlight?: (arg: string) => void
updateHighlight: (highlight: Highlight) => void
}
const StyledQuote = styled(Blockquote, {
margin: '0px 0px 0px 0px',
fontSize: '18px',
lineHeight: '27px',
color: '$grayText',
padding: '0px 16px',
borderLeft: '2px solid $omnivoreCtaYellow',
})
export function HighlightView(props: HighlightViewProps): JSX.Element {
const lines = useMemo(
() => props.highlight.quote.split('\n'),
[props.highlight.quote]
)
const [noteMode, setNoteMode] = useState<'preview' | 'edit'>('preview')
return (
<VStack css={{ width: '100%', boxSizing: 'border-box' }}>
<StyledQuote
onClick={() => {
if (props.scrollToHighlight) {
props.scrollToHighlight(props.highlight.id)
}
<HStack
css={{
width: '100%',
height: '100%',
alignItems: 'stretch',
}}
>
<VStack css={{ minHeight: '100%', width: '10px' }}>
<Box
css={{
mt: '5px',
width: '10px',
height: '10px',
background: '#FFD234',
borderRadius: '7px',
}}
/>
<Box
css={{
width: '2px',
flexGrow: '1',
background: '#FFD234',
marginLeft: '4px',
flex: '1',
marginBottom: '10px',
}}
/>
</VStack>
<VStack
css={{
width: '100%',
padding: '0px',
paddingLeft: '15px',
}}
>
<SpanBox css={{ p: '1px', borderRadius: '2px' }}>
{lines.map((line: string, index: number) => (
<Fragment key={index}>
{line}
{index !== lines.length - 1 && (
<>
<br />
<br />
</>
)}
</Fragment>
<StyledQuote>
<SpanBox
css={{
'> *': {
m: '0px',
},
fontSize: '15px',
lineHeight: 1.5,
color: '$grayText',
img: {
display: 'block',
margin: '0.5em auto !important',
maxWidth: '100% !important',
height: 'auto',
},
}}
>
<ReactMarkdown children={props.highlight.quote ?? ''} />
</SpanBox>
</StyledQuote>
<Box css={{ display: 'block', pt: '5px' }}>
{props.highlight.labels?.map(({ name, color }, index) => (
<LabelChip key={index} text={name || ''} color={color} />
))}
</SpanBox>
</StyledQuote>
<Box css={{ display: 'block', pt: '16px' }}>
{props.highlight.labels?.map(({ name, color }, index) => (
<LabelChip key={index} text={name || ''} color={color} />
))}
</Box>
</VStack>
</Box>
<HStack
css={{ width: '100%', height: '100%', pt: '15px' }}
alignment="start"
distribution="start"
>
<HighlightViewNote
text={props.highlight.annotation}
placeHolder="Add notes to this highlight..."
highlight={props.highlight}
sizeMode={'normal'}
mode={noteMode}
setEditMode={setNoteMode}
updateHighlight={props.updateHighlight}
/>
<SpanBox
css={{
lineHeight: '1',
marginLeft: '20px',
marginTop: '15px',
cursor: 'pointer',
borderRadius: '1000px',
'&:hover': {
background: '#EBEBEB',
},
}}
onClick={(event) => {
setNoteMode(noteMode == 'preview' ? 'edit' : 'preview')
event.preventDefault()
}}
>
{noteMode === 'edit' ? (
<BookOpen size={15} color="#898989" />
) : (
<PencilLine size={15} color="#898989" />
)}
</SpanBox>
</HStack>
</VStack>
</HStack>
)
}

View file

@ -1,4 +1,4 @@
import { Box, VStack, HStack } from '../../elements/LayoutPrimitives'
import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives'
import { useCallback, useMemo, useState } from 'react'
import { CaretDown, CaretUp } from 'phosphor-react'
import { MetaStyle, timeAgo, TitleStyle } from './LibraryCardStyles'
@ -7,9 +7,9 @@ import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery
import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { Button } from '../../elements/Button'
import { theme } from '../../tokens/stitches.config'
import { HighlightItem } from '../../templates/homeFeed/HighlightItem'
import { getHighlightLocation } from '../../templates/article/NotebookModal'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { HighlightView } from '../HighlightView'
export const GridSeparator = styled(Box, {
height: '1px',
@ -46,21 +46,23 @@ export function LibraryHighlightGridCard(
return []
}
return props.item.highlights.sort((a: Highlight, b: Highlight) => {
if (a.highlightPositionPercent && b.highlightPositionPercent) {
return sorted(a.highlightPositionPercent, b.highlightPositionPercent)
}
// We do this in a try/catch because it might be an invalid diff
// With PDF it will definitely be an invalid diff.
try {
const aPos = getHighlightLocation(a.patch)
const bPos = getHighlightLocation(b.patch)
if (aPos && bPos) {
return sorted(aPos, bPos)
return props.item.highlights
.filter((h) => h.type === 'HIGHLIGHT')
.sort((a: Highlight, b: Highlight) => {
if (a.highlightPositionPercent && b.highlightPositionPercent) {
return sorted(a.highlightPositionPercent, b.highlightPositionPercent)
}
} catch {}
return a.createdAt.localeCompare(b.createdAt)
})
// We do this in a try/catch because it might be an invalid diff
// With PDF it will definitely be an invalid diff.
try {
const aPos = getHighlightLocation(a.patch)
const bPos = getHighlightLocation(b.patch)
if (aPos && bPos) {
return sorted(aPos, bPos)
}
} catch {}
return a.createdAt.localeCompare(b.createdAt)
})
}, [props.item.highlights])
return (
@ -121,17 +123,20 @@ export function LibraryHighlightGridCard(
<>
<GridSeparator css={{ width: '100%' }} />
<VStack
css={{ height: '100%', width: '100%' }}
css={{ height: '100%', width: '100%', mt: '20px' }}
distribution="start"
>
{sortedHighlights.map((highlight) => (
<HighlightItem
key={highlight.id}
viewer={props.viewer}
item={props.item}
highlight={highlight}
deleteHighlight={props.deleteHighlight}
/>
<SpanBox key={`hv-${highlight.id}`}>
<HighlightView
key={highlight.id}
highlight={highlight}
updateHighlight={(highlight) => {
console.log('updated highlight: ', highlight)
}}
/>
<SpanBox css={{ height: '35px' }} />
</SpanBox>
))}
</VStack>
</>

View file

@ -18,14 +18,15 @@ import { LabelChip } from '../../elements/LabelChip'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { Recommendation } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { Avatar } from '../../elements/Avatar'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
type ArticleContainerProps = {
viewer: UserBasicData
article: ArticleAttributes
labels: Label[]
articleMutations: ArticleMutations
isAppleAppEmbed: boolean
highlightBarDisabled: boolean
highlightsBaseURL: string
margin?: number
fontSize?: number
fontFamily?: string
@ -107,15 +108,12 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
const [showReportIssuesModal, setShowReportIssuesModal] = useState(false)
const [fontSize, setFontSize] = useState(props.fontSize ?? 20)
// iOS app embed can overide the original margin and line height
const [maxWidthPercentageOverride, setMaxWidthPercentageOverride] = useState<
number | null
>(null)
const [lineHeightOverride, setLineHeightOverride] = useState<number | null>(
null
)
const [fontFamilyOverride, setFontFamilyOverride] = useState<string | null>(
null
)
const [maxWidthPercentageOverride, setMaxWidthPercentageOverride] =
useState<number | null>(null)
const [lineHeightOverride, setLineHeightOverride] =
useState<number | null>(null)
const [fontFamilyOverride, setFontFamilyOverride] =
useState<string | null>(null)
const [highContrastText, setHighContrastText] = useState(
props.highContrastText ?? false
)
@ -388,13 +386,14 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
<Box css={{ height: '100px' }} />
</Box>
<HighlightsLayer
viewer={props.viewer}
item={props.article}
scrollToHighlight={highlightHref}
highlights={props.article.highlights}
articleTitle={props.article.title}
articleAuthor={props.article.author ?? ''}
articleId={props.article.id}
isAppleAppEmbed={props.isAppleAppEmbed}
highlightsBaseURL={props.highlightsBaseURL}
highlightBarDisabled={props.highlightBarDisabled}
showHighlightsModal={props.showHighlightsModal}
setShowHighlightsModal={props.setShowHighlightsModal}

View file

@ -1,37 +0,0 @@
import { Box } from '../../elements/LayoutPrimitives'
import { theme } from '../../tokens/stitches.config'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { HighlightView } from '../../patterns/HighlightView'
type PageCoordinates = {
pageX: number
pageY: number
}
type HighlightHoverCardProps = {
highlight: Highlight
anchorCoordinates: PageCoordinates
}
export function HighlightHoverCard(
props: HighlightHoverCardProps
): JSX.Element {
return (
<Box
css={{
width: '100%',
maxWidth: '330px',
// height: '48px',
position: 'absolute',
background: '$grayBg',
borderRadius: '4px',
border: '1px solid $grayBorder',
boxShadow: theme.shadows.cardBoxShadow.toString(),
left: props.anchorCoordinates.pageX,
top: props.anchorCoordinates.pageY,
}}
>
<HighlightView highlight={props.highlight} />
</Box>
)
}

View file

@ -1,92 +0,0 @@
import {
ModalRoot,
ModalContent,
ModalOverlay,
} from './../../elements/ModalPrimitives'
import { Box, HStack } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { StyledText } from '../../elements/StyledText'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { HighlightView } from '../../patterns/HighlightView'
import { useCallback, useState } from 'react'
import { StyledTextArea } from '../../elements/StyledTextArea'
type HighlightPostToFeedModalProps = {
highlight: Highlight
author: string
title: string
onCommit: (highlight: Highlight, comment: string) => void
onOpenChange: (open: boolean) => void
}
export function HighlightPostToFeedModal(
props: HighlightPostToFeedModalProps
): JSX.Element {
const [comment, setComment] = useState('')
const handleCommentChange = useCallback(
(event: React.ChangeEvent<HTMLTextAreaElement>): void => {
setComment(event.target.value)
},
[setComment]
)
const postHighlight = useCallback(async () => {
props.onCommit(props.highlight, comment)
props.onOpenChange(false)
}, [comment, props])
return (
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
<ModalOverlay />
<ModalContent
onPointerDownOutside={(event) => {
event.preventDefault()
}}
css={{ overflow: 'auto' }}
>
<Box
css={{
display: 'grid',
gridTemplateColumns: '1fr auto 1fr',
px: '$2',
}}
>
<Button
style="ctaSecondary"
onClick={() => {
props.onOpenChange(false)
}}
css={{ justifySelf: 'start' }}
>
Cancel
</Button>
<HStack alignment="center">
<StyledText>Post Highlight</StyledText>
</HStack>
<Button
style="ctaSecondary"
onClick={postHighlight}
css={{ justifySelf: 'end' }}
>
Post
</Button>
</Box>
<HighlightView {...props} />
<StyledTextArea
css={{
mt: '$2',
width: '95%',
p: '$1',
minHeight: '$6',
}}
autoFocus
placeholder={'Leave comment (optional)'}
value={comment}
onChange={handleCommentChange}
maxLength={1000}
/>
</ModalContent>
</ModalRoot>
)
}

View file

@ -0,0 +1,66 @@
import { useState } from 'react'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import {
LibraryItem,
ReadableItem,
} from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { HighlightView } from '../../patterns/HighlightView'
import { HighlightsMenu } from '../homeFeed/HighlightItem'
type HighlightViewItemProps = {
viewer: UserBasicData
item: ReadableItem
highlight: Highlight
viewInReader: (highlightId: string) => void
deleteHighlightAction: () => void
updateHighlight: (highlight: Highlight) => void
setSetLabelsTarget: (highlight: Highlight) => void
setShowConfirmDeleteHighlightId: (id: string | undefined) => void
}
export function HighlightViewItem(props: HighlightViewItemProps): JSX.Element {
const [hover, setHover] = useState(false)
return (
<HStack
css={{ width: '100%', py: '20px' }}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
<VStack css={{ width: '100%' }}>
<HighlightView
highlight={props.highlight}
updateHighlight={props.updateHighlight}
/>
<SpanBox css={{ mb: '15px' }} />
</VStack>
<SpanBox
css={{
marginLeft: 'auto',
width: '20px',
visibility: hover ? 'unset' : 'hidden',
'@media (hover: none)': {
visibility: 'unset',
},
}}
>
<HighlightsMenu
item={props.item}
viewer={props.viewer}
highlight={props.highlight}
viewInReader={props.viewInReader}
setLabelsTarget={props.setSetLabelsTarget}
setShowConfirmDeleteHighlightId={
props.setShowConfirmDeleteHighlightId
}
/>
</SpanBox>
</HStack>
)
}

View file

@ -26,17 +26,25 @@ import { isTouchScreenDevice } from '../../../lib/deviceType'
import { SetLabelsModal } from './SetLabelsModal'
import { setLabelsForHighlight } from '../../../lib/networking/mutations/setLabelsForHighlight'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { useRouter } from 'next/router'
import { MarkdownModal } from '../../patterns/HighlightNotes'
type HighlightsLayerProps = {
viewer: UserBasicData
item: ReadableItem
highlights: Highlight[]
articleId: string
articleTitle: string
articleAuthor: string
isAppleAppEmbed: boolean
highlightBarDisabled: boolean
showHighlightsModal: boolean
highlightsBaseURL: string
scrollToHighlight: MutableRefObject<string | null>
setShowHighlightsModal: React.Dispatch<React.SetStateAction<boolean>>
articleMutations: ArticleMutations
}
@ -59,6 +67,7 @@ interface SpeakingSectionEvent extends Event {
}
export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
const router = useRouter()
const [highlights, setHighlights] = useState(props.highlights)
const [highlightModalAction, setHighlightModalAction] =
useState<HighlightActionProps>({ highlightModalAction: 'none' })
@ -68,15 +77,13 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
>([])
const focusedHighlightMousePos = useRef({ pageX: 0, pageY: 0 })
const [focusedHighlight, setFocusedHighlight] = useState<
Highlight | undefined
>(undefined)
const [focusedHighlight, setFocusedHighlight] =
useState<Highlight | undefined>(undefined)
const [selectionData, setSelectionData] = useSelection(highlightLocations)
const [labelsTarget, setLabelsTarget] = useState<Highlight | undefined>(
undefined
)
const [labelsTarget, setLabelsTarget] =
useState<Highlight | undefined>(undefined)
const canShareNative = useCanShareNative()
@ -121,14 +128,16 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
// Load the highlights
useEffect(() => {
const res: HighlightLocation[] = []
highlights.forEach((highlight) => {
try {
const offset = makeHighlightStartEndOffset(highlight)
res.push(offset)
} catch (err) {
console.error(err)
}
})
highlights
.filter((h) => h.type == 'HIGHLIGHT')
.forEach((highlight) => {
try {
const offset = makeHighlightStartEndOffset(highlight)
res.push(offset)
} catch (err) {
console.error(err)
}
})
setHighlightLocations(res)
// If we were given an initial highlight to scroll to we do
@ -139,7 +148,10 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
`[omnivore-highlight-id="${props.scrollToHighlight.current}"]`
)
if (anchorElement) {
anchorElement.scrollIntoView({ behavior: 'auto' })
anchorElement.scrollIntoView({
block: 'center',
behavior: 'auto',
})
}
}
}, [highlights, setHighlightLocations, props.scrollToHighlight])
@ -179,23 +191,20 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
[highlights, highlightLocations]
)
const handleNativeShare = useCallback(
(highlightID: string) => {
navigator
?.share({
title: props.articleTitle,
url: `${props.highlightsBaseURL}/${highlightID}`,
})
.then(() => {
setFocusedHighlight(undefined)
})
.catch((error) => {
console.log(error)
setFocusedHighlight(undefined)
})
},
[props.articleTitle, props.highlightsBaseURL]
)
// const handleNativeShare = useCallback((highlightID: string) => {
// // navigator
// // ?.share({
// // title: props.articleTitle,
// // url: `${props.highlightsBaseURL}/${highlightID}`,
// // })
// // .then(() => {
// // setFocusedHighlight(undefined)
// // })
// // .catch((error) => {
// // console.log(error)
// // setFocusedHighlight(undefined)
// // })
// }, [])
const openNoteModal = useCallback(
(inputs: HighlightActionProps) => {
@ -280,7 +289,6 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
}
},
[
handleNativeShare,
highlights,
openNoteModal,
props.articleId,
@ -356,6 +364,24 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
[highlights, highlightLocations, openNoteModal]
)
const handleCloseNotebook = useCallback(
(updatedHighlights: Highlight[], deletedHighlights: Highlight[]) => {
props.setShowHighlightsModal(false)
setHighlights(updatedHighlights)
removeHighlights(
deletedHighlights.map((h) => h.id),
highlightLocations
)
updatedHighlights.forEach((h) => {
updateHighlightsCallback(h)
})
},
[highlights, highlightLocations]
)
useEffect(() => {
if (typeof window === 'undefined') {
return
@ -388,37 +414,37 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
})
}
break
case 'share':
if (props.isAppleAppEmbed) {
window?.webkit?.messageHandlers.highlightAction?.postMessage({
actionID: 'share',
highlightID: focusedHighlight?.id,
})
}
// case 'share':
// if (props.isAppleAppEmbed) {
// window?.webkit?.messageHandlers.highlightAction?.postMessage({
// actionID: 'share',
// highlightID: focusedHighlight?.id,
// })
// }
window?.AndroidWebKitMessenger?.handleIdentifiableMessage(
'shareHighlight',
JSON.stringify({
highlightID: focusedHighlight?.id,
})
)
// window?.AndroidWebKitMessenger?.handleIdentifiableMessage(
// 'shareHighlight',
// JSON.stringify({
// highlightID: focusedHighlight?.id,
// })
// )
if (focusedHighlight) {
if (canShareNative) {
handleNativeShare(focusedHighlight.shortId)
} else {
setHighlightModalAction({
highlight: focusedHighlight,
highlightModalAction: 'share',
})
}
} else {
await createHighlightCallback('share')
}
break
case 'unshare':
console.log('unshare')
break // TODO: implement -- need to show confirmation dialog
// if (focusedHighlight) {
// if (canShareNative) {
// handleNativeShare(focusedHighlight.shortId)
// } else {
// setHighlightModalAction({
// highlight: focusedHighlight,
// highlightModalAction: 'share',
// })
// }
// } else {
// await createHighlightCallback('share')
// }
// break
// case 'unshare':
// console.log('unshare')
// break // TODO: implement -- need to show confirmation dialog
case 'setHighlightLabels':
if (props.isAppleAppEmbed) {
window?.webkit?.messageHandlers.highlightAction?.postMessage({
@ -434,7 +460,6 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
[
createHighlightCallback,
focusedHighlight,
handleNativeShare,
openNoteModal,
props.highlightBarDisabled,
props.isAppleAppEmbed,
@ -498,7 +523,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
}
const copy = async () => {
if (focusedHighlight) {
if (focusedHighlight && focusedHighlight.quote) {
if (window.AndroidWebKitMessenger) {
window.AndroidWebKitMessenger.handleIdentifiableMessage(
'writeToClipboard',
@ -647,12 +672,28 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
if (props.showHighlightsModal) {
return (
<NotebookModal
viewer={props.viewer}
item={props.item}
highlights={highlights}
onOpenChange={() => props.setShowHighlightsModal(false)}
deleteHighlightAction={(highlightId: string) => {
removeHighlightCallback(highlightId)
onClose={handleCloseNotebook}
viewHighlightInReader={(highlightId) => {
// The timeout here is a bit of a hack to work around rerendering
setTimeout(() => {
const target = document.querySelector(
`[omnivore-highlight-id="${highlightId}"]`
)
target?.scrollIntoView({
block: 'center',
behavior: 'auto',
})
}, 1)
history.replaceState(
undefined,
window.location.href,
`#${highlightId}`
)
props.setShowHighlightsModal(false)
}}
updateHighlight={updateHighlightsCallback}
/>
)
}

View file

@ -0,0 +1,482 @@
import { Box, HStack, VStack, SpanBox } from '../../elements/LayoutPrimitives'
import { StyledText } from '../../elements/StyledText'
import { theme } from '../../tokens/stitches.config'
import type { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { BookOpen, PencilLine, X } from 'phosphor-react'
import { SetLabelsModal } from './SetLabelsModal'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { setLabelsForHighlight } from '../../../lib/networking/mutations/setLabelsForHighlight'
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import { diff_match_patch } from 'diff-match-patch'
import { highlightsAsMarkdown } from '../homeFeed/HighlightItem'
import 'react-markdown-editor-lite/lib/index.css'
import { createHighlightMutation } from '../../../lib/networking/mutations/createHighlightMutation'
import { v4 as uuidv4 } from 'uuid'
import { nanoid } from 'nanoid'
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
import { HighlightNoteBox, MarkdownNote } from '../../patterns/HighlightNotes'
import { HighlightViewItem } from './HighlightViewItem'
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
import { TrashIcon } from '../../elements/images/TrashIcon'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import {
LibraryItem,
ReadableItem,
} from '../../../lib/networking/queries/useGetLibraryItemsQuery'
type NotebookProps = {
viewer: UserBasicData
item: ReadableItem
highlights: Highlight[]
sizeMode: 'normal' | 'maximized'
viewInReader: (highlightId: string) => void
onAnnotationsChanged?: (
highlights: Highlight[],
deletedAnnotations: Highlight[]
) => void
}
export const getHighlightLocation = (patch: string): number | undefined => {
const dmp = new diff_match_patch()
const patches = dmp.patch_fromText(patch)
return patches[0].start1 || undefined
}
type AnnotationInfo = {
loaded: boolean
note: Highlight | undefined
noteId: string
allAnnotations: Highlight[]
deletedAnnotations: Highlight[]
}
export function Notebook(props: NotebookProps): JSX.Element {
const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] =
useState<undefined | string>(undefined)
const [labelsTarget, setLabelsTarget] =
useState<Highlight | undefined>(undefined)
const [showConfirmDeleteNote, setShowConfirmDeleteNote] = useState(false)
const [notesEditMode, setNotesEditMode] =
useState<'edit' | 'preview'>('preview')
const [, updateState] = useState({})
const annotationsReducer = (
state: AnnotationInfo,
action: {
type: string
allHighlights?: Highlight[]
note?: Highlight | undefined
updateHighlight?: Highlight | undefined
deleteHighlightId?: string | undefined
}
) => {
switch (action.type) {
case 'RESET': {
const note = action.allHighlights?.find((h) => h.type == 'NOTE')
return {
...state,
loaded: true,
note: note,
noteId: note?.id ?? state.noteId,
allAnnotations: [...(action.allHighlights ?? [])],
}
}
case 'CREATE_NOTE': {
if (!action.note) {
throw new Error('No note on CREATE_NOTE action')
}
return {
...state,
note: action.note,
noteId: action.note.id,
allAnnotations: [...state.allAnnotations, action.note],
}
}
case 'DELETE_NOTE': {
// If there is no note to delete, just make sure we have cleared out the note
const noteId = action.note?.id
if (!action.note?.id) {
return {
...state,
node: undefined,
noteId: uuidv4(),
}
}
const idx = state.allAnnotations.findIndex((h) => h.id === noteId)
return {
...state,
note: undefined,
noteId: uuidv4(),
allAnnotations: state.allAnnotations.splice(idx, 1),
}
}
case 'DELETE_HIGHLIGHT': {
const highlightId = action.deleteHighlightId
console.log(' DELETE_HIGHLIGHT: ', highlightId)
if (!highlightId) {
throw new Error('No highlightId for delete action.')
}
const idx = state.allAnnotations.findIndex((h) => h.id === highlightId)
if (idx < 0) {
return { ...state }
}
const deleted = state.deletedAnnotations
deleted.push(state.allAnnotations[idx])
return {
...state,
deletedAnnotations: deleted,
allAnnotations: state.allAnnotations.splice(idx, 1),
}
}
case 'UPDATE_HIGHLIGHT': {
const highlight = action.updateHighlight
if (!highlight) {
throw new Error('No highlightId for delete action.')
}
const idx = state.allAnnotations.findIndex((h) => h.id === highlight.id)
if (idx !== -1) {
state.allAnnotations[idx] = highlight
}
return {
...state,
}
}
default:
return state
}
}
const [annotations, dispatchAnnotations] = useReducer(annotationsReducer, {
loaded: false,
note: undefined,
noteId: uuidv4(),
allAnnotations: [],
deletedAnnotations: [],
})
useEffect(() => {
dispatchAnnotations({
type: 'RESET',
allHighlights: props.highlights,
})
}, [props.highlights])
useEffect(() => {
if (props.onAnnotationsChanged) {
props.onAnnotationsChanged(
annotations.allAnnotations,
annotations.deletedAnnotations
)
}
}, [annotations])
const deleteDocumentNote = useCallback(() => {
const note = annotations.note
if (!note) {
showErrorToast('No note found')
return
}
;(async () => {
try {
const result = await deleteHighlightMutation(note.id)
if (!result) {
throw new Error()
}
showSuccessToast('Note deleted')
dispatchAnnotations({
note,
type: 'DELETE_NOTE',
})
} catch (err) {
console.log('error deleting note', err)
showErrorToast('Error deleting note')
}
})()
}, [annotations])
const sortedHighlights = useMemo(() => {
const sorted = (a: number, b: number) => {
if (a < b) {
return -1
}
if (a > b) {
return 1
}
return 0
}
return annotations.allAnnotations
.filter((h) => h.type === 'HIGHLIGHT')
.sort((a: Highlight, b: Highlight) => {
if (a.highlightPositionPercent && b.highlightPositionPercent) {
return sorted(a.highlightPositionPercent, b.highlightPositionPercent)
}
// We do this in a try/catch because it might be an invalid diff
// With PDF it will definitely be an invalid diff.
try {
const aPos = getHighlightLocation(a.patch)
const bPos = getHighlightLocation(b.patch)
if (aPos && bPos) {
return sorted(aPos, bPos)
}
} catch {}
return a.createdAt.localeCompare(b.createdAt)
})
}, [annotations])
const handleSaveNoteText = useCallback(
(text, cb: (success: boolean) => void) => {
if (!annotations.loaded) {
// We haven't loaded the user's annotations yet, so we can't
// find or create their highlight note.
return
}
if (!annotations.note) {
const noteId = annotations.noteId
;(async () => {
const success = await createHighlightMutation({
id: noteId,
shortId: nanoid(8),
type: 'NOTE',
articleId: props.item.id,
annotation: text,
})
console.log('success creating annotation note: ', success)
if (success) {
dispatchAnnotations({
type: 'CREATE_NOTE',
note: success,
})
}
cb(!!success)
})()
return
}
if (annotations.note) {
const note = annotations.note
;(async () => {
const success = await updateHighlightMutation({
highlightId: note.id,
annotation: text,
})
console.log('success updating annotation note: ', success)
if (success) {
note.annotation = text
dispatchAnnotations({
type: 'UPDATE_NOTE',
note: note,
})
}
cb(!!success)
})()
return
}
},
[annotations, props.item]
)
return (
<VStack
distribution="start"
css={{ height: '100%', width: '100%', p: '20px' }}
>
<TitledSection
title="ARTICLE NOTES"
editMode={notesEditMode == 'edit'}
setEditMode={(edit) => setNotesEditMode(edit ? 'edit' : 'preview')}
/>
<HighlightNoteBox
mode={notesEditMode}
sizeMode={props.sizeMode}
setEditMode={setNotesEditMode}
text={annotations.note?.annotation}
placeHolder="Add notes to this document..."
saveText={handleSaveNoteText}
/>
<SpanBox css={{ mt: '10px', mb: '25px' }} />
<Box css={{ width: '100%' }}>
<TitledSection title="HIGHLIGHTS" />
{sortedHighlights.map((highlight) => (
<HighlightViewItem
key={highlight.id}
item={props.item}
viewer={props.viewer}
highlight={highlight}
viewInReader={props.viewInReader}
setSetLabelsTarget={setLabelsTarget}
setShowConfirmDeleteHighlightId={setShowConfirmDeleteHighlightId}
deleteHighlightAction={() => {
dispatchAnnotations({
type: 'DELETE_HIGHLIGHT',
deleteHighlightId: highlight.id,
})
}}
updateHighlight={() => {
dispatchAnnotations({
type: 'UPDATE_HIGHLIGHT',
updateHighlight: highlight,
})
}}
/>
))}
{sortedHighlights.length === 0 && (
<Box
css={{
mt: '15px',
width: '100%',
fontSize: '9px',
color: '$thTextSubtle',
alignItems: 'center',
justifyContent: 'center',
mb: '100px',
}}
>
You have not added any highlights to this document.
</Box>
)}
<Box
css={{
'@mdDown': {
height: '320px',
width: '100%',
background: 'transparent',
},
}}
/>
</Box>
{showConfirmDeleteHighlightId && (
<ConfirmationModal
message={'Are you sure you want to delete this highlight?'}
onAccept={() => {
;(async () => {
const success = await deleteHighlightMutation(
showConfirmDeleteHighlightId
)
console.log(' ConfirmationModal::DeleteHighlight', success)
if (success) {
dispatchAnnotations({
type: 'DELETE_HIGHLIGHT',
deleteHighlightId: showConfirmDeleteHighlightId,
})
showSuccessToast('Highlight deleted.')
} else {
showErrorToast('Error deleting highlight')
}
})()
setShowConfirmDeleteHighlightId(undefined)
}}
onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)}
icon={
<TrashIcon
size={40}
strokeColor={theme.colors.grayTextContrast.toString()}
/>
}
/>
)}
{labelsTarget && (
<SetLabelsModal
provider={labelsTarget}
onOpenChange={function (open: boolean): void {
setLabelsTarget(undefined)
}}
onLabelsUpdated={function (labels: Label[]): void {
updateState({})
}}
save={function (labels: Label[]): Promise<Label[] | undefined> {
const result = setLabelsForHighlight(
labelsTarget.id,
labels.map((label) => label.id)
)
return result
}}
/>
)}
{showConfirmDeleteNote && (
<ConfirmationModal
message="Are you sure you want to delete the note from this document?"
acceptButtonLabel="Delete"
onAccept={() => {
deleteDocumentNote()
setShowConfirmDeleteNote(false)
}}
onOpenChange={() => setShowConfirmDeleteNote(false)}
/>
)}
</VStack>
)
}
type TitledSectionProps = {
title: string
editMode?: boolean
setEditMode?: (set: boolean) => void
}
function TitledSection(props: TitledSectionProps): JSX.Element {
return (
<>
<HStack
css={{ width: '100%', borderBottom: '1px solid $thBorderColor' }}
alignment="start"
distribution="start"
>
<StyledText
css={{
fontFamily: '$display',
fontStyle: 'normal',
fontWeight: '700',
fontSize: '12px',
lineHeight: '20px',
color: '#898989',
marginBottom: '1px',
}}
>
{props.title}
</StyledText>
{props.setEditMode && (
<SpanBox
css={{
marginLeft: 'auto',
justifyContent: 'end',
lineHeight: '1',
alignSelf: 'end',
padding: '2px',
cursor: 'pointer',
borderRadius: '1000px',
'&:hover': {
background: '#EBEBEB',
},
}}
onClick={(event) => {
if (props.setEditMode) {
props.setEditMode(!props.editMode)
}
event.preventDefault()
}}
>
{props.editMode ? (
<BookOpen size={15} color="#898989" />
) : (
<PencilLine size={15} color="#898989" />
)}
</SpanBox>
)}
</HStack>
</>
)
}

View file

@ -2,37 +2,33 @@ import {
ModalRoot,
ModalOverlay,
ModalContent,
ModalTitleBar,
} from '../../elements/ModalPrimitives'
import { Box, HStack, VStack, SpanBox } from '../../elements/LayoutPrimitives'
import { HStack, SpanBox } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { StyledText } from '../../elements/StyledText'
import { TrashIcon } from '../../elements/images/TrashIcon'
import { theme } from '../../tokens/stitches.config'
import type { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { HighlightView } from '../../patterns/HighlightView'
import { useCallback, useMemo, useState } from 'react'
import { StyledTextArea } from '../../elements/StyledTextArea'
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
import { DotsThree } from 'phosphor-react'
import { useCallback, useState } from 'react'
import { ArrowsIn, ArrowsOut, X } from 'phosphor-react'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { SetLabelsModal } from './SetLabelsModal'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { setLabelsForHighlight } from '../../../lib/networking/mutations/setLabelsForHighlight'
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import { diff_match_patch } from 'diff-match-patch'
import { HighlightNoteTextEditArea } from '../../elements/HighlightNoteTextEditArea'
import { CloseButton } from '../../elements/CloseButton'
import { MenuTrigger } from '../../elements/MenuTrigger'
import { highlightsAsMarkdown, HighlightsMenu } from '../homeFeed/HighlightItem'
import { highlightsAsMarkdown } from '../homeFeed/HighlightItem'
import 'react-markdown-editor-lite/lib/index.css'
import { Notebook } from './Notebook'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { MarkdownNote } from '../../patterns/HighlightNotes'
type NotebookModalProps = {
viewer: UserBasicData
item: ReadableItem
highlights: Highlight[]
scrollToHighlight?: (arg: string) => void
updateHighlight: (highlight: Highlight) => void
deleteHighlightAction?: (highlightId: string) => void
onOpenChange: (open: boolean) => void
viewHighlightInReader: (arg: string) => void
onClose: (highlights: Highlight[], deletedAnnotations: Highlight[]) => void
}
export const getHighlightLocation = (patch: string): number | undefined => {
@ -42,252 +38,179 @@ export const getHighlightLocation = (patch: string): number | undefined => {
}
export function NotebookModal(props: NotebookModalProps): JSX.Element {
const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] =
useState<undefined | string>(undefined)
const [labelsTarget, setLabelsTarget] = useState<Highlight | undefined>(
undefined
const [sizeMode, setSizeMode] = useState<'normal' | 'maximized'>('normal')
const [showConfirmDeleteNote, setShowConfirmDeleteNote] = useState(false)
const [allAnnotations, setAllAnnotations] =
useState<Highlight[] | undefined>(undefined)
const [deletedAnnotations, setDeletedAnnotations] =
useState<Highlight[] | undefined>(undefined)
const handleClose = useCallback(() => {
props.onClose(allAnnotations ?? [], deletedAnnotations ?? [])
}, [allAnnotations, deletedAnnotations])
const handleAnnotationsChange = useCallback(
(allAnnotations, deletedAnnotations) => {
setAllAnnotations(allAnnotations)
setDeletedAnnotations(deletedAnnotations)
},
[]
)
const [, updateState] = useState({})
const exportHighlights = useCallback(() => {
;(async () => {
if (!props.highlights) {
if (!allAnnotations) {
showErrorToast('No highlights to export')
return
}
const markdown = highlightsAsMarkdown(props.highlights)
const markdown = highlightsAsMarkdown(allAnnotations)
await navigator.clipboard.writeText(markdown)
showSuccessToast('Highlight copied')
})()
}, [props.highlights])
}, [allAnnotations])
const sortedHighlights = useMemo(() => {
const sorted = (a: number, b: number) => {
if (a < b) {
return -1
}
if (a > b) {
return 1
}
return 0
}
return props.highlights.sort((a: Highlight, b: Highlight) => {
if (a.highlightPositionPercent && b.highlightPositionPercent) {
return sorted(a.highlightPositionPercent, b.highlightPositionPercent)
}
// We do this in a try/catch because it might be an invalid diff
// With PDF it will definitely be an invalid diff.
try {
const aPos = getHighlightLocation(a.patch)
const bPos = getHighlightLocation(b.patch)
if (aPos && bPos) {
return sorted(aPos, bPos)
}
} catch {}
return a.createdAt.localeCompare(b.createdAt)
})
}, [props.highlights])
const viewInReader = useCallback(
(highlightId) => {
props.viewHighlightInReader(highlightId)
handleClose()
},
[props, handleClose]
)
return (
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
<ModalRoot defaultOpen onOpenChange={handleClose}>
<ModalOverlay />
<ModalContent
onPointerDownOutside={(event) => {
onInteractOutside={(event) => {
event.preventDefault()
props.onOpenChange(false)
}}
css={{ overflow: 'auto', px: '24px' }}
css={{
overflow: 'auto',
height: sizeMode === 'normal' ? 'unset' : '100%',
maxWidth: sizeMode === 'normal' ? '640px' : '100%',
minHeight: sizeMode === 'normal' ? '525px' : 'unset',
'@mdDown': {
top: '20px',
width: '100%',
height: '100%',
maxHeight: 'unset',
transform: 'translate(-50%)',
},
}}
>
<VStack distribution="start" css={{ height: '100%' }}>
<HStack
distribution="between"
alignment="center"
css={{
width: '100%',
position: 'sticky',
top: '0px',
height: '50px',
p: '20px',
bg: '$grayBg',
zIndex: 10,
}}
>
<StyledText style="modalHeadline" css={{ color: '$thTextSubtle2' }}>
Notebook
</StyledText>
<HStack
distribution="between"
css={{
ml: 'auto',
cursor: 'pointer',
gap: '15px',
mr: '-5px',
}}
distribution="center"
alignment="center"
css={{ height: '50px', width: '100%' }}
>
<StyledText style="modalHeadline">Notebook</StyledText>
<HStack css={{ ml: 'auto', gap: '10px' }}>
<Dropdown triggerElement={<MenuTrigger />}>
<DropdownOption
onSelect={() => {
exportHighlights()
}}
title="Export"
/>
</Dropdown>
<CloseButton close={() => props.onOpenChange(false)} />
</HStack>
</HStack>
<Box css={{ overflow: 'auto', width: '100%' }}>
{sortedHighlights.map((highlight) => (
<ModalHighlightView
key={highlight.id}
highlight={highlight}
showDelete={!!props.deleteHighlightAction}
scrollToHighlight={props.scrollToHighlight}
setSetLabelsTarget={setLabelsTarget}
setShowConfirmDeleteHighlightId={
setShowConfirmDeleteHighlightId
}
deleteHighlightAction={() => {
if (props.deleteHighlightAction) {
props.deleteHighlightAction(highlight.id)
}
<SizeToggle mode={sizeMode} setMode={setSizeMode} />
<Dropdown triggerElement={<MenuTrigger />}>
<DropdownOption
onSelect={() => {
exportHighlights()
}}
updateHighlight={props.updateHighlight}
title="Export Notebook"
/>
))}
{sortedHighlights.length === 0 && (
<SpanBox css={{ textAlign: 'center', width: '100%' }}>
<StyledText css={{ mb: '40px' }}>
You have not added any highlights or notes to this document
</StyledText>
</SpanBox>
)}
</Box>
</VStack>
<DropdownOption
onSelect={() => {
setShowConfirmDeleteNote(true)
}}
title="Delete Document Note"
/>
</Dropdown>
<CloseButton close={handleClose} />
</HStack>
</HStack>
<Notebook
{...props}
sizeMode={sizeMode}
viewInReader={viewInReader}
onAnnotationsChanged={handleAnnotationsChange}
/>
</ModalContent>
{showConfirmDeleteHighlightId && (
<ConfirmationModal
message={'Are you sure you want to delete this highlight?'}
onAccept={() => {
if (props.deleteHighlightAction) {
props.deleteHighlightAction(showConfirmDeleteHighlightId)
}
setShowConfirmDeleteHighlightId(undefined)
}}
onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)}
icon={
<TrashIcon
size={40}
strokeColor={theme.colors.grayTextContrast.toString()}
/>
}
/>
)}
{labelsTarget && (
<SetLabelsModal
provider={labelsTarget}
onOpenChange={function (open: boolean): void {
setLabelsTarget(undefined)
}}
onLabelsUpdated={function (labels: Label[]): void {
updateState({})
}}
save={function (labels: Label[]): Promise<Label[] | undefined> {
const result = setLabelsForHighlight(
labelsTarget.id,
labels.map((label) => label.id)
)
return result
}}
/>
)}
</ModalRoot>
)
}
type ModalHighlightViewProps = {
highlight: Highlight
showDelete: boolean
scrollToHighlight?: (arg: string) => void
deleteHighlightAction: () => void
updateHighlight: (highlight: Highlight) => void
setSetLabelsTarget: (highlight: Highlight) => void
setShowConfirmDeleteHighlightId: (id: string | undefined) => void
type SizeToggleProps = {
mode: 'normal' | 'maximized'
setMode: (mode: 'normal' | 'maximized') => void
}
function ModalHighlightView(props: ModalHighlightViewProps): JSX.Element {
const [hover, setHover] = useState(false)
const [isEditing, setIsEditing] = useState(false)
const copyHighlight = useCallback(async () => {
await navigator.clipboard.writeText(props.highlight.quote)
}, [props.highlight])
function CloseButton(props: { close: () => void }): JSX.Element {
return (
<HStack
css={{ width: '100%', py: '20px', cursor: 'pointer' }}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
<Button
style="plainIcon"
css={{
display: 'flex',
padding: '3px',
alignItems: 'center',
borderRadius: '9999px',
'&:hover': {
bg: '#898989',
},
}}
onClick={(event) => {
props.close()
event.preventDefault()
}}
>
<VStack>
{/* <SpanBox css={{ marginLeft: 'auto' }}>
<Dropdown
triggerElement={
<DotsThree size={24} color={theme.colors.readerFont.toString()} />
}
>
<DropdownOption
onSelect={async () => {
await copyHighlight()
}}
title="Copy"
/>
<DropdownOption
onSelect={() => {
props.setSetLabelsTarget(props.highlight)
}}
title="Labels"
/>
<DropdownOption
onSelect={() => {
props.setShowConfirmDeleteHighlightId(props.highlight.id)
}}
title="Delete"
/>
</Dropdown>
</SpanBox> */}
<HighlightView
scrollToHighlight={props.scrollToHighlight}
highlight={props.highlight}
/>
{!isEditing ? (
<StyledText
css={{
borderRadius: '5px',
p: '16px',
width: '100%',
marginTop: '24px',
bg: '#EBEBEB',
color: '#3D3D3D',
}}
onClick={() => setIsEditing(true)}
>
{props.highlight.annotation
? props.highlight.annotation
: 'Add your notes...'}
</StyledText>
) : null}
{isEditing && (
<HighlightNoteTextEditArea
setIsEditing={setIsEditing}
highlight={props.highlight}
updateHighlight={props.updateHighlight}
/>
)}
<SpanBox css={{ mt: '$2', mb: '$4' }} />
</VStack>
<SpanBox
css={{
marginLeft: 'auto',
width: '20px',
visibility: hover ? 'unset' : 'hidden',
'@media (hover: none)': {
visibility: 'unset',
},
}}
>
<HighlightsMenu
highlight={props.highlight}
setLabelsTarget={props.setSetLabelsTarget}
setShowConfirmDeleteHighlightId={
props.setShowConfirmDeleteHighlightId
}
/>
</SpanBox>
</HStack>
<X
width={17}
height={17}
color={theme.colors.thTextContrast2.toString()}
/>
</Button>
)
}
function SizeToggle(props: SizeToggleProps): JSX.Element {
return (
<Button
style="plainIcon"
css={{
display: 'flex',
padding: '2px',
alignItems: 'center',
borderRadius: '9999px',
'&:hover': {
bg: '#898989',
},
'@mdDown': {
display: 'none',
},
}}
onClick={(event) => {
props.setMode(props.mode == 'normal' ? 'maximized' : 'normal')
event.preventDefault()
}}
>
{props.mode == 'normal' ? (
<ArrowsOut size="15" color={theme.colors.thTextContrast2.toString()} />
) : (
<ArrowsIn size="15" color={theme.colors.thTextContrast2.toString()} />
)}
</Button>
)
}

View file

@ -2,7 +2,7 @@ import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticle
import { Box } from '../../elements/LayoutPrimitives'
import { v4 as uuidv4 } from 'uuid'
import { nanoid } from 'nanoid'
import { useState, useEffect, useCallback, useRef } from 'react'
import { useState, useEffect, useRef } from 'react'
import { isDarkTheme } from '../../../lib/themeUpdater'
import PSPDFKit from 'pspdfkit'
import { Instance, HighlightAnnotation, List, Annotation, Rect } from 'pspdfkit'
@ -12,15 +12,15 @@ import { deleteHighlightMutation } from '../../../lib/networking/mutations/delet
import { articleReadingProgressMutation } from '../../../lib/networking/mutations/articleReadingProgressMutation'
import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeHighlightMutation'
import { useCanShareNative } from '../../../lib/hooks/useCanShareNative'
import { webBaseURL } from '../../../lib/appConfig'
import { pspdfKitKey } from '../../../lib/appConfig'
import { NotebookModal } from './NotebookModal'
import { HighlightNoteModal } from './HighlightNoteModal'
import { showErrorToast } from '../../../lib/toastHelpers'
import { HEADER_HEIGHT, MOBILE_HEADER_HEIGHT } from '../homeFeed/HeaderSpacer'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
export type PdfArticleContainerProps = {
viewerUsername: string
viewer: UserBasicData
article: ArticleAttributes
showHighlightsModal: boolean
setShowHighlightsModal: React.Dispatch<React.SetStateAction<boolean>>
@ -30,43 +30,41 @@ export default function PdfArticleContainer(
props: PdfArticleContainerProps
): JSX.Element {
const containerRef = useRef<HTMLDivElement | null>(null)
const [shareTarget, setShareTarget] = useState<Highlight | undefined>(
undefined
)
const [shareTarget, setShareTarget] =
useState<Highlight | undefined>(undefined)
const [notebookKey, setNotebookKey] = useState<string>(uuidv4())
const [noteTarget, setNoteTarget] = useState<Highlight | undefined>(undefined)
const [noteTargetPageIndex, setNoteTargetPageIndex] = useState<
number | undefined
>(undefined)
const [noteTargetPageIndex, setNoteTargetPageIndex] =
useState<number | undefined>(undefined)
const highlightsRef = useRef<Highlight[]>([])
const canShareNative = useCanShareNative()
const getHighlightURL = useCallback(
(highlightID: string): string =>
`${webBaseURL}/${props.viewerUsername}/${props.article.slug}/highlights/${highlightID}`,
[props.article.slug, props.viewerUsername]
)
// const getHighlightURL = useCallback(
// (highlightID: string): string =>
// `${webBaseURL}/${props.viewerUsername}/${props.article.slug}/highlights/${highlightID}`,
// [props.article.slug, props.viewerUsername]
// )
const nativeShare = useCallback(
async (highlightID: string, title: string) => {
await navigator?.share({
title: title,
url: getHighlightURL(highlightID),
})
},
[getHighlightURL]
)
// const nativeShare = useCallback(
// async (highlightID: string, title: string) => {
// await navigator?.share({
// title: title,
// url: getHighlightURL(highlightID),
// })
// },
// [getHighlightURL]
// )
const handleOpenShare = useCallback(
(highlight: Highlight) => {
if (canShareNative) {
nativeShare(highlight.shortId, props.article.title)
} else {
setShareTarget(highlight)
}
},
[nativeShare, canShareNative, props.article.title]
)
// const handleOpenShare = useCallback(
// (highlight: Highlight) => {
// if (canShareNative) {
// nativeShare(highlight.shortId, props.article.title)
// } else {
// setShareTarget(highlight)
// }
// },
// [nativeShare, canShareNative, props.article.title]
// )
const annotationOmnivoreId = (annotation: Annotation): string | undefined => {
if (
@ -178,23 +176,23 @@ export default function PdfArticleContainer(
instance.setSelectedAnnotation(null)
},
}
const share = {
type: 'custom' as const,
title: 'Share',
id: 'tooltip-share-annotation',
className: 'TooltipItem-Share',
onPress: () => {
if (
annotation.customData &&
annotation.customData.omnivoreHighlight &&
(annotation.customData.omnivoreHighlight as Highlight).shortId
) {
const data = annotation.customData.omnivoreHighlight as Highlight
handleOpenShare(data)
}
instance.setSelectedAnnotation(null)
},
}
// const share = {
// type: 'custom' as const,
// title: 'Share',
// id: 'tooltip-share-annotation',
// className: 'TooltipItem-Share',
// onPress: () => {
// if (
// annotation.customData &&
// annotation.customData.omnivoreHighlight &&
// (annotation.customData.omnivoreHighlight as Highlight).shortId
// ) {
// const data = annotation.customData.omnivoreHighlight as Highlight
// handleOpenShare(data)
// }
// instance.setSelectedAnnotation(null)
// },
// }
return [copy, note, remove]
}
@ -237,7 +235,9 @@ export default function PdfArticleContainer(
// Store the highlights in the highlightsRef and apply them to the PDF
highlightsRef.current = props.article.highlights
for (const highlight of props.article.highlights) {
for (const highlight of props.article.highlights.filter(
(h) => h.type == 'HIGHLIGHT'
)) {
const patch = JSON.parse(highlight.patch)
if (highlight.annotation && patch.customData.omnivoreHighight) {
patch.customData.omnivoreHighight.annotation = highlight.annotation
@ -491,15 +491,26 @@ export default function PdfArticleContainer(
{props.showHighlightsModal && (
<NotebookModal
key={notebookKey}
viewer={props.viewer}
item={props.article}
highlights={highlightsRef.current}
onOpenChange={() => props.setShowHighlightsModal(false)}
/* eslint-disable @typescript-eslint/no-empty-function */
updateHighlight={() => {}}
deleteHighlightAction={(highlightId: string) => {
const event = new CustomEvent('deleteHighlightbyId', {
detail: highlightId,
onClose={(updatedHighlights, deletedAnnotations) => {
console.log(
'closed PDF notebook: ',
updatedHighlights,
deletedAnnotations
)
deletedAnnotations.forEach((highlight) => {
const event = new CustomEvent('deleteHighlightbyId', {
detail: highlight.id,
})
document.dispatchEvent(event)
})
document.dispatchEvent(event)
props.setShowHighlightsModal(false)
}}
viewHighlightInReader={(highlightId) => {
// TODO: scroll to highlight in PDF
props.setShowHighlightsModal(false)
}}
/>
)}

View file

@ -1,210 +1,61 @@
import { styled } from '@stitches/react'
import { Item } from '@radix-ui/react-dropdown-menu'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { DotsThreeVertical } from 'phosphor-react'
import { Fragment, useCallback, useMemo, useState } from 'react'
import { useCallback } from 'react'
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
import { setLabelsForHighlight } from '../../../lib/networking/mutations/setLabelsForHighlight'
import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { HighlightNoteTextEditArea } from '../../elements/HighlightNoteTextEditArea'
import { LabelChip } from '../../elements/LabelChip'
import {
Blockquote,
Box,
HStack,
SpanBox,
VStack,
} from '../../elements/LayoutPrimitives'
import { StyledText } from '../../elements/StyledText'
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
import { theme } from '../../tokens/stitches.config'
import { SetLabelsModal } from '../article/SetLabelsModal'
Dropdown,
DropdownOption,
DropdownSeparator,
} from '../../elements/DropdownElements'
import { Box, SpanBox } from '../../elements/LayoutPrimitives'
type HighlightItemProps = {
highlight: Highlight
viewer: UserBasicData | undefined
item: LibraryItemNode
deleteHighlight: (item: LibraryItemNode, highlight: Highlight) => void
}
const StyledQuote = styled(Blockquote, {
margin: '0px',
fontSize: '16px',
fontFamily: '$inter',
fontWeight: '500',
lineHeight: '1.50',
color: '$thHighContrast',
paddingLeft: '15px',
borderLeft: '2px solid $omnivoreCtaYellow',
})
export function HighlightItem(props: HighlightItemProps): JSX.Element {
const router = useRouter()
const [hover, setHover] = useState(false)
const [isEditing, setIsEditing] = useState(false)
const lines = useMemo(
() => props.highlight.quote.split('\n'),
[props.highlight.quote]
)
const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] =
useState<undefined | string>(undefined)
const [labelsTarget, setLabelsTarget] = useState<Highlight | undefined>(
undefined
)
const [, updateState] = useState({})
return (
<>
<HStack
css={{ width: '100%', py: '20px', cursor: 'pointer' }}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
<VStack
css={{
gap: '10px',
height: '100%',
width: '100%',
wordBreak: 'break-word',
overflow: 'clip',
}}
alignment="start"
distribution="start"
>
<StyledQuote
onClick={(event) => {
if (router && props.viewer) {
const dest = `/${props.viewer.profile.username}/${props.item.slug}#${props.highlight.id}`
router.push(dest)
}
event.preventDefault()
}}
>
<SpanBox css={{ p: '1px', borderRadius: '2px' }}>
{lines.map((line: string, index: number) => (
<Fragment key={index}>
{line}
{index !== lines.length - 1 && (
<>
<br />
<br />
</>
)}
</Fragment>
))}
</SpanBox>
</StyledQuote>
<Box css={{ display: 'block' }}>
{props.highlight.labels?.map((label: Label, index: number) => (
<LabelChip
key={index}
text={label.name || ''}
color={label.color}
/>
))}
</Box>
{!isEditing && (
<StyledText
css={{
borderRadius: '6px',
bg: '#EBEBEB',
p: '10px',
width: '100%',
marginTop: '5px',
color: '#3D3D3D',
}}
onClick={() => setIsEditing(true)}
>
{props.highlight.annotation
? props.highlight.annotation
: 'Add your notes...'}
</StyledText>
)}
{isEditing && (
<HighlightNoteTextEditArea
setIsEditing={setIsEditing}
highlight={props.highlight}
// eslint-disable-next-line @typescript-eslint/no-empty-function
updateHighlight={() => {}}
/>
)}
</VStack>
<SpanBox
css={{
marginLeft: 'auto',
width: '20px',
}}
>
<HighlightsMenu
highlight={props.highlight}
setLabelsTarget={setLabelsTarget}
setShowConfirmDeleteHighlightId={setShowConfirmDeleteHighlightId}
/>
</SpanBox>
</HStack>
{showConfirmDeleteHighlightId && (
<ConfirmationModal
message={'Are you sure you want to delete this highlight?'}
onAccept={async () => {
setShowConfirmDeleteHighlightId(undefined)
const result = await deleteHighlightMutation(
showConfirmDeleteHighlightId
)
if (result) {
showSuccessToast('Highlight deleted')
props.deleteHighlight(props.item, props.highlight)
} else {
showErrorToast('Error deleting highlight')
}
}}
onOpenChange={() => setShowConfirmDeleteHighlightId(undefined)}
/>
)}
{labelsTarget && (
<SetLabelsModal
provider={labelsTarget}
onOpenChange={function (open: boolean): void {
setLabelsTarget(undefined)
}}
onLabelsUpdated={function (labels: Label[]): void {
updateState({})
}}
save={function (labels: Label[]): Promise<Label[] | undefined> {
const result = setLabelsForHighlight(
labelsTarget.id,
labels.map((label) => label.id)
)
return result
}}
/>
)}
</>
)
}
import { styled, theme } from '../../tokens/stitches.config'
type HighlightsMenuProps = {
viewer: UserBasicData
item: ReadableItem
highlight: Highlight
viewInReader: (highlightId: string) => void
setLabelsTarget: (target: Highlight) => void
setShowConfirmDeleteHighlightId: (set: string) => void
}
const StyledLinkItem = styled('a', {
display: 'flex',
fontSize: '14px',
fontWeight: '400',
py: '10px',
px: '15px',
borderRadius: 3,
cursor: 'pointer',
color: '$utilityTextDefault',
textDecoration: 'none',
'&:hover': {
outline: 'none',
backgroundColor: '$grayBgHover',
},
})
export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
const copyHighlight = useCallback(() => {
;(async () => {
await navigator.clipboard.writeText(props.highlight.quote)
showSuccessToast('Highlight copied')
})()
const quote = props.highlight.quote
if (quote) {
;(async () => {
await navigator.clipboard.writeText(quote)
showSuccessToast('Highlight copied')
})()
} else {
showErrorToast('No highlight text.')
}
}, [props.highlight])
return (
@ -249,6 +100,28 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
}}
title="Delete"
/>
<DropdownSeparator />
<Link
href={`/${props.viewer.profile.username}/${props.item.slug}#${props.highlight.id}`}
>
<StyledLinkItem
onClick={(event) => {
console.log('event.ctrlKey: ', event.ctrlKey, event.metaKey)
if (event.ctrlKey || event.metaKey) {
window.open(
`/${props.viewer.profile.username}/${props.item.slug}#${props.highlight.id}`,
'_blank'
)
return
}
props.viewInReader(props.highlight.id)
event.preventDefault()
event.stopPropagation()
}}
>
View In Reader
</StyledLinkItem>
</Link>
</Dropdown>
)
}
@ -263,9 +136,17 @@ export function highlightAsMarkdown(highlight: Highlight) {
}
export function highlightsAsMarkdown(highlights: Highlight[]) {
return highlights
const noteMD = highlights.find((h) => h.type == 'NOTE')
const highlightMD = highlights
.filter((h) => h.type == 'HIGHLIGHT')
.map((highlight) => {
return highlightAsMarkdown(highlight)
})
.join('\n\n')
if (noteMD) {
return `${noteMD.annotation}\n\n${highlightMD}`
}
return highlightMD
}

View file

@ -1,3 +1,4 @@
import { useRouter } from 'next/router'
import { HighlighterCircle } from 'phosphor-react'
import { useCallback, useEffect, useReducer, useState } from 'react'
import { Toaster } from 'react-hot-toast'
@ -18,9 +19,10 @@ import {
timeAgo,
} from '../../patterns/LibraryCards/LibraryCardStyles'
import { LibraryHighlightGridCard } from '../../patterns/LibraryCards/LibraryHighlightGridCard'
import { Notebook } from '../article/Notebook'
import { EmptyHighlights } from './EmptyHighlights'
import { HEADER_HEIGHT, MOBILE_HEADER_HEIGHT } from './HeaderSpacer'
import { HighlightItem, highlightsAsMarkdown } from './HighlightItem'
import { highlightsAsMarkdown } from './HighlightItem'
type HighlightItemsLayoutProps = {
items: LibraryItem[]
@ -32,9 +34,8 @@ type HighlightItemsLayoutProps = {
export function HighlightItemsLayout(
props: HighlightItemsLayoutProps
): JSX.Element {
const [currentItem, setCurrentItem] = useState<LibraryItem | undefined>(
undefined
)
const [currentItem, setCurrentItem] =
useState<LibraryItem | undefined>(undefined)
const listReducer = (
state: LibraryItem[],
@ -124,6 +125,9 @@ export function HighlightItemsLayout(
'@xlgDown': {
height: `calc(100vh - ${MOBILE_HEADER_HEIGHT})`,
},
'@lgDown': {
overflowY: 'scroll',
},
bg: '$thBackground2',
overflow: 'hidden',
}}
@ -165,7 +169,7 @@ export function HighlightItemsLayout(
borderBottom: '1px solid $thBorderColor',
}}
alignment="center"
distribution="start"
distribution="center"
></HStack>
<LibraryItemsList
items={items}
@ -185,28 +189,19 @@ export function HighlightItemsLayout(
height: '100%',
width: '100%',
flexGrow: '1',
justifyContent: 'center',
overflowY: 'scroll',
'@lgDown': {
display: 'none',
flexGrow: 'unset',
},
}}
>
<HStack
css={{
flexGrow: '1',
overflowY: 'scroll',
height: '100%',
width: '100%',
}}
distribution="start"
alignment="start"
>
<HighlightList
item={currentItem}
viewer={props.viewer}
deleteHighlight={handleDelete}
/>
</HStack>
<HighlightList
item={currentItem}
viewer={props.viewer}
deleteHighlight={handleDelete}
/>
</SpanBox>
</>
)}
@ -256,6 +251,7 @@ function LibraryItemsList(props: LibraryItemsListProps): JSX.Element {
)}
</Box>
))}
<Box css={{ height: '240px' }} />
</>
)
}
@ -366,6 +362,8 @@ type HighlightListProps = {
}
function HighlightList(props: HighlightListProps): JSX.Element {
const router = useRouter()
const exportHighlights = useCallback(() => {
;(async () => {
if (!props.item.node.highlights) {
@ -378,68 +376,231 @@ function HighlightList(props: HighlightListProps): JSX.Element {
})()
}, [props.item.node.highlights])
const viewInReader = useCallback(
(highlightId) => {
if (!router || !router.isReady || !props.viewer) {
showErrorToast('Error navigating to highlight')
return
}
console.log(
'pushing user: ',
props.viewer,
'slug: ',
props.item.node.slug
)
router.push(
{
pathname: '/[username]/[slug]',
query: {
username: props.viewer.profile.username,
slug: props.item.node.slug,
},
hash: highlightId,
},
`${props.viewer.profile.username}/${props.item.node.slug}#${highlightId}`,
{
scroll: false,
}
)
},
[router, props]
)
return (
<HStack
<VStack
css={{
m: '20px',
height: '100%',
flexGrow: '1',
height: '100%',
minWidth: '425px',
maxWidth: '625px',
width: '100%',
justifyContent: 'flex-start',
}}
distribution="center"
alignment="start"
distribution="start"
alignment="center"
>
<VStack
<HStack
css={{
width: '425px',
borderRadius: '6px',
width: '100%',
borderBottom: '1px solid $thBorderColor',
}}
alignment="start"
distribution="start"
distribution="center"
>
<HStack
<StyledText
css={{
fontWeight: '600',
fontSize: '15px',
fontFamily: '$display',
width: '100%',
pt: '25px',
borderBottom: '1px solid $thBorderColor',
color: 'thTextContrast2',
m: '0px',
pb: '5px',
}}
alignment="start"
distribution="start"
>
<StyledText
css={{
fontWeight: '600',
fontSize: '15px',
fontFamily: '$display',
width: '100%',
color: 'thTextContrast2',
NOTEBOOK
</StyledText>
<Dropdown triggerElement={<MenuTrigger />}>
<DropdownOption
onSelect={() => {
exportHighlights()
}}
>
HIGHLIGHTS
</StyledText>
<Dropdown triggerElement={<MenuTrigger />}>
<DropdownOption
onSelect={() => {
exportHighlights()
}}
title="Export"
/>
</Dropdown>
</HStack>
<VStack css={{ width: '100%' }} distribution="start" alignment="start">
{(props.item.node.highlights ?? []).map((highlight) => (
<HighlightItem
key={highlight.id}
viewer={props.viewer}
item={props.item.node}
highlight={highlight}
deleteHighlight={props.deleteHighlight}
/>
))}
<Box css={{ height: '100px' }} />
</VStack>
</VStack>
</HStack>
title="Export"
/>
</Dropdown>
</HStack>
<HStack css={{ width: '100%', height: '100%' }}>
{props.viewer && (
<Notebook
sizeMode="normal"
viewer={props.viewer}
item={props.item.node}
highlights={props.item.node.highlights ?? []}
viewInReader={viewInReader}
/>
)}
</HStack>
</VStack>
)
// return (
// <HStack
// css={{
// m: '20px',
// height: '100%',
// flexGrow: '1',
// }}
// distribution="center"
// alignment="start"
// >
// <VStack
// css={{
// width: '425px',
// borderRadius: '6px',
// }}
// alignment="start"
// distribution="start"
// >
// <HStack
// css={{
// width: '100%',
// pt: '25px',
// borderBottom: '1px solid $thBorderColor',
// }}
// alignment="center"
// distribution="center"
// >
// <StyledText
// css={{
// fontWeight: '600',
// fontSize: '15px',
// fontFamily: '$display',
// width: '100%',
// color: 'thTextContrast2',
// }}
// >
// NOTEBOOK
// </StyledText>
// <Dropdown triggerElement={<MenuTrigger />}>
// <DropdownOption
// onSelect={() => {
// exportHighlights()
// }}
// title="Export"
// />
// </Dropdown>
// </HStack>
// <HStack
// css={{
// width: '100%',
// pt: '25px',
// borderBottom: '1px solid $thBorderColor',
// }}
// alignment="center"
// distribution="center"
// >
// <StyledText
// css={{
// fontWeight: '600',
// fontSize: '15px',
// fontFamily: '$display',
// width: '100%',
// color: 'thTextContrast2',
// }}
// >
// NOTE
// </StyledText>
// </HStack>
// <HighlightNoteBox
// sizeMode="normal"
// mode={notesEditMode}
// setEditMode={setNotesEditMode}
// text={note?.annotation}
// placeHolder="Add notes to this document..."
// saveText={(highlight) => {
// console.log('saving text', highlight)
// }}
// />
// <SpanBox css={{ mt: '10px', mb: '25px' }} />
// {sortedHighlights && (
// <>
// <HStack
// css={{
// width: '100%',
// pt: '25px',
// borderBottom: '1px solid $thBorderColor',
// }}
// alignment="center"
// distribution="center"
// >
// <StyledText
// css={{
// fontWeight: '600',
// fontSize: '15px',
// fontFamily: '$display',
// width: '100%',
// color: 'thTextContrast2',
// }}
// >
// HIGHLIGHTS
// </StyledText>
// </HStack>
// <VStack
// css={{ width: '100%', mt: '20px' }}
// distribution="start"
// alignment="start"
// >
// {sortedHighlights.map((highlight) => (
// <>
// <HighlightViewItem
// key={highlight.id}
// highlight={highlight}
// updateHighlight={(highlight) => {
// console.log('updated highlight: ', highlight)
// }}
// deleteHighlightAction={(highlight) => {
// console.log('deleting: ', highlight)
// }}
// setSetLabelsTarget: (highlight: Highlight) => void
// setShowConfirmDeleteHighlightId: (id: string | undefined) => void
// />
// <SpanBox css={{ mt: '10px', mb: '25px' }} />
// </>
// ))}
// <Box css={{ height: '100px' }} />
// </VStack>
// <SpanBox css={{ mt: '10px', mb: '25px' }} />
// </>
// )}
// </VStack>
// </HStack>
// )
}
type HighlightCountChipProps = {

View file

@ -78,9 +78,8 @@ export function HomeFeedContainer(): JSX.Element {
const gridContainerRef = useRef<HTMLDivElement>(null)
const [labelsTarget, setLabelsTarget] = useState<LibraryItem | undefined>(
undefined
)
const [labelsTarget, setLabelsTarget] =
useState<LibraryItem | undefined>(undefined)
const [showAddLinkModal, setShowAddLinkModal] = useState(false)
const [showEditTitleModal, setShowEditTitleModal] = useState(false)
@ -701,6 +700,10 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
{...props}
/>
)}
{props.showAddLinkModal && (
<AddLinkModal onOpenChange={() => props.setShowAddLinkModal(false)} />
)}
</HStack>
</VStack>
)
@ -916,10 +919,6 @@ function LibraryItemsLayout(props: LibraryItemsLayoutProps): JSX.Element {
)}
</Dropzone>
</VStack>
{props.showAddLinkModal && (
<AddLinkModal onOpenChange={() => props.setShowAddLinkModal(false)} />
)}
{props.showEditTitleModal && (
<EditLibraryItemModal
updateItem={(item: LibraryItem) =>

View file

@ -168,6 +168,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
thBackground2: '#F3F3F3',
thBackground3: '#FFFFFF',
thBackground4: '#EBEBEB',
thBackground5: '#F5F5F5',
thBackgroundActive: '#F9F9F9',
thBackgroundContrast: '#FFFFFF',
@ -254,6 +255,7 @@ const darkThemeSpec = {
thBackground2: '#3D3D3D',
thBackground3: '#242424',
thBackground4: '#3D3D3D',
thBackground5: '#3D3D3D',
thBackgroundActive: '#2E2E2E',
thBackgroundContrast: '#000000',

View file

@ -4,7 +4,6 @@ type AppEnvironment = 'prod' | 'dev' | 'demo' | 'local'
type BaseURLs = {
webBaseURL: string
serverBaseURL: string
highlightsBaseURL: string
}
type BaseURLRecords = Record<AppEnvironment, BaseURLs>
@ -14,22 +13,18 @@ const baseURLRecords: BaseURLRecords = {
prod: {
webBaseURL: process.env.NEXT_PUBLIC_BASE_URL ?? '',
serverBaseURL: process.env.NEXT_PUBLIC_SERVER_BASE_URL ?? '',
highlightsBaseURL: process.env.NEXT_PUBLIC_HIGHLIGHTS_BASE_URL ?? '',
},
dev: {
webBaseURL: process.env.NEXT_PUBLIC_DEV_BASE_URL ?? '',
serverBaseURL: process.env.NEXT_PUBLIC_DEV_SERVER_BASE_URL ?? '',
highlightsBaseURL: process.env.NEXT_PUBLIC_DEV_HIGHLIGHTS_BASE_URL ?? '',
},
demo: {
webBaseURL: process.env.NEXT_PUBLIC_DEMO_BASE_URL ?? '',
serverBaseURL: process.env.NEXT_PUBLIC_DEMO_SERVER_BASE_URL ?? '',
highlightsBaseURL: process.env.NEXT_PUBLIC_DEMO_HIGHLIGHTS_BASE_URL ?? '',
},
local: {
webBaseURL: process.env.NEXT_PUBLIC_LOCAL_BASE_URL ?? '',
serverBaseURL: process.env.NEXT_PUBLIC_LOCAL_SERVER_BASE_URL ?? '',
highlightsBaseURL: process.env.NEXT_PUBLIC_LOCAL_HIGHLIGHTS_BASE_URL ?? '',
},
}
@ -43,16 +38,6 @@ function serverBaseURL(env: AppEnvironment): string {
return value
}
function highlightsURL(env: AppEnvironment): string {
const value = baseURLRecords[appEnv].highlightsBaseURL
if (value.length == 0) {
throw new Error(
`Couldn't find environment variable for highlights base url in ${env} environment`
)
}
return value
}
function webURL(env: AppEnvironment): string {
const value = baseURLRecords[appEnv].webBaseURL
if (value.length == 0) {
@ -96,6 +81,4 @@ export const gqlEndpoint = `${serverBaseURL(appEnv)}/api/graphql`
export const fetchEndpoint = `${serverBaseURL(appEnv)}/api`
export const highlightsBaseURL = highlightsURL(appEnv)
export const webBaseURL = webURL(appEnv)

View file

@ -17,3 +17,10 @@ export function formattedShortDate(rawDate: string): string {
timeZone,
}).format(new Date(rawDate))
}
export function formattedShortTime(rawDate: string): string {
return new Intl.DateTimeFormat(locale, {
timeStyle: 'short',
timeZone,
}).format(new Date(rawDate))
}

View file

@ -11,6 +11,7 @@ import { extendRangeToWordBoundaries } from './normalizeHighlightRange'
import type { Highlight } from '../networking/fragments/highlightFragment'
import { removeHighlights } from './deleteHighlight'
import { ArticleMutations } from '../articleActions'
import { NodeHtmlMarkdown } from 'node-html-markdown'
type CreateHighlightInput = {
selection: SelectionAttributes
@ -28,6 +29,20 @@ type CreateHighlightOutput = {
newHighlightIndex?: number
}
/* ********************************************************* *
* Re-use
* If using it several times, creating an instance saves time
* ********************************************************* */
const nhm = new NodeHtmlMarkdown(
/* options (optional) */ {},
/* customTransformers (optional) */ undefined,
/* customCodeBlockTranslators (optional) */ undefined
)
export const htmlToMarkdown = (html: string) => {
return nhm.translate(/* html */ html)
}
export async function createHighlight(
input: CreateHighlightInput,
articleMutations: ArticleMutations
@ -42,6 +57,10 @@ export async function createHighlight(
extendRangeToWordBoundaries(range)
// Create a temp container for copying the range HTML
const container = document.createElement('div')
container.appendChild(range.cloneContents())
const id = uuidv4()
const patch = generateDiffPatch(range)
@ -79,12 +98,15 @@ export async function createHighlight(
)
const newHighlightAttributes = {
prefix: highlightAttributes.prefix,
suffix: highlightAttributes.suffix,
quote: highlightAttributes.quote,
id,
shortId: nanoid(8),
patch,
prefix: highlightAttributes.prefix,
suffix: highlightAttributes.suffix,
quote: htmlToMarkdown(container.innerHTML),
html: container.innerHTML,
annotation: annotations.length > 0 ? annotations.join('\n') : undefined,
articleId: input.articleId,
highlightPositionPercent: input.highlightPositionPercent,

View file

@ -1,7 +1,13 @@
import { HighlightLocation } from './highlightGenerator'
import { getHighlightElements, getHighlightNoteButton } from './highlightHelpers'
import {
getHighlightElements,
getHighlightNoteButton,
} from './highlightHelpers'
export function removeHighlights(ids: string[], locations: HighlightLocation[]): void {
export function removeHighlights(
ids: string[],
locations: HighlightLocation[]
): void {
ids.forEach((id) => {
const elements = getHighlightElements(id)
const noteButtons = getHighlightNoteButton(id)

View file

@ -4,6 +4,7 @@ import { Label } from './labelFragment'
export const highlightFragment = gql`
fragment HighlightFields on Highlight {
id
type
shortId
quote
prefix
@ -24,11 +25,13 @@ export const highlightFragment = gql`
}
}
`
export type HighlightType = 'HIGHLIGHT' | 'REDACTION' | 'NOTE'
export type Highlight = {
id: string
type: HighlightType
shortId: string
quote: string
quote?: string
prefix?: string
suffix?: string
patch: string

View file

@ -1,17 +1,28 @@
import { gql } from 'graphql-request'
import { gqlFetcher } from '../networkHelpers'
import { Highlight, highlightFragment } from './../fragments/highlightFragment'
import {
Highlight,
highlightFragment,
HighlightType,
} from './../fragments/highlightFragment'
export type CreateHighlightInput = {
prefix: string
suffix: string
quote: string
id: string
shortId: string
patch: string
articleId: string
prefix?: string
suffix?: string
quote?: string
html?: string
annotation?: string
patch?: string
highlightPositionPercent?: number
highlightPositionAnchorIndex?: number
type?: HighlightType
}
type CreateHighlightOutput = {

View file

@ -10,6 +10,7 @@ export type MergeHighlightInput = {
quote: string
prefix?: string
suffix?: string
html?: string
annotation?: string
overlapHighlightIdList: string[]
highlightPositionPercent?: number

View file

@ -11,6 +11,12 @@ import { Label } from './../fragments/labelFragment'
import { showErrorToast, showSuccessToast } from '../../toastHelpers'
import { Highlight, highlightFragment } from '../fragments/highlightFragment'
export interface ReadableItem {
id: string
title: string
slug: string
}
export type LibraryItemsQueryInput = {
limit: number
sortDescending: boolean

View file

@ -41,8 +41,10 @@
"downshift": "^6.1.9",
"graphql-request": "^3.6.1",
"kbar": "^0.1.0-beta.35",
"markdown-it": "^13.0.1",
"nanoid": "^3.1.29",
"next": "^12.1.0",
"node-html-markdown": "^1.3.0",
"phosphor-react": "^1.4.0",
"pspdfkit": "^2022.2.3",
"react": "^17.0.2",
@ -50,6 +52,8 @@
"react-dom": "^17.0.2",
"react-dropzone": "^14.2.3",
"react-hot-toast": "^2.1.1",
"react-markdown": "^8.0.6",
"react-markdown-editor-lite": "^1.3.4",
"react-masonry-css": "^1.0.16",
"react-pro-sidebar": "^0.7.1",
"react-spinners": "^0.13.7",
@ -77,6 +81,7 @@
"@types/diff-match-patch": "^1.0.32",
"@types/jest": "^27.0.2",
"@types/lodash.debounce": "^4.0.6",
"@types/markdown-it": "^12.2.3",
"@types/react": "17.0.2",
"@types/react-dom": "^17.0.2",
"@types/segment-analytics": "^0.0.34",

View file

@ -71,8 +71,6 @@ export default function Home(): JSX.Element {
const actionHandler = useCallback(
async (action: string, arg?: unknown) => {
console.log('handling action: ', action, article)
switch (action) {
case 'unarchive':
if (article) {
@ -348,7 +346,7 @@ export default function Home(): JSX.Element {
article={article}
showHighlightsModal={showHighlightsModal}
setShowHighlightsModal={setShowHighlightsModal}
viewerUsername={viewerData.me?.profile?.username}
viewer={viewerData.me}
/>
) : (
<VStack
@ -364,10 +362,10 @@ export default function Home(): JSX.Element {
>
{article && viewerData?.me ? (
<ArticleContainer
viewer={viewerData.me}
article={article}
isAppleAppEmbed={false}
highlightBarDisabled={false}
highlightsBaseURL={`${webBaseURL}/${viewerData.me?.profile?.username}/${slug}/highlights`}
fontSize={readerSettings.fontSize}
margin={readerSettings.marginWidth}
lineHeight={readerSettings.lineHeight}

View file

@ -13,6 +13,7 @@ import { mergeHighlightMutation } from '../../../../lib/networking/mutations/mer
import { updateHighlightMutation } from '../../../../lib/networking/mutations/updateHighlightMutation'
import { articleReadingProgressMutation } from '../../../../lib/networking/mutations/articleReadingProgressMutation'
import Script from 'next/script'
import { useGetViewerQuery } from '../../../../lib/networking/queries/useGetViewerQuery'
type AppArticleEmbedContentProps = {
slug: string
@ -28,9 +29,8 @@ export default function AppArticleEmbed(): JSX.Element {
const router = useRouter()
const [contentProps, setContentProps] = useState<
AppArticleEmbedContentProps | undefined
>(undefined)
const [contentProps, setContentProps] =
useState<AppArticleEmbedContentProps | undefined>(undefined)
useEffect(() => {
if (!router.isReady) return
@ -62,7 +62,7 @@ export default function AppArticleEmbed(): JSX.Element {
function AppArticleEmbedContent(
props: AppArticleEmbedContentProps
): JSX.Element {
const scrollRef = useRef<HTMLDivElement | null>(null)
const { viewerData } = useGetViewerQuery()
const [showHighlightsModal, setShowHighlightsModal] = useState(false)
const { articleData } = useGetArticleQuery({
@ -71,7 +71,7 @@ function AppArticleEmbedContent(
includeFriendsHighlights: false,
})
if (articleData) {
if (articleData && viewerData?.me) {
return (
<Box>
<Script async src="/static/scripts/mathJaxConfiguration.js" />
@ -86,10 +86,10 @@ function AppArticleEmbedContent(
className="disable-webkit-callout"
>
<ArticleContainer
viewer={viewerData.me}
article={articleData.article.article}
isAppleAppEmbed={true}
highlightBarDisabled={props.highlightBarDisabled}
highlightsBaseURL={`${webBaseURL}/${props.username}/${props.slug}/highlights`}
fontSize={props.fontSize}
margin={props.margin}
fontFamily={props.fontFamily}

504
yarn.lock
View file

@ -1970,6 +1970,13 @@
dependencies:
regenerator-runtime "^0.13.10"
"@babel/runtime@^7.6.2":
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
dependencies:
regenerator-runtime "^0.13.11"
"@babel/template@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
@ -8134,7 +8141,7 @@
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==
"@types/debug@^4.1.0":
"@types/debug@^4.0.0", "@types/debug@^4.1.0":
version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==
@ -8692,6 +8699,11 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
"@types/prop-types@^15.0.0":
version "15.7.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
"@types/qs@*", "@types/qs@^6.9.5":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
@ -10686,6 +10698,11 @@ bail@^1.0.0:
resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==
bail@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d"
integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@ -11504,6 +11521,11 @@ character-entities@^1.0.0:
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b"
integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==
character-entities@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22"
integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==
character-reference-invalid@^1.0.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
@ -11953,6 +11975,11 @@ comma-separated-tokens@^1.0.0:
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==
comma-separated-tokens@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
command-score@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/command-score/-/command-score-0.1.2.tgz#b986ad7e8c0beba17552a56636c44ae38363d381"
@ -12819,7 +12846,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
dependencies:
ms "2.0.0"
debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@ -12868,6 +12895,13 @@ decimal.js@^10.2.1:
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==
decode-named-character-reference@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e"
integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==
dependencies:
character-entities "^2.0.0"
decode-uri-component@^0.2.0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9"
@ -13056,6 +13090,11 @@ deprecation@^2.0.0, deprecation@^2.3.1:
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
dequal@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
des.js@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
@ -13702,6 +13741,11 @@ entities@~2.1.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
entities@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==
env-paths@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
@ -16170,6 +16214,11 @@ hast-util-to-parse5@^6.0.0:
xtend "^4.0.0"
zwitch "^1.0.0"
hast-util-whitespace@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz#0ec64e257e6fc216c7d14c8a1b74d27d650b4557"
integrity sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==
hastscript@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640"
@ -17249,6 +17298,11 @@ is-plain-obj@^3.0.0:
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7"
integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==
is-plain-obj@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0"
integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
is-plain-object@5.0.0, is-plain-object@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
@ -18576,6 +18630,11 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
kleur@^4.0.3:
version "4.1.5"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
klona@^2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
@ -18811,6 +18870,13 @@ linkify-it@^3.0.1:
dependencies:
uc.micro "^1.0.1"
linkify-it@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec"
integrity sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==
dependencies:
uc.micro "^1.0.1"
listr-silent-renderer@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
@ -19462,6 +19528,17 @@ markdown-it@^12.3.2:
mdurl "^1.0.1"
uc.micro "^1.0.5"
markdown-it@^13.0.1:
version "13.0.1"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430"
integrity sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==
dependencies:
argparse "^2.0.1"
entities "~3.0.1"
linkify-it "^4.0.1"
mdurl "^1.0.1"
uc.micro "^1.0.5"
markdown-to-jsx@^7.1.3:
version "7.1.7"
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.1.7.tgz#a5f22102fb12241c8cea1ca6a4050bb76b23a25d"
@ -19504,6 +19581,33 @@ mdast-util-definitions@^4.0.0:
dependencies:
unist-util-visit "^2.0.0"
mdast-util-definitions@^5.0.0:
version "5.1.2"
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz#9910abb60ac5d7115d6819b57ae0bcef07a3f7a7"
integrity sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==
dependencies:
"@types/mdast" "^3.0.0"
"@types/unist" "^2.0.0"
unist-util-visit "^4.0.0"
mdast-util-from-markdown@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.0.tgz#0214124154f26154a2b3f9d401155509be45e894"
integrity sha512-HN3W1gRIuN/ZW295c7zi7g9lVBllMgZE40RxCX37wrTPWXCWtpvOZdfnuK+1WNpvZje6XuJeI3Wnb4TJEUem+g==
dependencies:
"@types/mdast" "^3.0.0"
"@types/unist" "^2.0.0"
decode-named-character-reference "^1.0.0"
mdast-util-to-string "^3.1.0"
micromark "^3.0.0"
micromark-util-decode-numeric-character-reference "^1.0.0"
micromark-util-decode-string "^1.0.0"
micromark-util-normalize-identifier "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
unist-util-stringify-position "^3.0.0"
uvu "^0.5.0"
mdast-util-to-hast@10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.0.1.tgz#0cfc82089494c52d46eb0e3edb7a4eb2aea021eb"
@ -19518,11 +19622,32 @@ mdast-util-to-hast@10.0.1:
unist-util-position "^3.0.0"
unist-util-visit "^2.0.0"
mdast-util-to-hast@^12.1.0:
version "12.3.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz#045d2825fb04374e59970f5b3f279b5700f6fb49"
integrity sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==
dependencies:
"@types/hast" "^2.0.0"
"@types/mdast" "^3.0.0"
mdast-util-definitions "^5.0.0"
micromark-util-sanitize-uri "^1.1.0"
trim-lines "^3.0.0"
unist-util-generated "^2.0.0"
unist-util-position "^4.0.0"
unist-util-visit "^4.0.0"
mdast-util-to-string@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz#27055500103f51637bd07d01da01eb1967a43527"
integrity sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==
mdast-util-to-string@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.1.1.tgz#db859050d79d48cf9896d294de06f3ede7474d16"
integrity sha512-tGvhT94e+cVnQt8JWE9/b3cUQZWS732TJxXHktvP+BYo62PpYD53Ls/6cC60rW21dW+txxiM4zMdc6abASvZKA==
dependencies:
"@types/mdast" "^3.0.0"
mdurl@^1.0.0, mdurl@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
@ -19644,6 +19769,201 @@ microevent.ts@~0.1.1:
resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0"
integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==
micromark-core-commonmark@^1.0.1:
version "1.0.6"
resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz#edff4c72e5993d93724a3c206970f5a15b0585ad"
integrity sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==
dependencies:
decode-named-character-reference "^1.0.0"
micromark-factory-destination "^1.0.0"
micromark-factory-label "^1.0.0"
micromark-factory-space "^1.0.0"
micromark-factory-title "^1.0.0"
micromark-factory-whitespace "^1.0.0"
micromark-util-character "^1.0.0"
micromark-util-chunked "^1.0.0"
micromark-util-classify-character "^1.0.0"
micromark-util-html-tag-name "^1.0.0"
micromark-util-normalize-identifier "^1.0.0"
micromark-util-resolve-all "^1.0.0"
micromark-util-subtokenize "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.1"
uvu "^0.5.0"
micromark-factory-destination@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz#fef1cb59ad4997c496f887b6977aa3034a5a277e"
integrity sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
micromark-factory-label@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz#6be2551fa8d13542fcbbac478258fb7a20047137"
integrity sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
uvu "^0.5.0"
micromark-factory-space@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz#cebff49968f2b9616c0fcb239e96685cb9497633"
integrity sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-types "^1.0.0"
micromark-factory-title@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz#7e09287c3748ff1693930f176e1c4a328382494f"
integrity sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==
dependencies:
micromark-factory-space "^1.0.0"
micromark-util-character "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
uvu "^0.5.0"
micromark-factory-whitespace@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz#e991e043ad376c1ba52f4e49858ce0794678621c"
integrity sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==
dependencies:
micromark-factory-space "^1.0.0"
micromark-util-character "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
micromark-util-character@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.1.0.tgz#d97c54d5742a0d9611a68ca0cd4124331f264d86"
integrity sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==
dependencies:
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
micromark-util-chunked@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz#5b40d83f3d53b84c4c6bce30ed4257e9a4c79d06"
integrity sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==
dependencies:
micromark-util-symbol "^1.0.0"
micromark-util-classify-character@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz#cbd7b447cb79ee6997dd274a46fc4eb806460a20"
integrity sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
micromark-util-combine-extensions@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz#91418e1e74fb893e3628b8d496085639124ff3d5"
integrity sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==
dependencies:
micromark-util-chunked "^1.0.0"
micromark-util-types "^1.0.0"
micromark-util-decode-numeric-character-reference@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz#dcc85f13b5bd93ff8d2868c3dba28039d490b946"
integrity sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==
dependencies:
micromark-util-symbol "^1.0.0"
micromark-util-decode-string@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz#942252ab7a76dec2dbf089cc32505ee2bc3acf02"
integrity sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==
dependencies:
decode-named-character-reference "^1.0.0"
micromark-util-character "^1.0.0"
micromark-util-decode-numeric-character-reference "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-encode@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz#2c1c22d3800870ad770ece5686ebca5920353383"
integrity sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==
micromark-util-html-tag-name@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.1.0.tgz#eb227118befd51f48858e879b7a419fc0df20497"
integrity sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==
micromark-util-normalize-identifier@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz#4a3539cb8db954bbec5203952bfe8cedadae7828"
integrity sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==
dependencies:
micromark-util-symbol "^1.0.0"
micromark-util-resolve-all@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz#a7c363f49a0162e931960c44f3127ab58f031d88"
integrity sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==
dependencies:
micromark-util-types "^1.0.0"
micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.1.0.tgz#f12e07a85106b902645e0364feb07cf253a85aee"
integrity sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-encode "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-subtokenize@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz#ff6f1af6ac836f8bfdbf9b02f40431760ad89105"
integrity sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==
dependencies:
micromark-util-chunked "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
uvu "^0.5.0"
micromark-util-symbol@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz#b90344db62042ce454f351cf0bebcc0a6da4920e"
integrity sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==
micromark-util-types@^1.0.0, micromark-util-types@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.0.2.tgz#f4220fdb319205812f99c40f8c87a9be83eded20"
integrity sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==
micromark@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.1.0.tgz#eeba0fe0ac1c9aaef675157b52c166f125e89f62"
integrity sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==
dependencies:
"@types/debug" "^4.0.0"
debug "^4.0.0"
decode-named-character-reference "^1.0.0"
micromark-core-commonmark "^1.0.1"
micromark-factory-space "^1.0.0"
micromark-util-character "^1.0.0"
micromark-util-chunked "^1.0.0"
micromark-util-combine-extensions "^1.0.0"
micromark-util-decode-numeric-character-reference "^1.0.0"
micromark-util-encode "^1.0.0"
micromark-util-normalize-identifier "^1.0.0"
micromark-util-resolve-all "^1.0.0"
micromark-util-sanitize-uri "^1.0.0"
micromark-util-subtokenize "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.1"
uvu "^0.5.0"
micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
version "3.1.10"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
@ -20128,6 +20448,11 @@ move-concurrently@^1.0.1:
rimraf "^2.5.4"
run-queue "^1.0.3"
mri@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
mrmime@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.0.tgz#14d387f0585a5233d291baba339b063752a2398b"
@ -22260,6 +22585,11 @@ property-information@^5.0.0, property-information@^5.3.0:
dependencies:
xtend "^4.0.0"
property-information@^6.0.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.2.0.tgz#b74f522c31c097b5149e3c3cb8d7f3defd986a1d"
integrity sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==
proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@ -23209,6 +23539,42 @@ react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-is@^18.0.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-markdown-editor-lite@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/react-markdown-editor-lite/-/react-markdown-editor-lite-1.3.4.tgz#77992d2389b9427a06595c63d95f52be66e5fea9"
integrity sha512-PhS4HzLzSgCsr8O9CfJX75nAYmZ0NwpfviLxARlT0Tau+APOerDSHSw3u9hub5wd0EqmonWibw0vhXXNu4ldRA==
dependencies:
"@babel/runtime" "^7.6.2"
classnames "^2.2.6"
eventemitter3 "^4.0.0"
uuid "^8.3.2"
react-markdown@^8.0.6:
version "8.0.6"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.6.tgz#3e939018f8bfce800ffdf22cf50aba3cdded7ad1"
integrity sha512-KgPWsYgHuftdx510wwIzpwf+5js/iHqBR+fzxefv8Khk3mFbnioF1bmL2idHN3ler0LMQmICKeDrWnZrX9mtbQ==
dependencies:
"@types/hast" "^2.0.0"
"@types/prop-types" "^15.0.0"
"@types/unist" "^2.0.0"
comma-separated-tokens "^2.0.0"
hast-util-whitespace "^2.0.0"
prop-types "^15.0.0"
property-information "^6.0.0"
react-is "^18.0.0"
remark-parse "^10.0.0"
remark-rehype "^10.0.0"
space-separated-tokens "^2.0.0"
style-to-object "^0.4.0"
unified "^10.0.0"
unist-util-visit "^4.0.0"
vfile "^5.0.0"
react-masonry-css@^1.0.16:
version "1.0.16"
resolved "https://registry.yarnpkg.com/react-masonry-css/-/react-masonry-css-1.0.16.tgz#72b28b4ae3484e250534700860597553a10f1a2c"
@ -23592,7 +23958,7 @@ regenerate@^1.4.2:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
regenerator-runtime@^0.13.10:
regenerator-runtime@^0.13.10, regenerator-runtime@^0.13.11:
version "0.13.11"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
@ -23781,6 +24147,25 @@ remark-parse@8.0.3:
vfile-location "^3.0.0"
xtend "^4.0.1"
remark-parse@^10.0.0:
version "10.0.1"
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.1.tgz#6f60ae53edbf0cf38ea223fe643db64d112e0775"
integrity sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==
dependencies:
"@types/mdast" "^3.0.0"
mdast-util-from-markdown "^1.0.0"
unified "^10.0.0"
remark-rehype@^10.0.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279"
integrity sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==
dependencies:
"@types/hast" "^2.0.0"
"@types/mdast" "^3.0.0"
mdast-util-to-hast "^12.1.0"
unified "^10.0.0"
remark-slug@^6.0.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/remark-slug/-/remark-slug-6.1.0.tgz#0503268d5f0c4ecb1f33315c00465ccdd97923ce"
@ -24136,6 +24521,13 @@ rxjs@^7.5.1:
dependencies:
tslib "^2.1.0"
sade@^1.7.3:
version "1.8.1"
resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701"
integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
dependencies:
mri "^1.1.0"
safe-buffer@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
@ -24796,6 +25188,11 @@ space-separated-tokens@^1.0.0:
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899"
integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==
space-separated-tokens@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f"
integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==
spark-md5@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc"
@ -25353,6 +25750,13 @@ style-to-object@0.3.0, style-to-object@^0.3.0:
dependencies:
inline-style-parser "0.1.1"
style-to-object@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.1.tgz#53cf856f7cf7f172d72939d9679556469ba5de37"
integrity sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==
dependencies:
inline-style-parser "0.1.1"
styled-jsx@5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.2.tgz#ff230fd593b737e9e68b630a694d460425478729"
@ -25985,6 +26389,11 @@ tree-kill@^1.2.2:
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
trim-lines@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==
trim-newlines@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
@ -26015,6 +26424,11 @@ trough@^1.0.0:
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
trough@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876"
integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==
ts-dedent@^2.0.0, ts-dedent@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
@ -26431,6 +26845,19 @@ unified@9.2.0:
trough "^1.0.0"
vfile "^4.0.0"
unified@^10.0.0:
version "10.1.2"
resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df"
integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==
dependencies:
"@types/unist" "^2.0.0"
bail "^2.0.0"
extend "^3.0.0"
is-buffer "^2.0.0"
is-plain-obj "^4.0.0"
trough "^2.0.0"
vfile "^5.0.0"
union-value@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
@ -26472,16 +26899,35 @@ unist-util-generated@^1.0.0:
resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.6.tgz#5ab51f689e2992a472beb1b35f2ce7ff2f324d4b"
integrity sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==
unist-util-generated@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.1.tgz#e37c50af35d3ed185ac6ceacb6ca0afb28a85cae"
integrity sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==
unist-util-is@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.1.0.tgz#976e5f462a7a5de73d94b706bac1b90671b57797"
integrity sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==
unist-util-is@^5.0.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9"
integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==
dependencies:
"@types/unist" "^2.0.0"
unist-util-position@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.1.0.tgz#1c42ee6301f8d52f47d14f62bbdb796571fa2d47"
integrity sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==
unist-util-position@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.4.tgz#93f6d8c7d6b373d9b825844645877c127455f037"
integrity sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==
dependencies:
"@types/unist" "^2.0.0"
unist-util-remove-position@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz#5d19ca79fdba712301999b2b73553ca8f3b352cc"
@ -26503,6 +26949,13 @@ unist-util-stringify-position@^2.0.0:
dependencies:
"@types/unist" "^2.0.2"
unist-util-stringify-position@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d"
integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==
dependencies:
"@types/unist" "^2.0.0"
unist-util-visit-parents@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz#65a6ce698f78a6b0f56aa0e88f13801886cdaef6"
@ -26511,6 +26964,14 @@ unist-util-visit-parents@^3.0.0:
"@types/unist" "^2.0.0"
unist-util-is "^4.0.0"
unist-util-visit-parents@^5.1.1:
version "5.1.3"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb"
integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==
dependencies:
"@types/unist" "^2.0.0"
unist-util-is "^5.0.0"
unist-util-visit@2.0.3, unist-util-visit@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c"
@ -26520,6 +26981,15 @@ unist-util-visit@2.0.3, unist-util-visit@^2.0.0:
unist-util-is "^4.0.0"
unist-util-visit-parents "^3.0.0"
unist-util-visit@^4.0.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2"
integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==
dependencies:
"@types/unist" "^2.0.0"
unist-util-is "^5.0.0"
unist-util-visit-parents "^5.1.1"
universal-user-agent@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"
@ -26796,6 +27266,16 @@ uuid@^9.0.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
uvu@^0.5.0:
version "0.5.6"
resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df"
integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==
dependencies:
dequal "^2.0.0"
diff "^5.0.0"
kleur "^4.0.3"
sade "^1.7.3"
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
@ -26883,6 +27363,14 @@ vfile-message@^2.0.0:
"@types/unist" "^2.0.0"
unist-util-stringify-position "^2.0.0"
vfile-message@^3.0.0:
version "3.1.4"
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea"
integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==
dependencies:
"@types/unist" "^2.0.0"
unist-util-stringify-position "^3.0.0"
vfile@^4.0.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624"
@ -26893,6 +27381,16 @@ vfile@^4.0.0:
unist-util-stringify-position "^2.0.0"
vfile-message "^2.0.0"
vfile@^5.0.0:
version "5.3.7"
resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7"
integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==
dependencies:
"@types/unist" "^2.0.0"
is-buffer "^2.0.0"
unist-util-stringify-position "^3.0.0"
vfile-message "^3.0.0"
vm-browserify@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"