diff --git a/README.md b/README.md index ca231fc..4b769bf 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,4 @@ LLM Integration ([English](docs/llm/llm-integration.en.md), [Español](docs/llm/ Development Conditions ([English](docs/meta/Copilot.md)) For more details, check the documentation in the [docs](docs/) folder. +- The [Notion Editor](docs/notion-editor.md) template demonstrates a Notion-style layout with a basic AI extension. diff --git a/docs/README.md b/docs/README.md index 91166e0..baa34fb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,6 +9,7 @@ Welcome to the documentation for the macOS editor. Below is a summary of the ava - [Build Script](build-script.md) - [Package.json File](package-json.md) - [Splash Screen Functionality](splash-screen.md) +- [Notion Editor](notion-editor.md) - [LLM Integration](llm/llm-integration.en.md) ## Documentation Structure @@ -22,5 +23,6 @@ graph TD; A --> F[Build Script]; A --> G[Package.json File]; A --> H[Splash Screen Functionality]; + A --> I[Notion Editor]; A --> I[LLM Integration]; ``` diff --git a/docs/notion-editor.md b/docs/notion-editor.md new file mode 100644 index 0000000..def458a --- /dev/null +++ b/docs/notion-editor.md @@ -0,0 +1,3 @@ +# Notion Editor + +The Notion Editor uses the same toolbar from the Simple Editor but applies a Notion-style layout. It also loads a placeholder AI extension called `Agent` that demonstrates how to insert generated text with a custom command. diff --git a/src/components/tiptap-extension/agent-extension.ts b/src/components/tiptap-extension/agent-extension.ts new file mode 100644 index 0000000..6704309 --- /dev/null +++ b/src/components/tiptap-extension/agent-extension.ts @@ -0,0 +1,18 @@ +import { Extension } from "@tiptap/react" + +export const Agent = Extension.create({ + name: "agent", + addCommands() { + return { + generate: + (prompt: string) => + async ({ commands }) => { + const aiText = `AI: ${prompt}` + commands.insertContent(aiText) + return true + }, + } + }, +}) + +export default Agent diff --git a/src/components/tiptap-templates/notion/data/content.json b/src/components/tiptap-templates/notion/data/content.json new file mode 100644 index 0000000..b4e2ae9 --- /dev/null +++ b/src/components/tiptap-templates/notion/data/content.json @@ -0,0 +1,477 @@ +{ + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "textAlign": null, + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Getting started" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": null + }, + "content": [ + { + "type": "text", + "text": "Welcome to the " + }, + { + "type": "text", + "marks": [ + { + "type": "italic" + }, + { + "type": "highlight", + "attrs": { + "color": "var(--tt-highlight-yellow)" + } + } + ], + "text": "Simple Editor" + }, + { + "type": "text", + "text": " template! This template integrates " + }, + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "open source" + }, + { + "type": "text", + "text": " UI components and Tiptap extensions licensed under " + }, + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "MIT" + }, + { + "type": "text", + "text": "." + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": null + }, + "content": [ + { + "type": "text", + "text": "Integrate it by following the " + }, + { + "type": "text", + "marks": [ + { + "type": "link", + "attrs": { + "href": "https://tiptap.dev/docs/ui-components/templates/simple-editor", + "target": "_blank", + "rel": "noopener noreferrer nofollow", + "class": null + } + } + ], + "text": "Tiptap UI Components docs" + }, + { + "type": "text", + "text": " or using our CLI tool." + } + ] + }, + { + "type": "codeBlock", + "attrs": { + "language": null + }, + "content": [ + { + "type": "text", + "text": "npx @tiptap/cli init" + } + ] + }, + { + "type": "heading", + "attrs": { + "textAlign": null, + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Features" + } + ] + }, + { + "type": "blockquote", + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": null + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "italic" + } + ], + "text": "A fully responsive rich text editor with built-in support for common formatting and layout tools. Type markdown " + }, + { + "type": "text", + "marks": [ + { + "type": "code" + } + ], + "text": "**" + }, + { + "type": "text", + "marks": [ + { + "type": "italic" + } + ], + "text": " or use keyboard shortcuts " + }, + { + "type": "text", + "marks": [ + { + "type": "code" + } + ], + "text": "⌘+B" + }, + { + "type": "text", + "text": " for " + }, + { + "type": "text", + "marks": [ + { + "type": "strike" + } + ], + "text": "most" + }, + { + "type": "text", + "text": " all common markdown marks. 🪄" + } + ] + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "text": "Add images, customize alignment, and apply " + }, + { + "type": "text", + "marks": [ + { + "type": "highlight", + "attrs": { + "color": "var(--tt-highlight-blue)" + } + } + ], + "text": "advanced formatting" + }, + { + "type": "text", + "text": " to make your writing more engaging and professional." + } + ] + }, + { + "type": "image", + "attrs": { + "src": "/images/placeholder-image.png", + "alt": "placeholder-image", + "title": "placeholder-image" + } + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "Superscript" + }, + { + "type": "text", + "text": " (x" + }, + { + "type": "text", + "marks": [ + { + "type": "superscript" + } + ], + "text": "2" + }, + { + "type": "text", + "text": ") and " + }, + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "Subscript" + }, + { + "type": "text", + "text": " (H" + }, + { + "type": "text", + "marks": [ + { + "type": "subscript" + } + ], + "text": "2" + }, + { + "type": "text", + "text": "O) for precision." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "Typographic conversion" + }, + { + "type": "text", + "text": ": automatically convert to " + }, + { + "type": "text", + "marks": [ + { + "type": "code" + } + ], + "text": "->" + }, + { + "type": "text", + "text": " an arrow " + }, + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "→" + }, + { + "type": "text", + "text": "." + } + ] + } + ] + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "italic" + } + ], + "text": "→ " + }, + { + "type": "text", + "marks": [ + { + "type": "link", + "attrs": { + "href": "https://tiptap.dev/docs/ui-components/templates/simple-editor#features", + "target": "_blank", + "rel": "noopener noreferrer nofollow", + "class": null + } + } + ], + "text": "Learn more" + } + ] + }, + { + "type": "horizontalRule" + }, + { + "type": "heading", + "attrs": { + "textAlign": "left", + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Make it your own" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "text": "Switch between light and dark modes, and tailor the editor's appearance with customizable CSS to match your style." + } + ] + }, + { + "type": "taskList", + "content": [ + { + "type": "taskItem", + "attrs": { + "checked": true + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "text": "Test template" + } + ] + } + ] + }, + { + "type": "taskItem", + "attrs": { + "checked": false + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "link", + "attrs": { + "href": "https://tiptap.dev/docs/ui-components/templates/simple-editor", + "target": "_blank", + "rel": "noopener noreferrer nofollow", + "class": null + } + } + ], + "text": "Integrate the free template" + } + ] + } + ] + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + } + } + ] +} diff --git a/src/components/tiptap-templates/notion/notion-editor.scss b/src/components/tiptap-templates/notion/notion-editor.scss new file mode 100644 index 0000000..757ec58 --- /dev/null +++ b/src/components/tiptap-templates/notion/notion-editor.scss @@ -0,0 +1,74 @@ +@import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); + +body { + --tt-toolbar-height: 44px; + --tt-theme-text: var(--tt-gray-light-900); + + .dark & { + --tt-theme-text: var(--tt-gray-dark-900); + } +} + +body { + font-family: "Inter", sans-serif; + color: var(--tt-theme-text); + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + padding: 0; +} + +html, +body, +#root, +#app { + height: 100%; + background-color: var(--tt-bg-color); +} + +body { + overflow: hidden; +} + +.tiptap.ProseMirror { + font-family: "DM Sans", sans-serif; +} + +.content-wrapper { + height: calc(100% - var(--tt-toolbar-height)); + overflow-y: auto; + + &::-webkit-scrollbar { + display: block; + width: 0.5rem; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: var(--tt-scrollbar-color); + border-radius: 4px; + } + + /* Firefox scrollbar */ + scrollbar-width: thin; + scrollbar-color: var(--tt-scrollbar-color) transparent; +} + +.notion-editor-content { + max-width: 640px; + width: 100%; + margin: 0 auto; +} + +.notion-editor-content .tiptap.ProseMirror { + padding: 3rem 3rem; +} + +@media screen and (max-width: 480px) { + .notion-editor-content .tiptap.ProseMirror { + padding: 1rem 1.5rem; + } +} diff --git a/src/components/tiptap-templates/notion/notion-editor.tsx b/src/components/tiptap-templates/notion/notion-editor.tsx new file mode 100644 index 0000000..3c9bc15 --- /dev/null +++ b/src/components/tiptap-templates/notion/notion-editor.tsx @@ -0,0 +1,270 @@ +import * as React from "react" +import { EditorContent, EditorContext, useEditor } from "@tiptap/react" + +// --- Tiptap Core Extensions --- +import { StarterKit } from "@tiptap/starter-kit" +import { Image } from "@tiptap/extension-image" +import { TaskItem } from "@tiptap/extension-task-item" +import { TaskList } from "@tiptap/extension-task-list" +import { TextAlign } from "@tiptap/extension-text-align" +import { Typography } from "@tiptap/extension-typography" +import { Highlight } from "@tiptap/extension-highlight" +import { Subscript } from "@tiptap/extension-subscript" +import { Superscript } from "@tiptap/extension-superscript" +import { Underline } from "@tiptap/extension-underline" + +// --- Custom Extensions --- +import { Link } from "@/components/tiptap-extension/link-extension" +import { Selection } from "@/components/tiptap-extension/selection-extension" +import { TrailingNode } from "@/components/tiptap-extension/trailing-node-extension" + +// --- UI Primitives --- +import { Button } from "@/components/tiptap-ui-primitive/button" +import { Spacer } from "@/components/tiptap-ui-primitive/spacer" +import { + Toolbar, + ToolbarGroup, + ToolbarSeparator, +} from "@/components/tiptap-ui-primitive/toolbar" + +// --- Tiptap Node --- +import { ImageUploadNode } from "@/components/tiptap-node/image-upload-node/image-upload-node-extension" +import "@/components/tiptap-node/code-block-node/code-block-node.scss" +import "@/components/tiptap-node/list-node/list-node.scss" +import "@/components/tiptap-node/image-node/image-node.scss" +import "@/components/tiptap-node/paragraph-node/paragraph-node.scss" + +// --- Tiptap UI --- +import { HeadingDropdownMenu } from "@/components/tiptap-ui/heading-dropdown-menu" +import { ImageUploadButton } from "@/components/tiptap-ui/image-upload-button" +import { ListDropdownMenu } from "@/components/tiptap-ui/list-dropdown-menu" +import { NodeButton } from "@/components/tiptap-ui/node-button" +import { + HighlightPopover, + HighlightContent, + HighlighterButton, +} from "@/components/tiptap-ui/highlight-popover" +import { + LinkPopover, + LinkContent, + LinkButton, +} from "@/components/tiptap-ui/link-popover" +import { MarkButton } from "@/components/tiptap-ui/mark-button" +import { TextAlignButton } from "@/components/tiptap-ui/text-align-button" +import { UndoRedoButton } from "@/components/tiptap-ui/undo-redo-button" + +// --- Icons --- +import { ArrowLeftIcon } from "@/components/tiptap-icons/arrow-left-icon" +import { HighlighterIcon } from "@/components/tiptap-icons/highlighter-icon" +import { LinkIcon } from "@/components/tiptap-icons/link-icon" + +// --- Hooks --- +import { useMobile } from "@/hooks/use-mobile" +import { useWindowSize } from "@/hooks/use-window-size" +import { useCursorVisibility } from "@/hooks/use-cursor-visibility" + +// --- Components --- +import { ThemeToggle } from "@/components/tiptap-templates/simple/theme-toggle" + +// --- Lib --- +import { handleImageUpload, MAX_FILE_SIZE } from "@/lib/tiptap-utils" +import { Agent } from "@/components/tiptap-extension/agent-extension" + +// --- Styles --- +import "@/components/tiptap-templates/notion/notion-editor.scss" + +import content from "@/components/tiptap-templates/notion/data/content.json" + +const MainToolbarContent = ({ + onHighlighterClick, + onLinkClick, + isMobile, +}: { + onHighlighterClick: () => void + onLinkClick: () => void + isMobile: boolean +}) => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + {!isMobile ? ( + + ) : ( + + )} + {!isMobile ? : } + + + + + + + + + + + + + + + + + + + + + + + + + + + {isMobile && } + + + + + + ) +} + +const MobileToolbarContent = ({ + type, + onBack, +}: { + type: "highlighter" | "link" + onBack: () => void +}) => ( + <> + + + + + + + {type === "highlighter" ? : } + +) + +export function NotionEditor() { + const isMobile = useMobile() + const windowSize = useWindowSize() + const [mobileView, setMobileView] = React.useState< + "main" | "highlighter" | "link" + >("main") + const toolbarRef = React.useRef(null) + + const editor = useEditor({ + immediatelyRender: false, + editorProps: { + attributes: { + autocomplete: "off", + autocorrect: "off", + autocapitalize: "off", + "aria-label": "Main content area, start typing to enter text.", + }, + }, + extensions: [ + StarterKit, + TextAlign.configure({ types: ["heading", "paragraph"] }), + Underline, + TaskList, + TaskItem.configure({ nested: true }), + Highlight.configure({ multicolor: true }), + Image, + Typography, + Superscript, + Subscript, + + Selection, + ImageUploadNode.configure({ + accept: "image/*", + maxSize: MAX_FILE_SIZE, + limit: 3, + upload: handleImageUpload, + onError: (error) => console.error("Upload failed:", error), + }), + TrailingNode, + Link.configure({ openOnClick: false }), + Agent, + ], + content: content, + }) + + const bodyRect = useCursorVisibility({ + editor, + overlayHeight: toolbarRef.current?.getBoundingClientRect().height ?? 0, + }) + + React.useEffect(() => { + if (!isMobile && mobileView !== "main") { + setMobileView("main") + } + }, [isMobile, mobileView]) + + return ( + + + {mobileView === "main" ? ( + setMobileView("highlighter")} + onLinkClick={() => setMobileView("link")} + isMobile={isMobile} + /> + ) : ( + setMobileView("main")} + /> + )} + + +
+ +
+
+ ) +} diff --git a/src/main.tsx b/src/main.tsx index de76f80..527872e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,6 @@ import { createRoot } from "react-dom/client" import { useState, useEffect } from "react" -import { SimpleEditor } from "@/components/tiptap-templates/simple/simple-editor" +import { NotionEditor } from "@/components/tiptap-templates/notion/notion-editor" import { SplashScreen } from "@/components/splash-screen/splash-screen" import { AppRegistry } from 'react-native'; import { name as appName } from './app.json'; @@ -26,7 +26,7 @@ const App = () => { return ( <> {showSplashScreen && } - {appReady && } + {appReady && } ); };