diff --git a/apps/web/src/components/CustomThemeDialog.tsx b/apps/web/src/components/CustomThemeDialog.tsx index cf866d432..cce1ae691 100644 --- a/apps/web/src/components/CustomThemeDialog.tsx +++ b/apps/web/src/components/CustomThemeDialog.tsx @@ -190,7 +190,7 @@ export function CustomThemeDialog({ Import Custom Theme - Paste CSS or a{" "} + Paste a theme name, CSS, or a{" "} diff --git a/apps/web/src/lib/customTheme.ts b/apps/web/src/lib/customTheme.ts index 9c134e236..4448f82f3 100644 --- a/apps/web/src/lib/customTheme.ts +++ b/apps/web/src/lib/customTheme.ts @@ -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 */ // --------------------------------------------------------------------------- @@ -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. */ @@ -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 { - 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 { + 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"); } @@ -309,10 +326,17 @@ export async function parseThemeInput(input: string): Promise { } } - // 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;