diff --git a/.env.example b/.env.example index 361a4e5b..e42caad4 100644 --- a/.env.example +++ b/.env.example @@ -24,4 +24,8 @@ GOOGLE_CLIENT_SECRET="YOUR_GOOGLE_CLIENT_SECRET" # Get a Google API Key: https://aistudio.google.com/apikey GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY" # Get a Groq API Key: https://console.groq.com/keys -GROQ_API_KEY="YOUR_GROQ_API_KEY" \ No newline at end of file +GROQ_API_KEY="YOUR_GROQ_API_KEY" + +###### GOOGLE FONTS ###### +# Get a Google Fonts API Key: https://developers.google.com/fonts/docs/developer_api +GOOGLE_FONTS_API_KEY="YOUR_GOOGLE_FONTS_API_KEY" \ No newline at end of file diff --git a/app/ai/components/ai-announcement.tsx b/app/ai/components/ai-announcement.tsx index a44c3247..feaec178 100644 --- a/app/ai/components/ai-announcement.tsx +++ b/app/ai/components/ai-announcement.tsx @@ -1,18 +1,25 @@ +"use client"; + +import { useSubscription } from "@/hooks/use-subscription"; import { ArrowRight } from "lucide-react"; import Link from "next/link"; export function AIAnnouncement() { + const { subscriptionStatus, isPending } = useSubscription(); + const isPro = subscriptionStatus?.isSubscribed ?? false; + + if (isPending || isPro) { + return null; + } + return (
- - Beta - - Try the new AI Theme Editor + Upgrade to Pro for unlimited requests diff --git a/app/ai/page.tsx b/app/ai/page.tsx index 30a35446..9ed0545c 100644 --- a/app/ai/page.tsx +++ b/app/ai/page.tsx @@ -3,11 +3,11 @@ import { AIAnnouncement } from "./components/ai-announcement"; import { AIChatHero } from "./components/ai-chat-hero"; export const metadata: Metadata = { - title: "AI Theme Editor for shadcn/ui — tweakcn", + title: "Image to shadcn/ui theme. Generate with AI — tweakcn", description: - "Effortlessly customize and generate shadcn/ui themes using tweakcn's AI-powered editor. Describe your desired theme, and let AI bring it to life. Supports Tailwind CSS, custom styles, and real-time previews.", + "Transform images into stunning shadcn/ui themes instantly with tweakcn's AI theme generator. Upload any image or describe your vision—our AI creates custom Tailwind CSS themes with real-time preview. Perfect for developers who want beautiful, production-ready themes in seconds.", keywords: - "ai theme editor, shadcn/ui, tailwind css, theme generator, ai design, ui customization, tweakcn, AI assisted theming, frontend development, web design AI", + "ai theme generator, image to theme, shadcn/ui themes, tailwind css generator, ai design tool, theme from image, ui customization, tweakcn, visual theme creator, color palette generator, design system ai, frontend theming, web design automation", robots: "index, follow", }; diff --git a/app/api/generate-theme/route.ts b/app/api/generate-theme/route.ts index bd8b4549..842c0209 100644 --- a/app/api/generate-theme/route.ts +++ b/app/api/generate-theme/route.ts @@ -4,7 +4,7 @@ import { getCurrentUserId, logError } from "@/lib/shared"; import { validateSubscriptionAndUsage } from "@/lib/subscription"; import { SubscriptionRequiredError } from "@/types/errors"; import { requestSchema, responseSchema, SYSTEM_PROMPT } from "@/utils/ai/generate-theme"; -import { createGoogleGenerativeAI } from "@ai-sdk/google"; +import { createGoogleGenerativeAI, GoogleGenerativeAIProviderOptions } from "@ai-sdk/google"; import { Ratelimit } from "@upstash/ratelimit"; import { kv } from "@vercel/kv"; import { generateText, Output } from "ai"; @@ -61,6 +61,13 @@ export async function POST(req: NextRequest) { system: SYSTEM_PROMPT, messages, abortSignal: req.signal, + providerOptions: { + google: { + thinkingConfig: { + thinkingBudget: 128, + }, + } satisfies GoogleGenerativeAIProviderOptions, + }, }); if (usage) { diff --git a/app/api/google-fonts/route.ts b/app/api/google-fonts/route.ts new file mode 100644 index 00000000..ce110321 --- /dev/null +++ b/app/api/google-fonts/route.ts @@ -0,0 +1,54 @@ +import { PaginatedFontsResponse } from "@/types/fonts"; +import { FALLBACK_FONTS } from "@/utils/fonts"; +import { fetchGoogleFonts } from "@/utils/fonts/google-fonts"; +import { unstable_cache } from "next/cache"; +import { NextRequest, NextResponse } from "next/server"; + +const cachedFetchGoogleFonts = unstable_cache(fetchGoogleFonts, ["google-fonts-catalogue"], { + tags: ["google-fonts-catalogue"], +}); + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const query = searchParams.get("q")?.toLowerCase() || ""; + const category = searchParams.get("category")?.toLowerCase(); + const limit = Math.min(Number(searchParams.get("limit")) || 50, 100); + const offset = Number(searchParams.get("offset")) || 0; + + let googleFonts = FALLBACK_FONTS; + + try { + googleFonts = await cachedFetchGoogleFonts(process.env.GOOGLE_FONTS_API_KEY); + } catch (error) { + console.error("Error fetching Google Fonts:", error); + console.log("Using fallback fonts"); + } + + // Filter fonts based on search query and category + let filteredFonts = googleFonts; + + if (query) { + filteredFonts = filteredFonts.filter((font) => font.family.toLowerCase().includes(query)); + } + + if (category && category !== "all") { + filteredFonts = filteredFonts.filter((font) => font.category === category); + } + + const paginatedFonts = filteredFonts.slice(offset, offset + limit); + + const response: PaginatedFontsResponse = { + fonts: paginatedFonts, + total: filteredFonts.length, + offset, + limit, + hasMore: offset + limit < filteredFonts.length, + }; + + return NextResponse.json(response); + } catch (error) { + console.error("Error in Google Fonts API:", error); + return NextResponse.json({ error: "Failed to fetch fonts" }, { status: 500 }); + } +} diff --git a/app/globals.css b/app/globals.css index 9cbc8591..1744af5a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -63,6 +63,7 @@ } * { + color-scheme: light dark; border-color: var(--color-border); } diff --git a/app/layout.tsx b/app/layout.tsx index e1cee7c9..ef1ad163 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,5 @@ import { AuthDialogWrapper } from "@/components/auth-dialog-wrapper"; +import { DynamicFontLoader } from "@/components/dynamic-font-loader"; import { GetProDialogWrapper } from "@/components/get-pro-dialog-wrapper"; import { PostHogInit } from "@/components/posthog-init"; import { ThemeProvider } from "@/components/theme-provider"; @@ -54,6 +55,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) + @@ -65,6 +67,8 @@ export default function RootLayout({ children }: { children: React.ReactNode }) sizes="180x180" /> + + {/* PRELOAD FONTS USED BY BUILT-IN THEMES */} -

404

-

Page not found

+
+
+ + + + + +

Toggle theme

+
+
+
+ + + 404 + +

Oops, Lost in Space?

+

+ Go home or try switching the theme! +

+ + + Back to Home + + +
+ +
); } diff --git a/app/page.tsx b/app/page.tsx index 0a704be8..2e2179db 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -9,6 +9,7 @@ import { Header } from "@/components/home/header"; import { Hero } from "@/components/home/hero"; import { HowItWorks } from "@/components/home/how-it-works"; import { Roadmap } from "@/components/home/roadmap"; +import { ThemeHotKeyHandler } from "@/components/home/theme-hotkey-handler"; import { ThemePresetSelector } from "@/components/home/theme-preset-selector"; import { useEffect, useState } from "react"; @@ -18,7 +19,7 @@ export default function Home() { useEffect(() => { const handleScroll = () => { - if (window.scrollY > 10) { + if (window.scrollY > 10){ setIsScrolled(true); } else { setIsScrolled(false); @@ -31,6 +32,7 @@ export default function Home() { return (
+
+
); } diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx index 78653ec6..62f0b7aa 100644 --- a/app/pricing/page.tsx +++ b/app/pricing/page.tsx @@ -1,5 +1,3 @@ -"use client"; - import { NoiseEffect } from "@/components/effects/noise-effect"; import { Accordion, @@ -15,6 +13,13 @@ import { FREE_SUB_FEATURES, PRO_SUB_FEATURES } from "@/utils/subscription"; import { Calendar, Check, Circle, Mail } from "lucide-react"; import Link from "next/link"; import { CheckoutButton } from "./components/checkout-button"; +import { Metadata } from "next"; +import { Testimonials } from "@/components/home/testimonials"; + +export const metadata: Metadata = { + title: "Pricing — tweakcn", + robots: "index, follow", +}; export default function PricingPage() { return ( @@ -131,6 +136,10 @@ export default function PricingPage() { +
+ +
+ {/* FAQs Section */}
@@ -161,7 +170,6 @@ export default function PricingPage() { ))}
- {/* Bottom Section */}
diff --git a/app/sitemap.ts b/app/sitemap.ts index ec7e0b36..7742cc65 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -22,5 +22,11 @@ export default function sitemap(): MetadataRoute.Sitemap { changeFrequency: "weekly", priority: 0.8, }, + { + url: `${baseUrl}/pricing`, + lastModified: new Date(), + changeFrequency: "weekly", + priority: 0.6, + }, ]; } diff --git a/components/dynamic-font-loader.tsx b/components/dynamic-font-loader.tsx new file mode 100644 index 00000000..4e506e44 --- /dev/null +++ b/components/dynamic-font-loader.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { useMounted } from "@/hooks/use-mounted"; +import { useEditorStore } from "@/store/editor-store"; +import { extractFontFamily, getDefaultWeights } from "@/utils/fonts"; +import { loadGoogleFont } from "@/utils/fonts/google-fonts"; +import { useEffect, useMemo } from "react"; + +export function DynamicFontLoader() { + const { themeState } = useEditorStore(); + const isMounted = useMounted(); + + const fontSans = themeState.styles.light["font-sans"]; + const fontSerif = themeState.styles.light["font-serif"]; + const fontMono = themeState.styles.light["font-mono"]; + + const currentFonts = useMemo(() => { + return { + sans: fontSans, + serif: fontSerif, + mono: fontMono, + } as const; + }, [fontSans, fontSerif, fontMono]); + + useEffect(() => { + if (!isMounted) return; + + try { + Object.entries(currentFonts).forEach(([_type, fontValue]) => { + const fontFamily = extractFontFamily(fontValue); + if (fontFamily) { + const weights = getDefaultWeights(["400", "500", "600", "700"]); + loadGoogleFont(fontFamily, weights); + } + }); + } catch (e) { + console.warn("DynamicFontLoader: Failed to load Google fonts:", e); + } + }, [isMounted, currentFonts]); + + return null; +} diff --git a/components/editor/action-bar/components/theme-toggle.tsx b/components/editor/action-bar/components/theme-toggle.tsx index 15506fa7..3938c8ef 100644 --- a/components/editor/action-bar/components/theme-toggle.tsx +++ b/components/editor/action-bar/components/theme-toggle.tsx @@ -1,11 +1,8 @@ -import { Moon, Sun } from "lucide-react"; -import * as SwitchPrimitives from "@radix-ui/react-switch"; import { useTheme } from "@/components/theme-provider"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; +import { TooltipWrapper } from "@/components/tooltip-wrapper"; +import { cn } from "@/lib/utils"; +import * as SwitchPrimitives from "@radix-ui/react-switch"; +import { Moon, Sun } from "lucide-react"; export function ThemeToggle() { const { theme, toggleTheme } = useTheme(); @@ -17,24 +14,24 @@ export function ThemeToggle() { return (
- - - + + - - {theme === "dark" ? ( - - ) : ( - - )} - - - - Toggle light/dark mode - + {theme === "dark" ? : } + + +
); } diff --git a/components/editor/ai/chat-image-preview.tsx b/components/editor/ai/chat-image-preview.tsx index 64621aac..bbec2856 100644 --- a/components/editor/ai/chat-image-preview.tsx +++ b/components/editor/ai/chat-image-preview.tsx @@ -18,13 +18,13 @@ export function ChatImagePreview({ name, src, className, alt, ...props }: ChatIm return ( -
+
{alt +
); @@ -160,10 +160,7 @@ function UserMessage({ return (
{images.map((image, idx) => ( -
+
+
); diff --git a/components/editor/control-section.tsx b/components/editor/control-section.tsx index ceb0dbbf..ab259f6a 100644 --- a/components/editor/control-section.tsx +++ b/components/editor/control-section.tsx @@ -15,7 +15,7 @@ const ControlSection = ({ title, children, expanded = false, className }: Contro toggleExpanded: () => setIsExpanded((prev) => !prev), }} > -
+
setIsExpanded(!isExpanded)} @@ -36,7 +36,7 @@ const ControlSection = ({ title, children, expanded = false, className }: Contro isExpanded ? "max-h-[2000px] opacity-100" : "max-h-0 opacity-0" )} > -
{children}
+
{children}
diff --git a/components/editor/font-picker.tsx b/components/editor/font-picker.tsx new file mode 100644 index 00000000..341fbd8a --- /dev/null +++ b/components/editor/font-picker.tsx @@ -0,0 +1,300 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; +import { FilterFontCategory, useFontSearch } from "@/hooks/use-font-search"; +import { cn } from "@/lib/utils"; +import { FontInfo } from "@/types/fonts"; +import { buildFontFamily, getDefaultWeights, waitForFont } from "@/utils/fonts"; +import { loadGoogleFont } from "@/utils/fonts/google-fonts"; +import { Check, ChevronDown, FunnelX, Loader2 } from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { TooltipWrapper } from "../tooltip-wrapper"; + +interface FontPickerProps { + value?: string; + category?: FilterFontCategory; + onSelect: (font: FontInfo) => void; + placeholder?: string; + className?: string; +} + +export function FontPicker({ + value, + category, + onSelect, + placeholder = "Search fonts...", + className, +}: FontPickerProps) { + const [open, setOpen] = useState(false); + const [inputValue, setInputValue] = useState(""); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedCategory, setSelectedCategory] = useState(category || "all"); + const [loadingFont, setLoadingFont] = useState(null); + const scrollRef = useRef(null); + + const selectedFontRef = useRef(null); + const hasScrolledToSelectedFont = useRef(false); + + const debouncedSetSearchQuery = useDebouncedCallback(setSearchQuery, 300); + + useEffect(() => { + debouncedSetSearchQuery(inputValue); + }, [inputValue, debouncedSetSearchQuery]); + + const fontQuery = useFontSearch({ + query: searchQuery, + category: selectedCategory, + limit: 15, + enabled: open, + }); + + useEffect(() => { + if (!open) return; + scrollRef.current?.scrollTo({ top: 0 }); + }, [selectedCategory, searchQuery, open]); + + useEffect(() => { + if (open && fontQuery.data && !hasScrolledToSelectedFont.current) { + requestAnimationFrame(() => { + selectedFontRef.current?.scrollIntoView({ + block: "center", + inline: "nearest", + }); + }); + hasScrolledToSelectedFont.current = true; + } else if (!open) { + hasScrolledToSelectedFont.current = false; + } + }, [open, fontQuery.data]); + + // Flatten all pages into a single array + const allFonts = useMemo(() => { + if (!fontQuery.data) return []; + return fontQuery.data.pages.flatMap((page) => page.fonts); + }, [fontQuery.data]); + + // Intersection Observer ref callback for infinite scroll + const loadMoreRefCallback = useCallback( + (node: HTMLDivElement | null) => { + if (!node) return; + + const observer = new IntersectionObserver( + (entries) => { + const entry = entries[0]; + if (entry.isIntersecting && fontQuery.hasNextPage && !fontQuery.isFetchingNextPage) { + fontQuery.fetchNextPage(); + } + }, + { + root: scrollRef.current, + rootMargin: "100px", + threshold: 0, + } + ); + + observer.observe(node); + return () => observer.unobserve(node); + }, + [fontQuery.hasNextPage, fontQuery.isFetchingNextPage, fontQuery.fetchNextPage] + ); + + const handleFontSelect = useCallback( + async (font: FontInfo) => { + setLoadingFont(font.family); + + try { + const weights = getDefaultWeights(font.variants); + loadGoogleFont(font.family, weights); + await waitForFont(font.family, weights[0]); + } catch (error) { + console.warn(`Failed to load font ${font.family}:`, error); + } + + setLoadingFont(null); + onSelect(font); + }, + [onSelect] + ); + + // Get current font info for display + const currentFont = useMemo(() => { + if (!value) return null; + + // First try to find the font in the search results + const foundFont = allFonts.find((font: FontInfo) => font.family === value); + if (foundFont) return foundFont; + + // If not found in search results, create a fallback FontInfo object + // This happens when a font is selected and then the search changes + const extractedFontName = value.split(",")[0].trim().replace(/['"]/g, ""); + + return { + family: extractedFontName, + category: category || "sans-serif", + variants: ["400"], + variable: false, + } as FontInfo; + }, [value, allFonts, category]); + + return ( + + + + + + + +
+
+ + + {inputValue && ( + + + + )} +
+ +
+ +
+
+ + + +
+ {fontQuery.isLoading ? ( +
+ + Loading fonts... +
+ ) : allFonts.length === 0 ? ( + No fonts found. + ) : ( + + {allFonts.map((font: FontInfo) => { + const isSelected = font.family === value; + const isLoading = loadingFont === font.family; + const fontFamily = buildFontFamily(font.family, font.category); + + const handlePreloadOnHover = () => { + loadGoogleFont(font.family, ["400"]); + }; + + return ( + handleFontSelect(font)} + disabled={isLoading} + onMouseEnter={handlePreloadOnHover} + ref={isSelected ? selectedFontRef : null} + > +
+ + {font.family} + {isLoading && } + + +
+ {font.category} + + {font.variable && ( + + + Variable + + )} +
+
+ {isSelected && } +
+ ); + })} + + {/* Load more trigger element */} + {fontQuery.hasNextPage &&
} + + {/* Loading indicator for infinite scroll */} + {fontQuery.isFetchingNextPage && ( +
+ + Loading more fonts... +
+ )} + + )} +
+ + + + ); +} diff --git a/components/editor/keyboard-shortcut-overlay.tsx b/components/editor/keyboard-shortcut-overlay.tsx new file mode 100644 index 00000000..36e8d8e6 --- /dev/null +++ b/components/editor/keyboard-shortcut-overlay.tsx @@ -0,0 +1,180 @@ +"use client"; + +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { X } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { ScrollArea } from '@/components/ui/scroll-area'; + +interface KeyboardShortcutsOverlayProps { + children: React.ReactNode; +} + +const KeyboardShortcutsOverlay: React.FC = ({ children }) => { + const [isVisible, setIsVisible] = useState(false); + const overlayRef = useRef(null); + + const shortcuts = [ + { + category: "EDITING", + items: [ + { action: "Apply random theme", keys: ["Space"] }, + { action: "Undo", keys: ["Ctrl", "Z"] }, + { action: "Redo", keys: ["Ctrl", "Y"] }, + { action: "Reset to current preset", keys: ["Ctrl", "R"] }, + { action: "Save theme", keys: ["Ctrl", "S"] }, + ] + }, + { + category: "NAVIGATION", + items: [ + { action: "Next theme", keys: ["Ctrl", "→"] }, + { action: "Previous theme", keys: ["Ctrl", "←"] }, + { action: "Open AI tab", fun: () => console.log("Open AI Tab (Ctrl+Shift+O)"), keys: ["Ctrl", "Shift", "O"] }, + { action: "Toggle code panel", keys: ["Ctrl", "B"] }, + ] + }, + { + category: "COPY", + items: [ + { action: "Copy theme CSS", keys: ["Ctrl", "Shift", "C"] }, + { action: "Copy registry command", keys: ["Ctrl", "Alt", "C"] }, + ] + }, + { + category: "HELP", + items: [ + { action: "Show/hide shortcuts", keys: ["Ctrl", "/"] }, + ] + } + ]; + + const handleClose = useCallback(() => { + setIsVisible(false); + }, []); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.ctrlKey && event.code === 'Slash') { + event.preventDefault(); + setIsVisible(prev => !prev); + return; + } + + + if (event.code === 'Escape' && isVisible) { + event.preventDefault(); + handleClose(); + return; + } + }; + + const handleClickOutside = (event: MouseEvent) => { + if (isVisible && overlayRef.current && !overlayRef.current.contains(event.target as Node)) { + handleClose(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + document.addEventListener('mousedown', handleClickOutside); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isVisible, handleClose]); + + useEffect(() => { + if (isVisible) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + return () => { + document.body.style.overflow = ''; + }; + }, [isVisible]); + + const KeyBadge: React.FC<{ keyName: string; className?: string }> = ({ keyName, className }) => ( + + {keyName} + + ); + + return ( + <> + {children} + {isVisible && ( +
+
+
+
+
+ ⌘ +
+

Keyboard Shortcuts

+
+ +
+ + +

+ Speed up your theme editing workflow with these keyboard shortcuts. +

+ +
+ {shortcuts.map((category, categoryIndex) => ( +
+

+ {category.category} +

+
+ {category.items.map((shortcut, index) => ( +
+ + {shortcut.action} + +
+ {shortcut.keys.map((key, keyIndex) => ( + + + {keyIndex < shortcut.keys.length - 1 && ( + + + )} + + ))} +
+
+ ))} +
+
+ ))} +
+
+
+
+ )} + + ); +}; + +export default KeyboardShortcutsOverlay; \ No newline at end of file diff --git a/components/editor/theme-control-panel.tsx b/components/editor/theme-control-panel.tsx index 6b8c285b..42700b8d 100644 --- a/components/editor/theme-control-panel.tsx +++ b/components/editor/theme-control-panel.tsx @@ -1,35 +1,30 @@ "use client"; -import { AlertCircle, Sparkles } from "lucide-react"; +import { AlertCircle, Sparkle } from "lucide-react"; import React, { use } from "react"; +import { ChatInterface } from "@/components/editor/ai/chat-interface"; +import ColorPicker from "@/components/editor/color-picker"; +import ControlSection from "@/components/editor/control-section"; +import { FontPicker } from "@/components/editor/font-picker"; +import HslAdjustmentControls from "@/components/editor/hsl-adjustment-controls"; +import ShadowControl from "@/components/editor/shadow-control"; +import { SliderWithInput } from "@/components/editor/slider-with-input"; +import ThemeEditActions from "@/components/editor/theme-edit-actions"; +import ThemePresetSelect from "@/components/editor/theme-preset-select"; +import TabsTriggerPill from "@/components/editor/theme-preview/tabs-trigger-pill"; +import { HorizontalScrollArea } from "@/components/horizontal-scroll-area"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Separator } from "@/components/ui/separator"; import { Tabs, TabsContent, TabsList } from "@/components/ui/tabs"; -import { - COMMON_STYLES, - DEFAULT_FONT_MONO, - DEFAULT_FONT_SANS, - DEFAULT_FONT_SERIF, - defaultThemeState, -} from "@/config/theme"; +import { COMMON_STYLES, defaultThemeState } from "@/config/theme"; import { useAIThemeGenerationCore } from "@/hooks/use-ai-theme-generation-core"; import { useControlsTabFromUrl, type ControlTab } from "@/hooks/use-controls-tab-from-url"; import { useEditorStore } from "@/store/editor-store"; +import { type FontInfo } from "@/types/fonts"; import { ThemeEditorControlsProps, ThemeStyleProps } from "@/types/theme"; -import { getAppliedThemeFont, monoFonts, sansSerifFonts, serifFonts } from "@/utils/theme-fonts"; -import { HorizontalScrollArea } from "../horizontal-scroll-area"; -import { ChatInterface } from "./ai/chat-interface"; -import ColorPicker from "./color-picker"; -import ControlSection from "./control-section"; -import HslAdjustmentControls from "./hsl-adjustment-controls"; -import ShadowControl from "./shadow-control"; -import { SliderWithInput } from "./slider-with-input"; -import ThemeEditActions from "./theme-edit-actions"; -import ThemeFontSelect from "./theme-font-select"; -import ThemePresetSelect from "./theme-preset-select"; -import TabsTriggerPill from "./theme-preview/tabs-trigger-pill"; +import { buildFontFamily } from "@/utils/fonts"; +import { getAppliedThemeFont } from "@/utils/theme-fonts"; const ThemeControlPanel = ({ styles, @@ -105,7 +100,7 @@ const ThemeControlPanel = ({ value="ai" className="data-[state=active]:[--effect:var(--secondary-foreground)] data-[state=active]:[--foreground:var(--muted-foreground)] data-[state=active]:[--muted-foreground:var(--effect)]" > - + Generate @@ -362,43 +357,49 @@ const ThemeControlPanel = ({
- +
- updateStyle("font-sans", value)} + { + const fontFamily = buildFontFamily(font.family, font.category); + updateStyle("font-sans", fontFamily); + }} />
- -
- updateStyle("font-serif", value)} + { + const fontFamily = buildFontFamily(font.family, font.category); + updateStyle("font-serif", fontFamily); + }} />
-
- updateStyle("font-mono", value)} + { + const fontFamily = buildFontFamily(font.family, font.category); + updateStyle("font-mono", fontFamily); + }} />
diff --git a/components/editor/theme-font-select.tsx b/components/editor/theme-font-select.tsx index 71258129..a8a10eb8 100644 --- a/components/editor/theme-font-select.tsx +++ b/components/editor/theme-font-select.tsx @@ -1,3 +1,5 @@ +// THIS COMPONENT MIGHT BE REPLACED BY THE GOOGLE FONT PICKER + import React, { useMemo } from "react"; import { Select, @@ -22,12 +24,12 @@ const ThemeFontSelect: React.FC = ({ onFontChange, }) => { const fontNames = useMemo(() => ["System", ...Object.keys(fonts)], [fonts]); - const value = currentFont ? fonts[currentFont] ?? defaultValue : defaultValue; + const value = currentFont ? (fonts[currentFont] ?? defaultValue) : defaultValue; return (