diff --git a/package.json b/package.json index 6b433e7..ed6913c 100644 --- a/package.json +++ b/package.json @@ -1,50 +1,77 @@ { - "name": "vite-react", + "name": "@kevisual/markdown-editor", "private": true, "version": "0.0.1", "type": "module", - "basename": "/", + "basename": "/root/markdown-editor", + "app": { + "key": "markdown-editor" + }, "scripts": { "dev": "vite", "build": "vite build", "build:css": "tailwindcss -i ./src/index.css -o ./dist/render.css --minify", - "postbuild2": "pnpm build:css", + "postbuild": "pnpm build:css", "preview": "vite preview", - "pub": "envision deploy ./dist -k vite-react -v 0.0.1", + "pub": "envision deploy ./dist -k markdown-editor -v 0.0.1", "dev:lib": "turbo dev" }, "files": [ - "dist" + "dist", + "src" ], "author": "abearxiong ", "license": "MIT", "dependencies": { - "@kevisual/router": "0.0.9", + "@kevisual/router": "0.0.10", "clsx": "^2.1.1", "dayjs": "^1.11.13", "lodash-es": "^4.17.21", "lucide-react": "^0.487.0", + "@tiptap/core": "^2.11.7", + "@tiptap/extension-code-block-lowlight": "^2.11.7", + "@tiptap/extension-document": "^2.11.7", + "@tiptap/extension-highlight": "^2.11.7", + "@tiptap/extension-paragraph": "^2.11.7", + "@tiptap/extension-placeholder": "^2.11.7", + "@tiptap/extension-text": "^2.11.7", + "@tiptap/extension-typography": "^2.11.7", + "@tiptap/pm": "^2.11.7", + "@tiptap/starter-kit": "^2.11.7", + "@tiptap/suggestion": "^2.11.7", + "github-markdown-css": "^5.8.1", + "highlight.js": "^11.11.1", + "idb": "^8.0.2", + "idb-keyval": "^6.2.1", + "immer": "^10.1.1", + "lowlight": "^3.3.0", + "marked": "^15.0.7", + "tiptap-markdown": "^0.8.10", "nanoid": "^5.1.5", "react": "^19.1.0", "react-dom": "^19.1.0", "react-toastify": "^11.0.5", "zustand": "^5.0.3" }, + "exports": { + ".": "./src/index.tsx", + "./*": "./src/*" + }, "publishConfig": { "access": "public" }, "devDependencies": { "@kevisual/query": "0.0.15", "@kevisual/types": "^0.0.6", - "@tailwindcss/vite": "^4.1.1", - "@types/node": "^22.13.17", + "@tailwindcss/vite": "^4.1.3", + "@types/node": "^22.14.0", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.1", "@vitejs/plugin-basic-ssl": "^2.0.0", "@vitejs/plugin-react": "^4.3.4", - "tailwindcss": "^4.1.1", - "typescript": "^5.8.2", - "vite": "^6.2.4" + "tailwindcss": "^4.1.3", + "typescript": "^5.8.3", + "vite": "^6.2.5" }, "packageManager": "pnpm@10.7.1" } \ No newline at end of file diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index 4214824..0000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,3 +0,0 @@ -packages: - - 'submodules/*' - - 'packages/*' diff --git a/script/clear.ts b/script/clear.ts deleted file mode 100644 index 88faa5f..0000000 --- a/script/clear.ts +++ /dev/null @@ -1,11 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -export const root = process.cwd(); - -export const clearWorkspace = () => { - const files = ['submodules', 'packages', 'pnpm-workspace.yaml', 'turbo.json']; - for (const file of files) { - fs.rmSync(path.join(root, file), { recursive: true, force: true }); - } -}; diff --git a/src/index.css b/src/index.css index f1d8c73..ceb640a 100644 --- a/src/index.css +++ b/src/index.css @@ -1 +1,30 @@ -@import "tailwindcss"; +@import 'tailwindcss'; +@import 'github-markdown-css/github-markdown.css'; +@import 'highlight.js/styles/github.css'; +@import './tiptap/tiptap.css'; + +.markdown-body, +.tiptap { + ul, + li { + list-style: unset; + } + ol { + list-style: decimal; + } +} + +.ProseMirror.ProseMirror-focused { + outline: none; +} +.ProseMirror p.is-empty::before { + content: attr(data-placeholder); + color: #aaa; /* Adjust the color as needed */ + font-style: italic; /* Optional: make the placeholder italic */ + pointer-events: none; /* Ensure the placeholder is not interactive */ + height: 0; /* Ensure it doesn't affect layout */ + display: block; /* Ensure it displays as a block element */ +} +.tiptap.ProseMirror { + border: none; +} diff --git a/src/tiptap/components/CommandsList.tsx b/src/tiptap/components/CommandsList.tsx new file mode 100644 index 0000000..fe23e00 --- /dev/null +++ b/src/tiptap/components/CommandsList.tsx @@ -0,0 +1,112 @@ +import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; +import { CommandItem } from '../extensions/suggestions/commands'; + +interface CommandsListProps { + items: CommandItem[]; + command: (props: { content: string }) => void; +} + +export const CommandsList = forwardRef((props: CommandsListProps, ref) => { + const [selectedIndex, setSelectedIndex] = useState(0); + + const selectItem = (index: number) => { + const item = props.items[index]; + + if (item) { + props.command({ content: item.content }); + } + }; + + const upHandler = () => { + setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length); + }; + + const downHandler = () => { + setSelectedIndex((selectedIndex + 1) % props.items.length); + }; + + const enterHandler = () => { + selectItem(selectedIndex); + }; + + useEffect(() => setSelectedIndex(0), [props.items]); + + useImperativeHandle(ref, () => ({ + onKeyDown: ({ event }: { event: KeyboardEvent }) => { + if (event.key === 'ArrowUp') { + upHandler(); + return true; + } + + if (event.key === 'ArrowDown') { + downHandler(); + return true; + } + + if (event.key === 'Enter') { + enterHandler(); + return true; + } + + return false; + }, + })); + + // Scroll to selected item when it changes + useEffect(() => { + const element = document.getElementById(`command-item-${selectedIndex}`); + if (element) { + element.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + }, [selectedIndex]); + + return ( +
+
+
Commands ({props.items.length})
+
Type to filter commands
+
+ +
+ {props.items.length ? ( + props.items.map((item, index) => ( + + )) + ) : ( +
No results
+ )} +
+ +
+ + + + to navigate + + + Enter + to select + +
+
+ ); +}); + +CommandsList.displayName = 'CommandsList'; \ No newline at end of file diff --git a/src/tiptap/components/ReactRenderer.tsx b/src/tiptap/components/ReactRenderer.tsx new file mode 100644 index 0000000..1e17bf5 --- /dev/null +++ b/src/tiptap/components/ReactRenderer.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; + +export class ReactRenderer { + component: any; + element: HTMLElement; + ref: React.RefObject; + props: any; + editor: any; + root: any; + + constructor(component: any, { props, editor }: any) { + this.component = component; + this.element = document.createElement('div'); + this.ref = React.createRef(); + this.props = { + ...props, + ref: this.ref, + }; + this.editor = editor; + this.root = createRoot(this.element); + this.render(); + } + + updateProps(props: any) { + this.props = { + ...this.props, + ...props, + }; + this.render(); + } + + render() { + this.root.render(React.createElement(this.component, this.props)); + } + + destroy() { + this.root.unmount(); + } +} + +export default ReactRenderer; diff --git a/src/tiptap/editor.ts b/src/tiptap/editor.ts new file mode 100644 index 0000000..5ec3b2a --- /dev/null +++ b/src/tiptap/editor.ts @@ -0,0 +1,136 @@ +import { Editor } from '@tiptap/core'; +import StarterKit from '@tiptap/starter-kit'; +import Highlight from '@tiptap/extension-highlight'; +import Typography from '@tiptap/extension-typography'; +import { Markdown } from 'tiptap-markdown'; + +import Placeholder from '@tiptap/extension-placeholder'; +import { Commands, getSuggestionItems, createSuggestionConfig, CommandItem } from './extensions/suggestions'; +import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'; +import { all, createLowlight } from 'lowlight'; +// import 'highlight.js/styles/github.css'; +// 根据需要引入的语言支持 +import js from 'highlight.js/lib/languages/javascript'; +import ts from 'highlight.js/lib/languages/typescript'; +import html from 'highlight.js/lib/languages/xml'; +import css from 'highlight.js/lib/languages/css'; +import markdown from 'highlight.js/lib/languages/markdown'; +// import './editor.css'; + +const lowlight = createLowlight(all); + +// you can also register individual languages +lowlight.register('html', html); +lowlight.register('css', css); +lowlight.register('js', js); +lowlight.register('ts', ts); +lowlight.register('markdown', markdown); + +export class TextEditor { + private editor?: Editor; + private opts?: { markdown?: string; html?: string; items?: CommandItem[]; onUpdateHtml?: (html: string) => void }; + private element?: HTMLElement; + private isInitialSetup: boolean = true; + + constructor() {} + createEditor(el: HTMLElement, opts?: { markdown?: string; html?: string; items?: CommandItem[]; onUpdateHtml?: (html: string) => void }) { + if (this.editor) { + this.destroy(); + } + this.opts = opts; + this.element = el; + const html = opts?.html || ''; + const items = opts?.items || getSuggestionItems(); + const suggestionConfig = createSuggestionConfig(items); + this.isInitialSetup = true; + this.editor = new Editor({ + element: el, // 指定编辑器容器 + extensions: [ + StarterKit, // 使用 StarterKit 包含基础功能 + Highlight, + Placeholder.configure({ + placeholder: 'Type @ to see commands (e.g., @today, @list @test )...', + }), + Typography, + Markdown, + CodeBlockLowlight.extend({ + addKeyboardShortcuts() { + return { + Tab: () => { + const { state, dispatch } = this.editor.view; + const { tr, selection } = state; + const { from, to } = selection; + + // 插入4个空格的缩进 + dispatch(tr.insertText(' ', from, to)); + return true; + }, + 'Shift-Tab': () => { + const { state, dispatch } = this.editor.view; + const { tr, selection } = state; + const { from, to } = selection; + + // 获取当前选中的文本 + const selectedText = state.doc.textBetween(from, to, '\n'); + + // 取消缩进:移除前面的4个空格 + const unindentedText = selectedText.replace(/^ {1,4}/gm, ''); + dispatch(tr.insertText(unindentedText, from, to)); + return true; + }, + }; + }, + }).configure({ + lowlight, + }), + Commands.configure({ + suggestion: suggestionConfig, + }), + ], + content: html, // 初始化内容, + onUpdate: () => { + if (this.isInitialSetup) { + this.isInitialSetup = false; + return; + } + if (this.opts?.onUpdateHtml) { + this.opts.onUpdateHtml(this.editor?.getHTML() || ''); + } + }, + }); + } + updateSugestionConfig(items: CommandItem[]) { + if (!this.element) return; + const element = this.element; + if (this.editor) { + const content = this.editor.getHTML(); // Save current content + const opts = { ...this.opts, html: content, items }; + this.createEditor(element, opts); // Recreate the editor with the new config + } + } + setContent(html: string, emitUpdate?: boolean) { + this.editor?.commands.setContent(html, emitUpdate); + } + /** + * before set options ,you should has element and editor + * @param opts + */ + setOptions(opts: { markdown?: string; html?: string; items?: CommandItem[]; onUpdateHtml?: (html: string) => void }) { + this.opts = { ...this.opts, ...opts }; + this.createEditor(this.element!, this.opts!); + } + getHtml() { + return this.editor?.getHTML(); + } + getContent() { + return this.editor?.getText(); + } + foucus() { + this.editor?.view?.focus?.(); + } + + destroy() { + this.editor?.destroy(); + this.editor = undefined; + } +} diff --git a/src/tiptap/extensions/suggestions/commands.ts b/src/tiptap/extensions/suggestions/commands.ts new file mode 100644 index 0000000..f7bada2 --- /dev/null +++ b/src/tiptap/extensions/suggestions/commands.ts @@ -0,0 +1,40 @@ +import { Extension } from '@tiptap/core'; +import Suggestion from '@tiptap/suggestion'; +import { PluginKey } from '@tiptap/pm/state'; + +export const CommandsPluginKey = new PluginKey('commands'); + +export interface CommandItem { + title: string; + description: string; + content: string; +} + +export const Commands = Extension.create({ + name: 'commands', + + addOptions() { + return { + suggestion: { + char: '@', + command: ({ editor, range, props }: any) => { + editor + .chain() + .focus() + .deleteRange(range) + .insertContent(props.content) + .run(); + }, + }, + }; + }, + + addProseMirrorPlugins() { + return [ + Suggestion({ + editor: this.editor, + ...this.options.suggestion, + }), + ]; + }, +}); \ No newline at end of file diff --git a/src/tiptap/extensions/suggestions/index.ts b/src/tiptap/extensions/suggestions/index.ts new file mode 100644 index 0000000..ed53886 --- /dev/null +++ b/src/tiptap/extensions/suggestions/index.ts @@ -0,0 +1,3 @@ +export * from './commands'; +export * from './suggestionConfig'; +export * from './suggestionItems'; \ No newline at end of file diff --git a/src/tiptap/extensions/suggestions/suggestionConfig.ts b/src/tiptap/extensions/suggestions/suggestionConfig.ts new file mode 100644 index 0000000..db097fb --- /dev/null +++ b/src/tiptap/extensions/suggestions/suggestionConfig.ts @@ -0,0 +1,121 @@ +import { CommandItem } from './commands'; +import { CommandsList } from '../../components/CommandsList'; +import ReactRenderer from '../../components/ReactRenderer'; + +export const createSuggestionConfig = (items: CommandItem[]) => { + return { + items: ({ query }: { query: string }) => { + return items.filter(item => item.title.toLowerCase().startsWith(query.toLowerCase())); + }, + render: () => { + let component: ReactRenderer | null = null; + let popup: HTMLElement | null = null; + + const calculatePosition = (view: any, from: number) => { + const coords = view.coordsAtPos(from); + const editorRect = view.dom.getBoundingClientRect(); + const popupRect = popup?.getBoundingClientRect(); + + if (!popup || !popupRect) return { left: coords.left, top: coords.bottom + 10 }; + + // Default position below the cursor + let left = coords.left; + let top = coords.bottom + 10; + + // Check if we're near the bottom of the viewport + const viewportHeight = window.innerHeight; + const bottomSpace = viewportHeight - coords.bottom; + const popupHeight = popupRect.height; + + // If there's not enough space below, position above + if (bottomSpace < popupHeight + 10 && coords.top > popupHeight + 10) { + top = coords.top - popupHeight - 10; + } + + // Check if we're near the right edge of the viewport + const viewportWidth = window.innerWidth; + const rightSpace = viewportWidth - coords.left; + const popupWidth = popupRect.width; + + // If there's not enough space to the right, align right edge + if (rightSpace < popupWidth) { + left = Math.max(10, viewportWidth - popupWidth - 10); + } + + // Ensure popup stays within editor bounds horizontally if possible + if (left < editorRect.left) { + left = editorRect.left; + } + + return { left, top }; + }; + + return { + onStart: (props: any) => { + component = new ReactRenderer(CommandsList, { + props, + editor: props.editor, + }); + + popup = document.createElement('div'); + popup.className = 'commands-popup'; + popup.style.position = 'fixed'; // Use fixed instead of absolute for better viewport positioning + popup.style.zIndex = '9999'; + document.body.appendChild(popup); + + popup.appendChild(component.element); + + // Initial position + const { view } = props.editor; + const { from } = props.range; + + // Set initial position to get popup dimensions + popup.style.left = '0px'; + popup.style.top = '0px'; + + // Calculate proper position after the popup is rendered + setTimeout(() => { + if (!popup) return; + const { left, top } = calculatePosition(view, from); + popup.style.left = `${left}px`; + popup.style.top = `${top}px`; + }, 0); + }, + onUpdate: (props: any) => { + if (!component) return; + + component.updateProps(props); + + if (!popup) return; + + // Update position + const { view } = props.editor; + const { from } = props.range; + const { left, top } = calculatePosition(view, from); + + popup.style.left = `${left}px`; + popup.style.top = `${top}px`; + }, + onKeyDown: (props: any) => { + if (props.event.key === 'Escape') { + if (popup) popup.remove(); + if (component) component.destroy(); + return true; + } + + if (component && component.ref && component.ref.current) { + return component.ref.current.onKeyDown(props); + } + + return false; + }, + onExit: () => { + if (popup) popup.remove(); + if (component) component.destroy(); + component = null; + popup = null; + }, + }; + }, + }; +}; \ No newline at end of file diff --git a/src/tiptap/extensions/suggestions/suggestionItems.ts b/src/tiptap/extensions/suggestions/suggestionItems.ts new file mode 100644 index 0000000..49c4871 --- /dev/null +++ b/src/tiptap/extensions/suggestions/suggestionItems.ts @@ -0,0 +1,203 @@ +import { CommandItem } from './commands'; + +export const getSuggestionItems = (): CommandItem[] => { + // Basic commands + const basicCommands = [ + { + title: 'today', + description: 'Insert today\'s date', + content: new Date().toLocaleDateString(), + }, + { + title: 'now', + description: 'Insert current time', + content: new Date().toLocaleTimeString(), + }, + { + title: 'datetime', + description: 'Insert current date and time', + content: new Date().toLocaleString(), + }, + { + title: 'list', + description: 'Insert a bullet list', + content: '', + }, + { + title: 'numbered', + description: 'Insert a numbered list', + content: '
  1. First item
  2. Second item
  3. Third item
