@fakhrirafiki/theme-engine 0.4.7 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +281 -115
- package/dist/index.d.mts +65 -26
- package/dist/index.d.ts +65 -26
- package/dist/index.js +84 -64
- package/dist/index.mjs +83 -64
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
|
-
# Theme Engine
|
|
1
|
+
# 🎨 Theme Engine
|
|
2
2
|
|
|
3
|
-
Theme system for
|
|
3
|
+
Theme system for **Next.js (App Router)**: mode (`light | dark | system`) + theme presets (semantic tokens via CSS variables).
|
|
4
|
+
|
|
5
|
+
> ✅ Opinionated defaults, minimal setup, and TypeScript autocomplete that “just works”.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## ✨ Why use this?
|
|
12
|
+
|
|
13
|
+
- ⚡ **Fast setup**: 1 CSS import + 1 provider
|
|
14
|
+
- 🌓 **Mode support**: `light | dark | system` (with View Transition ripple when supported)
|
|
15
|
+
- 🎨 **Theme presets**: built-in presets + your own presets
|
|
16
|
+
- 🧠 **DX-first**: `useThemeEngine()` for everything
|
|
17
|
+
- 🧩 **Tailwind v4 friendly**: `@theme inline` tokens included (works with shadcn-style semantic tokens)
|
|
18
|
+
|
|
19
|
+
## 📚 Table of contents
|
|
20
|
+
|
|
21
|
+
- [Install](#-install)
|
|
22
|
+
- [Quick Start (Nextjs App Router)](#-quick-start-nextjs-app-router)
|
|
23
|
+
- [Usage](#-usage)
|
|
24
|
+
- [Custom presets](#-custom-presets-recommended)
|
|
25
|
+
- [Built-in presets](#-built-in-presets)
|
|
26
|
+
- [Tailwind tokens](#-tailwind-tokens-you-get)
|
|
27
|
+
- [Components](#-components)
|
|
28
|
+
- [API reference](#-api-reference)
|
|
29
|
+
- [Troubleshooting](#-troubleshooting)
|
|
4
30
|
|
|
5
31
|
## Install
|
|
6
32
|
|
|
@@ -8,45 +34,49 @@ Theme system for React: **mode** (`light | dark | system`) + **color presets** (
|
|
|
8
34
|
pnpm add @fakhrirafiki/theme-engine
|
|
9
35
|
```
|
|
10
36
|
|
|
11
|
-
|
|
37
|
+
> Using npm/yarn?
|
|
38
|
+
>
|
|
39
|
+
> - `npm i @fakhrirafiki/theme-engine`
|
|
40
|
+
> - `yarn add @fakhrirafiki/theme-engine`
|
|
41
|
+
|
|
42
|
+
## 🚀 Quick Start (Next.js App Router)
|
|
12
43
|
|
|
13
44
|
### 1) Import CSS once
|
|
14
45
|
|
|
15
|
-
In `app/globals.css`:
|
|
46
|
+
In `src/app/globals.css`:
|
|
16
47
|
|
|
17
48
|
```css
|
|
18
|
-
@import
|
|
49
|
+
@import '@fakhrirafiki/theme-engine/styles';
|
|
19
50
|
```
|
|
20
51
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- If you use Tailwind v4, you will typically also want:
|
|
52
|
+
✅ Tailwind v4 (recommended order):
|
|
24
53
|
|
|
25
54
|
```css
|
|
26
|
-
@import
|
|
27
|
-
@import
|
|
55
|
+
@import 'tailwindcss';
|
|
56
|
+
@import '@fakhrirafiki/theme-engine/styles';
|
|
28
57
|
|
|
29
58
|
@custom-variant dark (&:is(.dark *));
|
|
30
59
|
```
|
|
31
60
|
|
|
32
|
-
|
|
61
|
+
ℹ️ Not using Tailwind v4?
|
|
33
62
|
|
|
34
63
|
```css
|
|
35
|
-
@import
|
|
36
|
-
@import
|
|
37
|
-
@import
|
|
38
|
-
@import
|
|
64
|
+
@import '@fakhrirafiki/theme-engine/styles/base.css';
|
|
65
|
+
@import '@fakhrirafiki/theme-engine/styles/animations.css';
|
|
66
|
+
@import '@fakhrirafiki/theme-engine/styles/components.css';
|
|
67
|
+
@import '@fakhrirafiki/theme-engine/styles/utilities.css';
|
|
39
68
|
```
|
|
40
69
|
|
|
41
|
-
### 2) Wrap with `ThemeProvider`
|
|
70
|
+
### 2) Wrap your app with `ThemeProvider`
|
|
42
71
|
|
|
43
|
-
In `app/layout.tsx`:
|
|
72
|
+
In `src/app/layout.tsx`:
|
|
44
73
|
|
|
45
74
|
```tsx
|
|
46
|
-
import {
|
|
47
|
-
import
|
|
75
|
+
import type { ReactNode } from 'react';
|
|
76
|
+
import { ThemeProvider } from '@fakhrirafiki/theme-engine';
|
|
77
|
+
import './globals.css';
|
|
48
78
|
|
|
49
|
-
export default function RootLayout({ children }: { children:
|
|
79
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
50
80
|
return (
|
|
51
81
|
<html lang="en" suppressHydrationWarning>
|
|
52
82
|
<body>
|
|
@@ -59,133 +89,158 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
59
89
|
}
|
|
60
90
|
```
|
|
61
91
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
- By default, `ThemeProvider` injects `ThemeScript` for preset restoration (to reduce flashes).
|
|
65
|
-
- `ThemeScript` restores **preset colors** only (it does not set the `dark`/`light` class).
|
|
66
|
-
- `defaultPreset="..."` pre-hydration only works for **built-in presets** (because `ThemeScript` uses `getPresetById()` internally). Custom `defaultPreset` still works after hydration.
|
|
92
|
+
### 3) Use it
|
|
67
93
|
|
|
68
|
-
##
|
|
94
|
+
## 🧑💻 Usage
|
|
69
95
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
Use the ready-made button:
|
|
96
|
+
Toggle mode:
|
|
73
97
|
|
|
74
98
|
```tsx
|
|
75
|
-
|
|
99
|
+
'use client';
|
|
76
100
|
|
|
77
|
-
import {
|
|
101
|
+
import { useThemeEngine } from '@fakhrirafiki/theme-engine';
|
|
78
102
|
|
|
79
|
-
export function
|
|
80
|
-
|
|
103
|
+
export function ModeButtons() {
|
|
104
|
+
const { mode, setDarkMode, toggleDarkMode } = useThemeEngine();
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div>
|
|
108
|
+
<button onClick={() => setDarkMode('system')}>System</button>
|
|
109
|
+
<button onClick={() => setDarkMode('light')}>Light</button>
|
|
110
|
+
<button onClick={() => setDarkMode('dark')}>Dark</button>
|
|
111
|
+
<button onClick={() => toggleDarkMode()}>Toggle</button>
|
|
112
|
+
<div>Current: {mode}</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
81
115
|
}
|
|
82
116
|
```
|
|
83
117
|
|
|
84
|
-
|
|
118
|
+
Pick a theme preset by ID:
|
|
85
119
|
|
|
86
120
|
```tsx
|
|
87
|
-
|
|
121
|
+
'use client';
|
|
88
122
|
|
|
89
|
-
import {
|
|
123
|
+
import { useThemeEngine } from '@fakhrirafiki/theme-engine';
|
|
124
|
+
|
|
125
|
+
export function PresetButtons() {
|
|
126
|
+
const { applyThemeById, clearTheme, currentTheme } = useThemeEngine();
|
|
90
127
|
|
|
91
|
-
export function ModeButtons() {
|
|
92
|
-
const { mode, setMode, toggleMode } = useTheme();
|
|
93
128
|
return (
|
|
94
129
|
<div>
|
|
95
|
-
<button onClick={() =>
|
|
96
|
-
<button onClick={() =>
|
|
97
|
-
<
|
|
98
|
-
<button onClick={() => toggleMode()}>Toggle</button>
|
|
99
|
-
<div>Current: {mode}</div>
|
|
130
|
+
<button onClick={() => applyThemeById('modern-minimal')}>Modern Minimal</button>
|
|
131
|
+
<button onClick={() => clearTheme()}>Reset</button>
|
|
132
|
+
<div>Active: {currentTheme?.presetName ?? 'Default'}</div>
|
|
100
133
|
</div>
|
|
101
134
|
);
|
|
102
135
|
}
|
|
103
136
|
```
|
|
104
137
|
|
|
105
|
-
|
|
138
|
+
💡 Want typed autocomplete (built-in IDs + your custom IDs)? Use a generic:
|
|
106
139
|
|
|
107
140
|
```tsx
|
|
108
|
-
|
|
141
|
+
'use client';
|
|
109
142
|
|
|
110
|
-
import {
|
|
111
|
-
import { customPresets } from
|
|
143
|
+
import { ThemePresets, useThemeEngine } from '@fakhrirafiki/theme-engine';
|
|
144
|
+
import { customPresets } from './custom-theme-presets';
|
|
112
145
|
|
|
113
|
-
export function
|
|
114
|
-
const {
|
|
146
|
+
export function TypedPresetButtons() {
|
|
147
|
+
const { applyThemeById } = useThemeEngine<ThemePresets<typeof customPresets>>();
|
|
115
148
|
|
|
116
149
|
return (
|
|
117
150
|
<div>
|
|
118
|
-
<button onClick={() =>
|
|
119
|
-
<button onClick={() =>
|
|
120
|
-
<button onClick={() => clearPreset()}>Reset</button>
|
|
121
|
-
<div>Active: {currentPreset?.presetName ?? "Default"}</div>
|
|
151
|
+
<button onClick={() => applyThemeById('my-brand')}>My Brand</button>
|
|
152
|
+
<button onClick={() => applyThemeById('modern-minimal')}>Modern Minimal</button>
|
|
122
153
|
</div>
|
|
123
154
|
);
|
|
124
155
|
}
|
|
125
156
|
```
|
|
126
157
|
|
|
127
|
-
|
|
158
|
+
---
|
|
128
159
|
|
|
129
|
-
|
|
130
|
-
"use client";
|
|
160
|
+
## Concepts
|
|
131
161
|
|
|
132
|
-
|
|
162
|
+
### Mode vs preset
|
|
133
163
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
```
|
|
164
|
+
- 🌓 **Mode** controls the `<html>` class (`.light` / `.dark`) and `color-scheme`.
|
|
165
|
+
- 🎨 **Preset** controls semantic design tokens (CSS variables like `--background`, `--primary`, etc).
|
|
138
166
|
|
|
139
|
-
|
|
167
|
+
### SSR & flashes
|
|
140
168
|
|
|
141
|
-
|
|
169
|
+
- `ThemeProvider` injects `ThemeScript` to restore **preset colors** before hydration (reduces flashes).
|
|
170
|
+
- `ThemeScript` restores **preset colors only** (it does not set the `.dark` / `.light` class).
|
|
171
|
+
- `defaultPreset="..."` pre-hydration only works for **built-in presets**. Custom `defaultPreset` still works after hydration.
|
|
142
172
|
|
|
143
|
-
|
|
173
|
+
### Persistence
|
|
144
174
|
|
|
145
|
-
|
|
146
|
-
import { getPresetIds, getPresetById } from "@fakhrirafiki/theme-engine";
|
|
175
|
+
By default:
|
|
147
176
|
|
|
148
|
-
|
|
149
|
-
|
|
177
|
+
- Mode is stored in `localStorage['theme-engine-theme']`
|
|
178
|
+
- Preset is stored in `localStorage['theme-preset']`
|
|
179
|
+
|
|
180
|
+
If you run multiple apps on the same domain, override the keys:
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
<ThemeProvider modeStorageKey="my-app:mode" presetStorageKey="my-app:preset">
|
|
184
|
+
{children}
|
|
185
|
+
</ThemeProvider>
|
|
150
186
|
```
|
|
151
187
|
|
|
152
|
-
|
|
188
|
+
---
|
|
153
189
|
|
|
154
|
-
|
|
190
|
+
## 🧩 Custom presets (recommended)
|
|
155
191
|
|
|
156
|
-
|
|
157
|
-
|
|
192
|
+
Create presets in TweakCN-compatible format and pass them into `ThemeProvider`.
|
|
193
|
+
|
|
194
|
+
✅ Tip: use `satisfies` to preserve literal keys for TS autocomplete:
|
|
195
|
+
|
|
196
|
+
### 🎛️ Get a brand theme from TweakCN (recommended)
|
|
197
|
+
|
|
198
|
+
The fastest way to create a great-looking preset is to use the TweakCN editor:
|
|
199
|
+
|
|
200
|
+
- https://tweakcn.com/editor/theme
|
|
201
|
+
|
|
202
|
+
Pick a theme, tweak the colors, then copy the preset output and paste it into your `customPresets` object (it matches the `TweakCNThemePreset` shape).
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { type TweakCNThemePreset } from '@fakhrirafiki/theme-engine';
|
|
158
206
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
label: "My Brand",
|
|
207
|
+
export const customPresets = {
|
|
208
|
+
'my-brand': {
|
|
209
|
+
label: 'My Brand',
|
|
163
210
|
styles: {
|
|
164
211
|
light: {
|
|
165
|
-
background:
|
|
166
|
-
foreground:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
212
|
+
background: '#ffffff',
|
|
213
|
+
foreground: '#111827',
|
|
214
|
+
primary: '#2563eb',
|
|
215
|
+
'primary-foreground': '#ffffff',
|
|
216
|
+
secondary: '#e5e7eb',
|
|
217
|
+
'secondary-foreground': '#111827',
|
|
218
|
+
card: '#ffffff',
|
|
219
|
+
'card-foreground': '#111827',
|
|
173
220
|
},
|
|
174
221
|
dark: {
|
|
175
|
-
background:
|
|
176
|
-
foreground:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
222
|
+
background: '#0b1020',
|
|
223
|
+
foreground: '#f9fafb',
|
|
224
|
+
primary: '#60a5fa',
|
|
225
|
+
'primary-foreground': '#0b1020',
|
|
226
|
+
secondary: '#1f2937',
|
|
227
|
+
'secondary-foreground': '#f9fafb',
|
|
228
|
+
card: '#111827',
|
|
229
|
+
'card-foreground': '#f9fafb',
|
|
183
230
|
},
|
|
184
231
|
},
|
|
185
232
|
},
|
|
186
233
|
} satisfies Record<string, TweakCNThemePreset>;
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Then in your providers/layout:
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
import type { ReactNode } from 'react';
|
|
240
|
+
import { ThemeProvider } from '@fakhrirafiki/theme-engine';
|
|
241
|
+
import { customPresets } from './custom-theme-presets';
|
|
187
242
|
|
|
188
|
-
export function
|
|
243
|
+
export function AppProviders({ children }: { children: ReactNode }) {
|
|
189
244
|
return (
|
|
190
245
|
<ThemeProvider customPresets={customPresets} defaultPreset="my-brand">
|
|
191
246
|
{children}
|
|
@@ -194,28 +249,28 @@ export function AppRoot({ children }: { children: React.ReactNode }) {
|
|
|
194
249
|
}
|
|
195
250
|
```
|
|
196
251
|
|
|
197
|
-
|
|
252
|
+
Notes:
|
|
198
253
|
|
|
199
254
|
- Custom presets are validated in `ThemeProvider`.
|
|
200
|
-
- Invalid custom presets are skipped
|
|
201
|
-
-
|
|
255
|
+
- Invalid custom presets are skipped (warnings/errors are logged on `localhost`).
|
|
256
|
+
- Preset values can be `H S% L%`, `hsl(...)`, `#hex`, `rgb(...)`, and modern CSS colors like `oklch(...)` (they are normalized internally).
|
|
202
257
|
|
|
203
|
-
|
|
258
|
+
---
|
|
204
259
|
|
|
205
|
-
|
|
260
|
+
## 🎁 Built-in presets
|
|
206
261
|
|
|
207
|
-
|
|
208
|
-
- Preset is stored in `localStorage['theme-preset']`.
|
|
262
|
+
The package ships with a built-in preset collection:
|
|
209
263
|
|
|
210
|
-
|
|
264
|
+
```ts
|
|
265
|
+
import { getPresetIds, getPresetById } from '@fakhrirafiki/theme-engine';
|
|
211
266
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
{children}
|
|
215
|
-
</ThemeProvider>
|
|
267
|
+
const ids = getPresetIds();
|
|
268
|
+
const modernMinimal = getPresetById('modern-minimal');
|
|
216
269
|
```
|
|
217
270
|
|
|
218
|
-
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## 🎨 Tailwind tokens you get
|
|
219
274
|
|
|
220
275
|
After importing `@fakhrirafiki/theme-engine/styles`, you can use semantic tokens like:
|
|
221
276
|
|
|
@@ -229,28 +284,139 @@ After importing `@fakhrirafiki/theme-engine/styles`, you can use semantic tokens
|
|
|
229
284
|
| Muted | `bg-muted`, `text-muted-foreground` | `--muted`, `--muted-foreground` | Subtle backgrounds / helper text |
|
|
230
285
|
| Accent | `bg-accent`, `text-accent-foreground` | `--accent`, `--accent-foreground` | Emphasis (not status colors) |
|
|
231
286
|
| Destructive | `bg-destructive`, `text-destructive-foreground` | `--destructive`, `--destructive-foreground` | Danger actions |
|
|
232
|
-
| Borders / focus | `border-border`, `border-input`, `ring-ring` | `--border`, `--input`, `--ring` |
|
|
287
|
+
| Borders / focus | `border-border`, `border-input`, `ring-ring` | `--border`, `--input`, `--ring` | Used by `outline-ring/50` too |
|
|
233
288
|
| Charts | `bg-chart-1`, `text-chart-2` | `--chart-1` ... `--chart-5` | Data viz palettes |
|
|
234
289
|
| Sidebar | `bg-sidebar`, `text-sidebar-foreground`, `bg-sidebar-primary`, `border-sidebar-border` | `--sidebar-*` | Handy for dashboard layouts |
|
|
235
|
-
| Status accents | `bg-accent-success`, `text-accent-danger-foreground` | `--accent-<name>`, `--accent-<name>-foreground` | Optional: only if
|
|
290
|
+
| Status accents | `bg-accent-success`, `text-accent-danger-foreground` | `--accent-<name>`, `--accent-<name>-foreground` | Optional: only if preset defines `accent-*` |
|
|
236
291
|
| Radius scale | `rounded-sm`, `rounded-md`, `rounded-lg`, `rounded-xl` | `--radius-sm`, `--radius-md`, `--radius-lg`, `--radius-xl` | Derived from `--radius` |
|
|
237
292
|
| Tracking scale | `tracking-tighter`, `tracking-wide` | `--tracking-*` | Derived from `--letter-spacing` |
|
|
238
|
-
| Fonts | `font-sans`, `font-serif`, `font-mono` | `--font-sans`, `--font-serif`, `--font-mono` |
|
|
239
|
-
| Shadows | `shadow-sm`, `shadow-md`, `shadow-xl` | `--shadow-*` |
|
|
293
|
+
| Fonts | `font-sans`, `font-serif`, `font-mono` | `--font-sans`, `--font-serif`, `--font-mono` | Defaults in `base.css` |
|
|
294
|
+
| Shadows | `shadow-sm`, `shadow-md`, `shadow-xl` | `--shadow-*` | Derived from `--shadow-*` knobs |
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## 🧱 Components
|
|
299
|
+
|
|
300
|
+
### `ThemeToggle`
|
|
301
|
+
|
|
302
|
+
Ready-made mode toggle button (with View Transition ripple when supported).
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
'use client';
|
|
240
306
|
|
|
241
|
-
|
|
307
|
+
import { ThemeToggle } from '@fakhrirafiki/theme-engine';
|
|
242
308
|
|
|
243
|
-
|
|
309
|
+
export function HeaderThemeToggle() {
|
|
310
|
+
return <ThemeToggle size="md" variant="ghost" />;
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### `ThemePresetButtons`
|
|
315
|
+
|
|
316
|
+
Animated preset picker (shows custom presets first, then built-ins):
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
'use client';
|
|
320
|
+
|
|
321
|
+
import { ThemePresetButtons } from '@fakhrirafiki/theme-engine';
|
|
322
|
+
|
|
323
|
+
export function PresetPicker() {
|
|
324
|
+
return <ThemePresetButtons />;
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## 🧾 API Reference
|
|
331
|
+
|
|
332
|
+
### `ThemeProvider`
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
<ThemeProvider
|
|
336
|
+
defaultMode="system"
|
|
337
|
+
defaultPreset="modern-minimal"
|
|
338
|
+
modeStorageKey="theme-engine-theme"
|
|
339
|
+
presetStorageKey="theme-preset"
|
|
340
|
+
customPresets={customPresets}
|
|
341
|
+
/>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
| Prop | Type | Default | Description |
|
|
345
|
+
| --- | --- | --- | --- |
|
|
346
|
+
| `children` | `ReactNode` | required | React subtree |
|
|
347
|
+
| `defaultMode` | `Mode` | `'system'` | Used when no persisted value |
|
|
348
|
+
| `defaultPreset` | `BuiltInPresetId \| keyof customPresets` | `undefined` | Default preset (see SSR note) |
|
|
349
|
+
| `modeStorageKey` | `string` | `'theme-engine-theme'` | `localStorage` key for mode |
|
|
350
|
+
| `presetStorageKey` | `string` | `'theme-preset'` | `localStorage` key for preset |
|
|
351
|
+
| `customPresets` | `Record<string, TweakCNThemePreset>` | `undefined` | Add your own presets (can override built-ins by ID) |
|
|
352
|
+
| `ThemeScript` | n/a | always on | `ThemeProvider` always injects `ThemeScript` for pre-hydration preset restoration |
|
|
353
|
+
|
|
354
|
+
### `useThemeEngine()`
|
|
355
|
+
|
|
356
|
+
Signature:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
useThemeEngine<TCustomPresets = undefined>()
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
To get typed custom preset IDs:
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
useThemeEngine<ThemePresets<typeof customPresets>>()
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Return fields:
|
|
369
|
+
|
|
370
|
+
| Field | Type | Description |
|
|
371
|
+
| --- | --- | --- |
|
|
372
|
+
| `darkMode` | `boolean` | `resolvedMode === 'dark'` |
|
|
373
|
+
| `mode` | `'light' \| 'dark' \| 'system'` | Current user preference |
|
|
374
|
+
| `resolvedMode` | `'light' \| 'dark'` | Resolved mode (never `system`) |
|
|
375
|
+
| `setDarkMode` | `(mode: Mode) => void` | Set `light \| dark \| system` |
|
|
376
|
+
| `toggleDarkMode` | `(coords?: { x: number; y: number }) => void` | Toggles light/dark (and exits `system`) |
|
|
377
|
+
| `applyThemeById` | `(id: ThemeId) => void` | Apply a preset by ID (alias: `applyPresetById`) |
|
|
378
|
+
| `clearTheme` | `() => void` | Clear preset and fall back to `defaultPreset` if provided (alias: `clearPreset`) |
|
|
379
|
+
| `currentTheme` | `{ presetId; presetName; colors; appliedAt } \| null` | Current preset (alias: `currentPreset`) |
|
|
380
|
+
| `isUsingDefaultPreset` | `boolean` | Whether current preset equals `defaultPreset` |
|
|
381
|
+
| `availablePresets` | `Record<string, TweakCNThemePreset>` | Built-in + custom |
|
|
382
|
+
| `builtInPresets` | `Record<string, TweakCNThemePreset>` | Built-in only |
|
|
383
|
+
| `customPresets` | `Record<string, TweakCNThemePreset>` | Custom only |
|
|
384
|
+
|
|
385
|
+
### `ThemeScript`
|
|
386
|
+
|
|
387
|
+
Normally you don’t need this (it’s injected by `ThemeProvider` by default).
|
|
388
|
+
|
|
389
|
+
| Prop | Type | Default | Description |
|
|
390
|
+
| --- | --- | --- | --- |
|
|
391
|
+
| `presetStorageKey` | `string` | `'theme-preset'` | Must match `ThemeProvider` |
|
|
392
|
+
| `defaultPreset` | `string` | `undefined` | Built-in preset ID for pre-hydration default |
|
|
393
|
+
|
|
394
|
+
### Utilities
|
|
395
|
+
|
|
396
|
+
| Export | Description |
|
|
397
|
+
| --- | --- |
|
|
398
|
+
| `formatColor(color, format)` | Converts a color string into `hsl`/`rgb`/`hex` |
|
|
399
|
+
| `withAlpha(hslTriplet, alpha)` | Adds alpha to an HSL triplet |
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## 🩹 Troubleshooting
|
|
404
|
+
|
|
405
|
+
### `useThemeEngine must be used within a ThemeProvider`
|
|
244
406
|
|
|
245
407
|
Wrap your component tree with `ThemeProvider` (and ensure the component is a client component).
|
|
246
408
|
|
|
409
|
+
Note: the thrown error string might mention `useTheme` because `useThemeEngine()` uses it internally.
|
|
410
|
+
|
|
247
411
|
### Preset doesn’t apply on refresh
|
|
248
412
|
|
|
249
|
-
|
|
413
|
+
`ThemeProvider` injects `ThemeScript` automatically. Avoid rendering `ThemeScript` manually (you may end up with duplicates).
|
|
414
|
+
|
|
415
|
+
### Styles don’t load / components look unstyled
|
|
250
416
|
|
|
251
|
-
|
|
417
|
+
Ensure your `globals.css` imports `@fakhrirafiki/theme-engine/styles` (and Tailwind v4 is configured if you rely on Tailwind utilities).
|
|
252
418
|
|
|
253
|
-
|
|
419
|
+
---
|
|
254
420
|
|
|
255
421
|
## License
|
|
256
422
|
|