From 82540c93dd2b3111b35a6102b82b598fd0ecc365 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Sat, 20 Dec 2025 22:39:31 +0200 Subject: [PATCH 1/7] fix(zen): caret positioning for mixed language directions (@byseif21) --- frontend/src/ts/utils/caret.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/ts/utils/caret.ts b/frontend/src/ts/utils/caret.ts index 47408db78917..ab23777bf2c2 100644 --- a/frontend/src/ts/utils/caret.ts +++ b/frontend/src/ts/utils/caret.ts @@ -1,6 +1,7 @@ import { CaretStyle } from "@monkeytype/schemas/configs"; import Config from "../config"; import * as TestWords from "../test/test-words"; +import * as TestInput from "../test/test-input"; import { getTotalInlineMargin } from "./misc"; import { isWordRightToLeft } from "./strings"; import { requestDebouncedAnimationFrame } from "./debounced-animation-frame"; @@ -290,6 +291,12 @@ export class Caret { const letters = word?.qsa("letter") ?? []; const wordText = TestWords.words.get(options.wordIndex); + // in zen mode, use the input content to determine word direction + const wordTextForDirection = + Config.mode === "zen" + ? TestInput.input.current + : TestWords.words.get(options.wordIndex); + // caret can be either on the left side of the target letter or the right // we stick to the left side unless we are on the last letter or beyond // then we switch to the right side @@ -334,7 +341,7 @@ export class Caret { const { left, top, width } = this.getTargetPositionAndWidth({ word, letter, - wordText, + wordText: wordTextForDirection, side, isLanguageRightToLeft: options.isLanguageRightToLeft, isDirectionReversed: options.isDirectionReversed, From c1868c762f813108d1ac9d7348735652548b508f Mon Sep 17 00:00:00 2001 From: byseif21 Date: Sun, 21 Dec 2025 17:36:34 +0200 Subject: [PATCH 2/7] direction at letter level only in zen, so caret follows mixed RTL/LTR input without affecting non-zen behavior --- frontend/src/ts/utils/caret.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/ts/utils/caret.ts b/frontend/src/ts/utils/caret.ts index ab23777bf2c2..00fc1e9bba61 100644 --- a/frontend/src/ts/utils/caret.ts +++ b/frontend/src/ts/utils/caret.ts @@ -412,12 +412,22 @@ export class Caret { isLanguageRightToLeft: boolean; isDirectionReversed: boolean; }): { left: number; top: number; width: number } { - const isWordRTL = isWordRightToLeft( + const baseWordIsRTL = isWordRightToLeft( options.wordText, options.isLanguageRightToLeft, options.isDirectionReversed, ); + // that's for zen mode, for mixed RTL/LTR input + const isWordRTL = + Config.mode === "zen" + ? isWordRightToLeft( + options.letter.native.textContent ?? "", + options.isDirectionReversed ? !baseWordIsRTL : baseWordIsRTL, + options.isDirectionReversed, + ) + : baseWordIsRTL; + //if the letter is not visible, use the closest visible letter const isLetterVisible = options.letter.getOffsetWidth() > 0; if (!isLetterVisible) { From d03cc56e9679e79225c56f06418cc60123959e25 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Mon, 22 Dec 2025 02:29:07 +0200 Subject: [PATCH 3/7] unneeded fb --- frontend/src/ts/utils/caret.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ts/utils/caret.ts b/frontend/src/ts/utils/caret.ts index 00fc1e9bba61..5f3b8aa73fcb 100644 --- a/frontend/src/ts/utils/caret.ts +++ b/frontend/src/ts/utils/caret.ts @@ -423,7 +423,7 @@ export class Caret { Config.mode === "zen" ? isWordRightToLeft( options.letter.native.textContent ?? "", - options.isDirectionReversed ? !baseWordIsRTL : baseWordIsRTL, + options.isLanguageRightToLeft, options.isDirectionReversed, ) : baseWordIsRTL; From 130f09ee242870ed83cb0ad42fd775e1431aa6d9 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Tue, 6 Jan 2026 01:23:50 +0200 Subject: [PATCH 4/7] . --- frontend/src/ts/utils/caret.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/src/ts/utils/caret.ts b/frontend/src/ts/utils/caret.ts index e96bc6aae880..c29982f0fce4 100644 --- a/frontend/src/ts/utils/caret.ts +++ b/frontend/src/ts/utils/caret.ts @@ -293,9 +293,7 @@ export class Caret { // in zen mode, use the input content to determine word direction const wordTextForDirection = - Config.mode === "zen" - ? TestInput.input.current - : TestWords.words.get(options.wordIndex); + Config.mode === "zen" ? TestInput.input.current : wordText; // caret can be either on the left side of the target letter or the right // we stick to the left side unless we are on the last letter or beyond @@ -412,7 +410,6 @@ export class Caret { isLanguageRightToLeft: boolean; isDirectionReversed: boolean; }): { left: number; top: number; width: number } { - const [baseWordIsRTL, isFullMatch] = isWordRightToLeft( options.wordText, options.isLanguageRightToLeft, @@ -426,7 +423,7 @@ export class Caret { options.letter.native.textContent ?? "", options.isLanguageRightToLeft, options.isDirectionReversed, - ) + )[0] : baseWordIsRTL; //if the letter is not visible, use the closest visible letter From 86f86b7c91a64525f73a490b431a6638d92be9d1 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Wed, 7 Jan 2026 02:01:56 +0200 Subject: [PATCH 5/7] refactor move zen logic into getTargetPositionAndWidth & reduce redundancey also prevent wordRtl calss statment for zen --- frontend/src/ts/utils/caret.ts | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/frontend/src/ts/utils/caret.ts b/frontend/src/ts/utils/caret.ts index c29982f0fce4..5e554742f0d8 100644 --- a/frontend/src/ts/utils/caret.ts +++ b/frontend/src/ts/utils/caret.ts @@ -1,7 +1,6 @@ import { CaretStyle } from "@monkeytype/schemas/configs"; import Config from "../config"; import * as TestWords from "../test/test-words"; -import * as TestInput from "../test/test-input"; import { getTotalInlineMargin } from "./misc"; import { isWordRightToLeft } from "./strings"; import { requestDebouncedAnimationFrame } from "./debounced-animation-frame"; @@ -291,10 +290,6 @@ export class Caret { const letters = word?.qsa("letter") ?? []; const wordText = TestWords.words.get(options.wordIndex); - // in zen mode, use the input content to determine word direction - const wordTextForDirection = - Config.mode === "zen" ? TestInput.input.current : wordText; - // caret can be either on the left side of the target letter or the right // we stick to the left side unless we are on the last letter or beyond // then we switch to the right side @@ -339,7 +334,7 @@ export class Caret { const { left, top, width } = this.getTargetPositionAndWidth({ word, letter, - wordText: wordTextForDirection, + wordText, side, isLanguageRightToLeft: options.isLanguageRightToLeft, isDirectionReversed: options.isDirectionReversed, @@ -410,22 +405,14 @@ export class Caret { isLanguageRightToLeft: boolean; isDirectionReversed: boolean; }): { left: number; top: number; width: number } { - const [baseWordIsRTL, isFullMatch] = isWordRightToLeft( - options.wordText, + // in zen mode we need to check per-letter + const isZen = Config.mode === "zen"; + const [isWordRTL, isFullMatch] = isWordRightToLeft( + isZen ? (options.letter.native.textContent ?? "") : options.wordText, options.isLanguageRightToLeft, options.isDirectionReversed, ); - // that's for zen mode, for mixed RTL/LTR input - const isWordRTL = - Config.mode === "zen" - ? isWordRightToLeft( - options.letter.native.textContent ?? "", - options.isLanguageRightToLeft, - options.isDirectionReversed, - )[0] - : baseWordIsRTL; - //if the letter is not visible, use the closest visible letter const isLetterVisible = options.letter.getOffsetWidth() > 0; if (!isLetterVisible) { @@ -470,7 +457,7 @@ export class Caret { // yes, this is all super verbose, but its easier to maintain and understand if (isWordRTL) { - if (isFullMatch) options.word.addClass("wordRtl"); + if (!isZen && isFullMatch) options.word.addClass("wordRtl"); let afterLetterCorrection = 0; if (options.side === "afterLetter") { if (this.isFullWidth()) { From 54c275ccc3ac672dd5cbaaf44cb5577913612973 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Wed, 7 Jan 2026 20:51:07 +0200 Subject: [PATCH 6/7] per letter for custom and polyglot to fix the caret in mixing --- frontend/src/ts/utils/caret.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/src/ts/utils/caret.ts b/frontend/src/ts/utils/caret.ts index 5e554742f0d8..0a01e8550b07 100644 --- a/frontend/src/ts/utils/caret.ts +++ b/frontend/src/ts/utils/caret.ts @@ -405,10 +405,13 @@ export class Caret { isLanguageRightToLeft: boolean; isDirectionReversed: boolean; }): { left: number; top: number; width: number } { - // in zen mode we need to check per-letter - const isZen = Config.mode === "zen"; + // in zen, custom or polyglot mode we may need per-letter + const perLetter = + Config.mode === "zen" || + Config.mode === "custom" || + Config.funbox.includes("polyglot"); const [isWordRTL, isFullMatch] = isWordRightToLeft( - isZen ? (options.letter.native.textContent ?? "") : options.wordText, + perLetter ? (options.letter.native.textContent ?? "") : options.wordText, options.isLanguageRightToLeft, options.isDirectionReversed, ); @@ -457,7 +460,7 @@ export class Caret { // yes, this is all super verbose, but its easier to maintain and understand if (isWordRTL) { - if (!isZen && isFullMatch) options.word.addClass("wordRtl"); + if (!perLetter && isFullMatch) options.word.addClass("wordRtl"); let afterLetterCorrection = 0; if (options.side === "afterLetter") { if (this.isFullWidth()) { From b3749052ce97b24a7498f4ac9bd53f968b14cb2e Mon Sep 17 00:00:00 2001 From: byseif21 Date: Wed, 7 Jan 2026 21:01:39 +0200 Subject: [PATCH 7/7] naming --- frontend/src/ts/utils/caret.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/ts/utils/caret.ts b/frontend/src/ts/utils/caret.ts index 0a01e8550b07..97435fbc105f 100644 --- a/frontend/src/ts/utils/caret.ts +++ b/frontend/src/ts/utils/caret.ts @@ -405,13 +405,15 @@ export class Caret { isLanguageRightToLeft: boolean; isDirectionReversed: boolean; }): { left: number; top: number; width: number } { - // in zen, custom or polyglot mode we may need per-letter - const perLetter = + // in zen, custom or polyglot mode we need to check per-letter + const checkRtlByLetter = Config.mode === "zen" || Config.mode === "custom" || Config.funbox.includes("polyglot"); const [isWordRTL, isFullMatch] = isWordRightToLeft( - perLetter ? (options.letter.native.textContent ?? "") : options.wordText, + checkRtlByLetter + ? (options.letter.native.textContent ?? "") + : options.wordText, options.isLanguageRightToLeft, options.isDirectionReversed, ); @@ -460,7 +462,7 @@ export class Caret { // yes, this is all super verbose, but its easier to maintain and understand if (isWordRTL) { - if (!perLetter && isFullMatch) options.word.addClass("wordRtl"); + if (!checkRtlByLetter && isFullMatch) options.word.addClass("wordRtl"); let afterLetterCorrection = 0; if (options.side === "afterLetter") { if (this.isFullWidth()) {