Compare commits

...

10 Commits

Author SHA1 Message Date
3ddabb8a74 Merge branch 'master' of https://gitea.uniccoo.com/openSourceRemote/mind-map 2025-09-17 11:51:43 +08:00
093ebcb8b5 no message 2025-09-17 11:47:24 +08:00
9febf0777e no message 2025-07-10 16:50:07 +08:00
78823734da no message 2025-07-10 16:49:59 +08:00
bb1b91b6a6 no message 2025-07-10 16:09:10 +08:00
966cb55c25 no message 2025-07-10 15:56:27 +08:00
fd19e680f8 no message 2025-07-10 15:55:09 +08:00
5c042b5d53 no message 2025-07-10 15:41:31 +08:00
00d512860d no message 2025-07-10 15:25:17 +08:00
63442cb50e no message 2025-07-10 15:15:19 +08:00
22 changed files with 2158 additions and 50 deletions

View File

@ -18,7 +18,7 @@
},
"editor.tabSize": 2,
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.fixAll.eslint": true
"source.fixAll": "explicit",
"source.fixAll.eslint": "explicit"
}
}

View File

@ -10,6 +10,7 @@
},
"dependencies": {
"@mind-map/component": "workspace:*",
"@tiptap/component": "workspace:*",
"autoprefixer": "^10.4.14",
"pinia": "^2.1.6",
"postcss": "^8.4.27",

View File

@ -1,19 +1,29 @@
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHistory } from "vue-router";
import type { RouteRecordRaw } from "vue-router";
// 路由配置
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
path: "/",
name: "Home",
component: () => import("../views/Home.vue"),
},
]
{
path: "/mind-map",
name: "mindMap",
component: () => import("../views/mindMap/index.vue"),
},
{
path: "/tiptap",
name: "tiptap",
component: () => import("../views/tiptap/index.vue"),
},
];
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
routes,
});
export default router
export default router;

View File