', + }, + { + title: 'good', + description: 'Insert a positive message', + content: 'Great job! Keep up the good work! 👍', + }, + { + title: 'meeting', + description: 'Insert meeting template', + content: '

Meeting Notes

Date: ' + new Date().toLocaleDateString() + '

Attendees:

Agenda:

  1. Topic 1
  2. Topic 2

Action Items:

', + }, + { + title: 'signature', + description: 'Insert your signature', + content: '

Best regards,
Your Name
your.email@example.com

', + }, + ]; + + // Text formatting commands + const formattingCommands = [ + { + title: 'h1', + description: 'Insert heading 1', + content: '

Heading 1

', + }, + { + title: 'h2', + description: 'Insert heading 2', + content: '

Heading 2

', + }, + { + title: 'h3', + description: 'Insert heading 3', + content: '

Heading 3

', + }, + { + title: 'quote', + description: 'Insert blockquote', + content: '
This is a quote
', + }, + { + title: 'code', + description: 'Insert code block', + content: '
// Your code here\nconsole.log("Hello world");
', + }, + { + title: 'bold', + description: 'Insert bold text', + content: 'Bold text', + }, + { + title: 'italic', + description: 'Insert italic text', + content: 'Italic text', + }, + { + title: 'underline', + description: 'Insert underlined text', + content: 'Underlined text', + }, + { + title: 'strike', + description: 'Insert strikethrough text', + content: 'Strikethrough text', + }, + { + title: 'highlight', + description: 'Insert highlighted text', + content: 'Highlighted text', + }, + ]; + + // Template commands + const templateCommands = [ + { + title: 'email', + description: 'Insert email template', + content: '

