summaryrefslogtreecommitdiff
path: root/components
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
parent89be3b67b2d10d35d72b5c54e1e166beeeef3095 (diff)
parent6e3dcf585c35620c6804f3c208e6882c29dfc17e (diff)
Merge pull request #101 from org-roam/sidebar
feat: Add file preview functionality
Diffstat (limited to 'components')
-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
-rw-r--r--components/TagMenu.tsx117
-rw-r--r--components/Tweaks/BehaviorPanel.tsx22
-rw-r--r--components/Tweaks/ColorMenu.tsx73
-rw-r--r--components/Tweaks/SliderWithInfo.tsx2
-rw-r--r--components/Tweaks/index.tsx27
-rw-r--r--components/config.ts14
-rw-r--r--components/contextmenu.tsx249
17 files changed, 1644 insertions, 154 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'],
+ }),
+}
diff --git a/components/TagMenu.tsx b/components/TagMenu.tsx
new file mode 100644
index 0000000..432e9a2
--- /dev/null
+++ b/components/TagMenu.tsx
@@ -0,0 +1,117 @@
+import { MinusIcon, AddIcon, ViewOffIcon, ViewIcon } from '@chakra-ui/icons'
+import {
+ Text,
+ Box,
+ Button,
+ Flex,
+ Menu,
+ MenuButton,
+ MenuItem,
+ MenuList,
+ Portal,
+ useDisclosure,
+} from '@chakra-ui/react'
+import React from 'react'
+import { colorList, initialFilter, TagColors } from './config'
+import { Collapse } from './Sidebar/Collapse'
+import { ColorMenu } from './Tweaks/ColorMenu'
+
+export interface TagMenuProps {
+ setTagColors: any
+ tagColors: TagColors
+ setFilter: any
+ filter: typeof initialFilter
+ target: string | null
+}
+
+export const TagMenu = (props: TagMenuProps) => {
+ const { setTagColors, setFilter, filter, tagColors, target } = props
+ const bl: string[] = filter.tagsBlacklist
+ const wl: string[] = filter.tagsWhitelist
+ const blacklist = bl.indexOf(target as string) > -1
+ const whitelist = wl.indexOf(target as string) > -1
+ const colors = useDisclosure()
+ return (
+ <>
+ <MenuItem
+ icon={
+ <Box
+ bgColor={tagColors[target as string]}
+ borderRadius="sm"
+ height={3}
+ width={3}
+ borderColor={tagColors[target as string] || 'gray.600'}
+ borderWidth={1}
+ ></Box>
+ }
+ closeOnSelect={false}
+ onClick={colors.onToggle}
+ >
+ <Text>Change color</Text>
+ </MenuItem>
+ <Collapse in={colors.isOpen}>
+ <Flex ml={2} mt={2} flexWrap="wrap">
+ {colorList.map((color: string) => (
+ <Box key={color}>
+ <Box
+ tabIndex={0}
+ cursor="pointer"
+ onClick={() =>
+ setTagColors({
+ ...tagColors,
+ [target as string]: color,
+ })
+ }
+ bgColor={color}
+ m={1}
+ borderRadius="sm"
+ height={3}
+ width={3}
+ ></Box>
+ </Box>
+ ))}
+ </Flex>
+ </Collapse>
+ {!whitelist && (
+ <MenuItem
+ onClick={() => {
+ if (!blacklist) {
+ setFilter((filter: typeof initialFilter) => ({
+ ...filter,
+ tagsBlacklist: [...filter.tagsBlacklist, target],
+ }))
+ return
+ }
+ setFilter((filter: typeof initialFilter) => ({
+ ...filter,
+ tagsBlacklist: filter.tagsBlacklist.filter((t) => t !== target),
+ }))
+ }}
+ icon={blacklist ? <MinusIcon /> : <ViewOffIcon />}
+ >
+ {blacklist ? 'Remove from blacklist' : 'Add to blacklist'}
+ </MenuItem>
+ )}
+ {!blacklist && (
+ <MenuItem
+ onClick={() => {
+ if (!whitelist) {
+ setFilter((filter: typeof initialFilter) => ({
+ ...filter,
+ tagsWhitelist: [...filter.tagsWhitelist, target],
+ }))
+ return
+ }
+ setFilter((filter: typeof initialFilter) => ({
+ ...filter,
+ tagsWhitelist: filter.tagsWhitelist.filter((t) => t !== target),
+ }))
+ }}
+ icon={whitelist ? <MinusIcon /> : <ViewIcon />}
+ >
+ {whitelist ? 'Remove from whitelist' : 'Add to whitelist'}
+ </MenuItem>
+ )}
+ </>
+ )
+}
diff --git a/components/Tweaks/BehaviorPanel.tsx b/components/Tweaks/BehaviorPanel.tsx
index 8edb986..5d61730 100644
--- a/components/Tweaks/BehaviorPanel.tsx
+++ b/components/Tweaks/BehaviorPanel.tsx
@@ -10,6 +10,8 @@ import {
StackDivider,
VStack,
Text,
+ Box,
+ Switch,
} from '@chakra-ui/react'
import React from 'react'
import { initialBehavior, initialMouse } from '../config'
@@ -34,6 +36,26 @@ export const BehaviorPanel = (props: BehaviorPanelProps) => {
color="gray.800"
>
<Flex alignItems="center" justifyContent="space-between">
+ <Text>Preview node</Text>
+ <Menu isLazy placement="right">
+ <MenuButton as={Button} rightIcon={<ChevronDownIcon />} colorScheme="" color="black">
+ <Text>
+ {mouse.preview ? mouse.preview[0]!.toUpperCase() + mouse.preview!.slice(1) : 'Never'}
+ </Text>
+ </MenuButton>
+ <Portal>
+ {' '}
+ <MenuList bgColor="gray.200" zIndex="popover">
+ <MenuItem onClick={() => setMouse({ ...mouse, preview: '' })}>Never</MenuItem>
+ <MenuItem onClick={() => setMouse({ ...mouse, preview: 'click' })}>Click</MenuItem>
+ <MenuItem onClick={() => setMouse({ ...mouse, preview: 'double' })}>
+ Double Click
+ </MenuItem>
+ </MenuList>
+ </Portal>
+ </Menu>
+ </Flex>
+ <Flex alignItems="center" justifyContent="space-between">
<Flex>
<Text>Expand Node</Text>
<InfoTooltip infoText="View only the node and its direct neighbors" />
diff --git a/components/Tweaks/ColorMenu.tsx b/components/Tweaks/ColorMenu.tsx
index 3d90e36..b87f76b 100644
--- a/components/Tweaks/ColorMenu.tsx
+++ b/components/Tweaks/ColorMenu.tsx
@@ -9,6 +9,9 @@ import {
MenuItem,
MenuList,
Portal,
+ PopoverTrigger,
+ PopoverContent,
+ Popover,
} from '@chakra-ui/react'
import React, { useCallback } from 'react'
import { initialVisuals } from '../config'
@@ -19,10 +22,11 @@ export interface ColorMenuProps {
value: string
visValue: string
setVisuals?: any
+ noEmpty?: boolean
}
export const ColorMenu = (props: ColorMenuProps) => {
- const { label, colorList, value, visValue, setVisuals } = props
+ const { label, colorList, value, visValue, setVisuals, noEmpty } = props
const clickCallback = useCallback(
(color) =>
@@ -37,35 +41,48 @@ export const ColorMenu = (props: ColorMenuProps) => {
return (
<Flex alignItems="center" justifyContent="space-between">
<Text>{label}</Text>
- <Menu isLazy placement="right">
- <MenuButton as={Button} colorScheme="" color="black" rightIcon={<ChevronDownIcon />}>
- {<Box bgColor={visValue} borderRadius="sm" height={6} width={6}></Box>}
- </MenuButton>
+ <Popover isLazy placement="right">
+ <PopoverTrigger>
+ <Button colorScheme="" color="black" rightIcon={<ChevronDownIcon />}>
+ {<Box bgColor={visValue} borderRadius="sm" height={6} width={6}></Box>}
+ </Button>
+ </PopoverTrigger>
<Portal>
- {' '}
- <MenuList minW={10} zIndex="popover" bgColor="gray.200">
- <MenuItem
- onClick={() => clickCallback('')}
- justifyContent="space-between"
- alignItems="center"
- display="flex"
- >
- <Box height={6} width={6}></Box>
- </MenuItem>
- {colorList.map((color: string) => (
- <MenuItem
- key={color}
- onClick={() => clickCallback(color)}
- justifyContent="space-between"
- alignItems="center"
- display="flex"
- >
- <Box bgColor={color} borderRadius="sm" height={6} width={6}></Box>
- </MenuItem>
- ))}
- </MenuList>
+ <PopoverContent zIndex="tooltip" maxW={36} position="relative">
+ <Flex flexWrap="wrap" bgColor="gray.200">
+ {!noEmpty && (
+ <Box
+ onClick={() => clickCallback('')}
+ justifyContent="space-between"
+ alignItems="center"
+ display="flex"
+ m={1}
+ >
+ <Box
+ height={6}
+ width={6}
+ borderColor="gray.600"
+ borderRadius="xl"
+ borderWidth={1}
+ ></Box>
+ </Box>
+ )}
+ {colorList.map((color: string) => (
+ <Box
+ m={1}
+ key={color}
+ onClick={() => clickCallback(color)}
+ justifyContent="space-between"
+ alignItems="center"
+ display="flex"
+ >
+ <Box bgColor={color} borderRadius="xl" height={6} width={6}></Box>
+ </Box>
+ ))}
+ </Flex>
+ </PopoverContent>
</Portal>
- </Menu>
+ </Popover>
</Flex>
)
}
diff --git a/components/Tweaks/SliderWithInfo.tsx b/components/Tweaks/SliderWithInfo.tsx
index f70faae..9d6903a 100644
--- a/components/Tweaks/SliderWithInfo.tsx
+++ b/components/Tweaks/SliderWithInfo.tsx
@@ -30,7 +30,7 @@ export const SliderWithInfo = ({
const { onChange, label, infoText } = rest
const { highlightColor } = useContext(ThemeContext)
return (
- <Box key={label}>
+ <Box key={label} pt={1} pb={2}>
<Box display="flex" alignItems="flex-end">
<Text>{label}</Text>
{infoText && <InfoTooltip infoText={infoText} />}
diff --git a/components/Tweaks/index.tsx b/components/Tweaks/index.tsx
index c60e670..afd0ea7 100644
--- a/components/Tweaks/index.tsx
+++ b/components/Tweaks/index.tsx
@@ -68,6 +68,7 @@ export const Tweaks = (props: TweakProps) => {
tagColors,
setTagColors,
} = props
+
const [showTweaks, setShowTweaks] = usePersistantState('showTweaks', false)
const { highlightColor, setHighlightColor } = useContext(ThemeContext)
@@ -75,12 +76,12 @@ export const Tweaks = (props: TweakProps) => {
<Box
position="absolute"
zIndex="overlay"
- marginTop={10}
- marginLeft={10}
+ marginTop={1}
+ marginLeft={0}
display={showTweaks ? 'none' : 'block'}
>
<IconButton
- variant="ghost"
+ variant="subtle"
aria-label="Settings"
icon={<SettingsIcon />}
onClick={() => setShowTweaks(true)}
@@ -88,17 +89,17 @@ export const Tweaks = (props: TweakProps) => {
</Box>
) : (
<Box
+ position="absolute"
bg="alt.100"
w="xs"
- marginTop={10}
- marginLeft={10}
- borderRadius="xl"
+ marginTop={2}
+ marginLeft={2}
+ borderRadius="lg"
paddingBottom={5}
- zIndex={300}
- position="relative"
+ zIndex={10}
boxShadow="xl"
- maxH={0.92 * globalThis.innerHeight}
- marginBottom={10}
+ maxH={'95vh'}
+ fontSize="sm"
>
<Box
display="flex"
@@ -108,7 +109,7 @@ export const Tweaks = (props: TweakProps) => {
paddingTop={1}
>
<Tooltip label={'Switch to ' + threeDim ? '2D' : '3D' + ' view'}>
- <Button onClick={() => setThreeDim(!threeDim)} variant="ghost" zIndex="overlay">
+ <Button onClick={() => setThreeDim(!threeDim)} variant="subtle" zIndex="overlay">
{threeDim ? '3D' : '2D'}
</Button>
</Tooltip>
@@ -124,7 +125,7 @@ export const Tweaks = (props: TweakProps) => {
setPhysics(initialPhysics)
setBehavior(initialBehavior)
}}
- variant="none"
+ variant="subtle"
size="sm"
/>
</Tooltip>
@@ -132,7 +133,7 @@ export const Tweaks = (props: TweakProps) => {
size="sm"
icon={<CloseIcon />}
aria-label="Close Tweak Panel"
- variant="ghost"
+ variant="subtle"
onClick={() => setShowTweaks(false)}
/>
</Box>
diff --git a/components/config.ts b/components/config.ts
index 532f960..ec1c691 100644
--- a/components/config.ts
+++ b/components/config.ts
@@ -59,13 +59,13 @@ export const initialVisuals = {
nodeOpacity: 1,
nodeResolution: 12,
labels: 2,
- labelScale: 1.5,
- labelFontSize: 13,
+ labelScale: 1,
+ labelFontSize: 10,
labelLength: 40,
labelWordWrap: 25,
labelLineSpace: 1,
highlight: true,
- highlightNodeSize: 2,
+ highlightNodeSize: 1.2,
highlightLinkSize: 2,
highlightFade: 0.8,
highlightAnim: true,
@@ -88,7 +88,7 @@ export const initialVisuals = {
linkHighlight: 'purple.500',
backgroundColor: 'white',
emacsNodeColor: 'gray.800',
- labelTextColor: 'gray.900',
+ labelTextColor: 'black',
labelBackgroundColor: '',
labelBackgroundOpacity: 0.7,
citeDashes: true,
@@ -120,9 +120,11 @@ export const initialBehavior = {
export const initialMouse = {
highlight: 'hover',
- local: 'click',
- follow: 'double',
+ local: 'double',
+ follow: 'never',
context: 'right',
+ preview: 'click',
+ backgroundExitsLocal: false,
}
export const colorList = [
diff --git a/components/contextmenu.tsx b/components/contextmenu.tsx
index 755bd9d..77deb48 100644
--- a/components/contextmenu.tsx
+++ b/components/contextmenu.tsx
@@ -43,82 +43,105 @@ import {
} from '@chakra-ui/icons'
import { OrgRoamGraphReponse, OrgRoamLink, OrgRoamNode } from '../api'
+import { deleteNodeInEmacs, openNodeInEmacs, createNodeInEmacs } from '../util/webSocketFunctions'
+import { BiNetworkChart } from 'react-icons/bi'
+import { TagMenu } from './TagMenu'
+import { initialFilter, TagColors } from './config'
export default interface ContextMenuProps {
background: Boolean
- node?: OrgRoamNode
+ target: OrgRoamNode | string | null
nodeType?: string
- coordinates: number[]
+ coordinates: { [direction: string]: number | undefined }
handleLocal: (node: OrgRoamNode, add: string) => void
- openNodeInEmacs: (node: OrgRoamNode) => void
menuClose: () => void
scope: { nodeIds: string[] }
- deleteNodeInEmacs: (node: OrgRoamNode) => void
- createNodeInEmacs: (node: OrgRoamNode) => void
+ webSocket: any
+ setPreviewNode: any
+ setTagColors: any
+ tagColors: TagColors
+ setFilter: any
+ filter: typeof initialFilter
}
export const ContextMenu = (props: ContextMenuProps) => {
const {
background,
- node,
+ target,
nodeType,
coordinates,
handleLocal,
menuClose,
scope,
- openNodeInEmacs,
- deleteNodeInEmacs,
- createNodeInEmacs,
+ webSocket,
+ setPreviewNode,
+ setTagColors,
+ tagColors,
+ setFilter,
+ filter,
} = props
const { isOpen, onOpen, onClose } = useDisclosure()
const copyRef = useRef<any>()
return (
<>
- <Box
- position="absolute"
- zIndex="overlay"
- left={coordinates[0] + 10}
- top={coordinates[1] - 10}
- padding={5}
- >
- <Menu closeOnBlur={false} defaultIsOpen onClose={() => menuClose()}>
- <MenuList zIndex="overlay" bgColor="alt.100" borderColor="gray.500" maxWidth="xs">
- {node && (
- <>
- <Heading size="sm" isTruncated px={3} py={1}>
- {node.title}
- </Heading>
- <MenuDivider borderColor="gray.500" />
- </>
- )}
- {scope.nodeIds.length !== 0 && (
- <>
- <MenuItem onClick={() => handleLocal(node!, 'add')} icon={<PlusSquareIcon />}>
- Expand local graph at node
+ <Menu defaultIsOpen closeOnBlur={false} onClose={() => menuClose()}>
+ <MenuList
+ zIndex="overlay"
+ bgColor="white"
+ color="black"
+ //borderColor="gray.500"
+ position="absolute"
+ left={coordinates.left}
+ top={coordinates.top}
+ right={coordinates.right}
+ bottom={coordinates.bottom}
+ fontSize="xs"
+ boxShadow="xl"
+ >
+ {typeof target !== 'string' ? (
+ <>
+ {target && (
+ <>
+ <Heading size="xs" isTruncated px={3} py={1}>
+ {target.title}
+ </Heading>
+ <MenuDivider borderColor="gray.500" />
+ </>
+ )}
+ {scope.nodeIds.length !== 0 && (
+ <>
+ <MenuItem onClick={() => handleLocal(target!, 'add')} icon={<PlusSquareIcon />}>
+ Expand local graph at node
+ </MenuItem>
+ <MenuItem
+ onClick={() => handleLocal(target!, 'replace')}
+ icon={<BiNetworkChart />}
+ >
+ Open local graph for this node
+ </MenuItem>
+ </>
+ )}
+ {!target?.properties?.FILELESS ? (
+ <MenuItem
+ icon={<EditIcon />}
+ onClick={() => openNodeInEmacs(target as OrgRoamNode, webSocket)}
+ >
+ Open in Emacs
</MenuItem>
- <MenuItem onClick={() => handleLocal(node!, 'replace')} icon={<ViewIcon />}>
- Open local graph for this node
+ ) : (
+ <MenuItem icon={<AddIcon />} onClick={() => createNodeInEmacs(target, webSocket)}>
+ Create node
</MenuItem>
- </>
- )}
- {!node?.properties.FILELESS ? (
- <MenuItem icon={<EditIcon />} onClick={() => openNodeInEmacs(node as OrgRoamNode)}>
- Open in Emacs
- </MenuItem>
- ) : (
- <MenuItem icon={<AddIcon />} onClick={() => createNodeInEmacs(node)}>
- Create node
- </MenuItem>
- )}
- {node?.properties.ROAM_REFS && (
- <MenuItem icon={<ExternalLinkIcon />}>Open in Zotero</MenuItem>
- )}
- {scope.nodeIds.length === 0 && (
- <MenuItem icon={<ViewIcon />} onClick={() => handleLocal(node!, 'replace')}>
- Open local graph
- </MenuItem>
- )}
- {/* Doesn't work at the moment
+ )}
+ {target?.properties?.ROAM_REFS && (
+ <MenuItem icon={<ExternalLinkIcon />}>Open in Zotero</MenuItem>
+ )}
+ {scope.nodeIds.length === 0 && (
+ <MenuItem icon={<BiNetworkChart />} onClick={() => handleLocal(target!, 'replace')}>
+ Open local graph
+ </MenuItem>
+ )}
+ {/* Doesn't work at the moment
<MenuItem closeOnSelect={false} closeOnBlur={false}>
<Box _hover={{ bg: 'gray.200' }} width="100%">
<Popover
@@ -147,64 +170,78 @@ export const ContextMenu = (props: ContextMenuProps) => {
</Popover>
</Box>
</MenuItem> */}
- {node?.level === 0 && (
+
<MenuItem
- closeOnSelect={false}
- icon={<DeleteIcon color="red.500" />}
- color="red.500"
- onClick={onOpen}
+ icon={<ViewIcon />}
+ onClick={() => {
+ setPreviewNode(target)
+ }}
>
- Permenantly delete note
+ Preview
</MenuItem>
- )}
- </MenuList>
- </Menu>
- </Box>
- <Modal isCentered isOpen={isOpen} onClose={onClose}>
- <ModalOverlay />
- <ModalContent zIndex="popover">
- <ModalHeader>Delete node?</ModalHeader>
- <ModalCloseButton />
- <ModalBody>
- <VStack spacing={4} display="flex" alignItems="flex-start">
- <Text>This will permanently delete your note:</Text>
- <Text fontWeight="bold">{node?.title}</Text>
- {node?.level !== 0 && (
- <Text>
- This will only delete the from this heading until but not including the next node.
- Your parent file and all other nodes will not be deleted.
- </Text>
+ {target?.level === 0 && (
+ <MenuItem
+ closeOnSelect={false}
+ icon={<DeleteIcon color="red.500" />}
+ color="red.500"
+ onClick={onOpen}
+ >
+ Permenantly delete note
+ </MenuItem>
)}
- <Text>Are you sure you want to do continue?</Text>
- </VStack>
- </ModalBody>
- <ModalFooter>
- <Button
- mr={3}
- onClick={() => {
- console.log('closing')
- onClose()
- menuClose()
- }}
- >
- Cancel
- </Button>
- <Button
- variant="link"
- colorScheme="red"
- ml={3}
- onClick={() => {
- console.log('aaaaa')
- deleteNodeInEmacs(node!)
- onClose()
- menuClose()
- }}
- >
- Delete node
- </Button>
- </ModalFooter>
- </ModalContent>
- </Modal>
+ </>
+ ) : (
+ <TagMenu {...{ target, tagColors, filter, setTagColors, setFilter }} />
+ )}
+ </MenuList>
+ </Menu>
+ {typeof target !== 'string' && (
+ <Modal isCentered isOpen={isOpen} onClose={onClose}>
+ <ModalOverlay />
+ <ModalContent zIndex="popover">
+ <ModalHeader>Delete node?</ModalHeader>
+ <ModalCloseButton />
+ <ModalBody>
+ <VStack spacing={4} display="flex" alignItems="flex-start">
+ <Text>This will permanently delete your note:</Text>
+ <Text fontWeight="bold">{target?.title}</Text>
+ {target?.level !== 0 && (
+ <Text>
+ This will only delete the from this heading until but not including the next
+ node. Your parent file and all other nodes will not be deleted.
+ </Text>
+ )}
+ <Text>Are you sure you want to do continue?</Text>
+ </VStack>
+ </ModalBody>
+ <ModalFooter>
+ <Button
+ mr={3}
+ onClick={() => {
+ console.log('closing')
+ onClose()
+ menuClose()
+ }}
+ >
+ Cancel
+ </Button>
+ <Button
+ variant="link"
+ colorScheme="red"
+ ml={3}
+ onClick={() => {
+ console.log('aaaaa')
+ deleteNodeInEmacs(target!, webSocket)
+ onClose()
+ menuClose()
+ }}
+ >
+ Delete node
+ </Button>
+ </ModalFooter>
+ </ModalContent>
+ </Modal>
+ )}
</>
)
}