npm package: @novakit-app/theme
Description: Theme system for React Native UI Kit
Latest version: 1.0.35

# @novakit-app/theme

A comprehensive theme management system for React Native applications that
integrates seamlessly with `@novakit-app/foundation`. Provides light/dark mode
support, persistent theme storage, and dynamic theme switching.

## Features

- 🌓 **Light/Dark Mode**: Built-in light and dark theme variants
- 💾 **Persistent Storage**: Optional AsyncStorage integration for theme
  persistence
- 🔄 **System Integration**: Automatic system theme detection and following
- 🎨 **Custom Themes**: Easy theme customization and extension
- ⚡ **Performance**: Optimized with React.memo and useMemo
- 🔧 **TypeScript**: Full TypeScript support with comprehensive types
- 📱 **React Native**: Works with React Native, Expo, and React Native Web

## Installation

```bash
npm install @novakit-app/theme
```

**Note:** This package works with `@novakit-app/foundation` for styling. Install
both for the complete experience:

```bash
npm install @novakit-app/foundation @novakit-app/theme
```

**Setup Babel Plugin** (for `className` support):

Add to your `babel.config.js`:

```js
module.exports = {
	plugins: [["@novakit-app/foundation/babel-plugin"]],
};
```

For theme persistence (optional):

```bash
npm install @react-native-async-storage/async-storage
```

## Package Separation

**@novakit-app/theme** provides:

- `ThemeProvider` - React context for theme management
- `useTheme()` - Hooks for accessing theme state
- `lightTheme`, `darkTheme` - Pre-built theme configurations
- Theme switching, persistence, and customization utilities

**@novakit-app/foundation** provides:

- `parseClassNames()` - Style compiler for className strings
- `defaultTheme` - Base design tokens
- Babel plugin for compile-time className transformation
- All styling utilities and design system tokens
- Theme utilities like `resolveColor()`, `getSpacing()`, etc.

**They work together:** Theme provides the theme context, Foundation provides
the styling engine.

## Quick Start

### 1. Install Dependencies

```bash
npm install @novakit-app/foundation @novakit-app/theme
```

### 2. Setup Babel Plugin (for className support)

Add to your `babel.config.js`:

```js
module.exports = {
	plugins: [["@novakit-app/foundation/babel-plugin"]],
};
```

### 3. Wrap your app with ThemeProvider

```tsx
import React from "react";
import { ThemeProvider } from "@novakit-app/theme";

export default function App() {
	return (
		<ThemeProvider
			defaultMode="light" // Initial theme: "light" or "dark"
			followSystem={true} // Follow system theme changes
			persistMode={true} // Save user's theme preference
		>
			<YourApp />
		</ThemeProvider>
	);
}
```

## Light/Dark Theme Setup

The ThemeProvider automatically handles light and dark themes. Here's how it
works:

### Default Theme Behavior

```tsx
import { ThemeProvider } from "@novakit-app/theme";

// Option 1: Use built-in light/dark themes (recommended)
<ThemeProvider
	defaultMode="light"
	followSystem={true}
>
	<App />
</ThemeProvider>;
```

**What happens:**

- `defaultMode="light"` - Starts with light theme
- `followSystem={true}` - Automatically switches when user changes system theme
- Built-in `lightTheme` and `darkTheme` are used automatically

### Mode-Based Custom Themes (Recommended)

```tsx
import { ThemeProvider } from "@novakit-app/theme";

// ✅ Define both light and dark themes in one object
const customTheme = {
	light: {
		colors: {
			primary: "#1da1f2", // Twitter blue for light mode
			background: "#ffffff",
			surface: "#f8f9fa",
			text: "#14171a",
		},
	},
	dark: {
		colors: {
			primary: "#00d4ff", // Cyan for dark mode
			background: "#000000",
			surface: "#1a1a1a",
			text: "#ffffff",
		},
	},
};

<ThemeProvider
	customTheme={customTheme}
	defaultMode="light"
	followSystem={true}
>
	<App />
</ThemeProvider>;
```

**What happens:**

