diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html
index 4bc31f9c639b..c8b1a24ed56d 100644
--- a/frontend/src/html/pages/settings.html
+++ b/frontend/src/html/pages/settings.html
@@ -994,6 +994,22 @@
+
diff --git a/frontend/src/styles/animations.scss b/frontend/src/styles/animations.scss
index d826c7659e35..326d5825f10a 100644
--- a/frontend/src/styles/animations.scss
+++ b/frontend/src/styles/animations.scss
@@ -19,6 +19,15 @@
}
}
+@keyframes fadeOut {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+
@keyframes caretFlashSmooth {
0%,
100% {
@@ -130,3 +139,37 @@
background-position: 0% 50%;
}
}
+
+@keyframes typedWordsFadeIn {
+ 0% {
+ opacity: 0;
+ }
+ 75% {
+ opacity: 0.4;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+@keyframes typedWordsToDust {
+ 0% {
+ transform: scale(1);
+ color: var(--current-color);
+ }
+ 10% {
+ /* transform: scale(1); */
+ }
+ 15% {
+ transform: scale(1);
+ color: var(--c-dot);
+ }
+ 80% {
+ /* transform: scale(0.5); */
+ color: var(--c-dot);
+ }
+ 100% {
+ transform: scale(0.4);
+ color: transparent;
+ }
+}
diff --git a/frontend/src/styles/test.scss b/frontend/src/styles/test.scss
index d0064f2c7c24..6e71b255aa83 100644
--- a/frontend/src/styles/test.scss
+++ b/frontend/src/styles/test.scss
@@ -396,6 +396,8 @@
--untyped-letter-color: var(--sub-color);
--incorrect-letter-color: var(--colorful-error-color);
--extra-letter-color: var(--colorful-error-extra-color);
+ --c-dot: var(--main-color);
+ --c-dot--error: var(--colorful-error-color);
&.blind .word.error {
border-bottom: 2px solid transparent;
}
@@ -518,6 +520,62 @@
}
}
}
+
+ &.typed-words-hide {
+ .word.typed {
+ opacity: 0;
+ }
+ }
+
+ &.typed-words-fade {
+ .word.typed {
+ animation: fadeOut 250ms ease-in 1 forwards;
+ }
+ }
+
+ &.typed-words-dots:not(.withLigatures) {
+ /* transform already typed letters into appropriately colored dots */
+
+ .word letter {
+ position: relative;
+ &::after {
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 1em;
+ aspect-ratio: 1;
+ border-radius: 50%;
+ opacity: 0;
+ }
+ }
+ .typed letter {
+ color: var(--bg-color);
+ animation: typedWordsToDust 200ms ease-out 0ms 1 forwards !important;
+ &::after {
+ animation: typedWordsFadeIn 100ms ease-in 100ms 1 forwards;
+ background: var(--c-dot);
+ }
+ }
+ &:not(.blind) {
+ .word letter.incorrect::after {
+ background: var(--c-dot--error);
+ }
+ }
+
+ @media (prefers-reduced-motion) {
+ .typed letter {
+ animation: none !important;
+ transform: scale(0.4);
+ color: transparent;
+ &::after {
+ animation: none !important;
+ opacity: 1;
+ }
+ }
+ }
+ }
}
.word {
@@ -1416,6 +1474,9 @@
rgba(0, 0, 0, 0) 99%
);
}
+
+ --c-dot: var(--text-color);
+ --c-dot--error: var(--error-color);
}
#memoryTimer,
#layoutfluidTimer {
diff --git a/frontend/src/ts/commandline/commandline-metadata.ts b/frontend/src/ts/commandline/commandline-metadata.ts
index c8e4ab6a5400..44ecf78a0219 100644
--- a/frontend/src/ts/commandline/commandline-metadata.ts
+++ b/frontend/src/ts/commandline/commandline-metadata.ts
@@ -546,6 +546,11 @@ export const commandlineConfigMetadata: CommandlineConfigMetadataObject = {
options: "fromSchema",
},
},
+ typedWords: {
+ subgroup: {
+ options: "fromSchema",
+ },
+ },
tapeMode: {
subgroup: {
options: "fromSchema",
diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts
index 2aab6e96cd60..862a5c4662f6 100644
--- a/frontend/src/ts/commandline/lists.ts
+++ b/frontend/src/ts/commandline/lists.ts
@@ -157,6 +157,7 @@ export const commands: CommandsSubgroup = {
"timerColor",
"timerOpacity",
"highlightMode",
+ "typedWords",
"tapeMode",
"tapeMargin",
diff --git a/frontend/src/ts/config-metadata.ts b/frontend/src/ts/config-metadata.ts
index a9794c851c81..ae7457d345a9 100644
--- a/frontend/src/ts/config-metadata.ts
+++ b/frontend/src/ts/config-metadata.ts
@@ -558,6 +558,12 @@ export const configMetadata: ConfigMetadataObject = {
changeRequiresRestart: false,
group: "appearance",
},
+ typedWords: {
+ icon: "fa-eye",
+ displayString: "typed words",
+ changeRequiresRestart: false,
+ group: "appearance",
+ },
tapeMode: {
icon: "fa-tape",
triggerResize: true,
diff --git a/frontend/src/ts/constants/default-config.ts b/frontend/src/ts/constants/default-config.ts
index 8c14cc3bb76f..81433e246102 100644
--- a/frontend/src/ts/constants/default-config.ts
+++ b/frontend/src/ts/constants/default-config.ts
@@ -77,6 +77,7 @@ const obj: Config = {
minWpm: "off",
minWpmCustomSpeed: 100,
highlightMode: "letter",
+ typedWords: "show",
typingSpeedUnit: "wpm",
ads: "result",
hideExtraLetters: false,
diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts
index 201a3b52f10c..23fc839d7b3f 100644
--- a/frontend/src/ts/pages/settings.ts
+++ b/frontend/src/ts/pages/settings.ts
@@ -206,6 +206,7 @@ async function initGroups(): Promise {
groups["liveAccStyle"] = new SettingsGroup("liveAccStyle", "button");
groups["liveBurstStyle"] = new SettingsGroup("liveBurstStyle", "button");
groups["highlightMode"] = new SettingsGroup("highlightMode", "button");
+ groups["typedWords"] = new SettingsGroup("typedWords", "button");
groups["tapeMode"] = new SettingsGroup("tapeMode", "button");
groups["tapeMargin"] = new SettingsGroup("tapeMargin", "input", {
validation: { schema: true, inputValueConvert: Number },
diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts
index efbe30db3784..8bd96127be5e 100644
--- a/frontend/src/ts/test/test-ui.ts
+++ b/frontend/src/ts/test/test-ui.ts
@@ -450,10 +450,17 @@ function updateWordWrapperClasses(): void {
const existing =
wordsEl?.className
.split(/\s+/)
- .filter((className) => !className.startsWith("highlight-")) ?? [];
+ .filter(
+ (className) =>
+ !className.startsWith("highlight-") &&
+ !className.startsWith("typed-words-"),
+ ) ?? [];
if (Config.highlightMode !== null) {
existing.push("highlight-" + Config.highlightMode.replaceAll("_", "-"));
}
+ if (Config.typedWords !== null) {
+ existing.push("typed-words-" + Config.typedWords.replaceAll("_", "-"));
+ }
wordsEl.className = existing.join(" ");
updateWordsWidth();
@@ -2033,6 +2040,7 @@ ConfigEvent.subscribe(({ key, newValue }) => {
if (
[
"highlightMode",
+ "typedWords",
"blindMode",
"indicateTypos",
"tapeMode",
diff --git a/frontend/static/themes/dark_note.css b/frontend/static/themes/dark_note.css
index 5b156b33ebbf..8205587ab5bf 100644
--- a/frontend/static/themes/dark_note.css
+++ b/frontend/static/themes/dark_note.css
@@ -88,103 +88,3 @@ body::before {
text-decoration-color: var(--error-color);
text-decoration-thickness: 2px;
}
-
-/* transform already typed letters into appropriately colored dots */
-
-/* setting variables to the appropriate colors */
-#wordsWrapper {
- --c-dot: var(--text-color);
- --c-dot--error: var(--error-color);
-}
-
-.colorfulMode {
- --c-dot: var(--main-color);
- --c-dot--error: var(--colorful-error-color);
-}
-
-#words .typed letter {
- animation: darkNoteToDust 200ms ease-out 0ms 1 forwards !important;
-}
-#words .typed letter::after {
- animation: darkNoteFadeIn 100ms ease-in 100ms 1 forwards;
-}
-
-.word letter {
- position: relative;
-}
-
-#words:not(.withLigatures) .word letter::after {
- content: "";
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: 1em;
- height: 1em;
- border-radius: 50%;
- opacity: 0;
-}
-
-#wordsWrapper .typed letter::after {
- background: var(--c-dot);
-}
-
-#wordsWrapper #words:not(.blind) .word letter.incorrect::after {
- background: var(--c-dot--error);
-}
-
-/* hide hint during dot transformation */
-hint {
- transition: 300ms ease opacity;
- opacity: 1;
-}
-
-#wordsWrapper .word:not(.active) letter.incorrect hint {
- opacity: 0;
-}
-
-@media (prefers-reduced-motion) {
- #words .typed letter {
- animation: none !important;
- transform: scale(0.4);
- color: transparent;
- }
- #words .typed letter::after {
- animation: none !important;
- opacity: 1;
- }
-}
-
-@keyframes darkNoteFadeIn {
- 0% {
- opacity: 0;
- }
- 75% {
- opacity: 0.4;
- }
- 100% {
- opacity: 1;
- }
-}
-
-@keyframes darkNoteToDust {
- 0% {
- transform: scale(1);
- color: var(--current-color);
- }
- 10% {
- /* transform: scale(1); */
- }
- 15% {
- transform: scale(1);
- color: var(--c-dot);
- }
- 80% {
- /* transform: scale(0.5); */
- color: var(--c-dot);
- }
- 100% {
- transform: scale(0.4);
- color: transparent;
- }
-}
diff --git a/packages/schemas/src/configs.ts b/packages/schemas/src/configs.ts
index d6e8e1d510cf..d02f7fae56aa 100644
--- a/packages/schemas/src/configs.ts
+++ b/packages/schemas/src/configs.ts
@@ -182,6 +182,9 @@ export const HighlightModeSchema = z.enum([
]);
export type HighlightMode = z.infer;
+export const TypedWordsSchema = z.enum(["show", "hide", "fade", "dots"]);
+export type TypedWords = z.infer;
+
export const TapeModeSchema = z.enum(["off", "letter", "word"]);
export type TapeMode = z.infer;
@@ -441,6 +444,7 @@ export const ConfigSchema = z
timerColor: TimerColorSchema,
timerOpacity: TimerOpacitySchema,
highlightMode: HighlightModeSchema,
+ typedWords: TypedWordsSchema,
tapeMode: TapeModeSchema,
tapeMargin: TapeMarginSchema,
smoothLineScroll: z.boolean(),