[WEB-2045] dev: editor variable font sizes and styles support (#5340)

* chore: added variable font size and font style support

* chore: remove font style switcher

* chore: update typography
This commit is contained in:
Aaryan Khandelwal 2024-08-09 19:22:47 +05:30 committed by GitHub
parent 6d0cf1b4e9
commit 85f8fe9247
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 419 additions and 145 deletions

View file

@ -1,19 +1,28 @@
import React from "react";
// components
import { PageRenderer } from "@/components/editors";
// constants
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
// helpers
import { getEditorClassNames } from "@/helpers/common";
// hooks
import { useDocumentEditor } from "@/hooks/use-document-editor";
import { TFileHandler } from "@/hooks/use-editor";
// plane editor types
import { TEmbedConfig } from "@/plane-editor/types";
// types
import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TExtensions } from "@/types";
import {
EditorRefApi,
IMentionHighlight,
IMentionSuggestion,
TDisplayConfig,
TExtensions,
TFileHandler,
} from "@/types";
interface IDocumentEditor {
containerClassName?: string;
disabledExtensions?: TExtensions[];
displayConfig?: TDisplayConfig;
editorClassName?: string;
embedHandler: TEmbedConfig;
fileHandler: TFileHandler;
@ -34,6 +43,7 @@ const DocumentEditor = (props: IDocumentEditor) => {
const {
containerClassName,
disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
embedHandler,
fileHandler,
@ -72,7 +82,13 @@ const DocumentEditor = (props: IDocumentEditor) => {
if (!editor || !isIndexedDbSynced) return null;
return (
<PageRenderer editor={editor} editorContainerClassName={editorContainerClassNames} id={id} tabIndex={tabIndex} />
<PageRenderer
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassNames}
id={id}
tabIndex={tabIndex}
/>
);
};

View file

@ -16,8 +16,11 @@ import { Editor, ReactRenderer } from "@tiptap/react";
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
import { LinkView, LinkViewProps } from "@/components/links";
import { BlockMenu } from "@/components/menus";
// types
import { TDisplayConfig } from "@/types";
type IPageRenderer = {
displayConfig: TDisplayConfig;
editor: Editor;
editorContainerClassName: string;
id: string;
@ -25,7 +28,7 @@ type IPageRenderer = {
};
export const PageRenderer = (props: IPageRenderer) => {
const { editor, editorContainerClassName, id, tabIndex } = props;
const { displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
// states
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
const [isOpen, setIsOpen] = useState(false);
@ -128,7 +131,12 @@ export const PageRenderer = (props: IPageRenderer) => {
return (
<>
<div className="frame-renderer flex-grow w-full -mx-5" onMouseOver={handleLinkHover}>
<EditorContainer editor={editor} editorContainerClassName={editorContainerClassName} id={id}>
<EditorContainer
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassName}
id={id}
>
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
{editor.isEditable && <BlockMenu editor={editor} />}
</EditorContainer>

View file

@ -1,6 +1,8 @@
import { forwardRef, MutableRefObject } from "react";
// components
import { PageRenderer } from "@/components/editors";
// constants
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
// extensions
import { IssueWidget } from "@/extensions";
// helpers
@ -10,12 +12,13 @@ import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
// plane web types
import { TEmbedConfig } from "@/plane-editor/types";
// types
import { EditorReadOnlyRefApi, IMentionHighlight } from "@/types";
import { EditorReadOnlyRefApi, IMentionHighlight, TDisplayConfig } from "@/types";
interface IDocumentReadOnlyEditor {
id: string;
initialValue: string;
containerClassName: string;
displayConfig?: TDisplayConfig;
editorClassName?: string;
embedHandler: TEmbedConfig;
tabIndex?: number;
@ -29,6 +32,7 @@ interface IDocumentReadOnlyEditor {
const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
const {
containerClassName,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
embedHandler,
id,
@ -39,17 +43,17 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
mentionHandler,
} = props;
const editor = useReadOnlyEditor({
initialValue,
editorClassName,
mentionHandler,
forwardedRef,
handleEditorReady,
extensions: [
embedHandler?.issue &&
IssueWidget({
widgetCallback: embedHandler?.issue.widgetCallback,
}),
],
forwardedRef,
handleEditorReady,
initialValue,
mentionHandler,
});
if (!editor) {
@ -61,7 +65,13 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
});
return (
<PageRenderer editor={editor} editorContainerClassName={editorContainerClassName} id={id} tabIndex={tabIndex} />
<PageRenderer
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassName}
id={id}
tabIndex={tabIndex}
/>
);
};

View file

@ -1,17 +1,22 @@
import { FC, ReactNode } from "react";
import { Editor } from "@tiptap/react";
// constants
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
// helpers
import { cn } from "@/helpers/common";
// types
import { TDisplayConfig } from "@/types";
interface EditorContainerProps {
children: ReactNode;
displayConfig: TDisplayConfig;
editor: Editor | null;
editorContainerClassName: string;
id: string;
}
export const EditorContainer: FC<EditorContainerProps> = (props) => {
const { children, editor, editorContainerClassName, id } = props;
const { children, displayConfig, editor, editorContainerClassName, id } = props;
const handleContainerClick = () => {
if (!editor) return;
@ -65,10 +70,12 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
onClick={handleContainerClick}
onMouseLeave={handleContainerMouseLeave}
className={cn(
"cursor-text relative",
"editor-container cursor-text relative",
{
"active-editor": editor?.isFocused && editor?.isEditable,
},
displayConfig.fontSize ?? DEFAULT_DISPLAY_CONFIG.fontSize,
displayConfig.fontStyle ?? DEFAULT_DISPLAY_CONFIG.fontStyle,
editorContainerClassName
)}
>

View file

@ -1,6 +1,8 @@
import { Editor, Extension } from "@tiptap/core";
// components
import { EditorContainer } from "@/components/editors";
// constants
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
// hooks
import { getEditorClassNames } from "@/helpers/common";
import { useEditor } from "@/hooks/use-editor";
@ -17,6 +19,7 @@ export const EditorWrapper: React.FC<Props> = (props) => {
const {
children,
containerClassName,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
extensions,
id,
@ -54,7 +57,12 @@ export const EditorWrapper: React.FC<Props> = (props) => {
if (!editor) return null;
return (
<EditorContainer editor={editor} editorContainerClassName={editorContainerClassName} id={id}>
<EditorContainer
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassName}
id={id}
>
{children?.(editor)}
<div className="flex flex-col">
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />

View file

@ -1,5 +1,7 @@
// components
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
// constants
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
// helpers
import { getEditorClassNames } from "@/helpers/common";
// hooks
@ -8,12 +10,20 @@ import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
import { IReadOnlyEditorProps } from "@/types";
export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
const { containerClassName, editorClassName = "", id, initialValue, forwardedRef, mentionHandler } = props;
const {
containerClassName,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
id,
initialValue,
forwardedRef,
mentionHandler,
} = props;
const editor = useReadOnlyEditor({
initialValue,
editorClassName,
forwardedRef,
initialValue,
mentionHandler,
});
@ -24,7 +34,12 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
if (!editor) return null;
return (
<EditorContainer editor={editor} editorContainerClassName={editorContainerClassName} id={id}>
<EditorContainer
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassName}
id={id}
>
<div className="flex flex-col">
<EditorContentWrapper editor={editor} id={id} />
</div>

View file

@ -0,0 +1,7 @@
// types
import { TDisplayConfig } from "@/types";
export const DEFAULT_DISPLAY_CONFIG: TDisplayConfig = {
fontSize: "large-font",
fontStyle: "sans-serif",
};

View file

@ -56,7 +56,7 @@ export const CodeBlockComponent: React.FC<CodeBlockComponentProps> = ({ node })
</Tooltip>
<pre className="bg-custom-background-90 text-custom-text-100 rounded-lg p-4 my-2">
<NodeViewContent as="code" className="whitespace-[pre-wrap]" />
<NodeViewContent as="code" className="whitespace-pre-wrap" />
</pre>
</NodeViewWrapper>
);

View file

@ -5,7 +5,7 @@ import * as Y from "yjs";
// extensions
import { IssueWidget, SideMenuExtension } from "@/extensions";
// hooks
import { TFileHandler, useEditor } from "@/hooks/use-editor";
import { useEditor } from "@/hooks/use-editor";
// plane editor extensions
import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions";
// plane editor provider
@ -13,7 +13,7 @@ import { CollaborationProvider } from "@/plane-editor/providers";
// plane editor types
import { TEmbedConfig } from "@/plane-editor/types";
// types
import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TExtensions } from "@/types";
import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TExtensions, TFileHandler } from "@/types";
type DocumentEditorProps = {
disabledExtensions?: TExtensions[];

View file

@ -1,7 +1,7 @@
import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react";
import { Selection } from "@tiptap/pm/state";
import { EditorProps } from "@tiptap/pm/view";
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
import { useEditor as useTiptapEditor, Editor } from "@tiptap/react";
// components
import { getEditorMenuItems } from "@/components/menus";
// extensions
@ -14,22 +14,7 @@ import { CollaborationProvider } from "@/plane-editor/providers";
// props
import { CoreEditorProps } from "@/props";
// types
import {
DeleteImage,
EditorRefApi,
IMentionHighlight,
IMentionSuggestion,
RestoreImage,
TEditorCommands,
UploadImage,
} from "@/types";
export type TFileHandler = {
cancel: () => void;
delete: DeleteImage;
upload: UploadImage;
restore: RestoreImage;
};
import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TEditorCommands, TFileHandler } from "@/types";
export interface CustomEditorProps {
editorClassName: string;
@ -54,26 +39,30 @@ export interface CustomEditorProps {
value?: string | null | undefined;
}
export const useEditor = ({
editorClassName,
editorProps = {},
enableHistory,
extensions = [],
fileHandler,
forwardedRef,
handleEditorReady,
id = "",
initialValue,
mentionHandler,
onChange,
placeholder,
provider,
tabIndex,
value,
}: CustomEditorProps) => {
const editor = useCustomEditor({
export const useEditor = (props: CustomEditorProps) => {
const {
editorClassName,
editorProps = {},
enableHistory,
extensions = [],
fileHandler,
forwardedRef,
handleEditorReady,
id = "",
initialValue,
mentionHandler,
onChange,
placeholder,
provider,
tabIndex,
value,
} = props;
const editor = useTiptapEditor({
editorProps: {
...CoreEditorProps(editorClassName),
...CoreEditorProps({
editorClassName,
}),
...editorProps,
},
extensions: [
@ -95,18 +84,10 @@ export const useEditor = ({
...extensions,
],
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
onCreate: async () => {
handleEditorReady?.(true);
},
onTransaction: async ({ editor }) => {
setSavedSelection(editor.state.selection);
},
onUpdate: async ({ editor }) => {
onChange?.(editor.getJSON(), editor.getHTML());
},
onDestroy: async () => {
handleEditorReady?.(false);
},
onCreate: () => handleEditorReady?.(true),
onTransaction: ({ editor }) => setSavedSelection(editor.state.selection),
onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()),
onDestroy: () => handleEditorReady?.(false),
});
const editorRef: MutableRefObject<Editor | null> = useRef(null);

View file

@ -35,7 +35,9 @@ export const useReadOnlyEditor = ({
editable: false,
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
editorProps: {
...CoreReadOnlyEditorProps(editorClassName),
...CoreReadOnlyEditorProps({
editorClassName,
}),
...editorProps,
},
onCreate: async () => {

View file

@ -2,7 +2,13 @@ import { EditorProps } from "@tiptap/pm/view";
// helpers
import { cn } from "@/helpers/common";
export function CoreEditorProps(editorClassName: string): EditorProps {
export type TCoreEditorProps = {
editorClassName: string;
};
export const CoreEditorProps = (props: TCoreEditorProps): EditorProps => {
const { editorClassName } = props;
return {
attributes: {
class: cn(
@ -25,4 +31,4 @@ export function CoreEditorProps(editorClassName: string): EditorProps {
return html.replace(/<img.*?>/g, "");
},
};
}
};

View file

@ -1,12 +1,18 @@
import { EditorProps } from "@tiptap/pm/view";
// helpers
import { cn } from "@/helpers/common";
// props
import { TCoreEditorProps } from "@/props";
export const CoreReadOnlyEditorProps = (editorClassName: string): EditorProps => ({
attributes: {
class: cn(
"prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none",
editorClassName
),
},
});
export const CoreReadOnlyEditorProps = (props: TCoreEditorProps): EditorProps => {
const { editorClassName } = props;
return {
attributes: {
class: cn(
"prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none",
editorClassName
),
},
};
};

View file

@ -0,0 +1,17 @@
import { DeleteImage, RestoreImage, UploadImage } from "@/types";
export type TFileHandler = {
cancel: () => void;
delete: DeleteImage;
upload: UploadImage;
restore: RestoreImage;
};
export type TEditorFontStyle = "sans-serif" | "serif" | "monospace";
export type TEditorFontSize = "small-font" | "large-font";
export type TDisplayConfig = {
fontStyle?: TEditorFontStyle;
fontSize?: TEditorFontSize;
};

View file

@ -1,9 +1,7 @@
// helpers
import { IMarking } from "@/helpers/scroll-to-node";
// hooks
import { TFileHandler } from "@/hooks/use-editor";
// types
import { IMentionHighlight, IMentionSuggestion, TEditorCommands } from "@/types";
import { IMentionHighlight, IMentionSuggestion, TDisplayConfig, TEditorCommands, TFileHandler } from "@/types";
export type EditorReadOnlyRefApi = {
getMarkDown: () => string;
@ -26,6 +24,7 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
export interface IEditorProps {
containerClassName?: string;
displayConfig?: TDisplayConfig;
editorClassName?: string;
fileHandler: TFileHandler;
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
@ -50,6 +49,7 @@ export interface IRichTextEditor extends IEditorProps {
export interface IReadOnlyEditorProps {
containerClassName?: string;
displayConfig?: TDisplayConfig;
editorClassName?: string;
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
id: string;

View file

@ -1,3 +1,4 @@
export * from "./config";
export * from "./editor";
export * from "./embed";
export * from "./extensions";

View file

@ -34,5 +34,5 @@ export { type IMarking, useEditorMarkings } from "@/hooks/use-editor-markings";
export { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
// types
export type { CustomEditorProps, TFileHandler } from "@/hooks/use-editor";
export type { CustomEditorProps } from "@/hooks/use-editor";
export * from "@/types";

View file

@ -1,12 +1,82 @@
.editor-container {
&.large-font {
--font-size-h1: 1.75rem;
--font-size-h2: 1.5rem;
--font-size-h3: 1.375rem;
--font-size-h4: 1.25rem;
--font-size-h5: 1.125rem;
--font-size-h6: 1rem;
--font-size-regular: 1rem;
--font-size-list: var(--font-size-regular);
--font-size-code: var(--font-size-regular);
--line-height-h1: 2.25rem;
--line-height-h2: 2rem;
--line-height-h3: 1.75rem;
--line-height-h4: 1.5rem;
--line-height-h5: 1.5rem;
--line-height-h6: 1.5rem;
--line-height-regular: 1.5rem;
--line-height-list: var(--line-height-regular);
--line-height-code: var(--line-height-regular);
}
&.small-font {
--font-size-h1: 1.4rem;
--font-size-h2: 1.2rem;
--font-size-h3: 1.1rem;
--font-size-h4: 1rem;
--font-size-h5: 0.9rem;
--font-size-h6: 0.8rem;
--font-size-regular: 0.8rem;
--font-size-list: var(--font-size-regular);
--font-size-code: var(--font-size-regular);
--line-height-h1: 1.8rem;
--line-height-h2: 1.6rem;
--line-height-h3: 1.4rem;
--line-height-h4: 1.2rem;
--line-height-h5: 1.2rem;
--line-height-h6: 1.2rem;
--line-height-regular: 1.2rem;
--line-height-list: var(--line-height-regular);
--line-height-code: var(--line-height-regular);
}
&.sans-serif {
--font-style: sans-serif;
}
&.serif {
--font-style: serif;
}
&.monospace {
--font-style: monospace;
}
}
.ProseMirror {
--font-size-h1: 1.5rem;
--font-size-h2: 1.3125rem;
--font-size-h3: 1.125rem;
--font-size-h4: 0.9375rem;
--font-size-h5: 0.8125rem;
--font-size-h6: 0.75rem;
--font-size-regular: 0.9375rem;
--font-size-list: var(--font-size-regular);
position: relative;
word-wrap: break-word;
white-space: pre-wrap;
-moz-tab-size: 4;
tab-size: 4;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
outline: none;
cursor: text;
font-family: var(--font-style);
font-size: var(--font-size-regular);
line-height: 1.2;
color: inherit;
-moz-box-sizing: border-box;
box-sizing: border-box;
appearance: textfield;
-webkit-appearance: textfield;
-moz-appearance: textfield;
}
.ProseMirror p.is-editor-empty:first-child::before {
@ -179,29 +249,6 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
max-width: 400px !important;
}
.ProseMirror {
position: relative;
word-wrap: break-word;
white-space: pre-wrap;
-moz-tab-size: 4;
tab-size: 4;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
outline: none;
cursor: text;
line-height: 1.2;
font-family: inherit;
font-size: var(--font-size-regular);
color: inherit;
-moz-box-sizing: border-box;
box-sizing: border-box;
appearance: textfield;
-webkit-appearance: textfield;
-moz-appearance: textfield;
}
.fade-in {
opacity: 1;
transition: opacity 0.3s ease-in;
@ -248,6 +295,7 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
opacity: 0;
}
/* code block, inline code */
.ProseMirror pre {
font-family: JetBrainsMono, monospace;
tab-size: 2;
@ -256,10 +304,14 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
.ProseMirror pre code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
.ProseMirror code {
font-size: var(--font-size-code);
}
/* end code block, inline code */
div[data-type="horizontalRule"] {
line-height: 0;
padding: 0.25rem 0;
@ -342,48 +394,48 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
margin-top: 2rem;
margin-bottom: 4px;
font-size: var(--font-size-h1);
line-height: var(--line-height-h1);
font-weight: 600;
line-height: 1.3;
}
.prose :where(h2):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
margin-top: 1.4rem;
margin-bottom: 1px;
font-size: var(--font-size-h2);
line-height: var(--line-height-h2);
font-weight: 600;
line-height: 1.3;
}
.prose :where(h3):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
margin-top: 1rem;
margin-bottom: 1px;
font-size: var(--font-size-h3);
line-height: var(--line-height-h3);
font-weight: 600;
line-height: 1.3;
}
.prose :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
margin-top: 1rem;
margin-bottom: 1px;
font-size: var(--font-size-h4);
line-height: var(--line-height-h4);
font-weight: 600;
line-height: 1.5;
}
.prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
margin-top: 1rem;
margin-bottom: 1px;
font-size: var(--font-size-h5);
line-height: var(--line-height-h5);
font-weight: 600;
line-height: 1.5;
}
.prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
margin-top: 1rem;
margin-bottom: 1px;
font-size: var(--font-size-h6);
line-height: var(--line-height-h6);
font-weight: 600;
line-height: 1.5;
}
.prose :where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
@ -391,13 +443,13 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
margin-bottom: 1px;
padding: 3px 0;
font-size: var(--font-size-regular);
line-height: 1.5;
line-height: var(--line-height-regular);
}
.prose :where(ol):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p,
.prose :where(ul):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p {
font-size: var(--font-size-list);
line-height: 1.5;
line-height: var(--line-height-list);
}
.prose :where(.prose > :first-child):not(:where([class~="not-prose"], [class~="not-prose"] *)) {

View file

@ -12,10 +12,6 @@
width: 100%;
}
.table-wrapper table p {
font-size: 14px;
}
.table-wrapper table td,
.table-wrapper table th {
min-width: 1em;
@ -115,4 +111,3 @@
opacity: 0;
pointer-events: none;
}

View file

@ -15,9 +15,12 @@ export * from "./github-icon";
export * from "./gitlab-icon";
export * from "./layer-stack";
export * from "./layers-icon";
export * from "./monospace-icon";
export * from "./photo-filter-icon";
export * from "./priority-icon";
export * from "./related-icon";
export * from "./sans-serif-icon";
export * from "./serif-icon";
export * from "./side-panel-icon";
export * from "./transfer-icon";
export * from "./info-icon";

View file

@ -0,0 +1,16 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const MonospaceIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg" className={className} {...rest}>
<path
d="M10.6149 13.0746V11.9267H13.0648C13.4568 11.9267 13.7415 11.838 13.9188 11.6607C14.1055 11.4833 14.1988 11.208 14.1988 10.8347V9.8547L14.2268 8.45473H13.9748L14.2128 8.24474C14.2128 8.80472 14.0261 9.24805 13.6528 9.57471C13.2795 9.90137 12.7802 10.0647 12.1548 10.0647C11.3615 10.0647 10.7362 9.80804 10.2789 9.29472C9.82156 8.77206 9.5929 8.07207 9.5929 7.19476V5.57079C9.5929 4.69347 9.82156 3.99815 10.2789 3.48483C10.7362 2.97151 11.3615 2.71484 12.1548 2.71484C12.7802 2.71484 13.2795 2.87817 13.6528 3.20483C14.0261 3.53149 14.2128 3.97482 14.2128 4.53481L13.9748 4.32481H14.2128V2.85484H15.4588V10.8347C15.4588 11.5253 15.2441 12.0713 14.8148 12.4727C14.3948 12.874 13.8068 13.0746 13.0508 13.0746H10.6149ZM12.5328 8.97272C13.0555 8.97272 13.4662 8.80939 13.7648 8.48273C14.0635 8.15607 14.2128 7.70341 14.2128 7.12476V5.65479C14.2128 5.07613 14.0635 4.62347 13.7648 4.29681C13.4662 3.97015 13.0555 3.80682 12.5328 3.80682C12.0008 3.80682 11.5855 3.96549 11.2869 4.28281C10.9975 4.60014 10.8529 5.05746 10.8529 5.65479V7.12476C10.8529 7.72208 10.9975 8.1794 11.2869 8.49673C11.5855 8.81406 12.0008 8.97272 12.5328 8.97272Z"
fill="currentColor"
/>
<path
d="M0.666626 10.5538L3.32657 0.333984H5.02054L7.66649 10.5538H6.39251L5.72053 7.83784H2.62659L1.9546 10.5538H0.666626ZM2.87858 6.77386H5.45453L4.67055 3.62392C4.52122 3.0266 4.40455 2.52727 4.32055 2.12595C4.23656 1.72462 4.18522 1.46329 4.16656 1.34196C4.14789 1.46329 4.09656 1.72462 4.01256 2.12595C3.92856 2.52727 3.8119 3.02193 3.66257 3.60992L2.87858 6.77386Z"
fill="currentColor"
/>
</svg>
);

View file

@ -0,0 +1,16 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const SansSerifIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className} {...rest}>
<path
d="M12.4877 11.5341C11.9579 11.5341 11.502 11.4646 11.1198 11.3256C10.7406 11.1867 10.4308 11.0028 10.1905 10.7741C9.95021 10.5454 9.77071 10.295 9.65202 10.0228L10.7681 9.56253C10.8462 9.68991 10.9504 9.82453 11.0807 9.96639C11.2139 10.1111 11.3934 10.2342 11.6192 10.3355C11.8479 10.4368 12.1418 10.4875 12.5007 10.4875C12.9929 10.4875 13.3997 10.3674 13.721 10.1271C14.0424 9.88967 14.203 9.51042 14.203 8.98931V7.67785H14.1205C14.0424 7.81971 13.9295 7.97749 13.7818 8.15119C13.6371 8.3249 13.4373 8.47544 13.1825 8.60282C12.9278 8.7302 12.5963 8.79389 12.1881 8.79389C11.6612 8.79389 11.1864 8.67085 10.7637 8.42478C10.3439 8.1758 10.011 7.80958 9.76492 7.3261C9.52174 6.83973 9.40015 6.2419 9.40015 5.53262C9.40015 4.82333 9.52029 4.21537 9.76058 3.70873C10.0038 3.2021 10.3367 2.81416 10.7594 2.54492C11.1821 2.27279 11.6612 2.13672 12.1968 2.13672C12.6108 2.13672 12.9451 2.2062 13.1999 2.34516C13.4547 2.48123 13.653 2.64046 13.7948 2.82285C13.9396 3.00523 14.0511 3.16591 14.1292 3.30487H14.2248V2.22357H15.4971V9.04142C15.4971 9.61464 15.364 10.0851 15.0976 10.4528C14.8313 10.8204 14.4708 11.0926 14.0163 11.2692C13.5647 11.4458 13.0552 11.5341 12.4877 11.5341ZM12.4747 7.71693C12.8482 7.71693 13.1637 7.63008 13.4214 7.45638C13.6819 7.27978 13.8788 7.02791 14.012 6.70077C14.148 6.37073 14.2161 5.97556 14.2161 5.51525C14.2161 5.06651 14.1495 4.67134 14.0163 4.32972C13.8831 3.98811 13.6877 3.72176 13.4301 3.53069C13.1724 3.33672 12.8539 3.23973 12.4747 3.23973C12.0839 3.23973 11.7582 3.34106 11.4976 3.54371C11.2371 3.74347 11.0402 4.01561 10.907 4.36012C10.7767 4.70463 10.7116 5.08967 10.7116 5.51525C10.7116 5.9524 10.7782 6.33599 10.9114 6.66603C11.0445 6.99607 11.2414 7.25373 11.502 7.43901C11.7654 7.62429 12.0897 7.71693 12.4747 7.71693Z"
fill="currentColor"
/>
<path
d="M2.09099 8.8936H0.666626L3.86711 0H5.41741L8.61789 8.8936H7.19353L4.67917 1.61544H4.60969L2.09099 8.8936ZM2.32983 5.41085H6.95034V6.53993H2.32983V5.41085Z"
fill="currentColor"
/>
</svg>
);

View file

@ -0,0 +1,16 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const SerifIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className} {...rest}>
<path
d="M11.6021 7.09532C11.2602 6.92843 10.9976 6.69641 10.8145 6.39927C10.6313 6.09806 10.5397 5.76631 10.5397 5.40404C10.5397 4.85046 10.7473 4.37422 11.1625 3.97531C11.5818 3.57641 12.117 3.37695 12.7683 3.37695C13.3015 3.37695 13.7635 3.50721 14.1543 3.76772H15.3388C15.5138 3.76772 15.6156 3.77382 15.6441 3.78603C15.6726 3.79418 15.6929 3.81046 15.7051 3.83488C15.7296 3.87151 15.7418 3.93664 15.7418 4.03026C15.7418 4.13609 15.7316 4.20936 15.7112 4.25007C15.699 4.27042 15.6766 4.2867 15.6441 4.29891C15.6156 4.31112 15.5138 4.31723 15.3388 4.31723H14.6122C14.8402 4.6103 14.9541 4.98479 14.9541 5.44068C14.9541 5.9617 14.7547 6.40741 14.3558 6.77782C13.9569 7.14824 13.4216 7.33344 12.75 7.33344C12.4732 7.33344 12.1903 7.29274 11.9013 7.21133C11.7222 7.36601 11.6001 7.50237 11.5349 7.62041C11.4739 7.73438 11.4434 7.83207 11.4434 7.91348C11.4434 7.98268 11.4759 8.04984 11.541 8.11497C11.6102 8.1801 11.7425 8.22691 11.9379 8.2554C12.0519 8.27168 12.3368 8.28593 12.7927 8.29814C13.6312 8.31849 14.1746 8.34699 14.4229 8.38362C14.8015 8.43654 15.1027 8.57697 15.3266 8.80491C15.5545 9.03286 15.6685 9.31372 15.6685 9.6475C15.6685 10.1075 15.4528 10.5389 15.0213 10.9419C14.3863 11.5362 13.558 11.8333 12.5363 11.8333C11.7507 11.8333 11.0872 11.6563 10.5458 11.3021C10.2405 11.0986 10.0879 10.887 10.0879 10.6672C10.0879 10.5695 10.1103 10.4718 10.1551 10.3741C10.2243 10.2235 10.3667 10.0138 10.5825 9.74519C10.6109 9.70856 10.8185 9.48875 11.2052 9.08578C10.9936 8.95959 10.843 8.84765 10.7534 8.74996C10.6679 8.6482 10.6252 8.53423 10.6252 8.40804C10.6252 8.26558 10.6822 8.09869 10.7962 7.90738C10.9142 7.71607 11.1828 7.44538 11.6021 7.09532ZM12.6645 3.67003C12.3633 3.67003 12.1109 3.79011 11.9074 4.03026C11.7039 4.27042 11.6021 4.6388 11.6021 5.13539C11.6021 5.77853 11.7405 6.27716 12.0173 6.63129C12.229 6.89994 12.4976 7.03426 12.8232 7.03426C13.1326 7.03426 13.387 6.91826 13.5865 6.68624C13.7859 6.45422 13.8856 6.08992 13.8856 5.59332C13.8856 4.94612 13.7452 4.43934 13.4643 4.073C13.2567 3.80435 12.9901 3.67003 12.6645 3.67003ZM11.541 9.13462C11.3497 9.34222 11.2052 9.53556 11.1075 9.71466C11.0099 9.89376 10.961 10.0586 10.961 10.2092C10.961 10.4046 11.079 10.5756 11.3151 10.7221C11.7222 10.9745 12.3104 11.1007 13.0797 11.1007C13.8124 11.1007 14.3517 10.9704 14.6977 10.7099C15.0477 10.4535 15.2228 10.1787 15.2228 9.88562C15.2228 9.67396 15.119 9.52335 14.9114 9.4338C14.6997 9.34425 14.2805 9.29133 13.6536 9.27505C12.7378 9.25063 12.0336 9.20382 11.541 9.13462Z"
fill="currentColor"
/>
<path
d="M6.28997 6.36263H3.08448L2.52276 7.66925C2.38436 7.99081 2.31516 8.23097 2.31516 8.38972C2.31516 8.5159 2.37418 8.62784 2.49223 8.72553C2.61434 8.81915 2.87485 8.88021 3.27376 8.9087V9.13461H0.666626V8.9087C1.01262 8.84764 1.23649 8.76827 1.33825 8.67058C1.54585 8.4752 1.77583 8.07833 2.0282 7.47997L4.94061 0.666016H5.15431L8.0362 7.55324C8.26821 8.10682 8.47784 8.46706 8.66508 8.63394C8.8564 8.79676 9.12098 8.88835 9.45882 8.9087V9.13461H6.19228V8.9087C6.52199 8.89242 6.74383 8.83747 6.8578 8.74385C6.97584 8.65023 7.03486 8.53625 7.03486 8.40193C7.03486 8.22283 6.95345 7.93993 6.79064 7.55324L6.28997 6.36263ZM6.11901 5.91081L4.7147 2.56489L3.27376 5.91081H6.11901Z"
fill="currentColor"
/>
</svg>
);

View file

@ -8,6 +8,7 @@ import {
EditorReadOnlyRefApi,
EditorRefApi,
IMarking,
TDisplayConfig,
} from "@plane/editor";
// types
import { IUserLite } from "@plane/types";
@ -82,12 +83,16 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
});
// editor flaggings
const { documentEditor } = useEditorFlagging();
// page filters
const { isFullWidth } = usePageFilters();
const { fontSize, fontStyle, isFullWidth } = usePageFilters();
// issue-embed
const { issueEmbedProps } = useIssueEmbed(workspaceSlug?.toString() ?? "", projectId?.toString() ?? "");
const displayConfig: TDisplayConfig = {
fontSize,
fontStyle,
};
useEffect(() => {
updateMarkings(pageDescription ?? "<p></p>");
}, [pageDescription, updateMarkings]);
@ -139,6 +144,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
value={pageDescriptionYJS}
ref={editorRef}
containerClassName="p-0 pb-64"
displayConfig={displayConfig}
editorClassName="pl-10"
onChange={handleDescriptionChange}
mentionHandler={{
@ -157,6 +163,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
initialValue={pageDescription ?? "<p></p>"}
handleEditorReady={handleReadOnlyEditorReady}
containerClassName="p-0 pb-64 border-none"
displayConfig={displayConfig}
editorClassName="pl-10"
mentionHandler={{
highlights: mentionHighlights,

View file

@ -40,6 +40,7 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId } = useParams();
// page filters
const { isFullWidth, handleFullWidth } = usePageFilters();
const handleArchivePage = async () =>
await archive().catch(() =>
setToast({

View file

@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { CSSProperties, useState } from "react";
import { observer } from "mobx-react";
// editor
import { EditorRefApi } from "@plane/editor";
@ -8,6 +8,8 @@ import { EditorRefApi } from "@plane/editor";
import { TextArea } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { usePageFilters } from "@/hooks/use-page-filters";
type Props = {
editorRef: React.RefObject<EditorRefApi>;
@ -20,25 +22,28 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
const { editorRef, readOnly, title, updateTitle } = props;
// states
const [isLengthVisible, setIsLengthVisible] = useState(false);
// page filters
const { fontSize, fontStyle } = usePageFilters();
// ui
const titleClassName = cn("bg-transparent tracking-[-2%] font-semibold", {
"text-[1.6rem] leading-[1.8rem]": fontSize === "small-font",
"text-[2rem] leading-[2.25rem]": fontSize === "large-font",
});
const titleStyle: CSSProperties = {
fontFamily: fontStyle,
};
return (
<>
{readOnly ? (
<h6
className="break-words bg-transparent text-[1.75rem] font-semibold"
style={{
lineHeight: "1.2",
}}
>
<h6 className={cn(titleClassName, "break-words")} style={titleStyle}>
{title}
</h6>
) : (
<>
<TextArea
className="w-full bg-custom-background text-[1.75rem] font-semibold outline-none p-0 border-none resize-none rounded-none"
style={{
lineHeight: "1.2",
}}
className={cn(titleClassName, "w-full outline-none p-0 border-none resize-none rounded-none")}
style={titleStyle}
placeholder="Untitled"
onKeyDown={(e) => {
if (e.key === "Enter") {

View file

@ -20,7 +20,9 @@ import {
Underline,
} from "lucide-react";
// editor
import { TEditorCommands } from "@plane/editor";
import { TEditorCommands, TEditorFontStyle } from "@plane/editor";
// ui
import { MonospaceIcon, SansSerifIcon, SerifIcon } from "@plane/ui";
type TEditorTypes = "lite" | "document";
@ -107,3 +109,25 @@ export const TOOLBAR_ITEMS: {
complex: COMPLEX_ITEMS.filter((item) => item.editors.includes("document")),
},
};
export const EDITOR_FONT_STYLES: {
key: TEditorFontStyle;
label: string;
icon: any;
}[] = [
{
key: "sans-serif",
label: "Sans serif",
icon: SansSerifIcon,
},
{
key: "serif",
label: "Serif",
icon: SerifIcon,
},
{
key: "monospace",
label: "Mono",
icon: MonospaceIcon,
},
];

View file

@ -1,12 +1,67 @@
// plane editor
import { TEditorFontSize, TEditorFontStyle } from "@plane/editor";
// hooks
import useLocalStorage from "@/hooks/use-local-storage";
export type TPagesPersonalizationConfig = {
full_width: boolean;
font_size: TEditorFontSize;
font_style: TEditorFontStyle;
};
const DEFAULT_PERSONALIZATION_VALUES: TPagesPersonalizationConfig = {
full_width: false,
font_size: "large-font",
font_style: "sans-serif",
};
export const usePageFilters = () => {
const { storedValue: isFullWidth, setValue: setFullWidth } = useLocalStorage<boolean>("page_full_width", true);
const handleFullWidth = (value: boolean) => setFullWidth(value);
// local storage
const { storedValue: pagesConfig, setValue: setPagesConfig } = useLocalStorage<TPagesPersonalizationConfig>(
"pages_config",
DEFAULT_PERSONALIZATION_VALUES
);
// stored values
const isFullWidth = !!pagesConfig?.full_width;
const fontSize = pagesConfig?.font_size ?? DEFAULT_PERSONALIZATION_VALUES.font_size;
const fontStyle = pagesConfig?.font_style ?? DEFAULT_PERSONALIZATION_VALUES.font_style;
// update action
const handleUpdateConfig = (payload: Partial<TPagesPersonalizationConfig>) =>
setPagesConfig({
...(pagesConfig ?? DEFAULT_PERSONALIZATION_VALUES),
...payload,
});
/**
* @description action to update full_width value
* @param {boolean} value
*/
const handleFullWidth = (value: boolean) =>
handleUpdateConfig({
full_width: value,
});
/**
* @description action to update font_size value
* @param {TEditorFontSize} value
*/
const handleFontSize = (value: TEditorFontSize) =>
handleUpdateConfig({
font_size: value,
});
/**
* @description action to update font_size value
* @param {TEditorFontSize} value
*/
const handleFontStyle = (value: TEditorFontStyle) =>
handleUpdateConfig({
font_style: value,
});
return {
isFullWidth: !!isFullWidth,
fontSize,
handleFontSize,
fontStyle,
handleFontStyle,
isFullWidth,
handleFullWidth,
};
};