@stesura/react-wordgard 0.1.0 → 0.2.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +699 -237
  3. package/dist/ReactWordgard.js +2 -4
  4. package/dist/components/ChildNodeViews.js +2 -4
  5. package/dist/components/DecorationRoot.d.ts +6 -0
  6. package/dist/components/DecorationRoot.js +7 -4
  7. package/dist/components/ErrorBoundary.js +3 -5
  8. package/dist/components/SeparatorHackView.js +1 -2
  9. package/dist/components/ShapeView.js +2 -4
  10. package/dist/components/TextNodeView.js +2 -3
  11. package/dist/components/TrailingHackView.js +2 -3
  12. package/dist/components/WidgetView.js +2 -5
  13. package/dist/components/WordgardDoc.d.ts +5 -0
  14. package/dist/components/WordgardDoc.js +8 -6
  15. package/dist/components/WordgardEditor.d.ts +8 -0
  16. package/dist/components/WordgardEditor.js +10 -6
  17. package/dist/components/WordgardScope.d.ts +14 -0
  18. package/dist/components/WordgardScope.js +14 -0
  19. package/dist/components/WrapperView.js +1 -4
  20. package/dist/components/marks/MarkView.js +3 -6
  21. package/dist/components/marks/ReactMarkView.js +2 -4
  22. package/dist/components/nodes/DefaultNodeView.js +3 -7
  23. package/dist/components/nodes/DocNodeView.js +2 -3
  24. package/dist/composition.js +4 -6
  25. package/dist/contexts/DecorationContext.js +2 -5
  26. package/dist/contexts/DecorationRootContext.js +2 -4
  27. package/dist/contexts/EditorContext.d.ts +1 -0
  28. package/dist/contexts/EditorContext.js +3 -3
  29. package/dist/contexts/RenderStoreContext.js +3 -5
  30. package/dist/contexts/editorEvents.js +3 -6
  31. package/dist/decorations/ReactMarkType.d.ts +11 -0
  32. package/dist/decorations/ReactMarkType.js +6 -7
  33. package/dist/decorations/ReactShapeType.d.ts +16 -0
  34. package/dist/decorations/ReactShapeType.js +9 -11
  35. package/dist/decorations/ReactWidgetType.d.ts +10 -0
  36. package/dist/decorations/ReactWidgetType.js +6 -8
  37. package/dist/decorations/ReactWrapperType.d.ts +9 -0
  38. package/dist/decorations/ReactWrapperType.js +3 -6
  39. package/dist/decorations/iterDeco.js +3 -6
  40. package/dist/hooks/useEditorEffect.d.ts +7 -0
  41. package/dist/hooks/useEditorEffect.js +11 -7
  42. package/dist/hooks/useEditorEventCallback.d.ts +6 -0
  43. package/dist/hooks/useEditorEventCallback.js +7 -3
  44. package/dist/hooks/useEditorEventListener.d.ts +7 -0
  45. package/dist/hooks/useEditorEventListener.js +11 -11
  46. package/dist/hooks/useEditorLayoutEffect.d.ts +9 -0
  47. package/dist/hooks/useEditorLayoutEffect.js +13 -17
  48. package/dist/hooks/useEditorState.d.ts +5 -0
  49. package/dist/hooks/useEditorState.js +6 -4
  50. package/dist/hooks/useEditorStateSelector.d.ts +5 -0
  51. package/dist/hooks/useEditorStateSelector.js +5 -0
  52. package/dist/hooks/useIgnoreMutation.d.ts +5 -1
  53. package/dist/hooks/useIgnoreMutation.js +11 -10
  54. package/dist/hooks/useIsNodeSelected.d.ts +1 -0
  55. package/dist/hooks/useIsNodeSelected.js +3 -4
  56. package/dist/hooks/useMergedDOMRefs.d.ts +4 -0
  57. package/dist/hooks/useMergedDOMRefs.js +5 -4
  58. package/dist/hooks/useNodePos.d.ts +3 -0
  59. package/dist/hooks/useNodePos.js +5 -6
  60. package/dist/hooks/useSelectNode.d.ts +4 -0
  61. package/dist/hooks/useSelectNode.js +5 -4
  62. package/dist/hooks/useStopEvent.d.ts +6 -0
  63. package/dist/hooks/useStopEvent.js +8 -7
  64. package/dist/hooks/useWordgard.d.ts +1 -0
  65. package/dist/hooks/useWordgard.js +1 -2
  66. package/dist/index.d.ts +1 -0
  67. package/dist/index.js +1 -0
  68. package/dist/tile.js +3 -8
  69. package/dist/viewRoots.js +2 -3
  70. package/dist/wordgardInternals.js +2 -5
  71. package/package.json +1 -1
  72. package/skills/react-wordgard/SKILL.md +138 -10
package/README.md CHANGED
@@ -6,15 +6,59 @@
6
6
  react-wordgard stays on 0.x for as long as Wordgard does: it follows Wordgard's API and reaches
7
7
  into its internals, so it can't be more stable than Wordgard. Its 1.0 comes after Wordgard's.
8
8
 
