@mp-lb/mdkit 0.3.2 → 0.3.3
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 +8 -2
- package/dist/collaboration/useMdKitCollaboration.d.ts +5 -0
- package/dist/collaboration/useMdKitCollaboration.d.ts.map +1 -0
- package/dist/collaboration/useMdKitCollaboration.js +4 -0
- package/dist/core/checkpointPolicy.d.ts +10 -0
- package/dist/core/checkpointPolicy.d.ts.map +1 -0
- package/dist/core/checkpointPolicy.js +9 -0
- package/dist/core/documentEngine.d.ts +1 -0
- package/dist/core/documentEngine.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/document/MdKitConflictPanel.d.ts +5 -0
- package/dist/document/MdKitConflictPanel.d.ts.map +1 -0
- package/dist/document/MdKitConflictPanel.js +4 -0
- package/dist/document/MdKitDocumentToolbar.d.ts +6 -0
- package/dist/document/MdKitDocumentToolbar.d.ts.map +1 -0
- package/dist/document/MdKitDocumentToolbar.js +5 -0
- package/dist/document/documentTypes.d.ts +6 -0
- package/dist/document/documentTypes.d.ts.map +1 -0
- package/dist/document/useMdKitDocument.d.ts +5 -0
- package/dist/document/useMdKitDocument.d.ts.map +1 -0
- package/dist/document/useMdKitDocument.js +4 -0
- package/dist/fastify.d.ts +1 -0
- package/dist/fastify.d.ts.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/markdown/MarkdownBubbleMenu.d.ts +1 -0
- package/dist/markdown/MarkdownBubbleMenu.d.ts.map +1 -0
- package/dist/markdown/MarkdownPasteExtension.d.ts +1 -0
- package/dist/markdown/MarkdownPasteExtension.d.ts.map +1 -0
- package/dist/markdown/MarkdownSearchExtension.d.ts +1 -0
- package/dist/markdown/MarkdownSearchExtension.d.ts.map +1 -0
- package/dist/markdown/MarkdownSearchPanel.d.ts +1 -0
- package/dist/markdown/MarkdownSearchPanel.d.ts.map +1 -0
- package/dist/markdown/MdKitEditor.d.ts +11 -0
- package/dist/markdown/MdKitEditor.d.ts.map +1 -0
- package/dist/markdown/MdKitEditor.js +10 -2
- package/dist/markdown/MdKitView.d.ts +9 -1
- package/dist/markdown/MdKitView.d.ts.map +1 -0
- package/dist/markdown/MdKitView.js +7 -2
- package/dist/markdown/TiptapMarkdownSurface.d.ts +1 -0
- package/dist/markdown/TiptapMarkdownSurface.d.ts.map +1 -0
- package/dist/markdown/TiptapMarkdownSurface.js +3 -22
- package/dist/markdown/createMdKitTiptapExtensions.d.ts +1 -0
- package/dist/markdown/createMdKitTiptapExtensions.d.ts.map +1 -0
- package/dist/markdown/editorDebug.d.ts +1 -0
- package/dist/markdown/editorDebug.d.ts.map +1 -0
- package/dist/markdown/markdownFenceRanges.d.ts +1 -0
- package/dist/markdown/markdownFenceRanges.d.ts.map +1 -0
- package/dist/markdown/normalizeMarkdownSerialization.d.ts +1 -0
- package/dist/markdown/normalizeMarkdownSerialization.d.ts.map +1 -0
- package/dist/markdown/prepareMarkdownForEditorHydration.d.ts +1 -0
- package/dist/markdown/prepareMarkdownForEditorHydration.d.ts.map +1 -0
- package/dist/markdown/preserveMarkdownWhitespace.d.ts +1 -0
- package/dist/markdown/preserveMarkdownWhitespace.d.ts.map +1 -0
- package/dist/markdown/yamlFrontMatter.d.ts +1 -0
- package/dist/markdown/yamlFrontMatter.d.ts.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/theme/MdKitThemeEditor.d.ts +5 -0
- package/dist/theme/MdKitThemeEditor.d.ts.map +1 -0
- package/dist/theme/MdKitThemeEditor.js +4 -0
- package/dist/theme/editorTheme.d.ts +1 -0
- package/dist/theme/editorTheme.d.ts.map +1 -0
- package/dist/theme/editorTheme.js +8 -8
- package/dist/transport/backend.d.ts +13 -0
- package/dist/transport/backend.d.ts.map +1 -0
- package/dist/transport/backend.js +6 -0
- package/dist/transport/fastify.d.ts +5 -0
- package/dist/transport/fastify.d.ts.map +1 -0
- package/dist/transport/fastify.js +4 -0
- package/dist/transport/http.d.ts +1 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/index.d.ts +1 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/rest.d.ts +6 -0
- package/dist/transport/rest.d.ts.map +1 -0
- package/dist/transport/rest.js +5 -0
- package/dist/transport/store.d.ts +1 -0
- package/dist/transport/store.d.ts.map +1 -0
- package/dist/transport/trpcClient.d.ts +8 -0
- package/dist/transport/trpcClient.d.ts.map +1 -0
- package/dist/transport/trpcClient.js +7 -0
- package/dist/transport/trpcServer.d.ts +6 -0
- package/dist/transport/trpcServer.d.ts.map +1 -0
- package/dist/transport/trpcServer.js +5 -0
- package/dist/trpc/client.d.ts +1 -0
- package/dist/trpc/client.d.ts.map +1 -0
- package/dist/trpc/server.d.ts +1 -0
- package/dist/trpc/server.d.ts.map +1 -0
- package/dist/trpc.d.ts +1 -0
- package/dist/trpc.d.ts.map +1 -0
- package/dist/ui/joinClassNames.d.ts +1 -0
- package/dist/ui/joinClassNames.d.ts.map +1 -0
- package/dist/versioning/VersionHistoryPanel.d.ts +5 -0
- package/dist/versioning/VersionHistoryPanel.d.ts.map +1 -0
- package/dist/versioning/VersionHistoryPanel.js +4 -0
- package/dist/versioning/useMdKitDocumentVersions.d.ts +5 -0
- package/dist/versioning/useMdKitDocumentVersions.d.ts.map +1 -0
- package/dist/versioning/useMdKitDocumentVersions.js +4 -0
- package/dist/yjs/MdKitMarkdownYjs.d.ts +1 -0
- package/dist/yjs/MdKitMarkdownYjs.d.ts.map +1 -0
- package/dist/yjs/index.d.ts +1 -0
- package/dist/yjs/index.d.ts.map +1 -0
- package/package.json +10 -12
- package/src/collaboration/useMdKitCollaboration.ts +528 -0
- package/src/core/checkpointPolicy.ts +107 -0
- package/src/core/documentEngine.ts +175 -0
- package/src/core/index.ts +33 -0
- package/src/document/MdKitConflictPanel.tsx +129 -0
- package/src/document/MdKitDocumentToolbar.tsx +141 -0
- package/src/document/documentTypes.ts +89 -0
- package/src/document/useMdKitDocument.ts +543 -0
- package/src/fastify.ts +6 -0
- package/src/index.ts +89 -0
- package/src/markdown/MarkdownBubbleMenu.tsx +271 -0
- package/src/markdown/MarkdownPasteExtension.ts +81 -0
- package/src/markdown/MarkdownSearchExtension.ts +77 -0
- package/src/markdown/MarkdownSearchPanel.tsx +98 -0
- package/src/markdown/MdKitEditor.tsx +75 -0
- package/src/markdown/MdKitView.tsx +80 -0
- package/src/markdown/TiptapMarkdownSurface.tsx +923 -0
- package/src/markdown/createMdKitTiptapExtensions.ts +42 -0
- package/src/markdown/editorDebug.ts +5 -0
- package/src/markdown/markdownFenceRanges.ts +68 -0
- package/src/markdown/normalizeMarkdownSerialization.ts +55 -0
- package/src/markdown/prepareMarkdownForEditorHydration.ts +23 -0
- package/src/markdown/preserveMarkdownWhitespace.ts +143 -0
- package/src/markdown/yamlFrontMatter.ts +135 -0
- package/src/server.ts +6 -0
- package/src/styles.css +125 -53
- package/src/theme/MdKitThemeEditor.tsx +134 -0
- package/src/theme/editorTheme.ts +72 -0
- package/src/transport/backend.ts +220 -0
- package/src/transport/fastify.ts +57 -0
- package/src/transport/http.ts +126 -0
- package/src/transport/index.ts +12 -0
- package/src/transport/rest.ts +80 -0
- package/src/transport/store.ts +45 -0
- package/src/transport/trpcClient.ts +90 -0
- package/src/transport/trpcServer.ts +66 -0
- package/src/trpc/client.ts +11 -0
- package/src/trpc/server.ts +12 -0
- package/src/trpc.ts +11 -0
- package/src/ui/joinClassNames.ts +3 -0
- package/src/versioning/VersionHistoryPanel.tsx +146 -0
- package/src/versioning/useMdKitDocumentVersions.ts +146 -0
- package/src/yjs/MdKitMarkdownYjs.ts +111 -0
- package/src/yjs/index.ts +8 -0
- package/docs/.vitepress/config.ts +0 -47
- package/docs/api.md +0 -512
- package/docs/architecture.md +0 -96
- package/docs/collaboration-persistence.md +0 -147
- package/docs/index.md +0 -341
- package/docs/permissions.md +0 -139
- package/docs/plain-text.md +0 -131
- package/docs/rest.md +0 -98
- package/docs/shadcn.md +0 -125
- package/docs/styling.md +0 -373
- package/docs/use-cases.md +0 -148
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MdKitDocumentSnapshot,
|
|
3
|
+
MdKitDocumentVersionDetail,
|
|
4
|
+
MdKitDocumentVersionToken,
|
|
5
|
+
MdKitDocumentWriteResult,
|
|
6
|
+
} from "../document/documentTypes.js";
|
|
7
|
+
|
|
8
|
+
export type MdKitDocumentRecord = {
|
|
9
|
+
current: MdKitDocumentSnapshot;
|
|
10
|
+
versions: MdKitDocumentVersionDetail[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type CreateMdKitDocumentRecordInput = {
|
|
14
|
+
content?: string;
|
|
15
|
+
now?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type WriteMdKitDocumentRecordInput = {
|
|
19
|
+
baseVersion: MdKitDocumentVersionToken;
|
|
20
|
+
content: string;
|
|
21
|
+
force?: boolean;
|
|
22
|
+
label?: string;
|
|
23
|
+
now?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type WriteMdKitDocumentRecordResult = {
|
|
27
|
+
record: MdKitDocumentRecord;
|
|
28
|
+
result: MdKitDocumentWriteResult;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type RestoreMdKitDocumentVersionInput = {
|
|
32
|
+
label?: string;
|
|
33
|
+
now?: string;
|
|
34
|
+
versionId: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type RestoreMdKitDocumentVersionResult = {
|
|
38
|
+
record: MdKitDocumentRecord;
|
|
39
|
+
restoredVersion: MdKitDocumentVersionDetail;
|
|
40
|
+
result: MdKitDocumentWriteResult;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const initialVersion = "0";
|
|
44
|
+
|
|
45
|
+
const createTimestamp = () => new Date().toISOString();
|
|
46
|
+
|
|
47
|
+
export const normalizeMdKitVersionToken = (
|
|
48
|
+
version: MdKitDocumentVersionToken | undefined,
|
|
49
|
+
) => (version == null ? null : String(version));
|
|
50
|
+
|
|
51
|
+
export const detectMdKitDocumentConflict = (input: {
|
|
52
|
+
baseVersion: MdKitDocumentVersionToken;
|
|
53
|
+
currentVersion: MdKitDocumentVersionToken;
|
|
54
|
+
}) =>
|
|
55
|
+
normalizeMdKitVersionToken(input.baseVersion) !==
|
|
56
|
+
normalizeMdKitVersionToken(input.currentVersion);
|
|
57
|
+
|
|
58
|
+
const nextVersionToken = (currentVersion: MdKitDocumentVersionToken) => {
|
|
59
|
+
const current = Number(normalizeMdKitVersionToken(currentVersion));
|
|
60
|
+
|
|
61
|
+
return Number.isFinite(current) ? String(current + 1) : createTimestamp();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const createVersionDetail = (input: {
|
|
65
|
+
content: string;
|
|
66
|
+
id: string;
|
|
67
|
+
label?: string;
|
|
68
|
+
now: string;
|
|
69
|
+
}): MdKitDocumentVersionDetail => ({
|
|
70
|
+
content: input.content,
|
|
71
|
+
createdAt: input.now,
|
|
72
|
+
id: input.id,
|
|
73
|
+
label: input.label,
|
|
74
|
+
updatedAt: input.now,
|
|
75
|
+
version: input.id,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const createMdKitDocumentRecord = (
|
|
79
|
+
input: CreateMdKitDocumentRecordInput = {},
|
|
80
|
+
): MdKitDocumentRecord => {
|
|
81
|
+
const now = input.now ?? createTimestamp();
|
|
82
|
+
const content = input.content ?? "";
|
|
83
|
+
const current = {
|
|
84
|
+
content,
|
|
85
|
+
updatedAt: now,
|
|
86
|
+
version: initialVersion,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
current,
|
|
91
|
+
versions: [
|
|
92
|
+
createVersionDetail({
|
|
93
|
+
content,
|
|
94
|
+
id: initialVersion,
|
|
95
|
+
label: "Initial",
|
|
96
|
+
now,
|
|
97
|
+
}),
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const writeMdKitDocumentRecord = (
|
|
103
|
+
record: MdKitDocumentRecord,
|
|
104
|
+
input: WriteMdKitDocumentRecordInput,
|
|
105
|
+
): WriteMdKitDocumentRecordResult => {
|
|
106
|
+
if (
|
|
107
|
+
!input.force &&
|
|
108
|
+
detectMdKitDocumentConflict({
|
|
109
|
+
baseVersion: input.baseVersion,
|
|
110
|
+
currentVersion: record.current.version,
|
|
111
|
+
})
|
|
112
|
+
) {
|
|
113
|
+
return {
|
|
114
|
+
record,
|
|
115
|
+
result: {
|
|
116
|
+
conflict: true,
|
|
117
|
+
updatedAt: record.current.updatedAt,
|
|
118
|
+
version: record.current.version,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const now = input.now ?? createTimestamp();
|
|
124
|
+
const version = nextVersionToken(record.current.version);
|
|
125
|
+
const current = {
|
|
126
|
+
content: input.content,
|
|
127
|
+
updatedAt: now,
|
|
128
|
+
version,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
record: {
|
|
133
|
+
current,
|
|
134
|
+
versions: [
|
|
135
|
+
...record.versions,
|
|
136
|
+
createVersionDetail({
|
|
137
|
+
content: input.content,
|
|
138
|
+
id: version,
|
|
139
|
+
label: input.label ?? `Version ${version}`,
|
|
140
|
+
now,
|
|
141
|
+
}),
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
result: {
|
|
145
|
+
updatedAt: now,
|
|
146
|
+
version,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const restoreMdKitDocumentVersion = (
|
|
152
|
+
record: MdKitDocumentRecord,
|
|
153
|
+
input: RestoreMdKitDocumentVersionInput,
|
|
154
|
+
): RestoreMdKitDocumentVersionResult => {
|
|
155
|
+
const restoredVersion = record.versions.find(
|
|
156
|
+
(version) => version.id === input.versionId,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (!restoredVersion) {
|
|
160
|
+
throw new Error(`Version not found: ${input.versionId}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const written = writeMdKitDocumentRecord(record, {
|
|
164
|
+
baseVersion: record.current.version,
|
|
165
|
+
content: restoredVersion.content,
|
|
166
|
+
force: true,
|
|
167
|
+
label: input.label ?? `Restore ${restoredVersion.id}`,
|
|
168
|
+
now: input.now,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
...written,
|
|
173
|
+
restoredVersion,
|
|
174
|
+
};
|
|
175
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createMdKitDocumentRecord,
|
|
3
|
+
detectMdKitDocumentConflict,
|
|
4
|
+
normalizeMdKitVersionToken,
|
|
5
|
+
restoreMdKitDocumentVersion,
|
|
6
|
+
writeMdKitDocumentRecord,
|
|
7
|
+
} from "./documentEngine.js";
|
|
8
|
+
export {
|
|
9
|
+
CheckpointPolicy,
|
|
10
|
+
measureMdKitEditDistance,
|
|
11
|
+
} from "./checkpointPolicy.js";
|
|
12
|
+
|
|
13
|
+
export type {
|
|
14
|
+
CreateMdKitDocumentRecordInput,
|
|
15
|
+
MdKitDocumentRecord,
|
|
16
|
+
RestoreMdKitDocumentVersionInput,
|
|
17
|
+
RestoreMdKitDocumentVersionResult,
|
|
18
|
+
WriteMdKitDocumentRecordInput,
|
|
19
|
+
WriteMdKitDocumentRecordResult,
|
|
20
|
+
} from "./documentEngine.js";
|
|
21
|
+
export type {
|
|
22
|
+
MdKitCheckpointPolicy,
|
|
23
|
+
MdKitCheckpointPolicyInput,
|
|
24
|
+
MdKitSmartCheckpointPolicyOptions,
|
|
25
|
+
} from "./checkpointPolicy.js";
|
|
26
|
+
export type {
|
|
27
|
+
MdKitDocumentSnapshot,
|
|
28
|
+
MdKitDocumentVersionDetail,
|
|
29
|
+
MdKitDocumentVersionSummary,
|
|
30
|
+
MdKitDocumentVersionToken,
|
|
31
|
+
MdKitDocumentWriteInput,
|
|
32
|
+
MdKitDocumentWriteResult,
|
|
33
|
+
} from "../document/documentTypes.js";
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { MdKitDocumentController } from "./useMdKitDocument";
|
|
3
|
+
import { joinClassNames } from "../ui/joinClassNames";
|
|
4
|
+
|
|
5
|
+
export type MdKitConflictPanelProps = {
|
|
6
|
+
className?: string;
|
|
7
|
+
document: MdKitDocumentController;
|
|
8
|
+
title?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Base panel for resolving a save conflict. Previews remote and local content
|
|
13
|
+
* and keeps one side. Renders `null` when the document has no conflict.
|
|
14
|
+
*/
|
|
15
|
+
export const MdKitConflictPanel = ({
|
|
16
|
+
className,
|
|
17
|
+
document,
|
|
18
|
+
title = "Document conflict",
|
|
19
|
+
}: MdKitConflictPanelProps) => {
|
|
20
|
+
const [pendingAction, setPendingAction] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
const [activePreview, setActivePreview] = useState<"local" | "remote">(
|
|
23
|
+
"remote",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (!document.conflict) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const runAction = async (name: string, action: () => Promise<unknown>) => {
|
|
31
|
+
setPendingAction(name);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await action();
|
|
35
|
+
} finally {
|
|
36
|
+
setPendingAction(null);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const isBusy = pendingAction !== null || document.saveStatus === "saving";
|
|
41
|
+
const conflictDetails = document.conflictDetails;
|
|
42
|
+
|
|
43
|
+
const previewOptions = [
|
|
44
|
+
{
|
|
45
|
+
id: "remote" as const,
|
|
46
|
+
label: "Keep remote",
|
|
47
|
+
value:
|
|
48
|
+
conflictDetails?.remoteContent ??
|
|
49
|
+
"Remote content preview is not available. Keep remote will still reload the latest canonical document.",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "local" as const,
|
|
53
|
+
label: "Keep local",
|
|
54
|
+
value: conflictDetails?.localContent ?? document.value,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const activePreviewOption =
|
|
59
|
+
previewOptions.find((option) => option.id === activePreview) ??
|
|
60
|
+
previewOptions[0];
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<section className={joinClassNames("mp-lb-mdkit-conflict-panel", className)}>
|
|
64
|
+
<div className="mp-lb-mdkit-conflict-panel-content">
|
|
65
|
+
<h2>{title}</h2>
|
|
66
|
+
<p>
|
|
67
|
+
Remote changes conflict with local edits. Choose the remote document,
|
|
68
|
+
or keep your local document by overwriting the remote copy.
|
|
69
|
+
</p>
|
|
70
|
+
{document.error ? (
|
|
71
|
+
<p className="mp-lb-mdkit-conflict-panel-error">{document.error}</p>
|
|
72
|
+
) : null}
|
|
73
|
+
{conflictDetails ? (
|
|
74
|
+
<p className="mp-lb-mdkit-conflict-panel-meta">
|
|
75
|
+
Remote version {String(conflictDetails.remoteVersion ?? "none")}
|
|
76
|
+
{conflictDetails.remoteUpdatedAt
|
|
77
|
+
? ` saved ${new Date(
|
|
78
|
+
conflictDetails.remoteUpdatedAt,
|
|
79
|
+
).toLocaleTimeString()}`
|
|
80
|
+
: ""}
|
|
81
|
+
</p>
|
|
82
|
+
) : null}
|
|
83
|
+
</div>
|
|
84
|
+
<div className="mp-lb-mdkit-conflict-panel-preview">
|
|
85
|
+
<div className="mp-lb-mdkit-conflict-panel-tabs" role="tablist">
|
|
86
|
+
{previewOptions.map((option) => (
|
|
87
|
+
<button
|
|
88
|
+
key={option.id}
|
|
89
|
+
type="button"
|
|
90
|
+
aria-selected={activePreviewOption.id === option.id}
|
|
91
|
+
className={
|
|
92
|
+
activePreviewOption.id === option.id
|
|
93
|
+
? "mp-lb-mdkit-conflict-panel-tab mp-lb-mdkit-conflict-panel-tab-active"
|
|
94
|
+
: "mp-lb-mdkit-conflict-panel-tab"
|
|
95
|
+
}
|
|
96
|
+
role="tab"
|
|
97
|
+
onClick={() => setActivePreview(option.id)}
|
|
98
|
+
>
|
|
99
|
+
{option.label}
|
|
100
|
+
</button>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
<textarea
|
|
104
|
+
aria-label={`${activePreviewOption.label} conflict content`}
|
|
105
|
+
readOnly
|
|
106
|
+
value={activePreviewOption.value}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
<div className="mp-lb-mdkit-conflict-panel-action-row">
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
className="mp-lb-mdkit-panel-secondary-action"
|
|
113
|
+
disabled={isBusy}
|
|
114
|
+
onClick={() => void runAction("reload", document.resync)}
|
|
115
|
+
>
|
|
116
|
+
{pendingAction === "reload" ? "Keeping remote..." : "Keep remote"}
|
|
117
|
+
</button>
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
className="mp-lb-mdkit-panel-secondary-action"
|
|
121
|
+
disabled={isBusy}
|
|
122
|
+
onClick={() => void runAction("overwrite", document.forceSave)}
|
|
123
|
+
>
|
|
124
|
+
{pendingAction === "overwrite" ? "Keeping local..." : "Keep local"}
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
</section>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { MdKitCollaborationSession } from "./documentTypes";
|
|
3
|
+
import type { MdKitDocumentController } from "./useMdKitDocument";
|
|
4
|
+
import type { MdKitDocumentVersionsController } from "../versioning/useMdKitDocumentVersions";
|
|
5
|
+
import { joinClassNames } from "../ui/joinClassNames";
|
|
6
|
+
|
|
7
|
+
export type MdKitDocumentToolbarProps = {
|
|
8
|
+
className?: string;
|
|
9
|
+
collaboration?: MdKitCollaborationSession | null;
|
|
10
|
+
document: MdKitDocumentController;
|
|
11
|
+
onOpenConflict?: () => Promise<void> | void;
|
|
12
|
+
onOpenVersionHistory?: () => Promise<void> | void;
|
|
13
|
+
showConflictActions?: boolean;
|
|
14
|
+
versions?: MdKitDocumentVersionsController | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const formatUpdatedAt = (updatedAt: string | null) => {
|
|
18
|
+
if (!updatedAt) {
|
|
19
|
+
return "Never saved";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return `Saved ${new Date(updatedAt).toLocaleTimeString()}`;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Unstyled workflow controls for a connected document: save/collaboration
|
|
27
|
+
* status plus entry points for version history and conflict resolution. Starter
|
|
28
|
+
* UI — drop it for your own controls when it doesn't fit.
|
|
29
|
+
*/
|
|
30
|
+
export const MdKitDocumentToolbar = ({
|
|
31
|
+
className,
|
|
32
|
+
collaboration,
|
|
33
|
+
document,
|
|
34
|
+
onOpenConflict,
|
|
35
|
+
onOpenVersionHistory,
|
|
36
|
+
showConflictActions = false,
|
|
37
|
+
versions,
|
|
38
|
+
}: MdKitDocumentToolbarProps) => {
|
|
39
|
+
const [pendingAction, setPendingAction] = useState<string | null>(null);
|
|
40
|
+
const otherCollaboratorCount = collaboration?.otherParticipants.length ?? 0;
|
|
41
|
+
|
|
42
|
+
const runAction = async (name: string, action: () => Promise<unknown>) => {
|
|
43
|
+
setPendingAction(name);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await action();
|
|
47
|
+
} finally {
|
|
48
|
+
setPendingAction(null);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const hasVersionHistory = versions?.hasVersioning ?? false;
|
|
53
|
+
const hasActiveCollaborators = collaboration?.isCollaborating ?? false;
|
|
54
|
+
const isBusy = pendingAction !== null || document.saveStatus === "saving";
|
|
55
|
+
|
|
56
|
+
const status = document.conflict
|
|
57
|
+
? "Conflict"
|
|
58
|
+
: document.isLoading
|
|
59
|
+
? "Loading"
|
|
60
|
+
: document.saveStatus === "saving"
|
|
61
|
+
? "Saving"
|
|
62
|
+
: document.saveStatus === "pending"
|
|
63
|
+
? "Autosave pending"
|
|
64
|
+
: document.isDirty
|
|
65
|
+
? "Unsaved changes"
|
|
66
|
+
: document.saveStatus === "saved"
|
|
67
|
+
? "Saved"
|
|
68
|
+
: "Idle";
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
className={joinClassNames("mp-lb-mdkit-document-toolbar", className)}
|
|
73
|
+
data-conflict={document.conflict ? "true" : undefined}
|
|
74
|
+
data-dirty={document.isDirty ? "true" : undefined}
|
|
75
|
+
data-save-status={document.saveStatus}
|
|
76
|
+
data-status={status.toLowerCase().replace(/\s+/g, "-")}
|
|
77
|
+
>
|
|
78
|
+
<div className="mp-lb-mdkit-document-toolbar-status">
|
|
79
|
+
<strong>{status}</strong>
|
|
80
|
+
<span>{formatUpdatedAt(document.updatedAt)}</span>
|
|
81
|
+
{hasActiveCollaborators ? (
|
|
82
|
+
<span>{otherCollaboratorCount + 1} collaborators</span>
|
|
83
|
+
) : null}
|
|
84
|
+
</div>
|
|
85
|
+
{document.error && !document.conflict ? (
|
|
86
|
+
<div className="mp-lb-mdkit-document-toolbar-error">{document.error}</div>
|
|
87
|
+
) : null}
|
|
88
|
+
<div className="mp-lb-mdkit-document-toolbar-actions">
|
|
89
|
+
{hasVersionHistory && onOpenVersionHistory ? (
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
disabled={isBusy || document.conflict || versions?.isLoading}
|
|
93
|
+
onClick={() =>
|
|
94
|
+
void runAction("versions", async () => {
|
|
95
|
+
await versions?.refresh();
|
|
96
|
+
await onOpenVersionHistory();
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
>
|
|
100
|
+
{versions?.isLoading
|
|
101
|
+
? "Loading versions..."
|
|
102
|
+
: `Version ${String(document.version ?? "none")}`}
|
|
103
|
+
</button>
|
|
104
|
+
) : null}
|
|
105
|
+
{document.conflict && onOpenConflict ? (
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
className="mp-lb-mdkit-document-toolbar-conflict-trigger"
|
|
109
|
+
disabled={isBusy}
|
|
110
|
+
onClick={() =>
|
|
111
|
+
void runAction("conflict", async () => {
|
|
112
|
+
await onOpenConflict();
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
>
|
|
116
|
+
Resolve conflict
|
|
117
|
+
</button>
|
|
118
|
+
) : null}
|
|
119
|
+
</div>
|
|
120
|
+
{document.conflict && showConflictActions ? (
|
|
121
|
+
<div className="mp-lb-mdkit-document-toolbar-conflict">
|
|
122
|
+
<span>Remote changes conflict with local edits.</span>
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
disabled={isBusy}
|
|
126
|
+
onClick={() => void runAction("reload", document.resync)}
|
|
127
|
+
>
|
|
128
|
+
Keep remote
|
|
129
|
+
</button>
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
disabled={isBusy}
|
|
133
|
+
onClick={() => void runAction("overwrite", document.forceSave)}
|
|
134
|
+
>
|
|
135
|
+
Keep local
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
) : null}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { HocuspocusProvider } from "@hocuspocus/provider";
|
|
2
|
+
import type * as Y from "yjs";
|
|
3
|
+
|
|
4
|
+
export type MdKitDocumentVersionToken = string | number | null;
|
|
5
|
+
|
|
6
|
+
export type MdKitDocumentSnapshot = {
|
|
7
|
+
content: string;
|
|
8
|
+
version: MdKitDocumentVersionToken;
|
|
9
|
+
updatedAt?: string | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type MdKitDocumentWriteInput = {
|
|
13
|
+
documentId: string;
|
|
14
|
+
content: string;
|
|
15
|
+
baseVersion: MdKitDocumentVersionToken;
|
|
16
|
+
force?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type MdKitDocumentWriteResult =
|
|
20
|
+
| {
|
|
21
|
+
version: MdKitDocumentVersionToken;
|
|
22
|
+
updatedAt?: string | null;
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
conflict: true;
|
|
26
|
+
version?: MdKitDocumentVersionToken;
|
|
27
|
+
updatedAt?: string | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type MdKitDocumentVersionSummary = {
|
|
31
|
+
id: string;
|
|
32
|
+
label?: string;
|
|
33
|
+
createdAt: string;
|
|
34
|
+
authorLabel?: string | null;
|
|
35
|
+
updatedAt?: string | null;
|
|
36
|
+
version?: MdKitDocumentVersionToken;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type MdKitDocumentVersionDetail = MdKitDocumentVersionSummary & {
|
|
40
|
+
content: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Storage contract the document hooks talk to. Implement it over tRPC, REST, or
|
|
45
|
+
* anything else; only `readDocument`/`writeDocument` are required, the
|
|
46
|
+
* checkpoint methods are optional and enable version-history UI.
|
|
47
|
+
*/
|
|
48
|
+
export interface MdKitDocumentAdapter {
|
|
49
|
+
readDocument(documentId: string): Promise<MdKitDocumentSnapshot>;
|
|
50
|
+
writeDocument(
|
|
51
|
+
input: MdKitDocumentWriteInput,
|
|
52
|
+
): Promise<MdKitDocumentWriteResult>;
|
|
53
|
+
resyncDocument?(documentId: string): Promise<MdKitDocumentSnapshot>;
|
|
54
|
+
listDocumentVersions?(
|
|
55
|
+
documentId: string,
|
|
56
|
+
): Promise<MdKitDocumentVersionSummary[]>;
|
|
57
|
+
readDocumentVersion?(input: {
|
|
58
|
+
documentId: string;
|
|
59
|
+
versionId: string;
|
|
60
|
+
}): Promise<MdKitDocumentVersionDetail | null>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type MdKitCollaborationStatus =
|
|
64
|
+
| "connecting"
|
|
65
|
+
| "connected"
|
|
66
|
+
| "disconnected";
|
|
67
|
+
|
|
68
|
+
export type MdKitCollaborationParticipant = {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
color?: string;
|
|
72
|
+
imageUrl?: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type MdKitCollaborationPresence = MdKitCollaborationParticipant & {
|
|
76
|
+
clientId: number;
|
|
77
|
+
isLocal: boolean;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type MdKitCollaborationSession = {
|
|
81
|
+
collaborator: MdKitCollaborationParticipant;
|
|
82
|
+
document: Y.Doc;
|
|
83
|
+
isCollaborating: boolean;
|
|
84
|
+
otherParticipants: MdKitCollaborationPresence[];
|
|
85
|
+
participants: MdKitCollaborationPresence[];
|
|
86
|
+
provider: HocuspocusProvider | null;
|
|
87
|
+
roomName: string;
|
|
88
|
+
status: MdKitCollaborationStatus;
|
|
89
|
+
};
|