svelte-rtf-editor 1.3.0 → 1.4.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/README.md CHANGED
@@ -13,6 +13,7 @@ RTF viewer and rich-text editor components for **Svelte 5**.
13
13
  - **`InkEditor`** — Full rich-text editor that reads and writes RTF, built on `contenteditable`
14
14
  - Formatting toolbar: bold, italic, underline, strikethrough, font colour
15
15
  - Block-level controls: headings (H1–H3), paragraph, blockquote, code block
16
+ - Images: upload, paste or drag-and-drop, with a description below each picture, drag-to-resize and left/centre/right placement — embedded in the RTF, not linked
16
17
  - Import an RTF file from disk; export as RTF, HTML, or Markdown
17
18
  - Optional auto-save to `localStorage`
18
19
  - Word count and character count status bar
@@ -124,12 +125,15 @@ Pass an HTML string as `content`. If the content is already RTF, convert it firs
124
125
  | `showStatusBar` | `boolean` | `true` | Show the word/character count bar |
125
126
  | `minHeight` | `string` | `'40vh'` | CSS `min-height` of the editing area |
126
127
  | `readonly` | `boolean` | `false` | Disable editing |
128
+ | `maxImageEdge` | `number` | `1600` | Longest edge (px) an inserted image is scaled down to; `0` keeps full size |
129
+ | `maxImageBytes` | `number` | `524288` | Encoded byte ceiling per image; oversized pictures are re-encoded until they fit. `0` disables |
130
+ | `maxImageDisplayWidth` | `number` | `624` | Width (px) a newly inserted image is displayed at, unless the column is narrower — 6.5in of page. `0` fills the column |
127
131
 
128
132
  #### Callback props
129
133
 
130
134
  | Prop | Payload | Description |
131
135
  |------------|----------------------------------------------|---------------------------------------------|
132
- | `onchange` | `{ html, text, wordCount, charCount }` | Fired on every content change |
136
+ | `onchange` | `{ html, text, wordCount, charCount, estimatedRtfBytes }` | Fired on every content change |
133
137
  | `onsave` | `{ html }` | Fired on auto-save or `Ctrl+S` |
134
138
  | `onimport` | `{ html }` | Fired after a successful RTF file import |
135
139
 
@@ -141,6 +145,7 @@ editor.getText() // → string — plain text (no tags)
141
145
  editor.setHTML(html) // → void — replace content programmatically
142
146
  editor.getMarkdown() // → string — Markdown conversion of content
143
147
  editor.getRTF() // → string — RTF conversion of content
148
+ editor.getRtfSize() // → number — exact byte size of that RTF
144
149
  editor.clear() // → void — clear the editor
145
150
  editor.focus() // → void — focus the editor
146
151
  editor.exportFile(format) // → void — download as 'html' | 'md' | 'rtf'
@@ -152,13 +157,16 @@ editor.importRtf() // → void — open the file picker to import .rtf
152
157
  ## Utility functions
153
158
 
154
159
  ```ts
155
- import { rtfToHtml, htmlToRtf, readRtfFile, htmlToMarkdown, downloadFile } from 'svelte-rtf-editor';
160
+ import {
161
+ rtfToHtml, htmlToRtf, estimateRtfBytes, readRtfFile, htmlToMarkdown, downloadFile
162
+ } from 'svelte-rtf-editor';
156
163
  ```
157
164
 
158
165
  | Function | Description |
159
166
  |---------------------------------------|----------------------------------------------------|
160
167
  | `rtfToHtml(rtf: string): string` | Parse an RTF string and return an HTML string |
161
168
  | `htmlToRtf(html: string): string` | Convert an HTML string to RTF |
169
+ | `estimateRtfBytes(el: HTMLElement): number` | Approximate RTF size of a DOM element without converting it |
162
170
  | `readRtfFile(file: File): Promise<string>` | Read a `.rtf` File object and return HTML |
163
171
  | `htmlToMarkdown(el: HTMLElement): string` | Convert a DOM element's content to Markdown |
164
172
  | `downloadFile(name, content, mime): void` | Trigger a file download in the browser |
