docstar-editor 0.1.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.
- package/README.md +108 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +168 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +5 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# docstar-editor
|
|
2
|
+
|
|
3
|
+
A minimal, block-based Markdown editor for React, built on [BlockNote](https://www.blocknotejs.org). Works standalone with zero setup, and opts into real-time collaboration + server-side sync when you connect it to a Hocuspocus-compatible server.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i docstar-editor
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Import the base styles once in your app:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import "docstar-editor/styles.css";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Note for bundled apps**: `@blocknote/core` resolves a `prosemirror-view` version that's missing an export it needs. Add this to your own `package.json` to pin a working version for this package's dependency subtree:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
"overrides": {
|
|
21
|
+
"docstar-editor": {
|
|
22
|
+
"prosemirror-view": "1.33.9"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
(npm workspaces/pnpm/yarn have their own equivalent — `overrides`/`resolutions` — under the same idea: scope the pin to `docstar-editor`'s own dependency tree, not your whole app.)
|
|
28
|
+
|
|
29
|
+
## Standalone usage
|
|
30
|
+
|
|
31
|
+
No login, no server required:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { DocstarEditor } from "docstar-editor";
|
|
35
|
+
|
|
36
|
+
export function Page() {
|
|
37
|
+
return (
|
|
38
|
+
<DocstarEditor
|
|
39
|
+
defaultMarkdown="# Hello\n\nStart writing..."
|
|
40
|
+
onChange={(markdown) => console.log(markdown)}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Real-time collaboration
|
|
47
|
+
|
|
48
|
+
Connect the editor to your own Hocuspocus-compatible server (see [`doc-rtc`](../doc-rtc) for a reference implementation) for live multi-user sync with persistence:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { DocstarEditor } from "docstar-editor";
|
|
52
|
+
|
|
53
|
+
export function Page() {
|
|
54
|
+
return (
|
|
55
|
+
<DocstarEditor
|
|
56
|
+
collab={{
|
|
57
|
+
wsUrl: "wss://your-server.example.com",
|
|
58
|
+
token: authToken,
|
|
59
|
+
workspaceId: "workspace-abc",
|
|
60
|
+
documentId: "doc-123",
|
|
61
|
+
user: { name: "Swayam", color: "#5b8def" },
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
When `collab` is provided, the editor connects over WebSocket, syncs edits live via Yjs, and shows collaborator cursors/presence automatically. Omit `workspaceId`/`token` to connect to a server that identifies documents directly by `documentId` instead (see `wsParams` in `CollabConfig`).
|
|
69
|
+
|
|
70
|
+
## Local development / demo site
|
|
71
|
+
|
|
72
|
+
See [`website/`](./website) — a small standalone demo app (landing page + Playground/Import) that imports the editor directly from source, no build/pack step needed. It's local-only/single-user by design; it doesn't demonstrate the `collab` prop.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cd website
|
|
76
|
+
npm install
|
|
77
|
+
npm run dev
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The site auto-deploys to GitHub Pages on every push to `main` that touches `website/` or `src/` (see `.github/workflows/deploy-website.yml`). One-time setup: in the repo's **Settings → Pages**, set **Source** to **GitHub Actions**.
|
|
81
|
+
|
|
82
|
+
## Publishing to npm
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# 1. Build and sanity-check what will be published
|
|
86
|
+
npm run build
|
|
87
|
+
npm pack --dry-run
|
|
88
|
+
|
|
89
|
+
# 2. Log in (one-time per machine; opens a browser to authenticate)
|
|
90
|
+
npm login
|
|
91
|
+
|
|
92
|
+
# 3. Bump the version (pick one)
|
|
93
|
+
npm version patch # 0.1.0 -> 0.1.1, bug fixes
|
|
94
|
+
npm version minor # 0.1.0 -> 0.2.0, new features
|
|
95
|
+
npm version major # 0.1.0 -> 1.0.0, breaking changes
|
|
96
|
+
|
|
97
|
+
# 4. Publish
|
|
98
|
+
npm publish
|
|
99
|
+
|
|
100
|
+
# 5. Push the version bump commit + tag that `npm version` created
|
|
101
|
+
git push && git push --tags
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`npm version` refuses to run with uncommitted changes — commit your work first. `npm publish` is public and irreversible (a given version can never be re-published, only deprecated), so double-check `npm pack --dry-run`'s file list before running it.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { Block } from '@blocknote/core';
|
|
3
|
+
import * as Y from 'yjs';
|
|
4
|
+
import { HocuspocusProvider } from '@hocuspocus/provider';
|
|
5
|
+
|
|
6
|
+
interface CollabUser {
|
|
7
|
+
name: string;
|
|
8
|
+
color: string;
|
|
9
|
+
}
|
|
10
|
+
interface CollabConfig {
|
|
11
|
+
/** WebSocket URL of the Hocuspocus-compatible collaboration server, e.g. "wss://editor.docstar.io" */
|
|
12
|
+
wsUrl: string;
|
|
13
|
+
/**
|
|
14
|
+
* Identifies the document. With `workspaceId` set, this is scoped under it
|
|
15
|
+
* (`${workspaceId}:${documentId}`) and hits `${wsUrl}/workspace/${workspaceId}`.
|
|
16
|
+
* Without `workspaceId`, this is used verbatim as the Hocuspocus documentName
|
|
17
|
+
* against `wsUrl` as-is — for connecting to an existing/legacy server that
|
|
18
|
+
* doesn't use the workspace-scoped routing (e.g. `documentName` already
|
|
19
|
+
* equals a raw page id).
|
|
20
|
+
*/
|
|
21
|
+
documentId: string;
|
|
22
|
+
/** Auth token issued for the workspace/session, sent to the server's onAuthenticate hook. Only meaningful with `workspaceId`. */
|
|
23
|
+
token?: string;
|
|
24
|
+
/** The workspace this document belongs to. Omit to connect directly to `wsUrl` with `documentId` as the raw documentName. */
|
|
25
|
+
workspaceId?: string;
|
|
26
|
+
/** Extra query params appended to `wsUrl` as-is (e.g. `{ orgId, userId }` for a legacy server). Ignored when `workspaceId` is set. */
|
|
27
|
+
wsParams?: Record<string, string>;
|
|
28
|
+
/** Local user's presence info shown to collaborators. */
|
|
29
|
+
user: CollabUser;
|
|
30
|
+
}
|
|
31
|
+
interface DocstarEditorProps {
|
|
32
|
+
/** Initial content as markdown. Ignored once `collab` is set and a document already exists on the server. */
|
|
33
|
+
defaultMarkdown?: string;
|
|
34
|
+
/** Fires on every content change with the current markdown and raw blocks. */
|
|
35
|
+
onChange?: (markdown: string, blocks: Block[]) => void;
|
|
36
|
+
/** Enables real-time collaboration + server-side sync when provided. Omit for local, single-user mode. */
|
|
37
|
+
collab?: CollabConfig;
|
|
38
|
+
/** Optional extra class name for the editor container. */
|
|
39
|
+
className?: string;
|
|
40
|
+
/** Disables editing. */
|
|
41
|
+
editable?: boolean;
|
|
42
|
+
/** Visual theme. Defaults to "light". */
|
|
43
|
+
theme?: "light" | "dark";
|
|
44
|
+
/** Makes the editor's own background transparent, so it blends into whatever container it's placed in instead of showing its own distinct panel color. */
|
|
45
|
+
transparent?: boolean;
|
|
46
|
+
}
|
|
47
|
+
interface DocstarEditorHandle {
|
|
48
|
+
/** Serializes the current document to Markdown. */
|
|
49
|
+
getMarkdown: () => Promise<string>;
|
|
50
|
+
/** Replaces the current document with the parsed content of a Markdown string. */
|
|
51
|
+
setMarkdown: (markdown: string) => Promise<void>;
|
|
52
|
+
/** Moves focus into the editor. */
|
|
53
|
+
focus: () => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
declare const DocstarEditor: react.ForwardRefExoticComponent<DocstarEditorProps & react.RefAttributes<DocstarEditorHandle>>;
|
|
57
|
+
|
|
58
|
+
interface CollabConnection {
|
|
59
|
+
doc: Y.Doc;
|
|
60
|
+
provider: HocuspocusProvider;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Opens a Yjs doc synced over a Hocuspocus-compatible WebSocket connection.
|
|
64
|
+
*
|
|
65
|
+
* With `workspaceId` set, `workspaceId`/`documentId` are composed into the
|
|
66
|
+
* Hocuspocus documentName so persistence stays scoped per workspace, hitting
|
|
67
|
+
* `${wsUrl}/workspace/${workspaceId}`, and `token` is forwarded for the
|
|
68
|
+
* server's onAuthenticate hook to verify.
|
|
69
|
+
*
|
|
70
|
+
* Without `workspaceId`, `documentId` is used verbatim as the documentName
|
|
71
|
+
* against `wsUrl` as-is (optionally with `wsParams` appended as query
|
|
72
|
+
* params) — for connecting to a server that doesn't use workspace-scoped
|
|
73
|
+
* routing.
|
|
74
|
+
*/
|
|
75
|
+
declare function connectProvider(config: CollabConfig): CollabConnection;
|
|
76
|
+
|
|
77
|
+
export { type CollabConfig, type CollabConnection, type CollabUser, DocstarEditor, type DocstarEditorHandle, type DocstarEditorProps, connectProvider };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// src/DocstarEditor.tsx
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react";
|
|
3
|
+
import { useCreateBlockNote } from "@blocknote/react";
|
|
4
|
+
import { BlockNoteView, lightDefaultTheme, darkDefaultTheme } from "@blocknote/mantine";
|
|
5
|
+
import "@blocknote/core/fonts/inter.css";
|
|
6
|
+
import "@blocknote/mantine/style.css";
|
|
7
|
+
|
|
8
|
+
// src/collab/connectProvider.ts
|
|
9
|
+
import * as Y from "yjs";
|
|
10
|
+
import { HocuspocusProvider } from "@hocuspocus/provider";
|
|
11
|
+
function connectProvider(config) {
|
|
12
|
+
const doc = new Y.Doc();
|
|
13
|
+
const wsUrl = toWebSocketUrl(config.wsUrl);
|
|
14
|
+
const url = config.workspaceId ? `${wsUrl.replace(/\/$/, "")}/workspace/${config.workspaceId}` : appendParams(wsUrl, config.wsParams);
|
|
15
|
+
const provider = new HocuspocusProvider({
|
|
16
|
+
url,
|
|
17
|
+
name: config.workspaceId ? `${config.workspaceId}:${config.documentId}` : config.documentId,
|
|
18
|
+
// HocuspocusProvider only sends its Auth message when `token` is truthy
|
|
19
|
+
// (see its `isAuthenticationRequired` getter), and the Hocuspocus server
|
|
20
|
+
// core unconditionally queues every other incoming message until that
|
|
21
|
+
// Auth message arrives — regardless of whether an onAuthenticate hook is
|
|
22
|
+
// even configured server-side. Without a token at all (the no-workspace
|
|
23
|
+
// path, which has no auth server-side either), the client never sends
|
|
24
|
+
// that message and the connection hangs forever with no event ever
|
|
25
|
+
// firing. A placeholder value keeps the handshake moving; the server
|
|
26
|
+
// only actually validates it when `workspaceId` requires auth.
|
|
27
|
+
token: config.token || "no-auth-required",
|
|
28
|
+
document: doc
|
|
29
|
+
});
|
|
30
|
+
provider.setAwarenessField("user", config.user);
|
|
31
|
+
return { doc, provider };
|
|
32
|
+
}
|
|
33
|
+
function appendParams(url, params) {
|
|
34
|
+
if (!params || Object.keys(params).length === 0) return url;
|
|
35
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
36
|
+
return `${url}${separator}${new URLSearchParams(params).toString()}`;
|
|
37
|
+
}
|
|
38
|
+
function toWebSocketUrl(url) {
|
|
39
|
+
if (/^https:\/\//i.test(url)) return url.replace(/^https:\/\//i, "wss://");
|
|
40
|
+
if (/^http:\/\//i.test(url)) return url.replace(/^http:\/\//i, "ws://");
|
|
41
|
+
return url;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/DocstarEditor.tsx
|
|
45
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
46
|
+
var transparentLightTheme = {
|
|
47
|
+
...lightDefaultTheme,
|
|
48
|
+
colors: { ...lightDefaultTheme.colors, editor: { ...lightDefaultTheme.colors.editor, background: "transparent" } }
|
|
49
|
+
};
|
|
50
|
+
var transparentDarkTheme = {
|
|
51
|
+
...darkDefaultTheme,
|
|
52
|
+
colors: { ...darkDefaultTheme.colors, editor: { ...darkDefaultTheme.colors.editor, background: "transparent" } }
|
|
53
|
+
};
|
|
54
|
+
var DocstarEditor = forwardRef(
|
|
55
|
+
function DocstarEditor2({
|
|
56
|
+
defaultMarkdown,
|
|
57
|
+
onChange,
|
|
58
|
+
collab,
|
|
59
|
+
className,
|
|
60
|
+
editable = true,
|
|
61
|
+
theme = "light",
|
|
62
|
+
transparent = false
|
|
63
|
+
}, ref) {
|
|
64
|
+
const [connection, setConnection] = useState(null);
|
|
65
|
+
const [status, setStatus] = useState("connecting");
|
|
66
|
+
const [error, setError] = useState(null);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!collab) return;
|
|
69
|
+
setStatus("connecting");
|
|
70
|
+
setError(null);
|
|
71
|
+
const conn = connectProvider(collab);
|
|
72
|
+
setConnection(conn);
|
|
73
|
+
const onSynced = () => setStatus("synced");
|
|
74
|
+
const onAuthenticationFailed = ({ reason }) => {
|
|
75
|
+
setStatus("error");
|
|
76
|
+
setError(`Authentication failed: ${reason}`);
|
|
77
|
+
};
|
|
78
|
+
const onClose = ({ event }) => {
|
|
79
|
+
setStatus((current) => {
|
|
80
|
+
if (current === "synced") return current;
|
|
81
|
+
setError(`Connection closed (${event.code}) ${event.reason}`.trim());
|
|
82
|
+
return "error";
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
conn.provider.on("synced", onSynced);
|
|
86
|
+
conn.provider.on("authenticationFailed", onAuthenticationFailed);
|
|
87
|
+
conn.provider.on("close", onClose);
|
|
88
|
+
const timeout = setTimeout(() => {
|
|
89
|
+
setStatus((current) => {
|
|
90
|
+
if (current !== "connecting") return current;
|
|
91
|
+
setError("Timed out waiting for the server to respond.");
|
|
92
|
+
return "error";
|
|
93
|
+
});
|
|
94
|
+
}, 15e3);
|
|
95
|
+
return () => {
|
|
96
|
+
clearTimeout(timeout);
|
|
97
|
+
conn.provider.off("synced", onSynced);
|
|
98
|
+
conn.provider.off("authenticationFailed", onAuthenticationFailed);
|
|
99
|
+
conn.provider.off("close", onClose);
|
|
100
|
+
conn.provider.destroy();
|
|
101
|
+
conn.doc.destroy();
|
|
102
|
+
};
|
|
103
|
+
}, [collab?.wsUrl, collab?.documentId, collab?.token]);
|
|
104
|
+
const editor = useCreateBlockNote(
|
|
105
|
+
collab ? {
|
|
106
|
+
collaboration: connection ? {
|
|
107
|
+
provider: connection.provider,
|
|
108
|
+
fragment: connection.doc.getXmlFragment("default"),
|
|
109
|
+
user: collab.user
|
|
110
|
+
} : void 0
|
|
111
|
+
} : { initialContent: void 0 },
|
|
112
|
+
[connection]
|
|
113
|
+
);
|
|
114
|
+
const initialMarkdownLoaded = useMemo(() => ({ current: false }), [editor]);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (collab) return;
|
|
117
|
+
if (initialMarkdownLoaded.current) return;
|
|
118
|
+
if (!defaultMarkdown) return;
|
|
119
|
+
initialMarkdownLoaded.current = true;
|
|
120
|
+
editor.tryParseMarkdownToBlocks(defaultMarkdown).then((blocks) => {
|
|
121
|
+
editor.replaceBlocks(editor.document, blocks);
|
|
122
|
+
});
|
|
123
|
+
}, [collab, defaultMarkdown, editor, initialMarkdownLoaded]);
|
|
124
|
+
useImperativeHandle(
|
|
125
|
+
ref,
|
|
126
|
+
() => ({
|
|
127
|
+
getMarkdown: () => editor.blocksToMarkdownLossy(editor.document),
|
|
128
|
+
setMarkdown: async (markdown) => {
|
|
129
|
+
const blocks = await editor.tryParseMarkdownToBlocks(markdown);
|
|
130
|
+
editor.replaceBlocks(editor.document, blocks);
|
|
131
|
+
},
|
|
132
|
+
focus: () => editor.focus()
|
|
133
|
+
}),
|
|
134
|
+
[editor]
|
|
135
|
+
);
|
|
136
|
+
if (collab && status === "connecting") {
|
|
137
|
+
return /* @__PURE__ */ jsx("div", { className, "data-docstar-status": "connecting", children: "Connecting\u2026" });
|
|
138
|
+
}
|
|
139
|
+
if (collab && status === "error") {
|
|
140
|
+
return /* @__PURE__ */ jsxs("div", { className, "data-docstar-status": "error", children: [
|
|
141
|
+
"Couldn't connect to the collaboration server",
|
|
142
|
+
error ? `: ${error}` : ".",
|
|
143
|
+
" ",
|
|
144
|
+
"Changes won't be saved until this reconnects."
|
|
145
|
+
] });
|
|
146
|
+
}
|
|
147
|
+
const resolvedTheme = transparent ? theme === "dark" ? transparentDarkTheme : transparentLightTheme : theme;
|
|
148
|
+
return /* @__PURE__ */ jsx(
|
|
149
|
+
BlockNoteView,
|
|
150
|
+
{
|
|
151
|
+
editor,
|
|
152
|
+
editable,
|
|
153
|
+
className,
|
|
154
|
+
theme: resolvedTheme,
|
|
155
|
+
onChange: onChange ? () => {
|
|
156
|
+
editor.blocksToMarkdownLossy(editor.document).then((markdown) => {
|
|
157
|
+
onChange(markdown, editor.document);
|
|
158
|
+
});
|
|
159
|
+
} : void 0
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
export {
|
|
165
|
+
DocstarEditor,
|
|
166
|
+
connectProvider
|
|
167
|
+
};
|
|
168
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/DocstarEditor.tsx","../src/collab/connectProvider.ts"],"sourcesContent":["import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from \"react\";\nimport { useCreateBlockNote } from \"@blocknote/react\";\nimport { BlockNoteView, lightDefaultTheme, darkDefaultTheme } from \"@blocknote/mantine\";\nimport \"@blocknote/core/fonts/inter.css\";\nimport \"@blocknote/mantine/style.css\";\nimport { connectProvider, type CollabConnection } from \"./collab/connectProvider\";\nimport type { DocstarEditorHandle, DocstarEditorProps } from \"./types\";\n\nconst transparentLightTheme = {\n ...lightDefaultTheme,\n colors: { ...lightDefaultTheme.colors, editor: { ...lightDefaultTheme.colors.editor, background: \"transparent\" } },\n};\nconst transparentDarkTheme = {\n ...darkDefaultTheme,\n colors: { ...darkDefaultTheme.colors, editor: { ...darkDefaultTheme.colors.editor, background: \"transparent\" } },\n};\n\ntype CollabStatus = \"connecting\" | \"synced\" | \"error\";\n\nexport const DocstarEditor = forwardRef<DocstarEditorHandle, DocstarEditorProps>(\n function DocstarEditor(\n {\n defaultMarkdown,\n onChange,\n collab,\n className,\n editable = true,\n theme = \"light\",\n transparent = false,\n },\n ref\n ) {\n const [connection, setConnection] = useState<CollabConnection | null>(null);\n const [status, setStatus] = useState<CollabStatus>(\"connecting\");\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n if (!collab) return;\n setStatus(\"connecting\");\n setError(null);\n const conn = connectProvider(collab);\n setConnection(conn);\n\n const onSynced = () => setStatus(\"synced\");\n const onAuthenticationFailed = ({ reason }: { reason: string }) => {\n setStatus(\"error\");\n setError(`Authentication failed: ${reason}`);\n };\n const onClose = ({ event }: { event: { code: number; reason: string } }) => {\n setStatus((current) => {\n if (current === \"synced\") return current;\n setError(`Connection closed (${event.code}) ${event.reason}`.trim());\n return \"error\";\n });\n };\n\n conn.provider.on(\"synced\", onSynced);\n conn.provider.on(\"authenticationFailed\", onAuthenticationFailed);\n conn.provider.on(\"close\", onClose);\n\n // Safety net: if the underlying transport fails in a way that never\n // fires synced/authenticationFailed/close (e.g. an invalid WS URL\n // throwing inside the provider's connect logic), don't hang on\n // \"Connecting…\" forever.\n const timeout = setTimeout(() => {\n setStatus((current) => {\n if (current !== \"connecting\") return current;\n setError(\"Timed out waiting for the server to respond.\");\n return \"error\";\n });\n }, 15000);\n\n return () => {\n clearTimeout(timeout);\n conn.provider.off(\"synced\", onSynced);\n conn.provider.off(\"authenticationFailed\", onAuthenticationFailed);\n conn.provider.off(\"close\", onClose);\n conn.provider.destroy();\n conn.doc.destroy();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [collab?.wsUrl, collab?.documentId, collab?.token]);\n\n const editor = useCreateBlockNote(\n collab\n ? {\n collaboration: connection\n ? {\n provider: connection.provider,\n fragment: connection.doc.getXmlFragment(\"default\"),\n user: collab.user,\n }\n : undefined,\n }\n : { initialContent: undefined },\n [connection]\n );\n\n const initialMarkdownLoaded = useMemo(() => ({ current: false }), [editor]);\n\n useEffect(() => {\n if (collab) return; // collab documents load their content from the server\n if (initialMarkdownLoaded.current) return;\n if (!defaultMarkdown) return;\n initialMarkdownLoaded.current = true;\n editor.tryParseMarkdownToBlocks(defaultMarkdown).then((blocks) => {\n editor.replaceBlocks(editor.document, blocks);\n });\n }, [collab, defaultMarkdown, editor, initialMarkdownLoaded]);\n\n useImperativeHandle(\n ref,\n () => ({\n getMarkdown: () => editor.blocksToMarkdownLossy(editor.document),\n setMarkdown: async (markdown: string) => {\n const blocks = await editor.tryParseMarkdownToBlocks(markdown);\n editor.replaceBlocks(editor.document, blocks);\n },\n focus: () => editor.focus(),\n }),\n [editor]\n );\n\n if (collab && status === \"connecting\") {\n return (\n <div className={className} data-docstar-status=\"connecting\">\n Connecting…\n </div>\n );\n }\n\n if (collab && status === \"error\") {\n return (\n <div className={className} data-docstar-status=\"error\">\n Couldn't connect to the collaboration server{error ? `: ${error}` : \".\"}\n {\" \"}Changes won't be saved until this reconnects.\n </div>\n );\n }\n\n const resolvedTheme = transparent\n ? theme === \"dark\"\n ? transparentDarkTheme\n : transparentLightTheme\n : theme;\n\n return (\n <BlockNoteView\n editor={editor}\n editable={editable}\n className={className}\n theme={resolvedTheme}\n onChange={\n onChange\n ? () => {\n editor.blocksToMarkdownLossy(editor.document).then((markdown) => {\n onChange(markdown, editor.document);\n });\n }\n : undefined\n }\n />\n );\n }\n);\n","import * as Y from \"yjs\";\nimport { HocuspocusProvider } from \"@hocuspocus/provider\";\nimport type { CollabConfig } from \"../types\";\n\nexport interface CollabConnection {\n doc: Y.Doc;\n provider: HocuspocusProvider;\n}\n\n/**\n * Opens a Yjs doc synced over a Hocuspocus-compatible WebSocket connection.\n *\n * With `workspaceId` set, `workspaceId`/`documentId` are composed into the\n * Hocuspocus documentName so persistence stays scoped per workspace, hitting\n * `${wsUrl}/workspace/${workspaceId}`, and `token` is forwarded for the\n * server's onAuthenticate hook to verify.\n *\n * Without `workspaceId`, `documentId` is used verbatim as the documentName\n * against `wsUrl` as-is (optionally with `wsParams` appended as query\n * params) — for connecting to a server that doesn't use workspace-scoped\n * routing.\n */\nexport function connectProvider(config: CollabConfig): CollabConnection {\n const doc = new Y.Doc();\n\n const wsUrl = toWebSocketUrl(config.wsUrl);\n const url = config.workspaceId\n ? `${wsUrl.replace(/\\/$/, \"\")}/workspace/${config.workspaceId}`\n : appendParams(wsUrl, config.wsParams);\n\n const provider = new HocuspocusProvider({\n url,\n name: config.workspaceId ? `${config.workspaceId}:${config.documentId}` : config.documentId,\n // HocuspocusProvider only sends its Auth message when `token` is truthy\n // (see its `isAuthenticationRequired` getter), and the Hocuspocus server\n // core unconditionally queues every other incoming message until that\n // Auth message arrives — regardless of whether an onAuthenticate hook is\n // even configured server-side. Without a token at all (the no-workspace\n // path, which has no auth server-side either), the client never sends\n // that message and the connection hangs forever with no event ever\n // firing. A placeholder value keeps the handshake moving; the server\n // only actually validates it when `workspaceId` requires auth.\n token: config.token || \"no-auth-required\",\n document: doc,\n });\n\n provider.setAwarenessField(\"user\", config.user);\n\n return { doc, provider };\n}\n\nfunction appendParams(url: string, params?: Record<string, string>): string {\n if (!params || Object.keys(params).length === 0) return url;\n const separator = url.includes(\"?\") ? \"&\" : \"?\";\n return `${url}${separator}${new URLSearchParams(params).toString()}`;\n}\n\n/**\n * `new WebSocket()` requires a `ws:`/`wss:` scheme and throws a SyntaxError\n * for anything else (e.g. `http:`/`https:`), which HocuspocusProvider doesn't\n * guard against — that throw happens silently inside its connect logic, so\n * the UI is left stuck with no `close`/`synced`/`authenticationFailed` event\n * ever firing. Normalize here so a plain http(s) base URL (as apps commonly\n * configure for their API host) just works.\n */\nfunction toWebSocketUrl(url: string): string {\n if (/^https:\\/\\//i.test(url)) return url.replace(/^https:\\/\\//i, \"wss://\");\n if (/^http:\\/\\//i.test(url)) return url.replace(/^http:\\/\\//i, \"ws://\");\n return url;\n}\n"],"mappings":"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","names":["DocstarEditor"]}
|
package/dist/styles.css
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "docstar-editor",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "A minimal block/markdown editor for React with optional real-time collaboration.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"editor",
|
|
9
|
+
"markdown",
|
|
10
|
+
"blocknote",
|
|
11
|
+
"react",
|
|
12
|
+
"collaboration",
|
|
13
|
+
"yjs"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://swayammaheshwari.github.io/docstar-editor/",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/swayammaheshwari/docstar-editor.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/swayammaheshwari/docstar-editor/issues"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "dist/index.js",
|
|
25
|
+
"module": "dist/index.js",
|
|
26
|
+
"types": "dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./styles.css": "./dist/styles.css"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"dev": "tsup --watch"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"react": ">=18",
|
|
44
|
+
"react-dom": ">=18"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@blocknote/core": "^0.22.0",
|
|
48
|
+
"@blocknote/mantine": "^0.22.0",
|
|
49
|
+
"@blocknote/react": "^0.22.0",
|
|
50
|
+
"@hocuspocus/provider": "^2.15.0",
|
|
51
|
+
"yjs": "^13.6.18"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/react": "^18.3.0",
|
|
55
|
+
"@types/react-dom": "^18.3.0",
|
|
56
|
+
"react": "^18.3.0",
|
|
57
|
+
"react-dom": "^18.3.0",
|
|
58
|
+
"tsup": "^8.3.0",
|
|
59
|
+
"typescript": "^5.6.0"
|
|
60
|
+
},
|
|
61
|
+
"overrides": {
|
|
62
|
+
"prosemirror-view": "1.33.9"
|
|
63
|
+
}
|
|
64
|
+
}
|