tetrons 2.2.3 → 2.2.5
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/dist/app/api/register/route.d.ts +6 -0
- package/dist/app/api/register/route.js +26 -0
- package/dist/app/api/validate/route.d.ts +7 -0
- package/dist/app/api/validate/route.js +18 -0
- package/dist/app/layout.d.ts +1 -2
- package/dist/app/{layout.js → layout.jsx} +5 -2
- package/dist/app/page.d.ts +1 -1
- package/dist/app/page.jsx +62 -0
- package/dist/components/tetrons/EditorContent.d.ts +6 -1
- package/dist/components/tetrons/{EditorContent.js → EditorContent.jsx} +58 -3
- package/dist/components/tetrons/{ResizableImageComponent.js → ResizableImageComponent.jsx} +12 -10
- package/dist/components/tetrons/{ResizableVideoComponent.js → ResizableVideoComponent.jsx} +8 -7
- package/dist/components/tetrons/toolbar/ActionGroup.d.ts +2 -1
- package/dist/components/tetrons/toolbar/{ActionGroup.js → ActionGroup.jsx} +35 -12
- package/dist/components/tetrons/toolbar/ClipboardGroup.d.ts +2 -1
- package/dist/components/tetrons/toolbar/ClipboardGroup.jsx +36 -0
- package/dist/components/tetrons/toolbar/FileGroup.d.ts +2 -1
- package/dist/components/tetrons/toolbar/{FileGroup.js → FileGroup.jsx} +6 -3
- package/dist/components/tetrons/toolbar/FontStyleGroup.d.ts +2 -1
- package/dist/components/tetrons/toolbar/FontStyleGroup.jsx +104 -0
- package/dist/components/tetrons/toolbar/InsertGroup.d.ts +2 -1
- package/dist/components/tetrons/toolbar/InsertGroup.jsx +163 -0
- package/dist/components/tetrons/toolbar/ListAlignGroup.d.ts +2 -1
- package/dist/components/tetrons/toolbar/ListAlignGroup.jsx +16 -0
- package/dist/components/tetrons/toolbar/MiscGroup.d.ts +2 -1
- package/dist/components/tetrons/toolbar/MiscGroup.jsx +31 -0
- package/dist/components/tetrons/toolbar/TableContextMenu.d.ts +2 -1
- package/dist/components/tetrons/toolbar/{TableContextMenu.js → TableContextMenu.jsx} +21 -3
- package/dist/components/tetrons/toolbar/TetronsToolbar.d.ts +2 -1
- package/dist/components/tetrons/toolbar/{TetronsToolbar.js → TetronsToolbar.jsx} +14 -2
- package/dist/components/tetrons/toolbar/ToolbarButton.jsx +8 -0
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +658 -744
- package/dist/lib/db.d.ts +1 -0
- package/dist/lib/db.js +15 -0
- package/dist/models/ApiKey.d.ts +2 -0
- package/dist/models/ApiKey.js +14 -0
- package/dist/utils/apiKeyUtils.d.ts +11 -0
- package/dist/utils/apiKeyUtils.js +17 -0
- package/package.json +2 -1
- package/dist/app/page.js +0 -6
- package/dist/components/tetrons/toolbar/ClipboardGroup.js +0 -31
- package/dist/components/tetrons/toolbar/FontStyleGroup.js +0 -63
- package/dist/components/tetrons/toolbar/InsertGroup.js +0 -138
- package/dist/components/tetrons/toolbar/ListAlignGroup.js +0 -7
- package/dist/components/tetrons/toolbar/MiscGroup.js +0 -25
- package/dist/components/tetrons/toolbar/ToolbarButton.js +0 -7
- /package/dist/components/UI/{Button.js → Button.jsx} +0 -0
- /package/dist/components/UI/{Dropdown.js → Dropdown.jsx} +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { connectDB } from "../../../lib/db";
|
|
3
|
+
import { ApiKey } from "../../../models/ApiKey";
|
|
4
|
+
import { generateApiKey } from "../../../utils/apiKeyUtils";
|
|
5
|
+
export async function POST(req) {
|
|
6
|
+
const { email, organization, version } = await req.json();
|
|
7
|
+
if (!email || !organization || !version)
|
|
8
|
+
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
|
|
9
|
+
await connectDB();
|
|
10
|
+
if (version === "free") {
|
|
11
|
+
const expiresAt = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000);
|
|
12
|
+
const apiKey = generateApiKey(48);
|
|
13
|
+
await ApiKey.deleteMany({ email, version });
|
|
14
|
+
await ApiKey.create({
|
|
15
|
+
email,
|
|
16
|
+
organization,
|
|
17
|
+
version,
|
|
18
|
+
apiKey,
|
|
19
|
+
expiresAt,
|
|
20
|
+
});
|
|
21
|
+
return NextResponse.json({ apiKey, expiresAt });
|
|
22
|
+
}
|
|
23
|
+
const apiKey = generateApiKey(48);
|
|
24
|
+
await ApiKey.create({ email, organization, version, apiKey });
|
|
25
|
+
return NextResponse.json({ apiKey });
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { connectDB } from "../../../lib/db";
|
|
3
|
+
import { ApiKey } from "../../../models/ApiKey";
|
|
4
|
+
export async function POST(req) {
|
|
5
|
+
const { apiKey } = await req.json();
|
|
6
|
+
if (!apiKey)
|
|
7
|
+
return NextResponse.json({ error: "API key required" }, { status: 400 });
|
|
8
|
+
await connectDB();
|
|
9
|
+
const keyEntry = await ApiKey.findOne({ apiKey });
|
|
10
|
+
if (!keyEntry)
|
|
11
|
+
return NextResponse.json({ error: "Invalid key" }, { status: 401 });
|
|
12
|
+
if (keyEntry.version === "free" &&
|
|
13
|
+
keyEntry.expiresAt &&
|
|
14
|
+
new Date() > new Date(keyEntry.expiresAt)) {
|
|
15
|
+
return NextResponse.json({ error: "Free trial expired" }, { status: 403 });
|
|
16
|
+
}
|
|
17
|
+
return NextResponse.json({ valid: true, version: keyEntry.version });
|
|
18
|
+
}
|
package/dist/app/layout.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
/// <reference types="react" />
|
|
2
1
|
import type { Metadata } from "next";
|
|
3
2
|
import "./globals.css";
|
|
4
3
|
export declare const metadata: Metadata;
|
|
5
4
|
export default function RootLayout({ children, }: {
|
|
6
5
|
children: React.ReactNode;
|
|
7
|
-
}): import("react
|
|
6
|
+
}): import("react").JSX.Element;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
1
|
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
2
|
import "./globals.css";
|
|
4
3
|
const geistSans = Geist({
|
|
@@ -23,5 +22,9 @@ export const metadata = {
|
|
|
23
22
|
manifest: "/site.webmanifest",
|
|
24
23
|
};
|
|
25
24
|
export default function RootLayout({ children, }) {
|
|
26
|
-
return (
|
|
25
|
+
return (<html lang="en">
|
|
26
|
+
<body suppressHydrationWarning className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased bg-gray-50 text-gray-900`}>
|
|
27
|
+
<main className="max-w-screen-xl mx-auto p-4">{children}</main>
|
|
28
|
+
</body>
|
|
29
|
+
</html>);
|
|
27
30
|
}
|
package/dist/app/page.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import "../styles/tetrons.css";
|
|
2
|
-
export default function Home(): import("react
|
|
2
|
+
export default function Home(): import("react").JSX.Element;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { useSearchParams } from "next/navigation";
|
|
4
|
+
import EditorContent from "../components/tetrons/EditorContent";
|
|
5
|
+
import "../styles/tetrons.css";
|
|
6
|
+
export default function Home() {
|
|
7
|
+
const searchParams = useSearchParams();
|
|
8
|
+
const urlKey = searchParams.get("apiKey");
|
|
9
|
+
const [apiKey, setApiKey] = useState(null);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const fetchApiKey = async () => {
|
|
13
|
+
try {
|
|
14
|
+
if (urlKey) {
|
|
15
|
+
setApiKey(urlKey);
|
|
16
|
+
localStorage.setItem("my-api-key", urlKey);
|
|
17
|
+
setLoading(false);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const cachedKey = localStorage.getItem("my-api-key");
|
|
21
|
+
if (cachedKey) {
|
|
22
|
+
setApiKey(cachedKey);
|
|
23
|
+
setLoading(false);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const res = await fetch("/api/register", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
email: "mr.swastikjha@gmail.com",
|
|
31
|
+
organization: "FCSPL",
|
|
32
|
+
version: "free",
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
if (!res.ok || !data.apiKey) {
|
|
37
|
+
throw new Error(data.error || "Failed to fetch API key");
|
|
38
|
+
}
|
|
39
|
+
localStorage.setItem("my-api-key", data.apiKey);
|
|
40
|
+
setApiKey(data.apiKey);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error("Error fetching API key:", error);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
setLoading(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
fetchApiKey();
|
|
50
|
+
}, [urlKey]);
|
|
51
|
+
if (loading) {
|
|
52
|
+
return <div className="text-center p-4">⏳ Loading Editor...</div>;
|
|
53
|
+
}
|
|
54
|
+
if (!apiKey) {
|
|
55
|
+
return (<div className="text-center text-red-600">❌ Failed to load API key</div>);
|
|
56
|
+
}
|
|
57
|
+
return (<main className="flex flex-col h-screen overflow-hidden">
|
|
58
|
+
<div className="flex-1 overflow-auto flex flex-col">
|
|
59
|
+
<EditorContent apiKey={apiKey}/>
|
|
60
|
+
</div>
|
|
61
|
+
</main>);
|
|
62
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
2
|
import React from "react";
|
|
4
3
|
import { Comment } from "./toolbar/extensions/Comment";
|
|
5
4
|
import { useEffect, useRef } from "react";
|
|
@@ -46,9 +45,38 @@ import ts from "highlight.js/lib/languages/typescript";
|
|
|
46
45
|
const lowlight = createLowlight();
|
|
47
46
|
lowlight.register("js", js);
|
|
48
47
|
lowlight.register("ts", ts);
|
|
49
|
-
export default function EditorContent() {
|
|
48
|
+
export default function EditorContent({ apiKey }) {
|
|
49
|
+
const [isValid, setIsValid] = React.useState(null);
|
|
50
|
+
const [error, setError] = React.useState(null);
|
|
50
51
|
const [versions, setVersions] = React.useState([]);
|
|
51
52
|
const [currentVersionIndex, setCurrentVersionIndex] = React.useState(null);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const validateKey = async () => {
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch("/api/validate", {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({ apiKey }),
|
|
62
|
+
});
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
if (!res.ok)
|
|
65
|
+
throw new Error(data.error || "Invalid API Key");
|
|
66
|
+
setIsValid(true);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
if (err instanceof Error) {
|
|
70
|
+
setError(err.message || "Invalid API Key");
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
setError("Invalid API Key");
|
|
74
|
+
}
|
|
75
|
+
setIsValid(false);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
validateKey();
|
|
79
|
+
}, [apiKey]);
|
|
52
80
|
const editor = useEditor({
|
|
53
81
|
extensions: [
|
|
54
82
|
Document,
|
|
@@ -134,5 +162,32 @@ export default function EditorContent() {
|
|
|
134
162
|
setCurrentVersionIndex(index);
|
|
135
163
|
}
|
|
136
164
|
};
|
|
137
|
-
|
|
165
|
+
if (isValid === false) {
|
|
166
|
+
return <div className="editor-error">⚠️ {error}</div>;
|
|
167
|
+
}
|
|
168
|
+
if (isValid === null) {
|
|
169
|
+
return <div className="editor-loading">🔍 Validating license...</div>;
|
|
170
|
+
}
|
|
171
|
+
return (<div className="editor-container">
|
|
172
|
+
<div className="editor-toolbar">
|
|
173
|
+
<button type="button" onClick={saveVersion} disabled={!editor} className="editor-save-btn">
|
|
174
|
+
Save Version
|
|
175
|
+
</button>
|
|
176
|
+
|
|
177
|
+
<div className="editor-versions-wrapper">
|
|
178
|
+
{versions.length === 0 && (<span className="editor-no-versions">No saved versions</span>)}
|
|
179
|
+
{versions.map((_, idx) => (<button type="button" key={idx} onClick={() => restoreVersion(idx)} className={`editor-version-btn ${idx === currentVersionIndex ? "active" : ""}`} title={`Restore Version ${idx + 1}`}>
|
|
180
|
+
{`V${idx + 1}`}
|
|
181
|
+
</button>))}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{editor && <TetronsToolbar editor={editor}/>}
|
|
186
|
+
<div ref={wrapperRef} className="editor-content-wrapper" onClick={handleEditorClick}>
|
|
187
|
+
{editor ? (<>
|
|
188
|
+
<TiptapEditorContent editor={editor}/>
|
|
189
|
+
{editor && <TableContextMenu editor={editor}/>}
|
|
190
|
+
</>) : (<div className="editor-loading">Loading editor...</div>)}
|
|
191
|
+
</div>
|
|
192
|
+
</div>);
|
|
138
193
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useRef, useEffect } from "react";
|
|
1
|
+
import React, { useRef, useEffect } from "react";
|
|
3
2
|
import { NodeViewWrapper } from "@tiptap/react";
|
|
4
3
|
const ResizableImageComponent = ({ node, updateAttributes, selected, }) => {
|
|
5
4
|
const { src, alt, title, width, height } = node.attrs;
|
|
@@ -17,19 +16,22 @@ const ResizableImageComponent = ({ node, updateAttributes, selected, }) => {
|
|
|
17
16
|
observer.observe(img);
|
|
18
17
|
return () => observer.disconnect();
|
|
19
18
|
}, [updateAttributes]);
|
|
20
|
-
return (
|
|
19
|
+
return (<NodeViewWrapper ref={wrapperRef} contentEditable={false} className={`resizable-image-wrapper ${selected ? "ProseMirror-selectednode" : ""}`} style={{
|
|
21
20
|
resize: "both",
|
|
22
21
|
overflow: "auto",
|
|
23
22
|
border: "1px solid #ccc",
|
|
24
23
|
padding: 2,
|
|
25
24
|
display: "inline-block",
|
|
26
25
|
maxWidth: "100%",
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
}}>
|
|
27
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
28
|
+
<img ref={imgRef} src={src} alt={alt !== null && alt !== void 0 ? alt : ""} title={title !== null && title !== void 0 ? title : ""} loading="lazy" style={{
|
|
29
|
+
width: width ? `${width}px` : "auto",
|
|
30
|
+
height: height ? `${height}px` : "auto",
|
|
31
|
+
display: "block",
|
|
32
|
+
userSelect: "none",
|
|
33
|
+
pointerEvents: "auto",
|
|
34
|
+
}} draggable={false}/>
|
|
35
|
+
</NodeViewWrapper>);
|
|
34
36
|
};
|
|
35
37
|
export default ResizableImageComponent;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useRef, useEffect } from "react";
|
|
1
|
+
import React, { useRef, useEffect } from "react";
|
|
3
2
|
import { NodeViewWrapper } from "@tiptap/react";
|
|
4
3
|
const ResizableVideoComponent = ({ node, updateAttributes, selected, }) => {
|
|
5
4
|
const { src, controls, width, height } = node.attrs;
|
|
@@ -17,15 +16,17 @@ const ResizableVideoComponent = ({ node, updateAttributes, selected, }) => {
|
|
|
17
16
|
observer.observe(video);
|
|
18
17
|
return () => observer.disconnect();
|
|
19
18
|
}, [updateAttributes]);
|
|
20
|
-
return (
|
|
19
|
+
return (<NodeViewWrapper ref={wrapperRef} contentEditable={false} className={`resizable-video-wrapper ${selected ? "ProseMirror-selectednode" : ""}`} style={{
|
|
21
20
|
resize: "both",
|
|
22
21
|
overflow: "auto",
|
|
23
22
|
border: "1px solid #ccc",
|
|
24
23
|
padding: "2px",
|
|
25
24
|
display: "inline-block",
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
}}>
|
|
26
|
+
<video ref={videoRef} src={src} controls={controls} style={{
|
|
27
|
+
width: width ? `${width}px` : "auto",
|
|
28
|
+
height: height ? `${height}px` : "auto",
|
|
29
|
+
}}/>
|
|
30
|
+
</NodeViewWrapper>);
|
|
30
31
|
};
|
|
31
32
|
export default ResizableVideoComponent;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { Editor } from "@tiptap/react";
|
|
2
3
|
type ActionGroupProps = {
|
|
3
4
|
editor: Editor;
|
|
4
5
|
};
|
|
5
|
-
export default function ActionGroup({ editor }: ActionGroupProps):
|
|
6
|
+
export default function ActionGroup({ editor }: ActionGroupProps): React.JSX.Element;
|
|
6
7
|
export {};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
4
3
|
import { MdZoomIn, MdZoomOut, MdPrint, MdSave, MdDownload, } from "react-icons/md";
|
|
5
4
|
import ToolbarButton from "./ToolbarButton";
|
|
6
5
|
export default function ActionGroup({ editor }) {
|
|
@@ -131,14 +130,38 @@ export default function ActionGroup({ editor }) {
|
|
|
131
130
|
link.click();
|
|
132
131
|
document.body.removeChild(link);
|
|
133
132
|
};
|
|
134
|
-
return (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
133
|
+
return (<div className="action-group" role="group" aria-label="Editor actions">
|
|
134
|
+
<ToolbarButton icon={MdZoomIn} onClick={zoomIn} title="Zoom In"/>
|
|
135
|
+
<ToolbarButton icon={MdZoomOut} onClick={zoomOut} title="Zoom Out"/>
|
|
136
|
+
<ToolbarButton icon={MdPrint} onClick={handlePrint} title="Print"/>
|
|
137
|
+
<ToolbarButton icon={MdSave} onClick={handleSave} title="Save"/>
|
|
138
|
+
|
|
139
|
+
<div className="relative" ref={dropdownRef}>
|
|
140
|
+
<button type="button" onClick={() => setDropdownOpen((open) => !open)} aria-haspopup="menu" aria-expanded={dropdownOpen ? "true" : "false"} className="export-button" title="Export">
|
|
141
|
+
<MdDownload />
|
|
142
|
+
<span className="text-sm"></span>
|
|
143
|
+
</button>
|
|
144
|
+
|
|
145
|
+
{dropdownOpen && (<div className="export-dropdown">
|
|
146
|
+
<button type="button" onClick={() => {
|
|
147
|
+
setDropdownOpen(false);
|
|
148
|
+
handleDownloadPDF();
|
|
149
|
+
}}>
|
|
150
|
+
Export as PDF
|
|
151
|
+
</button>
|
|
152
|
+
<button type="button" onClick={() => {
|
|
153
|
+
setDropdownOpen(false);
|
|
154
|
+
handleDownloadHTML();
|
|
155
|
+
}}>
|
|
156
|
+
Export as HTML
|
|
157
|
+
</button>
|
|
158
|
+
<button type="button" onClick={() => {
|
|
159
|
+
setDropdownOpen(false);
|
|
160
|
+
handleDownloadDOCX();
|
|
161
|
+
}}>
|
|
162
|
+
Export as DOCX
|
|
163
|
+
</button>
|
|
164
|
+
</div>)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>);
|
|
144
167
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { MdContentPaste, MdContentCut, MdContentCopy, MdFormatPaint, } from "react-icons/md";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import ToolbarButton from "./ToolbarButton";
|
|
4
|
+
export default function ClipboardGroup({ editor }) {
|
|
5
|
+
return (<div className="clipboard-group">
|
|
6
|
+
<ToolbarButton icon={MdContentPaste} title="Paste" onClick={async () => {
|
|
7
|
+
try {
|
|
8
|
+
const text = await navigator.clipboard.readText();
|
|
9
|
+
editor.chain().focus().insertContent(text).run();
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
console.error("Failed to read clipboard contents:", error);
|
|
13
|
+
}
|
|
14
|
+
}}/>
|
|
15
|
+
<ToolbarButton icon={MdContentCut} title="Cut" onClick={() => {
|
|
16
|
+
const { from, to } = editor.state.selection;
|
|
17
|
+
if (from === to)
|
|
18
|
+
return;
|
|
19
|
+
const selectedText = editor.state.doc.textBetween(from, to);
|
|
20
|
+
navigator.clipboard.writeText(selectedText).then(() => {
|
|
21
|
+
editor.chain().focus().deleteRange({ from, to }).run();
|
|
22
|
+
});
|
|
23
|
+
}}/>
|
|
24
|
+
<ToolbarButton icon={MdContentCopy} title="Copy" onClick={() => {
|
|
25
|
+
const { from, to } = editor.state.selection;
|
|
26
|
+
if (from === to)
|
|
27
|
+
return;
|
|
28
|
+
const selectedText = editor.state.doc.textBetween(from, to);
|
|
29
|
+
navigator.clipboard.writeText(selectedText);
|
|
30
|
+
}}/>
|
|
31
|
+
<ToolbarButton icon={MdFormatPaint} title="Format Painter" onClick={() => {
|
|
32
|
+
const currentMarks = editor.getAttributes("textStyle");
|
|
33
|
+
localStorage.setItem("formatPainter", JSON.stringify(currentMarks));
|
|
34
|
+
}}/>
|
|
35
|
+
</div>);
|
|
36
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Editor } from "@tiptap/react";
|
|
2
|
+
import React from "react";
|
|
2
3
|
type FileGroupProps = {
|
|
3
4
|
editor: Editor;
|
|
4
5
|
};
|
|
5
|
-
export default function FileGroup({ editor }: FileGroupProps):
|
|
6
|
+
export default function FileGroup({ editor }: FileGroupProps): React.JSX.Element;
|
|
6
7
|
export {};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
2
|
import { FaRegFolderOpen } from "react-icons/fa";
|
|
4
3
|
import { VscNewFile } from "react-icons/vsc";
|
|
5
4
|
import ToolbarButton from "./ToolbarButton";
|
|
6
|
-
import { useRef } from "react";
|
|
5
|
+
import React, { useRef } from "react";
|
|
7
6
|
export default function FileGroup({ editor }) {
|
|
8
7
|
const fileInputRef = useRef(null);
|
|
9
8
|
const handleNew = () => {
|
|
@@ -33,5 +32,9 @@ export default function FileGroup({ editor }) {
|
|
|
33
32
|
e.target.value = "";
|
|
34
33
|
}
|
|
35
34
|
};
|
|
36
|
-
return (
|
|
35
|
+
return (<div className="file-group" role="group" aria-label="File actions">
|
|
36
|
+
<input type="file" accept=".json" ref={fileInputRef} onChange={handleFileChange} className="hidden" aria-label="Open JSON file"/>
|
|
37
|
+
<ToolbarButton icon={VscNewFile} onClick={handleNew} title="New"/>
|
|
38
|
+
<ToolbarButton icon={FaRegFolderOpen} onClick={handleOpen} title="Open File"/>
|
|
39
|
+
</div>);
|
|
37
40
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Editor } from "@tiptap/react";
|
|
2
|
+
import React from "react";
|
|
2
3
|
interface FontStyleGroupProps {
|
|
3
4
|
editor: Editor;
|
|
4
5
|
}
|
|
5
|
-
export default function FontStyleGroup({ editor }: FontStyleGroupProps):
|
|
6
|
+
export default function FontStyleGroup({ editor }: FontStyleGroupProps): React.JSX.Element;
|
|
6
7
|
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { MdFormatBold, MdFormatItalic, MdFormatUnderlined, MdStrikethroughS, MdSubscript, MdSuperscript, MdFormatClear, MdFormatPaint, } from "react-icons/md";
|
|
3
|
+
import { ImTextColor } from "react-icons/im";
|
|
4
|
+
import { BiSolidColorFill } from "react-icons/bi";
|
|
5
|
+
import ToolbarButton from "./ToolbarButton";
|
|
6
|
+
import React, { useEffect, useState } from "react";
|
|
7
|
+
export default function FontStyleGroup({ editor }) {
|
|
8
|
+
const [textColor, setTextColor] = useState("#000000");
|
|
9
|
+
const [highlightColor, setHighlightColor] = useState("#ffff00");
|
|
10
|
+
const [fontFamily, setFontFamily] = useState("Arial");
|
|
11
|
+
const [fontSize, setFontSize] = useState("16px");
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!editor)
|
|
14
|
+
return;
|
|
15
|
+
const updateStates = () => {
|
|
16
|
+
var _a, _b, _c;
|
|
17
|
+
const highlight = editor.getAttributes("highlight");
|
|
18
|
+
setHighlightColor((highlight === null || highlight === void 0 ? void 0 : highlight.color) || "#ffff00");
|
|
19
|
+
const color = (_a = editor.getAttributes("textStyle")) === null || _a === void 0 ? void 0 : _a.color;
|
|
20
|
+
setTextColor(color || "#000000");
|
|
21
|
+
const fontAttr = ((_b = editor.getAttributes("fontFamily")) === null || _b === void 0 ? void 0 : _b.font) || "Arial";
|
|
22
|
+
setFontFamily(fontAttr);
|
|
23
|
+
const sizeAttr = ((_c = editor.getAttributes("fontSize")) === null || _c === void 0 ? void 0 : _c.size) || "16px";
|
|
24
|
+
setFontSize(sizeAttr);
|
|
25
|
+
};
|
|
26
|
+
updateStates();
|
|
27
|
+
editor.on("selectionUpdate", updateStates);
|
|
28
|
+
editor.on("transaction", updateStates);
|
|
29
|
+
return () => {
|
|
30
|
+
editor.off("selectionUpdate", updateStates);
|
|
31
|
+
editor.off("transaction", updateStates);
|
|
32
|
+
};
|
|
33
|
+
}, [editor]);
|
|
34
|
+
return (<div className="font-style-group">
|
|
35
|
+
<select title="Font Family" value={fontFamily} onChange={(e) => {
|
|
36
|
+
const value = e.target.value;
|
|
37
|
+
setFontFamily(value);
|
|
38
|
+
editor.chain().focus().setFontFamily(value).run();
|
|
39
|
+
}}>
|
|
40
|
+
<option value="Arial">Arial</option>
|
|
41
|
+
<option value="Georgia">Georgia</option>
|
|
42
|
+
<option value="Times New Roman">Times New Roman</option>
|
|
43
|
+
<option value="Courier New">Courier New</option>
|
|
44
|
+
<option value="Verdana">Verdana</option>
|
|
45
|
+
</select>
|
|
46
|
+
|
|
47
|
+
<select title="Font Size" value={fontSize} onChange={(e) => {
|
|
48
|
+
const value = e.target.value;
|
|
49
|
+
setFontSize(value);
|
|
50
|
+
editor.chain().focus().setFontSize(value).run();
|
|
51
|
+
}}>
|
|
52
|
+
<option value="12px">12</option>
|
|
53
|
+
<option value="14px">14</option>
|
|
54
|
+
<option value="16px">16</option>
|
|
55
|
+
<option value="18px">18</option>
|
|
56
|
+
<option value="24px">24</option>
|
|
57
|
+
<option value="36px">36</option>
|
|
58
|
+
<option value="48px">48</option>
|
|
59
|
+
<option value="64px">64</option>
|
|
60
|
+
<option value="72px">72</option>
|
|
61
|
+
</select>
|
|
62
|
+
|
|
63
|
+
<ToolbarButton icon={MdFormatBold} label="Bold" onClick={() => editor.chain().focus().toggleBold().run()} isActive={editor.isActive("bold")}/>
|
|
64
|
+
<ToolbarButton icon={MdFormatItalic} label="Italic" onClick={() => editor.chain().focus().toggleItalic().run()} isActive={editor.isActive("italic")}/>
|
|
65
|
+
<ToolbarButton icon={MdFormatUnderlined} label="Underline" onClick={() => editor.chain().focus().toggleUnderline().run()} isActive={editor.isActive("underline")}/>
|
|
66
|
+
<ToolbarButton icon={MdStrikethroughS} label="Strikethrough" onClick={() => editor.chain().focus().toggleStrike().run()} isActive={editor.isActive("strike")}/>
|
|
67
|
+
<ToolbarButton icon={MdSubscript} label="Subscript" onClick={() => editor.chain().focus().toggleSubscript().run()} isActive={editor.isActive("subscript")}/>
|
|
68
|
+
<ToolbarButton icon={MdSuperscript} label="Superscript" onClick={() => editor.chain().focus().toggleSuperscript().run()} isActive={editor.isActive("superscript")}/>
|
|
69
|
+
|
|
70
|
+
<label title="Font Color" aria-label="Font Color" className="color-label" style={{ "--indicator-color": textColor }}>
|
|
71
|
+
<ImTextColor size={20}/>
|
|
72
|
+
<div className="color-indicator"/>
|
|
73
|
+
<input type="color" value={textColor} onChange={(e) => {
|
|
74
|
+
const color = e.target.value;
|
|
75
|
+
setTextColor(color);
|
|
76
|
+
editor.chain().focus().setColor(color).run();
|
|
77
|
+
}}/>
|
|
78
|
+
</label>
|
|
79
|
+
|
|
80
|
+
<label title="Highlight Color" aria-label="Highlight Color" className="color-label" style={{ "--indicator-color": highlightColor }}>
|
|
81
|
+
<BiSolidColorFill size={20}/>
|
|
82
|
+
<div className="color-indicator"/>
|
|
83
|
+
<input type="color" value={highlightColor} onChange={(e) => {
|
|
84
|
+
const color = e.target.value;
|
|
85
|
+
setHighlightColor(color);
|
|
86
|
+
editor.chain().focus().setHighlight({ color }).run();
|
|
87
|
+
}}/>
|
|
88
|
+
</label>
|
|
89
|
+
|
|
90
|
+
<ToolbarButton icon={MdFormatClear} label="Clear Formatting" onClick={() => editor.chain().focus().unsetAllMarks().run()}/>
|
|
91
|
+
<ToolbarButton icon={MdFormatPaint} label="Apply Painter Format" onClick={() => {
|
|
92
|
+
const format = JSON.parse(localStorage.getItem("formatPainter") || "{}");
|
|
93
|
+
if (format.color)
|
|
94
|
+
editor.chain().focus().setColor(format.color).run();
|
|
95
|
+
if (format.backgroundColor) {
|
|
96
|
+
editor
|
|
97
|
+
.chain()
|
|
98
|
+
.focus()
|
|
99
|
+
.setHighlight({ color: format.backgroundColor })
|
|
100
|
+
.run();
|
|
101
|
+
}
|
|
102
|
+
}}/>
|
|
103
|
+
</div>);
|
|
104
|
+
}
|