@fakhrirafiki/theme-engine 0.4.14 → 0.4.15

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/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
- * Simplified theme script that only handles preset restoration
470
- * Works in harmony with ThemeProvider for dark/light mode
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
- * Simplified theme script that only handles preset restoration
470
- * Works in harmony with ThemeProvider for dark/light mode
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({ presetStorageKey = "theme-preset", defaultPreset }) {
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
- // Load and apply persisted preset or default preset
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
- // Determine current mode (will be set by ThemeProvider)
4089
- // Default to light if no theme class is present yet
4090
- const isDark = document.documentElement.classList.contains('dark');
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
- const stored = getStoredMode(modeStorageKey);
4319
- return stored || defaultMode;
4357
+ return defaultMode;
4320
4358
  });
4321
4359
  const [resolvedMode, setResolvedMode] = (0, import_react2.useState)(() => {
4322
- if (mode === "system") {
4323
- return getSystemTheme();
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)(ThemeScript, { presetStorageKey, defaultPreset });
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,
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({ presetStorageKey = "theme-preset", defaultPreset }) {
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
- // Load and apply persisted preset or default preset
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
- // Determine current mode (will be set by ThemeProvider)
4045
- // Default to light if no theme class is present yet
4046
- const isDark = document.documentElement.classList.contains('dark');
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
- const stored = getStoredMode(modeStorageKey);
4275
- return stored || defaultMode;
4313
+ return defaultMode;
4276
4314
  });
4277
4315
  const [resolvedMode, setResolvedMode] = useState(() => {
4278
- if (mode === "system") {
4279
- return getSystemTheme();
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(ThemeScript, { presetStorageKey, defaultPreset });
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fakhrirafiki/theme-engine",
3
- "version": "0.4.14",
3
+ "version": "0.4.15",
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",