- When mode is "light" → uses `customTheme.light`
- When mode is "dark" → uses `customTheme.dark`
- Automatically switches themes when mode changes
- Foundation compiler uses the current mode-based theme

### Traditional Custom Themes

```tsx
import { ThemeProvider } from "@novakit-app/theme";
import { createLightTheme, createDarkTheme } from "@novakit-app/theme";

// Create separate themes (legacy approach)
const customLightTheme = createLightTheme({
	colors: {
		primary: "#1da1f2",
		background: "#ffffff",
		surface: "#f8f9fa",
		text: "#14171a",
	},
});

const customDarkTheme = createDarkTheme({
	colors: {
		primary: "#1da1f2", // Same blue for consistency
		background: "#000000",
		surface: "#16181c",
		text: "#ffffff",
	},
});

// Use custom theme (applies to both modes)
<ThemeProvider
	defaultMode="light"
	customTheme={customLightTheme}
>
	<App />
</ThemeProvider>;
```

### Partial Mode Themes

```tsx
import { ThemeProvider } from "@novakit-app/theme";

// ✅ Only customize light mode, use default dark theme
const customTheme = {
	light: {
		colors: {
			primary: "#1da1f2",
			background: "#ffffff",
			surface: "#f8f9fa",
		},
	},
	// No dark theme - will use default darkTheme
};

<ThemeProvider customTheme={customTheme}>
	<App />
</ThemeProvider>;
```

### Regular Custom Theme (Both Modes)

```tsx
import { ThemeProvider } from "@novakit-app/theme";

// ✅ Same theme for both light and dark modes
const customTheme = {
	colors: {
		primary: "#1da1f2", // Same color for both modes
		secondary: "#657786",
	},
	spacing: {
		4: 20,
		8: 40,
	},
};

<ThemeProvider customTheme={customTheme}>
	<App />
</ThemeProvider>;
```

## ThemeProvider Configuration Options

### Basic Configuration

```tsx
import { ThemeProvider } from "@novakit-app/theme";

<ThemeProvider
	defaultMode="light" // "light" | "dark" - Initial theme
	followSystem={true} // Follow system theme changes
	persistMode={false} // Save theme preference to storage
	storageKey="@myapp/theme" // Custom storage key
>
	<App />
</ThemeProvider>;
```

### Configuration Examples

```tsx
// Example 1: Simple setup with system following
<ThemeProvider
	defaultMode="light"
	followSystem={true}
>
	<App />
</ThemeProvider>

// Example 2: With persistence
<ThemeProvider
	defaultMode="dark"
	followSystem={true}
	persistMode={true}
	storageKey="@myapp/theme-mode"
>
	<App />
</ThemeProvider>

// Example 3: Custom theme with overrides
<ThemeProvider
	defaultMode="light"
	customTheme={{
		colors: {
			primary: "#1da1f2",
			secondary: "#657786",
		}
	}}
>
	<App />
</ThemeProvider>

// Example 4: Manual control (no system following)
<ThemeProvider
	defaultMode="light"
	followSystem={false}
	persistMode={true}
>
	<App />
</ThemeProvider>
```

### Understanding the Props

| Prop           | Type                                     | Default                 | Description                      |
| -------------- | ---------------------------------------- | ----------------------- | -------------------------------- |
| `defaultMode`  | `"light" \| "dark"`                      | `"light"`               | Initial theme mode               |
| `followSystem` | `boolean`                                | `true`                  | Follow system theme changes      |
| `persistMode`  | `boolean`                                | `false`                 | Save theme preference to storage |
| `storageKey`   | `string`                                 | `"@novakit/theme/mode"` | Storage key for persistence      |
| `customTheme`  | `Partial<ThemeConfig> \| ModeBasedTheme` | `undefined`             | Custom theme overrides           |

**customTheme Types:**

- `Partial<ThemeConfig>` - Regular theme overrides for both modes
- `ModeBasedTheme` - Separate themes for light and dark modes

### Theme Behavior Flow

