feat: allow user to set TOC display preference (#6943)

Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
Hemachandar
2024-06-16 21:51:08 +05:30
committed by GitHub
parent 3d0160463c
commit 05c1bee412
12 changed files with 222 additions and 150 deletions

View File

@@ -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;

View File

@@ -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)));

View File

@@ -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 = {

View File

@@ -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}

View File

@@ -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

View File

@@ -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>

View File

@@ -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(),
}), }),

View File

@@ -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 = {

View File

@@ -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;
} }

View File

@@ -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 teams logo on public pages like login and shared documents.": "Show your teams logo on public pages like login and shared documents.", "Show your teams logo on public pages like login and shared documents.": "Show your teams 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",

View File

@@ -1,4 +1,5 @@
const depths = { const depths = {
toc: 100,
header: 800, header: 800,
sidebar: 900, sidebar: 900,
editorToolbar: 925, editorToolbar: 925,

View File

@@ -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 {