svelte-rtf-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/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # svelte-rtf-editor
2
+
3
+ RTF viewer and rich-text editor components for **Svelte 5**.
4
+
5
+ [![npm](https://img.shields.io/npm/v/svelte-rtf-editor)](https://www.npmjs.com/package/svelte-rtf-editor)
6
+ [![license](https://img.shields.io/npm/l/svelte-rtf-editor)](./LICENSE)
7
+
8
+ ---
9
+
10
+ ## Features
11
+
12
+ - **`RtfViewer`** — Renders an RTF string as formatted HTML with a one-click "Copy RTF" button
13
+ - **`InkEditor`** — Full rich-text editor that reads and writes RTF, built on `contenteditable`
14
+ - Formatting toolbar: bold, italic, underline, strikethrough, font colour
15
+ - Block-level controls: headings (H1–H3), paragraph, blockquote, code block
16
+ - Import an RTF file from disk; export as RTF, HTML, or Markdown
17
+ - Optional auto-save to `localStorage`
18
+ - Word count and character count status bar
19
+ - Keyboard shortcuts (`Ctrl+S` to save, `Ctrl+B/I/U` for formatting)
20
+ - **Utility functions** — `rtfToHtml`, `htmlToRtf`, `readRtfFile`, `htmlToMarkdown`, `downloadFile`
21
+ - Zero runtime dependencies — just Svelte 5 as a peer dependency
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install svelte-rtf-editor
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Usage
34
+
35
+ ### Read-only RTF viewer
36
+
37
+ ```svelte
38
+ <script>
39
+ import { RtfViewer } from 'svelte-rtf-editor';
40
+
41
+ const rtf = String.raw`{\rtf1\ansi {\b Hello}, {\i world}!}`;
42
+ </script>
43
+
44
+ <RtfViewer content={rtf} />
45
+ ```
46
+
47
+ The viewer renders the RTF as HTML and places a **Copy RTF** button in the top-right corner.
48
+
49
+ ---
50
+
51
+ ### Rich-text editor
52
+
53
+ ```svelte
54
+ <script>
55
+ import { InkEditor } from 'svelte-rtf-editor';
56
+
57
+ let html = '';
58
+ </script>
59
+
60
+ <InkEditor
61
+ placeholder="Start writing…"
62
+ onchange={({ html: h }) => (html = h)}
63
+ />
64
+ ```
65
+
66
+ #### Getting RTF out of the editor
67
+
68
+ Bind a reference to the component with `bind:this` and call `getRTF()`:
69
+
70
+ ```svelte
71
+ <script>
72
+ import { InkEditor } from 'svelte-rtf-editor';
73
+
74
+ let editor;
75
+
76
+ function save() {
77
+ const rtf = editor.getRTF();
78
+ console.log(rtf); // {\rtf1\ansi ...}
79
+ }
80
+ </script>
81
+
82
+ <InkEditor bind:this={editor} />
83
+ <button onclick={save}>Save</button>
84
+ ```
85
+
86
+ #### Pre-loading content
87
+
88
+ Pass an HTML string as `content`. If the content is already RTF, convert it first:
89
+
90
+ ```svelte
91
+ <script>
92
+ import { InkEditor, rtfToHtml } from 'svelte-rtf-editor';
93
+
94
+ const rtfFromServer = String.raw`{\rtf1\ansi {\b Hello}}`;
95
+ const html = rtfToHtml(rtfFromServer);
96
+ </script>
97
+
98
+ <InkEditor content={html} autosave={false} />
99
+ ```
100
+
101
+ ---
102
+
103
+ ## API
104
+
105
+ ### `<RtfViewer>`
106
+
107
+ | Prop | Type | Default | Description |
108
+ |-----------|----------|---------|------------------------------------------|
109
+ | `content` | `string` | — | Raw RTF string to parse and display |
110
+
111
+ ---
112
+
113
+ ### `<InkEditor>`
114
+
115
+ #### Props
116
+
117
+ | Prop | Type | Default | Description |
118
+ |-----------------|------------|------------------------------|-----------------------------------------------------------|
119
+ | `content` | `string` | `'<p></p>'` | Initial HTML content |
120
+ | `placeholder` | `string` | `'Start writing something beautiful...'` | Placeholder text when editor is empty |
121
+ | `autosave` | `boolean` | `true` | Save content to `localStorage` on change |
122
+ | `storageKey` | `string` | `'ink-editor-content'` | `localStorage` key used for auto-save |
123
+ | `showToolbar` | `boolean` | `true` | Show the formatting toolbar |
124
+ | `showStatusBar` | `boolean` | `true` | Show the word/character count bar |
125
+ | `minHeight` | `string` | `'40vh'` | CSS `min-height` of the editing area |
126
+ | `readonly` | `boolean` | `false` | Disable editing |
127
+
128
+ #### Callback props
129
+
130
+ | Prop | Payload | Description |
131
+ |------------|----------------------------------------------|---------------------------------------------|
132
+ | `onchange` | `{ html, text, wordCount, charCount }` | Fired on every content change |
133
+ | `onsave` | `{ html }` | Fired on auto-save or `Ctrl+S` |
134
+ | `onimport` | `{ html }` | Fired after a successful RTF file import |
135
+
136
+ #### Methods (via `bind:this`)
137
+
138
+ ```ts
139
+ editor.getHTML() // → string — current editor HTML
140
+ editor.getText() // → string — plain text (no tags)
141
+ editor.setHTML(html) // → void — replace content programmatically
142
+ editor.getMarkdown() // → string — Markdown conversion of content
143
+ editor.getRTF() // → string — RTF conversion of content
144
+ editor.clear() // → void — clear the editor
145
+ editor.focus() // → void — focus the editor
146
+ editor.exportFile(format) // → void — download as 'html' | 'md' | 'rtf'
147
+ editor.importRtf() // → void — open the file picker to import .rtf
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Utility functions
153
+
154
+ ```ts
155
+ import { rtfToHtml, htmlToRtf, readRtfFile, htmlToMarkdown, downloadFile } from 'svelte-rtf-editor';
156
+ ```
157
+
158
+ | Function | Description |
159
+ |---------------------------------------|----------------------------------------------------|
160
+ | `rtfToHtml(rtf: string): string` | Parse an RTF string and return an HTML string |
161
+ | `htmlToRtf(html: string): string` | Convert an HTML string to RTF |
162
+ | `readRtfFile(file: File): Promise<string>` | Read a `.rtf` File object and return HTML |
163
+ | `htmlToMarkdown(el: HTMLElement): string` | Convert a DOM element's content to Markdown |
164
+ | `downloadFile(name, content, mime): void` | Trigger a file download in the browser |
165
+
166
+ ---
167
+
168
+ ## Theming
169
+
170
+ The components use CSS custom properties with sensible fallbacks, so they work out of the box but are easy to customise. Override these in your own CSS:
171
+
172
+ ```css
173
+ :root {
174
+ --text: #2c2520;
175
+ --text-muted: #8a7e72;
176
+ --surface: #f2f0ec;
177
+ --border: #e5e2dc;
178
+ --ink: #1a1714;
179
+ --accent: #6e56cf;
180
+ --accent-soft: #ede9fe;
181
+ --radius-sm: 5px;
182
+ --transition: 150ms ease;
183
+ }
184
+ ```
185
+
186
+ ---
187
+
188
+ ## Keyboard shortcuts
189
+
190
+ | Shortcut | Action |
191
+ |------------|-------------------|
192
+ | `Ctrl+B` | Bold |
193
+ | `Ctrl+I` | Italic |
194
+ | `Ctrl+U` | Underline |
195
+ | `Ctrl+S` | Save (fires `onsave`) |
196
+
197
+ ---
198
+
199
+ ## Local development
200
+
201
+ Clone this repo and link it to your project for instant iteration without publishing:
202
+
203
+ ```bash
204
+ # In svelte-rtf-editor/
205
+ npm run prepack # builds dist/
206
+ npm link
207
+
208
+ # In your project/
209
+ npm link svelte-rtf-editor
210
+ ```
211
+
212
+ Re-run `npm run prepack` in this repo after any change. When done:
213
+
214
+ ```bash
215
+ # In your project/
216
+ npm unlink svelte-rtf-editor
217
+ npm install svelte-rtf-editor
218
+ ```
219
+
220
+ ---
221
+
222
+ ## License
223
+
224
+ MIT
@@ -0,0 +1,95 @@
1
+ <script lang="ts">
2
+ import { rtfToHtml } from "./rtf-parser.js";
3
+
4
+ let { content }: { content: string } = $props();
5
+
6
+ const htmlContent = $derived(rtfToHtml(content));
7
+
8
+ let copied = $state(false);
9
+
10
+ async function copyRtf() {
11
+ await navigator.clipboard.writeText(content);
12
+ copied = true;
13
+ setTimeout(() => (copied = false), 2000);
14
+ }
15
+ </script>
16
+
17
+ <div class="rtf-viewer-wrap">
18
+ <button class="copy-btn" onclick={copyRtf} title="Copy raw RTF">
19
+ {#if copied}
20
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
21
+ Copied
22
+ {:else}
23
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
24
+ Copy RTF
25
+ {/if}
26
+ </button>
27
+ <div class="rtf-viewer">
28
+ {@html htmlContent}
29
+ </div>
30
+ </div>
31
+
32
+ <style>
33
+ .rtf-viewer-wrap {
34
+ position: relative;
35
+ }
36
+
37
+ .copy-btn {
38
+ position: absolute;
39
+ top: 0;
40
+ right: 0;
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 5px;
44
+ padding: 4px 10px;
45
+ font-size: 12px;
46
+ font-weight: 500;
47
+ border: 1px solid var(--border, #e5e2dc);
48
+ border-radius: var(--radius-sm, 5px);
49
+ background: var(--surface, #f2f0ec);
50
+ color: var(--text-muted, #8a7e72);
51
+ cursor: pointer;
52
+ transition: all 150ms ease;
53
+ }
54
+
55
+ .copy-btn:hover {
56
+ background: var(--border, #e5e2dc);
57
+ color: var(--text, #2c2520);
58
+ }
59
+
60
+ .copy-btn svg {
61
+ width: 13px;
62
+ height: 13px;
63
+ flex-shrink: 0;
64
+ }
65
+
66
+ .rtf-viewer {
67
+ line-height: 1.4;
68
+ white-space: normal;
69
+ word-wrap: break-word;
70
+ }
71
+
72
+ .rtf-viewer :global(br) {
73
+ display: inline;
74
+ }
75
+
76
+ .rtf-viewer :global(strong) {
77
+ font-weight: 600;
78
+ }
79
+
80
+ .rtf-viewer :global(em) {
81
+ font-style: italic;
82
+ }
83
+
84
+ .rtf-viewer :global(u) {
85
+ text-decoration: underline;
86
+ }
87
+
88
+ .rtf-viewer :global(p) {
89
+ margin: 0 0 0.2em;
90
+ }
91
+
92
+ .rtf-viewer :global(p:last-child) {
93
+ margin-bottom: 0;
94
+ }
95
+ </style>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ content: string;
3
+ };
4
+ declare const RtfViewer: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type RtfViewer = ReturnType<typeof RtfViewer>;
6
+ export default RtfViewer;
@@ -0,0 +1,72 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ value?: string;
4
+ title?: string;
5
+ onchange?: (color: string) => void;
6
+ }
7
+
8
+ let { value = '#2C2520', title = 'Color', onchange }: Props = $props();
9
+
10
+ let currentColor = $state(value);
11
+
12
+ function handleChange(e: Event) {
13
+ currentColor = (e.target as HTMLInputElement).value;
14
+ onchange?.(currentColor);
15
+ }
16
+ </script>
17
+
18
+ <div class="color-picker-wrap">
19
+ <button class="color-swatch" {title} type="button">
20
+ <div class="color-swatch-inner" style="background: {currentColor};"></div>
21
+ </button>
22
+ <input
23
+ type="color"
24
+ class="color-input"
25
+ value={currentColor}
26
+ onchange={handleChange}
27
+ {title}
28
+ />
29
+ </div>
30
+
31
+ <style>
32
+ .color-picker-wrap {
33
+ position: relative;
34
+ display: flex;
35
+ align-items: center;
36
+ }
37
+
38
+ .color-swatch {
39
+ width: 34px;
40
+ height: 34px;
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ border: none;
45
+ background: transparent;
46
+ border-radius: var(--radius-sm);
47
+ cursor: pointer;
48
+ transition: all var(--transition);
49
+ }
50
+
51
+ .color-swatch:hover {
52
+ background: var(--surface);
53
+ }
54
+
55
+ .color-swatch-inner {
56
+ width: 18px;
57
+ height: 18px;
58
+ border-radius: 4px;
59
+ border: 2px solid var(--border);
60
+ transition: all var(--transition);
61
+ }
62
+
63
+ .color-input {
64
+ position: absolute;
65
+ width: 34px;
66
+ height: 34px;
67
+ opacity: 0;
68
+ cursor: pointer;
69
+ top: 0;
70
+ left: 0;
71
+ }
72
+ </style>
@@ -0,0 +1,8 @@
1
+ interface Props {
2
+ value?: string;
3
+ title?: string;
4
+ onchange?: (color: string) => void;
5
+ }
6
+ declare const ColorPicker: import("svelte").Component<Props, {}, "">;
7
+ type ColorPicker = ReturnType<typeof ColorPicker>;
8
+ export default ColorPicker;