@mp-lb/mdkit 0.3.1 → 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 +10 -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,543 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import type {
3
+ MdKitDocumentAdapter,
4
+ MdKitDocumentVersionToken,
5
+ } from "./documentTypes";
6
+
7
+ export type UseMdKitDocumentOptions = {
8
+ adapter: Pick<
9
+ MdKitDocumentAdapter,
10
+ "readDocument" | "writeDocument" | "resyncDocument"
11
+ >;
12
+ debounceMs?: number;
13
+ documentId: string | null;
14
+ pollMs?: number;
15
+ };
16
+
17
+ export type MdKitDocumentConflictDetails = {
18
+ baseContent: string;
19
+ localContent: string;
20
+ remoteContent: string | null;
21
+ remoteUpdatedAt: string | null;
22
+ remoteVersion: MdKitDocumentVersionToken;
23
+ };
24
+
25
+ export type MdKitDocumentController = {
26
+ conflict: boolean;
27
+ conflictDetails: MdKitDocumentConflictDetails | null;
28
+ error: string | null;
29
+ isDirty: boolean;
30
+ isFocused: boolean;
31
+ isLoading: boolean;
32
+ revision: number;
33
+ saveNow: () => Promise<boolean>;
34
+ saveStatus: "idle" | "pending" | "saving" | "saved";
35
+ forceSave: () => Promise<boolean>;
36
+ resync: () => Promise<void>;
37
+ setContent: (next: string) => void;
38
+ setFocused: (focused: boolean) => void;
39
+ updatedAt: string | null;
40
+ value: string;
41
+ version: MdKitDocumentVersionToken;
42
+ };
43
+
44
+ const emptyDocumentState = {
45
+ content: "",
46
+ updatedAt: null,
47
+ version: null,
48
+ } as const;
49
+
50
+ /**
51
+ * Connects an editor to a storage adapter: loads the document, debounces
52
+ * autosave, tracks dirty/save status, and surfaces conflicts for resolution.
53
+ */
54
+ export const useMdKitDocument = (
55
+ options: UseMdKitDocumentOptions,
56
+ ): MdKitDocumentController => {
57
+ const { adapter, debounceMs = 1000, documentId, pollMs = 2000 } = options;
58
+
59
+ const [local, setLocal] = useState("");
60
+ const [base, setBase] = useState("");
61
+ const [version, setVersion] = useState<MdKitDocumentVersionToken>(null);
62
+ const [updatedAt, setUpdatedAt] = useState<string | null>(null);
63
+ const [revision, setRevision] = useState(0);
64
+
65
+ const [isLoading, setIsLoading] = useState(!!documentId);
66
+ const [error, setError] = useState<string | null>(null);
67
+ const [conflict, setConflict] = useState(false);
68
+
69
+ const [conflictDetails, setConflictDetails] =
70
+ useState<MdKitDocumentConflictDetails | null>(null);
71
+
72
+ const [isFocused, setIsFocused] = useState(false);
73
+
74
+ const [saveStatus, setSaveStatus] = useState<
75
+ "idle" | "pending" | "saving" | "saved"
76
+ >("idle");
77
+
78
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
79
+ const isSavingRef = useRef(false);
80
+ const queuedSaveRef = useRef(false);
81
+ const localRef = useRef(local);
82
+ const baseRef = useRef(base);
83
+ const versionRef = useRef(version);
84
+ const updatedAtRef = useRef(updatedAt);
85
+ const conflictRef = useRef(conflict);
86
+ const focusedRef = useRef(isFocused);
87
+
88
+ useEffect(() => {
89
+ localRef.current = local;
90
+ }, [local]);
91
+
92
+ useEffect(() => {
93
+ baseRef.current = base;
94
+ }, [base]);
95
+
96
+ useEffect(() => {
97
+ versionRef.current = version;
98
+ }, [version]);
99
+
100
+ useEffect(() => {
101
+ updatedAtRef.current = updatedAt;
102
+ }, [updatedAt]);
103
+
104
+ useEffect(() => {
105
+ conflictRef.current = conflict;
106
+ }, [conflict]);
107
+
108
+ useEffect(() => {
109
+ focusedRef.current = isFocused;
110
+ }, [isFocused]);
111
+
112
+ const isDirty = local !== base;
113
+
114
+ const setFocused = useCallback((focused: boolean) => {
115
+ focusedRef.current = focused;
116
+ setIsFocused(focused);
117
+ }, []);
118
+
119
+ const applyRemoteSnapshot = useCallback(
120
+ (
121
+ next: {
122
+ content: string;
123
+ updatedAt?: string | null;
124
+ version: MdKitDocumentVersionToken;
125
+ },
126
+ bumpRevision: boolean,
127
+ ) => {
128
+ localRef.current = next.content;
129
+ baseRef.current = next.content;
130
+ versionRef.current = next.version;
131
+ updatedAtRef.current = next.updatedAt ?? null;
132
+ conflictRef.current = false;
133
+
134
+ setLocal(next.content);
135
+ setBase(next.content);
136
+ setVersion(next.version);
137
+ setUpdatedAt(next.updatedAt ?? null);
138
+ setConflict(false);
139
+ setConflictDetails(null);
140
+ setError(null);
141
+
142
+ if (bumpRevision) {
143
+ setRevision((current: number) => current + 1);
144
+ }
145
+ },
146
+ [],
147
+ );
148
+
149
+ const reset = useCallback(() => {
150
+ localRef.current = emptyDocumentState.content;
151
+ baseRef.current = emptyDocumentState.content;
152
+ versionRef.current = emptyDocumentState.version;
153
+ updatedAtRef.current = emptyDocumentState.updatedAt;
154
+ conflictRef.current = false;
155
+ focusedRef.current = false;
156
+
157
+ setLocal(emptyDocumentState.content);
158
+ setBase(emptyDocumentState.content);
159
+ setVersion(emptyDocumentState.version);
160
+ setUpdatedAt(emptyDocumentState.updatedAt);
161
+ setRevision(0);
162
+ setIsLoading(false);
163
+ setError(null);
164
+ setConflict(false);
165
+ setConflictDetails(null);
166
+ setSaveStatus("idle");
167
+ setFocused(false);
168
+
169
+ if (debounceRef.current) {
170
+ clearTimeout(debounceRef.current);
171
+ debounceRef.current = null;
172
+ }
173
+
174
+ isSavingRef.current = false;
175
+ queuedSaveRef.current = false;
176
+ }, [setFocused]);
177
+
178
+ useEffect(() => {
179
+ if (!documentId) {
180
+ reset();
181
+ return;
182
+ }
183
+
184
+ let cancelled = false;
185
+
186
+ setIsLoading(true);
187
+ setError(null);
188
+ setConflict(false);
189
+ setConflictDetails(null);
190
+ setSaveStatus("idle");
191
+ queuedSaveRef.current = false;
192
+
193
+ if (debounceRef.current) {
194
+ clearTimeout(debounceRef.current);
195
+ debounceRef.current = null;
196
+ }
197
+
198
+ adapter
199
+ .readDocument(documentId)
200
+ .then((result) => {
201
+ if (cancelled) {
202
+ return;
203
+ }
204
+
205
+ applyRemoteSnapshot(result, true);
206
+ setIsLoading(false);
207
+ })
208
+ .catch((err) => {
209
+ if (cancelled) {
210
+ return;
211
+ }
212
+
213
+ setError(String(err));
214
+ setIsLoading(false);
215
+ });
216
+
217
+ return () => {
218
+ cancelled = true;
219
+ };
220
+ }, [adapter, applyRemoteSnapshot, documentId, reset]);
221
+
222
+ useEffect(() => {
223
+ if (!documentId || pollMs <= 0) {
224
+ return;
225
+ }
226
+
227
+ let cancelled = false;
228
+
229
+ const poll = async () => {
230
+ if (cancelled || isSavingRef.current) {
231
+ return;
232
+ }
233
+
234
+ try {
235
+ const remote = await adapter.readDocument(documentId);
236
+
237
+ if (cancelled) {
238
+ return;
239
+ }
240
+
241
+ const remoteContent = remote.content;
242
+ const remoteVersion = remote.version;
243
+ const localNow = localRef.current;
244
+ const baseNow = baseRef.current;
245
+ const versionNow = versionRef.current;
246
+ const dirtyNow = localNow !== baseNow;
247
+
248
+ if (dirtyNow) {
249
+ if (remoteContent !== baseNow || remoteVersion !== versionNow) {
250
+ const nextConflictDetails = {
251
+ baseContent: baseNow,
252
+ localContent: localNow,
253
+ remoteContent,
254
+ remoteUpdatedAt: remote.updatedAt ?? null,
255
+ remoteVersion,
256
+ };
257
+
258
+ conflictRef.current = true;
259
+ versionRef.current = remoteVersion;
260
+ updatedAtRef.current = remote.updatedAt ?? updatedAtRef.current;
261
+ setConflict(true);
262
+ setConflictDetails(nextConflictDetails);
263
+ setError("Remote document changed while you have unsaved edits.");
264
+ setVersion(remoteVersion);
265
+ setUpdatedAt(remote.updatedAt ?? updatedAtRef.current);
266
+ }
267
+
268
+ return;
269
+ }
270
+
271
+ if (focusedRef.current) {
272
+ return;
273
+ }
274
+
275
+ if (
276
+ remoteContent !== baseNow ||
277
+ remoteVersion !== versionNow ||
278
+ conflictRef.current
279
+ ) {
280
+ applyRemoteSnapshot(remote, true);
281
+ }
282
+ } catch {
283
+ // Explicit load/save actions surface errors for callers.
284
+ }
285
+ };
286
+
287
+ const interval = setInterval(() => {
288
+ void poll();
289
+ }, pollMs);
290
+
291
+ return () => {
292
+ cancelled = true;
293
+ clearInterval(interval);
294
+ };
295
+ }, [adapter, applyRemoteSnapshot, documentId, pollMs]);
296
+
297
+ const commitSave = useCallback(
298
+ async (input: {
299
+ baseVersion: MdKitDocumentVersionToken;
300
+ force?: boolean;
301
+ }) => {
302
+ if (!documentId) {
303
+ return false;
304
+ }
305
+
306
+ const contentToSave = localRef.current;
307
+ const currentBase = baseRef.current;
308
+
309
+ if (contentToSave === currentBase) {
310
+ setSaveStatus("saved");
311
+ return true;
312
+ }
313
+
314
+ const result = await adapter.writeDocument({
315
+ baseVersion: input.baseVersion,
316
+ content: contentToSave,
317
+ documentId,
318
+ ...(input.force ? { force: true } : {}),
319
+ });
320
+
321
+ if ("conflict" in result) {
322
+ let remoteContent: string | null = null;
323
+ let remoteVersion = result.version ?? versionRef.current;
324
+ let remoteUpdatedAt = result.updatedAt ?? updatedAtRef.current;
325
+
326
+ try {
327
+ const remote = await adapter.readDocument(documentId);
328
+ remoteContent = remote.content;
329
+ remoteVersion = remote.version;
330
+ remoteUpdatedAt = remote.updatedAt ?? null;
331
+ } catch {
332
+ // Conflict resolution can still proceed by keeping remote via resync
333
+ // or overwriting remote even if the preview fetch fails.
334
+ }
335
+
336
+ const nextConflictDetails = {
337
+ baseContent: currentBase,
338
+ localContent: contentToSave,
339
+ remoteContent,
340
+ remoteUpdatedAt,
341
+ remoteVersion,
342
+ };
343
+
344
+ conflictRef.current = true;
345
+ setConflict(true);
346
+ setConflictDetails(nextConflictDetails);
347
+ setSaveStatus("idle");
348
+ setError("Remote document changed while you have unsaved edits.");
349
+
350
+ if (remoteVersion !== undefined) {
351
+ versionRef.current = remoteVersion;
352
+ setVersion(remoteVersion);
353
+ }
354
+
355
+ if (remoteUpdatedAt !== undefined) {
356
+ updatedAtRef.current = remoteUpdatedAt ?? null;
357
+ setUpdatedAt(remoteUpdatedAt ?? null);
358
+ }
359
+
360
+ return false;
361
+ }
362
+
363
+ baseRef.current = contentToSave;
364
+ versionRef.current = result.version;
365
+ updatedAtRef.current = result.updatedAt ?? updatedAtRef.current;
366
+ conflictRef.current = false;
367
+
368
+ setBase(contentToSave);
369
+ setVersion(result.version);
370
+ setUpdatedAt(result.updatedAt ?? updatedAtRef.current);
371
+ setConflict(false);
372
+ setConflictDetails(null);
373
+ setSaveStatus("saved");
374
+ setError(null);
375
+ return true;
376
+ },
377
+ [adapter, documentId],
378
+ );
379
+
380
+ const scheduleSave = useCallback(() => {
381
+ if (!documentId || conflictRef.current) {
382
+ return;
383
+ }
384
+
385
+ if (localRef.current === baseRef.current) {
386
+ setSaveStatus("saved");
387
+ return;
388
+ }
389
+
390
+ setSaveStatus("pending");
391
+
392
+ if (debounceRef.current) {
393
+ clearTimeout(debounceRef.current);
394
+ }
395
+
396
+ debounceRef.current = setTimeout(() => {
397
+ if (isSavingRef.current || conflictRef.current) {
398
+ queuedSaveRef.current = true;
399
+ debounceRef.current = null;
400
+ return;
401
+ }
402
+
403
+ isSavingRef.current = true;
404
+ queuedSaveRef.current = false;
405
+ setSaveStatus("saving");
406
+
407
+ commitSave({ baseVersion: versionRef.current })
408
+ .catch((err: unknown) => {
409
+ setError(`Save failed: ${String(err)}`);
410
+ setSaveStatus("idle");
411
+ return false;
412
+ })
413
+ .finally(() => {
414
+ isSavingRef.current = false;
415
+ debounceRef.current = null;
416
+ });
417
+ }, debounceMs);
418
+ }, [commitSave, debounceMs, documentId]);
419
+
420
+ const setContent = useCallback(
421
+ (next: string) => {
422
+ setLocal(next);
423
+ localRef.current = next;
424
+ scheduleSave();
425
+ },
426
+ [scheduleSave],
427
+ );
428
+
429
+ useEffect(() => {
430
+ if (
431
+ saveStatus !== "saved" ||
432
+ !queuedSaveRef.current ||
433
+ isSavingRef.current ||
434
+ conflictRef.current ||
435
+ localRef.current === baseRef.current
436
+ ) {
437
+ return;
438
+ }
439
+
440
+ queuedSaveRef.current = false;
441
+ scheduleSave();
442
+ }, [saveStatus, scheduleSave]);
443
+
444
+ const resync = useCallback(async () => {
445
+ if (!documentId) {
446
+ return;
447
+ }
448
+
449
+ try {
450
+ const readRemote = adapter.resyncDocument ?? adapter.readDocument;
451
+ const result = await readRemote(documentId);
452
+ applyRemoteSnapshot(result, true);
453
+ } catch (err) {
454
+ setError(`Failed to resync: ${String(err)}`);
455
+ }
456
+ }, [adapter, applyRemoteSnapshot, documentId]);
457
+
458
+ const forceSave = useCallback(async (): Promise<boolean> => {
459
+ if (!documentId || isSavingRef.current) {
460
+ return false;
461
+ }
462
+
463
+ isSavingRef.current = true;
464
+ setSaveStatus("saving");
465
+
466
+ try {
467
+ return await commitSave({ baseVersion: null, force: true });
468
+ } catch (err) {
469
+ setError(`Force save failed: ${String(err)}`);
470
+ setSaveStatus("idle");
471
+ return false;
472
+ } finally {
473
+ isSavingRef.current = false;
474
+ }
475
+ }, [commitSave, documentId]);
476
+
477
+ const saveNow = useCallback(async (): Promise<boolean> => {
478
+ if (!documentId) {
479
+ return false;
480
+ }
481
+
482
+ if (debounceRef.current) {
483
+ clearTimeout(debounceRef.current);
484
+ debounceRef.current = null;
485
+ }
486
+
487
+ if (isSavingRef.current || conflictRef.current) {
488
+ return false;
489
+ }
490
+
491
+ isSavingRef.current = true;
492
+ setSaveStatus("saving");
493
+
494
+ try {
495
+ return await commitSave({ baseVersion: versionRef.current });
496
+ } catch (err) {
497
+ setError(`Save failed: ${String(err)}`);
498
+ setSaveStatus("idle");
499
+ return false;
500
+ } finally {
501
+ isSavingRef.current = false;
502
+ }
503
+ }, [commitSave, documentId]);
504
+
505
+ return useMemo(
506
+ () => ({
507
+ conflict,
508
+ conflictDetails,
509
+ error,
510
+ forceSave,
511
+ isDirty,
512
+ isFocused,
513
+ isLoading,
514
+ resync,
515
+ revision,
516
+ saveNow,
517
+ saveStatus,
518
+ setContent,
519
+ setFocused,
520
+ updatedAt,
521
+ value: local,
522
+ version,
523
+ }),
524
+ [
525
+ conflict,
526
+ conflictDetails,
527
+ error,
528
+ forceSave,
529
+ isDirty,
530
+ isFocused,
531
+ isLoading,
532
+ local,
533
+ resync,
534
+ revision,
535
+ saveNow,
536
+ saveStatus,
537
+ setContent,
538
+ setFocused,
539
+ updatedAt,
540
+ version,
541
+ ],
542
+ );
543
+ };
package/src/fastify.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { registerMdKitFastify } from "./transport/fastify";
2
+ export type { RegisterMdKitFastifyOptions } from "./transport/fastify";
3
+ export type {
4
+ MdKitRestoreDocumentVersionInput,
5
+ MdKitTransportStore,
6
+ } from "./transport/store";
package/src/index.ts ADDED
@@ -0,0 +1,89 @@
1
+ export { useMdKitCollaboration } from "./collaboration/useMdKitCollaboration";
2
+ export {
3
+ CheckpointPolicy,
4
+ measureMdKitEditDistance,
5
+ } from "./core/checkpointPolicy";
6
+ export {
7
+ createMdKitDocumentRecord,
8
+ detectMdKitDocumentConflict,
9
+ normalizeMdKitVersionToken,
10
+ restoreMdKitDocumentVersion,
11
+ writeMdKitDocumentRecord,
12
+ } from "./core/documentEngine";
13
+ export { useMdKitDocument } from "./document/useMdKitDocument";
14
+ export { MdKitConflictPanel } from "./document/MdKitConflictPanel";
15
+ export { MdKitDocumentToolbar } from "./document/MdKitDocumentToolbar";
16
+ export { MdKitEditor } from "./markdown/MdKitEditor";
17
+ export { MdKitView } from "./markdown/MdKitView";
18
+ export {
19
+ extractYamlFrontMatter,
20
+ hasYamlFrontMatter,
21
+ parseYamlFrontMatter,
22
+ prependYamlFrontMatter,
23
+ removeYamlFrontMatter,
24
+ } from "./markdown/yamlFrontMatter";
25
+ export { MdKitThemeEditor } from "./theme/MdKitThemeEditor";
26
+ export { createMdKitRestAdapter } from "./transport/rest";
27
+ export {
28
+ createMdKitEditorThemeStyle,
29
+ darkMdKitEditorTheme,
30
+ defaultMdKitEditorTheme,
31
+ } from "./theme/editorTheme";
32
+ export { VersionHistoryPanel } from "./versioning/VersionHistoryPanel";
33
+ export { useMdKitDocumentVersions } from "./versioning/useMdKitDocumentVersions";
34
+ export { yjs } from "./yjs/index";
35
+
36
+ export type {
37
+ MdKitCheckpointPolicy,
38
+ MdKitCheckpointPolicyInput,
39
+ MdKitSmartCheckpointPolicyOptions,
40
+ } from "./core/checkpointPolicy";
41
+ export type {
42
+ CreateMdKitDocumentRecordInput,
43
+ MdKitDocumentRecord,
44
+ RestoreMdKitDocumentVersionInput,
45
+ RestoreMdKitDocumentVersionResult,
46
+ WriteMdKitDocumentRecordInput,
47
+ WriteMdKitDocumentRecordResult,
48
+ } from "./core/documentEngine";
49
+ export type { UseMdKitCollaborationOptions } from "./collaboration/useMdKitCollaboration";
50
+ export type { UseMdKitDocumentOptions } from "./document/useMdKitDocument";
51
+ export type {
52
+ MdKitCollaborationParticipant,
53
+ MdKitCollaborationPresence,
54
+ MdKitCollaborationSession,
55
+ MdKitCollaborationStatus,
56
+ MdKitDocumentAdapter,
57
+ MdKitDocumentSnapshot,
58
+ MdKitDocumentVersionDetail,
59
+ MdKitDocumentVersionSummary,
60
+ MdKitDocumentVersionToken,
61
+ MdKitDocumentWriteInput,
62
+ MdKitDocumentWriteResult,
63
+ } from "./document/documentTypes";
64
+
65
+ export type {
66
+ MdKitDocumentConflictDetails,
67
+ MdKitDocumentController,
68
+ } from "./document/useMdKitDocument";
69
+ export type { MdKitConflictPanelProps } from "./document/MdKitConflictPanel";
70
+ export type { MdKitDocumentToolbarProps } from "./document/MdKitDocumentToolbar";
71
+ export type { MdKitEditorProps } from "./markdown/MdKitEditor";
72
+ export type { MdKitEditorDebugEvent } from "./markdown/editorDebug";
73
+ export type { MdKitViewProps } from "./markdown/MdKitView";
74
+ export type {
75
+ MdKitYamlFrontMatter,
76
+ MdKitYamlFrontMatterExtraction,
77
+ } from "./markdown/yamlFrontMatter";
78
+ export type { MdKitThemeEditorProps } from "./theme/MdKitThemeEditor";
79
+ export type { CreateMdKitRestAdapterOptions } from "./transport/rest";
80
+ export type {
81
+ MdKitEditorTheme,
82
+ MdKitEditorThemeStyle,
83
+ } from "./theme/editorTheme";
84
+ export type {
85
+ MdKitDocumentVersionsController,
86
+ UseMdKitDocumentVersionsOptions,
87
+ } from "./versioning/useMdKitDocumentVersions";
88
+ export type { VersionHistoryPanelProps } from "./versioning/VersionHistoryPanel";
89
+ export type { MdKitMarkdownYjsOptions } from "./yjs/index";