```tsx
// 1. App starts
<ThemeProvider
	defaultMode="light"
	followSystem={true}
	persistMode={true}
>
	<App />
</ThemeProvider>

// 2. ThemeProvider checks:
//    - Is there a saved preference? (if persistMode=true)
//    - What's the system theme? (if followSystem=true)
//    - Use defaultMode as fallback

// 3. User changes system theme (if followSystem=true)
//    - Theme automatically switches
//    - Preference is saved (if persistMode=true)

// 4. User manually toggles theme
//    - Theme switches immediately
//    - System following is disabled
//    - Preference is saved (if persistMode=true)
```

### 4. Use className in your components

```tsx
import React from "react";
import { View, Text } from "react-native";
import { useTheme } from "@novakit-app/theme";

export function MyComponent() {
	const { mode, toggleMode, isLight } = useTheme();

	return (
		<View className="p-4 bg-primary rounded-lg">
			<Text className="text-lg text-white">
				Current mode: {mode}
			</Text>
			<Text onPress={toggleMode}>
				Toggle to {isLight ? "dark" : "light"} mode
			</Text>
		</View>
	);
}
```

## API Reference

### ThemeProvider Props

```tsx
interface ThemeProviderProps {
	children: React.ReactNode;
	customTheme?: Partial<ThemeConfig> | ModeBasedTheme;
	defaultMode?: "light" | "dark";
	followSystem?: boolean;
	persistMode?: boolean;
	storageKey?: string;
}

interface ModeBasedTheme {
	light?: Partial<ThemeConfig>;
	dark?: Partial<ThemeConfig>;
}
```

- `customTheme`: Custom theme overrides to apply (supports mode-based themes)
- `defaultMode`: Initial theme mode (default: "light")
- `followSystem`: Follow system theme changes (default: true)
- `persistMode`: Save theme mode to storage (default: false)
- `storageKey`: Storage key for persistence (default: "@novakit/theme/mode")

### useTheme Hook

```tsx
const {
	theme, // Current theme configuration
	mode, // Current mode: "light" | "dark"
	setMode, // Function to set specific mode
	toggleMode, // Function to toggle between modes
	setTheme, // Function to update theme (limited)
	isSystemMode, // Whether following system theme
	isLight, // Boolean: is light mode
	isDark, // Boolean: is dark mode
} = useTheme();
```

### Additional Hooks

```tsx
// Get only the theme config
const theme = useThemeConfig();

// Get only the mode and mode helpers
const { mode, isLight, isDark } = useThemeMode();

// Get only the control functions
const { setMode, toggleMode, setTheme } = useThemeControls();
```

## Theme Customization

### Mode-Based Custom Themes (Recommended)

The easiest way to customize themes is using mode-based custom themes:

```tsx
import { ThemeProvider } from "@novakit-app/theme";

const customTheme = {
	light: {
		colors: {
			primary: "#1da1f2", // Twitter blue for light mode
			background: "#ffffff",
			surface: "#f8f9fa",
			text: "#14171a",
		},
		spacing: {
			4: 20,
			8: 40,
		},
	},
	dark: {
		colors: {
			primary: "#00d4ff", // Cyan for dark mode
			background: "#000000",
			surface: "#1a1a1a",
			text: "#ffffff",
		},
		spacing: {
			4: 20,
			8: 40,
		},
	},
};

<ThemeProvider customTheme={customTheme}>
	<App />
</ThemeProvider>;
```

**Benefits:**

- Define both light and dark themes in one object
- Automatic theme switching based on mode
- Foundation compiler automatically uses the current theme
- Type-safe with `ModeBasedTheme` interface

### Regular Custom Theme Overrides

```tsx
import { ThemeProvider } from "@novakit-app/theme";

// Same theme applied to both light and dark modes
const customTheme = {
	colors: {
		primary: "#1da1f2",
		secondary: "#657786",
	},
	spacing: {
		1: 2,
		2: 4,
	},
};

<ThemeProvider customTheme={customTheme}>
	<App />
</ThemeProvider>;
```

### Create Custom Themes