Subject: [Your Subject]

Dear [Name],

I hope this email finds you well.

[Your message here]

Thank you for your time and consideration.

Best regards,
Your Name

', + }, + { + title: 'letter', + description: 'Insert formal letter template', + content: '

[Your Name]
[Your Address]
[City, State ZIP]
[Your Email]
[Your Phone]

[Date]

[Recipient Name]
[Recipient Title]
[Company Name]
[Street Address]
[City, State ZIP]

Dear [Recipient Name],

[Letter content]

Sincerely,

[Your Name]

', + }, + { + title: 'report', + description: 'Insert report template', + content: '

Report Title

Date: ' + new Date().toLocaleDateString() + '

Author: Your Name

Executive Summary

[Brief summary of the report]

Introduction

[Introduction text]

Findings

[Detailed findings]

Conclusion

[Conclusion text]

Recommendations

[Recommendations]

', + }, + { + title: 'proposal', + description: 'Insert proposal template', + content: '

Project Proposal

Date: ' + new Date().toLocaleDateString() + '

Prepared by: Your Name

Project Overview

[Brief description of the project]

Objectives

Scope of Work

[Detailed scope]

Timeline

[Project timeline]

Budget

[Budget details]

', + }, + { + title: 'invoice', + description: 'Insert invoice template', + content: '

