Codync
A free, open-source 1:1 alternative to Grok Bot and Muse. Message Claude Code, Codex, Cursor and 40+ coding agents as persistent bots from your iPhone, Mac or Linux desktop. Group chats, threads, appr
Vue 3 port of editorcn — Tiptap editor components styled with shadcn tokens.
Translation pending
Developer toolsGitHub @guygubabyOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Repository information has not been collected yet.
No updates detected yet. GitHub releases will appear here.
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗Vue 3 port of editorcn — rich text editor components built on Tiptap, with a shadcn-style token layer.
Works in any Vue 3 project: the components ship plain CSS and read namespaced --rte-* theme variables, so they never collide with your app's tokens. Tailwind is optional, not required.
Docs: https://editorcn-vue.guygubaby.top (llms.txt: https://editorcn-vue.guygubaby.top/llms.txt)
This repo currently contains four packages:
RichTextEditor with 25+ controls, the bubble menu, the link popover, word count, and three visual variants.BlockEditor with slash commands, a drag handle, a node selector, the bubble menu, and the mobile bottom bar.Table with hover grips and add handles, ImagePlaceholder / ResizableImage with upload, embed, resize, align, duplicate and delete, and a Video block with a pluggable uploader.StaticRenderer for read-only HTML or ProseMirror JSON.No npm package. Each package is a shadcn-vue registry item served from GitHub, so the CLI writes the source straight into your project:
pnpm dlx shadcn-vue@latest add https://raw.githubusercontent.com/guygubaby/editorcn-vue/main/registry/editor.json
pnpm dlx shadcn-vue@latest add https://raw.githubusercontent.com/guygubaby/editorcn-vue/main/registry/block-editor.json
pnpm dlx shadcn-vue@latest add https://raw.githubusercontent.com/guygubaby/editorcn-vue/main/registry/extensions.json
pnpm dlx shadcn-vue@latest add https://raw.githubusercontent.com/guygubaby/editorcn-vue/main/registry/static-renderer.json
Files land in src/components/{editor,block-editor,extensions,static-renderer} and their dependencies are installed automatically. block-editor and extensions pull editor through registryDependencies.
Behind a proxy:
NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://127.0.0.1:7890 \
pnpm dlx shadcn-vue@latest add https://raw.githubusercontent.com/guygubaby/editorcn-vue/main/registry/editor.json
The repo is a pnpm monorepo:
apps/playground demo app; components installed by the shadcn-vue CLI
packages/editorcn-vue component source
registry/ generated shadcn-vue registry items
docs/ VitePress site (serves the playground at /playground/)
The playground does not import the workspace package. Its
src/components/{editor,block-editor,extensions,static-renderer} are the
CLI-installed output, the same source consumers get:
pnpm playground:sync # rebuild registry/, reinstall into apps/playground
pnpm install
pnpm dev # playground (apps/playground)
pnpm test
pnpm docs:dev # documentation site, including the playground page
pnpm build type-checks with vue-tsc and builds the playground; pnpm build:registry regenerates registry/*.json from packages/editorcn-vue/src.
The docs site lives in docs/ (VitePress): getting started, registry install, uploads, theming, and a page per package.
<script setup lang="ts">
import StarterKit from "@tiptap/starter-kit";
import TextAlign from "@tiptap/extension-text-align";
import Placeholder from "@tiptap/extension-placeholder";
import { useEditor } from "@tiptap/vue-3";
import { CodeBlock, Link, RichTextEditor } from "./editor";
import "./editor/style.css";
const editor = useEditor({
content: "<h2>Hello</h2><p>Start typing…</p>",
extensions: [
StarterKit.configure({ link: false, codeBlock: false }),
Link,
CodeBlock,
TextAlign.configure({ types: ["heading", "paragraph"] }),
Placeholder.configure({ placeholder: "Start typing..." }),
],
});
</script>
<template>
<RichTextEditor :editor="editor" variant="default">
<RichTextEditor.Toolbar sticky>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Link />
<RichTextEditor.Unlink />
<RichTextEditor.Undo />
<RichTextEditor.Redo />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content>
<RichTextEditor.BubbleMenu :editor="editor ?? null" />
</RichTextEditor.Content>
<RichTextEditor.Footer show-word-count />
</RichTextEditor>
</template>
Import ./editor/style.css after your global stylesheet. The stylesheet reads namespaced --rte-* variables (--rte-border, --rte-background, --rte-muted, --rte-radius, …); define them on :root/.dark or map them to your shadcn tokens. No Tailwind setup is involved.
Every rule is scoped under .rte, which the components add to their root element (and to anything they portal to <body>), so the stylesheets never leak into your app.
| Component | Notes |
|---|---|
RichTextEditor | Root wrapper; provides editor, labels, icons, variant and editable state |
.Toolbar | Hidden when editable is false; sticky + stickyOffset supported |
.ControlsGroup | Groups controls with separators |
.Control | Base button for custom controls |
.Content | Tiptap EditorContent wrapper |
.Footer | showWordCount, sticky, wordCountFormatter |
.BubbleMenu | Text formatting on selection; language picker inside code blocks |
.Bold .Italic .Underline .Strikethrough .Code .ClearFormatting | Inline marks |
.H1–.H6 .Blockquote .Hr | Block types |
.BulletList .OrderedList | Lists |
.AlignLeft .AlignCenter .AlignRight .AlignJustify | Needs TextAlign |
.Link .Unlink | Link popover, Mod-k opens it |
.Highlight .Subscript .Superscript | Needs the matching Tiptap extensions |
.Undo .Redo | Needs history (in StarterKit) |
All 25 controls are also exported flat from ./editor/controls and can be created with createControl.
<RichTextEditor
:editor="editor"
variant="compact"
:labels="{ boldControlLabel: '加粗' }"
:icons="{ boldControlIcon: CustomBoldIcon }"
/>
variant: default | subtle | compactlabels: partial override of DEFAULT_LABELSicons: partial override of DEFAULT_ICONS; each value is a Vue componenteditable: hides the toolbar and sets editor.setEditable(...)Icons default to @lucide/vue. Language icons are the original editorcn SVG set.
<script setup lang="ts">
import { useEditor } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import { TaskItem, TaskList } from "@tiptap/extension-list";
import {
BlockEditor,
CodeBlock,
Link,
SlashCommand,
getSlashCommandSuggestion,
} from "./block-editor";
import "./block-editor/style.css";
const editor = useEditor({
content: "<p>Type / for commands…</p>",
extensions: [
StarterKit.configure({ link: false, codeBlock: false }),
Link,
CodeBlock,
TaskList,
TaskItem.configure({ nested: true }),
SlashCommand.configure({ suggestion: getSlashCommandSuggestion() }),
],
});
</script>
<template>
<BlockEditor :editor="editor" />
</template>
<BlockEditor> renders the drag handle, bubble menu, content and bottom bar. You can compose it yourself with <BlockEditor.BubbleMenu>, <BlockEditor.BottomBar> and <BlockEditor.Content> inside the default slot.
getSlashCommandSuggestion(customItems) merges your items ahead of the defaults by id.hover: none + pointer: coarse); desktop uses the drag handle and bubble menu.<script setup lang="ts">
import {
ImagePlaceholder,
ImagePlaceholderToolbar,
ResizableImage,
Table,
TableHoverOverlay,
TableToolbar,
} from "./extensions";
import "./extensions/ui/style.css";
import "./extensions/table/style.css";
import "./extensions/image-placeholder/style.css";
const editor = useEditor({
extensions: [
StarterKit.configure({ gapcursor: false }), // Table registers its own gapcursor
Table,
ImagePlaceholder,
ResizableImage,
],
});
</script>
<template>
<RichTextEditor :editor="editor">
<RichTextEditor.Toolbar>
<TableToolbar :editor="editor" />
<ImagePlaceholderToolbar :editor="editor" />
</RichTextEditor.Toolbar>
<RichTextEditor.Content>
<TableHoverOverlay :editor="editor" />
</RichTextEditor.Content>
</RichTextEditor>
</template>
TableToolbar opens a drag-selectable grid picker; inside a table it shows row/column actions instead.TableHoverOverlay draws the row and column grips, the trailing add handles, and their menus. It measures the table every frame so the handles follow scroll and resize.ImagePlaceholder is an atom block that drops or embeds a file and replaces itself with the image node.ResizableImage adds align / width attributes and a drag-to-resize node view with a floating action bar.<ToolbarProvider :editor :labels="..."> to translate labels or inject the editor once.Uploading is not built in — you supply one function and the blocks do the rest:
import type { UploadHandler } from "./extensions";
const upload: UploadHandler = async (file, kind) => {
// e.g. presigned PUT to OSS/COS, or your own API
const { url, poster, width } = await api.upload(file, kind);
return { src: url, poster, width };
};
const editor = useEditor({
extensions: [
ImagePlaceholder.configure({ upload }),
Video,
VideoPlaceholder.configure({ upload }),
],
});
UploadHandler receives (file, "image" | "video") and resolves to:
| field | required | used by | notes |
|---|---|---|---|
src | yes | both | final URL of the file |
poster | no | video | cover image shown before playback |
width | no | both | number (px) or string ("60%"); image defaults to 60%, video to 100% |
height | no | both | optional fixed height |
alt | no | image | alt text |
Notes:
upload, the image block falls back to a base64 data URL; the video block shows an error instead.<script setup lang="ts">
import { StaticRenderer } from "./static-renderer";
import "./static-renderer/style.css";
</script>
<template>
<!-- HTML from editor.getHTML() -->
<StaticRenderer :content="html" />
<!-- ProseMirror JSON needs the extensions that define its nodes and marks -->
<StaticRenderer :content="json" :extensions="extensions" />
</template>
@tiptap/static-renderer has no Vue entry, so JSON content is rendered with its pm/html-string renderer and set through v-html. options.nodeMapping / markMapping therefore return HTML strings rather than components.
Content passed as a string is trusted input; sanitize it if it comes from users.
RichTextEditor object; <RichTextEditor.Bold /> works in templates.useEditorState(editor, selector) composable built on Tiptap editor events, since @tiptap/vue-3 has no selector hook.MIT, same as the upstream project.