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.
@@ -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 };