@stesura/react-wordgard 0.0.0-stage → 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 (111) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/LICENSE.txt +21 -0
  3. package/README.md +912 -2
  4. package/THIRD_PARTY_LICENSES.txt +93 -0
  5. package/dist/ReactWordgard.d.ts +13 -0
  6. package/dist/ReactWordgard.js +94 -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 +36 -0
  11. package/dist/components/DecorationRoot.d.ts +13 -0
  12. package/dist/components/DecorationRoot.js +31 -0
  13. package/dist/components/ErrorBoundary.d.ts +18 -0
  14. package/dist/components/ErrorBoundary.js +14 -0
  15. package/dist/components/SeparatorHackView.d.ts +2 -0
  16. package/dist/components/SeparatorHackView.js +4 -0
  17. package/dist/components/ShapeView.d.ts +12 -0
  18. package/dist/components/ShapeView.js +24 -0
  19. package/dist/components/TextNodeView.d.ts +5 -0
  20. package/dist/components/TextNodeView.js +4 -0
  21. package/dist/components/TrailingHackView.d.ts +2 -0
  22. package/dist/components/TrailingHackView.js +4 -0
  23. package/dist/components/WidgetView.d.ts +10 -0
  24. package/dist/components/WidgetView.js +32 -0
  25. package/dist/components/WordgardDoc.d.ts +9 -0
  26. package/dist/components/WordgardDoc.js +36 -0
  27. package/dist/components/WordgardEditor.d.ts +15 -0
  28. package/dist/components/WordgardEditor.js +17 -0
  29. package/dist/components/WordgardScope.d.ts +14 -0
  30. package/dist/components/WordgardScope.js +14 -0
  31. package/dist/components/WrapperView.d.ts +12 -0
  32. package/dist/components/WrapperView.js +23 -0
  33. package/dist/components/marks/MarkView.d.ts +11 -0
  34. package/dist/components/marks/MarkView.js +10 -0
  35. package/dist/components/marks/ReactMarkView.d.ts +10 -0
  36. package/dist/components/marks/ReactMarkView.js +30 -0
  37. package/dist/components/nodes/DefaultNodeView.d.ts +16 -0
  38. package/dist/components/nodes/DefaultNodeView.js +112 -0
  39. package/dist/components/nodes/DocNodeView.d.ts +7 -0
  40. package/dist/components/nodes/DocNodeView.js +5 -0
  41. package/dist/composition.d.ts +23 -0
  42. package/dist/composition.js +95 -0
  43. package/dist/contexts/DecorationContext.d.ts +15 -0
  44. package/dist/contexts/DecorationContext.js +35 -0
  45. package/dist/contexts/DecorationRootContext.d.ts +6 -0
  46. package/dist/contexts/DecorationRootContext.js +4 -0
  47. package/dist/contexts/EditorContext.d.ts +27 -0
  48. package/dist/contexts/EditorContext.js +87 -0
  49. package/dist/contexts/RenderStoreContext.d.ts +15 -0
  50. package/dist/contexts/RenderStoreContext.js +41 -0
  51. package/dist/contexts/editorEvents.d.ts +12 -0
  52. package/dist/contexts/editorEvents.js +92 -0
  53. package/dist/decorations/DecoIterator.d.ts +43 -0
  54. package/dist/decorations/DecoIterator.js +462 -0
  55. package/dist/decorations/ReactMarkType.d.ts +41 -0
  56. package/dist/decorations/ReactMarkType.js +47 -0
  57. package/dist/decorations/ReactShapeType.d.ts +65 -0
  58. package/dist/decorations/ReactShapeType.js +202 -0
  59. package/dist/decorations/ReactWidgetType.d.ts +29 -0
  60. package/dist/decorations/ReactWidgetType.js +67 -0
  61. package/dist/decorations/ReactWrapperType.d.ts +31 -0
  62. package/dist/decorations/ReactWrapperType.js +41 -0
  63. package/dist/decorations/iterDeco.d.ts +82 -0
  64. package/dist/decorations/iterDeco.js +406 -0
  65. package/dist/helpers/attributes.d.ts +2 -0
  66. package/dist/helpers/attributes.js +11 -0
  67. package/dist/helpers/index.d.ts +2 -0
  68. package/dist/helpers/index.js +2 -0
  69. package/dist/helpers/props.d.ts +8 -0
  70. package/dist/helpers/props.js +248 -0
  71. package/dist/helpers/refs.d.ts +6 -0
  72. package/dist/helpers/refs.js +20 -0
  73. package/dist/hooks/useEditorEffect.d.ts +10 -0
  74. package/dist/hooks/useEditorEffect.js +28 -0
  75. package/dist/hooks/useEditorEventCallback.d.ts +8 -0
  76. package/dist/hooks/useEditorEventCallback.js +17 -0
  77. package/dist/hooks/useEditorEventListener.d.ts +10 -0
  78. package/dist/hooks/useEditorEventListener.js +26 -0
  79. package/dist/hooks/useEditorLayoutEffect.d.ts +15 -0
  80. package/dist/hooks/useEditorLayoutEffect.js +34 -0
  81. package/dist/hooks/useEditorState.d.ts +6 -0
  82. package/dist/hooks/useEditorState.js +12 -0
  83. package/dist/hooks/useEditorStateSelector.d.ts +7 -0
  84. package/dist/hooks/useEditorStateSelector.js +11 -0
  85. package/dist/hooks/useIgnoreMutation.d.ts +6 -0
  86. package/dist/hooks/useIgnoreMutation.js +19 -0
  87. package/dist/hooks/useIsNodeSelected.d.ts +2 -0
  88. package/dist/hooks/useIsNodeSelected.js +5 -0
  89. package/dist/hooks/useMergedDOMRefs.d.ts +6 -0
  90. package/dist/hooks/useMergedDOMRefs.js +18 -0
  91. package/dist/hooks/useNodePos.d.ts +4 -0
  92. package/dist/hooks/useNodePos.js +7 -0
  93. package/dist/hooks/useSelectNode.d.ts +6 -0
  94. package/dist/hooks/useSelectNode.js +25 -0
  95. package/dist/hooks/useStopEvent.d.ts +8 -0
  96. package/dist/hooks/useStopEvent.js +21 -0
  97. package/dist/hooks/useWordgard.d.ts +2 -0
  98. package/dist/hooks/useWordgard.js +3 -0
  99. package/dist/index.d.ts +21 -0
  100. package/dist/index.js +21 -0
  101. package/dist/reactKeys.d.ts +39 -0
  102. package/dist/reactKeys.js +188 -0
  103. package/dist/tile.d.ts +32 -0
  104. package/dist/tile.js +1319 -0
  105. package/dist/viewRoots.d.ts +2 -0
  106. package/dist/viewRoots.js +7 -0
  107. package/dist/wordgardInternals.d.ts +211 -0
  108. package/dist/wordgardInternals.js +82 -0
  109. package/package.json +119 -4
  110. package/skills/react-wordgard/SKILL.md +525 -0
  111. package/skills/react-wordgard/references/wordgard.md +189 -0