INVOICE

Invoice #: [Number]

Date: ' + new Date().toLocaleDateString() + '

Due Date: [Due Date]

From:
[Your Name/Company]
[Your Address]
[Your Contact Info]
To:
[Client Name/Company]
[Client Address]
DescriptionAmount
[Item/Service Description][Amount]
Total[Total Amount]

Payment Terms: [Terms]

Payment Method: [Method]

', + }, + ]; + + // Task management commands + const taskCommands = [ + { + title: 'todo', + description: 'Insert todo list', + content: '

To-Do List

', + }, + { + title: 'checklist', + description: 'Insert checklist', + content: '

Checklist

', + }, + { + title: 'progress', + description: 'Insert progress tracker', + content: '

Project Progress

', + }, + { + title: 'timeline', + description: 'Insert project timeline', + content: '

Project Timeline

', + }, + { + title: 'goals', + description: 'Insert goals list', + content: '

Goals

  1. Short-term goal 1
  2. Short-term goal 2
  3. Long-term goal 1
  4. Long-term goal 2
', + }, + ]; + + // Table commands + const tableCommands = [ + { + title: 'table2x2', + description: 'Insert 2x2 table', + content: '
Header 1Header 2
Row 1, Cell 1Row 1, Cell 2
Row 2, Cell 1Row 2, Cell 2
', + }, + { + title: 'table3x3', + description: 'Insert 3x3 table', + content: '
Header 1Header 2Header 3
Row 1, Cell 1Row 1, Cell 2Row 1, Cell 3
Row 2, Cell 1Row 2, Cell 2Row 2, Cell 3
Row 3, Cell 1Row 3, Cell 2Row 3, Cell 3
', + }, + { + title: 'schedule', + description: 'Insert schedule table', + content: '
TimeMondayTuesdayWednesdayThursdayFriday
9:00 AM
10:00 AM
11:00 AM
', + }, + { + title: 'comparison', + description: 'Insert comparison table', + content: '
FeatureOption AOption BOption C
Feature 1
Feature 2
Feature 3
Price$$$$$$
', + }, + ]; + + // Additional commands to reach 100 total + const additionalCommands = Array.from({ length: 100 - (basicCommands.length + formattingCommands.length + templateCommands.length + taskCommands.length + tableCommands.length) }, (_, i) => { + const index = i + 1; + return { + title: `command${index}`, + description: `Example command ${index}`, + content: `

