stilo-editor 0.2.0 → 0.3.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 +111 -173
- package/dist/action-lB2jnYNO.js +66 -0
- package/dist/content.css +9 -0
- package/dist/core/editor.d.ts +12 -2
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +5 -0
- package/dist/react/stilo-editor.d.ts +37 -0
- package/dist/react/use-stilo-editor.d.ts +15 -0
- package/dist/react.js +75 -0
- package/dist/{src-CV6pgfTS.js → src-DxTXpULc.js} +402 -392
- package/dist/style.css +9 -0
- package/dist/svelte/action.d.ts +5 -0
- package/dist/svelte/action.js +1 -0
- package/dist/svelte/editor-action.d.ts +15 -0
- package/dist/svelte/editor-with-ui-action.d.ts +35 -0
- package/dist/svelte/index.d.ts +5 -0
- package/dist/svelte/index.js +3 -0
- package/dist/svelte/stilo-editor.svelte +47 -0
- package/dist/svelte/stilo-editor.svelte.d.ts +22 -0
- package/dist/ui/components/link-popover.d.ts +4 -1
- package/dist/ui-WFNvrkbM.js +2351 -0
- package/dist/ui.js +1 -2350
- package/dist/vue/index.d.ts +5 -0
- package/dist/vue/stilo-editor.d.ts +52 -0
- package/dist/vue/use-stilo-editor.d.ts +19 -0
- package/dist/vue.js +101 -0
- package/package.json +43 -4
package/README.md
CHANGED
|
@@ -5,144 +5,43 @@
|
|
|
5
5
|
</picture>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
+
<h3 align="center">A complete rich text editor. Everything included, ready in minutes.</h3>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
Free and open source (MIT) · clean HTML in and out · built on ProseMirror
|
|
12
|
+
</p>
|
|
13
|
+
|
|
8
14
|
<p align="center">
|
|
9
15
|
<a href="https://www.npmjs.com/package/stilo-editor"><img src="https://img.shields.io/npm/v/stilo-editor" alt="npm version"></a>
|
|
10
|
-
<a href="
|
|
16
|
+
<a href="https://github.com/kojak8791/stilo-editor/actions/workflows/ci.yml"><img src="https://github.com/kojak8791/stilo-editor/actions/workflows/ci.yml/badge.svg" alt="CI status"></a>
|
|
11
17
|
<img src="https://img.shields.io/npm/types/stilo-editor" alt="TypeScript types included">
|
|
18
|
+
<a href="LICENSE"><img src="https://img.shields.io/npm/l/stilo-editor" alt="MIT license"></a>
|
|
12
19
|
</p>
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
<p align="center">
|
|
22
|
+
<a href="https://kojak8791.github.io/stilo-editor/"><strong>Live demo</strong></a> ·
|
|
23
|
+
<a href="docs/frameworks/react.md"><strong>React</strong></a> ·
|
|
24
|
+
<a href="docs/frameworks/vue.md"><strong>Vue</strong></a> ·
|
|
25
|
+
<a href="docs/frameworks/svelte.md"><strong>Svelte</strong></a> ·
|
|
26
|
+
<a href="docs/API.md"><strong>API</strong></a> ·
|
|
27
|
+
<a href="docs/FEATURES.md"><strong>All features</strong></a>
|
|
28
|
+
</p>
|
|
16
29
|
|
|
17
30
|
<p align="center">
|
|
18
31
|
<img src="https://raw.githubusercontent.com/kojak8791/stilo-editor/main/assets/screenshot.png" alt="Stilo with the default toolbar: formatted text, a checklist, a table and the link popover open" width="800">
|
|
19
32
|
</p>
|
|
20
33
|
|
|
21
|
-
|
|
22
|
-
<a href="https://kojak8791.github.io/stilo-editor/">See it in action: try the live demo →</a>
|
|
23
|
-
</h2>
|
|
24
|
-
|
|
25
|
-
> [!WARNING]
|
|
26
|
-
> **Preview release.** Versions `0.x` are early previews: the editor works, but APIs may change
|
|
27
|
-
> in any minor version until `1.0.0`. Read the release notes before updating.
|
|
28
|
-
|
|
29
|
-
## Why Stilo
|
|
30
|
-
|
|
31
|
-
- **Built for very large documents.** Typing stays immediate in a document longer than most
|
|
32
|
-
novels (see [Large documents](https://github.com/kojak8791/stilo-editor#large-documents)).
|
|
33
|
-
Measured in a real browser, checked against budgets, not assumed.
|
|
34
|
-
- **Entirely open source (MIT).** Aiming at the feature level of the established rich text
|
|
35
|
-
editors, with no paid tiers and no closed "pro" extensions.
|
|
36
|
-
- **HTML is the public format.** Content goes in as HTML and comes out as clean, semantic,
|
|
37
|
-
self-contained HTML, with a strict allowlist for tags, attributes and inline styles.
|
|
38
|
-
- **Headless core, optional UI.** The core works without any UI; the default toolbar and menus are
|
|
39
|
-
a separate, replaceable module.
|
|
40
|
-
- **Extensible by design.** Every feature is an extension with the same shape.
|
|
41
|
-
- **Framework-agnostic.** Vanilla TypeScript first; React/Vue wrappers later as thin layers.
|
|
42
|
-
|
|
43
|
-
## Large documents
|
|
44
|
-
|
|
45
|
-
Stilo is made for long documents: contracts, manuals, books, reports. Typing feels immediate in
|
|
46
|
-
a document of 10,000 paragraphs — about 400,000 words, longer than most novels — and the editor
|
|
47
|
-
stays usable at five times that size, without pagination tricks and without splitting the
|
|
48
|
-
content. Formatting, search and selection keep up with the size of the text.
|
|
49
|
-
|
|
50
|
-
This is measured, not promised. A benchmark drives the editor in a real browser on generated
|
|
51
|
-
documents and times everything from loading and exporting the HTML down to the milliseconds
|
|
52
|
-
between a key press and the character on screen; every change is checked against performance
|
|
53
|
-
budgets. What it means in practice, the numbers and the limits:
|
|
54
|
-
[Performance](docs/PERFORMANCE.md).
|
|
55
|
-
|
|
56
|
-
## Features
|
|
57
|
-
|
|
58
|
-
What the editor does today, with `defaultExtensions` and the default toolbar. On macOS, use
|
|
59
|
-
<kbd>⌘</kbd> for <kbd>Ctrl</kbd> and <kbd>Option</kbd> for <kbd>Alt</kbd>. The *Markdown*
|
|
60
|
-
column is what you type to get the format as you write.
|
|
61
|
-
|
|
62
|
-
| Feature | Keyboard | Markdown | Notes |
|
|
63
|
-
|---|---|---|---|
|
|
64
|
-
| **Text** | | | |
|
|
65
|
-
| Bold | <kbd>Ctrl</kbd>+<kbd>B</kbd> | `**text**` | |
|
|
66
|
-
| Italic | <kbd>Ctrl</kbd>+<kbd>I</kbd> | `*text*` `_text_` | |
|
|
67
|
-
| Underline | <kbd>Ctrl</kbd>+<kbd>U</kbd> | | |
|
|
68
|
-
| Strikethrough | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> | `~~text~~` | |
|
|
69
|
-
| Inline code | <kbd>Ctrl</kbd>+<kbd>E</kbd> | `` `text` `` | |
|
|
70
|
-
| Subscript | <kbd>Ctrl</kbd>+<kbd>,</kbd> | | Subscript and superscript exclude each other |
|
|
71
|
-
| Superscript | <kbd>Ctrl</kbd>+<kbd>.</kbd> | | |
|
|
72
|
-
| Font family | | | Toolbar dropdown: generic families (sans serif, serif, monospace) and common fonts, each shown in its own font; a font that is not in the list (e.g. pasted from Word) is still shown |
|
|
73
|
-
| Font size | | | − and + change the size by one point; the field between them shows the size, takes a typed one and opens a list. Unit pt by default (px or rem as an option) |
|
|
74
|
-
| Text color | | | The button applies the last color; its arrow opens a palette, "No color" and a custom color |
|
|
75
|
-
| Highlight | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>H</kbd> | `==text==` | Same button as text color; the shortcut applies the default highlight |
|
|
76
|
-
| Clear formatting | <kbd>Ctrl</kbd>+<kbd>\\</kbd> | | Links stay |
|
|
77
|
-
| Text case | | | A menu: UPPERCASE, lowercase, Title Case, Sentence case, tOGGLE cASE; the letters change, the formatting stays |
|
|
78
|
-
| **Links** | | | |
|
|
79
|
-
| Link | <kbd>Ctrl</kbd>+<kbd>K</kbd> | | A popover sets the URL and "open in new tab"; on a link, a small bubble offers open, edit and remove |
|
|
80
|
-
| **Blocks** | | | |
|
|
81
|
-
| Paragraph | <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>0</kbd> | | |
|
|
82
|
-
| Headings 1–6 | <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>1</kbd>…<kbd>6</kbd> | `# ` … `###### ` | Also in the toolbar dropdown |
|
|
83
|
-
| Quote | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>B</kbd> | `> ` | |
|
|
84
|
-
| Code block | <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>C</kbd> | ```` ``` ```` + space, ```` ```js ```` + space for a language | Plain text in a monospace font, no formatting; the selected paragraphs become one block, a line each, and back. <kbd>Enter</kbd> on an empty last line, or <kbd>Ctrl</kbd>+<kbd>Enter</kbd>, leaves it. The language is written as `class="language-js"` for syntax highlighters |
|
|
85
|
-
| Horizontal line | | `---` | |
|
|
86
|
-
| Line break | <kbd>Shift</kbd>+<kbd>Enter</kbd> | | New line in the same paragraph |
|
|
87
|
-
| Align left / center / right / justify | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>L</kbd> / <kbd>E</kbd> / <kbd>R</kbd> / <kbd>J</kbd> | | Paragraphs and headings; the toolbar button opens the four alignments; changing the block type (also with `# `) keeps the alignment |
|
|
88
|
-
| Line spacing | | | Paragraphs and headings; the toolbar menu offers Default, 1, 1.15, 1.5 and 2; changing the block type keeps it |
|
|
89
|
-
| Move block up / down | <kbd>Alt</kbd>+<kbd>↑</kbd> / <kbd>Alt</kbd>+<kbd>↓</kbd> | | The block or list item at the cursor, or all the selected ones; sub-items move along |
|
|
90
|
-
| **Lists** | | | |
|
|
91
|
-
| Bulleted list | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>8</kbd> | `- ` `* ` `+ ` | |
|
|
92
|
-
| Numbered list | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>7</kbd> | `1. ` | Starts from the number you type |
|
|
93
|
-
| Checklist | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>9</kbd> | `[ ] ` `[x] ` | Click a checkbox, or press <kbd>Ctrl</kbd>+<kbd>Enter</kbd> to check the selected items; checked items are struck through |
|
|
94
|
-
| Indent / outdent item | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | | |
|
|
95
|
-
| **Tables** | | | |
|
|
96
|
-
| Insert a table | | | Toolbar grid up to 10 × 8, with or without a header row |
|
|
97
|
-
| Rows, columns, cells | | | With the cursor in a table, the same button opens: insert or delete rows and columns, merge and split cells, header row and column, delete table |
|
|
98
|
-
| Table handles | | | Handles on the row and column under the mouse select them and open their menu (insert, delete); a corner handle opens merge, split and delete table |
|
|
99
|
-
| Move between cells | <kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd> | | <kbd>Tab</kbd> in the last cell adds a row |
|
|
100
|
-
| Select cells | | | Drag across cells |
|
|
101
|
-
| Resize a column | | | Drag the border of a column; the width is kept in the HTML |
|
|
102
|
-
| **Images** | | | |
|
|
103
|
-
| Paste an image | <kbd>Ctrl</kbd>+<kbd>V</kbd> | | A copied image or screenshot is embedded in the HTML (PNG, JPEG, GIF, WebP, up to 5 MB by default); an image copied from a web page keeps its address |
|
|
104
|
-
| Resize an image | | | Select it and drag one of its side handles; the proportions stay |
|
|
105
|
-
| **Insert** | | | |
|
|
106
|
-
| Special characters and emoji | | | A popover with search, symbols and emoji; it stays open to insert several in a row |
|
|
107
|
-
| **Editing** | | | |
|
|
108
|
-
| Undo | <kbd>Ctrl</kbd>+<kbd>Z</kbd> | | <kbd>Backspace</kbd> right after a Markdown shortcut undoes it |
|
|
109
|
-
| Redo | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> or <kbd>Ctrl</kbd>+<kbd>Y</kbd> | | |
|
|
110
|
-
| Paste | <kbd>Ctrl</kbd>+<kbd>V</kbd> | | HTML is cleaned up; plain text lines become paragraphs. From Word and Google Docs the body font, size and color are dropped (text that differs keeps them) and Word lists become real lists; tables keep their column widths |
|
|
111
|
-
| Paste as plain text | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>V</kbd> | | Only the text, without formatting |
|
|
112
|
-
| Find and replace | <kbd>Ctrl</kbd>+<kbd>F</kbd> | | A bar under the toolbar: match case, whole word, previous and next match, replace one or all; the matches are counted exactly and the first 1,000 are highlighted; <kbd>Enter</kbd> / <kbd>Shift</kbd>+<kbd>Enter</kbd> go to the next / previous match, <kbd>Esc</kbd> closes it |
|
|
113
|
-
| Word and character count | | | Optional status bar under the editor; also `getWordCount()` in the API |
|
|
114
|
-
|
|
115
|
-
<table>
|
|
116
|
-
<tr>
|
|
117
|
-
<td width="50%"><img src="https://raw.githubusercontent.com/kojak8791/stilo-editor/main/assets/screenshot-find.png" alt="The find bar under the toolbar: five matches of espresso highlighted, the current one in orange, and ristretto in the replace field" width="400"></td>
|
|
118
|
-
<td width="50%"><img src="https://raw.githubusercontent.com/kojak8791/stilo-editor/main/assets/screenshot-table.png" alt="A coffee menu table with a row selected by its handle and the row menu open: insert row above, insert row below, delete row" width="400"></td>
|
|
119
|
-
</tr>
|
|
120
|
-
<tr>
|
|
121
|
-
<td>Find and replace: match case, whole word, an exact count of the matches.</td>
|
|
122
|
-
<td>Tables: handles on rows and columns open their menu.</td>
|
|
123
|
-
</tr>
|
|
124
|
-
<tr>
|
|
125
|
-
<td width="50%"><img src="https://raw.githubusercontent.com/kojak8791/stilo-editor/main/assets/screenshot-fonts.png" alt="The font dropdown open on a Times New Roman text, each font drawn in its own font" width="400"></td>
|
|
126
|
-
<td width="50%"><img src="https://raw.githubusercontent.com/kojak8791/stilo-editor/main/assets/screenshot-color.png" alt="The text color palette open, with No color, the color grid and a custom color field" width="400"></td>
|
|
127
|
-
</tr>
|
|
128
|
-
<tr>
|
|
129
|
-
<td>Fonts and sizes: the toolbar shows the font and the size at the cursor.</td>
|
|
130
|
-
<td>Text color and highlight: a palette, the last color one click away, a custom color.</td>
|
|
131
|
-
</tr>
|
|
132
|
-
</table>
|
|
133
|
-
|
|
134
|
-
**Keyboard and accessibility.** Most shortcuts are the ones of Google Docs; the others follow
|
|
135
|
-
the usual conventions of web editors, and every toolbar tooltip shows its shortcut. The toolbar
|
|
136
|
-
follows the WAI-ARIA *Toolbar* pattern: <kbd>Alt</kbd>+<kbd>F10</kbd> moves the focus from the
|
|
137
|
-
text to the toolbar, the arrow keys move between buttons, <kbd>Esc</kbd> goes back to the text.
|
|
138
|
-
Every popover works with the keyboard alone and <kbd>Esc</kbd> closes it.
|
|
139
|
-
|
|
140
|
-
## Usage
|
|
34
|
+
## Quick start
|
|
141
35
|
|
|
142
36
|
```sh
|
|
143
37
|
npm install stilo-editor
|
|
144
38
|
```
|
|
145
39
|
|
|
40
|
+
```html
|
|
41
|
+
<div id="toolbar"></div>
|
|
42
|
+
<div id="editor"></div>
|
|
43
|
+
```
|
|
44
|
+
|
|
146
45
|
```ts
|
|
147
46
|
import 'stilo-editor/style.css';
|
|
148
47
|
import { Editor, defaultExtensions } from 'stilo-editor';
|
|
@@ -159,69 +58,108 @@ createToolbar(editor, { container: document.querySelector<HTMLElement>('#toolbar
|
|
|
159
58
|
editor.getHTML(); // '<p>Hello <strong>world</strong></p>'
|
|
160
59
|
```
|
|
161
60
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
your own
|
|
61
|
+
✅ That is the whole setup.
|
|
62
|
+
|
|
63
|
+
### Want your own UI?
|
|
64
|
+
|
|
65
|
+
Skip the toolbar: the core is headless.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import 'stilo-editor/content.css';
|
|
69
|
+
import { Editor, defaultExtensions } from 'stilo-editor';
|
|
70
|
+
|
|
71
|
+
const editor = new Editor({
|
|
72
|
+
element: document.querySelector<HTMLElement>('#editor')!,
|
|
73
|
+
extensions: [defaultExtensions()],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
document.querySelector('#bold')!.addEventListener('click', () => editor.commands.toggleBold());
|
|
77
|
+
document.querySelector('#undo')!.addEventListener('click', () => editor.commands.undo());
|
|
78
|
+
```
|
|
165
79
|
|
|
166
|
-
|
|
80
|
+
Commands, button states and events: [API reference](docs/API.md).
|
|
167
81
|
|
|
168
|
-
|
|
169
|
-
[issue](https://github.com/kojak8791/stilo-editor/issues/new/choose); the forms ask for what
|
|
170
|
-
is needed to reproduce a bug (steps, Stilo version, browser).
|
|
171
|
-
- **Questions:** open an issue too, with a short title that says what you are trying to do.
|
|
172
|
-
- **Security vulnerabilities:** never in a public issue; report them privately as described in
|
|
173
|
-
[SECURITY.md](SECURITY.md).
|
|
82
|
+
### React
|
|
174
83
|
|
|
175
|
-
|
|
84
|
+
```tsx
|
|
85
|
+
import 'stilo-editor/style.css';
|
|
86
|
+
import { StiloEditor } from 'stilo-editor/react';
|
|
176
87
|
|
|
177
|
-
|
|
88
|
+
<StiloEditor content="<p>Hello</p>" onInput={({ editor }) => save(editor.getHTML())} />;
|
|
89
|
+
```
|
|
178
90
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
| M0 — Foundations | Docs, scaffolding, core contracts, HTML pipeline, test helpers, `Bold` reference extension | Done |
|
|
182
|
-
| M1 — MVP | Core editing, history, basic marks and blocks, lists, paste cleanup, default toolbar, demo, benchmarks | Done |
|
|
183
|
-
| M2 — v1.0 | Preview releases `0.x` on npm, then tables, images, fonts and colors, productivity tools up to `1.0.0` | In progress |
|
|
184
|
-
| M3 — Later | Real-time collaboration (Yjs), framework wrappers, embeds | Planned |
|
|
91
|
+
React 18 and 19, server-side rendering included; `useStiloEditor` for your own UI:
|
|
92
|
+
[React guide](docs/frameworks/react.md).
|
|
185
93
|
|
|
186
|
-
|
|
94
|
+
### Vue
|
|
187
95
|
|
|
188
|
-
|
|
96
|
+
```vue
|
|
97
|
+
<script setup>
|
|
98
|
+
import 'stilo-editor/style.css';
|
|
99
|
+
import { StiloEditor } from 'stilo-editor/vue';
|
|
100
|
+
</script>
|
|
189
101
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
it is merged, and performance is measured against explicit budgets (`npm run bench`).
|
|
102
|
+
<template>
|
|
103
|
+
<StiloEditor content="<p>Hello</p>" @input="({ editor }) => save(editor.getHTML())" />
|
|
104
|
+
</template>
|
|
105
|
+
```
|
|
195
106
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
| [API.md](docs/API.md) | API reference for applications |
|
|
199
|
-
| [VISION.md](docs/VISION.md) | Why and what |
|
|
200
|
-
| [SPEC.md](docs/SPEC.md) | Features, non-functional requirements, roadmap |
|
|
201
|
-
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Stack, file layout, extension API, HTML contract |
|
|
202
|
-
| [UI.md](docs/UI.md) | Design of the default UI |
|
|
203
|
-
| [PERFORMANCE.md](docs/PERFORMANCE.md) | Performance in practice, how it is measured, limits |
|
|
204
|
-
| [BACKLOG.md](docs/BACKLOG.md) | Tasks done and planned |
|
|
205
|
-
| [AGENTS.md](AGENTS.md) | Rules for contributors and coding agents |
|
|
206
|
-
| [CONTRIBUTING.md](CONTRIBUTING.md) | How to report bugs and propose changes |
|
|
207
|
-
| [SECURITY.md](SECURITY.md) | How to report a vulnerability |
|
|
107
|
+
Vue 3.4 and later, server-side rendering included; `useStiloEditor` for your own UI:
|
|
108
|
+
[Vue guide](docs/frameworks/vue.md).
|
|
208
109
|
|
|
209
|
-
|
|
110
|
+
### Svelte
|
|
210
111
|
|
|
211
|
-
|
|
112
|
+
```svelte
|
|
113
|
+
<script>
|
|
114
|
+
import 'stilo-editor/style.css';
|
|
115
|
+
import { StiloEditor } from 'stilo-editor/svelte';
|
|
116
|
+
</script>
|
|
212
117
|
|
|
213
|
-
|
|
214
|
-
npm install
|
|
215
|
-
npm run check # typecheck + lint + unit tests
|
|
216
|
-
npm run dev # demo page
|
|
217
|
-
npm run site # public site (published on GitHub Pages from main)
|
|
218
|
-
npm run bench # performance benchmarks (needs: npx playwright install chromium)
|
|
219
|
-
npm run test:e2e # E2E tests in Chromium, Firefox and WebKit (needs: npx playwright install)
|
|
118
|
+
<StiloEditor content="<p>Hello</p>" oninput={({ editor }) => save(editor.getHTML())} />
|
|
220
119
|
```
|
|
221
120
|
|
|
222
|
-
|
|
121
|
+
A component for Svelte 5, actions for Svelte 4 and 5, server-side rendering included:
|
|
122
|
+
[Svelte guide](docs/frameworks/svelte.md).
|
|
123
|
+
|
|
124
|
+
## What's included
|
|
223
125
|
|
|
224
|
-
|
|
126
|
+
- **Text:** bold, italic, underline, strikethrough, code, sub/superscript, fonts, sizes,
|
|
127
|
+
colors, highlight, text case
|
|
128
|
+
- **Blocks:** headings, quotes, code blocks, alignment, line spacing, move up/down
|
|
129
|
+
- **Lists:** bulleted, numbered, checklists
|
|
130
|
+
- **Tables:** insert, merge and split, row and column handles, resizable columns
|
|
131
|
+
- **Images:** paste and resize
|
|
132
|
+
- **Tools:** links, find and replace, special characters and emoji, word count
|
|
133
|
+
|
|
134
|
+
Markdown shortcuts as you type, a keyboard shortcut for everything, a toolbar you can drive
|
|
135
|
+
from the keyboard alone. Every feature, shortcut and option: [All features](docs/FEATURES.md).
|
|
136
|
+
|
|
137
|
+
## Why Stilo
|
|
138
|
+
|
|
139
|
+
- **Clean, safe HTML.** HTML in, semantic HTML out, through a strict allowlist of tags,
|
|
140
|
+
attributes and styles.
|
|
141
|
+
- **Paste from Word and Google Docs.** Real lists, kept tables, no stray fonts and colors.
|
|
142
|
+
- **Big documents welcome.** Typing stays instant at 10,000 paragraphs
|
|
143
|
+
([numbers](docs/PERFORMANCE.md)).
|
|
144
|
+
- **Tested in Chromium, Firefox and WebKit**, with TypeScript types included.
|
|
145
|
+
- **Nothing behind a paywall.** Tables, paste from Word, find and replace: all in the free,
|
|
146
|
+
MIT-licensed package.
|
|
147
|
+
|
|
148
|
+
## Status
|
|
149
|
+
|
|
150
|
+
`0.x` preview: it works, but APIs may change in minor versions until `1.0.0`.
|
|
151
|
+
Read the [changelog](CHANGELOG.md) before updating.
|
|
152
|
+
Next: real-time collaboration ([roadmap](docs/SPEC.md)).
|
|
153
|
+
|
|
154
|
+
## Contributing
|
|
155
|
+
|
|
156
|
+
Bugs, ideas and questions: [issues](https://github.com/kojak8791/stilo-editor/issues/new/choose).
|
|
157
|
+
Code: [CONTRIBUTING.md](CONTRIBUTING.md). Vulnerabilities: [SECURITY.md](SECURITY.md).
|
|
158
|
+
|
|
159
|
+
Designed by Alessio Coggiola and built with [Claude](https://claude.com/claude-code), from a
|
|
160
|
+
written [architecture](docs/ARCHITECTURE.md) and [decision records](docs/decisions/).
|
|
161
|
+
|
|
162
|
+
## License
|
|
225
163
|
|
|
226
|
-
|
|
227
|
-
[
|
|
164
|
+
[MIT](LICENSE) © 2026 Alessio Coggiola. Logo wordmark from
|
|
165
|
+
[IBM Plex](https://github.com/IBM/plex) ([OFL 1.1](https://openfontlicense.org)).
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Q as e, t } from "./src-DxTXpULc.js";
|
|
2
|
+
import { h as n, t as r } from "./ui-WFNvrkbM.js";
|
|
3
|
+
//#region src/svelte/editor-action.ts
|
|
4
|
+
function i(t, n) {
|
|
5
|
+
let { onready: r, ...i } = n, o = new e({
|
|
6
|
+
...i,
|
|
7
|
+
element: t
|
|
8
|
+
});
|
|
9
|
+
return r?.(o), {
|
|
10
|
+
update: (e) => a(o, e.editable),
|
|
11
|
+
destroy: () => o.destroy()
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function a(e, t) {
|
|
15
|
+
let n = t ?? !0;
|
|
16
|
+
e.isEditable !== n && e.setEditable(n);
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/svelte/editor-with-ui-action.ts
|
|
20
|
+
var o = {
|
|
21
|
+
input: "oninput",
|
|
22
|
+
selectionchange: "onselectionchange",
|
|
23
|
+
transaction: "ontransaction",
|
|
24
|
+
focus: "onfocus",
|
|
25
|
+
blur: "onblur"
|
|
26
|
+
};
|
|
27
|
+
function s(i, s = {}) {
|
|
28
|
+
let u = s, { extensions: d = [t()], toolbar: f = !0, statusBar: p = !1, messages: m, onready: h, ...g } = s, _ = f ? c(i) : null, v = c(i), y = p ? c(i) : null, b = new e({
|
|
29
|
+
...g,
|
|
30
|
+
extensions: d,
|
|
31
|
+
element: v
|
|
32
|
+
});
|
|
33
|
+
_ && r(b, {
|
|
34
|
+
...f === !0 ? {} : f,
|
|
35
|
+
container: _,
|
|
36
|
+
messages: m
|
|
37
|
+
}), y && n(b, {
|
|
38
|
+
container: y,
|
|
39
|
+
messages: m
|
|
40
|
+
});
|
|
41
|
+
for (let e of Object.keys(o)) l(b, e, () => u);
|
|
42
|
+
return h?.(b), {
|
|
43
|
+
update: (e) => {
|
|
44
|
+
u = e, a(b, e.editable);
|
|
45
|
+
},
|
|
46
|
+
destroy: () => {
|
|
47
|
+
b.destroy();
|
|
48
|
+
for (let e of [
|
|
49
|
+
_,
|
|
50
|
+
v,
|
|
51
|
+
y
|
|
52
|
+
]) e?.remove();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function c(e) {
|
|
57
|
+
return e.appendChild(document.createElement("div"));
|
|
58
|
+
}
|
|
59
|
+
function l(e, t, n) {
|
|
60
|
+
e.on(t, (e) => {
|
|
61
|
+
let r = n()[o[t]];
|
|
62
|
+
r?.(e);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { i as n, s as t };
|
package/dist/content.css
CHANGED
|
@@ -152,6 +152,15 @@ li.ProseMirror-selectednode:after {
|
|
|
152
152
|
|
|
153
153
|
/* Stilo content defaults */
|
|
154
154
|
|
|
155
|
+
/*
|
|
156
|
+
* The editable area gets some room around the text and a visible height when empty.
|
|
157
|
+
* `:where()` has no specificity, so any rule of the page replaces these values.
|
|
158
|
+
*/
|
|
159
|
+
:where(.stilo-content.ProseMirror) {
|
|
160
|
+
min-height: 6em;
|
|
161
|
+
padding: 12px 16px;
|
|
162
|
+
}
|
|
163
|
+
|
|
155
164
|
/* Lists: list items hold paragraphs, which must not add their own margins. */
|
|
156
165
|
.stilo-content ul,
|
|
157
166
|
.stilo-content ol {
|
package/dist/core/editor.d.ts
CHANGED
|
@@ -94,7 +94,10 @@ export declare class Editor {
|
|
|
94
94
|
* the name is unknown.
|
|
95
95
|
*/
|
|
96
96
|
getAttributes(name: string): Record<string, unknown>;
|
|
97
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* Switches read-only mode on/off. A change dispatches an empty transaction, so the UIs that
|
|
99
|
+
* refresh on `transaction` follow it.
|
|
100
|
+
*/
|
|
98
101
|
setEditable(editable: boolean): void;
|
|
99
102
|
/** Focuses the editor; optionally moves the cursor to the start or end of the document. */
|
|
100
103
|
focus(position?: 'start' | 'end'): void;
|
|
@@ -107,8 +110,15 @@ export declare class Editor {
|
|
|
107
110
|
/** Emits `destroy`, destroys the view, removes all handlers. Calling it twice is a no-op. */
|
|
108
111
|
destroy(): void;
|
|
109
112
|
private createState;
|
|
110
|
-
/** Builds `commands` from the extension commands; `can`
|
|
113
|
+
/** Builds `commands` from the extension commands; `can` does not apply them. */
|
|
111
114
|
private createRunners;
|
|
115
|
+
/**
|
|
116
|
+
* Runs `command`, or only tells whether it would apply. In read-only mode a command that
|
|
117
|
+
* changes the document or the stored marks does not apply and returns `false`; the others
|
|
118
|
+
* (selection, search) do. To know, the command builds its transaction: `can` costs as much
|
|
119
|
+
* as a run there.
|
|
120
|
+
*/
|
|
121
|
+
private runCommand;
|
|
112
122
|
private setContent;
|
|
113
123
|
/**
|
|
114
124
|
* Removes the DOM selection when it lies inside the editor. Replacing many nodes while the
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { B as Blockquote, z as Bold, R as BulletList, L as ClearFormatting, I as Code, F as CodeBlock, X as DEFAULT_PRIORITY, P as Document, N as DropCursor, Q as Editor, M as GapCursor, j as HardBreak, A as Heading, k as Highlight, O as History, D as HorizontalRule, E as Image, T as Italic, w as LineHeight, C as Link, S as ListItem, x as MoveBlock, b as OrderedList, y as Paragraph, v as Placeholder, g as Search, nt as StiloError, h as Strike, m as Subscript, p as Superscript, d as Table, u as TaskItem, l as TaskList, c as Text, s as TextAlign, o as TextCase, r as TextStyle, n as Underline, $ as attrsMatch, t as defaultExtensions, Z as defineExtension, i as getFontSize, _ as getSearchResults, f as getTableHeaders, V as getWordCount, U as insertBlock, W as insertBlockNode, et as isMarkActive, tt as isNodeActive, Y as markInputRule, G as setMark, H as textDecorationRules, a as toFontSize, J as toggleList, K as toggleMark, q as unsetMark } from "./src-
|
|
1
|
+
export { B as Blockquote, z as Bold, R as BulletList, L as ClearFormatting, I as Code, F as CodeBlock, X as DEFAULT_PRIORITY, P as Document, N as DropCursor, Q as Editor, M as GapCursor, j as HardBreak, A as Heading, k as Highlight, O as History, D as HorizontalRule, E as Image, T as Italic, w as LineHeight, C as Link, S as ListItem, x as MoveBlock, b as OrderedList, y as Paragraph, v as Placeholder, g as Search, nt as StiloError, h as Strike, m as Subscript, p as Superscript, d as Table, u as TaskItem, l as TaskList, c as Text, s as TextAlign, o as TextCase, r as TextStyle, n as Underline, $ as attrsMatch, t as defaultExtensions, Z as defineExtension, i as getFontSize, _ as getSearchResults, f as getTableHeaders, V as getWordCount, U as insertBlock, W as insertBlockNode, et as isMarkActive, tt as isNodeActive, Y as markInputRule, G as setMark, H as textDecorationRules, a as toFontSize, J as toggleList, K as toggleMark, q as unsetMark } from "./src-DxTXpULc.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Public API of the Stilo React wrapper. */
|
|
2
|
+
/** The editor with the default UI. */
|
|
3
|
+
export { StiloEditor, type StiloEditorEventProps, type StiloEditorProps } from './stilo-editor';
|
|
4
|
+
/** The editor alone, for a custom UI. */
|
|
5
|
+
export { type UseStiloEditorOptions, type UseStiloEditorResult, useStiloEditor, } from './use-stilo-editor';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import { type Editor, type EditorEvents, type EditorOptions, type EventHandler } from '../index';
|
|
3
|
+
import { type Messages, type ToolbarOptions } from '../ui/index';
|
|
4
|
+
/** Editor events forwarded to the props of {@link StiloEditor}. */
|
|
5
|
+
declare const EVENT_PROPS: {
|
|
6
|
+
readonly input: 'onInput';
|
|
7
|
+
readonly selectionchange: 'onSelectionChange';
|
|
8
|
+
readonly transaction: 'onTransaction';
|
|
9
|
+
readonly focus: 'onFocus';
|
|
10
|
+
readonly blur: 'onBlur';
|
|
11
|
+
};
|
|
12
|
+
type ForwardedEvent = keyof typeof EVENT_PROPS;
|
|
13
|
+
/** Event props of {@link StiloEditor}: `onInput` for `input`, and so on. */
|
|
14
|
+
export type StiloEditorEventProps = {
|
|
15
|
+
[E in ForwardedEvent as (typeof EVENT_PROPS)[E]]?: EventHandler<EditorEvents[E]>;
|
|
16
|
+
};
|
|
17
|
+
/** Props of {@link StiloEditor}. */
|
|
18
|
+
export interface StiloEditorProps extends Omit<EditorOptions, 'element' | 'extensions'>, StiloEditorEventProps {
|
|
19
|
+
/** Extensions to load. Default: `[defaultExtensions()]`. */
|
|
20
|
+
extensions?: EditorOptions['extensions'];
|
|
21
|
+
/** The default toolbar: `true` (default), `false`, or its options without `container`. */
|
|
22
|
+
toolbar?: boolean | Omit<ToolbarOptions, 'container' | 'messages'>;
|
|
23
|
+
/** The status bar with the word and character count, under the editor. Default: `false`. */
|
|
24
|
+
statusBar?: boolean;
|
|
25
|
+
/** Strings of the toolbar and the status bar. Default: `en`. */
|
|
26
|
+
messages?: Messages;
|
|
27
|
+
/** Class of the element that holds the toolbar, the editor and the status bar. */
|
|
28
|
+
className?: string;
|
|
29
|
+
/** Called once the editor is created, with the editor. */
|
|
30
|
+
onReady?: (editor: Editor) => void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The editor with the default UI. The props are read once, when the editor is created, except
|
|
34
|
+
* `editable` and the event handlers, which follow their changes.
|
|
35
|
+
*/
|
|
36
|
+
export declare function StiloEditor(props: StiloEditorProps): ReactElement;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Editor, type EditorOptions } from '../index';
|
|
2
|
+
/** Options of {@link useStiloEditor}: the editor options without `element`. */
|
|
3
|
+
export type UseStiloEditorOptions = Omit<EditorOptions, 'element'>;
|
|
4
|
+
/** What {@link useStiloEditor} returns. */
|
|
5
|
+
export interface UseStiloEditorResult {
|
|
6
|
+
/** Ref of the element that holds the editor. */
|
|
7
|
+
ref: (element: HTMLElement | null) => void;
|
|
8
|
+
/** The editor; `null` before the element is mounted, on the server and after unmount. */
|
|
9
|
+
editor: Editor | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Creates an editor inside the element that gets `ref` and destroys it when the element goes.
|
|
13
|
+
* The options are read once, when the editor is created; only `editable` follows its changes.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useStiloEditor(options: UseStiloEditorOptions): UseStiloEditorResult;
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Q as e, t } from "./src-DxTXpULc.js";
|
|
3
|
+
import { h as n, t as r } from "./ui-WFNvrkbM.js";
|
|
4
|
+
import { createElement as i, useEffect as a, useRef as o, useState as s } from "react";
|
|
5
|
+
//#region src/react/use-stilo-editor.ts
|
|
6
|
+
function c(t) {
|
|
7
|
+
let [n, r] = s(null), [i, c] = s(null), l = o(t);
|
|
8
|
+
a(() => {
|
|
9
|
+
if (!n) return;
|
|
10
|
+
let t = new e({
|
|
11
|
+
...l.current,
|
|
12
|
+
element: n
|
|
13
|
+
});
|
|
14
|
+
return c(t), () => {
|
|
15
|
+
t.destroy(), c(null);
|
|
16
|
+
};
|
|
17
|
+
}, [n]);
|
|
18
|
+
let u = t.editable ?? !0;
|
|
19
|
+
return a(() => {
|
|
20
|
+
i && !i.isDestroyed && i.isEditable !== u && i.setEditable(u);
|
|
21
|
+
}, [i, u]), {
|
|
22
|
+
ref: r,
|
|
23
|
+
editor: i
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/react/stilo-editor.ts
|
|
28
|
+
var l = {
|
|
29
|
+
input: "onInput",
|
|
30
|
+
selectionchange: "onSelectionChange",
|
|
31
|
+
transaction: "onTransaction",
|
|
32
|
+
focus: "onFocus",
|
|
33
|
+
blur: "onBlur"
|
|
34
|
+
};
|
|
35
|
+
function u(e) {
|
|
36
|
+
let [u] = s(() => ({
|
|
37
|
+
extensions: e.extensions ?? [t()],
|
|
38
|
+
toolbar: e.toolbar ?? !0,
|
|
39
|
+
statusBar: e.statusBar ?? !1,
|
|
40
|
+
messages: e.messages
|
|
41
|
+
})), { ref: f, editor: p } = c({
|
|
42
|
+
...e,
|
|
43
|
+
extensions: u.extensions
|
|
44
|
+
}), m = o(null), h = o(null), g = o(e);
|
|
45
|
+
return a(() => {
|
|
46
|
+
g.current = e;
|
|
47
|
+
}), a(() => {
|
|
48
|
+
if (!p || p.isDestroyed) return;
|
|
49
|
+
let { toolbar: e, statusBar: t, messages: i } = u, a, o;
|
|
50
|
+
e && m.current && (a = r(p, {
|
|
51
|
+
...e === !0 ? {} : e,
|
|
52
|
+
container: m.current,
|
|
53
|
+
messages: i
|
|
54
|
+
})), t && h.current && (o = n(p, {
|
|
55
|
+
container: h.current,
|
|
56
|
+
messages: i
|
|
57
|
+
}));
|
|
58
|
+
let s = Object.keys(l).map((e) => d(p, e, g));
|
|
59
|
+
return g.current.onReady?.(p), () => {
|
|
60
|
+
if (!p.isDestroyed) {
|
|
61
|
+
a?.destroy(), o?.destroy();
|
|
62
|
+
for (let e of s) e();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}, [p, u]), i("div", { className: e.className }, u.toolbar ? i("div", { ref: m }) : null, i("div", { ref: f }), u.statusBar ? i("div", { ref: h }) : null);
|
|
66
|
+
}
|
|
67
|
+
function d(e, t, n) {
|
|
68
|
+
let r = (e) => {
|
|
69
|
+
let r = n.current[l[t]];
|
|
70
|
+
r?.(e);
|
|
71
|
+
};
|
|
72
|
+
return e.on(t, r), () => e.off(t, r);
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { u as StiloEditor, c as useStiloEditor };
|