@stesura/react-wordgard 0.1.0 → 0.2.1

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 +12 -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 +2 -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
@@ -51,7 +51,8 @@ export const Editor = () => (
51
51
  `wg.dispatch(…)`, the configuration through Wordgard's compartments or
52
52
  `GardState.appendConfig`, never by re-rendering with new props.
53
53
  - `WordgardDoc` puts the editor in the page; one per editor. Components inside `WordgardEditor`
54
- (toolbars, menus, sidebars, next to `WordgardDoc`) can use the hooks: below.
54
+ (toolbars, menus, sidebars, next to `WordgardDoc`) can use the hooks: below. `WordgardEditor`
55
+ draws no DOM, so wrap as much of the page in it as needs the editor.
55
56
  - Don't create editors with `Wordgard.create` for a React app: react-wordgard's editor is made by
56
57
  `WordgardEditor`. In StrictMode (development) it creates the editor twice and drops one, which is
57
58
  never put in the page.
@@ -67,6 +68,7 @@ const Toolbar = () => {
67
68
  const bold = useEditorEventCallback((wg) => Command.dispatch(wg, toggleStrong));
68
69
  return (
69
70
  <button
71
+ disabled={empty}
70
72
  onMouseDown={(e) => {
71
73
  e.preventDefault();
72
74
  bold();
@@ -91,7 +93,74 @@ const Toolbar = () => {
91
93
  events, `mousedown`, `mouseup`, `mouseover`, `mouseout`, `click`, `dblclick`, `contextmenu`,
92
94
  `focus`, `blur`, `copy`, `cut`, `paste`, drag-and-drop events. Key events during an IME
93
95
  composition don't come. Handling `input`, `beforeinput` or a composition event skips Wordgard's
94
- own handling of it: leave typing to Wordgard.
96
+ own handling of it: leave typing to Wordgard. Touch, wheel, pointer events and `mousemove` aren't
97
+ offered (they'd make every editor's listener non-passive or busy): for those, add a
98
+ `Wordgard.domEventHandler` to `config`, or a listener on `wg.contentDOM` from `useEditorEffect`.
99
+ - `wg.state` in a callback is the latest state, dispatches not yet drawn included. To save the
100
+ document: `wg.state.doc.toJSON()`; to load it, `state.schema.docFromJSON(json)` as `doc`.
101
+
102
+ ### Reaching the editor from outside its `WordgardEditor`
103
+
104
+ The editor owns its state: there's no controlled `state` prop to lift into React, as with
105
+ react-prosemirror. The hooks throw outside `WordgardEditor`. Two ways:
106
+
107
+ 1. **Lift `WordgardEditor`** over everything that needs the editor (it draws no DOM; the document
108
+ goes where `WordgardDoc` is). Prefer this.
109
+ 2. **Share the editor through a store** where it can't wrap the component (another part of the
110
+ app, a portal): a component under `WordgardEditor` puts the editor in the store from
111
+ `useEditorEffect`, and the component outside reads it and uses `wg` directly (not the hooks).
112
+
113
+ ```tsx
114
+ import { atom, useAtomValue, useSetAtom } from "jotai";
115
+ import type { Wordgard } from "wordgard/editor";
116
+ import { basicSchema } from "wordgard/schema";
117
+ import { WordgardDoc, WordgardEditor, useEditorEffect } from "@stesura/react-wordgard";
118
+
119
+ const editorAtom = atom<Wordgard | null>(null);
120
+
121
+ const ShareEditor = () => {
122
+ const setEditor = useSetAtom(editorAtom);
123
+ useEditorEffect((wg) => {
124
+ setEditor(wg);
125
+ return () => setEditor(null);
126
+ }, []);
127
+ return null;
128
+ };
129
+
130
+ const SaveButton = () => {
131
+ const wg = useAtomValue(editorAtom);
132
+ const save = () => {
133
+ if (wg) localStorage.setItem("doc", JSON.stringify(wg.state.doc.toJSON()));
134
+ };
135
+ return (
136
+ <button onClick={save} disabled={!wg}>
137
+ Save
138
+ </button>
139
+ );
140
+ };
141
+
142
+ export const App = () => (
143
+ <>
144
+ <header>
145
+ <SaveButton />
146
+ </header>
147
+ <WordgardEditor doc="<p>Hello</p>" config={[basicSchema()]}>
148
+ <ShareEditor />
149
+ <WordgardDoc />
150
+ </WordgardEditor>
151
+ </>
152
+ );
153
+ ```
154
+
155
+ Any store works (Jotai here, Zustand, `useSyncExternalStore`). A sub-editor (a header's, a note's)
156
+ is its own `WordgardEditor`, often drawn inside a React shape of the main one; a toolbar acting on
157
+ whichever editor has focus keeps the focused one in such a store, set from
158
+ `useEditorEventListener("focus", …)` in each.
159
+
160
+ To react to changes without re-rendering (autosave, a debounced copy), add a
161
+ `Wordgard.updateListener.of((update) => …)` to `config`; `update.docChanged` and `update.state`
162
+ tell what changed. `config` is read once, so it should write to a store, not close over a
163
+ component's state.
95
164
 
96
165
  ### Effects and layout
97
166
 
@@ -194,7 +263,29 @@ puts both on the `<img>`. Check the parameter types of the node and mark types y
194
263
  one, type the root as its element): the element the decoration says, `{...props}` (Wordgard's
195
264
  attributes) and `ref` (`forwardRef` on React 18), never both ways at once. Join your classes with
196
265
  `className` (Wordgard's, the selection's among them), and spread your style over `style`
197
- (Wordgard's, as an object):
266
+ (Wordgard's, as an object). On React 19, `ref` is a prop:
267
+
268
+ ```tsx
269
+ const ImageShape = createShape<HTMLImageElement>(function ImageShape({
270
+ nodeProps,
271
+ children,
272
+ className,
273
+ style,
274
+ ref,
275
+ ...props
276
+ }) {
277
+ return (
278
+ <img
279
+ {...props}
280
+ ref={ref}
281
+ className={["picture", className].join(" ").trim()}
282
+ style={{ ...style, outline: "1px solid" }}
283
+ />
284
+ );
285
+ });
286
+ ```
287
+
288
+ On React 18, with `forwardRef`:
198
289
 
199
290
  ```tsx
200
291
  const ImageShape = createShape(
@@ -216,7 +307,7 @@ puts both on the `<img>`. Check the parameter types of the node and mark types y
216
307
 
217
308
  - **Only the outer element comes from the node type.** The elements inside a structure shape (a
218
309
  figure's `img`) are the component's to draw, from `nodeProps.node`; attributes aimed at them
219
- arrive on the root (see "A shape, a wrapper, or both").
310
+ arrive on the root (see "Should I use a shape, a wrapper or both?").
220
311
  - **Content goes where `contentDOMRef` points**, else in the root. `children` is the content (a
221
312
  wrapper's `children` is what it wraps; no `contentDOMRef` there).
222
313
  - **An atom** (a leaf, or a plot whose shape has no hole) has no content: `children` is empty,
@@ -283,13 +374,47 @@ disconnect? })` returns a real `Widget.Type`: its widgets are `Type.of(value)`.
283
374
  - A widget's component may read a context a React shape around it provides (line numbers beside a
284
375
  paragraph's text, from the paragraph's shape).
285
376
 
286
- ### A shape, a wrapper, or both
377
+ ### Should I use a shape, a wrapper or both?
378
+
379
+ A shape draws the node; a wrapper adds React around it. Unlike a ProseMirror node view, which picks
380
+ all its DOM, a node's outer element comes from its type's shape in the schema: Wordgard applies
381
+ marks and decorations to that shape (`target`, `preferTarget`) before React draws. A React shape
382
+ that draws more than its root gets the attributes aimed inside it on its root (a figure drawing its
383
+ own `img` gets `ImageAlt` and `ImageSize` on the `<figure>`). So the shape's root matches the node
384
+ type's element (`DecorationRoot`, or a root drawn by hand), and chrome around it is a wrapper:
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
+ // The shape draws the <img>, which keeps its src, alt and width.
392
+ const ImageShape = createShape(function ImageShape() {
393
+ return <DecorationRoot className="image" />;
394
+ });
395
+
396
+ // The wrapper draws the resize handle around it.
397
+ const Resizer = createWrapper(
398
+ function Resizer({ children }) {
399
+ useStopEvent(() => true);
400
+ return (
401
+ <DecorationRoot className="resizer">
402
+ {children}
403
+ <span className="resize-handle" aria-hidden="true" />
404
+ </DecorationRoot>
405
+ );
406
+ },
407
+ { element: "span" } // an image is inline
408
+ );
409
+
410
+ export const imageConfig = [
411
+ Decoration.Tag.shape(Image, ImageShape),
412
+ Decoration.Tag.wrapper(Image, Resizer),
413
+ ];
414
+ ```
287
415
 
288
- - The node type's shape decides where attributes go (`target`, `preferTarget`: `ImageAlt` and
289
- `ImageSize` land on a figure's `img`). Don't replace a shape just to add chrome.
290
- - React chrome around a node, or an element in its shape, is a **wrapper**:
291
- `Decoration.Tag.wrapper(Figure, createWrapper(Frame), { target: "img" })`. The `img` keeps its
292
- `src`, `alt` and width; `Frame` draws a resize handle beside `{children}`.
416
+ - A wrapper can also aim at an element inside the type's shape:
417
+ `Decoration.Tag.wrapper(Figure, createWrapper(Frame), { target: "img" })`.
293
418
  - A shape and wrappers on the same type apply to all its nodes. A table: a React shape drawing
294
419
  `table > tbody > {children}`, plus a wrapper for a scroll container around it.
295
420
  - **One `Decoration.Tag.shape` per node type** draws it: where several features draw the same type
@@ -342,6 +467,9 @@ parent.
342
467
 
343
468
  ## Migrating react-prosemirror node views
344
469
 
470
+ There are no node views: a node is drawn by decorations (its shape and wrappers), and a node view
471
+ component becomes one with `createShape` or `createWrapper`.
472
+
345
473
  | react-prosemirror | react-wordgard |
346
474
  | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
347
475
  | `<ProseMirror state dispatchTransaction>` | `<WordgardEditor doc config>`, uncontrolled; changes via `wg.dispatch` |