From 865d1f5073975f0dbe769a0b3c8bd85fafd152fb Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Thu, 10 Jul 2025 22:38:59 +0100 Subject: [PATCH] Improve cursor hover effect --- examples/website/src/index.html | 2 +- examples/website/src/index.ts | 19 +++++++++++-------- examples/website/src/style.scss | 31 +++++++++++++++++++++++++++---- examples/website/tsconfig.json | 6 ++---- reconcile-js/package.json | 2 +- 5 files changed, 42 insertions(+), 18 deletions(-) diff --git a/examples/website/src/index.html b/examples/website/src/index.html index 5136fc7..ce656d2 100644 --- a/examples/website/src/index.html +++ b/examples/website/src/index.html @@ -28,7 +28,7 @@
-

Reconcile-text: conflict-free 3-way text merging

+

reconcile-text: conflict-free 3-way text merging

Think ; -const sampleText = `The \`reconcile\` Rust library is embedded on this page as a WASM module and powers these text boxes. Experiment with changing the "Original", "First concurrent edit", and "Second concurrent edit" text boxes to see competing changes get merged in real-time within the "Deconflicted result" box. Here, you will see color-coded tokens marking the origin of each token, including ones that got deleted. The result highly depends on the tokenization strategy, for example, deciding how casing or whitespace is taken into account.`; +const sampleText = `The \`reconcile\` Rust library is embedded on this page as a WASM module and powers these text boxes. Experiment with changing the "Original", "First concurrent edit", and "Second concurrent edit" text boxes to see competing changes get merged in real-time within the "Deconflicted result" box. Here, you will see color-coded tokens marking the origin of each token, including ones that got deleted. The result highly depends on the tokenisation strategy, for example, deciding how casing or whitespace is taken into account.`; async function main(): Promise { originalTextArea.addEventListener('input', updateMergedText); @@ -76,11 +76,11 @@ function updateMergedText(): void { } const selectionSide = leftCursors ? 'left' : 'right'; - mergedTextArea.innerHTML = ''; + const fragment = document.createDocumentFragment(); let currentPosition = 0; if (selectionEnd === 0) { - mergedTextArea.appendChild(createCaret(selectionSide === 'left')); + fragment.appendChild(createCaret(selectionSide === 'left')); } for (const { text, history } of results.history) { @@ -93,10 +93,10 @@ function updateMergedText(): void { span.className += ` selection-${selectionSide}`; } - mergedTextArea.appendChild(span); + fragment.appendChild(span); - if (currentPosition == selectionEnd - 1) { - mergedTextArea.appendChild(createCaret(selectionSide === 'left')); + if (currentPosition === selectionEnd - 1) { + fragment.appendChild(createCaret(selectionSide === 'left')); } if (history !== 'RemovedFromLeft' && history !== 'RemovedFromRight') { @@ -105,6 +105,9 @@ function updateMergedText(): void { } } } + + mergedTextArea.innerHTML = ''; + mergedTextArea.appendChild(fragment); } function getCursorsFromActiveTextArea() { @@ -167,8 +170,8 @@ function autoResize(textarea: HTMLTextAreaElement): void { function focusTextArea(textarea: HTMLTextAreaElement): void { textarea.focus(); - textarea.selectionStart = textarea.value.length; - textarea.selectionEnd = textarea.value.length; + textarea.selectionStart = 0; + textarea.selectionEnd = 0; } main(); diff --git a/examples/website/src/style.scss b/examples/website/src/style.scss index a60438f..e39429f 100644 --- a/examples/website/src/style.scss +++ b/examples/website/src/style.scss @@ -1,3 +1,5 @@ +@use 'sass:color'; + // Colour palette $primary-blue: #2451a6; $light-blue: #85bff7; @@ -16,6 +18,10 @@ $gradient-end: #e0e7ef; @return rgba($colour, $opacity); } +@function caret-colour($colour, $amount: 20%) { + @return color.adjust($colour, $lightness: -$amount); +} + * { box-sizing: border-box; margin: 0; @@ -36,6 +42,7 @@ body { height: 100vh; height: 100dvh; overflow-y: auto; + overflow-x: hidden; } .background { @@ -274,9 +281,13 @@ textarea { #merged { width: 100%; user-select: text; + display: flex; + flex-wrap: wrap; > * { position: relative; + display: block; + white-space: pre-wrap; } } @@ -328,13 +339,14 @@ $DOT_RADIUS: 4; .selection-caret { position: relative; + z-index: 1000; &.selection-caret-left { - background: $green; + background: caret-colour($green); } &.selection-caret-right { - background: $light-blue; + background: caret-colour($light-blue); } > * { @@ -359,13 +371,24 @@ $DOT_RADIUS: 4; height: #{$DOT_RADIUS * 2}px; top: -#{$DOT_RADIUS}px; left: -#{$DOT_RADIUS}px; - transition: transform 0.3s ease-in-out; + transition: opacity 0.3s ease-in-out; transform-origin: bottom center; box-sizing: border-box; + + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 30px; + height: 30px; + border-radius: 50%; + } } &:hover > .dot { - transform: scale(0); + opacity: 0; } > .info { diff --git a/examples/website/tsconfig.json b/examples/website/tsconfig.json index 6cf10b0..fb72539 100644 --- a/examples/website/tsconfig.json +++ b/examples/website/tsconfig.json @@ -11,7 +11,5 @@ "skipLibCheck": true, "inlineSourceMap": true }, - "exclude": [ - "./dist" - ] -} \ No newline at end of file + "exclude": ["./dist"] +} diff --git a/reconcile-js/package.json b/reconcile-js/package.json index 0236597..dfa1f7c 100644 --- a/reconcile-js/package.json +++ b/reconcile-js/package.json @@ -48,4 +48,4 @@ "webpack-cli": "^6.0.1", "webpack-merge": "^6.0.1" } -} \ No newline at end of file +}