@thomasfosterau/prosekit-svelte 0.0.0-stage → 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 +255 -2
- package/dist/editor/BlockEditor.svelte +66 -0
- package/dist/editor/BlockEditor.svelte.d.ts +42 -0
- package/dist/editor/Editor.svelte +60 -0
- package/dist/editor/Editor.svelte.d.ts +57 -0
- package/dist/editor/InlineEditor.svelte +55 -0
- package/dist/editor/InlineEditor.svelte.d.ts +36 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/messages.svelte.d.ts +162 -0
- package/dist/messages.svelte.js +114 -0
- package/dist/toolbar/BlockToolbar.svelte +162 -0
- package/dist/toolbar/BlockToolbar.svelte.d.ts +52 -0
- package/dist/toolbar/ImageDialog.svelte +86 -0
- package/dist/toolbar/ImageDialog.svelte.d.ts +31 -0
- package/dist/toolbar/LinkDialog.svelte +113 -0
- package/dist/toolbar/LinkDialog.svelte.d.ts +40 -0
- package/dist/toolbar/SlashMenu.svelte +112 -0
- package/dist/toolbar/SlashMenu.svelte.d.ts +37 -0
- package/dist/toolbar/items.d.ts +125 -0
- package/dist/toolbar/items.js +262 -0
- package/package.json +65 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Foster
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,256 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@thomasfosterau/prosekit-svelte`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Svelte 5 rich-text editing on [ProseKit](https://prosekit.dev): `Editor`, `BlockEditor`, `InlineEditor`, a declarative `BlockToolbar` with an insert menu, and the link and image dialogs — all built on [bits-ui](https://bits-ui.com), all styled with daisyUI's semantic classes, every user-facing string a prop with an English default.
|
|
4
|
+
|
|
5
|
+
Requires Tailwind 4 + daisyUI in the consuming app (see [Peer requirements](#peer-requirements)). Accessible-name props end in `Label`. Every component spreads the native attributes you pass (`id`, `data-*`, `aria-*`, …) on the element it renders — `SlashMenu` on its `role="menu"` element, `LinkDialog` and `ImageDialog` on their `role="dialog"` element — with the dialogs' `role`, `aria-modal` and title wiring winning. `Editor`, `BlockEditor`, `InlineEditor`, `BlockToolbar` and `SlashMenu` own their accessible name through `label` / `menuLabel`, or `labelledBy` to name it by a visible heading: give `labelledBy` the heading's `id` (or space-separated `id`s) and the component renders `aria-labelledby`, which wins over the label. `label` / `menuLabel` (or its default) stays on as `aria-label`, the fallback name if no element has that `id`; an empty `labelledBy` counts as unset. `aria-label` and `aria-labelledby` are not among their props, and passed anyway (from untyped code) the component's own attributes override the first and drop the second. Every component merges the caller's `class` last.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @thomasfosterau/prosekit-svelte prosekit @prosekit/pm
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Peer requirements
|
|
14
|
+
|
|
15
|
+
| Package | Range | Why |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `svelte` | `^5.30.1` | A declared peer. The components are compiled by your build, not pre-compiled. |
|
|
18
|
+
| `prosekit` | `^0.22.2` | A declared peer. The editor runtime, and the two stylesheets `Editor.svelte` imports. |
|
|
19
|
+
| `@prosekit/pm` | `^0.1.19` | A declared peer. ProseMirror's model and view, at the version ProseKit expects. |
|
|
20
|
+
| `tailwindcss` | `^4.0.0` | Not a declared peer, but **required**: the components emit Tailwind classes and ship none. |
|
|
21
|
+
| `daisyui` | `^5.0.0` | Same — the class vocabulary is daisyUI's (`btn`, `menu`, `modal-box`, `input`, `bg-base-100`). |
|
|
22
|
+
|
|
23
|
+
Tailwind 4 has no `tailwind.config.js`, so wire it up from your CSS entry. The `@source` line is what makes Tailwind scan this package's `dist/` and emit the classes it asks for; without it the editor renders unstyled.
|
|
24
|
+
|
|
25
|
+
```css
|
|
26
|
+
@import 'tailwindcss';
|
|
27
|
+
@source '../node_modules/@thomasfosterau/prosekit-svelte/dist';
|
|
28
|
+
@plugin "daisyui" {
|
|
29
|
+
themes:
|
|
30
|
+
light --default,
|
|
31
|
+
dark --prefersdark;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
ProseKit's own stylesheets arrive through `Editor.svelte`'s bare imports — you do not import them yourself.
|
|
36
|
+
|
|
37
|
+
`@thomasfosterau/prosekit-utilities` and `bits-ui` are ordinary dependencies of this package, at the same release line, and come along automatically when you install it — you do not add either yourself.
|
|
38
|
+
|
|
39
|
+
## App integration
|
|
40
|
+
|
|
41
|
+
**Import by path, never re-export from a shared component barrel.** `Editor.svelte` imports `prosekit/basic/style.css` and `prosekit/basic/typography.css` at the top level, and through `createEditor` it pulls the ProseKit runtime behind them — the largest client chunk in the applications this package was extracted from. Whatever module imports this package carries all of that in its chunk, so a route that genuinely edits a document imports a component directly:
|
|
42
|
+
|
|
43
|
+
```svelte
|
|
44
|
+
<!-- A route that genuinely edits a document -->
|
|
45
|
+
<script lang="ts">
|
|
46
|
+
import { BlockEditor } from '@thomasfosterau/prosekit-svelte';
|
|
47
|
+
</script>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
and a route that only sometimes edits one reaches it behind a dynamic `import()`:
|
|
51
|
+
|
|
52
|
+
```svelte
|
|
53
|
+
{#await import('@thomasfosterau/prosekit-svelte') then { BlockEditor }}
|
|
54
|
+
<BlockEditor name="body" {defaultContent} />
|
|
55
|
+
{/await}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Never re-export a component from an **application-wide** component barrel (`$lib/components/index.ts` or similar) and never wire the messages context from a root layout — both put the editor's CSS and runtime into every page's shared chunk. `"sideEffects": false` lets a bundler tree-shake the rest of this package's exports away from a route that only imports one component, but only if nothing else in the bundle reaches this package through a barrel that is itself always loaded.
|
|
59
|
+
|
|
60
|
+
### The messages context
|
|
61
|
+
|
|
62
|
+
Every string is already a prop, so a single call site needs nothing here. `setEditorMessages(partial)` is for an application that mounts the editor from several places and would rather not thread twenty props through each: it changes the _defaults_ for one subtree, merged over the English ones, and a caller's own prop still wins.
|
|
63
|
+
|
|
64
|
+
Call it **per render site** — from the component that mounts the editor, for the reason above — never from a root layout.
|
|
65
|
+
|
|
66
|
+
```svelte
|
|
67
|
+
<!-- PostBodyField.svelte -->
|
|
68
|
+
<script lang="ts">
|
|
69
|
+
import { BlockEditor, setEditorMessages } from '@thomasfosterau/prosekit-svelte';
|
|
70
|
+
import * as m from '$lib/paraglide/messages';
|
|
71
|
+
|
|
72
|
+
setEditorMessages({
|
|
73
|
+
toolbarLabel: m.editor_toolbar(),
|
|
74
|
+
bold: m.editor_bold(),
|
|
75
|
+
slashParagraph: m.editor_slash_paragraph(),
|
|
76
|
+
});
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<BlockEditor name="body" label={m.post_body()} />
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
With no provider mounted — Storybook, a test, an application that never wires it — every string is its English default. That is the documented normal case, not an error.
|
|
83
|
+
|
|
84
|
+
### Forms
|
|
85
|
+
|
|
86
|
+
Given a `name`, `BlockEditor` and `InlineEditor` emit a hidden input carrying the current document as JSON, in the **application's** envelope shape (`{"type":"blockDocument",…}`), so the field posts with the form and decodes with the shape your document schema declares. `documentType` names the envelope; the components remap to and from ProseMirror's `doc` shape at the boundary in both directions.
|
|
87
|
+
|
|
88
|
+
A worked example: a form field that composes an app extension and an app toolbar item, and the server action that reads the field back.
|
|
89
|
+
|
|
90
|
+
```svelte
|
|
91
|
+
<!-- BlockEditorField.svelte -->
|
|
92
|
+
<script lang="ts">
|
|
93
|
+
import { BlockEditor, createDefaultToolbarItems, type ToolbarItem } from '@thomasfosterau/prosekit-svelte';
|
|
94
|
+
import type { NodeJSON } from '@thomasfosterau/prosekit-utilities';
|
|
95
|
+
import { blockExtension } from '$lib/server/document-schema.ts';
|
|
96
|
+
|
|
97
|
+
interface Props {
|
|
98
|
+
defaultContent?: NodeJSON | undefined;
|
|
99
|
+
}
|
|
100
|
+
let { defaultContent }: Props = $props();
|
|
101
|
+
|
|
102
|
+
const myItem: ToolbarItem = {
|
|
103
|
+
id: 'sidenote',
|
|
104
|
+
label: 'Sidenote',
|
|
105
|
+
kind: 'toggle',
|
|
106
|
+
isActive: (editor) => editor.nodes['sidenote']?.isActive() ?? false,
|
|
107
|
+
run: ({ editor }) => void editor.commands['insertNode']?.({ type: 'sidenote' }),
|
|
108
|
+
};
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<form method="post" action="?/save">
|
|
112
|
+
<BlockEditor
|
|
113
|
+
name="content"
|
|
114
|
+
extension={blockExtension}
|
|
115
|
+
{defaultContent}
|
|
116
|
+
items={[...createDefaultToolbarItems(), myItem]}
|
|
117
|
+
/>
|
|
118
|
+
<button type="submit">Save</button>
|
|
119
|
+
</form>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// +page.server.ts — reading the hidden input's JSON back with the core's DocumentSchema.decode
|
|
124
|
+
import * as Effect from 'effect/Effect';
|
|
125
|
+
import { blockDocumentSchema } from '$lib/server/document-schema.ts';
|
|
126
|
+
|
|
127
|
+
export const actions = {
|
|
128
|
+
save: async ({ request }) => {
|
|
129
|
+
const form = await request.formData();
|
|
130
|
+
const raw: unknown = JSON.parse(String(form.get('content')));
|
|
131
|
+
|
|
132
|
+
// decode: (v: unknown) => Effect.Effect<DocumentJSON<'blockDocument'>, DocumentDecodeError>
|
|
133
|
+
const body = await Effect.runPromise(
|
|
134
|
+
blockDocumentSchema
|
|
135
|
+
.decode(raw)
|
|
136
|
+
.pipe(Effect.catchTag('DocumentDecodeError', (error) => Effect.die(error))),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// … persist `body`.
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`blockDocumentSchema` here is the `DocumentSchema` a server module builds with `@thomasfosterau/prosekit-utilities/prosekit`'s `defineDocumentSchema` — see that package's README for `decode`'s full signature and its `DocumentDecodeError` channel. Feeding the stored envelope straight into ProseKit instead of through this boundary throws `RangeError: Unknown node type`.
|
|
145
|
+
|
|
146
|
+
# Editors
|
|
147
|
+
|
|
148
|
+
| Component | Purpose | Props | Snippets | ARIA |
|
|
149
|
+
| --- | --- | --- | --- | --- |
|
|
150
|
+
| `Editor<E extends Extension>` | The bare ProseKit mount; everything else in this package is built on it | `extension: E`, `content?: NodeJSON` (bindable), `defaultContent?: NodeJSON`, `label?`, `labelledBy?` (a visible element's `id`; wins over `label`, which stays the fallback), `class?` + `HTMLAttributes<HTMLDivElement>` without `aria-label` / `aria-labelledby` (`label` / `labelledBy` own the name) | `children?` — rendered inside `<ProseKit>`, **before** the mount node, which is where a toolbar goes | wrapper `role="group"` only when named: `aria-labelledby` given `labelledBy`, `aria-label` given `label` (the name when `labelledBy` resolves to no element); the content is ProseMirror's `.ProseMirror[contenteditable]` node, whose keyboard model is the extension's |
|
|
151
|
+
| `BlockEditor` | `Editor` on the block-document grammar, with the default toolbar, the envelope remap and a hidden form input | `extension?` (default `defineBlockDocumentExtension()`, built lazily), `documentType? = 'blockDocument'`, `content?` (bindable), `defaultContent?` (either shape), `name?`, `items?: ReadonlyArray<ToolbarItem \| ToolbarSeparator>`, `slashCommands?: ReadonlyArray<SlashCommand>`, `toolbar? = true` (`false` renders none, a snippet replaces the default), `label?`, `labelledBy?` (both forwarded to `Editor`), `class?` + `HTMLAttributes<HTMLDivElement>` without `aria-label` / `aria-labelledby` (forwarded to `Editor`'s wrapper; `label` / `labelledBy` own the name) | `toolbar?` — replaces the default `BlockToolbar`; `children?` — rendered after it | as `Editor`. Feeding a stored (`blockDocument`-typed) document straight into ProseKit throws `RangeError: Unknown node type`; this component is the boundary that stops that |
|
|
152
|
+
| `InlineEditor` | One line of rich text: marks but no blocks | `extension?` (default `defineInlineDocumentExtension()` plus the Enter binding), `documentType? = 'inlineDocument'`, `content?` (bindable), `defaultContent?` (either shape), `name?`, `label?`, `labelledBy?` (both forwarded to `Editor`), `class?` + `HTMLAttributes<HTMLDivElement>` without `aria-label` / `aria-labelledby` (forwarded to `Editor`'s wrapper; `label` / `labelledBy` own the name) | `children?` — rendered inside the provider, before the mount node | as `Editor`; **Enter is a no-op** — an inline document's top node is `inline*`, so there is no second block for it to make. The binding lives in the default extension; supply your own `extension` and you own the single-line semantics too |
|
|
153
|
+
|
|
154
|
+
```svelte
|
|
155
|
+
<script lang="ts">
|
|
156
|
+
import { BlockEditor } from '@thomasfosterau/prosekit-svelte';
|
|
157
|
+
let content = $state();
|
|
158
|
+
</script>
|
|
159
|
+
|
|
160
|
+
<BlockEditor label="Post body" bind:content />
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
# Toolbar
|
|
164
|
+
|
|
165
|
+
| Component | Purpose | Props | Snippets | ARIA |
|
|
166
|
+
| --- | --- | --- | --- | --- |
|
|
167
|
+
| `BlockToolbar` | The default toolbar, rendered from a list of items | `items? = createDefaultToolbarItems(messages)`, `slashCommands? = createDefaultSlashCommands(messages)`, `label? = 'Editor toolbar'`, `labelledBy?` (a visible element's `id`; wins over `label`, which stays the fallback), `insertLabel? = 'Insert'`, `insertMenuLabel? = 'Insert block'`, `linkDialog?: Partial<LinkDialogLabels>` (forwarded to `LinkDialog`), `imageDialog?: Partial<ImageDialogLabels>` (forwarded to `ImageDialog`), `class?` + `HTMLAttributes<HTMLDivElement>` without `aria-label` / `aria-labelledby` (on the `role="toolbar"` element; `label` / `labelledBy` own the name) | — (must be rendered inside an `Editor`; it reads the editor from ProseKit's context) | bits-ui `Toolbar.Root`: `role="toolbar"` named by `labelledBy` or else `label`, one tab stop, arrow keys move and wrap, Home/End to the ends, disabled items skipped; `kind: 'toggle'` items carry `aria-pressed` from `isActive` on every editor update; separators are `role="separator"` + `aria-orientation="vertical"` and not a keyboard stop; after a run, focus returns to the document unless the run opened a dialog |
|
|
168
|
+
| `SlashMenu` | The insert menu, exported for a toolbar of your own | `commands: ReadonlyArray<SlashCommand>`, `open?` (bindable, default `false`), `label? = 'Insert'`, `menuLabel? = 'Insert block'`, `labelledBy?` (a visible element's `id`, naming the menu; wins over `menuLabel`, which stays the fallback), `dialogs?: ToolbarRunContext['dialogs']` (omitted, `dialogs.link()` / `dialogs.image()` do nothing), `class?` (on the daisyUI `menu` list inside the content) + `HTMLAttributes<HTMLDivElement>` without `dir`, `aria-label` or `aria-labelledby` (on the `role="menu"` content element, while open; `menuLabel` / `labelledBy` own the name, and an `id` becomes the trigger's `aria-controls`) | `trigger?({ props })` — your own opening control, spreading `props`; omitted, a daisyUI `btn` labelled `label` | bits-ui `DropdownMenu`: `aria-haspopup="menu"` + `aria-expanded` on the trigger, `role="menu"` on the content (named by `labelledBy` or else `menuLabel`), `role="menuitem"` per entry, arrow keys, typeahead, Escape closes and returns focus to the trigger — or to the document after a choice, unless the command opened a dialog |
|
|
169
|
+
| `LinkDialog` | The link prompt, as a modal | `open?` (bindable), `href? = ''` (re-read on every open), `title? = ''`, `onSubmit: (attrs: { href: string; title: string \| null }) => void` (required), `onRemove?` (omitted, no remove button), `titleLabel? = 'Link'`, `hrefLabel? = 'URL'`, `linkTitleLabel? = 'Title (optional)'`, `submitLabel? = 'Apply'`, `removeLabel? = 'Remove link'`, `cancelLabel? = 'Cancel'`, `closeLabel? = 'Close'`, `invalidHrefMessage? = 'Enter an http, https or mailto URL.'`, `class?` + `HTMLAttributes<HTMLDivElement>` without `title` (on the `role="dialog"` element, while open; `role`, `aria-modal` and `aria-labelledby` are bits-ui's, and an `aria-describedby` of yours is kept) | — | bits-ui `Dialog`: `role="dialog"`, `aria-modal="true"`, `aria-labelledby` wired to the title, focus trap, Escape, scroll lock, focus return; a URL that fails `isSafeHref` is refused in a `role="alert"` with `aria-invalid` + `aria-describedby` on the field; the fields are in a `<form>`, so Enter submits |
|
|
170
|
+
| `ImageDialog` | The image prompt, as a modal | `open?` (bindable), `src? = ''`, `onSubmit: (attrs: { src: string }) => void` (required), `titleLabel? = 'Image'`, `srcLabel? = 'Image URL'`, `submitLabel? = 'Insert'`, `cancelLabel? = 'Cancel'`, `closeLabel? = 'Close'`, `invalidSrcMessage? = 'Enter an http, https or mailto URL.'`, `class?` + `HTMLAttributes<HTMLDivElement>` (as `LinkDialog`'s, `title` included) | — | as `LinkDialog` |
|
|
171
|
+
|
|
172
|
+
`BlockToolbar` renders `LinkDialog` and `ImageDialog` itself and opens them through `ToolbarRunContext.dialogs`; the link dialog is seeded from the selection's own link mark, so its button edits an existing link rather than replacing it, and its remove button runs `removeLink`.
|
|
173
|
+
|
|
174
|
+
## The toolbar's data model
|
|
175
|
+
|
|
176
|
+
`BlockToolbar` renders a list rather than a hard-coded row of buttons — an application appends its own item, reorders, drops one, or passes a list of its own making.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
/** The editor as an item sees it: any grammar, `commands` / `nodes` / `marks` read by name. */
|
|
180
|
+
type ToolbarEditor = Omit<Editor<any>, 'commands' | 'marks' | 'nodes'> & {
|
|
181
|
+
readonly commands: Record<string, CommandAction>;
|
|
182
|
+
readonly marks: Record<string, MarkAction>;
|
|
183
|
+
readonly nodes: Record<string, NodeAction>;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
interface ToolbarRunContext {
|
|
187
|
+
readonly editor: ToolbarEditor;
|
|
188
|
+
readonly dialogs: { readonly link: () => void; readonly image: () => void };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
interface ToolbarItem {
|
|
192
|
+
readonly id: string;
|
|
193
|
+
readonly label: string;
|
|
194
|
+
readonly kind?: ('toggle' | 'action') | undefined; // 'toggle' => aria-pressed from isActive
|
|
195
|
+
readonly isActive?: ((editor: ToolbarEditor) => boolean) | undefined;
|
|
196
|
+
readonly isEnabled?: ((editor: ToolbarEditor) => boolean) | undefined; // false => disabled button
|
|
197
|
+
readonly run: (context: ToolbarRunContext) => void;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
interface ToolbarSeparator {
|
|
201
|
+
readonly id: string;
|
|
202
|
+
readonly separator: true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface SlashCommand {
|
|
206
|
+
readonly id: string;
|
|
207
|
+
readonly label: string;
|
|
208
|
+
readonly run: (context: ToolbarRunContext) => void;
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`ToolbarEditor` is not `Editor<any>` because it cannot be: ProseKit derives `commands`, `nodes` and `marks` from the extension's own typing, and the derivation collapses to `never` for `any`, so `editor.commands.toggleBold()` in an item of your own would not compile. A real `Editor<E>` is assignable to `ToolbarEditor`, and naming a command the grammar lacks gets `undefined` rather than a type error.
|
|
213
|
+
|
|
214
|
+
| Export | Signature | Notes |
|
|
215
|
+
| --- | --- | --- |
|
|
216
|
+
| `isToolbarSeparator` | `(entry: ToolbarItem \| ToolbarSeparator) => entry is ToolbarSeparator` | Narrows an `items` entry to the separator arm. |
|
|
217
|
+
| `createDefaultToolbarItems` | `(messages?: Partial<EditorMessages>) => ReadonlyArray<ToolbarItem \| ToolbarSeparator>` | Nineteen items and two separators, in order: **bold, italic, underline, strike, highlight, subscript, superscript, link** \| **paragraph, heading 2, quote, bullets, numbered, code block, rule, table (3×3), image** \| **undo, redo**. |
|
|
218
|
+
| `createDefaultSlashCommands` | `(messages?: Partial<EditorMessages>) => ReadonlyArray<SlashCommand>` | Ten entries, in order: **paragraph, heading 2, blockquote, bullet list, ordered list, todo list, code block, table, image, horizontal rule**. |
|
|
219
|
+
|
|
220
|
+
Every default `run` goes through the editor's own `commands`, `marks` and `nodes`, so the set works against any extension registering those node and mark types and quietly does nothing against one that does not — the default item set covers exactly `defineBlockDocumentExtension()`'s grammar; an application composing extra nodes adds their items itself. Extending is appending:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { createDefaultToolbarItems, type ToolbarItem } from '@thomasfosterau/prosekit-svelte';
|
|
224
|
+
|
|
225
|
+
const sidenote: ToolbarItem = {
|
|
226
|
+
id: 'sidenote',
|
|
227
|
+
label: 'Sidenote',
|
|
228
|
+
kind: 'toggle',
|
|
229
|
+
isActive: (editor) => editor.nodes['sidenote']?.isActive() ?? false,
|
|
230
|
+
run: ({ editor }) => void editor.commands['insertNode']?.({ type: 'sidenote' }),
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const items = [...createDefaultToolbarItems(), { id: 'app', separator: true }, sidenote];
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
# Messages
|
|
237
|
+
|
|
238
|
+
`EditorMessages` is one flat interface of plain strings, grouped here by what they name:
|
|
239
|
+
|
|
240
|
+
- **Component labels** — `toolbarLabel`, `insertLabel`, `insertMenuLabel`; `linkDialogTitle`, `linkHrefLabel`, `linkTitleLabel`, `linkSubmitLabel`, `linkRemoveLabel`, `linkCancelLabel`, `linkCloseLabel`, `linkInvalidHrefMessage`; `imageDialogTitle`, `imageSrcLabel`, `imageSubmitLabel`, `imageCancelLabel`, `imageCloseLabel`, `imageInvalidSrcMessage`.
|
|
241
|
+
- **Default toolbar item labels** — `bold`, `italic`, `underline`, `strike`, `highlight`, `subscript`, `superscript`, `link`, `paragraph`, `heading2`, `quote`, `bullets`, `numbered`, `codeBlock`, `rule`, `table`, `image`, `undo`, `redo`.
|
|
242
|
+
- **Default insert-menu labels** — `slashParagraph`, `slashHeading2`, `slashBlockquote`, `slashBulletList`, `slashOrderedList`, `slashTodoList`, `slashCodeBlock`, `slashTable`, `slashImage`, `slashHorizontalRule`.
|
|
243
|
+
|
|
244
|
+
| Export | Signature | Notes |
|
|
245
|
+
| --- | --- | --- |
|
|
246
|
+
| `EditorMessages` | One flat interface of plain `string`s | Every component label prop, plus one key per default item and insert-menu entry. |
|
|
247
|
+
| `defaultEditorMessages` | `EditorMessages` | The English copy every prop falls back to — the applications this package was extracted from shipped these exact item labels, abbreviations included (`P`, `H2`, `Sub`, `Sup`). The default toolbar draws no icons, so an item's label is both its visible text and its accessible name. |
|
|
248
|
+
| `setEditorMessages` | `(messages: Partial<EditorMessages>) => void` | Provides copy for this subtree, merged over the defaults. Per render site, never from a root layout — see [App integration](#app-integration). |
|
|
249
|
+
| `getEditorMessages` | `() => EditorMessages` | Reads it, or `defaultEditorMessages` when no provider is mounted. |
|
|
250
|
+
|
|
251
|
+
The context key is `Symbol.for('@thomasfosterau/prosekit-svelte/messages')` — a registry symbol, so two copies of the module in one bundle still agree.
|
|
252
|
+
|
|
253
|
+
## Known limits
|
|
254
|
+
|
|
255
|
+
- ProseKit's stock `link` mark declares `href`, `target` and `rel` only — no `title`. `LinkDialog`'s `title` field reaches `addLink` and survives only for a grammar whose own `link` mark declares one; against the stock mark, ProseMirror's `computeAttrs` silently drops it.
|
|
256
|
+
- ProseKit's stock `image` node declares `src`, `width` and `height` only, which is why `ImageDialog` asks for one field, deliberately. The applications this package was extracted from also prompted for alt text, a title and a caption, and `computeAttrs` dropped all three — three prompts collecting text that went nowhere. An application whose image node declares more passes its own toolbar item.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<!-- @component An `Editor` on the block-document grammar with the default toolbar (`toolbar`, default `true`), the envelope remap (`documentType`, default `'blockDocument'`, ⇄ `doc`) and, given `name`, a hidden form input carrying the document JSON. -->
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
BlockEditor — an `Editor` on the block-document grammar, with the default
|
|
5
|
+
toolbar and a hidden form input.
|
|
6
|
+
|
|
7
|
+
Two things it adds over `Editor`.
|
|
8
|
+
|
|
9
|
+
The envelope remap. A stored document's root `type` is the application's
|
|
10
|
+
own name for the surface (`blockDocument` by default); a compiled
|
|
11
|
+
ProseMirror schema's top node is always `doc`. This component is that
|
|
12
|
+
boundary, so it remaps in both directions: `defaultContent` is rewritten to
|
|
13
|
+
`doc` on the way in, and the hidden input always carries the application
|
|
14
|
+
shape. Feeding the stored shape straight into ProseKit throws
|
|
15
|
+
`RangeError: Unknown node type: blockDocument` and takes the hosting page
|
|
16
|
+
down with it — the bug both applications this package was extracted from
|
|
17
|
+
hit (#459).
|
|
18
|
+
|
|
19
|
+
The hidden input. Given a `name`, the editor is a progressive-enhancement
|
|
20
|
+
form field: the JSON of the current document, in the application's shape,
|
|
21
|
+
posts with the form.
|
|
22
|
+
|
|
23
|
+
`toolbar` is one prop carrying both halves of the design: `true` (the
|
|
24
|
+
default) renders `BlockToolbar`, `false` renders none, and a snippet —
|
|
25
|
+
which is what `{#snippet toolbar()}` on the call site passes — replaces it.
|
|
26
|
+
-->
|
|
27
|
+
<script lang="ts">import { fromEditorDocument, toEditorDocument } from "@thomasfosterau/prosekit-utilities";
|
|
28
|
+
import { defineBlockDocumentExtension } from "@thomasfosterau/prosekit-utilities/prosekit";
|
|
29
|
+
import BlockToolbar from "../toolbar/BlockToolbar.svelte";
|
|
30
|
+
import EditorMount from "./Editor.svelte";
|
|
31
|
+
let { extension, documentType = "blockDocument", content = $bindable(), defaultContent, name, items, slashCommands, toolbar = true, label, labelledBy, class: className, children, ...rest } = $props();
|
|
32
|
+
// Built here rather than in the prop default so an application that passes
|
|
33
|
+
// its own extension never constructs the stock one, and so the grammar is
|
|
34
|
+
// captured once — `Editor` creates its editor once for the same reason.
|
|
35
|
+
// svelte-ignore state_referenced_locally
|
|
36
|
+
const resolvedExtension = extension ?? defineBlockDocumentExtension();
|
|
37
|
+
const toStored = $derived(fromEditorDocument(documentType));
|
|
38
|
+
const editorDefaultContent = $derived(defaultContent === undefined ? undefined : toEditorDocument(defaultContent));
|
|
39
|
+
const serialised = $derived.by(() => {
|
|
40
|
+
const current = content ?? defaultContent;
|
|
41
|
+
return current === undefined ? undefined : JSON.stringify(toStored(current));
|
|
42
|
+
});
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<EditorMount
|
|
46
|
+
{...rest}
|
|
47
|
+
bind:content
|
|
48
|
+
extension={resolvedExtension}
|
|
49
|
+
defaultContent={editorDefaultContent}
|
|
50
|
+
{label}
|
|
51
|
+
{labelledBy}
|
|
52
|
+
class={className}
|
|
53
|
+
>
|
|
54
|
+
{#if toolbar === true}
|
|
55
|
+
<BlockToolbar {items} {slashCommands} />
|
|
56
|
+
{:else if typeof toolbar === 'function'}
|
|
57
|
+
{@render toolbar()}
|
|
58
|
+
{/if}
|
|
59
|
+
{#if children}
|
|
60
|
+
{@render children()}
|
|
61
|
+
{/if}
|
|
62
|
+
</EditorMount>
|
|
63
|
+
|
|
64
|
+
{#if name !== undefined && serialised !== undefined}
|
|
65
|
+
<input type="hidden" {name} value={serialised} />
|
|
66
|
+
{/if}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type NodeJSON } from '@thomasfosterau/prosekit-utilities';
|
|
2
|
+
import type { Extension } from 'prosekit/core';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
5
|
+
import type { SlashCommand, ToolbarItem, ToolbarSeparator } from '../toolbar/items.ts';
|
|
6
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'aria-label' | 'aria-labelledby'> {
|
|
7
|
+
/**
|
|
8
|
+
* The grammar. Omitted, `defineBlockDocumentExtension()` — built lazily,
|
|
9
|
+
* so an application passing its own never constructs the default.
|
|
10
|
+
*/
|
|
11
|
+
extension?: Extension | undefined;
|
|
12
|
+
/** The envelope `type` of the stored document. */
|
|
13
|
+
documentType?: string | undefined;
|
|
14
|
+
/** Two-way bound current content, in editor (`doc`) shape. */
|
|
15
|
+
content?: NodeJSON | undefined;
|
|
16
|
+
/** Content the editor is created with, in either shape. */
|
|
17
|
+
defaultContent?: NodeJSON | undefined;
|
|
18
|
+
/** Form field name. Given one, a hidden input carries the document's JSON. */
|
|
19
|
+
name?: string | undefined;
|
|
20
|
+
/** The toolbar's items. Passed to the default `BlockToolbar`. */
|
|
21
|
+
items?: ReadonlyArray<ToolbarItem | ToolbarSeparator> | undefined;
|
|
22
|
+
/** The insert menu's entries. Passed to the default `BlockToolbar`. */
|
|
23
|
+
slashCommands?: ReadonlyArray<SlashCommand> | undefined;
|
|
24
|
+
/** `true` for the default toolbar, `false` for none, a snippet to replace it. */
|
|
25
|
+
toolbar?: boolean | Snippet<[]> | undefined;
|
|
26
|
+
/** Accessible name of the wrapper. */
|
|
27
|
+
label?: string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* The `id` (or space-separated `id`s) of the visible element that names
|
|
30
|
+
* the wrapper, rendered as `aria-labelledby`. Wins over `label`, which
|
|
31
|
+
* stays the fallback name if no element has the id.
|
|
32
|
+
*/
|
|
33
|
+
labelledBy?: string | undefined;
|
|
34
|
+
/** Extra classes on the wrapper. */
|
|
35
|
+
class?: string | undefined;
|
|
36
|
+
/** Rendered after the toolbar, inside the editor's provider. */
|
|
37
|
+
children?: Snippet<[]> | undefined;
|
|
38
|
+
}
|
|
39
|
+
/** An `Editor` on the block-document grammar with the default toolbar (`toolbar`, default `true`), the envelope remap (`documentType`, default `'blockDocument'`, ⇄ `doc`) and, given `name`, a hidden form input carrying the document JSON. */
|
|
40
|
+
declare const BlockEditor: import("svelte").Component<Props, {}, "content">;
|
|
41
|
+
type BlockEditor = ReturnType<typeof BlockEditor>;
|
|
42
|
+
export default BlockEditor;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<!-- @component The ProseKit editor mount: `content` binds two-way, `defaultContent` seeds once, and `children` (a toolbar) renders inside the editor context. Importing it pulls in ProseKit's runtime and stylesheets. -->
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
Editor — the ProseKit mount, and this package's only side-effecting
|
|
5
|
+
imports.
|
|
6
|
+
|
|
7
|
+
`prosekit/basic/style.css` and `prosekit/basic/typography.css` are bare
|
|
8
|
+
imports in another package's module graph, so the `"sideEffects": false`
|
|
9
|
+
this package declares about its own emitted modules still holds. What they
|
|
10
|
+
do mean is that anything importing this component pulls ProseKit's
|
|
11
|
+
stylesheets — and, through `createEditor`, the ProseKit runtime — into its
|
|
12
|
+
chunk. Import it by path from a route that genuinely edits a document, or
|
|
13
|
+
through a dynamic `import()` from one that only sometimes does, and never
|
|
14
|
+
re-export it from a barrel an application loads on every page.
|
|
15
|
+
|
|
16
|
+
The wrapper is a `role="group"` only when it is named, by `label` or
|
|
17
|
+
`labelledBy`: a group with no accessible name is noise in the
|
|
18
|
+
accessibility tree, and the thing that
|
|
19
|
+
matters — the `contenteditable` textbox ProseMirror builds — is named by
|
|
20
|
+
the application either way. `children` renders inside `<ProseKit>`, after
|
|
21
|
+
the mount node, which is where a toolbar goes: it needs the editor context
|
|
22
|
+
the provider sets.
|
|
23
|
+
-->
|
|
24
|
+
<script lang="ts" generics="E extends import('prosekit/core').Extension">import "prosekit/basic/style.css";
|
|
25
|
+
import "prosekit/basic/typography.css";
|
|
26
|
+
import { createEditor } from "prosekit/core";
|
|
27
|
+
import { ProseKit, useDocChange } from "prosekit/svelte";
|
|
28
|
+
let { extension, content = $bindable(), defaultContent, label, labelledBy, class: className, children, ...rest } = $props();
|
|
29
|
+
// The editor is created once. `extension` and the initial content are
|
|
30
|
+
// captured by value on purpose — reacting to later prop changes would
|
|
31
|
+
// recreate the editor as the user types (see `defaultContent` above).
|
|
32
|
+
// svelte-ignore state_referenced_locally
|
|
33
|
+
const initialContent = defaultContent ?? content;
|
|
34
|
+
// svelte-ignore state_referenced_locally
|
|
35
|
+
const editor = createEditor({
|
|
36
|
+
extension,
|
|
37
|
+
// The library's `NodeJSON` is the deeply-readonly twin of ProseKit's; the
|
|
38
|
+
// value is the same object either way, and `createEditor` only reads it.
|
|
39
|
+
...initialContent !== undefined && { defaultContent: initialContent }
|
|
40
|
+
});
|
|
41
|
+
// `useDocChange` returns its dispose function, which `$effect` re-uses as
|
|
42
|
+
// the cleanup — ProseKit's hooks are not Svelte-aware, so this is the
|
|
43
|
+
// wiring that unsubscribes the document listener on unmount.
|
|
44
|
+
$effect(() => useDocChange(() => content = editor.getDocJSON(), { editor }));
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<div
|
|
48
|
+
{...rest}
|
|
49
|
+
role={label === undefined && !labelledBy ? undefined : 'group'}
|
|
50
|
+
aria-label={label}
|
|
51
|
+
aria-labelledby={labelledBy || undefined}
|
|
52
|
+
class={['flex flex-col gap-2', className]}
|
|
53
|
+
>
|
|
54
|
+
<ProseKit {editor}>
|
|
55
|
+
{#if children}
|
|
56
|
+
{@render children()}
|
|
57
|
+
{/if}
|
|
58
|
+
<div {@attach editor.mount} class="outline-solid p-4"></div>
|
|
59
|
+
</ProseKit>
|
|
60
|
+
</div>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import 'prosekit/basic/style.css';
|
|
2
|
+
import 'prosekit/basic/typography.css';
|
|
3
|
+
import type { NodeJSON } from '@thomasfosterau/prosekit-utilities';
|
|
4
|
+
import type { Snippet } from 'svelte';
|
|
5
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
6
|
+
declare function $$render<E extends import('prosekit/core').Extension>(): {
|
|
7
|
+
props: Omit<HTMLAttributes<HTMLDivElement>, "children" | "aria-label" | "aria-labelledby"> & {
|
|
8
|
+
/** The grammar and behaviour this editor is built from. */
|
|
9
|
+
extension: E;
|
|
10
|
+
/** Two-way bound current content; reflects user edits. */
|
|
11
|
+
content?: NodeJSON | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Content the editor is created with. Decoupled from `content` so an
|
|
14
|
+
* update form can pre-populate without retriggering editor recreation as
|
|
15
|
+
* the user types.
|
|
16
|
+
*/
|
|
17
|
+
defaultContent?: NodeJSON | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Accessible name of the wrapper, and the fallback name when `labelledBy`
|
|
20
|
+
* names no element. With neither this nor `labelledBy`, the wrapper has
|
|
21
|
+
* no role.
|
|
22
|
+
*/
|
|
23
|
+
label?: string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* The `id` (or space-separated `id`s) of the visible element that names
|
|
26
|
+
* the wrapper, rendered as `aria-labelledby`. Wins over `label`, which
|
|
27
|
+
* stays the fallback name if no element has the id.
|
|
28
|
+
*/
|
|
29
|
+
labelledBy?: string | undefined;
|
|
30
|
+
/** Extra classes on the wrapper. */
|
|
31
|
+
class?: string | undefined;
|
|
32
|
+
/** Rendered inside the provider, before the mount node — the toolbar's place. */
|
|
33
|
+
children?: Snippet<[]> | undefined;
|
|
34
|
+
};
|
|
35
|
+
exports: {};
|
|
36
|
+
bindings: "content";
|
|
37
|
+
slots: {};
|
|
38
|
+
events: {};
|
|
39
|
+
};
|
|
40
|
+
declare class __sveltets_Render<E extends import('prosekit/core').Extension> {
|
|
41
|
+
props(): ReturnType<typeof $$render<E>>['props'];
|
|
42
|
+
events(): ReturnType<typeof $$render<E>>['events'];
|
|
43
|
+
slots(): ReturnType<typeof $$render<E>>['slots'];
|
|
44
|
+
bindings(): "content";
|
|
45
|
+
exports(): {};
|
|
46
|
+
}
|
|
47
|
+
interface $$IsomorphicComponent {
|
|
48
|
+
new <E extends import('prosekit/core').Extension>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<E>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<E>['props']>, ReturnType<__sveltets_Render<E>['events']>, ReturnType<__sveltets_Render<E>['slots']>> & {
|
|
49
|
+
$$bindings?: ReturnType<__sveltets_Render<E>['bindings']>;
|
|
50
|
+
} & ReturnType<__sveltets_Render<E>['exports']>;
|
|
51
|
+
<E extends import('prosekit/core').Extension>(internal: unknown, props: ReturnType<__sveltets_Render<E>['props']> & {}): ReturnType<__sveltets_Render<E>['exports']>;
|
|
52
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
53
|
+
}
|
|
54
|
+
/** The ProseKit editor mount: `content` binds two-way, `defaultContent` seeds once, and `children` (a toolbar) renders inside the editor context. Importing it pulls in ProseKit's runtime and stylesheets. */
|
|
55
|
+
declare const Editor: $$IsomorphicComponent;
|
|
56
|
+
type Editor<E extends import('prosekit/core').Extension> = InstanceType<typeof Editor<E>>;
|
|
57
|
+
export default Editor;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<!-- @component A single-line `Editor` on the inline-document grammar (Enter is a no-op), with the same envelope remap and hidden form input as `BlockEditor`. -->
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
InlineEditor — an `Editor` on the inline-document grammar: one line of
|
|
5
|
+
rich text, marks but no blocks.
|
|
6
|
+
|
|
7
|
+
It carries the same envelope remap and hidden form input as `BlockEditor`
|
|
8
|
+
(the applications this package was extracted from had the remap on the
|
|
9
|
+
block editor only, and a separate `InlineEditorInput` wrapper for the
|
|
10
|
+
field; both are folded in here), and one thing of its own: **Enter is a
|
|
11
|
+
no-op**. An inline document's top node is `inline*`, so there is no second
|
|
12
|
+
block for Enter to make; without the binding the base keymap's Enter runs
|
|
13
|
+
against a document that cannot hold its result, and a field that looks like
|
|
14
|
+
a single-line input behaves like a textarea.
|
|
15
|
+
|
|
16
|
+
That binding lives in the DEFAULT extension. An application passing its own
|
|
17
|
+
`extension` owns the single-line semantics too — union
|
|
18
|
+
`defineKeymap({ Enter: () => true })` into it, or accept the base keymap's
|
|
19
|
+
behaviour.
|
|
20
|
+
-->
|
|
21
|
+
<script lang="ts">import { fromEditorDocument, toEditorDocument } from "@thomasfosterau/prosekit-utilities";
|
|
22
|
+
import { defineInlineDocumentExtension } from "@thomasfosterau/prosekit-utilities/prosekit";
|
|
23
|
+
import { defineKeymap, union } from "prosekit/core";
|
|
24
|
+
import EditorMount from "./Editor.svelte";
|
|
25
|
+
let { extension, documentType = "inlineDocument", content = $bindable(), defaultContent, name, label, labelledBy, class: className, children, ...rest } = $props();
|
|
26
|
+
// Returning `true` claims the key: ProseKit runs the most recently added
|
|
27
|
+
// keymap first, so this shadows the base keymap's Enter rather than racing
|
|
28
|
+
// it.
|
|
29
|
+
// svelte-ignore state_referenced_locally
|
|
30
|
+
const resolvedExtension = extension ?? union(defineInlineDocumentExtension(), defineKeymap({ Enter: () => true }));
|
|
31
|
+
const toStored = $derived(fromEditorDocument(documentType));
|
|
32
|
+
const editorDefaultContent = $derived(defaultContent === undefined ? undefined : toEditorDocument(defaultContent));
|
|
33
|
+
const serialised = $derived.by(() => {
|
|
34
|
+
const current = content ?? defaultContent;
|
|
35
|
+
return current === undefined ? undefined : JSON.stringify(toStored(current));
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<EditorMount
|
|
40
|
+
{...rest}
|
|
41
|
+
bind:content
|
|
42
|
+
extension={resolvedExtension}
|
|
43
|
+
defaultContent={editorDefaultContent}
|
|
44
|
+
{label}
|
|
45
|
+
{labelledBy}
|
|
46
|
+
class={className}
|
|
47
|
+
>
|
|
48
|
+
{#if children}
|
|
49
|
+
{@render children()}
|
|
50
|
+
{/if}
|
|
51
|
+
</EditorMount>
|
|
52
|
+
|
|
53
|
+
{#if name !== undefined && serialised !== undefined}
|
|
54
|
+
<input type="hidden" {name} value={serialised} />
|
|
55
|
+
{/if}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type NodeJSON } from '@thomasfosterau/prosekit-utilities';
|
|
2
|
+
import { type Extension } from 'prosekit/core';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
5
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'aria-label' | 'aria-labelledby'> {
|
|
6
|
+
/**
|
|
7
|
+
* The grammar. Omitted, `defineInlineDocumentExtension()` plus the
|
|
8
|
+
* Enter binding — built lazily, so an application passing its own never
|
|
9
|
+
* constructs the default.
|
|
10
|
+
*/
|
|
11
|
+
extension?: Extension | undefined;
|
|
12
|
+
/** The envelope `type` of the stored document. */
|
|
13
|
+
documentType?: string | undefined;
|
|
14
|
+
/** Two-way bound current content, in editor (`doc`) shape. */
|
|
15
|
+
content?: NodeJSON | undefined;
|
|
16
|
+
/** Content the editor is created with, in either shape. */
|
|
17
|
+
defaultContent?: NodeJSON | undefined;
|
|
18
|
+
/** Form field name. Given one, a hidden input carries the document's JSON. */
|
|
19
|
+
name?: string | undefined;
|
|
20
|
+
/** Accessible name of the wrapper. */
|
|
21
|
+
label?: string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* The `id` (or space-separated `id`s) of the visible element that names
|
|
24
|
+
* the wrapper, rendered as `aria-labelledby`. Wins over `label`, which
|
|
25
|
+
* stays the fallback name if no element has the id.
|
|
26
|
+
*/
|
|
27
|
+
labelledBy?: string | undefined;
|
|
28
|
+
/** Extra classes on the wrapper. */
|
|
29
|
+
class?: string | undefined;
|
|
30
|
+
/** Rendered inside the editor's provider, after the mount node. */
|
|
31
|
+
children?: Snippet<[]> | undefined;
|
|
32
|
+
}
|
|
33
|
+
/** A single-line `Editor` on the inline-document grammar (Enter is a no-op), with the same envelope remap and hidden form input as `BlockEditor`. */
|
|
34
|
+
declare const InlineEditor: import("svelte").Component<Props, {}, "content">;
|
|
35
|
+
type InlineEditor = ReturnType<typeof InlineEditor>;
|
|
36
|
+
export default InlineEditor;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@thomasfosterau/prosekit-svelte` — Svelte 5 editor components on ProseKit.
|
|
3
|
+
*
|
|
4
|
+
* `Editor.svelte` carries this package's only side-effecting imports, the
|
|
5
|
+
* ProseKit basic stylesheets, and through `createEditor` it pulls the
|
|
6
|
+
* ProseKit runtime behind them. Import a component from here by path from a
|
|
7
|
+
* route that genuinely edits a document, or through a dynamic `import()`
|
|
8
|
+
* from one that only sometimes does; never re-export one from a barrel an
|
|
9
|
+
* application loads on every page.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
export { default as BlockEditor } from './editor/BlockEditor.svelte';
|
|
14
|
+
export { default as Editor } from './editor/Editor.svelte';
|
|
15
|
+
export { default as InlineEditor } from './editor/InlineEditor.svelte';
|
|
16
|
+
export { defaultEditorMessages, getEditorMessages, setEditorMessages, type EditorMessages, } from './messages.svelte.ts';
|
|
17
|
+
export { default as BlockToolbar } from './toolbar/BlockToolbar.svelte';
|
|
18
|
+
export { default as ImageDialog } from './toolbar/ImageDialog.svelte';
|
|
19
|
+
export { createDefaultSlashCommands, createDefaultToolbarItems, isToolbarSeparator, type SlashCommand, type ToolbarEditor, type ToolbarItem, type ToolbarRunContext, type ToolbarSeparator, } from './toolbar/items.ts';
|
|
20
|
+
export { default as LinkDialog } from './toolbar/LinkDialog.svelte';
|
|
21
|
+
export { default as SlashMenu } from './toolbar/SlashMenu.svelte';
|