diff --git a/app/actions/definitions/collections.tsx b/app/actions/definitions/collections.tsx index 387f31569e..07bc64406b 100644 --- a/app/actions/definitions/collections.tsx +++ b/app/actions/definitions/collections.tsx @@ -163,7 +163,7 @@ export const importDocument = createActionV2({ const { documents } = stores; const input = document.createElement("input"); input.type = "file"; - input.accept = documents.importFileTypes.join(", "); + input.accept = documents.importFileTypesString; input.onchange = async (ev) => { const files = getEventFiles(ev); diff --git a/app/actions/definitions/documents.tsx b/app/actions/definitions/documents.tsx index 6292dd9986..924e9654b9 100644 --- a/app/actions/definitions/documents.tsx +++ b/app/actions/definitions/documents.tsx @@ -870,7 +870,7 @@ export const importDocument = createActionV2({ const { documents } = stores; const input = document.createElement("input"); input.type = "file"; - input.accept = documents.importFileTypes.join(", "); + input.accept = documents.importFileTypesString; input.onchange = async (ev) => { const files = getEventFiles(ev); diff --git a/app/components/DocumentBreadcrumb.tsx b/app/components/DocumentBreadcrumb.tsx index dcd3360869..bdc9077dfa 100644 --- a/app/components/DocumentBreadcrumb.tsx +++ b/app/components/DocumentBreadcrumb.tsx @@ -99,7 +99,12 @@ function DocumentBreadcrumb( return createInternalLinkActionV2({ name: node.icon ? ( <> - {title} + {" "} + {title} ) : ( title diff --git a/app/components/EditableTitle.tsx b/app/components/EditableTitle.tsx index 9357e57fd4..b6f39c8e22 100644 --- a/app/components/EditableTitle.tsx +++ b/app/components/EditableTitle.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { toast } from "sonner"; import styled from "styled-components"; -import { s, truncateMultiline } from "@shared/styles"; +import { s, ellipsis } from "@shared/styles"; type Props = Omit, "onSubmit"> & { /** A callback when the title is submitted. */ @@ -168,8 +168,8 @@ function EditableTitle( ); } -const Text = styled.span` - ${truncateMultiline(3)} +const Text = styled.div` + ${ellipsis()} `; const Input = styled.input` diff --git a/app/components/Menu/ContextMenu.tsx b/app/components/Menu/ContextMenu.tsx index ca6d680a7c..5155f308d0 100644 --- a/app/components/Menu/ContextMenu.tsx +++ b/app/components/Menu/ContextMenu.tsx @@ -30,15 +30,13 @@ export const ContextMenu = observer( isMenu: true, }); - const menuItems = useComputed(() => { - if (!open) { - return []; - } - - return ((action?.children as ActionV2Variant[]) ?? []).map( - (childAction) => actionV2ToMenuItem(childAction, actionContext) - ); - }, [open, action?.children, actionContext]); + const menuItems = useComputed( + () => + ((action?.children as ActionV2Variant[]) ?? []).map((childAction) => + actionV2ToMenuItem(childAction, actionContext) + ), + [action?.children, actionContext] + ); const handleOpenChange = React.useCallback( (open: boolean) => { @@ -48,7 +46,7 @@ export const ContextMenu = observer( onClose?.(); } }, - [onOpen, onClose] + [open, onOpen, onClose] ); const enablePointerEvents = React.useCallback(() => { diff --git a/app/components/Sidebar/components/CollectionLink.tsx b/app/components/Sidebar/components/CollectionLink.tsx index 97546a1fa9..6d5894e463 100644 --- a/app/components/Sidebar/components/CollectionLink.tsx +++ b/app/components/Sidebar/components/CollectionLink.tsx @@ -32,7 +32,7 @@ import CollectionLinkChildren from "./CollectionLinkChildren"; type Props = { collection: Collection; expanded?: boolean; - onDisclosureClick: (ev?: React.MouseEvent) => void; + onDisclosureClick: (ev?: React.MouseEvent) => void; activeDocument: Document | undefined; isDraggingAnyCollection?: boolean; depth?: number; diff --git a/app/components/Sidebar/components/DocumentLink.tsx b/app/components/Sidebar/components/DocumentLink.tsx index 2dd4b03de0..117091dd38 100644 --- a/app/components/Sidebar/components/DocumentLink.tsx +++ b/app/components/Sidebar/components/DocumentLink.tsx @@ -130,17 +130,13 @@ function InnerDocumentLink( } }, [setCollapsed, expanded, hasChildDocuments]); - const handleDisclosureClick = React.useCallback( - (ev) => { - ev?.preventDefault(); - if (expanded) { - setCollapsed(); - } else { - setExpanded(); - } - }, - [setCollapsed, setExpanded, expanded] - ); + const handleDisclosureClick = React.useCallback(() => { + if (expanded) { + setCollapsed(); + } else { + setExpanded(); + } + }, [setCollapsed, setExpanded, expanded]); const handlePrefetch = React.useCallback(() => { void prefetchDocument?.(node.id); @@ -418,7 +414,7 @@ function InnerDocumentLink( onKeyDown={handleKeyDown} >
- + setPreRendered(true)); const canCollection = usePolicy(collectionId); const canDocument = usePolicy(documentId); @@ -42,6 +45,7 @@ function DropToImport({ disabled, children, collectionId, documentId }: Props) { if ( disabled || + !prerender || (collectionId && !canCollection.createDocument) || (documentId && !canDocument.createChildDocument) ) { @@ -50,7 +54,7 @@ function DropToImport({ disabled, children, collectionId, documentId }: Props) { return ( = ({ expanded, children }: Props) => { - const [openedOnce, setOpenedOnce] = React.useState(expanded); - - // allows us to avoid rendering all children when the folder hasn't been opened - React.useEffect(() => { - if (expanded) { - setOpenedOnce(true); - } - }, [expanded]); - - if (!openedOnce) { + if (!expanded) { return null; } - return {children}; + return <>{children}; }; -const Wrapper = styled.div<{ $expanded?: boolean }>` - display: ${(props) => (props.$expanded ? "block" : "none")}; -`; - export default Folder; diff --git a/app/components/Sidebar/components/SharedCollectionLink.tsx b/app/components/Sidebar/components/SharedCollectionLink.tsx index 22b8cfdb47..9ceb01f89a 100644 --- a/app/components/Sidebar/components/SharedCollectionLink.tsx +++ b/app/components/Sidebar/components/SharedCollectionLink.tsx @@ -40,7 +40,7 @@ function CollectionLink({ node, shareId, hideRootNode }: Props) { & { to?: LocationDescriptor; innerRef?: (ref: HTMLElement | null | undefined) => void; onClick?: React.MouseEventHandler; /** Callback when we expect the user to click on the link. Used for prefetching data. */ - onClickIntent?: () => void; - onDisclosureClick?: React.MouseEventHandler; + onClickIntent?: React.MouseEventHandler; + onDisclosureClick?: React.MouseEventHandler; icon?: React.ReactNode; label?: React.ReactNode; menu?: React.ReactNode; @@ -45,6 +44,7 @@ const activeDropStyle = { const preventDefault = (ev: React.MouseEvent) => { ev.preventDefault(); + ev.stopPropagation(); }; function SidebarLink( @@ -77,10 +77,10 @@ function SidebarLink( const { handleMouseEnter, handleMouseLeave } = useClickIntent(onClickIntent); const style = React.useMemo( () => ({ - paddingLeft: `${(depth || 0) * 16 + 12}px`, + paddingLeft: `${(depth || 0) * 16 + (icon ? -8 : 12)}px`, paddingRight: unreadBadge ? "32px" : undefined, }), - [depth] + [depth, icon, unreadBadge] ); const unreadStyle = React.useMemo( @@ -99,81 +99,71 @@ function SidebarLink( [theme.text, theme.sidebarActiveBackground, style] ); - const hoverStyle = React.useMemo( - () => ({ - color: theme.text, - ...style, - }), - [theme.text, style] - ); + const handleClick = React.useCallback( + (ev: React.MouseEvent) => { + onDisclosureClick?.(ev); - const [openContextMenu, setOpen, setClosed] = useBoolean(false); - const DisclosureComponent = depth === 0 ? HiddenDisclosure : Disclosure; - - const handleClickCapture = React.useCallback( - (event: React.MouseEvent) => { - if (event.altKey && onDisclosureClick && expanded !== undefined) { - event.preventDefault(); - event.stopPropagation(); - onDisclosureClick( - event as unknown as React.MouseEvent - ); + if (onClick && !disabled && ev.isDefaultPrevented() === false) { + onClick(ev); } }, - [onDisclosureClick, expanded] + [onClick, disabled, expanded] ); + const handleDisclosureClick = React.useCallback( + (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + onDisclosureClick?.(ev); + }, + [onDisclosureClick] + ); + + const DisclosureComponent = icon ? HiddenDisclosure : Disclosure; + return ( - <> - + - - - {expanded !== undefined && ( - - )} - {icon && {icon}} - - {unreadBadge && } - - - - {menu && {menu}} - + + {expanded !== undefined && ( + + )} + {icon && {icon}} + + {unreadBadge && } + + {menu && {menu}} + + ); } // accounts for whitespace around icon export const IconWrapper = styled.span` margin-left: -4px; - margin-right: 4px; height: 24px; overflow: hidden; flex-shrink: 0; @@ -191,12 +181,13 @@ const Actions = styled(EventBoundary)<{ showActions?: boolean }>` display: inline-flex; visibility: ${(props) => (props.showActions ? "visible" : "hidden")}; position: absolute; - top: 4px; + top: 3px; right: 4px; gap: 4px; color: ${s("textTertiary")}; transition: opacity 50ms; height: 24px; + background: var(--background); svg { color: ${s("textSecondary")}; @@ -226,16 +217,28 @@ const Link = styled(NavLink)<{ $isDraft?: boolean; $disabled?: boolean; }>` + &:hover, + &:active { + --background: ${s("sidebarHoverBackground")}; + } + + &[aria-current="page"] ${Actions} { + --background: ${s("sidebarActiveBackground")}; + } + + ${(props) => props.$isActiveDrop && `--background: ${props.theme.slateDark};`} + display: flex; position: relative; text-overflow: ellipsis; font-weight: 475; padding: ${isMobile() ? 12 : 6}px 16px; border-radius: 4px; - min-height: 32px; + min-height: 30px; user-select: none; - background: ${(props) => - props.$isActiveDrop ? props.theme.slateDark : "inherit"}; + white-space: nowrap; + margin-top: 1px; + background: var(--background); color: ${(props) => props.$isActiveDrop ? props.theme.white : props.theme.sidebarText}; font-size: 16px; @@ -282,30 +285,13 @@ const Link = styled(NavLink)<{ } } - & + ${Actions} { - background: ${s("sidebarBackground")}; - - ${NudeButton} { - background: transparent; - - &:hover, - &[aria-expanded="true"] { - background: ${s("sidebarControlHoverBackground")}; - } - } - } - - &[aria-current="page"] + ${Actions} { - background: ${s("sidebarActiveBackground")}; - } - ${breakpoint("tablet")` - padding: 4px 8px 4px 16px; + padding: 3px 8px 3px 12px; font-size: 14px; `} @media (hover: hover) { - &:hover + ${Actions}, &:active + ${Actions} { + &:hover ${Actions}, &:active ${Actions} { visibility: visible; svg { @@ -318,12 +304,25 @@ const Link = styled(NavLink)<{ props.$isActiveDrop ? props.theme.white : props.theme.text}; } } + + & ${Actions} { + ${NudeButton} { + background: transparent; + + &:hover, + &[aria-expanded="true"] { + background: ${s("sidebarControlHoverBackground")}; + } + } + } `; -const Label = styled.div` +const Label = styled.div<{ $ellipsis: boolean }>` position: relative; width: 100%; line-height: 24px; + margin-left: 2px; + ${(props) => props.$ellipsis && ellipsis()} * { unicode-bidi: plaintext; diff --git a/app/components/Sidebar/components/StarredLink.tsx b/app/components/Sidebar/components/StarredLink.tsx index eca6379777..df5e18a67c 100644 --- a/app/components/Sidebar/components/StarredLink.tsx +++ b/app/components/Sidebar/components/StarredLink.tsx @@ -41,7 +41,7 @@ type StarredDocumentLinkProps = { expanded: boolean; sidebarContext: SidebarContextType; isDragging: boolean; - handleDisclosureClick: (ev?: React.MouseEvent) => void; + handleDisclosureClick: React.MouseEventHandler; handlePrefetch: () => void; icon: React.ReactNode; label: React.ReactNode; @@ -234,7 +234,7 @@ function StarredLink({ star }: Props) { }, [documentId, documents]); const handleDisclosureClick = React.useCallback( - (ev?: React.MouseEvent) => { + (ev?: React.MouseEvent) => { ev?.preventDefault(); ev?.stopPropagation(); setExpanded((prevExpanded) => !prevExpanded); diff --git a/app/hooks/useClickIntent.ts b/app/hooks/useClickIntent.ts index 71710d9f23..a01a833e6b 100644 --- a/app/hooks/useClickIntent.ts +++ b/app/hooks/useClickIntent.ts @@ -7,7 +7,7 @@ import useUnmount from "./useUnmount"; * and clears the timer on mouse leave or component unmount. */ export default function useClickIntent( - onClickIntent?: () => void, + onClickIntent?: React.MouseEventHandler, delay = 100 ) { const timer = React.useRef(); diff --git a/app/scenes/Collection/index.tsx b/app/scenes/Collection/index.tsx index 64cab2847a..5428b50c97 100644 --- a/app/scenes/Collection/index.tsx +++ b/app/scenes/Collection/index.tsx @@ -197,7 +197,7 @@ const CollectionScene = observer(function _CollectionScene() { } > diff --git a/app/stores/DocumentsStore.ts b/app/stores/DocumentsStore.ts index 30d19abd61..9560eefe17 100644 --- a/app/stores/DocumentsStore.ts +++ b/app/stores/DocumentsStore.ts @@ -69,6 +69,11 @@ export default class DocumentsStore extends Store { super(rootStore, Document); } + @computed + get importFileTypesString(): string { + return this.importFileTypes.join(","); + } + @computed get all(): Document[] { return filter( diff --git a/app/typings/styled-components.d.ts b/app/typings/styled-components.d.ts index 56e1896e79..d33cd94082 100644 --- a/app/typings/styled-components.d.ts +++ b/app/typings/styled-components.d.ts @@ -145,6 +145,7 @@ declare module "styled-components" { placeholder: string; commentMarkBackground: string; sidebarBackground: string; + sidebarHoverBackground: string; sidebarActiveBackground: string; sidebarControlHoverBackground: string; sidebarDraftBorder: string; diff --git a/shared/styles/theme.ts b/shared/styles/theme.ts index 5529b8c7b4..3276adf557 100644 --- a/shared/styles/theme.ts +++ b/shared/styles/theme.ts @@ -129,7 +129,8 @@ export const buildLightTheme = (input: Partial): DefaultTheme => { textDiffDeletedBackground: "#ffebe9", placeholder: "#a2b2c3", sidebarBackground: colors.warmGrey, - sidebarActiveBackground: "#d7e0ea", + sidebarHoverBackground: "hsl(212 31% 90% / 1)", + sidebarActiveBackground: "hsl(212 31% 85% / 1)", sidebarControlHoverBackground: "rgb(138 164 193 / 20%)", sidebarDraftBorder: darken("0.25", colors.warmGrey), sidebarText: "rgb(78, 92, 110)", @@ -192,6 +193,7 @@ export const buildDarkTheme = (input: Partial): DefaultTheme => { textDiffDeletedBackground: "rgba(248,81,73,0.15)", placeholder: "#596673", sidebarBackground: colors.veryDarkBlue, + sidebarHoverBackground: lighten(0.05, colors.veryDarkBlue), sidebarActiveBackground: lighten(0.09, colors.veryDarkBlue), sidebarControlHoverBackground: colors.white10, sidebarDraftBorder: darken("0.35", colors.slate),