@djangocfg/ui-core 2.1.550 → 2.1.555

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-core",
3
- "version": "2.1.550",
3
+ "version": "2.1.555",
4
4
  "description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -130,7 +130,7 @@
130
130
  "check:contrast": "node scripts/check-preset-contrast.mjs"
131
131
  },
132
132
  "peerDependencies": {
133
- "@djangocfg/i18n": "^2.1.550",
133
+ "@djangocfg/i18n": "^2.1.555",
134
134
  "consola": "^3.4.2",
135
135
  "lucide-react": "^0.545.0",
136
136
  "moment": "^2.30.1",
@@ -206,9 +206,9 @@
206
206
  "vaul": "1.1.2"
207
207
  },
208
208
  "devDependencies": {
209
- "@djangocfg/eslint-config": "^2.1.550",
210
- "@djangocfg/i18n": "^2.1.550",
211
- "@djangocfg/typescript-config": "^2.1.550",
209
+ "@djangocfg/eslint-config": "^2.1.555",
210
+ "@djangocfg/i18n": "^2.1.555",
211
+ "@djangocfg/typescript-config": "^2.1.555",
212
212
  "@storybook/react-vite": "^10.5.0",
213
213
  "@types/node": "^24.13.3",
214
214
  "@types/react": "19.2.15",
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useState } from 'react';
3
+ import { useCallback, useSyncExternalStore } from 'react';
4
4
 
5
5
  /**
6
6
  * Tailwind v4 default breakpoints (rem → px at 16px base).
@@ -23,29 +23,44 @@ export type Breakpoint = keyof typeof BREAKPOINTS
23
23
  /**
24
24
  * Reactive media query hook.
25
25
  *
26
+ * WHY useSyncExternalStore:
27
+ * A media query has no server value, so the hydration render MUST return the
28
+ * same `false` the server rendered — reading `matchMedia` there makes React
29
+ * throw "server rendered HTML didn't match the client" for any component that
30
+ * branches on the result (a narrow-viewport `<Sidebar>` renders its drawer
31
+ * branch against the server's desktop markup). `getServerSnapshot` is the
32
+ * only reliable way to pin that first render: React calls it during hydration
33
+ * as well as on the server, then re-renders with the real match after mount.
34
+ * A lazy `useState` initializer cannot express that distinction.
35
+ *
36
+ * Consequence to design around: the first painted frame is always the
37
+ * `false` branch. Components that must not flash should let CSS decide
38
+ * (Tailwind `md:` variants) rather than branch in JS.
39
+ *
26
40
  * @example
27
41
  * const isPhone = useMediaQuery('(max-width: 639px)')
28
42
  * const isDark = useMediaQuery('(prefers-color-scheme: dark)')
29
43
  * const isLandscape = useMediaQuery('(orientation: landscape)')
30
44
  */
31
45
  export function useMediaQuery(query: string): boolean {
32
- // Lazy initializer: read the real match on first render in the browser
33
- // so we never paint a "desktop" frame on a phone (and vice-versa) before
34
- // the first effect runs. SSR / non-browser → false.
35
- const [matches, setMatches] = useState<boolean>(() => {
36
- if (typeof window === 'undefined') return false
37
- return window.matchMedia(query).matches
38
- })
46
+ const subscribe = useCallback(
47
+ (onChange: () => void) => {
48
+ if (typeof window === 'undefined' || !window.matchMedia) return () => {}
49
+ const mediaQuery = window.matchMedia(query)
50
+ mediaQuery.addEventListener('change', onChange)
51
+ return () => mediaQuery.removeEventListener('change', onChange)
52
+ },
53
+ [query]
54
+ )
39
55
 
40
- useEffect(() => {
41
- const mediaQuery = window.matchMedia(query)
42
- // Re-sync once on mount in case the lazy initializer was skipped
43
- // (e.g. hydration mismatch where SSR gave false).
44
- setMatches(mediaQuery.matches)
45
- const handler = (event: MediaQueryListEvent) => setMatches(event.matches)
46
- mediaQuery.addEventListener('change', handler)
47
- return () => mediaQuery.removeEventListener('change', handler)
56
+ const getSnapshot = useCallback(() => {
57
+ if (typeof window === 'undefined' || !window.matchMedia) return false
58
+ return window.matchMedia(query).matches
48
59
  }, [query])
49
60
 
50
- return matches
61
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
62
+ }
63
+
64
+ function getServerSnapshot(): boolean {
65
+ return false
51
66
  }