@scalar/use-hooks 0.4.5 → 0.4.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @scalar/use-hooks
2
2
 
3
+ ## 0.4.7
4
+
5
+ ### Patch Changes
6
+
7
+ - [#9540](https://github.com/scalar/scalar/pull/9540): Fix an SSR hydration mismatch in the color-mode toggle: the system preference is now resolved after mount so the first client render matches the server.
8
+
9
+ ## 0.4.6
10
+
3
11
  ## 0.4.5
4
12
 
5
13
  ### Patch Changes
@@ -9,7 +9,7 @@ import { type StyleValue } from 'vue';
9
9
  *
10
10
  * @example
11
11
  * <script setup>
12
- * import { useBindCx, cva } from '@scalar/components'
12
+ * import { useBindCx, cva } from '@scalar/use-hooks/useBindCx'
13
13
  *
14
14
  * defineProps<{ active?: boolean }>()
15
15
  *
@@ -9,7 +9,7 @@ import { cx } from './cva.js';
9
9
  *
10
10
  * @example
11
11
  * <script setup>
12
- * import { useBindCx, cva } from '@scalar/components'
12
+ * import { useBindCx, cva } from '@scalar/use-hooks/useBindCx'
13
13
  *
14
14
  * defineProps<{ active?: boolean }>()
15
15
  *
@@ -1 +1 @@
1
- {"version":3,"file":"useColorMode.d.ts","sourceRoot":"","sources":["../../src/useColorMode/useColorMode.ts"],"names":[],"mappings":"AAOA,2BAA2B;AAC3B,KAAK,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAA;AAE5C,+BAA+B;AAC/B,KAAK,aAAa,GAAG,OAAO,GAAG,MAAM,CAAA;AAErC;;GAEG;AACH,wBAAgB,YAAY,CAC1B,IAAI,GAAE;IACJ,oCAAoC;IACpC,gBAAgB,CAAC,EAAE,SAAS,CAAA;IAC5B,8BAA8B;IAC9B,iBAAiB,CAAC,EAAE,SAAS,CAAA;CACzB;IA6FJ,yCAAyC;;IAKzC,+CAA+C;;IAE/C,yDAAyD;;IAEzD,qDAAqD;;IAErD,kDAAkD;0BAvFvB,SAAS;IAyFpC,uCAAuC;mCAhFL,aAAa;EAmFlD"}
1
+ {"version":3,"file":"useColorMode.d.ts","sourceRoot":"","sources":["../../src/useColorMode/useColorMode.ts"],"names":[],"mappings":"AAmBA,2BAA2B;AAC3B,KAAK,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAA;AAE5C,+BAA+B;AAC/B,KAAK,aAAa,GAAG,OAAO,GAAG,MAAM,CAAA;AAErC;;GAEG;AACH,wBAAgB,YAAY,CAC1B,IAAI,GAAE;IACJ,oCAAoC;IACpC,gBAAgB,CAAC,EAAE,SAAS,CAAA;IAC5B,8BAA8B;IAC9B,iBAAiB,CAAC,EAAE,SAAS,CAAA;CACzB;IAsGJ,yCAAyC;;IAKzC,+CAA+C;;IAE/C,yDAAyD;;IAEzD,qDAAqD;;IAErD,kDAAkD;0BAhGvB,SAAS;IAkGpC,uCAAuC;mCAzFL,aAAa;EA4FlD"}
@@ -1,6 +1,17 @@
1
1
  import { literal, union, validate } from '@scalar/validation';
2
2
  import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
3
3
  const colorMode = ref('dark');
4
+ /**
5
+ * Reactive snapshot of the system preference, shared across every `useColorMode` instance.
6
+ *
7
+ * It defaults to `'light'` so the first client render matches the server, where
8
+ * `window`/`matchMedia` do not exist. We resolve the real value in `onMounted` to avoid a
9
+ * hydration mismatch in anything bound to the color mode (for example the dark-mode toggle).
10
+ *
11
+ * It lives at module scope (like `colorMode`) so all instances agree on a single value instead
12
+ * of each holding its own copy that resolves at a different mount time.
13
+ */
14
+ const systemPreference = ref('light');
4
15
  const colorModeSchema = union([literal('system'), literal('dark'), literal('light')]);
5
16
  /**
6
17
  * A composable hook that provides color mode (dark/light) functionality.
@@ -37,7 +48,7 @@ export function useColorMode(opts = {}) {
37
48
  }
38
49
  /** Writable computed ref for dark/light mode with system preference applied */
39
50
  const darkLightMode = computed({
40
- get: () => (colorMode.value === 'system' ? getSystemModePreference() : colorMode.value),
51
+ get: () => (colorMode.value === 'system' ? systemPreference.value : colorMode.value),
41
52
  set: setColorMode,
42
53
  });
43
54
  /** Writable computed ref for whether the current color mode is dark */
@@ -46,11 +57,13 @@ export function useColorMode(opts = {}) {
46
57
  set: (value) => setColorMode(value ? 'dark' : 'light'),
47
58
  });
