@fakhrirafiki/theme-engine 0.2.5 β 0.4.0
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 +142 -482
- package/dist/index.d.mts +57 -9
- package/dist/index.d.ts +57 -9
- package/dist/index.js +390 -262
- package/dist/index.mjs +378 -252
- package/dist/styles/animations.css +23 -1
- package/dist/styles/components.css +12 -3
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -1,582 +1,242 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Theme Engine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Theme system for React: **mode** (`light | dark | system`) + **color presets** (CSS variables) with optional View Transition ripple.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
- π **Unified Theming** - Seamlessly coordinate dark/light mode with custom color presets
|
|
8
|
-
- π¨ **Beautiful Presets** - 9 curated color themes with smooth animated selection
|
|
9
|
-
- β¨ **Smooth Transitions** - Ripple effects using View Transition API
|
|
10
|
-
- πΎ **Persistent** - localStorage integration with SSR-safe restoration
|
|
11
|
-
- βΏ **Accessible** - Full ARIA support and reduced motion compatibility
|
|
12
|
-
- π **Zero Config** - Works out of the box with sensible defaults
|
|
13
|
-
- π¦ **Lightweight** - Optimized bundle size and direct source consumption
|
|
14
|
-
|
|
15
|
-
## π Quick Start
|
|
5
|
+
## Install
|
|
16
6
|
|
|
17
7
|
```bash
|
|
18
|
-
|
|
19
|
-
npm install theme-engine
|
|
20
|
-
|
|
21
|
-
# Or with yarn/pnpm
|
|
22
|
-
yarn add theme-engine
|
|
23
|
-
pnpm add theme-engine
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
```tsx
|
|
27
|
-
import { ThemeProvider, ThemeScript, ThemeToggle } from 'theme-engine'
|
|
28
|
-
import 'theme-engine/styles'
|
|
29
|
-
|
|
30
|
-
function App() {
|
|
31
|
-
return (
|
|
32
|
-
<html lang="en" suppressHydrationWarning>
|
|
33
|
-
<head>
|
|
34
|
-
<ThemeScript
|
|
35
|
-
presetStorageKey="my-app-theme-preset"
|
|
36
|
-
defaultPreset="twitter"
|
|
37
|
-
/>
|
|
38
|
-
</head>
|
|
39
|
-
<body>
|
|
40
|
-
<ThemeProvider
|
|
41
|
-
defaultMode="system"
|
|
42
|
-
defaultPreset="twitter"
|
|
43
|
-
modeStorageKey="my-app-mode"
|
|
44
|
-
presetStorageKey="my-app-theme-preset"
|
|
45
|
-
enablePresets={true}
|
|
46
|
-
>
|
|
47
|
-
<header>
|
|
48
|
-
<ThemeToggle />
|
|
49
|
-
</header>
|
|
50
|
-
<main>
|
|
51
|
-
Your app content
|
|
52
|
-
</main>
|
|
53
|
-
</ThemeProvider>
|
|
54
|
-
</body>
|
|
55
|
-
</html>
|
|
56
|
-
)
|
|
57
|
-
}
|
|
8
|
+
pnpm add @fakhrirafiki/theme-engine
|
|
58
9
|
```
|
|
59
10
|
|
|
60
|
-
##
|
|
11
|
+
## Setup (Next.js App Router)
|
|
61
12
|
|
|
62
|
-
|
|
13
|
+
### 1) Import CSS once
|
|
63
14
|
|
|
64
|
-
|
|
65
|
-
import { ThemePresetButtons } from 'theme-engine'
|
|
15
|
+
In `app/globals.css`:
|
|
66
16
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<ThemePresetButtons
|
|
70
|
-
animation={{
|
|
71
|
-
enabled: true,
|
|
72
|
-
duration: 4,
|
|
73
|
-
rowCount: 2,
|
|
74
|
-
scrollSpeed: 1,
|
|
75
|
-
}}
|
|
76
|
-
layout={{
|
|
77
|
-
buttonWidth: 140,
|
|
78
|
-
buttonGap: 12,
|
|
79
|
-
showColorBoxes: true,
|
|
80
|
-
colorBoxCount: 3,
|
|
81
|
-
}}
|
|
82
|
-
/>
|
|
83
|
-
)
|
|
84
|
-
}
|
|
17
|
+
```css
|
|
18
|
+
@import "@fakhrirafiki/theme-engine/styles";
|
|
85
19
|
```
|
|
86
20
|
|
|
87
|
-
|
|
21
|
+
Notes:
|
|
88
22
|
|
|
89
|
-
|
|
23
|
+
- If you use Tailwind v4, you will typically also want:
|
|
90
24
|
|
|
91
|
-
|
|
25
|
+
```css
|
|
26
|
+
@import "tailwindcss";
|
|
27
|
+
@import "@fakhrirafiki/theme-engine/styles";
|
|
92
28
|
|
|
93
|
-
|
|
94
|
-
<ThemeProvider
|
|
95
|
-
defaultMode="system" // "light" | "dark" | "system"
|
|
96
|
-
defaultPreset="twitter" // Default preset ID to apply
|
|
97
|
-
modeStorageKey="app-mode" // localStorage key for theme mode
|
|
98
|
-
presetStorageKey="app-preset" // localStorage key for color preset
|
|
99
|
-
enablePresets={true} // Enable preset functionality
|
|
100
|
-
>
|
|
101
|
-
{children}
|
|
102
|
-
</ThemeProvider>
|
|
29
|
+
@custom-variant dark (&:is(.dark *));
|
|
103
30
|
```
|
|
104
31
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
Add your own theme presets alongside the built-in TweakCN collection:
|
|
108
|
-
|
|
109
|
-
```tsx
|
|
110
|
-
import { ThemeProvider, type TweakCNThemePreset } from 'theme-engine'
|
|
111
|
-
|
|
112
|
-
const customPresets: Record<string, TweakCNThemePreset> = {
|
|
113
|
-
"my-brand": {
|
|
114
|
-
label: "My Brand Theme",
|
|
115
|
-
styles: {
|
|
116
|
-
light: {
|
|
117
|
-
background: "#ffffff",
|
|
118
|
-
foreground: "#1a1a1a",
|
|
119
|
-
primary: "#0066cc",
|
|
120
|
-
"primary-foreground": "#ffffff",
|
|
121
|
-
secondary: "#f0f8ff",
|
|
122
|
-
"secondary-foreground": "#004499",
|
|
123
|
-
// ... all 36+ CSS properties
|
|
124
|
-
border: "#e2e8f0",
|
|
125
|
-
input: "#f1f5f9",
|
|
126
|
-
ring: "#0066cc",
|
|
127
|
-
radius: "0.5rem",
|
|
128
|
-
},
|
|
129
|
-
dark: {
|
|
130
|
-
background: "#0a0a0a",
|
|
131
|
-
foreground: "#fafafa",
|
|
132
|
-
primary: "#3399ff",
|
|
133
|
-
"primary-foreground": "#000000",
|
|
134
|
-
// ... all 36+ CSS properties
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
},
|
|
138
|
-
"retro-gaming": {
|
|
139
|
-
label: "Retro Gaming",
|
|
140
|
-
styles: {
|
|
141
|
-
light: {
|
|
142
|
-
background: "#f5f5dc",
|
|
143
|
-
foreground: "#2a1810",
|
|
144
|
-
primary: "#ff6b35",
|
|
145
|
-
"primary-foreground": "#ffffff",
|
|
146
|
-
accent: "#06ffa5",
|
|
147
|
-
"accent-foreground": "#2a1810",
|
|
148
|
-
// ... complete theme definition
|
|
149
|
-
},
|
|
150
|
-
dark: {
|
|
151
|
-
background: "#1a1a2e",
|
|
152
|
-
foreground: "#ffd23f",
|
|
153
|
-
primary: "#ff6b35",
|
|
154
|
-
"primary-foreground": "#000000",
|
|
155
|
-
// ... complete theme definition
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
32
|
+
- `@fakhrirafiki/theme-engine/styles` includes Tailwind v4 directives (`@theme inline`). If you are not using Tailwind v4, import only the non-tailwind CSS modules instead:
|
|
160
33
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
>
|
|
167
|
-
{/* Custom presets automatically appear in ThemePresetButtons */}
|
|
168
|
-
<ThemePresetButtons
|
|
169
|
-
showBuiltIn={true}
|
|
170
|
-
showCustom={true}
|
|
171
|
-
groupBy="provider"
|
|
172
|
-
labels={{
|
|
173
|
-
builtIn: "Built-in Themes",
|
|
174
|
-
custom: "π¨ Brand Themes"
|
|
175
|
-
}}
|
|
176
|
-
/>
|
|
177
|
-
</ThemeProvider>
|
|
178
|
-
)
|
|
179
|
-
}
|
|
34
|
+
```css
|
|
35
|
+
@import "@fakhrirafiki/theme-engine/styles/base.css";
|
|
36
|
+
@import "@fakhrirafiki/theme-engine/styles/animations.css";
|
|
37
|
+
@import "@fakhrirafiki/theme-engine/styles/components.css";
|
|
38
|
+
@import "@fakhrirafiki/theme-engine/styles/utilities.css";
|
|
180
39
|
```
|
|
181
40
|
|
|
182
|
-
###
|
|
41
|
+
### 2) Wrap with `ThemeProvider`
|
|
183
42
|
|
|
184
|
-
|
|
43
|
+
In `app/layout.tsx`:
|
|
185
44
|
|
|
186
45
|
```tsx
|
|
187
|
-
import {
|
|
188
|
-
|
|
189
|
-
const customPresets = {
|
|
190
|
-
"my-theme": {
|
|
191
|
-
label: "My Theme",
|
|
192
|
-
styles: {
|
|
193
|
-
light: { /* theme properties */ },
|
|
194
|
-
dark: { /* theme properties */ }
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
46
|
+
import { ThemeProvider } from "@fakhrirafiki/theme-engine";
|
|
47
|
+
import "./globals.css";
|
|
198
48
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
49
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
50
|
+
return (
|
|
51
|
+
<html lang="en" suppressHydrationWarning>
|
|
52
|
+
<body>
|
|
53
|
+
<ThemeProvider defaultMode="system" defaultPreset="modern-minimal">
|
|
54
|
+
{children}
|
|
55
|
+
</ThemeProvider>
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|
|
58
|
+
);
|
|
206
59
|
}
|
|
207
|
-
|
|
208
|
-
// Log detailed validation results
|
|
209
|
-
logValidationResult(validationResult, 'My Custom Presets');
|
|
210
60
|
```
|
|
211
61
|
|
|
212
|
-
|
|
213
|
-
- `background`, `foreground`
|
|
214
|
-
- `primary`, `primary-foreground`
|
|
215
|
-
- `secondary`, `secondary-foreground`
|
|
216
|
-
- `card`, `card-foreground`
|
|
217
|
-
|
|
218
|
-
**Recommended Properties** for complete themes:
|
|
219
|
-
- `border`, `input`, `ring`
|
|
220
|
-
- `muted`, `muted-foreground`
|
|
221
|
-
- `accent`, `accent-foreground`
|
|
222
|
-
- `destructive`, `destructive-foreground`
|
|
223
|
-
- All 36+ CSS properties for full compatibility
|
|
62
|
+
Notes:
|
|
224
63
|
|
|
225
|
-
|
|
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.
|
|
226
67
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
```tsx
|
|
230
|
-
<ThemeScript
|
|
231
|
-
presetStorageKey="app-preset"
|
|
232
|
-
defaultPreset="twitter" // Apply default preset if none stored
|
|
233
|
-
/>
|
|
234
|
-
```
|
|
68
|
+
## Day-to-day usage
|
|
235
69
|
|
|
236
|
-
###
|
|
70
|
+
### Toggle dark/light mode
|
|
237
71
|
|
|
238
|
-
|
|
72
|
+
Use the ready-made button:
|
|
239
73
|
|
|
240
74
|
```tsx
|
|
241
|
-
|
|
242
|
-
size="sm" // "sm" | "md" | "lg"
|
|
243
|
-
variant="ghost" // "default" | "outline" | "ghost"
|
|
244
|
-
className="custom-class"
|
|
245
|
-
/>
|
|
246
|
-
```
|
|
75
|
+
"use client";
|
|
247
76
|
|
|
248
|
-
|
|
77
|
+
import { ThemeToggle } from "@fakhrirafiki/theme-engine";
|
|
249
78
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
```tsx
|
|
253
|
-
{/* Basic usage - shows all presets */}
|
|
254
|
-
<ThemePresetButtons />
|
|
255
|
-
|
|
256
|
-
{/* Advanced usage with categorization */}
|
|
257
|
-
<ThemePresetButtons
|
|
258
|
-
categories={["nature", "vibrant"]} // Filter presets by category
|
|
259
|
-
maxPresets={20} // Limit number of presets shown
|
|
260
|
-
showBuiltIn={true} // Show built-in presets (default: true)
|
|
261
|
-
showCustom={true} // Show custom presets (default: true)
|
|
262
|
-
groupBy="provider" // Group by: 'none' | 'category' | 'provider'
|
|
263
|
-
labels={{
|
|
264
|
-
builtIn: "Built-in Themes",
|
|
265
|
-
custom: "π¨ Your Themes"
|
|
266
|
-
}}
|
|
267
|
-
showSectionHeaders={true} // Show section headers when grouping
|
|
268
|
-
animation={{
|
|
269
|
-
enabled: true,
|
|
270
|
-
duration: 4, // Animation duration in seconds
|
|
271
|
-
rowCount: 2, // Number of animation rows
|
|
272
|
-
scrollSpeed: 1, // Scroll speed multiplier
|
|
273
|
-
}}
|
|
274
|
-
layout={{
|
|
275
|
-
buttonWidth: 140,
|
|
276
|
-
buttonGap: 12,
|
|
277
|
-
rowGap: 12,
|
|
278
|
-
showColorBoxes: true,
|
|
279
|
-
colorBoxCount: 3,
|
|
280
|
-
}}
|
|
281
|
-
/>
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
## π£ Hooks
|
|
285
|
-
|
|
286
|
-
### useTheme
|
|
287
|
-
|
|
288
|
-
Access both theme mode and preset state:
|
|
289
|
-
|
|
290
|
-
```tsx
|
|
291
|
-
function ThemeStatus() {
|
|
292
|
-
const {
|
|
293
|
-
mode, // Current theme setting
|
|
294
|
-
setMode, // Change theme mode
|
|
295
|
-
currentPreset, // Current color preset
|
|
296
|
-
applyPreset, // Apply new preset
|
|
297
|
-
clearPreset // Clear current preset
|
|
298
|
-
} = useTheme()
|
|
299
|
-
|
|
300
|
-
return (
|
|
301
|
-
<div>
|
|
302
|
-
<p>Mode: {mode}</p>
|
|
303
|
-
<p>Preset: {currentPreset?.presetName || 'Default'}</p>
|
|
304
|
-
</div>
|
|
305
|
-
)
|
|
79
|
+
export function HeaderThemeToggle() {
|
|
80
|
+
return <ThemeToggle size="md" variant="ghost" />;
|
|
306
81
|
}
|
|
307
82
|
```
|
|
308
83
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
Theme Engine includes a complete collection of TweakCN-compatible presets with automatic built-in integration:
|
|
84
|
+
Or control it yourself:
|
|
312
85
|
|
|
313
86
|
```tsx
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// Get all preset IDs
|
|
317
|
-
const allPresetIds = getPresetIds()
|
|
318
|
-
|
|
319
|
-
// Get specific preset
|
|
320
|
-
const twitterPreset = getPresetById('twitter')
|
|
321
|
-
|
|
322
|
-
// Access raw TweakCN data
|
|
323
|
-
const rawPresets = tweakcnPresets
|
|
324
|
-
|
|
325
|
-
// Available presets: "twitter", "modern-minimal", "ocean-blue", etc.
|
|
326
|
-
```
|
|
87
|
+
"use client";
|
|
327
88
|
|
|
328
|
-
|
|
89
|
+
import { useTheme } from "@fakhrirafiki/theme-engine";
|
|
329
90
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
ThemePresetButtons automatically uses the built-in TweakCN preset collection - no configuration needed:
|
|
333
|
-
|
|
334
|
-
```tsx
|
|
335
|
-
import { ThemePresetButtons } from 'theme-engine'
|
|
336
|
-
|
|
337
|
-
function App() {
|
|
91
|
+
export function ModeButtons() {
|
|
92
|
+
const { mode, setMode, toggleMode } = useTheme();
|
|
338
93
|
return (
|
|
339
94
|
<div>
|
|
340
|
-
{
|
|
341
|
-
<
|
|
342
|
-
|
|
343
|
-
{
|
|
344
|
-
<
|
|
345
|
-
categories={["blue", "nature"]}
|
|
346
|
-
maxPresets={10}
|
|
347
|
-
/>
|
|
95
|
+
<button onClick={() => setMode("system")}>System</button>
|
|
96
|
+
<button onClick={() => setMode("light")}>Light</button>
|
|
97
|
+
<button onClick={() => setMode("dark")}>Dark</button>
|
|
98
|
+
<button onClick={() => toggleMode()}>Toggle</button>
|
|
99
|
+
<div>Current: {mode}</div>
|
|
348
100
|
</div>
|
|
349
|
-
)
|
|
101
|
+
);
|
|
350
102
|
}
|
|
351
103
|
```
|
|
352
104
|
|
|
353
|
-
###
|
|
354
|
-
|
|
355
|
-
Add your own presets to the TweakCN collection:
|
|
105
|
+
### Pick a preset (by ID)
|
|
356
106
|
|
|
357
107
|
```tsx
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const myCustomPreset: ThemePreset = {
|
|
361
|
-
id: "my-brand",
|
|
362
|
-
name: "Brand Theme",
|
|
363
|
-
colors: {
|
|
364
|
-
light: {
|
|
365
|
-
primary: "#0066cc",
|
|
366
|
-
secondary: "#f0f0f0",
|
|
367
|
-
accent: "#ff6600",
|
|
368
|
-
background: "#ffffff",
|
|
369
|
-
foreground: "#1a1a1a",
|
|
370
|
-
// ... all 36+ CSS properties
|
|
371
|
-
},
|
|
372
|
-
dark: {
|
|
373
|
-
primary: "#3399ff",
|
|
374
|
-
secondary: "#2a2a2a",
|
|
375
|
-
accent: "#ff8833",
|
|
376
|
-
background: "#1a1a1a",
|
|
377
|
-
foreground: "#ffffff",
|
|
378
|
-
// ... all 36+ CSS properties
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
108
|
+
"use client";
|
|
382
109
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const { applyPreset } = useTheme()
|
|
386
|
-
|
|
387
|
-
return (
|
|
388
|
-
<button onClick={() => applyPreset(myCustomPreset)}>
|
|
389
|
-
Apply Brand Theme
|
|
390
|
-
</button>
|
|
391
|
-
)
|
|
392
|
-
}
|
|
110
|
+
import { useTypedTheme } from "@fakhrirafiki/theme-engine";
|
|
111
|
+
import { customPresets } from "./custom-theme-presets";
|
|
393
112
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const { setThemePresetById } = useTheme()
|
|
113
|
+
export function PresetButtons() {
|
|
114
|
+
const { setThemePresetById, clearPreset, currentPreset } = useTypedTheme(customPresets);
|
|
397
115
|
|
|
398
116
|
return (
|
|
399
117
|
<div>
|
|
400
|
-
<button onClick={() => setThemePresetById(
|
|
401
|
-
|
|
402
|
-
</button>
|
|
403
|
-
<
|
|
404
|
-
Ngobrol Blue
|
|
405
|
-
</button>
|
|
118
|
+
<button onClick={() => setThemePresetById("my-brand")}>My Brand</button>
|
|
119
|
+
<button onClick={() => setThemePresetById("modern-minimal")}>Modern Minimal</button>
|
|
120
|
+
<button onClick={() => clearPreset()}>Reset</button>
|
|
121
|
+
<div>Active: {currentPreset?.presetName ?? "Default"}</div>
|
|
406
122
|
</div>
|
|
407
|
-
)
|
|
123
|
+
);
|
|
408
124
|
}
|
|
409
125
|
```
|
|
410
126
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
### Current API Usage
|
|
127
|
+
### Use the animated preset picker
|
|
414
128
|
|
|
415
|
-
Use the clean theming system for all projects:
|
|
416
129
|
```tsx
|
|
417
|
-
|
|
130
|
+
"use client";
|
|
418
131
|
|
|
419
|
-
|
|
420
|
-
return (
|
|
421
|
-
<html>
|
|
422
|
-
<head>
|
|
423
|
-
<ThemeScript presetStorageKey="app-preset" />
|
|
424
|
-
</head>
|
|
425
|
-
<body>
|
|
426
|
-
<ThemeProvider
|
|
427
|
-
defaultMode="system"
|
|
428
|
-
defaultPreset="twitter"
|
|
429
|
-
enablePresets={true}
|
|
430
|
-
>
|
|
431
|
-
{children}
|
|
432
|
-
</ThemeProvider>
|
|
433
|
-
</body>
|
|
434
|
-
</html>
|
|
435
|
-
)
|
|
436
|
-
}
|
|
132
|
+
import { ThemePresetButtons } from "@fakhrirafiki/theme-engine";
|
|
437
133
|
|
|
438
|
-
function
|
|
439
|
-
|
|
440
|
-
// Clean and simple!
|
|
134
|
+
export function PresetPicker() {
|
|
135
|
+
return <ThemePresetButtons />;
|
|
441
136
|
}
|
|
442
137
|
```
|
|
443
138
|
|
|
444
|
-
|
|
139
|
+
## Presets
|
|
445
140
|
|
|
446
|
-
|
|
447
|
-
- β
**Perfect coordination** - Dark/light mode works seamlessly with presets
|
|
448
|
-
- β
**Optimal performance** - Eliminates theme conflicts and infinite loops
|
|
449
|
-
- β
**Reliable persistence** - Separate localStorage keys prevent conflicts
|
|
141
|
+
### Built-in presets
|
|
450
142
|
|
|
451
|
-
|
|
143
|
+
The package ships with a built-in preset collection:
|
|
452
144
|
|
|
453
|
-
|
|
145
|
+
```tsx
|
|
146
|
+
import { getPresetIds, getPresetById } from "@fakhrirafiki/theme-engine";
|
|
454
147
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
@import "theme-engine/styles";
|
|
458
|
-
|
|
459
|
-
/* Or import specific modules */
|
|
460
|
-
@import "theme-engine/styles/base.css"; /* CSS variables & defaults */
|
|
461
|
-
@import "theme-engine/styles/animations.css"; /* View transition effects */
|
|
462
|
-
@import "theme-engine/styles/components.css"; /* Component styles */
|
|
463
|
-
@import "theme-engine/styles/utilities.css"; /* Dynamic utilities */
|
|
464
|
-
@import "theme-engine/styles/tailwind.css"; /* Complete Tailwind integration */
|
|
148
|
+
const ids = getPresetIds();
|
|
149
|
+
const modernMinimal = getPresetById("modern-minimal");
|
|
465
150
|
```
|
|
466
151
|
|
|
467
|
-
###
|
|
468
|
-
|
|
469
|
-
Theme engine manages 36+ CSS custom properties across categories:
|
|
470
|
-
|
|
471
|
-
**π¨ Colors (28 properties)**
|
|
472
|
-
- Base: `background`, `foreground`, `card`, `popover`
|
|
473
|
-
- Brand: `primary`, `secondary`, `accent`, `muted`
|
|
474
|
-
- System: `destructive`, `border`, `input`, `ring`
|
|
475
|
-
- Charts: `chart-1` through `chart-5`
|
|
476
|
-
- Sidebar: `sidebar`, `sidebar-primary`, `sidebar-accent`
|
|
477
|
-
|
|
478
|
-
**βοΈ Typography (3 properties)**
|
|
479
|
-
- `font-sans`, `font-serif`, `font-mono`
|
|
480
|
-
|
|
481
|
-
**π Layout (1 property)**
|
|
482
|
-
- `radius` - Border radius system
|
|
483
|
-
|
|
484
|
-
**π Shadows (6 properties)**
|
|
485
|
-
- `shadow-color`, `shadow-opacity`, `shadow-blur`
|
|
486
|
-
- `shadow-spread`, `shadow-offset-x`, `shadow-offset-y`
|
|
152
|
+
### Custom presets (recommended pattern)
|
|
487
153
|
|
|
488
|
-
|
|
489
|
-
- `letter-spacing`, `spacing`
|
|
154
|
+
Create your presets in TweakCN-compatible format and pass them into `ThemeProvider`:
|
|
490
155
|
|
|
491
|
-
|
|
156
|
+
```tsx
|
|
157
|
+
import { ThemeProvider, type TweakCNThemePreset } from "@fakhrirafiki/theme-engine";
|
|
492
158
|
|
|
493
|
-
|
|
159
|
+
// Tip: use `satisfies` (instead of `Record<string, ...>`) to keep literal keys for TS autocomplete
|
|
160
|
+
const customPresets = {
|
|
161
|
+
"my-brand": {
|
|
162
|
+
label: "My Brand",
|
|
163
|
+
styles: {
|
|
164
|
+
light: {
|
|
165
|
+
background: "#ffffff",
|
|
166
|
+
foreground: "#111827",
|
|
167
|
+
card: "#ffffff",
|
|
168
|
+
"card-foreground": "#111827",
|
|
169
|
+
primary: "#2563eb",
|
|
170
|
+
"primary-foreground": "#ffffff",
|
|
171
|
+
secondary: "#e5e7eb",
|
|
172
|
+
"secondary-foreground": "#111827",
|
|
173
|
+
},
|
|
174
|
+
dark: {
|
|
175
|
+
background: "#0b1020",
|
|
176
|
+
foreground: "#f9fafb",
|
|
177
|
+
card: "#111827",
|
|
178
|
+
"card-foreground": "#f9fafb",
|
|
179
|
+
primary: "#60a5fa",
|
|
180
|
+
"primary-foreground": "#0b1020",
|
|
181
|
+
secondary: "#1f2937",
|
|
182
|
+
"secondary-foreground": "#f9fafb",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
} satisfies Record<string, TweakCNThemePreset>;
|
|
494
187
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
/* Dynamic shadows */
|
|
502
|
-
.shadow-theme {
|
|
503
|
-
box-shadow: 0px var(--shadow-offset-y, 4px) var(--shadow-blur, 8px) 0px
|
|
504
|
-
rgba(0,0,0,var(--shadow-opacity, 0.1));
|
|
188
|
+
export function AppRoot({ children }: { children: React.ReactNode }) {
|
|
189
|
+
return (
|
|
190
|
+
<ThemeProvider customPresets={customPresets} defaultPreset="my-brand">
|
|
191
|
+
{children}
|
|
192
|
+
</ThemeProvider>
|
|
193
|
+
);
|
|
505
194
|
}
|
|
506
|
-
|
|
507
|
-
/* Dynamic spacing */
|
|
508
|
-
.tracking-theme { letter-spacing: var(--letter-spacing); }
|
|
509
|
-
.gap-theme { gap: var(--spacing, 0.25rem); }
|
|
510
195
|
```
|
|
511
196
|
|
|
512
|
-
|
|
197
|
+
Validation behavior:
|
|
513
198
|
|
|
514
|
-
|
|
199
|
+
- Custom presets are validated in `ThemeProvider`.
|
|
200
|
+
- Invalid custom presets are skipped; warnings are allowed.
|
|
201
|
+
- You can validate manually via `validateCustomPresets()` / `logValidationResult()`.
|
|
515
202
|
|
|
516
|
-
|
|
203
|
+
## Persistence keys
|
|
517
204
|
|
|
518
|
-
|
|
519
|
-
<head>
|
|
520
|
-
<ThemeScript presetStorageKey="your-key" />
|
|
521
|
-
</head>
|
|
522
|
-
```
|
|
205
|
+
By default:
|
|
523
206
|
|
|
524
|
-
|
|
207
|
+
- Mode is stored in `localStorage['theme-engine-theme']`.
|
|
208
|
+
- Preset is stored in `localStorage['theme-preset']`.
|
|
525
209
|
|
|
526
|
-
|
|
210
|
+
If you run multiple apps on the same domain, override the keys:
|
|
527
211
|
|
|
528
212
|
```tsx
|
|
529
|
-
<ThemeProvider
|
|
213
|
+
<ThemeProvider modeStorageKey="my-app:mode" presetStorageKey="my-app:preset">
|
|
530
214
|
{children}
|
|
531
215
|
</ThemeProvider>
|
|
532
216
|
```
|
|
533
217
|
|
|
534
|
-
|
|
218
|
+
## Tailwind tokens you get
|
|
535
219
|
|
|
536
|
-
|
|
220
|
+
After importing `@fakhrirafiki/theme-engine/styles`, you can use semantic tokens like:
|
|
537
221
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
222
|
+
- `bg-background`, `text-foreground`, `border-border`
|
|
223
|
+
- `bg-primary`, `text-primary-foreground`
|
|
224
|
+
- `bg-accent-success`, `text-accent-danger`, etc (if your presets include the `accent-*` variables)
|
|
541
225
|
|
|
542
|
-
##
|
|
226
|
+
## Troubleshooting
|
|
543
227
|
|
|
544
|
-
|
|
228
|
+
### `useTheme must be used within a ThemeProvider`
|
|
545
229
|
|
|
546
|
-
|
|
547
|
-
- **TypeScript declarations** - Full type safety
|
|
548
|
-
- **Complete CSS system** - All styles included
|
|
549
|
-
- **Source maps** - Enhanced debugging
|
|
550
|
-
- **Tree-shaking support** - Optimal bundle size
|
|
551
|
-
|
|
552
|
-
```
|
|
553
|
-
theme-engine/
|
|
554
|
-
βββ dist/
|
|
555
|
-
β βββ index.js # CommonJS build
|
|
556
|
-
β βββ index.mjs # ESM build
|
|
557
|
-
β βββ index.d.ts # TypeScript declarations
|
|
558
|
-
β βββ styles/ # CSS files
|
|
559
|
-
β βββ index.css # Complete system
|
|
560
|
-
β βββ base.css # CSS variables
|
|
561
|
-
β βββ animations.css
|
|
562
|
-
β βββ components.css
|
|
563
|
-
β βββ utilities.css
|
|
564
|
-
β βββ tailwind.css
|
|
565
|
-
βββ README.md
|
|
566
|
-
```
|
|
230
|
+
Wrap your component tree with `ThemeProvider` (and ensure the component is a client component).
|
|
567
231
|
|
|
568
|
-
|
|
232
|
+
### Preset doesnβt apply on refresh
|
|
569
233
|
|
|
570
|
-
|
|
234
|
+
If you render `ThemeScript` manually (using `disableScript`), make sure both use the same `presetStorageKey`.
|
|
571
235
|
|
|
572
|
-
|
|
573
|
-
- **Color Presets** - Managed by `ThemeProvider` using CSS variables with `!important`
|
|
574
|
-
- **Coordination** - MutationObserver detects mode changes and reapplies presets
|
|
575
|
-
- **Persistence** - Separate localStorage keys prevent conflicts
|
|
576
|
-
- **Zero-Config** - Built-in TweakCN presets work automatically
|
|
236
|
+
### `ThemePresetButtons` breaks
|
|
577
237
|
|
|
578
|
-
|
|
238
|
+
Ensure you imported `@fakhrirafiki/theme-engine/styles` (or at least `animations.css` + `components.css`).
|
|
579
239
|
|
|
580
|
-
##
|
|
240
|
+
## License
|
|
581
241
|
|
|
582
|
-
MIT
|
|
242
|
+
MIT
|