```tsx
import {
	createLightTheme,
	createDarkTheme,
	extendTheme,
} from "@novakit-app/theme";

// Create a custom light theme
const myLightTheme = createLightTheme({
	colors: {
		primary: "#1da1f2",
		brand: {
			50: "#eef6ff",
			500: "#1da1f2",
			900: "#0a1a3f",
		},
	},
});

// Create a custom dark theme
const myDarkTheme = createDarkTheme({
	colors: {
		primary: "#1da1f2",
		background: "#0a0a0a",
	},
});

// Extend existing themes
const extendedTheme = extendTheme(myLightTheme, {
	typography: {
		fontSize: {
			"2xs": 10,
			"3xl": 32,
		},
	},
});
```

### Theme with Color Palette

```tsx
import { createThemeWithPalette } from "@novakit-app/theme";

const palette = {
	brand: {
		50: "#eef6ff",
		100: "#dbeafe",
		500: "#3b82f6",
		900: "#1e3a8a",
	},
	accent: "#f59e0b",
};

const theme = createThemeWithPalette(palette);
```

## Advanced Usage

### Custom Storage Implementation

```tsx
import { ThemeProvider } from "@novakit-app/theme";

class CustomStorage {
	async getItem(key: string) {
		// Your custom storage logic
		return localStorage.getItem(key);
	}

	async setItem(key: string, value: string) {
		localStorage.setItem(key, value);
	}

	async removeItem(key: string) {
		localStorage.removeItem(key);
	}
}

<ThemeProvider
	storage={new CustomStorage()}
	storageKey="my-app-theme"
>
	<App />
</ThemeProvider>;
```

### Conditional Theme Application

```tsx
import { useTheme } from "@novakit-app/theme";

function ConditionalComponent() {
	const { theme, isDark } = useTheme();

	const containerStyle = {
		backgroundColor: isDark
			? theme.colors.surface
			: theme.colors.background,
		padding: theme.spacing[4],
	};

	return <View style={containerStyle} />;
}
```

### Theme-Aware Component

```tsx
import React from "react";
import { View, Text } from "react-native";

interface ThemedCardProps {
	title: string;
	children: React.ReactNode;
}

export function ThemedCard({ title, children }: ThemedCardProps) {
	return (
		<View className="p-4 bg-surface rounded-lg shadow-md">
			<Text className="text-lg font-semibold text-text mb-2">
				{title}
			</Text>
			{children}
		</View>
	);
}
```

## Mode-Based Theme Examples

### Complete App Example

```tsx
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { ThemeProvider, useTheme } from "@novakit-app/theme";

// Define mode-based custom theme
const appTheme = {
	light: {
		colors: {
			primary: "#1da1f2", // Twitter blue
			secondary: "#657786",
			background: "#ffffff",
			surface: "#f8f9fa",
			text: "#14171a",
			textSecondary: "#657786",
		},
	},
	dark: {
		colors: {
			primary: "#00d4ff", // Cyan
			secondary: "#8899a6",
			background: "#000000",
			surface: "#1a1a1a",
			text: "#ffffff",
			textSecondary: "#8899a6",
		},
	},
};

function App() {
	return (
		<ThemeProvider
			customTheme={appTheme}
			defaultMode="light"
			followSystem={true}
			persistMode={true}
		>
			<MainScreen />
		</ThemeProvider>
	);
}

function MainScreen() {
	const { mode, toggleMode, isLight } = useTheme();

	return (
		<View className="flex-1 bg-background">
			<View className="p-6">
				<Text className="text-2xl font-bold text-text mb-4">
					Theme Demo
				</Text>

				<Text className="text-textSecondary mb-6">
					Current mode: {mode}
				</Text>

				<TouchableOpacity
					className="bg-primary p-4 rounded-lg mb-6"
					onPress={toggleMode}
				>
					<Text className="text-white text-center font-semibold">
						Switch to {isLight ? "Dark" : "Light"}{" "}
						Mode
					</Text>
				</TouchableOpacity>

				{/* Color palette demonstration */}
				<View className="space-y-4">
					<View className="bg-primary p-4 rounded-lg">
						<Text className="text-white">
							Primary Color
						</Text>
					</View>
					<View className="bg-secondary p-4 rounded-lg">
						<Text className="text-white">
							Secondary Color
						</Text>
					</View>
					<View className="bg-surface p-4 rounded-lg border border-gray-200">
						<Text className="text-text">
							Surface Color
						</Text>
					</View>
				</View>
			</View>
		</View>
	);
}
```

