style: page editor width and layout updates (#6826)

This commit is contained in:
Aaryan Khandelwal 2025-03-26 21:10:44 +05:30 committed by GitHub
parent 993713925a
commit a25cd426a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 402 additions and 364 deletions

View file

@ -1,5 +1,7 @@
import { Extensions } from "@tiptap/core";
import React from "react";
// plane imports
import { cn } from "@plane/utils";
// components
import { DocumentContentLoader, PageRenderer } from "@/components/editors";
// constants
@ -73,7 +75,11 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
if (!editor) return null;
if (!hasServerSynced && !hasServerConnectionFailed) return <DocumentContentLoader />;
const blockWidthClassName = cn("w-full max-w-[720px] mx-auto transition-all duration-200 ease-in-out", {
"max-w-[1152px]": displayConfig.wideLayout,
});
if (!hasServerSynced && !hasServerConnectionFailed) return <DocumentContentLoader className={blockWidthClassName} />;
return (
<PageRenderer
@ -81,7 +87,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
bubbleMenuEnabled={bubbleMenuEnabled}
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassNames}
editorContainerClassName={cn(editorContainerClassNames, "document-editor")}
id={id}
tabIndex={tabIndex}
/>

View file

@ -1,42 +1,51 @@
// ui
// plane imports
import { Loader } from "@plane/ui";
import { cn } from "@plane/utils";
export const DocumentContentLoader = () => (
<div className="size-full px-5">
<Loader className="relative space-y-4">
<div className="space-y-2">
<div className="py-2">
<Loader.Item width="100%" height="36px" />
</div>
<Loader.Item width="80%" height="22px" />
<div className="relative flex items-center gap-2">
<Loader.Item width="30px" height="30px" />
<Loader.Item width="30%" height="22px" />
</div>
<div className="py-2">
<Loader.Item width="60%" height="36px" />
</div>
<Loader.Item width="70%" height="22px" />
<Loader.Item width="30%" height="22px" />
<div className="relative flex items-center gap-2">
<Loader.Item width="30px" height="30px" />
<Loader.Item width="30%" height="22px" />
</div>
<div className="py-2">
<Loader.Item width="50%" height="30px" />
</div>
<Loader.Item width="100%" height="22px" />
<div className="py-2">
<Loader.Item width="30%" height="30px" />
</div>
<Loader.Item width="30%" height="22px" />
<div className="relative flex items-center gap-2">
type Props = {
className?: string;
};
export const DocumentContentLoader = (props: Props) => {
const { className } = props;
return (
<div className={cn("size-full", className)}>
<Loader className="relative space-y-4">
<div className="space-y-2">
<div className="py-2">
<Loader.Item width="100%" height="36px" />
</div>
<Loader.Item width="80%" height="22px" />
<div className="relative flex items-center gap-2">
<Loader.Item width="30px" height="30px" />
<Loader.Item width="30%" height="22px" />
</div>
<div className="py-2">
<Loader.Item width="60%" height="36px" />
</div>
<Loader.Item width="70%" height="22px" />
<Loader.Item width="30%" height="22px" />
<div className="relative flex items-center gap-2">
<Loader.Item width="30px" height="30px" />
<Loader.Item width="30%" height="22px" />
</div>
<div className="py-2">
<Loader.Item width="50%" height="30px" />
</div>
<Loader.Item width="100%" height="22px" />
<div className="py-2">
<Loader.Item width="30%" height="30px" />
</div>
<Loader.Item width="30%" height="22px" />
<div className="relative flex items-center gap-2">
<div className="py-2">
<Loader.Item width="30px" height="30px" />
</div>
<Loader.Item width="30%" height="22px" />
</div>
</div>
</div>
</Loader>
</div>
);
</Loader>
</div>
);
};

View file

@ -132,7 +132,7 @@ export const PageRenderer = (props: IPageRenderer) => {
return (
<>
<div className="frame-renderer flex-grow w-full -mx-5" onMouseOver={handleLinkHover}>
<div className="frame-renderer flex-grow w-full" onMouseOver={handleLinkHover}>
<EditorContainer
displayConfig={displayConfig}
editor={editor}

View file

@ -1,5 +1,7 @@
import { Extensions } from "@tiptap/core";
import { forwardRef, MutableRefObject } from "react";
// plane imports
import { cn } from "@plane/utils";
// components
import { PageRenderer } from "@/components/editors";
// constants
@ -79,7 +81,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
bubbleMenuEnabled={false}
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassName}
editorContainerClassName={cn(editorContainerClassName, "document-editor")}
id={id}
/>
);

View file

@ -74,6 +74,7 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
`editor-container cursor-text relative line-spacing-${displayConfig.lineSpacing ?? DEFAULT_DISPLAY_CONFIG.lineSpacing}`,
{
"active-editor": editor?.isFocused && editor?.isEditable,
"wide-layout": displayConfig.wideLayout,
},
displayConfig.fontSize ?? DEFAULT_DISPLAY_CONFIG.fontSize,
displayConfig.fontStyle ?? DEFAULT_DISPLAY_CONFIG.fontStyle,

View file

@ -5,6 +5,7 @@ export const DEFAULT_DISPLAY_CONFIG: TDisplayConfig = {
fontSize: "large-font",
fontStyle: "sans-serif",
lineSpacing: "regular",
wideLayout: false,
};
export const ACCEPTED_FILE_MIME_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"];

View file

@ -49,7 +49,7 @@ export const isValidHttpUrl = (string: string): boolean => {
try {
url = new URL(string);
} catch (_) {
} catch {
return false;
}

View file

@ -29,4 +29,5 @@ export type TDisplayConfig = {
fontStyle?: TEditorFontStyle;
fontSize?: TEditorFontSize;
lineSpacing?: TEditorLineSpacing;
wideLayout?: boolean;
};

View file

@ -8,7 +8,7 @@
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
outline: none;
outline: none !important;
cursor: text;
font-family: var(--font-style);
font-size: var(--font-size-regular);

View file

@ -1,3 +1,47 @@
:root {
/* text colors */
--editor-colors-gray-text: #5c5e63;
--editor-colors-peach-text: #ff5b59;
--editor-colors-pink-text: #f65385;
--editor-colors-orange-text: #fd9038;
--editor-colors-green-text: #0fc27b;
--editor-colors-light-blue-text: #17bee9;
--editor-colors-dark-blue-text: #266df0;
--editor-colors-purple-text: #9162f9;
/* end text colors */
/* layout */
--normal-content-width: 720px;
--wide-content-width: 1152px;
--normal-content-margin: 20px;
--wide-content-margin: 96px;
/* end layout */
}
/* text background colors */
[data-theme*="light"] {
--editor-colors-gray-background: #d6d6d8;
--editor-colors-peach-background: #ffd5d7;
--editor-colors-pink-background: #fdd4e3;
--editor-colors-orange-background: #ffe3cd;
--editor-colors-green-background: #c3f0de;
--editor-colors-light-blue-background: #c5eff9;
--editor-colors-dark-blue-background: #c9dafb;
--editor-colors-purple-background: #e3d8fd;
}
[data-theme*="dark"] {
--editor-colors-gray-background: #404144;
--editor-colors-peach-background: #593032;
--editor-colors-pink-background: #562e3d;
--editor-colors-orange-background: #583e2a;
--editor-colors-green-background: #1d4a3b;
--editor-colors-light-blue-background: #1f495c;
--editor-colors-dark-blue-background: #223558;
--editor-colors-purple-background: #3d325a;
}
/* end text background colors */
/* font size and style */
.editor-container {
--color-placeholder: rgba(var(--color-text-100), 0.5);
@ -47,6 +91,8 @@
/* end font sizes and line heights */
/* font styles */
--font-style: "Inter", sans-serif;
&.sans-serif {
--font-style: "Inter", sans-serif;
}
@ -102,3 +148,94 @@
}
/* end spacing */
}
/* end font size and style */
/* layout config */
#page-header-container {
container-name: page-header-container;
container-type: inline-size;
.page-header-content {
--header-width: var(--normal-content-width);
&.wide-layout {
--header-width: var(--wide-content-width);
}
padding-left: calc(((100% - var(--header-width)) / 2) - 10px);
}
}
#page-content-container {
container-name: page-content-container;
container-type: inline-size;
}
.editor-container.document-editor {
--editor-content-width: var(--normal-content-width);
&.wide-layout {
--editor-content-width: var(--wide-content-width);
}
.ProseMirror {
max-width: var(--editor-content-width);
margin: 0 auto;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
}
/* keep a static padding of 96px for wide layouts for container width >912px and <1344px */
@container page-header-container (min-width: 912px) and (max-width: 1344px) {
.page-header-content.wide-layout {
padding-left: var(--wide-content-margin) !important;
}
}
/* keep a static padding of 96px for wide layouts for container width <912px */
@container page-header-container (max-width: 912) {
.page-header-content.wide-layout {
padding-left: var(--wide-content-margin) !important;
}
}
/* end layout config */
/* keep a static padding of 20px for wide layouts for container width <760px */
@container page-header-container (max-width: 760px) {
.page-header-content {
padding-left: var(--normal-content-margin) !important;
}
}
/* end layout config */
/* keep a static padding of 96px for wide layouts for container width >912px and <1344px */
@container page-content-container (min-width: 912px) and (max-width: 1344px) {
.editor-container.wide-layout,
.page-title-container {
padding-left: var(--wide-content-margin);
padding-right: var(--wide-content-margin);
}
}
/* keep a static padding of 20px for wide layouts for container width <912px */
@container page-content-container (max-width: 912px) {
.editor-container.wide-layout,
.page-title-container {
padding-left: var(--normal-content-margin);
padding-right: var(--normal-content-margin);
}
}
/* keep a static padding of 20px for normal layouts for container width <760px */
@container page-content-container (max-width: 760px) {
.editor-container:not(.wide-layout),
.page-title-container {
padding-left: var(--normal-content-margin);
padding-right: var(--normal-content-margin);
}
.page-summary-container {
display: none;
}
}
/* end layout config */

View file

@ -8,27 +8,25 @@ import { cn } from "@/helpers/common.helper";
export const IssueEmbedUpgradeCard: React.FC<any> = (props) => (
<div
className={cn(
"w-full h-20 bg-custom-background-80 rounded-md border-[0.5px] border-custom-border-200 shadow-custom-shadow-2xs",
"w-full bg-custom-background-80 rounded-md border-[0.5px] border-custom-border-200 shadow-custom-shadow-2xs flex items-center justify-between gap-5 px-5 py-2 max-md:flex-wrap",
{
"border-2": props.selected,
}
)}
>
<div className="flex items-center justify-between gap-5 mt-2.5 pl-4 pr-5 py-3 w-full max-md:max-w-full max-md:flex-wrap rounded-md">
<div className="flex items-center gap-4">
<ProIcon className="flex-shrink-0 size-4" />
<p className="text-custom-text !text-base">
Embed and access work items in pages seamlessly, upgrade to Plane Pro now.
</p>
</div>
<a
href="https://plane.so/pro"
target="_blank"
rel="noopener noreferrer"
className={cn(getButtonStyling("primary", "md"), "no-underline")}
>
Upgrade
</a>
<div className="flex items-center gap-4">
<ProIcon className="flex-shrink-0 size-4" />
<p className="text-custom-text !text-base">
Embed and access issues in pages seamlessly, upgrade to Plane Pro now.
</p>
</div>
<a
href="https://plane.so/pro"
target="_blank"
rel="noopener noreferrer"
className={cn(getButtonStyling("primary", "md"), "no-underline")}
>
Upgrade
</a>
</div>
);

View file

@ -1,6 +1,6 @@
import { Dispatch, SetStateAction, useCallback, useMemo } from "react";
import { observer } from "mobx-react";
// document-editor
// plane imports
import {
CollaborativeDocumentEditorWithRef,
EditorRefApi,
@ -10,15 +10,14 @@ import {
TRealtimeConfig,
TServerHandler,
} from "@plane/editor";
// plane types
import { TSearchEntityRequestPayload, TSearchResponse, TWebhookConnectionQueryParams } from "@plane/types";
// plane ui
import { Row } from "@plane/ui";
import { ERowVariant, Row } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { EditorMentionsRoot } from "@/components/editor";
import { PageContentBrowser, PageContentLoader, PageEditorTitle } from "@/components/pages";
// helpers
import { cn, LIVE_BASE_PATH, LIVE_BASE_URL } from "@/helpers/common.helper";
import { LIVE_BASE_PATH, LIVE_BASE_URL } from "@/helpers/common.helper";
import { generateRandomColor } from "@/helpers/string.helper";
// hooks
import { useEditorMention } from "@/hooks/editor";
@ -48,7 +47,6 @@ type Props = {
handleEditorReady: Dispatch<SetStateAction<boolean>>;
handlers: TEditorBodyHandlers;
page: TPageInstance;
sidePeekVisible: boolean;
webhookConnectionParams: TWebhookConnectionQueryParams;
workspaceSlug: string;
};
@ -61,7 +59,6 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
handleEditorReady,
handlers,
page,
sidePeekVisible,
webhookConnectionParams,
workspaceSlug,
} = props;
@ -91,8 +88,9 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
() => ({
fontSize,
fontStyle,
wideLayout: isFullWidth,
}),
[fontSize, fontStyle]
[fontSize, fontStyle, isFullWidth]
);
const getAIMenu = useCallback(
@ -152,69 +150,70 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
[currentUser?.display_name, currentUser?.id]
);
if (pageId === undefined || !realtimeConfig) return <PageContentLoader />;
const blockWidthClassName = cn(
"block bg-transparent w-full max-w-[720px] mx-auto transition-all duration-200 ease-in-out",
{
"max-w-[1152px]": isFullWidth,
}
);
if (pageId === undefined || !realtimeConfig) return <PageContentLoader className={blockWidthClassName} />;
return (
<div className="flex items-center h-full w-full overflow-y-auto">
<Row
className={cn("sticky top-0 hidden h-full flex-shrink-0 -translate-x-full py-5 duration-200 md:block", {
"translate-x-0": sidePeekVisible,
"w-[10rem] lg:w-[14rem]": !isFullWidth,
"w-[5%]": isFullWidth,
})}
>
{!isFullWidth && <PageContentBrowser editorRef={editorRef.current} />}
</Row>
<div
className={cn("size-full pt-5 duration-200", {
"md:w-[calc(100%-10rem)] xl:w-[calc(100%-28rem)]": !isFullWidth,
"md:w-[90%]": isFullWidth,
})}
>
<div className="size-full flex flex-col gap-y-7 overflow-y-auto overflow-x-hidden">
<PageEditorTitle
editorRef={editorRef}
title={pageTitle}
updateTitle={updateTitle}
readOnly={!isContentEditable}
/>
<CollaborativeDocumentEditorWithRef
editable={isContentEditable}
id={pageId}
fileHandler={config.fileHandler}
handleEditorReady={handleEditorReady}
ref={editorRef}
containerClassName="h-full p-0 pb-64"
displayConfig={displayConfig}
editorClassName="pl-10"
mentionHandler={{
searchCallback: async (query) => {
const res = await fetchMentions(query);
if (!res) throw new Error("Failed in fetching mentions");
return res;
},
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }),
}}
embedHandler={{
issue: issueEmbedProps,
}}
realtimeConfig={realtimeConfig}
serverHandler={serverHandler}
user={userConfig}
disabledExtensions={disabledExtensions}
aiHandler={{
menu: getAIMenu,
}}
/>
<Row
className="relative size-full flex flex-col pt-[64px] overflow-y-auto overflow-x-hidden vertical-scrollbar scrollbar-md duration-200"
variant={ERowVariant.HUGGING}
>
<div id="page-content-container" className="relative w-full flex-shrink-0 space-y-4">
{/* table of content */}
<div className="page-summary-container absolute h-full right-0 top-[64px] z-[5]">
<div className="sticky top-[72px]">
<div className="group/page-toc relative px-page-x">
<div className="cursor-pointer max-h-[50vh] overflow-hidden">
<PageContentBrowser editorRef={editorRef?.current} showOutline />
</div>
<div className="absolute top-0 right-0 opacity-0 translate-x-1/2 pointer-events-none group-hover/page-toc:opacity-100 group-hover/page-toc:-translate-x-1/4 group-hover/page-toc:pointer-events-auto transition-all duration-300 w-52 max-h-[70vh] overflow-y-scroll vertical-scrollbar scrollbar-sm whitespace-nowrap bg-custom-background-90 p-4 rounded">
<PageContentBrowser editorRef={editorRef?.current} />
</div>
</div>
</div>
</div>
<PageEditorTitle
editorRef={editorRef}
readOnly={!isContentEditable}
title={pageTitle}
updateTitle={updateTitle}
widthClassName={blockWidthClassName}
/>
<CollaborativeDocumentEditorWithRef
editable={isContentEditable}
id={pageId}
fileHandler={config.fileHandler}
handleEditorReady={handleEditorReady}
ref={editorRef}
containerClassName="h-full p-0 pb-64"
displayConfig={displayConfig}
mentionHandler={{
searchCallback: async (query) => {
const res = await fetchMentions(query);
if (!res) throw new Error("Failed in fetching mentions");
return res;
},
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }),
}}
embedHandler={{
issue: issueEmbedProps,
}}
realtimeConfig={realtimeConfig}
serverHandler={serverHandler}
user={userConfig}
disabledExtensions={disabledExtensions}
aiHandler={{
menu: getAIMenu,
}}
/>
</div>
<div
className={cn("hidden xl:block flex-shrink-0 duration-200", {
"w-[10rem] lg:w-[14rem]": !isFullWidth,
"w-[5%]": isFullWidth,
})}
/>
</div>
</Row>
);
});