@ -1,43 +1,110 @@
<template>
<div class="h-screen p-6 bg-gradient-to-br from-gray-50 to-gray-100">
<MindMap @onDel="onDel" @onAddChild="onAddChild" @onEdit="onEdit" />
<div class="home-container">
<div class="title">欢迎使用思维导图工具</div>
<div class="nav-buttons">
<button @click="goToMindMap" class="nav-btn mind-map-btn">
思维导图
</button>
<button @click="goToTiptap" class="nav-btn tiptap-btn">
富文本编辑器 (Tiptap)
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import { MindMap, useMindMapStore } from "@mind-map/component";
import type { MindMapNode } from "@mind-map/component";
import { useRouter } from "vue-router";
onMounted(() => {
console.log("Home view mounted");
});
const router = useRouter();
const { removeNode, insertChildNode } = useMindMapStore();
function onDel(node: MindMapNode) {
removeNode({
beforeRemoveCallback: async () => {
return true;
},
nodeId: node.uid,
allowRemoveWithChildren: false,
});
//
function goToMindMap() {
router.push({ name: "mindMap" });
}
function onAddChild(node: MindMapNode) {
insertChildNode({
parentNodeId: node.uid,
beforeInsertCallback: async () => {
return {
uid: "node.uid",
text: "node.text",
};
},
});
}
function onEdit(node: MindMapNode) {
console.log("Node edited:", node);
// Tiptap
function goToTiptap() {
router.push({ name: "tiptap" });
}
</script>
<style scoped>
.home-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
}
.title {
font-size: 2.5rem;
font-weight: bold;
color: white;
margin-bottom: 3rem;
text-align: center;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.nav-buttons {
display: flex;
gap: 2rem;
flex-wrap: wrap;
justify-content: center;
}
.nav-btn {
padding: 1rem 2rem;
font-size: 1.2rem;
font-weight: 600;
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
min-width: 200px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.mind-map-btn {
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
color: #333;
}
.mind-map-btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(255, 154, 158, 0.4);
}
.tiptap-btn {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
color: #333;
}
.tiptap-btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(168, 237, 234, 0.4);
}
.nav-btn:active {
transform: translateY(-1px);
}
@media (max-width: 768px) {
.nav-buttons {
flex-direction: column;
align-items: center;
}
.title {
font-size: 2rem;
}
.nav-btn {
min-width: 250px;
}
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<div class="h-screen p-6 bg-gradient-to-br from-gray-50 to-gray-100">
<MindMap @onDel="onDel" @onAddChild="onAddChild" @onEdit="onEdit" />
</div>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import { MindMap, useMindMapStore } from "@mind-map/component";
import type { MindMapNode } from "@mind-map/component";
onMounted(() => {
console.log("Home view mounted");
});
const { removeNode, insertChildNode } = useMindMapStore();
function onDel(node: MindMapNode) {
removeNode({
beforeRemoveCallback: async () => {
return true;
},
nodeId: node.uid,
allowRemoveWithChildren: false,
});
}
function onAddChild(node: MindMapNode) {
insertChildNode({
parentNodeId: node.uid,
beforeInsertCallback: async () => {
return {
uid: "node.uid",
text: "node.text",
};
},
});
}
function onEdit(node: MindMapNode) {
console.log("Node edited:", node);
}
</script>

View File

@ -0,0 +1,23 @@
<template>
<div>
<TiptapEditor />
</div>
</template>
<script lang="ts" setup>
import { TiptapEditor, DEMO_CONTENT, useTiptapStore } from "@tiptap/component";
import { onMounted } from "vue";
useTiptapStore({
content: DEMO_CONTENT,
minimal: false,
hideToolbar: false,
hideMenubar: false,
disabled: false,
theme: "light",
});
onMounted(() => {
// useTiptapStore();
});
</script>

View File

@ -1,18 +1,16 @@
// import { App } from 'vue';
// import pinia from './store';
import MindMap from './views/index.vue';
import MindMap from "./views/index.vue";
import type MindMapNode from "simple-mind-map/types/src/core/render/node/MindMapNode";
export { MindMap };
export * from './store';
export * from "./store";
export type { MindMapNode };
// export function installMindMap(app: App) {
// app.use(pinia);
// }

View File

@ -0,0 +1,40 @@
{
"name": "@tiptap/component",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"build:no-check": "vite build",
"type-check": "vue-tsc",
"preview": "vite preview"
},
"dependencies": {
"@tiptap/core": "^2.25.0",
"@types/file-saver": "^2.0.7",
"docx": "^9.5.1",
"echo-editor": "^0.4.8",
"file-saver": "^2.0.5",
"openai": "^5.8.3",
"prosemirror-docx": "latest",
"vite-svg-loader": "latest",
"vue": "^3.3.4"
},
"main": "src/index.ts",
"module": "src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"autoprefixer": "^10.4.14",
"ts-node": "^10.9.2",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vue-tsc": "^2.2.10"
}
}

View File

@ -0,0 +1,47 @@
<template>
<div class="flex items-center justify-between">
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors border rounded-md hover:bg-accent hover:text-accent-foreground"
@click="locale.setLang('zhHans')"
>
中文
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors border rounded-md hover:bg-accent hover:text-accent-foreground"
@click="locale.setLang('en')"
>
English
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors border rounded-md hover:bg-accent hover:text-accent-foreground"
@click="state.minimal = !state.minimal"
>
{{ state.minimal ? "Full" : "Minimal" }}
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors border rounded-md hover:bg-accent hover:text-accent-foreground"
@click="state.hideToolbar = !state.hideToolbar"
>
{{ !state.hideToolbar ? "Hide Toolbar" : "Show Toolbar" }}
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors border rounded-md hover:bg-accent hover:text-accent-foreground"
@click="state.hideMenubar = !state.hideMenubar"
>
{{ !state.hideMenubar ? "Hide Menubar" : "Show Menubar" }}
</button>
<button
class="inline-flex items-center justify-center px-3 py-2 text-sm transition-colors border rounded-md hover:bg-accent hover:text-accent-foreground"
@click="state.disabled = !state.disabled"
>
{{ state.disabled ? "Editable" : "Readonly" }}
</button>
</div>
</template>
<script setup lang="ts">
import { locale } from "echo-editor";
import { useTiptapContext } from "../context/useTiptapStore";
const { state } = useTiptapContext();
</script>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import { ActionButton } from "echo-editor";
import type { Editor } from "echo-editor";
import { ButtonViewReturnComponentProps } from "echo-editor";
import iconUrl from "../../../icons/exportWord.svg";
const props = withDefaults(defineProps<Props>(), {
disabled: false,
isActive: undefined,
});
interface Props {
disabled?: boolean;
isActive?: ButtonViewReturnComponentProps["isActive"];
editor?: Editor;
}
function handleExport() {
props.editor?.commands.exportToWord();
}
</script>
<template>
<action-button
tooltip="ExportToWord"
:is-active="isActive"
:disabled="disabled"
:action="handleExport"
>
<template #icon>
<img
v-if="typeof iconUrl === 'string'"
:src="iconUrl"
style="width: 16px; height: 16px"
alt=""
/>
<!-- <iconUrl style="width: 16px; height: 16px" /> -->
</template>
</action-button>
</template>