### Dynamic Theme Creation

```tsx
import { ThemeProvider } from "@novakit-app/theme";

function createBrandTheme(brandColor: string) {
	return {
		light: {
			colors: {
				primary: brandColor,
				background: "#ffffff",
				text: "#000000",
				surface: "#f8f9fa",
			},
		},
		dark: {
			colors: {
				primary: brandColor,
				background: "#000000",
				text: "#ffffff",
				surface: "#1a1a1a",
			},
		},
	};
}

function App() {
	const brandTheme = createBrandTheme("#1da1f2");

	return (
		<ThemeProvider customTheme={brandTheme}>
			<MyApp />
		</ThemeProvider>
	);
}
```

## How Theme Switching Works with Foundation

The theme package automatically provides the current theme to foundation's
styling system. Here's how it works:

### Automatic Theme Integration

```tsx
import { useTheme } from "@novakit-app/theme";
import { resolveColor, getSpacing } from "@novakit-app/foundation";

function MyComponent() {
	const { theme, mode, toggleMode } = useTheme();

	return (
		<View className="p-4 bg-primary rounded-lg">
			<Text className="text-white text-lg">
				Current mode: {mode}
			</Text>
			<Text
				className="text-white underline mt-2"
				onPress={toggleMode}
			>
				Switch to {mode === "light" ? "dark" : "light"} mode
			</Text>
		</View>
	);
}
```

**What happens when you toggle:**

1. `toggleMode()` changes the theme mode
2. ThemeProvider updates the theme object
3. All `className` styles automatically use the new theme
4. Foundation's compiler resolves colors using the current theme

### Manual Theme Integration

```tsx
import { useTheme } from "@novakit-app/theme";
import { resolveColor, getSpacing } from "@novakit-app/foundation";

function ManualThemeComponent() {
	const { theme, mode } = useTheme();

	// ✅ Foundation utilities automatically use current theme
	const primaryColor = resolveColor("primary", mode, theme);
	const padding = getSpacing(4, theme);
	const borderRadius = theme.borderRadius.lg;

	return (
		<View
			style={{
				backgroundColor: primaryColor,
				padding,
				borderRadius,
			}}
		>
			<Text style={{ color: theme.colors.white }}>
				Manual theme integration - Mode: {mode}
			</Text>
		</View>
	);
}
```

### Theme-Aware Component Example

```tsx
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { useTheme } from "@novakit-app/theme";

export function ThemeToggleCard() {
	const { mode, toggleMode, isLight, isDark } = useTheme();

	return (
		<View className="p-6 bg-surface rounded-xl shadow-lg mx-4">
			<Text className="text-xl font-bold text-text mb-4">
				Theme Settings
			</Text>

			<Text className="text-textSecondary mb-4">
				Current theme: {mode}
			</Text>

			<TouchableOpacity
				className="bg-primary p-4 rounded-lg"
				onPress={toggleMode}
			>
				<Text className="text-white text-center font-semibold">
					Switch to {isLight ? "Dark" : "Light"} Mode
				</Text>
			</TouchableOpacity>

			{/* These colors automatically change with theme */}
			<View className="flex-row mt-4 space-x-2">
				<View className="w-8 h-8 bg-primary rounded" />
				<View className="w-8 h-8 bg-secondary rounded" />
				<View className="w-8 h-8 bg-success rounded" />
				<View className="w-8 h-8 bg-danger rounded" />
			</View>
		</View>
	);
}
```

### Foundation Integration Deep Dive

