aboutsummaryrefslogtreecommitdiff
path: root/src/api/Styles.ts
diff options
context:
space:
mode:
authorVen <vendicated@riseup.net>2022-12-25 20:47:35 +0100
committerGitHub <noreply@github.com>2022-12-25 20:47:35 +0100
commit2e5d27b6b63097e96e25819df7a8cdd667c521b3 (patch)
tree082b0f1c7cb0210d208c7cb8017e9da97b3b4196 /src/api/Styles.ts
parent2172cae779fb24f9bcc8c54a0b6538da0b52bafd (diff)
downloadVencord-2e5d27b6b63097e96e25819df7a8cdd667c521b3.tar.gz
Vencord-2e5d27b6b63097e96e25819df7a8cdd667c521b3.tar.bz2
Vencord-2e5d27b6b63097e96e25819df7a8cdd667c521b3.zip
feat: Proper CSS api & css bundle (#269)
Co-authored-by: Vap0r1ze <superdash993@gmail.com>
Diffstat (limited to 'src/api/Styles.ts')
-rw-r--r--src/api/Styles.ts162
1 files changed, 162 insertions, 0 deletions
diff --git a/src/api/Styles.ts b/src/api/Styles.ts
new file mode 100644
index 0000000..6b189ca
--- /dev/null
+++ b/src/api/Styles.ts
@@ -0,0 +1,162 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import type { MapValue } from "type-fest/source/entry";
+
+export type Style = MapValue<typeof VencordStyles>;
+
+export const styleMap = window.VencordStyles ??= new Map();
+
+export function requireStyle(name: string) {
+ const style = styleMap.get(name);
+ if (!style) throw new Error(`Style "${name}" does not exist`);
+ return style;
+}
+
+/**
+ * A style's name can be obtained from importing a stylesheet with `?managed` at the end of the import
+ * @param name The name of the style
+ * @returns `false` if the style was already enabled, `true` otherwise
+ * @example
+ * import pluginStyle from "./plugin.css?managed";
+ *
+ * // Inside some plugin method like "start()" or "[option].onChange()"
+ * enableStyle(pluginStyle);
+ */
+export function enableStyle(name: string) {
+ const style = requireStyle(name);
+
+ if (style.dom?.isConnected)
+ return false;
+
+ if (!style.dom) {
+ style.dom = document.createElement("style");
+ style.dom.dataset.vencordName = style.name;
+ }
+ compileStyle(style);
+
+ document.head.appendChild(style.dom);
+ return true;
+}
+
+/**
+ * @param name The name of the style
+ * @returns `false` if the style was already disabled, `true` otherwise
+ * @see {@link enableStyle} for info on getting the name of an imported style
+ */
+export function disableStyle(name: string) {
+ const style = requireStyle(name);
+ if (!style.dom?.isConnected)
+ return false;
+
+ style.dom.remove();
+ style.dom = null;
+ return true;
+}
+
+/**
+ * @param name The name of the style
+ * @returns `true` in most cases, may return `false` in some edge cases
+ * @see {@link enableStyle} for info on getting the name of an imported style
+ */
+export const toggleStyle = (name: string) => isStyleEnabled(name) ? disableStyle(name) : enableStyle(name);
+
+/**
+ * @param name The name of the style
+ * @returns Whether the style is enabled
+ * @see {@link enableStyle} for info on getting the name of an imported style
+ */
+export const isStyleEnabled = (name: string) => requireStyle(name).dom?.isConnected ?? false;
+
+/**
+ * Sets the variables of a style
+ * ```ts
+ * // -- plugin.ts --
+ * import pluginStyle from "./plugin.css?managed";
+ * import { setStyleVars } from "@api/Styles";
+ * import { findByPropsLazy } from "@webpack";
+ * const classNames = findByPropsLazy("thin", "scrollerBase"); // { thin: "thin-31rlnD scrollerBase-_bVAAt", ... }
+ *
+ * // Inside some plugin method like "start()"
+ * setStyleClassNames(pluginStyle, classNames);
+ * enableStyle(pluginStyle);
+ * ```
+ * ```scss
+ * // -- plugin.css --
+ * .plugin-root [--thin]::-webkit-scrollbar { ... }
+ * ```
+ * ```scss
+ * // -- final stylesheet --
+ * .plugin-root .thin-31rlnD.scrollerBase-_bVAAt::-webkit-scrollbar { ... }
+ * ```
+ * @param name The name of the style
+ * @param classNames An object where the keys are the variable names and the values are the variable values
+ * @param recompile Whether to recompile the style after setting the variables, defaults to `true`
+ * @see {@link enableStyle} for info on getting the name of an imported style
+ */
+export const setStyleClassNames = (name: string, classNames: Record<string, string>, recompile = true) => {
+ const style = requireStyle(name);
+ style.classNames = classNames;
+ if (recompile && isStyleEnabled(style.name))
+ compileStyle(style);
+};
+
+/**
+ * Updates the stylesheet after doing the following to the sourcecode:
+ * - Interpolate style classnames
+ * @param style **_Must_ be a style with a DOM element**
+ * @see {@link setStyleClassNames} for more info on style classnames
+ */
+export const compileStyle = (style: Style) => {
+ if (!style.dom) throw new Error("Style has no DOM element");
+
+ style.dom.textContent = style.source
+ .replace(/\[--(\w+)\]/g, (match, name) => {
+ const className = style.classNames[name];
+ return className ? classNameToSelector(className) : match;
+ });
+};
+
+/**
+ * @param name The classname
+ * @param prefix A prefix to add each class, defaults to `""`
+ * @return A css selector for the classname
+ * @example
+ * classNameToSelector("foo bar") // => ".foo.bar"
+ */
+export const classNameToSelector = (name: string, prefix = "") => name.split(" ").map(n => `.${prefix}${n}`).join("");
+
+type ClassNameFactoryArg = string | string[] | Record<string, unknown>;
+/**
+ * @param prefix The prefix to add to each class, defaults to `""`
+ * @returns A classname generator function
+ * @example
+ * const cl = classNameFactory("plugin-");
+ *
+ * cl("base", ["item", "editable"], { selected: null, disabled: true })
+ * // => "plugin-base plugin-item plugin-editable plugin-disabled"
+ */
+export const classNameFactory = (prefix: string = "") => (...args: ClassNameFactoryArg[]) => {
+ const classNames = new Set<string>();
+ for (const arg of args) {
+ if (typeof arg === "string") classNames.add(arg);
+ else if (Array.isArray(arg)) arg.forEach(name => classNames.add(name));
+ else if (typeof arg === "object") Object.entries(arg).forEach(([name, value]) => value && classNames.add(name));
+ }
+ return Array.from(classNames, name => prefix + name).join(" ");
+};