feat: allow user to set TOC display preference (#6943)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
@@ -3,15 +3,14 @@ import * as React from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import breakpoint from "styled-components-breakpoint";
|
import breakpoint from "styled-components-breakpoint";
|
||||||
import { s } from "@shared/styles";
|
import { EditorStyleHelper } from "@shared/editor/styles/EditorStyleHelper";
|
||||||
|
import { depths, s } from "@shared/styles";
|
||||||
import Text from "~/components/Text";
|
import Text from "~/components/Text";
|
||||||
import useWindowScrollPosition from "~/hooks/useWindowScrollPosition";
|
import useWindowScrollPosition from "~/hooks/useWindowScrollPosition";
|
||||||
|
|
||||||
const HEADING_OFFSET = 20;
|
const HEADING_OFFSET = 20;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
/** Whether the document is rendering full width or not. */
|
|
||||||
isFullWidth: boolean;
|
|
||||||
/** The headings to render in the contents. */
|
/** The headings to render in the contents. */
|
||||||
headings: {
|
headings: {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -20,9 +19,9 @@ type Props = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Contents({ headings, isFullWidth }: Props) {
|
export default function Contents({ headings }: Props) {
|
||||||
const [activeSlug, setActiveSlug] = React.useState<string>();
|
const [activeSlug, setActiveSlug] = React.useState<string>();
|
||||||
const position = useWindowScrollPosition({
|
const scrollPosition = useWindowScrollPosition({
|
||||||
throttle: 100,
|
throttle: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ export default function Contents({ headings, isFullWidth }: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [position, headings]);
|
}, [scrollPosition, headings]);
|
||||||
|
|
||||||
// calculate the minimum heading level and adjust all the headings to make
|
// calculate the minimum heading level and adjust all the headings to make
|
||||||
// that the top-most. This prevents the contents from being weirdly indented
|
// that the top-most. This prevents the contents from being weirdly indented
|
||||||
@@ -56,70 +55,53 @@ export default function Contents({ headings, isFullWidth }: Props) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper isFullWidth={isFullWidth}>
|
<StickyWrapper>
|
||||||
<Sticky>
|
<Heading>{t("Contents")}</Heading>
|
||||||
<Heading>{t("Contents")}</Heading>
|
{headings.length ? (
|
||||||
{headings.length ? (
|
<List>
|
||||||
<List>
|
{headings
|
||||||
{headings
|
.filter((heading) => heading.level < 4)
|
||||||
.filter((heading) => heading.level < 4)
|
.map((heading) => (
|
||||||
.map((heading) => (
|
<ListItem
|
||||||
<ListItem
|
key={heading.id}
|
||||||
key={heading.id}
|
level={heading.level - headingAdjustment}
|
||||||
level={heading.level - headingAdjustment}
|
active={activeSlug === heading.id}
|
||||||
active={activeSlug === heading.id}
|
>
|
||||||
>
|
<Link href={`#${heading.id}`}>{heading.title}</Link>
|
||||||
<Link href={`#${heading.id}`}>{heading.title}</Link>
|
</ListItem>
|
||||||
</ListItem>
|
))}
|
||||||
))}
|
</List>
|
||||||
</List>
|
) : (
|
||||||
) : (
|
<Empty>{t("Headings you add to the document will appear here")}</Empty>
|
||||||
<Empty>
|
)}
|
||||||
{t("Headings you add to the document will appear here")}
|
</StickyWrapper>
|
||||||
</Empty>
|
|
||||||
)}
|
|
||||||
</Sticky>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wrapper = styled.div<{ isFullWidth: boolean }>`
|
const StickyWrapper = styled.div`
|
||||||
width: 256px;
|
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
${breakpoint("tablet")`
|
|
||||||
display: block;
|
|
||||||
`};
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
!props.isFullWidth &&
|
|
||||||
breakpoint("desktopLarge")`
|
|
||||||
transform: translateX(-256px);
|
|
||||||
width: 0;
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Sticky = styled.div`
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 80px;
|
top: 90px;
|
||||||
max-height: calc(100vh - 80px);
|
max-height: calc(100vh - 90px);
|
||||||
|
width: ${EditorStyleHelper.tocWidth}px;
|
||||||
|
|
||||||
|
padding: 0 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
background: ${s("background")};
|
background: ${s("background")};
|
||||||
transition: ${s("backgroundTransition")};
|
transition: ${s("backgroundTransition")};
|
||||||
|
|
||||||
margin-top: calc(50px + 6vh);
|
|
||||||
margin-right: 52px;
|
|
||||||
min-width: 204px;
|
|
||||||
width: 228px;
|
|
||||||
min-height: 40px;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 0 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
@supports (backdrop-filter: blur(20px)) {
|
@supports (backdrop-filter: blur(20px)) {
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
background: ${(props) => transparentize(0.2, props.theme.background)};
|
background: ${(props) => transparentize(0.2, props.theme.background)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${breakpoint("tablet")`
|
||||||
|
display: block;
|
||||||
|
z-index: ${depths.toc};
|
||||||
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Heading = styled.h3`
|
const Heading = styled.h3`
|
||||||
@@ -131,15 +113,12 @@ const Heading = styled.h3`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Empty = styled(Text)`
|
const Empty = styled(Text)`
|
||||||
margin: 1em 0 4em;
|
|
||||||
padding-right: 2em;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ListItem = styled.li<{ level: number; active?: boolean }>`
|
const ListItem = styled.li<{ level: number; active?: boolean }>`
|
||||||
margin-left: ${(props) => (props.level - 1) * 10}px;
|
margin-left: ${(props) => (props.level - 1) * 10}px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
padding-right: 2em;
|
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ import {
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import breakpoint from "styled-components-breakpoint";
|
import breakpoint from "styled-components-breakpoint";
|
||||||
|
import { EditorStyleHelper } from "@shared/editor/styles/EditorStyleHelper";
|
||||||
import { s } from "@shared/styles";
|
import { s } from "@shared/styles";
|
||||||
import { NavigationNode } from "@shared/types";
|
import { NavigationNode, TOCPosition, TeamPreference } from "@shared/types";
|
||||||
import { ProsemirrorHelper, Heading } from "@shared/utils/ProsemirrorHelper";
|
import { ProsemirrorHelper, Heading } from "@shared/utils/ProsemirrorHelper";
|
||||||
import { parseDomain } from "@shared/utils/domains";
|
import { parseDomain } from "@shared/utils/domains";
|
||||||
import RootStore from "~/stores/RootStore";
|
import RootStore from "~/stores/RootStore";
|
||||||
@@ -403,6 +404,9 @@ class DocumentScene extends React.Component<Props> {
|
|||||||
const hasHeadings = this.headings.length > 0;
|
const hasHeadings = this.headings.length > 0;
|
||||||
const showContents =
|
const showContents =
|
||||||
ui.tocVisible && ((readOnly && hasHeadings) || !readOnly);
|
ui.tocVisible && ((readOnly && hasHeadings) || !readOnly);
|
||||||
|
const tocPosition =
|
||||||
|
(team?.getPreference(TeamPreference.TocPosition) as TOCPosition) ||
|
||||||
|
TOCPosition.Left;
|
||||||
const multiplayerEditor =
|
const multiplayerEditor =
|
||||||
!document.isArchived && !document.isDeleted && !revision && !isShare;
|
!document.isArchived && !document.isDeleted && !revision && !isShare;
|
||||||
|
|
||||||
@@ -449,7 +453,7 @@ class DocumentScene extends React.Component<Props> {
|
|||||||
favicon={document.emoji ? emojiToUrl(document.emoji) : undefined}
|
favicon={document.emoji ? emojiToUrl(document.emoji) : undefined}
|
||||||
/>
|
/>
|
||||||
{(this.isUploading || this.isSaving) && <LoadingIndicator />}
|
{(this.isUploading || this.isSaving) && <LoadingIndicator />}
|
||||||
<Container justify="center" column auto>
|
<Container column>
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<Prompt
|
<Prompt
|
||||||
when={this.isUploading && !this.isEditorDirty}
|
when={this.isUploading && !this.isEditorDirty}
|
||||||
@@ -476,27 +480,39 @@ class DocumentScene extends React.Component<Props> {
|
|||||||
onSave={this.onSave}
|
onSave={this.onSave}
|
||||||
headings={this.headings}
|
headings={this.headings}
|
||||||
/>
|
/>
|
||||||
<MeasuredContainer
|
<Flex justify="center">
|
||||||
as={MaxWidth}
|
|
||||||
name="document"
|
|
||||||
archived={document.isArchived}
|
|
||||||
showContents={showContents}
|
|
||||||
isEditing={!readOnly}
|
|
||||||
isFullWidth={document.fullWidth}
|
|
||||||
column
|
|
||||||
auto
|
|
||||||
>
|
|
||||||
<Notices document={document} readOnly={readOnly} />
|
<Notices document={document} readOnly={readOnly} />
|
||||||
|
</Flex>
|
||||||
|
<MeasuredContainer
|
||||||
|
as={Main}
|
||||||
|
name="document"
|
||||||
|
fullWidth={document.fullWidth}
|
||||||
|
tocPosition={tocPosition}
|
||||||
|
>
|
||||||
<React.Suspense fallback={<PlaceholderDocument />}>
|
<React.Suspense fallback={<PlaceholderDocument />}>
|
||||||
<Flex auto={!readOnly} reverse>
|
{revision ? (
|
||||||
{revision ? (
|
<RevisionContainer docFullWidth={document.fullWidth}>
|
||||||
<RevisionViewer
|
<RevisionViewer
|
||||||
document={document}
|
document={document}
|
||||||
revision={revision}
|
revision={revision}
|
||||||
id={revision.id}
|
id={revision.id}
|
||||||
/>
|
/>
|
||||||
) : (
|
</RevisionContainer>
|
||||||
<>
|
) : (
|
||||||
|
<>
|
||||||
|
{showContents && (
|
||||||
|
<ContentsContainer
|
||||||
|
docFullWidth={document.fullWidth}
|
||||||
|
position={tocPosition}
|
||||||
|
>
|
||||||
|
<Contents headings={this.headings} />
|
||||||
|
</ContentsContainer>
|
||||||
|
)}
|
||||||
|
<EditorContainer
|
||||||
|
docFullWidth={document.fullWidth}
|
||||||
|
showContents={showContents}
|
||||||
|
tocPosition={tocPosition}
|
||||||
|
>
|
||||||
<Editor
|
<Editor
|
||||||
id={document.id}
|
id={document.id}
|
||||||
key={embedsDisabled ? "disabled" : "enabled"}
|
key={embedsDisabled ? "disabled" : "enabled"}
|
||||||
@@ -543,16 +559,9 @@ class DocumentScene extends React.Component<Props> {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Editor>
|
</Editor>
|
||||||
|
</EditorContainer>
|
||||||
{showContents && (
|
</>
|
||||||
<Contents
|
)}
|
||||||
headings={this.headings}
|
|
||||||
isFullWidth={document.fullWidth}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</MeasuredContainer>
|
</MeasuredContainer>
|
||||||
{isShare &&
|
{isShare &&
|
||||||
@@ -573,6 +582,95 @@ class DocumentScene extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MainProps = {
|
||||||
|
fullWidth: boolean;
|
||||||
|
tocPosition: TOCPosition;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Main = styled.div<MainProps>`
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
|
${breakpoint("tablet")`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: ${({ fullWidth, tocPosition }: MainProps) =>
|
||||||
|
fullWidth
|
||||||
|
? tocPosition === TOCPosition.Left
|
||||||
|
? `${EditorStyleHelper.tocWidth}px minmax(0, 1fr)`
|
||||||
|
: `minmax(0, 1fr) ${EditorStyleHelper.tocWidth}px`
|
||||||
|
: `1fr minmax(0, ${`calc(46em + 76px)`}) 1fr`};
|
||||||
|
`};
|
||||||
|
|
||||||
|
${breakpoint("desktopLarge")`
|
||||||
|
grid-template-columns: ${({ fullWidth, tocPosition }: MainProps) =>
|
||||||
|
fullWidth
|
||||||
|
? tocPosition === TOCPosition.Left
|
||||||
|
? `${EditorStyleHelper.tocWidth}px minmax(0, 1fr)`
|
||||||
|
: `minmax(0, 1fr) ${EditorStyleHelper.tocWidth}px`
|
||||||
|
: `1fr minmax(0, ${`calc(52em + 76px)`}) 1fr`};
|
||||||
|
`};
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ContentsContainerProps = {
|
||||||
|
docFullWidth: boolean;
|
||||||
|
position: TOCPosition;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContentsContainer = styled.div<ContentsContainerProps>`
|
||||||
|
margin-top: calc(44px + 6vh);
|
||||||
|
|
||||||
|
${breakpoint("tablet")`
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: ${({ docFullWidth, position }: ContentsContainerProps) =>
|
||||||
|
position === TOCPosition.Left ? 1 : docFullWidth ? 2 : 3};
|
||||||
|
justify-self: ${({ position }: ContentsContainerProps) =>
|
||||||
|
position === TOCPosition.Left ? "end" : "start"};
|
||||||
|
`};
|
||||||
|
`;
|
||||||
|
|
||||||
|
type EditorContainerProps = {
|
||||||
|
docFullWidth: boolean;
|
||||||
|
showContents: boolean;
|
||||||
|
tocPosition: TOCPosition;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditorContainer = styled.div<EditorContainerProps>`
|
||||||
|
// Adds space to the gutter to make room for icon & heading annotations
|
||||||
|
padding: 0 44px;
|
||||||
|
|
||||||
|
${breakpoint("tablet")`
|
||||||
|
grid-row: 1;
|
||||||
|
|
||||||
|
// Decides the editor column position & span
|
||||||
|
grid-column: ${({
|
||||||
|
docFullWidth,
|
||||||
|
showContents,
|
||||||
|
tocPosition,
|
||||||
|
}: EditorContainerProps) =>
|
||||||
|
docFullWidth
|
||||||
|
? showContents
|
||||||
|
? tocPosition === TOCPosition.Left
|
||||||
|
? 2
|
||||||
|
: 1
|
||||||
|
: "1 / -1"
|
||||||
|
: 2};
|
||||||
|
`};
|
||||||
|
`;
|
||||||
|
|
||||||
|
type RevisionContainerProps = {
|
||||||
|
docFullWidth: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RevisionContainer = styled.div<RevisionContainerProps>`
|
||||||
|
// Adds space to the gutter to make room for icon
|
||||||
|
padding: 0 44px;
|
||||||
|
|
||||||
|
${breakpoint("tablet")`
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: ${({ docFullWidth }: RevisionContainerProps) =>
|
||||||
|
docFullWidth ? "1 / -1" : 2};
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
const Footer = styled.div`
|
const Footer = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -595,34 +693,4 @@ const ReferencesWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type MaxWidthProps = {
|
|
||||||
isEditing?: boolean;
|
|
||||||
isFullWidth?: boolean;
|
|
||||||
archived?: boolean;
|
|
||||||
showContents?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MaxWidth = styled(Flex)<MaxWidthProps>`
|
|
||||||
// Adds space to the gutter to make room for heading annotations
|
|
||||||
padding: 0 32px;
|
|
||||||
transition: padding 100ms;
|
|
||||||
max-width: 100vw;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
padding-bottom: 16px;
|
|
||||||
|
|
||||||
${breakpoint("tablet")`
|
|
||||||
margin: 4px auto 12px;
|
|
||||||
max-width: ${(props: MaxWidthProps) =>
|
|
||||||
props.isFullWidth
|
|
||||||
? "100vw"
|
|
||||||
: `calc(64px + 46em + ${props.showContents ? "256px" : "0px"});`}
|
|
||||||
`};
|
|
||||||
|
|
||||||
${breakpoint("desktopLarge")`
|
|
||||||
max-width: ${(props: MaxWidthProps) =>
|
|
||||||
props.isFullWidth ? "100vw" : `calc(64px + 52em);`}
|
|
||||||
`};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default withTranslation()(withStores(withRouter(DocumentScene)));
|
export default withTranslation()(withStores(withRouter(DocumentScene)));
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Selection } from "prosemirror-state";
|
|||||||
import { __parseFromClipboard } from "prosemirror-view";
|
import { __parseFromClipboard } from "prosemirror-view";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { mergeRefs } from "react-merge-refs";
|
import { mergeRefs } from "react-merge-refs";
|
||||||
import styled, { css } from "styled-components";
|
import styled from "styled-components";
|
||||||
import breakpoint from "styled-components-breakpoint";
|
import breakpoint from "styled-components-breakpoint";
|
||||||
import isMarkdown from "@shared/editor/lib/isMarkdown";
|
import isMarkdown from "@shared/editor/lib/isMarkdown";
|
||||||
import normalizePastedMarkdown from "@shared/editor/lib/markdown/normalize";
|
import normalizePastedMarkdown from "@shared/editor/lib/markdown/normalize";
|
||||||
@@ -33,8 +33,6 @@ type Props = {
|
|||||||
title: string;
|
title: string;
|
||||||
/** Emoji to display */
|
/** Emoji to display */
|
||||||
emoji?: string | null;
|
emoji?: string | null;
|
||||||
/** Position of the emoji relative to text */
|
|
||||||
emojiPosition: "side" | "top";
|
|
||||||
/** Placeholder to display when the document has no title */
|
/** Placeholder to display when the document has no title */
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
/** Should the title be editable, policies will also be considered separately */
|
/** Should the title be editable, policies will also be considered separately */
|
||||||
@@ -59,7 +57,6 @@ const DocumentTitle = React.forwardRef(function _DocumentTitle(
|
|||||||
documentId,
|
documentId,
|
||||||
title,
|
title,
|
||||||
emoji,
|
emoji,
|
||||||
emojiPosition,
|
|
||||||
readOnly,
|
readOnly,
|
||||||
onChangeTitle,
|
onChangeTitle,
|
||||||
onChangeEmoji,
|
onChangeEmoji,
|
||||||
@@ -247,12 +244,7 @@ const DocumentTitle = React.forwardRef(function _DocumentTitle(
|
|||||||
ref={mergeRefs([ref, externalRef])}
|
ref={mergeRefs([ref, externalRef])}
|
||||||
>
|
>
|
||||||
{can.update && !readOnly ? (
|
{can.update && !readOnly ? (
|
||||||
<EmojiWrapper
|
<EmojiWrapper align="center" justify="center" dir={dir}>
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
$position={emojiPosition}
|
|
||||||
dir={dir}
|
|
||||||
>
|
|
||||||
<React.Suspense fallback={emojiIcon}>
|
<React.Suspense fallback={emojiIcon}>
|
||||||
<StyledEmojiPicker
|
<StyledEmojiPicker
|
||||||
value={emoji}
|
value={emoji}
|
||||||
@@ -265,12 +257,7 @@ const DocumentTitle = React.forwardRef(function _DocumentTitle(
|
|||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</EmojiWrapper>
|
</EmojiWrapper>
|
||||||
) : emoji ? (
|
) : emoji ? (
|
||||||
<EmojiWrapper
|
<EmojiWrapper align="center" justify="center" dir={dir}>
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
$position={emojiPosition}
|
|
||||||
dir={dir}
|
|
||||||
>
|
|
||||||
{emojiIcon}
|
{emojiIcon}
|
||||||
</EmojiWrapper>
|
</EmojiWrapper>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -282,25 +269,17 @@ const StyledEmojiPicker = styled(EmojiPicker)`
|
|||||||
${extraArea(8)}
|
${extraArea(8)}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EmojiWrapper = styled(Flex)<{ $position: "top" | "side"; dir?: string }>`
|
const EmojiWrapper = styled(Flex)<{ dir?: string }>`
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
|
|
||||||
// Always move above TOC
|
// Always move above TOC
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
${(props) =>
|
${(props: { dir?: string }) =>
|
||||||
props.$position === "top"
|
props.dir === "rtl" ? "right: -40px" : "left: -40px"};
|
||||||
? css`
|
|
||||||
position: relative;
|
|
||||||
top: -8px;
|
|
||||||
`
|
|
||||||
: css`
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
${(props: { dir?: string }) =>
|
|
||||||
props.dir === "rtl" ? "right: -40px" : "left: -40px"};
|
|
||||||
`}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type TitleProps = {
|
type TitleProps = {
|
||||||
|
|||||||
@@ -187,7 +187,6 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
|
|||||||
: document.title
|
: document.title
|
||||||
}
|
}
|
||||||
emoji={document.emoji}
|
emoji={document.emoji}
|
||||||
emojiPosition={document.fullWidth ? "top" : "side"}
|
|
||||||
onChangeTitle={onChangeTitle}
|
onChangeTitle={onChangeTitle}
|
||||||
onChangeEmoji={onChangeEmoji}
|
onChangeEmoji={onChangeEmoji}
|
||||||
onGoToNextInput={handleGoToNextInput}
|
onGoToNextInput={handleGoToNextInput}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ function RevisionViewer(props: Props) {
|
|||||||
documentId={revision.documentId}
|
documentId={revision.documentId}
|
||||||
title={revision.title}
|
title={revision.title}
|
||||||
emoji={revision.emoji}
|
emoji={revision.emoji}
|
||||||
emojiPosition={document.fullWidth ? "top" : "side"}
|
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<DocumentMeta
|
<DocumentMeta
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useTranslation, Trans } from "react-i18next";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ThemeProvider, useTheme } from "styled-components";
|
import { ThemeProvider, useTheme } from "styled-components";
|
||||||
import { buildDarkTheme, buildLightTheme } from "@shared/styles/theme";
|
import { buildDarkTheme, buildLightTheme } from "@shared/styles/theme";
|
||||||
import { CustomTheme, TeamPreference } from "@shared/types";
|
import { CustomTheme, TOCPosition, TeamPreference } from "@shared/types";
|
||||||
import { getBaseDomain } from "@shared/utils/domains";
|
import { getBaseDomain } from "@shared/utils/domains";
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import ButtonLink from "~/components/ButtonLink";
|
import ButtonLink from "~/components/ButtonLink";
|
||||||
@@ -16,6 +16,7 @@ import DefaultCollectionInputSelect from "~/components/DefaultCollectionInputSel
|
|||||||
import Heading from "~/components/Heading";
|
import Heading from "~/components/Heading";
|
||||||
import Input from "~/components/Input";
|
import Input from "~/components/Input";
|
||||||
import InputColor from "~/components/InputColor";
|
import InputColor from "~/components/InputColor";
|
||||||
|
import InputSelect from "~/components/InputSelect";
|
||||||
import Scene from "~/components/Scene";
|
import Scene from "~/components/Scene";
|
||||||
import Switch from "~/components/Switch";
|
import Switch from "~/components/Switch";
|
||||||
import Text from "~/components/Text";
|
import Text from "~/components/Text";
|
||||||
@@ -58,6 +59,10 @@ function Details() {
|
|||||||
isHexColor
|
isHexColor
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [tocPosition, setTocPosition] = useState(
|
||||||
|
team.getPreference(TeamPreference.TocPosition) as TOCPosition
|
||||||
|
);
|
||||||
|
|
||||||
const handleSubmit = React.useCallback(
|
const handleSubmit = React.useCallback(
|
||||||
async (event?: React.SyntheticEvent) => {
|
async (event?: React.SyntheticEvent) => {
|
||||||
if (event) {
|
if (event) {
|
||||||
@@ -73,6 +78,7 @@ function Details() {
|
|||||||
...team.preferences,
|
...team.preferences,
|
||||||
publicBranding,
|
publicBranding,
|
||||||
customTheme,
|
customTheme,
|
||||||
|
tocPosition,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
toast.success(t("Settings saved"));
|
toast.success(t("Settings saved"));
|
||||||
@@ -174,7 +180,6 @@ function Details() {
|
|||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingRow
|
<SettingRow
|
||||||
border={false}
|
|
||||||
label={t("Theme")}
|
label={t("Theme")}
|
||||||
name="accent"
|
name="accent"
|
||||||
description={
|
description={
|
||||||
@@ -212,7 +217,6 @@ function Details() {
|
|||||||
</SettingRow>
|
</SettingRow>
|
||||||
{team.avatarUrl && (
|
{team.avatarUrl && (
|
||||||
<SettingRow
|
<SettingRow
|
||||||
border={false}
|
|
||||||
name={TeamPreference.PublicBranding}
|
name={TeamPreference.PublicBranding}
|
||||||
label={t("Public branding")}
|
label={t("Public branding")}
|
||||||
description={t(
|
description={t(
|
||||||
@@ -229,6 +233,30 @@ function Details() {
|
|||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
)}
|
)}
|
||||||
|
<SettingRow
|
||||||
|
border={false}
|
||||||
|
label={t("Table of contents position")}
|
||||||
|
name="tocPosition"
|
||||||
|
description={t(
|
||||||
|
"The side to display the table of contents in relation to the main content."
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<InputSelect
|
||||||
|
ariaLabel={t("Table of contents position")}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: t("Left"),
|
||||||
|
value: TOCPosition.Left,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("Right"),
|
||||||
|
value: TOCPosition.Right,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
value={tocPosition}
|
||||||
|
onChange={(p: TOCPosition) => setTocPosition(p)}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
|
||||||
<Heading as="h2">{t("Behavior")}</Heading>
|
<Heading as="h2">{t("Behavior")}</Heading>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { UserRole } from "@shared/types";
|
import { TOCPosition, UserRole } from "@shared/types";
|
||||||
import { BaseSchema } from "@server/routes/api/schema";
|
import { BaseSchema } from "@server/routes/api/schema";
|
||||||
|
|
||||||
export const TeamsUpdateSchema = BaseSchema.extend({
|
export const TeamsUpdateSchema = BaseSchema.extend({
|
||||||
@@ -50,6 +50,8 @@ export const TeamsUpdateSchema = BaseSchema.extend({
|
|||||||
accentText: z.string().min(4).max(7).regex(/^#/).optional(),
|
accentText: z.string().min(4).max(7).regex(/^#/).optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
/** Side to display the document's table of contents in relation to the main content. */
|
||||||
|
tocPosition: z.nativeEnum(TOCPosition).optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
TOCPosition,
|
||||||
TeamPreference,
|
TeamPreference,
|
||||||
TeamPreferences,
|
TeamPreferences,
|
||||||
UserPreference,
|
UserPreference,
|
||||||
@@ -22,6 +23,7 @@ export const TeamPreferenceDefaults: TeamPreferences = {
|
|||||||
[TeamPreference.PublicBranding]: false,
|
[TeamPreference.PublicBranding]: false,
|
||||||
[TeamPreference.Commenting]: true,
|
[TeamPreference.Commenting]: true,
|
||||||
[TeamPreference.CustomTheme]: undefined,
|
[TeamPreference.CustomTheme]: undefined,
|
||||||
|
[TeamPreference.TocPosition]: TOCPosition.Left,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserPreferenceDefaults: UserPreferences = {
|
export const UserPreferenceDefaults: UserPreferences = {
|
||||||
|
|||||||
@@ -36,4 +36,7 @@ export class EditorStyleHelper {
|
|||||||
|
|
||||||
/** Minimum padding around editor */
|
/** Minimum padding around editor */
|
||||||
static readonly padding = 32;
|
static readonly padding = 32;
|
||||||
|
|
||||||
|
/** Table of contents width */
|
||||||
|
static readonly tocWidth = 256;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -823,6 +823,10 @@
|
|||||||
"Accent text color": "Accent text color",
|
"Accent text color": "Accent text color",
|
||||||
"Public branding": "Public branding",
|
"Public branding": "Public branding",
|
||||||
"Show your team’s logo on public pages like login and shared documents.": "Show your team’s logo on public pages like login and shared documents.",
|
"Show your team’s logo on public pages like login and shared documents.": "Show your team’s logo on public pages like login and shared documents.",
|
||||||
|
"Table of contents position": "Table of contents position",
|
||||||
|
"The side to display the table of contents in relation to the main content.": "The side to display the table of contents in relation to the main content.",
|
||||||
|
"Left": "Left",
|
||||||
|
"Right": "Right",
|
||||||
"Behavior": "Behavior",
|
"Behavior": "Behavior",
|
||||||
"Subdomain": "Subdomain",
|
"Subdomain": "Subdomain",
|
||||||
"Your workspace will be accessible at": "Your workspace will be accessible at",
|
"Your workspace will be accessible at": "Your workspace will be accessible at",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const depths = {
|
const depths = {
|
||||||
|
toc: 100,
|
||||||
header: 800,
|
header: 800,
|
||||||
sidebar: 900,
|
sidebar: 900,
|
||||||
editorToolbar: 925,
|
editorToolbar: 925,
|
||||||
|
|||||||
@@ -184,6 +184,11 @@ export type PublicTeam = {
|
|||||||
customTheme: Partial<CustomTheme>;
|
customTheme: Partial<CustomTheme>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum TOCPosition {
|
||||||
|
Left = "left",
|
||||||
|
Right = "right",
|
||||||
|
}
|
||||||
|
|
||||||
export enum TeamPreference {
|
export enum TeamPreference {
|
||||||
/** Whether documents have a separate edit mode instead of always editing. */
|
/** Whether documents have a separate edit mode instead of always editing. */
|
||||||
SeamlessEdit = "seamlessEdit",
|
SeamlessEdit = "seamlessEdit",
|
||||||
@@ -199,6 +204,8 @@ export enum TeamPreference {
|
|||||||
Commenting = "commenting",
|
Commenting = "commenting",
|
||||||
/** The custom theme for the team. */
|
/** The custom theme for the team. */
|
||||||
CustomTheme = "customTheme",
|
CustomTheme = "customTheme",
|
||||||
|
/** Side to display the document's table of contents in relation to the main content. */
|
||||||
|
TocPosition = "tocPosition",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TeamPreferences = {
|
export type TeamPreferences = {
|
||||||
@@ -209,6 +216,7 @@ export type TeamPreferences = {
|
|||||||
[TeamPreference.MembersCanCreateApiKey]?: boolean;
|
[TeamPreference.MembersCanCreateApiKey]?: boolean;
|
||||||
[TeamPreference.Commenting]?: boolean;
|
[TeamPreference.Commenting]?: boolean;
|
||||||
[TeamPreference.CustomTheme]?: Partial<CustomTheme>;
|
[TeamPreference.CustomTheme]?: Partial<CustomTheme>;
|
||||||
|
[TeamPreference.TocPosition]?: TOCPosition;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum NavigationNodeType {
|
export enum NavigationNodeType {
|
||||||
|
|||||||
Reference in New Issue
Block a user