View File

@ -0,0 +1,64 @@
import { Extension } from "@tiptap/core";
import { saveAs } from "file-saver";
import ActionButton from "./components/ActionButton.vue";
import { DocxSerializer, defaultNodes, defaultMarks } from "prosemirror-docx";
import { Packer } from "docx";
import type { GeneralOptions } from "echo-editor";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
exportWord: {
exportToWord: () => ReturnType;
};
}
}
export interface ExportWordOptions extends GeneralOptions<ExportWordOptions> {}
const nodeSerializer = {
...defaultNodes,
hardBreak: defaultNodes.hard_break,
codeBlock: defaultNodes.code_block,
orderedList: defaultNodes.ordered_list,
listItem: defaultNodes.list_item,
bulletList: defaultNodes.bullet_list,
horizontalRule: defaultNodes.horizontal_rule,
image(state: any, node: any) {
// No image
state.renderInline(node);
state.closeBlock(node);
},
};
const docxSerializer = new DocxSerializer(nodeSerializer, defaultMarks);
export const ExportWord = Extension.create<ExportWordOptions>({
name: "exportWord",
addOptions() {
return {
...this.parent?.(),
button: ({}) => ({
component: ActionButton,
componentProps: {},
}),
};
},
addCommands() {
return {
exportToWord:
() =>
({ editor }) => {
const opts: any = {
getImageBuffer: async (src: string) => {
const response = await fetch(src);
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
},
};
const wordDocument = docxSerializer.serialize(editor.state.doc, opts);
Packer.toBlob(wordDocument).then((blob) =>
saveAs(new Blob([blob]), "example.docx")
);
return true;
},
};
},
});

View File

