+ Change how composition is displayed. "off" will just underline the
+ letter if composition is active. "below" will show the composed
+ character below the test. "replace" will replace the letter in the test
+ with the composed character.
+
+
+
+
+
+
+
diff --git a/frontend/src/ts/commandline/commandline-metadata.ts b/frontend/src/ts/commandline/commandline-metadata.ts
index 1996583774e2..68ed146e49a4 100644
--- a/frontend/src/ts/commandline/commandline-metadata.ts
+++ b/frontend/src/ts/commandline/commandline-metadata.ts
@@ -323,6 +323,11 @@ export const commandlineConfigMetadata: CommandlineConfigMetadataObject = {
options: "fromSchema",
},
},
+ compositionDisplay: {
+ subgroup: {
+ options: "fromSchema",
+ },
+ },
hideExtraLetters: {
subgroup: {
options: "fromSchema",
diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts
index b303fbda4c31..dae0636b67cc 100644
--- a/frontend/src/ts/commandline/lists.ts
+++ b/frontend/src/ts/commandline/lists.ts
@@ -134,6 +134,7 @@ export const commands: CommandsSubgroup = {
confidenceModeCommand,
"quickEnd",
"indicateTypos",
+ "compositionDisplay",
"hideExtraLetters",
lazyModeCommand,
layoutCommand,
diff --git a/frontend/src/ts/config-metadata.ts b/frontend/src/ts/config-metadata.ts
index 1cd44f864e76..2dfa38ae0597 100644
--- a/frontend/src/ts/config-metadata.ts
+++ b/frontend/src/ts/config-metadata.ts
@@ -365,6 +365,11 @@ export const configMetadata: ConfigMetadataObject = {
displayString: "indicate typos",
changeRequiresRestart: false,
},
+ compositionDisplay: {
+ icon: "fa-language",
+ displayString: "composition display",
+ changeRequiresRestart: false,
+ },
hideExtraLetters: {
icon: "fa-eye-slash",
displayString: "hide extra letters",
diff --git a/frontend/src/ts/constants/default-config.ts b/frontend/src/ts/constants/default-config.ts
index 0fb967fb5c02..8c14cc3bb76f 100644
--- a/frontend/src/ts/constants/default-config.ts
+++ b/frontend/src/ts/constants/default-config.ts
@@ -42,6 +42,7 @@ const obj: Config = {
funbox: [],
confidenceMode: "off",
indicateTypos: "off",
+ compositionDisplay: "replace",
timerStyle: "mini",
liveSpeedStyle: "off",
liveAccStyle: "off",
diff --git a/frontend/src/ts/elements/composition-display.ts b/frontend/src/ts/elements/composition-display.ts
index 13b2a6b7de34..987bfef7f2fc 100644
--- a/frontend/src/ts/elements/composition-display.ts
+++ b/frontend/src/ts/elements/composition-display.ts
@@ -1,15 +1,7 @@
-import Config from "../config";
-
const compositionDisplay = document.getElementById(
"compositionDisplay",
) as HTMLElement;
-const languagesToShow = ["korean", "japanese", "chinese"];
-
-export function shouldShow(): boolean {
- return languagesToShow.some((lang) => Config.language.startsWith(lang));
-}
-
export function update(data: string): void {
compositionDisplay.innerText = data;
}
diff --git a/frontend/src/ts/input/handlers/keydown.ts b/frontend/src/ts/input/handlers/keydown.ts
index 4bbe80224e84..7f3daa7802f3 100644
--- a/frontend/src/ts/input/handlers/keydown.ts
+++ b/frontend/src/ts/input/handlers/keydown.ts
@@ -10,7 +10,6 @@ import * as JSONData from "../../utils/json-data";
import * as Notifications from "../../elements/notifications";
import * as KeyConverter from "../../utils/key-converter";
import * as ShiftTracker from "../../test/shift-tracker";
-import * as CompositionState from "../../states/composition";
import * as ManualRestart from "../../test/manual-restart-tracker";
import { canQuickRestart } from "../../utils/quick-restart";
import * as CustomText from "../../test/custom-text";
@@ -45,7 +44,7 @@ export async function handleTab(e: KeyboardEvent, now: number): Promise {
export async function handleEnter(
e: KeyboardEvent,
- now: number,
+ _now: number,
): Promise {
if (e.shiftKey) {
if (Config.mode === "zen") {
@@ -92,14 +91,6 @@ export async function handleEnter(
return;
}
}
- if (
- TestWords.hasNewline ||
- (Config.mode === "zen" && !CompositionState.getComposing())
- ) {
- await emulateInsertText({ data: "\n", now });
- e.preventDefault();
- return;
- }
}
export async function handleOppositeShift(event: KeyboardEvent): Promise {
diff --git a/frontend/src/ts/input/listeners/input.ts b/frontend/src/ts/input/listeners/input.ts
index f0ccea37bd6c..18539e00ee00 100644
--- a/frontend/src/ts/input/listeners/input.ts
+++ b/frontend/src/ts/input/listeners/input.ts
@@ -9,6 +9,11 @@ import {
import * as TestUI from "../../test/test-ui";
import { onBeforeInsertText } from "../handlers/before-insert-text";
import { onBeforeDelete } from "../handlers/before-delete";
+import * as TestInput from "../../test/test-input";
+import * as TestWords from "../../test/test-words";
+import * as CompositionState from "../../states/composition";
+import { activeWordIndex } from "../../test/test-state";
+import { areAllTestWordsGenerated } from "../../test/test-logic";
const inputEl = getInputElement();
@@ -114,6 +119,26 @@ inputEl.addEventListener("input", async (event) => {
inputType === "insertCompositionText" ||
inputType === "insertFromComposition"
) {
+ const allWordsTyped = activeWordIndex >= TestWords.words.length - 1;
+ const inputPlusComposition =
+ TestInput.input.current + (CompositionState.getData() ?? "");
+ const inputPlusCompositionIsCorrect =
+ TestWords.words.getCurrent() === inputPlusComposition;
+
+ // composition quick end
+ // if the user typed the entire word correctly but is still in composition
+ // dont wait for them to end the composition manually, just end the test
+ // by dispatching a compositionend which will trigger onInsertText
+ if (
+ areAllTestWordsGenerated() &&
+ allWordsTyped &&
+ inputPlusCompositionIsCorrect
+ ) {
+ getInputElement().dispatchEvent(
+ new CompositionEvent("compositionend", { data: event.data ?? "" }),
+ );
+ }
+
// in case the data is the same as the last one, just ignore it
if (getLastInsertCompositionTextData() !== event.data) {
setLastInsertCompositionTextData(event.data ?? "");
diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts
index 61368477b4c4..7e18d352716e 100644
--- a/frontend/src/ts/test/test-logic.ts
+++ b/frontend/src/ts/test/test-logic.ts
@@ -83,7 +83,6 @@ import * as Loader from "../elements/loader";
import * as TestInitFailed from "../elements/test-init-failed";
import { canQuickRestart } from "../utils/quick-restart";
import { animate } from "animejs";
-import * as CompositionDisplay from "../elements/composition-display";
import {
getInputElement,
isInputElementFocused,
@@ -329,13 +328,6 @@ export function restart(options = {} as RestartOptions): void {
getInputElement().style.left = "0";
setInputElementValue("");
- if (CompositionDisplay.shouldShow()) {
- CompositionDisplay.update(" ");
- CompositionDisplay.show();
- } else {
- CompositionDisplay.hide();
- }
-
Focus.set(false);
if (ActivePage.get() === "test") {
AdController.updateFooterAndVerticalAds(false);
@@ -381,8 +373,9 @@ export function restart(options = {} as RestartOptions): void {
if (isInputElementFocused()) OutOfFocus.hide();
TestUI.focusWords(true);
- const typingTestEl = document.querySelector("#typingTest") as HTMLElement;
+ TestUI.onTestRestart();
+ const typingTestEl = document.querySelector("#typingTest") as HTMLElement;
animate(typingTestEl, {
opacity: [0, 1],
onBegin: () => {
diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts
index f05bc21acf48..0b3f727c8174 100644
--- a/frontend/src/ts/test/test-ui.ts
+++ b/frontend/src/ts/test/test-ui.ts
@@ -49,6 +49,7 @@ import {
} from "../input/input-element";
import * as MonkeyPower from "../elements/monkey-power";
import * as SlowTimer from "../states/slow-timer";
+import * as CompositionDisplay from "../elements/composition-display";
const debouncedZipfCheck = debounce(250, async () => {
const supports = await JSONData.checkIfLanguageSupportsZipf(Config.language);
@@ -89,10 +90,9 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
debouncedZipfCheck();
}
if (eventKey === "fontSize") {
- $("#caret, #paceCaret, #liveStatsMini, #typingTest, #wordsInput").css(
- "fontSize",
- (eventValue as number) + "rem",
- );
+ $(
+ "#caret, #paceCaret, #liveStatsMini, #typingTest, #wordsInput, #compositionDisplay",
+ ).css("fontSize", (eventValue as number) + "rem");
if (!nosave) {
OutOfFocus.hide();
updateWordWrapperClasses();
@@ -880,11 +880,16 @@ export async function updateWordLetters({
let charToShow =
currentWordChars[input.length + i] ?? compositionChar;
- if (Config.indicateTypos === "replace") {
+ if (Config.compositionDisplay === "replace") {
charToShow = compositionChar === " " ? "_" : compositionChar;
}
- ret += `${charToShow}`;
+ let correctClass = "";
+ if (compositionChar === currentWordChars[input.length + i]) {
+ correctClass = "correct";
+ }
+
+ ret += `${charToShow}`;
}
for (
@@ -1914,6 +1919,15 @@ export function afterTestStart(): void {
TimerProgress.update();
}
+export function onTestRestart(): void {
+ if (Config.compositionDisplay === "below") {
+ CompositionDisplay.update(" ");
+ CompositionDisplay.show();
+ } else {
+ CompositionDisplay.hide();
+ }
+}
+
$(".pageTest #copyWordsListButton").on("click", async () => {
let words;
if (Config.mode === "zen") {
@@ -2038,4 +2052,12 @@ ConfigEvent.subscribe((key, value) => {
if (key === "showOutOfFocusWarning" && value === false) {
OutOfFocus.hide();
}
+ if (key === "compositionDisplay") {
+ if (value === "below") {
+ CompositionDisplay.update(" ");
+ CompositionDisplay.show();
+ } else {
+ CompositionDisplay.hide();
+ }
+ }
});
diff --git a/package.json b/package.json
index 697a694f783f..d59876550616 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
"start-fe": "turbo run start --filter @monkeytype/frontend",
"docker": "cd backend && npm run docker",
"audit-fe": "cd frontend && npm run audit",
+ "preview-fe": "monkeytype-release --preview-fe",
"release": "monkeytype-release",
"release-fe": "monkeytype-release --fe",
"release-be": "monkeytype-release --be",
diff --git a/packages/release/src/index.js b/packages/release/src/index.js
index dfbca0a07e97..05af3457684a 100755
--- a/packages/release/src/index.js
+++ b/packages/release/src/index.js
@@ -20,6 +20,7 @@ const isBackend = args.has("--be");
const isDryRun = args.has("--dry");
const noSyncCheck = args.has("--no-sync-check");
const hotfix = args.has("--hotfix");
+const previewFe = args.has("--preview-fe");
const PROJECT_ROOT = path.resolve(__dirname, "../../../");
@@ -252,6 +253,34 @@ const createGithubRelease = async (version, changelogContent) => {
};
const main = async () => {
+ if (previewFe) {
+ console.log(`Starting frontend preview deployment process...`);
+ installDependencies();
+ runProjectRootCommand(
+ "NODE_ENV=production npx turbo lint test check-assets build --filter @monkeytype/frontend --force",
+ );
+
+ const name = readlineSync.question(
+ "Enter preview channel name (default: preview): ",
+ );
+ const channelName = name.trim() || "preview";
+
+ const expirationTime = readlineSync.question(
+ "Enter expiration time (e.g., 2h, default: 1d): ",
+ );
+ const expires = expirationTime.trim() || "1d";
+
+ console.log(
+ `Deploying frontend preview to channel "${channelName}" with expiration "${expires}"...`,
+ );
+ const result = runProjectRootCommand(
+ `cd frontend && npx firebase hosting:channel:deploy ${channelName} -P live --expires ${expires}`,
+ );
+ console.log(result);
+ console.log("Frontend preview deployed successfully.");
+ process.exit(0);
+ }
+
console.log(`Starting ${hotfix ? "hotfix" : "release"} process...`);
if (!hotfix) checkBranchSync();
diff --git a/packages/schemas/src/configs.ts b/packages/schemas/src/configs.ts
index e7246854fee9..d8d1f54ab5cc 100644
--- a/packages/schemas/src/configs.ts
+++ b/packages/schemas/src/configs.ts
@@ -54,6 +54,9 @@ export type ConfidenceMode = z.infer;
export const IndicateTyposSchema = z.enum(["off", "below", "replace", "both"]);
export type IndicateTypos = z.infer;
+export const CompositionDisplaySchema = z.enum(["off", "below", "replace"]);
+export type CompositionDisplay = z.infer;
+
export const TimerStyleSchema = z.enum([
"off",
"bar",
@@ -408,6 +411,7 @@ export const ConfigSchema = z
confidenceMode: ConfidenceModeSchema,
quickEnd: z.boolean(),
indicateTypos: IndicateTyposSchema,
+ compositionDisplay: CompositionDisplaySchema,
hideExtraLetters: z.boolean(),
lazyMode: z.boolean(),
layout: LayoutSchema,
@@ -544,6 +548,7 @@ export const ConfigGroupsLiteral = {
confidenceMode: "input",
quickEnd: "input",
indicateTypos: "input",
+ compositionDisplay: "input",
hideExtraLetters: "input",
lazyMode: "input",
layout: "input",