diff options
Diffstat (limited to 'components')
-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/Toolbar.tsx | 103 | ||||
-rw-r--r-- | components/Sidebar/index.tsx | 218 | ||||
-rw-r--r-- | components/Sidebar/noteStyle.ts | 139 | ||||
-rw-r--r-- | components/Sidebar/transition-utils.tsx | 144 | ||||
-rw-r--r-- | components/Tweaks/BehaviorPanel.tsx | 22 | ||||
-rw-r--r-- | components/Tweaks/SliderWithInfo.tsx | 2 | ||||
-rw-r--r-- | components/Tweaks/index.tsx | 27 | ||||
-rw-r--r-- | components/config.ts | 10 | ||||
-rw-r--r-- | components/contextmenu.tsx | 152 |
14 files changed, 1307 insertions, 86 deletions
diff --git a/components/Sidebar/Backlinks.tsx b/components/Sidebar/Backlinks.tsx new file mode 100644 index 0000000..68ab551 --- /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 + pt={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..e425559 --- /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="85%" + 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/Toolbar.tsx b/components/Sidebar/Toolbar.tsx new file mode 100644 index 0000000..8741da5 --- /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 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..cbc0cc9 --- /dev/null +++ b/components/Sidebar/index.tsx @@ -0,0 +1,218 @@ +import React, { useContext, useEffect, useRef, useState } from 'react' + +import { Toolbar } from './Toolbar' +import { Note } from './Note' + +import { Button, Slide, VStack, Flex, Heading, Box, IconButton, Tooltip } from '@chakra-ui/react' +import { Collapse } from './Collapse' +import { Scrollbars } from 'react-custom-scrollbars-2' +import { ChevronLeftIcon, ChevronRightIcon, CloseIcon, HamburgerIcon } 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' + +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 +} + +const Sidebar = (props: SidebarProps) => { + const { + isOpen, + onOpen, + onClose, + previewNode, + setPreviewNode, + nodeById, + linksByNodeId, + nodeByCite, + setSidebarHighlightedNode, + canUndo, + canRedo, + resetPreviewNode, + previousPreviewNode, + nextPreviewNode, + openContextMenu, + scope, + setScope, + windowWidth, + } = 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: '100%', 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} + > + <Box pl={2} color="black" h="100%" bg="alt.100" width="100%"> + <Flex + whiteSpace="nowrap" + overflow="hidden" + textOverflow="ellipsis" + pl={4} + alignItems="center" + color="black" + width="100%" + > + <BiFile + onContextMenu={(e) => { + e.preventDefault() + openContextMenu(previewNode, e) + }} + /> + <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: 10, + backgroundColor: highlightColor, + }} + color="alt.100" + {...props} + /> + )} + > + <VStack height="100%" alignItems="left" bg="alt.100" paddingLeft={4}> + <Note + {...{ + setPreviewNode, + previewNode, + nodeById, + nodeByCite, + setSidebarHighlightedNode, + justification, + justificationList, + linksByNodeId, + openContextMenu, + }} + /> + </VStack> + </Scrollbars> + </Box> + </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/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/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..33f11ee 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={0} + 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="overlay" 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..0ae654e 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, + labelScale: 1, labelFontSize: 13, labelLength: 40, labelWordWrap: 25, labelLineSpace: 1, highlight: true, - highlightNodeSize: 2, + highlightNodeSize: 1.2, highlightLinkSize: 2, highlightFade: 0.8, highlightAnim: 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..118b8bb 100644 --- a/components/contextmenu.tsx +++ b/components/contextmenu.tsx @@ -43,82 +43,90 @@ import { } from '@chakra-ui/icons' import { OrgRoamGraphReponse, OrgRoamLink, OrgRoamNode } from '../api' +import { deleteNodeInEmacs, openNodeInEmacs, createNodeInEmacs } from '../util/webSocketFunctions' +import { BiNetworkChart } from 'react-icons/bi' export default interface ContextMenuProps { background: Boolean - node?: OrgRoamNode + target: OrgRoamNode | 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 } export const ContextMenu = (props: ContextMenuProps) => { const { background, - node, + target, nodeType, coordinates, handleLocal, menuClose, scope, - openNodeInEmacs, - deleteNodeInEmacs, - createNodeInEmacs, + webSocket, + setPreviewNode, } = 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 - </MenuItem> - <MenuItem onClick={() => handleLocal(node!, 'replace')} icon={<ViewIcon />}> - Open local graph for this node - </MenuItem> - </> - )} - {!node?.properties.FILELESS ? ( - <MenuItem icon={<EditIcon />} onClick={() => openNodeInEmacs(node as OrgRoamNode)}> - Open in Emacs + <Menu defaultIsOpen closeOnBlur={false} onClose={() => menuClose()}> + <MenuList + zIndex="overlay" + bgColor="white" + color="black" + borderColor="gray.500" + maxWidth="xs" + position="absolute" + left={coordinates.left} + top={coordinates.top} + right={coordinates.right} + bottom={coordinates.bottom} + fontSize="xs" + > + {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 icon={<AddIcon />} onClick={() => createNodeInEmacs(node)}> - Create node + <MenuItem onClick={() => handleLocal(target!, 'replace')} icon={<BiNetworkChart />}> + Open local graph for this 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?.FILELESS ? ( + <MenuItem + icon={<EditIcon />} + onClick={() => openNodeInEmacs(target as OrgRoamNode, webSocket)} + > + Open in Emacs + </MenuItem> + ) : ( + <MenuItem icon={<AddIcon />} onClick={() => createNodeInEmacs(target, webSocket)}> + Create node + </MenuItem> + )} + {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,19 +155,27 @@ export const ContextMenu = (props: ContextMenuProps) => { </Popover> </Box> </MenuItem> */} - {node?.level === 0 && ( - <MenuItem - closeOnSelect={false} - icon={<DeleteIcon color="red.500" />} - color="red.500" - onClick={onOpen} - > - Permenantly delete note - </MenuItem> - )} - </MenuList> - </Menu> - </Box> + + <MenuItem + icon={<ViewIcon />} + onClick={() => { + setPreviewNode(target) + }} + > + Preview + </MenuItem> + {target?.level === 0 && ( + <MenuItem + closeOnSelect={false} + icon={<DeleteIcon color="red.500" />} + color="red.500" + onClick={onOpen} + > + Permenantly delete note + </MenuItem> + )} + </MenuList> + </Menu> <Modal isCentered isOpen={isOpen} onClose={onClose}> <ModalOverlay /> <ModalContent zIndex="popover"> @@ -168,8 +184,8 @@ export const ContextMenu = (props: ContextMenuProps) => { <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 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. @@ -195,7 +211,7 @@ export const ContextMenu = (props: ContextMenuProps) => { ml={3} onClick={() => { console.log('aaaaa') - deleteNodeInEmacs(node!) + deleteNodeInEmacs(target!, webSocket) onClose() menuClose() }} |