@ -0,0 +1,232 @@
import { ExportWord } from "../components/ExportWord";
import OpenAI from "openai";
import {
Bold,
BulletList,
Italic,
BaseKit,
Underline,
Strike,
LineHeight,
Image,
History,
Heading,
CodeBlock,
FontSize,
Highlight,
Table,
Clear,
Blockquote,
Link,
Color,
Video,
OrderedList,
HorizontalRule,
Fullscreen,
TaskList,
MoreMark,
FormatPainter,
SlashCommand,
Indent,
ImportWord,
Columns,
TextAlign,
ImageUpload,
VideoUpload,
FontFamily,
FindAndReplace,
Code,
AI,
Preview,
Printer,
Iframe,
SpecialCharacter,
SourceCode,
} from "echo-editor";
async function handleFileUpload(files: File[]) {
const f = files.map((file) => ({
src: URL.createObjectURL(file),
alt: file.name,
}));
return Promise.resolve(f);
}
async function AICompletions(
history: Array<{ role: string; content: string }> = [],
signal?: AbortSignal
) {
// groq.com For free llm api recommend deepseek r1 70b
// SECURITY WARNING: API keys should never be exposed in the frontend
// This is just for demo purposes
// In production, implement this through your backend API
const apiKey = "import.meta.env.VITE_OPENAI_API_KEY";
const baseURL = "import.meta.env.VITE_OPENAI_BASE_URL";
const model = "import.meta.env.VITE_OPENAI_MODEL";
if (!apiKey || !baseURL || !model) {
throw new Error(
"OpenAI configuration is missing. Please check your environment variables."
);
}
const openai = new OpenAI({
apiKey: apiKey,
dangerouslyAllowBrowser: true,
baseURL: baseURL,
});
const systemMsg = `You are a professional writing assistant. Please respond based on the user's context:
1. Maintain a professional, accurate, and objective tone
2. Ensure responses are clear, coherent, and well-structured
3. Responses must be in HTML format, preserving all HTML tags, links, and styles
4. Support the following writing enhancements:
- Grammar and spelling corrections
- Improved sentence structure and expression
- Optimized article formatting and layout
- Maintain the core meaning of the original text
5. If context includes code, maintain code formatting and provide optimization suggestions
6. Add appropriate HTML elements like headings, lists, quotes etc. to enhance readability as needed
Please respond only based on the provided context, do not add irrelevant information.`;
const systemPrompt = [{ role: "system", content: systemMsg }];
const finalMessages = [...systemPrompt];
if (history.length > 0) {
finalMessages.push(...history);
}
try {
const stream = await openai.chat.completions.create(
{
model,
messages: finalMessages,
temperature: 0.7,
stream: true,
reasoning_format: "parsed", // groq deepseek r1 need this
} as any,
{ signal }
);
return stream;
} catch (error) {
if (error instanceof Error) {
console.error("Error in AI Completions:", error.message);
}
throw error;
}
}
export const minimalExtensions = [
BaseKit.configure({
characterCount: {
limit: 50000,
},
}),
Heading,
Bold.configure({ spacer: true }),
Italic,
Underline,
HorizontalRule,
TextAlign.configure({
types: ["heading", "paragraph", "image"],
spacer: true,
}),
Image,
Blockquote.configure({ spacer: true }),
Code,
Link,
Color,
TaskList.configure({ spacer: true }),
OrderedList,
BulletList,
];
export const fullExtensions = [
BaseKit.configure({
placeholder: {
showOnlyCurrent: true,
},
characterCount: {
limit: 50000,
},
}),
History,
Columns,
FormatPainter,
Clear,
Heading.configure({ spacer: true }),
FontSize,
FontFamily,
Bold,
Italic,
Underline,
Strike,
MoreMark,
Color.configure({ spacer: true }),
Highlight,
BulletList,
OrderedList,
TextAlign.configure({
types: ["heading", "paragraph", "image"],
spacer: true,
}),
Indent,
LineHeight,
TaskList.configure({
spacer: true,
taskItem: {
nested: true,
},
}),
Link,
Image,
ImageUpload.configure({
upload: (files: File) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(URL.createObjectURL(files));
}, 3000);
});
},
}),
Video,
VideoUpload.configure({
upload: handleFileUpload,
}),
Blockquote,
SlashCommand,
HorizontalRule,
CodeBlock,
Table.configure({ spacer: true }),
Code,
ExportWord,
AI.configure({
completions: AICompletions,
shortcuts: [
// 这里可以传入额外的自定义shortcuts
{
label: "Custom Actions",
children: [
{
label: "This is Custom Action",
prompt:
"Rewrite this content with no spelling mistakes, proper grammar, and with more descriptive language, using best writing practices without losing the original meaning.",
},
],
},
],
}),
// ImportWord.configure({
// upload: handleFileUpload,
// }),
SpecialCharacter,
Fullscreen.configure({ spacer: true }),
SourceCode,
Preview,
FindAndReplace.configure({ spacer: true }),
Printer,
Iframe,
];

View File

