summaryrefslogtreecommitdiff
path: root/components/Sidebar
diff options
context:
space:
mode:
authorThomas F. K. Jorna <[email protected]>2021-10-11 21:27:17 +0200
committerGitHub <[email protected]>2021-10-11 21:27:17 +0200
commit58b7030d45370072dee25214748670d6413343a9 (patch)
tree9632df7273415f4b197413c45ad11563af32d53a /components/Sidebar
parent89be3b67b2d10d35d72b5c54e1e166beeeef3095 (diff)
parent6e3dcf585c35620c6804f3c208e6882c29dfc17e (diff)
Merge pull request #101 from org-roam/sidebar
feat: Add file preview functionality
Diffstat (limited to 'components/Sidebar')
-rw-r--r--components/Sidebar/Backlinks.tsx74
-rw-r--r--components/Sidebar/Collapse.tsx146
-rw-r--r--components/Sidebar/Link.tsx241
-rw-r--r--components/Sidebar/Note.tsx73
-rw-r--r--components/Sidebar/OrgImage.tsx42
-rw-r--r--components/Sidebar/TagBar.tsx79
-rw-r--r--components/Sidebar/Toolbar.tsx103
-rw-r--r--components/Sidebar/index.tsx253
-rw-r--r--components/Sidebar/noteStyle.ts139
-rw-r--r--components/Sidebar/transition-utils.tsx144
10 files changed, 1294 insertions, 0 deletions
diff --git a/components/Sidebar/Backlinks.tsx b/components/Sidebar/Backlinks.tsx
new file mode 100644
index 0000000..d82fbba
--- /dev/null
+++ b/components/Sidebar/Backlinks.tsx
@@ -0,0 +1,74 @@
+import { LinksByNodeId, NodeByCite, NodeById } from '../../pages/index'
+
+import { GraphData, NodeObject, LinkObject } from 'force-graph'
+
+import { normalizeLinkEnds } from '../../pages/index'
+import { VStack, Box, Button, Heading, StackDivider } from '@chakra-ui/react'
+import React from 'react'
+import { ProcessedOrg } from '../../util/processOrg'
+
+export interface BacklinksProps {
+ previewNode: any
+ setPreviewNode: any
+ nodeById: NodeById
+ linksByNodeId: LinksByNodeId
+ nodeByCite: NodeByCite
+ setSidebarHighlightedNode: OrgRoamNode
+ openContextMenu: any
+}
+
+import { PreviewLink } from './Link'
+import { OrgRoamNode } from '../../api'
+
+export const Backlinks = (props: BacklinksProps) => {
+ const {
+ previewNode,
+ setPreviewNode,
+ setSidebarHighlightedNode,
+ nodeById,
+ linksByNodeId,
+ nodeByCite,
+ openContextMenu,
+ } = props
+ const links = linksByNodeId[previewNode?.id] ?? []
+
+ const backLinks = links
+ .filter((link: LinkObject) => {
+ const [source, target] = normalizeLinkEnds(link)
+ return source !== previewNode?.id
+ })
+ .map((l) => l.source)
+
+ return (
+ <Box>
+ <Heading pt={4}>{`Backlinks (${backLinks.length})`}</Heading>
+ <VStack
+ py={2}
+ spacing={3}
+ alignItems="start"
+ divider={<StackDivider borderColor="gray.500" />}
+ align="stretch"
+ color="gray.800"
+ >
+ {previewNode?.id &&
+ backLinks.map((link) => {
+ const title = nodeById[link as string]?.title ?? ''
+ return (
+ <Box overflow="hidden" p={3} bg="gray.300" width="100%" key={link}>
+ <PreviewLink
+ nodeByCite={nodeByCite}
+ setSidebarHighlightedNode={setSidebarHighlightedNode}
+ href={`id:${link as string}`}
+ nodeById={nodeById}
+ setPreviewNode={setPreviewNode}
+ openContextMenu={openContextMenu}
+ >
+ {nodeById[link as string]?.title}
+ </PreviewLink>
+ </Box>
+ )
+ })}
+ </VStack>
+ </Box>
+ )
+}
diff --git a/components/Sidebar/Collapse.tsx b/components/Sidebar/Collapse.tsx
new file mode 100644
index 0000000..b297605
--- /dev/null
+++ b/components/Sidebar/Collapse.tsx
@@ -0,0 +1,146 @@
+// modified from https://github.com/chakra-ui/chakra-ui/blob/fc3b97d0978cf2adb9fc79157c6e42b4b68155c5/packages/transition/src/collapse.tsx
+
+import { cx, mergeWith, warn, __DEV__ } from '@chakra-ui/utils'
+import { AnimatePresence, HTMLMotionProps, motion, Variants as _Variants } from 'framer-motion'
+import * as React from 'react'
+import { TransitionEasings, Variants, withDelay, WithTransitionConfig } from './transition-utils'
+
+const isNumeric = (value?: string | number) => value != null && parseInt(value.toString(), 10) > 0
+
+export interface CollapseOptions {
+ /**
+ * If `true`, the opacity of the content will be animated
+ * @default true
+ */
+ animateOpacity?: boolean
+ /**
+ * The size you want the content in its collapsed state.
+ * @default 0
+ */
+ startingSize?: number | string
+ /**
+ * The size you want the content in its expanded state.
+ * @default "auto"
+ */
+ endingSize?: number | string
+ /**
+ * The dimension you want to collapse by.
+ * @default "size"
+ */
+ dimension?: string
+}
+
+const defaultTransitions = {
+ exit: {
+ size: { duration: 0.2, ease: TransitionEasings.ease },
+ opacity: { duration: 0.3, ease: TransitionEasings.ease },
+ },
+ enter: {
+ size: { duration: 0.3, ease: TransitionEasings.ease },
+ opacity: { duration: 0.4, ease: TransitionEasings.ease },
+ },
+}
+
+const variants: Variants<CollapseOptions> = {
+ exit: ({ animateOpacity, startingSize, transition, transitionEnd, delay, dimension }) => ({
+ ...(animateOpacity && { opacity: isNumeric(startingSize) ? 1 : 0 }),
+ overflow: 'hidden',
+ [dimension as string]: startingSize,
+ transitionEnd: transitionEnd?.exit,
+ transition: transition?.exit ?? withDelay.exit(defaultTransitions.exit, delay),
+ }),
+ enter: ({ animateOpacity, endingSize, transition, transitionEnd, delay, dimension }) => ({
+ ...(animateOpacity && { opacity: 1 }),
+ [dimension as string]: endingSize,
+ transitionEnd: transitionEnd?.enter,
+ transition: transition?.enter ?? withDelay.enter(defaultTransitions.enter, delay),
+ }),
+}
+
+export type ICollapse = CollapseProps
+
+export interface CollapseProps
+ extends WithTransitionConfig<HTMLMotionProps<'div'>>,
+ CollapseOptions {}
+
+export const Collapse = React.forwardRef<HTMLDivElement, CollapseProps>((props, ref) => {
+ const {
+ in: isOpen,
+ unmountOnExit,
+ animateOpacity = true,
+ startingSize = 0,
+ endingSize = 'auto',
+ dimension = 'height',
+ style,
+ className,
+ transition,
+ transitionEnd,
+ ...rest
+ } = props
+
+ const [mounted, setMounted] = React.useState(false)
+ React.useEffect(() => {
+ const timeout = setTimeout(() => {
+ setMounted(true)
+ })
+ return () => clearTimeout(timeout)
+ }, [])
+
+ /**
+ * Warn 🚨: `startingSize` and `unmountOnExit` are mutually exclusive
+ *
+ * If you specify a starting size, the collapsed needs to be mounted
+ * for the size to take effect.
+ */
+ warn({
+ condition: Boolean(startingSize > 0 && unmountOnExit),
+ message: `startingSize and unmountOnExit are mutually exclusive. You can't use them together`,
+ })
+
+ const hasStartingSize = parseFloat(startingSize.toString()) > 0
+
+ const custom = {
+ startingSize,
+ endingSize,
+ animateOpacity,
+ dimension,
+ transition: !mounted ? { enter: { duration: 0 } } : transition,
+ transitionEnd: mergeWith(transitionEnd, {
+ enter: { overflow: 'initial' },
+ exit: unmountOnExit
+ ? undefined
+ : {
+ display: hasStartingSize ? 'block' : 'none',
+ },
+ }),
+ }
+
+ const show = unmountOnExit ? isOpen : true
+ const animate = isOpen || unmountOnExit ? 'enter' : 'exit'
+
+ return (
+ <AnimatePresence initial={false} custom={custom}>
+ {show && (
+ <motion.div
+ ref={ref}
+ {...rest}
+ className={cx('chakra-collapse', className)}
+ style={{
+ overflow: 'hidden',
+ display: 'block',
+ ...style,
+ }}
+ custom={custom}
+ variants={variants as _Variants}
+ initial={unmountOnExit ? 'exit' : false}
+ animate={animate}
+ exit="exit"
+ />
+ )}
+ </AnimatePresence>
+ )
+})
+
+if (__DEV__) {
+ Collapse.displayName = 'Collapse'
+}
diff --git a/components/Sidebar/Link.tsx b/components/Sidebar/Link.tsx
new file mode 100644
index 0000000..49fe9cf
--- /dev/null
+++ b/components/Sidebar/Link.tsx
@@ -0,0 +1,241 @@
+/* eslint-disable react/display-name */
+import {
+ Box,
+ Button,
+ Link,
+ Popover,
+ PopoverArrow,
+ PopoverBody,
+ PopoverCloseButton,
+ PopoverContent,
+ PopoverHeader,
+ PopoverTrigger,
+ Portal,
+ Text,
+ useTheme,
+} from '@chakra-ui/react'
+import React, { ReactElement, useContext, useEffect, useMemo, useState } from 'react'
+
+import unified from 'unified'
+//import createStream from 'unified-stream'
+import uniorgParse from 'uniorg-parse'
+import uniorg2rehype from 'uniorg-rehype'
+//import highlight from 'rehype-highlight'
+import katex from 'rehype-katex'
+import 'katex/dist/katex.css'
+import rehype2react from 'rehype-react'
+import { ThemeContext } from '../../util/themecontext'
+import { NodeByCite, NodeById } from '../../pages'
+
+export interface LinkProps {
+ href: any
+ children: any
+ previewNode?: any
+ setPreviewNode: any
+ setSidebarHighlightedNode: any
+ nodeByCite: NodeByCite
+ nodeById: NodeById
+ openContextMenu: any
+}
+
+export interface NormalLinkProps {
+ setPreviewNode: any
+ nodeById: NodeById
+ nodeByCite: NodeByCite
+ href: any
+ children: any
+ setSidebarHighlightedNode: any
+ openContextMenu: any
+}
+
+import { hexToRGBA, getThemeColor } from '../../pages/index'
+import noteStyle from './noteStyle'
+import { OrgImage } from './OrgImage'
+
+export const NormalLink = (props: NormalLinkProps) => {
+ const { setSidebarHighlightedNode, setPreviewNode, nodeById, openContextMenu, href, children } =
+ props
+ const { highlightColor } = useContext(ThemeContext)
+
+ const theme = useTheme()
+ const coolHighlightColor = getThemeColor(highlightColor, theme)
+ const [whatever, type, uri] = [...href.matchAll(/(.*?)\:(.*)/g)][0]
+ return (
+ <Text
+ onMouseEnter={() => setSidebarHighlightedNode(nodeById[uri])}
+ onMouseLeave={() => setSidebarHighlightedNode({})}
+ tabIndex={0}
+ display="inline"
+ overflow="hidden"
+ fontWeight={500}
+ color={highlightColor}
+ textDecoration="underline"
+ onContextMenu={(e) => {
+ e.preventDefault()
+ openContextMenu(nodeById[uri], e)
+ }}
+ onClick={() => setPreviewNode(nodeById[uri])}
+ // TODO don't hardcode the opacitycolor
+ _hover={{ textDecoration: 'none', cursor: 'pointer', bgColor: coolHighlightColor + '22' }}
+ _focus={{ outlineColor: highlightColor }}
+ >
+ {children}
+ </Text>
+ )
+}
+
+export const PreviewLink = (props: LinkProps) => {
+ const {
+ href,
+ children,
+ nodeById,
+ setSidebarHighlightedNode,
+ previewNode,
+ setPreviewNode,
+ nodeByCite,
+ openContextMenu,
+ } = props
+ // TODO figure out how to properly type this
+ // see https://github.com/rehypejs/rehype-react/issues/25
+ const [orgText, setOrgText] = useState<any>(null)
+ const [whatever, type, uri] = [...href.matchAll(/(.*?)\:(.*)/g)][0]
+ const [hover, setHover] = useState(false)
+
+ const getId = (type: string, uri: string) => {
+ if (type === 'id') {
+ return uri
+ }
+
+ if (type.includes('cite')) {
+ const node = nodeByCite[uri] ?? false
+ if (!node) {
+ return ''
+ }
+ if (node?.properties.FILELESS) {
+ return ''
+ }
+ return node?.id
+ }
+ return ''
+ }
+
+ const id = getId(type, uri)
+ const file = encodeURIComponent(encodeURIComponent(nodeById[id]?.file as string))
+
+ const processor = unified()
+ .use(uniorgParse)
+ .use(uniorg2rehype)
+ .use(katex)
+ .use(rehype2react, {
+ createElement: React.createElement,
+ components: {
+ // eslint-disable-next-line react/display-name
+ a: ({ children, href }) => (
+ <PreviewLink
+ nodeByCite={nodeByCite}
+ setSidebarHighlightedNode={setSidebarHighlightedNode}
+ href={href}
+ nodeById={nodeById}
+ setPreviewNode={setPreviewNode}
+ openContextMenu={openContextMenu}
+ >
+ {children}
+ </PreviewLink>
+ ),
+ img: ({ src }) => {
+ return <OrgImage src={src as string} file={nodeById[id]?.file as string} />
+ },
+ },
+ })
+
+ const getText = () => {
+ fetch(`http://localhost:35901/file/${file}`)
+ .then((res) => {
+ return res.text()
+ })
+ .then((res) => {
+ if (res !== 'error') {
+ const text = processor.processSync(res).result
+ setOrgText(text)
+ return
+ }
+ })
+ .catch((e) => {
+ console.log(e)
+ return 'Could not fetch the text for some reason, sorry!\n\n This can happen because you have an id with forward slashes (/) in it.'
+ })
+ }
+
+ useEffect(() => {
+ if (!!orgText) {
+ return
+ }
+ if (!hover) {
+ return
+ }
+ getText()
+ }, [hover, orgText])
+
+ if (id) {
+ return (
+ <>
+ <Popover gutter={12} trigger="hover" placement="top-start">
+ <PopoverTrigger>
+ <Box
+ display="inline"
+ onMouseEnter={() => setHover(true)}
+ onMouseLeave={() => setHover(false)}
+ >
+ <NormalLink
+ key={nodeById[id]?.title ?? id}
+ {...{
+ setSidebarHighlightedNode,
+ setPreviewNode,
+ nodeById,
+ href,
+ children,
+ nodeByCite,
+ openContextMenu,
+ }}
+ />
+ </Box>
+ </PopoverTrigger>
+ <Portal>
+ <PopoverContent
+ key={nodeById[id]?.title ?? id}
+ boxShadow="xl"
+ position="relative"
+ zIndex="tooltip"
+ onMouseEnter={() => {
+ setSidebarHighlightedNode(nodeById[id] ?? {})
+ }}
+ onMouseLeave={() => {
+ setSidebarHighlightedNode({})
+ }}
+ >
+ <PopoverArrow />
+ <PopoverBody
+ pb={5}
+ fontSize="xs"
+ px={5}
+ position="relative"
+ zIndex="tooltip"
+ maxHeight={300}
+ overflow="scroll"
+ >
+ <Box color="black" sx={noteStyle}>
+ {orgText}
+ </Box>
+ </PopoverBody>
+ </PopoverContent>
+ </Portal>
+ </Popover>
+ </>
+ )
+ }
+ return (
+ <Text display="inline" color="base.700" cursor="not-allowed">
+ {children}
+ </Text>
+ )
+}
diff --git a/components/Sidebar/Note.tsx b/components/Sidebar/Note.tsx
new file mode 100644
index 0000000..37e836d
--- /dev/null
+++ b/components/Sidebar/Note.tsx
@@ -0,0 +1,73 @@
+import React from 'react'
+import { NodeObject } from 'force-graph'
+
+import { NodeById, NodeByCite, LinksByNodeId } from '../../pages'
+import { Box, Flex } from '@chakra-ui/react'
+import { UniOrg } from '../../util/uniorg'
+import { Backlinks } from '../../components/Sidebar/Backlinks'
+import { noteStyle } from './noteStyle'
+
+export interface NoteProps {
+ setPreviewNode: any
+ previewNode: NodeObject
+ nodeById: NodeById
+ nodeByCite: NodeByCite
+ setSidebarHighlightedNode: any
+ justification: number
+ justificationList: string[]
+ linksByNodeId: LinksByNodeId
+ openContextMenu: any
+}
+
+export const Note = (props: NoteProps) => {
+ const {
+ setPreviewNode,
+ justificationList,
+ justification,
+ previewNode,
+ nodeById,
+ nodeByCite,
+ setSidebarHighlightedNode,
+ linksByNodeId,
+ openContextMenu,
+ } = props
+ return (
+ <Box
+ pr={8}
+ overflow="scroll"
+ height="100%"
+ className="org"
+ sx={{
+ ...noteStyle,
+
+ textAlign: justificationList[justification],
+ }}
+ >
+ {previewNode?.id && (
+ <Flex height="100%" flexDirection="column" justifyContent="space-between">
+ <UniOrg
+ {...{
+ setPreviewNode,
+ previewNode,
+ nodeById,
+ nodeByCite,
+ setSidebarHighlightedNode,
+ openContextMenu,
+ }}
+ />
+ <Backlinks
+ {...{
+ setPreviewNode,
+ previewNode,
+ nodeById,
+ linksByNodeId,
+ nodeByCite,
+ setSidebarHighlightedNode,
+ openContextMenu,
+ }}
+ />
+ </Flex>
+ )}
+ </Box>
+ )
+}
diff --git a/components/Sidebar/OrgImage.tsx b/components/Sidebar/OrgImage.tsx
new file mode 100644
index 0000000..f9f508a
--- /dev/null
+++ b/components/Sidebar/OrgImage.tsx
@@ -0,0 +1,42 @@
+import React, { useEffect, useState } from 'react'
+import Image from 'next/image'
+import path from 'path'
+//import '../../../public/placeholder.png'
+
+export interface OrgImageProps {
+ src: string
+ file: string
+}
+
+export const OrgImage = (props: OrgImageProps) => {
+ const { src, file } = props
+
+ const [image, setImage] = useState<any>(null)
+
+ /* )
+* .then((res) => res.blob())
+* .then((res) => setImage(res))
+* .catch((e) => {
+* setImage(null)
+* console.error(e)
+* })
+}, [fullPath]) */
+
+ const dir = path.dirname(file)
+ const fullPath = encodeURIComponent(encodeURIComponent(path.join(dir, src)))
+
+ const dumbLoader = ({ src, width, quality }: { [key: string]: string | number }) => {
+ return `http://localhost:35901/img/${src}`
+ }
+
+ return (
+ <Image
+ layout="responsive"
+ loader={dumbLoader}
+ src={fullPath}
+ alt=""
+ width="100%"
+ height="100%"
+ />
+ )
+}
diff --git a/components/Sidebar/TagBar.tsx b/components/Sidebar/TagBar.tsx
new file mode 100644
index 0000000..0fe0a18
--- /dev/null
+++ b/components/Sidebar/TagBar.tsx
@@ -0,0 +1,79 @@
+import React from 'react'
+import { initialFilter, TagColors } from '../config'
+import { NodeObject } from 'force-graph'
+import { OrgRoamNode } from '../../api'
+import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons'
+import { Flex, Tag, TagLabel, TagRightIcon } from '@chakra-ui/react'
+
+export interface TagBarProps {
+ filter: typeof initialFilter
+ setFilter: any
+ tagColors: TagColors
+ setTagColors: any
+ openContextMenu: any
+ previewNode: NodeObject
+}
+
+export const TagBar = (props: TagBarProps) => {
+ const { filter, setFilter, tagColors, setTagColors, openContextMenu, previewNode } = props
+
+ const node = previewNode as OrgRoamNode
+ if (!node.tags || node?.tags[0] === null) {
+ return null
+ }
+ return (
+ <Flex mb={2} flexWrap="wrap">
+ {node?.tags?.map((tag: string) => {
+ const bl: string[] = filter.tagsBlacklist ?? []
+ const wl: string[] = filter.tagsWhitelist ?? []
+ const blackList: boolean = bl.includes(tag)
+ const whiteList = wl.includes(tag)
+ return (
+ <Tag
+ tabIndex={0}
+ mr={2}
+ mt={2}
+ onContextMenu={(e) => {
+ e.preventDefault()
+ openContextMenu(tag, e)
+ }}
+ cursor="pointer"
+ onClick={() => {
+ if (blackList) {
+ setFilter((filter: typeof initialFilter) => ({
+ ...filter,
+ tagsBlacklist: filter.tagsBlacklist.filter((t) => t !== tag),
+ tagsWhitelist: [...filter.tagsWhitelist, tag],
+ }))
+ return
+ }
+ if (whiteList) {
+ setFilter((filter: typeof initialFilter) => ({
+ ...filter,
+ tagsWhitelist: filter.tagsWhitelist.filter((t) => t !== tag),
+ }))
+ return
+ }
+
+ setFilter((filter: typeof initialFilter) => ({
+ ...filter,
+ tagsBlacklist: [...filter.tagsBlacklist, tag],
+ }))
+ }}
+ size="sm"
+ key={tag}
+ variant="outline"
+ colorScheme={tagColors[tag]?.replaceAll(/(.*?)\..*/g, '$1') || undefined}
+ >
+ <TagLabel>{tag}</TagLabel>
+ {blackList ? (
+ <TagRightIcon as={ViewOffIcon} />
+ ) : whiteList ? (
+ <TagRightIcon as={ViewIcon} />
+ ) : null}
+ </Tag>
+ )
+ })}
+ </Flex>
+ )
+}
diff --git a/components/Sidebar/Toolbar.tsx b/components/Sidebar/Toolbar.tsx
new file mode 100644
index 0000000..6cbecae
--- /dev/null
+++ b/components/Sidebar/Toolbar.tsx
@@ -0,0 +1,103 @@
+import React from 'react'
+import { Text, Flex, IconButton, ButtonGroup, Tooltip } from '@chakra-ui/react'
+import {
+ BiAlignJustify,
+ BiAlignLeft,
+ BiAlignMiddle,
+ BiAlignRight,
+ BiFont,
+ BiRightIndent,
+} from 'react-icons/bi'
+import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'
+import { NodeObject } from 'force-graph'
+
+export interface ToolbarProps {
+ setJustification: any
+ justification: number
+ setIndent: any
+ setFont: any
+ setPreviewNode: any
+ canUndo: any
+ canRedo: any
+ resetPreviewNode: any
+ previousPreviewNode: any
+ nextPreviewNode: any
+}
+
+export const Toolbar = (props: ToolbarProps) => {
+ const {
+ setJustification,
+ setIndent,
+ setFont,
+ justification,
+ setPreviewNode,
+ canUndo,
+ canRedo,
+ resetPreviewNode,
+ previousPreviewNode,
+ nextPreviewNode,
+ } = props
+ return (
+ <Flex flex="0 1 40px" pb={3} alignItems="center" justifyContent="space-between" pl={1} pr={1}>
+ <Flex>
+ <ButtonGroup isAttached>
+ <Tooltip label="Go backward">
+ <IconButton
+ variant="subtle"
+ icon={<ChevronLeftIcon />}
+ aria-label="Previous node"
+ disabled={!canUndo}
+ onClick={() => previousPreviewNode()}
+ />
+ </Tooltip>
+ <Tooltip label="Go forward">
+ <IconButton
+ variant="subtle"
+ icon={<ChevronRightIcon />}
+ aria-label="Next node"
+ disabled={!canRedo}
+ onClick={() => nextPreviewNode()}
+ />
+ </Tooltip>
+ </ButtonGroup>
+ </Flex>
+ <Flex>
+ <Tooltip label="Justify content">
+ <IconButton
+ variant="subtle"
+ aria-label="Justify content"
+ icon={
+ [
+ <BiAlignJustify key="justify" />,
+ <BiAlignLeft key="left" />,
+ <BiAlignRight key="right" />,
+ <BiAlignMiddle key="center" />,
+ ][justification]
+ }
+ onClick={() => setJustification((curr: number) => (curr + 1) % 4)}
+ />
+ </Tooltip>
+ {/* <Tooltip label="Indent trees">
+ <IconButton
+ variant="subtle"
+ aria-label="Indent Text"
+ icon={<BiRightIndent />}
+ onClick={() => {
+ setIndent((curr: number) => (curr ? 0 : 1))
+ }}
+ />
+ </Tooltip>
+ <Tooltip label="Switch betwwen sans and serif">
+ <IconButton
+ variant="subtle"
+ aria-label="Change font"
+ icon={<BiFont />}
+ onClick={() => {
+ setFont((curr: string) => (curr === 'sans serif' ? 'serif' : 'sans serif'))
+ }}
+ />
+ </Tooltip> */}
+ </Flex>
+ </Flex>
+ )
+}
diff --git a/components/Sidebar/index.tsx b/components/Sidebar/index.tsx
new file mode 100644
index 0000000..957669c
--- /dev/null
+++ b/components/Sidebar/index.tsx
@@ -0,0 +1,253 @@
+import React, { useContext, useEffect, useRef, useState } from 'react'
+
+import { Toolbar } from './Toolbar'
+import { TagBar } from './TagBar'
+import { Note } from './Note'
+
+import {
+ Button,
+ Slide,
+ VStack,
+ Flex,
+ Heading,
+ Box,
+ IconButton,
+ Tooltip,
+ HStack,
+ TagLabel,
+ Tag,
+ TagRightIcon,
+} from '@chakra-ui/react'
+import { Collapse } from './Collapse'
+import { Scrollbars } from 'react-custom-scrollbars-2'
+import {
+ ChevronLeftIcon,
+ ChevronRightIcon,
+ CloseIcon,
+ HamburgerIcon,
+ ViewIcon,
+ ViewOffIcon,
+} from '@chakra-ui/icons'
+import { BiDotsVerticalRounded, BiFile, BiNetworkChart } from 'react-icons/bi'
+import { BsReverseLayoutSidebarInsetReverse } from 'react-icons/bs'
+
+import { GraphData, NodeObject, LinkObject } from 'force-graph'
+import { OrgRoamNode } from '../../api'
+import { ThemeContext } from '../../util/themecontext'
+import { LinksByNodeId, NodeByCite, NodeById, Scope } from '../../pages/index'
+import { Resizable } from 're-resizable'
+import { usePersistantState } from '../../util/persistant-state'
+import { initialFilter, TagColors } from '../config'
+
+export interface SidebarProps {
+ isOpen: boolean
+ onClose: any
+ onOpen: any
+ nodeById: NodeById
+ previewNode: NodeObject
+ setPreviewNode: any
+ linksByNodeId: LinksByNodeId
+ nodeByCite: NodeByCite
+ setSidebarHighlightedNode: any
+ canUndo: any
+ canRedo: any
+ resetPreviewNode: any
+ previousPreviewNode: any
+ nextPreviewNode: any
+ openContextMenu: any
+ scope: Scope
+ setScope: any
+ windowWidth: number
+ filter: typeof initialFilter
+ setFilter: any
+ tagColors: TagColors
+ setTagColors: any
+}
+
+const Sidebar = (props: SidebarProps) => {
+ const {
+ isOpen,
+ onOpen,
+ onClose,
+ previewNode,
+ setPreviewNode,
+ nodeById,
+ linksByNodeId,
+ nodeByCite,
+ setSidebarHighlightedNode,
+ canUndo,
+ canRedo,
+ resetPreviewNode,
+ previousPreviewNode,
+ nextPreviewNode,
+ openContextMenu,
+ scope,
+ setScope,
+ windowWidth,
+ filter,
+ setFilter,
+ tagColors,
+ setTagColors,
+ } = props
+
+ const { highlightColor } = useContext(ThemeContext)
+ const [previewRoamNode, setPreviewRoamNode] = useState<OrgRoamNode>()
+ const [sidebarWidth, setSidebarWidth] = usePersistantState<number>('sidebarWidth', 400)
+
+ useEffect(() => {
+ if (!previewNode?.id) {
+ onClose()
+ return
+ }
+ onOpen()
+ setPreviewRoamNode(previewNode as OrgRoamNode)
+ }, [previewNode?.id])
+
+ const [justification, setJustification] = useState(1)
+ const justificationList = ['justify', 'start', 'end', 'center']
+ const [font, setFont] = useState('sans serif')
+ const [indent, setIndent] = useState(0)
+ //maybe want to close it when clicking outside, but not sure
+ //const outsideClickRef = useRef();
+ return (
+ <Collapse
+ animateOpacity={false}
+ dimension="width"
+ in={isOpen}
+ //style={{ position: 'relative' }}
+ unmountOnExit
+ startingSize={0}
+ style={{ height: '100vh' }}
+ >
+ <Resizable
+ size={{ height: '100vh', width: sidebarWidth }}
+ onResizeStop={(e, direction, ref, d) => {
+ setSidebarWidth((curr: number) => curr + d.width)
+ }}
+ enable={{
+ top: false,
+ right: false,
+ bottom: false,
+ left: true,
+ topRight: false,
+ bottomRight: false,
+ bottomLeft: false,
+ topLeft: false,
+ }}
+ minWidth="220px"
+ maxWidth={windowWidth - 200}
+ >
+ <Flex flexDir="column" h="100vh" pl={2} color="black" bg="alt.100" width="100%">
+ <Flex
+ //whiteSpace="nowrap"
+ // overflow="hidden"
+ // textOverflow="ellipsis"
+ pl={4}
+ alignItems="center"
+ color="black"
+ width="100%"
+ >
+ <Flex flexShrink={0}>
+ <BiFile
+ onContextMenu={(e) => {
+ e.preventDefault()
+ openContextMenu(previewNode, e)
+ }}
+ />
+ </Flex>
+ <Flex
+ whiteSpace="nowrap"
+ textOverflow="ellipsis"
+ overflow="hidden"
+ onContextMenu={(e) => {
+ e.preventDefault()
+ openContextMenu(previewNode, e)
+ }}
+ >
+ <Heading
+ pl={2}
+ whiteSpace="nowrap"
+ textOverflow="ellipsis"
+ overflow="hidden"
+ lineHeight={1}
+ size="sm"
+ fontWeight={600}
+ color={'gray.800'}
+ >
+ {previewRoamNode?.title}
+ </Heading>
+ </Flex>
+ <Flex flexDir="row" ml="auto">
+ <IconButton
+ // eslint-disable-next-line react/jsx-no-undef
+ m={1}
+ icon={<BiDotsVerticalRounded />}
+ aria-label="Options"
+ variant="subtle"
+ onClick={(e) => {
+ openContextMenu(previewNode, e, {
+ left: undefined,
+ top: 12,
+ right: -windowWidth + 20,
+ bottom: undefined,
+ })
+ }}
+ />
+ </Flex>
+ </Flex>
+ <Toolbar
+ {...{
+ setJustification,
+ setIndent,
+ setFont,
+ justification,
+ setPreviewNode,
+ canUndo,
+ canRedo,
+ resetPreviewNode,
+ previousPreviewNode,
+ nextPreviewNode,
+ }}
+ />
+ {/* <Scrollbars
+ * //autoHeight
+ * autoHeightMax={600}
+ * autoHide
+ * renderThumbVertical={({ style, ...props }) => (
+ * <Box
+ * style={{
+ * ...style,
+ * borderRadius: 0,
+ * // backgroundColor: highlightColor,
+ * }}
+ * //color="alt.100"
+ * {...props}
+ * />
+ * )}
+ * > */}
+ <VStack flexGrow={1} overflow="scroll" alignItems="left" bg="alt.100" paddingLeft={4}>
+ <TagBar
+ {...{ filter, setFilter, tagColors, setTagColors, openContextMenu, previewNode }}
+ />
+ <Note
+ {...{
+ setPreviewNode,
+ previewNode,
+ nodeById,
+ nodeByCite,
+ setSidebarHighlightedNode,
+ justification,
+ justificationList,
+ linksByNodeId,
+ openContextMenu,
+ }}
+ />
+ </VStack>
+ {/*</Scrollbars>*/}
+ </Flex>
+ </Resizable>
+ </Collapse>
+ )
+}
+
+export default Sidebar
diff --git a/components/Sidebar/noteStyle.ts b/components/Sidebar/noteStyle.ts
new file mode 100644
index 0000000..42ecaf6
--- /dev/null
+++ b/components/Sidebar/noteStyle.ts
@@ -0,0 +1,139 @@
+export const noteStyle = {
+ '.katex': { overflowX: 'scroll' },
+ h1: { color: 'black', lineHeight: '1.2', fontSize: '20', fontWeight: 'bold', marginBottom: 3 },
+ h2: {
+ fontSize: '18',
+ marginBottom: 2,
+ color: 'black',
+ },
+ h3: {
+ fontSize: '16',
+ fontWeight: '600 !important',
+ marginBottom: '.5em',
+
+ color: 'black',
+ },
+ h4: {
+ fontSize: '14',
+ fontWeight: '500 !important',
+ marginBottom: '.25em',
+ fontStyle: 'italic',
+ color: 'black',
+ },
+ ol: {
+ paddingLeft: '5',
+ },
+ ul: {
+ paddingLeft: '5',
+ },
+ p: {
+ fontSize: '14',
+ fontWeight: '500 !important',
+ paddingBottom: '.5em',
+ },
+ div: {
+ hyphens: 'auto !important',
+ },
+ '.title': {
+ textAlign: 'center',
+ marginBottom: '.2em',
+ },
+ '.subtitle': {
+ textAlign: 'center',
+ fontSize: 'medium',
+ fontWeight: 'bold',
+ marginTop: 0,
+ },
+ '.TODO': { color: 'red.500' },
+ '.equationContainer': {
+ display: 'table',
+ textAlign: 'center',
+ width: '100%',
+ },
+ '.equation': {
+ verticalAlign: 'middle',
+ },
+ '.equation-label': {
+ display: 'tableCell',
+ textAlign: 'right',
+ verticalAlign: 'middle',
+ },
+ '.inlinetask': {
+ padding: '10px',
+ border: '2px solid gray',
+ margin: '10px',
+ background: '#ffffcc',
+ },
+ '#org-div-home-and-up': {
+ textAlign: 'right',
+ fontSize: '70 % ',
+ whiteSpace: 'nowrap',
+ },
+ textarea: { overflowX: 'auto' },
+ '.linenr': { fontSize: 'smaller' },
+ '.org-info-js_info-navigation': { borderStyle: 'none' },
+ '#org-info-js_console-label': {
+ fontSize: '10px',
+ fontWeight: 'bold',
+ whiteSpace: 'nowrap',
+ },
+ '.org-info-js_search-highlight': {
+ backgroundColor: '#ffff00',
+ color: '#000000',
+ fontWeight: 'bold',
+ },
+ '.org-svg': { width: '90%' },
+ '.DONE': { color: 'green' },
+ '.priority': { fontFamily: 'monospace', color: 'orange' },
+ '.tag': {
+ backgroundColor: 'white',
+ fontFamily: 'monospace',
+ padding: '2px',
+ fontSize: '80%',
+ fontWeight: 'normal',
+ },
+ '.timestamp': { color: '#bebebe' },
+ '.timestamp-kwd': { color: '#5f9ea0' },
+ '.org-right': { marginLeft: 'auto', marginRight: '0px', textAlign: 'right' },
+ '.org-left': { marginLeft: '0px', marginRight: 'auto', textAlign: 'left' },
+ '.org-center': { marginLeft: 'auto', marginRight: 'auto', textAlign: 'center' },
+ '.underline': { textDecoration: 'underline' },
+ '#postamble p': { fontSize: '90%', margin: '.2em' },
+ '#preamble p': { fontSize: '90%', margin: '.2em' },
+ 'p.verse': { marginLeft: '3%' },
+ pre: {
+ //border: '1px solid #e6e6e6',
+ borderRadius: '3px',
+ backgroundColor: 'white',
+ padding: '8pt',
+ fontFamily: 'monospace',
+ overflow: 'auto',
+ margin: '1.2em',
+ },
+ 'pre.src': {
+ position: 'relative',
+ overflow: 'auto',
+ },
+ 'pre.src:before': {
+ display: 'none',
+ position: 'absolute',
+ top: '-8px',
+ right: '12px',
+ padding: '3px',
+ //color: '#555',
+ backgroundColor: 'white',
+ },
+ 'caption.t-above': { captionSide: 'top' },
+ 'caption.t-bottom': { captionSide: 'bottom' },
+ 'th.org-right': { textAlign: 'center' },
+ 'th.org-left': { textAlign: 'center' },
+ 'th.org-center': { textAlign: 'center' },
+ 'td.org-right': { textAlign: 'right' },
+ 'td.org-left': { textAlign: 'left' },
+ 'td.org-center': { textAlign: 'center' },
+ '.footpara': { display: 'inline' },
+ '.footdef': { marginBottom: '1em' },
+ '.figure': { padding: '1em' },
+ '.figure p': { textAlign: 'center' },
+}
+export default noteStyle
diff --git a/components/Sidebar/transition-utils.tsx b/components/Sidebar/transition-utils.tsx
new file mode 100644
index 0000000..68634bb
--- /dev/null
+++ b/components/Sidebar/transition-utils.tsx
@@ -0,0 +1,144 @@
+// Taken from https://github.com/chakra-ui/chakra-ui/blob/fc3b97d0978cf2adb9fc79157c6e42b4b68155c5/packages/transition/src/transition-utils.ts
+import { isNumber } from '@chakra-ui/utils'
+import { Target, TargetAndTransition, Transition } from 'framer-motion'
+
+type TargetResolver<P = {}> = (
+ props: P & {
+ transition?: TransitionConfig
+ transitionEnd?: TransitionEndConfig
+ delay?: number | DelayConfig
+ },
+) => TargetAndTransition
+
+type Variant<P = {}> = TargetAndTransition | TargetResolver<P>
+
+export type Variants<P = {}> = {
+ enter: Variant<P>
+ exit: Variant<P>
+ initial?: Variant<P>
+}
+
+type WithMotionState<P> = Partial<Record<'enter' | 'exit', P>>
+
+export type TransitionConfig = WithMotionState<Transition>
+
+export type TransitionEndConfig = WithMotionState<Target>
+
+export type DelayConfig = WithMotionState<number>
+
+export const TransitionEasings = {
+ ease: [0.25, 0.1, 0.25, 1],
+ easeIn: [0.4, 0, 1, 1],
+ easeOut: [0, 0, 0.2, 1],
+ easeInOut: [0.4, 0, 0.2, 1],
+} as const
+
+export const TransitionVariants = {
+ scale: {
+ enter: { scale: 1 },
+ exit: { scale: 0.95 },
+ },
+ fade: {
+ enter: { opacity: 1 },
+ exit: { opacity: 0 },
+ },
+ pushLeft: {
+ enter: { x: '100%' },
+ exit: { x: '-30%' },
+ },
+ pushRight: {
+ enter: { x: '-100%' },
+ exit: { x: '30%' },
+ },
+ pushUp: {
+ enter: { y: '100%' },
+ exit: { y: '-30%' },
+ },
+ pushDown: {
+ enter: { y: '-100%' },
+ exit: { y: '30%' },
+ },
+ slideLeft: {
+ position: { left: 0, top: 0, bottom: 0, width: '100%' },
+ enter: { x: 0 },
+ exit: { x: '-100%' },
+ },
+ slideRight: {
+ position: { right: 0, top: 0, bottom: 0, width: '100%' },
+ enter: { x: 0 },
+ exit: { x: '100%' },
+ },
+ slideUp: {
+ position: { top: 0, left: 0, right: 0, maxWidth: '100vw' },
+ enter: { y: 0 },
+ exit: { y: '-100%' },
+ },
+ slideDown: {
+ position: { bottom: 0, left: 0, right: 0, maxWidth: '100vw' },
+ enter: { y: 0 },
+ exit: { y: '100%' },
+ },
+}
+
+export type SlideDirection = 'top' | 'left' | 'bottom' | 'right'
+
+export function slideTransition(options?: { direction?: SlideDirection }) {
+ const side = options?.direction ?? 'right'
+ switch (side) {
+ case 'right':
+ return TransitionVariants.slideRight
+ case 'left':
+ return TransitionVariants.slideLeft
+ case 'bottom':
+ return TransitionVariants.slideDown
+ case 'top':
+ return TransitionVariants.slideUp
+ default:
+ return TransitionVariants.slideRight
+ }
+}
+
+export const TransitionDefaults = {
+ enter: {
+ duration: 0.2,
+ ease: TransitionEasings.easeOut,
+ },
+ exit: {
+ duration: 0.1,
+ ease: TransitionEasings.easeIn,
+ },
+} as const
+
+export type WithTransitionConfig<P extends object> = Omit<P, 'transition'> & {
+ /**
+ * If `true`, the element will unmount when `in={false}` and animation is done
+ */
+ unmountOnExit?: boolean
+ /**
+ * Show the component; triggers the enter or exit states
+ */
+ in?: boolean
+ /**
+ * Custom `transition` definition for `enter` and `exit`
+ */
+ transition?: TransitionConfig
+ /**
+ * Custom `transitionEnd` definition for `enter` and `exit`
+ */
+ transitionEnd?: TransitionEndConfig
+ /**
+ * Custom `delay` definition for `enter` and `exit`
+ */
+ delay?: number | DelayConfig
+}
+
+export const withDelay = {
+ enter: (transition: Transition, delay?: number | DelayConfig) => ({
+ ...transition,
+ delay: isNumber(delay) ? delay : delay?.['enter'],
+ }),
+ exit: (transition: Transition, delay?: number | DelayConfig) => ({
+ ...transition,
+ delay: isNumber(delay) ? delay : delay?.['exit'],
+ }),
+}