diff --git a/frontend/package.json b/frontend/package.json index 2d4482084da5..96d05b93712d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,8 +15,6 @@ "madge": " madge --circular --extensions ts ./src", "start": "vite preview --port 3000", "dev": "vite dev", - "deploy-live": "npm run check-assets && npm run build && firebase deploy -P live --only hosting", - "deploy-preview": "npm run check-assets && npm run build && firebase hosting:channel:deploy preview -P live --expires 2h", "test": "vitest run", "test-coverage": "vitest run --coverage", "dev-test": "concurrently --kill-others \"vite dev\" \"vitest\"", diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html index 8abbd84c2352..3640a70d7a07 100644 --- a/frontend/src/html/pages/settings.html +++ b/frontend/src/html/pages/settings.html @@ -568,6 +568,26 @@ +
+
+ + composition display + +
+
+ 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",