@ -0,0 +1,59 @@
import {
provide,
inject,
reactive,
type InjectionKey,
computed,
type ComputedRef,
} from "vue";
import { minimalExtensions, fullExtensions } from "./consts";
const TiptapStoreKey: InjectionKey<TiptapStore> = Symbol("TiptapStore");
export interface TiptapState {
content: any;
theme: string | null;
hideToolbar: boolean;
hideMenubar: boolean;
disabled: boolean;
minimal: boolean;
}
export type TiptapStore = {
state: TiptapState;
extensions: ComputedRef<any[]>;
};
export function useTiptapStore(initialState?: Partial<TiptapState>) {
const state = reactive<TiptapState>({
content: "",
theme: null,
hideToolbar: false,
hideMenubar: false,
disabled: false,
minimal: false,
...initialState,
});
const extensions = computed(() => {
return state.minimal ? minimalExtensions : fullExtensions;
});
const store: TiptapStore = {
state: state,
extensions,
};
provide(TiptapStoreKey, store);
return store;
}
export function useTiptapContext() {
const store = inject(TiptapStoreKey);
if (!store) {
throw new Error("useTiptapContext must be used within a TiptapProvider");
}
return store;
}

View File

@ -0,0 +1 @@
<svg t="1721031224139" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7445" width="200" height="200"><path d="M679.253333 402.363733l-60.484266 158.651734-60.347734-158.651734a30.037333 30.037333 0 0 0-30.446933-18.6368 29.764267 29.764267 0 0 0-30.446933 18.6368l-60.416 158.651734-60.416-158.651734a30.5152 30.5152 0 0 0-38.843734-17.271466 28.945067 28.945067 0 0 0-17.954133 37.546666l88.814933 233.2672c4.369067 11.4688 15.701333 19.114667 28.398934 19.114667a30.3104 30.3104 0 0 0 28.4672-19.114667l62.395733-163.908266 62.395733 163.84c4.437333 11.605333 15.701333 19.182933 28.398934 19.182933a30.3104 30.3104 0 0 0 28.4672-19.114667l88.746666-233.2672a28.945067 28.945067 0 0 0-17.885866-37.546666 30.446933 30.446933 0 0 0-38.912 17.271466zM898.730667 797.969067l-51.882667-29.218134c-28.672-16.1792-52.224-3.072-52.224 29.0816v0.273067H643.208533a29.832533 29.832533 0 0 0-30.3104 29.354667c0 16.1792 13.585067 29.218133 30.3104 29.218133h151.825067c1.092267 30.5152 24.029867 43.076267 52.224 27.648l51.063467-27.989333c29.013333-15.906133 29.149867-42.1888 0.4096-58.368z" fill="#333333" p-id="7446"></path><path d="M810.666667 913.134933l-0.477867 0.068267H201.796267c-19.8656 0-36.727467-11.6736-36.727467-25.6V269.243733h154.965333c51.268267 0 92.910933-39.389867 92.910934-87.8592V93.525333h397.243733c19.797333 0 36.522667 11.741867 36.522667 25.668267v620.066133h61.986133V119.261867c0-46.421333-44.168533-84.241067-98.5088-84.241067H328.362667l-225.28 194.56v658.090667c0 46.2848 44.2368 84.104533 98.7136 84.104533h608.392533c43.758933 0 80.554667-24.712533 93.320533-58.5728h-92.842666zM350.890667 94.890667v86.562133c0 16.110933-13.858133 29.2864-30.9248 29.2864H216.814933l134.144-115.848533z" fill="#333333" p-id="7447"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,14 @@
import TiptapEditor from "./views/index.vue";
import { useTiptapStore, useTiptapContext } from "./context/useTiptapStore";
import { ThemeToggle } from "echo-editor";
import EditorActions from "./components/EditorActions.vue";
import { DEMO_CONTENT } from "./views/initContent";
export {
TiptapEditor,
ThemeToggle,
useTiptapStore,
useTiptapContext,
EditorActions,
DEMO_CONTENT,
};

View File

@ -0,0 +1,5 @@
declare module "*.svg" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any> | string;
export default component;
}

View File

