diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html
index b005e6c37f7c..e56ddfa10eb5 100644
--- a/frontend/src/html/pages/settings.html
+++ b/frontend/src/html/pages/settings.html
@@ -826,6 +826,30 @@
+
diff --git a/frontend/src/styles/test.scss b/frontend/src/styles/test.scss
index 3544dbeaf49a..abd02fa6a891 100644
--- a/frontend/src/styles/test.scss
+++ b/frontend/src/styles/test.scss
@@ -1254,6 +1254,8 @@
}
}
#liveStatsMini {
+ width: 0;
+ justify-content: start;
height: 0;
margin-left: 0.25em;
display: flex;
diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts
index 87358a2f98da..100032beff17 100644
--- a/frontend/src/ts/commandline/lists.ts
+++ b/frontend/src/ts/commandline/lists.ts
@@ -51,6 +51,7 @@ import TimerColorCommands from "./lists/timer-color";
import TimerOpacityCommands from "./lists/timer-opacity";
import HighlightModeCommands from "./lists/highlight-mode";
import TapeModeCommands from "./lists/tape-mode";
+import TapeMarginCommands from "./lists/tape-margin";
import BritishEnglishCommands from "./lists/british-english";
import KeymapModeCommands from "./lists/keymap-mode";
import KeymapStyleCommands from "./lists/keymap-style";
@@ -273,6 +274,7 @@ export const commands: CommandsSubgroup = {
...TimerOpacityCommands,
...HighlightModeCommands,
...TapeModeCommands,
+ ...TapeMarginCommands,
...SmoothLineScrollCommands,
...ShowAllLinesCommands,
...TypingSpeedUnitCommands,
diff --git a/frontend/src/ts/commandline/lists/tape-margin.ts b/frontend/src/ts/commandline/lists/tape-margin.ts
new file mode 100644
index 000000000000..b183c9599220
--- /dev/null
+++ b/frontend/src/ts/commandline/lists/tape-margin.ts
@@ -0,0 +1,19 @@
+import Config, * as UpdateConfig from "../../config";
+import { Command } from "../types";
+
+const commands: Command[] = [
+ {
+ id: "changeTapeMargin",
+ display: "Tape margin...",
+ icon: "fa-tape",
+ input: true,
+ defaultValue: (): string => {
+ return Config.tapeMargin.toString();
+ },
+ exec: ({ input }): void => {
+ if (input === undefined || input === "") return;
+ UpdateConfig.setTapeMargin(parseFloat(input));
+ },
+ },
+];
+export default commands;
diff --git a/frontend/src/ts/config.ts b/frontend/src/ts/config.ts
index 81d1c104215e..6d5442600c65 100644
--- a/frontend/src/ts/config.ts
+++ b/frontend/src/ts/config.ts
@@ -918,6 +918,34 @@ export function setTapeMode(
return true;
}
+export function setTapeMargin(
+ value: ConfigSchemas.TapeMargin,
+ nosave?: boolean
+): boolean {
+ if (value < 10) {
+ value = 10;
+ }
+ if (value > 90) {
+ value = 90;
+ }
+
+ if (
+ !isConfigValueValid("max line width", value, ConfigSchemas.TapeMarginSchema)
+ ) {
+ return false;
+ }
+
+ config.tapeMargin = value;
+
+ saveToLocalStorage("tapeMargin", nosave);
+ ConfigEvent.dispatch("tapeMargin", config.tapeMargin, nosave);
+
+ // trigger a resize event to update the layout - handled in ui.ts:108
+ $(window).trigger("resize");
+
+ return true;
+}
+
export function setHideExtraLetters(val: boolean, nosave?: boolean): boolean {
if (!isConfigValueValidBoolean("hide extra letters", val)) return false;
@@ -2053,6 +2081,7 @@ export async function apply(
setLazyMode(configObj.lazyMode, true);
setShowAverage(configObj.showAverage, true);
setTapeMode(configObj.tapeMode, true);
+ setTapeMargin(configObj.tapeMargin, true);
ConfigEvent.dispatch(
"configApplied",
diff --git a/frontend/src/ts/constants/default-config.ts b/frontend/src/ts/constants/default-config.ts
index bbfcf729101a..df27f01f8719 100644
--- a/frontend/src/ts/constants/default-config.ts
+++ b/frontend/src/ts/constants/default-config.ts
@@ -101,6 +101,7 @@ const obj = {
lazyMode: false,
showAverage: "off",
tapeMode: "off",
+ tapeMargin: 10,
maxLineWidth: 0,
} as Config;
diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts
index 2f1cf76b0847..539e0035837d 100644
--- a/frontend/src/ts/pages/settings.ts
+++ b/frontend/src/ts/pages/settings.ts
@@ -379,6 +379,11 @@ async function initGroups(): Promise {
UpdateConfig.setTapeMode,
"button"
) as SettingsGroup;
+ groups["tapeMargin"] = new SettingsGroup(
+ "tapeMargin",
+ UpdateConfig.setTapeMargin,
+ "button"
+ ) as SettingsGroup;
groups["timerOpacity"] = new SettingsGroup(
"timerOpacity",
UpdateConfig.setTimerOpacity,
@@ -694,6 +699,10 @@ async function fillSettingsPage(): Promise {
Config.customLayoutfluid.replace(/#/g, " ")
);
+ $(".pageSettings .section[data-config-name='tapeMargin'] input").val(
+ Config.tapeMargin
+ );
+
setEventDisabled(true);
if (!groupsInitialized) {
await initGroups();
@@ -1159,6 +1168,42 @@ $(
}
});
+$(
+ ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton button.save"
+).on("click", () => {
+ const didConfigSave = UpdateConfig.setTapeMargin(
+ parseFloat(
+ $(
+ ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton input"
+ ).val() as string
+ )
+ );
+ if (didConfigSave) {
+ Notifications.add("Saved", 1, {
+ duration: 1,
+ });
+ }
+});
+
+$(
+ ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton input"
+).on("keypress", (e) => {
+ if (e.key === "Enter") {
+ const didConfigSave = UpdateConfig.setTapeMargin(
+ parseFloat(
+ $(
+ ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton input"
+ ).val() as string
+ )
+ );
+ if (didConfigSave) {
+ Notifications.add("Saved", 1, {
+ duration: 1,
+ });
+ }
+ }
+});
+
$(
".pageSettings .section[data-config-name='maxLineWidth'] .inputAndButton button.save"
).on("click", () => {
diff --git a/frontend/src/ts/test/caret.ts b/frontend/src/ts/test/caret.ts
index 40108f7d39aa..ad34b3df3a48 100644
--- a/frontend/src/ts/test/caret.ts
+++ b/frontend/src/ts/test/caret.ts
@@ -106,8 +106,10 @@ function getTargetPositionLeft(
} else {
const wordsWrapperWidth =
$(document.querySelector("#wordsWrapper") as HTMLElement).width() ?? 0;
+ const tapeMargin = wordsWrapperWidth * (Config.tapeMargin / 100);
+
result =
- wordsWrapperWidth / 2 -
+ tapeMargin -
(fullWidthCaret && isLanguageRightToLeft ? fullWidthCaretWidth : 0);
if (Config.tapeMode === "word" && inputLen > 0) {
diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts
index 6fe21d6f4afb..e18c02056c96 100644
--- a/frontend/src/ts/test/test-ui.ts
+++ b/frontend/src/ts/test/test-ui.ts
@@ -184,13 +184,14 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
if (eventKey === "tapeMode" && !nosave) {
if (eventValue === "off") {
$("#words").css("margin-left", "unset");
- $("#liveStatsMini").css("display", "").css("justify-content", "");
} else {
scrollTape();
- $("#liveStatsMini")
- .css("display", "flex")
- .css("justify-content", "center");
}
+ updateLiveStatsMargin();
+ }
+
+ if (eventKey === "tapeMargin" && !nosave) {
+ updateLiveStatsMargin();
}
if (typeof eventValue !== "boolean") return;
@@ -474,7 +475,7 @@ export async function updateWordsInputPosition(initial = false): Promise {
if (Config.tapeMode !== "off") {
el.style.top = targetTop + "px";
- el.style.left = "50%";
+ el.style.left = Config.tapeMargin + "%";
return;
}
@@ -949,7 +950,7 @@ export function scrollTape(): void {
.stop(true, false)
.animate(
{
- marginLeft: "50%",
+ marginLeft: Config.tapeMargin + "%",
},
SlowTimer.get() ? 0 : 125
);
@@ -1000,7 +1001,9 @@ export function scrollTape(): void {
}
}
}
- const newMargin = wordsWrapperWidth / 2 - (fullWordsWidth + currentWordWidth);
+
+ const tapeMargin = wordsWrapperWidth * (Config.tapeMargin / 100);
+ const newMargin = tapeMargin - (fullWordsWidth + currentWordWidth);
if (Config.smoothLineScroll) {
$("#words")
.stop(true, false)
@@ -1478,6 +1481,20 @@ function updateWordsWidth(): void {
}
}
+function updateLiveStatsMargin(): void {
+ if (Config.tapeMode === "off") {
+ $("#liveStatsMini").css({
+ "justify-content": "start",
+ "margin-left": "unset",
+ });
+ } else {
+ $("#liveStatsMini").css({
+ "justify-content": "center",
+ "margin-left": Config.tapeMargin + "%",
+ });
+ }
+}
+
function updateLiveStatsOpacity(value: TimerOpacity): void {
$("#barTimerProgress").css("opacity", parseFloat(value as string));
$("#liveStatsTextTop").css("opacity", parseFloat(value as string));
diff --git a/packages/contracts/src/schemas/configs.ts b/packages/contracts/src/schemas/configs.ts
index 0a31ba41671f..00cda9be96d8 100644
--- a/packages/contracts/src/schemas/configs.ts
+++ b/packages/contracts/src/schemas/configs.ts
@@ -158,6 +158,9 @@ export type HighlightMode = z.infer;
export const TapeModeSchema = z.enum(["off", "letter", "word"]);
export type TapeMode = z.infer;
+export const TapeMarginSchema = z.number().min(10).max(90);
+export type TapeMargin = z.infer;
+
export const TypingSpeedUnitSchema = z.enum([
"wpm",
"cpm",
@@ -354,6 +357,7 @@ export const ConfigSchema = z
minWpmCustomSpeed: MinWpmCustomSpeedSchema,
highlightMode: HighlightModeSchema,
tapeMode: TapeModeSchema,
+ tapeMargin: TapeMarginSchema,
typingSpeedUnit: TypingSpeedUnitSchema,
ads: AdsSchema,
hideExtraLetters: z.boolean(),