dpk-editor 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.
- package/LICENSE +21 -0
- package/README.md +269 -0
- package/dist/index.cjs +990 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +334 -0
- package/dist/index.d.ts +334 -0
- package/dist/index.js +962 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +386 -0
- package/package.json +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 <AUTHOR>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# dpk-editor
|
|
2
|
+
|
|
3
|
+
A React rich-text editor built on **TipTap v3**, purpose-built for composing **HTML emails**.
|
|
4
|
+
|
|
5
|
+
Editing email HTML is not like editing article HTML. This package is designed around three constraints that ordinary rich-text editors get wrong:
|
|
6
|
+
|
|
7
|
+
1. **Emails are full documents** — `<!doctype html><html><body style="…"><table>…`. A `contentEditable`/ProseMirror surface cannot host `<html>`/`<head>`/`<body>`; it silently strips them. So this editor edits only the **body fragment** and preserves the surrounding document shell verbatim (the *document-shell bridge*).
|
|
8
|
+
2. **Emails rely on inline styles** — `style="padding:…;background:…"` on buttons, colored headings, table layouts. TipTap strips the `style` attribute by default. The bundled `PreserveStyles` extension keeps it.
|
|
9
|
+
3. **Email clients need bulletproof markup** — CTA buttons are `<a style="display:inline-block;…">` wrapped in an aligned `<p>`, not `<button>`.
|
|
10
|
+
|
|
11
|
+
- ✅ Ships **ESM + CJS + type declarations**
|
|
12
|
+
- ✅ Works in **Next.js App Router** and plain **Vite/CRA** (`immediatelyRender:false` set internally for SSR)
|
|
13
|
+
- ✅ No Tailwind/shadcn requirement — plain CSS you import
|
|
14
|
+
- ✅ TypeScript strict, no `any` in public types
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm i dpk-editor
|
|
20
|
+
# peer deps (you already have these in a React app):
|
|
21
|
+
npm i react react-dom
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { EmailEditor } from "dpk-editor";
|
|
26
|
+
import "dpk-editor/styles.css"; // required
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### `<EmailEditor>` — batteries included (default export)
|
|
32
|
+
|
|
33
|
+
Handles the document-shell bridge, placeholder chips, and image upload wiring. `value` may be a full `<!doctype><html><body>…` document **or** a bare fragment; `onChange` always emits in the same shape it received.
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { useState } from "react";
|
|
37
|
+
import { EmailEditor } from "dpk-editor";
|
|
38
|
+
import "dpk-editor/styles.css";
|
|
39
|
+
|
|
40
|
+
export default function Composer() {
|
|
41
|
+
const [html, setHtml] = useState(
|
|
42
|
+
"<html><body><h1>Hi {{FirstName}}</h1></body></html>",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<EmailEditor
|
|
47
|
+
value={html}
|
|
48
|
+
onChange={setHtml}
|
|
49
|
+
placeholders={[
|
|
50
|
+
{ token: "{{FirstName}}", label: "First name" },
|
|
51
|
+
{ token: "{{Email}}", label: "Email" },
|
|
52
|
+
]}
|
|
53
|
+
onUploadImage={async (file) => {
|
|
54
|
+
// upload `file` somewhere and return a hosted URL
|
|
55
|
+
return "https://cdn.example.com/x.png";
|
|
56
|
+
}}
|
|
57
|
+
minHeight={288}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### `<RichTextEditor>` — the generic body-HTML editor (named export)
|
|
64
|
+
|
|
65
|
+
The toolbar + editable surface operating on a **plain HTML fragment** (no `<html>`/`<body>`). `EmailEditor` is a thin wrapper that adds the shell bridge + chips around this. Use it if you want to build your own wrapper.
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { useRef, useState } from "react";
|
|
69
|
+
import { RichTextEditor, type RichTextEditorHandle } from "dpk-editor";
|
|
70
|
+
import "dpk-editor/styles.css";
|
|
71
|
+
|
|
72
|
+
function Body() {
|
|
73
|
+
const [body, setBody] = useState("<p>Body HTML only.</p>");
|
|
74
|
+
const ref = useRef<RichTextEditorHandle>(null);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<>
|
|
78
|
+
<button onClick={() => ref.current?.insertAtCaret("{{FirstName}}")}>
|
|
79
|
+
Insert token
|
|
80
|
+
</button>
|
|
81
|
+
<RichTextEditor
|
|
82
|
+
ref={ref}
|
|
83
|
+
value={body}
|
|
84
|
+
onChange={setBody}
|
|
85
|
+
editorStyle={{ minHeight: 240 }}
|
|
86
|
+
toolbar={{ button: false }} // hide the CTA-button control
|
|
87
|
+
/>
|
|
88
|
+
</>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Props
|
|
94
|
+
|
|
95
|
+
### `EmailEditorProps`
|
|
96
|
+
|
|
97
|
+
| Prop | Type | Default | Description |
|
|
98
|
+
| --------------- | ----------------------------------------- | -------------- | ----------- |
|
|
99
|
+
| `value` | `string` | — | Full HTML document **or** bare body fragment. |
|
|
100
|
+
| `onChange` | `(value: string) => void` | — | Fires with HTML in the same shape as `value` (shell re-applied). |
|
|
101
|
+
| `placeholders` | `EmailPlaceholder[]` | `undefined` | Merge tokens → renders a chip row that inserts at the caret. |
|
|
102
|
+
| `onUploadImage` | `(file: File) => Promise<string>` | `undefined` | Resolve an upload to a URL. If omitted, the image button prompts for a URL. |
|
|
103
|
+
| `minHeight` | `number` | `288` | Min height (px) of the editable surface. |
|
|
104
|
+
| `placeholder` | `string` | `"Write your email…"` | Empty-state text. |
|
|
105
|
+
| `toolbar` | `ToolbarConfig` | all on | Which controls to render. |
|
|
106
|
+
| `className` | `string` | `undefined` | Extra class on the wrapper. |
|
|
107
|
+
|
|
108
|
+
### `RichTextEditorProps`
|
|
109
|
+
|
|
110
|
+
| Prop | Type | Default | Description |
|
|
111
|
+
| --------------- | ----------------------------------- | ------- | ----------- |
|
|
112
|
+
| `value` | `string` | — | Body-level HTML fragment. |
|
|
113
|
+
| `onChange` | `(value: string) => void` | — | Fires with the body-level HTML fragment. |
|
|
114
|
+
| `placeholder` | `string` | `"Write your email…"` | Empty-state text. |
|
|
115
|
+
| `onUploadImage` | `(file: File) => Promise<string>` | `undefined` | Upload handler (else URL prompt). |
|
|
116
|
+
| `className` | `string` | `undefined` | Extra class on the wrapper. |
|
|
117
|
+
| `editorStyle` | `React.CSSProperties` | `undefined` | Applied to the editable surface (e.g. `{ minHeight }`). |
|
|
118
|
+
| `toolbar` | `ToolbarConfig` | all on | Which controls to render. |
|
|
119
|
+
|
|
120
|
+
`RichTextEditor` is a `forwardRef` exposing `RichTextEditorHandle`:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
type RichTextEditorHandle = { insertAtCaret: (htmlOrText: string) => void };
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `ToolbarConfig`
|
|
127
|
+
|
|
128
|
+
Every control defaults to **on**. There are two levels of control:
|
|
129
|
+
|
|
130
|
+
- **Single-control groups** (`link`, `image`, `button`, `html`) are plain
|
|
131
|
+
booleans — set to `false` to hide.
|
|
132
|
+
- **Multi-button groups** (`inline`, `headings`, `lists`, `align`, `blocks`)
|
|
133
|
+
accept **either** a boolean (whole group on/off) **or** an object of
|
|
134
|
+
per-button booleans for fine-grained control. Unlisted buttons stay on, so
|
|
135
|
+
you only list what you want to change.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
type ToolbarConfig = {
|
|
139
|
+
inline?: boolean | {
|
|
140
|
+
bold?: boolean; italic?: boolean; underline?: boolean;
|
|
141
|
+
strike?: boolean; code?: boolean;
|
|
142
|
+
};
|
|
143
|
+
headings?: boolean | {
|
|
144
|
+
h2?: boolean; h3?: boolean; h4?: boolean; h5?: boolean; h6?: boolean;
|
|
145
|
+
};
|
|
146
|
+
lists?: boolean | { bullet?: boolean; ordered?: boolean; blockquote?: boolean };
|
|
147
|
+
align?: boolean | { left?: boolean; center?: boolean; right?: boolean };
|
|
148
|
+
blocks?: boolean | { paragraph?: boolean; divider?: boolean; footer?: boolean };
|
|
149
|
+
link?: boolean; // Link control
|
|
150
|
+
image?: boolean; // Image control
|
|
151
|
+
button?: boolean; // Email CTA button dialog
|
|
152
|
+
html?: boolean; // Raw HTML source toggle
|
|
153
|
+
};
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Examples:
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
// Hide whole groups:
|
|
160
|
+
<EmailEditor toolbar={{ button: false, html: false, blocks: false }} … />
|
|
161
|
+
|
|
162
|
+
// Keep the inline group but drop just Underline and Inline-code:
|
|
163
|
+
<EmailEditor toolbar={{ inline: { underline: false, code: false } }} … />
|
|
164
|
+
|
|
165
|
+
// Only offer H2 and H3 headings:
|
|
166
|
+
<EmailEditor toolbar={{ headings: { h4: false, h5: false, h6: false } }} … />
|
|
167
|
+
|
|
168
|
+
// Mix both levels freely:
|
|
169
|
+
<EmailEditor
|
|
170
|
+
toolbar={{
|
|
171
|
+
inline: { code: false }, // group on, hide one button
|
|
172
|
+
align: false, // whole group off
|
|
173
|
+
image: false, // single control off
|
|
174
|
+
}}
|
|
175
|
+
…
|
|
176
|
+
/>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
> `true` and `{}` are equivalent (group on, all buttons on). `false` hides the
|
|
180
|
+
> whole group. Hidden groups leave no dangling toolbar dividers.
|
|
181
|
+
|
|
182
|
+
For advanced use, `resolveToolbarConfig(config)` (exported) returns the flat,
|
|
183
|
+
fully-expanded `ResolvedToolbarConfig` the toolbar renders from.
|
|
184
|
+
|
|
185
|
+
## Building blocks (also exported)
|
|
186
|
+
|
|
187
|
+
For advanced use, the underlying pieces are exported:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import {
|
|
191
|
+
PreserveStyles, // the TipTap extension that keeps inline `style`
|
|
192
|
+
createEmailExtensions, // the full extension set as a factory
|
|
193
|
+
buildButtonHtml, // (config: EmailButtonConfig) => string
|
|
194
|
+
EmailButtonDialog, // the button-config modal
|
|
195
|
+
splitEmailHtml, // (html) => { prefix, body, suffix }
|
|
196
|
+
joinEmailHtml, // (shell, body) => html
|
|
197
|
+
extractEmailBody, // (html) => body fragment
|
|
198
|
+
escapeHtml,
|
|
199
|
+
presets, // { paragraph, divider, footer, heading }
|
|
200
|
+
} from "dpk-editor";
|
|
201
|
+
|
|
202
|
+
import type {
|
|
203
|
+
EmailButtonConfig,
|
|
204
|
+
EmailPlaceholder,
|
|
205
|
+
ToolbarConfig,
|
|
206
|
+
EmailHtmlDocument,
|
|
207
|
+
} from "dpk-editor";
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### `buildButtonHtml`
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const html = buildButtonHtml({
|
|
214
|
+
text: "Get started",
|
|
215
|
+
href: "https://example.com",
|
|
216
|
+
bgColor: "#2563eb",
|
|
217
|
+
textColor: "#ffffff",
|
|
218
|
+
align: "center",
|
|
219
|
+
radius: 6,
|
|
220
|
+
fullWidth: false,
|
|
221
|
+
});
|
|
222
|
+
// → <p style="margin:16px 0;text-align:center"><a href="…" style="display:inline-block;…">Get started</a></p>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The anchor is wrapped in a `<p>` (not a `<div>`) on purpose: TipTap has no `div` node and would unwrap a `<div>`, **losing the alignment**. A paragraph's `text-align` is preserved by the TextAlign extension.
|
|
226
|
+
|
|
227
|
+
## Theming
|
|
228
|
+
|
|
229
|
+
`styles.css` targets `.rte-*` classes and the `.ProseMirror`/`.rte-content` surface, and exposes CSS custom properties with neutral defaults. Override any of them on a parent element (or `:root`):
|
|
230
|
+
|
|
231
|
+
```css
|
|
232
|
+
.my-editor {
|
|
233
|
+
--rte-accent: #7c3aed;
|
|
234
|
+
--rte-accent-contrast: #ffffff;
|
|
235
|
+
--rte-border: #e2e8f0;
|
|
236
|
+
--rte-border-strong: #cbd5e1;
|
|
237
|
+
--rte-bg: #ffffff;
|
|
238
|
+
--rte-bg-muted: #f8fafc;
|
|
239
|
+
--rte-fg: #0f172a;
|
|
240
|
+
--rte-fg-muted: #64748b;
|
|
241
|
+
--rte-radius: 10px;
|
|
242
|
+
--rte-active-bg: #f5f3ff;
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
<EmailEditor className="my-editor" value={html} onChange={setHtml} />
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## SSR / Next.js App Router
|
|
251
|
+
|
|
252
|
+
- The component entry is marked `"use client"`, so importing it from a Server Component is fine — render `<EmailEditor>` inside a client boundary.
|
|
253
|
+
- `immediatelyRender: false` is set **internally**, which prevents the hydration mismatch TipTap otherwise throws under SSR. You don't need to configure anything.
|
|
254
|
+
|
|
255
|
+
## Built-in extensions
|
|
256
|
+
|
|
257
|
+
The editor is configured with TipTap v3:
|
|
258
|
+
|
|
259
|
+
- `StarterKit` (which **already bundles** Link, Underline, lists, blockquote, code, and headings — do not add `@tiptap/extension-link` or `@tiptap/extension-underline` yourself), with Link configured `openOnClick:false`, `autolink:true`, `rel="noopener noreferrer"`.
|
|
260
|
+
- `@tiptap/extension-image` (`inline:false`, `allowBase64:false`)
|
|
261
|
+
- `@tiptap/extension-text-align` (`["heading","paragraph"]`)
|
|
262
|
+
- `@tiptap/extension-text-style` (`TextStyle` + `Color`, re-exported here — no separate `@tiptap/extension-color` needed)
|
|
263
|
+
- `@tiptap/extension-highlight` (`multicolor:true`)
|
|
264
|
+
- `@tiptap/extension-placeholder` (StarterKit v3 does **not** bundle it; added explicitly for the empty-state hint, which sets the `is-editor-empty`/`is-empty` class and `data-placeholder` attribute that `styles.css` renders)
|
|
265
|
+
- `PreserveStyles` (this package)
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
MIT
|