refactor: move themes list to typescript (@fehmer) (#6489)

Co-authored-by: fehmer <3728838+fehmer@users.noreply.github.com>
This commit is contained in:
Christian Fehmer 2025-05-05 13:30:09 +02:00 committed by GitHub
parent dc6d4518a9
commit 5ab7bfb438
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1544 additions and 1614 deletions

View file

@ -36,16 +36,29 @@ Then add this code to your file:
Here is an image showing what all the properties correspond to:
<img width="1552" alt="Screenshot showing the page elements controlled by each color property" src="https://user-images.githubusercontent.com/83455454/149196967-abb69795-0d38-466b-a867-5aaa46452976.png">
Change the corresponding hex codes to create your theme. Then, go to `./frontend/static/themes/_list.json` and add the following code to the very end of the file (inside the square brackets):
Change the corresponding hex codes to create your theme.
Then, go to `./packages/contracts/src/schemas/themes.ts` and add your new theme name at the _end_ of the `ThemeNameSchema` enum. Make sure to end the line with a comma.
```json
{
"name": "theme_name",
"bgColor": "#ffffff",
"mainColor": "#ffffff",
"subColor": "#ffffff",
"textColor": "#ffffff"
```typescript
export const ThemeNameSchema = z.enum([
"8008",
"80s_after_dark",
...
"your_theme_name",
```
Then, go to `./frontend/src/ts/constants/themes.ts` and add the following code to the _end_ of the `themes` object near to the very end of the file:
```typescript
export const themes: Record<ThemeName, Omit<Theme, "name">> = {
...
your_theme_name: {
bgColor "#ffffff",
mainColor "#ffffff",
subColor "#ffffff",
textColor "#ffffff"
},
}
```
Make sure the name you put matches the name of the file you created (without the `.css` file extension). Add the text color and background color of your theme to their respective fields.

View file

@ -0,0 +1,38 @@
import { readdirSync } from "fs";
import { ThemesList } from "../../src/ts/constants/themes";
describe("themes", () => {
it("should not have duplicates", () => {
const duplicates = ThemesList.filter(
(item, index) => ThemesList.indexOf(item) !== index
);
expect(duplicates).toEqual([]);
});
it("should have all related css files", () => {
const themeFiles = listThemeFiles();
const missingThemeFiles = ThemesList.filter(
(it) => !themeFiles.includes(it.name)
).map((it) => `fontend/static/themes/${it}.css`);
expect(missingThemeFiles, "missing theme css files").toEqual([]);
});
it("should not have additional css files", () => {
const themeFiles = listThemeFiles();
const additionalThemeFiles = themeFiles
.filter((it) => !ThemesList.some((theme) => theme.name === it))
.map((it) => `fontend/static/themes/${it}.css`);
expect(
additionalThemeFiles,
"additional theme css files not declared in frontend/src/ts/constants/themes.ts"
).toEqual([]);
});
});
function listThemeFiles() {
return readdirSync(import.meta.dirname + "/../../static/themes").map((it) =>
it.substring(0, it.length - 4)
);
}

View file

@ -328,11 +328,9 @@ describe("Config", () => {
});
it("setFavThemes", () => {
expect(Config.setFavThemes([])).toBe(true);
expect(Config.setFavThemes(["test"])).toBe(true);
expect(Config.setFavThemes([stringOfLength(50)])).toBe(true);
expect(Config.setFavThemes(["8008", "80s_after_dark"])).toBe(true);
expect(Config.setFavThemes(["test"] as any)).toBe(false);
expect(Config.setFavThemes("invalid" as any)).toBe(false);
expect(Config.setFavThemes([stringOfLength(51)])).toBe(false);
});
it("setFunbox", () => {
expect(Config.setFunbox(["mirror"])).toBe(true);
@ -407,29 +405,20 @@ describe("Config", () => {
it("setTheme", () => {
expect(Config.setTheme("serika")).toBe(true);
expect(Config.setTheme("serika_dark")).toBe(true);
expect(Config.setTheme(stringOfLength(50))).toBe(true);
expect(Config.setTheme("serika dark")).toBe(false);
expect(Config.setTheme("serika-dark")).toBe(false);
expect(Config.setTheme(stringOfLength(51))).toBe(false);
expect(Config.setTheme("invalid" as any)).toBe(false);
});
it("setThemeLight", () => {
expect(Config.setThemeLight("serika")).toBe(true);
expect(Config.setThemeLight("serika_dark")).toBe(true);
expect(Config.setThemeLight(stringOfLength(50))).toBe(true);
expect(Config.setThemeLight("serika dark")).toBe(false);
expect(Config.setThemeLight("serika-dark")).toBe(false);
expect(Config.setThemeLight(stringOfLength(51))).toBe(false);
expect(Config.setThemeLight("invalid" as any)).toBe(false);
});
it("setThemeDark", () => {
expect(Config.setThemeDark("serika")).toBe(true);
expect(Config.setThemeDark("serika_dark")).toBe(true);
expect(Config.setThemeDark(stringOfLength(50))).toBe(true);
expect(Config.setThemeDark("serika dark")).toBe(false);
expect(Config.setThemeDark("serika-dark")).toBe(false);
expect(Config.setThemeDark(stringOfLength(51))).toBe(false);
expect(Config.setThemeDark("invalid" as any)).toBe(false);
});
it("setLanguage", () => {
expect(Config.setLanguage("english")).toBe(true);

View file

@ -47,46 +47,6 @@ function validateOthers() {
return reject(new Error(fontsValidator.errors[0].message));
}
//themes
const themesData = JSON.parse(
fs.readFileSync("./static/themes/_list.json", {
encoding: "utf8",
flag: "r",
})
);
const themesSchema = {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
bgColor: { type: "string" },
mainColor: { type: "string" },
},
required: ["name", "bgColor", "mainColor"],
},
};
const themesValidator = ajv.compile(themesSchema);
if (themesValidator(themesData)) {
console.log("Themes list JSON schema is \u001b[32mvalid\u001b[0m");
} else {
console.log("Themes list JSON schema is \u001b[31minvalid\u001b[0m");
return reject(new Error(themesValidator.errors[0].message));
}
//check if files exist
for (const theme of themesData) {
const themeName = theme.name;
const fileName = `${themeName}.css`;
const themePath = `./static/themes/${fileName}`;
if (!fs.existsSync(themePath)) {
console.log(`File ${fileName} was \u001b[31mnot found\u001b[0m`);
// return reject(new Error(`File for theme ${themeName} does not exist`));
return reject(
`Could not find file ${fileName} for theme ${themeName} - make sure the file exists and is named correctly`
);
}
}
//challenges
const challengesSchema = {
type: "array",

View file

@ -77,7 +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";
import ThemesCommands from "./lists/themes";
import LoadChallengeCommands, {
update as updateLoadChallengeCommands,
} from "./lists/load-challenge";
@ -130,17 +130,6 @@ fontsPromise
);
});
const themesPromise = JSONData.getThemesList();
themesPromise
.then((themes) => {
updateThemesCommands(themes);
})
.catch((e: unknown) => {
console.error(
Misc.createErrorMessage(e, "Failed to update themes commands")
);
});
const challengesPromise = JSONData.getChallengeList();
challengesPromise
.then((challenges) => {
@ -501,12 +490,7 @@ export function doesListExist(listName: string): boolean {
export async function getList(
listName: ListsObjectKeys
): Promise<CommandsSubgroup> {
await Promise.allSettled([
languagesPromise,
fontsPromise,
themesPromise,
challengesPromise,
]);
await Promise.allSettled([languagesPromise, fontsPromise, challengesPromise]);
const list = lists[listName];
if (!list) {
Notifications.add(`List not found: ${listName}`, -1);
@ -547,12 +531,7 @@ export function getTopOfStack(): CommandsSubgroup {
let singleList: CommandsSubgroup | undefined;
export async function getSingleSubgroup(): Promise<CommandsSubgroup> {
await Promise.allSettled([
languagesPromise,
fontsPromise,
themesPromise,
challengesPromise,
]);
await Promise.allSettled([languagesPromise, fontsPromise, challengesPromise]);
const singleCommands: Command[] = [];
for (const command of commands.list) {

View file

@ -1,3 +1,4 @@
import { ThemeName } from "@monkeytype/contracts/schemas/configs";
import Config, * as UpdateConfig from "../../config";
import { randomTheme } from "../../controllers/theme-controller";
import { Command } from "../types";
@ -10,14 +11,14 @@ const commands: Command[] = [
available: (): boolean => {
return (
!Config.customTheme &&
!Config.favThemes.includes(randomTheme ?? Config.theme)
!Config.favThemes.includes((randomTheme as ThemeName) ?? Config.theme)
);
},
exec: (): void => {
const { theme, favThemes, customTheme } = Config;
const themeToUpdate = randomTheme ?? theme;
if (!customTheme && !favThemes.includes(themeToUpdate)) {
UpdateConfig.setFavThemes([...favThemes, themeToUpdate]);
if (!customTheme && !favThemes.includes(themeToUpdate as ThemeName)) {
UpdateConfig.setFavThemes([...favThemes, themeToUpdate as ThemeName]);
}
},
},
@ -28,12 +29,12 @@ const commands: Command[] = [
available: (): boolean => {
return (
!Config.customTheme &&
Config.favThemes.includes(randomTheme ?? Config.theme)
Config.favThemes.includes((randomTheme as ThemeName) ?? Config.theme)
);
},
exec: (): void => {
const { theme, favThemes, customTheme } = Config;
const themeToUpdate = randomTheme ?? theme;
const themeToUpdate = (randomTheme as ThemeName) ?? theme;
if (!customTheme && favThemes.includes(themeToUpdate)) {
UpdateConfig.setFavThemes([
...favThemes.filter((t) => t !== themeToUpdate),

View file

@ -2,12 +2,37 @@ import Config, * as UpdateConfig from "../../config";
import { capitalizeFirstLetterOfEachWord } from "../../utils/strings";
import * as ThemeController from "../../controllers/theme-controller";
import { Command, CommandsSubgroup } from "../types";
import { Theme } from "../../utils/json-data";
import { Theme, ThemesList } from "../../constants/themes";
import { not } from "@monkeytype/util/predicates";
const isFavorite = (theme: Theme): boolean =>
Config.favThemes.includes(theme.name);
const subgroup: CommandsSubgroup = {
title: "Theme...",
configKey: "theme",
list: [],
list: [
...ThemesList.filter(isFavorite),
...ThemesList.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);
},
})),
};
const commands: Command[] = [
@ -19,54 +44,4 @@ const commands: Command[] = [
},
];
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);
},
});
} 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 = [...favs, ...subgroup.list];
}
export default commands;
export { update };

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,7 @@ import {
import {
Config as ConfigType,
Difficulty,
ThemeName,
FunboxName,
} from "@monkeytype/contracts/schemas/configs";
import { Mode } from "@monkeytype/contracts/schemas/shared";
@ -282,7 +283,7 @@ export async function setup(challengeName: string): Promise<boolean> {
UpdateConfig.setMode("custom", true);
UpdateConfig.setDifficulty("normal", true);
if (challenge.parameters[1] !== null) {
UpdateConfig.setTheme(challenge.parameters[1] as string);
UpdateConfig.setTheme(challenge.parameters[1] as ThemeName);
}
if (challenge.parameters[2] !== null) {
void Funbox.activate(challenge.parameters[2] as FunboxName[]);

View file

@ -2,7 +2,6 @@ import * as ThemeColors from "../elements/theme-colors";
import * as ChartController from "./chart-controller";
import * as Misc from "../utils/misc";
import * as Arrays from "../utils/arrays";
import * as JSONData from "../utils/json-data";
import { isColorDark, isColorLight } from "../utils/colors";
import Config, { setAutoSwitchTheme, setCustomTheme } from "../config";
import * as BackgroundFilter from "../elements/custom-background-filter";
@ -11,9 +10,10 @@ import * as DB from "../db";
import * as Notifications from "../elements/notifications";
import * as Loader from "../elements/loader";
import { debounce } from "throttle-debounce";
import { tryCatch } from "@monkeytype/util/trycatch";
import { ThemeName } from "@monkeytype/contracts/schemas/configs";
import { ThemesList } from "../constants/themes";
export let randomTheme: string | null = null;
export let randomTheme: ThemeName | string | null = null;
let isPreviewingTheme = false;
let randomThemeIndex = 0;
@ -189,7 +189,7 @@ async function apply(
}
function updateFooterThemeName(nameOverride?: string): void {
let str = Config.theme;
let str: string = Config.theme;
if (randomTheme !== null) str = randomTheme;
if (Config.customTheme) str = "custom";
if (nameOverride !== undefined && nameOverride !== "") str = nameOverride;
@ -244,17 +244,10 @@ export async function clearPreview(applyTheme = true): Promise<void> {
}
}
let themesList: string[] = [];
let themesList: (ThemeName | string)[] = [];
async function changeThemeList(): Promise<void> {
const { data: themes, error } = await tryCatch(JSONData.getThemesList());
if (error) {
console.error(
Misc.createErrorMessage(error, "Failed to update random theme list")
);
return;
}
const themes = ThemesList;
if (Config.randomTheme === "fav" && Config.favThemes.length > 0) {
themesList = Config.favThemes;
} else if (Config.randomTheme === "light") {

View file

@ -1,7 +1,6 @@
import Config, * as UpdateConfig from "../../config";
import * as ThemeController from "../../controllers/theme-controller";
import * as Misc from "../../utils/misc";
import * as JSONData from "../../utils/json-data";
import * as Colors from "../../utils/colors";
import * as Notifications from "../notifications";
import * as ThemeColors from "../theme-colors";
@ -11,12 +10,15 @@ import * as DB from "../../db";
import * as ConfigEvent from "../../observables/config-event";
import { isAuthenticated } from "../../firebase";
import * as ActivePage from "../../states/active-page";
import { CustomThemeColors } from "@monkeytype/contracts/schemas/configs";
import { tryCatch } from "@monkeytype/util/trycatch";
import {
CustomThemeColors,
ThemeName,
} from "@monkeytype/contracts/schemas/configs";
import { captureException } from "../../sentry";
import { getSortedThemesList } from "../../constants/themes";
function updateActiveButton(): void {
let activeThemeName = Config.theme;
let activeThemeName: string = Config.theme;
if (
Config.randomTheme !== "off" &&
Config.randomTheme !== "custom" &&
@ -138,7 +140,7 @@ export async function refreshPresetButtons(): Promise<void> {
let favThemesElHTML = "";
let themesElHTML = "";
let activeThemeName = Config.theme;
let activeThemeName: string = Config.theme;
if (
Config.randomTheme !== "off" &&
Config.randomTheme !== "custom" &&
@ -147,17 +149,7 @@ export async function refreshPresetButtons(): Promise<void> {
activeThemeName = ThemeController.randomTheme;
}
const { data: themes, error } = await tryCatch(
JSONData.getSortedThemesList()
);
if (error) {
Notifications.add(
Misc.createErrorMessage(error, "Failed to fill preset theme buttons"),
-1
);
return;
}
const themes = getSortedThemesList();
//first show favourites
if (Config.favThemes.length > 0) {
@ -276,13 +268,13 @@ export function setCustomInputs(noThemeUpdate = false): void {
});
}
function toggleFavourite(themeName: string): void {
function toggleFavourite(themeName: ThemeName): void {
if (Config.favThemes.includes(themeName)) {
// already favourite, remove
UpdateConfig.setFavThemes(Config.favThemes.filter((t) => t !== themeName));
} else {
// add to favourites
const newList: string[] = Config.favThemes;
const newList: ThemeName[] = Config.favThemes;
newList.push(themeName);
UpdateConfig.setFavThemes(newList);
}
@ -367,7 +359,9 @@ $(".pageSettings").on("click", " .section.themes .customTheme.button", (e) => {
// Handle click on favorite preset theme button
$(".pageSettings").on("click", ".section.themes .theme .favButton", (e) => {
const theme = $(e.currentTarget).parents(".theme.button").attr("theme");
const theme = $(e.currentTarget)
.parents(".theme.button")
.attr("theme") as ThemeName;
if (theme !== undefined) {
toggleFavourite(theme);
void refreshPresetButtons();
@ -380,7 +374,7 @@ $(".pageSettings").on("click", ".section.themes .theme .favButton", (e) => {
// Handle click on preset theme button
$(".pageSettings").on("click", ".section.themes .theme.button", (e) => {
const theme = $(e.currentTarget).attr("theme");
const theme = $(e.currentTarget).attr("theme") as ThemeName;
if (!$(e.target).hasClass("favButton") && theme !== undefined) {
UpdateConfig.setTheme(theme);
}

View file

@ -17,12 +17,12 @@ import Page from "./page";
import { isAuthenticated } from "../firebase";
import { get as getTypingSpeedUnit } from "../utils/typing-speed-units";
import SlimSelect from "slim-select";
import * as Skeleton from "../utils/skeleton";
import * as CustomBackgroundFilter from "../elements/custom-background-filter";
import {
ConfigValue,
CustomBackgroundSchema,
ThemeName,
CustomLayoutFluid,
FunboxName,
} from "@monkeytype/contracts/schemas/configs";
@ -30,9 +30,11 @@ import { getAllFunboxes, checkCompatibility } from "@monkeytype/funbox";
import { getActiveFunboxNames } from "../test/funbox/list";
import { SnapshotPreset } from "../constants/default-snapshot";
import { LayoutsList } from "../constants/layouts";
import { DataArrayPartial, Optgroup } from "slim-select/store";
import { DataArrayPartial, Optgroup, OptionOptional } from "slim-select/store";
import { tryCatch } from "@monkeytype/util/trycatch";
import { Theme, ThemesList } from "../constants/themes";
import { areSortedArraysEqual, areUnsortedArraysEqual } from "../utils/arrays";
import { LayoutName } from "@monkeytype/contracts/schemas/layouts";
type SettingsGroups<T extends ConfigValue> = Record<string, SettingsGroup<T>>;
@ -473,12 +475,8 @@ async function fillSettingsPage(): Promise<void> {
);
}
const element = document.querySelector(
".pageSettings .section[data-config-name='language'] select"
) as Element;
new SlimSelect({
select: element,
select: ".pageSettings .section[data-config-name='language'] select",
data: getLanguageDropdownData(
languageGroups ?? [],
(language) => language === Config.language
@ -488,93 +486,47 @@ async function fillSettingsPage(): Promise<void> {
},
});
const layoutSelectElement = document.querySelector(
".pageSettings .section[data-config-name='layout'] select"
) as Element;
const keymapLayoutSelectElement = document.querySelector(
".pageSettings .section[data-config-name='keymapLayout'] select"
) as Element;
let layoutHtml = '<option value="default">off</option>';
let keymapLayoutHtml = '<option value="overrideSync">emulator sync</option>';
for (const layout of LayoutsList) {
const optionHtml = `<option value="${layout}">${layout.replace(
/_/g,
" "
)}</option>`;
if (layout.toString() !== "korean") {
layoutHtml += optionHtml;
}
if (layout.toString() !== "default") {
keymapLayoutHtml += optionHtml;
}
}
layoutSelectElement.innerHTML = layoutHtml;
keymapLayoutSelectElement.innerHTML = keymapLayoutHtml;
new SlimSelect({
select: layoutSelectElement,
const layoutToOption: (layout: LayoutName) => OptionOptional = (layout) => ({
value: layout,
text: layout.replace(/_/g, " "),
});
new SlimSelect({
select: keymapLayoutSelectElement,
select: ".pageSettings .section[data-config-name='layout'] select",
data: [
{ text: "off", value: "default" },
...LayoutsList.filter((layout) => layout !== "korean").map(
layoutToOption
),
],
});
const { data: themes, error: getThemesListError } = await tryCatch(
JSONData.getThemesList()
);
if (getThemesListError) {
console.error(
Misc.createErrorMessage(
getThemesListError,
"Failed to load themes into dropdown boxes"
)
);
}
const themeSelectLightElement = document.querySelector(
".pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.light"
) as Element;
const themeSelectDarkElement = document.querySelector(
".pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.dark"
) as Element;
let themeSelectLightHtml = "";
let themeSelectDarkHtml = "";
if (themes) {
for (const theme of themes) {
const optionHtml = `<option value="${theme.name}" ${
theme.name === Config.themeLight ? "selected" : ""
}>${theme.name.replace(/_/g, " ")}</option>`;
themeSelectLightHtml += optionHtml;
const optionDarkHtml = `<option value="${theme.name}" ${
theme.name === Config.themeDark ? "selected" : ""
}>${theme.name.replace(/_/g, " ")}</option>`;
themeSelectDarkHtml += optionDarkHtml;
}
}
themeSelectLightElement.innerHTML = themeSelectLightHtml;
themeSelectDarkElement.innerHTML = themeSelectDarkHtml;
new SlimSelect({
select: ".pageSettings .section[data-config-name='keymapLayout'] select",
data: [
{ text: "emulator sync", value: "overrideSync" },
...LayoutsList.map(layoutToOption),
],
});
new SlimSelect({
select: themeSelectLightElement,
select:
".pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.light",
data: getThemeDropdownData((theme) => theme.name === Config.themeLight),
events: {
afterChange: (newVal): void => {
UpdateConfig.setThemeLight(newVal[0]?.value as string);
UpdateConfig.setThemeLight(newVal[0]?.value as ThemeName);
},
},
});
new SlimSelect({
select: themeSelectDarkElement,
select:
".pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.dark",
data: getThemeDropdownData((theme) => theme.name === Config.themeDark),
events: {
afterChange: (newVal): void => {
UpdateConfig.setThemeDark(newVal[0]?.value as string);
UpdateConfig.setThemeDark(newVal[0]?.value as ThemeName);
},
},
});
@ -1411,6 +1363,16 @@ function getLayoutfluidDropdownData(): DataArrayPartial {
}));
}
function getThemeDropdownData(
isActive: (theme: Theme) => boolean
): DataArrayPartial {
return ThemesList.map((theme) => ({
value: theme.name,
text: theme.name.replace(/_/g, " "),
selected: isActive(theme),
}));
}
ConfigEvent.subscribe((eventKey, eventValue) => {
if (eventKey === "fullConfigChange") setEventDisabled(true);
if (eventKey === "fullConfigChangeFinished") setEventDisabled(false);

View file

@ -1,6 +1,5 @@
import { FunboxName } from "@monkeytype/contracts/schemas/configs";
import { Accents } from "../test/lazy-mode";
import { hexToHSL } from "./colors";
/**
* Fetches JSON data from the specified URL using the fetch API.
@ -92,66 +91,6 @@ export async function getLayout(layoutName: string): Promise<Layout> {
return await cachedFetchJson<Layout>(`/layouts/${layoutName}.json`);
}
export type Theme = {
name: string;
bgColor: string;
mainColor: string;
subColor: string;
textColor: string;
};
let themesList: Theme[] | undefined;
/**
* Fetches the list of themes from the server, sorting them alphabetically by name.
* If the list has already been fetched, returns the cached list.
* @returns A promise that resolves to the sorted list of themes.
*/
export async function getThemesList(): Promise<Theme[]> {
if (!themesList) {
let themes = await cachedFetchJson<Theme[]>("/themes/_list.json");
themes = themes.sort((a, b) => {
const nameA = a.name.toLowerCase();
const nameB = b.name.toLowerCase();
if (nameA < nameB) return -1;
if (nameA > nameB) return 1;
return 0;
});
themesList = themes;
return themesList;
} else {
return themesList;
}
}
let sortedThemesList: Theme[] | undefined;
/**
* Fetches the sorted list of themes from the server.
* @returns A promise that resolves to the sorted list of themes.
*/
export async function getSortedThemesList(): Promise<Theme[]> {
if (!sortedThemesList) {
if (!themesList) {
await getThemesList();
}
if (!themesList) {
throw new Error("Themes list is undefined");
}
let sorted = [...themesList];
sorted = sorted.sort((a, b) => {
const b1 = hexToHSL(a.bgColor);
const b2 = hexToHSL(b.bgColor);
return b2.lgt - b1.lgt;
});
sortedThemesList = sorted;
return sortedThemesList;
} else {
return sortedThemesList;
}
}
/**
* Fetches the list of languages from the server.
* @returns A promise that resolves to the list of languages.

File diff suppressed because it is too large Load diff

View file

@ -56,7 +56,7 @@
"pr-check-lint-json": "cd frontend && eslint './static/**/*.json'",
"pr-check-quote-json": "cd frontend && npx gulp pr-check-quote-json",
"pr-check-language-json": "cd frontend && npx gulp pr-check-language-json",
"pr-check-other-json": "cd frontend && npx gulp pr-check-other-json && turbo test -- constants/layouts"
"pr-check-other-json": "cd frontend && npx gulp pr-check-other-json && turbo test -- constants/layouts constants/themes"
},
"engines": {
"node": "20.16.0"

View file

@ -1,6 +1,7 @@
import { z, ZodSchema } from "zod";
import { LanguageSchema, token } from "./util";
import { LanguageSchema } from "./util";
import * as Shared from "./shared";
import * as Themes from "./themes";
import * as Layouts from "./layouts";
export const SmoothCaretSchema = z.enum(["off", "slow", "medium", "fast"]);
@ -234,7 +235,10 @@ export const CustomThemeColorsSchema = z.tuple([
]);
export type CustomThemeColors = z.infer<typeof CustomThemeColorsSchema>;
export const FavThemesSchema = z.array(token().max(50));
export const ThemeNameSchema = Themes.ThemeNameSchema;
export type ThemeName = z.infer<typeof ThemeNameSchema>;
export const FavThemesSchema = z.array(ThemeNameSchema);
export type FavThemes = z.infer<typeof FavThemesSchema>;
export const FunboxNameSchema = z.enum([
@ -315,9 +319,6 @@ export const FontFamilySchema = z
.regex(/^[a-zA-Z0-9_\-+.]+$/);
export type FontFamily = z.infer<typeof FontFamilySchema>;
export const ThemeNameSchema = token().max(50);
export type ThemeName = z.infer<typeof ThemeNameSchema>;
export const KeymapLayoutSchema = z
.literal("overrideSync")
.or(Layouts.LayoutNameSchema);

View file

@ -0,0 +1,188 @@
import { z } from "zod";
export const ThemeNameSchema = z.enum([
"8008",
"80s_after_dark",
"9009",
"aether",
"alduin",
"alpine",
"anti_hero",
"arch",
"aurora",
"beach",
"bento",
"bingsu",
"bliss",
"blue_dolphin",
"blueberry_dark",
"blueberry_light",
"botanical",
"bouquet",
"breeze",
"bushido",
"cafe",
"camping",
"carbon",
"catppuccin",
"chaos_theory",
"cheesecake",
"cherry_blossom",
"comfy",
"copper",
"creamsicle",
"cy_red",
"cyberspace",
"dark",
"dark_magic_girl",
"dark_note",
"darling",
"deku",
"desert_oasis",
"dev",
"diner",
"dino",
"discord",
"dmg",
"dollar",
"dots",
"dracula",
"drowning",
"dualshot",
"earthsong",
"everblush",
"evil_eye",
"ez_mode",
"fire",
"fledgling",
"fleuriste",
"floret",
"froyo",
"frozen_llama",
"fruit_chew",
"fundamentals",
"future_funk",
"github",
"godspeed",
"graen",
"grand_prix",
"grape",
"gruvbox_dark",
"gruvbox_light",
"hammerhead",
"hanok",
"hedge",
"honey",
"horizon",
"husqy",
"iceberg_dark",
"iceberg_light",
"incognito",
"ishtar",
"iv_clover",
"iv_spade",
"joker",
"laser",
"lavender",
"leather",
"lil_dragon",
"lilac_mist",
"lime",
"luna",
"macroblank",
"magic_girl",
"mashu",
"matcha_moccha",
"material",
"matrix",
"menthol",
"metaverse",
"metropolis",
"mexican",
"miami",
"miami_nights",
"midnight",
"milkshake",
"mint",
"mizu",
"modern_dolch",
"modern_dolch_light",
"modern_ink",
"monokai",
"moonlight",
"mountain",
"mr_sleeves",
"ms_cupcakes",
"muted",
"nautilus",
"nebula",
"night_runner",
"nord",
"nord_light",
"norse",
"oblivion",
"olive",
"olivia",
"onedark",
"our_theme",
"paper",
"passion_fruit",
"pastel",
"peach_blossom",
"peaches",
"phantom",
"pink_lemonade",
"pulse",
"purpleish",
"rainbow_trail",
"red_dragon",
"red_samurai",
"repose_dark",
"repose_light",
"retro",
"retrocast",
"rgb",
"rose_pine",
"rose_pine_dawn",
"rose_pine_moon",
"rudy",
"ryujinscales",
"serika",
"serika_dark",
"sewing_tin",
"sewing_tin_light",
"shadow",
"shoko",
"slambook",
"snes",
"soaring_skies",
"solarized_dark",
"solarized_light",
"solarized_osaka",
"sonokai",
"stealth",
"strawberry",
"striker",
"suisei",
"sunset",
"superuser",
"sweden",
"tangerine",
"taro",
"terminal",
"terra",
"terrazzo",
"terror_below",
"tiramisu",
"trackday",
"trance",
"tron_orange",
"vaporwave",
"vesper",
"viridescent",
"voc",
"vscode",
"watermelon",
"wavez",
"witch_girl",
]);

View file

@ -0,0 +1,40 @@
import { not } from "../src/predicates";
describe("predicates", () => {
describe("not", () => {
it("should not a simple boolean function", () => {
const isTrue = (): boolean => true;
const isFalse = not(isTrue);
expect(isFalse()).toBe(false);
});
it("should not a numeric predicate", () => {
const isPositive = (num: number): boolean => num > 0;
const isNotPositive = not(isPositive);
expect(isNotPositive(-5)).toBe(true);
expect(isNotPositive(10)).toBe(false);
});
it("should not a predicate taking multiple arguments", () => {
const containsLetter = (
str1: string,
str2: string,
letter: string
): boolean => str1.includes(letter) || str2.includes(letter);
const doesNotContainLetter = not(containsLetter);
expect(doesNotContainLetter("hello", "world", "x")).toBe(true);
expect(doesNotContainLetter("apple", "banana", "a")).toBe(false);
});
it("should preserve type safety", () => {
const isEven = (num: number): boolean => num % 2 === 0;
const isOdd = not(isEven);
expect(isOdd(3)).toBe(true);
expect(isOdd(4)).toBe(false);
});
});
});

View file

@ -0,0 +1,12 @@
export type Predicate<Args extends unknown[]> = (...args: Args) => boolean;
/**
* Negates a predicate function, returning a new function that returns the opposite boolean result.
*
* @param predicate - The function to negate.
* @returns A new function that returns the negated boolean result.
*/
export function not<T extends unknown[]>(
predicate: Predicate<T>
): Predicate<T> {
return (...args: T) => !predicate(...args);
}