This is example command ${index}

`, + }; + }); + + // Combine all command categories + return [ + ...basicCommands, + ...formattingCommands, + ...templateCommands, + ...taskCommands, + ...tableCommands, + ...additionalCommands, + ]; +}; \ No newline at end of file diff --git a/src/tiptap/extensions/suggestions/suggestions.ts b/src/tiptap/extensions/suggestions/suggestions.ts new file mode 100644 index 0000000..49c4871 --- /dev/null +++ b/src/tiptap/extensions/suggestions/suggestions.ts @@ -0,0 +1,203 @@ +import { CommandItem } from './commands'; + +export const getSuggestionItems = (): CommandItem[] => { + // Basic commands + const basicCommands = [ + { + title: 'today', + description: 'Insert today\'s date', + content: new Date().toLocaleDateString(), + }, + { + title: 'now', + description: 'Insert current time', + content: new Date().toLocaleTimeString(), + }, + { + title: 'datetime', + description: 'Insert current date and time', + content: new Date().toLocaleString(), + }, + { + title: 'list', + description: 'Insert a bullet list', + content: '', + }, + { + title: 'numbered', + description: 'Insert a numbered list', + content: '
  1. First item
  2. Second item
  3. Third item
', + }, + { + title: 'good', + description: 'Insert a positive message', + content: 'Great job! Keep up the good work! 👍', + }, + { + title: 'meeting', + description: 'Insert meeting template', + content: '

Meeting Notes

Date: ' + new Date().toLocaleDateString() + '

Attendees:

Agenda:

  1. Topic 1
  2. Topic 2

Action Items:

', + }, + { + title: 'signature', + description: 'Insert your signature', + content: '

Best regards,
Your Name
your.email@example.com

', + }, + ]; + + // Text formatting commands + const formattingCommands = [ + { + title: 'h1', + description: 'Insert heading 1', + content: '

Heading 1

', + }, + { + title: 'h2', + description: 'Insert heading 2', + content: '

Heading 2

', + }, + { + title: 'h3', + description: 'Insert heading 3', + content: '

Heading 3

', + }, + { + title: 'quote', + description: 'Insert blockquote', + content: '
This is a quote
', + }, + { + title: 'code', + description: 'Insert code block', + content: '
// Your code here\nconsole.log("Hello world");
', + }, + { + title: 'bold', + description: 'Insert bold text', + content: 'Bold text', + }, + { + title: 'italic', + description: 'Insert italic text', + content: 'Italic text', + }, + { + title: 'underline', + description: 'Insert underlined text', + content: 'Underlined text', + }, + { + title: 'strike', + description: 'Insert strikethrough text', + content: 'Strikethrough text', + }, + { + title: 'highlight', + description: 'Insert highlighted text', + content: 'Highlighted text', + }, + ]; + + // Template commands + const templateCommands = [ + { + title: 'email', + description: 'Insert email template', + content: '

Subject: [Your Subject]

Dear [Name],

I hope this email finds you well.

[Your message here]

Thank you for your time and consideration.

Best regards,
Your Name

', + }, + { + title: 'letter', + description: 'Insert formal letter template', + content: '

[Your Name]
[Your Address]
[City, State ZIP]
[Your Email]
[Your Phone]

[Date]

[Recipient Name]
[Recipient Title]
[Company Name]
[Street Address]
[City, State ZIP]

Dear [Recipient Name],

[Letter content]

Sincerely,

[Your Name]

', + }, + { + title: 'report', + description: 'Insert report template', + content: '

Report Title

Date: ' + new Date().toLocaleDateString() + '

Author: Your Name

Executive Summary

[Brief summary of the report]

Introduction

[Introduction text]

Findings

[Detailed findings]

Conclusion

[Conclusion text]

Recommendations

[Recommendations]

', + }, + { + title: 'proposal', + description: 'Insert proposal template', + content: '

Project Proposal

Date: ' + new Date().toLocaleDateString() + '

Prepared by: Your Name

Project Overview

[Brief description of the project]

Objectives

Scope of Work

[Detailed scope]

Timeline

[Project timeline]

Budget

[Budget details]

', + }, + { + title: 'invoice', + description: 'Insert invoice template', + content: '

INVOICE

Invoice #: [Number]

Date: ' + new Date().toLocaleDateString() + '

Due Date: [Due Date]

From:
[Your Name/Company]
[Your Address]
[Your Contact Info]
To:
[Client Name/Company]
[Client Address]
DescriptionAmount
[Item/Service Description][Amount]
Total[Total Amount]

Payment Terms: [Terms]

Payment Method: [Method]

', + }, + ]; + + // Task management commands + const taskCommands = [ + { + title: 'todo', + description: 'Insert todo list', + content: '

To-Do List

', + }, + { + title: 'checklist', + description: 'Insert checklist', + content: '

Checklist

', + }, + { + title: 'progress', + description: 'Insert progress tracker', + content: '

Project Progress

', + }, + { + title: 'timeline', + description: 'Insert project timeline', + content: '

Project Timeline

', + }, + { + title: 'goals', + description: 'Insert goals list', + content: '

Goals

  1. Short-term goal 1
  2. Short-term goal 2
  3. Long-term goal 1
  4. Long-term goal 2
', + }, + ]; + + // Table commands + const tableCommands = [ + { + title: 'table2x2', + description: 'Insert 2x2 table', + content: '
Header 1Header 2
Row 1, Cell 1Row 1, Cell 2
Row 2, Cell 1Row 2, Cell 2
', + }, + { + title: 'table3x3', + description: 'Insert 3x3 table', + content: '
Header 1Header 2Header 3
Row 1, Cell 1Row 1, Cell 2Row 1, Cell 3
Row 2, Cell 1Row 2, Cell 2Row 2, Cell 3
Row 3, Cell 1Row 3, Cell 2Row 3, Cell 3
', + }, + { + title: 'schedule', + description: 'Insert schedule table', + content: '
TimeMondayTuesdayWednesdayThursdayFriday
9:00 AM
10:00 AM
11:00 AM
', + }, + { + title: 'comparison', + description: 'Insert comparison table', + content: '
FeatureOption AOption BOption C
Feature 1
Feature 2
Feature 3
Price$$$$$$
', + }, + ]; + + // Additional commands to reach 100 total + const additionalCommands = Array.from({ length: 100 - (basicCommands.length + formattingCommands.length + templateCommands.length + taskCommands.length + tableCommands.length) }, (_, i) => { + const index = i + 1; + return { + title: `command${index}`, + description: `Example command ${index}`, + content: `