@@ -192,7 +200,72 @@ The components use CSS custom properties with sensible fallbacks, so they work o
192
200
  | `Ctrl+B` | Bold |
193
201
  | `Ctrl+I` | Italic |
194
202
  | `Ctrl+U` | Underline |
203
+ | `Ctrl+K` | Insert link |
195
204
  | `Ctrl+S` | Save (fires `onsave`) |
205
+ | `Backspace` / `Delete` | Remove the selected image |
206
+
207
+ ---
208
+
209
+ ## Working with images
210
+
211
+ Three ways to add a picture:
212
+
213
+ - **Toolbar** — the image button opens a dialog where you can pick one or several files from disk, or paste an image address
214
+ - **Paste** — paste an image straight from the clipboard (a screenshot, for example)
215
+ - **Drag and drop** — drop image files anywhere in the editing area; they are inserted where you dropped them
216
+
217
+ Each picture is inserted as a `<figure>` with a `<figcaption>` under it. Click the caption line and type to describe the image — the description travels with the picture through every export. Insert as many images as a document needs.
218
+
219
+ Click an image to select it. A frame appears with corner handles you can drag to resize (the aspect ratio is kept and the width is capped at the editor's text width), buttons for 25% / 50% / 100% width, left / centre / right placement, a shortcut to its description, and a remove button.
220
+
221
+ ### How images are stored
222
+
223
+ Images are held inline as base64 data URLs, so a document is self-contained — it auto-saves, exports and re-imports with the pictures intact, and needs no image server.
224
+
225
+ - **RTF export** writes each picture as a real `\pict` group (`\pngblip` / `\jpegblip`) sized with `\picwgoal` / `\pichgoal`, so it opens with the images in Word, TextEdit and other RTF readers. The description follows as an italic line, tagged with an ignorable `{\*\inkcap}` destination that this library uses to re-attach it to the figure on import; other readers simply skip the tag.
226
+ - **RTF import** decodes `\pict` PNG and JPEG data (including pictures wrapped in `{\*\shppict}`) back into images at their stored size. Picture formats a browser cannot display — metafiles and device-dependent bitmaps — are skipped.
227
+ - **Formats**: PNG and JPEG are embedded as-is. Anything else (GIF, WebP, SVG, …) is re-encoded to PNG when inserted, since RTF carries no other bitmap types. Animated images keep their first frame.
228
+ - **Images added by address** are fetched and inlined so they survive export. When the fetch is blocked (CORS or offline) the address is kept and the picture still displays, but it exports as an `[Image: …]` placeholder instead of picture data — the editor says so at insert time rather than letting it go unnoticed until the document is read downstream.
229
+ - **Size**: RTF stores picture data as hexadecimal — two characters per byte — so an embedded image costs twice its file size in the document. Resizing an image in the editor changes its display size only; two caps applied at insert time are what bound the payload:
230
+ - `maxImageEdge` (1600 px) scales the picture down so neither edge exceeds it.
231
+ - `maxImageBytes` (512 KB, so roughly 1 MB of document per image) is the ceiling that actually matters. A picture over it is re-encoded until it fits: a PNG is tried as PNG first so screenshots and diagrams stay sharp, falls back to JPEG only when it is too heavy *and* has no transparency to lose, then steps down through 80%, 64% and 50% of the target size. Whichever candidate fits first is kept.
232
+
233
+ Set either to `0` to disable it. Both caps are applied by re-encoding through a `<canvas>`, so they only take effect in a browser — in a runtime without one (SSR, jsdom/happy-dom tests) the original bytes are passed through untouched. Do not rely on them to bound a payload assembled server-side.
234
+ - **Page fit**: a newly inserted picture is displayed at `maxImageDisplayWidth` (624 px) rather than filling the editor. RTF records the display size in twips — 624 px × 15 = 9360 twips = 6.5 in, the text width of a Letter page with 1 in margins — so pictures arrive sized to the page they will be printed or filed on, not to the width of the browser window. Resize handles still go up to the full column width when a picture deserves it.
235
+
236
+ ### Sending RTF over a transport
237
+
238
+ `htmlToRtf` output is written to survive being carried inside another format — an HL7 `OBX-5` field, a JSON string, a CSV cell:
239
+
240
+ - **One line.** The document contains no CR or LF anywhere — not in picture data, and not in text either: a newline inside text content is collapsed to the space a browser would have rendered (RTF readers ignore raw CR/LF, so `Hello\nworld` used to render as "Helloworld"). `<pre>` blocks keep their line structure as `\line`.
241
+ - **Uppercase hex.** Picture bytes are written as uppercase hex, so a PNG begins with the `89504E470D0A1A0A` signature that receivers commonly match on.
242
+ - **Format read from the bytes.** `\pngblip` vs `\jpegblip` is decided by sniffing the image's leading bytes, never by its declared MIME type — an operating system assigns `File.type` from the file extension, so a WebP saved as `.png` claims to be PNG. Bytes that are neither PNG nor JPEG are re-encoded on insert, or exported as a text placeholder rather than as a blip whose declared type contradicts its content.
243
+ - **Sized for the pipe.** `maxImageBytes` bounds each picture, and the payload is roughly twice that in hex. Lower it to fit a field-length limit.
244
+
245
+ Some receivers require the whole field on one unbroken line when it carries an image — that is why the writer emits no line breaks at all, rather than wrapping picture data the way many RTF writers do.
246
+
247
+ ### Knowing how big a document is
248
+
249
+ The transport cares about the size of the finished RTF, and pictures make that unpredictable. Two ways to read it:
250
+
251
+ ```svelte
252
+ <script>
253
+ let editor;
254
+ let size = 0;
255
+ </script>
256
+
257
+ <!-- an estimate on every change, cheap enough for a live indicator -->
258
+ <InkEditor bind:this={editor} onchange={({ estimatedRtfBytes }) => (size = estimatedRtfBytes)} />
259
+
260
+ <!-- the exact figure, when it is time to send -->
261
+ <button onclick={() => console.log(editor.getRtfSize())}>Check size</button>
262
+ ```
263
+
264
+ `getRtfSize()` does the full conversion — call it when a decision depends on it, not on every keystroke. `estimatedRtfBytes` counts picture data exactly (two characters per byte, which is what dominates) and approximates the rest, leaving an error of a few hundred bytes at most. The output is ASCII, so bytes and characters are the same number.
265
+
266
+ The library reports the size and stops there; whether an oversized document may be sent is the host application's decision.
267
+
268
+ Escaping for the carrying format is the integration layer's job, not the editor's. For HL7 v2 that means escaping the whole string's delimiters — `\` → `\E\`, `|` → `\F\`, `^` → `\S\`, `&` → `\T\`, `~` → `\R\` — because ordinary typed text ("Smith & Jones") reaches the RTF unescaped.
196
269
 
197
270
  ---
198
271
 
@@ -92,4 +92,22 @@
92
92
  .rtf-viewer :global(p:last-child) {
93
93
  margin-bottom: 0;
94
94
  }
95
+
96
+ /* Pictures decoded from \pict groups keep their stored width but never
97
+ overflow the viewer. */
98
+ .rtf-viewer :global(img) {
99
+ max-width: 100%;
100
+ height: auto;
101
+ }
102
+
103
+ .rtf-viewer :global(figure) {
104
+ margin: 1em 0;
105
+ }
106
+
107
+ .rtf-viewer :global(figcaption) {
108
+ margin-top: 0.4em;
109
+ font-size: 0.85em;
110
+ font-style: italic;
111
+ color: var(--text-muted, #8a7e72);
112
+ }
95
113
  </style>
@@ -0,0 +1,302 @@
1
+ <script lang="ts">
2
+ /**
3
+ * ImageOverlay — selection frame for an image inside the editor.
4
+ *
5
+ * Renders outside the contenteditable area (so it never ends up in the
6
+ * exported HTML) and is absolutely positioned over the target image by
7
+ * `bounds`, which must be a positioned ancestor of the editing surface.
8
+ *
9
+ * Provides drag-to-resize corner handles plus a small toolbar for
10
+ * placement (left / centre / right), width presets, caption editing and
11
+ * deletion.
12
+ */
13
+
14
+ interface Props {
15
+ /** Image currently selected in the editor, or null when nothing is selected. */
16
+ target?: HTMLImageElement | null;
17
+ /** Positioned element the overlay coordinates are relative to. */
18
+ bounds?: HTMLElement | null;
19
+ /** Widest the image may become — the editor's content box width in px. */
20
+ maxWidth?: () => number;
21
+ onchange?: () => void;
22
+ onremove?: () => void;
23
+ oncaption?: (img: HTMLImageElement) => void;
24
+ }
25
+
26
+ let { target = null, bounds = null, maxWidth, onchange, onremove, oncaption }: Props = $props();
27
+
28
+ const MIN_WIDTH = 40;
29
+ const CORNERS = ['nw', 'ne', 'sw', 'se'] as const;
30
+
31
+ let frame = $state({ left: 0, top: 0, width: 0, height: 0 });
32
+ let visible = $state(false);
33
+ let dragging = $state(false);
34
+ let sizeLabel = $state('');
35
+
36
+ /** Recompute the frame position — call after anything that reflows content. */
37
+ export function reposition(): void {
38
+ if (!target || !bounds || !target.isConnected) {
39
+ visible = false;
40
+ return;
41
+ }
42
+ const t = target.getBoundingClientRect();
43
+ const b = bounds.getBoundingClientRect();
44
+ frame = { left: t.left - b.left, top: t.top - b.top, width: t.width, height: t.height };
45
+ visible = t.width > 0 && t.height > 0;
46
+ }
47
+
48
+ $effect(() => {
49
+ // Re-read `target` so the effect re-runs whenever the selection changes.
50
+ void target;
51
+ reposition();
52
+ });
53
+
54
+ function limit(): number {
55
+ const max = maxWidth?.() ?? 0;
56
+ return max > MIN_WIDTH ? max : Number.POSITIVE_INFINITY;
57
+ }
58
+
59
+ function applyWidth(px: number): void {
60
+ if (!target) return;
61
+ const width = Math.round(Math.min(Math.max(px, MIN_WIDTH), limit()));
62
+ // The width attribute would fight the style rule — the style is the
63
+ // single source of truth that both the RTF writer and CSS read.
64
+ target.removeAttribute('width');
65
+ target.removeAttribute('height');
66
+ target.style.width = `${width}px`;
67
+ target.style.height = 'auto';
68
+ reposition();
69
+ }
70
+
71
+ function startResize(e: PointerEvent, corner: string): void {
72
+ if (!target) return;
73
+ e.preventDefault();
74
+ e.stopPropagation();
75
+
76
+ const rect = target.getBoundingClientRect();
77
+ const startX = e.clientX;
78
+ const startWidth = rect.width;
79
+ const ratio = rect.width > 0 ? rect.height / rect.width : 1;
80
+ const handle = e.currentTarget as HTMLElement;
81
+
82
+ dragging = true;
83
+ sizeLabel = `${Math.round(startWidth)} × ${Math.round(rect.height)}`;
84
+ handle.setPointerCapture?.(e.pointerId);
85
+
86
+ const onMove = (ev: PointerEvent) => {
87
+ // West-side handles grow the image as the pointer moves left.
88
+ const dx = corner.includes('w') ? startX - ev.clientX : ev.clientX - startX;
89
+ applyWidth(startWidth + dx);
90
+ const w = target ? target.getBoundingClientRect().width : 0;
91
+ sizeLabel = `${Math.round(w)} × ${Math.round(w * ratio)}`;
92
+ };
93
+
94
+ const onUp = (ev: PointerEvent) => {
95
+ handle.releasePointerCapture?.(ev.pointerId);
96
+ handle.removeEventListener('pointermove', onMove);
97
+ handle.removeEventListener('pointerup', onUp);
98
+ handle.removeEventListener('pointercancel', onUp);
99
+ dragging = false;
100
+ onchange?.();
101
+ };
102
+
103
+ handle.addEventListener('pointermove', onMove);
104
+ handle.addEventListener('pointerup', onUp);
105
+ handle.addEventListener('pointercancel', onUp);
106
+ }
107
+
108
+ function align(value: 'left' | 'center' | 'right'): void {
109
+ if (!target) return;
110
+ const block = target.closest('figure') ?? target.parentElement;
111
+ if (!block) return;
112
+ if (value === 'left') block.removeAttribute('style');
113
+ else (block as HTMLElement).style.textAlign = value;
114
+ reposition();
115
+ onchange?.();
116
+ }
117
+
118
+ function currentAlign(): string {
119
+ const block = target?.closest('figure') ?? target?.parentElement;
120
+ return (block as HTMLElement | null)?.style?.textAlign || 'left';
121
+ }
122
+
123
+ function scaleTo(percent: number): void {
124
+ const max = maxWidth?.() ?? 0;
125
+ if (!max) return;
126
+ applyWidth((max * percent) / 100);
127
+ onchange?.();
128
+ }
129
+ </script>
130
+
131
+ <svelte:window onresize={reposition} onscroll={reposition} />
132
+
133
+ {#if visible && target}
134
+ <div
135
+ class="ink-img-frame"
136
+ class:dragging
137
+ style="left:{frame.left}px;top:{frame.top}px;width:{frame.width}px;height:{frame.height}px"
138
+ >
139
+ <div class="ink-img-tools">
140
+ <button
141
+ type="button"
142
+ title="Align left"
143
+ class:active={currentAlign() === 'left'}
144
+ onclick={() => align('left')}
145
+ >
146
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="15" y2="12"/><line x1="3" y1="18" x2="18" y2="18"/></svg>
147
+ </button>
148
+ <button
149
+ type="button"
150
+ title="Align center"
151
+ class:active={currentAlign() === 'center'}
152
+ onclick={() => align('center')}
153
+ >
154
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="6" y1="12" x2="18" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/></svg>
155
+ </button>
156
+ <button
157
+ type="button"
158
+ title="Align right"
159
+ class:active={currentAlign() === 'right'}
160
+ onclick={() => align('right')}
161
+ >
162
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="9" y1="12" x2="21" y2="12"/><line x1="6" y1="18" x2="21" y2="18"/></svg>
163
+ </button>
164
+
165
+ <span class="ink-img-sep"></span>
166
+
167
+ <button type="button" title="Quarter width" onclick={() => scaleTo(25)}>25%</button>
168
+ <button type="button" title="Half width" onclick={() => scaleTo(50)}>50%</button>
169
+ <button type="button" title="Full width" onclick={() => scaleTo(100)}>100%</button>
170
+
171
+ <span class="ink-img-sep"></span>
172
+
173
+ <button type="button" title="Edit description" onclick={() => target && oncaption?.(target)}>
174
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7V5h16v2"/><line x1="9" y1="19" x2="15" y2="19"/><line x1="12" y1="5" x2="12" y2="19"/></svg>
175
+ </button>
176
+ <button type="button" class="danger" title="Remove image" onclick={() => onremove?.()}>
177
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
178
+ </button>
179
+ </div>
180
+
181
+ {#each CORNERS as corner}
182
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
183
+ <span
184
+ class="ink-img-handle {corner}"
185
+ onpointerdown={(e) => startResize(e, corner)}
186
+ ></span>
187
+ {/each}
188
+
189
+ {#if dragging}
190
+ <span class="ink-img-size">{sizeLabel}</span>
191
+ {/if}
192
+ </div>
193
+ {/if}
194
+
195
+ <style>
196
+ .ink-img-frame {
197
+ position: absolute;
198
+ box-sizing: border-box;
199
+ border: 1.5px solid var(--accent, #d4622b);
200
+ border-radius: 2px;
201
+ /* Clicks fall through to the image/text underneath; only the handles
202
+ and the toolbar are interactive. */
203
+ pointer-events: none;
204
+ z-index: 20;
205
+ }
206
+
207
+ .ink-img-handle {
208
+ position: absolute;
209
+ width: 11px;
210
+ height: 11px;
211
+ background: white;
212
+ border: 1.5px solid var(--accent, #d4622b);
213
+ border-radius: 2px;
214
+ pointer-events: auto;
215
+ touch-action: none;
216
+ }
217
+
218
+ .ink-img-handle.nw { top: -6px; left: -6px; cursor: nwse-resize; }
219
+ .ink-img-handle.ne { top: -6px; right: -6px; cursor: nesw-resize; }
220
+ .ink-img-handle.sw { bottom: -6px; left: -6px; cursor: nesw-resize; }
221
+ .ink-img-handle.se { bottom: -6px; right: -6px; cursor: nwse-resize; }
222
+
223
+ .ink-img-tools {
224
+ position: absolute;
225
+ bottom: calc(100% + 8px);
226
+ left: 50%;
227
+ transform: translateX(-50%);
228
+ display: flex;
229
+ align-items: center;
230
+ gap: 2px;
231
+ padding: 4px;
232
+ background: white;
233
+ border: 1px solid var(--border, #e5e2dc);
234
+ border-radius: var(--radius-sm, 5px);
235
+ box-shadow: var(--shadow-md, 0 4px 12px rgba(44, 37, 32, 0.12));
236
+ pointer-events: auto;
237
+ white-space: nowrap;
238
+ }
239
+
240
+ .dragging .ink-img-tools {
241
+ opacity: 0;
242
+ }
243
+
244
+ .ink-img-tools button {
245
+ display: inline-flex;
246
+ align-items: center;
247
+ justify-content: center;
248
+ min-width: 26px;
249
+ height: 26px;
250
+ padding: 0 5px;
251
+ border: none;
252
+ background: none;
253
+ border-radius: 4px;
254
+ color: var(--text-muted, #8a7e72);
255
+ font-family: 'DM Sans', sans-serif;
256
+ font-size: 11px;
257
+ font-weight: 500;
258
+ cursor: pointer;
259
+ transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
260
+ }
261
+
262
+ .ink-img-tools button:hover {
263
+ background: var(--surface, #f2f0ec);
264
+ color: var(--text, #2c2520);
265
+ }
266
+
267
+ .ink-img-tools button.active {
268
+ background: var(--accent-soft, #fdf0e9);
269
+ color: var(--accent, #d4622b);
270
+ }
271
+
272
+ .ink-img-tools button.danger:hover {
273
+ background: #fdecea;
274
+ color: #d32f2f;
275
+ }
276
+
277
+ .ink-img-tools svg {
278
+ width: 14px;
279
+ height: 14px;
280
+ }
281
+
282
+ .ink-img-sep {
283
+ width: 1px;
284
+ height: 16px;
285
+ margin: 0 4px;
286
+ background: var(--border, #e5e2dc);
287
+ }
288
+
289
+ .ink-img-size {
290
+ position: absolute;
291
+ bottom: 6px;
292
+ right: 6px;
293
+ padding: 2px 7px;
294
+ border-radius: 3px;
295
+ background: rgba(26, 23, 20, 0.78);
296
+ color: white;
297
+ font-family: 'DM Sans', sans-serif;
298
+ font-size: 11px;
299
+ font-variant-numeric: tabular-nums;
300
+ pointer-events: none;
301
+ }
302
+ </style>
@@ -0,0 +1,27 @@
1
+ /**
2
+ * ImageOverlay — selection frame for an image inside the editor.
3
+ *
4
+ * Renders outside the contenteditable area (so it never ends up in the
5
+ * exported HTML) and is absolutely positioned over the target image by
6
+ * `bounds`, which must be a positioned ancestor of the editing surface.
7
+ *
8
+ * Provides drag-to-resize corner handles plus a small toolbar for
9
+ * placement (left / centre / right), width presets, caption editing and
10
+ * deletion.
11
+ */
12
+ interface Props {
13
+ /** Image currently selected in the editor, or null when nothing is selected. */
14
+ target?: HTMLImageElement | null;
15
+ /** Positioned element the overlay coordinates are relative to. */
16
+ bounds?: HTMLElement | null;
17
+ /** Widest the image may become — the editor's content box width in px. */
18
+ maxWidth?: () => number;
19
+ onchange?: () => void;
20
+ onremove?: () => void;
21
+ oncaption?: (img: HTMLImageElement) => void;
22
+ }
23
+ declare const ImageOverlay: import("svelte").Component<Props, {
24
+ reposition: () => void;
25
+ }, "">;
26
+ type ImageOverlay = ReturnType<typeof ImageOverlay>;
27
+ export default ImageOverlay;