View file

@ -69,7 +69,7 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
};
return (
<div className="flex items-center justify-end gap-3">
<div className="flex items-center gap-3">
{is_locked && <LockedComponent />}
{archived_at && (
<div className="flex-shrink-0 flex h-7 items-center gap-2 rounded-full bg-blue-500/20 px-3 py-0.5 text-xs font-medium text-blue-500">

View file

@ -2,9 +2,7 @@ import { observer } from "mobx-react";
import { EditorRefApi } from "@plane/editor";
// components
import { Header, EHeaderVariant } from "@plane/ui";
import { PageExtraOptions, PageSummaryPopover, PageToolbar } from "@/components/pages";
// hooks
import { usePageFilters } from "@/hooks/use-page-filters";
import { PageExtraOptions, PageToolbar } from "@/components/pages";
// plane web hooks
import { EPageStoreType } from "@/plane-web/hooks/store";
// store
@ -13,29 +11,17 @@ import { TPageInstance } from "@/store/pages/base-page";
type Props = {
editorRef: EditorRefApi;
page: TPageInstance;
setSidePeekVisible: (sidePeekState: boolean) => void;
sidePeekVisible: boolean;
storeType: EPageStoreType;
};
export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
const { editorRef, page, setSidePeekVisible, sidePeekVisible, storeType } = props;
const { editorRef, page, storeType } = props;
// derived values
const { isContentEditable } = page;
// page filters
const { isFullWidth } = usePageFilters();
return (
<>
<Header variant={EHeaderVariant.SECONDARY}>
<div className="flex-shrink-0 my-auto">
<PageSummaryPopover
editorRef={editorRef}
isFullWidth={isFullWidth}
sidePeekVisible={sidePeekVisible}
setSidePeekVisible={setSidePeekVisible}
/>
</div>
<PageExtraOptions editorRef={editorRef} page={page} storeType={storeType} />
</Header>
<Header variant={EHeaderVariant.TERNARY}>

View file

@ -1,8 +1,7 @@
import { observer } from "mobx-react";
import { EditorRefApi } from "@plane/editor";
// components
import { Header, EHeaderVariant } from "@plane/ui";
import { PageEditorMobileHeaderRoot, PageExtraOptions, PageSummaryPopover, PageToolbar } from "@/components/pages";
import { PageEditorMobileHeaderRoot, PageExtraOptions, PageToolbar } from "@/components/pages";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
@ -16,13 +15,11 @@ type Props = {
editorReady: boolean;
editorRef: React.RefObject<EditorRefApi>;
page: TPageInstance;
setSidePeekVisible: (sidePeekState: boolean) => void;
sidePeekVisible: boolean;
storeType: EPageStoreType;
};
export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
const { editorReady, editorRef, page, setSidePeekVisible, sidePeekVisible, storeType } = props;
const { editorReady, editorRef, page, storeType } = props;
// derived values
const { isContentEditable } = page;
// page filters
@ -33,39 +30,27 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
if (!resolvedEditorRef) return null;
return (
<>
<Header variant={EHeaderVariant.SECONDARY} showOnMobile={false}>
<Header.LeftItem className="gap-0 w-full">
{editorReady && (
<div
className={cn("flex-shrink-0 my-auto", {
"w-40 lg:w-56": !isFullWidth,
"w-[5%]": isFullWidth,
})}
>
<PageSummaryPopover
editorRef={editorRef.current}
isFullWidth={isFullWidth}
sidePeekVisible={sidePeekVisible}
setSidePeekVisible={setSidePeekVisible}
/>
</div>
)}
{isStickyToolbarEnabled && editorReady && isContentEditable && editorRef.current && (
<PageToolbar editorRef={editorRef?.current} />
)}
</Header.LeftItem>
<PageExtraOptions editorRef={resolvedEditorRef} page={page} storeType={storeType} />
</Header>
<div className="md:hidden">
<PageEditorMobileHeaderRoot
editorRef={resolvedEditorRef}
page={page}
sidePeekVisible={sidePeekVisible}
setSidePeekVisible={setSidePeekVisible}
storeType={storeType}
/>
<div id="page-header-container">
<div
className={cn(
"hidden md:flex items-center relative min-h-[52px] page-header-content border-b border-custom-border-200 px-page-x transition-all duration-200 ease-in-out",
{
"wide-layout": isFullWidth,
}
)}
>
<div className="max-w-full w-full flex items-center justify-between">
<div>
{isStickyToolbarEnabled && editorReady && isContentEditable && editorRef.current && (
<PageToolbar editorRef={editorRef?.current} />
)}
</div>
<PageExtraOptions editorRef={resolvedEditorRef} page={page} storeType={storeType} />
</div>
</div>
</>
<div className="md:hidden">
<PageEditorMobileHeaderRoot editorRef={resolvedEditorRef} page={page} storeType={storeType} />
</div>
</div>
);
});

View file

@ -48,7 +48,6 @@ export const PageRoot = observer((props: TPageRootProps) => {
// states
const [editorReady, setEditorReady] = useState(false);
const [hasConnectionFailed, setHasConnectionFailed] = useState(false);
const [sidePeekVisible, setSidePeekVisible] = useState(window.innerWidth >= 768);
const [isVersionsOverlayOpen, setIsVersionsOverlayOpen] = useState(false);
// refs
const editorRef = useRef<EditorRefApi>(null);
@ -104,14 +103,7 @@ export const PageRoot = observer((props: TPageRootProps) => {
pageId={page.id ?? ""}
restoreEnabled={isContentEditable}
/>
<PageEditorHeaderRoot
editorReady={editorReady}
editorRef={editorRef}
page={page}
setSidePeekVisible={(state) => setSidePeekVisible(state)}
sidePeekVisible={sidePeekVisible}
storeType={storeType}
/>
<PageEditorHeaderRoot editorReady={editorReady} editorRef={editorRef} page={page} storeType={storeType} />
<PageEditorBody
config={config}
editorReady={editorReady}
@ -120,7 +112,6 @@ export const PageRoot = observer((props: TPageRootProps) => {
handleEditorReady={setEditorReady}
handlers={handlers}
page={page}
sidePeekVisible={sidePeekVisible}
webhookConnectionParams={webhookConnectionParams}
workspaceSlug={workspaceSlug}
/>

View file

@ -7,10 +7,11 @@ import { OutlineHeading1, OutlineHeading2, OutlineHeading3 } from "./heading-com
type Props = {
editorRef: EditorRefApi | null;
setSidePeekVisible?: (sidePeekState: boolean) => void;
showOutline?: boolean;
};
export const PageContentBrowser: React.FC<Props> = (props) => {
const { editorRef, setSidePeekVisible } = props;
const { editorRef, setSidePeekVisible, showOutline = false } = props;
// states
const [headings, setHeadings] = useState<IMarking[]>([]);
@ -37,24 +38,28 @@ export const PageContentBrowser: React.FC<Props> = (props) => {
};
return (
<div className="h-full flex flex-col overflow-hidden">
<div className="h-full flex flex-col items-start gap-y-2 overflow-y-auto mt-2">
{headings && headings.length !== 0 ? (
headings.map((marking) => {
const Component = HeadingComponent[marking.level];
if (!Component) return null;
return (
<Component
key={`${marking.level}-${marking.sequence}`}
marking={marking}
onClick={() => handleOnClick(marking)}
/>
);
})
) : (
<p className="mt-3 text-xs text-custom-text-400">Headings will be displayed here for navigation</p>
)}
</div>
<div className="h-full flex flex-col items-start gap-y-2 overflow-y-auto mt-2">
{headings.map((marking) => {
const Component = HeadingComponent[marking.level];
if (!Component) return null;
if (showOutline === true)
return (
<div
key={`${marking.level}-${marking.sequence}`}
className="h-0.5 bg-custom-border-400 self-end rounded-sm"
style={{
width: marking.level === 1 ? "20px" : marking.level === 2 ? "18px" : "14px",
}}
/>
);
return (
<Component
key={`${marking.level}-${marking.sequence}`}
marking={marking}
onClick={() => handleOnClick(marking)}
/>
);
})}
</div>
);
};

View file

@ -1,36 +1,36 @@
// document editor
import { IMarking } from "@plane/editor";
// plane editor
import type { IMarking } from "@plane/editor";
type HeadingProps = {
export type THeadingComponentProps = {
marking: IMarking;
onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
};
export const OutlineHeading1 = ({ marking, onClick }: HeadingProps) => (
export const OutlineHeading1 = ({ marking, onClick }: THeadingComponentProps) => (
<button
type="button"
onClick={onClick}
className="text-sm text-left font-medium text-custom-text-300 hover:text-custom-primary-100"
className="text-sm text-left font-medium text-custom-text-300 hover:text-custom-primary-100 transition-colors"
>
{marking.text}
</button>
);
export const OutlineHeading2 = ({ marking, onClick }: HeadingProps) => (
export const OutlineHeading2 = ({ marking, onClick }: THeadingComponentProps) => (
<button
type="button"
onClick={onClick}
className="ml-2 text-xs text-left font-medium text-custom-text-300 hover:text-custom-primary-100"
className="ml-2 text-xs text-left font-medium text-custom-text-300 hover:text-custom-primary-100 transition-colors"
>
{marking.text}
</button>
);
export const OutlineHeading3 = ({ marking, onClick }: HeadingProps) => (
export const OutlineHeading3 = ({ marking, onClick }: THeadingComponentProps) => (
<button
type="button"
onClick={onClick}
className="ml-4 text-xs text-left font-medium text-custom-text-300 hover:text-custom-primary-100"
className="ml-4 text-xs text-left font-medium text-custom-text-300 hover:text-custom-primary-100 transition-colors"
>
{marking.text}
</button>

View file

@ -1,2 +1 @@
export * from "./content-browser";
export * from "./popover";

View file

@ -1,74 +0,0 @@
import { useState } from "react";
import { usePopper } from "react-popper";
import { List } from "lucide-react";
// document editor
import { EditorRefApi } from "@plane/editor";
// helpers
import { cn } from "@/helpers/common.helper";
// components
import { PageContentBrowser } from "./content-browser";
type Props = {
editorRef: EditorRefApi | null;
isFullWidth: boolean;
sidePeekVisible: boolean;
setSidePeekVisible: (sidePeekState: boolean) => void;
};
export const PageSummaryPopover: React.FC<Props> = (props) => {
const { editorRef, sidePeekVisible, setSidePeekVisible } = props;
// refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// popper-js
const { styles: summaryPopoverStyles, attributes: summaryPopoverAttributes } = usePopper(
referenceElement,
popperElement,
{
placement: "bottom-start",
}
);
return (
<div className="group/summary-popover w-min whitespace-nowrap">
<button
type="button"
ref={setReferenceElement}
className={`grid h-7 w-7 place-items-center rounded ${
sidePeekVisible ? "bg-custom-primary-100/20 text-custom-primary-100" : "text-custom-text-300"
}`}
onClick={() => setSidePeekVisible(!sidePeekVisible)}
>
<List className="h-4 w-4" />
</button>
<div
className={cn("block md:hidden", {
// "md:hidden": !isFullWidth,
})}
>
{sidePeekVisible && (
<div
className="z-10 max-h-80 w-64 overflow-y-auto rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg"
ref={setPopperElement}
style={summaryPopoverStyles.popper}
{...summaryPopoverAttributes.popper}
>
<PageContentBrowser setSidePeekVisible={setSidePeekVisible} editorRef={editorRef} />
</div>
)}
</div>
<div className="hidden md:block">
{!sidePeekVisible && (
<div
className="z-10 hidden max-h-80 w-64 overflow-y-auto rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg group-hover/summary-popover:block"
ref={setPopperElement}
style={summaryPopoverStyles.popper}
{...summaryPopoverAttributes.popper}
>
<PageContentBrowser editorRef={editorRef} />
</div>
)}
</div>
</div>
);
};

View file

@ -17,26 +17,28 @@ type Props = {
readOnly: boolean;
title: string | undefined;
updateTitle: (title: string) => void;
widthClassName: string;
};
export const PageEditorTitle: React.FC<Props> = observer((props) => {
const { editorRef, readOnly, title, updateTitle } = props;
const { editorRef, readOnly, title, updateTitle, widthClassName } = props;
// states
const [isLengthVisible, setIsLengthVisible] = useState(false);
// page filters
const { fontSize } = usePageFilters();
// ui
const titleClassName = cn("bg-transparent tracking-[-2%] font-bold", {
const titleFontClassName = cn("tracking-[-2%] font-bold", {
"text-[1.6rem] leading-[1.9rem]": fontSize === "small-font",
"text-[2rem] leading-[2.375rem]": fontSize === "large-font",
});
return (
<div className="relative w-full flex-shrink-0 md:pl-5 px-4">
<div className="relative w-full flex-shrink-0 py-3 page-title-container">
{readOnly ? (
<h6
className={cn(
titleClassName,
titleFontClassName,
widthClassName,
{
"text-custom-text-400": !title,
},
@ -46,9 +48,9 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
{getPageName(title)}
</h6>
) : (
<>
<div className={cn("relative", widthClassName)}>
<TextArea
className={cn(titleClassName, "w-full outline-none p-0 border-none resize-none rounded-none")}
className={cn(titleFontClassName, "block w-full border-none outline-none p-0 resize-none rounded-none")}
placeholder="Untitled"
onKeyDown={(e) => {
if (e.key === "Enter") {
@ -65,7 +67,7 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
/>
<div
className={cn(
"pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 p-0.5 text-xs text-custom-text-200 opacity-0 transition-opacity",
"pointer-events-none absolute bottom-1 right-1 z-[2] font-normal rounded bg-custom-background-100 p-0.5 text-xs text-custom-text-200 opacity-0 transition-opacity",
{
"opacity-100": isLengthVisible,
}
@ -80,7 +82,7 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
</span>
/255
</div>
</>
</div>
)}
</div>
);

View file

@ -1,19 +1,20 @@
"use client";
// ui
// plane imports
import { Loader } from "@plane/ui";
import { cn } from "@plane/utils";
export const PageContentLoader = () => (
<div className="relative w-full h-full flex flex-col">
{/* header */}
<div className="px-16 flex-shrink-0 relative flex items-center justify-between h-12 border-b border-custom-border-100">
{/* left options */}
<Loader className="flex-shrink-0 w-[280px]">
<Loader.Item width="26px" height="26px" />
</Loader>
type Props = {
className?: string;
};
{/* editor options */}
<div className="w-full relative flex items-center divide-x divide-custom-border-100">
export const PageContentLoader = (props: Props) => {
const { className } = props;
return (
<div className={cn("relative size-full flex flex-col", className)}>
{/* header */}
<div className="flex-shrink-0 w-full h-12 border-b border-custom-border-100 relative flex items-center divide-x divide-custom-border-100">
<Loader className="relative flex items-center gap-1 pr-2">
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
@ -37,56 +38,48 @@ export const PageContentLoader = () => (
</Loader>
</div>
{/* right options */}
<Loader className="w-full relative flex justify-end items-center gap-1">
<Loader.Item width="60px" height="26px" />
<Loader.Item width="40px" height="26px" />
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
</Loader>
</div>
{/* content */}
<div className="px-16 w-full h-full overflow-hidden relative flex">
{/* editor loader */}
<div className="w-full h-full py-5">
<Loader className="relative space-y-4">
<Loader.Item width="50%" height="36px" />
<div className="space-y-2">
<div className="py-2">
<Loader.Item width="100%" height="36px" />
</div>
<Loader.Item width="80%" height="22px" />
<div className="relative flex items-center gap-2">
<Loader.Item width="30px" height="30px" />
<Loader.Item width="30%" height="22px" />
</div>
<div className="py-2">
<Loader.Item width="60%" height="36px" />
</div>
<Loader.Item width="70%" height="22px" />
<Loader.Item width="30%" height="22px" />
<div className="relative flex items-center gap-2">
<Loader.Item width="30px" height="30px" />
<Loader.Item width="30%" height="22px" />
</div>
<div className="py-2">
<Loader.Item width="50%" height="30px" />
</div>
<Loader.Item width="100%" height="22px" />
<div className="py-2">
<Loader.Item width="30%" height="30px" />
</div>
<Loader.Item width="30%" height="22px" />
<div className="relative flex items-center gap-2">
{/* content */}
<div className="size-full pt-[64px] overflow-hidden relative flex">
{/* editor loader */}
<div className="size-full py-5">
<Loader className="relative space-y-4">
<Loader.Item width="50%" height="36px" />
<div className="space-y-2">
<div className="py-2">
<Loader.Item width="100%" height="36px" />
</div>
<Loader.Item width="80%" height="22px" />
<div className="relative flex items-center gap-2">
<Loader.Item width="30px" height="30px" />
<Loader.Item width="30%" height="22px" />
</div>
<div className="py-2">
<Loader.Item width="60%" height="36px" />
</div>
<Loader.Item width="70%" height="22px" />
<Loader.Item width="30%" height="22px" />
<div className="relative flex items-center gap-2">
<Loader.Item width="30px" height="30px" />
<Loader.Item width="30%" height="22px" />
</div>
<div className="py-2">
<Loader.Item width="50%" height="30px" />
</div>
<Loader.Item width="100%" height="22px" />
<div className="py-2">
<Loader.Item width="30%" height="30px" />
</div>
<Loader.Item width="30%" height="22px" />
<div className="relative flex items-center gap-2">
<div className="py-2">
<Loader.Item width="30px" height="30px" />
</div>
<Loader.Item width="30%" height="22px" />
</div>
</div>
</div>
</Loader>
</Loader>
</div>
</div>
</div>
</div>
);
);
};

View file

@ -1,19 +1,15 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane editor
// plane imports
import { DocumentReadOnlyEditorWithRef, TDisplayConfig } from "@plane/editor";
// plane types
import { TPageVersion } from "@plane/types";
// plane ui
import { Loader } from "@plane/ui";
// components
import { EditorMentionsRoot } from "@/components/editor";
// hooks
import { useEditorConfig } from "@/hooks/editor";
import { useWorkspace } from "@/hooks/store";
import { useMember, useWorkspace } from "@/hooks/store";
import { usePageFilters } from "@/hooks/use-page-filters";
// store hooks
import { useMember } from "@/hooks/store";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed";
@ -50,6 +46,7 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props
const displayConfig: TDisplayConfig = {
fontSize,
fontStyle,
wideLayout: true,
};
if (!isCurrentVersionActive && !versionDetails)