@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.
Files changed (160) hide show
  1. package/README.md +8 -2
  2. package/dist/collaboration/useMdKitCollaboration.d.ts +5 -0
  3. package/dist/collaboration/useMdKitCollaboration.d.ts.map +1 -0
  4. package/dist/collaboration/useMdKitCollaboration.js +4 -0
  5. package/dist/core/checkpointPolicy.d.ts +10 -0
  6. package/dist/core/checkpointPolicy.d.ts.map +1 -0
  7. package/dist/core/checkpointPolicy.js +9 -0
  8. package/dist/core/documentEngine.d.ts +1 -0
  9. package/dist/core/documentEngine.d.ts.map +1 -0
  10. package/dist/core/index.d.ts +1 -0
  11. package/dist/core/index.d.ts.map +1 -0
  12. package/dist/document/MdKitConflictPanel.d.ts +5 -0
  13. package/dist/document/MdKitConflictPanel.d.ts.map +1 -0
  14. package/dist/document/MdKitConflictPanel.js +4 -0
  15. package/dist/document/MdKitDocumentToolbar.d.ts +6 -0
  16. package/dist/document/MdKitDocumentToolbar.d.ts.map +1 -0
  17. package/dist/document/MdKitDocumentToolbar.js +5 -0
  18. package/dist/document/documentTypes.d.ts +6 -0
  19. package/dist/document/documentTypes.d.ts.map +1 -0
  20. package/dist/document/useMdKitDocument.d.ts +5 -0
  21. package/dist/document/useMdKitDocument.d.ts.map +1 -0
  22. package/dist/document/useMdKitDocument.js +4 -0
  23. package/dist/fastify.d.ts +1 -0
  24. package/dist/fastify.d.ts.map +1 -0
  25. package/dist/index.d.ts +4 -1
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/markdown/MarkdownBubbleMenu.d.ts +1 -0
  28. package/dist/markdown/MarkdownBubbleMenu.d.ts.map +1 -0
  29. package/dist/markdown/MarkdownPasteExtension.d.ts +1 -0
  30. package/dist/markdown/MarkdownPasteExtension.d.ts.map +1 -0
  31. package/dist/markdown/MarkdownSearchExtension.d.ts +1 -0
  32. package/dist/markdown/MarkdownSearchExtension.d.ts.map +1 -0
  33. package/dist/markdown/MarkdownSearchPanel.d.ts +1 -0
  34. package/dist/markdown/MarkdownSearchPanel.d.ts.map +1 -0
  35. package/dist/markdown/MdKitEditor.d.ts +11 -0
  36. package/dist/markdown/MdKitEditor.d.ts.map +1 -0
  37. package/dist/markdown/MdKitEditor.js +10 -2
  38. package/dist/markdown/MdKitView.d.ts +9 -1
  39. package/dist/markdown/MdKitView.d.ts.map +1 -0
  40. package/dist/markdown/MdKitView.js +7 -2
  41. package/dist/markdown/TiptapMarkdownSurface.d.ts +1 -0
  42. package/dist/markdown/TiptapMarkdownSurface.d.ts.map +1 -0
  43. package/dist/markdown/TiptapMarkdownSurface.js +3 -22
  44. package/dist/markdown/createMdKitTiptapExtensions.d.ts +1 -0
  45. package/dist/markdown/createMdKitTiptapExtensions.d.ts.map +1 -0
  46. package/dist/markdown/editorDebug.d.ts +1 -0
  47. package/dist/markdown/editorDebug.d.ts.map +1 -0
  48. package/dist/markdown/markdownFenceRanges.d.ts +1 -0
  49. package/dist/markdown/markdownFenceRanges.d.ts.map +1 -0
  50. package/dist/markdown/normalizeMarkdownSerialization.d.ts +1 -0
  51. package/dist/markdown/normalizeMarkdownSerialization.d.ts.map +1 -0
  52. package/dist/markdown/prepareMarkdownForEditorHydration.d.ts +1 -0
  53. package/dist/markdown/prepareMarkdownForEditorHydration.d.ts.map +1 -0
  54. package/dist/markdown/preserveMarkdownWhitespace.d.ts +1 -0
  55. package/dist/markdown/preserveMarkdownWhitespace.d.ts.map +1 -0
  56. package/dist/markdown/yamlFrontMatter.d.ts +1 -0
  57. package/dist/markdown/yamlFrontMatter.d.ts.map +1 -0
  58. package/dist/server.d.ts +1 -0
  59. package/dist/server.d.ts.map +1 -0
  60. package/dist/theme/MdKitThemeEditor.d.ts +5 -0
  61. package/dist/theme/MdKitThemeEditor.d.ts.map +1 -0
  62. package/dist/theme/MdKitThemeEditor.js +4 -0
  63. package/dist/theme/editorTheme.d.ts +1 -0
  64. package/dist/theme/editorTheme.d.ts.map +1 -0
  65. package/dist/theme/editorTheme.js +8 -8
  66. package/dist/transport/backend.d.ts +13 -0
  67. package/dist/transport/backend.d.ts.map +1 -0
  68. package/dist/transport/backend.js +6 -0
  69. package/dist/transport/fastify.d.ts +5 -0
  70. package/dist/transport/fastify.d.ts.map +1 -0
  71. package/dist/transport/fastify.js +4 -0
  72. package/dist/transport/http.d.ts +1 -0
  73. package/dist/transport/http.d.ts.map +1 -0
  74. package/dist/transport/index.d.ts +1 -0
  75. package/dist/transport/index.d.ts.map +1 -0
  76. package/dist/transport/rest.d.ts +6 -0
  77. package/dist/transport/rest.d.ts.map +1 -0
  78. package/dist/transport/rest.js +5 -0
  79. package/dist/transport/store.d.ts +1 -0
  80. package/dist/transport/store.d.ts.map +1 -0
  81. package/dist/transport/trpcClient.d.ts +8 -0
  82. package/dist/transport/trpcClient.d.ts.map +1 -0
  83. package/dist/transport/trpcClient.js +7 -0
  84. package/dist/transport/trpcServer.d.ts +6 -0
  85. package/dist/transport/trpcServer.d.ts.map +1 -0
  86. package/dist/transport/trpcServer.js +5 -0
  87. package/dist/trpc/client.d.ts +1 -0
  88. package/dist/trpc/client.d.ts.map +1 -0
  89. package/dist/trpc/server.d.ts +1 -0
  90. package/dist/trpc/server.d.ts.map +1 -0
  91. package/dist/trpc.d.ts +1 -0
  92. package/dist/trpc.d.ts.map +1 -0
  93. package/dist/ui/joinClassNames.d.ts +1 -0
  94. package/dist/ui/joinClassNames.d.ts.map +1 -0
  95. package/dist/versioning/VersionHistoryPanel.d.ts +5 -0
  96. package/dist/versioning/VersionHistoryPanel.d.ts.map +1 -0
  97. package/dist/versioning/VersionHistoryPanel.js +4 -0
  98. package/dist/versioning/useMdKitDocumentVersions.d.ts +5 -0
  99. package/dist/versioning/useMdKitDocumentVersions.d.ts.map +1 -0
  100. package/dist/versioning/useMdKitDocumentVersions.js +4 -0
  101. package/dist/yjs/MdKitMarkdownYjs.d.ts +1 -0
  102. package/dist/yjs/MdKitMarkdownYjs.d.ts.map +1 -0
  103. package/dist/yjs/index.d.ts +1 -0
  104. package/dist/yjs/index.d.ts.map +1 -0
  105. package/package.json +10 -12
  106. package/src/collaboration/useMdKitCollaboration.ts +528 -0
  107. package/src/core/checkpointPolicy.ts +107 -0
  108. package/src/core/documentEngine.ts +175 -0
  109. package/src/core/index.ts +33 -0
  110. package/src/document/MdKitConflictPanel.tsx +129 -0
  111. package/src/document/MdKitDocumentToolbar.tsx +141 -0
  112. package/src/document/documentTypes.ts +89 -0
  113. package/src/document/useMdKitDocument.ts +543 -0
  114. package/src/fastify.ts +6 -0
  115. package/src/index.ts +89 -0
  116. package/src/markdown/MarkdownBubbleMenu.tsx +271 -0
  117. package/src/markdown/MarkdownPasteExtension.ts +81 -0
  118. package/src/markdown/MarkdownSearchExtension.ts +77 -0
  119. package/src/markdown/MarkdownSearchPanel.tsx +98 -0
  120. package/src/markdown/MdKitEditor.tsx +75 -0
  121. package/src/markdown/MdKitView.tsx +80 -0
  122. package/src/markdown/TiptapMarkdownSurface.tsx +923 -0
  123. package/src/markdown/createMdKitTiptapExtensions.ts +42 -0
  124. package/src/markdown/editorDebug.ts +5 -0
  125. package/src/markdown/markdownFenceRanges.ts +68 -0
  126. package/src/markdown/normalizeMarkdownSerialization.ts +55 -0
  127. package/src/markdown/prepareMarkdownForEditorHydration.ts +23 -0
  128. package/src/markdown/preserveMarkdownWhitespace.ts +143 -0
  129. package/src/markdown/yamlFrontMatter.ts +135 -0
  130. package/src/server.ts +6 -0
  131. package/src/styles.css +125 -53
  132. package/src/theme/MdKitThemeEditor.tsx +134 -0
  133. package/src/theme/editorTheme.ts +72 -0
  134. package/src/transport/backend.ts +220 -0
  135. package/src/transport/fastify.ts +57 -0
  136. package/src/transport/http.ts +126 -0
  137. package/src/transport/index.ts +12 -0
  138. package/src/transport/rest.ts +80 -0
  139. package/src/transport/store.ts +45 -0
  140. package/src/transport/trpcClient.ts +90 -0
  141. package/src/transport/trpcServer.ts +66 -0
  142. package/src/trpc/client.ts +11 -0
  143. package/src/trpc/server.ts +12 -0
  144. package/src/trpc.ts +11 -0
  145. package/src/ui/joinClassNames.ts +3 -0
  146. package/src/versioning/VersionHistoryPanel.tsx +146 -0
  147. package/src/versioning/useMdKitDocumentVersions.ts +146 -0
  148. package/src/yjs/MdKitMarkdownYjs.ts +111 -0
  149. package/src/yjs/index.ts +8 -0
  150. package/docs/.vitepress/config.ts +0 -47
  151. package/docs/api.md +0 -512
  152. package/docs/architecture.md +0 -96
  153. package/docs/collaboration-persistence.md +0 -147
  154. package/docs/index.md +0 -341
  155. package/docs/permissions.md +0 -139
  156. package/docs/plain-text.md +0 -131
  157. package/docs/rest.md +0 -98
  158. package/docs/shadcn.md +0 -125
  159. package/docs/styling.md +0 -373
  160. 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
+ };