@fakhrirafiki/theme-engine 0.4.20 → 0.4.22
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 +3 -0
- package/dist/index.d.mts +231 -14
- package/dist/index.d.ts +231 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,6 +19,9 @@ Dark mode + theme presets (semantic tokens via CSS variables).
|
|
|
19
19
|
- 🎨 **Theme presets**: built-in presets + your own presets
|
|
20
20
|
- 🧩 **Tailwind v4 friendly**: `@theme inline` tokens included (works with shadcn-style semantic tokens)
|
|
21
21
|
|
|
22
|
+
> [!TIP]
|
|
23
|
+
> If you’re using **shadcn/ui** with **Next.js App Router**, you should use this package — it provides a complete, production-ready theme layer (mode + presets) that plugs straight into shadcn’s semantic tokens.
|
|
24
|
+
|
|
22
25
|
## 📚 Table of contents
|
|
23
26
|
|
|
24
27
|
- [Install](#-install)
|
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.22",
|
|
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",
|