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/dist/index.d.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import { Extension, Extensions } from '@tiptap/react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Public types for dpk-editor.
|
|
7
|
+
*
|
|
8
|
+
* Kept in a dedicated module so both the components and the building-block
|
|
9
|
+
* utilities can import them without introducing circular dependencies, and so
|
|
10
|
+
* consumers get a single, stable surface to import from.
|
|
11
|
+
*/
|
|
12
|
+
/** Configuration for a single email-safe call-to-action button. */
|
|
13
|
+
type EmailButtonConfig = {
|
|
14
|
+
/** Visible label of the button. */
|
|
15
|
+
text: string;
|
|
16
|
+
/** Destination URL. */
|
|
17
|
+
href: string;
|
|
18
|
+
/** Background color (any CSS color; used verbatim in the inline style). */
|
|
19
|
+
bgColor: string;
|
|
20
|
+
/** Text color (any CSS color; used verbatim in the inline style). */
|
|
21
|
+
textColor: string;
|
|
22
|
+
/** Horizontal alignment of the button within its block. */
|
|
23
|
+
align: "left" | "center" | "right";
|
|
24
|
+
/** Corner radius in pixels (typically 0–32). */
|
|
25
|
+
radius: number;
|
|
26
|
+
/** When true the anchor spans the full content width (block display). */
|
|
27
|
+
fullWidth: boolean;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* A merge token (a.k.a. personalization placeholder) such as
|
|
31
|
+
* `{{FirstName}}`. Rendered as a chip in `<EmailEditor>` that inserts the
|
|
32
|
+
* raw token text at the caret when clicked.
|
|
33
|
+
*/
|
|
34
|
+
type EmailPlaceholder = {
|
|
35
|
+
/** The literal token inserted into the document, e.g. `{{FirstName}}`. */
|
|
36
|
+
token: string;
|
|
37
|
+
/** Human-readable label shown on the chip, e.g. "First name". */
|
|
38
|
+
label: string;
|
|
39
|
+
};
|
|
40
|
+
/** Per-button visibility within the inline-formatting group. */
|
|
41
|
+
type InlineButtons = {
|
|
42
|
+
bold?: boolean;
|
|
43
|
+
italic?: boolean;
|
|
44
|
+
underline?: boolean;
|
|
45
|
+
strike?: boolean;
|
|
46
|
+
code?: boolean;
|
|
47
|
+
};
|
|
48
|
+
/** Per-button visibility within the headings group (H2–H6). */
|
|
49
|
+
type HeadingButtons = {
|
|
50
|
+
h2?: boolean;
|
|
51
|
+
h3?: boolean;
|
|
52
|
+
h4?: boolean;
|
|
53
|
+
h5?: boolean;
|
|
54
|
+
h6?: boolean;
|
|
55
|
+
};
|
|
56
|
+
/** Per-button visibility within the lists group. */
|
|
57
|
+
type ListButtons = {
|
|
58
|
+
bullet?: boolean;
|
|
59
|
+
ordered?: boolean;
|
|
60
|
+
blockquote?: boolean;
|
|
61
|
+
};
|
|
62
|
+
/** Per-button visibility within the text-align group. */
|
|
63
|
+
type AlignButtons = {
|
|
64
|
+
left?: boolean;
|
|
65
|
+
center?: boolean;
|
|
66
|
+
right?: boolean;
|
|
67
|
+
};
|
|
68
|
+
/** Per-button visibility within the preset-blocks group. */
|
|
69
|
+
type BlockButtons = {
|
|
70
|
+
paragraph?: boolean;
|
|
71
|
+
divider?: boolean;
|
|
72
|
+
footer?: boolean;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* A group of buttons can be controlled two ways:
|
|
76
|
+
* - a `boolean` — show (`true`/omitted) or hide (`false`) the whole group;
|
|
77
|
+
* - an object of per-button booleans — show the group but override individual
|
|
78
|
+
* buttons (each defaults to `true` when the group is shown).
|
|
79
|
+
*
|
|
80
|
+
* `true` and `{}` are equivalent (group on, all buttons on). `false` hides the
|
|
81
|
+
* whole group regardless of any per-button values.
|
|
82
|
+
*/
|
|
83
|
+
type ToolbarGroup<TButtons> = boolean | TButtons;
|
|
84
|
+
/**
|
|
85
|
+
* Controls which toolbar controls render. Every key defaults to `true` (all
|
|
86
|
+
* controls visible).
|
|
87
|
+
*
|
|
88
|
+
* - Single-control groups (`link`, `image`, `button`, `html`) are plain
|
|
89
|
+
* booleans: set to `false` to hide.
|
|
90
|
+
* - Multi-button groups (`inline`, `headings`, `lists`, `align`, `blocks`)
|
|
91
|
+
* accept either a boolean (whole group) OR an object for per-button control,
|
|
92
|
+
* e.g. `inline: { underline: false }` keeps Bold/Italic/Strike/Code but hides
|
|
93
|
+
* Underline, while `inline: false` hides the entire inline group.
|
|
94
|
+
*/
|
|
95
|
+
type ToolbarConfig = {
|
|
96
|
+
/** Bold / Italic / Underline / Strikethrough / Inline code group. */
|
|
97
|
+
inline?: ToolbarGroup<InlineButtons>;
|
|
98
|
+
/** Heading level buttons (H2–H6). */
|
|
99
|
+
headings?: ToolbarGroup<HeadingButtons>;
|
|
100
|
+
/** Bullet list / Ordered list / Blockquote group. */
|
|
101
|
+
lists?: ToolbarGroup<ListButtons>;
|
|
102
|
+
/** Text-align left/center/right group. */
|
|
103
|
+
align?: ToolbarGroup<AlignButtons>;
|
|
104
|
+
/** Link control. */
|
|
105
|
+
link?: boolean;
|
|
106
|
+
/** Image control. */
|
|
107
|
+
image?: boolean;
|
|
108
|
+
/** Email CTA-button control (opens the button dialog). */
|
|
109
|
+
button?: boolean;
|
|
110
|
+
/** Preset block snippets group (Paragraph / Divider / Footer). */
|
|
111
|
+
blocks?: ToolbarGroup<BlockButtons>;
|
|
112
|
+
/** Raw HTML source toggle. */
|
|
113
|
+
html?: boolean;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* The fully-resolved toolbar config used internally: every group is expanded to
|
|
117
|
+
* `{ enabled, buttons: {...all booleans} }`, so the Toolbar renders from a flat,
|
|
118
|
+
* non-optional shape. Produced by `resolveToolbarConfig`.
|
|
119
|
+
*/
|
|
120
|
+
type ResolvedToolbarConfig = {
|
|
121
|
+
inline: {
|
|
122
|
+
enabled: boolean;
|
|
123
|
+
buttons: Required<InlineButtons>;
|
|
124
|
+
};
|
|
125
|
+
headings: {
|
|
126
|
+
enabled: boolean;
|
|
127
|
+
buttons: Required<HeadingButtons>;
|
|
128
|
+
};
|
|
129
|
+
lists: {
|
|
130
|
+
enabled: boolean;
|
|
131
|
+
buttons: Required<ListButtons>;
|
|
132
|
+
};
|
|
133
|
+
align: {
|
|
134
|
+
enabled: boolean;
|
|
135
|
+
buttons: Required<AlignButtons>;
|
|
136
|
+
};
|
|
137
|
+
link: boolean;
|
|
138
|
+
image: boolean;
|
|
139
|
+
button: boolean;
|
|
140
|
+
blocks: {
|
|
141
|
+
enabled: boolean;
|
|
142
|
+
buttons: Required<BlockButtons>;
|
|
143
|
+
};
|
|
144
|
+
html: boolean;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* The split representation of an email document. `prefix` is everything up to
|
|
148
|
+
* and including the opening `<body ...>` tag, `body` is the editable inner
|
|
149
|
+
* HTML, and `suffix` is `</body>` plus anything after it. For a bare fragment
|
|
150
|
+
* (no `<body>`), `prefix` and `suffix` are empty strings.
|
|
151
|
+
*/
|
|
152
|
+
type EmailHtmlDocument = {
|
|
153
|
+
prefix: string;
|
|
154
|
+
body: string;
|
|
155
|
+
suffix: string;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
type EmailEditorProps = {
|
|
159
|
+
/** Full HTML document OR a bare body fragment — both are supported. */
|
|
160
|
+
value: string;
|
|
161
|
+
/** Fires with HTML in the same shape `value` was provided (shell re-applied). */
|
|
162
|
+
onChange: (value: string) => void;
|
|
163
|
+
/** Optional merge tokens; render a chip row that inserts at the caret. */
|
|
164
|
+
placeholders?: EmailPlaceholder[];
|
|
165
|
+
/** Resolve an uploaded file to a hosted image URL (else a URL prompt is used). */
|
|
166
|
+
onUploadImage?: (file: File) => Promise<string>;
|
|
167
|
+
/** Minimum height (px) of the editable surface. */
|
|
168
|
+
minHeight?: number;
|
|
169
|
+
/** Empty-state placeholder text for the editable surface. */
|
|
170
|
+
placeholder?: string;
|
|
171
|
+
/** Which toolbar controls to render (defaults to everything on). */
|
|
172
|
+
toolbar?: ToolbarConfig;
|
|
173
|
+
/** Extra class on the editor wrapper. */
|
|
174
|
+
className?: string;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* The batteries-included email editor. Owns the document-shell bridge so the
|
|
178
|
+
* surrounding `<!doctype>`/`<html>`/`<body style>` (and any `<table>` layout
|
|
179
|
+
* inside the body) is preserved verbatim across edits, while only the body
|
|
180
|
+
* fragment is handed to the underlying `<RichTextEditor>`.
|
|
181
|
+
*/
|
|
182
|
+
declare function EmailEditor({ value, onChange, placeholders, onUploadImage, minHeight, placeholder, toolbar, className, }: EmailEditorProps): react_jsx_runtime.JSX.Element;
|
|
183
|
+
|
|
184
|
+
type RichTextEditorHandle = {
|
|
185
|
+
/** Insert raw HTML or plain text at the current caret position. */
|
|
186
|
+
insertAtCaret: (htmlOrText: string) => void;
|
|
187
|
+
};
|
|
188
|
+
type RichTextEditorProps = {
|
|
189
|
+
/** Body-level HTML fragment (no `<html>`/`<body>`). */
|
|
190
|
+
value: string;
|
|
191
|
+
/** Fires with the updated body-level HTML fragment. */
|
|
192
|
+
onChange: (value: string) => void;
|
|
193
|
+
/** Empty-state placeholder text. */
|
|
194
|
+
placeholder?: string;
|
|
195
|
+
/** Resolve an uploaded file to a hosted image URL. */
|
|
196
|
+
onUploadImage?: (file: File) => Promise<string>;
|
|
197
|
+
/** Extra class on the editor wrapper. */
|
|
198
|
+
className?: string;
|
|
199
|
+
/** Inline style applied to the editable surface (e.g. minHeight/height). */
|
|
200
|
+
editorStyle?: React.CSSProperties;
|
|
201
|
+
/** Which toolbar controls to render (defaults to everything on). */
|
|
202
|
+
toolbar?: ToolbarConfig;
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* The generic body-HTML rich-text editor: a toolbar plus an editable surface
|
|
206
|
+
* operating on a plain HTML fragment. `<EmailEditor>` wraps this to add the
|
|
207
|
+
* document-shell bridge and placeholder chips.
|
|
208
|
+
*/
|
|
209
|
+
declare const RichTextEditor: react.ForwardRefExoticComponent<RichTextEditorProps & react.RefAttributes<RichTextEditorHandle>>;
|
|
210
|
+
|
|
211
|
+
type EmailButtonDialogProps = {
|
|
212
|
+
/** Whether the dialog is open. */
|
|
213
|
+
open: boolean;
|
|
214
|
+
/** Called when the user confirms; receives the email-safe button HTML. */
|
|
215
|
+
onConfirm: (html: string, config: EmailButtonConfig) => void;
|
|
216
|
+
/** Called when the user dismisses the dialog (Esc / click-outside / cancel). */
|
|
217
|
+
onClose: () => void;
|
|
218
|
+
/** Optional initial values for the form. */
|
|
219
|
+
initial?: Partial<EmailButtonConfig>;
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Modal dialog for configuring an email CTA button. Supports Esc to close,
|
|
223
|
+
* body-scroll lock while open, click-outside to dismiss, and a live preview of
|
|
224
|
+
* the rendered button.
|
|
225
|
+
*/
|
|
226
|
+
declare function EmailButtonDialog({ open, onConfirm, onClose, initial, }: EmailButtonDialogProps): react_jsx_runtime.JSX.Element | null;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* A TipTap extension that preserves the inline `style` attribute on every node
|
|
230
|
+
* and mark in the schema.
|
|
231
|
+
*
|
|
232
|
+
* TipTap (ProseMirror) strips any HTML attribute a node/mark does not declare.
|
|
233
|
+
* For article editing that is fine; for **email** HTML it is lossy — buttons
|
|
234
|
+
* lose their padding/background, headings lose their color, and so on. This
|
|
235
|
+
* extension declares a global `style` attribute so arbitrary inline-styled
|
|
236
|
+
* email HTML round-trips.
|
|
237
|
+
*
|
|
238
|
+
* It preserves `style` only on elements that map to a known node or mark
|
|
239
|
+
* (paragraph, heading, list, blockquote, link, image, hr, …). Raw
|
|
240
|
+
* `<table>`/`<td>` layouts are still flattened by ProseMirror's schema — those
|
|
241
|
+
* survive via the document-shell bridge, not here.
|
|
242
|
+
*/
|
|
243
|
+
declare const PreserveStyles: Extension<any, any>;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The canonical extension set for the email editor.
|
|
247
|
+
*
|
|
248
|
+
* Notes / watch-outs baked in here:
|
|
249
|
+
* - StarterKit v3 already bundles Link, Underline, lists, blockquote, code and
|
|
250
|
+
* headings — we do NOT add @tiptap/extension-link or -underline separately
|
|
251
|
+
* (that throws duplicate-extension warnings). Link is configured through
|
|
252
|
+
* StarterKit.configure({ link: {...} }).
|
|
253
|
+
* - Image is block-level and base64 is disabled (uploads should resolve to a
|
|
254
|
+
* hosted URL, which is what email clients can render).
|
|
255
|
+
* - PreserveStyles must be present so inline `style` survives the round-trip.
|
|
256
|
+
* - Placeholder is NOT bundled by StarterKit v3, so it is added explicitly to
|
|
257
|
+
* power the empty-state hint (it sets the `is-editor-empty` class and the
|
|
258
|
+
* `data-placeholder` attribute that styles.css renders via `::before`).
|
|
259
|
+
*
|
|
260
|
+
* Exported as a factory so the React component and the headless tests build an
|
|
261
|
+
* identical extension set. Pass the empty-state placeholder text in.
|
|
262
|
+
*/
|
|
263
|
+
declare function createEmailExtensions(placeholder?: string): Extensions;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Build email-safe HTML for a call-to-action button.
|
|
267
|
+
*
|
|
268
|
+
* Two non-obvious decisions, both load-bearing:
|
|
269
|
+
*
|
|
270
|
+
* 1. The anchor is wrapped in a `<p style="...;text-align:X">`, **never** a
|
|
271
|
+
* `<div>`. TipTap has no `div` node and unwraps a `<div>` into a paragraph,
|
|
272
|
+
* losing the `text-align`. A paragraph is a real node whose `text-align`
|
|
273
|
+
* the TextAlign extension preserves — so the alignment round-trips.
|
|
274
|
+
*
|
|
275
|
+
* 2. The button is an `<a style="display:inline-block;...">` (or
|
|
276
|
+
* `display:block` when full-width), not a `<button>`, because email clients
|
|
277
|
+
* need bulletproof, inline-styled markup.
|
|
278
|
+
*
|
|
279
|
+
* Both the label and the href are HTML-escaped.
|
|
280
|
+
*/
|
|
281
|
+
declare function buildButtonHtml(config: EmailButtonConfig): string;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Escape a string for safe insertion into HTML text or a double-quoted
|
|
285
|
+
* attribute value. Used by `buildButtonHtml` for the button label and href.
|
|
286
|
+
*/
|
|
287
|
+
declare function escapeHtml(value: string): string;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Split a full email HTML document into `{ prefix, body, suffix }`.
|
|
291
|
+
*
|
|
292
|
+
* - `prefix` = everything up to and including the opening `<body ...>` tag.
|
|
293
|
+
* - `body` = the inner body HTML (the editable fragment).
|
|
294
|
+
* - `suffix` = the closing `</body>` tag plus everything after it.
|
|
295
|
+
*
|
|
296
|
+
* If the input has no `<body>` tag it is treated as a **bare fragment**: the
|
|
297
|
+
* whole input becomes `body`, and `prefix`/`suffix` are empty strings.
|
|
298
|
+
*/
|
|
299
|
+
declare function splitEmailHtml(html: string): EmailHtmlDocument;
|
|
300
|
+
/**
|
|
301
|
+
* Reassemble a document from a shell and a (possibly edited) body fragment.
|
|
302
|
+
*
|
|
303
|
+
* If the shell has no surrounding markup (a bare fragment was originally
|
|
304
|
+
* supplied) the body is returned as-is, so `onChange` emits in the same shape
|
|
305
|
+
* the consumer provided `value`.
|
|
306
|
+
*/
|
|
307
|
+
declare function joinEmailHtml(shell: EmailHtmlDocument, newBody: string): string;
|
|
308
|
+
/**
|
|
309
|
+
* Convenience: extract just the editable body fragment from a full document
|
|
310
|
+
* (or return the input unchanged if it is already a bare fragment).
|
|
311
|
+
*/
|
|
312
|
+
declare function extractEmailBody(html: string): string;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Preset block snippets — small, inline-styled, email-safe HTML fragments
|
|
316
|
+
* inserted at the caret via `editor.chain().focus().insertContent(snippet)`.
|
|
317
|
+
*
|
|
318
|
+
* Every snippet uses inline styles only (no classes) so it survives both the
|
|
319
|
+
* editor schema (via PreserveStyles) and downstream email clients.
|
|
320
|
+
*/
|
|
321
|
+
declare const PRESET_PARAGRAPH = "<p style=\"margin:0 0 12px;color:#4b5563;line-height:1.6;\">Write your message here.</p>";
|
|
322
|
+
declare const PRESET_DIVIDER = "<hr style=\"border:none;border-top:1px solid #e5e7eb;margin:20px 0;\" />";
|
|
323
|
+
declare const PRESET_FOOTER = "<p style=\"margin:20px 0 0;color:#9ca3af;font-size:12px;\">\u00A9 Your Company</p>";
|
|
324
|
+
declare const PRESET_HEADING = "<h2 style=\"margin:0 0 12px;color:#111827;font-size:24px;line-height:1.3;\">Heading</h2>";
|
|
325
|
+
declare const presets: {
|
|
326
|
+
readonly paragraph: "<p style=\"margin:0 0 12px;color:#4b5563;line-height:1.6;\">Write your message here.</p>";
|
|
327
|
+
readonly divider: "<hr style=\"border:none;border-top:1px solid #e5e7eb;margin:20px 0;\" />";
|
|
328
|
+
readonly footer: "<p style=\"margin:20px 0 0;color:#9ca3af;font-size:12px;\">© Your Company</p>";
|
|
329
|
+
readonly heading: "<h2 style=\"margin:0 0 12px;color:#111827;font-size:24px;line-height:1.3;\">Heading</h2>";
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
declare function resolveToolbarConfig(config?: ToolbarConfig): ResolvedToolbarConfig;
|
|
333
|
+
|
|
334
|
+
export { type AlignButtons, type BlockButtons, type EmailButtonConfig, EmailButtonDialog, type EmailButtonDialogProps, EmailEditor, type EmailEditorProps, type EmailHtmlDocument, type EmailPlaceholder, type HeadingButtons, type InlineButtons, type ListButtons, PRESET_DIVIDER, PRESET_FOOTER, PRESET_HEADING, PRESET_PARAGRAPH, PreserveStyles, type ResolvedToolbarConfig, RichTextEditor, type RichTextEditorHandle, type RichTextEditorProps, type ToolbarConfig, type ToolbarGroup, buildButtonHtml, createEmailExtensions, EmailEditor as default, escapeHtml, extractEmailBody, joinEmailHtml, presets, resolveToolbarConfig, splitEmailHtml };
|