@mp-lb/mdkit 0.0.1-main.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +132 -0
  2. package/dist/collaboration/useMdKitCollaboration.d.ts +10 -0
  3. package/dist/collaboration/useMdKitCollaboration.js +90 -0
  4. package/dist/core/documentEngine.d.ts +38 -0
  5. package/dist/core/documentEngine.js +95 -0
  6. package/dist/core/documentEngine.test.d.ts +1 -0
  7. package/dist/core/documentEngine.test.js +119 -0
  8. package/dist/core/index.d.ts +3 -0
  9. package/dist/core/index.js +1 -0
  10. package/dist/document/MdKitConflictPanel.d.ts +7 -0
  11. package/dist/document/MdKitConflictPanel.js +41 -0
  12. package/dist/document/MdKitDocumentToolbar.d.ts +13 -0
  13. package/dist/document/MdKitDocumentToolbar.js +48 -0
  14. package/dist/document/documentTypes.d.ts +57 -0
  15. package/dist/document/documentTypes.js +1 -0
  16. package/dist/document/useMdKitDocument.d.ts +33 -0
  17. package/dist/document/useMdKitDocument.js +396 -0
  18. package/dist/document/useMdKitDocument.test.d.ts +1 -0
  19. package/dist/document/useMdKitDocument.test.js +151 -0
  20. package/dist/fastify.d.ts +3 -0
  21. package/dist/fastify.js +1 -0
  22. package/dist/index.d.ts +23 -0
  23. package/dist/index.js +11 -0
  24. package/dist/markdown/MarkdownBubbleMenu.d.ts +6 -0
  25. package/dist/markdown/MarkdownBubbleMenu.js +29 -0
  26. package/dist/markdown/MdKitEditor.d.ts +25 -0
  27. package/dist/markdown/MdKitEditor.js +7 -0
  28. package/dist/markdown/MdKitEditor.test.d.ts +1 -0
  29. package/dist/markdown/MdKitEditor.test.js +126 -0
  30. package/dist/markdown/TiptapMarkdownSurface.d.ts +23 -0
  31. package/dist/markdown/TiptapMarkdownSurface.js +430 -0
  32. package/dist/markdown/editorDebug.d.ts +5 -0
  33. package/dist/markdown/editorDebug.js +1 -0
  34. package/dist/markdown/markdownFenceRanges.d.ts +6 -0
  35. package/dist/markdown/markdownFenceRanges.js +41 -0
  36. package/dist/markdown/normalizeMarkdownSerialization.d.ts +1 -0
  37. package/dist/markdown/normalizeMarkdownSerialization.js +34 -0
  38. package/dist/markdown/normalizeMarkdownSerialization.test.d.ts +1 -0
  39. package/dist/markdown/normalizeMarkdownSerialization.test.js +16 -0
  40. package/dist/markdown/prepareMarkdownForEditorHydration.d.ts +1 -0
  41. package/dist/markdown/prepareMarkdownForEditorHydration.js +12 -0
  42. package/dist/markdown/prepareMarkdownForEditorHydration.test.d.ts +1 -0
  43. package/dist/markdown/prepareMarkdownForEditorHydration.test.js +13 -0
  44. package/dist/markdown/preserveMarkdownWhitespace.d.ts +1 -0
  45. package/dist/markdown/preserveMarkdownWhitespace.js +86 -0
  46. package/dist/markdown/preserveMarkdownWhitespace.test.d.ts +1 -0
  47. package/dist/markdown/preserveMarkdownWhitespace.test.js +25 -0
  48. package/dist/test/setup.d.ts +1 -0
  49. package/dist/test/setup.js +13 -0
  50. package/dist/theme/MdKitThemeEditor.d.ts +8 -0
  51. package/dist/theme/MdKitThemeEditor.js +13 -0
  52. package/dist/theme/editorTheme.d.ts +20 -0
  53. package/dist/theme/editorTheme.js +47 -0
  54. package/dist/transport/fastify.d.ts +7 -0
  55. package/dist/transport/fastify.js +19 -0
  56. package/dist/transport/http.d.ts +43 -0
  57. package/dist/transport/http.js +80 -0
  58. package/dist/transport/index.d.ts +5 -0
  59. package/dist/transport/index.js +2 -0
  60. package/dist/transport/rest.d.ts +6 -0
  61. package/dist/transport/rest.js +34 -0
  62. package/dist/transport/store.d.ts +21 -0
  63. package/dist/transport/store.js +1 -0
  64. package/dist/transport/trpcClient.d.ts +81 -0
  65. package/dist/transport/trpcClient.js +21 -0
  66. package/dist/transport/trpcServer.d.ts +72 -0
  67. package/dist/transport/trpcServer.js +45 -0
  68. package/dist/trpc/client.d.ts +3 -0
  69. package/dist/trpc/client.js +1 -0
  70. package/dist/trpc/server.d.ts +3 -0
  71. package/dist/trpc/server.js +1 -0
  72. package/dist/trpc.d.ts +3 -0
  73. package/dist/trpc.js +1 -0
  74. package/dist/ui/joinClassNames.d.ts +1 -0
  75. package/dist/ui/joinClassNames.js +1 -0
  76. package/dist/versioning/VersionHistoryPanel.d.ts +9 -0
  77. package/dist/versioning/VersionHistoryPanel.js +29 -0
  78. package/dist/versioning/useMdKitDocumentVersions.d.ts +16 -0
  79. package/dist/versioning/useMdKitDocumentVersions.js +88 -0
  80. package/dist/versioning/useMdKitDocumentVersions.test.d.ts +1 -0
  81. package/dist/versioning/useMdKitDocumentVersions.test.js +41 -0
  82. package/docs/.vitepress/config.ts +34 -0
  83. package/docs/.vitepress/dist/404.html +22 -0
  84. package/docs/.vitepress/dist/api.html +120 -0
  85. package/docs/.vitepress/dist/architecture.html +25 -0
  86. package/docs/.vitepress/dist/assets/api.md.asncK3PQ.js +96 -0
  87. package/docs/.vitepress/dist/assets/api.md.asncK3PQ.lean.js +1 -0
  88. package/docs/.vitepress/dist/assets/app.BQvrHyG0.js +1 -0
  89. package/docs/.vitepress/dist/assets/architecture.md.BHQLarmZ.js +1 -0
  90. package/docs/.vitepress/dist/assets/architecture.md.BHQLarmZ.lean.js +1 -0
  91. package/docs/.vitepress/dist/assets/chunks/framework.RRduUuAx.js +19 -0
  92. package/docs/.vitepress/dist/assets/chunks/theme.CkCo6Nk1.js +1 -0
  93. package/docs/.vitepress/dist/assets/index.md.CITl-897.js +137 -0
  94. package/docs/.vitepress/dist/assets/index.md.CITl-897.lean.js +1 -0
  95. package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  96. package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  97. package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  98. package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  99. package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  100. package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  101. package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  102. package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  103. package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  104. package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  105. package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  106. package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  107. package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  108. package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  109. package/docs/.vitepress/dist/assets/shadcn.md.C3idOo2N.js +57 -0
  110. package/docs/.vitepress/dist/assets/shadcn.md.C3idOo2N.lean.js +1 -0
  111. package/docs/.vitepress/dist/assets/style.BtrGaL3i.css +1 -0
  112. package/docs/.vitepress/dist/assets/styling.md.B2C6kVFa.js +91 -0
  113. package/docs/.vitepress/dist/assets/styling.md.B2C6kVFa.lean.js +1 -0
  114. package/docs/.vitepress/dist/hashmap.json +1 -0
  115. package/docs/.vitepress/dist/index.html +161 -0
  116. package/docs/.vitepress/dist/shadcn.html +81 -0
  117. package/docs/.vitepress/dist/styling.html +115 -0
  118. package/docs/.vitepress/dist/vp-icons.css +1 -0
  119. package/docs/api.md +343 -0
  120. package/docs/architecture.md +67 -0
  121. package/docs/index.md +244 -0
  122. package/docs/shadcn.md +118 -0
  123. package/docs/styling.md +247 -0
  124. package/package.json +105 -0
  125. package/src/styles.css +676 -0
