init markdown

This commit is contained in:
xion 2025-04-05 20:33:44 +08:00
parent 3600defda1
commit f724074b20
16 changed files with 1051 additions and 48 deletions

View File

@ -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 <xiongxiao@xiongxiao.me>",
"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"
}

View File

@ -1,3 +0,0 @@
packages:
- 'submodules/*'
- 'packages/*'

View File

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

View File

@ -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;
}

View File

@ -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 (
<div className="bg-white rounded-md shadow-lg border border-gray-200 overflow-hidden" style={{ width: '350px', maxHeight: '80vh' }}>
<div className="p-2 bg-gray-50 border-b border-gray-200 sticky top-0 z-10">
<div className="text-sm font-medium text-gray-700">Commands ({props.items.length})</div>
<div className="text-xs text-gray-500">Type to filter commands</div>
</div>
<div className="max-h-72 overflow-y-auto">
{props.items.length ? (
props.items.map((item, index) => (
<button
id={`command-item-${index}`}
key={index}
className={`block w-full text-left px-4 py-2 text-sm transition-colors ${
index === selectedIndex ? 'bg-blue-100 border-l-4 border-blue-500' : 'border-l-4 border-transparent'
} hover:bg-gray-50`}
onClick={() => selectItem(index)}
>
<div className="font-medium flex items-center">
!{item.title}
{index === selectedIndex && (
<span className="ml-2 text-xs bg-blue-500 text-white px-2 py-0.5 rounded">
Press Enter to select
</span>
)}
</div>
<div className="text-gray-500 text-xs">{item.description}</div>
</button>
))
) : (
<div className="px-4 py-2 text-sm text-gray-500">No results</div>
)}
</div>
<div className="bg-gray-50 px-3 py-2 text-xs text-gray-500 border-t flex justify-between items-center sticky bottom-0 z-10">
<span className="inline-flex items-center">
<kbd className="px-2 py-1 bg-white rounded border border-gray-300 shadow-sm mr-1"></kbd>
<kbd className="px-2 py-1 bg-white rounded border border-gray-300 shadow-sm"></kbd>
<span className="ml-1">to navigate</span>
</span>
<span className="inline-flex items-center">
<kbd className="px-2 py-1 bg-white rounded border border-gray-300 shadow-sm">Enter</kbd>
<span className="ml-1">to select</span>
</span>
</div>
</div>
);
});
CommandsList.displayName = 'CommandsList';

View File

@ -0,0 +1,42 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
export class ReactRenderer {
component: any;
element: HTMLElement;
ref: React.RefObject<any>;
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;

136
src/tiptap/editor.ts Normal file
View File

@ -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;
}
}

View File

@ -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,
}),
];
},
});

View File

@ -0,0 +1,3 @@
export * from './commands';
export * from './suggestionConfig';
export * from './suggestionItems';

View File

@ -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;
},
};
},
};
};

View File

