@tangle-network/ui 11.12.0 → 11.13.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/CHANGELOG.md +17 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/auth/auth.d.ts +53 -0
- package/dist/{auth-DOETfnd9.js → auth/auth.js} +7 -37
- package/dist/auth/login-layout.d.ts +20 -0
- package/dist/auth/login-layout.js +36 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +2 -1
- package/dist/chat/agent-timeline.d.ts +83 -0
- package/dist/chat/agent-timeline.js +236 -0
- package/dist/chat/chat-container.d.ts +51 -0
- package/dist/chat/chat-container.js +309 -0
- package/dist/chat/chat-message.d.ts +22 -0
- package/dist/chat/chat-message.js +38 -0
- package/dist/chat/message-list.d.ts +32 -0
- package/dist/chat/message-list.js +37 -0
- package/dist/chat/thinking-indicator.d.ts +6 -0
- package/dist/chat/thinking-indicator.js +38 -0
- package/dist/chat/user-message.d.ts +21 -0
- package/dist/chat/user-message.js +42 -0
- package/dist/chat.d.ts +6 -1
- package/dist/chat.js +6 -1
- package/dist/editor/document-editor-pane.d.ts +37 -0
- package/dist/editor/document-editor-pane.js +199 -0
- package/dist/editor/editor-lazy.js +37 -0
- package/dist/editor/editor-loading.js +21 -0
- package/dist/editor/editor-peers.d.ts +6 -0
- package/dist/editor/editor-peers.js +155 -0
- package/dist/editor/editor-provider.d.ts +97 -0
- package/dist/editor/editor-provider.js +253 -0
- package/dist/editor/editor-toolbar.d.ts +11 -0
- package/dist/editor/editor-toolbar.js +106 -0
- package/dist/editor/markdown-conversion.js +23 -0
- package/dist/editor/markdown-document-editor.js +103 -0
- package/dist/editor/tiptap-editor.d.ts +45 -0
- package/dist/editor/tiptap-editor.js +271 -0
- package/dist/editor/use-editor.d.ts +67 -0
- package/dist/editor/use-editor.js +158 -0
- package/dist/editor.d.ts +6 -119
- package/dist/editor.js +5 -1
- package/dist/files/file-artifact-pane.d.ts +47 -0
- package/dist/files/file-artifact-pane.js +114 -0
- package/dist/files/file-format.d.ts +51 -0
- package/dist/{file-format-BgXbr-Ib.js → files/file-format.js} +1 -1
- package/dist/files/file-preview.d.ts +27 -0
- package/dist/files/file-preview.js +359 -0
- package/dist/files/file-tabs.d.ts +19 -0
- package/dist/files/file-tabs.js +48 -0
- package/dist/files/file-tree.d.ts +31 -0
- package/dist/files/file-tree.js +135 -0
- package/dist/files/rich-file-tree.d.ts +62 -0
- package/dist/files/rich-file-tree.js +122 -0
- package/dist/files.d.ts +6 -1
- package/dist/files.js +6 -2
- package/dist/hooks/use-auth.d.ts +40 -0
- package/dist/hooks/use-auth.js +91 -0
- package/dist/hooks/use-auto-scroll.d.ts +11 -0
- package/dist/hooks/use-auto-scroll.js +48 -0
- package/dist/hooks/use-dropdown-menu.d.ts +12 -0
- package/dist/hooks/use-dropdown-menu.js +31 -0
- package/dist/hooks/use-live-time.d.ts +7 -0
- package/dist/hooks/use-live-time.js +22 -0
- package/dist/hooks/use-realtime-session.d.ts +32 -0
- package/dist/hooks/use-realtime-session.js +235 -0
- package/dist/hooks/use-run-collapse-state.d.ts +13 -0
- package/dist/hooks/use-run-collapse-state.js +24 -0
- package/dist/hooks/use-run-groups.d.ts +11 -0
- package/dist/hooks/use-run-groups.js +85 -0
- package/dist/hooks/use-sdk-session.d.ts +59 -0
- package/dist/hooks/use-sdk-session.js +322 -0
- package/dist/hooks/use-sse-stream.d.ts +124 -0
- package/dist/hooks/use-sse-stream.js +229 -0
- package/dist/hooks/use-tool-call-stream.d.ts +23 -0
- package/dist/hooks/use-tool-call-stream.js +75 -0
- package/dist/hooks.d.ts +11 -49
- package/dist/hooks.js +10 -110
- package/dist/index.d.ts +99 -25
- package/dist/index.js +86 -36
- package/dist/lib/focus.d.ts +55 -0
- package/dist/{focus-BcE_SHqz.js → lib/focus.js} +1 -1
- package/dist/lib/utils.d.ts +4 -0
- package/dist/{utils-C_uf36nf.js → lib/utils.js} +1 -1
- package/dist/{code-block-B9miUPht.d.ts → markdown/code-block.d.ts} +5 -6
- package/dist/{code-block-B36KVUSN.js → markdown/code-block.js} +22 -47
- package/dist/{markdown-C0atSabr.d.ts → markdown/markdown.d.ts} +4 -5
- package/dist/{markdown-Db08KAYF.js → markdown/markdown.js} +3 -3
- package/dist/markdown.d.ts +2 -2
- package/dist/markdown.js +2 -2
- package/dist/{openui-artifact-renderer-fkBQaoLO.d.ts → openui/openui-artifact-renderer.d.ts} +20 -21
- package/dist/{openui-artifact-renderer-DG_i2lOR.js → openui/openui-artifact-renderer.js} +9 -7
- package/dist/{schema-BpW9rSSG.js → openui/schema-compiled.js} +2 -354
- package/dist/{schema-DAstk-J4.d.ts → openui/schema-data.d.ts} +3 -24
- package/dist/openui/schema-data.js +206 -0
- package/dist/openui-schema.d.ts +22 -3
- package/dist/openui-schema.js +138 -1
- package/dist/openui.d.ts +3 -2
- package/dist/openui.js +3 -2
- package/dist/{artifact-pane-BL_DFWNl.d.ts → primitives/artifact-pane.d.ts} +3 -4
- package/dist/{artifact-pane-DMXT9a8U.js → primitives/artifact-pane.js} +2 -2
- package/dist/primitives/avatar.d.ts +8 -0
- package/dist/primitives/avatar.js +25 -0
- package/dist/primitives/badge.d.ts +22 -0
- package/dist/primitives/badge.js +47 -0
- package/dist/{button-yoIv77e2.d.ts → primitives/button.d.ts} +2 -2
- package/dist/primitives/button.js +113 -0
- package/dist/primitives/card.d.ts +18 -0
- package/dist/primitives/card.js +52 -0
- package/dist/primitives/code-block.d.ts +6 -0
- package/dist/primitives/code-block.js +13 -0
- package/dist/primitives/dialog.d.ts +23 -0
- package/dist/primitives/dialog.js +59 -0
- package/dist/primitives/drop-zone.d.ts +23 -0
- package/dist/primitives/drop-zone.js +104 -0
- package/dist/primitives/dropdown-menu.d.ts +29 -0
- package/dist/{dropdown-menu-BpcLa5vF.js → primitives/dropdown-menu.js} +2 -23
- package/dist/primitives/empty-state.d.ts +11 -0
- package/dist/primitives/empty-state.js +32 -0
- package/dist/primitives/heading.d.ts +23 -0
- package/dist/primitives/heading.js +38 -0
- package/dist/primitives/input.d.ts +23 -0
- package/dist/primitives/input.js +110 -0
- package/dist/primitives/label.d.ts +6 -0
- package/dist/primitives/label.js +16 -0
- package/dist/primitives/metric-strip.d.ts +48 -0
- package/dist/primitives/metric-strip.js +52 -0
- package/dist/primitives/page-header.d.ts +25 -0
- package/dist/primitives/page-header.js +47 -0
- package/dist/primitives/page-shell.d.ts +14 -0
- package/dist/primitives/page-shell.js +17 -0
- package/dist/primitives/progress.d.ts +9 -0
- package/dist/primitives/progress.js +29 -0
- package/dist/primitives/segmented-control.d.ts +40 -0
- package/dist/primitives/segmented-control.js +57 -0
- package/dist/primitives/select.d.ts +15 -0
- package/dist/primitives/select.js +74 -0
- package/dist/primitives/sidebar-drop-zone.d.ts +21 -0
- package/dist/primitives/sidebar-drop-zone.js +88 -0
- package/dist/primitives/skeleton.d.ts +10 -0
- package/dist/primitives/skeleton.js +50 -0
- package/dist/primitives/stat-card.d.ts +16 -0
- package/dist/primitives/stat-card.js +60 -0
- package/dist/primitives/status-pill.d.ts +44 -0
- package/dist/primitives/status-pill.js +119 -0
- package/dist/primitives/switch.d.ts +6 -0
- package/dist/primitives/switch.js +16 -0
- package/dist/primitives/table.d.ts +22 -0
- package/dist/primitives/table.js +63 -0
- package/dist/primitives/tabs.d.ts +13 -0
- package/dist/{tabs-BVHR6NkO.js → primitives/tabs.js} +3 -3
- package/dist/primitives/terminal-display.d.ts +25 -0
- package/dist/primitives/terminal-display.js +109 -0
- package/dist/primitives/text-shimmer.d.ts +16 -0
- package/dist/{text-shimmer-BUUo6I-g.js → primitives/text-shimmer.js} +2 -2
- package/dist/primitives/theme-toggle.d.ts +11 -0
- package/dist/primitives/theme-toggle.js +154 -0
- package/dist/primitives/toast.d.ts +29 -0
- package/dist/primitives/toast.js +142 -0
- package/dist/primitives/toolbar.d.ts +55 -0
- package/dist/primitives/toolbar.js +37 -0
- package/dist/primitives/upload-progress.d.ts +23 -0
- package/dist/primitives/upload-progress.js +72 -0
- package/dist/primitives.d.ts +35 -5
- package/dist/primitives.js +35 -9
- package/dist/{index-COgqsFAQ.d.ts → redaction/redacted-document.d.ts} +6 -7
- package/dist/{redaction-CGGB2ENG.js → redaction/redacted-document.js} +2 -2
- package/dist/redaction.d.ts +1 -1
- package/dist/redaction.js +1 -1
- package/dist/run/expandable-content.js +41 -0
- package/dist/run/expanded-tool-detail.d.ts +12 -0
- package/dist/run/expanded-tool-detail.js +163 -0
- package/dist/run/inline-thinking-item.d.ts +11 -0
- package/dist/run/inline-thinking-item.js +60 -0
- package/dist/run/inline-tool-item.d.ts +25 -0
- package/dist/run/inline-tool-item.js +63 -0
- package/dist/run/run-group.d.ts +26 -0
- package/dist/{run-group-BWGeesgs.js → run/run-group.js} +9 -60
- package/dist/run/run-item-primitives.d.ts +5 -0
- package/dist/{run-ZsIKPA6J.js → run/run-item-primitives.js} +2 -2
- package/dist/run/run-row-shell.d.ts +53 -0
- package/dist/run/run-row-shell.js +114 -0
- package/dist/{tool-call-feed-BeB1_4pX.d.ts → run/tool-call-feed.d.ts} +7 -12
- package/dist/{tool-call-feed-BpmMLpPX.js → run/tool-call-feed.js} +4 -4
- package/dist/run/tool-call-step.d.ts +5 -0
- package/dist/run/tool-call-step.js +82 -0
- package/dist/run.d.ts +8 -2
- package/dist/run.js +7 -4
- package/dist/sdk-hooks.d.ts +8 -1
- package/dist/sdk-hooks.js +8 -2
- package/dist/stores/active-sessions-store.d.ts +83 -0
- package/dist/{active-sessions-store-Gt1R0rFG.js → stores/active-sessions-store.js} +1 -1
- package/dist/stores/chat-store.d.ts +14 -0
- package/dist/stores/chat-store.js +33 -0
- package/dist/stores.d.ts +3 -16
- package/dist/stores.js +2 -33
- package/dist/tool-previews/command-preview.d.ts +11 -0
- package/dist/tool-previews/command-preview.js +91 -0
- package/dist/tool-previews/diff-preview.d.ts +7 -0
- package/dist/tool-previews/diff-preview.js +76 -0
- package/dist/tool-previews/glob-results-preview.d.ts +7 -0
- package/dist/tool-previews/glob-results-preview.js +53 -0
- package/dist/tool-previews/grep-results-preview.d.ts +7 -0
- package/dist/tool-previews/grep-results-preview.js +99 -0
- package/dist/tool-previews/preview-primitives.js +58 -0
- package/dist/tool-previews/question-preview.d.ts +7 -0
- package/dist/tool-previews/question-preview.js +63 -0
- package/dist/tool-previews/web-search-preview.d.ts +7 -0
- package/dist/tool-previews/web-search-preview.js +78 -0
- package/dist/tool-previews/write-file-preview.d.ts +11 -0
- package/dist/tool-previews/write-file-preview.js +52 -0
- package/dist/tool-previews.d.ts +7 -1
- package/dist/tool-previews.js +7 -1
- package/dist/{branding-26d5ARIr.d.ts → types/branding.d.ts} +2 -3
- package/dist/{message-vhUI0c5G.d.ts → types/message.d.ts} +2 -3
- package/dist/{parts-5mhy9yJ3.d.ts → types/parts.d.ts} +8 -9
- package/dist/{run-OyWmIDqu.d.ts → types/run.d.ts} +9 -10
- package/dist/{tool-display-DgmL5rIU.d.ts → types/tool-display.d.ts} +5 -6
- package/dist/types.d.ts +5 -5
- package/dist/utils/copy-text.d.ts +7 -0
- package/dist/{utils-Bl_CZ0Qp.js → utils/copy-text.js} +1 -11
- package/dist/utils/format.d.ts +22 -0
- package/dist/{format-DfcLWqMa.js → utils/format.js} +1 -1
- package/dist/utils/time-ago.d.ts +3 -0
- package/dist/utils/time-ago.js +11 -0
- package/dist/utils/tool-display.d.ts +16 -0
- package/dist/{tool-display-BAuAVa43.js → utils/tool-display.js} +1 -1
- package/dist/utils.d.ts +6 -1
- package/dist/utils.js +6 -5
- package/package.json +2 -2
- package/src/files/rich-file-tree.stories.tsx +1 -1
- package/src/files/rich-file-tree.tsx +12 -9
- package/src/markdown/code-block.test.tsx +46 -2
- package/src/markdown/code-block.tsx +23 -60
- package/src/primitives/button.stories.tsx +37 -1
- package/src/primitives/button.test.tsx +230 -0
- package/src/primitives/button.tsx +62 -14
- package/src/primitives/card-anatomy.stories.tsx +51 -0
- package/src/primitives/card.test.tsx +71 -0
- package/src/primitives/card.tsx +25 -13
- package/src/primitives/heading.stories.tsx +35 -0
- package/src/primitives/heading.test.tsx +56 -0
- package/src/primitives/heading.tsx +76 -0
- package/src/primitives/index.ts +8 -0
- package/src/primitives/input.stories.tsx +28 -0
- package/src/primitives/input.test.tsx +161 -0
- package/src/primitives/input.tsx +34 -28
- package/src/primitives/page-header.stories.tsx +51 -0
- package/src/primitives/page-header.test.tsx +48 -0
- package/src/primitives/page-header.tsx +35 -35
- package/src/primitives/page-shell.stories.tsx +89 -0
- package/src/primitives/page-shell.test.tsx +24 -0
- package/src/primitives/page-shell.tsx +26 -0
- package/src/primitives/table-wrapper.stories.tsx +54 -0
- package/src/primitives/table.test.tsx +72 -0
- package/src/primitives/table.tsx +33 -12
- package/src/primitives/theme-toggle.test.tsx +167 -0
- package/src/primitives/theme-toggle.tsx +111 -39
- package/dist/active-sessions-store-nl70ZnMC.d.ts +0 -84
- package/dist/button-C87nEe2R.js +0 -78
- package/dist/chat-DDvF2oGQ.js +0 -672
- package/dist/diff-preview-BcK3nRXb.js +0 -533
- package/dist/document-editor-pane-BilJfiCo.d.ts +0 -136
- package/dist/document-editor-pane-POsq5Q0o.js +0 -1279
- package/dist/files-hywocVZz.js +0 -753
- package/dist/index-2UntyjBb.d.ts +0 -52
- package/dist/index-BgUHilZm.d.ts +0 -108
- package/dist/index-CJ8wRfiC.d.ts +0 -236
- package/dist/index-CPDET_s1.d.ts +0 -549
- package/dist/index-Cg6gYAqh.d.ts +0 -73
- package/dist/index-Cs7j8V7l.d.ts +0 -203
- package/dist/index-DjKxO357.d.ts +0 -128
- package/dist/primitives-Bx59fT-U.js +0 -1239
- package/dist/table-Bss73DhB.js +0 -146
- package/dist/tool-call-step-DNebqhmX.js +0 -434
- package/dist/use-auto-scroll-C5Pjeqon.js +0 -153
- package/dist/use-sse-stream-DjqRuDft.js +0 -884
- package/dist/use-sse-stream-Dm6R5YWz.d.ts +0 -282
|
@@ -1,1279 +0,0 @@
|
|
|
1
|
-
import { o as __exportAll } from "./schema-BpW9rSSG.js";
|
|
2
|
-
import { i as focusRing } from "./focus-BcE_SHqz.js";
|
|
3
|
-
import { t as cn } from "./utils-C_uf36nf.js";
|
|
4
|
-
import { i as TabsTrigger, n as TabsContent, r as TabsList, t as Tabs } from "./tabs-BVHR6NkO.js";
|
|
5
|
-
import { t as ArtifactPane } from "./artifact-pane-DMXT9a8U.js";
|
|
6
|
-
import { t as Markdown } from "./markdown-Db08KAYF.js";
|
|
7
|
-
import { Suspense, createContext, lazy, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
8
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
-
import { PencilLine, Save, Users, Wifi, WifiOff } from "lucide-react";
|
|
10
|
-
import { marked } from "marked";
|
|
11
|
-
import TurndownService from "turndown";
|
|
12
|
-
//#region src/editor/editor-peers.ts
|
|
13
|
-
const DOCUMENT_PEERS_MISSING = "@tangle-network/ui/editor needs its optional editor peers. Install @tiptap/react and @tiptap/starter-kit.";
|
|
14
|
-
const PROVIDER_PEERS_MISSING = "@tangle-network/ui/editor needs its optional collaboration transport peers. Install @hocuspocus/provider and yjs.";
|
|
15
|
-
const COLLABORATION_PEERS_MISSING = "@tangle-network/ui/editor needs its optional collaboration peers. Install @tiptap/react, @tiptap/starter-kit, @tiptap/extension-collaboration, @tiptap/extension-collaboration-caret, @hocuspocus/provider and yjs.";
|
|
16
|
-
/**
|
|
17
|
-
* A peer the editor needs is absent, or resolved to a stub that carries none
|
|
18
|
-
* of the members the editor calls. The condition holds for the rest of the
|
|
19
|
-
* session, because a package does not install itself mid-run.
|
|
20
|
-
*/
|
|
21
|
-
var MissingEditorPeersError = class extends Error {
|
|
22
|
-
constructor(message, options) {
|
|
23
|
-
super(message, options);
|
|
24
|
-
this.name = "MissingEditorPeersError";
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
|
-
* True for the error above. The name carries the answer, so a duplicated copy
|
|
29
|
-
* of this module in a consumer's bundle still reports its own error correctly.
|
|
30
|
-
*/
|
|
31
|
-
function isMissingEditorPeersError(error) {
|
|
32
|
-
return error instanceof Error && error.name === "MissingEditorPeersError";
|
|
33
|
-
}
|
|
34
|
-
/** The messages a bundler or a runtime gives for a module it cannot resolve. */
|
|
35
|
-
const RESOLUTION_FAILURE = /could not resolve|cannot find (?:module|package)|can't resolve|failed to resolve|module not found/i;
|
|
36
|
-
/**
|
|
37
|
-
* True when the rejection says the package is not installed. Only such an
|
|
38
|
-
* error gets the install list: a transient chunk-fetch failure that reads as
|
|
39
|
-
* "install the peers" sends the reader to the wrong fix. An error this
|
|
40
|
-
* predicate does not match keeps its own message, so it can only
|
|
41
|
-
* under-report.
|
|
42
|
-
*/
|
|
43
|
-
function isMissingPeerError(error) {
|
|
44
|
-
return error instanceof Error && RESOLUTION_FAILURE.test(error.message);
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Hands an `import()` rejection on unchanged. esbuild reports an unresolvable
|
|
48
|
-
* literal `import()` as a build error and defers it to run time only when the
|
|
49
|
-
* call carries a `.catch()`, so every peer import attaches this handler. It
|
|
50
|
-
* changes nothing at run time.
|
|
51
|
-
*/
|
|
52
|
-
function rethrow(error) {
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Every optional peer the `./editor` entry needs at run time. The loaders
|
|
57
|
-
* import all but `@tiptap/core`, which the tiptap packages need in turn: a
|
|
58
|
-
* consumer that resolves it to nothing breaks the same way, so a failure that
|
|
59
|
-
* names it is a missing peer too. `scripts/validate-dist.mjs` holds the same
|
|
60
|
-
* list and rejects a build where the two disagree.
|
|
61
|
-
*/
|
|
62
|
-
const DEFERRED_PEERS = [
|
|
63
|
-
"@tiptap/core",
|
|
64
|
-
"@tiptap/react",
|
|
65
|
-
"@tiptap/starter-kit",
|
|
66
|
-
"@tiptap/extension-collaboration",
|
|
67
|
-
"@tiptap/extension-collaboration-caret",
|
|
68
|
-
"@hocuspocus/provider",
|
|
69
|
-
"yjs"
|
|
70
|
-
];
|
|
71
|
-
/** Every deferred peer a resolution failure names. */
|
|
72
|
-
function unresolvedPeersFrom(error) {
|
|
73
|
-
if (!(error instanceof Error)) return [];
|
|
74
|
-
const named = DEFERRED_PEERS.filter((name) => error.message.includes(name));
|
|
75
|
-
return named.filter((name) => !named.some((other) => other !== name && other.includes(name)));
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Turns a rejection that names an unresolved peer into the install-list error,
|
|
79
|
-
* and keeps the original as its cause. Any other rejection passes through, so
|
|
80
|
-
* a transient chunk fetch keeps its own message and stays retryable.
|
|
81
|
-
*/
|
|
82
|
-
function asMissingEditorPeersError(error, missingMessage) {
|
|
83
|
-
if (!isMissingPeerError(error)) return error;
|
|
84
|
-
const unresolved = unresolvedPeersFrom(error);
|
|
85
|
-
if (unresolved.length === 0) return error;
|
|
86
|
-
return new MissingEditorPeersError(missingMessage + (unresolved.length === 1 ? ` ${unresolved[0]} did not resolve.` : ""), { cause: error });
|
|
87
|
-
}
|
|
88
|
-
async function loadPeers(load, missingMessage) {
|
|
89
|
-
try {
|
|
90
|
-
return await load();
|
|
91
|
-
} catch (error) {
|
|
92
|
-
throw asMissingEditorPeersError(error, missingMessage);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* True for a tiptap extension the editor can configure. A bundler can stub a
|
|
97
|
-
* missing optional peer as a silent namespace whose default export is an empty
|
|
98
|
-
* object, which is defined but carries no `configure`. Reading the member the
|
|
99
|
-
* factories call separates that shape from a real extension.
|
|
100
|
-
*/
|
|
101
|
-
function isConfigurableExtension(value) {
|
|
102
|
-
return typeof value?.configure === "function";
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Reads the members the editor calls, so a stub namespace fails with the
|
|
106
|
-
* install list, and not as an undefined-property crash in the middle of a
|
|
107
|
-
* render.
|
|
108
|
-
*/
|
|
109
|
-
function assertDocumentEditorPeers(peers, missingMessage) {
|
|
110
|
-
if (typeof peers.react.useEditor !== "function" || peers.react.EditorContent === void 0 || !isConfigurableExtension(peers.starterKit.default)) throw new MissingEditorPeersError(missingMessage);
|
|
111
|
-
}
|
|
112
|
-
function assertEditorProviderPeers(peers, missingMessage) {
|
|
113
|
-
if (typeof peers.hocuspocus.HocuspocusProvider !== "function" || typeof peers.yjs.Doc !== "function") throw new MissingEditorPeersError(missingMessage);
|
|
114
|
-
}
|
|
115
|
-
function assertCollaborationPeers(peers) {
|
|
116
|
-
assertDocumentEditorPeers(peers, COLLABORATION_PEERS_MISSING);
|
|
117
|
-
assertEditorProviderPeers(peers, COLLABORATION_PEERS_MISSING);
|
|
118
|
-
if (!isConfigurableExtension(peers.collaboration.default) || !isConfigurableExtension(peers.collaborationCaret.default)) throw new MissingEditorPeersError(COLLABORATION_PEERS_MISSING);
|
|
119
|
-
}
|
|
120
|
-
async function importDocumentEditorPeers() {
|
|
121
|
-
const [react, starterKit] = await Promise.all([import("@tiptap/react").catch(rethrow), import("@tiptap/starter-kit").catch(rethrow)]);
|
|
122
|
-
return {
|
|
123
|
-
react,
|
|
124
|
-
starterKit
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
async function importEditorProviderPeers() {
|
|
128
|
-
const [hocuspocus, yjs] = await Promise.all([import("@hocuspocus/provider").catch(rethrow), import("yjs").catch(rethrow)]);
|
|
129
|
-
return {
|
|
130
|
-
hocuspocus,
|
|
131
|
-
yjs
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
/** Resolve the collaboration transport's peers, or throw and name them. */
|
|
135
|
-
async function loadEditorProviderPeers() {
|
|
136
|
-
const peers = await loadPeers(importEditorProviderPeers, PROVIDER_PEERS_MISSING);
|
|
137
|
-
assertEditorProviderPeers(peers, PROVIDER_PEERS_MISSING);
|
|
138
|
-
return peers;
|
|
139
|
-
}
|
|
140
|
-
/** Resolve the local markdown editor's peers, or throw and name them. */
|
|
141
|
-
async function loadDocumentEditorPeers() {
|
|
142
|
-
const peers = await loadPeers(importDocumentEditorPeers, DOCUMENT_PEERS_MISSING);
|
|
143
|
-
assertDocumentEditorPeers(peers, DOCUMENT_PEERS_MISSING);
|
|
144
|
-
return peers;
|
|
145
|
-
}
|
|
146
|
-
/** Resolve the collaborative editor's peers, or throw and name them. */
|
|
147
|
-
async function loadCollaborationPeers() {
|
|
148
|
-
const peers = await loadPeers(async () => {
|
|
149
|
-
const [documentPeers, providerPeers, collaboration, collaborationCaret] = await Promise.all([
|
|
150
|
-
importDocumentEditorPeers(),
|
|
151
|
-
importEditorProviderPeers(),
|
|
152
|
-
import("@tiptap/extension-collaboration").catch(rethrow),
|
|
153
|
-
import("@tiptap/extension-collaboration-caret").catch(rethrow)
|
|
154
|
-
]);
|
|
155
|
-
return {
|
|
156
|
-
...documentPeers,
|
|
157
|
-
...providerPeers,
|
|
158
|
-
collaboration,
|
|
159
|
-
collaborationCaret
|
|
160
|
-
};
|
|
161
|
-
}, COLLABORATION_PEERS_MISSING);
|
|
162
|
-
assertCollaborationPeers(peers);
|
|
163
|
-
return peers;
|
|
164
|
-
}
|
|
165
|
-
//#endregion
|
|
166
|
-
//#region src/editor/editor-lazy.ts
|
|
167
|
-
/**
|
|
168
|
-
* Builds a `lazy` editor component that a remount can re-attempt.
|
|
169
|
-
*
|
|
170
|
-
* `lazy` caches its first rejection for the life of the component. That is
|
|
171
|
-
* right for a missing peer, which stays missing for the session, and wrong for
|
|
172
|
-
* a transient chunk fetch: one failed network request would otherwise hold the
|
|
173
|
-
* editor broken until a full page reload.
|
|
174
|
-
*
|
|
175
|
-
* The returned function gives the component to render, and a caller must call
|
|
176
|
-
* it on every render: a transient failure replaces the component, and a caller
|
|
177
|
-
* that holds the one it got earlier keeps replaying the cached rejection.
|
|
178
|
-
*
|
|
179
|
-
* A `MissingEditorPeersError` keeps the cached rejection, so the install list
|
|
180
|
-
* reaches the consumer's error boundary once and the loader does not run
|
|
181
|
-
* again. Any other rejection replaces the component, so the next mount — an
|
|
182
|
-
* error boundary that resets, or a remount by the parent — starts a fresh
|
|
183
|
-
* attempt.
|
|
184
|
-
*/
|
|
185
|
-
function retryableLazyEditor(loadComponent) {
|
|
186
|
-
function build() {
|
|
187
|
-
return lazy(async () => {
|
|
188
|
-
try {
|
|
189
|
-
return { default: await loadComponent() };
|
|
190
|
-
} catch (error) {
|
|
191
|
-
if (!isMissingEditorPeersError(error)) current = build();
|
|
192
|
-
throw error;
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
let current = build();
|
|
197
|
-
return () => current;
|
|
198
|
-
}
|
|
199
|
-
//#endregion
|
|
200
|
-
//#region src/editor/editor-provider.tsx
|
|
201
|
-
const EditorContext = createContext(null);
|
|
202
|
-
/**
|
|
203
|
-
* Generate a random color for user presence.
|
|
204
|
-
*/
|
|
205
|
-
function generateUserColor() {
|
|
206
|
-
const colors = [
|
|
207
|
-
"#FF6B6B",
|
|
208
|
-
"#4ECDC4",
|
|
209
|
-
"#45B7D1",
|
|
210
|
-
"#96CEB4",
|
|
211
|
-
"#FFEAA7",
|
|
212
|
-
"#DDA0DD",
|
|
213
|
-
"#98D8C8",
|
|
214
|
-
"#F7DC6F",
|
|
215
|
-
"#BB8FCE",
|
|
216
|
-
"#85C1E9"
|
|
217
|
-
];
|
|
218
|
-
return colors[Math.floor(Math.random() * colors.length)];
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Builds the collaboration provider against loaded yjs and Hocuspocus
|
|
222
|
-
* namespaces. The peers arrive as an argument so that this module reaches
|
|
223
|
-
* them through type-only imports, which a bundler erases.
|
|
224
|
-
*/
|
|
225
|
-
function createEditorProvider(peers) {
|
|
226
|
-
const { Doc } = peers.yjs;
|
|
227
|
-
const { HocuspocusProvider } = peers.hocuspocus;
|
|
228
|
-
return function EditorProvider({ websocketUrl, documentName, token, tokenExpiresAt, user, autoConnect = true, autoReconnect = true, maxReconnectAttempts = 5, onConnectionChange, onSync, onAuthError, onRefreshToken, children }) {
|
|
229
|
-
const [connectionState, setConnectionState] = useState("disconnected");
|
|
230
|
-
const [collaborators, setCollaborators] = useState([]);
|
|
231
|
-
const [isSynced, setIsSynced] = useState(false);
|
|
232
|
-
const docRef = useRef(null);
|
|
233
|
-
const providerRef = useRef(null);
|
|
234
|
-
const reconnectAttemptsRef = useRef(0);
|
|
235
|
-
const tokenRef = useRef(token);
|
|
236
|
-
const tokenExpiryRef = useRef(tokenExpiresAt);
|
|
237
|
-
const refreshPromiseRef = useRef(null);
|
|
238
|
-
const refreshTimerRef = useRef(null);
|
|
239
|
-
tokenRef.current = token;
|
|
240
|
-
tokenExpiryRef.current = tokenExpiresAt;
|
|
241
|
-
if (!docRef.current) docRef.current = new Doc();
|
|
242
|
-
const doc = docRef.current;
|
|
243
|
-
const userColor = useMemo(() => user.color ?? generateUserColor(), [user.color]);
|
|
244
|
-
const updateConnectionState = useCallback((state) => {
|
|
245
|
-
setConnectionState(state);
|
|
246
|
-
onConnectionChange?.(state);
|
|
247
|
-
}, [onConnectionChange]);
|
|
248
|
-
const updateCollaborators = useCallback((awareness) => {
|
|
249
|
-
if (!awareness) return;
|
|
250
|
-
const states = awareness.getStates();
|
|
251
|
-
const collabs = [];
|
|
252
|
-
states.forEach((state, clientId) => {
|
|
253
|
-
if (clientId === awareness.clientID) return;
|
|
254
|
-
if (state.user) collabs.push({
|
|
255
|
-
clientId,
|
|
256
|
-
user: state.user
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
setCollaborators(collabs);
|
|
260
|
-
}, []);
|
|
261
|
-
const clearRefreshTimer = useCallback(() => {
|
|
262
|
-
if (refreshTimerRef.current != null) {
|
|
263
|
-
window.clearTimeout(refreshTimerRef.current);
|
|
264
|
-
refreshTimerRef.current = null;
|
|
265
|
-
}
|
|
266
|
-
}, []);
|
|
267
|
-
const refreshToken = useCallback(async () => {
|
|
268
|
-
if (!onRefreshToken) return null;
|
|
269
|
-
if (refreshPromiseRef.current) return refreshPromiseRef.current;
|
|
270
|
-
const refreshPromise = (async () => {
|
|
271
|
-
const next = await onRefreshToken();
|
|
272
|
-
const resolvedToken = typeof next === "string" ? next : next.token;
|
|
273
|
-
const resolvedExpiry = typeof next === "string" ? void 0 : next.expiresAt;
|
|
274
|
-
tokenRef.current = resolvedToken;
|
|
275
|
-
tokenExpiryRef.current = resolvedExpiry;
|
|
276
|
-
return resolvedToken;
|
|
277
|
-
})().catch((error) => {
|
|
278
|
-
onAuthError?.(error instanceof Error ? error : new Error(String(error)));
|
|
279
|
-
return null;
|
|
280
|
-
}).finally(() => {
|
|
281
|
-
refreshPromiseRef.current = null;
|
|
282
|
-
});
|
|
283
|
-
refreshPromiseRef.current = refreshPromise;
|
|
284
|
-
return refreshPromise;
|
|
285
|
-
}, [onAuthError, onRefreshToken]);
|
|
286
|
-
const scheduleTokenRefresh = useCallback(() => {
|
|
287
|
-
clearRefreshTimer();
|
|
288
|
-
if (!tokenExpiryRef.current || !onRefreshToken || typeof window === "undefined") return;
|
|
289
|
-
const delay = tokenExpiryRef.current * 1e3 - 6e4 - Date.now();
|
|
290
|
-
if (delay <= 0) {
|
|
291
|
-
refreshToken();
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
refreshTimerRef.current = window.setTimeout(() => {
|
|
295
|
-
refreshToken();
|
|
296
|
-
}, delay);
|
|
297
|
-
}, [
|
|
298
|
-
clearRefreshTimer,
|
|
299
|
-
onRefreshToken,
|
|
300
|
-
refreshToken
|
|
301
|
-
]);
|
|
302
|
-
const connect = useCallback(() => {
|
|
303
|
-
if (providerRef.current) {
|
|
304
|
-
providerRef.current.connect();
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
updateConnectionState("connecting");
|
|
308
|
-
const provider = new HocuspocusProvider({
|
|
309
|
-
url: websocketUrl,
|
|
310
|
-
name: documentName,
|
|
311
|
-
document: doc,
|
|
312
|
-
token: async () => tokenRef.current,
|
|
313
|
-
onConnect: () => {
|
|
314
|
-
reconnectAttemptsRef.current = 0;
|
|
315
|
-
updateConnectionState("connected");
|
|
316
|
-
scheduleTokenRefresh();
|
|
317
|
-
},
|
|
318
|
-
onSynced: () => {
|
|
319
|
-
setIsSynced(true);
|
|
320
|
-
updateConnectionState("synced");
|
|
321
|
-
onSync?.();
|
|
322
|
-
},
|
|
323
|
-
onDisconnect: () => {
|
|
324
|
-
updateConnectionState("disconnected");
|
|
325
|
-
setIsSynced(false);
|
|
326
|
-
clearRefreshTimer();
|
|
327
|
-
if (autoReconnect && reconnectAttemptsRef.current < maxReconnectAttempts) {
|
|
328
|
-
reconnectAttemptsRef.current += 1;
|
|
329
|
-
const delay = Math.min(1e3 * 2 ** reconnectAttemptsRef.current, 3e4);
|
|
330
|
-
setTimeout(() => {
|
|
331
|
-
if (providerRef.current && !providerRef.current.isConnected) providerRef.current.connect();
|
|
332
|
-
}, delay);
|
|
333
|
-
}
|
|
334
|
-
},
|
|
335
|
-
onAuthenticationFailed: ({ reason }) => {
|
|
336
|
-
const error = new Error(reason ?? "Authentication failed");
|
|
337
|
-
updateConnectionState("disconnected");
|
|
338
|
-
clearRefreshTimer();
|
|
339
|
-
if (onRefreshToken) {
|
|
340
|
-
refreshToken().then((nextToken) => {
|
|
341
|
-
if (nextToken && providerRef.current) {
|
|
342
|
-
providerRef.current.connect();
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
onAuthError?.(error);
|
|
346
|
-
});
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
onAuthError?.(error);
|
|
350
|
-
},
|
|
351
|
-
onAwarenessUpdate: () => {
|
|
352
|
-
updateCollaborators(provider.awareness);
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
provider.awareness?.setLocalStateField("user", {
|
|
356
|
-
name: user.name,
|
|
357
|
-
color: userColor,
|
|
358
|
-
userId: user.userId
|
|
359
|
-
});
|
|
360
|
-
providerRef.current = provider;
|
|
361
|
-
}, [
|
|
362
|
-
websocketUrl,
|
|
363
|
-
documentName,
|
|
364
|
-
doc,
|
|
365
|
-
user.name,
|
|
366
|
-
user.userId,
|
|
367
|
-
userColor,
|
|
368
|
-
autoReconnect,
|
|
369
|
-
maxReconnectAttempts,
|
|
370
|
-
clearRefreshTimer,
|
|
371
|
-
updateConnectionState,
|
|
372
|
-
updateCollaborators,
|
|
373
|
-
onSync,
|
|
374
|
-
onAuthError,
|
|
375
|
-
onRefreshToken,
|
|
376
|
-
refreshToken,
|
|
377
|
-
scheduleTokenRefresh
|
|
378
|
-
]);
|
|
379
|
-
const disconnect = useCallback(() => {
|
|
380
|
-
if (providerRef.current) {
|
|
381
|
-
providerRef.current.disconnect();
|
|
382
|
-
updateConnectionState("disconnected");
|
|
383
|
-
}
|
|
384
|
-
}, [updateConnectionState]);
|
|
385
|
-
useEffect(() => {
|
|
386
|
-
if (autoConnect) connect();
|
|
387
|
-
return () => {
|
|
388
|
-
clearRefreshTimer();
|
|
389
|
-
if (providerRef.current) {
|
|
390
|
-
providerRef.current.destroy();
|
|
391
|
-
providerRef.current = null;
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
}, [
|
|
395
|
-
autoConnect,
|
|
396
|
-
clearRefreshTimer,
|
|
397
|
-
connect
|
|
398
|
-
]);
|
|
399
|
-
const contextValue = useMemo(() => ({
|
|
400
|
-
doc,
|
|
401
|
-
provider: providerRef.current,
|
|
402
|
-
connectionState,
|
|
403
|
-
collaborators,
|
|
404
|
-
isSynced,
|
|
405
|
-
connect,
|
|
406
|
-
disconnect
|
|
407
|
-
}), [
|
|
408
|
-
doc,
|
|
409
|
-
connectionState,
|
|
410
|
-
collaborators,
|
|
411
|
-
isSynced,
|
|
412
|
-
connect,
|
|
413
|
-
disconnect
|
|
414
|
-
]);
|
|
415
|
-
return /* @__PURE__ */ jsx(EditorContext.Provider, {
|
|
416
|
-
value: contextValue,
|
|
417
|
-
children
|
|
418
|
-
});
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
const lazyEditorProvider = retryableLazyEditor(async () => createEditorProvider(await loadEditorProviderPeers()));
|
|
422
|
-
/**
|
|
423
|
-
* EditorProvider wraps children with Hocuspocus collaboration context.
|
|
424
|
-
* Manages WebSocket connection, Y.Doc, and awareness state. Loads `yjs` and
|
|
425
|
-
* `@hocuspocus/provider` — and no tiptap package — before it renders children,
|
|
426
|
-
* because every child hook reads a context that only the loaded provider can
|
|
427
|
-
* supply. A consumer that drives its own editor from that context therefore
|
|
428
|
-
* needs those two peers alone.
|
|
429
|
-
*/
|
|
430
|
-
function EditorProvider(props) {
|
|
431
|
-
const LazyEditorProvider = lazyEditorProvider();
|
|
432
|
-
return /* @__PURE__ */ jsx(Suspense, {
|
|
433
|
-
fallback: null,
|
|
434
|
-
children: /* @__PURE__ */ jsx(LazyEditorProvider, { ...props })
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
/**
|
|
438
|
-
* Hook to access the editor context.
|
|
439
|
-
* Must be used within an EditorProvider.
|
|
440
|
-
*/
|
|
441
|
-
function useEditorContext() {
|
|
442
|
-
const context = useContext(EditorContext);
|
|
443
|
-
if (!context) throw new Error("useEditorContext must be used within an EditorProvider");
|
|
444
|
-
return context;
|
|
445
|
-
}
|
|
446
|
-
//#endregion
|
|
447
|
-
//#region src/editor/editor-loading.tsx
|
|
448
|
-
/**
|
|
449
|
-
* Stands in for an editor while its optional peers load. The editors reach
|
|
450
|
-
* tiptap through a dynamic import, so an editor's first frame waits for that
|
|
451
|
-
* chunk. The live region announces the wait, which a screen reader has no
|
|
452
|
-
* other way to notice. Callers put their own minimum height first, so a
|
|
453
|
-
* caller-supplied className still wins.
|
|
454
|
-
*/
|
|
455
|
-
function EditorLoadingPlaceholder({ className }) {
|
|
456
|
-
return /* @__PURE__ */ jsx("div", {
|
|
457
|
-
role: "status",
|
|
458
|
-
"aria-live": "polite",
|
|
459
|
-
className: cn("flex w-full items-center justify-center rounded-lg border border-border border-dashed bg-muted text-muted-foreground text-sm", className),
|
|
460
|
-
children: "Loading editor…"
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
//#endregion
|
|
464
|
-
//#region src/editor/editor-toolbar.tsx
|
|
465
|
-
/**
|
|
466
|
-
* Editor toolbar component.
|
|
467
|
-
* Provides basic formatting controls.
|
|
468
|
-
*/
|
|
469
|
-
function EditorToolbar({ editor, className }) {
|
|
470
|
-
if (!editor) return null;
|
|
471
|
-
const buttons = [
|
|
472
|
-
{
|
|
473
|
-
id: "bold",
|
|
474
|
-
label: "Bold",
|
|
475
|
-
action: () => editor.chain().focus().toggleBold().run(),
|
|
476
|
-
isActive: editor.isActive("bold"),
|
|
477
|
-
shortcut: "Ctrl+B"
|
|
478
|
-
},
|
|
479
|
-
{
|
|
480
|
-
id: "italic",
|
|
481
|
-
label: "Italic",
|
|
482
|
-
action: () => editor.chain().focus().toggleItalic().run(),
|
|
483
|
-
isActive: editor.isActive("italic"),
|
|
484
|
-
shortcut: "Ctrl+I"
|
|
485
|
-
},
|
|
486
|
-
{
|
|
487
|
-
id: "strike",
|
|
488
|
-
label: "Strike",
|
|
489
|
-
action: () => editor.chain().focus().toggleStrike().run(),
|
|
490
|
-
isActive: editor.isActive("strike"),
|
|
491
|
-
shortcut: "Ctrl+Shift+X"
|
|
492
|
-
},
|
|
493
|
-
{
|
|
494
|
-
id: "code",
|
|
495
|
-
label: "Code",
|
|
496
|
-
action: () => editor.chain().focus().toggleCode().run(),
|
|
497
|
-
isActive: editor.isActive("code"),
|
|
498
|
-
shortcut: "Ctrl+E"
|
|
499
|
-
},
|
|
500
|
-
{
|
|
501
|
-
id: "sep-1",
|
|
502
|
-
type: "separator"
|
|
503
|
-
},
|
|
504
|
-
{
|
|
505
|
-
id: "h1",
|
|
506
|
-
label: "H1",
|
|
507
|
-
action: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
|
508
|
-
isActive: editor.isActive("heading", { level: 1 })
|
|
509
|
-
},
|
|
510
|
-
{
|
|
511
|
-
id: "h2",
|
|
512
|
-
label: "H2",
|
|
513
|
-
action: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
514
|
-
isActive: editor.isActive("heading", { level: 2 })
|
|
515
|
-
},
|
|
516
|
-
{
|
|
517
|
-
id: "h3",
|
|
518
|
-
label: "H3",
|
|
519
|
-
action: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
520
|
-
isActive: editor.isActive("heading", { level: 3 })
|
|
521
|
-
},
|
|
522
|
-
{
|
|
523
|
-
id: "sep-2",
|
|
524
|
-
type: "separator"
|
|
525
|
-
},
|
|
526
|
-
{
|
|
527
|
-
id: "bullet-list",
|
|
528
|
-
label: "Bullet List",
|
|
529
|
-
action: () => editor.chain().focus().toggleBulletList().run(),
|
|
530
|
-
isActive: editor.isActive("bulletList")
|
|
531
|
-
},
|
|
532
|
-
{
|
|
533
|
-
id: "ordered-list",
|
|
534
|
-
label: "Ordered List",
|
|
535
|
-
action: () => editor.chain().focus().toggleOrderedList().run(),
|
|
536
|
-
isActive: editor.isActive("orderedList")
|
|
537
|
-
},
|
|
538
|
-
{
|
|
539
|
-
id: "code-block",
|
|
540
|
-
label: "Code Block",
|
|
541
|
-
action: () => editor.chain().focus().toggleCodeBlock().run(),
|
|
542
|
-
isActive: editor.isActive("codeBlock")
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
id: "blockquote",
|
|
546
|
-
label: "Blockquote",
|
|
547
|
-
action: () => editor.chain().focus().toggleBlockquote().run(),
|
|
548
|
-
isActive: editor.isActive("blockquote")
|
|
549
|
-
}
|
|
550
|
-
];
|
|
551
|
-
return /* @__PURE__ */ jsx("div", {
|
|
552
|
-
className: cn("flex items-center gap-1 border-border border-b bg-card p-2", className),
|
|
553
|
-
children: buttons.map((button) => {
|
|
554
|
-
if ("type" in button && button.type === "separator") return /* @__PURE__ */ jsx("div", { className: "mx-1 h-6 w-px bg-border" }, button.id);
|
|
555
|
-
return /* @__PURE__ */ jsx("button", {
|
|
556
|
-
onClick: button.action,
|
|
557
|
-
type: "button",
|
|
558
|
-
title: "shortcut" in button ? `${button.label} (${button.shortcut})` : button.label,
|
|
559
|
-
className: cn("rounded px-2 py-1 font-medium text-xs transition-colors", "hover:bg-accent hover:text-accent-foreground", button.isActive && "bg-accent text-accent-foreground"),
|
|
560
|
-
children: button.label
|
|
561
|
-
}, button.id);
|
|
562
|
-
})
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
//#endregion
|
|
566
|
-
//#region src/editor/tiptap-editor.tsx
|
|
567
|
-
/**
|
|
568
|
-
* Cursor colors with contrasting text.
|
|
569
|
-
*/
|
|
570
|
-
const cursorColors = {
|
|
571
|
-
"#FF6B6B": {
|
|
572
|
-
background: "#FF6B6B",
|
|
573
|
-
text: "#FFFFFF"
|
|
574
|
-
},
|
|
575
|
-
"#4ECDC4": {
|
|
576
|
-
background: "#4ECDC4",
|
|
577
|
-
text: "#000000"
|
|
578
|
-
},
|
|
579
|
-
"#45B7D1": {
|
|
580
|
-
background: "#45B7D1",
|
|
581
|
-
text: "#000000"
|
|
582
|
-
},
|
|
583
|
-
"#96CEB4": {
|
|
584
|
-
background: "#96CEB4",
|
|
585
|
-
text: "#000000"
|
|
586
|
-
},
|
|
587
|
-
"#FFEAA7": {
|
|
588
|
-
background: "#FFEAA7",
|
|
589
|
-
text: "#000000"
|
|
590
|
-
},
|
|
591
|
-
"#DDA0DD": {
|
|
592
|
-
background: "#DDA0DD",
|
|
593
|
-
text: "#000000"
|
|
594
|
-
},
|
|
595
|
-
"#98D8C8": {
|
|
596
|
-
background: "#98D8C8",
|
|
597
|
-
text: "#000000"
|
|
598
|
-
},
|
|
599
|
-
"#F7DC6F": {
|
|
600
|
-
background: "#F7DC6F",
|
|
601
|
-
text: "#000000"
|
|
602
|
-
},
|
|
603
|
-
"#BB8FCE": {
|
|
604
|
-
background: "#BB8FCE",
|
|
605
|
-
text: "#000000"
|
|
606
|
-
},
|
|
607
|
-
"#85C1E9": {
|
|
608
|
-
background: "#85C1E9",
|
|
609
|
-
text: "#000000"
|
|
610
|
-
}
|
|
611
|
-
};
|
|
612
|
-
/**
|
|
613
|
-
* Get cursor label colors based on user color.
|
|
614
|
-
*/
|
|
615
|
-
function getCursorColors(color) {
|
|
616
|
-
return cursorColors[color] ?? {
|
|
617
|
-
background: color,
|
|
618
|
-
text: "#FFFFFF"
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
/**
|
|
622
|
-
* Builds the collaborative editor against loaded tiptap namespaces. The peers
|
|
623
|
-
* arrive as an argument so that this module reaches them through type-only
|
|
624
|
-
* imports, which a bundler erases.
|
|
625
|
-
*/
|
|
626
|
-
function createTiptapEditor(peers) {
|
|
627
|
-
const { EditorContent, useEditor } = peers.react;
|
|
628
|
-
const StarterKit = peers.starterKit.default;
|
|
629
|
-
const Collaboration = peers.collaboration.default;
|
|
630
|
-
const CollaborationCaret = peers.collaborationCaret.default;
|
|
631
|
-
return function TiptapEditor({ initialContent, placeholder = "Start writing...", readOnly = false, autoFocus = false, className, contentClassName, onUpdate, onSelectionUpdate, onReady }) {
|
|
632
|
-
const { doc, provider, connectionState } = useEditorContext();
|
|
633
|
-
const fragment = useMemo(() => doc.getXmlFragment("prosemirror"), [doc]);
|
|
634
|
-
const extensions = useMemo(() => {
|
|
635
|
-
const baseExtensions = [StarterKit.configure({
|
|
636
|
-
history: false,
|
|
637
|
-
codeBlock: { HTMLAttributes: { class: "hljs" } }
|
|
638
|
-
}), Collaboration.configure({ fragment })];
|
|
639
|
-
if (provider?.awareness) baseExtensions.push(CollaborationCaret.configure({
|
|
640
|
-
provider,
|
|
641
|
-
user: provider.awareness.getLocalState()?.user ?? {
|
|
642
|
-
name: "Anonymous",
|
|
643
|
-
color: "#808080"
|
|
644
|
-
},
|
|
645
|
-
render: (user) => {
|
|
646
|
-
const { background, text } = getCursorColors(user.color);
|
|
647
|
-
const cursor = document.createElement("span");
|
|
648
|
-
cursor.className = "collaboration-cursor";
|
|
649
|
-
cursor.style.borderColor = background;
|
|
650
|
-
const label = document.createElement("span");
|
|
651
|
-
label.className = "collaboration-cursor-label";
|
|
652
|
-
label.style.backgroundColor = background;
|
|
653
|
-
label.style.color = text;
|
|
654
|
-
label.textContent = user.name;
|
|
655
|
-
cursor.appendChild(label);
|
|
656
|
-
return cursor;
|
|
657
|
-
}
|
|
658
|
-
}));
|
|
659
|
-
return baseExtensions;
|
|
660
|
-
}, [fragment, provider]);
|
|
661
|
-
const editor = useEditor({
|
|
662
|
-
extensions,
|
|
663
|
-
editable: !readOnly,
|
|
664
|
-
autofocus: autoFocus,
|
|
665
|
-
editorProps: { attributes: {
|
|
666
|
-
class: cn("prose prose-sm sm:prose-base dark:prose-invert max-w-none", "focus:outline-none", contentClassName),
|
|
667
|
-
"data-placeholder": placeholder
|
|
668
|
-
} },
|
|
669
|
-
onUpdate: ({ editor: ed }) => {
|
|
670
|
-
onUpdate?.(ed);
|
|
671
|
-
},
|
|
672
|
-
onSelectionUpdate: ({ editor: ed }) => {
|
|
673
|
-
onSelectionUpdate?.(ed);
|
|
674
|
-
},
|
|
675
|
-
onCreate: ({ editor: ed }) => {
|
|
676
|
-
onReady?.(ed);
|
|
677
|
-
}
|
|
678
|
-
});
|
|
679
|
-
useEffect(() => {
|
|
680
|
-
if (editor) editor.setEditable(!readOnly);
|
|
681
|
-
}, [editor, readOnly]);
|
|
682
|
-
useEffect(() => {
|
|
683
|
-
if (editor && initialContent && connectionState === "synced" && editor.isEmpty) editor.commands.setContent(initialContent);
|
|
684
|
-
}, [
|
|
685
|
-
editor,
|
|
686
|
-
initialContent,
|
|
687
|
-
connectionState
|
|
688
|
-
]);
|
|
689
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
690
|
-
className: cn("relative min-h-[200px] w-full rounded-lg border border-border", "bg-background", className),
|
|
691
|
-
children: [
|
|
692
|
-
/* @__PURE__ */ jsx("div", {
|
|
693
|
-
className: "absolute top-2 right-2 z-10",
|
|
694
|
-
children: /* @__PURE__ */ jsx(ConnectionIndicator, { state: connectionState })
|
|
695
|
-
}),
|
|
696
|
-
/* @__PURE__ */ jsx("div", {
|
|
697
|
-
className: "p-4 pt-10",
|
|
698
|
-
children: /* @__PURE__ */ jsx(EditorContent, { editor })
|
|
699
|
-
}),
|
|
700
|
-
/* @__PURE__ */ jsx("style", { children: `
|
|
701
|
-
.ProseMirror p.is-editor-empty:first-child::before {
|
|
702
|
-
content: attr(data-placeholder);
|
|
703
|
-
float: left;
|
|
704
|
-
color: var(--muted-foreground, #999);
|
|
705
|
-
pointer-events: none;
|
|
706
|
-
height: 0;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
.collaboration-cursor {
|
|
710
|
-
position: relative;
|
|
711
|
-
border-left: 2px solid;
|
|
712
|
-
margin-left: -1px;
|
|
713
|
-
margin-right: -1px;
|
|
714
|
-
pointer-events: none;
|
|
715
|
-
word-break: normal;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
.collaboration-cursor-label {
|
|
719
|
-
position: absolute;
|
|
720
|
-
top: -1.4em;
|
|
721
|
-
left: -1px;
|
|
722
|
-
font-size: 12px;
|
|
723
|
-
font-style: normal;
|
|
724
|
-
font-weight: 600;
|
|
725
|
-
line-height: normal;
|
|
726
|
-
padding: 2px 6px;
|
|
727
|
-
border-radius: 4px 4px 4px 0;
|
|
728
|
-
white-space: nowrap;
|
|
729
|
-
user-select: none;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
.ProseMirror pre {
|
|
733
|
-
background: var(--muted, #f4f4f5);
|
|
734
|
-
border-radius: 0.375rem;
|
|
735
|
-
padding: 0.75rem 1rem;
|
|
736
|
-
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
737
|
-
font-size: 0.875rem;
|
|
738
|
-
overflow-x: auto;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
.dark .ProseMirror pre {
|
|
742
|
-
background: var(--muted, #27272a);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
.ProseMirror code {
|
|
746
|
-
background: var(--muted, #f4f4f5);
|
|
747
|
-
border-radius: 0.25rem;
|
|
748
|
-
padding: 0.125rem 0.25rem;
|
|
749
|
-
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
750
|
-
font-size: 0.875em;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
.dark .ProseMirror code {
|
|
754
|
-
background: var(--muted, #27272a);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
.ProseMirror pre code {
|
|
758
|
-
background: transparent;
|
|
759
|
-
padding: 0;
|
|
760
|
-
font-size: inherit;
|
|
761
|
-
}
|
|
762
|
-
` })
|
|
763
|
-
]
|
|
764
|
-
});
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
const lazyTiptapEditor = retryableLazyEditor(async () => createTiptapEditor(await loadCollaborationPeers()));
|
|
768
|
-
/**
|
|
769
|
-
* TiptapEditor - Collaborative markdown editor with Y.js sync.
|
|
770
|
-
* Must be used within an EditorProvider. Loads its tiptap, Hocuspocus and
|
|
771
|
-
* yjs peers on first render.
|
|
772
|
-
*/
|
|
773
|
-
function TiptapEditor(props) {
|
|
774
|
-
const LazyTiptapEditor = lazyTiptapEditor();
|
|
775
|
-
return /* @__PURE__ */ jsx(Suspense, {
|
|
776
|
-
fallback: /* @__PURE__ */ jsx(EditorLoadingPlaceholder, { className: cn("min-h-[200px]", props.className) }),
|
|
777
|
-
children: /* @__PURE__ */ jsx(LazyTiptapEditor, { ...props })
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
/**
|
|
781
|
-
* Connection status indicator component.
|
|
782
|
-
*/
|
|
783
|
-
function ConnectionIndicator({ state }) {
|
|
784
|
-
const config = {
|
|
785
|
-
disconnected: {
|
|
786
|
-
color: "bg-red-500",
|
|
787
|
-
label: "Disconnected"
|
|
788
|
-
},
|
|
789
|
-
connecting: {
|
|
790
|
-
color: "bg-yellow-500 animate-pulse",
|
|
791
|
-
label: "Connecting..."
|
|
792
|
-
},
|
|
793
|
-
connected: {
|
|
794
|
-
color: "bg-blue-500",
|
|
795
|
-
label: "Connected"
|
|
796
|
-
},
|
|
797
|
-
synced: {
|
|
798
|
-
color: "bg-green-500",
|
|
799
|
-
label: "Synced"
|
|
800
|
-
}
|
|
801
|
-
}[state];
|
|
802
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
803
|
-
className: "flex items-center gap-1.5 text-muted-foreground text-xs",
|
|
804
|
-
children: [/* @__PURE__ */ jsx("span", { className: cn("h-2 w-2 rounded-full", config.color) }), /* @__PURE__ */ jsx("span", { children: config.label })]
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
/**
|
|
808
|
-
* Collaborators list component.
|
|
809
|
-
* Shows active users in the document.
|
|
810
|
-
*/
|
|
811
|
-
function CollaboratorsList({ collaborators, className }) {
|
|
812
|
-
if (collaborators.length === 0) return null;
|
|
813
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
814
|
-
className: cn("flex items-center gap-1", className),
|
|
815
|
-
children: [collaborators.slice(0, 5).map((collab) => /* @__PURE__ */ jsx("div", {
|
|
816
|
-
className: "flex h-6 w-6 items-center justify-center rounded-full font-medium text-xs",
|
|
817
|
-
style: { backgroundColor: collab.user.color },
|
|
818
|
-
title: collab.user.name,
|
|
819
|
-
children: collab.user.name.charAt(0).toUpperCase()
|
|
820
|
-
}, collab.clientId)), collaborators.length > 5 && /* @__PURE__ */ jsxs("div", {
|
|
821
|
-
className: "flex h-6 w-6 items-center justify-center rounded-full bg-muted font-medium text-xs",
|
|
822
|
-
children: ["+", collaborators.length - 5]
|
|
823
|
-
})]
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
//#endregion
|
|
827
|
-
//#region src/editor/markdown-conversion.ts
|
|
828
|
-
const turndown = new TurndownService({
|
|
829
|
-
bulletListMarker: "-",
|
|
830
|
-
codeBlockStyle: "fenced",
|
|
831
|
-
emDelimiter: "*",
|
|
832
|
-
headingStyle: "atx"
|
|
833
|
-
});
|
|
834
|
-
function markdownToHtml(markdown) {
|
|
835
|
-
return String(marked.parse(markdown, {
|
|
836
|
-
async: false,
|
|
837
|
-
gfm: true
|
|
838
|
-
}));
|
|
839
|
-
}
|
|
840
|
-
function htmlToMarkdown(html) {
|
|
841
|
-
return turndown.turndown(html);
|
|
842
|
-
}
|
|
843
|
-
function normalizeMarkdown(markdown) {
|
|
844
|
-
return markdown.replace(/\r\n/g, "\n").trimEnd();
|
|
845
|
-
}
|
|
846
|
-
//#endregion
|
|
847
|
-
//#region src/editor/markdown-document-editor.tsx
|
|
848
|
-
/**
|
|
849
|
-
* Builds the local markdown editor against loaded tiptap namespaces. The
|
|
850
|
-
* peers arrive as an argument so that this module reaches them through
|
|
851
|
-
* type-only imports, which a bundler erases.
|
|
852
|
-
*/
|
|
853
|
-
function createMarkdownDocumentEditor(peers) {
|
|
854
|
-
const { EditorContent, useEditor } = peers.react;
|
|
855
|
-
const StarterKit = peers.starterKit.default;
|
|
856
|
-
return function MarkdownDocumentEditor({ value = "", placeholder = "Start writing...", readOnly = false, autoFocus = false, className, contentClassName, onChange, onReady }) {
|
|
857
|
-
const initialHtml = useMemo(() => markdownToHtml(value), []);
|
|
858
|
-
const lastAppliedMarkdownRef = useRef(normalizeMarkdown(value));
|
|
859
|
-
const editor = useEditor({
|
|
860
|
-
extensions: [StarterKit.configure({ codeBlock: { HTMLAttributes: { class: "hljs" } } })],
|
|
861
|
-
content: initialHtml,
|
|
862
|
-
editable: !readOnly,
|
|
863
|
-
autofocus: autoFocus,
|
|
864
|
-
editorProps: { attributes: {
|
|
865
|
-
class: cn("prose prose-sm sm:prose-base max-w-none focus:outline-none", "prose-headings:text-foreground prose-p:text-foreground prose-li:text-foreground", "prose-strong:text-foreground prose-code:text-foreground prose-pre:text-foreground", contentClassName),
|
|
866
|
-
"data-placeholder": placeholder
|
|
867
|
-
} },
|
|
868
|
-
onUpdate: ({ editor: currentEditor }) => {
|
|
869
|
-
const nextMarkdown = normalizeMarkdown(htmlToMarkdown(currentEditor.getHTML()));
|
|
870
|
-
lastAppliedMarkdownRef.current = nextMarkdown;
|
|
871
|
-
onChange?.(nextMarkdown, currentEditor);
|
|
872
|
-
},
|
|
873
|
-
onCreate: ({ editor: currentEditor }) => {
|
|
874
|
-
onReady?.(currentEditor);
|
|
875
|
-
}
|
|
876
|
-
});
|
|
877
|
-
useEffect(() => {
|
|
878
|
-
if (editor) editor.setEditable(!readOnly);
|
|
879
|
-
}, [editor, readOnly]);
|
|
880
|
-
useEffect(() => {
|
|
881
|
-
if (!editor) return;
|
|
882
|
-
const normalizedValue = normalizeMarkdown(value);
|
|
883
|
-
if (normalizedValue === lastAppliedMarkdownRef.current) return;
|
|
884
|
-
editor.commands.setContent(markdownToHtml(value), { emitUpdate: false });
|
|
885
|
-
lastAppliedMarkdownRef.current = normalizedValue;
|
|
886
|
-
}, [editor, value]);
|
|
887
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
888
|
-
className: cn("flex min-h-[14rem] w-full flex-col overflow-hidden rounded-lg border border-border bg-background", className),
|
|
889
|
-
children: [
|
|
890
|
-
/* @__PURE__ */ jsx(EditorToolbar, {
|
|
891
|
-
editor,
|
|
892
|
-
className: "border-border bg-card px-2 py-2"
|
|
893
|
-
}),
|
|
894
|
-
/* @__PURE__ */ jsx("div", {
|
|
895
|
-
className: "min-h-0 flex-1 overflow-auto px-5 py-4",
|
|
896
|
-
children: /* @__PURE__ */ jsx(EditorContent, { editor })
|
|
897
|
-
}),
|
|
898
|
-
/* @__PURE__ */ jsx("style", { children: `
|
|
899
|
-
.ProseMirror p.is-editor-empty:first-child::before {
|
|
900
|
-
content: attr(data-placeholder);
|
|
901
|
-
float: left;
|
|
902
|
-
color: hsl(var(--muted-foreground));
|
|
903
|
-
pointer-events: none;
|
|
904
|
-
height: 0;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
.ProseMirror pre {
|
|
908
|
-
background: hsl(var(--muted));
|
|
909
|
-
border: 1px solid hsl(var(--border));
|
|
910
|
-
border-radius: 0.75rem;
|
|
911
|
-
padding: 0.875rem 1rem;
|
|
912
|
-
overflow-x: auto;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
.ProseMirror code {
|
|
916
|
-
background: hsl(var(--muted));
|
|
917
|
-
border-radius: 0.35rem;
|
|
918
|
-
padding: 0.12rem 0.3rem;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
.ProseMirror pre code {
|
|
922
|
-
background: transparent;
|
|
923
|
-
padding: 0;
|
|
924
|
-
}
|
|
925
|
-
` })
|
|
926
|
-
]
|
|
927
|
-
});
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
const lazyMarkdownDocumentEditor = retryableLazyEditor(async () => createMarkdownDocumentEditor(await loadDocumentEditorPeers()));
|
|
931
|
-
/** Local markdown editor. Loads its tiptap peers on first render. */
|
|
932
|
-
function MarkdownDocumentEditor(props) {
|
|
933
|
-
const LazyMarkdownDocumentEditor = lazyMarkdownDocumentEditor();
|
|
934
|
-
return /* @__PURE__ */ jsx(Suspense, {
|
|
935
|
-
fallback: /* @__PURE__ */ jsx(EditorLoadingPlaceholder, { className: cn("min-h-[14rem]", props.className) }),
|
|
936
|
-
children: /* @__PURE__ */ jsx(LazyMarkdownDocumentEditor, { ...props })
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
//#endregion
|
|
940
|
-
//#region src/editor/use-editor.ts
|
|
941
|
-
/**
|
|
942
|
-
* Hook to get the current connection state and helpers.
|
|
943
|
-
*/
|
|
944
|
-
function useEditorConnection() {
|
|
945
|
-
const { connectionState, isSynced, connect, disconnect } = useEditorContext();
|
|
946
|
-
return {
|
|
947
|
-
/** Current connection state string */
|
|
948
|
-
state: connectionState,
|
|
949
|
-
/** Whether connected to the server (connected or synced) */
|
|
950
|
-
isConnected: connectionState === "connected" || connectionState === "synced",
|
|
951
|
-
/** Whether currently attempting to connect */
|
|
952
|
-
isConnecting: connectionState === "connecting",
|
|
953
|
-
/** Whether disconnected from the server */
|
|
954
|
-
isDisconnected: connectionState === "disconnected",
|
|
955
|
-
/** Whether the document is synced with the server */
|
|
956
|
-
isSynced,
|
|
957
|
-
/** Connect to the collaboration server */
|
|
958
|
-
connect,
|
|
959
|
-
/** Disconnect from the collaboration server */
|
|
960
|
-
disconnect
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
/**
|
|
964
|
-
* Hook to get the list of active collaborators.
|
|
965
|
-
*/
|
|
966
|
-
function useCollaborators() {
|
|
967
|
-
const { collaborators } = useEditorContext();
|
|
968
|
-
const count = collaborators.length;
|
|
969
|
-
return {
|
|
970
|
-
/** List of active collaborators (excluding self) */
|
|
971
|
-
collaborators,
|
|
972
|
-
/** Number of other collaborators */
|
|
973
|
-
count,
|
|
974
|
-
/** Whether there are other collaborators present */
|
|
975
|
-
hasOthers: count > 0
|
|
976
|
-
};
|
|
977
|
-
}
|
|
978
|
-
/**
|
|
979
|
-
* Hook to track a specific collaborator's presence.
|
|
980
|
-
*/
|
|
981
|
-
function useCollaboratorPresence(userId) {
|
|
982
|
-
const { collaborators } = useEditorContext();
|
|
983
|
-
return collaborators.find((c) => c.user.userId === userId) ?? null;
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Hook to sync local state with the Y.Doc.
|
|
987
|
-
* Useful for persisting editor metadata.
|
|
988
|
-
*/
|
|
989
|
-
function useYjsState(key, initialValue) {
|
|
990
|
-
const { doc } = useEditorContext();
|
|
991
|
-
const [value, setLocalValue] = useState(initialValue);
|
|
992
|
-
const metaMap = doc.getMap("metadata");
|
|
993
|
-
useEffect(() => {
|
|
994
|
-
const updateValue = () => {
|
|
995
|
-
const stored = metaMap.get(key);
|
|
996
|
-
if (stored !== void 0) setLocalValue(stored);
|
|
997
|
-
};
|
|
998
|
-
updateValue();
|
|
999
|
-
metaMap.observe(updateValue);
|
|
1000
|
-
return () => {
|
|
1001
|
-
metaMap.unobserve(updateValue);
|
|
1002
|
-
};
|
|
1003
|
-
}, [metaMap, key]);
|
|
1004
|
-
return [value, useCallback((newValue) => {
|
|
1005
|
-
doc.transact(() => {
|
|
1006
|
-
metaMap.set(key, newValue);
|
|
1007
|
-
});
|
|
1008
|
-
}, [
|
|
1009
|
-
doc,
|
|
1010
|
-
metaMap,
|
|
1011
|
-
key
|
|
1012
|
-
])];
|
|
1013
|
-
}
|
|
1014
|
-
/**
|
|
1015
|
-
* Hook to track document changes.
|
|
1016
|
-
* Returns true if there are unsaved changes since the last save.
|
|
1017
|
-
*/
|
|
1018
|
-
function useDocumentChanges(onSave) {
|
|
1019
|
-
const { doc } = useEditorContext();
|
|
1020
|
-
const [isDirty, setIsDirty] = useState(false);
|
|
1021
|
-
const [isSaving, setIsSaving] = useState(false);
|
|
1022
|
-
const [lastSaved, setLastSaved] = useState(null);
|
|
1023
|
-
useEffect(() => {
|
|
1024
|
-
const handleUpdate = () => {
|
|
1025
|
-
setIsDirty(true);
|
|
1026
|
-
};
|
|
1027
|
-
doc.on("update", handleUpdate);
|
|
1028
|
-
return () => {
|
|
1029
|
-
doc.off("update", handleUpdate);
|
|
1030
|
-
};
|
|
1031
|
-
}, [doc]);
|
|
1032
|
-
return {
|
|
1033
|
-
/** Whether there are unsaved changes */
|
|
1034
|
-
isDirty,
|
|
1035
|
-
/** Whether save is in progress */
|
|
1036
|
-
isSaving,
|
|
1037
|
-
/** When the document was last saved */
|
|
1038
|
-
lastSaved,
|
|
1039
|
-
/** Save the document */
|
|
1040
|
-
save: useCallback(async () => {
|
|
1041
|
-
if (!onSave || isSaving) return;
|
|
1042
|
-
setIsSaving(true);
|
|
1043
|
-
try {
|
|
1044
|
-
await onSave();
|
|
1045
|
-
setIsDirty(false);
|
|
1046
|
-
setLastSaved(/* @__PURE__ */ new Date());
|
|
1047
|
-
} finally {
|
|
1048
|
-
setIsSaving(false);
|
|
1049
|
-
}
|
|
1050
|
-
}, [onSave, isSaving])
|
|
1051
|
-
};
|
|
1052
|
-
}
|
|
1053
|
-
/**
|
|
1054
|
-
* Hook to get awareness state for presence features.
|
|
1055
|
-
* Provides lower-level access to the Hocuspocus awareness.
|
|
1056
|
-
*/
|
|
1057
|
-
function useAwareness() {
|
|
1058
|
-
const { provider } = useEditorContext();
|
|
1059
|
-
const [localState, setLocalStateValue] = useState({});
|
|
1060
|
-
const awareness = provider?.awareness;
|
|
1061
|
-
useEffect(() => {
|
|
1062
|
-
if (!awareness) return;
|
|
1063
|
-
const updateState = () => {
|
|
1064
|
-
setLocalStateValue(awareness.getLocalState() ?? {});
|
|
1065
|
-
};
|
|
1066
|
-
updateState();
|
|
1067
|
-
awareness.on("change", updateState);
|
|
1068
|
-
return () => {
|
|
1069
|
-
awareness.off("change", updateState);
|
|
1070
|
-
};
|
|
1071
|
-
}, [awareness]);
|
|
1072
|
-
return {
|
|
1073
|
-
/** Current local awareness state */
|
|
1074
|
-
localState,
|
|
1075
|
-
/** Set the entire local state (merges with existing) */
|
|
1076
|
-
setLocalState: useCallback((state) => {
|
|
1077
|
-
if (!awareness) return;
|
|
1078
|
-
const current = awareness.getLocalState() ?? {};
|
|
1079
|
-
awareness.setLocalState({
|
|
1080
|
-
...current,
|
|
1081
|
-
...state
|
|
1082
|
-
});
|
|
1083
|
-
}, [awareness]),
|
|
1084
|
-
/** Set a single field in the local state */
|
|
1085
|
-
setLocalStateField: useCallback((field, value) => {
|
|
1086
|
-
if (!awareness) return;
|
|
1087
|
-
awareness.setLocalStateField(field, value);
|
|
1088
|
-
}, [awareness]),
|
|
1089
|
-
/** The raw awareness instance */
|
|
1090
|
-
awareness
|
|
1091
|
-
};
|
|
1092
|
-
}
|
|
1093
|
-
//#endregion
|
|
1094
|
-
//#region src/editor/document-editor-pane.tsx
|
|
1095
|
-
var document_editor_pane_exports = /* @__PURE__ */ __exportAll({ DocumentEditorPane: () => DocumentEditorPane });
|
|
1096
|
-
function connectionTone(state) {
|
|
1097
|
-
switch (state) {
|
|
1098
|
-
case "synced": return "text-[var(--surface-success-text)] border-[var(--surface-success-border)] bg-[var(--surface-success-bg)]";
|
|
1099
|
-
case "connected":
|
|
1100
|
-
case "connecting": return "text-[var(--surface-info-text)] border-[var(--surface-info-border)] bg-[var(--surface-info-bg)]";
|
|
1101
|
-
default: return "text-[var(--surface-warning-text)] border-[var(--surface-warning-border)] bg-[var(--surface-warning-bg)]";
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
function connectionLabel(state) {
|
|
1105
|
-
switch (state) {
|
|
1106
|
-
case "synced": return "Live synced";
|
|
1107
|
-
case "connected": return "Connected";
|
|
1108
|
-
case "connecting": return "Connecting";
|
|
1109
|
-
default: return "Offline";
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
function connectionDescription(state, collaborators, readOnly) {
|
|
1113
|
-
if (readOnly) return state === "disconnected" ? "Live access is paused. You can keep reading while the editor reconnects." : "You are viewing the live document in read-only mode.";
|
|
1114
|
-
switch (state) {
|
|
1115
|
-
case "synced": return collaborators > 0 ? `You and ${collaborators} collaborator${collaborators === 1 ? "" : "s"} are editing the same document.` : "You are editing the live document. Changes sync automatically.";
|
|
1116
|
-
case "connected":
|
|
1117
|
-
case "connecting": return "Connecting the live document. Local edits stay in place while sync catches up.";
|
|
1118
|
-
default: return "Live updates are paused. You can keep editing and reconnect when the transport is healthy again.";
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
function CollaborativeDocumentSurface({ markdown, placeholder, autoFocus, readOnly, className, onChange }) {
|
|
1122
|
-
const { state } = useEditorConnection();
|
|
1123
|
-
const { collaborators } = useCollaborators();
|
|
1124
|
-
const initialContent = useMemo(() => markdownToHtml(markdown), [markdown]);
|
|
1125
|
-
const collaboratorCount = collaborators.length + 1;
|
|
1126
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1127
|
-
className: cn("flex h-full min-h-0 flex-col gap-3", className),
|
|
1128
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1129
|
-
className: "flex flex-wrap items-center justify-between gap-3 rounded-[var(--radius-lg)] border border-border bg-card px-3 py-2",
|
|
1130
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1131
|
-
className: "min-w-0 space-y-2",
|
|
1132
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1133
|
-
className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground",
|
|
1134
|
-
children: [/* @__PURE__ */ jsxs("span", {
|
|
1135
|
-
className: cn("inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 font-medium", connectionTone(state)),
|
|
1136
|
-
children: [state === "disconnected" ? /* @__PURE__ */ jsx(WifiOff, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Wifi, { className: "h-3.5 w-3.5" }), connectionLabel(state)]
|
|
1137
|
-
}), /* @__PURE__ */ jsxs("span", {
|
|
1138
|
-
className: "inline-flex items-center gap-1.5 rounded-full border border-border bg-background px-2.5 py-1",
|
|
1139
|
-
children: [/* @__PURE__ */ jsx(Users, { className: "h-3.5 w-3.5" }), collaborators.length === 0 ? "Solo editing" : `${collaboratorCount} active`]
|
|
1140
|
-
})]
|
|
1141
|
-
}), /* @__PURE__ */ jsx("p", {
|
|
1142
|
-
className: "text-xs text-muted-foreground",
|
|
1143
|
-
children: connectionDescription(state, collaborators.length, readOnly)
|
|
1144
|
-
})]
|
|
1145
|
-
}), /* @__PURE__ */ jsx(CollaboratorsList, { collaborators })]
|
|
1146
|
-
}), /* @__PURE__ */ jsx(TiptapEditor, {
|
|
1147
|
-
initialContent,
|
|
1148
|
-
placeholder,
|
|
1149
|
-
autoFocus,
|
|
1150
|
-
readOnly,
|
|
1151
|
-
className: cn("h-full min-h-[16rem]", className),
|
|
1152
|
-
onUpdate: (editor) => {
|
|
1153
|
-
onChange?.(normalizeMarkdown(htmlToMarkdown(editor.getHTML())));
|
|
1154
|
-
}
|
|
1155
|
-
})]
|
|
1156
|
-
});
|
|
1157
|
-
}
|
|
1158
|
-
/**
|
|
1159
|
-
* DocumentEditorPane — reusable markdown document surface with preview/edit
|
|
1160
|
-
* modes and optional collaborative editing backed by Yjs/Hocuspocus.
|
|
1161
|
-
*/
|
|
1162
|
-
function DocumentEditorPane({ eyebrow, title, subtitle, meta, headerActions, footer, className, contentClassName, headerClassName, hideTitleBlock, tabs, toolbar, markdown = "", savedMarkdown, urlTransform, mode, defaultMode = "preview", onModeChange, backend = "local", placeholder = "Start writing...", autoFocus = false, readOnly = false, onChange, onSave, saving = false, saveLabel = "Save changes", previewClassName, editorClassName, collaboration }) {
|
|
1163
|
-
const [draft, setDraft] = useState(markdown);
|
|
1164
|
-
const [uncontrolledMode, setUncontrolledMode] = useState(defaultMode);
|
|
1165
|
-
const activeMode = mode ?? uncontrolledMode;
|
|
1166
|
-
const isCollaborative = backend === "collaborative" && Boolean(collaboration);
|
|
1167
|
-
const isDirty = normalizeMarkdown(draft) !== normalizeMarkdown(savedMarkdown ?? markdown);
|
|
1168
|
-
const saveStateLabel = readOnly ? "Read only" : isCollaborative ? isDirty ? "Snapshot pending" : "Live document current" : isDirty ? "Unsaved changes" : "Saved";
|
|
1169
|
-
useEffect(() => {
|
|
1170
|
-
setDraft(markdown);
|
|
1171
|
-
}, [markdown]);
|
|
1172
|
-
useEffect(() => {
|
|
1173
|
-
if (mode === void 0) setUncontrolledMode(defaultMode);
|
|
1174
|
-
}, [defaultMode, mode]);
|
|
1175
|
-
const setMode = (nextMode) => {
|
|
1176
|
-
if (mode === void 0) setUncontrolledMode(nextMode);
|
|
1177
|
-
onModeChange?.(nextMode);
|
|
1178
|
-
};
|
|
1179
|
-
const handleChange = (nextMarkdown) => {
|
|
1180
|
-
setDraft(nextMarkdown);
|
|
1181
|
-
onChange?.(nextMarkdown);
|
|
1182
|
-
};
|
|
1183
|
-
const editorToolbar = /* @__PURE__ */ jsxs("div", {
|
|
1184
|
-
className: "flex flex-wrap items-center justify-between gap-3",
|
|
1185
|
-
children: [/* @__PURE__ */ jsxs(TabsList, {
|
|
1186
|
-
variant: "underline",
|
|
1187
|
-
className: "h-auto gap-4 border-0 bg-transparent p-0 text-muted-foreground",
|
|
1188
|
-
children: [/* @__PURE__ */ jsx(TabsTrigger, {
|
|
1189
|
-
value: "preview",
|
|
1190
|
-
variant: "underline",
|
|
1191
|
-
className: "pb-2 data-[state=active]:border-primary data-[state=active]:text-foreground",
|
|
1192
|
-
children: "Preview"
|
|
1193
|
-
}), /* @__PURE__ */ jsxs(TabsTrigger, {
|
|
1194
|
-
value: "edit",
|
|
1195
|
-
variant: "underline",
|
|
1196
|
-
className: "flex items-center gap-2 pb-2 data-[state=active]:border-primary data-[state=active]:text-foreground",
|
|
1197
|
-
children: [/* @__PURE__ */ jsx(PencilLine, { className: "h-3.5 w-3.5" }), isCollaborative ? "Live edit" : "Edit"]
|
|
1198
|
-
})]
|
|
1199
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1200
|
-
className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground",
|
|
1201
|
-
children: [
|
|
1202
|
-
toolbar,
|
|
1203
|
-
/* @__PURE__ */ jsx("span", {
|
|
1204
|
-
className: "rounded-full border border-border bg-card px-2.5 py-1 font-medium",
|
|
1205
|
-
children: isCollaborative ? "Live document" : "Local draft"
|
|
1206
|
-
}),
|
|
1207
|
-
/* @__PURE__ */ jsx("span", {
|
|
1208
|
-
className: "rounded-full border border-border bg-background px-2.5 py-1",
|
|
1209
|
-
children: saveStateLabel
|
|
1210
|
-
}),
|
|
1211
|
-
onSave && !readOnly && /* @__PURE__ */ jsxs("button", {
|
|
1212
|
-
type: "button",
|
|
1213
|
-
onClick: () => void onSave(draft),
|
|
1214
|
-
disabled: saving || !isDirty,
|
|
1215
|
-
className: cn("inline-flex items-center gap-2 rounded-[var(--radius-full)] border border-border bg-card px-3 py-1.5 text-xs font-semibold text-foreground transition-colors hover:border-[var(--border-strong)] hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50", focusRing),
|
|
1216
|
-
children: [/* @__PURE__ */ jsx(Save, { className: "h-3.5 w-3.5" }), saving ? "Saving..." : saveLabel]
|
|
1217
|
-
})
|
|
1218
|
-
]
|
|
1219
|
-
})]
|
|
1220
|
-
});
|
|
1221
|
-
const preview = /* @__PURE__ */ jsx("div", {
|
|
1222
|
-
className: cn("m-4 rounded-[var(--radius-lg)] border border-border bg-card p-5", previewClassName),
|
|
1223
|
-
children: /* @__PURE__ */ jsx(Markdown, {
|
|
1224
|
-
urlTransform,
|
|
1225
|
-
children: draft
|
|
1226
|
-
})
|
|
1227
|
-
});
|
|
1228
|
-
const localEditor = /* @__PURE__ */ jsx(MarkdownDocumentEditor, {
|
|
1229
|
-
value: draft,
|
|
1230
|
-
placeholder,
|
|
1231
|
-
autoFocus,
|
|
1232
|
-
readOnly,
|
|
1233
|
-
onChange: (nextMarkdown) => {
|
|
1234
|
-
handleChange(nextMarkdown);
|
|
1235
|
-
},
|
|
1236
|
-
className: editorClassName
|
|
1237
|
-
});
|
|
1238
|
-
const collaborativeEditor = collaboration ? /* @__PURE__ */ jsx(EditorProvider, {
|
|
1239
|
-
...collaboration,
|
|
1240
|
-
children: /* @__PURE__ */ jsx(CollaborativeDocumentSurface, {
|
|
1241
|
-
markdown: draft,
|
|
1242
|
-
placeholder,
|
|
1243
|
-
autoFocus,
|
|
1244
|
-
readOnly,
|
|
1245
|
-
className: editorClassName,
|
|
1246
|
-
onChange: handleChange
|
|
1247
|
-
})
|
|
1248
|
-
}, collaboration.documentName) : localEditor;
|
|
1249
|
-
return /* @__PURE__ */ jsx(Tabs, {
|
|
1250
|
-
value: activeMode,
|
|
1251
|
-
onValueChange: (nextValue) => setMode(nextValue),
|
|
1252
|
-
className: "h-full",
|
|
1253
|
-
children: /* @__PURE__ */ jsxs(ArtifactPane, {
|
|
1254
|
-
eyebrow,
|
|
1255
|
-
title,
|
|
1256
|
-
subtitle,
|
|
1257
|
-
meta,
|
|
1258
|
-
headerActions,
|
|
1259
|
-
footer,
|
|
1260
|
-
tabs,
|
|
1261
|
-
className,
|
|
1262
|
-
contentClassName,
|
|
1263
|
-
headerClassName,
|
|
1264
|
-
toolbar: editorToolbar,
|
|
1265
|
-
hideTitleBlock,
|
|
1266
|
-
children: [/* @__PURE__ */ jsx(TabsContent, {
|
|
1267
|
-
value: "preview",
|
|
1268
|
-
className: "mt-0 h-full",
|
|
1269
|
-
children: preview
|
|
1270
|
-
}), /* @__PURE__ */ jsx(TabsContent, {
|
|
1271
|
-
value: "edit",
|
|
1272
|
-
className: "mt-0 h-full px-4 py-4",
|
|
1273
|
-
children: isCollaborative ? collaborativeEditor : localEditor
|
|
1274
|
-
})]
|
|
1275
|
-
})
|
|
1276
|
-
});
|
|
1277
|
-
}
|
|
1278
|
-
//#endregion
|
|
1279
|
-
export { useCollaborators as a, useYjsState as c, EditorToolbar as d, EditorProvider as f, useCollaboratorPresence as i, CollaboratorsList as l, document_editor_pane_exports as n, useDocumentChanges as o, useEditorContext as p, useAwareness as r, useEditorConnection as s, DocumentEditorPane as t, TiptapEditor as u };
|