9
- A React view for [Wordgard](https://www.npmjs.com/package/wordgard): React draws Wordgard's
10
- document, inside Wordgard's own update, and Wordgard keeps doing what it does (input, selection,
11
- composition, decorations). The API is Wordgard's; react-wordgard adds what React needs to draw it.
12
-
13
- This repository is derived from
14
- [react-prosemirror](https://github.com/handlewithcarecollective/react-prosemirror),
15
- by [Handle with Care Collective](https://github.com/handlewithcarecollective)
16
- and originally The New York Times Company. Thanks to them for the design this
17
- builds on.
9
+ A React view for [Wordgard](https://www.wordgard.net): React draws the Wordgard document in a
10
+ single tree, your app's, and any decoration that takes a shape, a wrapper or a widget can be a
11
+ React component. Wordgard keeps doing the rest (input, selection, composition), and the API stays
12
+ Wordgard's.
13
+
14
+ A React decoration renders its content as `{children}`, reads your app's contexts and stores, and
15
+ can re-render on its own without breaking the editor.
16
+
17
+ ## Contents
18
+
19
+ - [Inspired by react-prosemirror](#inspired-by-react-prosemirror)
20
+ - [Install](#install)
21
+ - [Usage](#usage)
22
+ - [Accessing the state outside the editor](#accessing-the-state-outside-the-editor)
23
+ - [React decorations](#react-decorations)
24
+ - [Should I use a shape, a wrapper or both?](#should-i-use-a-shape-a-wrapper-or-both)
25
+ - [Accessibility](#accessibility)
26
+ - [Limits](#limits)
27
+ - [API](#api)
28
+ - [Components](#components)
29
+ - [Decoration factories](#decoration-factories)
30
+ - [Editor hooks](#editor-hooks)
31
+ - [Decoration hooks](#decoration-hooks)
32
+ - [Props types](#props-types)
33
+ - [Migrating from react-prosemirror](#migrating-from-react-prosemirror)
34
+ - [Migrating a node view](#migrating-a-node-view)
35
+ - [Effects and timing](#effects-and-timing)
36
+ - [AI coding agents](#ai-coding-agents)
37
+ - [Licence](#licence)
38
+
39
+ ## Inspired by react-prosemirror
40
+
41
+ react-wordgard is derived from
42
+ [react-prosemirror](https://github.com/handlewithcarecollective/react-prosemirror), by
43
+ [Handle with Care Collective](https://github.com/handlewithcarecollective) and originally The New
44
+ York Times Company. We're hugely grateful for their work: the design here builds on it.
45
+
46
+ Marijn Haverbeke's Wordgard makes the bridge much easier to build. Most of react-prosemirror's
47
+ difficulties come from React rendering in two phases while ProseMirror's view updates in one: during
48
+ a render, components can see a newer state than the view
49
+ ([react-prosemirror's README](https://github.com/handlewithcarecollective/react-prosemirror#the-problem)
50
+ explains the problem). Wordgard doesn't update the DOM when you dispatch, but in its flush, on the
51
+ next frame (or sooner, when something needs the layout). react-wordgard renders the document inside
52
+ that flush, in one synchronous render, and its hooks read the state of the last flush: components
53
+ never see a state the page doesn't show. Wordgard also tells its view which parts of the document
54
+ changed and reads its view tree as plain data, so react-wordgard can build that tree from the DOM
55
+ React drew.
56
+
57
+ What Wordgard doesn't give you is React in its decorations: a shape, wrapper or widget is DOM, and
58
+ a React component there would mean portals into DOM that Wordgard owns and rewrites. That's what
59
+ react-wordgard is for.
60
+
61
+ The API stays as close to react-prosemirror's as Wordgard allows.
18
62
 
19
63
  ## Install
20
64
 
@@ -22,21 +66,19 @@ builds on.
22
66
  npm install @stesura/react-wordgard wordgard react react-dom
23
67
  ```
24
68
 
25
- Peer dependencies: `wordgard` 0.6 (`~0.6.2`: react-wordgard reaches into Wordgard's internals, so
26
- only its patches), `react` and `react-dom` 18 or 19. An app must have one copy of `wordgard`, the
27
- one react-wordgard uses. With TypeScript, Wordgard's own typings need `skipLibCheck`.
28
-
29
- With an AI coding agent: the package ships a skill for building editors with Wordgard and
30
- react-wordgard, `skills/react-wordgard/SKILL.md` (in `node_modules/@stesura/react-wordgard/`). Point your
31
- agent to it, or copy the `react-wordgard` folder into your project's `.claude/skills/` for Claude
32
- Code.
69
+ - `wordgard` 0.6.2 or a later 0.6 patch. A new minor (0.7) needs a react-wordgard release made
70
+ for it, as react-wordgard uses Wordgard's internals.
71
+ - `react` and `react-dom` 18 or 19.
72
+ - Only one copy of `wordgard` in your app: `npm ls wordgard` should list one version. With two, the
73
+ editor won't work.
74
+ - With TypeScript, `"skipLibCheck": true` in your `tsconfig.json`: Wordgard's own typings don't
75
+ type-check ([#120](https://code.haverbeke.berlin/wordgard/wordgard/issues/120)).
33
76
 
34
77
  ## Usage
35
78
 
36
79
  ```tsx
37
80
  import { useState } from "react";
38
81
  import { Decoration, Wordgard } from "wordgard/editor";
39
- // `image()` adds images to the schema; `Image` is their node type, which decorations name.
40
82
  import { basicSchema, image } from "wordgard/schema";
41
83
  import { Image } from "wordgard/types";
42
84
  import {
@@ -47,12 +89,11 @@ import {
47
89
  useEditorStateSelector,
48
90
  } from "@stesura/react-wordgard";
49
91
 
50
- // An image's shape, drawn by a React component with state of its own: hovering outlines it.
51
- // DecorationRoot is its root: the node type's element, an <img>, with the node's attributes (its
52
- // `src` and `alt`, the selected node's class) and the component's own props.
53
92
  const ImageShape = createShape(function ImageShape() {
54
93
  const [hover, setHover] = useState(false);
55
94
  return (
95
+ // DecorationRoot is its root: the node type's element, an <img>, with the node's attributes (its
96
+ // `src` and `alt`, the selected node's class) and the component's own props.
56
97
  <DecorationRoot
57
98
  style={hover ? { outline: "2px solid" } : undefined}
58
99
  onMouseEnter={() => setHover(true)}
@@ -61,12 +102,12 @@ const ImageShape = createShape(function ImageShape() {
61
102
  );
62
103
  });
63
104
 
64
- function WordCount() {
105
+ const WordCount = () => {
65
106
  const words = useEditorStateSelector((state) => state.doc.textContent().split(/\s+/).length);
66
107
  return <output>{words} words</output>;
67
- }
108
+ };
68
109
 
69
- export function Editor() {
110
+ export const Editor = () => {
70
111
  return (
71
112
  <WordgardEditor
72
113
  doc='<p>Hello <strong>world</strong> <img src="/cat.png" alt="A cat"></p>'
@@ -81,52 +122,105 @@ export function Editor() {
81
122
  <WordgardDoc className="editor" />
82
123
  </WordgardEditor>
83
124
  );
84
- }
125
+ };
126
+ ```
127
+
128
+ `<WordgardEditor>` creates the editor and `<WordgardDoc>` puts it in the page; React decorations
129
+ are below, and every component and hook is under [API](#api).
130
+
131
+ ### Accessing the state outside the editor
132
+
133
+ Wordgard's editor owns its state: there's no `setState` and no way to intercept its dispatches, so
134
+ the state can't be lifted into React and passed back in, as react-prosemirror's
135
+ `<ProseMirror state>` allows. To reach it from outside the document, to save it as JSON for
136
+ example, you have two options:
137
+
138
+ 1. **Lift `<WordgardEditor>`.** It draws no DOM: the editor goes where `<WordgardDoc />` is. Wrap
139
+ everything that needs the editor in it:
140
+
141
+ ```tsx
142
+ import { basicSchema } from "wordgard/schema";
143
+ import { WordgardDoc, WordgardEditor, useEditorEventCallback } from "@stesura/react-wordgard";
144
+
145
+ const SaveButton = () => {
146
+ const save = useEditorEventCallback((wg) => {
147
+ localStorage.setItem("doc", JSON.stringify(wg.state.doc.toJSON()));
148
+ });
149
+ return <button onClick={save}>Save</button>;
150
+ };
151
+
152
+ export const Page = () => (
153
+ <WordgardEditor doc="<p>Hello</p>" config={[basicSchema()]}>
154
+ <header>
155
+ <SaveButton />
156
+ </header>
157
+ <main>
158
+ <WordgardDoc />
159
+ </main>
160
+ </WordgardEditor>
161
+ );
85
162
  ```
86
163
 
87
- - `<WordgardEditor>` creates the editor from a `Wordgard.Spec` and provides it to what it holds.
88
- The spec is read once, when it mounts: changing `doc` or `config` afterwards does nothing. The
89
- editor owns its state: change it with `dispatch`, and its configuration with Wordgard's
90
- compartments or `GardState.appendConfig`. `<WordgardDoc>` puts the editor in the page and
91
- renders the document through a portal, in your React tree: your contexts reach React
92
- decorations. One `<WordgardDoc>` per editor; a second one throws.
93
- - `createShape`, `createWrapper`, `createMarkShape` and `createWidgetType` make React components
94
- Wordgard decorations: below. Call them at module level, with components defined there.
95
- `createShape`, `createWrapper` and `createMarkShape` give the same value for the same component
96
- (and `element`), so a config built again is the same; a component defined inside a render is a
97
- new one each time, and remounts everything it draws. The components don't need
98
- memoizing: React draws a node again only when it changed (its content, its shape, its
99
- attributes, its widget's value), or from the component's own state, contexts and hooks.
100
- - Hooks, from inside `<WordgardEditor>`:
101
- - `useWordgard()`: the editor.
102
- - `useEditorState()`, `useEditorStateSelector(selector, isEqual?)`: the state as of the
103
- editor's last flush, rendered in the same React commit as the document. The selector's result
104
- re-renders when it changes, by `Object.is` or by `isEqual`.
105
- - `useEditorEffect(effect, deps)`: an effect run once the editor's DOM is up to date, where it
106
- may read layout (`coordsAtPos`) and dispatch.
107
- - `useEditorEventCallback(callback)`: a stable callback getting the editor.
108
- - `useEditorLayoutEffect({ read, write })`: reads in every update's read phase, writes in its write
109
- phase, on the same paint as the document, for what must move with the text (markers in the
110
- margin, pagination). It can't dispatch: Wordgard throws during its update. A `read` returning
111
- `undefined` skips its write; when none asks for one, no write is scheduled, which spares
112
- Wordgard measuring again. Reads run in the order the effects were registered: an engine whose
113
- parts must run in another order registers one effect and drives them.
114
- - `useEditorEventListener(type, (wg, event) => handled)`: the editor's DOM events of `type`
115
- before Wordgard's handlers and keymaps; returning true handles the event. Key events during
116
- a composition don't come; handling `input`, `beforeinput` or composition events skips
117
- Wordgard's own handling of them.
118
- - In React's StrictMode (development only), `<WordgardEditor>` creates its editor twice and
119
- drops one, which is never put in the page.
120
- - Don't read the editor's layout from a plain React effect: it runs inside Wordgard's update,
121
- before the editor's view of the new DOM exists. Use `useEditorEffect` or `useEditorLayoutEffect`.
164
+ 2. **Share the editor through a store.** Where `<WordgardEditor>` can't wrap the button (another
165
+ part of the app, a portal), a component under it hands the editor to a store from
166
+ `useEditorEffect`. This example uses [Jotai](https://jotai.org), but any store works.
167
+
168
+ ```tsx
169
+ import { atom, useAtomValue, useSetAtom } from "jotai";
170
+ import type { Wordgard } from "wordgard/editor";
171
+ import { basicSchema } from "wordgard/schema";
172
+ import { WordgardDoc, WordgardEditor, useEditorEffect } from "@stesura/react-wordgard";
173
+
174
+ const editorAtom = atom<Wordgard | null>(null);
175
+
176
+ const ShareEditor = () => {
177
+ const setEditor = useSetAtom(editorAtom);
178
+ useEditorEffect((wg) => {
179
+ setEditor(wg);
180
+ return () => setEditor(null);
181
+ }, []);
182
+ return null;
183
+ };
184
+
185
+ const SaveButton = () => {
186
+ const wg = useAtomValue(editorAtom);
187
+ const save = () => {
188
+ if (wg) localStorage.setItem("doc", JSON.stringify(wg.state.doc.toJSON()));
189
+ };
190
+ return (
191
+ <button onClick={save} disabled={!wg}>
192
+ Save
193
+ </button>
194
+ );
195
+ };
196
+
197
+ const Editor = () => (
198
+ <WordgardEditor doc="<p>Hello</p>" config={[basicSchema()]}>
199
+ <ShareEditor />
200
+ <WordgardDoc />
201
+ </WordgardEditor>
202
+ );
203
+
204
+ // The button is outside the editor's tree.
205
+ export const App = () => (
206
+ <>
207
+ <header>
208
+ <SaveButton />
209
+ </header>
210
+ <main>
211
+ <Editor />
212
+ </main>
213
+ </>
214
+ );
215
+ ```
122
216
 
123
217
  ### React decorations
124
218
 
125
- A node's shape, a wrapper, a mark's shape and a widget can each be a React component. Shapes,
126
- wrappers and marks render what they hold as `children`; each can change the DOM around it (its
127
- "chrome") on its own: from its state, a context or a store. Each factory returns Wordgard's own
128
- value (a shape, a wrapper, a mark's shape, a widget type), registered as drawn by the component,
129
- which you pass to Wordgard's decorations:
219
+ Node decorations (shapes, wrappers, widgets) and mark decorations can be React components. Shapes,
220
+ wrappers and marks render their content as `children`, and can change the DOM around it on their
221
+ own, from their state, a context or a store. Call the factories at module level: a component
222
+ defined inside a render is new each time, and remounts everything it draws. The components don't
223
+ need memoizing.
130
224
 
131
225
  ```tsx
132
226
  import { Decoration } from "wordgard/editor";
@@ -163,59 +257,57 @@ const config = [
163
257
  ];
164
258
  ```
165
259
 
166
- - `createShape(component)` returns a node's shape for `Decoration.Tag.shape`, a function of the
167
- node's tag (a form `Decoration.Tag.shape` takes): the type's nodes are drawn by the component.
168
- - Its root is the node type's own outer element, with its tag for each node (a heading's level
169
- gives `h1` to `h6`) and its attributes.
170
- - The component gets `nodeProps` (`node`, `getPos`, `contentDOMRef`) and `children` (the
171
- content), which goes in the `contentDOMRef` element, else in the root. Define the component
172
- inline, as a named function, and its props are typed for you.
173
- - Only the outer element comes from the node type: the elements inside a structure shape (a
174
- figure's `img`, a table's `tbody`) are the component's to draw, from `nodeProps.node`. Attributes
175
- aimed at them land on the root, as a `target` that matches nothing picks the root (see "A shape,
176
- a wrapper, or both").
177
- - Where the node type draws an atom (a leaf: an image, a mention; a plot whose shape has no
178
- hole), so does its React shape: `children` is empty, everything the component draws is chrome,
179
- and a `contentDOMRef` throws.
180
- - `shape.of(element, { atom? })` is the shape with `element` for its root, for
181
- `Decoration.Point.shape` (which applies to whatever node follows, so no type says the element)
182
- or a root other than the type's: `Decoration.Tag.shape(Blockquote, CalloutShape.of("aside"))`.
183
- With `atom: true`, it has no hole: a leaf needs it (Wordgard refuses a hole in a leaf's shape),
184
- and it draws a plot as an atom.
260
+ - `createShape(component)`, for `Decoration.Tag.shape`: the component gets `nodeProps` (`node`,
261
+ `getPos`, `contentDOMRef`) and the content as `children`, which goes in the `contentDOMRef`
262
+ element, else in the root. Define it inline, as a named function, and its props are typed for you.
263
+ - The root is the node type's outer element (`h1` to `h6` for a heading, `img` for an image).
264
+ `<DecorationRoot>` draws it with Wordgard's attributes (the node's, the selection classes, the
265
+ decorations') and its ref; your props win, `className` and `style` are merged. Or draw it by
266
+ hand: below. Another tag logs an error.
267
+ - What's inside the root (a figure's `img`, a table's `tbody`) is the component's to draw, and
268
+ attributes aimed there land on the root: see
269
+ [Should I use a shape, a wrapper or both?](#should-i-use-a-shape-a-wrapper-or-both).
270
+ - An atom (an image, a mention) gets empty `children`, and a `contentDOMRef` throws.
271
+ - `shape.of(element, { atom? })` is the shape with another root, also for
272
+ `Decoration.Point.shape`: `CalloutShape.of("aside")`. A leaf needs `atom: true`.
185
273
  - `createWrapper(component, { element? })`, for `Decoration.Tag.wrapper` and
186
- `Decoration.Point.wrapper`: the same props without `contentDOMRef`; `children` is what it
187
- wraps (the node's shape, or the element its `target` picks). `element` is its root, `div` by
188
- default: a wrapper is an element the node type doesn't have.
189
- - `createMarkShape(component, { element? })`, for `Decoration.Mark.shape` or a mark type's own
190
- `shape`: `markProps` (`mark`, the document's, `getPos`, `contentDOMRef`) and `children`. One
191
- component draws each run of an equal mark. `element` is its root (`span` by default): a mark
192
- shape is a static spec, so the element can't be read from the mark type. A React mark replacing
193
- an attribute-shaped mark makes it element-shaped.
194
- - The root is the element the decoration says, and stays that element. Wordgard applies
195
- decorations to a shape before anything is drawn (`target` selectors match its tag, attributes
196
- merge onto its elements), so the tag has to be known up front.
197
- - `<DecorationRoot>` draws it, in a shape's, wrapper's or mark's component: that tag, with
198
- Wordgard's attributes (the node type's own, the selection classes `wg-selected-node` and
199
- `wg-selected-cell`, tag, point and mark attributes, `contenteditable` on an atom) and the root's
200
- ref. The component's own props win, except `className`, joined with Wordgard's, and `style`,
201
- merged with it (the component's keys winning). Its `ref` gets the root too. Without a
202
- `contentDOMRef`, the content goes in it. One per component, at its root.
203
- - Or the component draws its root by hand, for more control over the decoration's attributes:
204
- below.
205
- - A root with another tag than the decoration's logs an error, once per tag.
206
- - The chrome is made uneditable unless it sets `contentEditable` itself.
274
+ `Decoration.Point.wrapper`: the same props without `contentDOMRef`, and what it wraps as
275
+ `children`. Its root is a `div` by default.
276
+ - `createMarkShape(component, { element? })`, for `Decoration.Mark.shape`: `markProps` (`mark`,
277
+ `getPos`, `contentDOMRef`) and `children`, one component per run of the mark. Its root is a
278
+ `span` by default.
279
+ - The chrome is uneditable unless it sets `contentEditable`.
207
280
 
208
281
  #### A root drawn by hand
209
282
 
210
283
  `DecorationRoot` applies all of Wordgard's attributes. To decide what reaches the root (drop one,
211
284
  change one, join Wordgard's classes your way) or to type the root as its element, the component
212
- draws the root itself. It gets Wordgard's attributes as props and a `ref` for the root:
285
+ draws the root itself. It gets Wordgard's attributes as props and a `ref` for the root.
286
+
287
+ On React 19, `ref` is a prop:
288
+
289
+ ```tsx
290
+ import { createShape } from "@stesura/react-wordgard";
291
+
292
+ // An image whose root drops the `title` a decoration gives it and adds a class of its own.
293
+ const ImageShape = createShape<HTMLImageElement>(function ImageShape({
294
+ nodeProps,
295
+ children,
296
+ className,
297
+ title,
298
+ ref,
299
+ ...props
300
+ }) {
301
+ return <img {...props} ref={ref} className={["picture", className].join(" ").trim()} />;
302
+ });
303
+ ```
304
+
305
+ On React 18, the component needs `forwardRef` to get it:
213
306
 
214
307
  ```tsx
215
308
  import { forwardRef } from "react";
216
309
  import { type ShapeComponentProps, createShape } from "@stesura/react-wordgard";
217
310
 
218
- // An image whose root drops the `title` a decoration gives it and adds a class of its own.
219
311
  const ImageShape = createShape(
220
312
  forwardRef<HTMLImageElement, ShapeComponentProps<HTMLImageElement>>(function ImageShape(
221
313
  { nodeProps, children, className, title, ...props },
@@ -227,13 +319,13 @@ const ImageShape = createShape(
227
319
  ```
228
320
 
229
321
  - Draw the element the decoration says (the node type's for a shape, `element` for a wrapper or
230
- mark), spread the props, and put `ref` on it. On React 19 `ref` is a prop too, so `forwardRef`
231
- isn't needed: `createShape<HTMLImageElement>(function ImageShape({ ref, ...props }) {…})`.
322
+ mark), spread the props, and put `ref` on it.
232
323
  - `className` holds Wordgard's classes (the selection's among them): join yours with them, or
233
324
  they're lost.
234
325
  - `style` holds Wordgard's style as an object: spread yours over it,
235
326
  `style={{ ...style, color: "red" }}`, or it replaces Wordgard's.
236
- - One way or the other: props spread onto `DecorationRoot` would apply twice.
327
+ - Use one or the other: spreading these props onto `<DecorationRoot>` applies Wordgard's
328
+ attributes twice (its classes appear twice).
237
329
  - `createWrapper` and `createMarkShape` components work the same way (`WrapperComponentProps`,
238
330
  `MarkComponentProps`).
239
331
 
@@ -274,153 +366,507 @@ const config = [Decoration.Tag.widget(Paragraph, "start", Pilcrow.of("¶"))];
274
366
 
275
367
  #### Hooks inside React decorations
276
368
 
277
- From a React shape's, wrapper's, mark's or widget's component:
278
-
279
- - `useNodePos()`: a function returning the position of what it draws, to call in handlers and
280
- effects.
281
- - `useStopEvent((wg, event) => stop)`: asked for events in the decoration's chrome, on its own
282
- elements (its root and the elements around its content element, unless the root is the content
283
- element) and in React widgets; never for those in its content. For an event it stops, Wordgard's
284
- handlers and keymaps are skipped and the browser's default goes ahead.
285
- - `useIgnoreMutation((wg, record) => ignore)`: Wordgard doesn't read the DOM mutations it claims,
286
- typing aside (Wordgard takes it from `beforeinput`).
287
- - `useIsNodeSelected()`, `useSelectNode(select, deselect?)`: the node selection.
288
- - `useMergedDOMRefs(...refs)`: one ref setting several, as for a root that's also the content
289
- element.
290
-
291
- ### A shape, a wrapper, or both
292
-
293
- - A node type's shape decides where attributes go: a mark's or an attribute decoration's `target`
294
- or `preferTarget` picks an element in it. `ImageAlt` and `ImageSize` go on a figure's `img`
295
- because the figure's shape has one. A React shape for the figure draws its `img` itself, so they
296
- land on its root instead: to keep them on the `img`, keep the type's shape and add React around
297
- it with a wrapper.
298
- - React chrome around a node, or around an element in its shape, is a React wrapper:
299
- `Decoration.Tag.wrapper(Figure, createWrapper(Frame), { target: "img" })` puts `Frame` around
300
- the figure's `img`, which keeps its `src`, `alt` and width.
301
- - A shape and wrappers register on the same node type and apply to all its nodes.
302
- - For example: an image is the type's `img`, with a React frame around it for a resize handle. A
303
- table is a React shape drawing `table > tbody > {children}`, with a wrapper for what goes outside
304
- the table (a scroll container, column handles). A node taller than a page is clipped by a wrapper
305
- whose component reads a store.
306
-
307
- That's a step more than a react-prosemirror node view, which draws all of its node's DOM. Wordgard
308
- applies decorations to the node type's shape before React draws, so an element decorations aim at
309
- must be in that shape. A wrapper adds React around it without taking it over.
310
-
311
- ### Node data that changes
312
-
313
- A component keeps its state while its node changes in place or moves within its parent, and
314
- remounts, with everything inside it, when its node is replaced. So how an editing action changes a
315
- node matters:
316
-
317
- - Data an action changes on a node (an image's size, a block's alignment, a callout's kind, a
318
- table's column widths) is a mark on its tag: a type made with `Mark.Type.define(name, { target })`,
319
- read with `node.tag.mark(Type)`, changed with `{ from: pos, add: Type.of(value) }` and
320
- `{ from: pos, remove: Type.of(value) }`. That's Wordgard's model: a mark change merges in
321
- collaboration and inverts in history, and the component re-renders. Wordgard's `ImageSize` is
322
- one; the demo's callout keeps its kind and title so (`demo/schema.ts`).
323
- - What identifies the node (an image's `src`, a heading's level) is its parameter. Change it by
324
- retagging, replacing the node's opening token by one of the same type, as Wordgard's commands
325
- do: `{ from: pos, to: pos + 1, insert: [Heading.of(2)] }`. The component re-renders, and its
326
- content keeps its components.
327
- - Replacing the node whole (`{ from, to, insert: [node] }`) or changing its type remounts it.
328
- - A move keeps the component, and the components inside, when the node stays in its parent: in one
329
- transaction, it's deleted and the same node object is inserted elsewhere in that parent. A copy
330
- (a new node, or the same one inserted while the original stays) mounts anew, and so does a node
331
- moved to another parent. React point widgets inside a moved node are drawn anew, as Wordgard
332
- redraws them. React moves the node's DOM, and a browser reloads an `<iframe>` on any
333
- DOM move. Wordgard's drag-and-drop rebuilds what it drops from HTML, so its moves remount.
334
-
335
- ProseMirror's `setNodeAttribute` and `setNodeMarkup` port to one of the first two.
369
+ A React shape's, wrapper's, mark's or widget's component can also use `useNodePos`,
370
+ `useStopEvent`, `useIgnoreMutation`, `useIsNodeSelected`, `useSelectNode` and
371
+ `useMergedDOMRefs`: see [Decoration hooks](#decoration-hooks).
372
+
373
+ ### Should I use a shape, a wrapper or both?
374
+
375
+ A shape draws the node; a wrapper adds React around it. Wordgard places attributes (a mark's or a
376
+ decoration's `target`) on the elements of the node type's shape before React draws, so a React
377
+ shape that draws more than its root gets the attributes aimed inside it on its root: a figure's
378
+ `alt` and width end up on the `<figure>`, not its `img`. Always match the shape's root to the node
379
+ type's element: `<DecorationRoot>` does it automatically, or draw the root by hand (see
380
+ [A root drawn by hand](#a-root-drawn-by-hand)). So `<DecorationRoot>` belongs at the root of a
381
+ shape, and chrome around it goes in an additional wrapper decoration.
382
+
383
+ For example, an image with a resize handle: the shape draws the `img`, which keeps its `src`, `alt`
384
+ and width, and the wrapper draws the handle.
385
+
386
+ ```tsx
387
+ import { Decoration } from "wordgard/editor";
388
+ import { Image } from "wordgard/types";
389
+ import { DecorationRoot, createShape, createWrapper, useStopEvent } from "@stesura/react-wordgard";
390
+
391
+ const ImageShape = createShape(function ImageShape() {
392
+ return <DecorationRoot className="image" />;
393
+ });
394
+
395
+ const Resizer = createWrapper(
396
+ function Resizer({ children }) {
397
+ // The handle's pointer events are its own, not Wordgard's.
398
+ useStopEvent(() => true);
399
+ return (
400
+ <DecorationRoot className="resizer">
401
+ {children}
402
+ <span className="resize-handle" aria-hidden="true" />
403
+ </DecorationRoot>
404
+ );
405
+ },
406
+ { element: "span" } // an image is inline
407
+ );
408
+
409
+ const config = [Decoration.Tag.shape(Image, ImageShape), Decoration.Tag.wrapper(Image, Resizer)];
410
+ ```
336
411
 
337
412
  ### Accessibility
338
413
 
339
- - Wordgard gives the editable element `role="textbox"` and `aria-multiline`. Name it with
340
- `Wordgard.contentAttributes.of({ "aria-label": "…" })`: in Wordgard 0.6.2, `Wordgard.label`
341
- puts the label on the editor's outer element, which leaves the textbox without a name.
342
- - A React decoration's chrome is read inline, with the document's text. Hide what's decorative
343
- (`aria-hidden` on icons and counters), and give every control a name (its text or
344
- `aria-label`).
345
- - Chrome is uneditable: a control in it needs to be focusable to be reached by keyboard, and
346
- `useStopEvent` for its events, so that Wordgard leaves its keys alone.
347
- - `DecorationRoot` puts the node's attributes on its root, an image's `alt` among them, in a React
348
- shape or a widget drawing a node. A root drawn by hand gets them as props: spread them onto it.
349
- - The tests run axe-core over editors with React shapes, marks and widgets
350
- (`src/__tests__/accessibility.browser.test.tsx`): nothing breaks a rule plain Wordgard keeps.
414
+ React decorations are your components: make them accessible as you would any other. What
415
+ react-wordgard adds:
416
+
417
+ - A control in a decoration's chrome needs `useStopEvent`, or Wordgard handles its keys.
418
+ - `DecorationRoot` puts the node's attributes on its root, an image's `alt` among them; a root
419
+ drawn by hand gets them as props, so spread them onto it.
420
+ - The tests run axe-core over editors with React shapes, marks and widgets: react-wordgard breaks
421
+ no rule plain Wordgard keeps.
351
422
 
352
423
  ### Limits
353
424
 
354
- - A React decoration must keep its root element. Moving `{children}` under another element
355
- remounts the content: the browser loses a selection or an IME composition in it.
356
- - In a textblock or a mark, the content element holds only the content: chrome there would be text
357
- that can't be told from the content's. Chrome next to a textblock's text (line numbers, a
358
- marker) is a tag widget at `"start"` or `"end"`, a React widget that may read the shape's
359
- context: `Decoration.Tag.widget(Paragraph, "start", LineNumber.of(null))`. Wordgard places the
360
- caret around it. In a node holding blocks, chrome may sit before or after
361
- the content in the same element, not between its nodes.
362
- - A React wrapper can't wrap a node a native widget draws (draw it with a React shape or widget),
363
- nor text.
364
- - An attribute code sets by hand (a resize observer's, a layout engine's) on a React decoration's
365
- root or structure stays through Wordgard's updates and the component's renders, until React sets
366
- that attribute itself: a `classList.add` goes when React sets `className` again (the node gets
367
- selected, a mark adds a class). On an element Wordgard draws, Wordgard's next update undoes it.
368
- Draw such state from decorations or the component's state instead; in a decoration's content,
369
- `useIgnoreMutation` keeps Wordgard from undoing a change it claims, until Wordgard redraws there.
370
- - `useEditorEventListener` takes these event types: keyboard (`keydown`, `keyup`, `keypress`),
371
- input (`beforeinput`, `input`, composition events), mouse (`mousedown`, `mouseup`, `mouseover`,
372
- `mouseout`, `click`, `dblclick`, `contextmenu`), `focus`, `blur`, clipboard (`copy`, `cut`,
373
- `paste`) and drag-and-drop events. Wordgard adds a handler per type when the editor is created;
374
- scroll-blocking and the most frequent events (touch, wheel, `mousemove`, pointer events) are left
375
- out.
376
- - A node drawn by a React widget has no element around it for its attributes, unlike plain
377
- Wordgard's: its component gets them as props, so a selection doesn't remount it.
378
- - Typing with marks before there's text draws a mark from its own type, not a
379
- `Decoration.Mark.shape` override; plain Wordgard does the same.
380
- - Wordgard 0.6.2's own typings need `skipLibCheck`.
425
+ - Keep `{children}` where it is: moving it under another element remounts the content, and the
426
+ browser loses a selection or an IME composition in it.
427
+ - A React wrapper can't wrap text, or a node a native widget draws.
428
+ - `useEditorEventListener` takes keyboard (`keydown`, `keyup`, `keypress`), input (`beforeinput`,
429
+ `input`, composition events), mouse (`mousedown`, `mouseup`, `mouseover`, `mouseout`, `click`,
430
+ `dblclick`, `contextmenu`), `focus`, `blur`, clipboard (`copy`, `cut`, `paste`) and
431
+ drag-and-drop events. Not touch, wheel, pointer events or `mousemove`.
432
+
433
+ ## API
434
+
435
+ Everything is exported from `@stesura/react-wordgard`. Editor hooks work under `<WordgardEditor>`,
436
+ decoration hooks in a React decoration's component; elsewhere, they throw.
437
+
438
+ ### Components
439
+
440
+ #### `WordgardEditor`
441
+
442
+ ```ts
443
+ type WordgardEditor = (
444
+ props: Omit<Wordgard.Spec, "parent"> & { children?: ReactNode }
445
+ ) => JSX.Element;
446
+ ```
447
+
448
+ Creates a Wordgard editor from the spec (`doc`, `config`, `selection`, `state`, `scrollTo`) and
449
+ provides it to its children. It draws no DOM of its own. The spec is read once, on mount: from then
450
+ on the editor owns its state (see
451
+ [Accessing the state outside the editor](#accessing-the-state-outside-the-editor)). In React's
452
+ StrictMode (development only), it creates its editor twice and drops one, which is never put in the
453
+ page.
454
+
455
+ #### `WordgardDoc`
456
+
457
+ ```ts
458
+ type WordgardDoc = (props: Omit<HTMLAttributes<HTMLDivElement>, "children">) => JSX.Element;
459
+ ```
460
+
461
+ Puts the editor in the page, inside a `div` taking the props, and renders the document through a
462
+ portal, in your React tree. It goes under its `<WordgardEditor>`, inside any other components, with
463
+ other children before or after it. One per editor: a second throws.
464
+
465
+ #### `DecorationRoot`
466
+
467
+ ```ts
468
+ type DecorationRoot = (
469
+ props: AllHTMLAttributes<HTMLElement> & { children?: ReactNode; ref?: Ref<Element> }
470
+ ) => JSX.Element;
471
+ ```
472
+
473
+ The root element of a React shape, wrapper, mark or widget: the tag the decoration says, with
474
+ Wordgard's attributes merged with the props. See [React decorations](#react-decorations).
475
+
476
+ ### Decoration factories
477
+
478
+ Call them at module level. Each returns Wordgard's own value, which you pass to its decorations;
479
+ the same component gives the same value. Details and examples:
480
+ [React decorations](#react-decorations).
481
+
482
+ #### `createShape`
483
+
484
+ ```ts
485
+ type createShape = <E extends Element = HTMLElement>(
486
+ component: ComponentType<ShapeComponentProps<E>>
487
+ ) => ReactShape;
488
+
489
+ type ReactShape = ((tag: Node.Tag) => Elt) & {
490
+ of: (element: string, options?: { atom?: boolean }) => Elt;
491
+ };
492
+ ```
493
+
494
+ A node's shape, for `Decoration.Tag.shape(NodeType, shape)`. `shape.of(element)` is the shape with
495
+ another root element, also for `Decoration.Point.shape`.
496
+
497
+ #### `createWrapper`
498
+
499
+ ```ts
500
+ type createWrapper = <E extends Element = HTMLElement>(
501
+ component: ComponentType<WrapperComponentProps<E>>,
502
+ options?: { element?: string } // "div" by default
503
+ ) => Elt;
504
+ ```
505
+
506
+ A wrapper, for `Decoration.Tag.wrapper` and `Decoration.Point.wrapper`.
507
+
508
+ #### `createMarkShape`
509
+
510
+ ```ts
511
+ type createMarkShape = <E extends Element = HTMLElement>(
512
+ component: ComponentType<MarkComponentProps<E>>,
513
+ options?: { element?: string } // "span" by default
514
+ ) => { element: string; attributes: Record<string, string> };
515
+ ```
516
+
517
+ A mark's shape, for `Decoration.Mark.shape(MarkType, shape)` or a mark type's own `shape`.
518
+
519
+ #### `createWidgetType`
520
+
521
+ ```ts
522
+ type createWidgetType = <Param, E extends Element = HTMLElement>(
523
+ spec: Omit<Widget.Spec<Param>, "render"> & {
524
+ component: ComponentType<WidgetComponentProps<Param, E>>;
525
+ element?: string; // "span" by default
526
+ }
527
+ ) => Widget.Type<Param>;
528
+ ```
529
+
530
+ A widget type whose widgets, `type.of(value)`, the component draws.
531
+
532
+ ### Editor hooks
533
+
534
+ #### `useWordgard`
535
+
536
+ ```ts
537
+ type useWordgard = () => Wordgard;
538
+ ```
539
+
540
+ The editor.
541
+
542
+ #### `useEditorState`
543
+
544
+ ```ts
545
+ type useEditorState = () => GardState;
546
+ ```
547
+
548
+ The state as of the editor's last flush, rendered in the same React commit as the document. The
549
+ component re-renders on every flush: for a part of the state, use `useEditorStateSelector`; in a
550
+ handler, `wg.state` is the latest.
551
+
552
+ #### `useEditorStateSelector`
553
+
554
+ ```ts
555
+ type useEditorStateSelector = <Result>(
556
+ selector: (state: GardState) => Result,
557
+ isEqual?: (a: Result, b: Result) => boolean
558
+ ) => Result;
559
+ ```
560
+
561
+ A part of the last flush's state, like Redux's `useSelector`: the component re-renders only when
562
+ the result changes, by `Object.is` or by `isEqual`. A selector building a new object or array each
563
+ time re-renders on every flush unless you pass `isEqual`.
564
+
565
+ ```tsx
566
+ export const SelectionInfo = () => {
567
+ const empty = useEditorStateSelector((state) => state.selection.empty);
568
+ return <output>{empty ? "Cursor" : "Selection"}</output>;
569
+ };
570
+ ```
571
+
572
+ #### `useEditorEffect`
573
+
574
+ ```ts
575
+ type useEditorEffect = (
576
+ effect: (wg: Wordgard) => void | (() => void),
577
+ dependencies?: DependencyList
578
+ ) => void;
579
+ ```
580
+
581
+ Runs `effect` once the editor's DOM is up to date: after the flush under way or pending, else in a
582
+ microtask, never inside React's commit. It may read layout and dispatch; what it sets shows a frame
583
+ later. For focus, follow-up dispatches and menus. The dependencies work as React's. Don't read the
584
+ editor's layout from a plain React effect: it can run inside Wordgard's update, before the editor's
585
+ view of the new DOM exists.
586
+
587
+ ```tsx
588
+ export const FocusOnMount = () => {
589
+ useEditorEffect((wg) => wg.focus(), []);
590
+ return null;
591
+ };
592
+ ```
593
+
594
+ #### `useEditorLayoutEffect`
595
+
596
+ ```ts
597
+ type useEditorLayoutEffect = <T>(effect: {
598
+ read: (wg: Wordgard) => T | undefined;
599
+ write: (value: T, wg: Wordgard) => void;
600
+ }) => void;
601
+ ```
602
+
603
+ Runs inside every editor update, without re-rendering: `read` in Wordgard's read phase, then
604
+ `write` with its value in the write phase, on the same paint as the document. For what moves with
605
+ the text (markers in the margin, pagination). It can't dispatch: Wordgard throws during its
606
+ update. A `read` returning `undefined` skips its write, and when no read asks for a write, none is
607
+ scheduled, which spares Wordgard measuring again. Reads run in the order the effects were
608
+ registered.
609
+
610
+ ```tsx
611
+ // A marker in the margin, level with the cursor.
612
+ export const CursorMarker = () => {
613
+ const ref = useRef<HTMLDivElement>(null);
614
+ useEditorLayoutEffect({
615
+ read: (wg) => wg.coordsAtPos(wg.state.selection.head).top - wg.dom.getBoundingClientRect().top,
616
+ write: (top) => {
617
+ if (ref.current) ref.current.style.top = `${top}px`;
618
+ },
619
+ });
620
+ return <div ref={ref} className="cursor-marker" style={{ position: "absolute" }} />;
621
+ };
622
+ ```
623
+
624
+ #### `useEditorEventCallback`
625
+
626
+ ```ts
627
+ type useEditorEventCallback = <Args extends unknown[], Result>(
628
+ callback: (wg: Wordgard, ...args: Args) => Result
629
+ ) => (...args: Args) => Result;
630
+ ```
631
+
632
+ A stable function calling the latest `callback` with the editor first, for event handlers. There,
633
+ `wg.state` is the latest state, dispatches not yet drawn included. See the `SaveButton` in
634
+ [Accessing the state outside the editor](#accessing-the-state-outside-the-editor).
635
+
636
+ #### `useEditorEventListener`
637
+
638
+ ```ts
639
+ type useEditorEventListener = <Type extends EditorEvent>(
640
+ type: Type,
641
+ handler: (wg: Wordgard, event: HTMLElementEventMap[Type]) => boolean | void
642
+ ) => void;
643
+ ```
644
+
645
+ Listens to the editor's DOM events of `type`, before Wordgard's handlers and keymaps. Returning
646
+ `true` handles the event: Wordgard calls `preventDefault` and skips its own handling. The event
647
+ types are listed under [Limits](#limits). Key events during a composition don't come; handling
648
+ `input`, `beforeinput` or a composition event skips Wordgard's own handling of it, so leave typing
649
+ to Wordgard.
650
+
651
+ ```tsx
652
+ export const EscapeToBlur = () => {
653
+ useEditorEventListener("keydown", (wg, event) => {
654
+ if (event.key != "Escape") return false;
655
+ wg.contentDOM.blur();
656
+ return true;
657
+ });
658
+ return null;
659
+ };
660
+ ```
661
+
662
+ ### Decoration hooks
663
+
664
+ From a React shape's, wrapper's, mark's or widget's component.
665
+
666
+ #### `useNodePos`
667
+
668
+ ```ts
669
+ type useNodePos = () => () => number;
670
+ ```
671
+
672
+ A function giving the position of what the decoration draws, to call in handlers and effects.
673
+
674
+ #### `useStopEvent`
675
+
676
+ ```ts
677
+ type useStopEvent = (stopEvent: (wg: Wordgard, event: Event) => boolean) => void;
678
+ ```
679
+
680
+ Asked for events in the decoration's chrome (its root and the elements around its content element,
681
+ unless the root is the content element) and in React widgets; never for those in its content. For
682
+ an event it stops, Wordgard's handlers and keymaps are skipped and the browser's default goes
683
+ ahead: for a control in the chrome, such as an input.
684
+
685
+ #### `useIgnoreMutation`
686
+
687
+ ```ts
688
+ type useIgnoreMutation = (ignore: (wg: Wordgard, mutation: MutationRecord) => boolean) => void;
689
+ ```
690
+
691
+ Wordgard doesn't read the DOM mutations `ignore` returns `true` for, typing aside (Wordgard takes
692
+ it from `beforeinput`). Mutations in chrome and React widgets never reach Wordgard anyway.
693
+
694
+ #### `useIsNodeSelected`
695
+
696
+ ```ts
697
+ type useIsNodeSelected = () => boolean;
698
+ ```
699
+
700
+ Whether the node the decoration draws is the editor's node selection.
701
+
702
+ #### `useSelectNode`
703
+
704
+ ```ts
705
+ type useSelectNode = (
706
+ selectNode: (wg: Wordgard) => void,
707
+ deselectNode?: (wg: Wordgard) => void
708
+ ) => void;
709
+ ```
710
+
711
+ Calls `selectNode` when the node becomes the node selection, and `deselectNode` when it stops being
712
+ it, once the editor's DOM is up to date (as `useEditorEffect`).
713
+
714
+ #### `useMergedDOMRefs`
715
+
716
+ ```ts
717
+ type useMergedDOMRefs = <Value>(...refs: (Ref<Value> | undefined)[]) => RefCallback<Value>;
718
+ ```
719
+
720
+ One ref setting several, as for a root that's also the content element (`ref` and
721
+ `contentDOMRef`).
722
+
723
+ ### Props types
724
+
725
+ #### `ShapeComponentProps`
726
+
727
+ ```ts
728
+ type ShapeComponentProps<E extends Element = HTMLElement> = AllHTMLAttributes<HTMLElement> & {
729
+ nodeProps: {
730
+ node: Node;
731
+ getPos: () => number;
732
+ contentDOMRef: RefCallback<Element>;
733
+ };
734
+ children: ReactNode;
735
+ } & RefAttributes<E>;
736
+ ```
737
+
738
+ A React shape's props: `nodeProps`, the content as `children`, and Wordgard's attributes and `ref`
739
+ for its root. The content goes in the `contentDOMRef` element, else in the root; in an atom,
740
+ `children` is empty and a `contentDOMRef` throws. With `<DecorationRoot>` as the root, the
741
+ attributes and `ref` are taken care of; a root drawn by hand spreads them onto itself (see
742
+ [A root drawn by hand](#a-root-drawn-by-hand)).
743
+
744
+ #### `WrapperComponentProps`
745
+
746
+ ```ts
747
+ type WrapperComponentProps<E extends Element = HTMLElement> = AllHTMLAttributes<HTMLElement> & {
748
+ nodeProps: {
749
+ node: Node;
750
+ getPos: () => number;
751
+ };
752
+ children: ReactNode;
753
+ } & RefAttributes<E>;
754
+ ```
755
+
756
+ A React wrapper's props: as a shape's, without `contentDOMRef`. `children` is what it wraps.
757
+
758
+ #### `MarkComponentProps`
759
+
760
+ ```ts
761
+ type MarkComponentProps<E extends Element = HTMLElement> = AllHTMLAttributes<HTMLElement> & {
762
+ markProps: {
763
+ mark: Mark;
764
+ getPos: () => number;
765
+ contentDOMRef: RefCallback<Element>;
766
+ };
767
+ children: ReactNode;
768
+ } & RefAttributes<E>;
769
+ ```
770
+
771
+ A React mark's props, for each run of an equal mark: `markProps.mark` is the document's mark.
772
+
773
+ #### `WidgetComponentProps`
774
+
775
+ ```ts
776
+ type WidgetComponentProps<Param, E extends Element = HTMLElement> = Omit<
777
+ AllHTMLAttributes<HTMLElement>,
778
+ "value"
779
+ > & { value: Param } & RefAttributes<E>;
780
+ ```
781
+
782
+ A React widget's props: its `value`, and, for a widget drawing a node, the node's attributes for
783
+ its root.
784
+
785
+ Also exported: `DecorationRootProps`, `ReactShape`, `WrapperOptions`, `MarkShapeOptions`,
786
+ `WidgetTypeSpec` and `EditorLayoutEffect`, the types used above.
381
787
 
382
788
  ## Migrating from react-prosemirror
383
789
 
384
790
  react-wordgard isn't a compatibility layer: it follows Wordgard's API, and most of a migration is
385
791
  moving from ProseMirror's concepts to Wordgard's (see Wordgard's
386
- [ProseMirror migration guide](https://wordgard.net/docs/prosemirror/)). What changes on the React
387
- side:
388
-
389
- | react-prosemirror | react-wordgard |
390
- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
391
- | `<ProseMirror state={…} dispatchTransaction={…}>` | `<WordgardEditor doc={…} config={…}>`, uncontrolled: the editor owns its state, changes go through `wg.dispatch` |
392
- | `<ProseMirrorDoc />` | `<WordgardDoc />` |
393
- | `plugins` prop | Wordgard extensions in `config`; to change them later, compartments or `GardState.appendConfig` |
394
- | `nodeViews` prop and node view components | Tag decorations: `Decoration.Tag.shape(NodeType, createShape(component))` for any node, atoms included; `createWrapper` with `Decoration.Tag.wrapper` |
395
- | `markViews` prop and mark view components | `createMarkShape` with `Decoration.Mark.shape` |
396
- | `widget()` decorations with a component | `Decoration.Point.widget(MyReactWidget.of(value))` |
397
- | `getPos` prop, `useNodePos` | `nodeProps.getPos`, `useNodePos()` (a function, not the position) |
398
- | `useStopEvent` | the same name, for events in a React decoration's chrome, on its own elements or in a React widget, not in its content |
399
- | `useIgnoreMutation` | the same name; chrome's and React widgets' mutations never reach Wordgard anyway |
400
- | `useEditorState`, `useEditorStateSelector` | the same names, with the state of the last flush |
401
- | `useEditorEffect` | the same name: runs after the editor's update; what it sets shows a frame later (below) |
402
- | `useIsNodeSelected`, `useSelectNode` | the same names |
403
- | `useMergedDOMRefs` | the same name |
404
- | `useEditorEventCallback` | the same name, getting the editor |
405
- | `useEditorEventListener` | the same name, before Wordgard's handlers and keymaps, for a fixed set of event types |
406
- | `useLayoutGroupEffect` | `useEditorEffect`; `useEditorLayoutEffect` to measure, then write on the same paint |
407
- | `reactKeys()` plugin | not needed: keys come from the changes Wordgard gives the view |
792
+ [ProseMirror migration guide](https://wordgard.net/docs/prosemirror/)). The biggest change: there
793
+ are no node views. A node is drawn by decorations, its shape and wrappers, and a React component
794
+ becomes one with `createShape` or `createWrapper`. What changes on the React side:
795
+
796
+ | react-prosemirror | react-wordgard |
797
+ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
798
+ | `<ProseMirror state={…} dispatchTransaction={…}>` | `<WordgardEditor doc={…} config={…}>`, uncontrolled: the editor owns its state, changes go through `wg.dispatch` |
799
+ | `<ProseMirrorDoc />` | `<WordgardDoc />` |
800
+ | `plugins` prop | Wordgard extensions in `config`; to change them later, compartments or `GardState.appendConfig` |
801
+ | `nodeViews` prop and node view components | **decorations**: no node views; a node's React component is a tag decoration, `Decoration.Tag.shape(NodeType, createShape(component))`, plus `Decoration.Tag.wrapper(NodeType, createWrapper(component))` for React around it |
802
+ | `markViews` prop and mark view components | `createMarkShape` with `Decoration.Mark.shape` |
803
+ | `widget()` decorations with a component | `Decoration.Point.widget(MyReactWidget.of(value))` |
804
+ | `getPos` prop, `useNodePos` | `nodeProps.getPos`, `useNodePos()` (a function, not the position) |
805
+ | `useStopEvent` | the same name, for events in a React decoration's chrome, on its own elements or in a React widget, not in its content |
806
+ | `useIgnoreMutation` | the same name; chrome's and React widgets' mutations never reach Wordgard anyway |
807
+ | `useEditorState`, `useEditorStateSelector` | the same names, with the state of the last flush |
808
+ | `useEditorEffect` | the same name: runs after the editor's update; what it sets shows a frame later (below) |
809
+ | `useIsNodeSelected`, `useSelectNode` | the same names |
810
+ | `useMergedDOMRefs` | the same name |
811
+ | `useEditorEventCallback` | the same name, getting the editor |
812
+ | `useEditorEventListener` | the same name, before Wordgard's handlers and keymaps, for a fixed set of event types |
813
+ | `useLayoutGroupEffect` | `useEditorEffect`; `useEditorLayoutEffect` to measure, then write on the same paint |
814
+ | `reactKeys()` plugin | not needed: keys come from the changes Wordgard gives the view |
408
815
 
409
816
  Not offered: `useIsComposingIn`, `useIsEditorStatic`.
410
817
 
411
818
  ### Migrating a node view
412
819
 
413
820
  A react-prosemirror node view keeps its body: the props keep their names (`nodeProps`,
414
- `contentDOMRef`, `children`, `ref`, the attributes). What changes:
821
+ `contentDOMRef`, `children`, `ref`, the attributes). A paragraph that deletes the selection when
822
+ clicked, in react-prosemirror:
823
+
824
+ ```tsx
825
+ function Paragraph({ children, nodeProps, ref, ...props }) {
826
+ const onClick = useEditorEventCallback((view) => view.dispatch(view.state.tr.deleteSelection()));
827
+
828
+ return (
829
+ <p {...props} ref={useMergedDOMRefs(ref, nodeProps.contentDOMRef)} onClick={onClick}>
830
+ {children}
831
+ </p>
832
+ );
833
+ }
834
+
835
+ const nodeViews = { paragraph: Paragraph };
836
+ ```
837
+
838
+ In react-wordgard:
839
+
840
+ ```tsx
841
+ import { deleteSelection } from "wordgard/command";
842
+ import { Decoration } from "wordgard/editor";
843
+ import { Paragraph } from "wordgard/types";
844
+ import { DecorationRoot, createShape, useEditorEventCallback } from "@stesura/react-wordgard";
845
+
846
+ const ParagraphShape = createShape(function ParagraphShape({ children }) {
847
+ const onClick = useEditorEventCallback((wg) => {
848
+ const spec = deleteSelection(wg.state);
849
+ if (spec) wg.dispatch(spec);
850
+ });
851
+
852
+ return <DecorationRoot onClick={onClick}>{children}</DecorationRoot>;
853
+ });
854
+
855
+ const config = [Decoration.Tag.shape(Paragraph, ParagraphShape)];
856
+ ```
857
+
858
+ What changes:
415
859
 
416
860
  - The props type: `ShapeComponentProps` (any node, atoms included), `WrapperComponentProps`,
417
861
  `MarkComponentProps`.
418
862
  - The root's tag isn't the component's choice: it's the node type's own element, what `toDOM`'s
419
- outer element was (see "React decorations" for why).
420
- - The root element, with its `{...props}` and `ref`, becomes `<DecorationRoot>`.
863
+ outer element was (see
864
+ [Should I use a shape, a wrapper or both?](#should-i-use-a-shape-a-wrapper-or-both) for why).
865
+ - The root element, with its `{...props}` and `ref`, becomes `<DecorationRoot>`. Without a
866
+ `contentDOMRef`, the content goes in it, so `useMergedDOMRefs` isn't needed.
421
867
  - `node.attrs` becomes the node's parameter (`node.tag.param`, `node.param` for a leaf) for what
422
- identifies it, and marks on its tag (`node.tag.mark(Type)`) for data changed in place: see "Node
423
- data that changes". `view` becomes `wg`, Wordgard's editor.
868
+ identifies it, and marks on its tag (`node.tag.mark(Type)`) for data changed in place. `view`
869
+ becomes `wg`, Wordgard's editor.
424
870
  - Registration: `Decoration.Tag.shape(NodeType, createShape(component))` in the editor's `config`
425
871
  instead of `nodeViews`.
426
872
  - Class names: `ProseMirror-selectednode` is `wg-selected-node`, a selected cell's class
@@ -430,11 +876,21 @@ A react-prosemirror node view keeps its body: the props keep their names (`nodeP
430
876
 
431
877
  ### Effects and timing
432
878
 
433
- In react-prosemirror React drives: `useEditorEffect` is a React layout effect that runs after the
879
+ In react-prosemirror, React drives: `useEditorEffect` is a React layout effect that runs after the
434
880
  view's update, inside React's commit, so it can read layout, dispatch, and what it sets shows on the
435
- same paint. In react-wordgard Wordgard drives: React commits inside Wordgard's update, and those
881
+ same paint. In react-wordgard, Wordgard drives: React commits inside Wordgard's update, and those
436
882
  properties split between two hooks.
437
883
 
884
+ In practice, React's own effects (`useLayoutEffect`, `useEffect`) can run inside Wordgard's update,
885
+ before Wordgard has caught up with the new DOM: reading layout there (`coordsAtPos`) throws ("ReactWordgard
886
+ can't update while React renders or commits"), and so does dispatching ("Cannot dispatch new
887
+ updates during the editor flush phase"). Use the editor
888
+ hooks instead. `useEditorEffect` runs after the update, so it can dispatch, but what it writes to
889
+ the DOM shows a frame late: a tooltip placed there trails the cursor. `useEditorLayoutEffect`
890
+ writes on the same paint, but can't dispatch. When porting, a react-prosemirror `useEditorEffect`
891
+ that positions something becomes `useEditorLayoutEffect`; one that dispatches or focuses stays
892
+ `useEditorEffect`.
893
+
438
894
  | | react-prosemirror `useEditorEffect` | `useEditorEffect` | `useEditorLayoutEffect` |
439
895
  | ----------------------------------------- | ----------------------------------- | ---------------------------------- | ---------------------------------------------------- |
440
896
  | Reads layout after the update | yes | yes | yes, in Wordgard's read phase |
@@ -443,6 +899,12 @@ properties split between two hooks.
443
899
  | Runs on every update without re-rendering | no | no | yes |
444
900
  | For | everything | menus, focus, follow-up dispatches | what moves with the text: margin markers, pagination |
445
901
 
902
+ ## AI coding agents
903
+
904
+ The package ships a skill for building editors with react-wordgard: `skills/react-wordgard/` (in
905
+ `node_modules/@stesura/react-wordgard/`). Copy that folder into your project's `.claude/skills/`,
906
+ or point your agent to its `SKILL.md`.
907
+
446
908
  ## Licence
447
909
 
448
910
  MIT, © 2026 Edoardo Caratti, see [LICENSE.txt](LICENSE.txt).