package/README.md CHANGED
@@ -1,3 +1,913 @@
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.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.
62
+
63
+ ## Install
64
+
65
+ ```sh
66
+ npm install @stesura/react-wordgard wordgard react react-dom
67
+ ```
68
+
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)).
76
+
77
+ ## Usage
78
+
79
+ ```tsx
80
+ import { useState } from "react";
81
+ import { Decoration, Wordgard } from "wordgard/editor";
82
+ import { basicSchema, image } from "wordgard/schema";
83
+ import { Image } from "wordgard/types";
84
+ import {
85
+ DecorationRoot,
86
+ WordgardDoc,
87
+ WordgardEditor,
88
+ createShape,
89
+ useEditorStateSelector,
90
+ } from "@stesura/react-wordgard";
91
+
92
+ const ImageShape = createShape(function ImageShape() {
93
+ const [hover, setHover] = useState(false);
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.
97
+ <DecorationRoot
98
+ style={hover ? { outline: "2px solid" } : undefined}
99
+ onMouseEnter={() => setHover(true)}
100
+ onMouseLeave={() => setHover(false)}
101
+ />
102
+ );
103
+ });
104
+
105
+ const WordCount = () => {
106
+ const words = useEditorStateSelector((state) => state.doc.textContent().split(/\s+/).length);
107
+ return <output>{words} words</output>;
108
+ };
109
+
110
+ export const Editor = () => {
111
+ return (
112
+ <WordgardEditor
113
+ doc='<p>Hello <strong>world</strong> <img src="/cat.png" alt="A cat"></p>'
114
+ config={[
115
+ basicSchema(),
116
+ image(),
117
+ Decoration.Tag.shape(Image, ImageShape),
118
+ Wordgard.contentAttributes.of({ "aria-label": "Document" }),
119
+ ]}
120
+ >
121
+ <WordCount />
122
+ <WordgardDoc className="editor" />
123
+ </WordgardEditor>
124
+ );
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
+ );
162
+ ```
163
+
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
+ ```
216
+
217
+ ### React decorations
218
+
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.
224
+
225
+ ```tsx
226
+ import { Decoration } from "wordgard/editor";
227
+ import { Blockquote, Link } from "wordgard/types";
228
+ import { DecorationRoot, createMarkShape, createShape } from "@stesura/react-wordgard";
229
+
230
+ // A blockquote drawn as a callout: a decorative icon, then the content.
231
+ const CalloutShape = createShape(function Callout({ nodeProps: { contentDOMRef }, children }) {
232
+ return (
233
+ <DecorationRoot>
234
+ <span aria-hidden="true">💡</span>
235
+ <div className="body" ref={contentDOMRef}>
236
+ {children}
237
+ </div>
238
+ </DecorationRoot>
239
+ );
240
+ });
241
+
242
+ // A link, its target the mark's value.
243
+ const LinkMark = createMarkShape(
244
+ function LinkMark({ markProps: { mark, contentDOMRef }, children }) {
245
+ return (
246
+ <DecorationRoot href={String(mark.value)}>
247
+ <span ref={contentDOMRef}>{children}</span>
248
+ </DecorationRoot>
249
+ );
250
+ },
251
+ { element: "a" }
252
+ );
253
+
254
+ const config = [
255
+ Decoration.Tag.shape(Blockquote, CalloutShape),
256
+ Decoration.Mark.shape(Link, LinkMark),
257
+ ];
258
+ ```
259
+
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`.
273
+ - `createWrapper(component, { element? })`, for `Decoration.Tag.wrapper` and
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`.
280
+
281
+ #### A root drawn by hand
282
+
283
+ `DecorationRoot` applies all of Wordgard's attributes. To decide what reaches the root (drop one,
284
+ change one, join Wordgard's classes your way) or to type the root as its element, the component
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:
306
+
307
+ ```tsx
308
+ import { forwardRef } from "react";
309
+ import { type ShapeComponentProps, createShape } from "@stesura/react-wordgard";
310
+
311
+ const ImageShape = createShape(
312
+ forwardRef<HTMLImageElement, ShapeComponentProps<HTMLImageElement>>(function ImageShape(
313
+ { nodeProps, children, className, title, ...props },
314
+ ref
315
+ ) {
316
+ return <img {...props} ref={ref} className={["picture", className].join(" ").trim()} />;
317
+ })
318
+ );
319
+ ```
320
+
321
+ - Draw the element the decoration says (the node type's for a shape, `element` for a wrapper or
322
+ mark), spread the props, and put `ref` on it.
323
+ - `className` holds Wordgard's classes (the selection's among them): join yours with them, or
324
+ they're lost.
325
+ - `style` holds Wordgard's style as an object: spread yours over it,
326
+ `style={{ ...style, color: "red" }}`, or it replaces Wordgard's.
327
+ - Use one or the other: spreading these props onto `<DecorationRoot>` applies Wordgard's
328
+ attributes twice (its classes appear twice).
329
+ - `createWrapper` and `createMarkShape` components work the same way (`WrapperComponentProps`,
330
+ `MarkComponentProps`).
331
+
332
+ #### React widgets
333
+
334
+ ```tsx
335
+ import { Decoration } from "wordgard/editor";
336
+ import { Paragraph } from "wordgard/types";
337
+ import {
338
+ DecorationRoot,
339
+ type WidgetComponentProps,
340
+ createWidgetType,
341
+ } from "@stesura/react-wordgard";
342
+
343
+ // A pilcrow at the start of every paragraph.
344
+ const Pilcrow = createWidgetType({
345
+ component: function Pilcrow({ value }: WidgetComponentProps<string>) {
346
+ return <DecorationRoot aria-hidden="true">{value}</DecorationRoot>;
347
+ },
348
+ });
349
+
350
+ const config = [Decoration.Tag.widget(Paragraph, "start", Pilcrow.of("¶"))];
351
+ ```
352
+
353
+ - `createWidgetType(spec)`, for a widget: at a position (`Decoration.Point.widget`), beside a node
354
+ (`Decoration.Tag.widget`), inside an `Elt` shape. It takes `Widget.define`'s spec, `component`
355
+ in place of `render` (`eq`, `editable`, `inFlow`, `propagateEvent`, `connect`, `disconnect` keep
356
+ their meaning), and returns a real `Widget.Type`: its widgets are `type.of(value)`.
357
+ - The component gets the `value`. Its root is a `DecorationRoot`, a `span` unless the spec gives
358
+ another `element` (`createWidgetType({ component, element: "div" })`): Wordgard doesn't look at
359
+ a widget's element, so it's the widget's choice. Or the component draws its root by hand, with
360
+ the `ref` it gets (`forwardRef` on React 18), as a shape's can.
361
+ - Widgets are uneditable unless their spec sets `editable`, as in Wordgard.
362
+ - A widget can also draw a node, as Wordgard allows:
363
+ `Decoration.Tag.shape(nodeType, (tag) => type.of(tag.param))`. `DecorationRoot` puts the node's
364
+ attributes on its root (a root drawn by hand gets them as props). A React shape is the usual
365
+ way.
366
+
367
+ #### Hooks inside React decorations
368
+
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
+ ```
411
+
412
+ ### Accessibility
413
+
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.
422
+
423
+ ### Limits
424
+
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.
787
+
788
+ ## Migrating from react-prosemirror
789
+
790
+ react-wordgard isn't a compatibility layer: it follows Wordgard's API, and most of a migration is
791
+ moving from ProseMirror's concepts to Wordgard's (see Wordgard's
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 |
815
+
816
+ Not offered: `useIsComposingIn`, `useIsEditorStatic`.
817
+
818
+ ### Migrating a node view
819
+
820
+ A react-prosemirror node view keeps its body: the props keep their names (`nodeProps`,
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:
859
+
860
+ - The props type: `ShapeComponentProps` (any node, atoms included), `WrapperComponentProps`,
861
+ `MarkComponentProps`.
862
+ - The root's tag isn't the component's choice: it's the node type's own element, what `toDOM`'s
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.
867
+ - `node.attrs` becomes the node's parameter (`node.tag.param`, `node.param` for a leaf) for what
868
+ identifies it, and marks on its tag (`node.tag.mark(Type)`) for data changed in place. `view`
869
+ becomes `wg`, Wordgard's editor.
870
+ - Registration: `Decoration.Tag.shape(NodeType, createShape(component))` in the editor's `config`
871
+ instead of `nodeViews`.
872
+ - Class names: `ProseMirror-selectednode` is `wg-selected-node`, a selected cell's class
873
+ `wg-selected-cell`.
874
+ - `useNodePos()` returns a function giving the position, not the position.
875
+ - `innerDecorations` has no equivalent.
876
+
877
+ ### Effects and timing
878
+
879
+ In react-prosemirror, React drives: `useEditorEffect` is a React layout effect that runs after the
880
+ view's update, inside React's commit, so it can read layout, dispatch, and what it sets shows on the
881
+ same paint. In react-wordgard, Wordgard drives: React commits inside Wordgard's update, and those
882
+ properties split between two hooks.
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
+
894
+ | | react-prosemirror `useEditorEffect` | `useEditorEffect` | `useEditorLayoutEffect` |
895
+ | ----------------------------------------- | ----------------------------------- | ---------------------------------- | ---------------------------------------------------- |
896
+ | Reads layout after the update | yes | yes | yes, in Wordgard's read phase |
897
+ | May dispatch | yes | yes | no |
898
+ | What it sets shows on the same paint | yes | no, a frame later | yes |
899
+ | Runs on every update without re-rendering | no | no | yes |
900
+ | For | everything | menus, focus, follow-up dispatches | what moves with the text: margin markers, pagination |
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
+
908
+ ## Licence
909
+
910
+ MIT, © 2026 Edoardo Caratti, see [LICENSE.txt](LICENSE.txt).
911
+
912
+ Includes code from react-prosemirror (© Handle with Care Collective and The New
913
+ York Times Company) and Wordgard (© Marijn Haverbeke); their notices are in [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt).