fix: merge conflicts from preview

This commit is contained in:
sriram veeraghanta 2024-08-16 17:55:08 +05:30
commit 3729011cb0
283 changed files with 4895 additions and 5157 deletions

View file

@ -1,10 +1,14 @@
import { Extensions } from "@tiptap/core";
import { SlashCommand } from "@/extensions";
// hooks
import { TFileHandler } from "@/hooks/use-editor";
// plane editor types
import { TIssueEmbedConfig } from "@/plane-editor/types";
// types
import { TExtensions } from "@/types";
type Props = {
disabledExtensions?: TExtensions[];
fileHandler: TFileHandler;
issueEmbedConfig: TIssueEmbedConfig | undefined;
};
@ -12,7 +16,7 @@ type Props = {
export const DocumentEditorAdditionalExtensions = (props: Props) => {
const { fileHandler } = props;
const extensions = [SlashCommand(fileHandler.upload)];
const extensions: Extensions = [SlashCommand(fileHandler.upload)];
return extensions;
};

View file

@ -1,18 +1,30 @@
import React, { useState } from "react";
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 } from "@/types";
import {
EditorRefApi,
IMentionHighlight,
IMentionSuggestion,
TAIHandler,
TDisplayConfig,
TExtensions,
TFileHandler,
} from "@/types";
interface IDocumentEditor {
aiHandler?: TAIHandler;
containerClassName?: string;
disabledExtensions?: TExtensions[];
displayConfig?: TDisplayConfig;
editorClassName?: string;
embedHandler: TEmbedConfig;
fileHandler: TFileHandler;
@ -31,7 +43,10 @@ interface IDocumentEditor {
const DocumentEditor = (props: IDocumentEditor) => {
const {
aiHandler,
containerClassName,
disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
embedHandler,
fileHandler,
@ -44,16 +59,10 @@ const DocumentEditor = (props: IDocumentEditor) => {
tabIndex,
value,
} = props;
// states
const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = useState<() => void>(() => {});
// this essentially sets the hideDragHandle function from the DragAndDrop extension as the Plugin
// loads such that we can invoke it from react when the cursor leaves the container
const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => {
setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop);
};
// use document editor
const { editor, isIndexedDbSynced } = useDocumentEditor({
disabledExtensions,
id,
editorClassName,
embedHandler,
@ -64,7 +73,6 @@ const DocumentEditor = (props: IDocumentEditor) => {
forwardedRef,
mentionHandler,
placeholder,
setHideDragHandleFunction,
tabIndex,
});
@ -78,9 +86,10 @@ const DocumentEditor = (props: IDocumentEditor) => {
return (
<PageRenderer
displayConfig={displayConfig}
aiHandler={aiHandler}
editor={editor}
editorContainerClassName={editorContainerClassNames}
hideDragHandle={hideDragHandleOnMouseLeave}
id={id}
tabIndex={tabIndex}
/>

View file

@ -15,18 +15,21 @@ import { Editor, ReactRenderer } from "@tiptap/react";
// components
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
import { LinkView, LinkViewProps } from "@/components/links";
import { BlockMenu } from "@/components/menus";
import { AIFeaturesMenu, BlockMenu } from "@/components/menus";
// types
import { TAIHandler, TDisplayConfig } from "@/types";
type IPageRenderer = {
aiHandler?: TAIHandler;
displayConfig: TDisplayConfig;
editor: Editor;
editorContainerClassName: string;
hideDragHandle?: () => void;
id: string;
tabIndex?: number;
};
export const PageRenderer = (props: IPageRenderer) => {
const { editor, editorContainerClassName, hideDragHandle, id, tabIndex } = props;
const { aiHandler, displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
// states
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
const [isOpen, setIsOpen] = useState(false);
@ -130,13 +133,18 @@ export const PageRenderer = (props: IPageRenderer) => {
<>
<div className="frame-renderer flex-grow w-full -mx-5" onMouseOver={handleLinkHover}>
<EditorContainer
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassName}
hideDragHandle={hideDragHandle}
id={id}
>
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
{editor && editor.isEditable && <BlockMenu editor={editor} />}
{editor.isEditable && (
<>
<BlockMenu editor={editor} />
<AIFeaturesMenu menu={aiHandler?.menu} />
</>
)}
</EditorContainer>
</div>
{isOpen && linkViewProps && coordinates && (

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,18 +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;
hideDragHandle?: () => void;
id: string;
}
export const EditorContainer: FC<EditorContainerProps> = (props) => {
const { children, editor, editorContainerClassName, hideDragHandle, id } = props;
const { children, displayConfig, editor, editorContainerClassName, id } = props;
const handleContainerClick = () => {
if (!editor) return;
@ -53,16 +57,25 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
}
};
const handleContainerMouseLeave = () => {
const dragHandleElement = document.querySelector("#editor-side-menu");
if (!dragHandleElement?.classList.contains("side-menu-hidden")) {
dragHandleElement?.classList.add("side-menu-hidden");
}
};
return (
<div
id={`editor-container-${id}`}
onClick={handleContainerClick}
onMouseLeave={hideDragHandle}
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";
@ -11,16 +13,15 @@ import { EditorContentWrapper } from "./editor-content";
type Props = IEditorProps & {
children?: (editor: Editor) => React.ReactNode;
extensions: Extension<any, any>[];
hideDragHandleOnMouseLeave: () => void;
};
export const EditorWrapper: React.FC<Props> = (props) => {
const {
children,
containerClassName,
displayConfig = DEFAULT_DISPLAY_CONFIG,
editorClassName = "",
extensions,
hideDragHandleOnMouseLeave,
id,
initialValue,
fileHandler,
@ -57,10 +58,10 @@ export const EditorWrapper: React.FC<Props> = (props) => {
return (
<EditorContainer
displayConfig={displayConfig}
editor={editor}
editorContainerClassName={editorContainerClassName}
id={id}
hideDragHandle={hideDragHandleOnMouseLeave}
>
{children?.(editor)}
<div className="flex flex-col">

View file

@ -11,7 +11,7 @@ const LiteTextEditor = (props: ILiteTextEditor) => {
const extensions = [EnterKeyExtension(onEnterKeyPress)];
return <EditorWrapper {...props} extensions={extensions} hideDragHandleOnMouseLeave={() => {}} />;
return <EditorWrapper {...props} extensions={extensions} />;
};
const LiteTextEditorWithRef = forwardRef<EditorRefApi, ILiteTextEditor>((props, ref) => (

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

@ -1,37 +1,30 @@
import { forwardRef, useCallback, useState } from "react";
import { forwardRef, useCallback } from "react";
// components
import { EditorWrapper } from "@/components/editors";
import { EditorBubbleMenu } from "@/components/menus";
// extensions
import { DragAndDrop, SlashCommand } from "@/extensions";
import { SideMenuExtension, SlashCommand } from "@/extensions";
// types
import { EditorRefApi, IRichTextEditor } from "@/types";
const RichTextEditor = (props: IRichTextEditor) => {
const { dragDropEnabled, fileHandler } = props;
// states
const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = useState<() => void>(() => {});
// this essentially sets the hideDragHandle function from the DragAndDrop extension as the Plugin
// loads such that we can invoke it from react when the cursor leaves the container
const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => {
setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop);
};
const getExtensions = useCallback(() => {
const extensions = [
SlashCommand(fileHandler.upload),
// TODO; add the extension conditionally for forms that don't require it
// EnterKeyExtension(onEnterKeyPress),
];
const extensions = [SlashCommand(fileHandler.upload)];
if (dragDropEnabled) extensions.push(DragAndDrop(setHideDragHandleFunction));
extensions.push(
SideMenuExtension({
aiEnabled: false,
dragDropEnabled: !!dragDropEnabled,
})
);
return extensions;
}, [dragDropEnabled, fileHandler.upload]);
return (
<EditorWrapper {...props} extensions={getExtensions()} hideDragHandleOnMouseLeave={hideDragHandleOnMouseLeave}>
<EditorWrapper {...props} extensions={getExtensions()}>
{(editor) => <>{editor && <EditorBubbleMenu editor={editor} />}</>}
</EditorWrapper>
);

View file

@ -0,0 +1,95 @@
import { useCallback, useEffect, useRef, useState } from "react";
import tippy, { Instance } from "tippy.js";
// helpers
import { cn } from "@/helpers/common";
// types
import { TAIHandler } from "@/types";
type Props = {
menu: TAIHandler["menu"];
};
export const AIFeaturesMenu: React.FC<Props> = (props) => {
const { menu } = props;
// states
const [isPopupVisible, setIsPopupVisible] = useState(false);
// refs
const menuRef = useRef<HTMLDivElement>(null);
const popup = useRef<Instance | null>(null);
useEffect(() => {
if (!menuRef.current) return;
menuRef.current.remove();
menuRef.current.style.visibility = "visible";
// @ts-expect-error - tippy types are incorrect
popup.current = tippy(document.body, {
getReferenceClientRect: null,
content: menuRef.current,
appendTo: () => document.querySelector(".frame-renderer"),
trigger: "manual",
interactive: true,
arrow: false,
placement: "bottom-start",
animation: "shift-away",
hideOnClick: true,
onShown: () => menuRef.current?.focus(),
});
return () => {
popup.current?.destroy();
popup.current = null;
};
}, []);
const hidePopup = useCallback(() => {
popup.current?.hide();
setIsPopupVisible(false);
}, []);
useEffect(() => {
const handleClickAIHandle = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (target.matches("#ai-handle") || menuRef.current?.contains(e.target as Node)) {
e.preventDefault();
if (!isPopupVisible) {
popup.current?.setProps({
getReferenceClientRect: () => target.getBoundingClientRect(),
});
popup.current?.show();
setIsPopupVisible(true);
}
return;
}
hidePopup();
return;
};
document.addEventListener("click", handleClickAIHandle);
document.addEventListener("contextmenu", handleClickAIHandle);
document.addEventListener("keydown", hidePopup);
return () => {
document.removeEventListener("click", handleClickAIHandle);
document.removeEventListener("contextmenu", handleClickAIHandle);
document.removeEventListener("keydown", hidePopup);
};
}, [hidePopup, isPopupVisible]);
return (
<div
className={cn("opacity-0 pointer-events-none fixed inset-0 size-full z-10 transition-opacity", {
"opacity-100 pointer-events-auto": isPopupVisible,
})}
>
<div ref={menuRef} className="z-10">
{menu?.({
onClose: hidePopup,
})}
</div>
</div>
);
};

View file

@ -14,7 +14,7 @@ export const BlockMenu = (props: BlockMenuProps) => {
const handleClickDragHandle = useCallback((event: MouseEvent) => {
const target = event.target as HTMLElement;
if (target.matches(".drag-handle-dots") || target.matches(".drag-handle-dot")) {
if (target.matches("#drag-handle")) {
event.preventDefault();
popup.current?.setProps({

View file

@ -1,3 +1,4 @@
export * from "./bubble-menu";
export * from "./ai-menu";
export * from "./block-menu";
export * from "./menu-items";

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

@ -1,6 +1,6 @@
import { Extension } from "@tiptap/core";
export const EnterKeyExtension = (onEnterKeyPress?: (descriptionHTML: string) => void) =>
export const EnterKeyExtension = (onEnterKeyPress?: () => void) =>
Extension.create({
name: "enterKey",
@ -8,7 +8,9 @@ export const EnterKeyExtension = (onEnterKeyPress?: (descriptionHTML: string) =>
return {
Enter: () => {
if (!this.editor.storage.mentionsOpen) {
onEnterKeyPress?.(this.editor.getHTML());
if (onEnterKeyPress) {
onEnterKeyPress();
}
return true;
}
return false;

View file

@ -10,7 +10,6 @@ export * from "./typography";
export * from "./core-without-props";
export * from "./document-without-props";
export * from "./custom-code-inline";
export * from "./drag-drop";
export * from "./drop";
export * from "./enter-key-extension";
export * from "./extensions";
@ -18,4 +17,5 @@ export * from "./horizontal-rule";
export * from "./keymap";
export * from "./quote";
export * from "./read-only-extensions";
export * from "./side-menu";
export * from "./slash-commands";

View file

@ -0,0 +1,205 @@
import { Extension } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { EditorView } from "@tiptap/pm/view";
// plugins
import { AIHandlePlugin } from "@/plugins/ai-handle";
import { DragHandlePlugin } from "@/plugins/drag-handle";
type Props = {
aiEnabled: boolean;
dragDropEnabled: boolean;
};
export type SideMenuPluginProps = {
dragHandleWidth: number;
handlesConfig: {
ai: boolean;
dragDrop: boolean;
};
scrollThreshold: {
up: number;
down: number;
};
};
export type SideMenuHandleOptions = {
view: (view: EditorView, sideMenu: HTMLDivElement | null) => void;
domEvents?: {
[key: string]: (...args: any) => void;
};
};
export const SideMenuExtension = (props: Props) => {
const { aiEnabled, dragDropEnabled } = props;
return Extension.create({
name: "editorSideMenu",
addProseMirrorPlugins() {
return [
SideMenu({
dragHandleWidth: 24,
handlesConfig: {
ai: aiEnabled,
dragDrop: dragDropEnabled,
},
scrollThreshold: { up: 300, down: 100 },
}),
];
},
});
};
const absoluteRect = (node: Element) => {
const data = node.getBoundingClientRect();
return {
top: data.top,
left: data.left,
width: data.width,
};
};
const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
const elements = document.elementsFromPoint(coords.x, coords.y);
const generalSelectors = [
"li",
"p:not(:first-child)",
".code-block",
"blockquote",
"img",
"h1, h2, h3, h4, h5, h6",
"[data-type=horizontalRule]",
".table-wrapper",
".issue-embed",
].join(", ");
for (const elem of elements) {
if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) {
return elem;
}
// if the element is a <p> tag that is the first child of a td or th
if (
(elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) &&
elem?.textContent?.trim() !== ""
) {
return elem; // Return only if p tag is not empty in td or th
}
// apply general selector
if (elem.matches(generalSelectors)) {
return elem;
}
}
return null;
};
const SideMenu = (options: SideMenuPluginProps) => {
const { handlesConfig } = options;
const editorSideMenu: HTMLDivElement | null = document.createElement("div");
editorSideMenu.id = "editor-side-menu";
// side menu view actions
const hideSideMenu = () => {
if (!editorSideMenu?.classList.contains("side-menu-hidden")) editorSideMenu?.classList.add("side-menu-hidden");
};
const showSideMenu = () => editorSideMenu?.classList.remove("side-menu-hidden");
// side menu elements
const { view: dragHandleView, domEvents: dragHandleDOMEvents } = DragHandlePlugin(options);
const { view: aiHandleView, domEvents: aiHandleDOMEvents } = AIHandlePlugin(options);
return new Plugin({
key: new PluginKey("sideMenu"),
view: (view) => {
hideSideMenu();
view?.dom.parentElement?.appendChild(editorSideMenu);
// side menu elements' initialization
if (handlesConfig.ai) {
aiHandleView(view, editorSideMenu);
}
if (handlesConfig.dragDrop) {
dragHandleView(view, editorSideMenu);
}
return {
destroy: () => hideSideMenu(),
};
},
props: {
handleDOMEvents: {
mousemove: (view, event) => {
if (!view.editable) return;
const node = nodeDOMAtCoords({
x: event.clientX + 50 + options.dragHandleWidth,
y: event.clientY,
});
if (!(node instanceof Element) || node.matches("ul, ol")) {
hideSideMenu();
return;
}
const compStyle = window.getComputedStyle(node);
const lineHeight = parseInt(compStyle.lineHeight, 10);
const paddingTop = parseInt(compStyle.paddingTop, 10);
const rect = absoluteRect(node);
rect.top += (lineHeight - 20) / 2;
rect.top += paddingTop;
if (node.parentElement?.parentElement?.matches("td") || node.parentElement?.parentElement?.matches("th")) {
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
rect.left -= 5;
}
} else {
// Li markers
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
rect.left -= 18;
}
}
if (node.matches(".table-wrapper")) {
rect.top += 8;
rect.left -= 8;
}
if (node.parentElement?.matches("td") || node.parentElement?.matches("th")) {
rect.left += 8;
}
rect.width = options.dragHandleWidth;
if (!editorSideMenu) return;
editorSideMenu.style.left = `${rect.left - rect.width}px`;
editorSideMenu.style.top = `${rect.top}px`;
showSideMenu();
if (handlesConfig.dragDrop) {
dragHandleDOMEvents?.mousemove();
}
if (handlesConfig.ai) {
aiHandleDOMEvents?.mousemove?.();
}
},
keydown: () => hideSideMenu(),
mousewheel: () => hideSideMenu(),
dragenter: (view) => {
if (handlesConfig.dragDrop) {
dragHandleDOMEvents?.dragenter?.(view);
}
},
drop: (view, event) => {
if (handlesConfig.dragDrop) {
dragHandleDOMEvents?.drop?.(view, event);
}
},
dragend: (view) => {
if (handlesConfig.dragDrop) {
dragHandleDOMEvents?.dragend?.(view);
}
},
},
},
});
};

View file

@ -3,9 +3,9 @@ import Collaboration from "@tiptap/extension-collaboration";
import { EditorProps } from "@tiptap/pm/view";
import * as Y from "yjs";
// extensions
import { DragAndDrop, IssueWidget } from "@/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,9 +13,10 @@ import { CollaborationProvider } from "@/plane-editor/providers";
// plane editor types
import { TEmbedConfig } from "@/plane-editor/types";
// types
import { EditorRefApi, IMentionHighlight, IMentionSuggestion } from "@/types";
import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TExtensions, TFileHandler } from "@/types";
type DocumentEditorProps = {
disabledExtensions?: TExtensions[];
editorClassName: string;
editorProps?: EditorProps;
embedHandler?: TEmbedConfig;
@ -29,13 +30,13 @@ type DocumentEditorProps = {
};
onChange: (updates: Uint8Array) => void;
placeholder?: string | ((isFocused: boolean, value: string) => string);
setHideDragHandleFunction: (hideDragHandlerFromDragDrop: () => void) => void;
tabIndex?: number;
value: Uint8Array;
};
export const useDocumentEditor = (props: DocumentEditorProps) => {
const {
disabledExtensions,
editorClassName,
editorProps = {},
embedHandler,
@ -46,7 +47,6 @@ export const useDocumentEditor = (props: DocumentEditorProps) => {
mentionHandler,
onChange,
placeholder,
setHideDragHandleFunction,
tabIndex,
value,
} = props;
@ -93,7 +93,10 @@ export const useDocumentEditor = (props: DocumentEditorProps) => {
forwardedRef,
mentionHandler,
extensions: [
DragAndDrop(setHideDragHandleFunction),
SideMenuExtension({
aiEnabled: !disabledExtensions?.includes("ai"),
dragDropEnabled: true,
}),
embedHandler?.issue &&
IssueWidget({
widgetCallback: embedHandler.issue.widgetCallback,
@ -102,6 +105,7 @@ export const useDocumentEditor = (props: DocumentEditorProps) => {
document: provider.document,
}),
...DocumentEditorAdditionalExtensions({
disabledExtensions,
fileHandler,
issueEmbedConfig: embedHandler?.issue,
}),
@ -111,5 +115,8 @@ export const useDocumentEditor = (props: DocumentEditorProps) => {
tabIndex,
});
return { editor, isIndexedDbSynced };
return {
editor,
isIndexedDbSynced,
};
};

View file

@ -1,7 +1,8 @@
import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react";
import { DOMSerializer } from "@tiptap/pm/model";
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 +15,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 +40,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 +85,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);
@ -232,6 +214,41 @@ export const useEditor = ({
console.error("An error occurred while setting focus at position:", error);
}
},
getSelectedText: () => {
if (!editorRef.current) return null;
const { state } = editorRef.current;
const { from, to, empty } = state.selection;
if (empty) return null;
const nodesArray: string[] = [];
state.doc.nodesBetween(from, to, (node, pos, parent) => {
if (parent === state.doc && editorRef.current) {
const serializer = DOMSerializer.fromSchema(editorRef.current?.schema);
const dom = serializer.serializeNode(node);
const tempDiv = document.createElement("div");
tempDiv.appendChild(dom);
nodesArray.push(tempDiv.innerHTML);
}
});
const selection = nodesArray.join("");
console.log(selection);
return selection;
},
insertText: (contentHTML, insertOnNextLine) => {
if (!editor) return;
// get selection
const { from, to, empty } = editor.state.selection;
if (empty) return;
if (insertOnNextLine) {
// move cursor to the end of the selection and insert a new line
editor.chain().focus().setTextSelection(to).insertContent("<br />").insertContent(contentHTML).run();
} else {
// replace selected text with the content provided
editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
}
},
}),
[editorRef, savedSelection, fileHandler.upload]
);

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

@ -0,0 +1,153 @@
import { NodeSelection } from "@tiptap/pm/state";
import { EditorView } from "@tiptap/pm/view";
// extensions
import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions";
const sparklesIcon =
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkles"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>';
const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
const elements = document.elementsFromPoint(coords.x, coords.y);
const generalSelectors = [
"li",
"p:not(:first-child)",
".code-block",
"blockquote",
"img",
"h1, h2, h3, h4, h5, h6",
"[data-type=horizontalRule]",
".table-wrapper",
".issue-embed",
].join(", ");
for (const elem of elements) {
if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) {
return elem;
}
// if the element is a <p> tag that is the first child of a td or th
if (
(elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) &&
elem?.textContent?.trim() !== ""
) {
return elem; // Return only if p tag is not empty in td or th
}
// apply general selector
if (elem.matches(generalSelectors)) {
return elem;
}
}
return null;
};
const nodePosAtDOM = (node: Element, view: EditorView, options: SideMenuPluginProps) => {
const boundingRect = node.getBoundingClientRect();
return view.posAtCoords({
left: boundingRect.left + 50 + options.dragHandleWidth,
top: boundingRect.top + 1,
})?.inside;
};
const nodePosAtDOMForBlockQuotes = (node: Element, view: EditorView) => {
const boundingRect = node.getBoundingClientRect();
return view.posAtCoords({
left: boundingRect.left + 1,
top: boundingRect.top + 1,
})?.inside;
};
const calcNodePos = (pos: number, view: EditorView, node: Element) => {
const maxPos = view.state.doc.content.size;
const safePos = Math.max(0, Math.min(pos, maxPos));
const $pos = view.state.doc.resolve(safePos);
if ($pos.depth > 1) {
if (node.matches("ul li, ol li")) {
// only for nested lists
const newPos = $pos.before($pos.depth);
return Math.max(0, Math.min(newPos, maxPos));
}
}
return safePos;
};
export const AIHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOptions => {
let aiHandleElement: HTMLButtonElement | null = null;
const handleClick = (event: MouseEvent, view: EditorView) => {
view.focus();
const node = nodeDOMAtCoords({
x: event.clientX + 50 + options.dragHandleWidth,
y: event.clientY,
});
if (!(node instanceof Element)) return;
if (node.matches("blockquote")) {
let nodePosForBlockQuotes = nodePosAtDOMForBlockQuotes(node, view);
if (nodePosForBlockQuotes === null || nodePosForBlockQuotes === undefined) return;
const docSize = view.state.doc.content.size;
nodePosForBlockQuotes = Math.max(0, Math.min(nodePosForBlockQuotes, docSize));
if (nodePosForBlockQuotes >= 0 && nodePosForBlockQuotes <= docSize) {
// TODO FIX ERROR
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockQuotes);
view.dispatch(view.state.tr.setSelection(nodeSelection));
}
return;
}
let nodePos = nodePosAtDOM(node, view, options);
if (nodePos === null || nodePos === undefined) return;
// Adjust the nodePos to point to the start of the node, ensuring NodeSelection can be applied
nodePos = calcNodePos(nodePos, view, node);
// TODO FIX ERROR
// Use NodeSelection to select the node at the calculated position
const nodeSelection = NodeSelection.create(view.state.doc, nodePos);
// Dispatch the transaction to update the selection
view.dispatch(view.state.tr.setSelection(nodeSelection));
};
const view = (view: EditorView, sideMenu: HTMLDivElement | null) => {
// create handle element
const className =
"grid place-items-center font-medium size-5 aspect-square text-xs text-custom-text-300 hover:bg-custom-background-80 rounded-sm opacity-100 !outline-none z-[5] transition-[background-color,_opacity] duration-200 ease-linear";
aiHandleElement = document.createElement("button");
aiHandleElement.type = "button";
aiHandleElement.id = "ai-handle";
aiHandleElement.classList.value = className;
const iconElement = document.createElement("span");
iconElement.classList.value = "pointer-events-none";
iconElement.innerHTML = sparklesIcon;
aiHandleElement.appendChild(iconElement);
// bind events
aiHandleElement.addEventListener("click", (e) => handleClick(e, view));
sideMenu?.appendChild(aiHandleElement);
return {
// destroy the handle element on un-initialize
destroy: () => {
aiHandleElement?.remove();
aiHandleElement = null;
},
};
};
const domEvents = {};
return {
view,
domEvents,
};
};

View file

@ -1,68 +1,35 @@
import { Extension } from "@tiptap/core";
import { Fragment, Slice, Node } from "@tiptap/pm/model";
import { NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
import { NodeSelection, TextSelection } from "@tiptap/pm/state";
// @ts-expect-error __serializeForClipboard's is not exported
import { __serializeForClipboard, EditorView } from "@tiptap/pm/view";
// extensions
import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions";
export interface DragHandleOptions {
dragHandleWidth: number;
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void;
scrollThreshold: {
up: number;
down: number;
};
}
export const DragAndDrop = (setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void) =>
Extension.create({
name: "dragAndDrop",
addProseMirrorPlugins() {
return [
DragHandle({
dragHandleWidth: 24,
scrollThreshold: { up: 300, down: 100 },
setHideDragHandle,
}),
];
},
});
const verticalEllipsisIcon =
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ellipsis-vertical"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>';
const createDragHandleElement = (): HTMLElement => {
const dragHandleElement = document.createElement("button");
dragHandleElement.type = "button";
dragHandleElement.id = "drag-handle";
dragHandleElement.draggable = true;
dragHandleElement.dataset.dragHandle = "";
dragHandleElement.classList.add("drag-handle");
dragHandleElement.classList.value =
"hidden sm:flex items-center size-5 aspect-square rounded-sm cursor-grab outline-none hover:bg-custom-background-80 active:bg-custom-background-80 active:cursor-grabbing transition-[background-color,_opacity] duration-200 ease-linear";
const dragHandleContainer = document.createElement("span");
dragHandleContainer.classList.add("drag-handle-container");
dragHandleElement.appendChild(dragHandleContainer);
const iconElement1 = document.createElement("span");
iconElement1.classList.value = "pointer-events-none text-custom-text-300";
iconElement1.innerHTML = verticalEllipsisIcon;
const iconElement2 = document.createElement("span");
iconElement2.classList.value = "pointer-events-none text-custom-text-300 -ml-2.5";
iconElement2.innerHTML = verticalEllipsisIcon;
const dotsContainer = document.createElement("span");
dotsContainer.classList.add("drag-handle-dots");
for (let i = 0; i < 6; i++) {
const spanElement = document.createElement("span");
spanElement.classList.add("drag-handle-dot");
dotsContainer.appendChild(spanElement);
}
dragHandleContainer.appendChild(dotsContainer);
dragHandleElement.appendChild(iconElement1);
dragHandleElement.appendChild(iconElement2);
return dragHandleElement;
};
const absoluteRect = (node: Element) => {
const data = node.getBoundingClientRect();
return {
top: data.top,
left: data.left,
width: data.width,
};
};
const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
const elements = document.elementsFromPoint(coords.x, coords.y);
const generalSelectors = [
@ -98,7 +65,7 @@ const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
return null;
};
const nodePosAtDOM = (node: Element, view: EditorView, options: DragHandleOptions) => {
const nodePosAtDOM = (node: Element, view: EditorView, options: SideMenuPluginProps) => {
const boundingRect = node.getBoundingClientRect();
return view.posAtCoords({
@ -132,7 +99,7 @@ const calcNodePos = (pos: number, view: EditorView, node: Element) => {
return safePos;
};
const DragHandle = (options: DragHandleOptions) => {
export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOptions => {
let listType = "";
const handleDragStart = (event: DragEvent, view: EditorView) => {
view.focus();
@ -222,15 +189,15 @@ const DragHandle = (options: DragHandleOptions) => {
if (!(node instanceof Element)) return;
if (node.matches("blockquote")) {
let nodePosForBlockquotes = nodePosAtDOMForBlockQuotes(node, view);
if (nodePosForBlockquotes === null || nodePosForBlockquotes === undefined) return;
let nodePosForBlockQuotes = nodePosAtDOMForBlockQuotes(node, view);
if (nodePosForBlockQuotes === null || nodePosForBlockQuotes === undefined) return;
const docSize = view.state.doc.content.size;
nodePosForBlockquotes = Math.max(0, Math.min(nodePosForBlockquotes, docSize));
nodePosForBlockQuotes = Math.max(0, Math.min(nodePosForBlockQuotes, docSize));
if (nodePosForBlockquotes >= 0 && nodePosForBlockquotes <= docSize) {
if (nodePosForBlockQuotes >= 0 && nodePosForBlockQuotes <= docSize) {
// TODO FIX ERROR
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockquotes);
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockQuotes);
view.dispatch(view.state.tr.setSelection(nodeSelection));
}
return;
@ -253,152 +220,98 @@ const DragHandle = (options: DragHandleOptions) => {
let dragHandleElement: HTMLElement | null = null;
// drag handle view actions
const hideDragHandle = () => dragHandleElement?.classList.add("drag-handle-hidden");
const showDragHandle = () => dragHandleElement?.classList.remove("drag-handle-hidden");
const hideDragHandle = () => {
if (!dragHandleElement?.classList.contains("drag-handle-hidden"))
dragHandleElement?.classList.add("drag-handle-hidden");
};
options.setHideDragHandle?.(hideDragHandle);
const view = (view: EditorView, sideMenu: HTMLDivElement | null) => {
dragHandleElement = createDragHandleElement();
dragHandleElement.addEventListener("dragstart", (e) => handleDragStart(e, view));
dragHandleElement.addEventListener("click", (e) => handleClick(e, view));
dragHandleElement.addEventListener("contextmenu", (e) => handleClick(e, view));
return new Plugin({
key: new PluginKey("dragHandle"),
view: (view) => {
dragHandleElement = createDragHandleElement();
dragHandleElement.addEventListener("dragstart", (e) => handleDragStart(e, view));
dragHandleElement.addEventListener("click", (e) => handleClick(e, view));
dragHandleElement.addEventListener("contextmenu", (e) => handleClick(e, view));
dragHandleElement.addEventListener("drag", (e) => {
hideDragHandle();
const frameRenderer = document.querySelector(".frame-renderer");
if (!frameRenderer) return;
if (e.clientY < options.scrollThreshold.up) {
frameRenderer.scrollBy({ top: -70, behavior: "smooth" });
} else if (window.innerHeight - e.clientY < options.scrollThreshold.down) {
frameRenderer.scrollBy({ top: 70, behavior: "smooth" });
}
});
dragHandleElement.addEventListener("drag", (e) => {
hideDragHandle();
const frameRenderer = document.querySelector(".frame-renderer");
if (!frameRenderer) return;
if (e.clientY < options.scrollThreshold.up) {
frameRenderer.scrollBy({ top: -70, behavior: "smooth" });
} else if (window.innerHeight - e.clientY < options.scrollThreshold.down) {
frameRenderer.scrollBy({ top: 70, behavior: "smooth" });
}
hideDragHandle();
sideMenu?.appendChild(dragHandleElement);
return {
destroy: () => {
dragHandleElement?.remove?.();
dragHandleElement = null;
},
};
};
const domEvents = {
mousemove: () => showDragHandle(),
dragenter: (view: EditorView) => {
view.dom.classList.add("dragging");
hideDragHandle();
},
drop: (view: EditorView, event: DragEvent) => {
view.dom.classList.remove("dragging");
hideDragHandle();
let droppedNode: Node | null = null;
const dropPos = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
hideDragHandle();
if (!dropPos) return;
view?.dom?.parentElement?.appendChild(dragHandleElement);
if (view.state.selection instanceof NodeSelection) {
droppedNode = view.state.selection.node;
}
return {
destroy: () => {
dragHandleElement?.remove?.();
dragHandleElement = null;
},
};
if (!droppedNode) return;
const resolvedPos = view.state.doc.resolve(dropPos.pos);
let isDroppedInsideList = false;
// Traverse up the document tree to find if we're inside a list item
for (let i = resolvedPos.depth; i > 0; i--) {
if (resolvedPos.node(i).type.name === "listItem") {
isDroppedInsideList = true;
break;
}
}
// If the selected node is a list item and is not dropped inside a list, we need to wrap it inside <ol> tag otherwise ol list items will be transformed into ul list item when dropped
if (
view.state.selection instanceof NodeSelection &&
view.state.selection.node.type.name === "listItem" &&
!isDroppedInsideList &&
listType == "OL"
) {
const text = droppedNode.textContent;
if (!text) return;
const paragraph = view.state.schema.nodes.paragraph?.createAndFill({}, view.state.schema.text(text));
const listItem = view.state.schema.nodes.listItem?.createAndFill({}, paragraph);
const newList = view.state.schema.nodes.orderedList?.createAndFill(null, listItem);
const slice = new Slice(Fragment.from(newList), 0, 0);
view.dragging = { slice, move: event.ctrlKey };
}
},
props: {
handleDOMEvents: {
mousemove: (view, event) => {
if (!view.editable) return;
const node = nodeDOMAtCoords({
x: event.clientX + 50 + options.dragHandleWidth,
y: event.clientY,
});
if (!(node instanceof Element) || node.matches("ul, ol")) {
hideDragHandle();
return;
}
const compStyle = window.getComputedStyle(node);
const lineHeight = parseInt(compStyle.lineHeight, 10);
const paddingTop = parseInt(compStyle.paddingTop, 10);
const rect = absoluteRect(node);
rect.top += (lineHeight - 20) / 2;
rect.top += paddingTop;
if (node.parentElement?.parentElement?.matches("td") || node.parentElement?.parentElement?.matches("th")) {
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
rect.left -= 5;
}
} else {
// Li markers
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
rect.left -= 18;
}
}
if (node.matches(".table-wrapper")) {
rect.top += 8;
rect.left -= 8;
}
if (node.parentElement?.matches("td") || node.parentElement?.matches("th")) {
rect.left += 8;
}
rect.width = options.dragHandleWidth;
if (!dragHandleElement) return;
dragHandleElement.style.left = `${rect.left - rect.width}px`;
dragHandleElement.style.top = `${rect.top}px`;
showDragHandle();
},
keydown: () => {
hideDragHandle();
},
mousewheel: () => {
hideDragHandle();
},
dragenter: (view) => {
view.dom.classList.add("dragging");
hideDragHandle();
},
drop: (view, event) => {
view.dom.classList.remove("dragging");
hideDragHandle();
let droppedNode: Node | null = null;
const dropPos = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
if (!dropPos) return;
if (view.state.selection instanceof NodeSelection) {
droppedNode = view.state.selection.node;
}
if (!droppedNode) return;
const resolvedPos = view.state.doc.resolve(dropPos.pos);
let isDroppedInsideList = false;
// Traverse up the document tree to find if we're inside a list item
for (let i = resolvedPos.depth; i > 0; i--) {
if (resolvedPos.node(i).type.name === "listItem") {
isDroppedInsideList = true;
break;
}
}
// If the selected node is a list item and is not dropped inside a list, we need to wrap it inside <ol> tag otherwise ol list items will be transformed into ul list item when dropped
if (
view.state.selection instanceof NodeSelection &&
view.state.selection.node.type.name === "listItem" &&
!isDroppedInsideList &&
listType == "OL"
) {
const text = droppedNode.textContent;
if (!text) return;
const paragraph = view.state.schema.nodes.paragraph?.createAndFill({}, view.state.schema.text(text));
const listItem = view.state.schema.nodes.listItem?.createAndFill({}, paragraph);
const newList = view.state.schema.nodes.orderedList?.createAndFill(null, listItem);
const slice = new Slice(Fragment.from(newList), 0, 0);
view.dragging = { slice, move: event.ctrlKey };
}
},
dragend: (view) => {
view.dom.classList.remove("dragging");
},
},
dragend: (view: EditorView) => {
view.dom.classList.remove("dragging");
},
});
};
return {
view,
domEvents,
};
};

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,7 @@
type TMenuProps = {
onClose: () => void;
};
export type TAIHandler = {
menu?: (props: TMenuProps) => React.ReactNode;
};

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;
@ -22,10 +20,13 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
isEditorReadyToDiscard: () => boolean;
setSynced: () => void;
hasUnsyncedChanges: () => boolean;
getSelectedText: () => string | null;
insertText: (contentHTML: string, insertOnNextLine?: boolean) => void;
}
export interface IEditorProps {
containerClassName?: string;
displayConfig?: TDisplayConfig;
editorClassName?: string;
fileHandler: TFileHandler;
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
@ -36,7 +37,7 @@ export interface IEditorProps {
suggestions?: () => Promise<IMentionSuggestion[]>;
};
onChange?: (json: object, html: string) => void;
onEnterKeyPress?: (descriptionHTML: string) => void;
onEnterKeyPress?: (e?: any) => void;
placeholder?: string | ((isFocused: boolean, value: string) => string);
tabIndex?: number;
value?: string | null;
@ -50,6 +51,7 @@ export interface IRichTextEditor extends IEditorProps {
export interface IReadOnlyEditorProps {
containerClassName?: string;
displayConfig?: TDisplayConfig;
editorClassName?: string;
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
id: string;

View file

@ -0,0 +1 @@
export type TExtensions = "ai" | "issue-embed";

View file

@ -1,5 +1,8 @@
export * from "./ai";
export * from "./config";
export * from "./editor";
export * from "./embed";
export * from "./extensions";
export * from "./image";
export * from "./mention-suggestion";
export * from "./slash-commands-suggestion";

View file

@ -20,7 +20,8 @@ export type TEditorCommands =
| "code"
| "table"
| "image"
| "divider";
| "divider"
| "issue-embed";
export type CommandProps = {
editor: Editor;

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,66 +1,43 @@
/* drag handle */
.drag-handle {
/* side menu */
#editor-side-menu {
position: fixed;
display: flex;
align-items: center;
opacity: 1;
height: 20px;
width: 20px;
aspect-ratio: 1 / 1;
display: grid;
place-items: center;
z-index: 5;
cursor: grab;
border-radius: 2px;
outline: none !important;
transition:
opacity 0.2s ease 0.2s,
background-color 0.2s ease,
top 0.2s ease,
left 0.2s ease;
transform: translateX(-50%);
&:hover {
background-color: rgba(var(--color-background-80));
&.side-menu-hidden {
opacity: 0;
pointer-events: none;
}
}
/* end side menu */
&:active {
background-color: rgba(var(--color-background-80));
cursor: grabbing;
}
/* drag handle */
#drag-handle {
opacity: 1;
&.drag-handle-hidden {
opacity: 0;
pointer-events: none;
}
}
/* end drag handle */
@media screen and (max-width: 600px) {
.drag-handle {
display: none;
/* ai handle */
#ai-handle {
opacity: 1;
&.handle-hidden {
opacity: 0;
pointer-events: none;
}
}
.drag-handle-container {
height: 15px;
width: 15px;
display: grid;
place-items: center;
}
.drag-handle-dots {
height: 100%;
width: 12px;
display: grid;
grid-template-columns: repeat(2, 1fr);
place-items: center;
}
.drag-handle-dot {
height: 2.5px;
width: 2.5px;
background-color: rgba(var(--color-text-300));
border-radius: 50%;
}
/* end drag handle */
/* end ai handle */
.ProseMirror:not(.dragging) .ProseMirror-selectednode {
position: relative;

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

@ -1,9 +1,23 @@
type TLogoProps = {
in_use: "emoji" | "icon";
emoji?: {
value?: string;
url?: string;
};
icon?: {
name?: string;
color?: string;
};
};
export type IFavorite = {
id: string;
name: string;
entity_type: string;
entity_data: {
id?: string;
name: string;
logo_props?: TLogoProps | undefined;
};
is_folder: boolean;
sort_order: number;

View file

@ -28,7 +28,7 @@ export type TNotificationData = {
actor: string | undefined;
field: string | undefined;
issue_comment: string | undefined;
verb: "created" | "updated";
verb: "created" | "updated" | "deleted";
new_value: string | undefined;
old_value: string | undefined;
};

View file

@ -39,7 +39,6 @@ export const Collapsible: FC<TCollapsibleProps> = (props) => {
</Disclosure.Button>
<Transition
show={localIsOpen}
className="overflow-hidden"
enter="transition-max-height duration-400 ease-in-out"
enterFrom="max-h-0"
enterTo="max-h-screen"

View file

@ -7,10 +7,21 @@ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>
inputSize?: "xs" | "sm" | "md";
hasError?: boolean;
className?: string;
autoComplete?: "on" | "off";
}
const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const { id, type, name, mode = "primary", inputSize = "sm", hasError = false, className = "", ...rest } = props;
const {
id,
type,
name,
mode = "primary",
inputSize = "sm",
hasError = false,
className = "",
autoComplete = "off",
...rest
} = props;
return (
<input
@ -32,6 +43,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
},
className
)}
autoComplete={autoComplete}
{...rest}
/>
);

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