``` ├── .gitignore ├── LICENSE ├── README.md ├── actions/ ├── themes.ts ├── app/ ├── (auth)/ ├── components/ ├── auth-dialog.tsx ├── (legal)/ ├── layout.tsx ├── privacy-policy/ ├── page.tsx ├── api/ ├── auth/ ├── [...all]/ ├── route.ts ├── apple-touch-icon.png ├── dashboard/ ├── components/ ├── theme-card.tsx ├── themes-list.tsx ├── loading.tsx ├── page.tsx ├── editor/ ├── theme/ ├── [[...themeId]]/ ├── page.tsx ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── globals.css ├── layout.tsx ├── not-found.tsx ├── page.tsx ├── sitemap.ts ├── assets/ ├── buymeacoffee.svg ├── discord.svg ├── github.svg ├── google.svg ├── heart.svg ├── logo.svg ├── og-image.png ├── twitter.svg ├── components/ ├── auth-dialog-wrapper.tsx ├── editor/ ├── action-bar/ ├── action-bar.tsx ├── components/ ├── action-bar-buttons.tsx ├── code-button.tsx ├── edit-button.tsx ├── import-button.tsx ├── reset-button.tsx ├── save-button.tsx ├── theme-toggle.tsx ├── code-panel-dialog.tsx ├── code-panel.tsx ├── color-picker.tsx ├── contrast-checker.tsx ├── control-section.tsx ├── css-import-dialog.tsx ├── editor.tsx ├── header.tsx ├── shadow-control.tsx ├── slider-with-input.tsx ├── theme-control-actions.tsx ├── theme-control-panel.tsx ├── theme-edit-actions.tsx ├── theme-font-select.tsx ├── theme-preset-select.tsx ├── theme-preview-panel.tsx ├── theme-preview/ ├── color-preview.tsx ├── components-showcase.tsx ├── examples-preview-container.tsx ├── tabs-trigger-pill.tsx ├── theme-save-dialog.tsx ├── examples/ ├── cards/ ├── chat.tsx ├── cookie-settings.tsx ├── create-account.tsx ├── date-picker-with-range.tsx ├── date-picker.tsx ├── font-showcase.tsx ├── github-card.tsx ├── notifications.tsx ├── payment-method.tsx ├── report-an-issue.tsx ├── share-document.tsx ├── stats.tsx ├── team-members.tsx ├── dashboard/ ├── components/ ├── app-sidebar.tsx ├── chart-area-interactive.tsx ├── data-table.tsx ``` ## /.gitignore ```gitignore path="/.gitignore" # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies node_modules /.pnp .pnp.* .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/versions # bun lock bun.lock # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) .env* # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts # build artifacts public/r/themes ``` ## /LICENSE ``` path="/LICENSE" Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ## /README.md

tweakcn.com

Discord GitHub Repo stars X (formerly Twitter) URL

