init
This commit is contained in:
117
docs/clerk_setup.md
Normal file
117
docs/clerk_setup.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Clerk Setup Guide
|
||||
|
||||
This guide covers the setup and configuration of Clerk features used in this starter template.
|
||||
|
||||
## Clerk Scopes Required
|
||||
|
||||
- **Authentication** - User sign-in/sign-up and session management
|
||||
- **Organizations** - Multi-tenant workspace management (see setup below)
|
||||
- **Billing** - Organization-level subscription management (see setup below)
|
||||
|
||||
## Clerk Organizations Setup (Workspaces & Teams)
|
||||
|
||||
This starter kit includes multi-tenant workspace management powered by **Clerk Organizations**. To enable this feature:
|
||||
|
||||
### Enable Organizations in Clerk Dashboard:
|
||||
|
||||
1. Go to [Clerk Dashboard](https://dashboard.clerk.com)
|
||||
2. Navigate to **configure**
|
||||
3. Click **Organizations settings**
|
||||
4. Configure default roles if needed in the roles and permissions.
|
||||
|
||||
### Server-Side Permission Checks:
|
||||
|
||||
- This starter follows [Clerk's recommended patterns](https://clerk.com/blog/how-to-build-multitenant-authentication-with-clerk)
|
||||
|
||||
### Navigation RBAC System:
|
||||
|
||||
- Fully client-side navigation filtering using `useNav` hook
|
||||
- Supports `requireOrg`, `permission`, and `role` checks (all client-side, instant)
|
||||
- Configured in `src/config/nav-config.ts` with `access` properties
|
||||
- See `docs/nav-rbac.md` for detailed documentation
|
||||
|
||||
### For more information, see:
|
||||
|
||||
- [Clerk Organizations documentation](https://clerk.com/docs/organizations/overview)
|
||||
- [Multi-tenant authentication guide](https://clerk.com/blog/how-to-build-multitenant-authentication-with-clerk)
|
||||
|
||||
## Clerk Billing Setup (Organization Subscriptions)
|
||||
|
||||
This starter kit includes **Clerk Billing for B2B** to manage organization-level subscriptions. Plans and features are managed through the Clerk Dashboard, and the application checks access using Clerk's `has()` function.
|
||||
|
||||
> [!WARNING]
|
||||
> Billing is currently in Beta and its APIs are experimental and may undergo breaking changes. To mitigate potential disruptions, we recommend pinning your SDK and `clerk-js` package versions.
|
||||
|
||||
### Key Features:
|
||||
|
||||
- Organization-level subscription management
|
||||
- Plan-based access control using `<Protect>` component
|
||||
- Feature-based authorization
|
||||
- Integrated Stripe payment processing
|
||||
- Server-side plan/feature checks using `has()` function
|
||||
|
||||
### Billing Cost Structure:
|
||||
|
||||
Clerk Billing costs **0.7% per transaction**, plus transaction fees which are paid directly to Stripe. Clerk Billing is **not** the same as Stripe Billing. Plans and pricing are managed directly through the Clerk Dashboard and won't sync with your existing Stripe products or plans. Clerk uses Stripe **only** for payment processing, so you don't need to set up Stripe Billing.
|
||||
|
||||
### Setup Instructions:
|
||||
|
||||
#### 1. Enable Billing:
|
||||
|
||||
- Navigate to [Billing Settings](https://dashboard.clerk.com/~/billing/settings) in the Clerk Dashboard
|
||||
- Enable billing for your application
|
||||
- Choose payment gateway:
|
||||
- **Clerk development gateway**: A shared **test** Stripe account for development instances. This allows developers to test and build Billing flows **in development** without needing to create and configure a Stripe account.
|
||||
- **Stripe account**: Use your own Stripe account for production. **A Stripe account created for a development instance cannot be used for production**. You will need to create a separate Stripe account for your production environment.
|
||||
|
||||
#### 2. Create Plans:
|
||||
|
||||
- Navigate to [Plans page](https://dashboard.clerk.com/~/billing/plans) in the Clerk Dashboard
|
||||
- Select **Plans for Organizations** tab
|
||||
- Click **Add Plan** and create plans (e.g., `free`, `pro`, `team`)
|
||||
- Set pricing and billing intervals
|
||||
- Toggle **Publicly available** to show in `<PricingTable />` and `<OrganizationProfile />` components
|
||||
|
||||
#### 3. Add Features to Plans:
|
||||
|
||||
- You can add Features when creating a Plan, or add them later:
|
||||
1. Navigate to the [Plans](https://dashboard.clerk.com/~/billing/plans) page
|
||||
2. Select the Plan you'd like to add a Feature to
|
||||
3. In the **Features** section, select **Add Feature**
|
||||
- Feature names in Clerk Dashboard should match what you check in code
|
||||
|
||||
#### 4. Usage in Code:
|
||||
|
||||
**Server-side checks using `has()`:**
|
||||
|
||||
```typescript
|
||||
// Check if organization has a Plan
|
||||
const hasPremiumAccess = has({ plan: 'gold' });
|
||||
|
||||
// Check if organization has a Feature
|
||||
const hasPremiumAccess = has({ feature: 'widgets' });
|
||||
```
|
||||
|
||||
The `has()` method is available on the auth object and checks if the Organization has been granted a specific type of access control (Role, Permission, Feature, or Plan) and returns a boolean value.
|
||||
|
||||
**Client-side protection using `<Protect>`:**
|
||||
|
||||
```tsx
|
||||
<Protect
|
||||
plan='bronze'
|
||||
fallback={<p>Only subscribers to the Bronze plan can access this content.</p>}
|
||||
>
|
||||
<h1>Exclusive Bronze Content</h1>
|
||||
</Protect>
|
||||
```
|
||||
|
||||
Or protect by Feature:
|
||||
|
||||
```tsx
|
||||
<Protect
|
||||
feature='premium_access'
|
||||
fallback={<p>Only subscribers with the Premium Access feature can access this content.</p>}
|
||||
>
|
||||
<h1>Exclusive Premium Content</h1>
|
||||
</Protect>
|
||||
```
|
||||
1046
docs/forms.md
Normal file
1046
docs/forms.md
Normal file
File diff suppressed because it is too large
Load Diff
70
docs/nav-rbac.md
Normal file
70
docs/nav-rbac.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Navigation RBAC
|
||||
|
||||
## Overview
|
||||
|
||||
Navigation visibility in this repo is driven by the Auth.js session and is treated as UX only. Real protection must still happen in route handlers and server-rendered pages.
|
||||
|
||||
Current session fields used by navigation:
|
||||
|
||||
- `systemRole`
|
||||
- `activeOrganizationId`
|
||||
- `activeMembershipRole`
|
||||
- `activeBusinessRole`
|
||||
- `activePermissions`
|
||||
|
||||
## Access model
|
||||
|
||||
### System role
|
||||
|
||||
- `super_admin` is global and can see system-level areas such as `Workspaces`
|
||||
- `user` is the default system role
|
||||
|
||||
### Workspace membership
|
||||
|
||||
- `activeMembershipRole` represents the current workspace role, currently `admin | user`
|
||||
- `activeBusinessRole` represents the current IT Center profile in the selected workspace
|
||||
- `activePermissions` is the effective permission list for the selected workspace
|
||||
|
||||
## Navigation config
|
||||
|
||||
Navigation is configured in `src/config/nav-config.ts`.
|
||||
|
||||
Supported access properties:
|
||||
|
||||
- `systemRole`
|
||||
- `requireOrg`
|
||||
- `role`
|
||||
- `permission`
|
||||
- `plan`
|
||||
- `feature`
|
||||
|
||||
Typical patterns:
|
||||
|
||||
```ts
|
||||
access: { systemRole: 'super_admin' }
|
||||
access: { requireOrg: true, permission: 'asset:read' }
|
||||
access: { requireOrg: true, permission: 'master_data:manage' }
|
||||
```
|
||||
|
||||
## Filtering behavior
|
||||
|
||||
`src/hooks/use-nav.ts` reads the current Auth.js session and filters visible items client-side.
|
||||
|
||||
Rules:
|
||||
|
||||
- items with `systemRole` require a matching global role
|
||||
- items with `requireOrg` require an active workspace
|
||||
- items with `permission` require that permission in `activePermissions`
|
||||
- items with `role` require a matching `activeMembershipRole`
|
||||
|
||||
`super_admin` can bypass workspace permission checks only after an active workspace is selected for workspace-scoped areas.
|
||||
|
||||
## Server-side enforcement
|
||||
|
||||
Use these helpers for real protection:
|
||||
|
||||
- `requireSession()`
|
||||
- `requireSystemRole()`
|
||||
- `requireOrganizationAccess()`
|
||||
|
||||
Do not rely on hidden sidebar items as security.
|
||||
595
docs/themes.md
Normal file
595
docs/themes.md
Normal file
@@ -0,0 +1,595 @@
|
||||
# Adding New Themes
|
||||
|
||||
This guide explains how to add a new theme to the application. The theme system uses CSS custom properties with `[data-theme]` selectors for easy theme switching.
|
||||
|
||||
## The Journey: Adding a New Theme
|
||||
|
||||
When adding a new theme, follow this journey:
|
||||
|
||||
1. **Create theme CSS file** → `src/styles/themes/your-theme-name.css` with `[data-theme='your-theme-name']`
|
||||
2. **Import theme** → Add `@import` to `src/styles/theme.css`
|
||||
3. **Register theme** → Add to `THEMES` array in `src/components/themes/theme.config.ts`
|
||||
4. **Add fonts (if needed)** → Import fonts in `src/components/themes/font.config.ts` if using custom Google Fonts
|
||||
5. **Set as default (optional)** → Update `DEFAULT_THEME` in `src/components/themes/active-theme.tsx`
|
||||
|
||||
See the **Step-by-Step Guide** section below for detailed instructions.
|
||||
|
||||
## Quick Start: Set Your Theme as Default
|
||||
|
||||
To make your new theme the default (so it loads automatically without the theme switcher):
|
||||
|
||||
1. Open `src/components/themes/active-theme.tsx`
|
||||
2. Change line 12: `const DEFAULT_THEME = 'your-theme-name';`
|
||||
3. Save and restart your dev server
|
||||
|
||||
That's it! Your theme will now be the default for all new users.
|
||||
|
||||
> **Note:** Make sure you've completed steps 1-3 above before setting a theme as default.
|
||||
|
||||
## Theme Structure
|
||||
|
||||
All themes are located in `src/styles/themes/` directory. Each theme is a complete, self-contained CSS file that defines all design tokens for both light and dark modes.
|
||||
|
||||
## File Format
|
||||
|
||||
Each theme file must follow this structure:
|
||||
|
||||
```css
|
||||
/* Light mode tokens */
|
||||
[data-theme='your-theme-name'] {
|
||||
/* Color tokens */
|
||||
--background: oklch(...);
|
||||
--foreground: oklch(...);
|
||||
--card: oklch(...);
|
||||
--card-foreground: oklch(...);
|
||||
--popover: oklch(...);
|
||||
--popover-foreground: oklch(...);
|
||||
--primary: oklch(...);
|
||||
--primary-foreground: oklch(...);
|
||||
--secondary: oklch(...);
|
||||
--secondary-foreground: oklch(...);
|
||||
--muted: oklch(...);
|
||||
--muted-foreground: oklch(...);
|
||||
--accent: oklch(...);
|
||||
--accent-foreground: oklch(...);
|
||||
--destructive: oklch(...);
|
||||
--destructive-foreground: oklch(...);
|
||||
--border: oklch(...);
|
||||
--input: oklch(...);
|
||||
--ring: oklch(...);
|
||||
|
||||
/* Chart colors */
|
||||
--chart-1: oklch(...);
|
||||
--chart-2: oklch(...);
|
||||
--chart-3: oklch(...);
|
||||
--chart-4: oklch(...);
|
||||
--chart-5: oklch(...);
|
||||
|
||||
/* Sidebar colors */
|
||||
--sidebar: oklch(...);
|
||||
--sidebar-foreground: oklch(...);
|
||||
--sidebar-primary: oklch(...);
|
||||
--sidebar-primary-foreground: oklch(...);
|
||||
--sidebar-accent: oklch(...);
|
||||
--sidebar-accent-foreground: oklch(...);
|
||||
--sidebar-border: oklch(...);
|
||||
--sidebar-ring: oklch(...);
|
||||
|
||||
/* Typography */
|
||||
/* Option 1: Use fonts from next/font/google (recommended) */
|
||||
--font-sans: 'Font Name', sans-serif; /* Use the font's display name */
|
||||
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
--font-mono: 'Mono Font Name', monospace;
|
||||
|
||||
/* Option 2: Use system fonts */
|
||||
/* --font-sans: ui-sans-serif, system-ui, -apple-system, sans-serif; */
|
||||
|
||||
/* Spacing & Layout */
|
||||
--radius: 0.5rem;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
/* Shadows (optional) */
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 3px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.17;
|
||||
--shadow-color: #000000;
|
||||
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
|
||||
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
|
||||
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
|
||||
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
|
||||
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
|
||||
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
|
||||
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
|
||||
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
|
||||
|
||||
/* Letter spacing (optional) */
|
||||
--tracking-normal: 0em;
|
||||
}
|
||||
|
||||
/* Dark mode tokens */
|
||||
[data-theme='your-theme-name'].dark {
|
||||
/* Same tokens as above, but with dark mode values */
|
||||
--background: oklch(...);
|
||||
--foreground: oklch(...);
|
||||
/* ... all other tokens with dark mode values */
|
||||
}
|
||||
|
||||
/* Theme inline mappings */
|
||||
[data-theme='your-theme-name'] {
|
||||
@theme inline {
|
||||
/* Color mappings */
|
||||
--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 mappings */
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--font-serif: var(--font-serif);
|
||||
|
||||
/* Radius variants */
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
|
||||
/* Shadow mappings (if shadows are defined) */
|
||||
--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 variants (if tracking-normal is defined) */
|
||||
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||
--tracking-normal: var(--tracking-normal);
|
||||
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step-by-Step Guide: Adding a New Theme
|
||||
|
||||
Follow these steps in order to add a new theme to your application.
|
||||
|
||||
### Step 1: Create Theme CSS File
|
||||
|
||||
Create a new file in `src/styles/themes/` with a descriptive name (use kebab-case):
|
||||
|
||||
```bash
|
||||
src/styles/themes/your-theme-name.css
|
||||
```
|
||||
|
||||
**Important:** The filename should match the `data-theme` attribute value you'll use in the CSS.
|
||||
|
||||
### Step 2: Define Your Theme with `[data-theme]` Attribute
|
||||
|
||||
Copy the structure from the "File Format" section above and fill in your color values. Use OKLCH color format for better color consistency:
|
||||
|
||||
```css
|
||||
/* Light mode tokens */
|
||||
[data-theme='your-theme-name'] {
|
||||
--background: oklch(1 0 0); /* White */
|
||||
--foreground: oklch(0.145 0 0); /* Dark gray */
|
||||
--card: oklch(...);
|
||||
/* ... all other tokens */
|
||||
}
|
||||
|
||||
/* Dark mode tokens */
|
||||
[data-theme='your-theme-name'].dark {
|
||||
--background: oklch(0.145 0 0); /* Dark */
|
||||
--foreground: oklch(0.985 0 0); /* Light */
|
||||
/* ... all other tokens with dark mode values */
|
||||
}
|
||||
|
||||
/* Theme inline mappings for Tailwind */
|
||||
[data-theme='your-theme-name'] {
|
||||
@theme inline {
|
||||
/* All the mappings as shown in the File Format section */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Color Format:**
|
||||
|
||||
- Use `oklch()` format: `oklch(lightness chroma hue)`
|
||||
- Example: `oklch(0.852 0.199 91.936)` = light green-blue
|
||||
- Lightness: 0-1 (0 = black, 1 = white)
|
||||
- Chroma: 0+ (0 = grayscale, higher = more saturated)
|
||||
- Hue: 0-360 (color wheel position)
|
||||
|
||||
**Key Points:**
|
||||
|
||||
- The `[data-theme='your-theme-name']` selector is what makes your theme work
|
||||
- The value `'your-theme-name'` must match exactly in all places (CSS file, config, etc.)
|
||||
- Always include both light and dark mode variants
|
||||
- Include the `@theme inline` block for Tailwind CSS integration
|
||||
|
||||
### Step 3: Import Theme in theme.css
|
||||
|
||||
Add your theme import to `src/styles/theme.css`:
|
||||
|
||||
```css
|
||||
@import './themes/your-theme-name.css';
|
||||
```
|
||||
|
||||
This makes your theme available to the application.
|
||||
|
||||
### Step 4: Add Theme to theme.config.ts
|
||||
|
||||
Add your theme to the `THEMES` array in `src/components/themes/theme.config.ts`:
|
||||
|
||||
```typescript
|
||||
export const THEMES = [
|
||||
// ... existing themes
|
||||
{
|
||||
name: 'Your Theme Name', // Display name in the UI
|
||||
value: 'your-theme-name' // Must match [data-theme] value exactly
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
**Important:** The `value` field must match the `data-theme` attribute value from your CSS file exactly.
|
||||
|
||||
### Step 5: Add Custom Fonts (If Needed)
|
||||
|
||||
**Only do this step if your theme requires a custom Google Font that isn't already loaded.**
|
||||
|
||||
If you want to use a Google Font in your theme:
|
||||
|
||||
**File:** `src/components/themes/font.config.ts`
|
||||
|
||||
1. **Import the font** from `next/font/google`:
|
||||
|
||||
```typescript
|
||||
import { Your_Font_Name } from 'next/font/google';
|
||||
```
|
||||
|
||||
2. **Configure the font** with a CSS variable:
|
||||
|
||||
```typescript
|
||||
const fontYourName = Your_Font_Name({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '500', '700'], // Adjust weights as needed
|
||||
variable: '--font-your-name' // Optional: custom variable name
|
||||
});
|
||||
```
|
||||
|
||||
3. **Add it to the `fontVariables` export**:
|
||||
|
||||
```typescript
|
||||
export const fontVariables = cn(
|
||||
// ... existing fonts
|
||||
fontYourName.variable
|
||||
);
|
||||
```
|
||||
|
||||
4. **Use the font in your theme CSS** by its display name (not the CSS variable):
|
||||
|
||||
```css
|
||||
[data-theme='your-theme-name'] {
|
||||
--font-sans: 'Your Font Name', sans-serif; /* Use the actual font name */
|
||||
--font-mono: 'Your Mono Font', monospace;
|
||||
}
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
|
||||
- Use the font's **display name** in CSS (e.g., `'Geist'`, `'Architects Daughter'`), not the CSS variable
|
||||
- The font must be imported in `font.config.ts` for it to be loaded by Next.js
|
||||
- Font variables from `font.config.ts` are automatically applied to the body via `layout.tsx`
|
||||
- You can use any Google Font available in `next/font/google`
|
||||
- Check existing fonts in `font.config.ts` before adding new ones - you might be able to reuse them
|
||||
|
||||
**Example:** The `notebook` theme uses `Architects Daughter`:
|
||||
|
||||
- Imported in `font.config.ts` as `Architects_Daughter`
|
||||
- Used in `notebook.css` as `'Architects Daughter'` (with quotes and space)
|
||||
|
||||
### Step 6: Set as Default Theme (Optional)
|
||||
|
||||
If you want your theme to be the default theme that loads when users first visit the application (without needing the theme switcher), update the default theme constant:
|
||||
|
||||
**File:** `src/components/themes/theme.config.ts`
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Default theme that loads when no user preference is set
|
||||
* Change this value to set a different default theme
|
||||
*/
|
||||
export const DEFAULT_THEME = 'your-theme-name'; // Change from 'vercel' to your theme name
|
||||
```
|
||||
|
||||
**Note:**
|
||||
|
||||
- This is the **single source of truth** for the default theme - it's automatically used in both server-side rendering and client-side code
|
||||
- This will make your theme the default for all new users
|
||||
- Existing users who have already selected a theme will still see their saved preference (stored in cookies)
|
||||
- The default theme is applied immediately on page load (no flash of unstyled content)
|
||||
|
||||
### Step 7: Test Your Theme
|
||||
|
||||
1. Start your development server
|
||||
2. Open the theme selector in the UI
|
||||
3. Select your new theme
|
||||
4. Verify it works in both light and dark modes
|
||||
5. Test scaled variant by selecting "Your Theme Name (Scaled)"
|
||||
6. If you set it as default, clear your browser cookies and refresh to see it load automatically
|
||||
|
||||
## Quick Reference: File Locations
|
||||
|
||||
When adding a new theme, you'll work with these files in this order:
|
||||
|
||||
1. ✅ `src/styles/themes/your-theme-name.css` - Create theme file with `[data-theme]` attribute
|
||||
2. ✅ `src/styles/theme.css` - Import your theme file
|
||||
3. ✅ `src/components/themes/theme.config.ts` - Add theme to `THEMES` array
|
||||
4. ⚠️ `src/components/themes/font.config.ts` - Add fonts only if needed
|
||||
5. ⚠️ `src/components/themes/active-theme.tsx` - Set as default only if desired
|
||||
|
||||
## Required Tokens
|
||||
|
||||
### Minimum Required
|
||||
|
||||
At minimum, your theme should define these tokens:
|
||||
|
||||
- `--background`
|
||||
- `--foreground`
|
||||
- `--card` & `--card-foreground`
|
||||
- `--popover` & `--popover-foreground`
|
||||
- `--primary` & `--primary-foreground`
|
||||
- `--secondary` & `--secondary-foreground`
|
||||
- `--muted` & `--muted-foreground`
|
||||
- `--accent` & `--accent-foreground`
|
||||
- `--destructive` & `--destructive-foreground`
|
||||
- `--border`
|
||||
- `--input`
|
||||
- `--ring`
|
||||
- `--radius`
|
||||
|
||||
### Optional Tokens
|
||||
|
||||
These can be omitted if not needed:
|
||||
|
||||
- `--chart-1` through `--chart-5` (defaults to primary colors)
|
||||
- `--sidebar-*` tokens (defaults to card colors)
|
||||
- `--font-*` tokens (uses system defaults)
|
||||
- `--shadow-*` tokens (no shadows if omitted)
|
||||
- `--tracking-normal` (no letter spacing if omitted)
|
||||
- `--spacing` (uses default)
|
||||
|
||||
## Example: Complete Theme
|
||||
|
||||
See `src/styles/themes/claude.css` for a complete example with all tokens defined.
|
||||
|
||||
## Example: Minimal Theme
|
||||
|
||||
For a minimal theme, you can copy an existing theme and modify only the colors you want to change. The system will fall back to defaults for any missing tokens.
|
||||
|
||||
## Color Format Reference
|
||||
|
||||
### OKLCH Format
|
||||
|
||||
```
|
||||
oklch(lightness chroma hue)
|
||||
```
|
||||
|
||||
- **Lightness**: 0-1 (0 = black, 1 = white)
|
||||
- **Chroma**: 0+ (0 = grayscale, 0.2+ = colorful)
|
||||
- **Hue**: 0-360 degrees
|
||||
- 0/360 = Red
|
||||
- 60 = Yellow
|
||||
- 120 = Green
|
||||
- 180 = Cyan
|
||||
- 240 = Blue
|
||||
- 300 = Magenta
|
||||
|
||||
### Examples
|
||||
|
||||
```css
|
||||
/* Pure white */
|
||||
--background: oklch(1 0 0);
|
||||
|
||||
/* Pure black */
|
||||
--foreground: oklch(0 0 0);
|
||||
|
||||
/* Bright blue */
|
||||
--primary: oklch(0.7 0.2 240);
|
||||
|
||||
/* Muted gray */
|
||||
--muted: oklch(0.5 0 0);
|
||||
```
|
||||
|
||||
## Scaled Variants
|
||||
|
||||
All themes automatically support scaled variants. When a user selects "Theme Name (Scaled)", the `.theme-scaled` class is applied, which adjusts spacing and text sizes. No additional CSS is needed in your theme file.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use descriptive theme names**: Use kebab-case (e.g., `ocean-blue`, `forest-green`)
|
||||
2. **Provide both light and dark modes**: Always define both variants
|
||||
3. **Test accessibility**: Ensure sufficient contrast between foreground and background
|
||||
4. **Keep tokens consistent**: Use similar lightness/chroma values for related colors
|
||||
5. **Document special features**: If your theme has unique characteristics (like no shadows or custom fonts), add comments
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Theme Not Appearing
|
||||
|
||||
- Check that the file is imported in `src/styles/theme.css`
|
||||
- Verify the theme name matches in both CSS file and theme-selector.tsx
|
||||
- Ensure the file is saved and the dev server has reloaded
|
||||
|
||||
### Colors Not Applying
|
||||
|
||||
- Verify all required tokens are defined
|
||||
- Check that `@theme inline` block includes all color mappings
|
||||
- Ensure OKLCH format is correct (no typos)
|
||||
|
||||
### Dark Mode Not Working
|
||||
|
||||
- Verify `.dark` selector is correct: `[data-theme='name'].dark`
|
||||
- Check that dark mode tokens are defined
|
||||
- Ensure `next-themes` is properly configured
|
||||
|
||||
## Setting a Default Theme
|
||||
|
||||
By default, the application uses the `vercel` theme. To change the default theme that loads for new users:
|
||||
|
||||
### Change Default Theme Constant
|
||||
|
||||
Edit `src/components/themes/theme.config.ts` and update the `DEFAULT_THEME` constant:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Default theme that loads when no user preference is set
|
||||
* Change this value to set a different default theme
|
||||
*/
|
||||
export const DEFAULT_THEME = 'your-theme-name'; // Change this value
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
|
||||
- **Single source of truth**: `DEFAULT_THEME` is defined in `theme.config.ts` and imported everywhere it's needed
|
||||
- **Server-side**: Applied immediately in the HTML `data-theme` attribute (no flash)
|
||||
- **Client-side**: Used as fallback when no cookie preference exists
|
||||
- **User preferences**: Still respects saved user preferences (stored in cookies)
|
||||
- **Automatic**: No need to update multiple files - change it once and it works everywhere
|
||||
|
||||
**Benefits of this approach:**
|
||||
|
||||
✅ No code duplication - defined once, used everywhere
|
||||
✅ Type-safe - TypeScript ensures consistency
|
||||
✅ Easy to change - update one line in one file
|
||||
✅ Well-documented - clear comments explain its purpose
|
||||
✅ Immediate application - no flash of unstyled content
|
||||
|
||||
## Using Google Fonts in Themes
|
||||
|
||||
> **Note:** This section provides additional details about fonts. For the complete step-by-step process, see **Step 5** in the "Step-by-Step Guide" above.
|
||||
|
||||
### When to Add Fonts
|
||||
|
||||
You only need to add fonts to `font.config.ts` if:
|
||||
|
||||
- Your theme uses a Google Font that isn't already imported
|
||||
- You want to use a custom font that requires loading
|
||||
|
||||
**Tip:** Check `src/components/themes/font.config.ts` first - many fonts may already be available!
|
||||
|
||||
### Font Loading Process
|
||||
|
||||
1. **Import the font** in `src/components/themes/font.config.ts`:
|
||||
|
||||
```typescript
|
||||
import { Roboto, Roboto_Mono } from 'next/font/google';
|
||||
```
|
||||
|
||||
2. **Configure the font** with a CSS variable:
|
||||
|
||||
```typescript
|
||||
const fontRoboto = Roboto({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '500', '700'],
|
||||
variable: '--font-roboto'
|
||||
});
|
||||
```
|
||||
|
||||
3. **Add to fontVariables export**:
|
||||
|
||||
```typescript
|
||||
export const fontVariables = cn(
|
||||
// ... existing fonts
|
||||
fontRoboto.variable
|
||||
);
|
||||
```
|
||||
|
||||
4. **Use in your theme CSS** with the font's display name:
|
||||
|
||||
```css
|
||||
[data-theme='your-theme'] {
|
||||
--font-sans: 'Roboto', sans-serif; /* Use display name, not CSS variable */
|
||||
--font-mono: 'Roboto Mono', monospace;
|
||||
}
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Font names**: Use the font's display name in CSS (e.g., `'Roboto'`, `'Open Sans'`), not the CSS variable name
|
||||
- **Font loading**: Fonts must be imported in `font.config.ts` to be loaded by Next.js
|
||||
- **Automatic application**: Font variables are automatically applied to the body element via `layout.tsx`
|
||||
- **Available fonts**: Check [Next.js Font Optimization](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) for available Google Fonts
|
||||
|
||||
### Example: Notebook Theme
|
||||
|
||||
The `notebook` theme uses `Architects Daughter`:
|
||||
|
||||
**In `font.config.ts`:**
|
||||
|
||||
```typescript
|
||||
import { Architects_Daughter } from 'next/font/google';
|
||||
|
||||
const fontArchitectsDaughter = Architects_Daughter({
|
||||
subsets: ['latin'],
|
||||
weight: '400',
|
||||
variable: '--font-architects-daughter'
|
||||
});
|
||||
|
||||
export const fontVariables = cn(
|
||||
// ... other fonts
|
||||
fontArchitectsDaughter.variable
|
||||
);
|
||||
```
|
||||
|
||||
**In `notebook.css`:**
|
||||
|
||||
```css
|
||||
[data-theme='notebook'] {
|
||||
--font-sans: 'Architects Daughter', sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
## Reference Files
|
||||
|
||||
- **Complete theme example**: `src/styles/themes/claude.css`
|
||||
- **Theme aggregator**: `src/styles/theme.css`
|
||||
- **Theme selector component**: `src/components/themes/theme-selector.tsx`
|
||||
- **Theme provider**: `src/components/themes/active-theme.tsx`
|
||||
- **Theme configuration** (includes default theme): `src/components/themes/theme.config.ts`
|
||||
- **Font configuration**: `src/components/themes/font.config.ts`
|
||||
Reference in New Issue
Block a user