@ -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: '<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>',
},
{
title: 'numbered',
description: 'Insert a numbered list',
content: '<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>',
},
{
title: 'good',
description: 'Insert a positive message',
content: 'Great job! Keep up the good work! 👍',
},
{
title: 'meeting',
description: 'Insert meeting template',
content: '<h3>Meeting Notes</h3><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Attendees:</strong></p><ul><li>Person 1</li><li>Person 2</li></ul><p><strong>Agenda:</strong></p><ol><li>Topic 1</li><li>Topic 2</li></ol><p><strong>Action Items:</strong></p><ul><li>[ ] Task 1</li><li>[ ] Task 2</li></ul>',
},
{
title: 'signature',
description: 'Insert your signature',
content: '<p>Best regards,<br>Your Name<br>your.email@example.com</p>',
},
];
// Text formatting commands
const formattingCommands = [
{
title: 'h1',
description: 'Insert heading 1',
content: '<h1>Heading 1</h1>',
},
{
title: 'h2',
description: 'Insert heading 2',
content: '<h2>Heading 2</h2>',
},
{
title: 'h3',
description: 'Insert heading 3',
content: '<h3>Heading 3</h3>',
},
{
title: 'quote',
description: 'Insert blockquote',
content: '<blockquote>This is a quote</blockquote>',
},
{
title: 'code',
description: 'Insert code block',
content: '<pre><code>// Your code here\nconsole.log("Hello world");</code></pre>',
},
{
title: 'bold',
description: 'Insert bold text',
content: '<strong>Bold text</strong>',
},
{
title: 'italic',
description: 'Insert italic text',
content: '<em>Italic text</em>',
},
{
title: 'underline',
description: 'Insert underlined text',
content: '<u>Underlined text</u>',
},
{
title: 'strike',
description: 'Insert strikethrough text',
content: '<s>Strikethrough text</s>',
},
{
title: 'highlight',
description: 'Insert highlighted text',
content: '<mark>Highlighted text</mark>',
},
];
// Template commands
const templateCommands = [
{
title: 'email',
description: 'Insert email template',
content: '<p>Subject: [Your Subject]</p><p>Dear [Name],</p><p>I hope this email finds you well.</p><p>[Your message here]</p><p>Thank you for your time and consideration.</p><p>Best regards,<br>Your Name</p>',
},
{
title: 'letter',
description: 'Insert formal letter template',
content: '<p>[Your Name]<br>[Your Address]<br>[City, State ZIP]<br>[Your Email]<br>[Your Phone]</p><p>[Date]</p><p>[Recipient Name]<br>[Recipient Title]<br>[Company Name]<br>[Street Address]<br>[City, State ZIP]</p><p>Dear [Recipient Name],</p><p>[Letter content]</p><p>Sincerely,</p><p>[Your Name]</p>',
},
{
title: 'report',
description: 'Insert report template',
content: '<h1>Report Title</h1><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Author:</strong> Your Name</p><h2>Executive Summary</h2><p>[Brief summary of the report]</p><h2>Introduction</h2><p>[Introduction text]</p><h2>Findings</h2><p>[Detailed findings]</p><h2>Conclusion</h2><p>[Conclusion text]</p><h2>Recommendations</h2><p>[Recommendations]</p>',
},
{
title: 'proposal',
description: 'Insert proposal template',
content: '<h1>Project Proposal</h1><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Prepared by:</strong> Your Name</p><h2>Project Overview</h2><p>[Brief description of the project]</p><h2>Objectives</h2><ul><li>[Objective 1]</li><li>[Objective 2]</li></ul><h2>Scope of Work</h2><p>[Detailed scope]</p><h2>Timeline</h2><p>[Project timeline]</p><h2>Budget</h2><p>[Budget details]</p>',
},
{
title: 'invoice',
description: 'Insert invoice template',
content: '<h1>INVOICE</h1><p><strong>Invoice #:</strong> [Number]</p><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Due Date:</strong> [Due Date]</p><div><strong>From:</strong><br>[Your Name/Company]<br>[Your Address]<br>[Your Contact Info]</div><div><strong>To:</strong><br>[Client Name/Company]<br>[Client Address]</div><table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="text-align:left; padding: 8px;">Description</th><th style="text-align:right; padding: 8px;">Amount</th></tr><tr style="border-bottom: 1px solid #ddd;"><td style="padding: 8px;">[Item/Service Description]</td><td style="text-align:right; padding: 8px;">[Amount]</td></tr><tr><td style="text-align:right; padding: 8px;"><strong>Total</strong></td><td style="text-align:right; padding: 8px;"><strong>[Total Amount]</strong></td></tr></table><p><strong>Payment Terms:</strong> [Terms]</p><p><strong>Payment Method:</strong> [Method]</p>',
},
];
// Task management commands
const taskCommands = [
{
title: 'todo',
description: 'Insert todo list',
content: '<h3>To-Do List</h3><ul><li>[ ] Task 1</li><li>[ ] Task 2</li><li>[ ] Task 3</li></ul>',
},
{
title: 'checklist',
description: 'Insert checklist',
content: '<h3>Checklist</h3><ul><li>[ ] Item 1</li><li>[ ] Item 2</li><li>[ ] Item 3</li></ul>',
},
{
title: 'progress',
description: 'Insert progress tracker',
content: '<h3>Project Progress</h3><ul><li>[x] Planning - Complete</li><li>[x] Research - Complete</li><li>[ ] Implementation - In Progress</li><li>[ ] Testing</li><li>[ ] Deployment</li></ul>',
},
{
title: 'timeline',
description: 'Insert project timeline',
content: '<h3>Project Timeline</h3><ul><li><strong>Week 1:</strong> Planning and Research</li><li><strong>Week 2-3:</strong> Design and Development</li><li><strong>Week 4:</strong> Testing</li><li><strong>Week 5:</strong> Deployment</li></ul>',
},
{
title: 'goals',
description: 'Insert goals list',
content: '<h3>Goals</h3><ol><li>Short-term goal 1</li><li>Short-term goal 2</li><li>Long-term goal 1</li><li>Long-term goal 2</li></ol>',
},
];
// Table commands
const tableCommands = [
{
title: 'table2x2',
description: 'Insert 2x2 table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Header 1</th><th style="border: 1px solid #ddd; padding: 8px;">Header 2</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 2</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 2</td></tr></table>',
},
{
title: 'table3x3',
description: 'Insert 3x3 table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Header 1</th><th style="border: 1px solid #ddd; padding: 8px;">Header 2</th><th style="border: 1px solid #ddd; padding: 8px;">Header 3</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 3</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 3</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 3</td></tr></table>',
},
{
title: 'schedule',
description: 'Insert schedule table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Time</th><th style="border: 1px solid #ddd; padding: 8px;">Monday</th><th style="border: 1px solid #ddd; padding: 8px;">Tuesday</th><th style="border: 1px solid #ddd; padding: 8px;">Wednesday</th><th style="border: 1px solid #ddd; padding: 8px;">Thursday</th><th style="border: 1px solid #ddd; padding: 8px;">Friday</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">9:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">10:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">11:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr></table>',
},
{
title: 'comparison',
description: 'Insert comparison table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Feature</th><th style="border: 1px solid #ddd; padding: 8px;">Option A</th><th style="border: 1px solid #ddd; padding: 8px;">Option B</th><th style="border: 1px solid #ddd; padding: 8px;">Option C</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 1</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 2</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✗</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 3</td><td style="border: 1px solid #ddd; padding: 8px;">✗</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Price</td><td style="border: 1px solid #ddd; padding: 8px;">$</td><td style="border: 1px solid #ddd; padding: 8px;">$$</td><td style="border: 1px solid #ddd; padding: 8px;">$$$</td></tr></table>',
},
];
// 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: `<p>This is example command ${index}</p>`,
};
});
// Combine all command categories
return [
...basicCommands,
...formattingCommands,
...templateCommands,
...taskCommands,
...tableCommands,
...additionalCommands,
];
};