**[tweakcn](https://tweakcn.com)** is a powerful Visual Theme Editor for tailwind CSS & shadcn/ui components. It comes with Beautiful theme presets to get started, while aiming to offer advanced customisation for each aspect of your UI ![tweakcn.com](public/og-image.png) ## Motivation Websites made with shadcn/ui famously look the same. tweakcn is a tool that helps you customize shadcn/ui components visually, to make your components stand-out. Currently in beta, starting with a Tailwind CSS theme editor. Support for all other shadcn/ui components is planned. ## Current Features You can find the full feature list here: https://tweakcn.com/#features ## Roadmap You can find the updated roadmap here: https://tweakcn.com/#roadmap ## Run Locally ### Prerequisites - Node.js 18+ - npm / yarn / pnpm ### Installation 1. Clone the repository: ```bash git clone https://github.com/jnsahaj/tweakcn.git cd tweakcn ``` 2. Install dependencies: ```bash npm install ``` 3. Start the development server: ```bash npm run dev ``` 4. Open [http://localhost:3000](http://localhost:3000) in your browser. ## Contributors Made with [contrib.rocks](https://contrib.rocks). ### Interested in Contributing? Contributions are welcome! Please feel free to submit a Pull Request. # Star History

GitHub Star History for jnsahaj/tweakcn

## /actions/themes.ts ```ts path="/actions/themes.ts" "use server"; import { z } from "zod"; import { revalidatePath } from "next/cache"; import { db } from "@/db"; import { theme as themeTable } from "@/db/schema"; import { eq, and } from "drizzle-orm"; import cuid from "cuid"; import { auth } from "@/lib/auth"; import { headers } from "next/headers"; // Keep for session, but actions handle auth differently import { themeStylesSchema, type ThemeStyles } from "@/types/theme"; // Helper to get user ID (Consider centralizing auth checks) async function getCurrentUserId(): Promise { const session = await auth.api.getSession({ headers: await headers(), // you need to pass the headers object. }); return session?.user?.id ?? null; } const createThemeSchema = z.object({ name: z.string().min(1, "Theme name cannot be empty"), styles: themeStylesSchema, }); const updateThemeSchema = z.object({ id: z.string(), // ID is needed to know which theme to update name: z.string().min(1, "Theme name cannot be empty").optional(), styles: themeStylesSchema.optional(), }); // --- Server Actions --- // Action to get user themes export async function getThemes() { const userId = await getCurrentUserId(); if (!userId) { // In actions, throwing errors is common for auth failures // Or return a specific structure like { success: false, error: "Unauthorized" } throw new Error("Unauthorized"); } try { const userThemes = await db .select() .from(themeTable) .where(eq(themeTable.userId, userId)); return userThemes; } catch (error) { console.error("Error fetching themes:", error); throw new Error("Failed to fetch themes."); // Propagate a generic error } } export async function getTheme(themeId: string) { const userId = await getCurrentUserId(); if (!userId) { throw new Error("Unauthorized"); } try { const [theme] = await db .select() .from(themeTable) .where(and(eq(themeTable.id, themeId), eq(themeTable.userId, userId))) .limit(1); return theme; } catch (error) { console.error("Error fetching theme:", error); throw new Error("Failed to fetch theme."); } } // Action to create a new theme export async function createTheme(formData: { name: string; styles: ThemeStyles; }) { const userId = await getCurrentUserId(); if (!userId) { throw new Error("Unauthorized"); } const validation = createThemeSchema.safeParse(formData); if (!validation.success) { // Return validation errors for the client to handle return { success: false, error: "Invalid input", details: validation.error.format(), }; } // Check if user already has 10 themes const userThemes = await db .select() .from(themeTable) .where(eq(themeTable.userId, userId)); if (userThemes.length >= 10) { return { success: false, error: "Theme limit reached", message: "You cannot have more than 10 themes yet.", }; } const { name, styles } = validation.data; const newThemeId = cuid(); const now = new Date(); try { const [insertedTheme] = await db .insert(themeTable) .values({ id: newThemeId, userId: userId, name: name, styles: styles, // Already validated createdAt: now, updatedAt: now, }) .returning(); revalidatePath("/"); // Or a more specific path where themes are displayed return { success: true, theme: insertedTheme, }; } catch (error) { console.error("Error creating theme:", error); return { success: false, error: "Internal Server Error" }; } } // Action to update an existing theme export async function updateTheme(formData: { id: string; name?: string; styles?: ThemeStyles; }) { const userId = await getCurrentUserId(); if (!userId) { throw new Error("Unauthorized"); } const validation = updateThemeSchema.safeParse(formData); if (!validation.success) { return { success: false, error: "Invalid input", details: validation.error.format(), }; } const { id: themeId, name, styles } = validation.data; if (!name && !styles) { return { success: false, error: "No update data provided" }; } const updateData: Partial = { updatedAt: new Date(), }; if (name) updateData.name = name; if (styles) updateData.styles = styles; // Already validated try { const [updatedTheme] = await db .update(themeTable) .set(updateData) .where(and(eq(themeTable.id, themeId), eq(themeTable.userId, userId))) .returning(); if (!updatedTheme) { return { success: false, error: "Theme not found or not owned by user" }; } revalidatePath("/"); // Or a more specific path return { success: true, theme: updatedTheme, }; } catch (error) { console.error(`Error updating theme ${themeId}:`, error); return { success: false, error: "Internal Server Error" }; } } // Action to delete a theme export async function deleteTheme(themeId: string) { const userId = await getCurrentUserId(); if (!userId) { throw new Error("Unauthorized"); } if (!themeId) { return { success: false, error: "Theme ID required" }; } try { const [deletedInfo] = await db .delete(themeTable) .where(and(eq(themeTable.id, themeId), eq(themeTable.userId, userId))) .returning({ id: themeTable.id }); if (!deletedInfo) { return { success: false, error: "Theme not found or not owned by user" }; } revalidatePath("/dashboard"); // Or a more specific path return { success: true, deletedId: themeId }; } catch (error) { console.error(`Error deleting theme ${themeId}:`, error); return { success: false, error: "Internal Server Error" }; } } ``` ## /app/(auth)/components/auth-dialog.tsx ```tsx path="/app/(auth)/components/auth-dialog.tsx" "use client"; import { useState, useEffect } from "react"; import { Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { authClient } from "@/lib/auth-client"; import Google from "@/assets/google.svg"; import Github from "@/assets/github.svg"; interface AuthDialogProps { open: boolean; onOpenChange: (open: boolean) => void; initialMode?: "signin" | "signup"; trigger?: React.ReactNode; // Optional trigger element } export function AuthDialog({ open, onOpenChange, initialMode = "signin", trigger, }: AuthDialogProps) { const [isSignIn, setIsSignIn] = useState(initialMode === "signin"); const [isGoogleLoading, setIsGoogleLoading] = useState(false); const [isGithubLoading, setIsGithubLoading] = useState(false); useEffect(() => { if (open) { setIsSignIn(initialMode === "signin"); } }, [open, initialMode]); const handleGoogleSignIn = async () => { setIsGoogleLoading(true); try { const data = await authClient.signIn.social({ provider: "google", callbackURL: "/editor/theme", }); console.log(data); } catch (error) { console.error("Google Sign In Error:", error); // Handle error appropriately (e.g., show a toast notification) } }; const handleGithubSignIn = async () => { setIsGithubLoading(true); try { const data = await authClient.signIn.social({ provider: "github", callbackURL: "/editor/theme", }); console.log(data); } catch (error) { console.error("GitHub Sign In Error:", error); // Handle error appropriately } }; const toggleMode = () => { setIsSignIn(!isSignIn); }; return ( {trigger && {trigger}} {isSignIn ? "Welcome back" : "Create account"}

{isSignIn ? "Sign in to your account to continue" : "Sign up to get started with tweakcn"}

{isSignIn ? "New to tweakcn?" : "Already have an account?"}
); } ``` ## /app/(legal)/layout.tsx ```tsx path="/app/(legal)/layout.tsx" import React from "react"; import { Header } from "@/components/editor/header"; interface LegalLayoutProps { children: React.ReactNode; } export default function LegalLayout({ children }: LegalLayoutProps) { return (
{children}
{/* You might want to add a footer here later */}
); } ``` ## /app/(legal)/privacy-policy/page.tsx ```tsx path="/app/(legal)/privacy-policy/page.tsx" import { Metadata } from "next"; export const metadata: Metadata = { title: "Privacy Policy | tweakcn", description: "Privacy Policy for tweakcn.", }; export default function PrivacyPolicyPage() { return (

Privacy Policy

Last Updated: 24 Apr 2025

1. Introduction

We value your privacy and are committed to safeguarding your personal data. This privacy policy explains how we collect, use, and protect your information when you use our website, as well as your privacy rights and how they are protected by law.

2. Data Collection

When you use our website, we collect and process the following types of data:

  • Web Analytics: Anonymous user data is collecting using PostHog.
  • Authentication Data: When you sign up, we collect necessary information such as your email address.

3. Sharing and Transferring Your Data

We do not sell, lease, or trade your personal information. However, we may share your data with trusted third parties like to process payments or provide other services on our behalf. We ensure that all third-party providers we work with adhere to data protection standards, in compliance with relevant laws such as the Information Technology Act 2000 and Digital Personal Data Protection Act 2023 under Indian law or any other applicable laws.

4. Data Security

We have implemented suitable technical and organizational measures as per the level of risk to protect your personal data from unauthorized access, loss, or misuse.

5. Data Protection

You have the following rights concerning your personal data:

  • Access: You have the right to request a copy of the personal data we hold about you.
  • Correction: If any of your data is incorrect or incomplete, you can request to have it updated.
  • Erasure: You can request that we delete your personal data, subject to applicable legal exceptions.
  • Objection: You can object to the processing of your personal data for certain purposes.
  • Restriction: You can ask us to restrict the use of your data under certain conditions.
  • Data Portability: You have the right to transfer your personal data to another service provider, if applicable.
  • Withdrawal of Consent: You can withdraw your consent to process your personal data at any time.

6. Cookies

Our website uses essential cookies necessary for user authentication and session management. These cookies help maintain your login status and ensure secure access to your account. We do not use cookies for tracking or advertising purposes.

7. Modifications to This Privacy Policy

We may update this privacy policy occasionally. Any changes will be posted here, and the effective date will be updated at this page. It shall be assumed that you are aware of any changes and accept the same.

8. Contact Us

If you have any questions or concerns about this privacy policy, please reach out at{" "} sahaj@tweakcn.com

By continuing to use this website, you confirm that you have read and understood this Privacy Policy.

); } ``` ## /app/api/auth/[...all]/route.ts ```ts path="/app/api/auth/[...all]/route.ts" import { auth } from "@/lib/auth"; import { toNextJsHandler } from "better-auth/next-js"; export const { GET, POST } = toNextJsHandler(auth.handler); ``` ## /app/apple-touch-icon.png Binary file available at https://raw.githubusercontent.com/jnsahaj/tweakcn/refs/heads/main/app/apple-touch-icon.png ## /app/dashboard/components/theme-card.tsx ```tsx path="/app/dashboard/components/theme-card.tsx" "use client"; import { Theme } from "@/types/theme"; // Assuming Theme type includes foreground colors import { Card } from "@/components/ui/card"; import { cn } from "@/lib/utils"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; import { MoreVertical, Trash2, Edit, Loader2, Zap } from "lucide-react"; import { useMemo } from "react"; import { useEditorStore } from "@/store/editor-store"; import { useThemeActions } from "@/hooks/use-theme-actions"; import Link from "next/link"; interface ThemeCardProps { theme: Theme; className?: string; } type SwatchDefinition = { name: string; // Text to display on hover bgKey: keyof Theme["styles"]["light" | "dark"]; // Key for background color fgKey: keyof Theme["styles"]["light" | "dark"]; // Key for text color }; const swatchDefinitions: SwatchDefinition[] = [ { name: "Primary", bgKey: "primary", fgKey: "primary-foreground" }, { name: "Secondary", bgKey: "secondary", fgKey: "secondary-foreground" }, { name: "Accent", bgKey: "accent", fgKey: "accent-foreground" }, { name: "Muted", bgKey: "muted", fgKey: "muted-foreground" }, // Special case: Background swatch shows "Foreground" text using the main foreground color { name: "Background", bgKey: "background", fgKey: "foreground" }, ]; export function ThemeCard({ theme, className }: ThemeCardProps) { const { themeState, setThemeState } = useEditorStore(); const { deleteTheme, isDeletingTheme } = useThemeActions(); const mode = themeState.currentMode; const handleDelete = () => { deleteTheme(theme.id); }; const handleQuickApply = () => { setThemeState({ ...themeState, styles: theme.styles, }); }; const colorSwatches = useMemo(() => { return swatchDefinitions.map((def) => ({ name: def.name, // Get background color, fallback to a default if necessary (e.g., white) bg: theme.styles[mode][def.bgKey] || "#ffffff", // Get foreground color, fallback to main foreground or a default (e.g., black) fg: theme.styles[mode][def.fgKey] || theme.styles[mode].foreground || "#000000", })); }, [mode, theme.styles]); return (
{colorSwatches.map((swatch) => (
{swatch.name}
))}

