@stesura/react-wordgard 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.
Files changed (109) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE.txt +21 -0
  3. package/README.md +450 -2
  4. package/THIRD_PARTY_LICENSES.txt +93 -0
  5. package/dist/ReactWordgard.d.ts +13 -0
  6. package/dist/ReactWordgard.js +96 -0
  7. package/dist/changes.d.ts +14 -0
  8. package/dist/changes.js +163 -0
  9. package/dist/components/ChildNodeViews.d.ts +8 -0
  10. package/dist/components/ChildNodeViews.js +38 -0
  11. package/dist/components/DecorationRoot.d.ts +7 -0
  12. package/dist/components/DecorationRoot.js +28 -0
  13. package/dist/components/ErrorBoundary.d.ts +18 -0
  14. package/dist/components/ErrorBoundary.js +16 -0
  15. package/dist/components/SeparatorHackView.d.ts +2 -0
  16. package/dist/components/SeparatorHackView.js +5 -0
  17. package/dist/components/ShapeView.d.ts +12 -0
  18. package/dist/components/ShapeView.js +26 -0
  19. package/dist/components/TextNodeView.d.ts +5 -0
  20. package/dist/components/TextNodeView.js +5 -0
  21. package/dist/components/TrailingHackView.d.ts +2 -0
  22. package/dist/components/TrailingHackView.js +5 -0
  23. package/dist/components/WidgetView.d.ts +10 -0
  24. package/dist/components/WidgetView.js +35 -0
  25. package/dist/components/WordgardDoc.d.ts +4 -0
  26. package/dist/components/WordgardDoc.js +34 -0
  27. package/dist/components/WordgardEditor.d.ts +7 -0
  28. package/dist/components/WordgardEditor.js +13 -0
  29. package/dist/components/WrapperView.d.ts +12 -0
  30. package/dist/components/WrapperView.js +26 -0
  31. package/dist/components/marks/MarkView.d.ts +11 -0
  32. package/dist/components/marks/MarkView.js +13 -0
  33. package/dist/components/marks/ReactMarkView.d.ts +10 -0
  34. package/dist/components/marks/ReactMarkView.js +32 -0
  35. package/dist/components/nodes/DefaultNodeView.d.ts +16 -0
  36. package/dist/components/nodes/DefaultNodeView.js +116 -0
  37. package/dist/components/nodes/DocNodeView.d.ts +7 -0
  38. package/dist/components/nodes/DocNodeView.js +6 -0
  39. package/dist/composition.d.ts +23 -0
  40. package/dist/composition.js +97 -0
  41. package/dist/contexts/DecorationContext.d.ts +15 -0
  42. package/dist/contexts/DecorationContext.js +38 -0
  43. package/dist/contexts/DecorationRootContext.d.ts +6 -0
  44. package/dist/contexts/DecorationRootContext.js +6 -0
  45. package/dist/contexts/EditorContext.d.ts +26 -0
  46. package/dist/contexts/EditorContext.js +87 -0
  47. package/dist/contexts/RenderStoreContext.d.ts +15 -0
  48. package/dist/contexts/RenderStoreContext.js +43 -0
  49. package/dist/contexts/editorEvents.d.ts +12 -0
  50. package/dist/contexts/editorEvents.js +95 -0
  51. package/dist/decorations/DecoIterator.d.ts +43 -0
  52. package/dist/decorations/DecoIterator.js +462 -0
  53. package/dist/decorations/ReactMarkType.d.ts +30 -0
  54. package/dist/decorations/ReactMarkType.js +48 -0
  55. package/dist/decorations/ReactShapeType.d.ts +49 -0
  56. package/dist/decorations/ReactShapeType.js +204 -0
  57. package/dist/decorations/ReactWidgetType.d.ts +19 -0
  58. package/dist/decorations/ReactWidgetType.js +69 -0
  59. package/dist/decorations/ReactWrapperType.d.ts +22 -0
  60. package/dist/decorations/ReactWrapperType.js +44 -0
  61. package/dist/decorations/iterDeco.d.ts +82 -0
  62. package/dist/decorations/iterDeco.js +409 -0
  63. package/dist/helpers/attributes.d.ts +2 -0
  64. package/dist/helpers/attributes.js +11 -0
  65. package/dist/helpers/index.d.ts +2 -0
  66. package/dist/helpers/index.js +2 -0
  67. package/dist/helpers/props.d.ts +8 -0
  68. package/dist/helpers/props.js +248 -0
  69. package/dist/helpers/refs.d.ts +6 -0
  70. package/dist/helpers/refs.js +20 -0
  71. package/dist/hooks/useEditorEffect.d.ts +3 -0
  72. package/dist/hooks/useEditorEffect.js +24 -0
  73. package/dist/hooks/useEditorEventCallback.d.ts +2 -0
  74. package/dist/hooks/useEditorEventCallback.js +13 -0
  75. package/dist/hooks/useEditorEventListener.d.ts +3 -0
  76. package/dist/hooks/useEditorEventListener.js +26 -0
  77. package/dist/hooks/useEditorLayoutEffect.d.ts +6 -0
  78. package/dist/hooks/useEditorLayoutEffect.js +38 -0
  79. package/dist/hooks/useEditorState.d.ts +1 -0
  80. package/dist/hooks/useEditorState.js +10 -0
  81. package/dist/hooks/useEditorStateSelector.d.ts +2 -0
  82. package/dist/hooks/useEditorStateSelector.js +6 -0
  83. package/dist/hooks/useIgnoreMutation.d.ts +2 -0
  84. package/dist/hooks/useIgnoreMutation.js +18 -0
  85. package/dist/hooks/useIsNodeSelected.d.ts +1 -0
  86. package/dist/hooks/useIsNodeSelected.js +6 -0
  87. package/dist/hooks/useMergedDOMRefs.d.ts +2 -0
  88. package/dist/hooks/useMergedDOMRefs.js +17 -0
  89. package/dist/hooks/useNodePos.d.ts +1 -0
  90. package/dist/hooks/useNodePos.js +8 -0
  91. package/dist/hooks/useSelectNode.d.ts +2 -0
  92. package/dist/hooks/useSelectNode.js +24 -0
  93. package/dist/hooks/useStopEvent.d.ts +2 -0
  94. package/dist/hooks/useStopEvent.js +20 -0
  95. package/dist/hooks/useWordgard.d.ts +1 -0
  96. package/dist/hooks/useWordgard.js +4 -0
  97. package/dist/index.d.ts +20 -0
  98. package/dist/index.js +20 -0
  99. package/dist/reactKeys.d.ts +39 -0
  100. package/dist/reactKeys.js +188 -0
  101. package/dist/tile.d.ts +32 -0
  102. package/dist/tile.js +1324 -0
  103. package/dist/viewRoots.d.ts +2 -0
  104. package/dist/viewRoots.js +8 -0
  105. package/dist/wordgardInternals.d.ts +211 -0
  106. package/dist/wordgardInternals.js +85 -0
  107. package/package.json +119 -4
  108. package/skills/react-wordgard/SKILL.md +397 -0
  109. package/skills/react-wordgard/references/wordgard.md +189 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ ## 0.1.0 (2026-11-03)
