From c6afb3c678a951e58b42c8a6d0f0852f0174f5e0 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Thu, 17 Apr 2025 03:55:31 +0200 Subject: [PATCH 01/50] feat: add quick favorite toggle to theme picker (via footer current theme button) (@byseif21) --- frontend/src/styles/commandline.scss | 29 +++++ frontend/src/styles/footer.scss | 26 +++++ frontend/src/ts/commandline/commandline.ts | 46 ++++++-- frontend/src/ts/commandline/lists.ts | 1 + frontend/src/ts/commandline/lists/themes.ts | 112 +++++++++++++------- frontend/src/ts/commandline/types.ts | 2 + frontend/src/ts/event-handlers/footer.ts | 62 +++++++++++ 7 files changed, 233 insertions(+), 45 deletions(-) diff --git a/frontend/src/styles/commandline.scss b/frontend/src/styles/commandline.scss index 63cc4e70b7a7..52a497a242e5 100644 --- a/frontend/src/styles/commandline.scss +++ b/frontend/src/styles/commandline.scss @@ -88,6 +88,7 @@ &.withThemeBubbles { grid-template-columns: auto 1fr auto; + position: relative; .themeBubbles { display: grid; grid-auto-flow: column; @@ -100,6 +101,34 @@ } } } + + .themeFavIcon { + position: absolute; + right: 5rem; + //top: 0.5rem; + color: var(--sub-color); + opacity: 0; + transition: opacity 0.125s; + cursor: pointer; + z-index: 999; + padding: 0.5rem; + pointer-events: auto; + + &.active { + .fas.fa-star { + margin-right: 0; + } + opacity: 1; + color: var(--text-color); + } + } + + &:hover .themeFavIcon { + .fas.fa-star { + margin-right: 0; + } + opacity: 1; + } } } } diff --git a/frontend/src/styles/footer.scss b/frontend/src/styles/footer.scss index c416c5d27b41..4b668f1270de 100644 --- a/frontend/src/styles/footer.scss +++ b/frontend/src/styles/footer.scss @@ -85,6 +85,32 @@ footer { } } + .current-theme { + position: relative; + + .favIcon { + position: absolute; + //right: -10px; + top: -5px; + font-size: 0.7rem; + color: var(--sub-color); + cursor: pointer; + transition: 0.125s; + + &:hover { + color: var(--text-color); + transform: scale(1.2); + } + + &.active { + color: var(--sub-color); + } + &.active:hover { + color: var(--text-color); + } + } + } + &.focus { .keyTips { opacity: 0 !important; diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index ec27b45a5f97..5a2bcd4aa7de 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -11,6 +11,8 @@ import * as ActivePage from "../states/active-page"; import { focusWords } from "../test/test-ui"; import * as Loader from "../elements/loader"; import { Command, CommandsSubgroup } from "./types"; +import * as JSONData from "../utils/json-data"; +import * as Misc from "../utils/misc"; type CommandlineMode = "search" | "input"; type InputModeParams = { @@ -108,6 +110,19 @@ export function show( activeCommand = null; Focus.set(false); CommandlineLists.setStackToDefault(); + + // Update themes list with current favorites status when commandline is opened + const themesPromise = JSONData.getThemesList(); + themesPromise + .then((themes) => { + CommandlineLists.updateThemesCommands(themes); + }) + .catch((e: unknown) => { + console.error( + Misc.createErrorMessage(e, "Failed to update themes commands") + ); + }); + updateInput(); await filterSubgroup(); await showCommands(); @@ -390,13 +405,24 @@ async function showCommands(): Promise { if (command.customData !== undefined) { if (command.id.startsWith("changeTheme")) { - html += `
+ html += `
${iconHTML}
${display}
-
-
-
-
+
+
+
+
+ ${command.html ?? ""}
`; } if (command.id.startsWith("changeFont")) { @@ -433,12 +459,20 @@ async function showCommands(): Promise { activeIndex = parseInt(command.getAttribute("data-index") ?? "0"); await updateActiveCommand(); }); - command.addEventListener("click", async () => { + command.addEventListener("click", async (e) => { const previous = activeIndex; activeIndex = parseInt(command.getAttribute("data-index") ?? "0"); if (previous !== activeIndex) { await updateActiveCommand(); } + const commandObj = (await getList()).filter((c) => c.found)[activeIndex]; + if (commandObj && commandObj.customHandler) { + const shouldProceed = commandObj.customHandler( + e as MouseEvent, + commandObj + ); + if (!shouldProceed) return; + } await runActiveCommand(); }); } diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index 1c3bc84bcc48..9f302a95e6ec 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -80,6 +80,7 @@ import LayoutsCommands, { } from "./lists/layouts"; import FunboxCommands from "./lists/funbox"; import ThemesCommands, { update as updateThemesCommands } from "./lists/themes"; +export { updateThemesCommands }; import LoadChallengeCommands, { update as updateLoadChallengeCommands, } from "./lists/load-challenge"; diff --git a/frontend/src/ts/commandline/lists/themes.ts b/frontend/src/ts/commandline/lists/themes.ts index ad3a05532058..907e25edc32b 100644 --- a/frontend/src/ts/commandline/lists/themes.ts +++ b/frontend/src/ts/commandline/lists/themes.ts @@ -3,6 +3,7 @@ import { capitalizeFirstLetterOfEachWord } from "../../utils/strings"; import * as ThemeController from "../../controllers/theme-controller"; import { Command, CommandsSubgroup } from "../types"; import { Theme } from "../../utils/json-data"; +import * as Notifications from "../../elements/notifications"; const subgroup: CommandsSubgroup = { title: "Theme...", @@ -19,50 +20,83 @@ const commands: Command[] = [ }, ]; +function createThemeCommand(theme: Theme, isFavorite: boolean): Command { + return { + id: "changeTheme" + capitalizeFirstLetterOfEachWord(theme.name), + display: theme.name.replace(/_/g, " "), + configValue: theme.name, + // customStyle: `color:${theme.mainColor};background:${theme.bgColor}`, + customData: { + mainColor: theme.mainColor, + bgColor: theme.bgColor, + subColor: theme.subColor, + textColor: theme.textColor, + isFavorite: isFavorite, + }, + hover: (): void => { + // previewTheme(theme.name); + ThemeController.preview(theme.name); + }, + exec: (): void => { + UpdateConfig.setTheme(theme.name); + }, + // custom HTML element for the favorite star + html: `
+ +
`, + // click handler for the favorite star + customHandler: (e: MouseEvent, command: Command): boolean => { + // click was on the favorite star? + const target = e.target as HTMLElement; + if (target.closest(".themeFavIcon")) { + e.stopPropagation(); + + const themeName = command.configValue as string; + + if (Config.favThemes.includes(themeName)) { + // remove from favorites + UpdateConfig.setFavThemes( + Config.favThemes.filter((t) => t !== themeName) + ); + Notifications.add("Removed from favorites", 1); + } else { + UpdateConfig.setFavThemes([...Config.favThemes, themeName]); + Notifications.add("Added to favorites", 1); + } + + // update the star icon immediately + const starIcon = target.closest(".themeFavIcon"); + if (starIcon) { + const isFavorite = Config.favThemes.includes(themeName); + if (isFavorite) { + starIcon.classList.add("active"); + } else { + starIcon.classList.remove("active"); + } + // update icon based on current state + const iconElement = starIcon.querySelector("i"); + if (iconElement) { + iconElement.className = isFavorite ? "fas fa-star" : "far fa-star"; + } + } + + return false; + } + return true; + }, + }; +} + function update(themes: Theme[]): void { subgroup.list = []; const favs: Command[] = []; themes.forEach((theme) => { - if (Config.favThemes.includes(theme.name)) { - favs.push({ - id: "changeTheme" + capitalizeFirstLetterOfEachWord(theme.name), - display: theme.name.replace(/_/g, " "), - configValue: theme.name, - // customStyle: `color:${theme.mainColor};background:${theme.bgColor};`, - customData: { - mainColor: theme.mainColor, - bgColor: theme.bgColor, - subColor: theme.subColor, - textColor: theme.textColor, - }, - hover: (): void => { - // previewTheme(theme.name); - ThemeController.preview(theme.name); - }, - exec: (): void => { - UpdateConfig.setTheme(theme.name); - }, - }); + const isFavorite = Config.favThemes.includes(theme.name); + const themeCommand = createThemeCommand(theme, isFavorite); + if (isFavorite) { + favs.push(themeCommand); } else { - subgroup.list.push({ - id: "changeTheme" + capitalizeFirstLetterOfEachWord(theme.name), - display: theme.name.replace(/_/g, " "), - configValue: theme.name, - // customStyle: `color:${theme.mainColor};background:${theme.bgColor}`, - customData: { - mainColor: theme.mainColor, - bgColor: theme.bgColor, - subColor: theme.subColor, - textColor: theme.textColor, - }, - hover: (): void => { - // previewTheme(theme.name); - ThemeController.preview(theme.name); - }, - exec: (): void => { - UpdateConfig.setTheme(theme.name); - }, - }); + subgroup.list.push(themeCommand); } }); subgroup.list = [...favs, ...subgroup.list]; diff --git a/frontend/src/ts/commandline/types.ts b/frontend/src/ts/commandline/types.ts index 4932b274d9af..f4a3883a8666 100644 --- a/frontend/src/ts/commandline/types.ts +++ b/frontend/src/ts/commandline/types.ts @@ -32,6 +32,8 @@ export type Command = { active?: () => boolean; shouldFocusTestUI?: boolean; customData?: Record; + html?: string; + customHandler?: (e: MouseEvent, command: Command) => boolean; }; export type CommandsSubgroup = { diff --git a/frontend/src/ts/event-handlers/footer.ts b/frontend/src/ts/event-handlers/footer.ts index 3161d9451a06..560a7dffb6f8 100644 --- a/frontend/src/ts/event-handlers/footer.ts +++ b/frontend/src/ts/event-handlers/footer.ts @@ -7,6 +7,8 @@ import * as SupportPopup from "../modals/support"; import * as ContactModal from "../modals/contact"; import * as VersionHistoryModal from "../modals/version-history"; import { envConfig } from "../constants/env-config"; +import * as ThemeController from "../controllers/theme-controller"; +import * as ConfigEvent from "../observables/config-event"; document .querySelector("footer #commandLineMobileButton") @@ -34,6 +36,59 @@ document } }); +// update the favorite icon in the current theme button +function updateCurrentThemeFavIcon(): void { + const favIconEl = document.querySelector( + "footer .right .current-theme .favIcon" + ); + if (!favIconEl) return; + const currentTheme = Config.customTheme + ? "custom" + : ThemeController.randomTheme ?? Config.theme; + if (!Config.customTheme && Config.favThemes.includes(currentTheme)) { + favIconEl.innerHTML = ''; + favIconEl.classList.add("active"); + } else { + favIconEl.innerHTML = ''; + favIconEl.classList.remove("active"); + } +} + +// add favorite icon to the current theme button +const currentThemeButton = document.querySelector( + "footer .right .current-theme" +); +if (currentThemeButton) { + const favIconDiv = document.createElement("div"); + favIconDiv.className = "favIcon"; + favIconDiv.innerHTML = ''; + currentThemeButton.appendChild(favIconDiv); + updateCurrentThemeFavIcon(); +} +document + .querySelector("footer .right .current-theme .favIcon") + ?.addEventListener("click", (event) => { + event.stopPropagation(); + if (Config.customTheme) { + Notifications.add("Cannot favorite custom themes", 0); + return; + } + const currentTheme = ThemeController.randomTheme ?? Config.theme; + if (Config.favThemes.includes(currentTheme)) { + // remove from favorites + UpdateConfig.setFavThemes( + Config.favThemes.filter((t) => t !== currentTheme) + ); + Notifications.add("Removed from favorites", 1); + } else { + // add + UpdateConfig.setFavThemes([...Config.favThemes, currentTheme]); + Notifications.add("Added to favorites", 1); + } + + updateCurrentThemeFavIcon(); + }); + document .querySelector("footer .right .current-theme") ?.addEventListener("click", async (event) => { @@ -60,6 +115,13 @@ document } }); +// subscribe to theme-related config events to update the favorite icon +ConfigEvent.subscribe((eventKey, _eventValue) => { + if (["theme", "customTheme", "favThemes"].includes(eventKey)) { + updateCurrentThemeFavIcon(); + } +}); + document .querySelector("footer #supportMeButton") ?.addEventListener("click", () => { From 466343c9e0cd479c4d5dc678376aef00390bb940 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Sat, 19 Apr 2025 00:09:36 +0200 Subject: [PATCH 02/50] remove the notifications --- backend/src/dal/user.ts | 5 +++-- frontend/src/ts/commandline/lists/themes.ts | 3 --- frontend/src/ts/elements/xp-bar.ts | 2 +- frontend/src/ts/event-handlers/footer.ts | 2 -- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/backend/src/dal/user.ts b/backend/src/dal/user.ts index 8ee5a655b0e3..1315d64c8c51 100644 --- a/backend/src/dal/user.ts +++ b/backend/src/dal/user.ts @@ -1023,8 +1023,9 @@ export async function updateInbox( //flatMap rewards const rewards: AllRewards[] = [...toBeRead, ...toBeDeleted] .filter((it) => !it.read) - .reduce((arr, current) => { - return [...arr, ...current.rewards]; + + .reduce((arr: AllRewards[], current) => { + return arr.concat(current.rewards); }, []); const xpGain = rewards diff --git a/frontend/src/ts/commandline/lists/themes.ts b/frontend/src/ts/commandline/lists/themes.ts index 907e25edc32b..ee09214caec1 100644 --- a/frontend/src/ts/commandline/lists/themes.ts +++ b/frontend/src/ts/commandline/lists/themes.ts @@ -3,7 +3,6 @@ import { capitalizeFirstLetterOfEachWord } from "../../utils/strings"; import * as ThemeController from "../../controllers/theme-controller"; import { Command, CommandsSubgroup } from "../types"; import { Theme } from "../../utils/json-data"; -import * as Notifications from "../../elements/notifications"; const subgroup: CommandsSubgroup = { title: "Theme...", @@ -58,10 +57,8 @@ function createThemeCommand(theme: Theme, isFavorite: boolean): Command { UpdateConfig.setFavThemes( Config.favThemes.filter((t) => t !== themeName) ); - Notifications.add("Removed from favorites", 1); } else { UpdateConfig.setFavThemes([...Config.favThemes, themeName]); - Notifications.add("Added to favorites", 1); } // update the star icon immediately diff --git a/frontend/src/ts/elements/xp-bar.ts b/frontend/src/ts/elements/xp-bar.ts index f2be033dc4a7..e2292cc07cf8 100644 --- a/frontend/src/ts/elements/xp-bar.ts +++ b/frontend/src/ts/elements/xp-bar.ts @@ -215,7 +215,7 @@ async function addBreakdownListItem(
` ); } else { - const positive = amount == undefined ? undefined : amount >= 0; + const positive = amount === undefined ? undefined : amount >= 0; xpBreakdownListEl.append(`
diff --git a/frontend/src/ts/event-handlers/footer.ts b/frontend/src/ts/event-handlers/footer.ts index 560a7dffb6f8..663c6577fdd4 100644 --- a/frontend/src/ts/event-handlers/footer.ts +++ b/frontend/src/ts/event-handlers/footer.ts @@ -79,11 +79,9 @@ document UpdateConfig.setFavThemes( Config.favThemes.filter((t) => t !== currentTheme) ); - Notifications.add("Removed from favorites", 1); } else { // add UpdateConfig.setFavThemes([...Config.favThemes, currentTheme]); - Notifications.add("Added to favorites", 1); } updateCurrentThemeFavIcon(); From 99561f986a04b980cc1594e7f7d646dda63fbbea Mon Sep 17 00:00:00 2001 From: byseif21 Date: Sat, 19 Apr 2025 04:28:53 +0200 Subject: [PATCH 03/50] add keyboard accesibility for the star favorite toggle in the commandline --- frontend/src/styles/commandline.scss | 7 +++ frontend/src/ts/commandline/commandline.ts | 47 +++++++++++++++++++++ frontend/src/ts/commandline/lists/themes.ts | 37 ++++++++++++---- 3 files changed, 82 insertions(+), 9 deletions(-) diff --git a/frontend/src/styles/commandline.scss b/frontend/src/styles/commandline.scss index 52a497a242e5..303f0aa29c01 100644 --- a/frontend/src/styles/commandline.scss +++ b/frontend/src/styles/commandline.scss @@ -114,6 +114,13 @@ padding: 0.5rem; pointer-events: auto; + &:focus-visible { + outline: 2px solid var(--sub-color); + border-radius: var(--roundness); + box-shadow: 0 0 0 0.1rem var(--text-color); + opacity: 1; + } + &.active { .fas.fa-star { margin-right: 0; diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index 732b3db46f19..4ff22cd95255 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -750,6 +750,53 @@ const modal = new AnimatedModal({ e.preventDefault(); await incrementActiveIndex(); } + // accesibility for the star icon + if (e.key === "ArrowRight") { + // check if cursor is at the end of the input field + if (input.selectionStart === input.value.length) { + e.preventDefault(); + // find the active command's star icon + const activeCommand = document.querySelector( + "#commandLine .suggestions .command.active" + ); + if (activeCommand) { + const starIcon = activeCommand.querySelector(".themeFavIcon"); + if (starIcon) { + // focus + (starIcon as HTMLElement).tabIndex = 0; + (starIcon as HTMLElement).focus(); + // remove any existing event listeners to prevent duplicates + const existingListener = (starIcon as HTMLElement).getAttribute( + "data-has-keydown" + ); + if (existingListener !== "true") { + // using enter + (starIcon as HTMLElement).addEventListener( + "keydown", + (starEvent) => { + if (starEvent.key === "Enter") { + starEvent.preventDefault(); + (starIcon as HTMLElement).click(); + // return focus to input field after clicking + setTimeout(() => input.focus(), 50); + } else if ( + starEvent.key === "Escape" || + starEvent.key === "ArrowLeft" + ) { + starEvent.preventDefault(); + input.focus(); + } + } + ); + (starIcon as HTMLElement).setAttribute( + "data-has-keydown", + "true" + ); + } + } + } + } + } if (e.key === "Tab") { e.preventDefault(); if (e.shiftKey) { diff --git a/frontend/src/ts/commandline/lists/themes.ts b/frontend/src/ts/commandline/lists/themes.ts index ee09214caec1..3b3c7b67bd48 100644 --- a/frontend/src/ts/commandline/lists/themes.ts +++ b/frontend/src/ts/commandline/lists/themes.ts @@ -40,14 +40,27 @@ function createThemeCommand(theme: Theme, isFavorite: boolean): Command { UpdateConfig.setTheme(theme.name); }, // custom HTML element for the favorite star - html: `
+ html: `
`, // click handler for the favorite star - customHandler: (e: MouseEvent, command: Command): boolean => { - // click was on the favorite star? + customHandler: ( + e: MouseEvent | KeyboardEvent, + command: Command + ): boolean => { + // handle both mouse clicks and keyboard events const target = e.target as HTMLElement; - if (target.closest(".themeFavIcon")) { + const starIcon = target.closest(".themeFavIcon"); + + // check if interaction is with the favorite star + if ( + starIcon || + (e instanceof KeyboardEvent && + e.key === "Enter" && + target.classList.contains("themeFavIcon")) + ) { e.stopPropagation(); const themeName = command.configValue as string; @@ -62,16 +75,22 @@ function createThemeCommand(theme: Theme, isFavorite: boolean): Command { } // update the star icon immediately - const starIcon = target.closest(".themeFavIcon"); - if (starIcon) { + const iconElement = target.classList.contains("themeFavIcon") + ? target.querySelector("i") + : starIcon?.querySelector("i"); + + const iconContainer = target.classList.contains("themeFavIcon") + ? target + : starIcon; + + if (iconContainer) { const isFavorite = Config.favThemes.includes(themeName); if (isFavorite) { - starIcon.classList.add("active"); + iconContainer.classList.add("active"); } else { - starIcon.classList.remove("active"); + iconContainer.classList.remove("active"); } // update icon based on current state - const iconElement = starIcon.querySelector("i"); if (iconElement) { iconElement.className = isFavorite ? "fas fa-star" : "far fa-star"; } From 44ca0910064de67bfb8a7b5a008793603b9f2b93 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Sun, 27 Apr 2025 17:51:05 +0300 Subject: [PATCH 04/50] unclickable star indcator --- frontend/src/ts/event-handlers/footer.ts | 29 ++++-------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/frontend/src/ts/event-handlers/footer.ts b/frontend/src/ts/event-handlers/footer.ts index 663c6577fdd4..39fa5f4041ae 100644 --- a/frontend/src/ts/event-handlers/footer.ts +++ b/frontend/src/ts/event-handlers/footer.ts @@ -46,46 +46,25 @@ function updateCurrentThemeFavIcon(): void { ? "custom" : ThemeController.randomTheme ?? Config.theme; if (!Config.customTheme && Config.favThemes.includes(currentTheme)) { - favIconEl.innerHTML = ''; + favIconEl.innerHTML = ''; favIconEl.classList.add("active"); } else { - favIconEl.innerHTML = ''; + favIconEl.innerHTML = ''; favIconEl.classList.remove("active"); } } -// add favorite icon to the current theme button +// favorite icon to the current theme button const currentThemeButton = document.querySelector( "footer .right .current-theme" ); if (currentThemeButton) { const favIconDiv = document.createElement("div"); favIconDiv.className = "favIcon"; - favIconDiv.innerHTML = ''; + favIconDiv.innerHTML = ''; currentThemeButton.appendChild(favIconDiv); updateCurrentThemeFavIcon(); } -document - .querySelector("footer .right .current-theme .favIcon") - ?.addEventListener("click", (event) => { - event.stopPropagation(); - if (Config.customTheme) { - Notifications.add("Cannot favorite custom themes", 0); - return; - } - const currentTheme = ThemeController.randomTheme ?? Config.theme; - if (Config.favThemes.includes(currentTheme)) { - // remove from favorites - UpdateConfig.setFavThemes( - Config.favThemes.filter((t) => t !== currentTheme) - ); - } else { - // add - UpdateConfig.setFavThemes([...Config.favThemes, currentTheme]); - } - - updateCurrentThemeFavIcon(); - }); document .querySelector("footer .right .current-theme") From 16084712b85ae93042b6ca8e515818319052c242 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Sun, 27 Apr 2025 17:57:41 +0300 Subject: [PATCH 05/50] modify the footer star to not sound clickable --- frontend/src/styles/footer.scss | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/src/styles/footer.scss b/frontend/src/styles/footer.scss index 4b668f1270de..4f4690fd19fb 100644 --- a/frontend/src/styles/footer.scss +++ b/frontend/src/styles/footer.scss @@ -91,23 +91,23 @@ footer { .favIcon { position: absolute; //right: -10px; - top: -5px; + top: -8px; font-size: 0.7rem; color: var(--sub-color); - cursor: pointer; + pointer-events: none; transition: 0.125s; + } + + &:hover .favIcon.active { + color: var(--text-color); + } - &:hover { - color: var(--text-color); - transform: scale(1.2); - } + .favIcon.active { + color: var(--sub-color); + } - &.active { - color: var(--sub-color); - } - &.active:hover { - color: var(--text-color); - } + &:hover .favIcon.active { + color: var(--text-color); } } From 243e6d7c12a883584c5b67c1595d45128ca0e230 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Thu, 8 May 2025 04:59:37 +0300 Subject: [PATCH 06/50] remove keys acces --- frontend/src/ts/commandline/commandline.ts | 47 ---------------------- 1 file changed, 47 deletions(-) diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index 9b07a61684f2..dcf0707ff217 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -746,53 +746,6 @@ const modal = new AnimatedModal({ e.preventDefault(); await incrementActiveIndex(); } - // accesibility for the star icon - if (e.key === "ArrowRight") { - // check if cursor is at the end of the input field - if (input.selectionStart === input.value.length) { - e.preventDefault(); - // find the active command's star icon - const activeCommand = document.querySelector( - "#commandLine .suggestions .command.active" - ); - if (activeCommand) { - const starIcon = activeCommand.querySelector(".themeFavIcon"); - if (starIcon) { - // focus - (starIcon as HTMLElement).tabIndex = 0; - (starIcon as HTMLElement).focus(); - // remove any existing event listeners to prevent duplicates - const existingListener = (starIcon as HTMLElement).getAttribute( - "data-has-keydown" - ); - if (existingListener !== "true") { - // using enter - (starIcon as HTMLElement).addEventListener( - "keydown", - (starEvent) => { - if (starEvent.key === "Enter") { - starEvent.preventDefault(); - (starIcon as HTMLElement).click(); - // return focus to input field after clicking - setTimeout(() => input.focus(), 50); - } else if ( - starEvent.key === "Escape" || - starEvent.key === "ArrowLeft" - ) { - starEvent.preventDefault(); - input.focus(); - } - } - ); - (starIcon as HTMLElement).setAttribute( - "data-has-keydown", - "true" - ); - } - } - } - } - } if (e.key === "Tab") { e.preventDefault(); if (e.shiftKey) { From d8a8f64f346fda1fb1b4fae672f5542ce8836db9 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Thu, 8 May 2025 05:06:15 +0300 Subject: [PATCH 07/50] no need for hover if not favorite to not sound clickable --- frontend/src/styles/commandline.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/styles/commandline.scss b/frontend/src/styles/commandline.scss index 303f0aa29c01..c903c066b862 100644 --- a/frontend/src/styles/commandline.scss +++ b/frontend/src/styles/commandline.scss @@ -130,7 +130,7 @@ } } - &:hover .themeFavIcon { + &:hover .themeFavIcon.active { .fas.fa-star { margin-right: 0; } From 8b0cd0c138fd4b4e135f59c8a66bb13161f4803f Mon Sep 17 00:00:00 2001 From: byseif21 Date: Thu, 8 May 2025 07:03:02 +0300 Subject: [PATCH 08/50] resolve the conflictsand try aligning with the new changes --- frontend/src/ts/commandline/commandline.ts | 3 +- frontend/src/ts/commandline/lists.ts | 3 +- frontend/src/ts/commandline/lists/themes.ts | 89 +++------------------ 3 files changed, 15 insertions(+), 80 deletions(-) diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index dcf0707ff217..b0122caa7231 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -13,6 +13,7 @@ import * as Loader from "../elements/loader"; import { Command, CommandsSubgroup } from "./types"; import * as JSONData from "../utils/json-data"; import * as Misc from "../utils/misc"; +import * as ThemesModule from "./lists/themes"; type CommandlineMode = "search" | "input"; type InputModeParams = { @@ -135,7 +136,7 @@ export function show( const themesPromise = JSONData.getThemesList(); themesPromise .then((themes) => { - CommandlineLists.updateThemesCommands(themes); + ThemesModule.update(themes); }) .catch((e: unknown) => { console.error( diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index 8b6155b88e47..fc64161d7733 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -77,8 +77,7 @@ import CustomThemesListCommands from "./lists/custom-themes-list"; import PresetsCommands from "./lists/presets"; import LayoutsCommands from "./lists/layouts"; import FunboxCommands from "./lists/funbox"; -import ThemesCommands, { update as updateThemesCommands } from "./lists/themes"; -export { updateThemesCommands }; +import ThemesCommands from "./lists/themes"; import LoadChallengeCommands, { update as updateLoadChallengeCommands, } from "./lists/load-challenge"; diff --git a/frontend/src/ts/commandline/lists/themes.ts b/frontend/src/ts/commandline/lists/themes.ts index 2a3fb4119b0f..07559654e22e 100644 --- a/frontend/src/ts/commandline/lists/themes.ts +++ b/frontend/src/ts/commandline/lists/themes.ts @@ -44,8 +44,15 @@ const commands: Command[] = [ }, ]; -function createThemeCommand(theme: Theme, isFavorite: boolean): Command { - return { +export function update(themes: Theme[]): void { + // clear the current list + subgroup.list = []; + + // rebuild with favorites first, then non-favorites + subgroup.list = [ + ...themes.filter(isFavorite), + ...themes.filter(not(isFavorite)), + ].map((theme: Theme) => ({ id: "changeTheme" + capitalizeFirstLetterOfEachWord(theme.name), display: theme.name.replace(/_/g, " "), configValue: theme.name, @@ -55,7 +62,6 @@ function createThemeCommand(theme: Theme, isFavorite: boolean): Command { bgColor: theme.bgColor, subColor: theme.subColor, textColor: theme.textColor, - isFavorite: isFavorite, }, hover: (): void => { // previewTheme(theme.name); @@ -66,81 +72,10 @@ function createThemeCommand(theme: Theme, isFavorite: boolean): Command { }, // custom HTML element for the favorite star html: `
- +
`, - // click handler for the favorite star - customHandler: ( - e: MouseEvent | KeyboardEvent, - command: Command - ): boolean => { - // handle both mouse clicks and keyboard events - const target = e.target as HTMLElement; - const starIcon = target.closest(".themeFavIcon"); - - // check if interaction is with the favorite star - if ( - starIcon || - (e instanceof KeyboardEvent && - e.key === "Enter" && - target.classList.contains("themeFavIcon")) - ) { - e.stopPropagation(); - - const themeName = command.configValue as string; - - if (Config.favThemes.includes(themeName)) { - // remove from favorites - UpdateConfig.setFavThemes( - Config.favThemes.filter((t) => t !== themeName) - ); - } else { - UpdateConfig.setFavThemes([...Config.favThemes, themeName]); - } - - // update the star icon immediately - const iconElement = target.classList.contains("themeFavIcon") - ? target.querySelector("i") - : starIcon?.querySelector("i"); - - const iconContainer = target.classList.contains("themeFavIcon") - ? target - : starIcon; - - if (iconContainer) { - const isFavorite = Config.favThemes.includes(themeName); - if (isFavorite) { - iconContainer.classList.add("active"); - } else { - iconContainer.classList.remove("active"); - } - // update icon based on current state - if (iconElement) { - iconElement.className = isFavorite ? "fas fa-star" : "far fa-star"; - } - } - - return false; - } - return true; - }, - }; + })); } - -function update(themes: Theme[]): void { - subgroup.list = []; - const favs: Command[] = []; - themes.forEach((theme) => { - const isFavorite = Config.favThemes.includes(theme.name); - const themeCommand = createThemeCommand(theme, isFavorite); - if (isFavorite) { - favs.push(themeCommand); - } else { - subgroup.list.push(themeCommand); - } - }); - subgroup.list = [...favs, ...subgroup.list]; -} - export default commands; From 3915e30c265ab2aeb9cce0f3f2757569cdb503ba Mon Sep 17 00:00:00 2001 From: byseif21 Date: Thu, 8 May 2025 07:08:49 +0300 Subject: [PATCH 09/50] fix --- frontend/src/ts/utils/json-data.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/ts/utils/json-data.ts b/frontend/src/ts/utils/json-data.ts index c2c5c6457a03..83bd2ae81cfa 100644 --- a/frontend/src/ts/utils/json-data.ts +++ b/frontend/src/ts/utils/json-data.ts @@ -1,6 +1,7 @@ import { FunboxName } from "@monkeytype/contracts/schemas/configs"; import { Language } from "@monkeytype/contracts/schemas/languages"; import { Accents } from "../test/lazy-mode"; +import { Theme, ThemesList } from "../constants/themes"; /** * Fetches JSON data from the specified URL using the fetch API. @@ -202,6 +203,14 @@ export async function getChallengeList(): Promise { return data; } +/** + * Fetches the list of themes from the server. + * @returns A promise that resolves to the list of themes. + */ +export async function getThemesList(): Promise { + return ThemesList; +} + /** * Fetches the list of supporters from the server. * @returns A promise that resolves to the list of supporters. From fcf942db69f0e33bfbacb2f2aa6108cbda7a9d02 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Thu, 8 May 2025 07:25:14 +0300 Subject: [PATCH 10/50] fix to algin with the cahnges and add initialization function, not necessary but thought it will be better in that case? --- frontend/src/ts/event-handlers/footer.ts | 31 +++++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/frontend/src/ts/event-handlers/footer.ts b/frontend/src/ts/event-handlers/footer.ts index 99e8e9da9da5..572000d42140 100644 --- a/frontend/src/ts/event-handlers/footer.ts +++ b/frontend/src/ts/event-handlers/footer.ts @@ -11,6 +11,7 @@ import * as ThemeController from "../controllers/theme-controller"; import * as ConfigEvent from "../observables/config-event"; import { COMPATIBILITY_CHECK } from "@monkeytype/contracts"; import { lastSeenServerCompatibility } from "../ape/adapters/ts-rest-adapter"; +import { ThemeName } from "@monkeytype/contracts/schemas/configs"; document .querySelector("footer #commandLineMobileButton") @@ -57,7 +58,10 @@ function updateCurrentThemeFavIcon(): void { const currentTheme = Config.customTheme ? "custom" : ThemeController.randomTheme ?? Config.theme; - if (!Config.customTheme && Config.favThemes.includes(currentTheme)) { + if ( + !Config.customTheme && + Config.favThemes.includes(currentTheme as ThemeName) + ) { favIconEl.innerHTML = ''; favIconEl.classList.add("active"); } else { @@ -66,17 +70,20 @@ function updateCurrentThemeFavIcon(): void { } } -// favorite icon to the current theme button -const currentThemeButton = document.querySelector( - "footer .right .current-theme" -); -if (currentThemeButton) { - const favIconDiv = document.createElement("div"); - favIconDiv.className = "favIcon"; - favIconDiv.innerHTML = ''; - currentThemeButton.appendChild(favIconDiv); - updateCurrentThemeFavIcon(); -} +// initialize favorite icon for the current theme button +const initializeFavIcon = (): void => { + const currentThemeButton = document.querySelector( + "footer .right .current-theme" + ); + if (currentThemeButton && !currentThemeButton.querySelector(".favIcon")) { + const favIconDiv = document.createElement("div"); + favIconDiv.className = "favIcon"; + favIconDiv.innerHTML = ''; + currentThemeButton.appendChild(favIconDiv); + updateCurrentThemeFavIcon(); + } +}; +initializeFavIcon(); document .querySelector("footer .right .current-theme") From ddd1ff35466550db02f8fd2876d2f868e09a850e Mon Sep 17 00:00:00 2001 From: byseif21 Date: Thu, 8 May 2025 07:55:59 +0300 Subject: [PATCH 11/50] remove duplicated --- frontend/src/styles/footer.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/src/styles/footer.scss b/frontend/src/styles/footer.scss index e11751608336..1d6177fd54ec 100644 --- a/frontend/src/styles/footer.scss +++ b/frontend/src/styles/footer.scss @@ -95,10 +95,6 @@ footer { transition: 0.125s; } - &:hover .favIcon.active { - color: var(--text-color); - } - .favIcon.active { color: var(--sub-color); } From 647a32a0892a01ca9e12ae8c3436f256cd0ef7b7 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Thu, 8 May 2025 08:09:07 +0300 Subject: [PATCH 12/50] cleanup --- frontend/src/styles/commandline.scss | 9 --------- frontend/src/ts/commandline/commandline.ts | 10 +--------- frontend/src/ts/commandline/lists/themes.ts | 4 +--- 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/frontend/src/styles/commandline.scss b/frontend/src/styles/commandline.scss index c903c066b862..16ccf5be1ecf 100644 --- a/frontend/src/styles/commandline.scss +++ b/frontend/src/styles/commandline.scss @@ -109,17 +109,8 @@ color: var(--sub-color); opacity: 0; transition: opacity 0.125s; - cursor: pointer; z-index: 999; padding: 0.5rem; - pointer-events: auto; - - &:focus-visible { - outline: 2px solid var(--sub-color); - border-radius: var(--roundness); - box-shadow: 0 0 0 0.1rem var(--text-color); - opacity: 1; - } &.active { .fas.fa-star { diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index b0122caa7231..eebc7a2c0cb7 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -498,20 +498,12 @@ async function showCommands(): Promise { activeIndex = parseInt(command.getAttribute("data-index") ?? "0"); await updateActiveCommand(); }); - command.addEventListener("click", async (e) => { + command.addEventListener("click", async () => { const previous = activeIndex; activeIndex = parseInt(command.getAttribute("data-index") ?? "0"); if (previous !== activeIndex) { await updateActiveCommand(); } - const commandObj = (await getList()).filter((c) => c.found)[activeIndex]; - if (commandObj && commandObj.customHandler) { - const shouldProceed = commandObj.customHandler( - e as MouseEvent, - commandObj - ); - if (!shouldProceed) return; - } await runActiveCommand(); }); } diff --git a/frontend/src/ts/commandline/lists/themes.ts b/frontend/src/ts/commandline/lists/themes.ts index 07559654e22e..eeefb6fe34fd 100644 --- a/frontend/src/ts/commandline/lists/themes.ts +++ b/frontend/src/ts/commandline/lists/themes.ts @@ -71,9 +71,7 @@ export function update(themes: Theme[]): void { UpdateConfig.setTheme(theme.name); }, // custom HTML element for the favorite star - html: `
+ html: `
`, })); From d936bf4e1b9f4b14273f7ce582863e7419e3fe0d Mon Sep 17 00:00:00 2001 From: byseif21 Date: Thu, 8 May 2025 08:18:34 +0300 Subject: [PATCH 13/50] replace the heart with star in the commandline for consistency? --- .../ts/commandline/lists/add-or-remove-theme-to-favorites.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/ts/commandline/lists/add-or-remove-theme-to-favorites.ts b/frontend/src/ts/commandline/lists/add-or-remove-theme-to-favorites.ts index e40f2055de84..a79db7dac082 100644 --- a/frontend/src/ts/commandline/lists/add-or-remove-theme-to-favorites.ts +++ b/frontend/src/ts/commandline/lists/add-or-remove-theme-to-favorites.ts @@ -7,7 +7,7 @@ const commands: Command[] = [ { id: "addThemeToFavorite", display: "Add current theme to favorite", - icon: "fa-heart", + icon: "fa-star", available: (): boolean => { return ( !Config.customTheme && @@ -25,7 +25,7 @@ const commands: Command[] = [ { id: "removeThemeFromFavorite", display: "Remove current theme from favorite", - icon: "fa-heart-broken", + icon: "fa-star-half", available: (): boolean => { return ( !Config.customTheme && From b852e1a869138bf89271d7a8b63db46426e65239 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Thu, 8 May 2025 08:25:19 +0300 Subject: [PATCH 14/50] cleanup --- frontend/src/ts/commandline/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/ts/commandline/types.ts b/frontend/src/ts/commandline/types.ts index f4a3883a8666..a0626b5bd61f 100644 --- a/frontend/src/ts/commandline/types.ts +++ b/frontend/src/ts/commandline/types.ts @@ -33,7 +33,6 @@ export type Command = { shouldFocusTestUI?: boolean; customData?: Record; html?: string; - customHandler?: (e: MouseEvent, command: Command) => boolean; }; export type CommandsSubgroup = { From da4226a5b99dd6470a275c234d1ef1da1e0d9a4d Mon Sep 17 00:00:00 2001 From: byseif21 Date: Thu, 8 May 2025 16:56:29 +0300 Subject: [PATCH 15/50] refactor themes.ts a bit to impr maintainability --- frontend/src/ts/commandline/lists/themes.ts | 76 ++++++++++++--------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/frontend/src/ts/commandline/lists/themes.ts b/frontend/src/ts/commandline/lists/themes.ts index eeefb6fe34fd..a3b71b6dc544 100644 --- a/frontend/src/ts/commandline/lists/themes.ts +++ b/frontend/src/ts/commandline/lists/themes.ts @@ -8,13 +8,14 @@ import { not } from "@monkeytype/util/predicates"; const isFavorite = (theme: Theme): boolean => Config.favThemes.includes(theme.name); -const subgroup: CommandsSubgroup = { - title: "Theme...", - configKey: "theme", - list: [ - ...ThemesList.filter(isFavorite), - ...ThemesList.filter(not(isFavorite)), - ].map((theme: Theme) => ({ +/** + * creates a theme command object for the given theme + * @param theme the theme to create a command for + * @param includeHtml whether to include the HTML for the favorite star + * @returns a command object for the theme + */ +const createThemeCommand = (theme: Theme, includeHtml = false): Command => { + const command = { id: "changeTheme" + capitalizeFirstLetterOfEachWord(theme.name), display: theme.name.replace(/_/g, " "), configValue: theme.name, @@ -32,7 +33,37 @@ const subgroup: CommandsSubgroup = { exec: (): void => { UpdateConfig.setTheme(theme.name); }, - })), + }; + + // add HTML for favorite star if requested + if (includeHtml) { + return { + ...command, + html: `
+ +
`, + }; + } + + return command; +}; + +/** + * sorts themes with favorites first, then non-favorites + * @param themes the themes to sort + * @returns sorted array of themes + */ +const sortThemesByFavorite = (themes: Theme[]): Theme[] => [ + ...themes.filter(isFavorite), + ...themes.filter(not(isFavorite)), +]; + +const subgroup: CommandsSubgroup = { + title: "Theme...", + configKey: "theme", + list: sortThemesByFavorite(ThemesList).map((theme) => + createThemeCommand(theme, true) + ), }; const commands: Command[] = [ @@ -49,31 +80,8 @@ export function update(themes: Theme[]): void { subgroup.list = []; // rebuild with favorites first, then non-favorites - subgroup.list = [ - ...themes.filter(isFavorite), - ...themes.filter(not(isFavorite)), - ].map((theme: Theme) => ({ - id: "changeTheme" + capitalizeFirstLetterOfEachWord(theme.name), - display: theme.name.replace(/_/g, " "), - configValue: theme.name, - // customStyle: `color:${theme.mainColor};background:${theme.bgColor}`, - customData: { - mainColor: theme.mainColor, - bgColor: theme.bgColor, - subColor: theme.subColor, - textColor: theme.textColor, - }, - hover: (): void => { - // previewTheme(theme.name); - ThemeController.preview(theme.name); - }, - exec: (): void => { - UpdateConfig.setTheme(theme.name); - }, - // custom HTML element for the favorite star - html: `
- -
`, - })); + subgroup.list = sortThemesByFavorite(themes).map((theme) => + createThemeCommand(theme, true) + ); } export default commands; From ec296a19b3b2c0a8250b821f879237d3ab0c7acc Mon Sep 17 00:00:00 2001 From: byseif21 Date: Thu, 15 May 2025 21:13:19 +0300 Subject: [PATCH 16/50] update the star footer while themes being previewed --- .../src/ts/controllers/theme-controller.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/frontend/src/ts/controllers/theme-controller.ts b/frontend/src/ts/controllers/theme-controller.ts index 87783ace3c86..b947f90e7abd 100644 --- a/frontend/src/ts/controllers/theme-controller.ts +++ b/frontend/src/ts/controllers/theme-controller.ts @@ -209,9 +209,29 @@ const debouncedPreview = debounce<(t: string, c?: string[]) => void>( (themeIdenfitier, customColorsOverride) => { isPreviewingTheme = true; void apply(themeIdenfitier, customColorsOverride, true); + updateFooterThemeFavIcon(themeIdenfitier); } ); +// update the favorite icon in the current theme button based on the theme being previewed +function updateFooterThemeFavIcon(themeName: string): void { + const favIconEl = document.querySelector( + "footer .right .current-theme .favIcon" + ); + if (!favIconEl) return; + + if ( + !Config.customTheme && + Config.favThemes.includes(themeName as ThemeName) + ) { + favIconEl.innerHTML = ''; + favIconEl.classList.add("active"); + } else { + favIconEl.innerHTML = ''; + favIconEl.classList.remove("active"); + } +} + async function set( themeIdentifier: string, isAutoSwitch = false @@ -235,10 +255,21 @@ export async function clearPreview(applyTheme = true): Promise { if (applyTheme) { if (randomTheme !== null) { await apply(randomTheme); + // restore the correct favorite icon state for the current theme + updateFooterThemeFavIcon(randomTheme); } else if (Config.customTheme) { await apply("custom"); + // custom themes don't have favorite status + const favIconEl = document.querySelector( + "footer .right .current-theme .favIcon" + ); + if (favIconEl) { + favIconEl.innerHTML = ''; + favIconEl.classList.remove("active"); + } } else { await apply(Config.theme); + updateFooterThemeFavIcon(Config.theme); } } } From ba7083cc73f752bab0ffe00b9b4d6dc889903241 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Fri, 16 May 2025 17:06:37 +0300 Subject: [PATCH 17/50] refactor to reduce duplication --- .../src/ts/controllers/theme-controller.ts | 60 +++++++++---------- frontend/src/ts/event-handlers/footer.ts | 38 ++++-------- frontend/src/ts/pages/settings.ts | 13 +++- 3 files changed, 50 insertions(+), 61 deletions(-) diff --git a/frontend/src/ts/controllers/theme-controller.ts b/frontend/src/ts/controllers/theme-controller.ts index b947f90e7abd..0409ff77fc03 100644 --- a/frontend/src/ts/controllers/theme-controller.ts +++ b/frontend/src/ts/controllers/theme-controller.ts @@ -135,12 +135,31 @@ export async function loadStyle(name: string): Promise { }); } -// export function changeCustomTheme(themeId: string, nosave = false): void { -// const customThemes = DB.getSnapshot().customThemes; -// const colors = customThemes.find((e) => e._id === themeId) -// ?.colors as string[]; -// UpdateConfig.setCustomThemeColors(colors, nosave); -// } +// update the favorite icon in the current theme button +export function updateFooterThemeFavIcon( + themeName: string | null = null +): void { + const favIconEl = document.querySelector( + "footer .right .current-theme .favIcon" + ); + if (!favIconEl) return; + + if (themeName === "custom" || Config.customTheme) { + // custom themes don't have favorite status + favIconEl.innerHTML = ''; + favIconEl.classList.remove("active"); + } else { + // use provided theme name or get current theme + const currentTheme = themeName ?? randomTheme ?? Config.theme; + if (Config.favThemes.includes(currentTheme as ThemeName)) { + favIconEl.innerHTML = ''; + favIconEl.classList.add("active"); + } else { + favIconEl.innerHTML = ''; + favIconEl.classList.remove("active"); + } + } +} async function apply( themeName: string, @@ -213,25 +232,6 @@ const debouncedPreview = debounce<(t: string, c?: string[]) => void>( } ); -// update the favorite icon in the current theme button based on the theme being previewed -function updateFooterThemeFavIcon(themeName: string): void { - const favIconEl = document.querySelector( - "footer .right .current-theme .favIcon" - ); - if (!favIconEl) return; - - if ( - !Config.customTheme && - Config.favThemes.includes(themeName as ThemeName) - ) { - favIconEl.innerHTML = ''; - favIconEl.classList.add("active"); - } else { - favIconEl.innerHTML = ''; - favIconEl.classList.remove("active"); - } -} - async function set( themeIdentifier: string, isAutoSwitch = false @@ -242,6 +242,7 @@ async function set( isAutoSwitch ); await apply(themeIdentifier, undefined, isAutoSwitch); + updateFooterThemeFavIcon(themeIdentifier); if (!isAutoSwitch && Config.autoSwitchTheme) { setAutoSwitchTheme(false); @@ -259,14 +260,7 @@ export async function clearPreview(applyTheme = true): Promise { updateFooterThemeFavIcon(randomTheme); } else if (Config.customTheme) { await apply("custom"); - // custom themes don't have favorite status - const favIconEl = document.querySelector( - "footer .right .current-theme .favIcon" - ); - if (favIconEl) { - favIconEl.innerHTML = ''; - favIconEl.classList.remove("active"); - } + updateFooterThemeFavIcon("custom"); } else { await apply(Config.theme); updateFooterThemeFavIcon(Config.theme); diff --git a/frontend/src/ts/event-handlers/footer.ts b/frontend/src/ts/event-handlers/footer.ts index 572000d42140..3e6b725da1eb 100644 --- a/frontend/src/ts/event-handlers/footer.ts +++ b/frontend/src/ts/event-handlers/footer.ts @@ -7,11 +7,10 @@ import * as SupportPopup from "../modals/support"; import * as ContactModal from "../modals/contact"; import * as VersionHistoryModal from "../modals/version-history"; import { envConfig } from "../constants/env-config"; -import * as ThemeController from "../controllers/theme-controller"; +import { updateFooterThemeFavIcon } from "../controllers/theme-controller"; import * as ConfigEvent from "../observables/config-event"; import { COMPATIBILITY_CHECK } from "@monkeytype/contracts"; import { lastSeenServerCompatibility } from "../ape/adapters/ts-rest-adapter"; -import { ThemeName } from "@monkeytype/contracts/schemas/configs"; document .querySelector("footer #commandLineMobileButton") @@ -49,27 +48,6 @@ document } }); -// update the favorite icon in the current theme button -function updateCurrentThemeFavIcon(): void { - const favIconEl = document.querySelector( - "footer .right .current-theme .favIcon" - ); - if (!favIconEl) return; - const currentTheme = Config.customTheme - ? "custom" - : ThemeController.randomTheme ?? Config.theme; - if ( - !Config.customTheme && - Config.favThemes.includes(currentTheme as ThemeName) - ) { - favIconEl.innerHTML = ''; - favIconEl.classList.add("active"); - } else { - favIconEl.innerHTML = ''; - favIconEl.classList.remove("active"); - } -} - // initialize favorite icon for the current theme button const initializeFavIcon = (): void => { const currentThemeButton = document.querySelector( @@ -80,7 +58,7 @@ const initializeFavIcon = (): void => { favIconDiv.className = "favIcon"; favIconDiv.innerHTML = ''; currentThemeButton.appendChild(favIconDiv); - updateCurrentThemeFavIcon(); + updateFooterThemeFavIcon(); } }; initializeFavIcon(); @@ -113,8 +91,16 @@ document // subscribe to theme-related config events to update the favorite icon ConfigEvent.subscribe((eventKey, _eventValue) => { - if (["theme", "customTheme", "favThemes"].includes(eventKey)) { - updateCurrentThemeFavIcon(); + if ( + [ + "theme", + "customTheme", + "customThemeColors", + "randomTheme", + "favThemes", + ].includes(eventKey) + ) { + updateFooterThemeFavIcon(); } }); diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index bfd65a8dab9a..89bf49ebfe24 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -1347,8 +1347,17 @@ ConfigEvent.subscribe((eventKey, eventValue) => { } //make sure the page doesnt update a billion times when applying a preset/config at once if (configEventDisabled || eventKey === "saveToLocalStorage") return; - if (ActivePage.get() === "settings" && eventKey !== "theme") { - void update(); + if (ActivePage.get() === "settings") { + if ( + eventKey === "theme" || + eventKey === "customTheme" || + eventKey === "randomTheme" + ) { + void ThemePicker.refreshPresetButtons(); + } + if (eventKey !== "theme") { + void update(); + } } }); From 1c2baed9b5426af3d18f2a42e506409653cee3c1 Mon Sep 17 00:00:00 2001 From: byseif21 Date: Fri, 16 May 2025 22:40:35 +0300 Subject: [PATCH 18/50] refactor & move star footer to html --- frontend/src/html/footer.html | 1 + frontend/src/ts/commandline/lists/themes.ts | 25 ++++-------- .../src/ts/controllers/theme-controller.ts | 39 ++++++++++++------- frontend/src/ts/event-handlers/footer.ts | 15 ------- 4 files changed, 34 insertions(+), 46 deletions(-) diff --git a/frontend/src/html/footer.html b/frontend/src/html/footer.html index 84f476f8ad53..d2bba8cad255 100644 --- a/frontend/src/html/footer.html +++ b/frontend/src/html/footer.html @@ -76,6 +76,7 @@ >
serika dark
+
- Normal is the classic type test experience. Expert fails the test if you - submit (press space) an incorrect word. Master fails if you press a + Normal is the classic typing test experience. Expert fails the test if + you submit (press space) an incorrect word. Master fails if you press a single incorrect key (meaning you have to achieve 100% accuracy).
From 80d675b6eda237471a8a2c6558c4dd321a9b0314 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 27 May 2025 17:12:05 +0200 Subject: [PATCH 41/50] perf: use cache in local-storage-with-schema (@fehmer) (#6596) --- .../utils/local-storage-with-schema.spec.ts | 276 +++++++++++------- .../src/ts/utils/local-storage-with-schema.ts | 23 +- 2 files changed, 185 insertions(+), 114 deletions(-) diff --git a/frontend/__tests__/utils/local-storage-with-schema.spec.ts b/frontend/__tests__/utils/local-storage-with-schema.spec.ts index fa74cf0407d9..d722e579e1c5 100644 --- a/frontend/__tests__/utils/local-storage-with-schema.spec.ts +++ b/frontend/__tests__/utils/local-storage-with-schema.spec.ts @@ -15,7 +15,7 @@ describe("local-storage-with-schema.ts", () => { fontSize: 16, }; - const ls = new LocalStorageWithSchema({ + let ls = new LocalStorageWithSchema({ key: "config", schema: objectSchema, fallback: defaultObject, @@ -37,144 +37,204 @@ describe("local-storage-with-schema.ts", () => { removeItemMock.mockReset(); }); - it("should save to localStorage if schema is correct and return true", () => { - const res = ls.set(defaultObject); - - expect(localStorage.setItem).toHaveBeenCalledWith( - "config", - JSON.stringify(defaultObject) - ); - expect(res).toBe(true); + beforeEach(() => { + ls = new LocalStorageWithSchema({ + key: "config", + schema: objectSchema, + fallback: defaultObject, + }); }); - it("should fail to save to localStorage if schema is incorrect and return false", () => { - const obj = { - hi: "hello", - }; + describe("set", () => { + it("should save to localStorage if schema is correct and return true", () => { + const res = ls.set(defaultObject); - const res = ls.set(obj as any); + expect(localStorage.setItem).toHaveBeenCalledWith( + "config", + JSON.stringify(defaultObject) + ); + expect(res).toBe(true); + }); - expect(localStorage.setItem).not.toHaveBeenCalled(); - expect(res).toBe(false); - }); + it("should fail to save to localStorage if schema is incorrect and return false", () => { + const obj = { + hi: "hello", + }; - it("should revert to the fallback value if localstorage is null", () => { - getItemMock.mockReturnValue(null); + const res = ls.set(obj as any); - const res = ls.get(); + expect(localStorage.setItem).not.toHaveBeenCalled(); + expect(res).toBe(false); + }); - expect(localStorage.getItem).toHaveBeenCalledWith("config"); - expect(localStorage.setItem).not.toHaveBeenCalled(); - expect(res).toEqual(defaultObject); - }); + it("should update cache on set", () => { + ls.set(defaultObject); - it("should revert to the fallback value if localstorage json is malformed", () => { - getItemMock.mockReturnValue("badjson"); + expect(ls.get()).toStrictEqual(defaultObject); - const res = ls.get(); + const update = { ...defaultObject, fontSize: 5 }; + ls.set(update); - expect(localStorage.getItem).toHaveBeenCalledWith("config"); - expect(localStorage.setItem).toHaveBeenCalledWith( - "config", - JSON.stringify(defaultObject) - ); - expect(res).toEqual(defaultObject); - }); + expect(ls.get()).toStrictEqual(update); - it("should get from localStorage", () => { - getItemMock.mockReturnValue(JSON.stringify(defaultObject)); + expect(getItemMock).not.toHaveBeenCalled(); + }); - const res = ls.get(); + it("should get last valid value if schema is incorrect", () => { + ls.set(defaultObject); - expect(localStorage.getItem).toHaveBeenCalledWith("config"); - expect(localStorage.setItem).not.toHaveBeenCalled(); - expect(res).toEqual(defaultObject); - }); + ls.set({ hi: "hello" } as any); - it("should revert to fallback value if no migrate function and schema failed", () => { - getItemMock.mockReturnValue(JSON.stringify({ hi: "hello" })); - const ls = new LocalStorageWithSchema({ - key: "config", - schema: objectSchema, - fallback: defaultObject, + expect(ls.get()).toEqual(defaultObject); + + expect(setItemMock).toHaveBeenCalledOnce(); + expect(getItemMock).not.toHaveBeenCalled(); }); + }); - const res = ls.get(); + describe("get", () => { + it("should revert to the fallback value if localstorage is null", () => { + getItemMock.mockReturnValue(null); - expect(localStorage.getItem).toHaveBeenCalledWith("config"); - expect(localStorage.setItem).toHaveBeenCalledWith( - "config", - JSON.stringify(defaultObject) - ); - expect(res).toEqual(defaultObject); - }); + const res = ls.get(); + + expect(getItemMock).toHaveBeenCalledWith("config"); + expect(setItemMock).not.toHaveBeenCalled(); + expect(res).toEqual(defaultObject); - it("should migrate (when function is provided) if schema failed", () => { - const existingValue = { hi: "hello" }; + //cache used + expect(ls.get()).toEqual(res); + expect(getItemMock).toHaveBeenCalledOnce(); + }); - getItemMock.mockReturnValue(JSON.stringify(existingValue)); + it("should revert to the fallback value if localstorage json is malformed", () => { + getItemMock.mockReturnValue("badjson"); - const migrated = { - punctuation: false, - mode: "time", - fontSize: 1, - }; + const res = ls.get(); - const migrateFnMock = vi.fn(() => migrated as any); + expect(getItemMock).toHaveBeenCalledWith("config"); + expect(setItemMock).toHaveBeenCalledWith( + "config", + JSON.stringify(defaultObject) + ); + expect(res).toEqual(defaultObject); - const ls = new LocalStorageWithSchema({ - key: "config", - schema: objectSchema, - fallback: defaultObject, - migrate: migrateFnMock, + //cache used + expect(ls.get()).toEqual(defaultObject); + expect(getItemMock).toHaveBeenCalledOnce(); }); - const res = ls.get(); - - expect(localStorage.getItem).toHaveBeenCalledWith("config"); - expect(migrateFnMock).toHaveBeenCalledWith( - existingValue, - expect.any(Array) - ); - expect(localStorage.setItem).toHaveBeenCalledWith( - "config", - JSON.stringify(migrated) - ); - expect(res).toEqual(migrated); - }); + it("should get from localStorage", () => { + getItemMock.mockReturnValue(JSON.stringify(defaultObject)); - it("should revert to fallback if migration ran but schema still failed", () => { - const existingValue = { hi: "hello" }; + const res = ls.get(); - getItemMock.mockReturnValue(JSON.stringify(existingValue)); + expect(getItemMock).toHaveBeenCalledWith("config"); + expect(setItemMock).not.toHaveBeenCalled(); + expect(res).toEqual(defaultObject); - const invalidMigrated = { - punctuation: 1, - mode: "time", - fontSize: 1, - }; + //cache used + expect(ls.get()).toEqual(res); + expect(getItemMock).toHaveBeenCalledOnce(); + }); - const migrateFnMock = vi.fn(() => invalidMigrated as any); + it("should revert to fallback value if no migrate function and schema failed", () => { + getItemMock.mockReturnValue(JSON.stringify({ hi: "hello" })); + const ls = new LocalStorageWithSchema({ + key: "config", + schema: objectSchema, + fallback: defaultObject, + }); + + const res = ls.get(); + + expect(getItemMock).toHaveBeenCalledWith("config"); + expect(setItemMock).toHaveBeenCalledWith( + "config", + JSON.stringify(defaultObject) + ); + expect(res).toEqual(defaultObject); + + //cache used + expect(ls.get()).toEqual(defaultObject); + expect(getItemMock).toHaveBeenCalledOnce(); + }); - const ls = new LocalStorageWithSchema({ - key: "config", - schema: objectSchema, - fallback: defaultObject, - migrate: migrateFnMock, + it("should migrate (when function is provided) if schema failed", () => { + const existingValue = { hi: "hello" }; + + getItemMock.mockReturnValue(JSON.stringify(existingValue)); + + const migrated = { + punctuation: false, + mode: "time", + fontSize: 1, + }; + + const migrateFnMock = vi.fn(() => migrated as any); + + const ls = new LocalStorageWithSchema({ + key: "config", + schema: objectSchema, + fallback: defaultObject, + migrate: migrateFnMock, + }); + + const res = ls.get(); + + expect(getItemMock).toHaveBeenCalledWith("config"); + expect(migrateFnMock).toHaveBeenCalledWith( + existingValue, + expect.any(Array) + ); + expect(setItemMock).toHaveBeenCalledWith( + "config", + JSON.stringify(migrated) + ); + expect(res).toEqual(migrated); + + //cache used + expect(ls.get()).toEqual(migrated); + expect(getItemMock).toHaveBeenCalledOnce(); }); - const res = ls.get(); - - expect(localStorage.getItem).toHaveBeenCalledWith("config"); - expect(migrateFnMock).toHaveBeenCalledWith( - existingValue, - expect.any(Array) - ); - expect(localStorage.setItem).toHaveBeenCalledWith( - "config", - JSON.stringify(defaultObject) - ); - expect(res).toEqual(defaultObject); + it("should revert to fallback if migration ran but schema still failed", () => { + const existingValue = { hi: "hello" }; + + getItemMock.mockReturnValue(JSON.stringify(existingValue)); + + const invalidMigrated = { + punctuation: 1, + mode: "time", + fontSize: 1, + }; + + const migrateFnMock = vi.fn(() => invalidMigrated as any); + + const ls = new LocalStorageWithSchema({ + key: "config", + schema: objectSchema, + fallback: defaultObject, + migrate: migrateFnMock, + }); + + const res = ls.get(); + + expect(getItemMock).toHaveBeenCalledWith("config"); + expect(migrateFnMock).toHaveBeenCalledWith( + existingValue, + expect.any(Array) + ); + expect(setItemMock).toHaveBeenCalledWith( + "config", + JSON.stringify(defaultObject) + ); + expect(res).toEqual(defaultObject); + + //cache used + expect(ls.get()).toEqual(defaultObject); + expect(getItemMock).toHaveBeenCalledOnce(); + }); }); }); }); diff --git a/frontend/src/ts/utils/local-storage-with-schema.ts b/frontend/src/ts/utils/local-storage-with-schema.ts index 7d2477334cb0..64637f182973 100644 --- a/frontend/src/ts/utils/local-storage-with-schema.ts +++ b/frontend/src/ts/utils/local-storage-with-schema.ts @@ -12,6 +12,7 @@ export class LocalStorageWithSchema { value: Record | unknown[], zodIssues?: ZodIssue[] ) => T; + private cache?: T; constructor(options: { key: string; @@ -29,13 +30,18 @@ export class LocalStorageWithSchema { } public get(): T { - console.debug(`LS ${this.key} Getting value from localStorage`); + if (this.cache !== undefined) { + console.debug(`LS ${this.key} Got cached value:`, this.cache); + return this.cache; + } + console.debug(`LS ${this.key} Getting value from localStorage`); const value = window.localStorage.getItem(this.key); if (value === null) { console.debug(`LS ${this.key} No value found, returning fallback`); - return this.fallback; + this.cache = this.fallback; + return this.cache; } let migrated = false; @@ -49,12 +55,14 @@ export class LocalStorageWithSchema { console.debug( `LS ${this.key} Migrating from old format to new format` ); - return this.migrate(oldData, zodIssues); + this.cache = this.migrate(oldData, zodIssues); + return this.cache; } else { console.debug( `LS ${this.key} No migration function provided, returning fallback` ); - return this.fallback; + this.cache = this.fallback; + return this.cache; } }, }) @@ -65,7 +73,8 @@ export class LocalStorageWithSchema { `LS ${this.key} Failed to parse from localStorage: ${error.message}` ); window.localStorage.setItem(this.key, JSON.stringify(this.fallback)); - return this.fallback; + this.cache = this.fallback; + return this.cache; } if (migrated || parsed === this.fallback) { @@ -74,7 +83,8 @@ export class LocalStorageWithSchema { } console.debug(`LS ${this.key} Got value:`, parsed); - return parsed; + this.cache = parsed; + return this.cache; } public set(data: T): boolean { @@ -83,6 +93,7 @@ export class LocalStorageWithSchema { const parsed = this.schema.parse(data); console.debug(`LS ${this.key} Setting in localStorage`); window.localStorage.setItem(this.key, JSON.stringify(parsed)); + this.cache = parsed; return true; } catch (e) { let message = "Unknown error occurred"; From 61e685205a422c024eb4636f8b120c788c46b19f Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 27 May 2025 17:17:48 +0200 Subject: [PATCH 42/50] chore: release v25.22.0 --- package.json | 2 +- packages/release/src/index.js | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 13ebe07f6481..aab050264baa 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,6 @@ "eslint" ] }, - "version": "25.19.0", + "version": "25.22.0", "packageManager": "pnpm@9.6.0" } diff --git a/packages/release/src/index.js b/packages/release/src/index.js index cfebaa934866..b18af205c78f 100755 --- a/packages/release/src/index.js +++ b/packages/release/src/index.js @@ -165,6 +165,15 @@ const checkUncommittedChanges = () => { } }; +const installDependencies = () => { + console.log("Installing dependencies..."); + if (isDryRun) { + console.log("[Dry Run] Dependencies would be installed."); + } else { + runProjectRootCommand("pnpm i"); + } +}; + const buildProject = () => { console.log("Building project..."); let filter = ""; @@ -246,6 +255,8 @@ const main = async () => { checkUncommittedChanges(); + installDependencies(); + let changelogContent; let newVersion; if (!hotfix) { From c1c7e369aa40c8f6de1f5d32de9e70e7a5010b0d Mon Sep 17 00:00:00 2001 From: Seif Soliman Date: Tue, 27 May 2025 18:37:43 +0300 Subject: [PATCH 43/50] fix(commandline): improve caching to fix stale checkmark and UI state (@byseif21, @fehmer) (#6586) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description Previously, when selecting a language via the text button on the test page, the checkmark (fa-check) in the commandline language list wouldn't update until a page refresh. This was due to the commandline's caching mechanism not detecting changes triggered outside its own control. Although it first appeared to be a language-specific issue, it was later identified that the caching logic was generally insufficient — it didn’t account for updates to the active command state or configuration flags like usingSingleList. ## Solution Fixed the caching logic in the commandline module by tracking a more complete internal state. The system now correctly detects changes in the command list, active state, and configuration, and rebuilds the list UI when necessary. ## Technical Details - The commandline uses a caching mechanism (`lastList`) to avoid rebuilding the HTML if the list hasn't changed done in #6559 - Replaced the old lastList cache with a new lastState object that stores: the list of commands, each with its isActive flag, the usingSingleList configuration flag - Improved the cache comparison logic, uses areSortedArraysEqual to compare command lists including active state ,compares the usingSingleList flag - Previously, this cache wasn't being cleared when the language changed through the text button - Now we clear the cache on changes, forcing a rebuild of the list with the correct checkmark ## Performance Impact - Minimal performance impact - The list is only rebuilt when: 1. The language actually changes 2. The list content changes 3. The input value changes - The caching mechanism still prevents unnecessary rebuilds in all other cases ## Testing - [x] Language selection through text button updates checkmark immediately - [x] Language selection through commandline works as before - [x] No unnecessary rebuilds when language hasn't changed - [x] Checkmark appears next to correct language in all cases --- frontend/src/ts/commandline/commandline.ts | 87 +++++++++++++--------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index caa9028e5991..6166232956d0 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -39,6 +39,15 @@ let subgroupOverride: CommandsSubgroup | null = null; let isAnimating = false; let lastSingleListModeInputValue = ""; +type CommandWithActiveState = Omit & { isActive: boolean }; + +let lastState: + | { + list: CommandWithActiveState[]; + usingSingleList: boolean; + } + | undefined; + function removeCommandlineBackground(): void { $("#commandLine").addClass("noBackground"); if (Config.showOutOfFocusWarning) { @@ -328,7 +337,7 @@ function hideCommands(): void { throw new Error("Commandline element not found"); } element.innerHTML = ""; - lastList = undefined; + lastState = undefined; } let cachedSingleSubgroup: CommandsSubgroup | null = null; @@ -353,8 +362,6 @@ async function getList(): Promise { return (await getSubgroup()).list; } -let lastList: Command[] | undefined; - async function showCommands(): Promise { const element = document.querySelector("#commandLine .suggestions"); if (element === null) { @@ -366,11 +373,42 @@ async function showCommands(): Promise { return; } - const list = (await getList()).filter((c) => c.found === true); - if (lastList && areSortedArraysEqual(list, lastList)) { + const subgroup = await getSubgroup(); + + const list = subgroup.list + .filter((c) => c.found === true) + .map((command) => { + let isActive = false; + if (command.active !== undefined) { + isActive = command.active(); + } else { + const configKey = command.configKey ?? subgroup.configKey; + if (configKey !== undefined) { + if (command.configValueMode === "include") { + isActive = (Config[configKey] as unknown[]).includes( + command.configValue + ); + } else { + isActive = Config[configKey] === command.configValue; + } + } + } + const { active: _active, ...restOfCommand } = command; + return { ...restOfCommand, isActive } as CommandWithActiveState; + }); + + if ( + lastState && + usingSingleList === lastState.usingSingleList && + areSortedArraysEqual(list, lastState.list) + ) { return; } - lastList = list; + + lastState = { + list: list, + usingSingleList: usingSingleList, + }; let html = ""; let index = 0; @@ -389,38 +427,13 @@ async function showCommands(): Promise { icon = ``; } let configIcon = ""; - const configKey = command.configKey ?? (await getSubgroup()).configKey; - if (command.active !== undefined) { - if (command.active()) { - firstActive = firstActive ?? index; - configIcon = ``; - } else { - configIcon = ``; - } - } else if (configKey !== undefined) { - let isActive; - - if (command.configValueMode === "include") { - isActive = ( - Config[configKey] as ( - | string - | number - | boolean - | number[] - | undefined - )[] - ).includes(command.configValue); - } else { - isActive = Config[configKey] === command.configValue; - } - - if (isActive) { - firstActive = firstActive ?? index; - configIcon = ``; - } else { - configIcon = ``; - } + if (command.isActive) { + firstActive = firstActive ?? index; + configIcon = ``; + } else { + configIcon = ``; } + const iconHTML = `
${ usingSingleList || configIcon === "" ? icon : configIcon }
`; From 4276c45045e48d9692157008f31d85cafbc6d2d4 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 29 May 2025 12:37:46 +0200 Subject: [PATCH 44/50] update indicator styling, html structure --- frontend/src/html/footer.html | 6 ++-- frontend/src/styles/footer.scss | 29 +++++++------------ .../src/ts/controllers/theme-controller.ts | 12 ++++---- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/frontend/src/html/footer.html b/frontend/src/html/footer.html index d2bba8cad255..25ff8906347b 100644 --- a/frontend/src/html/footer.html +++ b/frontend/src/html/footer.html @@ -74,9 +74,11 @@ aria-label="Shift-click to toggle custom theme" data-balloon-pos="left" > - +
+ + +
serika dark
-
`; } if (command.id.startsWith("changeFont")) { diff --git a/frontend/src/ts/commandline/lists/themes.ts b/frontend/src/ts/commandline/lists/themes.ts index ca018f1db1ec..4cdf2e858a83 100644 --- a/frontend/src/ts/commandline/lists/themes.ts +++ b/frontend/src/ts/commandline/lists/themes.ts @@ -35,9 +35,6 @@ const createThemeCommand = (theme: Theme): Command => { exec: (): void => { UpdateConfig.setTheme(theme.name); }, - html: `
- -
`, }; }; diff --git a/frontend/src/ts/commandline/types.ts b/frontend/src/ts/commandline/types.ts index f1cccf3596f1..bcb244bb5ef1 100644 --- a/frontend/src/ts/commandline/types.ts +++ b/frontend/src/ts/commandline/types.ts @@ -33,7 +33,6 @@ export type Command = { active?: () => boolean; shouldFocusTestUI?: boolean; customData?: Record; - html?: string; }; export type CommandsSubgroup = { From eaef01141842dd851f2b6d199219bc673be5450c Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 29 May 2025 12:53:43 +0200 Subject: [PATCH 47/50] unnecessary code --- frontend/src/ts/commandline/lists/custom-themes-list.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/frontend/src/ts/commandline/lists/custom-themes-list.ts b/frontend/src/ts/commandline/lists/custom-themes-list.ts index 4296ce7a5860..ca6aae004754 100644 --- a/frontend/src/ts/commandline/lists/custom-themes-list.ts +++ b/frontend/src/ts/commandline/lists/custom-themes-list.ts @@ -3,7 +3,6 @@ import { isAuthenticated } from "../../firebase"; import * as DB from "../../db"; import * as ThemeController from "../../controllers/theme-controller"; import { Command, CommandsSubgroup } from "../types"; -import * as ConfigEvent from "../../observables/config-event"; const subgroup: CommandsSubgroup = { title: "Custom themes list...", @@ -59,11 +58,4 @@ export function update(): void { } } -// subscribe to theme-related config events to update the custom theme command list -ConfigEvent.subscribe((eventKey, _eventValue) => { - if (["customTheme", "customThemeColors"].includes(eventKey)) { - update(); - } -}); - export default commands; From fc300b44e4a10517aa9086573d749f0bc566dc20 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 29 May 2025 13:08:01 +0200 Subject: [PATCH 48/50] move config event from footer to theme controller, connect text and fav icon functions into one --- .../src/ts/controllers/theme-controller.ts | 89 +++++++++++-------- frontend/src/ts/event-handlers/footer.ts | 17 ---- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/frontend/src/ts/controllers/theme-controller.ts b/frontend/src/ts/controllers/theme-controller.ts index 987356a00f4c..ce7bc3e78a6e 100644 --- a/frontend/src/ts/controllers/theme-controller.ts +++ b/frontend/src/ts/controllers/theme-controller.ts @@ -135,34 +135,6 @@ export async function loadStyle(name: string): Promise { }); } -// update the favorite icon in the current theme button -export function updateFooterThemeFavIcon( - themeName: string | null = null -): void { - const favIconEl = document.querySelector( - "footer .right .current-theme .icon .favIndicator" - ); - if (!(favIconEl instanceof HTMLElement)) return; - - const isCustom = themeName === "custom" || Config.customTheme; - // hide the favorite icon completely for custom themes - if (isCustom) { - favIconEl.style.display = "none"; - return; - } - favIconEl.style.display = ""; - const currentTheme = themeName ?? randomTheme ?? Config.theme; - const isFavorite = - currentTheme !== null && - Config.favThemes.includes(currentTheme as ThemeName); - - if (isFavorite) { - favIconEl.style.display = "block"; - } else { - favIconEl.style.display = "none"; - } -} - /****KEEPING FOR NOW AS REFRENCE AND THE PREVIEW!*****/ // export function changeCustomTheme(themeId: string, nosave = false): void { // const customThemes = DB.getSnapshot().customThemes; @@ -208,7 +180,7 @@ async function apply( void updateFavicon(); $("#metaThemeColor").attr("content", colors.bg); // } - updateFooterThemeName(isPreview ? themeName : undefined); + updateFooterIndicator(isPreview ? themeName : undefined); if (isColorDark(await ThemeColors.get("bg"))) { $("body").addClass("darkMode"); @@ -217,13 +189,47 @@ async function apply( } } -function updateFooterThemeName(nameOverride?: string): void { +function updateFooterIndicator(nameOverride?: string): void { + const indicator = document.querySelector( + "footer .right .current-theme" + ); + const text = indicator?.querySelector(".text"); + const favIcon = indicator?.querySelector(".favIndicator"); + + if ( + !(indicator instanceof HTMLElement) || + !(text instanceof HTMLElement) || + !(favIcon instanceof HTMLElement) + ) { + return; + } + + //text let str: string = Config.theme; if (randomTheme !== null) str = randomTheme; if (Config.customTheme) str = "custom"; if (nameOverride !== undefined && nameOverride !== "") str = nameOverride; str = str.replace(/_/g, " "); - $(".current-theme .text").text(str); + text.innerText = str; + + //fav icon + const isCustom = Config.customTheme; + // hide the favorite icon completely for custom themes + if (isCustom) { + favIcon.style.display = "none"; + return; + } + favIcon.style.display = ""; + const currentTheme = nameOverride ?? randomTheme ?? Config.theme; + const isFavorite = + currentTheme !== null && + Config.favThemes.includes(currentTheme as ThemeName); + + if (isFavorite) { + favIcon.style.display = "block"; + } else { + favIcon.style.display = "none"; + } } export function preview( @@ -238,7 +244,7 @@ const debouncedPreview = debounce<(t: string, c?: string[]) => void>( (themeIdenfitier, customColorsOverride) => { isPreviewingTheme = true; void apply(themeIdenfitier, customColorsOverride, true); - updateFooterThemeFavIcon(themeIdenfitier); + updateFooterIndicator(themeIdenfitier); } ); @@ -252,7 +258,7 @@ async function set( isAutoSwitch ); await apply(themeIdentifier, undefined, isAutoSwitch); - updateFooterThemeFavIcon(themeIdentifier); + updateFooterIndicator(themeIdentifier); if (!isAutoSwitch && Config.autoSwitchTheme) { setAutoSwitchTheme(false); @@ -267,13 +273,13 @@ export async function clearPreview(applyTheme = true): Promise { if (randomTheme !== null) { await apply(randomTheme); // restore the correct favorite icon state for the current theme - updateFooterThemeFavIcon(randomTheme); + updateFooterIndicator(randomTheme); } else if (Config.customTheme) { await apply("custom"); - updateFooterThemeFavIcon("custom"); + updateFooterIndicator("custom"); } else { await apply(Config.theme); - updateFooterThemeFavIcon(Config.theme); + updateFooterIndicator(Config.theme); } } } @@ -468,6 +474,17 @@ ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => { ) { await set(Config.themeDark, true); } + if ( + [ + "theme", + "customTheme", + "customThemeColors", + "randomTheme", + "favThemes", + ].includes(eventKey) + ) { + updateFooterIndicator(); + } }); window.addEventListener("customBackgroundFailed", () => { diff --git a/frontend/src/ts/event-handlers/footer.ts b/frontend/src/ts/event-handlers/footer.ts index e0ca1fda9394..4e047b6fbade 100644 --- a/frontend/src/ts/event-handlers/footer.ts +++ b/frontend/src/ts/event-handlers/footer.ts @@ -7,8 +7,6 @@ import * as SupportPopup from "../modals/support"; import * as ContactModal from "../modals/contact"; import * as VersionHistoryModal from "../modals/version-history"; import { envConfig } from "../constants/env-config"; -import { updateFooterThemeFavIcon } from "../controllers/theme-controller"; -import * as ConfigEvent from "../observables/config-event"; import { COMPATIBILITY_CHECK } from "@monkeytype/contracts"; import { lastSeenServerCompatibility } from "../ape/adapters/ts-rest-adapter"; @@ -74,21 +72,6 @@ document } }); -// subscribe to theme-related config events to update the favorite icon -ConfigEvent.subscribe((eventKey, _eventValue) => { - if ( - [ - "theme", - "customTheme", - "customThemeColors", - "randomTheme", - "favThemes", - ].includes(eventKey) - ) { - updateFooterThemeFavIcon(); - } -}); - document .querySelector("footer #supportMeButton") ?.addEventListener("click", () => { From 648befc9e3ad2a5fc0d4ea8500defb858c3ba22e Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 29 May 2025 13:11:28 +0200 Subject: [PATCH 49/50] unnecessary function calls --- frontend/src/ts/controllers/theme-controller.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/frontend/src/ts/controllers/theme-controller.ts b/frontend/src/ts/controllers/theme-controller.ts index ce7bc3e78a6e..b8b7c847dee9 100644 --- a/frontend/src/ts/controllers/theme-controller.ts +++ b/frontend/src/ts/controllers/theme-controller.ts @@ -244,7 +244,6 @@ const debouncedPreview = debounce<(t: string, c?: string[]) => void>( (themeIdenfitier, customColorsOverride) => { isPreviewingTheme = true; void apply(themeIdenfitier, customColorsOverride, true); - updateFooterIndicator(themeIdenfitier); } ); @@ -258,7 +257,6 @@ async function set( isAutoSwitch ); await apply(themeIdentifier, undefined, isAutoSwitch); - updateFooterIndicator(themeIdentifier); if (!isAutoSwitch && Config.autoSwitchTheme) { setAutoSwitchTheme(false); @@ -272,14 +270,10 @@ export async function clearPreview(applyTheme = true): Promise { if (applyTheme) { if (randomTheme !== null) { await apply(randomTheme); - // restore the correct favorite icon state for the current theme - updateFooterIndicator(randomTheme); } else if (Config.customTheme) { await apply("custom"); - updateFooterIndicator("custom"); } else { await apply(Config.theme); - updateFooterIndicator(Config.theme); } } } From d52796ebfef905c830e86bb8ee7b15dfdf15a017 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 29 May 2025 13:12:17 +0200 Subject: [PATCH 50/50] unnecessary extra comment --- frontend/src/ts/controllers/theme-controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/ts/controllers/theme-controller.ts b/frontend/src/ts/controllers/theme-controller.ts index b8b7c847dee9..95011cacb9a3 100644 --- a/frontend/src/ts/controllers/theme-controller.ts +++ b/frontend/src/ts/controllers/theme-controller.ts @@ -135,7 +135,6 @@ export async function loadStyle(name: string): Promise { }); } -/****KEEPING FOR NOW AS REFRENCE AND THE PREVIEW!*****/ // export function changeCustomTheme(themeId: string, nosave = false): void { // const customThemes = DB.getSnapshot().customThemes; // const colors = customThemes.find((e) => e._id === themeId)