@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 CHANGED
@@ -1,6 +1,6 @@
1
- # 🎨 Theme Engine
1
+ # 🎨 useThemeEngine for **Next.js (App Router)**
2
2
 
3
- Theme system for **Next.js (App Router)**: mode (`light | dark | system`) + theme presets (semantic tokens via CSS variables).
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
  ![npm downloads](https://img.shields.io/npm/dm/@fakhrirafiki/theme-engine)
9
9
  ![license](https://img.shields.io/npm/l/@fakhrirafiki/theme-engine)
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 '@fakhrirafiki/theme-engine/styles';
53
+ @import "@fakhrirafiki/theme-engine/styles";
54
54
  ```
55
55
 
56
56
  βœ… Tailwind v4 (recommended order):
57
57
 
58
58
  ```css
59
- @import 'tailwindcss';
60
- @import '@fakhrirafiki/theme-engine/styles';
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 '@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';
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 'react';
80
- import { ThemeProvider } from '@fakhrirafiki/theme-engine';
81
- import './globals.css';
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
- 'use client';
103
+ "use client";
104
104
 
105
- import { useThemeEngine } from '@fakhrirafiki/theme-engine';
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('system')}>System</button>
113
- <button onClick={() => setDarkMode('light')}>Light</button>
114
- <button onClick={() => setDarkMode('dark')}>Dark</button>
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
- 'use client';
125
+ "use client";
126
126
 
127
- import { useThemeEngine } from '@fakhrirafiki/theme-engine';
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('modern-minimal')}>Modern Minimal</button>
134
+ <button onClick={() => applyThemeById("modern-minimal")}>Modern Minimal</button>
135
135
  <button onClick={() => clearTheme()}>Reset</button>
136
- <div>Active: {currentTheme?.presetName ?? 'Default'}</div>
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
- 'use client';
145
+ "use client";
146
146
 
147
- import { ThemePresets, useThemeEngine } from '@fakhrirafiki/theme-engine';
148
- import { customPresets } from './custom-theme-presets';
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('my-brand')}>My Brand</button>
156
- <button onClick={() => applyThemeById('modern-minimal')}>Modern Minimal</button>
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
- ## 🧩 Custom presets (recommended)
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 '@fakhrirafiki/theme-engine';
203
+ import { type TweakCNThemePreset } from "@fakhrirafiki/theme-engine";
210
204
 
