Contribute rewrite Phase 1: CM6 markdown source editor
Swap the Contribute-mode editing surface from Tiptap WYSIWYG to a CodeMirror 6 markdown source editor. Discuss mode and any read-only viewing continues to render through Tiptap. The §8.11 manual-edit debounce now reads the raw markdown source via CodeMirror's doc, eliminating the lossy `editor.getText()` round-trip that RFCView.jsx flagged as a §19.2 candidate; what the contributor typed is exactly what gets POSTed to manual flush. The §8.10 paragraph-margin gutter accent on Tiptap is dropped — it was dead in read-only Discuss anyway, and Phase 4 of the Contribute rewrite adds a change-anchored gutter against the CM6 raw pane. The Tiptap-side accept-time injection of <span class="tracked-*"> markup also drops out — CM6 can't render those spans, and Phase 3's preview pane is the proper home for tracked changes. The reviewMode / DiffView toggle still works against marked-rendered HTML in the interim until Phase 7 retires it. Notes: - Bundle gzipped grows ~50KB for CM6 modules (state/view/commands/ language/lang-markdown). Mermaid in Phase 2 will be the larger lift and should lazy-load. - Verified the CM6 editor mounts cleanly via Vite dev preview: ref handle (`view/getDoc/setDoc`) is wired, `onUpdate` fires on doc changes, gutter / line-numbers / active-line all render, and the source round-trips verbatim. Could not drive the full RFCView Contribute flow end-to-end without a running backend; the manual countdown + save-now + accept/decline pathways are verified by inspection only. - 125 backend integration tests still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+220
@@ -8,6 +8,11 @@
|
|||||||
"name": "rfc-app-frontend",
|
"name": "rfc-app-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/commands": "^6.10.3",
|
||||||
|
"@codemirror/lang-markdown": "^6.5.0",
|
||||||
|
"@codemirror/language": "^6.12.3",
|
||||||
|
"@codemirror/state": "^6.6.0",
|
||||||
|
"@codemirror/view": "^6.43.0",
|
||||||
"@tiptap/extension-placeholder": "^3.5.0",
|
"@tiptap/extension-placeholder": "^3.5.0",
|
||||||
"@tiptap/pm": "^3.5.0",
|
"@tiptap/pm": "^3.5.0",
|
||||||
"@tiptap/react": "^3.5.0",
|
"@tiptap/react": "^3.5.0",
|
||||||
@@ -24,6 +29,136 @@
|
|||||||
"vite": "^8.0.12"
|
"vite": "^8.0.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@codemirror/autocomplete": {
|
||||||
|
"version": "6.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.2.tgz",
|
||||||
|
"integrity": "sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.17.0",
|
||||||
|
"@lezer/common": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/commands": {
|
||||||
|
"version": "6.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz",
|
||||||
|
"integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.6.0",
|
||||||
|
"@codemirror/view": "^6.27.0",
|
||||||
|
"@lezer/common": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lang-css": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@lezer/common": "^1.0.2",
|
||||||
|
"@lezer/css": "^1.1.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lang-html": {
|
||||||
|
"version": "6.4.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
|
||||||
|
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/lang-css": "^6.0.0",
|
||||||
|
"@codemirror/lang-javascript": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.4.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.17.0",
|
||||||
|
"@lezer/common": "^1.0.0",
|
||||||
|
"@lezer/css": "^1.1.0",
|
||||||
|
"@lezer/html": "^1.3.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lang-javascript": {
|
||||||
|
"version": "6.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
|
||||||
|
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.6.0",
|
||||||
|
"@codemirror/lint": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.17.0",
|
||||||
|
"@lezer/common": "^1.0.0",
|
||||||
|
"@lezer/javascript": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lang-markdown": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.7.1",
|
||||||
|
"@codemirror/lang-html": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.3.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0",
|
||||||
|
"@lezer/common": "^1.2.1",
|
||||||
|
"@lezer/markdown": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/language": {
|
||||||
|
"version": "6.12.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz",
|
||||||
|
"integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.23.0",
|
||||||
|
"@lezer/common": "^1.5.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.0.0",
|
||||||
|
"style-mod": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lint": {
|
||||||
|
"version": "6.9.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.6.tgz",
|
||||||
|
"integrity": "sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.42.0",
|
||||||
|
"crelt": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/state": {
|
||||||
|
"version": "6.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz",
|
||||||
|
"integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@marijn/find-cluster-break": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/view": {
|
||||||
|
"version": "6.43.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.0.tgz",
|
||||||
|
"integrity": "sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.6.0",
|
||||||
|
"crelt": "^1.0.6",
|
||||||
|
"style-mod": "^4.1.0",
|
||||||
|
"w3c-keyname": "^2.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
@@ -86,6 +221,79 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@lezer/common": {
|
||||||
|
"version": "1.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
|
||||||
|
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/css": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/highlight": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/html": {
|
||||||
|
"version": "1.3.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
|
||||||
|
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/javascript": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.1.3",
|
||||||
|
"@lezer/lr": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/lr": {
|
||||||
|
"version": "1.4.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
|
||||||
|
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/markdown": {
|
||||||
|
"version": "1.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz",
|
||||||
|
"integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.5.0",
|
||||||
|
"@lezer/highlight": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@marijn/find-cluster-break": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||||
@@ -912,6 +1120,12 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crelt": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -1585,6 +1799,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/style-mod": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.16",
|
"version": "0.2.16",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,11 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/commands": "^6.10.3",
|
||||||
|
"@codemirror/lang-markdown": "^6.5.0",
|
||||||
|
"@codemirror/language": "^6.12.3",
|
||||||
|
"@codemirror/state": "^6.6.0",
|
||||||
|
"@codemirror/view": "^6.43.0",
|
||||||
"@tiptap/extension-placeholder": "^3.5.0",
|
"@tiptap/extension-placeholder": "^3.5.0",
|
||||||
"@tiptap/pm": "^3.5.0",
|
"@tiptap/pm": "^3.5.0",
|
||||||
"@tiptap/react": "^3.5.0",
|
"@tiptap/react": "^3.5.0",
|
||||||
|
|||||||
+26
-12
@@ -1,4 +1,4 @@
|
|||||||
/* Adapted from the prototype's App.css per §18, narrowed to slice-1 surfaces. */
|
/* App.css — narrowed to slice-1 surfaces. */
|
||||||
|
|
||||||
.boot {
|
.boot {
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
@@ -441,13 +441,6 @@
|
|||||||
.editor-content .tiptap ul, .editor-content .tiptap ol { padding-left: 24px; }
|
.editor-content .tiptap ul, .editor-content .tiptap ol { padding-left: 24px; }
|
||||||
.editor-content .tiptap code { background: #f0f0ee; padding: 1px 5px; border-radius: 3px; font-size: 13px; }
|
.editor-content .tiptap code { background: #f0f0ee; padding: 1px 5px; border-radius: 3px; font-size: 13px; }
|
||||||
|
|
||||||
.editor-content .tiptap .paragraph-changed {
|
|
||||||
border-left: 3px solid #f59e0b;
|
|
||||||
padding-left: 10px;
|
|
||||||
margin-left: -13px;
|
|
||||||
background: linear-gradient(to right, #fffbeb 0%, transparent 60%);
|
|
||||||
border-radius: 0 4px 4px 0;
|
|
||||||
}
|
|
||||||
.editor-content .tiptap .selection-highlight {
|
.editor-content .tiptap .selection-highlight {
|
||||||
background: rgba(99, 102, 241, 0.15);
|
background: rgba(99, 102, 241, 0.15);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
@@ -463,6 +456,27 @@
|
|||||||
border-radius: 2px; padding: 1px 2px; cursor: pointer;
|
border-radius: 2px; padding: 1px 2px; cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* CodeMirror 6 source editor (Contribute mode). */
|
||||||
|
.cm-source-editor {
|
||||||
|
flex: 1; min-height: 0;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.cm-source-editor .cm-editor {
|
||||||
|
flex: 1; min-height: 0;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||||
|
font-size: 13px; line-height: 1.6;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.cm-source-editor .cm-editor.cm-focused { outline: none; }
|
||||||
|
.cm-source-editor .cm-scroller { padding: 24px 0; }
|
||||||
|
.cm-source-editor .cm-content { padding: 0 32px; max-width: 880px; }
|
||||||
|
.cm-source-editor .cm-gutters {
|
||||||
|
background: #fafafa; border-right: 1px solid #f0f0ee; color: #b0b0b0;
|
||||||
|
}
|
||||||
|
.cm-source-editor .cm-activeLineGutter { background: #f3f4f6; color: #555; }
|
||||||
|
.cm-source-editor .cm-activeLine { background: #fafbfc; }
|
||||||
|
|
||||||
.readonly-bar {
|
.readonly-bar {
|
||||||
border-top: 1px solid #e5e5e5;
|
border-top: 1px solid #e5e5e5;
|
||||||
padding: 10px 16px; text-align: center;
|
padding: 10px 16px; text-align: center;
|
||||||
@@ -1236,8 +1250,8 @@
|
|||||||
color: #fbbf24; /* admin link sits a notch warmer to signal authority */
|
color: #fbbf24; /* admin link sits a notch warmer to signal authority */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* /philosophy — the §14.2 read surface. The body inherits the
|
/* /philosophy — the §14.2 read surface. The body uses the shared
|
||||||
prototype's markdown styling; the header is a thin chrome strip. */
|
markdown styling; the header is a thin chrome strip. */
|
||||||
.chrome-pane {
|
.chrome-pane {
|
||||||
flex: 1; min-width: 0; overflow: auto;
|
flex: 1; min-width: 0; overflow: auto;
|
||||||
padding: 0; background: #fff;
|
padding: 0; background: #fff;
|
||||||
@@ -1298,8 +1312,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Richer landing page (§14.1) — adds the three-item deck under the
|
/* Richer landing page (§14.1) — adds the three-item deck under the
|
||||||
pitch. The .landing container's flex centering stays from the
|
pitch. The .landing container handles flex centering; the new
|
||||||
prototype; the new content lives inside .landing-inner. */
|
content lives inside .landing-inner. */
|
||||||
.landing-inner {
|
.landing-inner {
|
||||||
max-width: 620px;
|
max-width: 620px;
|
||||||
display: flex; flex-direction: column; align-items: center;
|
display: flex; flex-direction: column; align-items: center;
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
// Editor.jsx — the §8 center-column editor.
|
// Editor.jsx — the Discuss-mode read-only renderer (and main/no-contribute
|
||||||
|
// fallback). Tiptap on ProseMirror per §18; the Contribute-mode editing
|
||||||
|
// surface moved to MarkdownSourceEditor (CodeMirror 6) in the Phase 1
|
||||||
|
// rewrite of Contribute, leaving this surface for read-only rendering
|
||||||
|
// plus the §8.12 selection tooltip's coords.
|
||||||
//
|
//
|
||||||
// Tiptap on ProseMirror per §18. Two ProseMirror plugins live alongside
|
// One ProseMirror plugin still lives alongside StarterKit:
|
||||||
// StarterKit:
|
|
||||||
//
|
|
||||||
// • paragraphDiff — the §8.10 paragraph-margin gutter accent. Compares
|
|
||||||
// each paragraph against an open-session baseline (the
|
|
||||||
// `originalParagraphsRef` ref the parent owns and refreshes when the
|
|
||||||
// baseline shifts — e.g. on branch switch or a server-side flush).
|
|
||||||
//
|
//
|
||||||
// • selectionHighlight — keeps a selected passage highlighted while
|
// • selectionHighlight — keeps a selected passage highlighted while
|
||||||
// focus moves to the §8.12 selection tooltip. Driven by meta
|
// focus moves to the §8.12 selection tooltip. Driven by meta
|
||||||
// transactions dispatched from the parent.
|
// transactions dispatched from the parent.
|
||||||
//
|
//
|
||||||
// The inline tracked-delete / tracked-insert markup from §8.10 is
|
// The paragraph-margin gutter accent that used to live here is dropped;
|
||||||
// session-local HTML the parent injects via `editor.commands.setContent`
|
// Phase 4 of the Contribute rewrite adds a proper change-anchored gutter
|
||||||
// when a change is accepted; the editor itself doesn't own that state.
|
// against the CodeMirror raw pane.
|
||||||
// On reload the markup clears and DiffView (toolbar toggle) is the
|
|
||||||
// durable read of accepted changes.
|
|
||||||
|
|
||||||
import { useEditor, EditorContent, Extension } from '@tiptap/react'
|
import { useEditor, EditorContent, Extension } from '@tiptap/react'
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
@@ -25,47 +21,6 @@ import { marked } from 'marked'
|
|||||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||||
import { Decoration, DecorationSet } from 'prosemirror-view'
|
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||||
|
|
||||||
// ── Paragraph diff plugin ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const diffKey = new PluginKey('paragraphDiff')
|
|
||||||
|
|
||||||
function makeDiffPlugin(originalParagraphsRef) {
|
|
||||||
return new Plugin({
|
|
||||||
key: diffKey,
|
|
||||||
props: {
|
|
||||||
decorations(state) {
|
|
||||||
const originals = originalParagraphsRef?.current
|
|
||||||
if (!originals || originals.length === 0) return DecorationSet.empty
|
|
||||||
|
|
||||||
const decorations = []
|
|
||||||
let idx = 0
|
|
||||||
state.doc.descendants((node, pos) => {
|
|
||||||
if (node.type.name === 'paragraph' || node.type.name === 'heading') {
|
|
||||||
const current = node.textContent.trim()
|
|
||||||
const original = (originals[idx] ?? '').trim()
|
|
||||||
if (current !== original) {
|
|
||||||
decorations.push(
|
|
||||||
Decoration.node(pos, pos + node.nodeSize, { class: 'paragraph-changed' })
|
|
||||||
)
|
|
||||||
}
|
|
||||||
idx++
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return DecorationSet.create(state.doc, decorations)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function DiffExtension(originalParagraphsRef) {
|
|
||||||
return Extension.create({
|
|
||||||
name: 'paragraphDiff',
|
|
||||||
addProseMirrorPlugins() {
|
|
||||||
return [makeDiffPlugin(originalParagraphsRef)]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Selection highlight plugin ────────────────────────────────────────────
|
// ── Selection highlight plugin ────────────────────────────────────────────
|
||||||
|
|
||||||
export const selectionHighlightKey = new PluginKey('selectionHighlight')
|
export const selectionHighlightKey = new PluginKey('selectionHighlight')
|
||||||
@@ -110,7 +65,6 @@ function SelectionHighlightExtension() {
|
|||||||
export default function Editor({
|
export default function Editor({
|
||||||
content,
|
content,
|
||||||
editorRef,
|
editorRef,
|
||||||
originalParagraphsRef,
|
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
editable = true,
|
editable = true,
|
||||||
@@ -132,7 +86,6 @@ export default function Editor({
|
|||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit,
|
StarterKit,
|
||||||
DiffExtension(originalParagraphsRef),
|
|
||||||
SelectionHighlightExtension(),
|
SelectionHighlightExtension(),
|
||||||
],
|
],
|
||||||
content: '<p></p>',
|
content: '<p></p>',
|
||||||
@@ -167,20 +120,10 @@ export default function Editor({
|
|||||||
}
|
}
|
||||||
}, [editor, onSelectionChange, reportSelection])
|
}, [editor, onSelectionChange, reportSelection])
|
||||||
|
|
||||||
// Reload content + snapshot baseline paragraphs.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor || content == null) return
|
if (!editor || content == null) return
|
||||||
const html = marked.parse(content)
|
const html = marked.parse(content)
|
||||||
editor.commands.setContent(html, false)
|
editor.commands.setContent(html, false)
|
||||||
if (originalParagraphsRef) {
|
|
||||||
const paragraphs = []
|
|
||||||
editor.state.doc.descendants(node => {
|
|
||||||
if (node.type.name === 'paragraph' || node.type.name === 'heading') {
|
|
||||||
paragraphs.push(node.textContent.trim())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
originalParagraphsRef.current = paragraphs
|
|
||||||
}
|
|
||||||
}, [content, editor])
|
}, [content, editor])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// MarkdownSourceEditor.jsx — the Contribute-mode raw markdown source editor.
|
||||||
|
//
|
||||||
|
// CodeMirror 6 per §18 (Phase 1 of the Contribute rewrite). The editing
|
||||||
|
// surface is plain markdown; the rendered preview lives in a sibling pane
|
||||||
|
// (added in Phase 2). No HTML→markdown round-trip — `getDoc()` returns
|
||||||
|
// the source verbatim, which is what we POST in §8.11's manual flush.
|
||||||
|
//
|
||||||
|
// The parent receives an imperative handle on `editorRef`:
|
||||||
|
// { view, getDoc(), setDoc(text) }
|
||||||
|
// modeled lightly after the Tiptap surface so RFCView can branch on
|
||||||
|
// whichever editor is mounted without an adapter layer.
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { EditorState } from '@codemirror/state'
|
||||||
|
import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter } from '@codemirror/view'
|
||||||
|
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'
|
||||||
|
import { markdown } from '@codemirror/lang-markdown'
|
||||||
|
import { syntaxHighlighting, defaultHighlightStyle, indentOnInput, bracketMatching } from '@codemirror/language'
|
||||||
|
|
||||||
|
export default function MarkdownSourceEditor({
|
||||||
|
initialDoc = '',
|
||||||
|
editorRef,
|
||||||
|
onUpdate,
|
||||||
|
}) {
|
||||||
|
const hostRef = useRef(null)
|
||||||
|
const viewRef = useRef(null)
|
||||||
|
const onUpdateRef = useRef(onUpdate)
|
||||||
|
|
||||||
|
useEffect(() => { onUpdateRef.current = onUpdate }, [onUpdate])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hostRef.current) return
|
||||||
|
const state = EditorState.create({
|
||||||
|
doc: initialDoc,
|
||||||
|
extensions: [
|
||||||
|
history(),
|
||||||
|
lineNumbers(),
|
||||||
|
highlightActiveLine(),
|
||||||
|
highlightActiveLineGutter(),
|
||||||
|
indentOnInput(),
|
||||||
|
bracketMatching(),
|
||||||
|
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
|
||||||
|
markdown(),
|
||||||
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
|
EditorView.lineWrapping,
|
||||||
|
EditorView.updateListener.of(u => {
|
||||||
|
if (u.docChanged) {
|
||||||
|
onUpdateRef.current?.(u.state.doc.toString())
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const view = new EditorView({ state, parent: hostRef.current })
|
||||||
|
viewRef.current = view
|
||||||
|
if (editorRef) {
|
||||||
|
editorRef.current = {
|
||||||
|
view,
|
||||||
|
getDoc: () => view.state.doc.toString(),
|
||||||
|
setDoc: (text) => {
|
||||||
|
const cur = view.state.doc.toString()
|
||||||
|
if (cur === text) return
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from: 0, to: view.state.doc.length, insert: text },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
view.destroy()
|
||||||
|
viewRef.current = null
|
||||||
|
if (editorRef && editorRef.current?.view === view) editorRef.current = null
|
||||||
|
}
|
||||||
|
// Construct once. Doc changes flow through the setDoc effect below.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Reflect external doc changes (branch reload, server-side flush, etc).
|
||||||
|
useEffect(() => {
|
||||||
|
const v = viewRef.current
|
||||||
|
if (!v) return
|
||||||
|
if (v.state.doc.toString() === initialDoc) return
|
||||||
|
v.dispatch({
|
||||||
|
changes: { from: 0, to: v.state.doc.length, insert: initialDoc ?? '' },
|
||||||
|
})
|
||||||
|
}, [initialDoc])
|
||||||
|
|
||||||
|
return <div ref={hostRef} className="cm-source-editor" />
|
||||||
|
}
|
||||||
@@ -34,7 +34,9 @@ import {
|
|||||||
streamChatTurn,
|
streamChatTurn,
|
||||||
} from '../api'
|
} from '../api'
|
||||||
import Editor, { selectionHighlightKey } from './Editor.jsx'
|
import Editor, { selectionHighlightKey } from './Editor.jsx'
|
||||||
|
import MarkdownSourceEditor from './MarkdownSourceEditor.jsx'
|
||||||
import SelectionTooltip from './SelectionTooltip.jsx'
|
import SelectionTooltip from './SelectionTooltip.jsx'
|
||||||
|
import { marked } from 'marked'
|
||||||
import PromptBar from './PromptBar.jsx'
|
import PromptBar from './PromptBar.jsx'
|
||||||
import ChatPanel from './ChatPanel.jsx'
|
import ChatPanel from './ChatPanel.jsx'
|
||||||
import ChangePanel from './ChangePanel.jsx'
|
import ChangePanel from './ChangePanel.jsx'
|
||||||
@@ -51,6 +53,15 @@ function debounce(fn, ms) {
|
|||||||
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms) }
|
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Split markdown source into paragraph-ish blocks on blank lines for the
|
||||||
|
// §8.11 manual-edit count. Headings and lists each count as one block.
|
||||||
|
function splitSourceParagraphs(md) {
|
||||||
|
return (md || '')
|
||||||
|
.split(/\n\s*\n/)
|
||||||
|
.map(p => p.trim())
|
||||||
|
.filter(p => p.length > 0)
|
||||||
|
}
|
||||||
|
|
||||||
export default function RFCView({ viewer }) {
|
export default function RFCView({ viewer }) {
|
||||||
const { slug } = useParams()
|
const { slug } = useParams()
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
@@ -66,8 +77,11 @@ export default function RFCView({ viewer }) {
|
|||||||
const [selectedModel, setSelectedModel] = useState('')
|
const [selectedModel, setSelectedModel] = useState('')
|
||||||
|
|
||||||
// Editor state — owned here so accept/decline can mutate it.
|
// Editor state — owned here so accept/decline can mutate it.
|
||||||
|
// editorRef holds either a Tiptap instance (Discuss / read-only) or a
|
||||||
|
// MarkdownSourceEditor handle ({view, getDoc, setDoc}) (Contribute);
|
||||||
|
// call sites branch on shape.
|
||||||
const editorRef = useRef(null)
|
const editorRef = useRef(null)
|
||||||
const originalParagraphsRef = useRef([])
|
const originalSourceLinesRef = useRef([])
|
||||||
const [editorContent, setEditorContent] = useState('')
|
const [editorContent, setEditorContent] = useState('')
|
||||||
|
|
||||||
// Selection + tooltip + selection highlight per §8.12.
|
// Selection + tooltip + selection highlight per §8.12.
|
||||||
@@ -132,6 +146,7 @@ export default function RFCView({ viewer }) {
|
|||||||
.then(view => {
|
.then(view => {
|
||||||
setBranchView(view)
|
setBranchView(view)
|
||||||
setEditorContent(view.body || '')
|
setEditorContent(view.body || '')
|
||||||
|
originalSourceLinesRef.current = splitSourceParagraphs(view.body || '')
|
||||||
setChanges(view.changes || [])
|
setChanges(view.changes || [])
|
||||||
})
|
})
|
||||||
.catch(err => setError(err.message))
|
.catch(err => setError(err.message))
|
||||||
@@ -152,21 +167,23 @@ export default function RFCView({ viewer }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const editor = editorRef.current
|
const editor = editorRef.current
|
||||||
if (!editor?.view) return
|
// Only Tiptap exposes `state.tr` — when CM6 is mounted, the selection
|
||||||
|
// highlight has no surface to render against, so skip the dispatch.
|
||||||
|
if (!editor?.view || !editor?.state?.tr) return
|
||||||
editor.view.dispatch(editor.state.tr.setMeta(selectionHighlightKey, highlightRange))
|
editor.view.dispatch(editor.state.tr.setMeta(selectionHighlightKey, highlightRange))
|
||||||
}, [highlightRange])
|
}, [highlightRange])
|
||||||
|
|
||||||
// Manual-edit debounced upsert per §8.11 — produces a pending manual
|
// Manual-edit debounced upsert per §8.11 — produces a pending manual
|
||||||
// card with a live countdown and an explicit Save now.
|
// card with a live countdown and an explicit Save now. With the CM6
|
||||||
|
// markdown source editor (Phase 1), the doc IS the markdown — no more
|
||||||
|
// HTML→getText round-trip, so §19.2 lossiness is gone.
|
||||||
const flushManualBuffer = useCallback(async () => {
|
const flushManualBuffer = useCallback(async () => {
|
||||||
const editor = editorRef.current
|
const editor = editorRef.current
|
||||||
if (!editor || !branchView || mode !== 'contribute') return
|
if (!editor || !branchView || mode !== 'contribute') return
|
||||||
const text = editor.getText()
|
if (typeof editor.getDoc !== 'function') return
|
||||||
// Convert to a rough markdown by stripping HTML — for v1 we round-trip
|
const raw = editor.getDoc()
|
||||||
// through the editor's getText; this matches the prototype's behavior.
|
const newContent = raw.endsWith('\n') ? raw : raw + '\n'
|
||||||
// A faithful HTML→markdown round-trip is a §19.2 candidate.
|
if (newContent.trim() === (branchView.body || '').trim()) {
|
||||||
const newContent = text.trim() + '\n'
|
|
||||||
if (!newContent || newContent.trim() === (branchView.body || '').trim()) {
|
|
||||||
setManualPending(null)
|
setManualPending(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -179,6 +196,7 @@ export default function RFCView({ viewer }) {
|
|||||||
const fresh = await getBranch(slug, branchParam)
|
const fresh = await getBranch(slug, branchParam)
|
||||||
setBranchView(fresh)
|
setBranchView(fresh)
|
||||||
setChanges(fresh.changes || [])
|
setChanges(fresh.changes || [])
|
||||||
|
originalSourceLinesRef.current = splitSourceParagraphs(fresh.body || '')
|
||||||
setManualPending(null)
|
setManualPending(null)
|
||||||
loadAllMessages(slug, branchParam, fresh.threads).then(setMessages)
|
loadAllMessages(slug, branchParam, fresh.threads).then(setMessages)
|
||||||
}
|
}
|
||||||
@@ -187,22 +205,15 @@ export default function RFCView({ viewer }) {
|
|||||||
}
|
}
|
||||||
}, [slug, branchParam, branchView, mode, manualPending?.paragraphCount])
|
}, [slug, branchParam, branchView, mode, manualPending?.paragraphCount])
|
||||||
|
|
||||||
const handleEditorUpdate = useMemo(() => debounce((plainText) => {
|
const handleEditorUpdate = useMemo(() => debounce((doc) => {
|
||||||
if (mode !== 'contribute' || !branchView) return
|
if (mode !== 'contribute' || !branchView) return
|
||||||
const editor = editorRef.current
|
const current = splitSourceParagraphs(doc)
|
||||||
if (!editor) return
|
const baseline = originalSourceLinesRef.current || []
|
||||||
const currentParagraphs = []
|
const len = Math.max(current.length, baseline.length)
|
||||||
editor.state.doc.descendants(node => {
|
|
||||||
if (node.type.name === 'paragraph' || node.type.name === 'heading') {
|
|
||||||
currentParagraphs.push(node.textContent.trim())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const baseline = originalParagraphsRef.current || []
|
|
||||||
let changed = 0
|
let changed = 0
|
||||||
currentParagraphs.forEach((t, i) => {
|
for (let i = 0; i < len; i++) {
|
||||||
const orig = (baseline[i] ?? '').trim()
|
if ((current[i] ?? '') !== (baseline[i] ?? '')) changed++
|
||||||
if (t !== orig) changed++
|
}
|
||||||
})
|
|
||||||
if (changed > 0) {
|
if (changed > 0) {
|
||||||
setManualPending({ paragraphCount: changed })
|
setManualPending({ paragraphCount: changed })
|
||||||
setManualCountdown({ deadline: Date.now() + MANUAL_IDLE_MS })
|
setManualCountdown({ deadline: Date.now() + MANUAL_IDLE_MS })
|
||||||
@@ -347,24 +358,16 @@ export default function RFCView({ viewer }) {
|
|||||||
// ── Accept / decline / reask ──────────────────────────────────────────
|
// ── Accept / decline / reask ──────────────────────────────────────────
|
||||||
const handleAccept = useCallback(async ({ change, proposed, wasEdited }) => {
|
const handleAccept = useCallback(async ({ change, proposed, wasEdited }) => {
|
||||||
try {
|
try {
|
||||||
const { commit_sha } = await apiAccept(slug, branchParam, change.id, { proposed, wasEdited })
|
await apiAccept(slug, branchParam, change.id, { proposed, wasEdited })
|
||||||
// Inject tracked-change markup into the editor so it renders inline.
|
// Pull authoritative branch state and reset the editor to the new
|
||||||
const editor = editorRef.current
|
// body. The proper tracked-changes overlay lives in Phase 3's
|
||||||
if (editor && change.original) {
|
// preview pane; with CM6 as the source editor there is no HTML
|
||||||
const html = editor.getHTML()
|
// surface to inject `<span class="tracked-*">` into.
|
||||||
const tracked =
|
|
||||||
`<span class="tracked-delete" data-change-id="${change.id}">${change.original}</span>` +
|
|
||||||
`<span class="tracked-insert" data-change-id="${change.id}">${proposed}</span>`
|
|
||||||
const next = html.replace(change.original, tracked)
|
|
||||||
if (next !== html) editor.commands.setContent(next, false)
|
|
||||||
}
|
|
||||||
// Pull the authoritative branch state — body, sha, changes.
|
|
||||||
const fresh = await getBranch(slug, branchParam)
|
const fresh = await getBranch(slug, branchParam)
|
||||||
setBranchView(fresh)
|
setBranchView(fresh)
|
||||||
setChanges(fresh.changes || [])
|
setChanges(fresh.changes || [])
|
||||||
// We do not reset editorContent here — the editor is showing the
|
setEditorContent(fresh.body || '')
|
||||||
// tracked markup overlay; resetting would clear the visual diff
|
originalSourceLinesRef.current = splitSourceParagraphs(fresh.body || '')
|
||||||
// until DiffView is toggled.
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
}
|
}
|
||||||
@@ -419,10 +422,21 @@ export default function RFCView({ viewer }) {
|
|||||||
|
|
||||||
const toggleReviewMode = useCallback(() => {
|
const toggleReviewMode = useCallback(() => {
|
||||||
setReviewMode(prev => {
|
setReviewMode(prev => {
|
||||||
if (!prev) setReviewHTML(editorRef.current?.getHTML() || '')
|
if (!prev) {
|
||||||
|
// CM6 is the contribute-mode editor in Phase 1, so source HTML
|
||||||
|
// comes from rendering the current markdown via marked. The
|
||||||
|
// per-session inline tracked-change spans that Tiptap accumulated
|
||||||
|
// are gone for now; Phase 3's preview pane is the proper home
|
||||||
|
// for tracked changes anyway.
|
||||||
|
const editor = editorRef.current
|
||||||
|
const md = typeof editor?.getDoc === 'function'
|
||||||
|
? editor.getDoc()
|
||||||
|
: (branchView?.body || '')
|
||||||
|
setReviewHTML(marked.parse(md))
|
||||||
|
}
|
||||||
return !prev
|
return !prev
|
||||||
})
|
})
|
||||||
}, [])
|
}, [branchView])
|
||||||
|
|
||||||
// ── Branch dropdown navigation ─────────────────────────────────────────
|
// ── Branch dropdown navigation ─────────────────────────────────────────
|
||||||
const onPickBranch = useCallback((name) => {
|
const onPickBranch = useCallback((name) => {
|
||||||
@@ -621,23 +635,31 @@ export default function RFCView({ viewer }) {
|
|||||||
<DiffView html={reviewHTML} changes={changes} messages={messages} />
|
<DiffView html={reviewHTML} changes={changes} messages={messages} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Editor
|
{editorEditable ? (
|
||||||
content={editorContent}
|
<MarkdownSourceEditor
|
||||||
editorRef={editorRef}
|
initialDoc={editorContent}
|
||||||
originalParagraphsRef={originalParagraphsRef}
|
editorRef={editorRef}
|
||||||
onSelectionChange={handleSelectionChange}
|
onUpdate={handleEditorUpdate}
|
||||||
onUpdate={editorEditable ? handleEditorUpdate : undefined}
|
/>
|
||||||
editable={editorEditable}
|
) : (
|
||||||
/>
|
<Editor
|
||||||
<SelectionTooltip
|
content={editorContent}
|
||||||
selection={selection}
|
editorRef={editorRef}
|
||||||
onAsk={handleTooltipAsk}
|
onSelectionChange={handleSelectionChange}
|
||||||
onFlag={handleTooltipFlag}
|
editable={false}
|
||||||
disabled={isStreaming || !viewer}
|
/>
|
||||||
models={models}
|
)}
|
||||||
selectedModel={selectedModel}
|
{inDiscuss && (
|
||||||
onModelChange={setSelectedModel}
|
<SelectionTooltip
|
||||||
/>
|
selection={selection}
|
||||||
|
onAsk={handleTooltipAsk}
|
||||||
|
onFlag={handleTooltipFlag}
|
||||||
|
disabled={isStreaming || !viewer}
|
||||||
|
models={models}
|
||||||
|
selectedModel={selectedModel}
|
||||||
|
onModelChange={setSelectedModel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{showPromptBar ? (
|
{showPromptBar ? (
|
||||||
<PromptBar
|
<PromptBar
|
||||||
selection={selection?.text || null}
|
selection={selection?.text || null}
|
||||||
|
|||||||
Reference in New Issue
Block a user