fix: Add ability to collapse and expand collections that are not active (#3102)
* fix: add disclosure and transition * fix: keep collections expanded * fix: tune transition and collapsing conditions * fix: collectionIcon expanded props is no longer driven by expanded state * fix: sync issue * fix: managing state together * fix: remove comment * fix: simplify expanded state * fix: remove extra state * fix: remove animation and retain expanded state * fix: remove isCollectionDropped * fix: don't use ref * review suggestions * fix many functional and design issues * don't render every single document in the sidebar, just ones that the user has seen before * chore: Sidebar refinement (#3154) * stash * wip: More sidebar tweaks * Simplify draft bubble * disclosure refactor * wip wip * lint * tweak menu position * Use document emoji for starred docs where available * feat: Trigger cmd+k from sidebar (#3149) * feat: Trigger cmd+k from sidebar * Add hint when opening command bar from sidebar * fix: Clicking internal links in shared documents sometimes reroutes to Login * fix: Spacing issues on connected slack channels list * Merge * fix: Do not prefetch JS bundles on public share links * fix: Buttons show on collection empty state when user does not have permission to edit * fix: the hover area for the "collections" subheading was being obfuscated by the initial collection drop cursor * fix: top-align disclosures * fix: Disclosure color PR feedback fix: Starred no longer draggable * fix: Overflow on sidebar button * fix: Scrollbar in sidebar when command menu is open * Minor alignment issues, clarify back in settings sidebar * fix: Fade component causes SidebarButton missizing Co-authored-by: Nan Yu <thenanyu@gmail.com> Co-authored-by: Tom Moor <tom.moor@gmail.com> Co-authored-by: Nan Yu <thenanyu@gmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
KeyboardIcon,
|
||||
EmailIcon,
|
||||
LogoutIcon,
|
||||
ProfileIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import {
|
||||
@@ -29,7 +30,8 @@ import {
|
||||
} from "~/actions/sections";
|
||||
import history from "~/utils/history";
|
||||
import {
|
||||
settingsPath,
|
||||
organizationSettingsPath,
|
||||
profileSettingsPath,
|
||||
homePath,
|
||||
searchPath,
|
||||
draftsPath,
|
||||
@@ -103,9 +105,16 @@ export const navigateToSettings = createAction({
|
||||
name: ({ t }) => t("Settings"),
|
||||
section: NavigationSection,
|
||||
shortcut: ["g", "s"],
|
||||
iconInContextMenu: false,
|
||||
icon: <SettingsIcon />,
|
||||
perform: () => history.push(settingsPath()),
|
||||
perform: () => history.push(organizationSettingsPath()),
|
||||
});
|
||||
|
||||
export const navigateToProfileSettings = createAction({
|
||||
name: ({ t }) => t("Profile"),
|
||||
section: NavigationSection,
|
||||
iconInContextMenu: false,
|
||||
icon: <ProfileIcon />,
|
||||
perform: () => history.push(profileSettingsPath()),
|
||||
});
|
||||
|
||||
export const openAPIDocumentation = createAction({
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
newDocumentPath,
|
||||
settingsPath,
|
||||
} from "~/utils/routeHelpers";
|
||||
import Fade from "./Fade";
|
||||
import withStores from "./withStores";
|
||||
|
||||
const DocumentHistory = React.lazy(
|
||||
@@ -74,10 +75,12 @@ class AuthenticatedLayout extends React.Component<Props> {
|
||||
}
|
||||
|
||||
const sidebar = showSidebar ? (
|
||||
<Fade>
|
||||
<Switch>
|
||||
<Route path={settingsPath()} component={SettingsSidebar} />
|
||||
<Route component={Sidebar} />
|
||||
</Switch>
|
||||
</Fade>
|
||||
) : undefined;
|
||||
|
||||
const rightRail = (
|
||||
|
||||
@@ -11,6 +11,7 @@ type Props = {
|
||||
icon?: React.ReactNode;
|
||||
user?: User;
|
||||
alt?: string;
|
||||
showBorder?: boolean;
|
||||
onClick?: React.MouseEventHandler<HTMLImageElement>;
|
||||
className?: string;
|
||||
};
|
||||
@@ -29,12 +30,13 @@ class Avatar extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { src, icon, ...rest } = this.props;
|
||||
const { src, icon, showBorder, ...rest } = this.props;
|
||||
return (
|
||||
<AvatarWrapper>
|
||||
<CircleImg
|
||||
onError={this.handleError}
|
||||
src={this.error ? placeholder : src}
|
||||
$showBorder={showBorder}
|
||||
{...rest}
|
||||
/>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
@@ -59,12 +61,14 @@ const IconWrapper = styled.div`
|
||||
height: 20px;
|
||||
`;
|
||||
|
||||
const CircleImg = styled.img<{ size: number }>`
|
||||
const CircleImg = styled.img<{ size: number; $showBorder?: boolean }>`
|
||||
display: block;
|
||||
width: ${(props) => props.size}px;
|
||||
height: ${(props) => props.size}px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid ${(props) => props.theme.background};
|
||||
border: 2px solid
|
||||
${(props) =>
|
||||
props.$showBorder === false ? "transparent" : props.theme.background};
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { bounceIn } from "~/styles/animations";
|
||||
|
||||
type Props = {
|
||||
count: number;
|
||||
};
|
||||
|
||||
const Bubble = ({ count }: Props) => {
|
||||
if (!count) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Count>{count}</Count>;
|
||||
};
|
||||
|
||||
const Count = styled.div`
|
||||
animation: ${bounceIn} 600ms;
|
||||
transform-origin: center center;
|
||||
color: ${(props) => props.theme.white};
|
||||
background: ${(props) => props.theme.slateDark};
|
||||
display: inline-block;
|
||||
font-feature-settings: "tnum";
|
||||
font-weight: 600;
|
||||
font-size: 9px;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
min-width: 16px;
|
||||
min-height: 16px;
|
||||
line-height: 16px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
padding: 0 4px;
|
||||
margin-left: 8px;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export default Bubble;
|
||||
32
app/components/EmojiIcon.tsx
Normal file
32
app/components/EmojiIcon.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
/* The emoji to render */
|
||||
emoji: string;
|
||||
/* The size of the emoji, 24px is default to match standard icons */
|
||||
size?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* EmojiIcon is a component that renders an emoji in the size of a standard icon
|
||||
* in a way that can be used wherever an Icon would be.
|
||||
*/
|
||||
export default function EmojiIcon({ size = 24, emoji, ...rest }: Props) {
|
||||
return (
|
||||
<Span $size={size} {...rest}>
|
||||
{emoji}
|
||||
</Span>
|
||||
);
|
||||
}
|
||||
|
||||
const Span = styled.span<{ $size: number }>`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
width: ${(props) => props.$size}px;
|
||||
height: ${(props) => props.$size}px;
|
||||
text-indent: -0.15em;
|
||||
font-size: 14px;
|
||||
`;
|
||||
@@ -118,7 +118,7 @@ const Wrapper = styled(Flex)<{ $passThrough?: boolean }>`
|
||||
padding: 12px;
|
||||
transition: all 100ms ease-out;
|
||||
transform: translate3d(0, 0, 0);
|
||||
min-height: 56px;
|
||||
min-height: 64px;
|
||||
justify-content: flex-start;
|
||||
|
||||
@supports (backdrop-filter: blur(20px)) {
|
||||
|
||||
@@ -1,44 +1,40 @@
|
||||
import { useKBar } from "kbar";
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
EditIcon,
|
||||
SearchIcon,
|
||||
ShapesIcon,
|
||||
HomeIcon,
|
||||
SettingsIcon,
|
||||
} from "outline-icons";
|
||||
import { EditIcon, SearchIcon, ShapesIcon, HomeIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import Bubble from "~/components/Bubble";
|
||||
import Flex from "~/components/Flex";
|
||||
import Scrollable from "~/components/Scrollable";
|
||||
import Text from "~/components/Text";
|
||||
import { inviteUser } from "~/actions/definitions/users";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import AccountMenu from "~/menus/AccountMenu";
|
||||
import OrganizationMenu from "~/menus/OrganizationMenu";
|
||||
import {
|
||||
homePath,
|
||||
draftsPath,
|
||||
templatesPath,
|
||||
settingsPath,
|
||||
searchPath,
|
||||
} from "~/utils/routeHelpers";
|
||||
import Avatar from "../Avatar";
|
||||
import TeamLogo from "../TeamLogo";
|
||||
import Sidebar from "./Sidebar";
|
||||
import ArchiveLink from "./components/ArchiveLink";
|
||||
import Collections from "./components/Collections";
|
||||
import Section from "./components/Section";
|
||||
import SidebarAction from "./components/SidebarAction";
|
||||
import SidebarButton from "./components/SidebarButton";
|
||||
import SidebarLink from "./components/SidebarLink";
|
||||
import Starred from "./components/Starred";
|
||||
import TeamButton from "./components/TeamButton";
|
||||
import TrashLink from "./components/TrashLink";
|
||||
|
||||
function MainSidebar() {
|
||||
function AppSidebar() {
|
||||
const { t } = useTranslation();
|
||||
const { ui, policies, documents } = useStores();
|
||||
const team = useCurrentTeam();
|
||||
@@ -76,18 +72,19 @@ function MainSidebar() {
|
||||
<Sidebar ref={handleSidebarRef}>
|
||||
{dndArea && (
|
||||
<DndProvider backend={HTML5Backend} options={html5Options}>
|
||||
<AccountMenu>
|
||||
<OrganizationMenu>
|
||||
{(props) => (
|
||||
<TeamButton
|
||||
<SidebarButton
|
||||
{...props}
|
||||
subheading={user.name}
|
||||
teamName={team.name}
|
||||
logoUrl={team.avatarUrl}
|
||||
title={team.name}
|
||||
image={
|
||||
<StyledTeamLogo src={team.avatarUrl} width={32} height={32} />
|
||||
}
|
||||
showDisclosure
|
||||
/>
|
||||
)}
|
||||
</AccountMenu>
|
||||
<Scrollable flex topShadow>
|
||||
</OrganizationMenu>
|
||||
<Scrollable flex shadow>
|
||||
<Section>
|
||||
<SidebarLink
|
||||
to={homePath()}
|
||||
@@ -106,15 +103,19 @@ function MainSidebar() {
|
||||
to={draftsPath()}
|
||||
icon={<EditIcon color="currentColor" />}
|
||||
label={
|
||||
<Drafts align="center">
|
||||
<Flex align="center" justify="space-between">
|
||||
{t("Drafts")}
|
||||
<Bubble count={documents.totalDrafts} />
|
||||
<Drafts size="xsmall" type="tertiary">
|
||||
{documents.totalDrafts}
|
||||
</Drafts>
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
<Section>
|
||||
<Starred />
|
||||
</Section>
|
||||
<Section auto>
|
||||
<Collections />
|
||||
</Section>
|
||||
@@ -138,23 +139,41 @@ function MainSidebar() {
|
||||
<TrashLink />
|
||||
</>
|
||||
)}
|
||||
<SidebarLink
|
||||
to={settingsPath()}
|
||||
icon={<SettingsIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label={t("Settings")}
|
||||
/>
|
||||
<SidebarAction action={inviteUser} />
|
||||
</Section>
|
||||
</Scrollable>
|
||||
<AccountMenu>
|
||||
{(props) => (
|
||||
<SidebarButton
|
||||
{...props}
|
||||
showMoreMenu
|
||||
title={user.name}
|
||||
image={
|
||||
<StyledAvatar
|
||||
src={user.avatarUrl}
|
||||
size={24}
|
||||
showBorder={false}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</AccountMenu>
|
||||
</DndProvider>
|
||||
)}
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
const Drafts = styled(Flex)`
|
||||
height: 24px;
|
||||
const StyledTeamLogo = styled(TeamLogo)`
|
||||
margin-right: 4px;
|
||||
`;
|
||||
|
||||
export default observer(MainSidebar);
|
||||
const StyledAvatar = styled(Avatar)`
|
||||
margin-left: 4px;
|
||||
`;
|
||||
|
||||
const Drafts = styled(Text)`
|
||||
margin: 0 4px;
|
||||
`;
|
||||
|
||||
export default observer(AppSidebar);
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
GroupIcon,
|
||||
LinkIcon,
|
||||
TeamIcon,
|
||||
ExpandedIcon,
|
||||
BackIcon,
|
||||
BeakerIcon,
|
||||
DownloadIcon,
|
||||
} from "outline-icons";
|
||||
@@ -27,8 +27,8 @@ import useStores from "~/hooks/useStores";
|
||||
import Sidebar from "./Sidebar";
|
||||
import Header from "./components/Header";
|
||||
import Section from "./components/Section";
|
||||
import SidebarButton from "./components/SidebarButton";
|
||||
import SidebarLink from "./components/SidebarLink";
|
||||
import TeamButton from "./components/TeamButton";
|
||||
import Version from "./components/Version";
|
||||
|
||||
const isHosted = env.DEPLOYMENT === "hosted";
|
||||
@@ -40,21 +40,17 @@ function SettingsSidebar() {
|
||||
const { policies } = useStores();
|
||||
const can = policies.abilities(team.id);
|
||||
|
||||
const returnToDashboard = React.useCallback(() => {
|
||||
const returnToApp = React.useCallback(() => {
|
||||
history.push("/home");
|
||||
}, [history]);
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
<TeamButton
|
||||
subheading={
|
||||
<ReturnToApp align="center">
|
||||
<BackIcon color="currentColor" /> {t("Return to App")}
|
||||
</ReturnToApp>
|
||||
}
|
||||
teamName={team.name}
|
||||
logoUrl={team.avatarUrl}
|
||||
onClick={returnToDashboard}
|
||||
<SidebarButton
|
||||
title={t("Return to App")}
|
||||
image={<StyledBackIcon color="currentColor" />}
|
||||
onClick={returnToApp}
|
||||
minHeight={48}
|
||||
/>
|
||||
|
||||
<Flex auto column>
|
||||
@@ -165,13 +161,8 @@ function SettingsSidebar() {
|
||||
);
|
||||
}
|
||||
|
||||
const BackIcon = styled(ExpandedIcon)`
|
||||
transform: rotate(90deg);
|
||||
margin-left: -8px;
|
||||
`;
|
||||
|
||||
const ReturnToApp = styled(Flex)`
|
||||
height: 16px;
|
||||
const StyledBackIcon = styled(BackIcon)`
|
||||
margin-left: 4px;
|
||||
`;
|
||||
|
||||
export default observer(SettingsSidebar);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Portal } from "react-portal";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import Fade from "~/components/Fade";
|
||||
import Flex from "~/components/Flex";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
import useStores from "~/hooks/useStores";
|
||||
@@ -14,7 +13,6 @@ import ResizeBorder from "./components/ResizeBorder";
|
||||
import Toggle, { ToggleButton, Positioner } from "./components/Toggle";
|
||||
|
||||
const ANIMATION_MS = 250;
|
||||
let isFirstRender = true;
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
@@ -126,7 +124,6 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(
|
||||
|
||||
React.useEffect(() => {
|
||||
if (location !== previousLocation) {
|
||||
isFirstRender = false;
|
||||
ui.hideMobileSidebar();
|
||||
}
|
||||
}, [ui, location, previousLocation]);
|
||||
@@ -146,8 +143,17 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(
|
||||
[width, theme.sidebarCollapsedWidth, collapsed]
|
||||
);
|
||||
|
||||
const content = (
|
||||
return (
|
||||
<>
|
||||
<Container
|
||||
ref={ref}
|
||||
style={style}
|
||||
$isAnimating={isAnimating}
|
||||
$isSmallerThanMinimum={isSmallerThanMinimum}
|
||||
$mobileSidebarVisible={ui.mobileSidebarVisible}
|
||||
$collapsed={collapsed}
|
||||
column
|
||||
>
|
||||
{ui.mobileSidebarVisible && (
|
||||
<Portal>
|
||||
<Backdrop onClick={ui.toggleMobileSidebar} />
|
||||
@@ -165,21 +171,6 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(
|
||||
aria-label={t("Expand")}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container
|
||||
ref={ref}
|
||||
style={style}
|
||||
$isAnimating={isAnimating}
|
||||
$isSmallerThanMinimum={isSmallerThanMinimum}
|
||||
$mobileSidebarVisible={ui.mobileSidebarVisible}
|
||||
$collapsed={collapsed}
|
||||
column
|
||||
>
|
||||
{isFirstRender ? <Fade>{content}</Fade> : content}
|
||||
</Container>
|
||||
{!ui.isEditing && (
|
||||
<Toggle
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useDrop, useDrag } from "react-dnd";
|
||||
import { useDrop, useDrag, DropTargetMonitor } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useHistory } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
@@ -70,6 +70,13 @@ function CollectionLink({
|
||||
collection.id === ui.activeCollectionId
|
||||
);
|
||||
|
||||
const [openedOnce, setOpenedOnce] = React.useState(expanded);
|
||||
React.useEffect(() => {
|
||||
if (expanded) {
|
||||
setOpenedOnce(true);
|
||||
}
|
||||
}, [expanded]);
|
||||
|
||||
const manualSort = collection.sort.field === "index";
|
||||
const can = policies.abilities(collection.id);
|
||||
const belowCollectionIndex = belowCollection ? belowCollection.index : null;
|
||||
@@ -118,7 +125,7 @@ function CollectionLink({
|
||||
// Drop to reorder document
|
||||
const [{ isOverReorder }, dropToReorder] = useDrop({
|
||||
accept: "document",
|
||||
drop: async (item: DragObject) => {
|
||||
drop: (item: DragObject) => {
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
@@ -131,11 +138,11 @@ function CollectionLink({
|
||||
|
||||
// Drop to reorder collection
|
||||
const [
|
||||
{ isCollectionDropping, isDraggingAnotherCollection },
|
||||
{ isCollectionDropping, isDraggingAnyCollection },
|
||||
dropToReorderCollection,
|
||||
] = useDrop({
|
||||
accept: "collection",
|
||||
drop: async (item: DragObject) => {
|
||||
drop: (item: DragObject) => {
|
||||
collections.move(
|
||||
item.id,
|
||||
fractionalIndex(collection.index, belowCollectionIndex)
|
||||
@@ -147,9 +154,9 @@ function CollectionLink({
|
||||
(!belowCollection || item.id !== belowCollection.id)
|
||||
);
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
collect: (monitor: DropTargetMonitor<Collection, Collection>) => ({
|
||||
isCollectionDropping: monitor.isOver(),
|
||||
isDraggingAnotherCollection: monitor.canDrop(),
|
||||
isDraggingAnyCollection: monitor.getItemType() === "collection",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -194,8 +201,7 @@ function CollectionLink({
|
||||
collection.sort,
|
||||
]);
|
||||
|
||||
const isDraggingAnyCollection =
|
||||
isDraggingAnotherCollection || isCollectionDragging;
|
||||
const displayDocumentLinks = expanded && !isCollectionDragging;
|
||||
|
||||
React.useEffect(() => {
|
||||
// If we're viewing a starred document through the starred menu then don't
|
||||
@@ -204,21 +210,14 @@ function CollectionLink({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDraggingAnyCollection) {
|
||||
setExpanded(false);
|
||||
} else {
|
||||
setExpanded(collection.id === ui.activeCollectionId);
|
||||
if (collection.id === ui.activeCollectionId) {
|
||||
setExpanded(true);
|
||||
}
|
||||
}, [isDraggingAnyCollection, collection.id, ui.activeCollectionId, search]);
|
||||
}, [collection.id, ui.activeCollectionId, search]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={drop}
|
||||
style={{
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Relative ref={drop}>
|
||||
<Draggable
|
||||
key={collection.id}
|
||||
ref={dragToReorderCollection}
|
||||
@@ -228,8 +227,16 @@ function CollectionLink({
|
||||
<DropToImport collectionId={collection.id}>
|
||||
<SidebarLink
|
||||
to={collection.url}
|
||||
expanded={displayDocumentLinks}
|
||||
onDisclosureClick={(event) => {
|
||||
event.preventDefault();
|
||||
setExpanded((prev) => !prev);
|
||||
}}
|
||||
icon={
|
||||
<CollectionIcon collection={collection} expanded={expanded} />
|
||||
<CollectionIcon
|
||||
collection={collection}
|
||||
expanded={displayDocumentLinks}
|
||||
/>
|
||||
}
|
||||
showActions={menuOpen}
|
||||
isActiveDrop={isOver && canDrop}
|
||||
@@ -242,12 +249,13 @@ function CollectionLink({
|
||||
/>
|
||||
}
|
||||
exact={false}
|
||||
depth={0.5}
|
||||
depth={0}
|
||||
menu={
|
||||
!isEditing && (
|
||||
!isEditing &&
|
||||
!isDraggingAnyCollection && (
|
||||
<>
|
||||
{can.update && (
|
||||
<CollectionSortMenuWithMargin
|
||||
{can.update && displayDocumentLinks && (
|
||||
<CollectionSortMenu
|
||||
collection={collection}
|
||||
onOpen={handleMenuOpen}
|
||||
onClose={handleMenuClose}
|
||||
@@ -264,18 +272,18 @@ function CollectionLink({
|
||||
/>
|
||||
</DropToImport>
|
||||
</Draggable>
|
||||
{expanded && manualSort && (
|
||||
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
|
||||
)}
|
||||
{isDraggingAnyCollection && (
|
||||
</Relative>
|
||||
<Relative>
|
||||
{openedOnce && (
|
||||
<Folder $open={displayDocumentLinks}>
|
||||
{manualSort && (
|
||||
<DropCursor
|
||||
isActiveDrop={isCollectionDropping}
|
||||
innerRef={dropToReorderCollection}
|
||||
isActiveDrop={isOverReorder}
|
||||
innerRef={dropToReorder}
|
||||
position="top"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{expanded &&
|
||||
collectionDocuments.map((node, index) => (
|
||||
{collectionDocuments.map((node, index) => (
|
||||
<DocumentLink
|
||||
key={node.id}
|
||||
node={node}
|
||||
@@ -288,6 +296,16 @@ function CollectionLink({
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</Folder>
|
||||
)}
|
||||
{isDraggingAnyCollection && (
|
||||
<DropCursor
|
||||
isActiveDrop={isCollectionDropping}
|
||||
innerRef={dropToReorderCollection}
|
||||
/>
|
||||
)}
|
||||
</Relative>
|
||||
|
||||
<Modal
|
||||
title={t("Move document")}
|
||||
onRequestClose={handlePermissionClose}
|
||||
@@ -306,13 +324,17 @@ function CollectionLink({
|
||||
);
|
||||
}
|
||||
|
||||
const Relative = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const Folder = styled.div<{ $open?: boolean }>`
|
||||
display: ${(props) => (props.$open ? "block" : "none")};
|
||||
`;
|
||||
|
||||
const Draggable = styled("div")<{ $isDragging: boolean; $isMoving: boolean }>`
|
||||
opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
|
||||
pointer-events: ${(props) => (props.$isMoving ? "none" : "auto")};
|
||||
`;
|
||||
|
||||
const CollectionSortMenuWithMargin = styled(CollectionSortMenu)`
|
||||
margin-right: 4px;
|
||||
`;
|
||||
|
||||
export default observer(CollectionLink);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { observer } from "mobx-react";
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -13,9 +12,10 @@ import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
import CollectionLink from "./CollectionLink";
|
||||
import DropCursor from "./DropCursor";
|
||||
import Header from "./Header";
|
||||
import PlaceholderCollections from "./PlaceholderCollections";
|
||||
import SidebarAction from "./SidebarAction";
|
||||
import SidebarLink, { DragObject } from "./SidebarLink";
|
||||
import { DragObject } from "./SidebarLink";
|
||||
|
||||
function Collections() {
|
||||
const [isFetching, setFetching] = React.useState(false);
|
||||
@@ -52,7 +52,10 @@ function Collections() {
|
||||
load();
|
||||
}, [collections, isFetching, showToast, fetchError, t]);
|
||||
|
||||
const [{ isCollectionDropping }, dropToReorderCollection] = useDrop({
|
||||
const [
|
||||
{ isCollectionDropping, isDraggingAnyCollection },
|
||||
dropToReorderCollection,
|
||||
] = useDrop({
|
||||
accept: "collection",
|
||||
drop: async (item: DragObject) => {
|
||||
collections.move(
|
||||
@@ -65,16 +68,19 @@ function Collections() {
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isCollectionDropping: monitor.isOver(),
|
||||
isDraggingAnyCollection: monitor.getItemType() === "collection",
|
||||
}),
|
||||
});
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{isDraggingAnyCollection && (
|
||||
<DropCursor
|
||||
isActiveDrop={isCollectionDropping}
|
||||
innerRef={dropToReorderCollection}
|
||||
position="top"
|
||||
/>
|
||||
)}
|
||||
{orderedCollections.map((collection: Collection, index: number) => (
|
||||
<CollectionLink
|
||||
key={collection.id}
|
||||
@@ -85,17 +91,14 @@ function Collections() {
|
||||
belowCollection={orderedCollections[index + 1]}
|
||||
/>
|
||||
))}
|
||||
<SidebarAction action={createCollection} depth={0.5} />
|
||||
<SidebarAction action={createCollection} depth={0} />
|
||||
</>
|
||||
);
|
||||
|
||||
if (!collections.isLoaded || fetchError) {
|
||||
return (
|
||||
<Flex column>
|
||||
<SidebarLink
|
||||
label={t("Collections")}
|
||||
icon={<Disclosure expanded={expanded} color="currentColor" />}
|
||||
/>
|
||||
<Header>{t("Collections")}</Header>
|
||||
<PlaceholderCollections />
|
||||
</Flex>
|
||||
);
|
||||
@@ -103,19 +106,18 @@ function Collections() {
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<SidebarLink
|
||||
onClick={() => setExpanded((prev) => !prev)}
|
||||
label={t("Collections")}
|
||||
icon={<Disclosure expanded={expanded} color="currentColor" />}
|
||||
/>
|
||||
{expanded && (isPreloaded ? content : <Fade>{content}</Fade>)}
|
||||
<Header onClick={() => setExpanded((prev) => !prev)} expanded={expanded}>
|
||||
{t("Collections")}
|
||||
</Header>
|
||||
{expanded && (
|
||||
<Relative>{isPreloaded ? content : <Fade>{content}</Fade>}</Relative>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const Disclosure = styled(CollapsedIcon)<{ expanded?: boolean }>`
|
||||
transition: transform 100ms ease, fill 50ms !important;
|
||||
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
||||
const Relative = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export default observer(Collections);
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Disclosure = styled(CollapsedIcon)<{ expanded?: boolean }>`
|
||||
transition: transform 100ms ease, fill 50ms !important;
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
|
||||
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
||||
`;
|
||||
|
||||
export default Disclosure;
|
||||
54
app/components/Sidebar/components/Disclosure.tsx
Normal file
54
app/components/Sidebar/components/Disclosure.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled, { css } from "styled-components";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
|
||||
type Props = {
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
expanded: boolean;
|
||||
root?: boolean;
|
||||
};
|
||||
|
||||
function Disclosure({ onClick, root, expanded, ...rest }: Props) {
|
||||
return (
|
||||
<Button size={20} onClick={onClick} $root={root} {...rest}>
|
||||
<StyledCollapsedIcon expanded={expanded} size={20} color="currentColor" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const Button = styled(NudeButton)<{ $root?: boolean }>`
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
flex-shrink: 0;
|
||||
color: ${(props) => props.theme.textSecondary};
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.text};
|
||||
background: ${(props) => props.theme.sidebarControlHoverBackground};
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
props.$root &&
|
||||
css`
|
||||
opacity: 0;
|
||||
left: -16px;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background: none;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledCollapsedIcon = styled(CollapsedIcon)<{
|
||||
expanded?: boolean;
|
||||
}>`
|
||||
transition: opacity 100ms ease, transform 100ms ease, fill 50ms !important;
|
||||
${(props) => !props.expanded && "transform: rotate(-90deg);"};
|
||||
`;
|
||||
|
||||
// Enables identifying this component within styled components
|
||||
const StyledDisclosure = styled(Disclosure)``;
|
||||
|
||||
export default StyledDisclosure;
|
||||
@@ -16,7 +16,6 @@ import useStores from "~/hooks/useStores";
|
||||
import DocumentMenu from "~/menus/DocumentMenu";
|
||||
import { NavigationNode } from "~/types";
|
||||
import { newDocumentPath } from "~/utils/routeHelpers";
|
||||
import Disclosure from "./Disclosure";
|
||||
import DropCursor from "./DropCursor";
|
||||
import DropToImport from "./DropToImport";
|
||||
import EditableTitle from "./EditableTitle";
|
||||
@@ -81,7 +80,9 @@ function DocumentLink(
|
||||
isActiveDocument)
|
||||
);
|
||||
}, [hasChildDocuments, activeDocument, isActiveDocument, node, collection]);
|
||||
|
||||
const [expanded, setExpanded] = React.useState(showChildren);
|
||||
const [openedOnce, setOpenedOnce] = React.useState(expanded);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (showChildren) {
|
||||
@@ -89,6 +90,12 @@ function DocumentLink(
|
||||
}
|
||||
}, [showChildren]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (expanded) {
|
||||
setOpenedOnce(true);
|
||||
}
|
||||
}, [expanded]);
|
||||
|
||||
// when the last child document is removed,
|
||||
// also close the local folder state to closed
|
||||
React.useEffect(() => {
|
||||
@@ -98,7 +105,7 @@ function DocumentLink(
|
||||
}, [expanded, hasChildDocuments]);
|
||||
|
||||
const handleDisclosureClick = React.useCallback(
|
||||
(ev: React.SyntheticEvent) => {
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
setExpanded(!expanded);
|
||||
@@ -270,6 +277,8 @@ function DocumentLink(
|
||||
t("Untitled");
|
||||
|
||||
const can = policies.abilities(node.id);
|
||||
const isExpanded = expanded && !isDragging;
|
||||
const hasChildren = nodeChildren.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -283,6 +292,8 @@ function DocumentLink(
|
||||
<div ref={dropToReparent}>
|
||||
<DropToImport documentId={node.id} activeClassName="activeDropZone">
|
||||
<SidebarLink
|
||||
expanded={hasChildren ? isExpanded : undefined}
|
||||
onDisclosureClick={handleDisclosureClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
to={{
|
||||
pathname: node.url,
|
||||
@@ -291,13 +302,6 @@ function DocumentLink(
|
||||
},
|
||||
}}
|
||||
label={
|
||||
<>
|
||||
{hasChildDocuments && (
|
||||
<Disclosure
|
||||
expanded={expanded && !isDragging}
|
||||
onClick={handleDisclosureClick}
|
||||
/>
|
||||
)}
|
||||
<EditableTitle
|
||||
title={title}
|
||||
onSubmit={handleTitleChange}
|
||||
@@ -305,7 +309,6 @@ function DocumentLink(
|
||||
canUpdate={canUpdate}
|
||||
maxLength={MAX_TITLE_LENGTH}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
isActive={(match, location) =>
|
||||
!!match && location.search !== "?starred"
|
||||
@@ -351,9 +354,9 @@ function DocumentLink(
|
||||
<DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
|
||||
)}
|
||||
</Relative>
|
||||
{expanded &&
|
||||
!isDragging &&
|
||||
nodeChildren.map((childNode, index) => (
|
||||
{openedOnce && (
|
||||
<Folder $open={expanded && !isDragging}>
|
||||
{nodeChildren.map((childNode, index) => (
|
||||
<ObservedDocumentLink
|
||||
key={childNode.id}
|
||||
collection={collection}
|
||||
@@ -367,10 +370,16 @@ function DocumentLink(
|
||||
parentId={node.id}
|
||||
/>
|
||||
))}
|
||||
</Folder>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Folder = styled.div<{ $open?: boolean }>`
|
||||
display: ${(props) => (props.$open ? "block" : "none")};
|
||||
`;
|
||||
|
||||
const Relative = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
@@ -23,7 +23,7 @@ const Cursor = styled.div<{ isOver?: boolean; position?: "top" }>`
|
||||
width: 100%;
|
||||
height: 14px;
|
||||
background: transparent;
|
||||
${(props) => (props.position === "top" ? "top: 25px;" : "bottom: -7px;")}
|
||||
${(props) => (props.position === "top" ? "top: -7px;" : "bottom: -7px;")}
|
||||
|
||||
::after {
|
||||
background: ${(props) => props.theme.slateDark};
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
import Flex from "~/components/Flex";
|
||||
|
||||
const Header = styled(Flex)`
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
user-select: none;
|
||||
text-transform: uppercase;
|
||||
color: ${(props) => props.theme.sidebarText};
|
||||
letter-spacing: 0.04em;
|
||||
margin: 4px 12px;
|
||||
`;
|
||||
|
||||
export default Header;
|
||||
64
app/components/Sidebar/components/Header.tsx
Normal file
64
app/components/Sidebar/components/Header.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
onClick?: React.MouseEventHandler;
|
||||
expanded?: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function Header({ onClick, expanded, children }: Props) {
|
||||
return (
|
||||
<H3>
|
||||
<Button onClick={onClick} disabled={!onClick}>
|
||||
{children}
|
||||
{onClick && (
|
||||
<Disclosure expanded={expanded} color="currentColor" size={20} />
|
||||
)}
|
||||
</Button>
|
||||
</H3>
|
||||
);
|
||||
}
|
||||
|
||||
const Button = styled.button`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
user-select: none;
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
letter-spacing: 0.03em;
|
||||
margin: 0;
|
||||
padding: 4px 2px 4px 12px;
|
||||
height: 22px;
|
||||
border: 0;
|
||||
background: none;
|
||||
border-radius: 4px;
|
||||
-webkit-appearance: none;
|
||||
transition: all 100ms ease;
|
||||
|
||||
&:not(:disabled):hover,
|
||||
&:not(:disabled):active {
|
||||
color: ${(props) => props.theme.textSecondary};
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const Disclosure = styled(CollapsedIcon)<{ expanded?: boolean }>`
|
||||
transition: opacity 100ms ease, transform 100ms ease, fill 50ms !important;
|
||||
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
||||
opacity: 0;
|
||||
`;
|
||||
|
||||
const H3 = styled.h3`
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
${Disclosure} {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Header;
|
||||
81
app/components/Sidebar/components/SidebarButton.tsx
Normal file
81
app/components/Sidebar/components/SidebarButton.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { ExpandedIcon, MoreIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Flex from "~/components/Flex";
|
||||
|
||||
type Props = {
|
||||
title: React.ReactNode;
|
||||
image: React.ReactNode;
|
||||
minHeight?: number;
|
||||
rounded?: boolean;
|
||||
showDisclosure?: boolean;
|
||||
showMoreMenu?: boolean;
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
const SidebarButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
(
|
||||
{
|
||||
showDisclosure,
|
||||
showMoreMenu,
|
||||
image,
|
||||
title,
|
||||
minHeight = 0,
|
||||
...rest
|
||||
}: Props,
|
||||
ref
|
||||
) => (
|
||||
<Wrapper
|
||||
justify="space-between"
|
||||
align="center"
|
||||
as="button"
|
||||
minHeight={minHeight}
|
||||
{...rest}
|
||||
ref={ref}
|
||||
>
|
||||
<Title gap={4} align="center">
|
||||
{image}
|
||||
{title}
|
||||
</Title>
|
||||
{showDisclosure && <ExpandedIcon color="currentColor" />}
|
||||
{showMoreMenu && <MoreIcon color="currentColor" />}
|
||||
</Wrapper>
|
||||
)
|
||||
);
|
||||
|
||||
const Title = styled(Flex)`
|
||||
color: ${(props) => props.theme.text};
|
||||
flex-shrink: 1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const Wrapper = styled(Flex)<{ minHeight: number }>`
|
||||
padding: 8px 4px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
margin: 8px;
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
border: 0;
|
||||
background: none;
|
||||
flex-shrink: 0;
|
||||
min-height: ${(props) => props.minHeight}px;
|
||||
|
||||
-webkit-appearance: none;
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:active,
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.sidebarText};
|
||||
transition: background 100ms ease-in-out;
|
||||
background: ${(props) => props.theme.sidebarActiveBackground};
|
||||
}
|
||||
`;
|
||||
|
||||
export default SidebarButton;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { transparentize } from "polished";
|
||||
import * as React from "react";
|
||||
import styled, { useTheme, css } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import EventBoundary from "~/components/EventBoundary";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import { NavigationNode } from "~/types";
|
||||
import Disclosure from "./Disclosure";
|
||||
import NavLink, { Props as NavLinkProps } from "./NavLink";
|
||||
|
||||
export type DragObject = NavigationNode & {
|
||||
@@ -19,11 +19,14 @@ type Props = Omit<NavLinkProps, "to"> & {
|
||||
innerRef?: (arg0: HTMLElement | null | undefined) => void;
|
||||
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||
onMouseEnter?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||
onDisclosureClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
icon?: React.ReactNode;
|
||||
label?: React.ReactNode;
|
||||
menu?: React.ReactNode;
|
||||
showActions?: boolean;
|
||||
active?: boolean;
|
||||
/* If set, a disclosure will be rendered to the left of any icon */
|
||||
expanded?: boolean;
|
||||
isActiveDrop?: boolean;
|
||||
isDraft?: boolean;
|
||||
depth?: number;
|
||||
@@ -50,6 +53,8 @@ function SidebarLink(
|
||||
href,
|
||||
depth,
|
||||
className,
|
||||
expanded,
|
||||
onDisclosureClick,
|
||||
...rest
|
||||
}: Props,
|
||||
ref: React.RefObject<HTMLAnchorElement>
|
||||
@@ -66,10 +71,10 @@ function SidebarLink(
|
||||
() => ({
|
||||
fontWeight: 600,
|
||||
color: theme.text,
|
||||
background: theme.sidebarItemBackground,
|
||||
background: theme.sidebarActiveBackground,
|
||||
...style,
|
||||
}),
|
||||
[theme, style]
|
||||
[theme.text, theme.sidebarActiveBackground, style]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -90,14 +95,34 @@ function SidebarLink(
|
||||
ref={ref}
|
||||
{...rest}
|
||||
>
|
||||
<Content>
|
||||
{expanded !== undefined && (
|
||||
<Disclosure
|
||||
expanded={expanded}
|
||||
onClick={onDisclosureClick}
|
||||
root={depth === 0}
|
||||
/>
|
||||
)}
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
<Label>{label}</Label>
|
||||
</Content>
|
||||
</Link>
|
||||
{menu && <Actions showActions={showActions}>{menu}</Actions>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Content = styled.span`
|
||||
display: flex;
|
||||
align-items: start;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
${Disclosure} {
|
||||
margin-top: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
// accounts for whitespace around icon
|
||||
export const IconWrapper = styled.span`
|
||||
margin-left: -4px;
|
||||
@@ -112,6 +137,7 @@ const Actions = styled(EventBoundary)<{ showActions?: boolean }>`
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
gap: 4px;
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
transition: opacity 50ms;
|
||||
|
||||
@@ -158,10 +184,8 @@ const Link = styled(NavLink)<{ $isActiveDrop?: boolean; $isDraft?: boolean }>`
|
||||
transition: fill 50ms;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: ${(props) => props.theme.text};
|
||||
background: ${(props) =>
|
||||
transparentize("0.25", props.theme.sidebarItemBackground)};
|
||||
&:hover svg {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
& + ${Actions} {
|
||||
@@ -170,16 +194,9 @@ const Link = styled(NavLink)<{ $isActiveDrop?: boolean; $isDraft?: boolean }>`
|
||||
}
|
||||
}
|
||||
|
||||
&:focus + ${Actions} {
|
||||
${NudeButton} {
|
||||
background: ${(props) =>
|
||||
transparentize("0.25", props.theme.sidebarItemBackground)};
|
||||
}
|
||||
}
|
||||
|
||||
&[aria-current="page"] + ${Actions} {
|
||||
${NudeButton} {
|
||||
background: ${(props) => props.theme.sidebarItemBackground};
|
||||
background: ${(props) => props.theme.sidebarActiveBackground};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +219,12 @@ const Link = styled(NavLink)<{ $isActiveDrop?: boolean; $isDraft?: boolean }>`
|
||||
props.$isActiveDrop ? props.theme.white : props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
${Disclosure} {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Label = styled.div`
|
||||
@@ -209,6 +232,7 @@ const Label = styled.div`
|
||||
width: 100%;
|
||||
max-height: 4.8em;
|
||||
line-height: 1.6;
|
||||
|
||||
* {
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { observer } from "mobx-react";
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -10,8 +9,8 @@ import Flex from "~/components/Flex";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import useToasts from "~/hooks/useToasts";
|
||||
import DropCursor from "./DropCursor";
|
||||
import Header from "./Header";
|
||||
import PlaceholderCollections from "./PlaceholderCollections";
|
||||
import Section from "./Section";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
import StarredLink from "./StarredLink";
|
||||
|
||||
@@ -119,7 +118,23 @@ function Starred() {
|
||||
}),
|
||||
});
|
||||
|
||||
const content = stars.orderedData.slice(0, upperBound).map((star) => {
|
||||
if (!stars.orderedData.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<Header onClick={handleExpandClick} expanded={expanded}>
|
||||
{t("Starred")}
|
||||
</Header>
|
||||
{expanded && (
|
||||
<Relative>
|
||||
<DropCursor
|
||||
isActiveDrop={isOverReorder}
|
||||
innerRef={dropToReorder}
|
||||
position="top"
|
||||
/>
|
||||
{stars.orderedData.slice(0, upperBound).map((star) => {
|
||||
const document = documents.get(star.documentId);
|
||||
|
||||
return document ? (
|
||||
@@ -130,43 +145,22 @@ function Starred() {
|
||||
collectionId={document.collectionId}
|
||||
to={document.url}
|
||||
title={document.title}
|
||||
depth={2}
|
||||
depth={0}
|
||||
/>
|
||||
) : null;
|
||||
});
|
||||
|
||||
if (!stars.orderedData.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<Flex column>
|
||||
<SidebarLink
|
||||
onClick={handleExpandClick}
|
||||
label={t("Starred")}
|
||||
icon={<Disclosure expanded={expanded} color="currentColor" />}
|
||||
/>
|
||||
{expanded && (
|
||||
<>
|
||||
<DropCursor
|
||||
isActiveDrop={isOverReorder}
|
||||
innerRef={dropToReorder}
|
||||
position="top"
|
||||
/>
|
||||
{content}
|
||||
})}
|
||||
{show === "More" && !isFetching && (
|
||||
<SidebarLink
|
||||
onClick={handleShowMore}
|
||||
label={`${t("Show more")}…`}
|
||||
depth={2}
|
||||
depth={0}
|
||||
/>
|
||||
)}
|
||||
{show === "Less" && !isFetching && (
|
||||
<SidebarLink
|
||||
onClick={handleShowLess}
|
||||
label={`${t("Show less")}…`}
|
||||
depth={2}
|
||||
depth={0}
|
||||
/>
|
||||
)}
|
||||
{(isFetching || fetchError) && !stars.orderedData.length && (
|
||||
@@ -174,16 +168,14 @@ function Starred() {
|
||||
<PlaceholderCollections />
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
</Relative>
|
||||
)}
|
||||
</Flex>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
const Disclosure = styled(CollapsedIcon)<{ expanded?: boolean }>`
|
||||
transition: transform 100ms ease, fill 50ms !important;
|
||||
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
||||
const Relative = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export default observer(Starred);
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { observer } from "mobx-react";
|
||||
import { StarredIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDrag, useDrop } from "react-dnd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { MAX_TITLE_LENGTH } from "@shared/constants";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import parseTitle from "@shared/utils/parseTitle";
|
||||
import Star from "~/models/Star";
|
||||
import EmojiIcon from "~/components/EmojiIcon";
|
||||
import Fade from "~/components/Fade";
|
||||
import useBoolean from "~/hooks/useBoolean";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import DocumentMenu from "~/menus/DocumentMenu";
|
||||
import Disclosure from "./Disclosure";
|
||||
import DropCursor from "./DropCursor";
|
||||
import EditableTitle from "./EditableTitle";
|
||||
import SidebarLink from "./SidebarLink";
|
||||
|
||||
type Props = {
|
||||
@@ -27,24 +26,22 @@ type Props = {
|
||||
|
||||
function StarredLink({
|
||||
depth,
|
||||
title,
|
||||
to,
|
||||
documentId,
|
||||
title,
|
||||
collectionId,
|
||||
star,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { collections, documents, policies } = useStores();
|
||||
const theme = useTheme();
|
||||
const { collections, documents } = useStores();
|
||||
const collection = collections.get(collectionId);
|
||||
const document = documents.get(documentId);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
|
||||
const canUpdate = policies.abilities(documentId).update;
|
||||
const childDocuments = collection
|
||||
? collection.getDocumentChildren(documentId)
|
||||
: [];
|
||||
const hasChildDocuments = childDocuments.length > 0;
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
@@ -57,7 +54,7 @@ function StarredLink({
|
||||
}, [collection, collectionId, collections, document, documentId, documents]);
|
||||
|
||||
const handleDisclosureClick = React.useCallback(
|
||||
(ev: React.MouseEvent<SVGElement>) => {
|
||||
(ev: React.MouseEvent<HTMLButtonElement>) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
setExpanded((prevExpanded) => !prevExpanded);
|
||||
@@ -65,29 +62,6 @@ function StarredLink({
|
||||
[]
|
||||
);
|
||||
|
||||
const handleTitleChange = React.useCallback(
|
||||
async (title: string) => {
|
||||
if (!document) {
|
||||
return;
|
||||
}
|
||||
await documents.update(
|
||||
{
|
||||
id: document.id,
|
||||
text: document.text,
|
||||
title,
|
||||
},
|
||||
{
|
||||
lastRevision: document.revision,
|
||||
}
|
||||
);
|
||||
},
|
||||
[documents, document]
|
||||
);
|
||||
|
||||
const handleTitleEditing = React.useCallback((isEditing: boolean) => {
|
||||
setIsEditing(isEditing);
|
||||
}, []);
|
||||
|
||||
// Draggable
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: "star",
|
||||
@@ -96,7 +70,7 @@ function StarredLink({
|
||||
isDragging: !!monitor.isDragging(),
|
||||
}),
|
||||
canDrag: () => {
|
||||
return depth === 2;
|
||||
return depth === 0;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -116,36 +90,34 @@ function StarredLink({
|
||||
}),
|
||||
});
|
||||
|
||||
const { emoji } = parseTitle(title);
|
||||
const label = emoji ? title.replace(emoji, "") : title;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Draggable key={documentId} ref={drag} $isDragging={isDragging}>
|
||||
<SidebarLink
|
||||
depth={depth}
|
||||
expanded={hasChildDocuments ? expanded : undefined}
|
||||
onDisclosureClick={handleDisclosureClick}
|
||||
to={`${to}?starred`}
|
||||
icon={
|
||||
depth === 0 ? (
|
||||
emoji ? (
|
||||
<EmojiIcon emoji={emoji} />
|
||||
) : (
|
||||
<StarredIcon color={theme.yellow} />
|
||||
)
|
||||
) : undefined
|
||||
}
|
||||
isActive={(match, location) =>
|
||||
!!match && location.search === "?starred"
|
||||
}
|
||||
label={
|
||||
<>
|
||||
{hasChildDocuments && (
|
||||
<Disclosure
|
||||
expanded={expanded}
|
||||
onClick={handleDisclosureClick}
|
||||
/>
|
||||
)}
|
||||
<EditableTitle
|
||||
title={title || t("Untitled")}
|
||||
onSubmit={handleTitleChange}
|
||||
onEditing={handleTitleEditing}
|
||||
canUpdate={canUpdate}
|
||||
maxLength={MAX_TITLE_LENGTH}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
label={depth === 0 ? label : title}
|
||||
exact={false}
|
||||
showActions={menuOpen}
|
||||
menu={
|
||||
document && !isEditing ? (
|
||||
document ? (
|
||||
<Fade>
|
||||
<DocumentMenu
|
||||
document={document}
|
||||
@@ -164,7 +136,7 @@ function StarredLink({
|
||||
childDocuments.map((childDocument) => (
|
||||
<ObserveredStarredLink
|
||||
key={childDocument.id}
|
||||
depth={depth + 1}
|
||||
depth={depth === 0 ? 2 : depth + 1}
|
||||
title={childDocument.title}
|
||||
to={childDocument.url}
|
||||
documentId={childDocument.id}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { ExpandedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Flex from "~/components/Flex";
|
||||
import TeamLogo from "~/components/TeamLogo";
|
||||
|
||||
type Props = {
|
||||
teamName: string;
|
||||
subheading: React.ReactNode;
|
||||
showDisclosure?: boolean;
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
logoUrl: string;
|
||||
};
|
||||
|
||||
const TeamButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
({ showDisclosure, teamName, subheading, logoUrl, ...rest }: Props, ref) => (
|
||||
<Wrapper>
|
||||
<Header ref={ref} {...rest}>
|
||||
<TeamLogo
|
||||
alt={`${teamName} logo`}
|
||||
src={logoUrl}
|
||||
width={38}
|
||||
height={38}
|
||||
/>
|
||||
<Flex align="flex-start" column>
|
||||
<TeamName>
|
||||
{teamName} {showDisclosure && <Disclosure color="currentColor" />}
|
||||
</TeamName>
|
||||
<Subheading>{subheading}</Subheading>
|
||||
</Flex>
|
||||
</Header>
|
||||
</Wrapper>
|
||||
)
|
||||
);
|
||||
|
||||
const Disclosure = styled(ExpandedIcon)`
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
`;
|
||||
|
||||
const Subheading = styled.div`
|
||||
padding-left: 10px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
color: ${(props) => props.theme.sidebarText};
|
||||
`;
|
||||
|
||||
const TeamName = styled.div`
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
padding-right: 24px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.text};
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const Header = styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: none;
|
||||
line-height: inherit;
|
||||
border: 0;
|
||||
padding: 8px;
|
||||
margin: 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
width: calc(100% - 16px);
|
||||
|
||||
&:active,
|
||||
&:hover {
|
||||
transition: background 100ms ease-in-out;
|
||||
background: ${(props) => props.theme.sidebarItemBackground};
|
||||
}
|
||||
`;
|
||||
|
||||
export default observer(TeamButton);
|
||||
@@ -1,3 +1,3 @@
|
||||
import Sidebar from "./Main";
|
||||
import Sidebar from "./App";
|
||||
|
||||
export default Sidebar;
|
||||
|
||||
@@ -6,7 +6,7 @@ const TeamLogo = styled.img<{ width?: number; height?: number; size?: string }>`
|
||||
height: ${(props) =>
|
||||
props.height ? `${props.height}px` : props.size || "38px"};
|
||||
border-radius: 4px;
|
||||
background: ${(props) => props.theme.background};
|
||||
background: white;
|
||||
border: 1px solid ${(props) => props.theme.divider};
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -2,13 +2,10 @@ import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MenuButton, useMenuState } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import { createAction } from "~/actions";
|
||||
import { development } from "~/actions/definitions/debug";
|
||||
import {
|
||||
navigateToSettings,
|
||||
navigateToProfileSettings,
|
||||
openKeyboardShortcuts,
|
||||
openChangelog,
|
||||
openAPIDocumentation,
|
||||
@@ -17,9 +14,7 @@ import {
|
||||
logout,
|
||||
} from "~/actions/definitions/navigation";
|
||||
import { changeTheme } from "~/actions/definitions/settings";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
import useSessions from "~/hooks/useSessions";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import separator from "~/menus/separator";
|
||||
|
||||
@@ -28,15 +23,12 @@ type Props = {
|
||||
};
|
||||
|
||||
function AccountMenu(props: Props) {
|
||||
const [sessions] = useSessions();
|
||||
const menu = useMenuState({
|
||||
unstable_offset: [8, 0],
|
||||
placement: "bottom-start",
|
||||
placement: "bottom-end",
|
||||
modal: true,
|
||||
});
|
||||
const { ui } = useStores();
|
||||
const { theme } = ui;
|
||||
const team = useCurrentTeam();
|
||||
const previousTheme = usePrevious(theme);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -47,39 +39,19 @@ function AccountMenu(props: Props) {
|
||||
}, [menu, theme, previousTheme]);
|
||||
|
||||
const actions = React.useMemo(() => {
|
||||
const otherSessions = sessions.filter(
|
||||
(session) => session.teamId !== team.id && session.url !== team.url
|
||||
);
|
||||
|
||||
return [
|
||||
navigateToSettings,
|
||||
openKeyboardShortcuts,
|
||||
openAPIDocumentation,
|
||||
separator(),
|
||||
openChangelog,
|
||||
openFeedbackUrl,
|
||||
openBugReportUrl,
|
||||
development,
|
||||
changeTheme,
|
||||
navigateToProfileSettings,
|
||||
separator(),
|
||||
...(otherSessions.length
|
||||
? [
|
||||
createAction({
|
||||
name: t("Switch team"),
|
||||
section: "account",
|
||||
children: otherSessions.map((session) => ({
|
||||
id: session.url,
|
||||
name: session.name,
|
||||
section: "account",
|
||||
icon: <Logo alt={session.name} src={session.logoUrl} />,
|
||||
perform: () => (window.location.href = session.url),
|
||||
})),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
logout,
|
||||
];
|
||||
}, [team.id, team.url, sessions, t]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -91,10 +63,4 @@ function AccountMenu(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const Logo = styled("img")`
|
||||
border-radius: 2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
export default observer(AccountMenu);
|
||||
|
||||
82
app/menus/OrganizationMenu.tsx
Normal file
82
app/menus/OrganizationMenu.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MenuButton, useMenuState } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import { createAction } from "~/actions";
|
||||
import { navigateToSettings, logout } from "~/actions/definitions/navigation";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
import useSessions from "~/hooks/useSessions";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import separator from "~/menus/separator";
|
||||
|
||||
type Props = {
|
||||
children: (props: any) => React.ReactNode;
|
||||
};
|
||||
|
||||
function OrganizationMenu(props: Props) {
|
||||
const [sessions] = useSessions();
|
||||
const menu = useMenuState({
|
||||
unstable_offset: [4, -4],
|
||||
placement: "bottom-start",
|
||||
modal: true,
|
||||
});
|
||||
const { ui } = useStores();
|
||||
const { theme } = ui;
|
||||
const team = useCurrentTeam();
|
||||
const previousTheme = usePrevious(theme);
|
||||
const { t } = useTranslation();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (theme !== previousTheme) {
|
||||
menu.hide();
|
||||
}
|
||||
}, [menu, theme, previousTheme]);
|
||||
|
||||
const actions = React.useMemo(() => {
|
||||
const otherSessions = sessions.filter(
|
||||
(session) => session.teamId !== team.id && session.url !== team.url
|
||||
);
|
||||
|
||||
return [
|
||||
navigateToSettings,
|
||||
separator(),
|
||||
...(otherSessions.length
|
||||
? [
|
||||
createAction({
|
||||
name: t("Switch team"),
|
||||
section: "account",
|
||||
children: otherSessions.map((session) => ({
|
||||
id: session.url,
|
||||
name: session.name,
|
||||
section: "account",
|
||||
icon: <Logo alt={session.name} src={session.logoUrl} />,
|
||||
perform: () => (window.location.href = session.url),
|
||||
})),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
logout,
|
||||
];
|
||||
}, [team.id, team.url, sessions, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuButton {...menu}>{props.children}</MenuButton>
|
||||
<ContextMenu {...menu} aria-label={t("Account")}>
|
||||
<Template {...menu} items={undefined} actions={actions} />
|
||||
</ContextMenu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Logo = styled("img")`
|
||||
border-radius: 2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
export default observer(OrganizationMenu);
|
||||
3
app/typings/styled-components.d.ts
vendored
3
app/typings/styled-components.d.ts
vendored
@@ -150,7 +150,8 @@ declare module "styled-components" {
|
||||
textTertiary: string;
|
||||
placeholder: string;
|
||||
sidebarBackground: string;
|
||||
sidebarItemBackground: string;
|
||||
sidebarActiveBackground: string;
|
||||
sidebarControlHoverBackground: string;
|
||||
sidebarDraftBorder: string;
|
||||
sidebarText: string;
|
||||
backdrop: string;
|
||||
|
||||
@@ -14,10 +14,6 @@ export function templatesPath(): string {
|
||||
return "/templates";
|
||||
}
|
||||
|
||||
export function settingsPath(): string {
|
||||
return "/settings";
|
||||
}
|
||||
|
||||
export function archivePath(): string {
|
||||
return "/archive";
|
||||
}
|
||||
@@ -26,6 +22,18 @@ export function trashPath(): string {
|
||||
return "/trash";
|
||||
}
|
||||
|
||||
export function settingsPath(): string {
|
||||
return "/settings";
|
||||
}
|
||||
|
||||
export function organizationSettingsPath(): string {
|
||||
return "/settings/details";
|
||||
}
|
||||
|
||||
export function profileSettingsPath(): string {
|
||||
return "/settings/profile";
|
||||
}
|
||||
|
||||
export function groupSettingsPath(): string {
|
||||
return "/settings/groups";
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"Archive": "Archive",
|
||||
"Trash": "Trash",
|
||||
"Settings": "Settings",
|
||||
"Profile": "Profile",
|
||||
"API documentation": "API documentation",
|
||||
"Send us feedback": "Send us feedback",
|
||||
"Report a bug": "Report a bug",
|
||||
@@ -149,7 +150,6 @@
|
||||
"Delete {{ documentName }}": "Delete {{ documentName }}",
|
||||
"Return to App": "Back to App",
|
||||
"Account": "Account",
|
||||
"Profile": "Profile",
|
||||
"Notifications": "Notifications",
|
||||
"API Tokens": "API Tokens",
|
||||
"Team": "Team",
|
||||
@@ -227,7 +227,6 @@
|
||||
"Warning": "Warning",
|
||||
"Warning notice": "Warning notice",
|
||||
"Could not import file": "Could not import file",
|
||||
"Switch team": "Switch team",
|
||||
"Show path to document": "Show path to document",
|
||||
"Path to document": "Path to document",
|
||||
"Group member options": "Group member options",
|
||||
@@ -264,6 +263,7 @@
|
||||
"New child document": "New child document",
|
||||
"New document in <em>{{ collectionName }}</em>": "New document in <em>{{ collectionName }}</em>",
|
||||
"New template": "New template",
|
||||
"Switch team": "Switch team",
|
||||
"Link copied": "Link copied",
|
||||
"Revision options": "Revision options",
|
||||
"Restore version": "Restore version",
|
||||
|
||||
@@ -134,7 +134,8 @@ export const light = {
|
||||
textTertiary: colors.slate,
|
||||
placeholder: "#a2b2c3",
|
||||
sidebarBackground: colors.warmGrey,
|
||||
sidebarItemBackground: "#d7e0ea",
|
||||
sidebarActiveBackground: "#d7e0ea",
|
||||
sidebarControlHoverBackground: "rgba(0,0,0,0.1)",
|
||||
sidebarDraftBorder: darken("0.25", colors.warmGrey),
|
||||
sidebarText: "rgb(78, 92, 110)",
|
||||
backdrop: "rgba(0, 0, 0, 0.2)",
|
||||
@@ -184,7 +185,8 @@ export const dark = {
|
||||
textTertiary: colors.slate,
|
||||
placeholder: colors.slateDark,
|
||||
sidebarBackground: colors.veryDarkBlue,
|
||||
sidebarItemBackground: lighten(0.015, colors.almostBlack),
|
||||
sidebarActiveBackground: lighten(0.02, colors.almostBlack),
|
||||
sidebarControlHoverBackground: "rgba(255,255,255,0.1)",
|
||||
sidebarDraftBorder: darken("0.35", colors.slate),
|
||||
sidebarText: colors.slate,
|
||||
backdrop: "rgba(255, 255, 255, 0.3)",
|
||||
|
||||
Reference in New Issue
Block a user