This is example command ${index}

`, + }; + }); + + // Combine all command categories + return [ + ...basicCommands, + ...formattingCommands, + ...templateCommands, + ...taskCommands, + ...tableCommands, + ...additionalCommands, + ]; +}; \ No newline at end of file diff --git a/packages/.gitkeep b/src/tiptap/index.ts similarity index 100% rename from packages/.gitkeep rename to src/tiptap/index.ts diff --git a/src/tiptap/tiptap.css b/src/tiptap/tiptap.css new file mode 100644 index 0000000..d1ff8a2 --- /dev/null +++ b/src/tiptap/tiptap.css @@ -0,0 +1,123 @@ +:root { + --purple-light: #e0e0ff; /* 默认浅紫色背景 */ + --black: #000000; /* 默认黑色 */ + --white: #ffffff; /* 默认白色 */ + --gray-3: #d3d3d3; /* 默认灰色3 */ + --gray-2: #e5e5e5; /* 默认灰色2 */ +} +.tiptap-preview { + .tiptap { + margin: 0; + padding: 0.5rem; + border: unset; + } +} +.tiptap { + /* margin: 0.5rem 1rem; */ + margin: 0; + padding: 0.5rem; + border-radius: 5px; + border: 1px solid #ccc; +} +/* Basic editor styles */ +.tiptap:first-child { + margin-top: 0; +} + +/* List styles */ +.tiptap ul, +.tiptap ol { + padding: 0 1rem; + margin: 1.25rem 1rem 1.25rem 0.4rem; +} + +.tiptap ul li p, +.tiptap ol li p { + margin-top: 0.25em; + margin-bottom: 0.25em; +} + +/* Heading styles */ +.tiptap h1, +.tiptap h2, +.tiptap h3, +.tiptap h4, +.tiptap h5, +.tiptap h6 { + line-height: 1.1; + margin-top: 2.5rem; + text-wrap: pretty; +} + +.tiptap h1, +.tiptap h2 { + /* margin-top: 3.5rem; */ + margin-top: 1rem; + margin-bottom: 0.5rem; +} + +.tiptap h1 { + font-size: 1.4rem; + font-weight: 800; +} + +.tiptap h2 { + font-size: 1.2rem; + font-weight: 600; +} + +.tiptap h3 { + font-size: 1.1rem; + font-weight: 500; +} + +.tiptap h4, +.tiptap h5, +.tiptap h6 { + font-size: 1rem; +} + +/* Code and preformatted text styles */ +.tiptap code { + background-color: var(--purple-light); + border-radius: 0.4rem; + color: var(--black); + font-size: 0.85rem; + padding: 0.25em 0.3em; +} + +.tiptap pre { + border: 1px solid #ccc; + /* background: var(--black); */ + border-radius: 0.5rem; + /* color: var(--white); */ + font-family: 'JetBrainsMono', monospace; + margin: 1.5rem 0; + padding: 0.75rem 1rem; +} + +.tiptap pre code { + background: none; + color: inherit; + font-size: 0.8rem; + padding: 0; +} + +.tiptap mark { + background-color: #faf594; + border-radius: 0.4rem; + box-decoration-break: clone; + padding: 0.1rem 0.3rem; +} + +.tiptap blockquote { + border-left: 3px solid var(--gray-3); + margin: 1.5rem 0; + padding-left: 1rem; +} + +.tiptap hr { + border: none; + border-top: 1px solid var(--gray-2); + margin: 2rem 0; +} diff --git a/submodules/.gitkeep b/submodules/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/turbo.json b/turbo.json deleted file mode 100644 index 2163d21..0000000 --- a/turbo.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://turbo.build/schema.json", - "tasks": { - "build": { - "dependsOn": [ - "^build" - ], - "outputs": [ - "dist/**" - ] - }, - "dev:lib": { - "persistent": true, - "cache": true - }, - "build:lib": { - "dependsOn": [ - "^build:lib" - ] - } - } -} \ No newline at end of file