@canvist/react 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.
package/dist/mod.d.ts ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * React bindings for the canvist canvas editor.
3
+ *
4
+ * `CanvistEditor` renders an editor; `useCanvistEditor` gives you the
5
+ * editor itself, and `useEditorState` re-renders a component when
6
+ * something it reads from the editor changes. `BubbleMenu` and
7
+ * `FloatingMenu` keep their children next to the selection. Commands, queries, and
8
+ * plugin APIs are typed from the extensions and plugins you pass, as in
9
+ * `@canvist/web`.
10
+ *
11
+ * ```tsx
12
+ * function Notes() {
13
+ * const { ref, editor } = useCanvistEditor({
14
+ * content: { format: "markdown", input: "Hello **world**" },
15
+ * });
16
+ * const bold = useEditorState(editor, (current) => current.can.toggleBold(), false);
17
+ *
18
+ * return (
19
+ * <>
20
+ * <button disabled={!bold} onClick={() => editor?.commands.toggleBold()}>Bold</button>
21
+ * <div ref={ref} />
22
+ * </>
23
+ * );
24
+ * }
25
+ * ```
26
+ *
27
+ * @module
28
+ */
29
+ import { type Editor, type EditorOptions, type Extensions, type Plugins, type StarterKit } from "@canvist/web";
30
+ import { type MenuOptions } from "@canvist/web/menus";
31
+ import { type CSSProperties, type ReactElement, type ReactNode, type RefCallback } from "react";
32
+ /** What {@link useCanvistEditor} returns. */
33
+ export interface CanvistEditorHandle<E extends Extensions, P extends Plugins> {
34
+ /** Attach to the element the editor renders into. */
35
+ readonly ref: RefCallback<HTMLElement>;
36
+ /** The editor, once it has loaded; `null` before that and after unmounting. */
37
+ readonly editor: Editor<E, P> | null;
38
+ }
39
+ /**
40
+ * Create an editor in the element `ref` is attached to, and destroy it
41
+ * when the element goes away. `options` are read when the editor is
42
+ * created; later changes to them have no effect.
43
+ */
44
+ export declare function useCanvistEditor<const E extends Extensions = StarterKit, const P extends Plugins = []>(options?: EditorOptions<E, P>): CanvistEditorHandle<E, P>;
45
+ /**
46
+ * Read `select(editor)` and re-render whenever it changes, after any edit
47
+ * or selection change. `select` must return a primitive or a value that
48
+ * stays the same object while nothing changed, as with
49
+ * `useSyncExternalStore`. Until the editor exists, the value is
50
+ * `fallback`.
51
+ */
52
+ export declare function useEditorState<E extends Extensions, P extends Plugins, T>(editor: Editor<E, P> | null, select: (editor: Editor<E, P>) => T, fallback: T): T;
53
+ /**
54
+ * Props for {@link CanvistEditor}: the editor's options, read once except
55
+ * `editable` and `placeholder`, which follow the props, and the element's.
56
+ */
57
+ export interface CanvistEditorProps<E extends Extensions, P extends Plugins> extends EditorOptions<E, P> {
58
+ readonly className?: string;
59
+ readonly style?: CSSProperties;
60
+ /**
61
+ * Server-rendered HTML (`.cv-doc` markup from canvist's renderer) to
62
+ * show until the editor takes over. The editor reads the document from
63
+ * it when `content` is not given.
64
+ */
65
+ readonly html?: string;
66
+ /** Called once the editor has loaded. */
67
+ readonly onReady?: (editor: Editor<E, P>) => void;
68
+ /** Called after every change to the document or selection. */
69
+ readonly onUpdate?: (editor: Editor<E, P>) => void;
70
+ }
71
+ /** A canvist editor. Give it a width; without a height it grows with its document. */
72
+ export declare function CanvistEditor<const E extends Extensions = StarterKit, const P extends Plugins = []>(props: CanvistEditorProps<E, P>): ReactElement;
73
+ /** Props for {@link BubbleMenu} and {@link FloatingMenu}. */
74
+ export interface MenuProps<E extends Extensions, P extends Plugins> extends MenuOptions<E, P> {
75
+ /** The editor to follow; the menu stays hidden while it is `null`. */
76
+ readonly editor: Editor<E, P> | null;
77
+ readonly className?: string;
78
+ readonly style?: CSSProperties;
79
+ readonly children?: ReactNode;
80
+ }
81
+ /**
82
+ * A menu over selected text, as `bubbleMenu` from `@canvist/web/menus`
83
+ * places it. Pressing inside it keeps the editor's focus and selection.
84
+ */
85
+ export declare function BubbleMenu<E extends Extensions, P extends Plugins>(props: MenuProps<E, P>): ReactElement;
86
+ /**
87
+ * A menu beside the caret on an empty line, as `floatingMenu` from
88
+ * `@canvist/web/menus` places it.
89
+ */
90
+ export declare function FloatingMenu<E extends Extensions, P extends Plugins>(props: MenuProps<E, P>): ReactElement;
package/dist/mod.js ADDED
@@ -0,0 +1,162 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * React bindings for the canvist canvas editor.
4
+ *
5
+ * `CanvistEditor` renders an editor; `useCanvistEditor` gives you the
6
+ * editor itself, and `useEditorState` re-renders a component when
7
+ * something it reads from the editor changes. `BubbleMenu` and
8
+ * `FloatingMenu` keep their children next to the selection. Commands, queries, and
9
+ * plugin APIs are typed from the extensions and plugins you pass, as in
10
+ * `@canvist/web`.
11
+ *
12
+ * ```tsx
13
+ * function Notes() {
14
+ * const { ref, editor } = useCanvistEditor({
15
+ * content: { format: "markdown", input: "Hello **world**" },
16
+ * });
17
+ * const bold = useEditorState(editor, (current) => current.can.toggleBold(), false);
18
+ *
19
+ * return (
20
+ * <>
21
+ * <button disabled={!bold} onClick={() => editor?.commands.toggleBold()}>Bold</button>
22
+ * <div ref={ref} />
23
+ * </>
24
+ * );
25
+ * }
26
+ * ```
27
+ *
28
+ * @module
29
+ */
30
+ import { createEditor, } from "@canvist/web";
31
+ import { bubbleMenu, floatingMenu, } from "@canvist/web/menus";
32
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore, } from "react";
33
+ /**
34
+ * Create an editor in the element `ref` is attached to, and destroy it
35
+ * when the element goes away. `options` are read when the editor is
36
+ * created; later changes to them have no effect.
37
+ */
38
+ export function useCanvistEditor(options = {}) {
39
+ const [element, setElement] = useState(null);
40
+ const [editor, setEditor] = useState(null);
41
+ const initial = useRef(options);
42
+ useEffect(() => {
43
+ if (element === null) {
44
+ return;
45
+ }
46
+ const mount = { live: true, editor: null };
47
+ void create(element, initial.current, mount, setEditor);
48
+ return () => {
49
+ mount.live = false;
50
+ mount.editor?.destroy();
51
+ setEditor(null);
52
+ };
53
+ }, [element]);
54
+ return { ref: setElement, editor };
55
+ }
56
+ /**
57
+ * Read `select(editor)` and re-render whenever it changes, after any edit
58
+ * or selection change. `select` must return a primitive or a value that
59
+ * stays the same object while nothing changed, as with
60
+ * `useSyncExternalStore`. Until the editor exists, the value is
61
+ * `fallback`.
62
+ */
63
+ export function useEditorState(editor, select, fallback) {
64
+ const subscribe = useCallback((listener) => editor?.subscribe(listener) ?? noop, [editor]);
65
+ return useSyncExternalStore(subscribe, () => editor === null ? fallback : select(editor), () => fallback);
66
+ }
67
+ /** A canvist editor. Give it a width; without a height it grows with its document. */
68
+ export function CanvistEditor(props) {
69
+ const { className, style, html, onReady, onUpdate, ...options } = props;
70
+ const { ref, editor } = useCanvistEditor(options);
71
+ const callbacks = useRef({ onReady, onUpdate });
72
+ const { editable = true, placeholder = "" } = options;
73
+ useEffect(() => {
74
+ editor?.setEditable(editable);
75
+ }, [editor, editable]);
76
+ useEffect(() => {
77
+ editor?.setPlaceholder(placeholder);
78
+ }, [editor, placeholder]);
79
+ useEffect(() => {
80
+ callbacks.current = { onReady, onUpdate };
81
+ });
82
+ useEffect(() => {
83
+ if (editor === null) {
84
+ return;
85
+ }
86
+ callbacks.current.onReady?.(editor);
87
+ return editor.subscribe(() => callbacks.current.onUpdate?.(editor));
88
+ }, [editor]);
89
+ return (_jsx("div", { ref: ref, className: className, style: style,
90
+ // Set once: React leaves the editor's elements alone afterwards.
91
+ dangerouslySetInnerHTML: html === undefined
92
+ ? undefined
93
+ : { __html: html } }));
94
+ }
95
+ /**
96
+ * A menu over selected text, as `bubbleMenu` from `@canvist/web/menus`
97
+ * places it. Pressing inside it keeps the editor's focus and selection.
98
+ */
99
+ export function BubbleMenu(props) {
100
+ return _jsx(FollowingMenu, { ...props, attach: bubbleMenu });
101
+ }
102
+ /**
103
+ * A menu beside the caret on an empty line, as `floatingMenu` from
104
+ * `@canvist/web/menus` places it.
105
+ */
106
+ export function FloatingMenu(props) {
107
+ return _jsx(FollowingMenu, { ...props, attach: floatingMenu });
108
+ }
109
+ function FollowingMenu(props) {
110
+ const { editor, attach, className, style, children, ...options } = props;
111
+ const element = useRef(null);
112
+ const menu = useRef(null);
113
+ // The latest rule, without re-attaching each time a new closure renders.
114
+ const shouldShow = useRef(options.shouldShow);
115
+ shouldShow.current = options.shouldShow;
116
+ const custom = options.shouldShow !== undefined;
117
+ const { placement, offset, padding } = options;
118
+ useEffect(() => {
119
+ if (editor === null || element.current === null) {
120
+ return;
121
+ }
122
+ const attached = attach(editor, element.current, {
123
+ ...(placement === undefined ? {} : { placement }),
124
+ ...(offset === undefined ? {} : { offset }),
125
+ ...(padding === undefined ? {} : { padding }),
126
+ ...(custom
127
+ ? { shouldShow: (current) => shouldShow.current?.(current) ?? false }
128
+ : {}),
129
+ });
130
+ menu.current = attached;
131
+ return () => {
132
+ attached.destroy();
133
+ menu.current = null;
134
+ };
135
+ }, [editor, attach, placement, offset, padding, custom]);
136
+ // A re-render can change what `shouldShow` or the children decide.
137
+ useEffect(() => {
138
+ menu.current?.update();
139
+ });
140
+ return (_jsx("div", { ref: element, className: className, style: style, hidden: true, children: children }));
141
+ }
142
+ /**
143
+ * Create the editor for `mount`. An effect cleaned up while the module
144
+ * loaded (as React's Strict Mode does once) destroys what arrives late.
145
+ */
146
+ async function create(element, options, mount, created) {
147
+ try {
148
+ const editor = await createEditor(element, options);
149
+ if (!mount.live) {
150
+ editor.destroy();
151
+ return;
152
+ }
153
+ mount.editor = editor;
154
+ created(editor);
155
+ }
156
+ catch (error) {
157
+ if (mount.live) {
158
+ reportError(error);
159
+ }
160
+ }
161
+ }
162
+ function noop() { }
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
- "name": "@canvist/react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@canvist/react",
3
+ "version": "0.2.0",
4
+ "description": "React bindings for the canvist canvas editor: a component, and hooks typed from the extensions you register.",
5
+ "license": "Unlicense",
6
+ "homepage": "https://github.com/canvist/canvist/tree/main/packages/canvist-react#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/canvist/canvist.git",
10
+ "directory": "packages/canvist-react"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/canvist/canvist/issues",
14
+ "email": "support@canvist.dev"
15
+ },
16
+ "type": "module",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/mod.d.ts",
20
+ "default": "./dist/mod.js"
21
+ }
22
+ },
23
+ "files": ["dist"],
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "check:types": "tsc --noEmit",
27
+ "build": "tsc -p tsconfig.build.json",
28
+ "prepack": "npm run build"
29
+ },
30
+ "dependencies": {
31
+ "@canvist/web": "^0.2.0"
32
+ },
33
+ "peerDependencies": {
34
+ "react": "^18.3.0 || ^19.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/react": "^19.0.0",
38
+ "react": "^19.0.0",
39
+ "typescript": "^7.0.2"
40
+ }
41
+ }
package/readme.md ADDED
@@ -0,0 +1,61 @@
1
+ <p align="center">
2
+ <a href="https://github.com/canvist/canvist">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/canvist/canvist/main/assets/brand/vector/lockup-dark.svg">
5
+ <img width="220" src="https://raw.githubusercontent.com/canvist/canvist/main/assets/brand/vector/lockup.svg" alt="canvist" />
6
+ </picture>
7
+ </a>
8
+ </p>
9
+
10
+ <p align="center">
11
+ <a href="https://github.com/canvist/canvist/blob/main/docs/src/quick-start/react.md"><strong>Quick start</strong></a> ·
12
+ <a href="https://github.com/canvist/canvist/blob/main/docs/src/readme.md"><strong>Docs</strong></a> ·
13
+ <a href="https://github.com/canvist/canvist/blob/main/docs/src/reference/contributing.md"><strong>Contributing</strong></a> ·
14
+ <a href="https://github.com/canvist/canvist"><strong>canvist</strong></a>
15
+ </p>
16
+
17
+ <p align="center">
18
+ <a href="https://www.npmjs.com/package/@canvist/react"><img src="https://img.shields.io/npm/v/%40canvist%2Freact?color=3B2CF5" alt="npm version" /></a>
19
+ <a href="https://github.com/canvist/canvist/actions/workflows/web.yml"><img src="https://github.com/canvist/canvist/actions/workflows/web.yml/badge.svg?branch=main" alt="web status" /></a>
20
+ <a href="https://unlicense.org"><img src="https://img.shields.io/badge/license-Unlicense-3B2CF5" alt="Unlicense" /></a>
21
+ </p>
22
+
23
+ # @canvist/react
24
+
25
+ React bindings for [`@canvist/web`](../canvist-web), the canvist canvas editor. Commands, queries, and plugin APIs are typed from the extensions and plugins you pass.
26
+
27
+ ```sh
28
+ npm install @canvist/react
29
+ ```
30
+
31
+ ```tsx
32
+ import { useCanvistEditor, useEditorState } from "@canvist/react";
33
+
34
+ function Notes() {
35
+ const { ref, editor } = useCanvistEditor({ label: "Notes" });
36
+ const isBold = useEditorState(
37
+ editor,
38
+ (current) => current.query.isBold(),
39
+ false,
40
+ );
41
+
42
+ return (
43
+ <>
44
+ <button
45
+ aria-pressed={isBold}
46
+ onClick={() => editor?.commands.toggleBold()}
47
+ >
48
+ Bold
49
+ </button>
50
+ <div ref={ref} />
51
+ </>
52
+ );
53
+ }
54
+ ```
55
+
56
+ - `useCanvistEditor(options)` creates the editor in the element `ref` is attached to and destroys it on unmount, Strict Mode included.
57
+ - `useEditorState(editor, select, fallback)` re-renders when `select(editor)` changes, through `useSyncExternalStore`.
58
+ - `<CanvistEditor />` takes the editor's options as props, plus `onReady`, `onUpdate`, and `html` for server-rendered HTML to hydrate. `editable` and `placeholder` follow their props.
59
+ - `<BubbleMenu editor={editor}>` and `<FloatingMenu editor={editor}>` keep their children next to the selection.
60
+
61
+ See [Quick start: React](../../docs/src/quick-start/react.md).
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
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.