@fakhrirafiki/theme-engine 0.4.19 β 0.4.21
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 +110 -122
- package/dist/index.d.mts +231 -14
- package/dist/index.d.ts +231 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# π¨
|
|
1
|
+
# π¨ useThemeEngine for **Next.js (App Router)**
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Dark mode + theme presets (semantic tokens via CSS variables).
|
|
4
4
|
|
|
5
5
|
> β
Opinionated defaults, minimal setup, and TypeScript autocomplete that βjust worksβ.
|
|
6
6
|
|
|
@@ -8,15 +8,15 @@ Theme system for **Next.js (App Router)**: mode (`light | dark | system`) + them
|
|
|
8
8
|

|
|
9
9
|

|
|
10
10
|
|
|
11
|
-
Live demo: https://theme-engine-example.vercel.app/
|
|
12
|
-
Example repo: https://github.com/fakhrirafiki/theme-engine-example
|
|
11
|
+
- Live demo: https://theme-engine-example.vercel.app/
|
|
12
|
+
- Example repo: https://github.com/fakhrirafiki/theme-engine-example
|
|
13
13
|
|
|
14
14
|
## β¨ Why use this?
|
|
15
15
|
|
|
16
|
+
- π§ **DX-first**: `useThemeEngine()` for everything
|
|
16
17
|
- β‘ **Fast setup**: 1 CSS import + 1 provider
|
|
17
18
|
- π **Mode support**: `light | dark | system` (with View Transition ripple when supported)
|
|
18
19
|
- π¨ **Theme presets**: built-in presets + your own presets
|
|
19
|
-
- π§ **DX-first**: `useThemeEngine()` for everything
|
|
20
20
|
- π§© **Tailwind v4 friendly**: `@theme inline` tokens included (works with shadcn-style semantic tokens)
|
|
21
21
|
|
|
22
22
|
## π Table of contents
|
|
@@ -50,14 +50,14 @@ pnpm add @fakhrirafiki/theme-engine
|
|
|
50
50
|
In `src/app/globals.css`:
|
|
51
51
|
|
|
52
52
|
```css
|
|
53
|
-
@import
|
|
53
|
+
@import "@fakhrirafiki/theme-engine/styles";
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
β
Tailwind v4 (recommended order):
|
|
57
57
|
|
|
58
58
|
```css
|
|
59
|
-
@import
|
|
60
|
-
@import
|
|
59
|
+
@import "tailwindcss";
|
|
60
|
+
@import "@fakhrirafiki/theme-engine/styles";
|
|
61
61
|
|
|
62
62
|
@custom-variant dark (&:is(.dark *));
|
|
63
63
|
```
|
|
@@ -65,10 +65,10 @@ In `src/app/globals.css`:
|
|
|
65
65
|
βΉοΈ Not using Tailwind v4?
|
|
66
66
|
|
|
67
67
|
```css
|
|
68
|
-
@import
|
|
69
|
-
@import
|
|
70
|
-
@import
|
|
71
|
-
@import
|
|
68
|
+
@import "@fakhrirafiki/theme-engine/styles/base.css";
|
|
69
|
+
@import "@fakhrirafiki/theme-engine/styles/animations.css";
|
|
70
|
+
@import "@fakhrirafiki/theme-engine/styles/components.css";
|
|
71
|
+
@import "@fakhrirafiki/theme-engine/styles/utilities.css";
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
### 2) Wrap your app with `ThemeProvider`
|
|
@@ -76,9 +76,9 @@ In `src/app/globals.css`:
|
|
|
76
76
|
In `src/app/layout.tsx`:
|
|
77
77
|
|
|
78
78
|
```tsx
|
|
79
|
-
import type { ReactNode } from
|
|
80
|
-
import { ThemeProvider } from
|
|
81
|
-
import
|
|
79
|
+
import type { ReactNode } from "react";
|
|
80
|
+
import { ThemeProvider } from "@fakhrirafiki/theme-engine";
|
|
81
|
+
import "./globals.css";
|
|
82
82
|
|
|
83
83
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
84
84
|
return (
|
|
@@ -100,18 +100,18 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
|
|
100
100
|
Toggle mode:
|
|
101
101
|
|
|
102
102
|
```tsx
|
|
103
|
-
|
|
103
|
+
"use client";
|
|
104
104
|
|
|
105
|
-
import { useThemeEngine } from
|
|
105
|
+
import { useThemeEngine } from "@fakhrirafiki/theme-engine";
|
|
106
106
|
|
|
107
107
|
export function ModeButtons() {
|
|
108
108
|
const { mode, setDarkMode, toggleDarkMode } = useThemeEngine();
|
|
109
109
|
|
|
110
110
|
return (
|
|
111
111
|
<div>
|
|
112
|
-
<button onClick={() => setDarkMode(
|
|
113
|
-
<button onClick={() => setDarkMode(
|
|
114
|
-
<button onClick={() => setDarkMode(
|
|
112
|
+
<button onClick={() => setDarkMode("system")}>System</button>
|
|
113
|
+
<button onClick={() => setDarkMode("light")}>Light</button>
|
|
114
|
+
<button onClick={() => setDarkMode("dark")}>Dark</button>
|
|
115
115
|
<button onClick={() => toggleDarkMode()}>Toggle</button>
|
|
116
116
|
<div>Current: {mode}</div>
|
|
117
117
|
</div>
|
|
@@ -122,18 +122,18 @@ export function ModeButtons() {
|
|
|
122
122
|
Pick a theme preset by ID:
|
|
123
123
|
|
|
124
124
|
```tsx
|
|
125
|
-
|
|
125
|
+
"use client";
|
|
126
126
|
|
|
127
|
-
import { useThemeEngine } from
|
|
127
|
+
import { useThemeEngine } from "@fakhrirafiki/theme-engine";
|
|
128
128
|
|
|
129
129
|
export function PresetButtons() {
|
|
130
130
|
const { applyThemeById, clearTheme, currentTheme } = useThemeEngine();
|
|
131
131
|
|
|
132
132
|
return (
|
|
133
133
|
<div>
|
|
134
|
-
<button onClick={() => applyThemeById(
|
|
134
|
+
<button onClick={() => applyThemeById("modern-minimal")}>Modern Minimal</button>
|
|
135
135
|
<button onClick={() => clearTheme()}>Reset</button>
|
|
136
|
-
<div>Active: {currentTheme?.presetName ??
|
|
136
|
+
<div>Active: {currentTheme?.presetName ?? "Default"}</div>
|
|
137
137
|
</div>
|
|
138
138
|
);
|
|
139
139
|
}
|
|
@@ -142,18 +142,18 @@ export function PresetButtons() {
|
|
|
142
142
|
π‘ Want typed autocomplete (built-in IDs + your custom IDs)? Use a generic:
|
|
143
143
|
|
|
144
144
|
```tsx
|
|
145
|
-
|
|
145
|
+
"use client";
|
|
146
146
|
|
|
147
|
-
import { ThemePresets, useThemeEngine } from
|
|
148
|
-
import { customPresets } from
|
|
147
|
+
import { ThemePresets, useThemeEngine } from "@fakhrirafiki/theme-engine";
|
|
148
|
+
import { customPresets } from "./custom-theme-presets";
|
|
149
149
|
|
|
150
150
|
export function TypedPresetButtons() {
|
|
151
151
|
const { applyThemeById } = useThemeEngine<ThemePresets<typeof customPresets>>();
|
|
152
152
|
|
|
153
153
|
return (
|
|
154
154
|
<div>
|
|
155
|
-
<button onClick={() => applyThemeById(
|
|
156
|
-
<button onClick={() => applyThemeById(
|
|
155
|
+
<button onClick={() => applyThemeById("my-brand")}>My Brand</button>
|
|
156
|
+
<button onClick={() => applyThemeById("modern-minimal")}>Modern Minimal</button>
|
|
157
157
|
</div>
|
|
158
158
|
);
|
|
159
159
|
}
|
|
@@ -191,13 +191,7 @@ If you run multiple apps on the same domain, override the keys:
|
|
|
191
191
|
|
|
192
192
|
---
|
|
193
193
|
|
|
194
|
-
## π§©
|
|
195
|
-
|
|
196
|
-
Create presets in TweakCN-compatible format and pass them into `ThemeProvider`.
|
|
197
|
-
|
|
198
|
-
β
Tip: use `satisfies` to preserve literal keys for TS autocomplete:
|
|
199
|
-
|
|
200
|
-
### ποΈ Get a brand theme from TweakCN (recommended)
|
|
194
|
+
## π§© Get your brand theme from TweakCN (recommended)
|
|
201
195
|
|
|
202
196
|
The fastest way to create a great-looking preset is to use the TweakCN editor:
|
|
203
197
|
|
|
@@ -206,31 +200,31 @@ The fastest way to create a great-looking preset is to use the TweakCN editor:
|
|
|
206
200
|
Pick a theme, tweak the colors, then copy the preset output and paste it into your `customPresets` object (it matches the `TweakCNThemePreset` shape).
|
|
207
201
|
|
|
208
202
|
```ts
|
|
209
|
-
import { type TweakCNThemePreset } from
|
|
203
|
+
import { type TweakCNThemePreset } from "@fakhrirafiki/theme-engine";
|
|
210
204
|
|
|
211
205
|
export const customPresets = {
|
|
212
|
-
|
|
213
|
-
label:
|
|
206
|
+
"my-brand": {
|
|
207
|
+
label: "My Brand",
|
|
214
208
|
styles: {
|
|
215
209
|
light: {
|
|
216
|
-
background:
|
|
217
|
-
foreground:
|
|
218
|
-
primary:
|
|
219
|
-
|
|
220
|
-
secondary:
|
|
221
|
-
|
|
222
|
-
card:
|
|
223
|
-
|
|
210
|
+
background: "#ffffff",
|
|
211
|
+
foreground: "#111827",
|
|
212
|
+
primary: "#2563eb",
|
|
213
|
+
"primary-foreground": "#ffffff",
|
|
214
|
+
secondary: "#e5e7eb",
|
|
215
|
+
"secondary-foreground": "#111827",
|
|
216
|
+
card: "#ffffff",
|
|
217
|
+
"card-foreground": "#111827",
|
|
224
218
|
},
|
|
225
219
|
dark: {
|
|
226
|
-
background:
|
|
227
|
-
foreground:
|
|
228
|
-
primary:
|
|
229
|
-
|
|
230
|
-
secondary:
|
|
231
|
-
|
|
232
|
-
card:
|
|
233
|
-
|
|
220
|
+
background: "#0b1020",
|
|
221
|
+
foreground: "#f9fafb",
|
|
222
|
+
primary: "#60a5fa",
|
|
223
|
+
"primary-foreground": "#0b1020",
|
|
224
|
+
secondary: "#1f2937",
|
|
225
|
+
"secondary-foreground": "#f9fafb",
|
|
226
|
+
card: "#111827",
|
|
227
|
+
"card-foreground": "#f9fafb",
|
|
234
228
|
},
|
|
235
229
|
},
|
|
236
230
|
},
|
|
@@ -240,9 +234,9 @@ export const customPresets = {
|
|
|
240
234
|
Then in your providers/layout:
|
|
241
235
|
|
|
242
236
|
```tsx
|
|
243
|
-
import type { ReactNode } from
|
|
244
|
-
import { ThemeProvider } from
|
|
245
|
-
import { customPresets } from
|
|
237
|
+
import type { ReactNode } from "react";
|
|
238
|
+
import { ThemeProvider } from "@fakhrirafiki/theme-engine";
|
|
239
|
+
import { customPresets } from "./custom-theme-presets";
|
|
246
240
|
|
|
247
241
|
export function AppProviders({ children }: { children: ReactNode }) {
|
|
248
242
|
return (
|
|
@@ -266,10 +260,10 @@ Notes:
|
|
|
266
260
|
The package ships with a built-in preset collection:
|
|
267
261
|
|
|
268
262
|
```ts
|
|
269
|
-
import { getPresetIds, getPresetById } from
|
|
263
|
+
import { getPresetIds, getPresetById } from "@fakhrirafiki/theme-engine";
|
|
270
264
|
|
|
271
265
|
const ids = getPresetIds();
|
|
272
|
-
const modernMinimal = getPresetById(
|
|
266
|
+
const modernMinimal = getPresetById("modern-minimal");
|
|
273
267
|
```
|
|
274
268
|
|
|
275
269
|
---
|
|
@@ -278,24 +272,24 @@ const modernMinimal = getPresetById('modern-minimal');
|
|
|
278
272
|
|
|
279
273
|
After importing `@fakhrirafiki/theme-engine/styles`, you can use semantic tokens like:
|
|
280
274
|
|
|
281
|
-
| Category
|
|
282
|
-
|
|
|
283
|
-
| Surfaces
|
|
284
|
-
| Cards
|
|
285
|
-
| Popovers
|
|
286
|
-
| Brand / actions | `bg-primary`, `text-primary-foreground`
|
|
287
|
-
| Secondary
|
|
288
|
-
| Muted
|
|
289
|
-
| Accent
|
|
290
|
-
| Destructive
|
|
291
|
-
| Borders / focus | `border-border`, `border-input`, `ring-ring`
|
|
292
|
-
| Charts
|
|
293
|
-
| Sidebar
|
|
294
|
-
| Status accents
|
|
295
|
-
| Radius scale
|
|
296
|
-
| Tracking scale
|
|
297
|
-
| Fonts
|
|
298
|
-
| Shadows
|
|
275
|
+
| Category | Tailwind class examples | Backed by preset CSS variables | Notes |
|
|
276
|
+
| --------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------- |
|
|
277
|
+
| Surfaces | `bg-background`, `text-foreground` | `--background`, `--foreground` | Base app background + text |
|
|
278
|
+
| Cards | `bg-card`, `text-card-foreground` | `--card`, `--card-foreground` | Cards / panels |
|
|
279
|
+
| Popovers | `bg-popover`, `text-popover-foreground` | `--popover`, `--popover-foreground` | Popovers / dropdowns |
|
|
280
|
+
| Brand / actions | `bg-primary`, `text-primary-foreground` | `--primary`, `--primary-foreground` | Primary buttons / highlights |
|
|
281
|
+
| Secondary | `bg-secondary`, `text-secondary-foreground` | `--secondary`, `--secondary-foreground` | Secondary UI surfaces |
|
|
282
|
+
| Muted | `bg-muted`, `text-muted-foreground` | `--muted`, `--muted-foreground` | Subtle backgrounds / helper text |
|
|
283
|
+
| Accent | `bg-accent`, `text-accent-foreground` | `--accent`, `--accent-foreground` | Emphasis (not status colors) |
|
|
284
|
+
| Destructive | `bg-destructive`, `text-destructive-foreground` | `--destructive`, `--destructive-foreground` | Danger actions |
|
|
285
|
+
| Borders / focus | `border-border`, `border-input`, `ring-ring` | `--border`, `--input`, `--ring` | Used by `outline-ring/50` too |
|
|
286
|
+
| Charts | `bg-chart-1`, `text-chart-2` | `--chart-1` ... `--chart-5` | Data viz palettes |
|
|
287
|
+
| Sidebar | `bg-sidebar`, `text-sidebar-foreground`, `bg-sidebar-primary`, `border-sidebar-border` | `--sidebar-*` | Handy for dashboard layouts |
|
|
288
|
+
| Status accents | `bg-accent-success`, `text-accent-danger-foreground` | `--accent-<name>`, `--accent-<name>-foreground` | Optional: only if preset defines `accent-*` |
|
|
289
|
+
| Radius scale | `rounded-sm`, `rounded-md`, `rounded-lg`, `rounded-xl` | `--radius-sm`, `--radius-md`, `--radius-lg`, `--radius-xl` | Derived from `--radius` |
|
|
290
|
+
| Tracking scale | `tracking-tighter`, `tracking-wide` | `--tracking-*` | Derived from `--letter-spacing` |
|
|
291
|
+
| Fonts | `font-sans`, `font-serif`, `font-mono` | `--font-sans`, `--font-serif`, `--font-mono` | Defaults in `base.css` |
|
|
292
|
+
| Shadows | `shadow-sm`, `shadow-md`, `shadow-xl` | `--shadow-*` | Derived from `--shadow-*` knobs |
|
|
299
293
|
|
|
300
294
|
---
|
|
301
295
|
|
|
@@ -306,9 +300,9 @@ After importing `@fakhrirafiki/theme-engine/styles`, you can use semantic tokens
|
|
|
306
300
|
Ready-made mode toggle button (with View Transition ripple when supported).
|
|
307
301
|
|
|
308
302
|
```tsx
|
|
309
|
-
|
|
303
|
+
"use client";
|
|
310
304
|
|
|
311
|
-
import { ThemeToggle } from
|
|
305
|
+
import { ThemeToggle } from "@fakhrirafiki/theme-engine";
|
|
312
306
|
|
|
313
307
|
export function HeaderThemeToggle() {
|
|
314
308
|
return <ThemeToggle size="md" variant="ghost" />;
|
|
@@ -320,9 +314,9 @@ export function HeaderThemeToggle() {
|
|
|
320
314
|
Animated preset picker (shows custom presets first, then built-ins):
|
|
321
315
|
|
|
322
316
|
```tsx
|
|
323
|
-
|
|
317
|
+
"use client";
|
|
324
318
|
|
|
325
|
-
import { ThemePresetButtons } from
|
|
319
|
+
import { ThemePresetButtons } from "@fakhrirafiki/theme-engine";
|
|
326
320
|
|
|
327
321
|
export function PresetPicker() {
|
|
328
322
|
return <ThemePresetButtons />;
|
|
@@ -336,21 +330,16 @@ Want a simple, scrollable preset list (e.g. for a settings modal)? Copy-paste th
|
|
|
336
330
|
> Note: this snippet uses Tailwind utility classes. If you donβt use Tailwind, replace the classes with your own styles/UI components.
|
|
337
331
|
|
|
338
332
|
```tsx
|
|
339
|
-
|
|
333
|
+
"use client";
|
|
340
334
|
|
|
341
|
-
import { formatColor, useThemeEngine } from
|
|
335
|
+
import { formatColor, useThemeEngine } from "@fakhrirafiki/theme-engine";
|
|
342
336
|
|
|
343
337
|
type ThemePresetSelectProps = {
|
|
344
338
|
allowedPresetIds?: string[];
|
|
345
339
|
};
|
|
346
340
|
|
|
347
341
|
export function ThemePresetSelect({
|
|
348
|
-
allowedPresetIds = [
|
|
349
|
-
'modern-minimal',
|
|
350
|
-
'violet-bloom',
|
|
351
|
-
'vercel',
|
|
352
|
-
'mono',
|
|
353
|
-
],
|
|
342
|
+
allowedPresetIds = ["modern-minimal", "violet-bloom", "vercel", "mono"],
|
|
354
343
|
}: ThemePresetSelectProps) {
|
|
355
344
|
const { currentTheme, applyThemeById, availablePresets, resolvedMode } = useThemeEngine();
|
|
356
345
|
|
|
@@ -366,7 +355,7 @@ export function ThemePresetSelect({
|
|
|
366
355
|
const preset = availablePresets[presetId];
|
|
367
356
|
if (!preset) return [];
|
|
368
357
|
|
|
369
|
-
const scheme = resolvedMode ===
|
|
358
|
+
const scheme = resolvedMode === "dark" ? preset.styles.dark : preset.styles.light;
|
|
370
359
|
const primary = (scheme as any).primary as string | undefined;
|
|
371
360
|
const secondary = (scheme as any).secondary as string | undefined;
|
|
372
361
|
const accent = (scheme as any).accent as string | undefined;
|
|
@@ -386,8 +375,8 @@ export function ThemePresetSelect({
|
|
|
386
375
|
type="button"
|
|
387
376
|
className={`w-full rounded-full border px-3 py-2 text-xs transition-colors ${
|
|
388
377
|
isActive
|
|
389
|
-
?
|
|
390
|
-
:
|
|
378
|
+
? "border-primary/70 bg-primary/10 text-foreground"
|
|
379
|
+
: "border-border bg-muted/40 text-muted-foreground hover:border-muted-foreground/40 hover:bg-muted/60"
|
|
391
380
|
}`}
|
|
392
381
|
onClick={() => applyThemeById(preset.id)}
|
|
393
382
|
>
|
|
@@ -399,7 +388,7 @@ export function ThemePresetSelect({
|
|
|
399
388
|
<span
|
|
400
389
|
key={index}
|
|
401
390
|
className="inline-block h-2.5 w-2.5 rounded-full border border-foreground/10 shadow-sm"
|
|
402
|
-
style={{ backgroundColor: formatColor(color,
|
|
391
|
+
style={{ backgroundColor: formatColor(color, "hex") }}
|
|
403
392
|
/>
|
|
404
393
|
))}
|
|
405
394
|
</span>
|
|
@@ -440,15 +429,14 @@ export function ThemePresetSelect({
|
|
|
440
429
|
/>
|
|
441
430
|
```
|
|
442
431
|
|
|
443
|
-
| Prop
|
|
444
|
-
|
|
|
445
|
-
| `children`
|
|
446
|
-
| `defaultMode`
|
|
447
|
-
| `defaultPreset`
|
|
448
|
-
| `modeStorageKey`
|
|
449
|
-
| `presetStorageKey` | `string`
|
|
450
|
-
| `customPresets`
|
|
451
|
-
| `Pre-hydration script` | n/a | always on | `ThemeProvider` always injects a pre-hydration script for preset restoration |
|
|
432
|
+
| Prop | Type | Default | Description |
|
|
433
|
+
| ------------------ | ---------------------------------------- | ---------------------- | --------------------------------------------------- |
|
|
434
|
+
| `children` | `ReactNode` | required | React subtree |
|
|
435
|
+
| `defaultMode` | `Mode` | `'system'` | Used when no persisted value for dark mode |
|
|
436
|
+
| `defaultPreset` | `BuiltInPresetId \| keyof customPresets` | `undefined` | Default preset (see SSR note) |
|
|
437
|
+
| `modeStorageKey` | `string` | `'theme-engine-theme'` | `localStorage` key for mode |
|
|
438
|
+
| `presetStorageKey` | `string` | `'theme-preset'` | `localStorage` key for preset |
|
|
439
|
+
| `customPresets` | `Record<string, TweakCNThemePreset>` | `undefined` | Add your own presets (can override built-ins by ID) |
|
|
452
440
|
|
|
453
441
|
### `useThemeEngine()`
|
|
454
442
|
|
|
@@ -461,32 +449,32 @@ useThemeEngine<TCustomPresets = undefined>()
|
|
|
461
449
|
To get typed custom preset IDs:
|
|
462
450
|
|
|
463
451
|
```ts
|
|
464
|
-
useThemeEngine<ThemePresets<typeof customPresets>>()
|
|
452
|
+
useThemeEngine<ThemePresets<typeof customPresets>>();
|
|
465
453
|
```
|
|
466
454
|
|
|
467
455
|
Return fields:
|
|
468
456
|
|
|
469
|
-
| Field
|
|
470
|
-
|
|
|
471
|
-
| `darkMode`
|
|
472
|
-
| `mode`
|
|
473
|
-
| `resolvedMode`
|
|
474
|
-
| `setDarkMode`
|
|
475
|
-
| `toggleDarkMode`
|
|
476
|
-
| `applyThemeById`
|
|
477
|
-
| `clearTheme`
|
|
478
|
-
| `currentTheme`
|
|
479
|
-
| `isUsingDefaultPreset` | `boolean`
|
|
480
|
-
| `availablePresets`
|
|
481
|
-
| `builtInPresets`
|
|
482
|
-
| `customPresets`
|
|
457
|
+
| Field | Type | Description |
|
|
458
|
+
| ---------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
459
|
+
| `darkMode` | `boolean` | `resolvedMode === 'dark'` |
|
|
460
|
+
| `mode` | `'light' \| 'dark' \| 'system'` | Current user preference |
|
|
461
|
+
| `resolvedMode` | `'light' \| 'dark'` | Resolved mode (never `system`) |
|
|
462
|
+
| `setDarkMode` | `(mode: Mode) => void` | Set `light \| dark \| system` |
|
|
463
|
+
| `toggleDarkMode` | `(coords?: { x: number; y: number }) => void` | Toggles light/dark (and exits `system`) |
|
|
464
|
+
| `applyThemeById` | `(id: ThemeId) => void` | Apply a preset by ID (alias: `applyPresetById`) |
|
|
465
|
+
| `clearTheme` | `() => void` | Clear preset and fall back to `defaultPreset` if provided (alias: `clearPreset`) |
|
|
466
|
+
| `currentTheme` | `{ presetId; presetName; colors; appliedAt } \| null` | Current preset (alias: `currentPreset`) |
|
|
467
|
+
| `isUsingDefaultPreset` | `boolean` | Whether current preset equals `defaultPreset` |
|
|
468
|
+
| `availablePresets` | `Record<string, TweakCNThemePreset>` | Built-in + custom |
|
|
469
|
+
| `builtInPresets` | `Record<string, TweakCNThemePreset>` | Built-in only |
|
|
470
|
+
| `customPresets` | `Record<string, TweakCNThemePreset>` | Custom only |
|
|
483
471
|
|
|
484
472
|
### Utilities
|
|
485
473
|
|
|
486
|
-
| Export
|
|
487
|
-
|
|
|
488
|
-
| `formatColor(color, format)`
|
|
489
|
-
| `withAlpha(hslTriplet, alpha)` | Adds alpha to an HSL triplet
|
|
474
|
+
| Export | Description |
|
|
475
|
+
| ------------------------------ | ---------------------------------------------- |
|
|
476
|
+
| `formatColor(color, format)` | Converts a color string into `hsl`/`rgb`/`hex` |
|
|
477
|
+
| `withAlpha(hslTriplet, alpha)` | Adds alpha to an HSL triplet |
|
|
490
478
|
|
|
491
479
|
---
|
|
492
480
|
|
package/dist/index.d.mts
CHANGED
|
@@ -234,15 +234,37 @@ interface ThemePresetButtonsProps {
|
|
|
234
234
|
showSectionHeaders?: boolean;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Appearance mode.
|
|
239
|
+
*
|
|
240
|
+
* - `"system"` follows `prefers-color-scheme`
|
|
241
|
+
* - `resolvedMode` (from hooks/provider) is always `"light"` or `"dark"`
|
|
242
|
+
*
|
|
243
|
+
* @public
|
|
244
|
+
*/
|
|
237
245
|
type Mode = "light" | "dark" | "system";
|
|
246
|
+
/**
|
|
247
|
+
* Screen coordinates used for the optional view-transition ripple when toggling modes.
|
|
248
|
+
*
|
|
249
|
+
* @public
|
|
250
|
+
*/
|
|
238
251
|
interface Coordinates {
|
|
239
252
|
x: number;
|
|
240
253
|
y: number;
|
|
241
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Props for `ThemeToggle`.
|
|
257
|
+
*
|
|
258
|
+
* @public
|
|
259
|
+
*/
|
|
242
260
|
interface ThemeToggleProps {
|
|
261
|
+
/** Additional class name(s) applied to the button element */
|
|
243
262
|
className?: string;
|
|
263
|
+
/** Styling hook exposed via `data-size` */
|
|
244
264
|
size?: "sm" | "md" | "lg";
|
|
265
|
+
/** Styling hook exposed via `data-variant` */
|
|
245
266
|
variant?: "default" | "outline" | "ghost";
|
|
267
|
+
/** Optional custom icon/content (overrides the default icon) */
|
|
246
268
|
children?: ReactNode;
|
|
247
269
|
}
|
|
248
270
|
|
|
@@ -366,13 +388,33 @@ type PresetId<TCustomPresets> = BuiltInPresetId | CustomPresetId$2<TCustomPreset
|
|
|
366
388
|
interface UnifiedThemeProviderProps<TCustomPresets extends CustomPresetsRecord$2 | undefined = undefined> {
|
|
367
389
|
/** React children to wrap with theming context */
|
|
368
390
|
children: react__default.ReactNode;
|
|
369
|
-
/**
|
|
391
|
+
/**
|
|
392
|
+
* Default appearance mode when no stored preference exists.
|
|
393
|
+
*
|
|
394
|
+
* Notes for SSR/App Router:
|
|
395
|
+
* - The initial render must be deterministic between server and client to avoid hydration mismatches.
|
|
396
|
+
* - Persisted mode is restored after hydration (and also pre-hydration via the injected `ThemeScript`).
|
|
397
|
+
*/
|
|
370
398
|
defaultMode?: Mode;
|
|
371
|
-
/**
|
|
399
|
+
/**
|
|
400
|
+
* Default preset ID to use when no stored preset exists or when resetting.
|
|
401
|
+
*
|
|
402
|
+
* If provided, the preset will be applied when:
|
|
403
|
+
* - there is no persisted preset in `localStorage`, or
|
|
404
|
+
* - `clearPreset()` is called.
|
|
405
|
+
*/
|
|
372
406
|
defaultPreset?: PresetId<TCustomPresets>;
|
|
373
|
-
/**
|
|
407
|
+
/**
|
|
408
|
+
* `localStorage` key for appearance mode persistence.
|
|
409
|
+
*
|
|
410
|
+
* Stored value is one of: `"light" | "dark" | "system"`.
|
|
411
|
+
*/
|
|
374
412
|
modeStorageKey?: string;
|
|
375
|
-
/**
|
|
413
|
+
/**
|
|
414
|
+
* `localStorage` key for color preset persistence.
|
|
415
|
+
*
|
|
416
|
+
* Stored value is a JSON blob written by this provider and restored on subsequent loads.
|
|
417
|
+
*/
|
|
376
418
|
presetStorageKey?: string;
|
|
377
419
|
/** Custom presets to add to the available collection */
|
|
378
420
|
customPresets?: TCustomPresets;
|
|
@@ -391,6 +433,14 @@ interface UnifiedThemeProviderProps<TCustomPresets extends CustomPresetsRecord$2
|
|
|
391
433
|
* - π¨ **CSS `!important`** ensures presets override mode defaults
|
|
392
434
|
* - π **MutationObserver** automatically reapplies presets on mode changes
|
|
393
435
|
*
|
|
436
|
+
* ## SSR / hydration behavior
|
|
437
|
+
* This provider is designed for Next.js App Router where Client Components are still SSR-ed.
|
|
438
|
+
* To avoid hydration mismatches:
|
|
439
|
+
* - The initial render does not read `localStorage`.
|
|
440
|
+
* - A pre-hydration `ThemeScript` is injected to apply the correct `html` mode class (`light`/`dark`)
|
|
441
|
+
* and restore preset CSS variables as early as possible.
|
|
442
|
+
* - Persisted mode and preset are then reconciled after hydration.
|
|
443
|
+
*
|
|
394
444
|
* @example
|
|
395
445
|
* ```tsx
|
|
396
446
|
* <ThemeProvider
|
|
@@ -411,6 +461,8 @@ declare function ThemeProvider<const TCustomPresets extends CustomPresetsRecord$
|
|
|
411
461
|
* Provides access to both appearance mode controls and preset management
|
|
412
462
|
* in a single, coordinated interface.
|
|
413
463
|
*
|
|
464
|
+
* Prefer `useThemeEngine()` for a DX-first API with aliases and typed preset IDs.
|
|
465
|
+
*
|
|
414
466
|
* @example
|
|
415
467
|
* ```tsx
|
|
416
468
|
* // Mode controls
|
|
@@ -476,17 +528,79 @@ interface ThemeScriptProps {
|
|
|
476
528
|
defaultPreset?: string;
|
|
477
529
|
}
|
|
478
530
|
/**
|
|
479
|
-
* Pre-hydration theme script
|
|
480
|
-
*
|
|
481
|
-
*
|
|
531
|
+
* Pre-hydration theme script injected by `ThemeProvider`.
|
|
532
|
+
*
|
|
533
|
+
* This runs before React hydration and is intentionally implemented as an inline script so it can:
|
|
534
|
+
* - restore the `html` mode class (`light`/`dark`) and `color-scheme` as early as possible
|
|
535
|
+
* - restore preset CSS variables early to prevent FOUC (unstyled/incorrect tokens on first paint)
|
|
536
|
+
*
|
|
537
|
+
* It reads:
|
|
538
|
+
* - `localStorage[modeStorageKey]` (mode persistence)
|
|
539
|
+
* - `localStorage[presetStorageKey]` (preset persistence)
|
|
540
|
+
*
|
|
541
|
+
* It writes:
|
|
542
|
+
* - `document.documentElement.classList` (`light`/`dark`)
|
|
543
|
+
* - `document.documentElement.style.colorScheme`
|
|
544
|
+
* - `document.documentElement.dataset.themeEngineMode` and `dataset.themeEngineResolvedMode` (best-effort)
|
|
545
|
+
*
|
|
546
|
+
* You typically do not render this manually β `ThemeProvider` includes it automatically.
|
|
482
547
|
*/
|
|
483
548
|
declare function ThemeScript({ presetStorageKey, modeStorageKey, defaultMode, defaultPreset, }: ThemeScriptProps): react_jsx_runtime.JSX.Element;
|
|
484
549
|
|
|
550
|
+
/**
|
|
551
|
+
* Button that toggles the current appearance mode (light β dark) using Theme Engine.
|
|
552
|
+
*
|
|
553
|
+
* - Reads `mode` / `resolvedMode` from `ThemeProvider` via `useTheme()`
|
|
554
|
+
* - On click, calls `toggleMode({ x, y })` to enable the optional view-transition ripple
|
|
555
|
+
* - Renders an icon that reflects the current mode (`light`/`dark`/`system`) unless you pass `children`
|
|
556
|
+
*
|
|
557
|
+
* Data attributes:
|
|
558
|
+
* - `data-size`: `"sm" | "md" | "lg"` (for styling hooks)
|
|
559
|
+
* - `data-variant`: `"default" | "outline" | "ghost"`
|
|
560
|
+
* - `data-mode`: resolved mode (`"light" | "dark"`) for CSS hooks
|
|
561
|
+
* - `data-theme`: alias of `data-mode`
|
|
562
|
+
*
|
|
563
|
+
* @example
|
|
564
|
+
* ```tsx
|
|
565
|
+
* import { ThemeToggle } from "@fakhrirafiki/theme-engine";
|
|
566
|
+
*
|
|
567
|
+
* export function Header() {
|
|
568
|
+
* return <ThemeToggle className="ml-auto" />;
|
|
569
|
+
* }
|
|
570
|
+
* ```
|
|
571
|
+
*/
|
|
485
572
|
declare const ThemeToggle: react.ForwardRefExoticComponent<ThemeToggleProps & react.RefAttributes<HTMLButtonElement>>;
|
|
486
573
|
|
|
487
574
|
/**
|
|
488
575
|
* Main ThemePresetButtons component
|
|
489
576
|
*/
|
|
577
|
+
/**
|
|
578
|
+
* Preset picker UI for Theme Engine.
|
|
579
|
+
*
|
|
580
|
+
* Renders a horizontally scrolling set of preset buttons (optionally in multiple rows) and applies
|
|
581
|
+
* the selected preset via `ThemeProvider` context.
|
|
582
|
+
*
|
|
583
|
+
* Requirements:
|
|
584
|
+
* - Must be used under `ThemeProvider` (it reads `availablePresets`/`currentPreset` from context).
|
|
585
|
+
*
|
|
586
|
+
* Behavior:
|
|
587
|
+
* - Built-in + custom presets are merged from context and displayed (custom presets are shown first).
|
|
588
|
+
* - Selecting a preset calls `applyPreset()` from the provider, which also persists it to `localStorage`.
|
|
589
|
+
* - Supports infinite marquee animation; disable via `animation={{ enabled: false }}`.
|
|
590
|
+
*
|
|
591
|
+
* Customization:
|
|
592
|
+
* - Use `renderPreset` to fully control the button UI (selection handling is still managed internally).
|
|
593
|
+
* - Use `renderColorBox` to customize the color dots while keeping the default layout.
|
|
594
|
+
*
|
|
595
|
+
* @example
|
|
596
|
+
* ```tsx
|
|
597
|
+
* import { ThemePresetButtons } from "@fakhrirafiki/theme-engine";
|
|
598
|
+
*
|
|
599
|
+
* export function PresetsSection() {
|
|
600
|
+
* return <ThemePresetButtons className="mt-6" maxPresets={24} />;
|
|
601
|
+
* }
|
|
602
|
+
* ```
|
|
603
|
+
*/
|
|
490
604
|
declare const ThemePresetButtons: ({ animation: animationOverrides, layout: layoutOverrides, renderPreset, renderColorBox, className, categories, maxPresets, showBuiltIn, showCustom, }: ThemePresetButtonsProps) => react_jsx_runtime.JSX.Element;
|
|
491
605
|
|
|
492
606
|
type CustomPresetsRecord$1 = Record<string, TweakCNThemePreset>;
|
|
@@ -500,6 +614,28 @@ type LooseString$1 = string & {};
|
|
|
500
614
|
* - Custom IDs are inferred from the keys of the `customPresets` argument
|
|
501
615
|
*
|
|
502
616
|
* The resulting `setThemePresetById()` still accepts any string, but VS Code will suggest known IDs first.
|
|
617
|
+
*
|
|
618
|
+
* Notes:
|
|
619
|
+
* - `customPresets` is only used for TypeScript inference (no runtime effect).
|
|
620
|
+
* - Prefer `useThemeEngine()` if you want a higher-level DX API with aliases.
|
|
621
|
+
*
|
|
622
|
+
* @example
|
|
623
|
+
* ```tsx
|
|
624
|
+
* "use client";
|
|
625
|
+
*
|
|
626
|
+
* import { useTypedTheme } from "@fakhrirafiki/theme-engine";
|
|
627
|
+
* import { customPresets } from "./custom-presets";
|
|
628
|
+
*
|
|
629
|
+
* export function PresetPicker() {
|
|
630
|
+
* const { currentPreset, setThemePresetById } = useTypedTheme(customPresets);
|
|
631
|
+
*
|
|
632
|
+
* return (
|
|
633
|
+
* <button onClick={() => setThemePresetById("my-brand")}>
|
|
634
|
+
* Active: {currentPreset?.presetName ?? "default"}
|
|
635
|
+
* </button>
|
|
636
|
+
* );
|
|
637
|
+
* }
|
|
638
|
+
* ```
|
|
503
639
|
*/
|
|
504
640
|
declare function useTypedTheme<const TCustomPresets extends CustomPresetsRecord$1 | undefined = undefined>(customPresets?: TCustomPresets): {
|
|
505
641
|
setThemePresetById: (presetId: LooseString$1 | ThemePresetId$1<TCustomPresets>) => void;
|
|
@@ -528,11 +664,26 @@ type LooseString = string & {};
|
|
|
528
664
|
/**
|
|
529
665
|
* Type helper to "register" your presets for autocomplete.
|
|
530
666
|
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
667
|
+
* @example
|
|
668
|
+
* ```ts
|
|
669
|
+
* import { type ThemePresets, useThemeEngine } from "@fakhrirafiki/theme-engine";
|
|
670
|
+
* import { customPresets } from "./custom-presets";
|
|
671
|
+
*
|
|
672
|
+
* type PresetRegistry = ThemePresets<typeof customPresets>;
|
|
673
|
+
*
|
|
674
|
+
* const theme = useThemeEngine<PresetRegistry>();
|
|
675
|
+
* // theme.applyThemeById("my-custom-id") // β
autocomplete for keys in customPresets + built-ins
|
|
676
|
+
* ```
|
|
533
677
|
*/
|
|
534
678
|
type ThemePresets<T> = T extends CustomPresetsRecord ? T : never;
|
|
535
679
|
type ThemeEnginePresetId<TCustomPresets extends CustomPresetsRecord | undefined = undefined> = ThemePresetId<TCustomPresets>;
|
|
680
|
+
/**
|
|
681
|
+
* Accepts either:
|
|
682
|
+
* - a typed preset ID (built-in + inferred custom preset IDs), or
|
|
683
|
+
* - any string (runtime safety / forwards compatibility)
|
|
684
|
+
*
|
|
685
|
+
* This is useful when you receive preset IDs dynamically (e.g. from a URL param).
|
|
686
|
+
*/
|
|
536
687
|
type ThemeId<TCustomPresets extends CustomPresetsRecord | undefined = undefined> = ThemeEnginePresetId<TCustomPresets> | LooseString;
|
|
537
688
|
/**
|
|
538
689
|
* DX-first hook for Theme Engine.
|
|
@@ -543,6 +694,45 @@ type ThemeId<TCustomPresets extends CustomPresetsRecord | undefined = undefined>
|
|
|
543
694
|
*
|
|
544
695
|
* For typed preset ID autocomplete (built-in + your custom IDs):
|
|
545
696
|
* `useThemeEngine<ThemePresets<typeof customPresets>>()`
|
|
697
|
+
*
|
|
698
|
+
* Naming:
|
|
699
|
+
* - `applyThemeById` and `applyPresetById` are aliases
|
|
700
|
+
* - `clearTheme` and `clearPreset` are aliases
|
|
701
|
+
*
|
|
702
|
+
* @example
|
|
703
|
+
* ```tsx
|
|
704
|
+
* "use client";
|
|
705
|
+
*
|
|
706
|
+
* import { ThemeProvider, useThemeEngine, type ThemePresets } from "@fakhrirafiki/theme-engine";
|
|
707
|
+
* import { customPresets } from "./custom-presets";
|
|
708
|
+
*
|
|
709
|
+
* type Presets = ThemePresets<typeof customPresets>;
|
|
710
|
+
*
|
|
711
|
+
* function Controls() {
|
|
712
|
+
* const { mode, resolvedMode, setDarkMode, applyThemeById, clearTheme } = useThemeEngine<Presets>();
|
|
713
|
+
*
|
|
714
|
+
* return (
|
|
715
|
+
* <div>
|
|
716
|
+
* <button onClick={() => setDarkMode("system")}>System</button>
|
|
717
|
+
* <button onClick={() => setDarkMode("light")}>Light</button>
|
|
718
|
+
* <button onClick={() => setDarkMode("dark")}>Dark</button>
|
|
719
|
+
*
|
|
720
|
+
* <button onClick={() => applyThemeById("modern-minimal")}>Modern Minimal</button>
|
|
721
|
+
* <button onClick={() => clearTheme()}>Reset</button>
|
|
722
|
+
*
|
|
723
|
+
* <div>mode: {mode} Β· resolved: {resolvedMode}</div>
|
|
724
|
+
* </div>
|
|
725
|
+
* );
|
|
726
|
+
* }
|
|
727
|
+
*
|
|
728
|
+
* export default function Page() {
|
|
729
|
+
* return (
|
|
730
|
+
* <ThemeProvider customPresets={customPresets} defaultPreset="modern-minimal">
|
|
731
|
+
* <Controls />
|
|
732
|
+
* </ThemeProvider>
|
|
733
|
+
* );
|
|
734
|
+
* }
|
|
735
|
+
* ```
|
|
546
736
|
*/
|
|
547
737
|
declare function useThemeEngine<const TCustomPresets extends CustomPresetsRecord | undefined = undefined>(): {
|
|
548
738
|
darkMode: boolean;
|
|
@@ -585,7 +775,14 @@ type ColorFormat = 'hsl' | 'rgb' | 'hex';
|
|
|
585
775
|
*/
|
|
586
776
|
declare function formatColor(colorInput: string, outputFormat?: ColorFormat, includeFunctionWrapper?: boolean): string;
|
|
587
777
|
/**
|
|
588
|
-
* Create color with alpha transparency
|
|
778
|
+
* Create a color with alpha transparency.
|
|
779
|
+
*
|
|
780
|
+
* Notes:
|
|
781
|
+
* - This helper only supports HSL-like inputs that `parseHSL()` can parse
|
|
782
|
+
* (e.g. `"hsl(210 40% 98%)"` or `"210 40% 98%"`).
|
|
783
|
+
* - For hex/rgb inputs, convert first with `formatColor(color, "hsl")`.
|
|
784
|
+
*
|
|
785
|
+
* @public
|
|
589
786
|
*/
|
|
590
787
|
declare function withAlpha(colorInput: string, alpha: number): string;
|
|
591
788
|
|
|
@@ -595,7 +792,12 @@ declare function withAlpha(colorInput: string, alpha: number): string;
|
|
|
595
792
|
*/
|
|
596
793
|
|
|
597
794
|
/**
|
|
598
|
-
* Validation result type
|
|
795
|
+
* Validation result type.
|
|
796
|
+
*
|
|
797
|
+
* - `errors` should be treated as invalid input (preset should be rejected)
|
|
798
|
+
* - `warnings` indicate potentially incomplete presets but may still be usable
|
|
799
|
+
*
|
|
800
|
+
* @public
|
|
599
801
|
*/
|
|
600
802
|
interface ValidationResult {
|
|
601
803
|
isValid: boolean;
|
|
@@ -603,15 +805,30 @@ interface ValidationResult {
|
|
|
603
805
|
warnings: string[];
|
|
604
806
|
}
|
|
605
807
|
/**
|
|
606
|
-
* Validate a single TweakCN
|
|
808
|
+
* Validate a single preset in the TweakCN-compatible shape.
|
|
809
|
+
*
|
|
810
|
+
* Intended usage:
|
|
811
|
+
* - validating user-provided presets before passing them to `ThemeProvider`
|
|
812
|
+
* - debugging preset issues in development
|
|
813
|
+
*
|
|
814
|
+
* Notes:
|
|
815
|
+
* - This is a lightweight validator (it does not fully parse/compute CSS colors)
|
|
816
|
+
*
|
|
817
|
+
* @public
|
|
607
818
|
*/
|
|
608
819
|
declare function validateTweakCNPreset(preset: any, presetId?: string): ValidationResult;
|
|
609
820
|
/**
|
|
610
|
-
* Validate a collection of custom presets
|
|
821
|
+
* Validate a collection of custom presets (record keyed by preset ID).
|
|
822
|
+
*
|
|
823
|
+
* @public
|
|
611
824
|
*/
|
|
612
825
|
declare function validateCustomPresets(customPresets: Record<string, TweakCNThemePreset>): ValidationResult;
|
|
613
826
|
/**
|
|
614
|
-
*
|
|
827
|
+
* Convenience logger for `ValidationResult`.
|
|
828
|
+
*
|
|
829
|
+
* This is primarily intended for local development diagnostics.
|
|
830
|
+
*
|
|
831
|
+
* @public
|
|
615
832
|
*/
|
|
616
833
|
declare function logValidationResult(result: ValidationResult, context?: string): void;
|
|
617
834
|
|
package/dist/index.d.ts
CHANGED
|
@@ -234,15 +234,37 @@ interface ThemePresetButtonsProps {
|
|
|
234
234
|
showSectionHeaders?: boolean;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Appearance mode.
|
|
239
|
+
*
|
|
240
|
+
* - `"system"` follows `prefers-color-scheme`
|
|
241
|
+
* - `resolvedMode` (from hooks/provider) is always `"light"` or `"dark"`
|
|
242
|
+
*
|
|
243
|
+
* @public
|
|
244
|
+
*/
|
|
237
245
|
type Mode = "light" | "dark" | "system";
|
|
246
|
+
/**
|
|
247
|
+
* Screen coordinates used for the optional view-transition ripple when toggling modes.
|
|
248
|
+
*
|
|
249
|
+
* @public
|
|
250
|
+
*/
|
|
238
251
|
interface Coordinates {
|
|
239
252
|
x: number;
|
|
240
253
|
y: number;
|
|
241
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Props for `ThemeToggle`.
|
|
257
|
+
*
|
|
258
|
+
* @public
|
|
259
|
+
*/
|
|
242
260
|
interface ThemeToggleProps {
|
|
261
|
+
/** Additional class name(s) applied to the button element */
|
|
243
262
|
className?: string;
|
|
263
|
+
/** Styling hook exposed via `data-size` */
|
|
244
264
|
size?: "sm" | "md" | "lg";
|
|
265
|
+
/** Styling hook exposed via `data-variant` */
|
|
245
266
|
variant?: "default" | "outline" | "ghost";
|
|
267
|
+
/** Optional custom icon/content (overrides the default icon) */
|
|
246
268
|
children?: ReactNode;
|
|
247
269
|
}
|
|
248
270
|
|
|
@@ -366,13 +388,33 @@ type PresetId<TCustomPresets> = BuiltInPresetId | CustomPresetId$2<TCustomPreset
|
|
|
366
388
|
interface UnifiedThemeProviderProps<TCustomPresets extends CustomPresetsRecord$2 | undefined = undefined> {
|
|
367
389
|
/** React children to wrap with theming context */
|
|
368
390
|
children: react__default.ReactNode;
|
|
369
|
-
/**
|
|
391
|
+
/**
|
|
392
|
+
* Default appearance mode when no stored preference exists.
|
|
393
|
+
*
|
|
394
|
+
* Notes for SSR/App Router:
|
|
395
|
+
* - The initial render must be deterministic between server and client to avoid hydration mismatches.
|
|
396
|
+
* - Persisted mode is restored after hydration (and also pre-hydration via the injected `ThemeScript`).
|
|
397
|
+
*/
|
|
370
398
|
defaultMode?: Mode;
|
|
371
|
-
/**
|
|
399
|
+
/**
|
|
400
|
+
* Default preset ID to use when no stored preset exists or when resetting.
|
|
401
|
+
*
|
|
402
|
+
* If provided, the preset will be applied when:
|
|
403
|
+
* - there is no persisted preset in `localStorage`, or
|
|
404
|
+
* - `clearPreset()` is called.
|
|
405
|
+
*/
|
|
372
406
|
defaultPreset?: PresetId<TCustomPresets>;
|
|
373
|
-
/**
|
|
407
|
+
/**
|
|
408
|
+
* `localStorage` key for appearance mode persistence.
|
|
409
|
+
*
|
|
410
|
+
* Stored value is one of: `"light" | "dark" | "system"`.
|
|
411
|
+
*/
|
|
374
412
|
modeStorageKey?: string;
|
|
375
|
-
/**
|
|
413
|
+
/**
|
|
414
|
+
* `localStorage` key for color preset persistence.
|
|
415
|
+
*
|
|
416
|
+
* Stored value is a JSON blob written by this provider and restored on subsequent loads.
|
|
417
|
+
*/
|
|
376
418
|
presetStorageKey?: string;
|
|
377
419
|
/** Custom presets to add to the available collection */
|
|
378
420
|
customPresets?: TCustomPresets;
|
|
@@ -391,6 +433,14 @@ interface UnifiedThemeProviderProps<TCustomPresets extends CustomPresetsRecord$2
|
|
|
391
433
|
* - π¨ **CSS `!important`** ensures presets override mode defaults
|
|
392
434
|
* - π **MutationObserver** automatically reapplies presets on mode changes
|
|
393
435
|
*
|
|
436
|
+
* ## SSR / hydration behavior
|
|
437
|
+
* This provider is designed for Next.js App Router where Client Components are still SSR-ed.
|
|
438
|
+
* To avoid hydration mismatches:
|
|
439
|
+
* - The initial render does not read `localStorage`.
|
|
440
|
+
* - A pre-hydration `ThemeScript` is injected to apply the correct `html` mode class (`light`/`dark`)
|
|
441
|
+
* and restore preset CSS variables as early as possible.
|
|
442
|
+
* - Persisted mode and preset are then reconciled after hydration.
|
|
443
|
+
*
|
|
394
444
|
* @example
|
|
395
445
|
* ```tsx
|
|
396
446
|
* <ThemeProvider
|
|
@@ -411,6 +461,8 @@ declare function ThemeProvider<const TCustomPresets extends CustomPresetsRecord$
|
|
|
411
461
|
* Provides access to both appearance mode controls and preset management
|
|
412
462
|
* in a single, coordinated interface.
|
|
413
463
|
*
|
|
464
|
+
* Prefer `useThemeEngine()` for a DX-first API with aliases and typed preset IDs.
|
|
465
|
+
*
|
|
414
466
|
* @example
|
|
415
467
|
* ```tsx
|
|
416
468
|
* // Mode controls
|
|
@@ -476,17 +528,79 @@ interface ThemeScriptProps {
|
|
|
476
528
|
defaultPreset?: string;
|
|
477
529
|
}
|
|
478
530
|
/**
|
|
479
|
-
* Pre-hydration theme script
|
|
480
|
-
*
|
|
481
|
-
*
|
|
531
|
+
* Pre-hydration theme script injected by `ThemeProvider`.
|
|
532
|
+
*
|
|
533
|
+
* This runs before React hydration and is intentionally implemented as an inline script so it can:
|
|
534
|
+
* - restore the `html` mode class (`light`/`dark`) and `color-scheme` as early as possible
|
|
535
|
+
* - restore preset CSS variables early to prevent FOUC (unstyled/incorrect tokens on first paint)
|
|
536
|
+
*
|
|
537
|
+
* It reads:
|
|
538
|
+
* - `localStorage[modeStorageKey]` (mode persistence)
|
|
539
|
+
* - `localStorage[presetStorageKey]` (preset persistence)
|
|
540
|
+
*
|
|
541
|
+
* It writes:
|
|
542
|
+
* - `document.documentElement.classList` (`light`/`dark`)
|
|
543
|
+
* - `document.documentElement.style.colorScheme`
|
|
544
|
+
* - `document.documentElement.dataset.themeEngineMode` and `dataset.themeEngineResolvedMode` (best-effort)
|
|
545
|
+
*
|
|
546
|
+
* You typically do not render this manually β `ThemeProvider` includes it automatically.
|
|
482
547
|
*/
|
|
483
548
|
declare function ThemeScript({ presetStorageKey, modeStorageKey, defaultMode, defaultPreset, }: ThemeScriptProps): react_jsx_runtime.JSX.Element;
|
|
484
549
|
|
|
550
|
+
/**
|
|
551
|
+
* Button that toggles the current appearance mode (light β dark) using Theme Engine.
|
|
552
|
+
*
|
|
553
|
+
* - Reads `mode` / `resolvedMode` from `ThemeProvider` via `useTheme()`
|
|
554
|
+
* - On click, calls `toggleMode({ x, y })` to enable the optional view-transition ripple
|
|
555
|
+
* - Renders an icon that reflects the current mode (`light`/`dark`/`system`) unless you pass `children`
|
|
556
|
+
*
|
|
557
|
+
* Data attributes:
|
|
558
|
+
* - `data-size`: `"sm" | "md" | "lg"` (for styling hooks)
|
|
559
|
+
* - `data-variant`: `"default" | "outline" | "ghost"`
|
|
560
|
+
* - `data-mode`: resolved mode (`"light" | "dark"`) for CSS hooks
|
|
561
|
+
* - `data-theme`: alias of `data-mode`
|
|
562
|
+
*
|
|
563
|
+
* @example
|
|
564
|
+
* ```tsx
|
|
565
|
+
* import { ThemeToggle } from "@fakhrirafiki/theme-engine";
|
|
566
|
+
*
|
|
567
|
+
* export function Header() {
|
|
568
|
+
* return <ThemeToggle className="ml-auto" />;
|
|
569
|
+
* }
|
|
570
|
+
* ```
|
|
571
|
+
*/
|
|
485
572
|
declare const ThemeToggle: react.ForwardRefExoticComponent<ThemeToggleProps & react.RefAttributes<HTMLButtonElement>>;
|
|
486
573
|
|
|
487
574
|
/**
|
|
488
575
|
* Main ThemePresetButtons component
|
|
489
576
|
*/
|
|
577
|
+
/**
|
|
578
|
+
* Preset picker UI for Theme Engine.
|
|
579
|
+
*
|
|
580
|
+
* Renders a horizontally scrolling set of preset buttons (optionally in multiple rows) and applies
|
|
581
|
+
* the selected preset via `ThemeProvider` context.
|
|
582
|
+
*
|
|
583
|
+
* Requirements:
|
|
584
|
+
* - Must be used under `ThemeProvider` (it reads `availablePresets`/`currentPreset` from context).
|
|
585
|
+
*
|
|
586
|
+
* Behavior:
|
|
587
|
+
* - Built-in + custom presets are merged from context and displayed (custom presets are shown first).
|
|
588
|
+
* - Selecting a preset calls `applyPreset()` from the provider, which also persists it to `localStorage`.
|
|
589
|
+
* - Supports infinite marquee animation; disable via `animation={{ enabled: false }}`.
|
|
590
|
+
*
|
|
591
|
+
* Customization:
|
|
592
|
+
* - Use `renderPreset` to fully control the button UI (selection handling is still managed internally).
|
|
593
|
+
* - Use `renderColorBox` to customize the color dots while keeping the default layout.
|
|
594
|
+
*
|
|
595
|
+
* @example
|
|
596
|
+
* ```tsx
|
|
597
|
+
* import { ThemePresetButtons } from "@fakhrirafiki/theme-engine";
|
|
598
|
+
*
|
|
599
|
+
* export function PresetsSection() {
|
|
600
|
+
* return <ThemePresetButtons className="mt-6" maxPresets={24} />;
|
|
601
|
+
* }
|
|
602
|
+
* ```
|
|
603
|
+
*/
|
|
490
604
|
declare const ThemePresetButtons: ({ animation: animationOverrides, layout: layoutOverrides, renderPreset, renderColorBox, className, categories, maxPresets, showBuiltIn, showCustom, }: ThemePresetButtonsProps) => react_jsx_runtime.JSX.Element;
|
|
491
605
|
|
|
492
606
|
type CustomPresetsRecord$1 = Record<string, TweakCNThemePreset>;
|
|
@@ -500,6 +614,28 @@ type LooseString$1 = string & {};
|
|
|
500
614
|
* - Custom IDs are inferred from the keys of the `customPresets` argument
|
|
501
615
|
*
|
|
502
616
|
* The resulting `setThemePresetById()` still accepts any string, but VS Code will suggest known IDs first.
|
|
617
|
+
*
|
|
618
|
+
* Notes:
|
|
619
|
+
* - `customPresets` is only used for TypeScript inference (no runtime effect).
|
|
620
|
+
* - Prefer `useThemeEngine()` if you want a higher-level DX API with aliases.
|
|
621
|
+
*
|
|
622
|
+
* @example
|
|
623
|
+
* ```tsx
|
|
624
|
+
* "use client";
|
|
625
|
+
*
|
|
626
|
+
* import { useTypedTheme } from "@fakhrirafiki/theme-engine";
|
|
627
|
+
* import { customPresets } from "./custom-presets";
|
|
628
|
+
*
|
|
629
|
+
* export function PresetPicker() {
|
|
630
|
+
* const { currentPreset, setThemePresetById } = useTypedTheme(customPresets);
|
|
631
|
+
*
|
|
632
|
+
* return (
|
|
633
|
+
* <button onClick={() => setThemePresetById("my-brand")}>
|
|
634
|
+
* Active: {currentPreset?.presetName ?? "default"}
|
|
635
|
+
* </button>
|
|
636
|
+
* );
|
|
637
|
+
* }
|
|
638
|
+
* ```
|
|
503
639
|
*/
|
|
504
640
|
declare function useTypedTheme<const TCustomPresets extends CustomPresetsRecord$1 | undefined = undefined>(customPresets?: TCustomPresets): {
|
|
505
641
|
setThemePresetById: (presetId: LooseString$1 | ThemePresetId$1<TCustomPresets>) => void;
|
|
@@ -528,11 +664,26 @@ type LooseString = string & {};
|
|
|
528
664
|
/**
|
|
529
665
|
* Type helper to "register" your presets for autocomplete.
|
|
530
666
|
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
667
|
+
* @example
|
|
668
|
+
* ```ts
|
|
669
|
+
* import { type ThemePresets, useThemeEngine } from "@fakhrirafiki/theme-engine";
|
|
670
|
+
* import { customPresets } from "./custom-presets";
|
|
671
|
+
*
|
|
672
|
+
* type PresetRegistry = ThemePresets<typeof customPresets>;
|
|
673
|
+
*
|
|
674
|
+
* const theme = useThemeEngine<PresetRegistry>();
|
|
675
|
+
* // theme.applyThemeById("my-custom-id") // β
autocomplete for keys in customPresets + built-ins
|
|
676
|
+
* ```
|
|
533
677
|
*/
|
|
534
678
|
type ThemePresets<T> = T extends CustomPresetsRecord ? T : never;
|
|
535
679
|
type ThemeEnginePresetId<TCustomPresets extends CustomPresetsRecord | undefined = undefined> = ThemePresetId<TCustomPresets>;
|
|
680
|
+
/**
|
|
681
|
+
* Accepts either:
|
|
682
|
+
* - a typed preset ID (built-in + inferred custom preset IDs), or
|
|
683
|
+
* - any string (runtime safety / forwards compatibility)
|
|
684
|
+
*
|
|
685
|
+
* This is useful when you receive preset IDs dynamically (e.g. from a URL param).
|
|
686
|
+
*/
|
|
536
687
|
type ThemeId<TCustomPresets extends CustomPresetsRecord | undefined = undefined> = ThemeEnginePresetId<TCustomPresets> | LooseString;
|
|
537
688
|
/**
|
|
538
689
|
* DX-first hook for Theme Engine.
|
|
@@ -543,6 +694,45 @@ type ThemeId<TCustomPresets extends CustomPresetsRecord | undefined = undefined>
|
|
|
543
694
|
*
|
|
544
695
|
* For typed preset ID autocomplete (built-in + your custom IDs):
|
|
545
696
|
* `useThemeEngine<ThemePresets<typeof customPresets>>()`
|
|
697
|
+
*
|
|
698
|
+
* Naming:
|
|
699
|
+
* - `applyThemeById` and `applyPresetById` are aliases
|
|
700
|
+
* - `clearTheme` and `clearPreset` are aliases
|
|
701
|
+
*
|
|
702
|
+
* @example
|
|
703
|
+
* ```tsx
|
|
704
|
+
* "use client";
|
|
705
|
+
*
|
|
706
|
+
* import { ThemeProvider, useThemeEngine, type ThemePresets } from "@fakhrirafiki/theme-engine";
|
|
707
|
+
* import { customPresets } from "./custom-presets";
|
|
708
|
+
*
|
|
709
|
+
* type Presets = ThemePresets<typeof customPresets>;
|
|
710
|
+
*
|
|
711
|
+
* function Controls() {
|
|
712
|
+
* const { mode, resolvedMode, setDarkMode, applyThemeById, clearTheme } = useThemeEngine<Presets>();
|
|
713
|
+
*
|
|
714
|
+
* return (
|
|
715
|
+
* <div>
|
|
716
|
+
* <button onClick={() => setDarkMode("system")}>System</button>
|
|
717
|
+
* <button onClick={() => setDarkMode("light")}>Light</button>
|
|
718
|
+
* <button onClick={() => setDarkMode("dark")}>Dark</button>
|
|
719
|
+
*
|
|
720
|
+
* <button onClick={() => applyThemeById("modern-minimal")}>Modern Minimal</button>
|
|
721
|
+
* <button onClick={() => clearTheme()}>Reset</button>
|
|
722
|
+
*
|
|
723
|
+
* <div>mode: {mode} Β· resolved: {resolvedMode}</div>
|
|
724
|
+
* </div>
|
|
725
|
+
* );
|
|
726
|
+
* }
|
|
727
|
+
*
|
|
728
|
+
* export default function Page() {
|
|
729
|
+
* return (
|
|
730
|
+
* <ThemeProvider customPresets={customPresets} defaultPreset="modern-minimal">
|
|
731
|
+
* <Controls />
|
|
732
|
+
* </ThemeProvider>
|
|
733
|
+
* );
|
|
734
|
+
* }
|
|
735
|
+
* ```
|
|
546
736
|
*/
|
|
547
737
|
declare function useThemeEngine<const TCustomPresets extends CustomPresetsRecord | undefined = undefined>(): {
|
|
548
738
|
darkMode: boolean;
|
|
@@ -585,7 +775,14 @@ type ColorFormat = 'hsl' | 'rgb' | 'hex';
|
|
|
585
775
|
*/
|
|
586
776
|
declare function formatColor(colorInput: string, outputFormat?: ColorFormat, includeFunctionWrapper?: boolean): string;
|
|
587
777
|
/**
|
|
588
|
-
* Create color with alpha transparency
|
|
778
|
+
* Create a color with alpha transparency.
|
|
779
|
+
*
|
|
780
|
+
* Notes:
|
|
781
|
+
* - This helper only supports HSL-like inputs that `parseHSL()` can parse
|
|
782
|
+
* (e.g. `"hsl(210 40% 98%)"` or `"210 40% 98%"`).
|
|
783
|
+
* - For hex/rgb inputs, convert first with `formatColor(color, "hsl")`.
|
|
784
|
+
*
|
|
785
|
+
* @public
|
|
589
786
|
*/
|
|
590
787
|
declare function withAlpha(colorInput: string, alpha: number): string;
|
|
591
788
|
|
|
@@ -595,7 +792,12 @@ declare function withAlpha(colorInput: string, alpha: number): string;
|
|
|
595
792
|
*/
|
|
596
793
|
|
|
597
794
|
/**
|
|
598
|
-
* Validation result type
|
|
795
|
+
* Validation result type.
|
|
796
|
+
*
|
|
797
|
+
* - `errors` should be treated as invalid input (preset should be rejected)
|
|
798
|
+
* - `warnings` indicate potentially incomplete presets but may still be usable
|
|
799
|
+
*
|
|
800
|
+
* @public
|
|
599
801
|
*/
|
|
600
802
|
interface ValidationResult {
|
|
601
803
|
isValid: boolean;
|
|
@@ -603,15 +805,30 @@ interface ValidationResult {
|
|
|
603
805
|
warnings: string[];
|
|
604
806
|
}
|
|
605
807
|
/**
|
|
606
|
-
* Validate a single TweakCN
|
|
808
|
+
* Validate a single preset in the TweakCN-compatible shape.
|
|
809
|
+
*
|
|
810
|
+
* Intended usage:
|
|
811
|
+
* - validating user-provided presets before passing them to `ThemeProvider`
|
|
812
|
+
* - debugging preset issues in development
|
|
813
|
+
*
|
|
814
|
+
* Notes:
|
|
815
|
+
* - This is a lightweight validator (it does not fully parse/compute CSS colors)
|
|
816
|
+
*
|
|
817
|
+
* @public
|
|
607
818
|
*/
|
|
608
819
|
declare function validateTweakCNPreset(preset: any, presetId?: string): ValidationResult;
|
|
609
820
|
/**
|
|
610
|
-
* Validate a collection of custom presets
|
|
821
|
+
* Validate a collection of custom presets (record keyed by preset ID).
|
|
822
|
+
*
|
|
823
|
+
* @public
|
|
611
824
|
*/
|
|
612
825
|
declare function validateCustomPresets(customPresets: Record<string, TweakCNThemePreset>): ValidationResult;
|
|
613
826
|
/**
|
|
614
|
-
*
|
|
827
|
+
* Convenience logger for `ValidationResult`.
|
|
828
|
+
*
|
|
829
|
+
* This is primarily intended for local development diagnostics.
|
|
830
|
+
*
|
|
831
|
+
* @public
|
|
615
832
|
*/
|
|
616
833
|
declare function logValidationResult(result: ValidationResult, context?: string): void;
|
|
617
834
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fakhrirafiki/theme-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.21",
|
|
4
4
|
"description": "Elegant theming system with smooth transitions, custom presets, semantic accent colors, and complete shadcn/ui support for modern React applications",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|