@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
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # `@mp-lb/mdkit`
2
+
3
+ Frontend primitives for document editing flows:
4
+
5
+ - `MdKitEditor` for a Markdown-first rich text surface with local and collaborative modes
6
+ - `useMdKitDocument` for autosave, conflict handling, and resync against a CRUD backend
7
+ - `MdKitDocumentToolbar` for unstyled connected-document workflow controls
8
+ - `useMdKitDocumentVersions` and `VersionHistoryPanel` for version browsing and restore flows
9
+ - `useMdKitCollaboration` for wiring the same editor into a Hocuspocus/Yjs session
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pnpm add @mp-lb/mdkit
15
+ ```
16
+
17
+ The editor behavior does not require package CSS. Import the optional stylesheet
18
+ when you want mdkit's reset-resistant markdown baseline and CSS-variable theme
19
+ system:
20
+
21
+ ```ts
22
+ import "@mp-lb/mdkit/styles.css";
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```tsx
28
+ import { useState } from "react";
29
+ import { MdKitEditor } from "@mp-lb/mdkit";
30
+ import "@mp-lb/mdkit/styles.css";
31
+
32
+ export function MarkdownEditorExample() {
33
+ const [markdown, setMarkdown] = useState("# Hello markdown");
34
+
35
+ return <MdKitEditor value={markdown} onChange={setMarkdown} />;
36
+ }
37
+ ```
38
+
39
+ `MdKitEditor` is the textarea-like entry point. It has no persistence,
40
+ version history, or collaboration. You own the `value` and `onChange` state.
41
+
42
+ ## Exports
43
+
44
+ - `MdKitEditor`
45
+ - `MdKitDocumentToolbar`
46
+ - `useMdKitDocument`
47
+ - `useMdKitDocumentVersions`
48
+ - `useMdKitCollaboration`
49
+ - `MdKitThemeEditor`
50
+ - `VersionHistoryPanel`
51
+
52
+ The package also exports the related prop, adapter, document, versioning, and
53
+ collaboration types.
54
+
55
+ ## Add Persistence
56
+
57
+ ```tsx
58
+ import {
59
+ MdKitDocumentToolbar,
60
+ MdKitEditor,
61
+ useMdKitDocument,
62
+ } from "@mp-lb/mdkit";
63
+
64
+ const document = useMdKitDocument({
65
+ adapter,
66
+ documentId: "docs/brief.md",
67
+ });
68
+
69
+ <>
70
+ <MdKitDocumentToolbar document={document} />
71
+ <MdKitEditor
72
+ value={document.value}
73
+ onChange={document.setContent}
74
+ onFocusChange={document.setFocused}
75
+ />
76
+ </>;
77
+ ```
78
+
79
+ The storage adapter provides `readDocument`, `writeDocument`, and optional
80
+ `resyncDocument`.
81
+
82
+ ## Version History
83
+
84
+ ```tsx
85
+ import {
86
+ VersionHistoryPanel,
87
+ useMdKitDocumentVersions,
88
+ } from "@mp-lb/mdkit";
89
+
90
+ const versions = useMdKitDocumentVersions({
91
+ adapter,
92
+ documentId: "docs/brief.md",
93
+ });
94
+
95
+ <VersionHistoryPanel
96
+ controller={versions}
97
+ onRestoreVersion={async (version) => {
98
+ document.setContent(version.content);
99
+ await document.saveNow();
100
+ }}
101
+ />;
102
+ ```
103
+
104
+ ## Collaboration
105
+
106
+ ```tsx
107
+ import {
108
+ MdKitEditor,
109
+ useMdKitCollaboration,
110
+ } from "@mp-lb/mdkit";
111
+
112
+ const collaboration = useMdKitCollaboration({
113
+ collaborator: {
114
+ id: "felix",
115
+ name: "Felix",
116
+ },
117
+ documentId: "docs/brief.md",
118
+ endpoint: "ws://127.0.0.1:1234",
119
+ });
120
+
121
+ <MdKitEditor collaboration={collaboration} />;
122
+ ```
123
+
124
+ ## Testbench
125
+
126
+ The workspace includes `apps/mdkit-testbench`, a Vite app for debugging the
127
+ package in unconnected and connected modes.
128
+
129
+ ## Package Docs
130
+
131
+ Published library docs live in [`docs/index.md`](./docs/index.md). Internal
132
+ project notes live in the repository under [`../../docs/mdkit`](../../docs/mdkit).
@@ -0,0 +1,10 @@
1
+ import type { MdKitCollaborationParticipant, MdKitCollaborationSession } from "../document/documentTypes";
2
+ export type UseMdKitCollaborationOptions = {
3
+ collaborator: MdKitCollaborationParticipant;
4
+ documentId: string | null;
5
+ enabled?: boolean;
6
+ endpoint: string | null;
7
+ getToken?: () => Promise<string | null>;
8
+ resolveRoomName?: (documentId: string) => string;
9
+ };
10
+ export declare const useMdKitCollaboration: (options: UseMdKitCollaborationOptions) => MdKitCollaborationSession | null;
@@ -0,0 +1,90 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { HocuspocusProvider } from "@hocuspocus/provider";
3
+ import * as Y from "yjs";
4
+ const createColorFromId = (id) => {
5
+ let hash = 0;
6
+ for (let index = 0; index < id.length; index += 1) {
7
+ hash = (hash << 5) - hash + id.charCodeAt(index);
8
+ hash |= 0;
9
+ }
10
+ return `hsl(${Math.abs(hash) % 360}, 85%, 55%)`;
11
+ };
12
+ export const useMdKitCollaboration = (options) => {
13
+ const { collaborator, documentId, enabled = true, endpoint, getToken, resolveRoomName, } = options;
14
+ const [status, setStatus] = useState("disconnected");
15
+ const normalizedCollaborator = useMemo(() => ({
16
+ ...collaborator,
17
+ color: collaborator.color || createColorFromId(collaborator.id),
18
+ }), [collaborator]);
19
+ const roomName = useMemo(() => {
20
+ if (!documentId) {
21
+ return "";
22
+ }
23
+ return resolveRoomName ? resolveRoomName(documentId) : documentId;
24
+ }, [documentId, resolveRoomName]);
25
+ const ydoc = useMemo(() => {
26
+ void roomName;
27
+ return new Y.Doc();
28
+ }, [roomName]);
29
+ const provider = useMemo(() => {
30
+ if (!enabled || !documentId || !endpoint) {
31
+ return null;
32
+ }
33
+ return new HocuspocusProvider({
34
+ document: ydoc,
35
+ name: roomName,
36
+ onConnect: () => setStatus("connected"),
37
+ onDisconnect: () => setStatus("disconnected"),
38
+ onStatus: ({ status: nextStatus }) => {
39
+ if (nextStatus === "connecting") {
40
+ setStatus("connecting");
41
+ }
42
+ },
43
+ token: async () => {
44
+ const token = getToken ? await getToken() : null;
45
+ return token || "";
46
+ },
47
+ url: endpoint,
48
+ });
49
+ }, [documentId, enabled, endpoint, getToken, roomName, ydoc]);
50
+ useEffect(() => {
51
+ if (!provider) {
52
+ return;
53
+ }
54
+ return () => {
55
+ provider.destroy();
56
+ };
57
+ }, [provider]);
58
+ useEffect(() => {
59
+ if (!provider) {
60
+ return;
61
+ }
62
+ provider.setAwarenessField("user", {
63
+ color: normalizedCollaborator.color,
64
+ id: normalizedCollaborator.id,
65
+ imageUrl: normalizedCollaborator.imageUrl || undefined,
66
+ name: normalizedCollaborator.name,
67
+ });
68
+ }, [normalizedCollaborator, provider]);
69
+ return useMemo(() => {
70
+ if (!enabled || !documentId || !endpoint) {
71
+ return null;
72
+ }
73
+ return {
74
+ collaborator: normalizedCollaborator,
75
+ document: ydoc,
76
+ provider,
77
+ roomName,
78
+ status: provider ? status : "disconnected",
79
+ };
80
+ }, [
81
+ documentId,
82
+ enabled,
83
+ endpoint,
84
+ normalizedCollaborator,
85
+ provider,
86
+ roomName,
87
+ status,
88
+ ydoc,
89
+ ]);
90
+ };
@@ -0,0 +1,38 @@
1
+ import type { MdKitDocumentSnapshot, MdKitDocumentVersionDetail, MdKitDocumentVersionToken, MdKitDocumentWriteResult } from "../document/documentTypes.js";
2
+ export type MdKitDocumentRecord = {
3
+ current: MdKitDocumentSnapshot;
4
+ versions: MdKitDocumentVersionDetail[];
5
+ };
6
+ export type CreateMdKitDocumentRecordInput = {
7
+ content?: string;
8
+ now?: string;
9
+ };
10
+ export type WriteMdKitDocumentRecordInput = {
11
+ baseVersion: MdKitDocumentVersionToken;
12
+ content: string;
13
+ force?: boolean;
14
+ label?: string;
15
+ now?: string;
16
+ };
17
+ export type WriteMdKitDocumentRecordResult = {
18
+ record: MdKitDocumentRecord;
19
+ result: MdKitDocumentWriteResult;
20
+ };
21
+ export type RestoreMdKitDocumentVersionInput = {
22
+ label?: string;
23
+ now?: string;
24
+ versionId: string;
25
+ };
26
+ export type RestoreMdKitDocumentVersionResult = {
27
+ record: MdKitDocumentRecord;
28
+ restoredVersion: MdKitDocumentVersionDetail;
29
+ result: MdKitDocumentWriteResult;
30
+ };
31
+ export declare const normalizeMdKitVersionToken: (version: MdKitDocumentVersionToken | undefined) => string | null;
32
+ export declare const detectMdKitDocumentConflict: (input: {
33
+ baseVersion: MdKitDocumentVersionToken;
34
+ currentVersion: MdKitDocumentVersionToken;
35
+ }) => boolean;
36
+ export declare const createMdKitDocumentRecord: (input?: CreateMdKitDocumentRecordInput) => MdKitDocumentRecord;
37
+ export declare const writeMdKitDocumentRecord: (record: MdKitDocumentRecord, input: WriteMdKitDocumentRecordInput) => WriteMdKitDocumentRecordResult;
38
+ export declare const restoreMdKitDocumentVersion: (record: MdKitDocumentRecord, input: RestoreMdKitDocumentVersionInput) => RestoreMdKitDocumentVersionResult;
@@ -0,0 +1,95 @@
1
+ const initialVersion = "0";
2
+ const createTimestamp = () => new Date().toISOString();
3
+ export const normalizeMdKitVersionToken = (version) => (version == null ? null : String(version));
4
+ export const detectMdKitDocumentConflict = (input) => normalizeMdKitVersionToken(input.baseVersion) !==
5
+ normalizeMdKitVersionToken(input.currentVersion);
6
+ const nextVersionToken = (currentVersion) => {
7
+ const current = Number(normalizeMdKitVersionToken(currentVersion));
8
+ return Number.isFinite(current) ? String(current + 1) : createTimestamp();
9
+ };
10
+ const createVersionDetail = (input) => ({
11
+ content: input.content,
12
+ createdAt: input.now,
13
+ id: input.id,
14
+ label: input.label,
15
+ updatedAt: input.now,
16
+ version: input.id,
17
+ });
18
+ export const createMdKitDocumentRecord = (input = {}) => {
19
+ const now = input.now ?? createTimestamp();
20
+ const content = input.content ?? "";
21
+ const current = {
22
+ content,
23
+ updatedAt: now,
24
+ version: initialVersion,
25
+ };
26
+ return {
27
+ current,
28
+ versions: [
29
+ createVersionDetail({
30
+ content,
31
+ id: initialVersion,
32
+ label: "Initial",
33
+ now,
34
+ }),
35
+ ],
36
+ };
37
+ };
38
+ export const writeMdKitDocumentRecord = (record, input) => {
39
+ if (!input.force &&
40
+ detectMdKitDocumentConflict({
41
+ baseVersion: input.baseVersion,
42
+ currentVersion: record.current.version,
43
+ })) {
44
+ return {
45
+ record,
46
+ result: {
47
+ conflict: true,
48
+ updatedAt: record.current.updatedAt,
49
+ version: record.current.version,
50
+ },
51
+ };
52
+ }
53
+ const now = input.now ?? createTimestamp();
54
+ const version = nextVersionToken(record.current.version);
55
+ const current = {
56
+ content: input.content,
57
+ updatedAt: now,
58
+ version,
59
+ };
60
+ return {
61
+ record: {
62
+ current,
63
+ versions: [
64
+ ...record.versions,
65
+ createVersionDetail({
66
+ content: input.content,
67
+ id: version,
68
+ label: input.label ?? `Version ${version}`,
69
+ now,
70
+ }),
71
+ ],
72
+ },
73
+ result: {
74
+ updatedAt: now,
75
+ version,
76
+ },
77
+ };
78
+ };
79
+ export const restoreMdKitDocumentVersion = (record, input) => {
80
+ const restoredVersion = record.versions.find((version) => version.id === input.versionId);
81
+ if (!restoredVersion) {
82
+ throw new Error(`Version not found: ${input.versionId}`);
83
+ }
84
+ const written = writeMdKitDocumentRecord(record, {
85
+ baseVersion: record.current.version,
86
+ content: restoredVersion.content,
87
+ force: true,
88
+ label: input.label ?? `Restore ${restoredVersion.id}`,
89
+ now: input.now,
90
+ });
91
+ return {
92
+ ...written,
93
+ restoredVersion,
94
+ };
95
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createMdKitDocumentRecord, detectMdKitDocumentConflict, restoreMdKitDocumentVersion, writeMdKitDocumentRecord, } from "./documentEngine";
3
+ const now = "2026-04-30T12:00:00.000Z";
4
+ describe("documentEngine", () => {
5
+ it("creates a current markdown snapshot with an initial version", () => {
6
+ const record = createMdKitDocumentRecord({
7
+ content: "# Initial",
8
+ now,
9
+ });
10
+ expect(record.current).toEqual({
11
+ content: "# Initial",
12
+ updatedAt: now,
13
+ version: "0",
14
+ });
15
+ expect(record.versions).toEqual([
16
+ {
17
+ content: "# Initial",
18
+ createdAt: now,
19
+ id: "0",
20
+ label: "Initial",
21
+ updatedAt: now,
22
+ version: "0",
23
+ },
24
+ ]);
25
+ });
26
+ it("writes a new version when the base version matches", () => {
27
+ const record = createMdKitDocumentRecord({ now });
28
+ const written = writeMdKitDocumentRecord(record, {
29
+ baseVersion: "0",
30
+ content: "# Saved",
31
+ now: "2026-04-30T12:01:00.000Z",
32
+ });
33
+ expect(written.result).toEqual({
34
+ updatedAt: "2026-04-30T12:01:00.000Z",
35
+ version: "1",
36
+ });
37
+ expect(written.record.current).toMatchObject({
38
+ content: "# Saved",
39
+ version: "1",
40
+ });
41
+ expect(written.record.versions.map((version) => version.id)).toEqual([
42
+ "0",
43
+ "1",
44
+ ]);
45
+ });
46
+ it("returns a conflict when the base version is stale", () => {
47
+ const record = createMdKitDocumentRecord({ now });
48
+ const first = writeMdKitDocumentRecord(record, {
49
+ baseVersion: "0",
50
+ content: "first",
51
+ now: "2026-04-30T12:01:00.000Z",
52
+ });
53
+ const second = writeMdKitDocumentRecord(first.record, {
54
+ baseVersion: "0",
55
+ content: "second",
56
+ now: "2026-04-30T12:02:00.000Z",
57
+ });
58
+ expect(second.result).toEqual({
59
+ conflict: true,
60
+ updatedAt: "2026-04-30T12:01:00.000Z",
61
+ version: "1",
62
+ });
63
+ expect(second.record).toBe(first.record);
64
+ });
65
+ it("can force-write a local version over a remote conflict", () => {
66
+ const record = createMdKitDocumentRecord({ now });
67
+ const first = writeMdKitDocumentRecord(record, {
68
+ baseVersion: "0",
69
+ content: "remote",
70
+ now: "2026-04-30T12:01:00.000Z",
71
+ });
72
+ const forced = writeMdKitDocumentRecord(first.record, {
73
+ baseVersion: "0",
74
+ content: "local",
75
+ force: true,
76
+ label: "Force save",
77
+ now: "2026-04-30T12:02:00.000Z",
78
+ });
79
+ expect(forced.record.current).toMatchObject({
80
+ content: "local",
81
+ version: "2",
82
+ });
83
+ expect(forced.record.versions.at(-1)).toMatchObject({
84
+ content: "local",
85
+ id: "2",
86
+ label: "Force save",
87
+ });
88
+ });
89
+ it("restores a saved version into the current snapshot", () => {
90
+ const record = createMdKitDocumentRecord({ content: "initial", now });
91
+ const first = writeMdKitDocumentRecord(record, {
92
+ baseVersion: "0",
93
+ content: "later",
94
+ now: "2026-04-30T12:01:00.000Z",
95
+ });
96
+ const restored = restoreMdKitDocumentVersion(first.record, {
97
+ now: "2026-04-30T12:02:00.000Z",
98
+ versionId: "0",
99
+ });
100
+ expect(restored.record.current).toMatchObject({
101
+ content: "initial",
102
+ version: "2",
103
+ });
104
+ expect(restored.record.versions.at(-1)).toMatchObject({
105
+ content: "initial",
106
+ label: "Restore 0",
107
+ });
108
+ });
109
+ it("detects conflict from base and current version tokens", () => {
110
+ expect(detectMdKitDocumentConflict({
111
+ baseVersion: "1",
112
+ currentVersion: "2",
113
+ })).toBe(true);
114
+ expect(detectMdKitDocumentConflict({
115
+ baseVersion: 1,
116
+ currentVersion: "1",
117
+ })).toBe(false);
118
+ });
119
+ });
@@ -0,0 +1,3 @@
1
+ export { createMdKitDocumentRecord, detectMdKitDocumentConflict, normalizeMdKitVersionToken, restoreMdKitDocumentVersion, writeMdKitDocumentRecord, } from "./documentEngine.js";
2
+ export type { CreateMdKitDocumentRecordInput, MdKitDocumentRecord, RestoreMdKitDocumentVersionInput, RestoreMdKitDocumentVersionResult, WriteMdKitDocumentRecordInput, WriteMdKitDocumentRecordResult, } from "./documentEngine.js";
3
+ export type { MdKitDocumentSnapshot, MdKitDocumentVersionDetail, MdKitDocumentVersionSummary, MdKitDocumentVersionToken, MdKitDocumentWriteInput, MdKitDocumentWriteResult, } from "../document/documentTypes.js";
@@ -0,0 +1 @@
1
+ export { createMdKitDocumentRecord, detectMdKitDocumentConflict, normalizeMdKitVersionToken, restoreMdKitDocumentVersion, writeMdKitDocumentRecord, } from "./documentEngine.js";
@@ -0,0 +1,7 @@
1
+ import type { MdKitDocumentController } from "./useMdKitDocument";
2
+ export type MdKitConflictPanelProps = {
3
+ className?: string;
4
+ document: MdKitDocumentController;
5
+ title?: string;
6
+ };
7
+ export declare const MdKitConflictPanel: ({ className, document, title, }: MdKitConflictPanelProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { joinClassNames } from "../ui/joinClassNames";
4
+ export const MdKitConflictPanel = ({ className, document, title = "Document conflict", }) => {
5
+ const [pendingAction, setPendingAction] = useState(null);
6
+ const [activePreview, setActivePreview] = useState("remote");
7
+ if (!document.conflict) {
8
+ return null;
9
+ }
10
+ const runAction = async (name, action) => {
11
+ setPendingAction(name);
12
+ try {
13
+ await action();
14
+ }
15
+ finally {
16
+ setPendingAction(null);
17
+ }
18
+ };
19
+ const isBusy = pendingAction !== null || document.saveStatus === "saving";
20
+ const conflictDetails = document.conflictDetails;
21
+ const previewOptions = [
22
+ {
23
+ id: "remote",
24
+ label: "Keep remote",
25
+ value: conflictDetails?.remoteContent ??
26
+ "Remote content preview is not available. Keep remote will still reload the latest canonical document.",
27
+ },
28
+ {
29
+ id: "local",
30
+ label: "Keep local",
31
+ value: conflictDetails?.localContent ?? document.value,
32
+ },
33
+ ];
34
+ const activePreviewOption = previewOptions.find((option) => option.id === activePreview) ??
35
+ previewOptions[0];
36
+ return (_jsxs("section", { className: joinClassNames("mdkit-conflict-panel", className), children: [_jsxs("div", { className: "mdkit-conflict-panel-content", children: [_jsx("h2", { children: title }), _jsx("p", { children: "Remote changes conflict with local edits. Choose the remote document, or keep your local document by overwriting the remote copy." }), document.error ? (_jsx("p", { className: "mdkit-conflict-panel-error", children: document.error })) : null, conflictDetails ? (_jsxs("p", { className: "mdkit-conflict-panel-meta", children: ["Remote version ", String(conflictDetails.remoteVersion ?? "none"), conflictDetails.remoteUpdatedAt
37
+ ? ` saved ${new Date(conflictDetails.remoteUpdatedAt).toLocaleTimeString()}`
38
+ : ""] })) : null] }), _jsxs("div", { className: "mdkit-conflict-panel-preview", children: [_jsx("div", { className: "mdkit-conflict-panel-tabs", role: "tablist", children: previewOptions.map((option) => (_jsx("button", { type: "button", "aria-selected": activePreviewOption.id === option.id, className: activePreviewOption.id === option.id
39
+ ? "mdkit-conflict-panel-tab mdkit-conflict-panel-tab-active"
40
+ : "mdkit-conflict-panel-tab", role: "tab", onClick: () => setActivePreview(option.id), children: option.label }, option.id))) }), _jsx("textarea", { "aria-label": `${activePreviewOption.label} conflict content`, readOnly: true, value: activePreviewOption.value })] }), _jsxs("div", { className: "mdkit-conflict-panel-action-row", children: [_jsx("button", { type: "button", className: "mdkit-panel-secondary-action", disabled: isBusy, onClick: () => void runAction("reload", document.resync), children: pendingAction === "reload" ? "Keeping remote..." : "Keep remote" }), _jsx("button", { type: "button", className: "mdkit-panel-secondary-action", disabled: isBusy, onClick: () => void runAction("overwrite", document.forceSave), children: pendingAction === "overwrite" ? "Keeping local..." : "Keep local" })] })] }));
41
+ };
@@ -0,0 +1,13 @@
1
+ import type { MdKitCollaborationSession } from "./documentTypes";
2
+ import type { MdKitDocumentController } from "./useMdKitDocument";
3
+ import type { MdKitDocumentVersionsController } from "../versioning/useMdKitDocumentVersions";
4
+ export type MdKitDocumentToolbarProps = {
5
+ className?: string;
6
+ collaboration?: MdKitCollaborationSession | null;
7
+ document: MdKitDocumentController;
8
+ onOpenConflict?: () => Promise<void> | void;
9
+ onOpenVersionHistory?: () => Promise<void> | void;
10
+ showConflictActions?: boolean;
11
+ versions?: MdKitDocumentVersionsController | null;
12
+ };
13
+ export declare const MdKitDocumentToolbar: ({ className, collaboration, document, onOpenConflict, onOpenVersionHistory, showConflictActions, versions, }: MdKitDocumentToolbarProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { joinClassNames } from "../ui/joinClassNames";
4
+ const formatUpdatedAt = (updatedAt) => {
5
+ if (!updatedAt) {
6
+ return "Never saved";
7
+ }
8
+ return `Saved ${new Date(updatedAt).toLocaleTimeString()}`;
9
+ };
10
+ export const MdKitDocumentToolbar = ({ className, collaboration, document, onOpenConflict, onOpenVersionHistory, showConflictActions = false, versions, }) => {
11
+ const [pendingAction, setPendingAction] = useState(null);
12
+ const runAction = async (name, action) => {
13
+ setPendingAction(name);
14
+ try {
15
+ await action();
16
+ }
17
+ finally {
18
+ setPendingAction(null);
19
+ }
20
+ };
21
+ const hasVersionHistory = versions?.hasVersioning ?? false;
22
+ const isBusy = pendingAction !== null || document.saveStatus === "saving";
23
+ const status = document.conflict
24
+ ? "Conflict"
25
+ : document.isLoading
26
+ ? "Loading"
27
+ : document.saveStatus === "saving"
28
+ ? "Saving"
29
+ : document.saveStatus === "pending"
30
+ ? "Autosave pending"
31
+ : document.isDirty
32
+ ? "Unsaved changes"
33
+ : document.saveStatus === "saved"
34
+ ? "Saved"
35
+ : "Idle";
36
+ return (_jsxs("div", { className: joinClassNames("mdkit-document-toolbar", className), "data-conflict": document.conflict ? "true" : undefined, "data-dirty": document.isDirty ? "true" : undefined, "data-save-status": document.saveStatus, "data-status": status.toLowerCase().replace(/\s+/g, "-"), children: [_jsxs("div", { className: "mdkit-document-toolbar-status", children: [_jsx("strong", { children: status }), _jsx("span", { children: formatUpdatedAt(document.updatedAt) }), _jsxs("span", { children: ["Collaboration ", collaboration ? collaboration.status : "off"] })] }), document.error && !document.conflict ? (_jsx("div", { className: "mdkit-document-toolbar-error", children: document.error })) : null, _jsxs("div", { className: "mdkit-document-toolbar-actions", children: [_jsx("button", { type: "button", disabled: isBusy ||
37
+ document.conflict ||
38
+ !hasVersionHistory ||
39
+ versions?.isLoading ||
40
+ !onOpenVersionHistory, onClick: () => void runAction("versions", async () => {
41
+ await versions?.refresh();
42
+ await onOpenVersionHistory?.();
43
+ }), children: versions?.isLoading
44
+ ? "Loading versions..."
45
+ : `Version ${String(document.version ?? "none")}` }), document.conflict && onOpenConflict ? (_jsx("button", { type: "button", className: "mdkit-document-toolbar-conflict-trigger", disabled: isBusy, onClick: () => void runAction("conflict", async () => {
46
+ await onOpenConflict();
47
+ }), children: "Resolve conflict" })) : null] }), document.conflict && showConflictActions ? (_jsxs("div", { className: "mdkit-document-toolbar-conflict", children: [_jsx("span", { children: "Remote changes conflict with local edits." }), _jsx("button", { type: "button", disabled: isBusy, onClick: () => void runAction("reload", document.resync), children: "Keep remote" }), _jsx("button", { type: "button", disabled: isBusy, onClick: () => void runAction("overwrite", document.forceSave), children: "Keep local" })] })) : null] }));
48
+ };
@@ -0,0 +1,57 @@
1
+ import type { HocuspocusProvider } from "@hocuspocus/provider";
2
+ import type * as Y from "yjs";
3
+ export type MdKitDocumentVersionToken = string | number | null;
4
+ export type MdKitDocumentSnapshot = {
5
+ content: string;
6
+ version: MdKitDocumentVersionToken;
7
+ updatedAt?: string | null;
8
+ };
9
+ export type MdKitDocumentWriteInput = {
10
+ documentId: string;
11
+ content: string;
12
+ baseVersion: MdKitDocumentVersionToken;
13
+ force?: boolean;
14
+ };
15
+ export type MdKitDocumentWriteResult = {
16
+ version: MdKitDocumentVersionToken;
17
+ updatedAt?: string | null;
18
+ } | {
19
+ conflict: true;
20
+ version?: MdKitDocumentVersionToken;
21
+ updatedAt?: string | null;
22
+ };
23
+ export type MdKitDocumentVersionSummary = {
24
+ id: string;
25
+ label?: string;
26
+ createdAt: string;
27
+ authorLabel?: string | null;
28
+ updatedAt?: string | null;
29
+ version?: MdKitDocumentVersionToken;
30
+ };
31
+ export type MdKitDocumentVersionDetail = MdKitDocumentVersionSummary & {
32
+ content: string;
33
+ };
34
+ export interface MdKitDocumentAdapter {
35
+ readDocument(documentId: string): Promise<MdKitDocumentSnapshot>;
36
+ writeDocument(input: MdKitDocumentWriteInput): Promise<MdKitDocumentWriteResult>;
37
+ resyncDocument?(documentId: string): Promise<MdKitDocumentSnapshot>;
38
+ listDocumentVersions?(documentId: string): Promise<MdKitDocumentVersionSummary[]>;
39
+ readDocumentVersion?(input: {
40
+ documentId: string;
41
+ versionId: string;
42
+ }): Promise<MdKitDocumentVersionDetail | null>;
43
+ }
44
+ export type MdKitCollaborationStatus = "connecting" | "connected" | "disconnected";
45
+ export type MdKitCollaborationParticipant = {
46
+ id: string;
47
+ name: string;
48
+ color?: string;
49
+ imageUrl?: string;
50
+ };
51
+ export type MdKitCollaborationSession = {
52
+ collaborator: MdKitCollaborationParticipant;
53
+ document: Y.Doc;
54
+ provider: HocuspocusProvider | null;
55
+ roomName: string;
56
+ status: MdKitCollaborationStatus;
57
+ };
@@ -0,0 +1 @@
1
+ export {};