diff --git a/README.md b/README.md
index 29cbb43..c65f4bf 100644
--- a/README.md
+++ b/README.md
@@ -83,6 +83,23 @@ Use a comma to separate the light and dark theme.
[](https://leetcode.com/jacoblincool)
+#### `colors` (default: `""`)
+
+Override the card's theme palette with your own comma-separated list of hex colors.
+
+Format:
+`colors=bg0,bg1,text0,text1,color0,color1,color2,color3`
+
+You can provide fewer values; missing values will be padded by repeating the last provided one.
+
+Example (Blue Palette):
+
+```md
+
+```
+
+When both `theme` and `colors` are provided, `colors` takes precedence.
+
#### `font` (default: `Baloo_2`)
Card font, you can use almost all fonts on [Google Fonts](https://fonts.google.com/).
diff --git a/packages/cloudflare-worker/src/demo/demo.html b/packages/cloudflare-worker/src/demo/demo.html
index 44c5901..4aacfb3 100644
--- a/packages/cloudflare-worker/src/demo/demo.html
+++ b/packages/cloudflare-worker/src/demo/demo.html
@@ -35,6 +35,10 @@
LeetCode Stats Card
${font_options}
+
+ Colors
+
+
Extension
@@ -381,8 +385,10 @@ LeetCode Stats Card
document.querySelector("#username").addEventListener("input", debouncedPreview);
// Immediate preview for other inputs
- ["theme", "font", "extension", "site"].forEach((id) => {
- document.querySelector("#" + id).addEventListener("change", preview);
+ ["theme","font","extension","site","colors"].forEach((id) => {
+ const el = document.querySelector("#" + id);
+ el.addEventListener("change", preview);
+ if (id === "colors") el.addEventListener("input", preview);
});
}
@@ -406,6 +412,7 @@ LeetCode Stats Card
encodeURIComponent(value("theme")) +
"&font=" +
encodeURIComponent(value("font")) +
+ (value("colors") ? "&colors=" + encodeURIComponent(value("colors")) : "") +
(value("extension") ? "&ext=" + encodeURIComponent(value("extension")) : "") +
(value("site") === "cn" ? "&site=cn" : "")
);
diff --git a/packages/cloudflare-worker/src/sanitize.ts b/packages/cloudflare-worker/src/sanitize.ts
index 7290a04..93fe282 100644
--- a/packages/cloudflare-worker/src/sanitize.ts
+++ b/packages/cloudflare-worker/src/sanitize.ts
@@ -84,6 +84,21 @@ export function sanitize(config: Record): Config {
: { light: themes[0].trim(), dark: themes[1].trim() };
}
+ // Handle custom colors (comma-separated hex values)
+ if (config.colors) {
+ const raw = config.colors
+ .split(",")
+ .map((x) => x.trim())
+ .filter(Boolean);
+ const hex = raw
+ .map((c) => (c.startsWith("#") ? c : `#${c}`))
+ .map((c) => c.toLowerCase())
+ .filter((c) => /^#([0-9a-f]{3}|[0-9a-f]{6})$/.test(c));
+ if (hex.length > 0) {
+ sanitized.colors = hex;
+ }
+ }
+
// Handle border
if (config.border) {
const size = parseFloat(config.border) ?? 1;
diff --git a/packages/core/src/_test/index.test.ts b/packages/core/src/_test/index.test.ts
index 0434564..059957e 100644
--- a/packages/core/src/_test/index.test.ts
+++ b/packages/core/src/_test/index.test.ts
@@ -4,6 +4,7 @@ import { ActivityExtension } from "../exts/activity";
import { AnimationExtension } from "../exts/animation";
import { FontExtension } from "../exts/font";
import { ThemeExtension } from "../exts/theme";
+import type { Config } from "../types";
describe("generate", () => {
test("should work (us)", async () => {
@@ -49,4 +50,34 @@ describe("generate", () => {
expect(svg.includes("#2e3440")).toBeTruthy();
expect(svg.includes("Source Code Pro")).toBeTruthy();
});
+
+ test("applies custom colors via `colors` option", async () => {
+ const svg = await generate({
+ username: "jacoblincool",
+ extensions: [ThemeExtension],
+ colors: [
+ "#111111",
+ "#222222",
+ "#333333",
+ "#444444",
+ "#aa0000",
+ "#00aa00",
+ "#0000aa",
+ "#ffaa00",
+ ],
+ } as unknown as Config);
+
+ // Background and text variables
+ expect(svg).toContain("--bg-0:#111111");
+ expect(svg).toContain("--bg-1:#222222");
+ expect(svg).toContain("--text-0:#333333");
+ expect(svg).toContain("--text-1:#444444");
+
+ // Accent colors
+ expect(svg).toContain("--color-0:#aa0000");
+ expect(svg).toContain("--color-3:#ffaa00");
+
+ // Make sure they affect an element
+ expect(svg).toMatch(/stroke:var\(--color-0\)/);
+ });
});
diff --git a/packages/core/src/exts/theme.ts b/packages/core/src/exts/theme.ts
index 1f631e7..670780a 100644
--- a/packages/core/src/exts/theme.ts
+++ b/packages/core/src/exts/theme.ts
@@ -9,7 +9,15 @@ import radical from "../theme/radical";
import transparent from "../theme/transparent";
import unicorn from "../theme/unicorn";
import wtf from "../theme/wtf";
-import { Extension, Item } from "../types";
+import { Config, Extension, Item } from "../types";
+
+function themeFromColors(list: string[]): Theme {
+ // Map: first 2 -> bg, next 2 -> text, next 4 -> accent colors
+ const bg = list.slice(0, 2);
+ const text = list.slice(2, 4);
+ const color = list.slice(4, 8);
+ return Theme({ palette: { bg, text, color } });
+}
export const supported: Record = {
dark,
@@ -59,6 +67,13 @@ export function ThemeExtension(): Extension {
body["theme-ext-dark"] = () => theme.extends as Item;
}
}
+
+ // If explicit colors are provided, apply them as an overriding theme
+ const colors = (generator.config as Config | undefined)?.colors;
+ if (Array.isArray(colors) && colors.length > 0) {
+ const t = themeFromColors(colors);
+ styles.push(css(t)); // push LAST = highest precedence
+ }
};
}
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index ec5cc30..c45fb10 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -11,6 +11,8 @@ export interface Config {
extensions: ExtensionInit[];
+ colors?: string[];
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}