impr: add quick theme favorite indicator to the commandline & footer (@byseif21, @miodec) (#6460)

### Add Quick Favorite ~~Toggle~~ **Indicator** and Prioritized
Favorites to Theme Picker

This pull request adds a new feature to MonkeyType’s theme picker &
(**current theme button** in the **footer**) ~~making it super easy for
users to save/unsave and access their favorite themes instead of going
to the settings or any other thing~~ to display a small **star icon**
indicating whether the **the theme** is marked as a favorite. Favorited
themes appear at the top of the theme list for quick access.

~~Also, I added a small heart icon next to the current theme button in
the footer to quickly favorite the active theme with one click—making it
even easier to save a new favorite without needing to open the list.~~

The star icon next to the current theme button **only acts as a visual
indicator**, showing whether the current theme is a favorite or not.
**No longer clickable**.

---

**User Benefits:**

* **Favorites at the Top:**
Favorite themes appear first in the theme picker, so you can switch to
them instantly without searching through the list.

* ~~**One-Click Star Toggle:**~~
~~Click a star next to any theme to favorite or unfavorite it right in
the picker. No need to dig through settings, saving you time and
effort.~~

* **Visual Indicator for Active Theme:**
A small star icon beside the current theme button tells you at a glance
whether your active theme is in your favorites—without needing to open
the list.

---

**What I Did:**

* Added a star icon appears next to the favorite themes ~~each theme in
the theme picker for quick favoriting or unfavoriting.~~
* Made favorite themes show up at the top of the list for easy access.
* ~~Added a small heart icon next to the current theme button in the
footer to favorite the active theme quickly.~~
* Added a small star icon next to the current theme button in the footer
**as an indicator only** — it shows whether the active theme is a
favorite, but **is not clickable**.
* ~~- Added notifications to confirm when a theme is favorited or
unfavorited.~~

---

**Preview:** (**OUTDATED**)


[https://github.com/user-attachments/assets/5bba15c4-edbb-4577-abfe-fd581f196b98](https://github.com/user-attachments/assets/5bba15c4-edbb-4577-abfe-fd581f196b98)

---

### 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 or a theme?

* [ ] If it’s a language, did you edit `_list.json`, `_groups.json`, and
add `languages.json`?
  * [ ] If it’s a theme, did you add the theme.css?

* Also please add a screenshot of the theme, it would be extra awesome
if you do so!
* [x] Check if any open issues are related to this PR; if so, be sure to
tag them below.
* [x] Make sure the PR title follows the Conventional Commits standard.
([https://www.conventionalcommits.org/](https://www.conventionalcommits.org)
for more info)
* [x] Make sure to include your GitHub username prefixed with @ inside
parentheses at the end of the PR title.

---

---------

Co-authored-by: Miodec <jack@monkeytype.com>
Co-authored-by: Nginearing <142851004+Nginearing@users.noreply.github.com>
Co-authored-by: Pavel Ivashkov <paiv@users.noreply.github.com>
Co-authored-by: Christian Fehmer <fehmer@users.noreply.github.com>
Co-authored-by: siilyg <149881151+siily-g@users.noreply.github.com>
Co-authored-by: Omar Abdelrahman Abbas <tryomarabbas@gmail.com>
This commit is contained in:
Seif Soliman 2025-05-29 14:15:44 +03:00 committed by GitHub
parent 2d878b8700
commit 0bf76e8990
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 156 additions and 23 deletions

View file

@ -74,7 +74,10 @@
aria-label="Shift-click to toggle custom theme"
data-balloon-pos="left"
>
<i class="fas fa-fw fa-palette"></i>
<div class="icon">
<i class="fas fa-star favIndicator"></i>
<i class="fas fa-fw fa-palette"></i>
</div>
<div class="text">serika dark</div>
</button>
<button class="currentVersion textButton">

View file

@ -67,7 +67,8 @@
color: var(--sub-color);
}
.fas {
.fas,
.far {
margin-right: 0.5rem;
color: var(--sub-color);
}
@ -86,11 +87,13 @@
}
}
&.withThemeBubbles {
grid-template-columns: auto 1fr auto;
&.changeThemeCommand {
grid-template-columns: auto 1fr auto auto;
position: relative;
.themeBubbles {
display: grid;
grid-auto-flow: column;
place-content: center;
gap: 0.5em;
border-radius: 1em;
.themeBubble {
@ -99,6 +102,9 @@
border-radius: 100%;
}
}
.themeFavIcon {
margin-right: 0.25rem;
}
}
}
}

View file