@ -0,0 +1,30 @@
<template>
<div class="">
<echo-editor
:model-value="state.content"
:extensions="extensions"
:hideToolbar="state.hideToolbar"
:hideMenubar="state.hideMenubar || state.minimal"
:key="state.minimal ? 'minimal' : 'full'"
:disabled="state.disabled"
:maxHeight="512"
output="html"
@update:theme="state.theme"
@update:model-value="state.content"
:dark="state.theme === 'dark'"
:theme="state.theme"
/>
</div>
</template>
<script setup lang="ts">
import { EchoEditor } from "echo-editor";
import { DEMO_CONTENT } from "./initContent";
import { type TiptapState, useTiptapContext } from "../context/useTiptapStore";
import "./style.css";
import "echo-editor/style.css";
const { state, extensions } = useTiptapContext();
console.log("Tiptap state:", state);
</script>

View File

@ -0,0 +1,3 @@
export const DEMO_CONTENT = `
<h1 style="text-align: center">Echo Editor</h1><p style="text-align: left">A modern WYSIWYG AI rich text editor based on <a target="_blank" rel="noopener noreferrer nofollow" class="link" href="https://github.com/scrumpy/tiptap" stricturl="true">tiptap</a> and <a target="_blank" rel="noopener noreferrer nofollow" class="link" href="https://www.shadcn-vue.com/" stricturl="true">shadcn ui</a> for Vue.js</p><p style="text-align: left"></p><p style="text-align: left"></p><p style="text-align: left"></p><img height="auto" style="margin-left: auto; margin-right: auto; text-align: center" src="https://picsum.photos/1920/1080.webp?t=1" flipx="false" flipy="false" originwidth="500" originheight="281.25" width="500"><p style="text-align: left"></p><div data-type="horizontalRule"><hr></div><h2 style="text-align: left">Demo</h2><p style="text-align: left">👉<a target="_blank" rel="noopener noreferrer nofollow" class="link" href="https://echo-editor.jzcloud.site/" stricturl="true">Demo</a></p><h2 style="text-align: left">Features</h2><ul style="list-style-type: disc"><li><p style="text-align: left">Use <a target="_blank" rel="noopener noreferrer nofollow" class="link" href="https://www.shadcn-vue.com/" stricturl="true">shadcn ui</a> components</p></li><li><p style="text-align: left">Markdown support</p></li><li><p style="text-align: left">TypeScript support</p></li><li><p style="text-align: left">I18n support</p></li><li><p style="text-align: left">Vue 3.x support</p></li><li><p style="text-align: left">Slash Command</p></li><li><p style="text-align: left">Dark Mode</p></li><li><p style="text-align: left">Multi Column</p></li><li><p style="text-align: left">AI Power</p></li><li><p style="text-align: left">Embed</p></li><li><p style="text-align: left">TailwindCss</p></li></ul><h2 style="text-align: left">Installation</h2><pre vnode="true" code="pnpm add echo-editor // or or yarn add echo-editor // or or npm i echo-editor -S" language="bash" linenumbers="true" wordwrap="false" tabsize="2" shouldfocus="false"><code>pnpm add echo-editor // or or yarn add echo-editor // or or npm i echo-editor -S</code></pre><h3 style="text-align: left">install plugin</h3><pre vnode="true" code="import { createApp } from 'vue' import App from './App.vue' import EchoEditor from 'echo-editor' import 'echo-editor/style.css' const app = createApp(App) app.use(EchoEditor) app.mount('#app')" language="typescript" linenumbers="true" wordwrap="false" tabsize="2" shouldfocus="false"><code>import { createApp } from 'vue' import App from './App.vue' import EchoEditor from 'echo-editor' import 'echo-editor/style.css' const app = createApp(App) app.use(EchoEditor) app.mount('#app')</code></pre><p style="text-align: left"></p>
`;

View File

@ -0,0 +1,89 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--ring: 240 10% 3.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}

View File

@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "node",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path Aliases */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@views/*": ["src/views/*"],
"@components/*": ["src/components/*"]
// "@store/*": ["src/store/*"],
// "@assets/*": ["src/assets/*"],
// "@utils/*": ["src/utils/*"],
// "@styles/*": ["src/styles/*"],
// "@helpers/*": ["src/store/helpers/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

1310
web3/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
packages:
- 'packages/*'
- 'packages/mind-map/*'
- 'packages/tiptap/*'
- 'apps/*'