Skip to content
Merged
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
4 changes: 2 additions & 2 deletions apps/web/src/components/CustomThemeDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export function CustomThemeDialog({
Import Custom Theme
</DialogTitle>
<DialogDescription>
Paste CSS or a{" "}
Paste a theme name, CSS, or a{" "}
<a
href="https://tweakcn.com"
target="_blank"
Expand Down Expand Up @@ -220,7 +220,7 @@ export function CustomThemeDialog({
handleParse();
}, 50);
}}
placeholder={`Paste theme CSS, JSON, or a tweakcn.com URL...\n\nExample:\nhttps://tweakcn.com/themes/catppuccin\n\nor\n\n:root {\n --background: oklch(1 0 0);\n --primary: oklch(0.58 0.2 277);\n ...\n}`}
placeholder={`Paste a theme name, URL, CSS, or JSON...\n\nExamples:\ncatppuccin\nhttps://tweakcn.com/editor/theme?theme=catppuccin\nhttps://tweakcn.com/themes/catppuccin\n\nor\n\n:root {\n --background: oklch(1 0 0);\n --primary: oklch(0.58 0.2 277);\n ...\n}`}
className="min-h-36 w-full resize-y rounded-lg border border-input bg-background p-3 font-mono text-xs text-foreground placeholder:text-muted-foreground/50 focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring/24"
spellCheck={false}
/>
Expand Down
36 changes: 30 additions & 6 deletions apps/web/src/lib/customTheme.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
/**
* Custom theme support — parse, apply, and persist themes from tweakcn.com
*
* Supports three input formats:
* Supports four input formats:
* 1. Raw CSS (from tweakcn "Copy CSS" or any shadcn-compatible CSS)
* 2. JSON (shadcn registry format from tweakcn API)
* 3. tweakcn.com URLs (fetched via their CORS-enabled API)
* - https://tweakcn.com/themes/{name}
* - https://tweakcn.com/r/themes/{name}
* - https://tweakcn.com/editor/theme?theme={name}
* 4. Bare theme name (e.g. "catppuccin") — fetched from tweakcn API
*/

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -248,6 +252,7 @@ export function parseTweakcnJSON(json: unknown): CustomThemeData {
const TWEAKCN_URL_PATTERNS = [
/^https?:\/\/(?:www\.)?tweakcn\.com\/r\/themes\/([^/?#]+)/,
/^https?:\/\/(?:www\.)?tweakcn\.com\/themes\/([^/?#]+)/,
/^https?:\/\/(?:www\.)?tweakcn\.com\/editor\/theme\?theme=([^&#]+)/,
];

/** Check if a string looks like a tweakcn.com URL. */
Expand All @@ -266,9 +271,21 @@ function extractTweakcnThemeId(url: string): string | null {
return null;
}

/** Fetch a theme from tweakcn's API (has `Access-Control-Allow-Origin: *`). */
export async function fetchTweakcnTheme(url: string): Promise<CustomThemeData> {
const themeId = extractTweakcnThemeId(url);
/** Check if a string looks like a bare tweakcn theme name (e.g. "catppuccin"). */
function isBareThemeName(input: string): boolean {
return /^[a-zA-Z0-9][\w-]*$/.test(input);
}

/** Fetch a theme from tweakcn's API by URL or bare theme name. */
export async function fetchTweakcnTheme(urlOrName: string): Promise<CustomThemeData> {
let themeId: string | null;

if (isBareThemeName(urlOrName)) {
themeId = urlOrName;
} else {
themeId = extractTweakcnThemeId(urlOrName);
}

if (!themeId) {
throw new Error("Could not extract theme ID from URL");
}
Expand Down Expand Up @@ -309,10 +326,17 @@ export async function parseThemeInput(input: string): Promise<CustomThemeData> {
}
}

// 3. CSS
// 3. Bare theme name (e.g. "catppuccin") — try fetching from tweakcn
if (isBareThemeName(trimmed)) {
return fetchTweakcnTheme(trimmed);
}

// 4. CSS
const parsed = parseThemeCSS(trimmed);
if (Object.keys(parsed.light).length === 0 && Object.keys(parsed.dark).length === 0) {
throw new Error("No theme variables found. Paste CSS from tweakcn.com or a tweakcn theme URL.");
throw new Error(
"No theme variables found. Paste CSS, a tweakcn theme URL, or a theme name.",
);
}

return parsed;
Expand Down
Loading