diff --git a/apps/docs/content/docs/ui/auto-form.mdx b/apps/docs/content/docs/ui/auto-form.mdx
index 7e5b2e761..55dd75627 100644
--- a/apps/docs/content/docs/ui/auto-form.mdx
+++ b/apps/docs/content/docs/ui/auto-form.mdx
@@ -12,6 +12,7 @@ description: Component creates form based on Zod schemas & react-hook-form with
```tsx
import { AutoForm } from "@vitnode/core/components/form/auto-form";
import { AutoFormCheckbox } from "@vitnode/core/components/form/fields/checkbox";
+import { AutoFormEditor } from "@vitnode/core/components/form/fields/editor";
import { AutoFormInput } from "@vitnode/core/components/form/fields/input";
import { AutoFormSelect } from "@vitnode/core/components/form/fields/select";
import { AutoFormTextarea } from "@vitnode/core/components/form/fields/textarea";
@@ -40,6 +41,10 @@ const formSchema = z.object({
message: "You must accept the terms and conditions",
}),
description: z.string().min(10, "Description must be at least 10 characters"),
+ content: z
+ .string()
+ .min(1, "Content is required")
+ .default("
Write your content here...
"),
search: z.string().optional(),
});
```
@@ -119,6 +124,16 @@ const formSchema = z.object({
/>
),
},
+ {
+ id: "content",
+ component: props => (
+
+ ),
+ },
{
id: "search",
component: props => (
diff --git a/apps/docs/content/docs/ui/editor.mdx b/apps/docs/content/docs/ui/editor.mdx
new file mode 100644
index 000000000..2d954c5de
--- /dev/null
+++ b/apps/docs/content/docs/ui/editor.mdx
@@ -0,0 +1,112 @@
+---
+title: Editor
+description: Rich text editor built on TipTap for editing and rendering HTML content.
+---
+
+## Preview
+
+
+
+## Usage
+
+import { Tab, Tabs } from "fumadocs-ui/components/tabs";
+
+
+
+
+```ts
+import { z } from "zod";
+import { AutoForm } from "@vitnode/core/components/form/auto-form";
+import { AutoFormEditor } from "@vitnode/core/components/form/fields/editor";
+```
+
+```ts
+const formSchema = z.object({
+ content: z
+ .string()
+ .min(1, "Content is required")
+ .default("Write your content here...
"),
+});
+```
+
+```tsx
+ (
+
+ ),
+ },
+ ]}
+/>
+```
+
+
+
+
+
+```ts
+import { Editor } from "@vitnode/core/components/ui/editor";
+```
+
+The `Editor` is uncontrolled by default — pass `value` as the initial HTML and
+listen for changes with `onChange`, which receives the current HTML string:
+
+```tsx
+const [content, setContent] = useState("Hello World! 🌎️
");
+
+;
+```
+
+
+
+
+## Rendering Content
+
+Use `EditorContent` to render the stored HTML as read-only content outside of
+the editor (e.g. on a public page):
+
+```ts
+import { EditorContent } from "@vitnode/core/components/ui/editor-content";
+```
+
+```tsx
+
+```
+
+## Props
+
+import { TypeTable } from "fumadocs-ui/components/type-table";
+
+ void",
+ default: "",
+ },
+ disableScroll: {
+ description:
+ "Disables the internal max-height scroll area so the editor grows with its content.",
+ type: "boolean",
+ default: "false",
+ },
+ className: {
+ description: "Additional classes applied to the editor wrapper.",
+ type: "string",
+ default: "",
+ },
+ }}
+/>
diff --git a/apps/docs/content/docs/ui/meta.json b/apps/docs/content/docs/ui/meta.json
index 8fc96c8b4..288445241 100644
--- a/apps/docs/content/docs/ui/meta.json
+++ b/apps/docs/content/docs/ui/meta.json
@@ -16,6 +16,7 @@
"auto-form",
"checkbox",
"combobox",
+ "editor",
"input",
"input-group",
"radio-group",
diff --git a/apps/docs/src/examples/auto-form.tsx b/apps/docs/src/examples/auto-form.tsx
index 3a8993858..685901a54 100644
--- a/apps/docs/src/examples/auto-form.tsx
+++ b/apps/docs/src/examples/auto-form.tsx
@@ -2,6 +2,7 @@
import { AutoForm } from "@vitnode/core/components/form/auto-form";
import { AutoFormCheckbox } from "@vitnode/core/components/form/fields/checkbox";
+import { AutoFormEditor } from "@vitnode/core/components/form/fields/editor";
import { AutoFormInput } from "@vitnode/core/components/form/fields/input";
import { AutoFormSelect } from "@vitnode/core/components/form/fields/select";
import { AutoFormTextarea } from "@vitnode/core/components/form/fields/textarea";
@@ -22,6 +23,10 @@ export default function AutoFormExample() {
description: z
.string()
.min(10, "Description must be at least 10 characters"),
+ content: z
+ .string()
+ .min(1, "Content is required")
+ .default("Write your content here...
"),
search: z.string().optional(),
});
@@ -79,6 +84,16 @@ export default function AutoFormExample() {
/>
),
},
+ {
+ id: "content",
+ component: props => (
+
+ ),
+ },
{
id: "search",
component: props => (
diff --git a/apps/docs/src/examples/editor.tsx b/apps/docs/src/examples/editor.tsx
new file mode 100644
index 000000000..1c9242e8c
--- /dev/null
+++ b/apps/docs/src/examples/editor.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import { AutoForm } from "@vitnode/core/components/form/auto-form";
+import { AutoFormEditor } from "@vitnode/core/components/form/fields/editor";
+import { z } from "zod";
+
+export default function EditorExample() {
+ const formSchema = z.object({
+ content: z
+ .string()
+ .min(1, "Content is required")
+ .default("Write your content here...
"),
+ });
+
+ return (
+ (
+
+ ),
+ },
+ ]}
+ formSchema={formSchema}
+ />
+ );
+}
diff --git a/packages/create-vitnode-app/copy-of-vitnode-plugin/root/.swcrc b/packages/create-vitnode-app/copy-of-vitnode-plugin/root/.swcrc
index eba97079c..8f099dc7a 100644
--- a/packages/create-vitnode-app/copy-of-vitnode-plugin/root/.swcrc
+++ b/packages/create-vitnode-app/copy-of-vitnode-plugin/root/.swcrc
@@ -1,5 +1,6 @@
{
"$schema": "https://swc.rs/schema.json",
+ "exclude": ["\\.test\\.tsx?$"],
"minify": true,
"jsc": {
"baseUrl": "./",
diff --git a/packages/create-vitnode-app/copy-of-vitnode-plugin/root/tsconfig.build.json b/packages/create-vitnode-app/copy-of-vitnode-plugin/root/tsconfig.build.json
new file mode 100644
index 000000000..997f1529d
--- /dev/null
+++ b/packages/create-vitnode-app/copy-of-vitnode-plugin/root/tsconfig.build.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "./tsconfig.json",
+ "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx"]
+}
diff --git a/packages/create-vitnode-app/package.json b/packages/create-vitnode-app/package.json
index e75459a0b..b25c161d5 100644
--- a/packages/create-vitnode-app/package.json
+++ b/packages/create-vitnode-app/package.json
@@ -22,7 +22,7 @@
"eslint-react"
],
"scripts": {
- "build:scripts": "tsc && node dist/src/prepare/prepare.js",
+ "build:scripts": "tsc -p tsconfig.build.json && node dist/src/prepare/prepare.js",
"start:scripts": "node dist/src/index.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
diff --git a/packages/create-vitnode-app/src/plugin/create/create-package-json.ts b/packages/create-vitnode-app/src/plugin/create/create-package-json.ts
index c08f862f3..cb9e9d6ed 100644
--- a/packages/create-vitnode-app/src/plugin/create/create-package-json.ts
+++ b/packages/create-vitnode-app/src/plugin/create/create-package-json.ts
@@ -27,8 +27,8 @@ export const createPluginPackageJSON = async ({
type: "module",
scripts: {
"build:plugins":
- "tsc && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.json",
- dev: 'concurrently "tsc -w --preserveWatchOutput" "swc src -d dist --config-file .swcrc -w" "tsc-alias -w" "vitnode plugin --w"',
+ "tsc -p tsconfig.build.json && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.build.json",
+ dev: 'concurrently "tsc -w -p tsconfig.build.json --preserveWatchOutput" "swc src -d dist --config-file .swcrc -w" "tsc-alias -w -p tsconfig.build.json" "vitnode plugin --w"',
"dev:email": "email dev --dir src/emails",
...withIf(eslint, {
lint: "turbo lint",
diff --git a/packages/create-vitnode-app/tsconfig.build.json b/packages/create-vitnode-app/tsconfig.build.json
new file mode 100644
index 000000000..e15e9be99
--- /dev/null
+++ b/packages/create-vitnode-app/tsconfig.build.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "./tsconfig.json",
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"]
+}
diff --git a/packages/node-cron/.swcrc b/packages/node-cron/.swcrc
index eba97079c..8f099dc7a 100644
--- a/packages/node-cron/.swcrc
+++ b/packages/node-cron/.swcrc
@@ -1,5 +1,6 @@
{
"$schema": "https://swc.rs/schema.json",
+ "exclude": ["\\.test\\.tsx?$"],
"minify": true,
"jsc": {
"baseUrl": "./",
diff --git a/packages/node-cron/package.json b/packages/node-cron/package.json
index 973a400cb..625bf1f0b 100644
--- a/packages/node-cron/package.json
+++ b/packages/node-cron/package.json
@@ -25,8 +25,8 @@
}
},
"scripts": {
- "build:plugins": "tsc && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.json",
- "dev:plugins": "concurrently \"tsc -w --preserveWatchOutput\" \"swc src -d dist --config-file .swcrc -w\" \"tsc-alias -w\"",
+ "build:plugins": "tsc -p tsconfig.build.json && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.build.json",
+ "dev:plugins": "concurrently \"tsc -w -p tsconfig.build.json --preserveWatchOutput\" \"swc src -d dist --config-file .swcrc -w\" \"tsc-alias -w -p tsconfig.build.json\"",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
diff --git a/packages/node-cron/tsconfig.build.json b/packages/node-cron/tsconfig.build.json
new file mode 100644
index 000000000..997f1529d
--- /dev/null
+++ b/packages/node-cron/tsconfig.build.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "./tsconfig.json",
+ "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx"]
+}
diff --git a/packages/nodemailer/.swcrc b/packages/nodemailer/.swcrc
index eba97079c..8f099dc7a 100644
--- a/packages/nodemailer/.swcrc
+++ b/packages/nodemailer/.swcrc
@@ -1,5 +1,6 @@
{
"$schema": "https://swc.rs/schema.json",
+ "exclude": ["\\.test\\.tsx?$"],
"minify": true,
"jsc": {
"baseUrl": "./",
diff --git a/packages/nodemailer/package.json b/packages/nodemailer/package.json
index 485b77ab5..99a0ec7b3 100644
--- a/packages/nodemailer/package.json
+++ b/packages/nodemailer/package.json
@@ -24,8 +24,8 @@
}
},
"scripts": {
- "build:plugins": "tsc && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.json",
- "dev:plugins": "concurrently \"tsc -w --preserveWatchOutput\" \"swc src -d dist --config-file .swcrc -w\" \"tsc-alias -w\"",
+ "build:plugins": "tsc -p tsconfig.build.json && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.build.json",
+ "dev:plugins": "concurrently \"tsc -w -p tsconfig.build.json --preserveWatchOutput\" \"swc src -d dist --config-file .swcrc -w\" \"tsc-alias -w -p tsconfig.build.json\"",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
diff --git a/packages/nodemailer/tsconfig.build.json b/packages/nodemailer/tsconfig.build.json
new file mode 100644
index 000000000..997f1529d
--- /dev/null
+++ b/packages/nodemailer/tsconfig.build.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "./tsconfig.json",
+ "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx"]
+}
diff --git a/packages/resend/.swcrc b/packages/resend/.swcrc
index eba97079c..8f099dc7a 100644
--- a/packages/resend/.swcrc
+++ b/packages/resend/.swcrc
@@ -1,5 +1,6 @@
{
"$schema": "https://swc.rs/schema.json",
+ "exclude": ["\\.test\\.tsx?$"],
"minify": true,
"jsc": {
"baseUrl": "./",
diff --git a/packages/resend/package.json b/packages/resend/package.json
index 751f6cb2a..9f58e9061 100644
--- a/packages/resend/package.json
+++ b/packages/resend/package.json
@@ -23,8 +23,8 @@
}
},
"scripts": {
- "build:plugins": "tsc && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.json",
- "dev:plugins": "concurrently \"tsc -w --preserveWatchOutput\" \"swc src -d dist --config-file .swcrc -w\" \"tsc-alias -w\"",
+ "build:plugins": "tsc -p tsconfig.build.json && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.build.json",
+ "dev:plugins": "concurrently \"tsc -w -p tsconfig.build.json --preserveWatchOutput\" \"swc src -d dist --config-file .swcrc -w\" \"tsc-alias -w -p tsconfig.build.json\"",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
diff --git a/packages/resend/tsconfig.build.json b/packages/resend/tsconfig.build.json
new file mode 100644
index 000000000..997f1529d
--- /dev/null
+++ b/packages/resend/tsconfig.build.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "./tsconfig.json",
+ "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx"]
+}
diff --git a/packages/s3/.swcrc b/packages/s3/.swcrc
index eba97079c..8f099dc7a 100644
--- a/packages/s3/.swcrc
+++ b/packages/s3/.swcrc
@@ -1,5 +1,6 @@
{
"$schema": "https://swc.rs/schema.json",
+ "exclude": ["\\.test\\.tsx?$"],
"minify": true,
"jsc": {
"baseUrl": "./",
diff --git a/packages/s3/package.json b/packages/s3/package.json
index 547d26574..19db36a0f 100644
--- a/packages/s3/package.json
+++ b/packages/s3/package.json
@@ -25,8 +25,8 @@
}
},
"scripts": {
- "build:plugins": "tsc && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.json",
- "dev:plugins": "concurrently \"tsc -w --preserveWatchOutput\" \"swc src -d dist --config-file .swcrc -w\" \"tsc-alias -w\"",
+ "build:plugins": "tsc -p tsconfig.build.json && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.build.json",
+ "dev:plugins": "concurrently \"tsc -w -p tsconfig.build.json --preserveWatchOutput\" \"swc src -d dist --config-file .swcrc -w\" \"tsc-alias -w -p tsconfig.build.json\"",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
diff --git a/packages/s3/tsconfig.build.json b/packages/s3/tsconfig.build.json
new file mode 100644
index 000000000..997f1529d
--- /dev/null
+++ b/packages/s3/tsconfig.build.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "./tsconfig.json",
+ "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx"]
+}
diff --git a/packages/supabase-storage/.swcrc b/packages/supabase-storage/.swcrc
index eba97079c..8f099dc7a 100644
--- a/packages/supabase-storage/.swcrc
+++ b/packages/supabase-storage/.swcrc
@@ -1,5 +1,6 @@
{
"$schema": "https://swc.rs/schema.json",
+ "exclude": ["\\.test\\.tsx?$"],
"minify": true,
"jsc": {
"baseUrl": "./",
diff --git a/packages/supabase-storage/package.json b/packages/supabase-storage/package.json
index 520ab2b59..0cb93a69c 100644
--- a/packages/supabase-storage/package.json
+++ b/packages/supabase-storage/package.json
@@ -24,8 +24,8 @@
}
},
"scripts": {
- "build:plugins": "tsc && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.json",
- "dev:plugins": "concurrently \"tsc -w --preserveWatchOutput\" \"swc src -d dist --config-file .swcrc -w\" \"tsc-alias -w\"",
+ "build:plugins": "tsc -p tsconfig.build.json && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.build.json",
+ "dev:plugins": "concurrently \"tsc -w -p tsconfig.build.json --preserveWatchOutput\" \"swc src -d dist --config-file .swcrc -w\" \"tsc-alias -w -p tsconfig.build.json\"",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
diff --git a/packages/supabase-storage/tsconfig.build.json b/packages/supabase-storage/tsconfig.build.json
new file mode 100644
index 000000000..997f1529d
--- /dev/null
+++ b/packages/supabase-storage/tsconfig.build.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "./tsconfig.json",
+ "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx"]
+}
diff --git a/packages/vitnode/.swcrc b/packages/vitnode/.swcrc
index eba97079c..8f099dc7a 100644
--- a/packages/vitnode/.swcrc
+++ b/packages/vitnode/.swcrc
@@ -1,5 +1,6 @@
{
"$schema": "https://swc.rs/schema.json",
+ "exclude": ["\\.test\\.tsx?$"],
"minify": true,
"jsc": {
"baseUrl": "./",
diff --git a/packages/vitnode/package.json b/packages/vitnode/package.json
index 0c09772fb..95dcddb18 100644
--- a/packages/vitnode/package.json
+++ b/packages/vitnode/package.json
@@ -98,8 +98,8 @@
"scripts": {
"build:scripts": "tsup",
"start:scripts": "node dist/scripts/scripts.js init",
- "build:plugins": "tsc && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.json",
- "dev": "concurrently \"tsc -w --preserveWatchOutput\" \"swc src -d dist --config-file .swcrc -w\" \"tsc-alias -w\" \"node dist/scripts/scripts.js plugin --w\"",
+ "build:plugins": "tsc -p tsconfig.build.json && swc src -d dist --config-file .swcrc && tsc-alias -p tsconfig.build.json",
+ "dev": "concurrently \"tsc -w -p tsconfig.build.json --preserveWatchOutput\" \"swc src -d dist --config-file .swcrc -w\" \"tsc-alias -w -p tsconfig.build.json\" \"node dist/scripts/scripts.js plugin --w\"",
"dev:email": "email dev --dir src/emails",
"test": "vitest run",
"test:watch": "vitest",
diff --git a/packages/vitnode/src/components/form/fields/editor.test.tsx b/packages/vitnode/src/components/form/fields/editor.test.tsx
new file mode 100644
index 000000000..dcd0bcb55
--- /dev/null
+++ b/packages/vitnode/src/components/form/fields/editor.test.tsx
@@ -0,0 +1,113 @@
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { type FieldValues, useForm } from "react-hook-form";
+import { describe, expect, it, vi } from "vitest";
+
+import { Form, FormField } from "@/components/ui/form";
+
+import { AutoFormEditor } from "./editor";
+
+vi.mock("next-intl", () => ({
+ useLocale: () => "en",
+ useTranslations: () => (key: string) => key,
+}));
+
+vi.mock("@/components/ui/editor", () => ({
+ Editor: ({
+ value,
+ onChange,
+ onBlur,
+ ...props
+ }: {
+ onBlur?: () => void;
+ onChange?: (value: string) => void;
+ value?: string;
+ }) => (
+