diff options
author | Thomas F. K. Jorna <[email protected]> | 2021-10-11 21:27:17 +0200 |
---|---|---|
committer | GitHub <[email protected]> | 2021-10-11 21:27:17 +0200 |
commit | 58b7030d45370072dee25214748670d6413343a9 (patch) | |
tree | 9632df7273415f4b197413c45ad11563af32d53a /components/Sidebar | |
parent | 89be3b67b2d10d35d72b5c54e1e166beeeef3095 (diff) | |
parent | 6e3dcf585c35620c6804f3c208e6882c29dfc17e (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.tsx | 74 | ||||
-rw-r--r-- | components/Sidebar/Collapse.tsx | 146 | ||||
-rw-r--r-- | components/Sidebar/Link.tsx | 241 | ||||
-rw-r--r-- | components/Sidebar/Note.tsx | 73 | ||||
-rw-r--r-- | components/Sidebar/OrgImage.tsx | 42 | ||||
-rw-r--r-- | components/Sidebar/TagBar.tsx | 79 | ||||
-rw-r--r-- | components/Sidebar/Toolbar.tsx | 103 | ||||
-rw-r--r-- | components/Sidebar/index.tsx | 253 | ||||
-rw-r--r-- | components/Sidebar/noteStyle.ts | 139 | ||||
-rw-r--r-- | components/Sidebar/transition-utils.tsx | 144 |
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'], + }), +} |