{theme.name}

{new Date(theme.createdAt).toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric", })}

Quick Apply Open in Editor {/* onShare?.(theme)} className="gap-2" > Share Theme */} {isDeletingTheme ? ( ) : ( )} Delete Theme
); } ``` ## /app/dashboard/components/themes-list.tsx ```tsx path="/app/dashboard/components/themes-list.tsx" "use client"; import { useState, useEffect } from "react"; import type { Theme } from "@/types/theme"; import { ThemeCard } from "./theme-card"; import { Search, ArrowUpDown, Layers } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useIsMobile } from "@/hooks/use-mobile"; interface ThemesListProps { themes: Theme[]; totalCount: number; } export function ThemesList({ themes, totalCount }: ThemesListProps) { const [filteredThemes, setFilteredThemes] = useState(themes); const [searchTerm, setSearchTerm] = useState(""); const [sortOption, setSortOption] = useState("newest"); const isMobile = useIsMobile(); useEffect(() => { const filtered = themes.filter((theme) => theme.name?.toLowerCase().includes(searchTerm.toLowerCase()) ); // Sort based on selected option const sorted = [...filtered].sort((a, b) => { switch (sortOption) { case "newest": return (b.createdAt?.getTime() || 0) - (a.createdAt?.getTime() || 0); case "oldest": return (a.createdAt?.getTime() || 0) - (b.createdAt?.getTime() || 0); case "name": return (a.name || "").localeCompare(b.name || ""); default: return 0; } }); setFilteredThemes(sorted); }, [themes, searchTerm, sortOption]); return (
{totalCount} theme{totalCount === 1 ? "" : "s"}
setSearchTerm(e.target.value)} />
{filteredThemes.length === 0 && searchTerm ? (

No themes found

No themes match your search term "{searchTerm}"

) : (
{filteredThemes.map((theme: Theme) => ( ))}
)}
); } ``` ## /app/dashboard/loading.tsx ```tsx path="/app/dashboard/loading.tsx" import { Loading } from "@/components/loading"; import { Header } from "@/components/editor/header"; export default function DashboardLoading() { return ( <>
); } ``` ## /app/dashboard/page.tsx ```tsx path="/app/dashboard/page.tsx" import { getThemes } from "@/actions/themes"; import { Header } from "@/components/editor/header"; import { Palette, Plus } from "lucide-react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { ThemesList } from "@/app/dashboard/components/themes-list"; export default async function ProfilePage() { const themes = await getThemes(); const sortedThemes = themes.sort((a, b) => { return b.createdAt?.getTime() - a.createdAt?.getTime(); }); return (

