@fakhrirafiki/theme-engine 0.4.18 β 0.4.20
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.js +2 -18
- package/dist/index.mjs +2 -18
- package/dist/styles/components.css +38 -0
- 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.js
CHANGED
|
@@ -4742,27 +4742,9 @@ var ThemeToggle = (0, import_react3.forwardRef)(
|
|
|
4742
4742
|
const { clientX: x, clientY: y } = event;
|
|
4743
4743
|
toggleMode({ x, y });
|
|
4744
4744
|
};
|
|
4745
|
-
const sizeClasses = {
|
|
4746
|
-
sm: "h-8 w-8 p-1.5",
|
|
4747
|
-
md: "h-9 w-9 p-2",
|
|
4748
|
-
lg: "h-10 w-10 p-2.5"
|
|
4749
|
-
};
|
|
4750
|
-
const variantClasses = {
|
|
4751
|
-
default: "bg-background border border-input hover:bg-accent hover:text-accent-foreground",
|
|
4752
|
-
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
4753
|
-
ghost: "hover:bg-accent hover:text-accent-foreground"
|
|
4754
|
-
};
|
|
4755
4745
|
const baseClasses = (0, import_clsx.clsx)(
|
|
4756
4746
|
// Base button styles
|
|
4757
4747
|
"theme-toggle",
|
|
4758
|
-
"inline-flex items-center justify-center",
|
|
4759
|
-
"rounded-md text-sm font-medium",
|
|
4760
|
-
"ring-offset-background transition-colors",
|
|
4761
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
4762
|
-
"disabled:pointer-events-none disabled:opacity-50",
|
|
4763
|
-
// Size and variant
|
|
4764
|
-
sizeClasses[size],
|
|
4765
|
-
variantClasses[variant],
|
|
4766
4748
|
className
|
|
4767
4749
|
);
|
|
4768
4750
|
const renderIcon = () => {
|
|
@@ -4784,6 +4766,8 @@ var ThemeToggle = (0, import_react3.forwardRef)(
|
|
|
4784
4766
|
ref,
|
|
4785
4767
|
className: baseClasses,
|
|
4786
4768
|
onClick: handleClick,
|
|
4769
|
+
"data-size": size,
|
|
4770
|
+
"data-variant": variant,
|
|
4787
4771
|
"data-mode": resolvedMode,
|
|
4788
4772
|
"data-theme": resolvedMode,
|
|
4789
4773
|
"aria-label": `Switch to ${resolvedMode === "light" ? "dark" : "light"} mode`,
|
package/dist/index.mjs
CHANGED
|
@@ -4698,27 +4698,9 @@ var ThemeToggle = forwardRef(
|
|
|
4698
4698
|
const { clientX: x, clientY: y } = event;
|
|
4699
4699
|
toggleMode({ x, y });
|
|
4700
4700
|
};
|
|
4701
|
-
const sizeClasses = {
|
|
4702
|
-
sm: "h-8 w-8 p-1.5",
|
|
4703
|
-
md: "h-9 w-9 p-2",
|
|
4704
|
-
lg: "h-10 w-10 p-2.5"
|
|
4705
|
-
};
|
|
4706
|
-
const variantClasses = {
|
|
4707
|
-
default: "bg-background border border-input hover:bg-accent hover:text-accent-foreground",
|
|
4708
|
-
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
4709
|
-
ghost: "hover:bg-accent hover:text-accent-foreground"
|
|
4710
|
-
};
|
|
4711
4701
|
const baseClasses = clsx(
|
|
4712
4702
|
// Base button styles
|
|
4713
4703
|
"theme-toggle",
|
|
4714
|
-
"inline-flex items-center justify-center",
|
|
4715
|
-
"rounded-md text-sm font-medium",
|
|
4716
|
-
"ring-offset-background transition-colors",
|
|
4717
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
4718
|
-
"disabled:pointer-events-none disabled:opacity-50",
|
|
4719
|
-
// Size and variant
|
|
4720
|
-
sizeClasses[size],
|
|
4721
|
-
variantClasses[variant],
|
|
4722
4704
|
className
|
|
4723
4705
|
);
|
|
4724
4706
|
const renderIcon = () => {
|
|
@@ -4740,6 +4722,8 @@ var ThemeToggle = forwardRef(
|
|
|
4740
4722
|
ref,
|
|
4741
4723
|
className: baseClasses,
|
|
4742
4724
|
onClick: handleClick,
|
|
4725
|
+
"data-size": size,
|
|
4726
|
+
"data-variant": variant,
|
|
4743
4727
|
"data-mode": resolvedMode,
|
|
4744
4728
|
"data-theme": resolvedMode,
|
|
4745
4729
|
"aria-label": `Switch to ${resolvedMode === "light" ? "dark" : "light"} mode`,
|
|
@@ -8,6 +8,44 @@
|
|
|
8
8
|
border: 1px solid hsl(var(--border));
|
|
9
9
|
background-color: hsl(var(--background));
|
|
10
10
|
color: hsl(var(--foreground));
|
|
11
|
+
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
border-radius: calc(var(--radius) + 0.125rem);
|
|
17
|
+
font-size: 0.875rem;
|
|
18
|
+
font-weight: 500;
|
|
19
|
+
line-height: 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.theme-toggle:disabled {
|
|
23
|
+
opacity: 0.5;
|
|
24
|
+
cursor: not-allowed;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.theme-toggle[data-size="sm"] {
|
|
29
|
+
width: 2rem;
|
|
30
|
+
height: 2rem;
|
|
31
|
+
padding: 0.375rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.theme-toggle[data-size="md"] {
|
|
35
|
+
width: 2.25rem;
|
|
36
|
+
height: 2.25rem;
|
|
37
|
+
padding: 0.5rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.theme-toggle[data-size="lg"] {
|
|
41
|
+
width: 2.5rem;
|
|
42
|
+
height: 2.5rem;
|
|
43
|
+
padding: 0.625rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.theme-toggle[data-variant="ghost"] {
|
|
47
|
+
border-color: transparent;
|
|
48
|
+
background-color: transparent;
|
|
11
49
|
}
|
|
12
50
|
|
|
13
51
|
.theme-toggle:hover {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fakhrirafiki/theme-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.20",
|
|
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",
|