@opensaas/stack-tiptap 0.1.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.
@@ -0,0 +1,8 @@
1
+
2
+ > @opensaas/stack-tiptap@0.1.0 build /home/runner/work/stack/stack/packages/tiptap
3
+ > tsc && npm run copy:css
4
+
5
+
6
+ > @opensaas/stack-tiptap@0.1.0 copy:css
7
+ > mkdir -p dist/styles && cp src/styles/tiptap.css dist/styles/
8
+
package/README.md ADDED
@@ -0,0 +1,335 @@
1
+ # @opensaas/stack-tiptap
2
+
3
+ Rich text editor integration for OpenSaas Stack using [Tiptap](https://tiptap.dev).
4
+
5
+ ## Features
6
+
7
+ - ✅ Rich text editing with Tiptap editor
8
+ - ✅ JSON storage in database
9
+ - ✅ SSR-safe Next.js integration
10
+ - ✅ Edit and read-only modes
11
+ - ✅ Customizable toolbar and UI options
12
+ - ✅ Full TypeScript support
13
+ - ✅ Integrates with OpenSaas access control
14
+
15
+ ## Installation
16
+
17
+ This package is designed as a separate optional dependency to keep the core stack lightweight.
18
+
19
+ ```bash
20
+ pnpm add @opensaas/stack-tiptap
21
+ ```
22
+
23
+ The following peer dependencies are required:
24
+
25
+ - `@opensaas/stack-core`
26
+ - `@opensaas/stack-ui`
27
+ - `next`
28
+ - `react`
29
+ - `react-dom`
30
+
31
+ ## Usage
32
+
33
+ ### Basic Setup
34
+
35
+ 1. **Register the field component** on the client side:
36
+
37
+ ```typescript
38
+ // lib/register-fields.ts
39
+ 'use client'
40
+
41
+ import { registerFieldComponent } from '@opensaas/stack-ui'
42
+ import { TiptapField } from '@opensaas/stack-tiptap'
43
+
44
+ registerFieldComponent('richText', TiptapField)
45
+ ```
46
+
47
+ 2. **Import the registration in your admin page**:
48
+
49
+ ```typescript
50
+ // app/admin/[[...admin]]/page.tsx
51
+ import { AdminUI } from "@opensaas/stack-ui";
52
+ import config from "../../../opensaas.config";
53
+ import "../../../lib/register-fields"; // Import to trigger registration
54
+
55
+ export default async function AdminPage() {
56
+ // ... your code
57
+ return <AdminUI config={config} />;
58
+ }
59
+ ```
60
+
61
+ 3. **Define your schema** with the `richText` field builder:
62
+
63
+ ```typescript
64
+ // opensaas.config.ts
65
+ import { config, list } from '@opensaas/stack-core'
66
+ import { text } from '@opensaas/stack-core/fields'
67
+ import { richText } from '@opensaas/stack-tiptap/fields'
68
+
69
+ export default config({
70
+ db: {
71
+ provider: 'sqlite',
72
+ url: 'file:./dev.db',
73
+ },
74
+ lists: {
75
+ Article: list({
76
+ fields: {
77
+ title: text({ validation: { isRequired: true } }),
78
+ content: richText({
79
+ validation: { isRequired: true },
80
+ }),
81
+ },
82
+ }),
83
+ },
84
+ })
85
+ ```
86
+
87
+ 4. Generate Prisma schema:
88
+
89
+ ```bash
90
+ pnpm generate
91
+ ```
92
+
93
+ This will create a Prisma field with type `Json`:
94
+
95
+ ```prisma
96
+ model Article {
97
+ id String @id @default(cuid())
98
+ title String
99
+ content Json // Tiptap JSON content
100
+ createdAt DateTime @default(now())
101
+ updatedAt DateTime @updatedAt
102
+ }
103
+ ```
104
+
105
+ ### Field Options
106
+
107
+ #### Validation
108
+
109
+ ```typescript
110
+ content: richText({
111
+ validation: {
112
+ isRequired: true, // Make field required
113
+ },
114
+ })
115
+ ```
116
+
117
+ #### UI Customization
118
+
119
+ ```typescript
120
+ content: richText({
121
+ ui: {
122
+ placeholder: 'Start writing...',
123
+ minHeight: 200, // Minimum editor height in pixels
124
+ maxHeight: 800, // Maximum editor height (scrollable)
125
+ },
126
+ })
127
+ ```
128
+
129
+ ### Access Control
130
+
131
+ Rich text fields work seamlessly with OpenSaas access control:
132
+
133
+ ```typescript
134
+ Article: list({
135
+ fields: {
136
+ content: richText({
137
+ validation: { isRequired: true },
138
+ access: {
139
+ read: () => true,
140
+ create: isSignedIn,
141
+ update: isAuthor,
142
+ },
143
+ }),
144
+ },
145
+ })
146
+ ```
147
+
148
+ ### Database Operations
149
+
150
+ Content is stored as JSON and can be queried using Prisma's JSON operations:
151
+
152
+ ```typescript
153
+ import { prisma } from './lib/context'
154
+
155
+ // Create article with rich text
156
+ const article = await prisma.article.create({
157
+ data: {
158
+ title: 'My Article',
159
+ content: {
160
+ type: 'doc',
161
+ content: [
162
+ {
163
+ type: 'paragraph',
164
+ content: [{ type: 'text', text: 'Hello world!' }],
165
+ },
166
+ ],
167
+ },
168
+ },
169
+ })
170
+
171
+ // Query articles
172
+ const articles = await prisma.article.findMany({
173
+ select: {
174
+ title: true,
175
+ content: true,
176
+ },
177
+ })
178
+ ```
179
+
180
+ ## Component Features
181
+
182
+ The `TiptapField` component includes:
183
+
184
+ ### Text Formatting
185
+
186
+ - **Bold**
187
+ - _Italic_
188
+ - ~~Strike-through~~
189
+
190
+ ### Headings
191
+
192
+ - H1, H2, H3
193
+
194
+ ### Lists
195
+
196
+ - Bullet lists
197
+ - Ordered lists
198
+
199
+ ### Blockquotes
200
+
201
+ - Quote blocks
202
+
203
+ ### Modes
204
+
205
+ - **Edit mode**: Full toolbar with all formatting options
206
+ - **Read mode**: Render-only view (no toolbar)
207
+
208
+ ## Advanced Usage
209
+
210
+ ### Custom Field Component
211
+
212
+ Create a custom Tiptap component with additional extensions:
213
+
214
+ ```typescript
215
+ // components/CustomTiptapField.tsx
216
+ "use client";
217
+
218
+ import { useEditor, EditorContent } from "@tiptap/react";
219
+ import StarterKit from "@tiptap/starter-kit";
220
+ import Link from "@tiptap/extension-link";
221
+ import Image from "@tiptap/extension-image";
222
+
223
+ export function CustomTiptapField(props) {
224
+ const editor = useEditor({
225
+ extensions: [
226
+ StarterKit,
227
+ Link,
228
+ Image,
229
+ ],
230
+ content: props.value,
231
+ immediatelyRender: false,
232
+ onUpdate: ({ editor }) => {
233
+ props.onChange(editor.getJSON());
234
+ },
235
+ });
236
+
237
+ return <EditorContent editor={editor} />;
238
+ }
239
+ ```
240
+
241
+ Then use it in your config:
242
+
243
+ ```typescript
244
+ import { registerFieldComponent } from '@opensaas/stack-ui'
245
+ import { CustomTiptapField } from './components/CustomTiptapField'
246
+
247
+ // Global registration
248
+ registerFieldComponent('richTextExtended', CustomTiptapField)
249
+
250
+ // Use in config
251
+ fields: {
252
+ content: richText({
253
+ ui: { fieldType: 'richTextExtended' },
254
+ })
255
+ }
256
+
257
+ // Or per-field override
258
+ fields: {
259
+ content: richText({
260
+ ui: { component: CustomTiptapField },
261
+ })
262
+ }
263
+ ```
264
+
265
+ ## Architecture
266
+
267
+ This package follows OpenSaas's extensibility pattern:
268
+
269
+ 1. **Field Builder** (`richText()`) - Defines field configuration
270
+ - Returns `RichTextField` type
271
+ - Implements `getZodSchema()`, `getPrismaType()`, `getTypeScriptType()`
272
+ - Stores data as `Json` in Prisma
273
+
274
+ 2. **React Component** (`TiptapField`) - UI implementation
275
+ - Client component with `"use client"` directive
276
+ - SSR-safe with `immediatelyRender: false`
277
+ - Supports edit and read modes
278
+
279
+ 3. **No Core Modifications** - Extends stack without changes
280
+ - Uses `BaseFieldConfig` extension point
281
+ - Compatible with access control system
282
+ - Works with hooks and validation
283
+
284
+ ## Example
285
+
286
+ See `examples/tiptap-demo` for a complete working example demonstrating:
287
+
288
+ - Multiple rich text fields
289
+ - Custom UI options
290
+ - Access control integration
291
+ - Database operations
292
+
293
+ ## API Reference
294
+
295
+ ### `richText(options?)`
296
+
297
+ Creates a rich text field configuration.
298
+
299
+ **Options:**
300
+
301
+ - `validation.isRequired` - Make field required (default: `false`)
302
+ - `ui.placeholder` - Placeholder text (default: `"Start writing..."`)
303
+ - `ui.minHeight` - Minimum editor height in pixels (default: `200`)
304
+ - `ui.maxHeight` - Maximum editor height in pixels (default: `undefined`)
305
+ - `ui.component` - Custom React component
306
+ - `ui.fieldType` - Global field type name
307
+ - `access` - Field-level access control
308
+
309
+ **Returns:** `RichTextField`
310
+
311
+ ### `TiptapField` Component
312
+
313
+ React component for rendering the Tiptap editor.
314
+
315
+ **Props:**
316
+
317
+ - `name: string` - Field name
318
+ - `value: any` - JSON content value
319
+ - `onChange: (value: any) => void` - Change handler
320
+ - `label: string` - Field label
321
+ - `error?: string` - Validation error message
322
+ - `disabled?: boolean` - Disable editing
323
+ - `required?: boolean` - Show required indicator
324
+ - `mode?: "read" | "edit"` - Display mode
325
+ - `placeholder?: string` - Placeholder text
326
+ - `minHeight?: number` - Minimum height
327
+ - `maxHeight?: number` - Maximum height
328
+
329
+ ## Contributing
330
+
331
+ Contributions are welcome! This package is part of the OpenSaas Stack monorepo.
332
+
333
+ ## License
334
+
335
+ MIT
@@ -0,0 +1,21 @@
1
+ import { UseEditorOptions } from '@tiptap/react';
2
+ import '../styles/tiptap.css';
3
+ export interface TiptapFieldProps {
4
+ name: string;
5
+ value: UseEditorOptions['content'];
6
+ onChange: UseEditorOptions['onUpdate'];
7
+ label: string;
8
+ error?: string;
9
+ disabled?: boolean;
10
+ required?: boolean;
11
+ mode?: 'read' | 'edit';
12
+ placeholder?: string;
13
+ minHeight?: number;
14
+ maxHeight?: number;
15
+ }
16
+ /**
17
+ * Tiptap rich text editor field component
18
+ * Supports both edit and read-only modes with JSON content storage
19
+ */
20
+ export declare function TiptapField({ name, value, onChange, label, error, disabled, required, mode, placeholder, minHeight, maxHeight, }: TiptapFieldProps): import("react/jsx-runtime").JSX.Element;
21
+ //# sourceMappingURL=TiptapField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TiptapField.d.ts","sourceRoot":"","sources":["../../src/components/TiptapField.tsx"],"names":[],"mappings":"AAEA,OAAO,EAA4B,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAI1E,OAAO,sBAAsB,CAAA;AAE7B,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,gBAAgB,CAAC,SAAS,CAAC,CAAA;IAClC,QAAQ,EAAE,gBAAgB,CAAC,UAAU,CAAC,CAAA;IACtC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAa,EACb,WAAgC,EAChC,SAAe,EACf,SAAS,GACV,EAAE,gBAAgB,2CA0JlB"}
@@ -0,0 +1,60 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEditor, EditorContent } from '@tiptap/react';
4
+ import StarterKit from '@tiptap/starter-kit';
5
+ import Placeholder from '@tiptap/extension-placeholder';
6
+ import { useEffect } from 'react';
7
+ import '../styles/tiptap.css';
8
+ /**
9
+ * Tiptap rich text editor field component
10
+ * Supports both edit and read-only modes with JSON content storage
11
+ */
12
+ export function TiptapField({ name, value, onChange, label, error, disabled, required, mode = 'edit', placeholder = 'Start writing...', minHeight = 200, maxHeight, }) {
13
+ const isEditable = mode === 'edit' && !disabled;
14
+ const editor = useEditor({
15
+ extensions: [
16
+ StarterKit.configure({
17
+ heading: {
18
+ levels: [1, 2, 3],
19
+ },
20
+ }),
21
+ Placeholder.configure({
22
+ placeholder,
23
+ }),
24
+ ],
25
+ content: value || undefined,
26
+ editable: isEditable,
27
+ // Don't render immediately on the server to avoid SSR issues
28
+ immediatelyRender: false,
29
+ onUpdate: (props) => {
30
+ if (isEditable && onChange) {
31
+ onChange(props);
32
+ }
33
+ },
34
+ editorProps: {
35
+ attributes: {
36
+ class: 'tiptap',
37
+ },
38
+ },
39
+ });
40
+ // Update content when value changes externally
41
+ useEffect(() => {
42
+ if (editor && value !== editor.getJSON()) {
43
+ editor.commands.setContent(value || '');
44
+ }
45
+ }, [editor, value]);
46
+ // Update editable state when mode or disabled changes
47
+ useEffect(() => {
48
+ if (editor) {
49
+ editor.setEditable(isEditable);
50
+ }
51
+ }, [editor, isEditable]);
52
+ if (mode === 'read') {
53
+ return (_jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-sm font-medium text-foreground", children: label }), _jsx("div", { className: "tiptap-read-only", children: _jsx(EditorContent, { editor: editor }) })] }));
54
+ }
55
+ return (_jsxs("div", { className: "space-y-2", children: [_jsxs("label", { htmlFor: name, className: "text-sm font-medium text-foreground flex items-center gap-1", children: [label, required && _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs("div", { className: `tiptap-editor ${error ? 'border-destructive' : ''}`, children: [isEditable && editor && (_jsxs("div", { className: "tiptap-toolbar", children: [_jsx("button", { type: "button", onClick: () => editor.chain().focus().toggleBold().run(), disabled: !editor.can().chain().focus().toggleBold().run(), className: editor.isActive('bold') ? 'is-active' : '', children: _jsx("strong", { children: "Bold" }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().toggleItalic().run(), disabled: !editor.can().chain().focus().toggleItalic().run(), className: editor.isActive('italic') ? 'is-active' : '', children: _jsx("em", { children: "Italic" }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().toggleStrike().run(), disabled: !editor.can().chain().focus().toggleStrike().run(), className: editor.isActive('strike') ? 'is-active' : '', children: _jsx("s", { children: "Strike" }) }), _jsx("div", { className: "tiptap-divider" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), className: editor.isActive('heading', { level: 1 }) ? 'is-active' : '', children: "H1" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), className: editor.isActive('heading', { level: 2 }) ? 'is-active' : '', children: "H2" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), className: editor.isActive('heading', { level: 3 }) ? 'is-active' : '', children: "H3" }), _jsx("div", { className: "tiptap-divider" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().toggleBulletList().run(), className: editor.isActive('bulletList') ? 'is-active' : '', children: "Bullet List" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().toggleOrderedList().run(), className: editor.isActive('orderedList') ? 'is-active' : '', children: "Ordered List" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().toggleBlockquote().run(), className: editor.isActive('blockquote') ? 'is-active' : '', children: "Quote" })] })), _jsx("div", { className: disabled ? 'opacity-50 cursor-not-allowed' : '', style: {
56
+ minHeight: `${minHeight}px`,
57
+ maxHeight: maxHeight ? `${maxHeight}px` : undefined,
58
+ overflowY: maxHeight ? 'auto' : undefined,
59
+ }, children: _jsx(EditorContent, { editor: editor }) })] }), error && _jsx("p", { className: "text-sm text-destructive", children: error })] }));
60
+ }
@@ -0,0 +1,3 @@
1
+ export { TiptapField } from './TiptapField.js';
2
+ export type { TiptapFieldProps } from './TiptapField.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1 @@
1
+ export { TiptapField } from './TiptapField.js';
@@ -0,0 +1,34 @@
1
+ import type { BaseFieldConfig } from '@opensaas/stack-core';
2
+ /**
3
+ * Rich text field configuration using Tiptap editor
4
+ * Stores content as JSON in the database
5
+ */
6
+ export type RichTextField = BaseFieldConfig & {
7
+ type: 'richText';
8
+ validation?: {
9
+ isRequired?: boolean;
10
+ };
11
+ ui?: {
12
+ /**
13
+ * Placeholder text for empty editor
14
+ */
15
+ placeholder?: string;
16
+ /**
17
+ * Minimum height for editor in pixels
18
+ */
19
+ minHeight?: number;
20
+ /**
21
+ * Maximum height for editor in pixels
22
+ */
23
+ maxHeight?: number;
24
+ /**
25
+ * Custom React component to render this field
26
+ */
27
+ component?: any;
28
+ /**
29
+ * Custom field type name to use from the global registry
30
+ */
31
+ fieldType?: string;
32
+ };
33
+ };
34
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAE3D;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,eAAe,GAAG;IAC5C,IAAI,EAAE,UAAU,CAAA;IAChB,UAAU,CAAC,EAAE;QACX,UAAU,CAAC,EAAE,OAAO,CAAA;KACrB,CAAA;IACD,EAAE,CAAC,EAAE;QACH;;WAEG;QACH,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB;;WAEG;QACH,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB;;WAEG;QACH,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB;;WAEG;QAEH,SAAS,CAAC,EAAE,GAAG,CAAA;QACf;;WAEG;QACH,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,CAAA;CACF,CAAA"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export { richText } from './richText.js';
2
+ export type { RichTextField } from '../config/types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fields/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA"}
@@ -0,0 +1 @@
1
+ export { richText } from './richText.js';
@@ -0,0 +1,22 @@
1
+ import type { RichTextField } from '../config/types.js';
2
+ /**
3
+ * Rich text field using Tiptap editor
4
+ * Stores content as JSON in the database
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { richText } from '@opensaas/stack-tiptap/fields'
9
+ *
10
+ * fields: {
11
+ * content: richText({
12
+ * validation: { isRequired: true },
13
+ * ui: {
14
+ * placeholder: "Write your content here...",
15
+ * minHeight: 200
16
+ * }
17
+ * })
18
+ * }
19
+ * ```
20
+ */
21
+ export declare function richText(options?: Omit<RichTextField, 'type'>): RichTextField;
22
+ //# sourceMappingURL=richText.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"richText.d.ts","sourceRoot":"","sources":["../../src/fields/richText.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAEvD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,GAAG,aAAa,CAwC7E"}
@@ -0,0 +1,59 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Rich text field using Tiptap editor
4
+ * Stores content as JSON in the database
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { richText } from '@opensaas/stack-tiptap/fields'
9
+ *
10
+ * fields: {
11
+ * content: richText({
12
+ * validation: { isRequired: true },
13
+ * ui: {
14
+ * placeholder: "Write your content here...",
15
+ * minHeight: 200
16
+ * }
17
+ * })
18
+ * }
19
+ * ```
20
+ */
21
+ export function richText(options) {
22
+ return {
23
+ type: 'richText',
24
+ ...options,
25
+ getZodSchema: (fieldName, operation) => {
26
+ const validation = options?.validation;
27
+ const isRequired = validation?.isRequired;
28
+ // Accept any valid JSON structure from Tiptap
29
+ // Tiptap outputs JSONContent which is a complex nested structure
30
+ const baseSchema = z.any();
31
+ if (isRequired && operation === 'create') {
32
+ // For create, reject undefined
33
+ return baseSchema;
34
+ }
35
+ else if (isRequired && operation === 'update') {
36
+ // For update, allow undefined (partial updates)
37
+ return z.union([baseSchema, z.undefined()]);
38
+ }
39
+ else {
40
+ // Not required
41
+ return baseSchema.optional();
42
+ }
43
+ },
44
+ getPrismaType: () => {
45
+ const isRequired = options?.validation?.isRequired;
46
+ return {
47
+ type: 'Json',
48
+ modifiers: isRequired ? undefined : '?',
49
+ };
50
+ },
51
+ getTypeScriptType: () => {
52
+ const isRequired = options?.validation?.isRequired;
53
+ return {
54
+ type: 'any',
55
+ optional: !isRequired,
56
+ };
57
+ },
58
+ };
59
+ }
@@ -0,0 +1,5 @@
1
+ export { richText } from './fields/richText.js';
2
+ export { TiptapField } from './components/TiptapField.js';
3
+ export type { RichTextField } from './config/types.js';
4
+ export type { TiptapFieldProps } from './components/TiptapField.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAG/C,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AAGzD,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtD,YAAY,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // Field builder
2
+ export { richText } from './fields/richText.js';
3
+ // Components
4
+ export { TiptapField } from './components/TiptapField.js';
@@ -0,0 +1,170 @@
1
+ /* Tiptap Editor Styles */
2
+
3
+ /* Editor container */
4
+ .tiptap-editor {
5
+ border: 1px solid hsl(var(--border));
6
+ border-radius: 0.5rem;
7
+ background: hsl(var(--background));
8
+ }
9
+
10
+ /* Toolbar */
11
+ .tiptap-toolbar {
12
+ display: flex;
13
+ flex-wrap: wrap;
14
+ gap: 0.25rem;
15
+ padding: 0.5rem;
16
+ border-bottom: 1px solid hsl(var(--border));
17
+ background: hsl(var(--muted) / 0.3);
18
+ border-radius: 0.5rem 0.5rem 0 0;
19
+ }
20
+
21
+ .tiptap-toolbar button {
22
+ padding: 0.5rem 0.75rem;
23
+ font-size: 0.875rem;
24
+ border-radius: 0.375rem;
25
+ background: transparent;
26
+ border: none;
27
+ color: hsl(var(--foreground));
28
+ cursor: pointer;
29
+ transition: background-color 0.2s;
30
+ }
31
+
32
+ .tiptap-toolbar button:hover:not(:disabled) {
33
+ background: hsl(var(--accent));
34
+ }
35
+
36
+ .tiptap-toolbar button:disabled {
37
+ opacity: 0.5;
38
+ cursor: not-allowed;
39
+ }
40
+
41
+ .tiptap-toolbar button.is-active {
42
+ background: hsl(var(--accent));
43
+ font-weight: 600;
44
+ }
45
+
46
+ .tiptap-divider {
47
+ width: 1px;
48
+ height: 1.5rem;
49
+ background: hsl(var(--border));
50
+ margin: 0 0.25rem;
51
+ }
52
+
53
+ /* Editor content */
54
+ .tiptap {
55
+ padding: 1rem;
56
+ outline: none;
57
+ min-height: 200px;
58
+ }
59
+
60
+ .tiptap:focus {
61
+ outline: none;
62
+ }
63
+
64
+ /* Prose styles for rich text content */
65
+ .tiptap p {
66
+ margin: 0.75rem 0;
67
+ }
68
+
69
+ .tiptap p:first-child {
70
+ margin-top: 0;
71
+ }
72
+
73
+ .tiptap p:last-child {
74
+ margin-bottom: 0;
75
+ }
76
+
77
+ .tiptap h1 {
78
+ font-size: 2rem;
79
+ font-weight: 700;
80
+ margin: 1.5rem 0 1rem;
81
+ line-height: 1.2;
82
+ }
83
+
84
+ .tiptap h2 {
85
+ font-size: 1.5rem;
86
+ font-weight: 600;
87
+ margin: 1.25rem 0 0.75rem;
88
+ line-height: 1.3;
89
+ }
90
+
91
+ .tiptap h3 {
92
+ font-size: 1.25rem;
93
+ font-weight: 600;
94
+ margin: 1rem 0 0.5rem;
95
+ line-height: 1.4;
96
+ }
97
+
98
+ .tiptap ul,
99
+ .tiptap ol {
100
+ padding-left: 1.5rem;
101
+ margin: 0.75rem 0;
102
+ }
103
+
104
+ .tiptap ul {
105
+ list-style-type: disc;
106
+ }
107
+
108
+ .tiptap ol {
109
+ list-style-type: decimal;
110
+ }
111
+
112
+ .tiptap li {
113
+ margin: 0.25rem 0;
114
+ }
115
+
116
+ .tiptap blockquote {
117
+ border-left: 3px solid hsl(var(--border));
118
+ padding-left: 1rem;
119
+ margin: 1rem 0;
120
+ color: hsl(var(--muted-foreground));
121
+ font-style: italic;
122
+ }
123
+
124
+ .tiptap code {
125
+ background: hsl(var(--muted));
126
+ padding: 0.125rem 0.25rem;
127
+ border-radius: 0.25rem;
128
+ font-size: 0.875em;
129
+ font-family: monospace;
130
+ }
131
+
132
+ .tiptap pre {
133
+ background: hsl(var(--muted));
134
+ padding: 1rem;
135
+ border-radius: 0.5rem;
136
+ overflow-x: auto;
137
+ margin: 1rem 0;
138
+ }
139
+
140
+ .tiptap pre code {
141
+ background: transparent;
142
+ padding: 0;
143
+ }
144
+
145
+ /* Placeholder */
146
+ .tiptap p.is-editor-empty:first-child::before {
147
+ content: attr(data-placeholder);
148
+ float: left;
149
+ color: hsl(var(--muted-foreground));
150
+ pointer-events: none;
151
+ height: 0;
152
+ }
153
+
154
+ /* Selection */
155
+ .tiptap ::selection {
156
+ background: hsl(var(--accent));
157
+ }
158
+
159
+ /* Read-only mode */
160
+ .tiptap-read-only {
161
+ border: 1px solid hsl(var(--border));
162
+ border-radius: 0.5rem;
163
+ background: hsl(var(--muted) / 0.5);
164
+ padding: 1rem;
165
+ }
166
+
167
+ .tiptap-read-only .tiptap {
168
+ padding: 0;
169
+ min-height: auto;
170
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@opensaas/stack-tiptap",
3
+ "version": "0.1.0",
4
+ "description": "Tiptap rich text editor integration for OpenSaas Stack",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./fields": {
14
+ "types": "./dist/fields/index.d.ts",
15
+ "default": "./dist/fields/index.js"
16
+ }
17
+ },
18
+ "keywords": [
19
+ "opensaas",
20
+ "tiptap",
21
+ "rich-text",
22
+ "editor",
23
+ "nextjs"
24
+ ],
25
+ "author": "",
26
+ "license": "MIT",
27
+ "peerDependencies": {
28
+ "next": "^15.0.0 || ^16.0.0",
29
+ "react": "^19.0.0",
30
+ "react-dom": "^19.0.0",
31
+ "@opensaas/stack-core": "0.1.0",
32
+ "@opensaas/stack-ui": "0.1.0"
33
+ },
34
+ "dependencies": {
35
+ "@tiptap/react": "^3.7.2",
36
+ "@tiptap/pm": "^3.7.2",
37
+ "@tiptap/starter-kit": "^3.7.2",
38
+ "@tiptap/extension-placeholder": "^3.7.2",
39
+ "zod": "^4.1.12"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^24.7.2",
43
+ "@types/react": "^19.2.2",
44
+ "@types/react-dom": "^19.2.2",
45
+ "typescript": "^5.9.3",
46
+ "@opensaas/stack-core": "0.1.0",
47
+ "@opensaas/stack-ui": "0.1.0"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc && npm run copy:css",
51
+ "copy:css": "mkdir -p dist/styles && cp src/styles/tiptap.css dist/styles/",
52
+ "dev": "tsc --watch",
53
+ "clean": "rm -rf .turbo dist tsconfig.tsbuildinfo"
54
+ }
55
+ }
@@ -0,0 +1,193 @@
1
+ 'use client'
2
+
3
+ import { useEditor, EditorContent, UseEditorOptions } from '@tiptap/react'
4
+ import StarterKit from '@tiptap/starter-kit'
5
+ import Placeholder from '@tiptap/extension-placeholder'
6
+ import { useEffect } from 'react'
7
+ import '../styles/tiptap.css'
8
+
9
+ export interface TiptapFieldProps {
10
+ name: string
11
+ value: UseEditorOptions['content']
12
+ onChange: UseEditorOptions['onUpdate']
13
+ label: string
14
+ error?: string
15
+ disabled?: boolean
16
+ required?: boolean
17
+ mode?: 'read' | 'edit'
18
+ placeholder?: string
19
+ minHeight?: number
20
+ maxHeight?: number
21
+ }
22
+
23
+ /**
24
+ * Tiptap rich text editor field component
25
+ * Supports both edit and read-only modes with JSON content storage
26
+ */
27
+ export function TiptapField({
28
+ name,
29
+ value,
30
+ onChange,
31
+ label,
32
+ error,
33
+ disabled,
34
+ required,
35
+ mode = 'edit',
36
+ placeholder = 'Start writing...',
37
+ minHeight = 200,
38
+ maxHeight,
39
+ }: TiptapFieldProps) {
40
+ const isEditable = mode === 'edit' && !disabled
41
+
42
+ const editor = useEditor({
43
+ extensions: [
44
+ StarterKit.configure({
45
+ heading: {
46
+ levels: [1, 2, 3],
47
+ },
48
+ }),
49
+ Placeholder.configure({
50
+ placeholder,
51
+ }),
52
+ ],
53
+ content: value || undefined,
54
+ editable: isEditable,
55
+ // Don't render immediately on the server to avoid SSR issues
56
+ immediatelyRender: false,
57
+ onUpdate: (props) => {
58
+ if (isEditable && onChange) {
59
+ onChange(props)
60
+ }
61
+ },
62
+ editorProps: {
63
+ attributes: {
64
+ class: 'tiptap',
65
+ },
66
+ },
67
+ })
68
+
69
+ // Update content when value changes externally
70
+ useEffect(() => {
71
+ if (editor && value !== editor.getJSON()) {
72
+ editor.commands.setContent(value || '')
73
+ }
74
+ }, [editor, value])
75
+
76
+ // Update editable state when mode or disabled changes
77
+ useEffect(() => {
78
+ if (editor) {
79
+ editor.setEditable(isEditable)
80
+ }
81
+ }, [editor, isEditable])
82
+
83
+ if (mode === 'read') {
84
+ return (
85
+ <div className="space-y-2">
86
+ <label className="text-sm font-medium text-foreground">{label}</label>
87
+ <div className="tiptap-read-only">
88
+ <EditorContent editor={editor} />
89
+ </div>
90
+ </div>
91
+ )
92
+ }
93
+
94
+ return (
95
+ <div className="space-y-2">
96
+ <label htmlFor={name} className="text-sm font-medium text-foreground flex items-center gap-1">
97
+ {label}
98
+ {required && <span className="text-destructive">*</span>}
99
+ </label>
100
+
101
+ {/* Editor with toolbar */}
102
+ <div className={`tiptap-editor ${error ? 'border-destructive' : ''}`}>
103
+ {/* Toolbar */}
104
+ {isEditable && editor && (
105
+ <div className="tiptap-toolbar">
106
+ <button
107
+ type="button"
108
+ onClick={() => editor.chain().focus().toggleBold().run()}
109
+ disabled={!editor.can().chain().focus().toggleBold().run()}
110
+ className={editor.isActive('bold') ? 'is-active' : ''}
111
+ >
112
+ <strong>Bold</strong>
113
+ </button>
114
+ <button
115
+ type="button"
116
+ onClick={() => editor.chain().focus().toggleItalic().run()}
117
+ disabled={!editor.can().chain().focus().toggleItalic().run()}
118
+ className={editor.isActive('italic') ? 'is-active' : ''}
119
+ >
120
+ <em>Italic</em>
121
+ </button>
122
+ <button
123
+ type="button"
124
+ onClick={() => editor.chain().focus().toggleStrike().run()}
125
+ disabled={!editor.can().chain().focus().toggleStrike().run()}
126
+ className={editor.isActive('strike') ? 'is-active' : ''}
127
+ >
128
+ <s>Strike</s>
129
+ </button>
130
+ <div className="tiptap-divider" />
131
+ <button
132
+ type="button"
133
+ onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
134
+ className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
135
+ >
136
+ H1
137
+ </button>
138
+ <button
139
+ type="button"
140
+ onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
141
+ className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}
142
+ >
143
+ H2
144
+ </button>
145
+ <button
146
+ type="button"
147
+ onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
148
+ className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''}
149
+ >
150
+ H3
151
+ </button>
152
+ <div className="tiptap-divider" />
153
+ <button
154
+ type="button"
155
+ onClick={() => editor.chain().focus().toggleBulletList().run()}
156
+ className={editor.isActive('bulletList') ? 'is-active' : ''}
157
+ >
158
+ Bullet List
159
+ </button>
160
+ <button
161
+ type="button"
162
+ onClick={() => editor.chain().focus().toggleOrderedList().run()}
163
+ className={editor.isActive('orderedList') ? 'is-active' : ''}
164
+ >
165
+ Ordered List
166
+ </button>
167
+ <button
168
+ type="button"
169
+ onClick={() => editor.chain().focus().toggleBlockquote().run()}
170
+ className={editor.isActive('blockquote') ? 'is-active' : ''}
171
+ >
172
+ Quote
173
+ </button>
174
+ </div>
175
+ )}
176
+
177
+ {/* Editor content */}
178
+ <div
179
+ className={disabled ? 'opacity-50 cursor-not-allowed' : ''}
180
+ style={{
181
+ minHeight: `${minHeight}px`,
182
+ maxHeight: maxHeight ? `${maxHeight}px` : undefined,
183
+ overflowY: maxHeight ? 'auto' : undefined,
184
+ }}
185
+ >
186
+ <EditorContent editor={editor} />
187
+ </div>
188
+ </div>
189
+
190
+ {error && <p className="text-sm text-destructive">{error}</p>}
191
+ </div>
192
+ )
193
+ }
@@ -0,0 +1,2 @@
1
+ export { TiptapField } from './TiptapField.js'
2
+ export type { TiptapFieldProps } from './TiptapField.js'
@@ -0,0 +1,35 @@
1
+ import type { BaseFieldConfig } from '@opensaas/stack-core'
2
+
3
+ /**
4
+ * Rich text field configuration using Tiptap editor
5
+ * Stores content as JSON in the database
6
+ */
7
+ export type RichTextField = BaseFieldConfig & {
8
+ type: 'richText'
9
+ validation?: {
10
+ isRequired?: boolean
11
+ }
12
+ ui?: {
13
+ /**
14
+ * Placeholder text for empty editor
15
+ */
16
+ placeholder?: string
17
+ /**
18
+ * Minimum height for editor in pixels
19
+ */
20
+ minHeight?: number
21
+ /**
22
+ * Maximum height for editor in pixels
23
+ */
24
+ maxHeight?: number
25
+ /**
26
+ * Custom React component to render this field
27
+ */
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ component?: any
30
+ /**
31
+ * Custom field type name to use from the global registry
32
+ */
33
+ fieldType?: string
34
+ }
35
+ }
@@ -0,0 +1,2 @@
1
+ export { richText } from './richText.js'
2
+ export type { RichTextField } from '../config/types.js'
@@ -0,0 +1,63 @@
1
+ import { z } from 'zod'
2
+ import type { RichTextField } from '../config/types.js'
3
+
4
+ /**
5
+ * Rich text field using Tiptap editor
6
+ * Stores content as JSON in the database
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { richText } from '@opensaas/stack-tiptap/fields'
11
+ *
12
+ * fields: {
13
+ * content: richText({
14
+ * validation: { isRequired: true },
15
+ * ui: {
16
+ * placeholder: "Write your content here...",
17
+ * minHeight: 200
18
+ * }
19
+ * })
20
+ * }
21
+ * ```
22
+ */
23
+ export function richText(options?: Omit<RichTextField, 'type'>): RichTextField {
24
+ return {
25
+ type: 'richText',
26
+ ...options,
27
+ getZodSchema: (fieldName: string, operation: 'create' | 'update') => {
28
+ const validation = options?.validation
29
+ const isRequired = validation?.isRequired
30
+
31
+ // Accept any valid JSON structure from Tiptap
32
+ // Tiptap outputs JSONContent which is a complex nested structure
33
+ const baseSchema = z.any()
34
+
35
+ if (isRequired && operation === 'create') {
36
+ // For create, reject undefined
37
+ return baseSchema
38
+ } else if (isRequired && operation === 'update') {
39
+ // For update, allow undefined (partial updates)
40
+ return z.union([baseSchema, z.undefined()])
41
+ } else {
42
+ // Not required
43
+ return baseSchema.optional()
44
+ }
45
+ },
46
+ getPrismaType: () => {
47
+ const isRequired = options?.validation?.isRequired
48
+
49
+ return {
50
+ type: 'Json',
51
+ modifiers: isRequired ? undefined : '?',
52
+ }
53
+ },
54
+ getTypeScriptType: () => {
55
+ const isRequired = options?.validation?.isRequired
56
+
57
+ return {
58
+ type: 'any',
59
+ optional: !isRequired,
60
+ }
61
+ },
62
+ }
63
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ // Field builder
2
+ export { richText } from './fields/richText.js'
3
+
4
+ // Components
5
+ export { TiptapField } from './components/TiptapField.js'
6
+
7
+ // Types
8
+ export type { RichTextField } from './config/types.js'
9
+ export type { TiptapFieldProps } from './components/TiptapField.js'
@@ -0,0 +1,170 @@
1
+ /* Tiptap Editor Styles */
2
+
3
+ /* Editor container */
4
+ .tiptap-editor {
5
+ border: 1px solid hsl(var(--border));
6
+ border-radius: 0.5rem;
7
+ background: hsl(var(--background));
8
+ }
9
+
10
+ /* Toolbar */
11
+ .tiptap-toolbar {
12
+ display: flex;
13
+ flex-wrap: wrap;
14
+ gap: 0.25rem;
15
+ padding: 0.5rem;
16
+ border-bottom: 1px solid hsl(var(--border));
17
+ background: hsl(var(--muted) / 0.3);
18
+ border-radius: 0.5rem 0.5rem 0 0;
19
+ }
20
+
21
+ .tiptap-toolbar button {
22
+ padding: 0.5rem 0.75rem;
23
+ font-size: 0.875rem;
24
+ border-radius: 0.375rem;
25
+ background: transparent;
26
+ border: none;
27
+ color: hsl(var(--foreground));
28
+ cursor: pointer;
29
+ transition: background-color 0.2s;
30
+ }
31
+
32
+ .tiptap-toolbar button:hover:not(:disabled) {
33
+ background: hsl(var(--accent));
34
+ }
35
+
36
+ .tiptap-toolbar button:disabled {
37
+ opacity: 0.5;
38
+ cursor: not-allowed;
39
+ }
40
+
41
+ .tiptap-toolbar button.is-active {
42
+ background: hsl(var(--accent));
43
+ font-weight: 600;
44
+ }
45
+
46
+ .tiptap-divider {
47
+ width: 1px;
48
+ height: 1.5rem;
49
+ background: hsl(var(--border));
50
+ margin: 0 0.25rem;
51
+ }
52
+
53
+ /* Editor content */
54
+ .tiptap {
55
+ padding: 1rem;
56
+ outline: none;
57
+ min-height: 200px;
58
+ }
59
+
60
+ .tiptap:focus {
61
+ outline: none;
62
+ }
63
+
64
+ /* Prose styles for rich text content */
65
+ .tiptap p {
66
+ margin: 0.75rem 0;
67
+ }
68
+
69
+ .tiptap p:first-child {
70
+ margin-top: 0;
71
+ }
72
+
73
+ .tiptap p:last-child {
74
+ margin-bottom: 0;
75
+ }
76
+
77
+ .tiptap h1 {
78
+ font-size: 2rem;
79
+ font-weight: 700;
80
+ margin: 1.5rem 0 1rem;
81
+ line-height: 1.2;
82
+ }
83
+
84
+ .tiptap h2 {
85
+ font-size: 1.5rem;
86
+ font-weight: 600;
87
+ margin: 1.25rem 0 0.75rem;
88
+ line-height: 1.3;
89
+ }
90
+
91
+ .tiptap h3 {
92
+ font-size: 1.25rem;
93
+ font-weight: 600;
94
+ margin: 1rem 0 0.5rem;
95
+ line-height: 1.4;
96
+ }
97
+
98
+ .tiptap ul,
99
+ .tiptap ol {
100
+ padding-left: 1.5rem;
101
+ margin: 0.75rem 0;
102
+ }
103
+
104
+ .tiptap ul {
105
+ list-style-type: disc;
106
+ }
107
+
108
+ .tiptap ol {
109
+ list-style-type: decimal;
110
+ }
111
+
112
+ .tiptap li {
113
+ margin: 0.25rem 0;
114
+ }
115
+
116
+ .tiptap blockquote {
117
+ border-left: 3px solid hsl(var(--border));
118
+ padding-left: 1rem;
119
+ margin: 1rem 0;
120
+ color: hsl(var(--muted-foreground));
121
+ font-style: italic;
122
+ }
123
+
124
+ .tiptap code {
125
+ background: hsl(var(--muted));
126
+ padding: 0.125rem 0.25rem;
127
+ border-radius: 0.25rem;
128
+ font-size: 0.875em;
129
+ font-family: monospace;
130
+ }
131
+
132
+ .tiptap pre {
133
+ background: hsl(var(--muted));
134
+ padding: 1rem;
135
+ border-radius: 0.5rem;
136
+ overflow-x: auto;
137
+ margin: 1rem 0;
138
+ }
139
+
140
+ .tiptap pre code {
141
+ background: transparent;
142
+ padding: 0;
143
+ }
144
+
145
+ /* Placeholder */
146
+ .tiptap p.is-editor-empty:first-child::before {
147
+ content: attr(data-placeholder);
148
+ float: left;
149
+ color: hsl(var(--muted-foreground));
150
+ pointer-events: none;
151
+ height: 0;
152
+ }
153
+
154
+ /* Selection */
155
+ .tiptap ::selection {
156
+ background: hsl(var(--accent));
157
+ }
158
+
159
+ /* Read-only mode */
160
+ .tiptap-read-only {
161
+ border: 1px solid hsl(var(--border));
162
+ border-radius: 0.5rem;
163
+ background: hsl(var(--muted) / 0.5);
164
+ padding: 1rem;
165
+ }
166
+
167
+ .tiptap-read-only .tiptap {
168
+ padding: 0;
169
+ min-height: auto;
170
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "moduleResolution": "bundler",
7
+ "resolveJsonModule": true,
8
+ "allowJs": true,
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "outDir": "./dist",
16
+ "rootDir": "./src",
17
+ "jsx": "react-jsx"
18
+ },
19
+ "include": ["src/**/*"],
20
+ "exclude": ["node_modules", "dist"]
21
+ }