2
+
3
+ ### New features
4
+
5
+ First release: React draws Wordgard's documents inside Wordgard's own update, in the app's React tree, and Wordgard keeps handling input, selection, composition and decorations.
6
+
7
+ Requires `wordgard` ~0.6.2 and React 18 or 19.
8
+
9
+ `WordgardEditor` creates an editor and provides it to its children; `WordgardDoc` puts it in the page and renders its document through a portal, so the app's contexts reach what it draws.
10
+
11
+ `createShape` makes a node's shape (`Decoration.Tag.shape`) a React component that renders the node's content as `children` and may change the elements around it on its own; its root is the node type's own outer element, and `shape.of(element, { atom? })` gives one for `Decoration.Point.shape` or another root.
12
+
13
+ A React shape can draw an atom (an image, a mention, a plot drawn as one), its component getting the node and its attributes, everything it draws chrome.
14
+
15
+ `createWrapper` does the same for `Decoration.Tag.wrapper` and `Decoration.Point.wrapper`, and `createMarkShape` for a mark's shape (`Decoration.Mark.shape`, or a mark type's own), each taking the component and an `element` for its root.
16
+
17
+ `createWidgetType` defines a Wordgard widget type drawn by a React component, usable wherever Wordgard takes a widget, its value and element types inferred from the component.
18
+
19
+ `DecorationRoot` draws the root of a React shape, wrapper, mark or widget: the tag its decoration says, Wordgard's attributes and the root's ref, merged with the component's own props, with no `forwardRef` needed on React 18.
20
+
21
+ A component can draw its root by hand instead, from the ref and Wordgard's attributes it gets as props, its style as an object to merge with its own.
22
+
23
+ `createShape`, `createWrapper` and `createMarkShape` give the same value for the same component (and `element`).
24
+
25
+ A component keeps its state through a mark change on its node, a retag of the node's opening token, and a move within its parent (the same node object deleted and inserted in one transaction); replacing the node, changing its type or moving it to another parent remounts it.
26
+
27
+ `useWordgard`, `useEditorState` and `useEditorStateSelector` read the editor and its state as of its last update.
28
+
29
+ `useEditorEffect` runs after the editor's update; `useEditorLayoutEffect` reads and writes within it, on the same paint as the document, a read returning `undefined` skipping its write; `useEditorEventCallback` gives a stable callback getting the editor.
30
+
31
+ `useEditorEventListener` handles the editor's DOM events before Wordgard's own handlers and keymaps.
32
+
33
+ `useNodePos`, `useStopEvent`, `useIgnoreMutation`, `useIsNodeSelected`, `useSelectNode` and `useMergedDOMRefs` give React decorations what react-prosemirror's node views had; `useStopEvent` is asked for events in a decoration's chrome, on its own elements and in React widgets, never in its content.
34
+
35
+ The package ships a skill for AI coding agents, `skills/react-wordgard/SKILL.md`, on building editors with Wordgard and react-wordgard, with a reference on Wordgard's model for ProseMirror developers.
package/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Edoardo Caratti
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,451 @@
1
- # Temporary Holding Version
1
+ # react-wordgard
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > **Under construction.** react-wordgard isn't ready for production yet, and its API may still
4
+ > change.
5
+
6
+ react-wordgard stays on 0.x for as long as Wordgard does: it follows Wordgard's API and reaches
7
+ into its internals, so it can't be more stable than Wordgard. Its 1.0 comes after Wordgard's.
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.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ npm install @stesura/react-wordgard wordgard react react-dom
23
+ ```
24
+
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.
33
+
34
+ ## Usage
35
+
36
+ ```tsx
37
+ import { useState } from "react";
38
+ import { Decoration, Wordgard } from "wordgard/editor";
39
+ // `image()` adds images to the schema; `Image` is their node type, which decorations name.
40
+ import { basicSchema, image } from "wordgard/schema";
41
+ import { Image } from "wordgard/types";
42
+ import {
43
+ DecorationRoot,
44
+ WordgardDoc,
45
+ WordgardEditor,
46
+ createShape,
47
+ useEditorStateSelector,
48
+ } from "@stesura/react-wordgard";
49
+
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
+ const ImageShape = createShape(function ImageShape() {
54
+ const [hover, setHover] = useState(false);
55
+ return (
56
+ <DecorationRoot
57
+ style={hover ? { outline: "2px solid" } : undefined}
58
+ onMouseEnter={() => setHover(true)}
59
+ onMouseLeave={() => setHover(false)}
60
+ />
61
+ );
62
+ });
63
+
64
+ function WordCount() {
65
+ const words = useEditorStateSelector((state) => state.doc.textContent().split(/\s+/).length);
66
+ return <output>{words} words</output>;
67
+ }
68
+
69
+ export function Editor() {
70
+ return (
71
+ <WordgardEditor
72
+ doc='<p>Hello <strong>world</strong> <img src="/cat.png" alt="A cat"></p>'
73
+ config={[
74
+ basicSchema(),
75
+ image(),
76
+ Decoration.Tag.shape(Image, ImageShape),
77
+ Wordgard.contentAttributes.of({ "aria-label": "Document" }),
78
+ ]}
79
+ >
80
+ <WordCount />
81
+ <WordgardDoc className="editor" />
82
+ </WordgardEditor>
83
+ );
84
+ }
85
+ ```
86
+
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`.
122
+
123
+ ### React decorations
124
+
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:
130
+
131
+ ```tsx
132
+ import { Decoration } from "wordgard/editor";
133
+ import { Blockquote, Link } from "wordgard/types";
134
+ import { DecorationRoot, createMarkShape, createShape } from "@stesura/react-wordgard";
135
+
136
+ // A blockquote drawn as a callout: a decorative icon, then the content.
137
+ const CalloutShape = createShape(function Callout({ nodeProps: { contentDOMRef }, children }) {
138
+ return (
139
+ <DecorationRoot>
140
+ <span aria-hidden="true">💡</span>
141
+ <div className="body" ref={contentDOMRef}>
142
+ {children}
143
+ </div>
144
+ </DecorationRoot>
145
+ );
146
+ });
147
+
148
+ // A link, its target the mark's value.
149
+ const LinkMark = createMarkShape(
150
+ function LinkMark({ markProps: { mark, contentDOMRef }, children }) {
151
+ return (
152
+ <DecorationRoot href={String(mark.value)}>
153
+ <span ref={contentDOMRef}>{children}</span>
154
+ </DecorationRoot>
155
+ );
156
+ },
157
+ { element: "a" }
158
+ );
159
+
160
+ const config = [
161
+ Decoration.Tag.shape(Blockquote, CalloutShape),
162
+ Decoration.Mark.shape(Link, LinkMark),
163
+ ];
164
+ ```
165
+
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.
185
+ - `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.
207
+
208
+ #### A root drawn by hand
209
+
210
+ `DecorationRoot` applies all of Wordgard's attributes. To decide what reaches the root (drop one,
211
+ 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:
213
+
214
+ ```tsx
215
+ import { forwardRef } from "react";
216
+ import { type ShapeComponentProps, createShape } from "@stesura/react-wordgard";
217
+
218
+ // An image whose root drops the `title` a decoration gives it and adds a class of its own.
219
+ const ImageShape = createShape(
220
+ forwardRef<HTMLImageElement, ShapeComponentProps<HTMLImageElement>>(function ImageShape(
221
+ { nodeProps, children, className, title, ...props },
222
+ ref
223
+ ) {
224
+ return <img {...props} ref={ref} className={["picture", className].join(" ").trim()} />;
225
+ })
226
+ );
227
+ ```
228
+
229
+ - 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 }) {…})`.
232
+ - `className` holds Wordgard's classes (the selection's among them): join yours with them, or
233
+ they're lost.
234
+ - `style` holds Wordgard's style as an object: spread yours over it,
235
+ `style={{ ...style, color: "red" }}`, or it replaces Wordgard's.
236
+ - One way or the other: props spread onto `DecorationRoot` would apply twice.
237
+ - `createWrapper` and `createMarkShape` components work the same way (`WrapperComponentProps`,
238
+ `MarkComponentProps`).
239
+
240
+ #### React widgets
241
+
242
+ ```tsx
243
+ import { Decoration } from "wordgard/editor";
244
+ import { Paragraph } from "wordgard/types";
245
+ import {
246
+ DecorationRoot,
247
+ type WidgetComponentProps,
248
+ createWidgetType,
249
+ } from "@stesura/react-wordgard";
250
+
251
+ // A pilcrow at the start of every paragraph.
252
+ const Pilcrow = createWidgetType({
253
+ component: function Pilcrow({ value }: WidgetComponentProps<string>) {
254
+ return <DecorationRoot aria-hidden="true">{value}</DecorationRoot>;
255
+ },
256
+ });
257
+
258
+ const config = [Decoration.Tag.widget(Paragraph, "start", Pilcrow.of("¶"))];
259
+ ```
260
+
261
+ - `createWidgetType(spec)`, for a widget: at a position (`Decoration.Point.widget`), beside a node
262
+ (`Decoration.Tag.widget`), inside an `Elt` shape. It takes `Widget.define`'s spec, `component`
263
+ in place of `render` (`eq`, `editable`, `inFlow`, `propagateEvent`, `connect`, `disconnect` keep
264
+ their meaning), and returns a real `Widget.Type`: its widgets are `type.of(value)`.
265
+ - The component gets the `value`. Its root is a `DecorationRoot`, a `span` unless the spec gives
266
+ another `element` (`createWidgetType({ component, element: "div" })`): Wordgard doesn't look at
267
+ a widget's element, so it's the widget's choice. Or the component draws its root by hand, with
268
+ the `ref` it gets (`forwardRef` on React 18), as a shape's can.
269
+ - Widgets are uneditable unless their spec sets `editable`, as in Wordgard.
270
+ - A widget can also draw a node, as Wordgard allows:
271
+ `Decoration.Tag.shape(nodeType, (tag) => type.of(tag.param))`. `DecorationRoot` puts the node's
272
+ attributes on its root (a root drawn by hand gets them as props). A React shape is the usual
273
+ way.
274
+
275
+ #### Hooks inside React decorations
276
+
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.
336
+
337
+ ### Accessibility
338
+
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.
351
+
352
+ ### Limits
353
+
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`.
381
+
382
+ ## Migrating from react-prosemirror
383
+
384
+ react-wordgard isn't a compatibility layer: it follows Wordgard's API, and most of a migration is
385
+ 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 |
408
+
409
+ Not offered: `useIsComposingIn`, `useIsEditorStatic`.
410
+
411
+ ### Migrating a node view
412
+
413
+ A react-prosemirror node view keeps its body: the props keep their names (`nodeProps`,
414
+ `contentDOMRef`, `children`, `ref`, the attributes). What changes:
415
+
416
+ - The props type: `ShapeComponentProps` (any node, atoms included), `WrapperComponentProps`,
417
+ `MarkComponentProps`.
418
+ - 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>`.
421
+ - `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.
424
+ - Registration: `Decoration.Tag.shape(NodeType, createShape(component))` in the editor's `config`
425
+ instead of `nodeViews`.
426
+ - Class names: `ProseMirror-selectednode` is `wg-selected-node`, a selected cell's class
427
+ `wg-selected-cell`.
428
+ - `useNodePos()` returns a function giving the position, not the position.
429
+ - `innerDecorations` has no equivalent.
430
+
431
+ ### Effects and timing
432
+
433
+ In react-prosemirror React drives: `useEditorEffect` is a React layout effect that runs after the
434
+ 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
436
+ properties split between two hooks.
437
+
438
+ | | react-prosemirror `useEditorEffect` | `useEditorEffect` | `useEditorLayoutEffect` |
439
+ | ----------------------------------------- | ----------------------------------- | ---------------------------------- | ---------------------------------------------------- |
440
+ | Reads layout after the update | yes | yes | yes, in Wordgard's read phase |
441
+ | May dispatch | yes | yes | no |
442
+ | What it sets shows on the same paint | yes | no, a frame later | yes |
443
+ | Runs on every update without re-rendering | no | no | yes |
444
+ | For | everything | menus, focus, follow-up dispatches | what moves with the text: margin markers, pagination |
445
+
446
+ ## Licence
447
+
448
+ MIT, © 2026 Edoardo Caratti, see [LICENSE.txt](LICENSE.txt).
449
+
450
+ Includes code from react-prosemirror (© Handle with Care Collective and The New
451
+ York Times Company) and Wordgard (© Marijn Haverbeke); their notices are in [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt).
@@ -0,0 +1,93 @@
1
+ react-wordgard includes code from the projects below. Their copyright and
2
+ license notices are reproduced here, as their licenses require.
3
+
4
+
5
+ ================================================================================
6
+ react-prosemirror
7
+ https://github.com/handlewithcarecollective/react-prosemirror
8
+ ================================================================================
9
+
10
+ react-wordgard is derived from react-prosemirror, maintained by Handle with
11
+ Care Collective and originally created at The New York Times Company. Files
12
+ that came from it keep their "Copyright (c) The New York Times Company"
13
+ headers.
14
+
15
+ react-prosemirror was released under the Apache License 2.0 until 2026-10-02
16
+ (commit 1827d3a) and under the MIT License since then. Both notices follow.
17
+
18
+ --- MIT License (since 2026-10-02) ---------------------------------------------
19
+
20
+ MIT License
21
+
22
+ Copyright (c) 2026 Handle with Care Collective
23
+
24
+ Permission is hereby granted, free of charge, to any person obtaining a copy
25
+ of this software and associated documentation files (the "Software"), to deal
26
+ in the Software without restriction, including without limitation the rights
27
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
28
+ copies of the Software, and to permit persons to whom the Software is
29
+ furnished to do so, subject to the following conditions:
30
+
31
+ The above copyright notice and this permission notice shall be included in all
32
+ copies or substantial portions of the Software.
33
+
34
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
39
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
40
+ SOFTWARE.
41
+
42
+ --- Apache License 2.0 (until 2026-10-02) --------------------------------------
43
+
44
+ Copyright (c) 2022 The New York Times Company
45
+ Copyright (c) 2025 Handle with Care Collective
46
+
47
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
48
+ this library except in compliance with the License. You may obtain a copy of the
49
+ License at
50
+
51
+ www.apache.org/licenses/LICENSE-2.0
52
+
53
+ Unless required by applicable law or agreed to in writing, software distributed
54
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
55
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
56
+ specific language governing permissions and limitations under the License.
57
+
58
+
59
+ ================================================================================
60
+ Wordgard
61
+ https://wordgard.net
62
+ ================================================================================
63
+
64
+ test/wordgard/ holds copies of Wordgard's browser tests and their helpers;
65
+ demo/main.tsx is adapted from Wordgard's demo (demo/demo.ts), and the
66
+ IME emulation in src/__tests__/tile.browser.test.ts from its `compose` test
67
+ helper (test/webtest-composition.ts). src/decorations/DecoIterator.ts copies
68
+ Wordgard's decoration walker (src/editor/decoration.ts), with changes noted in
69
+ the file. src/tile.ts copies `setUneditable` from Wordgard's tile tree
70
+ (src/editor/tile.ts) and follows its tile builder there.
71
+
72
+ MIT License
73
+
74
+ Copyright (C) 2026 by Marijn Haverbeke <marijn@haverbeke.berlin>.
75
+
76
+ Permission is hereby granted, free of charge, to any person obtaining
77
+ a copy of this software and associated documentation files (the
78
+ "Software"), to deal in the Software without restriction, including
79
+ without limitation the rights to use, copy, modify, merge, publish,
80
+ distribute, sublicense, and/or sell copies of the Software, and to
81
+ permit persons to whom the Software is furnished to do so, subject to
82
+ the following conditions:
83
+
84
+ The above copyright notice and this permission notice shall be
85
+ included in all copies or substantial portions of the Software.
86
+
87
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
88
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
89
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
90
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
91
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
92
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
93
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,13 @@
1
+ import { Wordgard } from "wordgard/editor";
2
+ import { ReactDocTile } from "./tile.js";
3
+ import { type DOMObserver, DocTile } from "./wordgardInternals.js";
4
+ export declare class ReactWordgard extends Wordgard {
5
+ docTile: DocTile;
6
+ observer: DOMObserver;
7
+ scheduleFlush: () => void;
8
+ static create: (spec: Wordgard.Spec) => ReactWordgard;
9
+ static host: (spec: Wordgard.Spec) => ReactWordgard;
10
+ static DocTile: typeof ReactDocTile;
11
+ constructor(spec: Wordgard.Spec, hosted?: boolean);
12
+ flush(): void;
13
+ }