Your Themes

Manage and explore your custom color themes

{sortedThemes.length === 0 ? (

No themes created yet

Create your first custom theme to personalize your projects with unique color palettes

) : ( )}
); } ``` ## /app/editor/theme/[[...themeId]]/page.tsx ```tsx path="/app/editor/theme/[[...themeId]]/page.tsx" import { getEditorConfig } from "@/config/editors"; import { cn } from "@/lib/utils"; import Editor from "@/components/editor/editor"; import { Metadata } from "next"; import { Header } from "../../../../components/editor/header"; import { getTheme } from "@/actions/themes"; import { Suspense } from "react"; import { Loading } from "@/components/loading"; export const metadata: Metadata = { title: "tweakcn — Theme Generator for shadcn/ui", description: "Easily customize and preview your shadcn/ui theme with tweakcn. Modify colors, fonts, and styles in real-time.", }; export default async function Component({ params, }: { params: Promise<{ themeId: string[] }>; }) { const { themeId } = await params; const themePromise = themeId?.length > 0 ? getTheme(themeId?.[0]) : Promise.resolve(null); return ( <>
}>
); } ``` ## /app/favicon-16x16.png Binary file available at https://raw.githubusercontent.com/jnsahaj/tweakcn/refs/heads/main/app/favicon-16x16.png ## /app/favicon-32x32.png Binary file available at https://raw.githubusercontent.com/jnsahaj/tweakcn/refs/heads/main/app/favicon-32x32.png ## /app/favicon.ico Binary file available at https://raw.githubusercontent.com/jnsahaj/tweakcn/refs/heads/main/app/favicon.ico ## /app/globals.css ```css path="/app/globals.css" @import "tailwindcss"; @import "tw-animate-css"; @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); --color-card-foreground: var(--card-foreground); --color-popover: var(--popover); --color-popover-foreground: var(--popover-foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); --color-destructive-foreground: var(--destructive-foreground); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); --color-chart-1: var(--chart-1); --color-chart-2: var(--chart-2); --color-chart-3: var(--chart-3); --color-chart-4: var(--chart-4); --color-chart-5: var(--chart-5); --color-sidebar: var(--sidebar); --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar-primary: var(--sidebar-primary); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-accent: var(--sidebar-accent); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); --font-sans: var(--font-sans); --font-mono: var(--font-mono); --font-serif: var(--font-serif); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); /* Computed Shadow Variants */ --shadow-2xs: var(--shadow-2xs); --shadow-xs: var(--shadow-xs); --shadow-sm: var(--shadow-sm); --shadow: var(--shadow); --shadow-md: var(--shadow-md); --shadow-lg: var(--shadow-lg); --shadow-xl: var(--shadow-xl); --shadow-2xl: var(--shadow-2xl); --tracking-tighter: calc(var(--letter-spacing) - 0.05em); --tracking-tight: calc(var(--letter-spacing) - 0.025em); --tracking-normal: var(--letter-spacing); --tracking-wide: calc(var(--letter-spacing) + 0.025em); --tracking-wider: calc(var(--letter-spacing) + 0.05em); --tracking-widest: calc(var(--letter-spacing) + 0.1em); } * { border-color: var(--color-border); } body { background-color: var(--color-background); color: var(--color-foreground); -webkit-font-smoothing: antialiased; letter-spacing: var(--letter-spacing); } @layer base { button:not(:disabled), [role="button"]:not(:disabled) { cursor: pointer; } } /* View Transition Wave Effect */ ::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; } ::view-transition-old(root) { /* Ensure the outgoing view (old theme) is beneath */ z-index: 0; } ::view-transition-new(root) { /* Ensure the incoming view (new theme) is always on top */ z-index: 1; } @keyframes reveal { from { /* Use CSS variables for the origin, defaulting to center if not set */ clip-path: circle(0% at var(--x, 50%) var(--y, 50%)); opacity: 0.7; } to { /* Use CSS variables for the origin, defaulting to center if not set */ clip-path: circle(150% at var(--x, 50%) var(--y, 50%)); opacity: 1; } } ::view-transition-new(root) { /* Apply the reveal animation */ animation: reveal 0.4s ease-in-out forwards; } ``` ## /app/layout.tsx ```tsx path="/app/layout.tsx" import { NuqsAdapter } from "nuqs/adapters/next/app"; import type { Metadata, Viewport } from "next"; import { ThemeProvider } from "@/components/theme-provider"; import { Toaster } from "@/components/ui/toaster"; import { TooltipProvider } from "@/components/ui/tooltip"; import { ThemeScript } from "@/components/theme-script"; import { AuthDialogWrapper } from "@/components/auth-dialog-wrapper"; import "./globals.css"; import { PostHogInit } from "@/components/posthog-init"; import { Suspense } from "react"; export const metadata: Metadata = { title: "Beautiful themes for shadcn/ui — tweakcn | Theme Editor & Generator", description: "Customize theme for shadcn/ui with tweakcn's interactive editor. Supports Tailwind CSS v4, Shadcn UI, and custom styles. Modify properties, preview changes, and get the code in real time.", keywords: "theme editor, theme generator, shadcn, ui, components, react, tailwind, button, editor, visual editor, component editor, web development, frontend, design system, UI components, React components, Tailwind CSS, shadcn/ui themes", authors: [{ name: "Sahaj Jain" }], openGraph: { title: "Beautiful themes for shadcn/ui — tweakcn | Theme Editor & Generator", description: "Customize theme for shadcn/ui with tweakcn's interactive editor. Supports Tailwind CSS v4, Shadcn UI, and custom styles. Modify properties, preview changes, and get the code in real time.", url: "https://tweakcn.com/", siteName: "tweakcn", images: [ { url: "https://tweakcn.com/og-image.png", width: 1200, height: 630, }, ], locale: "en_US", type: "website", }, twitter: { card: "summary_large_image", title: "Beautiful themes for shadcn/ui — tweakcn | Theme Editor & Generator", description: "Customize theme for shadcn/ui with tweakcn's interactive editor. Supports Tailwind CSS v4, Shadcn UI, and custom styles. Modify properties, preview changes, and get the code in real time.", images: ["https://tweakcn.com/og-image.png"], }, robots: "index, follow", }; export const viewport: Viewport = { width: "device-width", initialScale: 1.0, }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ``` ## /app/not-found.tsx ```tsx path="/app/not-found.tsx" export default function NotFound() { return (

404

Page not found

); } ``` ## /app/page.tsx ```tsx path="/app/page.tsx" "use client"; import { useEffect, useState } from "react"; import { Header } from "@/components/home/header"; import { Hero } from "@/components/home/hero"; import { ThemePresetSelector } from "@/components/home/theme-preset-selector"; import { Features } from "@/components/home/features"; import { HowItWorks } from "@/components/home/how-it-works"; import { Roadmap } from "@/components/home/roadmap"; import { FAQ } from "@/components/home/faq"; import { CTA } from "@/components/home/cta"; import { Footer } from "@/components/home/footer"; export default function Home() { const [isScrolled, setIsScrolled] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); useEffect(() => { const handleScroll = () => { if (window.scrollY > 10) { setIsScrolled(true); } else { setIsScrolled(false); } }; window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); }, []); return (
); } ``` ## /app/sitemap.ts ```ts path="/app/sitemap.ts" import { MetadataRoute } from "next"; export default function sitemap(): MetadataRoute.Sitemap { const baseUrl = process.env.BASE_URL ?? "https://tweakcn.com"; return [ { url: baseUrl, lastModified: new Date(), changeFrequency: "weekly", priority: 1, }, { url: `${baseUrl}/editor/theme`, lastModified: new Date(), changeFrequency: "weekly", priority: 0.8, }, ]; } ``` ## /assets/buymeacoffee.svg ```svg path="/assets/buymeacoffee.svg" ``` ## /assets/discord.svg ```svg path="/assets/discord.svg" ``` ## /assets/github.svg ```svg path="/assets/github.svg" ``` ## /assets/google.svg ```svg path="/assets/google.svg" ``` ## /assets/heart.svg ```svg path="/assets/heart.svg" ``` ## /assets/logo.svg ```svg path="/assets/logo.svg" ``` ## /assets/og-image.png Binary file available at https://raw.githubusercontent.com/jnsahaj/tweakcn/refs/heads/main/assets/og-image.png ## /assets/twitter.svg ```svg path="/assets/twitter.svg" ``` ## /components/auth-dialog-wrapper.tsx ```tsx path="/components/auth-dialog-wrapper.tsx" "use client"; import { AuthDialog } from "@/app/(auth)/components/auth-dialog"; import { useAuthStore } from "@/store/auth-store"; import { useEffect } from "react"; import { authClient } from "@/lib/auth-client"; import { executePostLoginAction } from "@/hooks/use-post-login-action"; export function AuthDialogWrapper() { const { isOpen, mode, closeAuthDialog, postLoginAction, clearPostLoginAction, } = useAuthStore(); const { data: session } = authClient.useSession(); useEffect(() => { if (isOpen && session) { closeAuthDialog(); } if (session && postLoginAction) { executePostLoginAction(postLoginAction); clearPostLoginAction(); } }, [session, isOpen, closeAuthDialog, postLoginAction, clearPostLoginAction]); return ( ); } ``` ## /components/editor/action-bar/action-bar.tsx ```tsx path="/components/editor/action-bar/action-bar.tsx" "use client"; import { useState } from "react"; import { useEditorStore } from "@/store/editor-store"; import { parseCssInput } from "@/utils/parse-css-input"; import { toast } from "@/hooks/use-toast"; import { CodePanelDialog } from "@/components/editor/code-panel-dialog"; import CssImportDialog from "@/components/editor/css-import-dialog"; import { ThemeSaveDialog } from "@/components/editor/theme-save-dialog"; import { authClient } from "@/lib/auth-client"; import { useAuthStore } from "@/store/auth-store"; import { usePostLoginAction } from "@/hooks/use-post-login-action"; import { useThemeActions } from "@/hooks/use-theme-actions"; import { ActionBarButtons } from "@/components/editor/action-bar/components/action-bar-buttons"; import { usePostHog } from "posthog-js/react"; export function ActionBar() { const { themeState, setThemeState, applyThemePreset } = useEditorStore(); const [cssImportOpen, setCssImportOpen] = useState(false); const [codePanelOpen, setCodePanelOpen] = useState(false); const [saveDialogOpen, setSaveDialogOpen] = useState(false); const { data: session } = authClient.useSession(); const { openAuthDialog } = useAuthStore(); const { createTheme, isCreatingTheme } = useThemeActions(); const posthog = usePostHog(); usePostLoginAction("SAVE_THEME", () => { setSaveDialogOpen(true); }); const handleCssImport = (css: string) => { const { lightColors, darkColors } = parseCssInput(css); const styles = { ...themeState.styles, light: { ...themeState.styles.light, ...lightColors }, dark: { ...themeState.styles.dark, ...darkColors }, }; setThemeState({ ...themeState, styles, }); toast({ title: "CSS imported", description: "Your custom CSS has been imported successfully", }); }; const handleSaveClick = () => { if (!session) { openAuthDialog("signin", "SAVE_THEME"); return; } setSaveDialogOpen(true); }; const saveTheme = async (themeName: string) => { const themeData = { name: themeName, styles: themeState.styles, }; try { const theme = await createTheme(themeData); posthog.capture("CREATE_THEME", { theme_id: theme?.id, theme_name: theme?.name, }); applyThemePreset(theme?.id || themeState.preset || "default"); setTimeout(() => { if (!theme) return; setSaveDialogOpen(false); }, 50); } catch (error) { console.error( "Save operation failed (error likely handled by hook):", error ); } }; return (
setCssImportOpen(true)} onCodeClick={() => setCodePanelOpen(true)} onSaveClick={handleSaveClick} isSaving={isCreatingTheme} />
); } ``` ## /components/editor/action-bar/components/action-bar-buttons.tsx ```tsx path="/components/editor/action-bar/components/action-bar-buttons.tsx" import { Separator } from "@/components/ui/separator"; import { ThemeToggle } from "./theme-toggle"; import { ImportButton } from "./import-button"; import { ResetButton } from "./reset-button"; import { SaveButton } from "./save-button"; import { CodeButton } from "./code-button"; import ContrastChecker from "@/components/editor/contrast-checker"; import { useEditorStore } from "@/store/editor-store"; import { useThemePresetStore } from "@/store/theme-preset-store"; import { EditButton } from "./edit-button"; interface ActionBarButtonsProps { onImportClick: () => void; onCodeClick: () => void; onSaveClick: () => void; isSaving: boolean; } export function ActionBarButtons({ onImportClick, onCodeClick, onSaveClick, isSaving, }: ActionBarButtonsProps) { const { themeState, restoreThemeCheckpoint, hasThemeChangedFromCheckpoint } = useEditorStore(); const { getPreset } = useThemePresetStore(); const currentPreset = themeState?.preset && getPreset(themeState?.preset); const showEditButton = !!currentPreset && currentPreset.source === "SAVED"; return (
{showEditButton && }
); } ``` ## /components/editor/action-bar/components/code-button.tsx ```tsx path="/components/editor/action-bar/components/code-button.tsx" import { Braces } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; interface CodeButtonProps { onCodeClick: () => void; } export function CodeButton({ onCodeClick }: CodeButtonProps) { return ( View theme code ); } ``` ## /components/editor/action-bar/components/edit-button.tsx ```tsx path="/components/editor/action-bar/components/edit-button.tsx" import { PenLine } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import Link from "next/link"; interface EditButtonProps { themeId: string; } export function EditButton({ themeId }: EditButtonProps) { return ( Edit theme ); } ``` ## /components/editor/action-bar/components/import-button.tsx ```tsx path="/components/editor/action-bar/components/import-button.tsx" import { FileCode } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; interface ImportButtonProps { onImportClick: () => void; } export function ImportButton({ onImportClick }: ImportButtonProps) { return ( Import CSS variables ); } ``` ## /components/editor/action-bar/components/reset-button.tsx ```tsx path="/components/editor/action-bar/components/reset-button.tsx" import { RefreshCw } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; interface ResetButtonProps { onReset: () => void; isDisabled: boolean; } export function ResetButton({ onReset, isDisabled }: ResetButtonProps) { return ( Reset to preset defaults ); } ``` ## /components/editor/action-bar/components/save-button.tsx ```tsx path="/components/editor/action-bar/components/save-button.tsx" import { Heart, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; interface SaveButtonProps { onSaveClick: () => void; isSaving: boolean; } export function SaveButton({ onSaveClick, isSaving }: SaveButtonProps) { return ( Save theme ); } ``` ## /components/editor/action-bar/components/theme-toggle.tsx ```tsx path="/components/editor/action-bar/components/theme-toggle.tsx" 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"; export function ThemeToggle() { const { theme, toggleTheme } = useTheme(); const handleThemeToggle = (event: React.MouseEvent) => { const { clientX: x, clientY: y } = event; toggleTheme({ x, y }); }; return (
{theme === "dark" ? ( ) : ( )} Toggle light/dark mode
); } ``` ## /components/editor/code-panel-dialog.tsx ```tsx path="/components/editor/code-panel-dialog.tsx" import { Dialog, DialogContent } from "@/components/ui/dialog"; import CodePanel from "./code-panel"; import { ThemeEditorState } from "@/types/editor"; interface CodePanelDialogProps { open: boolean; onOpenChange: (open: boolean) => void; themeEditorState: ThemeEditorState; } export function CodePanelDialog({ open, onOpenChange, themeEditorState, }: CodePanelDialogProps) { return (
); } ``` ## /components/editor/code-panel.tsx ```tsx path="/components/editor/code-panel.tsx" import React, { useState } from "react"; import { Button } from "@/components/ui/button"; import { Copy, Check, AlertTriangle } from "lucide-react"; import { ThemeEditorState } from "@/types/editor"; import { ScrollArea, ScrollBar } from "../ui/scroll-area"; import { ColorFormat } from "../../types"; import { Select, SelectContent, SelectTrigger, SelectValue, SelectItem, } from "../ui/select"; import { usePostHog } from "posthog-js/react"; import { useEditorStore } from "@/store/editor-store"; import { usePreferencesStore } from "@/store/preferences-store"; import { generateThemeCode } from "@/utils/theme-style-generator"; import { useThemePresetStore } from "@/store/theme-preset-store"; import { Alert, AlertDescription, AlertTitle } from "../ui/alert"; interface CodePanelProps { themeEditorState: ThemeEditorState; } const CodePanel: React.FC = ({ themeEditorState }) => { const [registryCopied, setRegistryCopied] = useState(false); const [copied, setCopied] = useState(false); const posthog = usePostHog(); const preset = useEditorStore((state) => state.themeState.preset); const colorFormat = usePreferencesStore((state) => state.colorFormat); const tailwindVersion = usePreferencesStore((state) => state.tailwindVersion); const packageManager = usePreferencesStore((state) => state.packageManager); const setColorFormat = usePreferencesStore((state) => state.setColorFormat); const setTailwindVersion = usePreferencesStore( (state) => state.setTailwindVersion ); const setPackageManager = usePreferencesStore( (state) => state.setPackageManager ); const isSavedPreset = useThemePresetStore( (state) => preset && state.getPreset(preset)?.source === "SAVED" ); const code = generateThemeCode( themeEditorState, colorFormat, tailwindVersion ); const getRegistryCommand = (preset: string) => { const url = `https://tweakcn.com/r/themes/${preset}.json`; switch (packageManager) { case "pnpm": return `pnpm dlx shadcn@latest add ${url}`; case "npm": return `npx shadcn@latest add ${url}`; case "yarn": return `yarn dlx shadcn@latest add ${url}`; case "bun": return `bunx shadcn@latest add ${url}`; } }; const copyRegistryCommand = async () => { try { await navigator.clipboard.writeText( getRegistryCommand(preset ?? "default") ); setRegistryCopied(true); setTimeout(() => setRegistryCopied(false), 2000); captureCopyEvent("COPY_REGISTRY_COMMAND"); } catch (err) { console.error("Failed to copy text:", err); } }; const captureCopyEvent = (event: string) => { posthog.capture(event, { editorType: "theme", preset, colorFormat, tailwindVersion, }); }; const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); captureCopyEvent("COPY_CODE"); } catch (err) { console.error("Failed to copy text:", err); } }; return (

Theme Code

{preset && preset !== "default" && !isSavedPreset && (
{(["pnpm", "npm", "yarn", "bun"] as const).map((pm) => ( ))}
{getRegistryCommand(preset)}
)} {isSavedPreset && ( Registry commands are not supported for saved themes yet Coming Soon You can still copy and use the theme code directly. )}
index.css
            {code}
          
); }; export default CodePanel; ``` ## /components/editor/color-picker.tsx ```tsx path="/components/editor/color-picker.tsx" import React, { useState, useEffect, useMemo } from "react"; import { Label } from "@/components/ui/label"; import { ColorPickerProps } from "@/types"; import { debounce } from "@/utils/debounce"; const ColorPicker = ({ color, onChange, label }: ColorPickerProps) => { const [isOpen, setIsOpen] = useState(false); const [localColor, setLocalColor] = useState(color); // Update localColor if the prop changes externally useEffect(() => { setLocalColor(color); }, [color]); // Create a stable debounced onChange handler const debouncedOnChange = useMemo( () => debounce((value: string) => onChange(value), 20), [onChange] ); const handleColorChange = (e: React.ChangeEvent) => { const newColor = e.target.value; setLocalColor(newColor); debouncedOnChange(newColor); }; // Cleanup debounced function on unmount useEffect(() => { return () => { debouncedOnChange.cancel(); }; }, [debouncedOnChange]); return (
setIsOpen(!isOpen)} >
); }; export default ColorPicker; ``` ## /components/editor/contrast-checker.tsx ```tsx path="/components/editor/contrast-checker.tsx" import React, { useState } from "react"; import { useContrastChecker } from "../../hooks/use-contrast-checker"; import { ThemeStyleProps } from "@/types/theme"; import { Button } from "../ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription, } from "../ui/dialog"; import { Contrast, Check, AlertTriangle, Moon, Sun } from "lucide-react"; import { cn } from "@/lib/utils"; import { Card, CardContent } from "../ui/card"; import { Badge } from "../ui/badge"; import { ScrollArea } from "../ui/scroll-area"; import { Separator } from "../ui/separator"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { useTheme } from "@/components/theme-provider"; type ContrastCheckerProps = { currentStyles: ThemeStyleProps; }; const MIN_CONTRAST_RATIO = 4.5; type ColorCategory = "content" | "interactive" | "functional"; type ColorPair = { id: string; foregroundId: keyof ThemeStyleProps; backgroundId: keyof ThemeStyleProps; foreground: string | undefined; background: string | undefined; label: string; category: ColorCategory; }; const ContrastChecker = ({ currentStyles }: ContrastCheckerProps) => { const [filter, setFilter] = useState<"all" | "issues">("all"); const { theme, toggleTheme } = useTheme(); const colorPairsToCheck: ColorPair[] = [ // Content - Base, background, cards, containers { id: "base", foregroundId: "foreground", backgroundId: "background", foreground: currentStyles?.["foreground"], background: currentStyles?.["background"], label: "Base", category: "content", }, { id: "card", foregroundId: "card-foreground", backgroundId: "card", foreground: currentStyles?.["card-foreground"], background: currentStyles?.["card"], label: "Card", category: "content", }, { id: "popover", foregroundId: "popover-foreground", backgroundId: "popover", foreground: currentStyles?.["popover-foreground"], background: currentStyles?.["popover"], label: "Popover", category: "content", }, { id: "muted", foregroundId: "muted-foreground", backgroundId: "muted", foreground: currentStyles?.["muted-foreground"], background: currentStyles?.["muted"], label: "Muted", category: "content", }, // Interactive - Buttons, links, actions { id: "primary", foregroundId: "primary-foreground", backgroundId: "primary", foreground: currentStyles?.["primary-foreground"], background: currentStyles?.["primary"], label: "Primary", category: "interactive", }, { id: "secondary", foregroundId: "secondary-foreground", backgroundId: "secondary", foreground: currentStyles?.["secondary-foreground"], background: currentStyles?.["secondary"], label: "Secondary", category: "interactive", }, { id: "accent", foregroundId: "accent-foreground", backgroundId: "accent", foreground: currentStyles?.["accent-foreground"], background: currentStyles?.["accent"], label: "Accent", category: "interactive", }, // Functional - Sidebar, destructive, special purposes { id: "destructive", foregroundId: "destructive-foreground", backgroundId: "destructive", foreground: currentStyles?.["destructive-foreground"], background: currentStyles?.["destructive"], label: "Destructive", category: "functional", }, { id: "sidebar", foregroundId: "sidebar-foreground", backgroundId: "sidebar", foreground: currentStyles?.["sidebar-foreground"], background: currentStyles?.["sidebar"], label: "Sidebar Base", category: "functional", }, { id: "sidebar-primary", foregroundId: "sidebar-primary-foreground", backgroundId: "sidebar-primary", foreground: currentStyles?.["sidebar-primary-foreground"], background: currentStyles?.["sidebar-primary"], label: "Sidebar Primary", category: "functional", }, { id: "sidebar-accent", foregroundId: "sidebar-accent-foreground", backgroundId: "sidebar-accent", foreground: currentStyles?.["sidebar-accent-foreground"], background: currentStyles?.["sidebar-accent"], label: "Sidebar Accent", category: "functional", }, ]; const validColorPairsToCheck = colorPairsToCheck.filter( (pair): pair is ColorPair & { foreground: string; background: string } => !!pair.foreground && !!pair.background ); const contrastResults = useContrastChecker(validColorPairsToCheck); const getContrastResult = (pairId: string) => { return contrastResults?.find((res) => res.id === pairId); }; const totalIssues = contrastResults?.filter( (result) => result.contrastRatio < MIN_CONTRAST_RATIO ).length; const filteredPairs = filter === "all" ? colorPairsToCheck : colorPairsToCheck.filter((pair) => { const result = getContrastResult(pair.id); return result && result.contrastRatio < MIN_CONTRAST_RATIO; }); // Group color pairs by category const categoryLabels: Record = { content: "Content & Containers", interactive: "Interactive Elements", functional: "Navigation & Functional", }; const categories: ColorCategory[] = ["content", "interactive", "functional"]; const groupedPairs = categories .map((category) => ({ category, label: categoryLabels[category], pairs: filteredPairs.filter((pair) => pair.category === category), })) .filter((group) => group.pairs.length > 0); return ( Check contrast accessibility
Contrast Checker WCAG 2.0 AA requires a contrast ratio of at least{" "} {MIN_CONTRAST_RATIO}:1{" • "} Learn more

