Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,23 @@ Use a comma to separate the light and dark theme.

[![](https://leetcard.jacoblin.cool/jacoblincool?theme=unicorn)](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
![](https://leetcard.jacoblin.cool/jacoblincool?colors=012a4a,013a63,a9d6e5,ffffff,0077b6,0096c7,00b4d8,90e0ef)
```

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/).
Expand Down
11 changes: 9 additions & 2 deletions packages/cloudflare-worker/src/demo/demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ <h1>LeetCode Stats Card</h1>
${font_options}
</select>
</div>
<div class="input-group">
<label for="colors">Colors</label>
<input id="colors" placeholder="#1e1e2e,#45475a,#cdd6f4,#bac2de,#fab387,#a6e3a1,#f9e2af,#f38ba8" />
</div>
<div class="input-group">
<label for="extension">Extension</label>
<select id="extension">
Expand Down Expand Up @@ -381,8 +385,10 @@ <h1>LeetCode Stats Card</h1>
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);
});
}

Expand All @@ -406,6 +412,7 @@ <h1>LeetCode Stats Card</h1>
encodeURIComponent(value("theme")) +
"&font=" +
encodeURIComponent(value("font")) +
(value("colors") ? "&colors=" + encodeURIComponent(value("colors")) : "") +
(value("extension") ? "&ext=" + encodeURIComponent(value("extension")) : "") +
(value("site") === "cn" ? "&site=cn" : "")
);
Expand Down
15 changes: 15 additions & 0 deletions packages/cloudflare-worker/src/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,21 @@ export function sanitize(config: Record<string, string>): 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;
Expand Down
31 changes: 31 additions & 0 deletions packages/core/src/_test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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\)/);
});
});
17 changes: 16 additions & 1 deletion packages/core/src/exts/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Theme> = {
dark,
Expand Down Expand Up @@ -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
}
};
}

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export interface Config {

extensions: ExtensionInit[];

colors?: string[];

// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
Expand Down