211
205
  export const customPresets = {
212
- 'my-brand': {
213
- label: 'My Brand',
206
+ "my-brand": {
207
+ label: "My Brand",
214
208
  styles: {
215
209
  light: {
216
- background: '#ffffff',
217
- foreground: '#111827',
218
- primary: '#2563eb',
219
- 'primary-foreground': '#ffffff',
220
- secondary: '#e5e7eb',
221
- 'secondary-foreground': '#111827',
222
- card: '#ffffff',
223
- 'card-foreground': '#111827',
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: '#0b1020',
227
- foreground: '#f9fafb',
228
- primary: '#60a5fa',
229
- 'primary-foreground': '#0b1020',
230
- secondary: '#1f2937',
231
- 'secondary-foreground': '#f9fafb',
232
- card: '#111827',
233
- 'card-foreground': '#f9fafb',
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 'react';
244
- import { ThemeProvider } from '@fakhrirafiki/theme-engine';
245
- import { customPresets } from './custom-theme-presets';
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 '@fakhrirafiki/theme-engine';
263
+ import { getPresetIds, getPresetById } from "@fakhrirafiki/theme-engine";
270
264
 
271
265
  const ids = getPresetIds();
272
- const modernMinimal = getPresetById('modern-minimal');
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 | Tailwind class examples | Backed by preset CSS variables | Notes |
282
- | --- | --- | --- | --- |
283
- | Surfaces | `bg-background`, `text-foreground` | `--background`, `--foreground` | Base app background + text |
284
- | Cards | `bg-card`, `text-card-foreground` | `--card`, `--card-foreground` | Cards / panels |
285
- | Popovers | `bg-popover`, `text-popover-foreground` | `--popover`, `--popover-foreground` | Popovers / dropdowns |
286
- | Brand / actions | `bg-primary`, `text-primary-foreground` | `--primary`, `--primary-foreground` | Primary buttons / highlights |
287
- | Secondary | `bg-secondary`, `text-secondary-foreground` | `--secondary`, `--secondary-foreground` | Secondary UI surfaces |
288
- | Muted | `bg-muted`, `text-muted-foreground` | `--muted`, `--muted-foreground` | Subtle backgrounds / helper text |
289
- | Accent | `bg-accent`, `text-accent-foreground` | `--accent`, `--accent-foreground` | Emphasis (not status colors) |
290
- | Destructive | `bg-destructive`, `text-destructive-foreground` | `--destructive`, `--destructive-foreground` | Danger actions |
291
- | Borders / focus | `border-border`, `border-input`, `ring-ring` | `--border`, `--input`, `--ring` | Used by `outline-ring/50` too |
292
- | Charts | `bg-chart-1`, `text-chart-2` | `--chart-1` ... `--chart-5` | Data viz palettes |
293
- | Sidebar | `bg-sidebar`, `text-sidebar-foreground`, `bg-sidebar-primary`, `border-sidebar-border` | `--sidebar-*` | Handy for dashboard layouts |
294
- | Status accents | `bg-accent-success`, `text-accent-danger-foreground` | `--accent-<name>`, `--accent-<name>-foreground` | Optional: only if preset defines `accent-*` |
295
- | Radius scale | `rounded-sm`, `rounded-md`, `rounded-lg`, `rounded-xl` | `--radius-sm`, `--radius-md`, `--radius-lg`, `--radius-xl` | Derived from `--radius` |
296
- | Tracking scale | `tracking-tighter`, `tracking-wide` | `--tracking-*` | Derived from `--letter-spacing` |
297
- | Fonts | `font-sans`, `font-serif`, `font-mono` | `--font-sans`, `--font-serif`, `--font-mono` | Defaults in `base.css` |
298
- | Shadows | `shadow-sm`, `shadow-md`, `shadow-xl` | `--shadow-*` | Derived from `--shadow-*` knobs |
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
- 'use client';
303
+ "use client";
310
304
 
311
- import { ThemeToggle } from '@fakhrirafiki/theme-engine';
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
- 'use client';
317
+ "use client";
324
318
 
325
- import { ThemePresetButtons } from '@fakhrirafiki/theme-engine';
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
- 'use client';
333
+ "use client";
340
334
 
341
- import { formatColor, useThemeEngine } from '@fakhrirafiki/theme-engine';
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 === 'dark' ? preset.styles.dark : preset.styles.light;
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
- ? 'border-primary/70 bg-primary/10 text-foreground'
390
- : 'border-border bg-muted/40 text-muted-foreground hover:border-muted-foreground/40 hover:bg-muted/60'
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, 'hex') }}
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 | Type | Default | Description |
444
- | --- | --- | --- | --- |
445
- | `children` | `ReactNode` | required | React subtree |
446
- | `defaultMode` | `Mode` | `'system'` | Used when no persisted value |
447
- | `defaultPreset` | `BuiltInPresetId \| keyof customPresets` | `undefined` | Default preset (see SSR note) |
448
- | `modeStorageKey` | `string` | `'theme-engine-theme'` | `localStorage` key for mode |
449
- | `presetStorageKey` | `string` | `'theme-preset'` | `localStorage` key for preset |
450
- | `customPresets` | `Record<string, TweakCNThemePreset>` | `undefined` | Add your own presets (can override built-ins by ID) |
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 | Type | Description |
470
- | --- | --- | --- |
471
- | `darkMode` | `boolean` | `resolvedMode === 'dark'` |
472
- | `mode` | `'light' \| 'dark' \| 'system'` | Current user preference |
473
- | `resolvedMode` | `'light' \| 'dark'` | Resolved mode (never `system`) |
474
- | `setDarkMode` | `(mode: Mode) => void` | Set `light \| dark \| system` |
475
- | `toggleDarkMode` | `(coords?: { x: number; y: number }) => void` | Toggles light/dark (and exits `system`) |
476
- | `applyThemeById` | `(id: ThemeId) => void` | Apply a preset by ID (alias: `applyPresetById`) |
477
- | `clearTheme` | `() => void` | Clear preset and fall back to `defaultPreset` if provided (alias: `clearPreset`) |
478
- | `currentTheme` | `{ presetId; presetName; colors; appliedAt } \| null` | Current preset (alias: `currentPreset`) |
479
- | `isUsingDefaultPreset` | `boolean` | Whether current preset equals `defaultPreset` |
480
- | `availablePresets` | `Record<string, TweakCNThemePreset>` | Built-in + custom |
481
- | `builtInPresets` | `Record<string, TweakCNThemePreset>` | Built-in only |
482
- | `customPresets` | `Record<string, TweakCNThemePreset>` | Custom only |
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 | Description |
487
- | --- | --- |
488
- | `formatColor(color, format)` | Converts a color string into `hsl`/`rgb`/`hex` |
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
- /** Default appearance mode when no stored preference exists */
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
- /** Default preset ID to use when no stored preset exists or when resetting */
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
- /** localStorage key for appearance mode persistence */
407
+ /**
408
+ * `localStorage` key for appearance mode persistence.
409
+ *
410
+ * Stored value is one of: `"light" | "dark" | "system"`.
411
+ */
374
412
  modeStorageKey?: string;
375
- /** localStorage key for color preset persistence */
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
- * - Restores appearance mode (light/dark/system) to avoid hydration mismatch + FOUC.
481
- * - Restores preset CSS variables early so Tailwind/shadcn tokens render correctly on first paint.
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
- * Usage:
532
- * `useThemeEngine<ThemePresets<typeof customPresets>>()`
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 preset
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
- * Helper function to log validation results
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
- /** Default appearance mode when no stored preference exists */
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
- /** Default preset ID to use when no stored preset exists or when resetting */
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
- /** localStorage key for appearance mode persistence */
407
+ /**
408
+ * `localStorage` key for appearance mode persistence.
409
+ *
410
+ * Stored value is one of: `"light" | "dark" | "system"`.
411
+ */
374
412
  modeStorageKey?: string;
375
- /** localStorage key for color preset persistence */
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
- * - Restores appearance mode (light/dark/system) to avoid hydration mismatch + FOUC.
481
- * - Restores preset CSS variables early so Tailwind/shadcn tokens render correctly on first paint.
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
- * Usage:
532
- * `useThemeEngine<ThemePresets<typeof customPresets>>()`
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 preset
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
- * Helper function to log validation results
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.19",
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",