Toggle theme

{groupedPairs.map((group) => (

{group.label}

{group.pairs.map((pair) => { const result = getContrastResult(pair.id); const isValid = result?.contrastRatio !== undefined && result?.contrastRatio >= MIN_CONTRAST_RATIO; const contrastRatio = result?.contrastRatio?.toFixed(2) ?? "N/A"; return (

{pair.label} {!isValid && ( )}

{isValid ? ( <> {contrastRatio} ) : ( <> {contrastRatio} )}
Background {pair.background}
Foreground {pair.foreground}
{pair.foreground && pair.background ? (

Aa

Sample Text

) : (

Preview

)}
); })}
))}
); }; export default ContrastChecker; ``` ## /components/editor/control-section.tsx ```tsx path="/components/editor/control-section.tsx" import React, { useState } from "react"; import { ChevronDown, ChevronUp } from "lucide-react"; import { cn } from "@/lib/utils"; import { ControlSectionProps } from "@/types"; const ControlSection = ({ title, children, expanded = false, className, id, }: ControlSectionProps) => { const [isExpanded, setIsExpanded] = useState(expanded); return (
setIsExpanded(!isExpanded)} >

{title}

{children}
); }; export default ControlSection; ``` ## /components/editor/css-import-dialog.tsx ```tsx path="/components/editor/css-import-dialog.tsx" import React, { useState } from "react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { AlertCircle } from "lucide-react"; interface CssImportDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onImport: (css: string) => void; } const CssImportDialog: React.FC = ({ open, onOpenChange, onImport, }) => { const [cssText, setCssText] = useState(""); const [error, setError] = useState(null); const handleImport = () => { // Basic validation - check if the CSS contains some expected variables if (!cssText.trim()) { setError("Please enter CSS content"); return; } try { // Here you would add more sophisticated CSS parsing validation // For now we'll just do a simple check if (!cssText.includes("--") || !cssText.includes(":")) { setError( "Invalid CSS format. CSS should contain variable definitions like --primary: #color" ); return; } onImport(cssText); setCssText(""); setError(null); onOpenChange(false); } catch { setError("Failed to parse CSS. Please check your syntax."); } }; const handleClose = () => { setCssText(""); setError(null); onOpenChange(false); }; return ( Import Custom CSS Paste your CSS file below to customize the theme colors. Make sure to include variables like --primary, --background, etc. {error && ( {error} )}