@shadospace/editor 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # Next.js template
2
+
3
+ This is a Next.js template with shadcn/ui.
4
+
5
+ ## Adding components
6
+
7
+ To add components to your app, run the following command:
8
+
9
+ ```bash
10
+ npx shadcn@latest add button
11
+ ```
12
+
13
+ This will place the ui components in the `components` directory.
14
+
15
+ ## Using components
16
+
17
+ To use the components in your app, import them as follows:
18
+
19
+ ```tsx
20
+ import { Button } from "@/components/ui/button";
21
+ ```
@@ -0,0 +1,67 @@
1
+ "use client"
2
+ import { useEditor, EditorContent } from "@tiptap/react"
3
+ import StarterKit from "@tiptap/starter-kit"
4
+ import Image from "@tiptap/extension-image"
5
+ import { TableKit } from "@tiptap/extension-table"
6
+ import { TextStyleKit } from "@tiptap/extension-text-style"
7
+ import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"
8
+ import css from "highlight.js/lib/languages/css"
9
+ import js from "highlight.js/lib/languages/javascript"
10
+ import ts from "highlight.js/lib/languages/typescript"
11
+ import html from "highlight.js/lib/languages/xml"
12
+ import { MenuBar } from "./menu-bar"
13
+ import { cn } from "@/lib/utils"
14
+
15
+ import { all, createLowlight } from "lowlight"
16
+ const lowlight = createLowlight(all)
17
+
18
+ lowlight.register("html", html)
19
+ lowlight.register("css", css)
20
+ lowlight.register("js", js)
21
+ lowlight.register("ts", ts)
22
+
23
+ export default function Editor({
24
+ content,
25
+ onChange,
26
+ readonly,
27
+ }: {
28
+ content?: string
29
+ onChange?: (content: string) => void
30
+ readonly?: boolean
31
+ }) {
32
+ const editor = useEditor({
33
+ extensions: [
34
+ StarterKit,
35
+ TextStyleKit,
36
+ Image,
37
+ TableKit,
38
+ CodeBlockLowlight.configure({ lowlight }),
39
+ ],
40
+ editorProps: {
41
+ attributes: {
42
+ class: "max-w-none min-h-64 w-full outline-none",
43
+ },
44
+ },
45
+ content,
46
+ immediatelyRender: false,
47
+ editable: !readonly,
48
+ onUpdate: ({ editor }) => {
49
+ onChange?.(editor.getHTML())
50
+ },
51
+ })
52
+
53
+ return (
54
+ <div>
55
+ {!readonly && editor && (
56
+ <div className="sticky top-17 z-50 px-4 sm:px-0">
57
+ <MenuBar editor={editor} />
58
+ </div>
59
+ )}
60
+ <div className="px-4 sm:px-0">
61
+ <div className={cn(!readonly && "border p-4")}>
62
+ <EditorContent editor={editor} />
63
+ </div>
64
+ </div>
65
+ </div>
66
+ )
67
+ }
@@ -0,0 +1,190 @@
1
+ import type { Editor } from "@tiptap/core"
2
+ import { useEditorState } from "@tiptap/react"
3
+
4
+ import { menuBarStateSelector } from "./menubar-state"
5
+ import {
6
+ Bold,
7
+ Code,
8
+ Heading1,
9
+ Heading2,
10
+ Heading3,
11
+ Italic,
12
+ List,
13
+ ListOrdered,
14
+ Minus,
15
+ Pilcrow,
16
+ Quote,
17
+ Redo2,
18
+ SquareCode,
19
+ Strikethrough,
20
+ TextWrap,
21
+ Undo2,
22
+ } from "lucide-react"
23
+ import { Button } from "../ui/button"
24
+
25
+ export const MenuBar = ({ editor }: { editor: Editor }) => {
26
+ const editorState = useEditorState({
27
+ editor,
28
+ selector: menuBarStateSelector,
29
+ })
30
+
31
+ return (
32
+ <div>
33
+ <div className="overflow-x-auto border bg-card">
34
+ <div className="flex w-max items-center p-2">
35
+ <Button
36
+ type="button"
37
+ variant={"ghost"}
38
+ size={"icon"}
39
+ aria-label="Toggle H1"
40
+ onClick={() =>
41
+ editor.chain().focus().toggleHeading({ level: 1 }).run()
42
+ }
43
+ disabled={editorState.isHeading1}
44
+ >
45
+ <Heading1 />
46
+ </Button>
47
+ <Button
48
+ type="button"
49
+ variant={"ghost"}
50
+ size={"icon"}
51
+ aria-label="Toggle H2"
52
+ onClick={() =>
53
+ editor.chain().focus().toggleHeading({ level: 2 }).run()
54
+ }
55
+ disabled={editorState.isHeading2}
56
+ >
57
+ <Heading2 />
58
+ </Button>
59
+ <Button
60
+ type="button"
61
+ variant={"ghost"}
62
+ size={"icon"}
63
+ onClick={() =>
64
+ editor.chain().focus().toggleHeading({ level: 3 }).run()
65
+ }
66
+ disabled={editorState.isHeading3}
67
+ >
68
+ <Heading3 />
69
+ </Button>
70
+ <Button
71
+ type="button"
72
+ variant={"ghost"}
73
+ size={"icon"}
74
+ onClick={() => editor.chain().focus().toggleBold().run()}
75
+ disabled={!editorState.canBold}
76
+ >
77
+ <Bold />
78
+ </Button>
79
+ <Button
80
+ type="button"
81
+ variant={"ghost"}
82
+ size={"icon"}
83
+ onClick={() => editor.chain().focus().toggleItalic().run()}
84
+ disabled={!editorState.canItalic}
85
+ >
86
+ <Italic />
87
+ </Button>
88
+ <Button
89
+ type="button"
90
+ variant={"ghost"}
91
+ size={"icon"}
92
+ onClick={() => editor.chain().focus().toggleStrike().run()}
93
+ disabled={!editorState.canStrike}
94
+ >
95
+ <Strikethrough />
96
+ </Button>
97
+ <Button
98
+ type="button"
99
+ variant={"ghost"}
100
+ size={"icon"}
101
+ onClick={() => editor.chain().focus().toggleCode().run()}
102
+ disabled={!editorState.canCode}
103
+ >
104
+ <Code />
105
+ </Button>
106
+ <Button
107
+ type="button"
108
+ variant={"ghost"}
109
+ size={"icon"}
110
+ onClick={() => editor.chain().focus().setParagraph().run()}
111
+ disabled={editorState.isParagraph}
112
+ >
113
+ <Pilcrow />
114
+ </Button>
115
+
116
+ <Button
117
+ type="button"
118
+ variant={"ghost"}
119
+ size={"icon"}
120
+ onClick={() => editor.chain().focus().toggleBulletList().run()}
121
+ disabled={editorState.isBulletList}
122
+ >
123
+ <List />
124
+ </Button>
125
+ <Button
126
+ type="button"
127
+ variant={"ghost"}
128
+ size={"icon"}
129
+ onClick={() => editor.chain().focus().toggleOrderedList().run()}
130
+ disabled={editorState.isOrderedList}
131
+ >
132
+ <ListOrdered />
133
+ </Button>
134
+ <Button
135
+ type="button"
136
+ variant={"ghost"}
137
+ size={"icon"}
138
+ onClick={() => editor.chain().focus().toggleCodeBlock().run()}
139
+ disabled={editorState.isCodeBlock}
140
+ >
141
+ <SquareCode />
142
+ </Button>
143
+ <Button
144
+ type="button"
145
+ variant={"ghost"}
146
+ size={"icon"}
147
+ onClick={() => editor.chain().focus().toggleBlockquote().run()}
148
+ disabled={editorState.isBlockquote}
149
+ >
150
+ <Quote />
151
+ </Button>
152
+ <Button
153
+ type="button"
154
+ variant={"ghost"}
155
+ size={"icon"}
156
+ onClick={() => editor.chain().focus().setHorizontalRule().run()}
157
+ >
158
+ <Minus />
159
+ </Button>
160
+ <Button
161
+ type="button"
162
+ variant={"ghost"}
163
+ size={"icon"}
164
+ onClick={() => editor.chain().focus().setHardBreak().run()}
165
+ >
166
+ <TextWrap />
167
+ </Button>
168
+ <Button
169
+ type="button"
170
+ variant={"ghost"}
171
+ size={"icon"}
172
+ onClick={() => editor.chain().focus().undo().run()}
173
+ disabled={!editorState.canUndo}
174
+ >
175
+ <Undo2 />
176
+ </Button>
177
+ <Button
178
+ type="button"
179
+ variant={"ghost"}
180
+ size={"icon"}
181
+ onClick={() => editor.chain().focus().redo().run()}
182
+ disabled={!editorState.canRedo}
183
+ >
184
+ <Redo2 />
185
+ </Button>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ )
190
+ }
@@ -0,0 +1,42 @@
1
+ import type { Editor } from "@tiptap/core"
2
+ import type { EditorStateSnapshot } from "@tiptap/react"
3
+
4
+ /**
5
+ * State selector for the MenuBar component.
6
+ * Extracts the relevant editor state for rendering menu buttons.
7
+ */
8
+ export function menuBarStateSelector(ctx: EditorStateSnapshot<Editor>) {
9
+ return {
10
+ // Text formatting
11
+ isBold: ctx.editor.isActive("bold") ?? false,
12
+ canBold: ctx.editor.can().chain().toggleBold().run() ?? false,
13
+ isItalic: ctx.editor.isActive("italic") ?? false,
14
+ canItalic: ctx.editor.can().chain().toggleItalic().run() ?? false,
15
+ isStrike: ctx.editor.isActive("strike") ?? false,
16
+ canStrike: ctx.editor.can().chain().toggleStrike().run() ?? false,
17
+ isCode: ctx.editor.isActive("code") ?? false,
18
+ canCode: ctx.editor.can().chain().toggleCode().run() ?? false,
19
+ canClearMarks: ctx.editor.can().chain().unsetAllMarks().run() ?? false,
20
+
21
+ // Block types
22
+ isParagraph: ctx.editor.isActive("paragraph") ?? false,
23
+ isHeading1: ctx.editor.isActive("heading", { level: 1 }) ?? false,
24
+ isHeading2: ctx.editor.isActive("heading", { level: 2 }) ?? false,
25
+ isHeading3: ctx.editor.isActive("heading", { level: 3 }) ?? false,
26
+ isHeading4: ctx.editor.isActive("heading", { level: 4 }) ?? false,
27
+ isHeading5: ctx.editor.isActive("heading", { level: 5 }) ?? false,
28
+ isHeading6: ctx.editor.isActive("heading", { level: 6 }) ?? false,
29
+
30
+ // Lists and blocks
31
+ isBulletList: ctx.editor.isActive("bulletList") ?? false,
32
+ isOrderedList: ctx.editor.isActive("orderedList") ?? false,
33
+ isCodeBlock: ctx.editor.isActive("codeBlock") ?? false,
34
+ isBlockquote: ctx.editor.isActive("blockquote") ?? false,
35
+
36
+ // History
37
+ canUndo: ctx.editor.can().chain().undo().run() ?? false,
38
+ canRedo: ctx.editor.can().chain().redo().run() ?? false,
39
+ }
40
+ }
41
+
42
+ export type MenuBarState = ReturnType<typeof menuBarStateSelector>
@@ -0,0 +1,102 @@
1
+ .tiptap {
2
+ p {
3
+ @apply my-4 text-base first:mt-0 last:mb-0;
4
+ }
5
+
6
+ h1 {
7
+ @apply mt-8 mb-4 text-3xl font-bold first:mt-0 last:mb-0;
8
+ }
9
+
10
+ h2 {
11
+ @apply mt-8 mb-4 text-2xl font-semibold first:mt-0 last:mb-0;
12
+ }
13
+
14
+ h3 {
15
+ @apply mt-8 mb-4 text-xl font-medium first:mt-0 last:mb-0;
16
+ }
17
+
18
+ a {
19
+ @apply text-red-400 underline underline-offset-2;
20
+ }
21
+
22
+ blockquote {
23
+ @apply my-6 border-l-4 border-primary pl-4 italic;
24
+ }
25
+
26
+ ul,
27
+ ol {
28
+ @apply my-4 ml-6 list-disc pl-1;
29
+ }
30
+
31
+ li {
32
+ @apply my-1;
33
+ }
34
+
35
+ img {
36
+ @apply my-6 max-w-full;
37
+ }
38
+
39
+ hr {
40
+ @apply my-8 border-border;
41
+ }
42
+ pre {
43
+ @apply my-6 overflow-x-auto overflow-y-clip bg-card/50 p-4 font-mono text-sm text-foreground;
44
+
45
+ code {
46
+ @apply whitespace-pre-wrap;
47
+ }
48
+
49
+ /* Code styling */
50
+ .hljs-comment,
51
+ .hljs-quote {
52
+ color: #616161;
53
+ }
54
+
55
+ .hljs-variable,
56
+ .hljs-template-variable,
57
+ .hljs-attribute,
58
+ .hljs-tag,
59
+ .hljs-name,
60
+ .hljs-regexp,
61
+ .hljs-link,
62
+ .hljs-name,
63
+ .hljs-selector-id,
64
+ .hljs-selector-class {
65
+ color: #f98181;
66
+ }
67
+
68
+ .hljs-number,
69
+ .hljs-meta,
70
+ .hljs-built_in,
71
+ .hljs-builtin-name,
72
+ .hljs-literal,
73
+ .hljs-type,
74
+ .hljs-params {
75
+ color: #fbbc88;
76
+ }
77
+
78
+ .hljs-string,
79
+ .hljs-symbol,
80
+ .hljs-bullet {
81
+ color: #b9f18d;
82
+ }
83
+
84
+ .hljs-title,
85
+ .hljs-section {
86
+ color: #faf594;
87
+ }
88
+
89
+ .hljs-keyword,
90
+ .hljs-selector-tag {
91
+ color: #70cff8;
92
+ }
93
+
94
+ .hljs-emphasis {
95
+ font-style: italic;
96
+ }
97
+
98
+ .hljs-strong {
99
+ font-weight: 700;
100
+ }
101
+ }
102
+ }
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@shadospace/editor",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "private": false,
6
+ "files": [
7
+ "scripts/init.js",
8
+ "components/editor"
9
+ ],
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "scripts": {
14
+ "dev": "next dev --turbopack",
15
+ "build": "next build",
16
+ "start": "next start",
17
+ "lint": "eslint",
18
+ "format": "prettier --write \"**/*.{ts,tsx}\"",
19
+ "typecheck": "tsc --noEmit",
20
+ "postinstall": "bun scripts/init.js"
21
+ },
22
+ "dependencies": {
23
+ "@hookform/resolvers": "^5.2.2",
24
+ "@neondatabase/serverless": "^1.1.0",
25
+ "@tiptap/extension-code-block-lowlight": "^3.23.1",
26
+ "@tiptap/extension-image": "^3.23.1",
27
+ "@tiptap/extension-table": "^3.23.1",
28
+ "@tiptap/extension-text-style": "^3.23.1",
29
+ "@tiptap/pm": "^3.23.1",
30
+ "@tiptap/react": "^3.23.1",
31
+ "@tiptap/starter-kit": "^3.23.1",
32
+ "better-auth": "^1.6.9",
33
+ "class-variance-authority": "^0.7.1",
34
+ "clsx": "^2.1.1",
35
+ "dotenv": "^17.4.2",
36
+ "drizzle-orm": "^0.45.2",
37
+ "lucide-react": "^1.14.0",
38
+ "next": "16.1.7",
39
+ "next-themes": "^0.4.6",
40
+ "pg": "^8.20.0",
41
+ "radix-ui": "^1.4.3",
42
+ "react": "^19.2.4",
43
+ "react-dom": "^19.2.4",
44
+ "react-hook-form": "^7.75.0",
45
+ "shadcn": "^4.7.0",
46
+ "sonner": "^2.0.7",
47
+ "tailwind-merge": "^3.5.0",
48
+ "tw-animate-css": "^1.4.0",
49
+ "zod": "^4.4.3"
50
+ },
51
+ "devDependencies": {
52
+ "@eslint/eslintrc": "^3",
53
+ "@tailwindcss/postcss": "^4.2.1",
54
+ "@types/node": "^25.5.0",
55
+ "@types/pg": "^8.20.0",
56
+ "@types/react": "^19.2.14",
57
+ "@types/react-dom": "^19.2.3",
58
+ "drizzle-kit": "^0.31.10",
59
+ "eslint": "^9.39.4",
60
+ "eslint-config-next": "16.1.7",
61
+ "postcss": "^8",
62
+ "prettier": "^3.8.1",
63
+ "prettier-plugin-tailwindcss": "^0.7.2",
64
+ "tailwindcss": "^4.2.1",
65
+ "tsx": "^4.21.0",
66
+ "typescript": "^5.9.3"
67
+ }
68
+ }
@@ -0,0 +1,88 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ // Target directory (where the user is installing the package)
9
+ // INIT_CWD is set by npm/yarn/pnpm/bun during postinstall
10
+ const targetDir = process.env.INIT_CWD || process.cwd();
11
+
12
+ console.log(`[tiptap-starter] Initializing in ${targetDir}`);
13
+
14
+ const sourceEditorDir = path.join(__dirname, '../components/editor');
15
+
16
+ // Detect if target project uses src directory
17
+ const hasSrc = fs.existsSync(path.join(targetDir, 'src'));
18
+ const baseTargetDir = hasSrc ? path.join(targetDir, 'src') : targetDir;
19
+
20
+ const targetEditorDir = path.join(baseTargetDir, 'components/editor');
21
+
22
+ // Function to copy files
23
+ function copyFile(src, dest) {
24
+ if (fs.existsSync(dest)) {
25
+ console.log(`[tiptap-starter] ${path.basename(dest)} already exists, skipping.`);
26
+ return;
27
+ }
28
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
29
+ fs.copyFileSync(src, dest);
30
+ console.log(`[tiptap-starter] Created ${dest}`);
31
+ }
32
+
33
+ // Files to copy
34
+ const files = [
35
+ 'index.tsx',
36
+ 'menu-bar.tsx',
37
+ 'menubar-state.tsx',
38
+ 'styles.css'
39
+ ];
40
+
41
+ try {
42
+ // Check if source files exist
43
+ files.forEach(file => {
44
+ const src = path.join(sourceEditorDir, file);
45
+ const dest = path.join(targetEditorDir, file);
46
+ if (fs.existsSync(src)) {
47
+ copyFile(src, dest);
48
+ } else {
49
+ console.error(`[tiptap-starter] Source file missing: ${src}`);
50
+ }
51
+ });
52
+
53
+ // Update globals.css
54
+ const possiblePaths = [
55
+ path.join(targetDir, 'app/globals.css'),
56
+ path.join(targetDir, 'src/app/globals.css'),
57
+ path.join(targetDir, 'styles/globals.css')
58
+ ];
59
+
60
+ let globalsCssPath = possiblePaths.find(p => fs.existsSync(p));
61
+
62
+ if (globalsCssPath) {
63
+ const content = fs.readFileSync(globalsCssPath, 'utf8');
64
+ const importStatement = `@import "../components/editor/styles.css";`;
65
+
66
+ if (!content.includes(importStatement)) {
67
+ const lines = content.split('\n');
68
+ let insertIndex = 0;
69
+ for (let i = 0; i < lines.length; i++) {
70
+ if (lines[i].startsWith('@import')) {
71
+ insertIndex = i + 1;
72
+ } else if (lines[i].trim() !== '') {
73
+ break;
74
+ }
75
+ }
76
+ lines.splice(insertIndex, 0, importStatement);
77
+ fs.writeFileSync(globalsCssPath, lines.join('\n'));
78
+ console.log(`[tiptap-starter] Referenced styles in ${globalsCssPath}`);
79
+ } else {
80
+ console.log(`[tiptap-starter] Styles already referenced in ${globalsCssPath}`);
81
+ }
82
+ } else {
83
+ console.warn(`[tiptap-starter] globals.css not found. Please add \`@import "../components/editor/styles.css";\` manually to your CSS file.`);
84
+ }
85
+
86
+ } catch (error) {
87
+ console.error(`[tiptap-starter] Error during initialization:`, error);
88
+ }