```
├── .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
**[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

## 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
## /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"}
Continue with Google
{isGoogleLoading && }
Continue with GitHub
{isGithubLoading && }
{isSignIn ? "New to tweakcn?" : "Already have an account?"}
{isSignIn ? "Create an account" : "Sign in to your 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) => (
))}
{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"}
{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
New Theme
{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 (
);
}
```
## /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 (
Code
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
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
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
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 (
{isSaving ? (
) : (
)}
Save
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) => (
setPackageManager(pm)}
className={`px-3 py-1.5 text-sm font-medium ${
packageManager === pm
? "bg-muted text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
{pm}
))}
{registryCopied ? (
) : (
)}
{getRegistryCommand(preset)}
)}
{isSavedPreset && (
Registry commands are not supported for saved themes yet
Coming Soon
You can still copy and use the theme code directly.
)}
{
setTailwindVersion(value);
if (value === "4" && colorFormat === "hsl") {
setColorFormat("oklch");
}
}}
>
Tailwind v3
Tailwind v4
setColorFormat(value)}
>
hsl
oklch
rgb
hex
index.css
copyToClipboard(code)}
className="h-8"
aria-label={copied ? "Copied to clipboard" : "Copy to clipboard"}
>
{copied ? (
<>
Copied
>
) : (
<>
Copy
>
)}
{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 (
);
};
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 (
Contrast
Check contrast accessibility
Contrast Checker
WCAG 2.0 AA requires a contrast ratio of at least{" "}
{MIN_CONTRAST_RATIO}:1{" • "}
Learn more
toggleTheme({ x: e.clientX, y: e.clientY })}
>
{theme === "light" ? (
) : (
)}
Toggle theme
setFilter("all")}
>
All
setFilter("issues")}
>
Issues ({totalIssues})
{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 ? (
) : (
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}
{isExpanded ? (
) : (
)}
);
};
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}
)}
Cancel
Import
);
};
export default CssImportDialog;
```
## /components/editor/editor.tsx
```tsx path="/components/editor/editor.tsx"
"use client";
import React, { useEffect, use } from "react";
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from "@/components/ui/resizable";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { EditorConfig } from "@/types/editor";
import { Theme, ThemeStyles } from "@/types/theme";
import { Sliders } from "lucide-react";
import { useEditorStore } from "@/store/editor-store";
import { useThemePresetStore } from "@/store/theme-preset-store";
import { authClient } from "@/lib/auth-client";
interface EditorProps {
config: EditorConfig;
themePromise: Promise;
}
const isThemeStyles = (styles: unknown): styles is ThemeStyles => {
return (
!!styles &&
typeof styles === "object" &&
styles !== null &&
"light" in styles &&
"dark" in styles
);
};
const Editor: React.FC = ({ config, themePromise }) => {
const themeState = useEditorStore((state) => state.themeState);
const setThemeState = useEditorStore((state) => state.setThemeState);
const saveThemeCheckpoint = useEditorStore(
(state) => state.saveThemeCheckpoint
);
const Controls = config.controls;
const Preview = config.preview;
const loadSavedPresets = useThemePresetStore(
(state) => state.loadSavedPresets
);
const { data: session } = authClient.useSession();
useEffect(() => {
if (session?.user) {
console.log("EXXX");
loadSavedPresets();
}
}, [loadSavedPresets, session?.user]);
const initialTheme = themePromise ? use(themePromise) : null;
const handleStyleChange = React.useCallback(
(newStyles: ThemeStyles) => {
const prev = useEditorStore.getState().themeState;
setThemeState({ ...prev, styles: newStyles });
},
[setThemeState]
);
useEffect(() => {
if (initialTheme && isThemeStyles(initialTheme.styles)) {
const prev = useEditorStore.getState().themeState;
setThemeState({ ...prev, styles: initialTheme.styles });
saveThemeCheckpoint();
}
}, [initialTheme, setThemeState, saveThemeCheckpoint]);
if (initialTheme && !isThemeStyles(initialTheme.styles)) {
return (
Fetched theme data is invalid.
);
}
const styles = themeState.styles;
return (
{/* Desktop Layout */}
{/* Mobile Layout */}
);
};
export default Editor;
```
## /components/editor/header.tsx
```tsx path="/components/editor/header.tsx"
"use client";
import Link from "next/link";
import GitHubIcon from "@/assets/github.svg";
import TwitterIcon from "@/assets/twitter.svg";
import DiscordIcon from "@/assets/discord.svg";
import Logo from "@/assets/logo.svg";
import { useGithubStars } from "@/hooks/use-github-stars";
import { SocialLink } from "@/components/social-link";
import { Separator } from "@/components/ui/separator";
import { UserProfileDropdown } from "@/components/user-profile-dropdown";
import { formatCompactNumber } from "@/utils/format";
export function Header() {
const { stargazersCount } = useGithubStars("jnsahaj", "tweakcn");
return (
tweakcn
{stargazersCount > 0 && formatCompactNumber(stargazersCount)}
);
}
```
## /components/editor/shadow-control.tsx
```tsx path="/components/editor/shadow-control.tsx"
import React from "react";
import { SliderWithInput } from "./slider-with-input";
import ColorPicker from "./color-picker";
import ControlSection from "./control-section";
interface ShadowControlProps {
shadowColor: string;
shadowOpacity: number;
shadowBlur: number;
shadowSpread: number;
shadowOffsetX: number;
shadowOffsetY: number;
onChange: (key: string, value: string | number) => void;
}
const ShadowControl: React.FC = ({
shadowColor,
shadowOpacity,
shadowBlur,
shadowSpread,
shadowOffsetX,
shadowOffsetY,
onChange,
}) => {
return (
onChange("shadow-color", color)}
label="Shadow Color"
/>
onChange("shadow-opacity", value)}
min={0}
max={1}
step={0.01}
unit=""
label="Shadow Opacity"
/>
onChange("shadow-blur", value)}
min={0}
max={50}
step={0.5}
unit="px"
label="Blur Radius"
/>
onChange("shadow-spread", value)}
min={-50}
max={50}
step={0.5}
unit="px"
label="Spread"
/>
onChange("shadow-offset-x", value)}
min={-50}
max={50}
step={0.5}
unit="px"
label="Offset X"
/>
onChange("shadow-offset-y", value)}
min={-50}
max={50}
step={0.5}
unit="px"
label="Offset Y"
/>
);
};
export default ShadowControl;
```
## /components/editor/slider-with-input.tsx
```tsx path="/components/editor/slider-with-input.tsx"
import { useEffect, useState } from "react";
import { Label } from "../ui/label";
import { Input } from "../ui/input";
import { Slider } from "../ui/slider";
export const SliderWithInput = ({
value,
onChange,
min,
max,
step = 1,
label,
unit = "px",
}: {
value: number;
onChange: (value: number) => void;
min: number;
max: number;
step?: number;
label: string;
unit?: string;
}) => {
const [localValue, setLocalValue] = useState(value);
useEffect(() => {
setLocalValue(value);
}, [value]);
return (
{
setLocalValue(values[0]);
onChange(values[0]);
}}
className="py-1"
/>
);
};
```
## /components/editor/theme-control-actions.tsx
```tsx path="/components/editor/theme-control-actions.tsx"
import { FileCode, Palette, RefreshCw, LucideIcon, Undo2 } from "lucide-react";
import { Button } from "../ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { cn } from "@/lib/utils";
interface MenuItemProps {
icon: LucideIcon;
label: string;
onClick: () => void;
disabled?: boolean;
title?: string;
}
const MenuItem = ({
icon: Icon,
label,
onClick,
disabled,
title,
}: MenuItemProps) => {
return (
{label}
);
};
interface ThemeControlActionsProps {
hasChanges: boolean;
hasPresetChanges: boolean;
onReset: () => void;
onResetToPreset: () => void;
onImportClick: () => void;
}
const ThemeControlActions = ({
hasChanges,
hasPresetChanges,
onReset,
onResetToPreset,
onImportClick,
}: ThemeControlActionsProps) => {
const menuItems: MenuItemProps[] = [
{
icon: FileCode,
label: "Import from CSS file",
onClick: onImportClick,
},
{
icon: RefreshCw,
label: "Reset to Current Preset",
onClick: onResetToPreset,
disabled: !hasPresetChanges,
title: hasPresetChanges ? "Reset to current preset" : "No changes from preset",
},
{
icon: Undo2,
label: "Reset to Default Theme",
onClick: onReset,
disabled: !hasChanges,
title: hasChanges ? "Reset to base theme" : "No changes to reset",
},
];
return (
Options
{menuItems.map((item, index) => (
))}
);
};
export default ThemeControlActions;
```
## /components/editor/theme-control-panel.tsx
```tsx path="/components/editor/theme-control-panel.tsx"
"use client";
import React, { use } from "react";
import { ThemeEditorControlsProps, ThemeStyleProps } from "@/types/theme";
import ControlSection from "./control-section";
import ColorPicker from "./color-picker";
import { ScrollArea } from "../ui/scroll-area";
import ThemePresetSelect from "./theme-preset-select";
import {
getAppliedThemeFont,
monoFonts,
sansSerifFonts,
serifFonts,
} from "../../utils/theme-fonts";
import { useEditorStore } from "../../store/editor-store";
import { Label } from "../ui/label";
import { SliderWithInput } from "./slider-with-input";
import { Tabs, TabsList, TabsContent } from "../ui/tabs";
import ThemeFontSelect from "./theme-font-select";
import {
COMMON_STYLES,
DEFAULT_FONT_MONO,
DEFAULT_FONT_SANS,
DEFAULT_FONT_SERIF,
defaultThemeState,
} from "../../config/theme";
import { Separator } from "../ui/separator";
import { AlertCircle } from "lucide-react";
import ShadowControl from "./shadow-control";
import TabsTriggerPill from "./theme-preview/tabs-trigger-pill";
import ThemeEditActions from "./theme-edit-actions";
import { useThemePresetStore } from "@/store/theme-preset-store";
const ThemeControlPanel = ({
styles,
currentMode,
onChange,
themePromise,
}: ThemeEditorControlsProps) => {
const { applyThemePreset, themeState } = useEditorStore();
const presets = useThemePresetStore((state) => state.getAllPresets());
const currentStyles = React.useMemo(
() => ({
...defaultThemeState.styles[currentMode],
...styles?.[currentMode],
}),
[currentMode, styles]
);
const updateStyle = React.useCallback(
(
key: K,
value: (typeof currentStyles)[K]
) => {
// apply common styles to both light and dark modes
if (COMMON_STYLES.includes(key)) {
onChange({
...styles,
light: { ...styles.light, [key]: value },
dark: { ...styles.dark, [key]: value },
});
return;
}
onChange({
...styles,
[currentMode]: {
...currentStyles,
[key]: value,
},
});
},
[onChange, styles, currentMode, currentStyles]
);
// Ensure we have valid styles for the current mode
if (!currentStyles) {
return null; // Or some fallback UI
}
const radius = parseFloat(currentStyles.radius.replace("rem", ""));
const theme = use(themePromise);
return (
<>
{!theme ? (
) : (
)}
Colors
Typography
Other
updateStyle("primary", color)}
label="Primary"
/>
updateStyle("primary-foreground", color)}
label="Primary Foreground"
/>
updateStyle("secondary", color)}
label="Secondary"
/>
updateStyle("secondary-foreground", color)
}
label="Secondary Foreground"
/>
updateStyle("accent", color)}
label="Accent"
/>
updateStyle("accent-foreground", color)}
label="Accent Foreground"
/>
updateStyle("background", color)}
label="Background"
/>
updateStyle("foreground", color)}
label="Foreground"
/>
updateStyle("card", color)}
label="Card Background"
/>
updateStyle("card-foreground", color)}
label="Card Foreground"
/>
updateStyle("popover", color)}
label="Popover Background"
/>
updateStyle("popover-foreground", color)}
label="Popover Foreground"
/>
updateStyle("muted", color)}
label="Muted"
/>
updateStyle("muted-foreground", color)}
label="Muted Foreground"
/>
updateStyle("destructive", color)}
label="Destructive"
/>
updateStyle("destructive-foreground", color)
}
label="Destructive Foreground"
/>
updateStyle("border", color)}
label="Border"
/>
updateStyle("input", color)}
label="Input"
/>
updateStyle("ring", color)}
label="Ring"
/>
updateStyle("chart-1", color)}
label="Chart 1"
/>
updateStyle("chart-2", color)}
label="Chart 2"
/>
updateStyle("chart-3", color)}
label="Chart 3"
/>
updateStyle("chart-4", color)}
label="Chart 4"
/>
updateStyle("chart-5", color)}
label="Chart 5"
/>
updateStyle("sidebar", color)}
label="Sidebar Background"
/>
updateStyle("sidebar-foreground", color)}
label="Sidebar Foreground"
/>
updateStyle("sidebar-primary", color)}
label="Sidebar Primary"
/>
updateStyle("sidebar-primary-foreground", color)
}
label="Sidebar Primary Foreground"
/>
updateStyle("sidebar-accent", color)}
label="Sidebar Accent"
/>
updateStyle("sidebar-accent-foreground", color)
}
label="Sidebar Accent Foreground"
/>
updateStyle("sidebar-border", color)}
label="Sidebar Border"
/>
updateStyle("sidebar-ring", color)}
label="Sidebar Ring"
/>
To use custom fonts, embed them in your project.
See{" "}
Tailwind docs
{" "}
for details.
Sans-Serif Font
updateStyle("font-sans", value)}
/>
Serif Font
updateStyle("font-serif", value)}
/>
Monospace Font
updateStyle("font-mono", value)}
/>
updateStyle("letter-spacing", `${value}em`)
}
min={-0.5}
max={0.5}
step={0.025}
unit="em"
label="Letter Spacing"
/>
updateStyle("radius", `${value}rem`)}
min={0}
max={5}
step={0.025}
unit="rem"
label="Radius"
/>
updateStyle("spacing", `${value}rem`)}
min={0.15}
max={0.35}
step={0.01}
unit="rem"
label="Spacing"
/>
{
if (key === "shadow-color") {
updateStyle(key, value as string);
} else if (key === "shadow-opacity") {
updateStyle(key, value.toString());
} else {
updateStyle(key as keyof ThemeStyleProps, `${value}px`);
}
}}
/>
>
);
};
export default ThemeControlPanel;
```
## /components/editor/theme-edit-actions.tsx
```tsx path="/components/editor/theme-edit-actions.tsx"
import { Button } from "../ui/button";
import { X, Check } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
import { Separator } from "../ui/separator";
import { useRouter } from "next/navigation";
import { useThemeActions } from "@/hooks/use-theme-actions";
import { useEditorStore } from "@/store/editor-store";
import { Theme } from "@/types/theme";
import { ThemeSaveDialog } from "./theme-save-dialog";
import { useState } from "react";
interface ThemeEditActionsProps {
theme: Theme;
}
const ThemeEditActions: React.FC = ({ theme }) => {
const router = useRouter();
const { updateTheme } = useThemeActions();
const { themeState, applyThemePreset } = useEditorStore();
const [isNameDialogOpen, setIsNameDialogOpen] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const handleThemeEditCancel = () => {
router.push("/editor/theme");
applyThemePreset(themeState?.preset || "default");
};
const handleSaveTheme = async (newName: string) => {
setIsSaving(true);
const dataToUpdate: {
id: string;
name?: string;
styles?: Theme["styles"];
} = {
id: theme.id,
};
if (newName !== theme.name) {
dataToUpdate.name = newName;
} else {
dataToUpdate.name = theme.name;
}
if (themeState.styles) {
dataToUpdate.styles = themeState.styles;
}
if (!dataToUpdate.name && !dataToUpdate.styles) {
setIsNameDialogOpen(false);
setIsSaving(false);
return;
}
const result = await updateTheme(dataToUpdate);
setIsSaving(false);
if (result) {
setIsNameDialogOpen(false);
router.push("/editor/theme");
applyThemePreset(result?.id || themeState?.preset || "default");
} else {
console.error("Failed to update theme");
}
};
const handleThemeEditSave = () => {
setIsNameDialogOpen(true);
};
return (
<>
Cancel changes
Save changes
>
);
};
export default ThemeEditActions;
```
## /components/editor/theme-font-select.tsx
```tsx path="/components/editor/theme-font-select.tsx"
import React, { useMemo } from "react";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface ThemeFontSelectProps {
fonts: Record;
defaultValue: string;
currentFont: string | null;
onFontChange: (font: string) => void;
}
const ThemeFontSelect: React.FC = ({
fonts,
defaultValue,
currentFont,
onFontChange,
}) => {
const fontNames = useMemo(() => ["System", ...Object.keys(fonts)], [fonts]);
const value = currentFont ? fonts[currentFont] ?? defaultValue : defaultValue;
return (
{fontNames.map((fontName) => (
{fontName}
))}
);
};
export default ThemeFontSelect;
```
## /components/editor/theme-preset-select.tsx
```tsx path="/components/editor/theme-preset-select.tsx"
import React, { useCallback, useMemo, useState } from "react";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ThemePreset } from "../../types/theme";
import { useEditorStore } from "../../store/editor-store";
import { getPresetThemeStyles } from "../../utils/theme-preset-helper";
import { Button } from "../ui/button";
import {
ArrowLeft,
ArrowRight,
Check,
ChevronDown,
Moon,
Search,
Shuffle,
Sun,
Heart,
} from "lucide-react";
import { useTheme } from "@/components/theme-provider";
import { Separator } from "../ui/separator";
import { ScrollArea } from "../ui/scroll-area";
import {
Command,
CommandEmpty,
CommandGroup,
CommandItem,
} from "../ui/command";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
import { Input } from "../ui/input";
import { Badge } from "../ui/badge";
import { cn } from "@/lib/utils";
interface ThemePresetSelectProps {
presets: Record;
currentPreset: string | null;
onPresetChange: (preset: string) => void;
}
interface ColorBoxProps {
color: string;
}
const ColorBox: React.FC = ({ color }) => (
);
interface ThemeColorsProps {
presetName: string;
mode: "light" | "dark";
}
const ThemeColors: React.FC = ({ presetName, mode }) => {
const styles = getPresetThemeStyles(presetName)[mode];
return (
);
};
const isThemeNew = (preset: ThemePreset) => {
if (!preset.createdAt) return false;
const createdAt = new Date(preset.createdAt);
const timePeriod = new Date();
timePeriod.setDate(timePeriod.getDate() - 5);
return createdAt > timePeriod;
};
interface ThemeControlsProps {
onRandomize: () => void;
onThemeToggle: (event: React.MouseEvent) => void;
theme: string;
}
const ThemeControls: React.FC = ({
onRandomize,
onThemeToggle,
theme,
}) => (
{theme === "light" ? (
) : (
)}
Toggle theme
Random theme
);
interface ThemeCycleButtonProps {
direction: "prev" | "next";
onClick: () => void;
}
const ThemeCycleButton: React.FC = ({
direction,
onClick,
}) => (
<>
{direction === "prev" ? (
) : (
)}
{direction === "prev" ? "Previous theme" : "Next theme"}
>
);
const ThemePresetSelect: React.FC = ({
presets,
currentPreset,
onPresetChange,
}) => {
const { themeState } = useEditorStore();
const { theme, toggleTheme } = useTheme();
const mode = themeState.currentMode;
const [search, setSearch] = useState("");
const isSavedTheme = useCallback(
(presetId: string) => {
return presets[presetId]?.source === "SAVED";
},
[presets]
);
const presetNames = useMemo(
() => ["default", ...Object.keys(presets)],
[presets]
);
const value = presetNames?.find((name) => name === currentPreset);
const filteredPresets = useMemo(() => {
const filteredList =
search.trim() === ""
? presetNames
: Object.entries(presets)
.filter(([_, preset]) =>
preset.label?.toLowerCase().includes(search.toLowerCase())
)
.map(([name]) => name);
// Separate saved and default themes
const savedThemesList = filteredList.filter(
(name) => name !== "default" && isSavedTheme(name)
);
const defaultThemesList = filteredList.filter(
(name) => !savedThemesList.includes(name)
);
// Sort each list
const sortThemes = (list: string[]) =>
list.sort((a, b) => {
const labelA = presets[a]?.label || a;
const labelB = presets[b]?.label || b;
return labelA.localeCompare(labelB);
});
// Combine saved themes first, then default themes
return [...sortThemes(savedThemesList), ...sortThemes(defaultThemesList)];
}, [presetNames, search, presets, isSavedTheme]);
const currentIndex =
useMemo(
() => filteredPresets.indexOf(value || "default"),
[filteredPresets, value]
) ?? 0;
const randomize = useCallback(() => {
const random = Math.floor(Math.random() * filteredPresets.length);
onPresetChange(filteredPresets[random]);
}, [onPresetChange, filteredPresets]);
const cycleTheme = useCallback(
(direction: "prev" | "next") => {
const newIndex =
direction === "next"
? (currentIndex + 1) % filteredPresets.length
: (currentIndex - 1 + filteredPresets.length) %
filteredPresets.length;
onPresetChange(filteredPresets[newIndex]);
},
[currentIndex, filteredPresets, onPresetChange]
);
const handleThemeToggle = (event: React.MouseEvent) => {
const { clientX: x, clientY: y } = event;
toggleTheme({ x, y });
};
const filteredSavedThemes = useMemo(() => {
return filteredPresets.filter(
(name) => name !== "default" && isSavedTheme(name)
);
}, [filteredPresets, isSavedTheme]);
const filteredDefaultThemes = useMemo(() => {
return filteredPresets.filter(
(name) => name === "default" || !isSavedTheme(name)
);
}, [filteredPresets, isSavedTheme]);
return (
{value !== "default" && value && isSavedTheme(value) && (
)}
{presets[value || "default"]?.label || "default"}
{filteredPresets.length} theme
{filteredPresets.length !== 1 ? "s" : ""}
No themes found.
{/* Saved Themes Group */}
{filteredSavedThemes.length > 0 && (
<>
{filteredSavedThemes
.filter(
(name) => name !== "default" && isSavedTheme(name)
)
.map((presetName) => (
<>
{
onPresetChange(presetName);
setSearch("");
}}
className="flex items-center gap-2 py-2 hover:bg-secondary/50"
>
{presets[presetName]?.label || presetName}
{presets[presetName] &&
isThemeNew(presets[presetName]) && (
New
)}
{presetName === value && (
)}
>
))}
>
)}
{filteredSavedThemes.length === 0 && search.trim() === "" && (
<>
Save
a theme to find it here.
>
)}
{/* Default Theme Group */}
{filteredDefaultThemes.length > 0 && (
{filteredDefaultThemes.map((presetName) => (
{
onPresetChange(presetName);
setSearch("");
}}
className="flex items-center gap-2 py-2 hover:bg-secondary/50"
>
{presets[presetName]?.label || presetName}
{presetName === value && (
)}
))}
)}
cycleTheme("prev")} />
cycleTheme("next")} />
);
};
export default ThemePresetSelect;
```
## /components/editor/theme-preview-panel.tsx
```tsx path="/components/editor/theme-preview-panel.tsx"
"use client";
import { ThemeEditorPreviewProps } from "@/types/theme";
import { Tabs, TabsContent, TabsList } from "@/components/ui/tabs";
import { ScrollArea, ScrollBar } from "../ui/scroll-area";
import ColorPreview from "./theme-preview/color-preview";
import TabsTriggerPill from "./theme-preview/tabs-trigger-pill";
import ExamplesPreviewContainer from "./theme-preview/examples-preview-container";
import { lazy } from "react";
import { Button } from "@/components/ui/button";
import { Maximize, Minimize, Moon, Sun } from "lucide-react";
import { useFullscreen } from "@/hooks/use-fullscreen";
import { cn } from "@/lib/utils";
import { useTheme } from "@/components/theme-provider";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { ActionBar } from "./action-bar/action-bar";
const DemoCards = lazy(() => import("@/components/examples/demo-cards"));
const DemoMail = lazy(() => import("@/components/examples/mail"));
const DemoTasks = lazy(() => import("@/components/examples/tasks"));
const DemoMusic = lazy(() => import("@/components/examples/music"));
const DemoDashboard = lazy(() => import("@/components/examples/dashboard"));
const ThemePreviewPanel = ({
styles,
currentMode,
}: ThemeEditorPreviewProps) => {
const { isFullscreen, toggleFullscreen } = useFullscreen();
const { theme, toggleTheme } = useTheme();
if (!styles || !styles[currentMode]) {
return null;
}
const handleThemeToggle = (event: React.MouseEvent) => {
const { clientX: x, clientY: y } = event;
toggleTheme({ x, y });
};
return (
<>
Cards
Mail
Tasks
Music
Dashboard
Color Palette
{isFullscreen && (
{theme === "light" ? (
) : (
)}
Toggle Theme
)}
{isFullscreen ? (
) : (
)}
{isFullscreen ? "Exit full screen" : "Full screen"}
>
);
};
export default ThemePreviewPanel;
```
## /components/editor/theme-preview/color-preview.tsx
```tsx path="/components/editor/theme-preview/color-preview.tsx"
import { ThemeEditorPreviewProps } from "@/types/theme";
interface ColorPreviewProps {
styles: ThemeEditorPreviewProps["styles"];
currentMode: ThemeEditorPreviewProps["currentMode"];
}
const renderColorPreview = (label: string, color: string) => (
);
const ColorPreview = ({ styles, currentMode }: ColorPreviewProps) => {
if (!styles || !styles[currentMode]) {
return null;
}
return (
{/* Primary Colors */}
Primary Theme Colors
{renderColorPreview("Background", styles[currentMode].background)}
{renderColorPreview("Foreground", styles[currentMode].foreground)}
{renderColorPreview("Primary", styles[currentMode].primary)}
{renderColorPreview(
"Primary Foreground",
styles[currentMode]["primary-foreground"]
)}
{/* Secondary & Accent Colors */}
Secondary & Accent Colors
{renderColorPreview("Secondary", styles[currentMode].secondary)}
{renderColorPreview(
"Secondary Foreground",
styles[currentMode]["secondary-foreground"]
)}
{renderColorPreview("Accent", styles[currentMode].accent)}
{renderColorPreview(
"Accent Foreground",
styles[currentMode]["accent-foreground"]
)}
{/* UI Component Colors */}
UI Component Colors
{renderColorPreview("Card", styles[currentMode].card)}
{renderColorPreview(
"Card Foreground",
styles[currentMode]["card-foreground"]
)}
{renderColorPreview("Popover", styles[currentMode].popover)}
{renderColorPreview(
"Popover Foreground",
styles[currentMode]["popover-foreground"]
)}
{renderColorPreview("Muted", styles[currentMode].muted)}
{renderColorPreview(
"Muted Foreground",
styles[currentMode]["muted-foreground"]
)}
{/* Utility & Form Colors */}
Utility & Form Colors
{renderColorPreview("Border", styles[currentMode].border)}
{renderColorPreview("Input", styles[currentMode].input)}
{renderColorPreview("Ring", styles[currentMode].ring)}
{renderColorPreview("Radius", styles[currentMode].radius)}
{/* Status & Feedback Colors */}
Status & Feedback Colors
{renderColorPreview("Destructive", styles[currentMode].destructive)}
{renderColorPreview(
"Destructive Foreground",
styles[currentMode]["destructive-foreground"]
)}
{/* Chart & Data Visualization Colors */}
Chart & Visualization Colors
{renderColorPreview("Chart 1", styles[currentMode]["chart-1"])}
{renderColorPreview("Chart 2", styles[currentMode]["chart-2"])}
{renderColorPreview("Chart 3", styles[currentMode]["chart-3"])}
{renderColorPreview("Chart 4", styles[currentMode]["chart-4"])}
{renderColorPreview("Chart 5", styles[currentMode]["chart-5"])}
{/* Sidebar Colors */}
Sidebar & Navigation Colors
{renderColorPreview("Sidebar Background", styles[currentMode].sidebar)}
{renderColorPreview(
"Sidebar Foreground",
styles[currentMode]["sidebar-foreground"]
)}
{renderColorPreview(
"Sidebar Primary",
styles[currentMode]["sidebar-primary"]
)}
{renderColorPreview(
"Sidebar Primary Foreground",
styles[currentMode]["sidebar-primary-foreground"]
)}
{renderColorPreview(
"Sidebar Accent",
styles[currentMode]["sidebar-accent"]
)}
{renderColorPreview(
"Sidebar Accent Foreground",
styles[currentMode]["sidebar-accent-foreground"]
)}
{renderColorPreview(
"Sidebar Border",
styles[currentMode]["sidebar-border"]
)}
{renderColorPreview("Sidebar Ring", styles[currentMode]["sidebar-ring"])}
);
};
export default ColorPreview;
```
## /components/editor/theme-preview/components-showcase.tsx
```tsx path="/components/editor/theme-preview/components-showcase.tsx"
import { ThemeEditorPreviewProps } from "@/types/theme";
import { Settings, Info, AlertTriangle, Star } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Table,
TableHeader,
TableRow,
TableHead,
TableBody,
TableCell,
} from "@/components/ui/table";
interface ComponentsShowcaseProps {
styles: ThemeEditorPreviewProps["styles"];
currentMode: ThemeEditorPreviewProps["currentMode"];
}
const ComponentsShowcase = ({ styles, currentMode }: ComponentsShowcaseProps) => {
if (!styles || !styles[currentMode]) {
return null;
}
return (
{/* Button showcase */}
Buttons & Interactive Elements
Primary
Secondary
Outline
Ghost
Link
Delete
{/* Cards & Containers */}
Cards & Containers
Feature Card
Card description with muted foreground color
This card demonstrates the card background and foreground colors,
with content showing regular text.
Cancel
Continue
Popover Container
This container shows popover colors and styling.
Muted Container
Container with muted background and foreground colors.
{/* Status Indicators */}
Status Indicators & Alerts
Default Badge
Secondary
Outline
Error
Custom
Information
Standard alert with default styling.
Error
Destructive alert showcasing error state colors.
Success Alert
Custom alert using accent colors with an opacity modifier.
{/* Data Display */}
Data Display
User
Status
Role
Actions
Alex Johnson
Active
Admin
Sarah Chen
Inactive
User
);
};
export default ComponentsShowcase;
```
## /components/editor/theme-preview/examples-preview-container.tsx
```tsx path="/components/editor/theme-preview/examples-preview-container.tsx"
import { Suspense } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
const LoadingSkeleton = () => (
);
const ExamplesPreviewContainer = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
);
};
export default ExamplesPreviewContainer;
```
## /components/editor/theme-preview/tabs-trigger-pill.tsx
```tsx path="/components/editor/theme-preview/tabs-trigger-pill.tsx"
import { TabsTrigger } from "@/components/ui/tabs";
import { TabsTriggerProps } from "@radix-ui/react-tabs";
const TabsTriggerPill = ({ children, ...props }: TabsTriggerProps) => {
return (
{children}
);
};
export default TabsTriggerPill;
```
## /components/editor/theme-save-dialog.tsx
```tsx path="/components/editor/theme-save-dialog.tsx"
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { useEffect } from "react";
import { Loader2 } from "lucide-react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
const formSchema = z.object({
themeName: z.string().min(1, "Theme name cannot be empty."),
});
interface ThemeSaveDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSave: (themeName: string) => Promise;
isSaving: boolean;
initialThemeName?: string;
ctaLabel?: string;
title?: string;
description?: string;
}
export function ThemeSaveDialog({
open,
onOpenChange,
onSave,
isSaving,
initialThemeName = "",
ctaLabel = "Save Theme",
title = "Save Theme",
description = "Enter a name for your theme so you can find it later.",
}: ThemeSaveDialogProps) {
const form = useForm>({
resolver: zodResolver(formSchema),
defaultValues: {
themeName: initialThemeName,
},
});
const onSubmit = (values: z.infer) => {
onSave(values.themeName);
};
useEffect(() => {
if (open) {
form.reset({ themeName: initialThemeName });
}
}, [open, initialThemeName, form]);
const handleOpenChange = (newOpen: boolean) => {
onOpenChange(newOpen);
};
return (
);
}
```
## /components/examples/cards/chat.tsx
```tsx path="/components/examples/cards/chat.tsx"
import * as React from "react";
import { Check, Plus, Send } from "lucide-react";
import { cn } from "@/lib/utils";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
const users = [
{
name: "Olivia Martin",
email: "m@example.com",
avatar: "/avatars/01.png",
},
{
name: "Isabella Nguyen",
email: "isabella.nguyen@email.com",
avatar: "/avatars/03.png",
},
{
name: "Emma Wilson",
email: "emma@example.com",
avatar: "/avatars/05.png",
},
{
name: "Jackson Lee",
email: "lee@example.com",
avatar: "/avatars/02.png",
},
{
name: "William Kim",
email: "will@email.com",
avatar: "/avatars/04.png",
},
] as const;
type User = (typeof users)[number];
export function DemoChat() {
const [open, setOpen] = React.useState(false);
const [selectedUsers, setSelectedUsers] = React.useState([]);
const [messages, setMessages] = React.useState([
{
role: "agent",
content: "Hi, how can I help you today?",
},
{
role: "user",
content: "Hey, I'm having trouble with my account.",
},
{
role: "agent",
content: "What seems to be the problem?",
},
{
role: "user",
content: "I can't log in.",
},
]);
const [input, setInput] = React.useState("");
const inputLength = input.trim().length;
return (
<>
OM
Sofia Davis
m@example.com
setOpen(true)}
>
New message
New message
{messages.map((message, index) => (
{message.content}
))}
New message
Invite a user to this thread. This will create a new group message.
No users found.
{users.map((user) => (
{
if (selectedUsers.includes(user)) {
return setSelectedUsers(
selectedUsers.filter(
(selectedUser) => selectedUser !== user,
),
);
}
return setSelectedUsers(
[...users].filter((u) =>
[...selectedUsers, user].includes(u),
),
);
}}
>
{user.name[0]}
{selectedUsers.includes(user) ? (
) : null}
))}
{selectedUsers.length > 0 ? (
{selectedUsers.map((user) => (
{user.name[0]}
))}
) : (
Select users to add to this thread.
)}
{
setOpen(false);
}}
>
Continue
>
);
}
```
## /components/examples/cards/cookie-settings.tsx
```tsx path="/components/examples/cards/cookie-settings.tsx"
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
export function DemoCookieSettings() {
return (
Cookie Settings
Manage your cookie settings here.
Strictly Necessary
These cookies are essential in order to use the website and use its
features.
Functional Cookies
These cookies allow the website to provide personalized functionality.
Performance Cookies
These cookies help to improve the performance of the website.
Save preferences
);
}
```
## /components/examples/cards/create-account.tsx
```tsx path="/components/examples/cards/create-account.tsx"
import { Icons } from "@/components/icons";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function DemoCreateAccount() {
return (
Create an account
Enter your email below to create your account
Github
Google
Email
Password
Create account
);
}
```
## /components/examples/cards/date-picker-with-range.tsx
```tsx path="/components/examples/cards/date-picker-with-range.tsx"
import * as React from "react";
import { addDays, format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import { DateRange } from "react-day-picker";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
export default function DatePickerWithRange({
className,
}: React.HTMLAttributes) {
const [date, setDate] = React.useState({
from: new Date(2022, 0, 20),
to: addDays(new Date(2022, 0, 20), 20),
});
return (
{date?.from ? (
date.to ? (
<>
{format(date.from, "LLL dd, y")} - {format(date.to, "LLL dd, y")}
>
) : (
format(date.from, "LLL dd, y")
)
) : (
Pick a date
)}
);
}
```
## /components/examples/cards/date-picker.tsx
```tsx path="/components/examples/cards/date-picker.tsx"
import { Card, CardContent } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import DatePickerWithRange from "./date-picker-with-range";
export function DemoDatePicker() {
return (
Pick a date
);
}
```
## /components/examples/cards/font-showcase.tsx
```tsx path="/components/examples/cards/font-showcase.tsx"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export function DemoFontShowcase() {
return (
Font Showcase
View theme fonts in different styles
Sans-Serif
Light Weight Text
Regular Weight Text
Medium Weight Text
Semibold Weight Text
Bold Weight Text
Serif
Light Weight Text
Regular Weight Text
Medium Weight Text
Semibold Weight Text
Bold Weight Text
Monospace
Light Weight Text
Regular Weight Text
Medium Weight Text
Semibold Weight Text
Bold Weight Text
);
}
```
## /components/examples/cards/github-card.tsx
```tsx path="/components/examples/cards/github-card.tsx"
import { ChevronDown, Circle, Plus, Star } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import Link from "next/link";
export function DemoGithub() {
return (
tweakcn
A visual editor for shadcn/ui components with beautiful themes.
Accessible. Customizable. Open Source.
Star
Suggested Lists
Future Ideas
My Stack
Inspiration
Create List
TypeScript
20k
Updated April 2023
);
}
```
## /components/examples/cards/notifications.tsx
```tsx path="/components/examples/cards/notifications.tsx"
import { Bell, EyeOff, User } from "lucide-react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
export function DemoNotifications() {
return (
Notifications
Choose what you want to be notified about.
Everything
Email digest, mentions & all activity.
Available
Only mentions and comments.
Ignoring
Turn off all notifications.
);
}
```
## /components/examples/cards/payment-method.tsx
```tsx path="/components/examples/cards/payment-method.tsx"
import { Icons } from "@/components/icons";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export function DemoPaymentMethod() {
return (
Payment Method
Add a new payment method to your account.
Paypal
Apple
Name
Card number
Expires
January
February
March
April
May
June
July
August
September
October
November
December
Year
{Array.from({ length: 10 }, (_, i) => (
{new Date().getFullYear() + i}
))}
CVC
Continue
);
}
```
## /components/examples/cards/report-an-issue.tsx
```tsx path="/components/examples/cards/report-an-issue.tsx"
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
export function DemoReportAnIssue() {
return (
Report an issue
What area are you having problems with?
Area
Team
Billing
Account
Deployments
Support
Security Level
Severity 1 (Highest)
Severity 2
Severity 3
Severity 4 (Lowest)
Subject
Description
Cancel
Submit
);
}
```
## /components/examples/cards/share-document.tsx
```tsx path="/components/examples/cards/share-document.tsx"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
export function DemoShareDocument() {
return (
Share this document
Anyone with the link can view this document.
Copy Link
People with access
OM
Olivia Martin
m@example.com
Can edit
Can view
IN
Isabella Nguyen
b@example.com
Can edit
Can view
SD
Sofia Davis
p@example.com
Can edit
Can view
);
}
```
## /components/examples/cards/stats.tsx
```tsx path="/components/examples/cards/stats.tsx"
import { Bar, BarChart, Line, LineChart } from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ChartConfig, ChartContainer } from "@/components/ui/chart";
const data = [
{
revenue: 10400,
subscription: 240,
},
{
revenue: 14405,
subscription: 300,
},
{
revenue: 9400,
subscription: 200,
},
{
revenue: 8200,
subscription: 278,
},
{
revenue: 7000,
subscription: 189,
},
{
revenue: 9600,
subscription: 239,
},
{
revenue: 11244,
subscription: 278,
},
{
revenue: 26475,
subscription: 189,
},
];
const chartConfig = {
revenue: {
label: "Revenue",
color: "var(--primary)",
},
subscription: {
label: "Subscriptions",
color: "var(--primary)",
},
} satisfies ChartConfig;
export function DemoStats() {
return (
Total Revenue
$15,231.89
+20.1% from last month
Subscriptions
+2350
+180.1% from last month
);
}
```
## /components/examples/cards/team-members.tsx
```tsx path="/components/examples/cards/team-members.tsx"
import { ChevronDown } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
export function DemoTeamMembers() {
return (
Team Members
Invite your team members to collaborate.
OM
Sofia Davis
m@example.com
Owner
No roles found.
Viewer
Can view and comment.
Developer
Can view, comment and edit.
Billing
Can view, comment and manage billing.
Owner
Admin-level access to all resources.
JL
Jackson Lee
p@example.com
Member
No roles found.
Viewer
Can view and comment.
Developer
Can view, comment and edit.
Billing
Can view, comment and manage billing.
Owner
Admin-level access to all resources.
);
}
```
## /components/examples/dashboard/components/app-sidebar.tsx
```tsx path="/components/examples/dashboard/components/app-sidebar.tsx"
import * as React from "react";
import {
ArrowUpCircleIcon,
BarChartIcon,
CameraIcon,
ClipboardListIcon,
DatabaseIcon,
FileCodeIcon,
FileIcon,
FileTextIcon,
FolderIcon,
HelpCircleIcon,
LayoutDashboardIcon,
ListIcon,
SearchIcon,
SettingsIcon,
UsersIcon,
} from "lucide-react";
import { NavDocuments } from "@/components/examples/dashboard/components/nav-documents";
import { NavMain } from "@/components/examples/dashboard/components/nav-main";
import { NavSecondary } from "@/components/examples/dashboard/components/nav-secondary";
import { NavUser } from "@/components/examples/dashboard/components/nav-user";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [
{
title: "Dashboard",
url: "#",
icon: LayoutDashboardIcon,
},
{
title: "Lifecycle",
url: "#",
icon: ListIcon,
},
{
title: "Analytics",
url: "#",
icon: BarChartIcon,
},
{
title: "Projects",
url: "#",
icon: FolderIcon,
},
{
title: "Team",
url: "#",
icon: UsersIcon,
},
],
navClouds: [
{
title: "Capture",
icon: CameraIcon,
isActive: true,
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
},
{
title: "Proposal",
icon: FileTextIcon,
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
},
{
title: "Prompts",
icon: FileCodeIcon,
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
},
],
navSecondary: [
{
title: "Settings",
url: "#",
icon: SettingsIcon,
},
{
title: "Get Help",
url: "#",
icon: HelpCircleIcon,
},
{
title: "Search",
url: "#",
icon: SearchIcon,
},
],
documents: [
{
name: "Data Library",
url: "#",
icon: DatabaseIcon,
},
{
name: "Reports",
url: "#",
icon: ClipboardListIcon,
},
{
name: "Word Assistant",
url: "#",
icon: FileIcon,
},
],
};
export function AppSidebar({ ...props }: React.ComponentProps) {
return (
Acme Inc.
);
}
```
## /components/examples/dashboard/components/chart-area-interactive.tsx
```tsx path="/components/examples/dashboard/components/chart-area-interactive.tsx"
import * as React from "react";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import { useIsMobile } from "@/hooks/use-mobile";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
export const description = "An interactive area chart";
const chartData = [
{ date: "2024-04-01", desktop: 222, mobile: 150 },
{ date: "2024-04-02", desktop: 97, mobile: 180 },
{ date: "2024-04-03", desktop: 167, mobile: 120 },
{ date: "2024-04-04", desktop: 242, mobile: 260 },
{ date: "2024-04-05", desktop: 373, mobile: 290 },
{ date: "2024-04-06", desktop: 301, mobile: 340 },
{ date: "2024-04-07", desktop: 245, mobile: 180 },
{ date: "2024-04-08", desktop: 409, mobile: 320 },
{ date: "2024-04-09", desktop: 59, mobile: 110 },
{ date: "2024-04-10", desktop: 261, mobile: 190 },
{ date: "2024-04-11", desktop: 327, mobile: 350 },
{ date: "2024-04-12", desktop: 292, mobile: 210 },
{ date: "2024-04-13", desktop: 342, mobile: 380 },
{ date: "2024-04-14", desktop: 137, mobile: 220 },
{ date: "2024-04-15", desktop: 120, mobile: 170 },
{ date: "2024-04-16", desktop: 138, mobile: 190 },
{ date: "2024-04-17", desktop: 446, mobile: 360 },
{ date: "2024-04-18", desktop: 364, mobile: 410 },
{ date: "2024-04-19", desktop: 243, mobile: 180 },
{ date: "2024-04-20", desktop: 89, mobile: 150 },
{ date: "2024-04-21", desktop: 137, mobile: 200 },
{ date: "2024-04-22", desktop: 224, mobile: 170 },
{ date: "2024-04-23", desktop: 138, mobile: 230 },
{ date: "2024-04-24", desktop: 387, mobile: 290 },
{ date: "2024-04-25", desktop: 215, mobile: 250 },
{ date: "2024-04-26", desktop: 75, mobile: 130 },
{ date: "2024-04-27", desktop: 383, mobile: 420 },
{ date: "2024-04-28", desktop: 122, mobile: 180 },
{ date: "2024-04-29", desktop: 315, mobile: 240 },
{ date: "2024-04-30", desktop: 454, mobile: 380 },
{ date: "2024-05-01", desktop: 165, mobile: 220 },
{ date: "2024-05-02", desktop: 293, mobile: 310 },
{ date: "2024-05-03", desktop: 247, mobile: 190 },
{ date: "2024-05-04", desktop: 385, mobile: 420 },
{ date: "2024-05-05", desktop: 481, mobile: 390 },
{ date: "2024-05-06", desktop: 498, mobile: 520 },
{ date: "2024-05-07", desktop: 388, mobile: 300 },
{ date: "2024-05-08", desktop: 149, mobile: 210 },
{ date: "2024-05-09", desktop: 227, mobile: 180 },
{ date: "2024-05-10", desktop: 293, mobile: 330 },
{ date: "2024-05-11", desktop: 335, mobile: 270 },
{ date: "2024-05-12", desktop: 197, mobile: 240 },
{ date: "2024-05-13", desktop: 197, mobile: 160 },
{ date: "2024-05-14", desktop: 448, mobile: 490 },
{ date: "2024-05-15", desktop: 473, mobile: 380 },
{ date: "2024-05-16", desktop: 338, mobile: 400 },
{ date: "2024-05-17", desktop: 499, mobile: 420 },
{ date: "2024-05-18", desktop: 315, mobile: 350 },
{ date: "2024-05-19", desktop: 235, mobile: 180 },
{ date: "2024-05-20", desktop: 177, mobile: 230 },
{ date: "2024-05-21", desktop: 82, mobile: 140 },
{ date: "2024-05-22", desktop: 81, mobile: 120 },
{ date: "2024-05-23", desktop: 252, mobile: 290 },
{ date: "2024-05-24", desktop: 294, mobile: 220 },
{ date: "2024-05-25", desktop: 201, mobile: 250 },
{ date: "2024-05-26", desktop: 213, mobile: 170 },
{ date: "2024-05-27", desktop: 420, mobile: 460 },
{ date: "2024-05-28", desktop: 233, mobile: 190 },
{ date: "2024-05-29", desktop: 78, mobile: 130 },
{ date: "2024-05-30", desktop: 340, mobile: 280 },
{ date: "2024-05-31", desktop: 178, mobile: 230 },
{ date: "2024-06-01", desktop: 178, mobile: 200 },
{ date: "2024-06-02", desktop: 470, mobile: 410 },
{ date: "2024-06-03", desktop: 103, mobile: 160 },
{ date: "2024-06-04", desktop: 439, mobile: 380 },
{ date: "2024-06-05", desktop: 88, mobile: 140 },
{ date: "2024-06-06", desktop: 294, mobile: 250 },
{ date: "2024-06-07", desktop: 323, mobile: 370 },
{ date: "2024-06-08", desktop: 385, mobile: 320 },
{ date: "2024-06-09", desktop: 438, mobile: 480 },
{ date: "2024-06-10", desktop: 155, mobile: 200 },
{ date: "2024-06-11", desktop: 92, mobile: 150 },
{ date: "2024-06-12", desktop: 492, mobile: 420 },
{ date: "2024-06-13", desktop: 81, mobile: 130 },
{ date: "2024-06-14", desktop: 426, mobile: 380 },
{ date: "2024-06-15", desktop: 307, mobile: 350 },
{ date: "2024-06-16", desktop: 371, mobile: 310 },
{ date: "2024-06-17", desktop: 475, mobile: 520 },
{ date: "2024-06-18", desktop: 107, mobile: 170 },
{ date: "2024-06-19", desktop: 341, mobile: 290 },
{ date: "2024-06-20", desktop: 408, mobile: 450 },
{ date: "2024-06-21", desktop: 169, mobile: 210 },
{ date: "2024-06-22", desktop: 317, mobile: 270 },
{ date: "2024-06-23", desktop: 480, mobile: 530 },
{ date: "2024-06-24", desktop: 132, mobile: 180 },
{ date: "2024-06-25", desktop: 141, mobile: 190 },
{ date: "2024-06-26", desktop: 434, mobile: 380 },
{ date: "2024-06-27", desktop: 448, mobile: 490 },
{ date: "2024-06-28", desktop: 149, mobile: 200 },
{ date: "2024-06-29", desktop: 103, mobile: 160 },
{ date: "2024-06-30", desktop: 446, mobile: 400 },
];
const chartConfig = {
visitors: {
label: "Visitors",
},
desktop: {
label: "Desktop",
color: "var(--chart-1)",
},
mobile: {
label: "Mobile",
color: "var(--chart-2)",
},
} satisfies ChartConfig;
export function ChartAreaInteractive() {
const isMobile = useIsMobile();
const [timeRange, setTimeRange] = React.useState("30d");
React.useEffect(() => {
if (isMobile) {
setTimeRange("7d");
}
}, [isMobile]);
const filteredData = chartData.filter((item) => {
const date = new Date(item.date);
const referenceDate = new Date("2024-06-30");
let daysToSubtract = 90;
if (timeRange === "30d") {
daysToSubtract = 30;
} else if (timeRange === "7d") {
daysToSubtract = 7;
}
const startDate = new Date(referenceDate);
startDate.setDate(startDate.getDate() - daysToSubtract);
return date >= startDate;
});
return (
Total Visitors
Total for the last 3 months
Last 3 months
Last 3 months
Last 30 days
Last 7 days
Last 3 months
Last 30 days
Last 7 days
{
const date = new Date(value);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
}}
/>
{
return new Date(value).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
}}
indicator="dot"
/>
}
/>
);
}
```
The content has been capped at 50000 tokens, and files over NaN bytes have been omitted. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.