@fakhrirafiki/theme-engine 0.4.14 → 0.4.17
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 +13 -2
- package/dist/index.d.mts +14 -3
- package/dist/index.d.ts +14 -3
- package/dist/index.js +68 -47
- package/dist/index.mjs +68 -47
- package/dist/styles/components.css +7 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,6 +8,9 @@ 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
|
|
13
|
+
|
|
11
14
|
## ✨ Why use this?
|
|
12
15
|
|
|
13
16
|
- ⚡ **Fast setup**: 1 CSS import + 1 provider
|
|
@@ -407,7 +410,7 @@ export function ThemePresetSelect({
|
|
|
407
410
|
|
|
408
411
|
{isActive && (
|
|
409
412
|
<span className="inline-flex items-center rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-medium text-foreground">
|
|
410
|
-
|
|
413
|
+
Active
|
|
411
414
|
</span>
|
|
412
415
|
)}
|
|
413
416
|
</span>
|
|
@@ -415,7 +418,7 @@ export function ThemePresetSelect({
|
|
|
415
418
|
);
|
|
416
419
|
})}
|
|
417
420
|
|
|
418
|
-
{presets.length === 0 && <p className="text-xs text-muted-foreground">
|
|
421
|
+
{presets.length === 0 && <p className="text-xs text-muted-foreground">No themes available yet.</p>}
|
|
419
422
|
</div>
|
|
420
423
|
);
|
|
421
424
|
}
|
|
@@ -503,6 +506,14 @@ Note: the thrown error string might mention `useTheme` because `useThemeEngine()
|
|
|
503
506
|
|
|
504
507
|
Ensure your `globals.css` imports `@fakhrirafiki/theme-engine/styles` (and Tailwind v4 is configured if you rely on Tailwind utilities).
|
|
505
508
|
|
|
509
|
+
### Turbopack: “module factory is not available” (HMR) after upgrading
|
|
510
|
+
|
|
511
|
+
This is a Next.js Turbopack dev/HMR issue that can happen after updating dependencies in `node_modules` (or when using a locally linked package that rebuilds `dist/` while `next dev` is running).
|
|
512
|
+
|
|
513
|
+
- Restart `next dev` (often enough).
|
|
514
|
+
- If it persists: delete `.next/` and restart.
|
|
515
|
+
- Workaround: run dev server with webpack: `next dev --webpack`
|
|
516
|
+
|
|
506
517
|
---
|
|
507
518
|
|
|
508
519
|
## License
|
package/dist/index.d.mts
CHANGED
|
@@ -460,16 +460,27 @@ interface ThemeScriptProps {
|
|
|
460
460
|
* @default 'theme-preset'
|
|
461
461
|
*/
|
|
462
462
|
presetStorageKey?: string;
|
|
463
|
+
/**
|
|
464
|
+
* Storage key for appearance mode persistence
|
|
465
|
+
* @default 'theme-engine-theme'
|
|
466
|
+
*/
|
|
467
|
+
modeStorageKey?: string;
|
|
468
|
+
/**
|
|
469
|
+
* Default appearance mode when no stored preference exists
|
|
470
|
+
* @default 'system'
|
|
471
|
+
*/
|
|
472
|
+
defaultMode?: Mode;
|
|
463
473
|
/**
|
|
464
474
|
* Default preset ID to apply when no stored preset exists
|
|
465
475
|
*/
|
|
466
476
|
defaultPreset?: string;
|
|
467
477
|
}
|
|
468
478
|
/**
|
|
469
|
-
*
|
|
470
|
-
*
|
|
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.
|
|
471
482
|
*/
|
|
472
|
-
declare function ThemeScript({ presetStorageKey, defaultPreset }: ThemeScriptProps): react_jsx_runtime.JSX.Element;
|
|
483
|
+
declare function ThemeScript({ presetStorageKey, modeStorageKey, defaultMode, defaultPreset, }: ThemeScriptProps): react_jsx_runtime.JSX.Element;
|
|
473
484
|
|
|
474
485
|
declare const ThemeToggle: react.ForwardRefExoticComponent<ThemeToggleProps & react.RefAttributes<HTMLButtonElement>>;
|
|
475
486
|
|
package/dist/index.d.ts
CHANGED
|
@@ -460,16 +460,27 @@ interface ThemeScriptProps {
|
|
|
460
460
|
* @default 'theme-preset'
|
|
461
461
|
*/
|
|
462
462
|
presetStorageKey?: string;
|
|
463
|
+
/**
|
|
464
|
+
* Storage key for appearance mode persistence
|
|
465
|
+
* @default 'theme-engine-theme'
|
|
466
|
+
*/
|
|
467
|
+
modeStorageKey?: string;
|
|
468
|
+
/**
|
|
469
|
+
* Default appearance mode when no stored preference exists
|
|
470
|
+
* @default 'system'
|
|
471
|
+
*/
|
|
472
|
+
defaultMode?: Mode;
|
|
463
473
|
/**
|
|
464
474
|
* Default preset ID to apply when no stored preset exists
|
|
465
475
|
*/
|
|
466
476
|
defaultPreset?: string;
|
|
467
477
|
}
|
|
468
478
|
/**
|
|
469
|
-
*
|
|
470
|
-
*
|
|
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.
|
|
471
482
|
*/
|
|
472
|
-
declare function ThemeScript({ presetStorageKey, defaultPreset }: ThemeScriptProps): react_jsx_runtime.JSX.Element;
|
|
483
|
+
declare function ThemeScript({ presetStorageKey, modeStorageKey, defaultMode, defaultPreset, }: ThemeScriptProps): react_jsx_runtime.JSX.Element;
|
|
473
484
|
|
|
474
485
|
declare const ThemeToggle: react.ForwardRefExoticComponent<ThemeToggleProps & react.RefAttributes<HTMLButtonElement>>;
|
|
475
486
|
|
package/dist/index.js
CHANGED
|
@@ -3838,7 +3838,12 @@ function logValidationResult(result, context = "Custom presets") {
|
|
|
3838
3838
|
// src/components/UnifiedThemeScript.tsx
|
|
3839
3839
|
var import_react = require("react");
|
|
3840
3840
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
3841
|
-
function ThemeScript({
|
|
3841
|
+
function ThemeScript({
|
|
3842
|
+
presetStorageKey = "theme-preset",
|
|
3843
|
+
modeStorageKey = "theme-engine-theme",
|
|
3844
|
+
defaultMode = "system",
|
|
3845
|
+
defaultPreset
|
|
3846
|
+
}) {
|
|
3842
3847
|
const defaultPresetData = (0, import_react.useMemo)(() => {
|
|
3843
3848
|
if (!defaultPreset) return null;
|
|
3844
3849
|
const preset = getPresetById(defaultPreset);
|
|
@@ -3850,10 +3855,12 @@ function ThemeScript({ presetStorageKey = "theme-preset", defaultPreset }) {
|
|
|
3850
3855
|
}, [defaultPreset]);
|
|
3851
3856
|
const scriptContent = (0, import_react.useMemo)(
|
|
3852
3857
|
() => `
|
|
3853
|
-
// Unified Theme Engine: Restore preset colors before hydration
|
|
3858
|
+
// Unified Theme Engine: Restore mode + preset colors before hydration
|
|
3854
3859
|
(function() {
|
|
3855
3860
|
try {
|
|
3856
3861
|
const presetStorageKey = "${presetStorageKey}";
|
|
3862
|
+
const modeStorageKey = "${modeStorageKey}";
|
|
3863
|
+
const defaultMode = "${defaultMode}";
|
|
3857
3864
|
const isDev = (function() {
|
|
3858
3865
|
try {
|
|
3859
3866
|
return location.hostname === 'localhost' || location.hostname === '127.0.0.1';
|
|
@@ -3861,6 +3868,39 @@ function ThemeScript({ presetStorageKey = "theme-preset", defaultPreset }) {
|
|
|
3861
3868
|
return false;
|
|
3862
3869
|
}
|
|
3863
3870
|
})();
|
|
3871
|
+
|
|
3872
|
+
// ---- Mode restoration (pre-hydration) ----
|
|
3873
|
+
(function() {
|
|
3874
|
+
try {
|
|
3875
|
+
const root = document.documentElement;
|
|
3876
|
+
let storedMode = null;
|
|
3877
|
+
try {
|
|
3878
|
+
storedMode = localStorage.getItem(modeStorageKey);
|
|
3879
|
+
} catch {}
|
|
3880
|
+
|
|
3881
|
+
const isValidMode = storedMode === 'light' || storedMode === 'dark' || storedMode === 'system';
|
|
3882
|
+
const mode = isValidMode ? storedMode : defaultMode;
|
|
3883
|
+
|
|
3884
|
+
let systemMode = 'light';
|
|
3885
|
+
try {
|
|
3886
|
+
systemMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
3887
|
+
} catch {}
|
|
3888
|
+
|
|
3889
|
+
const resolvedMode = mode === 'system' ? systemMode : mode;
|
|
3890
|
+
|
|
3891
|
+
root.classList.remove('light', 'dark');
|
|
3892
|
+
root.classList.add(resolvedMode);
|
|
3893
|
+
root.style.colorScheme = resolvedMode;
|
|
3894
|
+
|
|
3895
|
+
// Expose for runtime consumers (optional)
|
|
3896
|
+
try {
|
|
3897
|
+
root.dataset.themeEngineMode = mode;
|
|
3898
|
+
root.dataset.themeEngineResolvedMode = resolvedMode;
|
|
3899
|
+
} catch {}
|
|
3900
|
+
} catch (error) {
|
|
3901
|
+
if (isDev) console.warn('\u{1F3A8} UnifiedThemeScript: Mode restoration failed:', error);
|
|
3902
|
+
}
|
|
3903
|
+
})();
|
|
3864
3904
|
|
|
3865
3905
|
// CSS property categories (inline for script)
|
|
3866
3906
|
const CSS_CATEGORIES = {
|
|
@@ -4067,7 +4107,7 @@ function ThemeScript({ presetStorageKey = "theme-preset", defaultPreset }) {
|
|
|
4067
4107
|
|
|
4068
4108
|
}
|
|
4069
4109
|
|
|
4070
|
-
//
|
|
4110
|
+
// ---- Preset restoration (pre-hydration) ----
|
|
4071
4111
|
const storedPreset = localStorage.getItem(presetStorageKey);
|
|
4072
4112
|
let presetToApply = null;
|
|
4073
4113
|
|
|
@@ -4085,10 +4125,9 @@ function ThemeScript({ presetStorageKey = "theme-preset", defaultPreset }) {
|
|
|
4085
4125
|
}
|
|
4086
4126
|
|
|
4087
4127
|
if (presetToApply) {
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
const
|
|
4091
|
-
const mode = isDark ? 'dark' : 'light';
|
|
4128
|
+
const root = document.documentElement;
|
|
4129
|
+
const resolved = (root.dataset && root.dataset.themeEngineResolvedMode) || (root.classList.contains('dark') ? 'dark' : 'light');
|
|
4130
|
+
const mode = resolved === 'dark' ? 'dark' : 'light';
|
|
4092
4131
|
const colors = presetToApply.colors && presetToApply.colors[mode];
|
|
4093
4132
|
|
|
4094
4133
|
if (colors) {
|
|
@@ -4315,15 +4354,17 @@ function ThemeProvider({
|
|
|
4315
4354
|
}) {
|
|
4316
4355
|
const normalizedCustomPresets = (0, import_react2.useMemo)(() => customPresets ?? {}, [customPresets]);
|
|
4317
4356
|
const [mode, setMode] = (0, import_react2.useState)(() => {
|
|
4318
|
-
|
|
4319
|
-
return stored || defaultMode;
|
|
4357
|
+
return defaultMode;
|
|
4320
4358
|
});
|
|
4321
4359
|
const [resolvedMode, setResolvedMode] = (0, import_react2.useState)(() => {
|
|
4322
|
-
if (
|
|
4323
|
-
|
|
4324
|
-
}
|
|
4325
|
-
return mode;
|
|
4360
|
+
if (defaultMode === "dark") return "dark";
|
|
4361
|
+
return "light";
|
|
4326
4362
|
});
|
|
4363
|
+
(0, import_react2.useEffect)(() => {
|
|
4364
|
+
if (typeof window === "undefined") return;
|
|
4365
|
+
const stored = getStoredMode(modeStorageKey);
|
|
4366
|
+
if (stored) setMode(stored);
|
|
4367
|
+
}, [modeStorageKey]);
|
|
4327
4368
|
const availablePresets = (0, import_react2.useMemo)(() => {
|
|
4328
4369
|
const merged = {};
|
|
4329
4370
|
Object.assign(merged, tweakcnPresets);
|
|
@@ -4591,7 +4632,15 @@ function ThemeProvider({
|
|
|
4591
4632
|
});
|
|
4592
4633
|
}
|
|
4593
4634
|
}, [presetStorageKey, defaultPreset, getAvailablePresetById, applyPresetColors, resolvedMode]);
|
|
4594
|
-
const scriptElement = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
4635
|
+
const scriptElement = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
4636
|
+
ThemeScript,
|
|
4637
|
+
{
|
|
4638
|
+
presetStorageKey,
|
|
4639
|
+
modeStorageKey,
|
|
4640
|
+
defaultMode,
|
|
4641
|
+
defaultPreset
|
|
4642
|
+
}
|
|
4643
|
+
);
|
|
4595
4644
|
const isUsingDefaultPreset = !!defaultPreset && currentPreset?.presetId === defaultPreset;
|
|
4596
4645
|
const contextValue = {
|
|
4597
4646
|
mode,
|
|
@@ -4666,29 +4715,10 @@ var MoonIcon = () => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
|
4666
4715
|
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "m12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" })
|
|
4667
4716
|
}
|
|
4668
4717
|
);
|
|
4669
|
-
var SystemIcon = () => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
4670
|
-
"svg",
|
|
4671
|
-
{
|
|
4672
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
4673
|
-
width: "16",
|
|
4674
|
-
height: "16",
|
|
4675
|
-
viewBox: "0 0 24 24",
|
|
4676
|
-
fill: "none",
|
|
4677
|
-
stroke: "currentColor",
|
|
4678
|
-
strokeWidth: "2",
|
|
4679
|
-
strokeLinecap: "round",
|
|
4680
|
-
strokeLinejoin: "round",
|
|
4681
|
-
className: "theme-toggle-icon",
|
|
4682
|
-
children: [
|
|
4683
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { width: "20", height: "14", x: "2", y: "3", rx: "2" }),
|
|
4684
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "8", x2: "16", y1: "21", y2: "21" }),
|
|
4685
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "12", x2: "12", y1: "17", y2: "21" })
|
|
4686
|
-
]
|
|
4687
|
-
}
|
|
4688
|
-
);
|
|
4689
4718
|
var ThemeToggle = (0, import_react3.forwardRef)(
|
|
4690
4719
|
({ className, size = "md", variant = "default", children, ...props }, ref) => {
|
|
4691
4720
|
const { mode, resolvedMode, toggleMode } = useTheme();
|
|
4721
|
+
const nextResolvedMode = resolvedMode === "light" ? "dark" : "light";
|
|
4692
4722
|
const handleClick = (event) => {
|
|
4693
4723
|
const { clientX: x, clientY: y } = event;
|
|
4694
4724
|
toggleMode({ x, y });
|
|
@@ -4718,16 +4748,7 @@ var ThemeToggle = (0, import_react3.forwardRef)(
|
|
|
4718
4748
|
);
|
|
4719
4749
|
const renderIcon = () => {
|
|
4720
4750
|
if (children) return children;
|
|
4721
|
-
|
|
4722
|
-
case "light":
|
|
4723
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SunIcon, {});
|
|
4724
|
-
case "dark":
|
|
4725
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MoonIcon, {});
|
|
4726
|
-
case "system":
|
|
4727
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SystemIcon, {});
|
|
4728
|
-
default:
|
|
4729
|
-
return resolvedMode === "dark" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MoonIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SunIcon, {});
|
|
4730
|
-
}
|
|
4751
|
+
return nextResolvedMode === "dark" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MoonIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SunIcon, {});
|
|
4731
4752
|
};
|
|
4732
4753
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
4733
4754
|
"button",
|
|
@@ -4735,10 +4756,10 @@ var ThemeToggle = (0, import_react3.forwardRef)(
|
|
|
4735
4756
|
ref,
|
|
4736
4757
|
className: baseClasses,
|
|
4737
4758
|
onClick: handleClick,
|
|
4738
|
-
"data-mode":
|
|
4759
|
+
"data-mode": mode,
|
|
4739
4760
|
"data-theme": resolvedMode,
|
|
4740
|
-
"aria-label": `Switch to ${
|
|
4741
|
-
title: `Switch to ${
|
|
4761
|
+
"aria-label": `Switch to ${nextResolvedMode} mode`,
|
|
4762
|
+
title: `Switch to ${nextResolvedMode} mode`,
|
|
4742
4763
|
...props,
|
|
4743
4764
|
children: renderIcon()
|
|
4744
4765
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -3794,7 +3794,12 @@ function logValidationResult(result, context = "Custom presets") {
|
|
|
3794
3794
|
// src/components/UnifiedThemeScript.tsx
|
|
3795
3795
|
import { useMemo } from "react";
|
|
3796
3796
|
import { jsx } from "react/jsx-runtime";
|
|
3797
|
-
function ThemeScript({
|
|
3797
|
+
function ThemeScript({
|
|
3798
|
+
presetStorageKey = "theme-preset",
|
|
3799
|
+
modeStorageKey = "theme-engine-theme",
|
|
3800
|
+
defaultMode = "system",
|
|
3801
|
+
defaultPreset
|
|
3802
|
+
}) {
|
|
3798
3803
|
const defaultPresetData = useMemo(() => {
|
|
3799
3804
|
if (!defaultPreset) return null;
|
|
3800
3805
|
const preset = getPresetById(defaultPreset);
|
|
@@ -3806,10 +3811,12 @@ function ThemeScript({ presetStorageKey = "theme-preset", defaultPreset }) {
|
|
|
3806
3811
|
}, [defaultPreset]);
|
|
3807
3812
|
const scriptContent = useMemo(
|
|
3808
3813
|
() => `
|
|
3809
|
-
// Unified Theme Engine: Restore preset colors before hydration
|
|
3814
|
+
// Unified Theme Engine: Restore mode + preset colors before hydration
|
|
3810
3815
|
(function() {
|
|
3811
3816
|
try {
|
|
3812
3817
|
const presetStorageKey = "${presetStorageKey}";
|
|
3818
|
+
const modeStorageKey = "${modeStorageKey}";
|
|
3819
|
+
const defaultMode = "${defaultMode}";
|
|
3813
3820
|
const isDev = (function() {
|
|
3814
3821
|
try {
|
|
3815
3822
|
return location.hostname === 'localhost' || location.hostname === '127.0.0.1';
|
|
@@ -3817,6 +3824,39 @@ function ThemeScript({ presetStorageKey = "theme-preset", defaultPreset }) {
|
|
|
3817
3824
|
return false;
|
|
3818
3825
|
}
|
|
3819
3826
|
})();
|
|
3827
|
+
|
|
3828
|
+
// ---- Mode restoration (pre-hydration) ----
|
|
3829
|
+
(function() {
|
|
3830
|
+
try {
|
|
3831
|
+
const root = document.documentElement;
|
|
3832
|
+
let storedMode = null;
|
|
3833
|
+
try {
|
|
3834
|
+
storedMode = localStorage.getItem(modeStorageKey);
|
|
3835
|
+
} catch {}
|
|
3836
|
+
|
|
3837
|
+
const isValidMode = storedMode === 'light' || storedMode === 'dark' || storedMode === 'system';
|
|
3838
|
+
const mode = isValidMode ? storedMode : defaultMode;
|
|
3839
|
+
|
|
3840
|
+
let systemMode = 'light';
|
|
3841
|
+
try {
|
|
3842
|
+
systemMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
3843
|
+
} catch {}
|
|
3844
|
+
|
|
3845
|
+
const resolvedMode = mode === 'system' ? systemMode : mode;
|
|
3846
|
+
|
|
3847
|
+
root.classList.remove('light', 'dark');
|
|
3848
|
+
root.classList.add(resolvedMode);
|
|
3849
|
+
root.style.colorScheme = resolvedMode;
|
|
3850
|
+
|
|
3851
|
+
// Expose for runtime consumers (optional)
|
|
3852
|
+
try {
|
|
3853
|
+
root.dataset.themeEngineMode = mode;
|
|
3854
|
+
root.dataset.themeEngineResolvedMode = resolvedMode;
|
|
3855
|
+
} catch {}
|
|
3856
|
+
} catch (error) {
|
|
3857
|
+
if (isDev) console.warn('\u{1F3A8} UnifiedThemeScript: Mode restoration failed:', error);
|
|
3858
|
+
}
|
|
3859
|
+
})();
|
|
3820
3860
|
|
|
3821
3861
|
// CSS property categories (inline for script)
|
|
3822
3862
|
const CSS_CATEGORIES = {
|
|
@@ -4023,7 +4063,7 @@ function ThemeScript({ presetStorageKey = "theme-preset", defaultPreset }) {
|
|
|
4023
4063
|
|
|
4024
4064
|
}
|
|
4025
4065
|
|
|
4026
|
-
//
|
|
4066
|
+
// ---- Preset restoration (pre-hydration) ----
|
|
4027
4067
|
const storedPreset = localStorage.getItem(presetStorageKey);
|
|
4028
4068
|
let presetToApply = null;
|
|
4029
4069
|
|
|
@@ -4041,10 +4081,9 @@ function ThemeScript({ presetStorageKey = "theme-preset", defaultPreset }) {
|
|
|
4041
4081
|
}
|
|
4042
4082
|
|
|
4043
4083
|
if (presetToApply) {
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
const
|
|
4047
|
-
const mode = isDark ? 'dark' : 'light';
|
|
4084
|
+
const root = document.documentElement;
|
|
4085
|
+
const resolved = (root.dataset && root.dataset.themeEngineResolvedMode) || (root.classList.contains('dark') ? 'dark' : 'light');
|
|
4086
|
+
const mode = resolved === 'dark' ? 'dark' : 'light';
|
|
4048
4087
|
const colors = presetToApply.colors && presetToApply.colors[mode];
|
|
4049
4088
|
|
|
4050
4089
|
if (colors) {
|
|
@@ -4271,15 +4310,17 @@ function ThemeProvider({
|
|
|
4271
4310
|
}) {
|
|
4272
4311
|
const normalizedCustomPresets = useMemo2(() => customPresets ?? {}, [customPresets]);
|
|
4273
4312
|
const [mode, setMode] = useState(() => {
|
|
4274
|
-
|
|
4275
|
-
return stored || defaultMode;
|
|
4313
|
+
return defaultMode;
|
|
4276
4314
|
});
|
|
4277
4315
|
const [resolvedMode, setResolvedMode] = useState(() => {
|
|
4278
|
-
if (
|
|
4279
|
-
|
|
4280
|
-
}
|
|
4281
|
-
return mode;
|
|
4316
|
+
if (defaultMode === "dark") return "dark";
|
|
4317
|
+
return "light";
|
|
4282
4318
|
});
|
|
4319
|
+
useEffect(() => {
|
|
4320
|
+
if (typeof window === "undefined") return;
|
|
4321
|
+
const stored = getStoredMode(modeStorageKey);
|
|
4322
|
+
if (stored) setMode(stored);
|
|
4323
|
+
}, [modeStorageKey]);
|
|
4283
4324
|
const availablePresets = useMemo2(() => {
|
|
4284
4325
|
const merged = {};
|
|
4285
4326
|
Object.assign(merged, tweakcnPresets);
|
|
@@ -4547,7 +4588,15 @@ function ThemeProvider({
|
|
|
4547
4588
|
});
|
|
4548
4589
|
}
|
|
4549
4590
|
}, [presetStorageKey, defaultPreset, getAvailablePresetById, applyPresetColors, resolvedMode]);
|
|
4550
|
-
const scriptElement = /* @__PURE__ */ jsx2(
|
|
4591
|
+
const scriptElement = /* @__PURE__ */ jsx2(
|
|
4592
|
+
ThemeScript,
|
|
4593
|
+
{
|
|
4594
|
+
presetStorageKey,
|
|
4595
|
+
modeStorageKey,
|
|
4596
|
+
defaultMode,
|
|
4597
|
+
defaultPreset
|
|
4598
|
+
}
|
|
4599
|
+
);
|
|
4551
4600
|
const isUsingDefaultPreset = !!defaultPreset && currentPreset?.presetId === defaultPreset;
|
|
4552
4601
|
const contextValue = {
|
|
4553
4602
|
mode,
|
|
@@ -4622,29 +4671,10 @@ var MoonIcon = () => /* @__PURE__ */ jsx3(
|
|
|
4622
4671
|
children: /* @__PURE__ */ jsx3("path", { d: "m12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" })
|
|
4623
4672
|
}
|
|
4624
4673
|
);
|
|
4625
|
-
var SystemIcon = () => /* @__PURE__ */ jsxs2(
|
|
4626
|
-
"svg",
|
|
4627
|
-
{
|
|
4628
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
4629
|
-
width: "16",
|
|
4630
|
-
height: "16",
|
|
4631
|
-
viewBox: "0 0 24 24",
|
|
4632
|
-
fill: "none",
|
|
4633
|
-
stroke: "currentColor",
|
|
4634
|
-
strokeWidth: "2",
|
|
4635
|
-
strokeLinecap: "round",
|
|
4636
|
-
strokeLinejoin: "round",
|
|
4637
|
-
className: "theme-toggle-icon",
|
|
4638
|
-
children: [
|
|
4639
|
-
/* @__PURE__ */ jsx3("rect", { width: "20", height: "14", x: "2", y: "3", rx: "2" }),
|
|
4640
|
-
/* @__PURE__ */ jsx3("line", { x1: "8", x2: "16", y1: "21", y2: "21" }),
|
|
4641
|
-
/* @__PURE__ */ jsx3("line", { x1: "12", x2: "12", y1: "17", y2: "21" })
|
|
4642
|
-
]
|
|
4643
|
-
}
|
|
4644
|
-
);
|
|
4645
4674
|
var ThemeToggle = forwardRef(
|
|
4646
4675
|
({ className, size = "md", variant = "default", children, ...props }, ref) => {
|
|
4647
4676
|
const { mode, resolvedMode, toggleMode } = useTheme();
|
|
4677
|
+
const nextResolvedMode = resolvedMode === "light" ? "dark" : "light";
|
|
4648
4678
|
const handleClick = (event) => {
|
|
4649
4679
|
const { clientX: x, clientY: y } = event;
|
|
4650
4680
|
toggleMode({ x, y });
|
|
@@ -4674,16 +4704,7 @@ var ThemeToggle = forwardRef(
|
|
|
4674
4704
|
);
|
|
4675
4705
|
const renderIcon = () => {
|
|
4676
4706
|
if (children) return children;
|
|
4677
|
-
|
|
4678
|
-
case "light":
|
|
4679
|
-
return /* @__PURE__ */ jsx3(SunIcon, {});
|
|
4680
|
-
case "dark":
|
|
4681
|
-
return /* @__PURE__ */ jsx3(MoonIcon, {});
|
|
4682
|
-
case "system":
|
|
4683
|
-
return /* @__PURE__ */ jsx3(SystemIcon, {});
|
|
4684
|
-
default:
|
|
4685
|
-
return resolvedMode === "dark" ? /* @__PURE__ */ jsx3(MoonIcon, {}) : /* @__PURE__ */ jsx3(SunIcon, {});
|
|
4686
|
-
}
|
|
4707
|
+
return nextResolvedMode === "dark" ? /* @__PURE__ */ jsx3(MoonIcon, {}) : /* @__PURE__ */ jsx3(SunIcon, {});
|
|
4687
4708
|
};
|
|
4688
4709
|
return /* @__PURE__ */ jsx3(
|
|
4689
4710
|
"button",
|
|
@@ -4691,10 +4712,10 @@ var ThemeToggle = forwardRef(
|
|
|
4691
4712
|
ref,
|
|
4692
4713
|
className: baseClasses,
|
|
4693
4714
|
onClick: handleClick,
|
|
4694
|
-
"data-mode":
|
|
4715
|
+
"data-mode": mode,
|
|
4695
4716
|
"data-theme": resolvedMode,
|
|
4696
|
-
"aria-label": `Switch to ${
|
|
4697
|
-
title: `Switch to ${
|
|
4717
|
+
"aria-label": `Switch to ${nextResolvedMode} mode`,
|
|
4718
|
+
title: `Switch to ${nextResolvedMode} mode`,
|
|
4698
4719
|
...props,
|
|
4699
4720
|
children: renderIcon()
|
|
4700
4721
|
}
|
|
@@ -26,16 +26,17 @@
|
|
|
26
26
|
|
|
27
27
|
/* Icon transition for theme toggle */
|
|
28
28
|
.theme-toggle-icon {
|
|
29
|
+
display: block;
|
|
29
30
|
transition: all 0.3s ease-in-out;
|
|
30
31
|
color: hsl(var(--foreground));
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
.theme-toggle[data-theme="light"] .theme-toggle-icon {
|
|
34
|
-
rotate:
|
|
35
|
+
rotate: 180deg;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
.theme-toggle[data-theme="dark"] .theme-toggle-icon {
|
|
38
|
-
rotate:
|
|
39
|
+
rotate: 0deg;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/* ThemePresetButtons: lightweight CSS-based animations and hover affordances */
|
|
@@ -59,10 +60,7 @@
|
|
|
59
60
|
box-shadow: 0 1px 0 hsl(var(--border) / 0.35);
|
|
60
61
|
cursor: pointer;
|
|
61
62
|
|
|
62
|
-
transition:
|
|
63
|
-
transform 0.2s ease-out,
|
|
64
|
-
background-color 0.2s ease-out,
|
|
65
|
-
border-color 0.2s ease-out,
|
|
63
|
+
transition: transform 0.2s ease-out, background-color 0.2s ease-out, border-color 0.2s ease-out,
|
|
66
64
|
box-shadow 0.2s ease-out;
|
|
67
65
|
}
|
|
68
66
|
|
|
@@ -111,9 +109,7 @@
|
|
|
111
109
|
z-index: 20;
|
|
112
110
|
background: hsl(var(--card) / 0.95);
|
|
113
111
|
border-color: hsl(var(--primary) / 0.2);
|
|
114
|
-
box-shadow:
|
|
115
|
-
0 10px 25px hsl(0 0% 0% / 0.12),
|
|
116
|
-
0 0 0 1px hsl(var(--border) / 0.35);
|
|
112
|
+
box-shadow: 0 10px 25px hsl(0 0% 0% / 0.12), 0 0 0 1px hsl(var(--border) / 0.35);
|
|
117
113
|
}
|
|
118
114
|
|
|
119
115
|
/* Color boxes within preset buttons - fallback only */
|
|
@@ -192,7 +188,7 @@
|
|
|
192
188
|
.theme-preset-button {
|
|
193
189
|
min-width: 120px;
|
|
194
190
|
}
|
|
195
|
-
|
|
191
|
+
|
|
196
192
|
.color-palette-grid {
|
|
197
193
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
|
198
194
|
}
|
|
@@ -204,7 +200,7 @@
|
|
|
204
200
|
.theme-preset-button {
|
|
205
201
|
border-width: 2px;
|
|
206
202
|
}
|
|
207
|
-
|
|
203
|
+
|
|
208
204
|
.theme-color-box {
|
|
209
205
|
border-width: 2px;
|
|
210
206
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fakhrirafiki/theme-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.17",
|
|
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",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"type": "git",
|
|
38
38
|
"url": "git+https://github.com/fakhrirafiki/theme-engine.git"
|
|
39
39
|
},
|
|
40
|
-
"homepage": "https://
|
|
40
|
+
"homepage": "https://theme-engine-example.vercel.app/",
|
|
41
41
|
"bugs": {
|
|
42
42
|
"url": "https://github.com/fakhrirafiki/theme-engine/issues"
|
|
43
43
|
},
|