@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 +21 -0
- package/components/editor/index.tsx +67 -0
- package/components/editor/menu-bar.tsx +190 -0
- package/components/editor/menubar-state.tsx +42 -0
- package/components/editor/styles.css +102 -0
- package/package.json +68 -0
- package/scripts/init.js +88 -0
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
|
+
}
|
package/scripts/init.js
ADDED
|
@@ -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
|
+
}
|