From 02e92d0f3b191eec7916f1759e48a1a2b5d766b0 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 22 Sep 2025 13:10:50 +0200 Subject: [PATCH 01/22] impr: add debounced animation frame util !nuf --- .../src/ts/utils/debounced-animation-frame.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 frontend/src/ts/utils/debounced-animation-frame.ts diff --git a/frontend/src/ts/utils/debounced-animation-frame.ts b/frontend/src/ts/utils/debounced-animation-frame.ts new file mode 100644 index 000000000..afb60c3e9 --- /dev/null +++ b/frontend/src/ts/utils/debounced-animation-frame.ts @@ -0,0 +1,21 @@ +const pendingFrames = new Map(); + +export function requestDebouncedAnimationFrame( + frameId: string, + callback: () => void +): void { + cancelIfPending(frameId); + const frame = requestAnimationFrame(() => { + pendingFrames.delete(frameId); + callback(); + }); + pendingFrames.set(frameId, frame); +} + +function cancelIfPending(frameId: string): void { + const pending = pendingFrames.get(frameId); + if (pending !== undefined) { + cancelAnimationFrame(pending); + pendingFrames.delete(frameId); + } +} From 1bf03e8dc26b0742a59032b1bd9b9ce28dccb019 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 22 Sep 2025 13:13:01 +0200 Subject: [PATCH 02/22] refactor: use new util function --- frontend/src/ts/test/focus.ts | 41 ++++++++++++++++----------------- frontend/src/ts/test/test-ui.ts | 9 +++----- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/frontend/src/ts/test/focus.ts b/frontend/src/ts/test/focus.ts index 35b655bca..7b9842e2f 100644 --- a/frontend/src/ts/test/focus.ts +++ b/frontend/src/ts/test/focus.ts @@ -4,6 +4,7 @@ import * as LiveBurst from "./live-burst"; import * as LiveAcc from "./live-acc"; import * as TimerProgress from "./timer-progress"; import * as PageTransition from "../states/page-transition"; +import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame"; const unfocusPx = 3; let state = false; @@ -41,13 +42,13 @@ function initializeCache(): void { // with cursor is a special case that is only used on the initial page load // to avoid the cursor being invisible and confusing the user export function set(value: boolean, withCursor = false): void { - initializeCache(); + requestDebouncedAnimationFrame("focus.set", () => { + initializeCache(); - if (value && !state) { - state = true; + if (value && !state) { + state = true; - // batch DOM operations for better performance - requestAnimationFrame(() => { + // batch DOM operations for better performance if (cache.focus) { for (const el of cache.focus) { el.classList.add("focus"); @@ -58,17 +59,15 @@ export function set(value: boolean, withCursor = false): void { el.style.cursor = "none"; } } - }); - Caret.stopAnimation(); - LiveSpeed.show(); - LiveBurst.show(); - LiveAcc.show(); - TimerProgress.show(); - } else if (!value && state) { - state = false; + Caret.stopAnimation(); + LiveSpeed.show(); + LiveBurst.show(); + LiveAcc.show(); + TimerProgress.show(); + } else if (!value && state) { + state = false; - requestAnimationFrame(() => { if (cache.focus) { for (const el of cache.focus) { el.classList.remove("focus"); @@ -79,14 +78,14 @@ export function set(value: boolean, withCursor = false): void { el.style.cursor = ""; } } - }); - Caret.startAnimation(); - LiveSpeed.hide(); - LiveBurst.hide(); - LiveAcc.hide(); - TimerProgress.hide(); - } + Caret.startAnimation(); + LiveSpeed.hide(); + LiveBurst.hide(); + LiveAcc.hide(); + TimerProgress.hide(); + } + }); } $(document).on("mousemove", function (event) { diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 0e3f478b2..778c5711f 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -23,6 +23,7 @@ import { TimerColor, TimerOpacity } from "@monkeytype/schemas/configs"; import { convertRemToPixels } from "../utils/numbers"; import { findSingleActiveFunboxWithFunction } from "./funbox/list"; import * as TestState from "./test-state"; +import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame"; const debouncedZipfCheck = debounce(250, async () => { const supports = await JSONData.checkIfLanguageSupportsZipf(Config.language); @@ -491,13 +492,9 @@ export function appendEmptyWordElement( `
` ); } -let updateWordsInputPositionAnimationFrameId: null | number = null; + export function updateWordsInputPosition(): void { - if (updateWordsInputPositionAnimationFrameId !== null) { - cancelAnimationFrame(updateWordsInputPositionAnimationFrameId); - } - updateWordsInputPositionAnimationFrameId = requestAnimationFrame(() => { - updateWordsInputPositionAnimationFrameId = null; + requestDebouncedAnimationFrame("test-ui.updateWordsInputPosition", () => { if (ActivePage.get() !== "test") return; const isTestRightToLeft = TestState.isDirectionReversed ? !TestState.isLanguageRightToLeft From c08572bd898ca1fa8bdec276d1de7c4fb43178f1 Mon Sep 17 00:00:00 2001 From: TheFrenchTechMan <95764063+TheFrenchTechMan@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:40:09 +0200 Subject: [PATCH 03/22] impr(quotes): Add English quotes (@TheFrenchTechMan) (#6961) ### Description ### Checks - [X] Adding quotes? - [X] Make sure to include translations for the quotes in the description (or another comment) so we can verify their content. - [ ] Adding a language? - Make sure to follow the [languages documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/LANGUAGES.md) - [ ] Add language to `packages/schemas/src/languages.ts` - [ ] Add language to exactly one group in `frontend/src/ts/constants/languages.ts` - [ ] Add language json file to `frontend/static/languages` - [ ] Adding a theme? - Make sure to follow the [themes documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/THEMES.md) - [ ] Add theme to `packages/schemas/src/themes.ts` - [ ] Add theme to `frontend/src/ts/constants/themes.ts` - [ ] Add theme css file to `frontend/static/themes` - [ ] Add some screenshot of the theme, especially with different test settings (colorful, flip colors) to your pull request - [ ] Adding a layout? - [ ] Make sure to follow the [layouts documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/LAYOUTS.md) - [ ] Add layout to `packages/schemas/src/layouts.ts` - [ ] Add layout json file to `frontend/static/layouts` - [ ] Adding a font? - Make sure to follow the [themes documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/FONTS.md) - [ ] Add font file to `frontend/static/webfonts` - [ ] Add font to `packages/schemas/src/fonts.ts` - [ ] Add font to `frontend/src/ts/constants/fonts.ts` - [ ] Check if any open issues are related to this PR; if so, be sure to tag them below. - [ ] Make sure the PR title follows the Conventional Commits standard. (https://www.conventionalcommits.org for more info) - [ ] Make sure to include your GitHub username prefixed with @ inside parentheses at the end of the PR title. Closes # --- frontend/static/quotes/english.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/frontend/static/quotes/english.json b/frontend/static/quotes/english.json index 4074941f4..ff9e6d8c4 100644 --- a/frontend/static/quotes/english.json +++ b/frontend/static/quotes/english.json @@ -38799,6 +38799,30 @@ "source": "James Clear, Atomic Habits: An Easy & Proven Way to Build Good Habits & Break Bad Ones", "id": 7679, "length": 381 + }, + { + "text": "A traveler from beyond the stars, Bard is an agent of serendipity who fights to maintain a balance where life can endure the indifference of chaos. Many Runeterrans sing songs that ponder his extraordinary nature, yet they all agree that the cosmic vagabond is drawn to artifacts of great magical power. Surrounded by a jubilant choir of helpful spirit meeps, it is impossible to mistake his actions as malevolent, as Bard always serves the greater good... in his own odd way.", + "source": "League of Legends", + "id": 7680, + "length": 476 + }, + { + "text": "The eccentric Professor Cecil B. Heimerdinger is one of the most innovative and esteemed inventors the world has ever known. As the longest serving member of the Council of Piltover, he saw the best and the worst of the city's unending desire for progress. Nonetheless, this brilliant scientist and teacher will always remain dedicated to using his unconventional devices to improve the lives of others.", + "source": "League of Legends", + "id": 7681, + "length": 403 + }, + { + "text": "An enthusiastic master of dark sorcery, Veigar has embraced powers that few mortals dare approach. As a free-spirited inhabitant of Bandle City, he longed to push beyond the limitations of yordle magic, and turned instead to arcane texts that had been hidden away for thousands of years. Now a stubborn creature with an endless fascination for the mysteries of the universe, Veigar is often underestimated by others - but even though he believes himself truly evil, he possesses an inner morality that leads some to question his deeper motivations.", + "source": "League of Legends", + "id": 7682, + "length": 548 + }, + { + "text": "Xerath is an Ascended Magus of ancient Shurima, a being of arcane energy writhing in the broken shards of a magical sarcophagus. For millennia, he was trapped beneath the desert sands, but the rise of Shurima freed him from his ancient prison. Driven insane with power, he now seeks to take what he believes is rightfully his and replace the upstart civilizations of the world with one fashioned in his image.", + "source": "League of Legends", + "id": 7683, + "length": 409 } ] } From 18c465e82da201448d8341306acd65be7f85ff9a Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 23 Sep 2025 16:41:06 +0200 Subject: [PATCH 04/22] impr: add validation for preset names (@fehmer) (#6969) --- frontend/src/ts/elements/input-validation.ts | 10 ++++++ frontend/src/ts/modals/edit-preset.ts | 35 ++++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/frontend/src/ts/elements/input-validation.ts b/frontend/src/ts/elements/input-validation.ts index 21499aed6..73a595679 100644 --- a/frontend/src/ts/elements/input-validation.ts +++ b/frontend/src/ts/elements/input-validation.ts @@ -145,6 +145,7 @@ export type ValidationOptions = (T extends string export type ValidatedHtmlInputElement = HTMLInputElement & { isValid: () => boolean | undefined; + setValue: (val: string | null) => void; }; /** * adds an 'InputIndicator` to the given `inputElement` and updates its status depending on the given validation @@ -197,6 +198,15 @@ export function validateWithIndicator( const result = inputElement as ValidatedHtmlInputElement; result.isValid = () => isValid; + result.setValue = (val: string | null) => { + inputElement.value = val ?? ""; + if (val === null) { + isValid = undefined; + indicator.hide(); + } else { + inputElement.dispatchEvent(new Event("input")); + } + }; return result; } diff --git a/frontend/src/ts/modals/edit-preset.ts b/frontend/src/ts/modals/edit-preset.ts index 29b213bf1..6f9cdd91a 100644 --- a/frontend/src/ts/modals/edit-preset.ts +++ b/frontend/src/ts/modals/edit-preset.ts @@ -6,7 +6,11 @@ import * as Settings from "../pages/settings"; import * as Notifications from "../elements/notifications"; import * as ConnectionState from "../states/connection"; import AnimatedModal from "../utils/animated-modal"; -import { PresetType, PresetTypeSchema } from "@monkeytype/schemas/presets"; +import { + PresetNameSchema, + PresetType, + PresetTypeSchema, +} from "@monkeytype/schemas/presets"; import { getPreset } from "../controllers/preset-controller"; import { ConfigGroupName, @@ -17,6 +21,10 @@ import { } from "@monkeytype/schemas/configs"; import { getDefaultConfig } from "../constants/default-config"; import { SnapshotPreset } from "../constants/default-snapshot"; +import { + ValidatedHtmlInputElement, + validateWithIndicator, +} from "../elements/input-validation"; const state = { presetType: "full" as PresetType, @@ -26,6 +34,8 @@ const state = { setPresetToCurrent: false, }; +let presetNameEl: ValidatedHtmlInputElement | null = null; + export function show(action: string, id?: string, name?: string): void { if (!ConnectionState.get()) { Notifications.add("You are offline", 0, { @@ -39,11 +49,22 @@ export function show(action: string, id?: string, name?: string): void { beforeAnimation: async () => { $("#editPresetModal .modal .text").addClass("hidden"); addCheckBoxes(); + if (!presetNameEl) { + presetNameEl = validateWithIndicator( + document.querySelector( + "#editPresetModal .modal input" + ) as HTMLInputElement, + { + schema: PresetNameSchema, + } + ); + } if (action === "add") { $("#editPresetModal .modal").attr("data-action", "add"); $("#editPresetModal .modal .popupTitle").html("Add new preset"); $("#editPresetModal .modal .submit").html(`add`); - $("#editPresetModal .modal input").val(""); + presetNameEl?.setValue(null); + presetNameEl?.parentElement?.classList.remove("hidden"); $("#editPresetModal .modal input").removeClass("hidden"); $( "#editPresetModal .modal label.changePresetToCurrentCheckbox" @@ -57,7 +78,9 @@ export function show(action: string, id?: string, name?: string): void { $("#editPresetModal .modal").attr("data-preset-id", id); $("#editPresetModal .modal .popupTitle").html("Edit preset"); $("#editPresetModal .modal .submit").html(`save`); - $("#editPresetModal .modal input").val(name); + presetNameEl?.setValue(name); + presetNameEl?.parentElement?.classList.remove("hidden"); + $("#editPresetModal .modal input").removeClass("hidden"); $( "#editPresetModal .modal label.changePresetToCurrentCheckbox" @@ -85,6 +108,7 @@ export function show(action: string, id?: string, name?: string): void { $("#editPresetModal .modal .inputs").addClass("hidden"); $("#editPresetModal .modal .presetType").addClass("hidden"); $("#editPresetModal .modal .presetNameTitle").addClass("hidden"); + presetNameEl?.parentElement?.classList.add("hidden"); } updateUI(); }, @@ -238,6 +262,11 @@ async function apply(): Promise { return; } + if (presetNameEl?.isValid() === false) { + Notifications.add("Preset name is not valid", 0); + return; + } + hide(); Loader.show(); From 94006b7e1f4105677f23eb35c6d4a2e80b597d9e Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 23 Sep 2025 10:18:52 +0200 Subject: [PATCH 05/22] chore: add reminder --- frontend/src/ts/utils/url-handler.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/ts/utils/url-handler.ts b/frontend/src/ts/utils/url-handler.ts index d197cde2a..8f719cd57 100644 --- a/frontend/src/ts/utils/url-handler.ts +++ b/frontend/src/ts/utils/url-handler.ts @@ -163,6 +163,9 @@ export function loadTestSettingsFromUrl(getOverride?: string): void { const getValue = Misc.findGetParameter("testSettings", getOverride); if (getValue === null) return; + // if the encoding structure or method ever changes, make sure to support the old data format + // otherwise eiko will be sad + const { data: de, error } = tryCatchSync(() => parseJsonWithSchema(decompressFromURI(getValue) ?? "", TestSettingsSchema) ); From df81d7ce97ee326ddd62a69eeba1e88ebd800593 Mon Sep 17 00:00:00 2001 From: Lucas Bourneuf Date: Tue, 23 Sep 2025 16:56:24 +0200 Subject: [PATCH 06/22] impr(quotes): add 9 new english quotes (@aluriak) (#6935) Adding Carl Sagan, internet culture, various well-known IT-related citations, and 2 citations from my favorite writer of late. Co-authored-by: Lucas Bourneuf Co-authored-by: Miodec --- frontend/static/quotes/english.json | 54 +++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/frontend/static/quotes/english.json b/frontend/static/quotes/english.json index ff9e6d8c4..09d88924b 100644 --- a/frontend/static/quotes/english.json +++ b/frontend/static/quotes/english.json @@ -38823,6 +38823,60 @@ "source": "League of Legends", "id": 7683, "length": 409 + }, + { + "text": "The whole idea of what happens when you read a book, I find absolutely stunning. Here’s some product of a tree, little black squiggles on it, you open it up, and inside your head is the voice of someone speaking, who may have been dead 3000 years, and there he is talking directly to you, what a magical thing that is.", + "source": "Carl Sagan", + "length": 318, + "id": 7684 + }, + { + "text": "Mathematics, in an earlier view, is the science of space and quantity; in a later view, it is the science of pattern and deductive structure.", + "source": "Philip J. Davis, Reuben Hersh - The mathematical experience (1981)", + "length": 141, + "id": 7685 + }, + { + "text": "His Holiness the Flying Spaghetti Monster is Eternal, without beginning and without end, and with a whole tangled mess in the middle.", + "source": "Evangelical Pastafarian church - The Loose Canon, Second Announcement Regarding Canonical Belief", + "length": 133, + "id": 7686 + }, + { + "text": "We are stuck with technology when all we really want is just stuff that works. How do you recognize something that is still technology? A good clue is if it comes with a manual.", + "source": "Douglas Adams", + "length": 177, + "id": 7687 + }, + { + "text": "Nothing disturbs me more than the glorification of stupidity.", + "source": "Carl Sagan", + "length": 61, + "id": 7688 + }, + { + "text": "“Then how,” Dex said, “how does the idea of maybe being meaningless sit well with you?” Mosscap considered. “Because I know that no matter what, I’m wonderful.”", + "source": "Becky Chambers — Monk and Robot", + "length": 160, + "id": 7689 + }, + { + "text": "I can wait for the galaxy outside to get a little kinder.", + "source": "Becky Chambers — The Long Way to a Small, Angry Planet", + "length": 57, + "id": 7690 + }, + { + "text": "That’s such an incredibly organic bias, the idea that your squishy physical existence is some sort of pinnacle that all programs aspire to.", + "source": "Becky Chambers — The Long Way to a Small, Angry Planet", + "length": 139, + "id": 7691 + }, + { + "text": "There are only two hard things in Computer Science: cache invalidation, naming things, and off-by-one error.", + "source": "Phil Karlton", + "length": 108, + "id": 7692 } ] } From 3d819115eb1399bb5ba36357f7be648eff6642cd Mon Sep 17 00:00:00 2001 From: Lucas Bourneuf Date: Tue, 23 Sep 2025 16:56:48 +0200 Subject: [PATCH 07/22] impr(quotes): add 4 new french quotes (@aluriak) (#6934) Few quotes. - *The man of science knows full well that only science has been able, over the centuries, to bring him the time clock and the automatic parking meter, without which no earthly happiness is possible.* (Pierre Desproges) - *A pre-revolutionary situation erupts when those at the top can no longer rule, those at the bottom no longer want to be ruled, and those in the middle switch sides to join those at the bottom.* (Lenine) - *There are decades when nothing happens, and weeks when decades happen.* (idem) - *There are no supreme saviors Neither God, nor Caesar, nor Tribune, Producers, let us save ourselves Let us decree our common salvation.* (l'internationale, a famous french song) Co-authored-by: Lucas Bourneuf --- frontend/static/quotes/french.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/frontend/static/quotes/french.json b/frontend/static/quotes/french.json index 10c0be059..72bc59e6e 100644 --- a/frontend/static/quotes/french.json +++ b/frontend/static/quotes/french.json @@ -6624,6 +6624,30 @@ "source": "Ken Liu - La ménagerie de papier", "length": 153, "id": 1125 + }, + { + "text": "L’homme de science le sait bien, lui, que seule la science a pu, au fil des siècles, lui apporter l’horloge pointeuse et le parcmètre automatique sans lesquels il n’est pas de bonheur terrestre possible.", + "source": "Pierre Desproges - Vivons heureux en attendant la mort", + "length": 203, + "id": 1126 + }, + { + "text": "Une situation prérévolutionnaire éclate lorsque ceux d’en haut ne peuvent plus, ceux d’en bas ne veulent plus, et ceux du milieu basculent avec ceux d’en bas.", + "source": "Lénine", + "length": 158, + "id": 1127 + }, + { + "text": "Il y a des décennies où rien ne se passe et des semaines où des décennies se produisent.", + "source": "Lénine", + "length": 88, + "id": 1128 + }, + { + "text": "Il n'est pas de sauveurs suprêmes\nNi Dieu, ni César, ni Tribun,\nProducteurs, sauvons-nous nous-mêmes\nDécrétons le salut commun.", + "source": "L'internationale", + "length": 127, + "id": 1129 } ] } From a56107667779c97fb768c97e9b7f72739662e2e0 Mon Sep 17 00:00:00 2001 From: Gabriel <162917831+thesomewhatyou@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:04:32 -0400 Subject: [PATCH 08/22] impr(quotes): add English quotes (@thesomewhatyou) (#6964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description Just added a few English quotes from some of my favorite movies. It wasn't much, but I would love to see these quotes in Monkeytype. ### Checks - [✓] Adding quotes? - [N/A] Make sure to include translations for the quotes in the description (or another comment) so we can verify their content. --------- Co-authored-by: Miodec --- frontend/static/quotes/english.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/frontend/static/quotes/english.json b/frontend/static/quotes/english.json index 09d88924b..e5556da3a 100644 --- a/frontend/static/quotes/english.json +++ b/frontend/static/quotes/english.json @@ -38877,6 +38877,30 @@ "source": "Phil Karlton", "length": 108, "id": 7692 + }, + { + "text": "You gotta put your past behind you. Look, kid, bad things happen, and you can't do anything about it. Right? Wrong!", + "source": "The Lion King (1994)", + "id": 7693, + "length": 115 + }, + { + "text": "One often meets his destiny on the road he takes to avoid it. Your mind is like this water my friend; when it is agitated, it is hard to see, but if you allow it to settle, the answer becomes clear.", + "source": "Kung Fu Panda (2008)", + "id": 7694, + "length": 198 + }, + { + "text": "Yesterday is history, tomorrow is a mystery, but today is a gift. That is why it is called the present.", + "source": "Kung Fu Panda (2008)", + "id": 7695, + "length": 103 + }, + { + "text": "Your story may not have such a happy beginning, but that doesn't make you who you are. It is the rest of your story. Who you choose to be.", + "source": "Kung Fu Panda 2 (2011)", + "id": 7696, + "length": 138 } ] } From a277bdd0d3026c945e24ee9cb22bc9dd1dd0a7d9 Mon Sep 17 00:00:00 2001 From: Leonabcd123 <156839416+Leonabcd123@users.noreply.github.com> Date: Tue, 23 Sep 2025 18:06:03 +0300 Subject: [PATCH 09/22] docs: fixed links on contributing.md not working from the main page (@Leonabcd123) (#6982) ### Description Used absolute paths instead of relative to make sure that the links in the contributing.md file always work. Closes #6981 Edit: just found #6848 which seemed to try and do the same thing, they just didn't get the right solution, because they still used relative paths. --- docs/CONTRIBUTING.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 5007270aa..a65217e23 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -17,9 +17,9 @@ When contributing to Monkeytype, it's good to know our best practices, tips, and ## How to Contribute -We have two separate contribution guides based on what you're looking to contribute. If you're simply looking to help us augment our language or quotes data, please refer to [CONTRIBUTING_BASIC.md](./CONTRIBUTING_BASIC.md). This guide will go over how to do so easily and without the need to set up a local development server. +We have two separate contribution guides based on what you're looking to contribute. If you're simply looking to help us augment our language or quotes data, please refer to [CONTRIBUTING_BASIC.md](/docs/CONTRIBUTING_BASIC.md). This guide will go over how to do so easily and without the need to set up a local development server. -If you're looking to make deeper code changes that affect functionality, or will require screenshots of the changes, please refer to [CONTRIBUTING_ADVANCED.md](./CONTRIBUTING_ADVANCED.md). +If you're looking to make deeper code changes that affect functionality, or will require screenshots of the changes, please refer to [CONTRIBUTING_ADVANCED.md](/docs/CONTRIBUTING_ADVANCED.md). ## Standards and Guidelines @@ -55,7 +55,7 @@ Before submitting a theme make sure... - your theme has been added to the `_list` file and the `textColor` property is the theme's main color - your theme is clear and readable with both `flip test colors` and `colorful mode` enabled and disabled -If you want to contribute themes but don't know how, check [THEMES.md](./THEMES.md) +If you want to contribute themes but don't know how, check [THEMES.md](/docs/THEMES.md) ### Language Guidelines @@ -64,7 +64,7 @@ If you want to contribute themes but don't know how, check [THEMES.md](./THEMES. - Be sure to add your language to the `_list` and `_groups` files - Make sure the number of words in the file corresponds to the file name (for example: `languageName.json` is 200 words, `languageName_1k.json` is 1000 words, and so on) -If you want to contribute languages but don't know how, check [LANGUAGES.md](./LANGUAGES.md) +If you want to contribute languages but don't know how, check [LANGUAGES.md](/docs/LANGUAGES.md) ### Quote Guidelines @@ -78,11 +78,11 @@ If you want to contribute languages but don't know how, check [LANGUAGES.md](./L - Remember to name your pull request properly. For example, if you are adding new quotes for the language `French`, your pull request should be named `impr(quotes): add French quotes`. -If you want to contribute quotes but don't know how, check [QUOTES.md](./QUOTES.md) +If you want to contribute quotes but don't know how, check [QUOTES.md](/docs/QUOTES.md) ### Layout Guidelines -If you want to contribute layouts but don't know how, check [LAYOUTS.md](./LAYOUTS.md) +If you want to contribute layouts but don't know how, check [LAYOUTS.md](/docs/LAYOUTS.md) ## Questions From 37418d0cf1a442bf20918ed6a99a4868ed1a7590 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 23 Sep 2025 17:06:28 +0200 Subject: [PATCH 10/22] docs: update link to redis install guide (@fehmer) (#6984) fixes #6983 --- docs/CONTRIBUTING_ADVANCED.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CONTRIBUTING_ADVANCED.md b/docs/CONTRIBUTING_ADVANCED.md index 8a15265e2..4abee2bba 100644 --- a/docs/CONTRIBUTING_ADVANCED.md +++ b/docs/CONTRIBUTING_ADVANCED.md @@ -111,7 +111,7 @@ Follow these steps if you want to work on anything involving the database/accoun | Manual | Docker (recommended) | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -|
  1. Install [MongoDB Community Edition](https://docs.mongodb.com/manual/administration/install-community/)
  2. Install [Redis](https://redis.io/docs/install/install-redis/)
  3. Make sure both are running
|
  1. Install [Docker](http://www.docker.io/gettingstarted/#h_installation) on your machine
  2. Run `npm run docker-db-only` from the `./backend` directory
| +|
  1. Install [MongoDB Community Edition](https://docs.mongodb.com/manual/administration/install-community/)
  2. Install [Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-stack/)
  3. Make sure both are running
|
  1. Install [Docker](http://www.docker.io/gettingstarted/#h_installation) on your machine
  2. Run `npm run docker-db-only` from the `./backend` directory
| 3. (Optional) Install [MongoDB-compass](https://www.mongodb.com/try/download/compass?tck=docs_compass). This tool can be used to see and manipulate your database visually. - To connect, type `mongodb://localhost:27017` in the connection string box and press connect. The Monkeytype database will be created and shown after the server is started. From a846d192d19db670a0fd02c04fd2d84b472fa78b Mon Sep 17 00:00:00 2001 From: Eduardo Paul <56889692+eduardopaul@users.noreply.github.com> Date: Tue, 23 Sep 2025 12:07:38 -0300 Subject: [PATCH 11/22] impr(quotes): add Portuguese quotes (@eduardopaul) (#6985) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These are some beautiful quotes in Portuguese by Machado de Assis. > Entendeu o céu que devia regar com as suas lágrimas o solo da formosa Petrópolis. - "The sky understood that it should water the soil of the beautiful Petrópolis with its tears." > Olhos de ressaca? Vá, de ressaca. É o que me dá ideia daquela feição nova. Traziam não sei que fluido misterioso e enérgico, uma força que arrastava para dentro, como a vaga que se retira da praia, nos dias de ressaca. - "Eyes of a riptide? Yes, of a riptide. That’s the feeling I get from that new expression. They carried some mysterious and energetic fluid, a force that pulled inward, like the wave that draws back from the shore after a storm." > Apesar de generosa, é exigente, e quer da parte dos seus afilhados algum esforço próprio. A fortuna não é Danaide. Quando vê que um tonel esgota a água que se lhe põe dentro vai levar os seus cântaros a outra parte. - "Despite being generous, she is demanding and expects her godchildren to make some effort of their own. Fortune isn’t a Danaid. When she sees that a barrel runs out of water, she will take her jugs elsewhere." --- frontend/static/quotes/portuguese.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/frontend/static/quotes/portuguese.json b/frontend/static/quotes/portuguese.json index 25ecc76e1..36238acf3 100644 --- a/frontend/static/quotes/portuguese.json +++ b/frontend/static/quotes/portuguese.json @@ -636,6 +636,24 @@ "source": "Tabacaria, Fernando Pessoa", "length": 88, "id": 106 + }, + { + "text": "Entendeu o céu que devia regar com as suas lágrimas o solo da formosa Petrópolis.", + "source": "Linha Reta e Linha Curva, Machado de Assis", + "length": 81, + "id": 107 + }, + { + "text": "Olhos de ressaca? Vá, de ressaca. É o que me dá ideia daquela feição nova. Traziam não sei que fluido misterioso e enérgico, uma força que arrastava para dentro, como a vaga que se retira da praia, nos dias de ressaca.", + "source": "Dom Casmurro, Machado de Assis", + "length": 218, + "id": 108 + }, + { + "text": "Apesar de generosa, é exigente, e quer da parte dos seus afilhados algum esforço próprio. A fortuna não é Danaide. Quando vê que um tonel esgota a água que se lhe põe dentro vai levar os seus cântaros a outra parte.", + "source": "Luís Soares, Machado de Assis", + "length": 215, + "id": 109 } ] } From 5da071320938e1ea8773f8df6558ce42a5323156 Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 24 Sep 2025 12:25:48 +0200 Subject: [PATCH 12/22] perf: speed up escapeHTML by ~3x --- frontend/src/ts/utils/misc.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 976cf5d7d..5920e7694 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -164,18 +164,22 @@ export function escapeRegExp(str: string): string { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } -export function escapeHTML(str: string): string { +export function escapeHTML(str: T): T { if (str === null || str === undefined) { return str; } - str = str - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - return str; + const escapeMap: Record = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "/": "/", + "`": "`", + }; + + return str.replace(/[&<>"'/`]/g, (char) => escapeMap[char] as string) as T; } export function isUsernameValid(name: string): boolean { From 0d7d68f8b4b17e86551fe3f16f541f4b2c4305b2 Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 24 Sep 2025 12:30:58 +0200 Subject: [PATCH 13/22] test: add unit tests for escapeHTML --- frontend/__tests__/utils/misc.spec.ts | 42 ++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/frontend/__tests__/utils/misc.spec.ts b/frontend/__tests__/utils/misc.spec.ts index fc0a29781..893282946 100644 --- a/frontend/__tests__/utils/misc.spec.ts +++ b/frontend/__tests__/utils/misc.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { getErrorMessage, isObject } from "../../src/ts/utils/misc"; +import { getErrorMessage, isObject, escapeHTML } from "../../src/ts/utils/misc"; import { getLanguageDisplayString, removeLanguageSize, @@ -123,6 +123,46 @@ describe("misc.ts", () => { }); }); + describe("escapeHTML", () => { + it("should escape HTML characters correctly", () => { + const tests = [ + { + input: "hello world", + expected: "hello world", + }, + { + input: "", + expected: "<script>alert('xss')</script>", + }, + { + input: 'Hello "world" & friends', + expected: "Hello "world" & friends", + }, + { + input: "Click `here` to continue", + expected: "Click `here` to continue", + }, + { + input: null, + expected: null, + }, + { + input: undefined, + expected: undefined, + }, + { + input: "", + expected: "", + }, + ]; + + tests.forEach((test) => { + const result = escapeHTML(test.input); + expect(result).toBe(test.expected); + }); + }); + }); + describe("getErrorMesssage", () => { it("should correctly get the error message", () => { const tests = [ From 505049338ffd11fc2e61173bcf360d6c2bd3dfa3 Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 24 Sep 2025 12:31:55 +0200 Subject: [PATCH 14/22] chore(about): escape supporters and contributors --- frontend/src/ts/pages/about.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/ts/pages/about.ts b/frontend/src/ts/pages/about.ts index 9f1c880c9..0656a87f9 100644 --- a/frontend/src/ts/pages/about.ts +++ b/frontend/src/ts/pages/about.ts @@ -151,7 +151,7 @@ async function fill(): Promise { const supportersEl = document.querySelector(".pageAbout .supporters"); let supportersHTML = ""; for (const supporter of supporters ?? []) { - supportersHTML += `
${supporter}
`; + supportersHTML += `
${Misc.escapeHTML(supporter)}
`; } if (supportersEl) { supportersEl.innerHTML = supportersHTML; @@ -160,7 +160,7 @@ async function fill(): Promise { const contributorsEl = document.querySelector(".pageAbout .contributors"); let contributorsHTML = ""; for (const contributor of contributors ?? []) { - contributorsHTML += `
${contributor}
`; + contributorsHTML += `
${Misc.escapeHTML(contributor)}
`; } if (contributorsEl) { contributorsEl.innerHTML = contributorsHTML; From 9e5e4831a845a6f2c626a402226a60652520245e Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 24 Sep 2025 14:35:46 +0200 Subject: [PATCH 15/22] impr(input-validation): add trigger validation function !nuf --- frontend/src/ts/elements/input-validation.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/ts/elements/input-validation.ts b/frontend/src/ts/elements/input-validation.ts index 73a595679..c07c61760 100644 --- a/frontend/src/ts/elements/input-validation.ts +++ b/frontend/src/ts/elements/input-validation.ts @@ -146,6 +146,7 @@ export type ValidationOptions = (T extends string export type ValidatedHtmlInputElement = HTMLInputElement & { isValid: () => boolean | undefined; setValue: (val: string | null) => void; + triggerValidation: () => void; }; /** * adds an 'InputIndicator` to the given `inputElement` and updates its status depending on the given validation @@ -207,6 +208,9 @@ export function validateWithIndicator( inputElement.dispatchEvent(new Event("input")); } }; + result.triggerValidation = () => { + inputElement.dispatchEvent(new Event("input")); + }; return result; } From 42f6a16c6255d66cbb2be279131454866d98f2c9 Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 24 Sep 2025 14:45:13 +0200 Subject: [PATCH 16/22] impr(modes-notice): escape custom text name for safe HTML rendering !nuf --- frontend/src/ts/elements/modes-notice.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/ts/elements/modes-notice.ts b/frontend/src/ts/elements/modes-notice.ts index ebd21d05e..aa2888565 100644 --- a/frontend/src/ts/elements/modes-notice.ts +++ b/frontend/src/ts/elements/modes-notice.ts @@ -10,6 +10,7 @@ import * as CustomTextState from "../states/custom-text-name"; import { getLanguageDisplayString } from "../utils/strings"; import Format from "../utils/format"; import { getActiveFunboxNames } from "../test/funbox/list"; +import { escapeHTML } from "../utils/misc"; ConfigEvent.subscribe((eventKey) => { const configKeys: ConfigEvent.ConfigEventKey[] = [ @@ -79,7 +80,9 @@ export async function update(): Promise { const isLong = CustomTextState.isCustomTextLong(); if (Config.mode === "custom" && customTextName !== "" && isLong) { $(".pageTest #testModesNotice").append( - `
${customTextName} (shift + enter to save progress)
` + `
${escapeHTML( + customTextName + )} (shift + enter to save progress)
` ); } From f025b121cbe437e29de432b4aa72e0de22c755b7 Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 24 Sep 2025 14:46:19 +0200 Subject: [PATCH 17/22] impr(save custom text modal): add validation for custom text name input --- frontend/src/ts/modals/save-custom-text.ts | 98 ++++++++-------------- 1 file changed, 37 insertions(+), 61 deletions(-) diff --git a/frontend/src/ts/modals/save-custom-text.ts b/frontend/src/ts/modals/save-custom-text.ts index d63e77f94..af3b78f9f 100644 --- a/frontend/src/ts/modals/save-custom-text.ts +++ b/frontend/src/ts/modals/save-custom-text.ts @@ -1,11 +1,13 @@ import * as CustomText from "../test/custom-text"; import * as Notifications from "../elements/notifications"; import * as CustomTextState from "../states/custom-text-name"; -import { InputIndicator } from "../elements/input-indicator"; -import { debounce } from "throttle-debounce"; import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; +import { validateWithIndicator } from "../elements/input-validation"; +import { z } from "zod"; -let indicator: InputIndicator | undefined; +type IncomingData = { + text: string[]; +}; type State = { textToSave: string[]; @@ -15,6 +17,35 @@ const state: State = { textToSave: [], }; +const validatedInput = validateWithIndicator( + $("#saveCustomTextModal .textName")[0] as HTMLInputElement, + { + debounceDelay: 500, + schema: z + .string() + .min(1) + .max(32) + .regex(/^[\w\s-]+$/, { + message: + "Name can only contain letters, numbers, spaces, underscores and hyphens", + }), + isValid: async (value) => { + const checkbox = $("#saveCustomTextModal .isLongText").prop( + "checked" + ) as boolean; + const names = CustomText.getCustomTextNames(checkbox); + return !names.includes(value) ? true : "Duplicate name"; + }, + callback: (result) => { + if (result.status === "success") { + $("#saveCustomTextModal button.save").prop("disabled", false); + } else { + $("#saveCustomTextModal button.save").prop("disabled", true); + } + }, + } +); + export async function show(options: ShowOptions): Promise { state.textToSave = []; void modal.show({ @@ -28,10 +59,6 @@ export async function show(options: ShowOptions): Promise { }); } -function hide(): void { - void modal.hide(); -} - function save(): boolean { const name = $("#saveCustomTextModal .textName").val() as string; const checkbox = $("#saveCustomTextModal .isLongText").prop( @@ -59,69 +86,18 @@ function save(): boolean { } } -function updateIndicatorAndButton(): void { - const val = $("#saveCustomTextModal .textName").val() as string; - const checkbox = $("#saveCustomTextModal .isLongText").prop( - "checked" - ) as boolean; - - if (!val) { - indicator?.hide(); - $("#saveCustomTextModal button.save").prop("disabled", true); - } else { - const names = CustomText.getCustomTextNames(checkbox); - if (names.includes(val)) { - indicator?.show("unavailable"); - $("#saveCustomTextModal button.save").prop("disabled", true); - } else { - indicator?.show("available"); - $("#saveCustomTextModal button.save").prop("disabled", false); - } - } -} - -const updateInputAndButtonDebounced = debounce(500, updateIndicatorAndButton); - async function setup(modalEl: HTMLElement): Promise { - indicator = new InputIndicator($("#saveCustomTextModal .textName"), { - available: { - icon: "fa-check", - level: 1, - }, - unavailable: { - icon: "fa-times", - level: -1, - }, - loading: { - icon: "fa-circle-notch", - spinIcon: true, - level: 0, - }, - }); modalEl.addEventListener("submit", (e) => { e.preventDefault(); - if (save()) hide(); - }); - modalEl.querySelector(".textName")?.addEventListener("input", (e) => { - const val = (e.target as HTMLInputElement).value; - if (val.length > 0) { - indicator?.show("loading"); - updateInputAndButtonDebounced(); + if (validatedInput.isValid() === true && save()) { + void modal.hide(); } }); modalEl.querySelector(".isLongText")?.addEventListener("input", (e) => { - const val = (e.target as HTMLInputElement).value; - if (val.length > 0) { - indicator?.show("loading"); - updateInputAndButtonDebounced(); - } + validatedInput.triggerValidation(); }); } -type IncomingData = { - text: string[]; -}; - const modal = new AnimatedModal({ dialogId: "saveCustomTextModal", setup, From de847fc314cfc39852fa374f53844477f92cb72e Mon Sep 17 00:00:00 2001 From: Jack Date: Wed, 24 Sep 2025 16:00:37 +0200 Subject: [PATCH 18/22] refactor(input validation): rework getting current validation status (@miodec) (#6988) Rename to diffrentiate between the predicate `isValid` and the element `isValid` Return `ValidationResult` instead of just a boolean Update usage to the new format Move config specific `resetIfEmpty` to the `ConfigInputOptions` type --- frontend/src/ts/elements/input-validation.ts | 18 ++++++++++-------- frontend/src/ts/modals/edit-preset.ts | 2 +- frontend/src/ts/modals/save-custom-text.ts | 2 +- frontend/src/ts/pages/login.ts | 6 ++++-- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/frontend/src/ts/elements/input-validation.ts b/frontend/src/ts/elements/input-validation.ts index c07c61760..a1971bd62 100644 --- a/frontend/src/ts/elements/input-validation.ts +++ b/frontend/src/ts/elements/input-validation.ts @@ -32,9 +32,6 @@ export type Validation = { /** custom debounce delay for `isValid` call. defaults to 100 */ debounceDelay?: number; - - /** Resets the value to the current config if empty */ - resetIfEmpty?: false; }; // oxlint-disable-next-line no-explicit-any @@ -144,7 +141,7 @@ export type ValidationOptions = (T extends string }; export type ValidatedHtmlInputElement = HTMLInputElement & { - isValid: () => boolean | undefined; + getValidationResult: () => ValidationResult; setValue: (val: string | null) => void; triggerValidation: () => void; }; @@ -178,9 +175,11 @@ export function validateWithIndicator( }, }); - let isValid: boolean | undefined = undefined; + let currentStatus: ValidationResult = { + status: "checking", + }; const callback = (result: ValidationResult): void => { - isValid = result.status === "success" || result.status === "warning"; + currentStatus = result; if (result.status === "failed" || result.status === "warning") { indicator.show(result.status, result.errorMessage); } else { @@ -198,11 +197,12 @@ export function validateWithIndicator( inputElement.addEventListener("input", handler); const result = inputElement as ValidatedHtmlInputElement; - result.isValid = () => isValid; + result.getValidationResult = () => { + return currentStatus; + }; result.setValue = (val: string | null) => { inputElement.value = val ?? ""; if (val === null) { - isValid = undefined; indicator.hide(); } else { inputElement.dispatchEvent(new Event("input")); @@ -227,6 +227,8 @@ export type ConfigInputOptions = { schema: boolean; /** optional callback is called for each change of the validation result */ validationCallback?: (result: ValidationResult) => void; + /** Resets the value to the current config if empty */ + resetIfEmpty?: false; }; }; diff --git a/frontend/src/ts/modals/edit-preset.ts b/frontend/src/ts/modals/edit-preset.ts index 6f9cdd91a..cf0e6a258 100644 --- a/frontend/src/ts/modals/edit-preset.ts +++ b/frontend/src/ts/modals/edit-preset.ts @@ -262,7 +262,7 @@ async function apply(): Promise { return; } - if (presetNameEl?.isValid() === false) { + if (presetNameEl?.getValidationResult().status === "failed") { Notifications.add("Preset name is not valid", 0); return; } diff --git a/frontend/src/ts/modals/save-custom-text.ts b/frontend/src/ts/modals/save-custom-text.ts index af3b78f9f..6bf9cd8d6 100644 --- a/frontend/src/ts/modals/save-custom-text.ts +++ b/frontend/src/ts/modals/save-custom-text.ts @@ -89,7 +89,7 @@ function save(): boolean { async function setup(modalEl: HTMLElement): Promise { modalEl.addEventListener("submit", (e) => { e.preventDefault(); - if (validatedInput.isValid() === true && save()) { + if (validatedInput.getValidationResult().status === "success" && save()) { void modal.hide(); } }); diff --git a/frontend/src/ts/pages/login.ts b/frontend/src/ts/pages/login.ts index 632b6f594..715c9c4d4 100644 --- a/frontend/src/ts/pages/login.ts +++ b/frontend/src/ts/pages/login.ts @@ -170,7 +170,8 @@ validateWithIndicator(emailVerifyInputEl, { debounceDelay: 0, callback: (result) => { registerForm.email = - emailInputEl.isValid() && result.status === "success" + emailInputEl.getValidationResult().status === "success" && + result.status === "success" ? emailInputEl.value : undefined; updateSignupButton(); @@ -204,7 +205,8 @@ validateWithIndicator(passwordVerifyInputEl, { debounceDelay: 0, callback: (result) => { registerForm.password = - passwordInputEl.isValid() && result.status === "success" + passwordInputEl.getValidationResult().status === "success" && + result.status === "success" ? passwordInputEl.value : undefined; updateSignupButton(); From b903abd863a7864c3b42cc4eac64619ac61f49f6 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Thu, 25 Sep 2025 12:31:35 +0200 Subject: [PATCH 19/22] chore: add search links to bug report template (@fehmer) (#6990) --- .github/ISSUE_TEMPLATE/bug_report.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index baa8d3381..6309b4e56 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -20,10 +20,10 @@ body: - type: checkboxes attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue already exists for the bug you encountered. + label: Is there an [existing issue](https://github.com/monkeytypegame/monkeytype/issues?q=is%3Aissue) for this? + description: Please [search](https://github.com/monkeytypegame/monkeytype/issues?q=is%3Aissue) to see if an issue already exists for the bug you encountered. options: - - label: I have searched the existing issues + - label: I have [searched](https://github.com/monkeytypegame/monkeytype/issues?q=is%3Aissue) the existing open and closed issues required: true - type: markdown From b6ef5c0d8d70efc5c5270a86dcbfe98485c7a51d Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Thu, 25 Sep 2025 12:46:13 +0200 Subject: [PATCH 20/22] ci: run asset validation on any changes not just json files (@fehmer) (#6992) --- .github/workflows/monkey-ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/monkey-ci.yml b/.github/workflows/monkey-ci.yml index 21252fdf4..60ed2fcaf 100644 --- a/.github/workflows/monkey-ci.yml +++ b/.github/workflows/monkey-ci.yml @@ -239,14 +239,14 @@ jobs: with: filters: | languages: - - 'frontend/static/languages/*.json' + - 'frontend/static/languages/**' quotes: - - 'frontend/static/quotes/*.json' + - 'frontend/static/quotes/**' others: - - 'frontend/static/layouts/*.json' + - 'frontend/static/layouts/**' - 'frontend/static/themes/**' - 'frontend/static/webfonts/**' - - 'frontend/static/challenges/*.json' + - 'frontend/static/challenges/**' - name: Set up Node.js uses: actions/setup-node@v4 From 42dcf16ad30fac768beefd46df8736735801fcea Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Thu, 25 Sep 2025 12:53:15 +0200 Subject: [PATCH 21/22] chore: remove search link not working in issue template (@fehmer) (#6993) --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 6309b4e56..ba477f28a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -20,7 +20,7 @@ body: - type: checkboxes attributes: - label: Is there an [existing issue](https://github.com/monkeytypegame/monkeytype/issues?q=is%3Aissue) for this? + label: Is there an existing issue for this? description: Please [search](https://github.com/monkeytypegame/monkeytype/issues?q=is%3Aissue) to see if an issue already exists for the bug you encountered. options: - label: I have [searched](https://github.com/monkeytypegame/monkeytype/issues?q=is%3Aissue) the existing open and closed issues From a15d84e0ce4f20f18df27b1c59192af1e518a278 Mon Sep 17 00:00:00 2001 From: MasterRon <89069355+MasterRon@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:47:17 +0700 Subject: [PATCH 22/22] feat(layout): add gust layout (@MasterRon) (#6991) ### Description ### Checks - [ ] Adding quotes? - [ ] Make sure to include translations for the quotes in the description (or another comment) so we can verify their content. - [ ] Adding a language? - Make sure to follow the [languages documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/LANGUAGES.md) - [ ] Add language to `packages/schemas/src/languages.ts` - [ ] Add language to exactly one group in `frontend/src/ts/constants/languages.ts` - [ ] Add language json file to `frontend/static/languages` - [ ] Adding a theme? - Make sure to follow the [themes documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/THEMES.md) - [ ] Add theme to `packages/schemas/src/themes.ts` - [ ] Add theme to `frontend/src/ts/constants/themes.ts` - [ ] Add theme css file to `frontend/static/themes` - [ ] Add some screenshot of the theme, especially with different test settings (colorful, flip colors) to your pull request - [ ] Adding a layout? - [ ] Make sure to follow the [layouts documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/LAYOUTS.md) - [ ] Add layout to `packages/schemas/src/layouts.ts` - [ ] Add layout json file to `frontend/static/layouts` - [ ] Adding a font? - Make sure to follow the [themes documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/FONTS.md) - [ ] Add font file to `frontend/static/webfonts` - [ ] Add font to `packages/schemas/src/fonts.ts` - [ ] Add font to `frontend/src/ts/constants/fonts.ts` - [ ] Check if any open issues are related to this PR; if so, be sure to tag them below. - [ ] Make sure the PR title follows the Conventional Commits standard. (https://www.conventionalcommits.org for more info) - [ ] Make sure to include your GitHub username prefixed with @ inside parentheses at the end of the PR title. Closes # --- frontend/static/layouts/gust.json | 62 +++++++++++++++++++++++++++++++ packages/schemas/src/layouts.ts | 1 + 2 files changed, 63 insertions(+) create mode 100644 frontend/static/layouts/gust.json diff --git a/frontend/static/layouts/gust.json b/frontend/static/layouts/gust.json new file mode 100644 index 000000000..26b2bdc57 --- /dev/null +++ b/frontend/static/layouts/gust.json @@ -0,0 +1,62 @@ +{ + "keymapShowTopRow": false, + "type": "ansi", + "keys": { + "row1": [ + ["`", "~"], + ["1", "!"], + ["2", "@"], + ["3", "#"], + ["4", "$"], + ["5", "%"], + ["6", "^"], + ["7", "&"], + ["8", "*"], + ["9", "("], + ["0", ")"], + ["[", "{"], + ["]", "}"] + ], + "row2": [ + [";", ":"], + ["u", "U"], + ["o", "O"], + ["f", "F"], + ["j", "J"], + ["q", "Q"], + ["k", "K"], + ["l", "L"], + ["r", "R"], + ["v", "V"], + ["/", "?"], + ["=", "+"], + ["\\", "|"] + ], + "row3": [ + ["e", "E"], + ["i", "I"], + ["a", "A"], + ["c", "C"], + ["y", "Y"], + ["d", "D"], + ["h", "H"], + ["t", "T"], + ["n", "N"], + ["s", "S"], + ["-", "_"] + ], + "row4": [ + [",", "<"], + [".", ">"], + ["p", "P"], + ["g", "G"], + ["'", "\""], + ["b", "B"], + ["m", "M"], + ["w", "W"], + ["x", "X"], + ["z", "Z"] + ], + "row5": [[" "]] + } +} diff --git a/packages/schemas/src/layouts.ts b/packages/schemas/src/layouts.ts index 89466b784..08bcc604b 100644 --- a/packages/schemas/src/layouts.ts +++ b/packages/schemas/src/layouts.ts @@ -182,6 +182,7 @@ export const LayoutNameSchema = z.enum( "estonian", "stronk", "dhorf", + "gust", "recurva", "seht-drai", "ints",