@@ -0,0 +1,33 @@
1
+ import type { MdKitDocumentAdapter, MdKitDocumentVersionToken } from "./documentTypes";
2
+ export type UseMdKitDocumentOptions = {
3
+ adapter: Pick<MdKitDocumentAdapter, "readDocument" | "writeDocument" | "resyncDocument">;
4
+ debounceMs?: number;
5
+ documentId: string | null;
6
+ pollMs?: number;
7
+ };
8
+ export type MdKitDocumentConflictDetails = {
9
+ baseContent: string;
10
+ localContent: string;
11
+ remoteContent: string | null;
12
+ remoteUpdatedAt: string | null;
13
+ remoteVersion: MdKitDocumentVersionToken;
14
+ };
15
+ export type MdKitDocumentController = {
16
+ conflict: boolean;
17
+ conflictDetails: MdKitDocumentConflictDetails | null;
18
+ error: string | null;
19
+ isDirty: boolean;
20
+ isFocused: boolean;
21
+ isLoading: boolean;
22
+ revision: number;
23
+ saveNow: () => Promise<boolean>;
24
+ saveStatus: "idle" | "pending" | "saving" | "saved";
25
+ forceSave: () => Promise<boolean>;
26
+ resync: () => Promise<void>;
27
+ setContent: (next: string) => void;
28
+ setFocused: (focused: boolean) => void;
29
+ updatedAt: string | null;
30
+ value: string;
31
+ version: MdKitDocumentVersionToken;
32
+ };
33
+ export declare const useMdKitDocument: (options: UseMdKitDocumentOptions) => MdKitDocumentController;
@@ -0,0 +1,396 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ const emptyDocumentState = {
3
+ content: "",
4
+ updatedAt: null,
5
+ version: null,
6
+ };
7
+ export const useMdKitDocument = (options) => {
8
+ const { adapter, debounceMs = 350, documentId, pollMs = 2000 } = options;
9
+ const [local, setLocal] = useState("");
10
+ const [base, setBase] = useState("");
11
+ const [version, setVersion] = useState(null);
12
+ const [updatedAt, setUpdatedAt] = useState(null);
13
+ const [revision, setRevision] = useState(0);
14
+ const [isLoading, setIsLoading] = useState(!!documentId);
15
+ const [error, setError] = useState(null);
16
+ const [conflict, setConflict] = useState(false);
17
+ const [conflictDetails, setConflictDetails] = useState(null);
18
+ const [isFocused, setIsFocused] = useState(false);
19
+ const [saveStatus, setSaveStatus] = useState("idle");
20
+ const debounceRef = useRef(null);
21
+ const isSavingRef = useRef(false);
22
+ const queuedSaveRef = useRef(false);
23
+ const localRef = useRef(local);
24
+ const baseRef = useRef(base);
25
+ const versionRef = useRef(version);
26
+ const updatedAtRef = useRef(updatedAt);
27
+ const conflictRef = useRef(conflict);
28
+ const focusedRef = useRef(isFocused);
29
+ useEffect(() => {
30
+ localRef.current = local;
31
+ }, [local]);
32
+ useEffect(() => {
33
+ baseRef.current = base;
34
+ }, [base]);
35
+ useEffect(() => {
36
+ versionRef.current = version;
37
+ }, [version]);
38
+ useEffect(() => {
39
+ updatedAtRef.current = updatedAt;
40
+ }, [updatedAt]);
41
+ useEffect(() => {
42
+ conflictRef.current = conflict;
43
+ }, [conflict]);
44
+ useEffect(() => {
45
+ focusedRef.current = isFocused;
46
+ }, [isFocused]);
47
+ const isDirty = local !== base;
48
+ const setFocused = useCallback((focused) => {
49
+ focusedRef.current = focused;
50
+ setIsFocused(focused);
51
+ }, []);
52
+ const applyRemoteSnapshot = useCallback((next, bumpRevision) => {
53
+ localRef.current = next.content;
54
+ baseRef.current = next.content;
55
+ versionRef.current = next.version;
56
+ updatedAtRef.current = next.updatedAt ?? null;
57
+ conflictRef.current = false;
58
+ setLocal(next.content);
59
+ setBase(next.content);
60
+ setVersion(next.version);
61
+ setUpdatedAt(next.updatedAt ?? null);
62
+ setConflict(false);
63
+ setConflictDetails(null);
64
+ setError(null);
65
+ if (bumpRevision) {
66
+ setRevision((current) => current + 1);
67
+ }
68
+ }, []);
69
+ const reset = useCallback(() => {
70
+ localRef.current = emptyDocumentState.content;
71
+ baseRef.current = emptyDocumentState.content;
72
+ versionRef.current = emptyDocumentState.version;
73
+ updatedAtRef.current = emptyDocumentState.updatedAt;
74
+ conflictRef.current = false;
75
+ focusedRef.current = false;
76
+ setLocal(emptyDocumentState.content);
77
+ setBase(emptyDocumentState.content);
78
+ setVersion(emptyDocumentState.version);
79
+ setUpdatedAt(emptyDocumentState.updatedAt);
80
+ setRevision(0);
81
+ setIsLoading(false);
82
+ setError(null);
83
+ setConflict(false);
84
+ setConflictDetails(null);
85
+ setSaveStatus("idle");
86
+ setFocused(false);
87
+ if (debounceRef.current) {
88
+ clearTimeout(debounceRef.current);
89
+ debounceRef.current = null;
90
+ }
91
+ isSavingRef.current = false;
92
+ queuedSaveRef.current = false;
93
+ }, [setFocused]);
94
+ useEffect(() => {
95
+ if (!documentId) {
96
+ reset();
97
+ return;
98
+ }
99
+ let cancelled = false;
100
+ setIsLoading(true);
101
+ setError(null);
102
+ setConflict(false);
103
+ setConflictDetails(null);
104
+ setSaveStatus("idle");
105
+ queuedSaveRef.current = false;
106
+ if (debounceRef.current) {
107
+ clearTimeout(debounceRef.current);
108
+ debounceRef.current = null;
109
+ }
110
+ adapter
111
+ .readDocument(documentId)
112
+ .then((result) => {
113
+ if (cancelled) {
114
+ return;
115
+ }
116
+ applyRemoteSnapshot(result, true);
117
+ setIsLoading(false);
118
+ })
119
+ .catch((err) => {
120
+ if (cancelled) {
121
+ return;
122
+ }
123
+ setError(String(err));
124
+ setIsLoading(false);
125
+ });
126
+ return () => {
127
+ cancelled = true;
128
+ };
129
+ }, [adapter, applyRemoteSnapshot, documentId, reset]);
130
+ useEffect(() => {
131
+ if (!documentId || pollMs <= 0) {
132
+ return;
133
+ }
134
+ let cancelled = false;
135
+ const poll = async () => {
136
+ if (cancelled || isSavingRef.current) {
137
+ return;
138
+ }
139
+ try {
140
+ const remote = await adapter.readDocument(documentId);
141
+ if (cancelled) {
142
+ return;
143
+ }
144
+ const remoteContent = remote.content;
145
+ const remoteVersion = remote.version;
146
+ const localNow = localRef.current;
147
+ const baseNow = baseRef.current;
148
+ const versionNow = versionRef.current;
149
+ const dirtyNow = localNow !== baseNow;
150
+ if (dirtyNow) {
151
+ if (remoteContent !== baseNow || remoteVersion !== versionNow) {
152
+ const nextConflictDetails = {
153
+ baseContent: baseNow,
154
+ localContent: localNow,
155
+ remoteContent,
156
+ remoteUpdatedAt: remote.updatedAt ?? null,
157
+ remoteVersion,
158
+ };
159
+ conflictRef.current = true;
160
+ versionRef.current = remoteVersion;
161
+ updatedAtRef.current = remote.updatedAt ?? updatedAtRef.current;
162
+ setConflict(true);
163
+ setConflictDetails(nextConflictDetails);
164
+ setError("Remote document changed while you have unsaved edits.");
165
+ setVersion(remoteVersion);
166
+ setUpdatedAt(remote.updatedAt ?? updatedAtRef.current);
167
+ }
168
+ return;
169
+ }
170
+ if (focusedRef.current) {
171
+ return;
172
+ }
173
+ if (remoteContent !== baseNow ||
174
+ remoteVersion !== versionNow ||
175
+ conflictRef.current) {
176
+ applyRemoteSnapshot(remote, true);
177
+ }
178
+ }
179
+ catch {
180
+ // Explicit load/save actions surface errors for callers.
181
+ }
182
+ };
183
+ const interval = setInterval(() => {
184
+ void poll();
185
+ }, pollMs);
186
+ return () => {
187
+ cancelled = true;
188
+ clearInterval(interval);
189
+ };
190
+ }, [adapter, applyRemoteSnapshot, documentId, pollMs]);
191
+ const commitSave = useCallback(async (input) => {
192
+ if (!documentId) {
193
+ return false;
194
+ }
195
+ const contentToSave = localRef.current;
196
+ const currentBase = baseRef.current;
197
+ if (contentToSave === currentBase) {
198
+ setSaveStatus("saved");
199
+ return true;
200
+ }
201
+ const result = await adapter.writeDocument({
202
+ baseVersion: input.baseVersion,
203
+ content: contentToSave,
204
+ documentId,
205
+ ...(input.force ? { force: true } : {}),
206
+ });
207
+ if ("conflict" in result) {
208
+ let remoteContent = null;
209
+ let remoteVersion = result.version ?? versionRef.current;
210
+ let remoteUpdatedAt = result.updatedAt ?? updatedAtRef.current;
211
+ try {
212
+ const remote = await adapter.readDocument(documentId);
213
+ remoteContent = remote.content;
214
+ remoteVersion = remote.version;
215
+ remoteUpdatedAt = remote.updatedAt ?? null;
216
+ }
217
+ catch {
218
+ // Conflict resolution can still proceed by keeping remote via resync
219
+ // or overwriting remote even if the preview fetch fails.
220
+ }
221
+ const nextConflictDetails = {
222
+ baseContent: currentBase,
223
+ localContent: contentToSave,
224
+ remoteContent,
225
+ remoteUpdatedAt,
226
+ remoteVersion,
227
+ };
228
+ conflictRef.current = true;
229
+ setConflict(true);
230
+ setConflictDetails(nextConflictDetails);
231
+ setSaveStatus("idle");
232
+ setError("Remote document changed while you have unsaved edits.");
233
+ if (remoteVersion !== undefined) {
234
+ versionRef.current = remoteVersion;
235
+ setVersion(remoteVersion);
236
+ }
237
+ if (remoteUpdatedAt !== undefined) {
238
+ updatedAtRef.current = remoteUpdatedAt ?? null;
239
+ setUpdatedAt(remoteUpdatedAt ?? null);
240
+ }
241
+ return false;
242
+ }
243
+ baseRef.current = contentToSave;
244
+ versionRef.current = result.version;
245
+ updatedAtRef.current = result.updatedAt ?? updatedAtRef.current;
246
+ conflictRef.current = false;
247
+ setBase(contentToSave);
248
+ setVersion(result.version);
249
+ setUpdatedAt(result.updatedAt ?? updatedAtRef.current);
250
+ setConflict(false);
251
+ setConflictDetails(null);
252
+ setSaveStatus("saved");
253
+ setError(null);
254
+ return true;
255
+ }, [adapter, documentId]);
256
+ const scheduleSave = useCallback(() => {
257
+ if (!documentId || conflictRef.current) {
258
+ return;
259
+ }
260
+ if (localRef.current === baseRef.current) {
261
+ setSaveStatus("saved");
262
+ return;
263
+ }
264
+ setSaveStatus("pending");
265
+ if (debounceRef.current) {
266
+ clearTimeout(debounceRef.current);
267
+ }
268
+ debounceRef.current = setTimeout(() => {
269
+ if (isSavingRef.current || conflictRef.current) {
270
+ queuedSaveRef.current = true;
271
+ debounceRef.current = null;
272
+ return;
273
+ }
274
+ isSavingRef.current = true;
275
+ queuedSaveRef.current = false;
276
+ setSaveStatus("saving");
277
+ commitSave({ baseVersion: versionRef.current })
278
+ .catch((err) => {
279
+ setError(`Save failed: ${String(err)}`);
280
+ setSaveStatus("idle");
281
+ return false;
282
+ })
283
+ .finally(() => {
284
+ isSavingRef.current = false;
285
+ debounceRef.current = null;
286
+ });
287
+ }, debounceMs);
288
+ }, [commitSave, debounceMs, documentId]);
289
+ const setContent = useCallback((next) => {
290
+ setLocal(next);
291
+ localRef.current = next;
292
+ scheduleSave();
293
+ }, [scheduleSave]);
294
+ useEffect(() => {
295
+ if (saveStatus !== "saved" ||
296
+ !queuedSaveRef.current ||
297
+ isSavingRef.current ||
298
+ conflictRef.current ||
299
+ localRef.current === baseRef.current) {
300
+ return;
301
+ }
302
+ queuedSaveRef.current = false;
303
+ scheduleSave();
304
+ }, [saveStatus, scheduleSave]);
305
+ const resync = useCallback(async () => {
306
+ if (!documentId) {
307
+ return;
308
+ }
309
+ try {
310
+ const readRemote = adapter.resyncDocument ?? adapter.readDocument;
311
+ const result = await readRemote(documentId);
312
+ applyRemoteSnapshot(result, true);
313
+ }
314
+ catch (err) {
315
+ setError(`Failed to resync: ${String(err)}`);
316
+ }
317
+ }, [adapter, applyRemoteSnapshot, documentId]);
318
+ const forceSave = useCallback(async () => {
319
+ if (!documentId || isSavingRef.current) {
320
+ return false;
321
+ }
322
+ isSavingRef.current = true;
323
+ setSaveStatus("saving");
324
+ try {
325
+ return await commitSave({ baseVersion: null, force: true });
326
+ }
327
+ catch (err) {
328
+ setError(`Force save failed: ${String(err)}`);
329
+ setSaveStatus("idle");
330
+ return false;
331
+ }
332
+ finally {
333
+ isSavingRef.current = false;
334
+ }
335
+ }, [commitSave, documentId]);
336
+ const saveNow = useCallback(async () => {
337
+ if (!documentId) {
338
+ return false;
339
+ }
340
+ if (debounceRef.current) {
341
+ clearTimeout(debounceRef.current);
342
+ debounceRef.current = null;
343
+ }
344
+ if (isSavingRef.current || conflictRef.current) {
345
+ return false;
346
+ }
347
+ isSavingRef.current = true;
348
+ setSaveStatus("saving");
349
+ try {
350
+ return await commitSave({ baseVersion: versionRef.current });
351
+ }
352
+ catch (err) {
353
+ setError(`Save failed: ${String(err)}`);
354
+ setSaveStatus("idle");
355
+ return false;
356
+ }
357
+ finally {
358
+ isSavingRef.current = false;
359
+ }
360
+ }, [commitSave, documentId]);
361
+ return useMemo(() => ({
362
+ conflict,
363
+ conflictDetails,
364
+ error,
365
+ forceSave,
366
+ isDirty,
367
+ isFocused,
368
+ isLoading,
369
+ resync,
370
+ revision,
371
+ saveNow,
372
+ saveStatus,
373
+ setContent,
374
+ setFocused,
375
+ updatedAt,
376
+ value: local,
377
+ version,
378
+ }), [
379
+ conflict,
380
+ conflictDetails,
381
+ error,
382
+ forceSave,
383
+ isDirty,
384
+ isFocused,
385
+ isLoading,
386
+ local,
387
+ resync,
388
+ revision,
389
+ saveNow,
390
+ saveStatus,
391
+ setContent,
392
+ setFocused,
393
+ updatedAt,
394
+ version,
395
+ ]);
396
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,151 @@
1
+ import { act, renderHook, waitFor } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { useMdKitDocument } from "./useMdKitDocument";
4
+ describe("useMdKitDocument", () => {
5
+ afterEach(() => {
6
+ vi.clearAllTimers();
7
+ vi.useRealTimers();
8
+ });
9
+ it("marks autosave as pending during the debounce window", async () => {
10
+ const adapter = {
11
+ readDocument: vi.fn(async () => ({
12
+ content: "Initial content",
13
+ updatedAt: "2026-01-01T00:00:00.000Z",
14
+ version: 1,
15
+ })),
16
+ resyncDocument: vi.fn(async () => ({
17
+ content: "Initial content",
18
+ updatedAt: "2026-01-01T00:00:00.000Z",
19
+ version: 1,
20
+ })),
21
+ writeDocument: vi.fn(async () => ({
22
+ updatedAt: "2026-01-01T00:00:01.000Z",
23
+ version: 2,
24
+ })),
25
+ };
26
+ const { result } = renderHook(() => useMdKitDocument({
27
+ adapter,
28
+ debounceMs: 250,
29
+ documentId: "docs/example.md",
30
+ pollMs: 0,
31
+ }));
32
+ await waitFor(() => {
33
+ expect(result.current.value).toBe("Initial content");
34
+ });
35
+ vi.useFakeTimers();
36
+ act(() => {
37
+ result.current.setContent("Changed content");
38
+ });
39
+ expect(result.current.isDirty).toBe(true);
40
+ expect(result.current.saveStatus).toBe("pending");
41
+ expect(adapter.writeDocument).not.toHaveBeenCalled();
42
+ await act(async () => {
43
+ await vi.advanceTimersByTimeAsync(250);
44
+ });
45
+ expect(result.current.saveStatus).toBe("saved");
46
+ expect(result.current.isDirty).toBe(false);
47
+ expect(adapter.writeDocument).toHaveBeenCalledWith({
48
+ baseVersion: 1,
49
+ content: "Changed content",
50
+ documentId: "docs/example.md",
51
+ });
52
+ });
53
+ it("exposes remote, local, and base content when a save conflicts", async () => {
54
+ const adapter = {
55
+ readDocument: vi
56
+ .fn()
57
+ .mockResolvedValueOnce({
58
+ content: "Base content",
59
+ updatedAt: "2026-01-01T00:00:00.000Z",
60
+ version: 1,
61
+ })
62
+ .mockResolvedValueOnce({
63
+ content: "Remote content",
64
+ updatedAt: "2026-01-01T00:00:02.000Z",
65
+ version: 2,
66
+ }),
67
+ resyncDocument: vi.fn(),
68
+ writeDocument: vi.fn(async () => ({
69
+ conflict: true,
70
+ updatedAt: "2026-01-01T00:00:02.000Z",
71
+ version: 2,
72
+ })),
73
+ };
74
+ const { result } = renderHook(() => useMdKitDocument({
75
+ adapter,
76
+ documentId: "docs/example.md",
77
+ pollMs: 0,
78
+ }));
79
+ await waitFor(() => {
80
+ expect(result.current.value).toBe("Base content");
81
+ });
82
+ act(() => {
83
+ result.current.setContent("Local content");
84
+ });
85
+ await act(async () => {
86
+ await result.current.saveNow();
87
+ });
88
+ expect(result.current.conflict).toBe(true);
89
+ expect(result.current.conflictDetails).toEqual({
90
+ baseContent: "Base content",
91
+ localContent: "Local content",
92
+ remoteContent: "Remote content",
93
+ remoteUpdatedAt: "2026-01-01T00:00:02.000Z",
94
+ remoteVersion: 2,
95
+ });
96
+ });
97
+ it("force saves local content after a conflict", async () => {
98
+ const adapter = {
99
+ readDocument: vi
100
+ .fn()
101
+ .mockResolvedValueOnce({
102
+ content: "Base content",
103
+ updatedAt: "2026-01-01T00:00:00.000Z",
104
+ version: 1,
105
+ })
106
+ .mockResolvedValueOnce({
107
+ content: "Remote content",
108
+ updatedAt: "2026-01-01T00:00:02.000Z",
109
+ version: 2,
110
+ }),
111
+ resyncDocument: vi.fn(),
112
+ writeDocument: vi
113
+ .fn()
114
+ .mockResolvedValueOnce({
115
+ conflict: true,
116
+ updatedAt: "2026-01-01T00:00:02.000Z",
117
+ version: 2,
118
+ })
119
+ .mockResolvedValueOnce({
120
+ updatedAt: "2026-01-01T00:00:03.000Z",
121
+ version: 3,
122
+ }),
123
+ };
124
+ const { result } = renderHook(() => useMdKitDocument({
125
+ adapter,
126
+ documentId: "docs/example.md",
127
+ pollMs: 0,
128
+ }));
129
+ await waitFor(() => {
130
+ expect(result.current.value).toBe("Base content");
131
+ });
132
+ act(() => {
133
+ result.current.setContent("Local content");
134
+ });
135
+ await act(async () => {
136
+ await result.current.saveNow();
137
+ });
138
+ expect(result.current.conflict).toBe(true);
139
+ await act(async () => {
140
+ await result.current.forceSave();
141
+ });
142
+ expect(result.current.conflict).toBe(false);
143
+ expect(result.current.isDirty).toBe(false);
144
+ expect(adapter.writeDocument).toHaveBeenLastCalledWith({
145
+ baseVersion: null,
146
+ content: "Local content",
147
+ documentId: "docs/example.md",
148
+ force: true,
149
+ });
150
+ });
151
+ });
@@ -0,0 +1,3 @@
1
+ export { registerMdKitFastify } from "./transport/fastify";
2
+ export type { RegisterMdKitFastifyOptions } from "./transport/fastify";
3
+ export type { MdKitRestoreDocumentVersionInput, MdKitTransportStore, } from "./transport/store";
@@ -0,0 +1 @@
1
+ export { registerMdKitFastify } from "./transport/fastify";
@@ -0,0 +1,23 @@
1
+ export { useMdKitCollaboration } from "./collaboration/useMdKitCollaboration";
2
+ export { createMdKitDocumentRecord, detectMdKitDocumentConflict, normalizeMdKitVersionToken, restoreMdKitDocumentVersion, writeMdKitDocumentRecord, } from "./core/documentEngine";
3
+ export { useMdKitDocument } from "./document/useMdKitDocument";
4
+ export { MdKitConflictPanel } from "./document/MdKitConflictPanel";
5
+ export { MdKitDocumentToolbar } from "./document/MdKitDocumentToolbar";
6
+ export { MdKitEditor } from "./markdown/MdKitEditor";
7
+ export { MdKitThemeEditor } from "./theme/MdKitThemeEditor";
8
+ export { createMdKitRestAdapter } from "./transport/rest";
9
+ export { createMdKitEditorThemeStyle, darkMdKitEditorTheme, defaultMdKitEditorTheme, } from "./theme/editorTheme";
10
+ export { VersionHistoryPanel } from "./versioning/VersionHistoryPanel";
11
+ export { useMdKitDocumentVersions } from "./versioning/useMdKitDocumentVersions";
12
+ export type { CreateMdKitDocumentRecordInput, MdKitDocumentRecord, RestoreMdKitDocumentVersionInput, RestoreMdKitDocumentVersionResult, WriteMdKitDocumentRecordInput, WriteMdKitDocumentRecordResult, } from "./core/documentEngine";
13
+ export type { MdKitCollaborationParticipant, MdKitCollaborationSession, MdKitCollaborationStatus, MdKitDocumentAdapter, MdKitDocumentSnapshot, MdKitDocumentVersionDetail, MdKitDocumentVersionSummary, MdKitDocumentVersionToken, MdKitDocumentWriteInput, MdKitDocumentWriteResult, } from "./document/documentTypes";
14
+ export type { MdKitDocumentConflictDetails, MdKitDocumentController, } from "./document/useMdKitDocument";
15
+ export type { MdKitConflictPanelProps } from "./document/MdKitConflictPanel";
16
+ export type { MdKitDocumentToolbarProps } from "./document/MdKitDocumentToolbar";
17
+ export type { MdKitEditorProps } from "./markdown/MdKitEditor";
18
+ export type { MdKitEditorDebugEvent } from "./markdown/editorDebug";
19
+ export type { MdKitThemeEditorProps } from "./theme/MdKitThemeEditor";
20
+ export type { CreateMdKitRestAdapterOptions } from "./transport/rest";
21
+ export type { MdKitEditorTheme, MdKitEditorThemeStyle, } from "./theme/editorTheme";
22
+ export type { MdKitDocumentVersionsController, UseMdKitDocumentVersionsOptions, } from "./versioning/useMdKitDocumentVersions";
23
+ export type { VersionHistoryPanelProps } from "./versioning/VersionHistoryPanel";
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { useMdKitCollaboration } from "./collaboration/useMdKitCollaboration";
2
+ export { createMdKitDocumentRecord, detectMdKitDocumentConflict, normalizeMdKitVersionToken, restoreMdKitDocumentVersion, writeMdKitDocumentRecord, } from "./core/documentEngine";
3
+ export { useMdKitDocument } from "./document/useMdKitDocument";
4
+ export { MdKitConflictPanel } from "./document/MdKitConflictPanel";
5
+ export { MdKitDocumentToolbar } from "./document/MdKitDocumentToolbar";
6
+ export { MdKitEditor } from "./markdown/MdKitEditor";
7
+ export { MdKitThemeEditor } from "./theme/MdKitThemeEditor";
8
+ export { createMdKitRestAdapter } from "./transport/rest";
9
+ export { createMdKitEditorThemeStyle, darkMdKitEditorTheme, defaultMdKitEditorTheme, } from "./theme/editorTheme";
10
+ export { VersionHistoryPanel } from "./versioning/VersionHistoryPanel";
11
+ export { useMdKitDocumentVersions } from "./versioning/useMdKitDocumentVersions";
@@ -0,0 +1,6 @@
1
+ import type { Editor } from "@tiptap/react";
2
+ type MarkdownBubbleMenuProps = {
3
+ editor: Editor;
4
+ };
5
+ export declare const MarkdownBubbleMenu: ({ editor }: MarkdownBubbleMenuProps) => import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { BubbleMenu } from "@tiptap/react/menus";
3
+ import { Bold, Code2, Heading1, Heading2, Italic, Link2, List, ListOrdered, Quote, Strikethrough, } from "lucide-react";
4
+ import { joinClassNames } from "../ui/joinClassNames";
5
+ const setLink = (editor) => {
6
+ const previousUrl = editor.getAttributes("link").href;
7
+ const nextUrl = window.prompt("URL", previousUrl);
8
+ if (nextUrl === null) {
9
+ return;
10
+ }
11
+ if (nextUrl === "") {
12
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
13
+ return;
14
+ }
15
+ editor
16
+ .chain()
17
+ .focus()
18
+ .extendMarkRange("link")
19
+ .setLink({ href: nextUrl })
20
+ .run();
21
+ };
22
+ const ToolbarButton = ({ ariaLabel, children, isActive, onClick, }) => (_jsx("button", { type: "button", className: joinClassNames("hsk-toolbar-button", isActive && "hsk-toolbar-button-active"), "aria-label": ariaLabel, onClick: onClick, onMouseDown: (event) => event.preventDefault(), children: children }));
23
+ export const MarkdownBubbleMenu = ({ editor }) => (_jsxs(BubbleMenu, { appendTo: () => document.body, className: "hsk-toolbar", editor: editor, options: {
24
+ placement: "top",
25
+ strategy: "fixed",
26
+ }, shouldShow: ({ editor: currentEditor, state }) => {
27
+ const { empty } = state.selection;
28
+ return currentEditor.isEditable && !empty;
29
+ }, children: [_jsx(ToolbarButton, { ariaLabel: "Bold", isActive: editor.isActive("bold"), onClick: () => editor.chain().focus().toggleBold().run(), children: _jsx(Bold, {}) }), _jsx(ToolbarButton, { ariaLabel: "Italic", isActive: editor.isActive("italic"), onClick: () => editor.chain().focus().toggleItalic().run(), children: _jsx(Italic, {}) }), _jsx(ToolbarButton, { ariaLabel: "Strikethrough", isActive: editor.isActive("strike"), onClick: () => editor.chain().focus().toggleStrike().run(), children: _jsx(Strikethrough, {}) }), _jsx(ToolbarButton, { ariaLabel: "Code block", isActive: editor.isActive("codeBlock"), onClick: () => editor.chain().focus().toggleCodeBlock().run(), children: _jsx(Code2, {}) }), _jsx("div", { className: "hsk-toolbar-divider" }), _jsx(ToolbarButton, { ariaLabel: "Heading 1", isActive: editor.isActive("heading", { level: 1 }), onClick: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), children: _jsx(Heading1, {}) }), _jsx(ToolbarButton, { ariaLabel: "Heading 2", isActive: editor.isActive("heading", { level: 2 }), onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), children: _jsx(Heading2, {}) }), _jsx("div", { className: "hsk-toolbar-divider" }), _jsx(ToolbarButton, { ariaLabel: "Bullet list", isActive: editor.isActive("bulletList"), onClick: () => editor.chain().focus().toggleBulletList().run(), children: _jsx(List, {}) }), _jsx(ToolbarButton, { ariaLabel: "Ordered list", isActive: editor.isActive("orderedList"), onClick: () => editor.chain().focus().toggleOrderedList().run(), children: _jsx(ListOrdered, {}) }), _jsx(ToolbarButton, { ariaLabel: "Blockquote", isActive: editor.isActive("blockquote"), onClick: () => editor.chain().focus().toggleBlockquote().run(), children: _jsx(Quote, {}) }), _jsx("div", { className: "hsk-toolbar-divider" }), _jsx(ToolbarButton, { ariaLabel: "Link", isActive: editor.isActive("link"), onClick: () => setLink(editor), children: _jsx(Link2, {}) })] }));