@ -82,6 +82,21 @@ footer {
}
}
.current-theme {
.icon {
position: relative;
.favIndicator {
font-size: 0.5em;
top: -0.5em;
right: -0.5em;
padding: 0.25em;
position: absolute;
background: var(--bg-color);
border-radius: 100%;
}
}
}
&.focus {
.keyTips {
opacity: 0 !important;

View file

@ -419,10 +419,12 @@ async function showCommands(): Promise<void> {
if (command.found !== true) continue;
let icon = command.icon ?? "fa-chevron-right";
const faIcon = icon.startsWith("fa-");
const iconType = command.iconType ?? "solid";
const iconTypeClass = iconType === "solid" ? "fas" : "far";
if (!faIcon) {
icon = `<div class="textIcon">${icon}</div>`;
} else {
icon = `<i class="fas fa-fw ${icon}"></i>`;
icon = `<i class="${iconTypeClass} fa-fw ${icon}"></i>`;
}
let configIcon = "";
if (command.isActive) {
@ -451,12 +453,27 @@ async function showCommands(): Promise<void> {
if (command.customData !== undefined) {
if (command.id.startsWith("changeTheme")) {
html += `<div class="command withThemeBubbles" data-command-id="${command.id}" data-index="${index}" style="${customStyle}">
html += `<div class="command changeThemeCommand" data-command-id="${
command.id
}" data-index="${index}" style="${customStyle}">
${iconHTML}<div>${display}</div>
<div class="themeBubbles" style="background: ${command.customData["bgColor"]};outline: 0.25rem solid ${command.customData["bgColor"]};">
<div class="themeBubble" style="background: ${command.customData["mainColor"]}"></div>
<div class="themeBubble" style="background: ${command.customData["subColor"]}"></div>
<div class="themeBubble" style="background: ${command.customData["textColor"]}"></div>
<div class="themeFavIcon ${
command.customData["isFavorite"] === true ? "" : "hidden"
}">
<i class="fas fa-star"></i>
</div>
<div class="themeBubbles" style="background: ${
command.customData["bgColor"]
};outline: 0.25rem solid ${command.customData["bgColor"]};">
<div class="themeBubble" style="background: ${
command.customData["mainColor"]
}"></div>
<div class="themeBubble" style="background: ${
command.customData["subColor"]
}"></div>
<div class="themeBubble" style="background: ${
command.customData["textColor"]
}"></div>
</div>
</div>`;
}

View file

@ -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,8 @@ const commands: Command[] = [
{
id: "removeThemeFromFavorite",
display: "Remove current theme from favorite",
icon: "fa-heart-broken",
icon: "fa-star",
iconType: "regular",
available: (): boolean => {
return (
!Config.customTheme &&

View file

@ -4,17 +4,19 @@ import * as ThemeController from "../../controllers/theme-controller";
import { Command, CommandsSubgroup } from "../types";
import { Theme, ThemesList } from "../../constants/themes";
import { not } from "@monkeytype/util/predicates";
import * as ConfigEvent from "../../observables/config-event";
import * as Misc from "../../utils/misc";
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
* @returns a command object for the theme
*/
const createThemeCommand = (theme: Theme): Command => {
return {
id: "changeTheme" + capitalizeFirstLetterOfEachWord(theme.name),
display: theme.name.replace(/_/g, " "),
configValue: theme.name,
@ -24,6 +26,7 @@ const subgroup: CommandsSubgroup = {
bgColor: theme.bgColor,
subColor: theme.subColor,
textColor: theme.textColor,
isFavorite: isFavorite(theme),
},
hover: (): void => {
// previewTheme(theme.name);
@ -32,7 +35,25 @@ const subgroup: CommandsSubgroup = {
exec: (): void => {
UpdateConfig.setTheme(theme.name);
},
})),
};
};
/**
* 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)
),
};
const commands: Command[] = [
@ -44,4 +65,28 @@ const commands: Command[] = [
},
];
export function update(themes: Theme[]): void {
// clear the current list
subgroup.list = [];
// rebuild with favorites first, then non-favorites
subgroup.list = sortThemesByFavorite(themes).map((theme) =>
createThemeCommand(theme)
);
}
// subscribe to theme-related config events to update the theme command list
ConfigEvent.subscribe((eventKey, _eventValue) => {
if (eventKey === "favThemes") {
// update themes list when favorites change
try {
update(ThemesList);
} catch (e: unknown) {
console.error(
Misc.createErrorMessage(e, "Failed to update themes commands")
);
}
}
});
export default commands;

View file

@ -16,6 +16,7 @@ export type Command = {
subgroup?: CommandsSubgroup;
found?: boolean;
icon?: string;
iconType?: "regular" | "solid";
sticky?: boolean;
alias?: string;
input?: boolean;

View file

@ -179,7 +179,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");
@ -188,13 +188,47 @@ async function apply(
}
}
function updateFooterThemeName(nameOverride?: string): void {
function updateFooterIndicator(nameOverride?: string): void {
const indicator = document.querySelector<HTMLElement>(
"footer .right .current-theme"
);
const text = indicator?.querySelector<HTMLElement>(".text");
const favIcon = indicator?.querySelector<HTMLElement>(".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(
@ -433,6 +467,17 @@ ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => {
) {
await set(Config.themeDark, true);
}
if (
[
"theme",
"customTheme",
"customThemeColors",
"randomTheme",
"favThemes",
].includes(eventKey)
) {
updateFooterIndicator();
}
});
window.addEventListener("customBackgroundFailed", () => {