View File

@ -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: '<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>',
},
{
title: 'numbered',
description: 'Insert a numbered list',
content: '<ol><li>First item</li><li>Second item</li><li>Third item</li></ol>',
},
{
title: 'good',
description: 'Insert a positive message',
content: 'Great job! Keep up the good work! 👍',
},
{
title: 'meeting',
description: 'Insert meeting template',
content: '<h3>Meeting Notes</h3><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Attendees:</strong></p><ul><li>Person 1</li><li>Person 2</li></ul><p><strong>Agenda:</strong></p><ol><li>Topic 1</li><li>Topic 2</li></ol><p><strong>Action Items:</strong></p><ul><li>[ ] Task 1</li><li>[ ] Task 2</li></ul>',
},
{
title: 'signature',
description: 'Insert your signature',
content: '<p>Best regards,<br>Your Name<br>your.email@example.com</p>',
},
];
// Text formatting commands
const formattingCommands = [
{
title: 'h1',
description: 'Insert heading 1',
content: '<h1>Heading 1</h1>',
},
{
title: 'h2',
description: 'Insert heading 2',
content: '<h2>Heading 2</h2>',
},
{
title: 'h3',
description: 'Insert heading 3',
content: '<h3>Heading 3</h3>',
},
{
title: 'quote',
description: 'Insert blockquote',
content: '<blockquote>This is a quote</blockquote>',
},
{
title: 'code',
description: 'Insert code block',
content: '<pre><code>// Your code here\nconsole.log("Hello world");</code></pre>',
},
{
title: 'bold',
description: 'Insert bold text',
content: '<strong>Bold text</strong>',
},
{
title: 'italic',
description: 'Insert italic text',
content: '<em>Italic text</em>',
},
{
title: 'underline',
description: 'Insert underlined text',
content: '<u>Underlined text</u>',
},
{
title: 'strike',
description: 'Insert strikethrough text',
content: '<s>Strikethrough text</s>',
},
{
title: 'highlight',
description: 'Insert highlighted text',
content: '<mark>Highlighted text</mark>',
},
];
// Template commands
const templateCommands = [
{
title: 'email',
description: 'Insert email template',
content: '<p>Subject: [Your Subject]</p><p>Dear [Name],</p><p>I hope this email finds you well.</p><p>[Your message here]</p><p>Thank you for your time and consideration.</p><p>Best regards,<br>Your Name</p>',
},
{
title: 'letter',
description: 'Insert formal letter template',
content: '<p>[Your Name]<br>[Your Address]<br>[City, State ZIP]<br>[Your Email]<br>[Your Phone]</p><p>[Date]</p><p>[Recipient Name]<br>[Recipient Title]<br>[Company Name]<br>[Street Address]<br>[City, State ZIP]</p><p>Dear [Recipient Name],</p><p>[Letter content]</p><p>Sincerely,</p><p>[Your Name]</p>',
},
{
title: 'report',
description: 'Insert report template',
content: '<h1>Report Title</h1><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Author:</strong> Your Name</p><h2>Executive Summary</h2><p>[Brief summary of the report]</p><h2>Introduction</h2><p>[Introduction text]</p><h2>Findings</h2><p>[Detailed findings]</p><h2>Conclusion</h2><p>[Conclusion text]</p><h2>Recommendations</h2><p>[Recommendations]</p>',
},
{
title: 'proposal',
description: 'Insert proposal template',
content: '<h1>Project Proposal</h1><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Prepared by:</strong> Your Name</p><h2>Project Overview</h2><p>[Brief description of the project]</p><h2>Objectives</h2><ul><li>[Objective 1]</li><li>[Objective 2]</li></ul><h2>Scope of Work</h2><p>[Detailed scope]</p><h2>Timeline</h2><p>[Project timeline]</p><h2>Budget</h2><p>[Budget details]</p>',
},
{
title: 'invoice',
description: 'Insert invoice template',
content: '<h1>INVOICE</h1><p><strong>Invoice #:</strong> [Number]</p><p><strong>Date:</strong> ' + new Date().toLocaleDateString() + '</p><p><strong>Due Date:</strong> [Due Date]</p><div><strong>From:</strong><br>[Your Name/Company]<br>[Your Address]<br>[Your Contact Info]</div><div><strong>To:</strong><br>[Client Name/Company]<br>[Client Address]</div><table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="text-align:left; padding: 8px;">Description</th><th style="text-align:right; padding: 8px;">Amount</th></tr><tr style="border-bottom: 1px solid #ddd;"><td style="padding: 8px;">[Item/Service Description]</td><td style="text-align:right; padding: 8px;">[Amount]</td></tr><tr><td style="text-align:right; padding: 8px;"><strong>Total</strong></td><td style="text-align:right; padding: 8px;"><strong>[Total Amount]</strong></td></tr></table><p><strong>Payment Terms:</strong> [Terms]</p><p><strong>Payment Method:</strong> [Method]</p>',
},
];
// Task management commands
const taskCommands = [
{
title: 'todo',
description: 'Insert todo list',
content: '<h3>To-Do List</h3><ul><li>[ ] Task 1</li><li>[ ] Task 2</li><li>[ ] Task 3</li></ul>',
},
{
title: 'checklist',
description: 'Insert checklist',
content: '<h3>Checklist</h3><ul><li>[ ] Item 1</li><li>[ ] Item 2</li><li>[ ] Item 3</li></ul>',
},
{
title: 'progress',
description: 'Insert progress tracker',
content: '<h3>Project Progress</h3><ul><li>[x] Planning - Complete</li><li>[x] Research - Complete</li><li>[ ] Implementation - In Progress</li><li>[ ] Testing</li><li>[ ] Deployment</li></ul>',
},
{
title: 'timeline',
description: 'Insert project timeline',
content: '<h3>Project Timeline</h3><ul><li><strong>Week 1:</strong> Planning and Research</li><li><strong>Week 2-3:</strong> Design and Development</li><li><strong>Week 4:</strong> Testing</li><li><strong>Week 5:</strong> Deployment</li></ul>',
},
{
title: 'goals',
description: 'Insert goals list',
content: '<h3>Goals</h3><ol><li>Short-term goal 1</li><li>Short-term goal 2</li><li>Long-term goal 1</li><li>Long-term goal 2</li></ol>',
},
];
// Table commands
const tableCommands = [
{
title: 'table2x2',
description: 'Insert 2x2 table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Header 1</th><th style="border: 1px solid #ddd; padding: 8px;">Header 2</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 2</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 2</td></tr></table>',
},
{
title: 'table3x3',
description: 'Insert 3x3 table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Header 1</th><th style="border: 1px solid #ddd; padding: 8px;">Header 2</th><th style="border: 1px solid #ddd; padding: 8px;">Header 3</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 1, Cell 3</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 2, Cell 3</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 1</td><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 2</td><td style="border: 1px solid #ddd; padding: 8px;">Row 3, Cell 3</td></tr></table>',
},
{
title: 'schedule',
description: 'Insert schedule table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Time</th><th style="border: 1px solid #ddd; padding: 8px;">Monday</th><th style="border: 1px solid #ddd; padding: 8px;">Tuesday</th><th style="border: 1px solid #ddd; padding: 8px;">Wednesday</th><th style="border: 1px solid #ddd; padding: 8px;">Thursday</th><th style="border: 1px solid #ddd; padding: 8px;">Friday</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">9:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">10:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">11:00 AM</td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td><td style="border: 1px solid #ddd; padding: 8px;"></td></tr></table>',
},
{
title: 'comparison',
description: 'Insert comparison table',
content: '<table style="width:100%; border-collapse: collapse;"><tr style="border-bottom: 1px solid #ddd;"><th style="border: 1px solid #ddd; padding: 8px;">Feature</th><th style="border: 1px solid #ddd; padding: 8px;">Option A</th><th style="border: 1px solid #ddd; padding: 8px;">Option B</th><th style="border: 1px solid #ddd; padding: 8px;">Option C</th></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 1</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 2</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✗</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Feature 3</td><td style="border: 1px solid #ddd; padding: 8px;">✗</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td><td style="border: 1px solid #ddd; padding: 8px;">✓</td></tr><tr><td style="border: 1px solid #ddd; padding: 8px;">Price</td><td style="border: 1px solid #ddd; padding: 8px;">$</td><td style="border: 1px solid #ddd; padding: 8px;">$$</td><td style="border: 1px solid #ddd; padding: 8px;">$$$</td></tr></table>',
},
];
// 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: `<p>This is example command ${index}</p>`,
};
});
// Combine all command categories
return [
...basicCommands,
...formattingCommands,
...templateCommands,
...taskCommands,
...tableCommands,
...additionalCommands,
];
};

123
src/tiptap/tiptap.css Normal file
View File

@ -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;
}

View File

View File

@ -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"
]
}
}
}