@thomasfosterau/prosekit-svelte 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,262 @@
1
+ /**
2
+ * The toolbar's data model, and the default sets built from it.
3
+ *
4
+ * `BlockToolbar` renders a list rather than a hard-coded row of buttons.
5
+ * The applications this package was extracted from hard-coded theirs, and
6
+ * the cost showed: one of them carried five buttons for node types its
7
+ * grammar no longer had, and neither could add an item without editing the
8
+ * component. Here an item is a value — an application appends its own,
9
+ * reorders, drops one, or passes a list of its own making.
10
+ *
11
+ * An item's `run` receives a {@link ToolbarRunContext} rather than the
12
+ * editor alone, because two of the default items open a dialog the toolbar
13
+ * owns. `dialogs.link()` / `dialogs.image()` are how an item asks for one
14
+ * without importing it.
15
+ *
16
+ * @module
17
+ */
18
+ import { defaultEditorMessages } from "../messages.svelte.js";
19
+ /** Narrows an `items` entry to the separator arm. */
20
+ export function isToolbarSeparator(entry) {
21
+ return 'separator' in entry;
22
+ }
23
+ /**
24
+ * Reads a mark's active state without assuming the schema has it.
25
+ *
26
+ * The default item set covers `defineBlockDocumentExtension()`'s grammar,
27
+ * but an application may pass a narrower extension and keep the default
28
+ * items; a missing mark then makes the button inert rather than throwing on
29
+ * every editor update.
30
+ */
31
+ const isMarkActive = (editor, name) => editor.marks[name]?.isActive() ?? false;
32
+ /** {@link isMarkActive} for a node type, optionally matching `attrs`. */
33
+ const isNodeActive = (editor, name, attrs) => editor.nodes[name]?.isActive(attrs) ?? false;
34
+ /**
35
+ * Toggles one of the three marks that have no toggle command of their own.
36
+ *
37
+ * `highlight`, `subscript` and `superscript` are the applications' own mark
38
+ * specs, and ProseKit's generic `addMark` is the only command that reaches
39
+ * them. `addMark` alone can never turn a mark off, which is exactly the lie
40
+ * `aria-pressed` would then tell, so the item reads its own `isActive` and
41
+ * picks `removeMark` when the mark is on.
42
+ */
43
+ const toggleGenericMark = (editor, type) => {
44
+ if (isMarkActive(editor, type))
45
+ editor.commands['removeMark']?.({ type });
46
+ else
47
+ editor.commands['addMark']?.({ type });
48
+ };
49
+ /**
50
+ * The default toolbar: the nineteen items the applications shipped, in their
51
+ * order, with two separators marking the mark / block / history groups.
52
+ *
53
+ * The twentieth control those toolbars carried was a button that toggled an
54
+ * ad-hoc "slash" panel; it is the insert menu now, and `BlockToolbar`
55
+ * renders it itself from {@link createDefaultSlashCommands}.
56
+ *
57
+ * Every `run` goes through the editor's own `commands`, `marks` and `nodes`,
58
+ * so the set works against any extension that registers those node and mark
59
+ * types and quietly does nothing against one that does not.
60
+ *
61
+ * @param messages - Labels to use instead of the English defaults. A partial
62
+ * record; unlisted keys keep their default.
63
+ */
64
+ export function createDefaultToolbarItems(messages) {
65
+ const m = { ...defaultEditorMessages, ...messages };
66
+ return [
67
+ {
68
+ id: 'bold',
69
+ label: m.bold,
70
+ kind: 'toggle',
71
+ isActive: (editor) => isMarkActive(editor, 'bold'),
72
+ run: ({ editor }) => void editor.commands['toggleBold']?.(),
73
+ },
74
+ {
75
+ id: 'italic',
76
+ label: m.italic,
77
+ kind: 'toggle',
78
+ isActive: (editor) => isMarkActive(editor, 'italic'),
79
+ run: ({ editor }) => void editor.commands['toggleItalic']?.(),
80
+ },
81
+ {
82
+ id: 'underline',
83
+ label: m.underline,
84
+ kind: 'toggle',
85
+ isActive: (editor) => isMarkActive(editor, 'underline'),
86
+ run: ({ editor }) => void editor.commands['toggleUnderline']?.(),
87
+ },
88
+ {
89
+ id: 'strike',
90
+ label: m.strike,
91
+ kind: 'toggle',
92
+ isActive: (editor) => isMarkActive(editor, 'strike'),
93
+ run: ({ editor }) => void editor.commands['toggleStrike']?.(),
94
+ },
95
+ {
96
+ id: 'highlight',
97
+ label: m.highlight,
98
+ kind: 'toggle',
99
+ isActive: (editor) => isMarkActive(editor, 'highlight'),
100
+ run: ({ editor }) => toggleGenericMark(editor, 'highlight'),
101
+ },
102
+ {
103
+ id: 'subscript',
104
+ label: m.subscript,
105
+ kind: 'toggle',
106
+ isActive: (editor) => isMarkActive(editor, 'subscript'),
107
+ run: ({ editor }) => toggleGenericMark(editor, 'subscript'),
108
+ },
109
+ {
110
+ id: 'superscript',
111
+ label: m.superscript,
112
+ kind: 'toggle',
113
+ isActive: (editor) => isMarkActive(editor, 'superscript'),
114
+ run: ({ editor }) => toggleGenericMark(editor, 'superscript'),
115
+ },
116
+ {
117
+ id: 'link',
118
+ label: m.link,
119
+ kind: 'action',
120
+ run: ({ dialogs }) => dialogs.link(),
121
+ },
122
+ { id: 'marks-blocks', separator: true },
123
+ {
124
+ id: 'paragraph',
125
+ // An action, not a toggle: `setParagraph` only ever sets, so a pressed
126
+ // state that can never be un-pressed would be a claim `aria-pressed`
127
+ // makes out loud and the button cannot honour. The eleven items around
128
+ // it each have a real toggle command.
129
+ label: m.paragraph,
130
+ kind: 'action',
131
+ run: ({ editor }) => void editor.commands['setParagraph']?.(),
132
+ },
133
+ {
134
+ id: 'heading2',
135
+ label: m.heading2,
136
+ kind: 'toggle',
137
+ isActive: (editor) => isNodeActive(editor, 'heading', { level: 2 }),
138
+ run: ({ editor }) => void editor.commands['toggleHeading']?.({ level: 2 }),
139
+ },
140
+ {
141
+ id: 'quote',
142
+ label: m.quote,
143
+ kind: 'toggle',
144
+ isActive: (editor) => isNodeActive(editor, 'blockquote'),
145
+ run: ({ editor }) => void editor.commands['toggleBlockquote']?.(),
146
+ },
147
+ {
148
+ id: 'bullets',
149
+ label: m.bullets,
150
+ kind: 'toggle',
151
+ isActive: (editor) => isNodeActive(editor, 'list', { kind: 'bullet' }),
152
+ run: ({ editor }) => void editor.commands['toggleList']?.({ kind: 'bullet' }),
153
+ },
154
+ {
155
+ id: 'numbered',
156
+ label: m.numbered,
157
+ kind: 'toggle',
158
+ isActive: (editor) => isNodeActive(editor, 'list', { kind: 'ordered' }),
159
+ run: ({ editor }) => void editor.commands['toggleList']?.({ kind: 'ordered' }),
160
+ },
161
+ {
162
+ id: 'code-block',
163
+ label: m.codeBlock,
164
+ kind: 'toggle',
165
+ isActive: (editor) => isNodeActive(editor, 'codeBlock'),
166
+ run: ({ editor }) => void editor.commands['toggleCodeBlock']?.(),
167
+ },
168
+ {
169
+ id: 'rule',
170
+ label: m.rule,
171
+ kind: 'action',
172
+ run: ({ editor }) => void editor.commands['insertHorizontalRule']?.(),
173
+ },
174
+ {
175
+ id: 'table',
176
+ label: m.table,
177
+ kind: 'action',
178
+ run: ({ editor }) => void editor.commands['insertTable']?.({ row: 3, col: 3 }),
179
+ },
180
+ {
181
+ id: 'image',
182
+ label: m.image,
183
+ kind: 'action',
184
+ run: ({ dialogs }) => dialogs.image(),
185
+ },
186
+ { id: 'blocks-history', separator: true },
187
+ {
188
+ id: 'undo',
189
+ label: m.undo,
190
+ kind: 'action',
191
+ run: ({ editor }) => void editor.commands['undo']?.(),
192
+ },
193
+ {
194
+ id: 'redo',
195
+ label: m.redo,
196
+ kind: 'action',
197
+ run: ({ editor }) => void editor.commands['redo']?.(),
198
+ },
199
+ ];
200
+ }
201
+ /**
202
+ * The default insert menu: the ten entries the applications shipped, in
203
+ * their order.
204
+ *
205
+ * @param messages - Labels to use instead of the English defaults. A partial
206
+ * record; unlisted keys keep their default.
207
+ */
208
+ export function createDefaultSlashCommands(messages) {
209
+ const m = { ...defaultEditorMessages, ...messages };
210
+ return [
211
+ {
212
+ id: 'paragraph',
213
+ label: m.slashParagraph,
214
+ run: ({ editor }) => void editor.commands['setParagraph']?.(),
215
+ },
216
+ {
217
+ id: 'heading2',
218
+ label: m.slashHeading2,
219
+ run: ({ editor }) => void editor.commands['toggleHeading']?.({ level: 2 }),
220
+ },
221
+ {
222
+ id: 'blockquote',
223
+ label: m.slashBlockquote,
224
+ run: ({ editor }) => void editor.commands['toggleBlockquote']?.(),
225
+ },
226
+ {
227
+ id: 'bullet-list',
228
+ label: m.slashBulletList,
229
+ run: ({ editor }) => void editor.commands['toggleList']?.({ kind: 'bullet' }),
230
+ },
231
+ {
232
+ id: 'ordered-list',
233
+ label: m.slashOrderedList,
234
+ run: ({ editor }) => void editor.commands['toggleList']?.({ kind: 'ordered' }),
235
+ },
236
+ {
237
+ id: 'todo-list',
238
+ label: m.slashTodoList,
239
+ run: ({ editor }) => void editor.commands['toggleList']?.({ kind: 'task' }),
240
+ },
241
+ {
242
+ id: 'code-block',
243
+ label: m.slashCodeBlock,
244
+ run: ({ editor }) => void editor.commands['toggleCodeBlock']?.(),
245
+ },
246
+ {
247
+ id: 'table',
248
+ label: m.slashTable,
249
+ run: ({ editor }) => void editor.commands['insertTable']?.({ row: 3, col: 3 }),
250
+ },
251
+ {
252
+ id: 'image',
253
+ label: m.slashImage,
254
+ run: ({ dialogs }) => dialogs.image(),
255
+ },
256
+ {
257
+ id: 'horizontal-rule',
258
+ label: m.slashHorizontalRule,
259
+ run: ({ editor }) => void editor.commands['insertHorizontalRule']?.(),
260
+ },
261
+ ];
262
+ }
package/package.json CHANGED
@@ -1,14 +1,68 @@
1
1
  {
2
+ "$schema": "https://www.schemastore.org/package",
2
3
  "name": "@thomasfosterau/prosekit-svelte",
3
- "version": "0.0.0",
4
- "description": "Placeholder reserved for npm trusted publishing; the first real release of @thomasfosterau/prosekit-svelte is 0.1.0.",
4
+ "version": "0.1.0",
5
+ "description": "Svelte 5 rich-text editor components on ProseKit — Editor, BlockEditor, InlineEditor, a declarative toolbar with an insert menu and link/image dialogs on bits-ui, every string a prop",
6
+ "keywords": [
7
+ "accessibility",
8
+ "bits-ui",
9
+ "editor",
10
+ "prosekit",
11
+ "prosemirror",
12
+ "rich-text",
13
+ "svelte",
14
+ "svelte5"
15
+ ],
5
16
  "license": "MIT",
17
+ "author": "Thomas Foster <me@thomasfoster.au>",
6
18
  "repository": {
7
19
  "type": "git",
8
- "url": "git+https://github.com/thomasfosterau/prosekit-utilities.git",
20
+ "url": "https://github.com/thomasfosterau/prosekit-utilities.git",
9
21
  "directory": "packages/prosekit-svelte"
10
22
  },
11
- "publishConfig": {
12
- "access": "public"
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "types": "./dist/index.d.ts",
29
+ "svelte": "./dist/index.js",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "svelte": "./dist/index.js",
34
+ "default": "./dist/index.js"
35
+ }
36
+ },
37
+ "dependencies": {
38
+ "@thomasfosterau/prosekit-utilities": "0.1.0",
39
+ "bits-ui": "^2.20.2"
40
+ },
41
+ "devDependencies": {
42
+ "@prosekit/pm": "^0.1.21",
43
+ "@storybook/addon-a11y": "^10.6.6",
44
+ "@storybook/addon-svelte-csf": "^5.1.6",
45
+ "@storybook/sveltekit": "^10.6.6",
46
+ "@sveltejs/kit": "3.1.1",
47
+ "@sveltejs/package": "3.0.2",
48
+ "@sveltejs/vite-plugin-svelte": "^7.3.1",
49
+ "@tailwindcss/vite": "^4.3.3",
50
+ "@testing-library/jest-dom": "^7.0.1",
51
+ "@types/node": "^26.6.6",
52
+ "@vitest/browser-playwright": "5.0.5",
53
+ "daisyui": "^5.7.50",
54
+ "playwright": "1.60.0",
55
+ "prosekit": "^0.23.0",
56
+ "storybook": "^10.6.6",
57
+ "svelte": "^5.57.3",
58
+ "svelte-check": "^4.7.8",
59
+ "tailwindcss": "^4.3.3",
60
+ "vitest": "5.0.5",
61
+ "vitest-browser-svelte": "^3.1.0"
62
+ },
63
+ "peerDependencies": {
64
+ "@prosekit/pm": "^0.1.19",
65
+ "prosekit": "^0.22.2",
66
+ "svelte": "^5.30.1"
13
67
  }
14
- }
68
+ }