@canvist/react 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +1,41 @@
1
- {"description":"Placeholder package for @canvist/react","name":"@canvist/react","repository":"https://github.com/ifiokjr/canvist","version":"0.0.0"}
1
+ {
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/CHANGELOG.md DELETED
@@ -1,3 +0,0 @@
1
- ## 0.0.0
2
-
3
- - Placeholder release.
package/LICENSE DELETED
@@ -1,24 +0,0 @@
1
- This is free and unencumbered software released into the public domain.
2
-
3
- Anyone is free to copy, modify, publish, use, compile, sell, or
4
- distribute this software, either in source code form or as a compiled
5
- binary, for any purpose, commercial or non-commercial, and by any
6
- means.
7
-
8
- In jurisdictions that recognize copyright laws, the author or authors
9
- of this software dedicate any and all copyright interest in the
10
- software to the public domain. We make this dedication for the benefit
11
- of the public at large and to the detriment of our heirs and
12
- successors. We intend this dedication to be an overt act of
13
- relinquishment in perpetuity of all present and future rights to this
14
- software under copyright law.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
- IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
- OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
- ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
- OTHER DEALINGS IN THE SOFTWARE.
23
-
24
- For more information, please refer to <http://unlicense.org/>
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # @canvist/react
2
-
3
- This is a placeholder release published by monochange to bootstrap trusted publishing.