```tsx
import { useTheme } from "@novakit-app/theme";
import {
	resolveColor,
	getSpacing,
	getFontSize,
	parseClassNamesWithTheme,
} from "@novakit-app/foundation";

function AdvancedThemeComponent() {
	const { theme, mode } = useTheme();

	// ✅ All foundation utilities work with current theme
	const styles = {
		container: {
			backgroundColor: resolveColor("surface", mode, theme),
			padding: getSpacing(4, theme),
			borderRadius: theme.borderRadius.lg,
		},
		text: {
			color: resolveColor("text", mode, theme),
			fontSize: getFontSize("lg", theme),
		},
		button: {
			backgroundColor: resolveColor("primary", mode, theme),
			paddingHorizontal: getSpacing(6, theme),
			paddingVertical: getSpacing(3, theme),
		},
	};

	// ✅ Or use className with theme context
	const classNameStyles = parseClassNamesWithTheme(
		"p-4 bg-primary rounded-lg text-white",
		theme
	);

	return (
		<View style={styles.container}>
			<Text style={styles.text}>Advanced theme integration</Text>
			<View style={styles.button}>
				<Text style={{ color: "white" }}>Button</Text>
			</View>
		</View>
	);
}
```

## Best Practices

1. **Wrap your app early**: Place `ThemeProvider` at the root of your app
2. **Use theme tokens**: Prefer theme tokens over hardcoded values
3. **Leverage system integration**: Use `followSystem={true}` for better UX
4. **Persist user preference**: Enable `persistMode` for user convenience
5. **Custom themes sparingly**: Only override what you need to change
6. **Performance**: The theme is memoized, but avoid creating new objects in
   render

## Migration from Basic Theme

If you're migrating from a basic theme setup:

```tsx
// Before
const theme = { colors: { primary: "#007bff" } };

// After
import { ThemeProvider } from "@novakit-app/theme";

<ThemeProvider customTheme={{ colors: { primary: "#007bff" } }}>
	<App />
</ThemeProvider>;
```

## Troubleshooting

### Theme not updating

- Ensure `ThemeProvider` wraps your component tree
- Check that you're using `useTheme()` hook correctly
- Verify theme overrides are properly structured

### Storage issues

- AsyncStorage is optional; the package works without it
- Check storage permissions on your platform
- Use custom storage implementation if needed

### Performance issues

- Theme is memoized, but custom theme objects should be stable
- Avoid creating new theme objects in render
- Use `useThemeConfig()` if you only need the theme object

## TypeScript Support

Full TypeScript support with comprehensive types:

```tsx
import type {
	ThemeConfig,
	ThemeMode,
	ThemeProviderProps,
	ModeBasedTheme,
	ThemeContextType,
	UseThemeReturn,
} from "@novakit-app/theme";

// Type-safe mode-based theme
const customTheme: ModeBasedTheme = {
	light: {
		colors: { primary: "#1da1f2" },
	},
	dark: {
		colors: { primary: "#00d4ff" },
	},
};

// Type-safe theme provider props
const themeProps: ThemeProviderProps = {
	customTheme,
	defaultMode: "light",
	followSystem: true,
	persistMode: true,
};
```

## Quick Reference

### Mode-Based Theme Setup

```tsx
// ✅ Recommended: Mode-based custom theme
const customTheme = {
	light: { colors: { primary: "#1da1f2" } },
	dark: { colors: { primary: "#00d4ff" } },
};

<ThemeProvider customTheme={customTheme}>
	<App />
</ThemeProvider>;
```

### Usage in Components

```tsx
// ✅ Automatic theme integration
<View className="p-4 bg-primary rounded-lg">
	<Text className="text-white">Uses current theme</Text>
</View>;

// ✅ Manual theme access
const { theme, mode, toggleMode } = useTheme();
const primaryColor = theme.colors.primary;
```

### Key Benefits

- **Mode-Based Themes**: Define light and dark themes in one object
- **Automatic Switching**: Themes switch automatically when mode changes
- **Foundation Integration**: Compiler automatically uses current theme
- **Type Safety**: Full TypeScript support with `ModeBasedTheme` interface
- **Backward Compatible**: Regular custom themes still work

## License

MIT