48
59
  /** Applies the appropriate color mode class to the body. */
49
- function applyColorMode(mode) {
60
+ function applyColorMode() {
50
61
  if (typeof document === 'undefined' || typeof window === 'undefined') {
51
62
  return;
52
63
  }
53
- const classMode = overrideColorMode ?? (mode === 'system' ? getSystemModePreference() : mode);
64
+ // Read from the deferred `systemPreference` ref (not `getSystemModePreference()` directly) so
65
+ // the body class agrees with `darkLightMode`/the toggle before `onMounted` resolves the real value.
66
+ const classMode = overrideColorMode ?? (colorMode.value === 'system' ? systemPreference.value : colorMode.value);
54
67
  if (classMode === 'dark') {
55
68
  document.body.classList.add('dark-mode');
56
69
  document.body.classList.remove('light-mode');
@@ -66,12 +79,18 @@ export function useColorMode(opts = {}) {
66
79
  const storedValue = typeof window === 'undefined' ? 'system' : window?.localStorage?.getItem('colorMode');
67
80
  const savedColorMode = validate(colorModeSchema, storedValue) ? storedValue : null;
68
81
  colorMode.value = overrideColorMode ?? savedColorMode ?? initialColorMode;
69
- // Watch for colorMode changes and update the body class
70
- watch(colorMode, applyColorMode, { immediate: true });
71
- const handleChange = () => colorMode.value === 'system' && applyColorMode('system');
82
+ // Watch for color mode or system preference changes and update the body class. Watching
83
+ // `systemPreference` means resolving it in `onMounted` (or an OS theme change) re-applies the class.
84
+ watch([colorMode, systemPreference], applyColorMode, { immediate: true });
85
+ const handleChange = () => {
86
+ systemPreference.value = getSystemModePreference();
87
+ };
72
88
  const mediaQuery = ref(null);
73
89
  // Listen to system preference changes
74
90
  onMounted(() => {
91
+ // Now that we are on the client, resolve the real system preference so the
92
+ // color mode updates after hydration instead of mismatching the server.
93
+ systemPreference.value = getSystemModePreference();
75
94
  if (typeof window !== 'undefined' && typeof window?.matchMedia === 'function') {
76
95
  mediaQuery.value = window.matchMedia('(prefers-color-scheme: dark)');
77
96
  mediaQuery.value?.addEventListener('change', handleChange);
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "url": "git+https://github.com/scalar/scalar.git",
11
11
  "directory": "packages/use-hooks"
12
12
  },
13
- "version": "0.4.5",
13
+ "version": "0.4.7",
14
14
  "engines": {
15
15
  "node": ">=22"
16
16
  },
@@ -47,7 +47,7 @@
47
47
  "tailwind-merge": "3.5.0",
48
48
  "vue": "^3.5.30",
49
49
  "@scalar/use-toasts": "0.10.2",
50
- "@scalar/validation": "0.5.0"
50
+ "@scalar/validation": "0.6.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@vue/test-utils": "2.4.6",