@primer/styled-react 0.0.0-20260326162136 → 0.0.0-20260326174604

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ThemeProvider.d.ts","sourceRoot":"","sources":["../../src/components/ThemeProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,eAAO,MAAM,gBAAgB,QAAQ,CAAA;AAKrC,MAAM,MAAM,KAAK,GAAG;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAC,CAAA;AACxC,KAAK,SAAS,GAAG,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;AACnD,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,MAAM,CAAA;AAElD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,SAAS,CAAC,EAAE,iBAAiB,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAC7B,CAAA;AA8CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,CAyE/E,CAAA;AAED,wBAAgB,QAAQ;YAtHd,KAAK;kBACC,MAAM;gBACR,iBAAiB;wBACT,SAAS;0BACP,MAAM;gBAChB,MAAM;kBACJ,MAAM;kBACN,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;kBACvD,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;oBAC1C,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;EA+G7D;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,UAG1F;AAqED,eAAe,aAAa,CAAA"}
1
+ {"version":3,"file":"ThemeProvider.d.ts","sourceRoot":"","sources":["../../src/components/ThemeProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,eAAO,MAAM,gBAAgB,QAAQ,CAAA;AAKrC,MAAM,MAAM,KAAK,GAAG;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAC,CAAA;AACxC,KAAK,SAAS,GAAG,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;AACnD,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,MAAM,CAAA;AAElD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,SAAS,CAAC,EAAE,iBAAiB,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAC7B,CAAA;AA8BD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,iBAAiB,CAAC,kBAAkB,CAAC,CAkF/E,CAAA;AAED,wBAAgB,QAAQ;YA/Gd,KAAK;kBACC,MAAM;gBACR,iBAAiB;wBACT,SAAS;0BACP,MAAM;gBAChB,MAAM;kBACJ,MAAM;kBACN,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;kBACvD,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;oBAC1C,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;EAwG7D;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,UAG1F;AAqGD,eAAe,aAAa,CAAA"}
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import ReactDOM from 'react-dom';
2
3
  import { ThemeProvider as ThemeProvider$1 } from 'styled-components';
3
4
  import { theme, useId, useSyncedState } from '@primer/react';
4
5
  import deepmerge from 'deepmerge';
@@ -17,27 +18,15 @@ const ThemeContext = /*#__PURE__*/React.createContext({
17
18
  });
18
19
 
19
20
  // inspired from __NEXT_DATA__, we use application/json to avoid CSRF policy with inline scripts
20
- const serverHandoffCache = new Map();
21
- const emptyHandoff = {};
22
21
  const getServerHandoff = id => {
23
- if (typeof document === 'undefined') return emptyHandoff;
24
- const cached = serverHandoffCache.get(id);
25
- if (cached !== undefined) return cached;
26
22
  try {
27
23
  const serverData = document.getElementById(`__PRIMER_DATA_${id}__`)?.textContent;
28
- if (serverData) {
29
- const parsed = JSON.parse(serverData);
30
- serverHandoffCache.set(id, parsed);
31
- return parsed;
32
- }
24
+ if (serverData) return JSON.parse(serverData);
33
25
  } catch (_error) {
34
26
  // if document/element does not exist or JSON is invalid, supress error
35
27
  }
36
- const empty = {};
37
- serverHandoffCache.set(id, empty);
38
- return empty;
28
+ return {};
39
29
  };
40
- const emptySubscribe = () => () => {};
41
30
  const ThemeProvider = ({
42
31
  children,
43
32
  ...props
@@ -53,33 +42,56 @@ const ThemeProvider = ({
53
42
  // Initialize state
54
43
  const theme$1 = props.theme ?? fallbackTheme ?? theme;
55
44
  const uniqueDataId = useId();
45
+ const {
46
+ resolvedServerColorMode
47
+ } = getServerHandoff(uniqueDataId);
48
+ const resolvedColorModePassthrough = React.useRef(resolvedServerColorMode);
56
49
  const [colorMode, setColorMode] = useSyncedState(props.colorMode ?? fallbackColorMode ?? defaultColorMode);
57
50
  const [dayScheme, setDayScheme] = useSyncedState(props.dayScheme ?? fallbackDayScheme ?? defaultDayScheme);
58
51
  const [nightScheme, setNightScheme] = useSyncedState(props.nightScheme ?? fallbackNightScheme ?? defaultNightScheme);
59
52
  const systemColorMode = useSystemColorMode();
60
- const clientColorMode = resolveColorMode(colorMode, systemColorMode);
61
- // During SSR/hydration, use the server-rendered color mode from the handoff script tag
62
- // to avoid mismatches. After hydration, resolve from client state.
63
- const resolvedColorMode = React.useSyncExternalStore(emptySubscribe, () => clientColorMode, () => getServerHandoff(uniqueDataId).resolvedServerColorMode ?? clientColorMode);
53
+ // eslint-disable-next-line react-hooks/refs
54
+ const resolvedColorMode = resolvedColorModePassthrough.current || resolveColorMode(colorMode, systemColorMode);
64
55
  const colorScheme = chooseColorScheme(resolvedColorMode, dayScheme, nightScheme);
65
56
  const {
66
57
  resolvedTheme,
67
58
  resolvedColorScheme
68
59
  } = React.useMemo(() => applyColorScheme(theme$1, colorScheme), [theme$1, colorScheme]);
69
- const contextValue = React.useMemo(() => ({
70
- theme: resolvedTheme,
71
- colorScheme,
72
- colorMode,
73
- resolvedColorMode,
74
- resolvedColorScheme,
75
- dayScheme,
76
- nightScheme,
77
- setColorMode,
78
- setDayScheme,
79
- setNightScheme
80
- }), [resolvedTheme, colorScheme, colorMode, resolvedColorMode, resolvedColorScheme, dayScheme, nightScheme, setColorMode, setDayScheme, setNightScheme]);
60
+
61
+ // this effect will only run on client
62
+ React.useEffect(function updateColorModeAfterServerPassthrough() {
63
+ const resolvedColorModeOnClient = resolveColorMode(colorMode, systemColorMode);
64
+ if (resolvedColorModePassthrough.current) {
65
+ // if the resolved color mode passed on from the server is not the resolved color mode on client, change it!
66
+ if (resolvedColorModePassthrough.current !== resolvedColorModeOnClient) {
67
+ window.setTimeout(() => {
68
+ // use ReactDOM.flushSync to prevent automatic batching of state updates since React 18
69
+ // ref: https://github.com/reactwg/react-18/discussions/21
70
+ ReactDOM.flushSync(() => {
71
+ // override colorMode to whatever is resolved on the client to get a re-render
72
+ setColorMode(resolvedColorModeOnClient);
73
+ });
74
+
75
+ // immediately after that, set the colorMode to what the user passed to respond to system color mode changes
76
+ setColorMode(colorMode);
77
+ });
78
+ }
79
+ resolvedColorModePassthrough.current = null;
80
+ }
81
+ }, [colorMode, systemColorMode, setColorMode]);
81
82
  return /*#__PURE__*/jsx(ThemeContext.Provider, {
82
- value: contextValue,
83
+ value: {
84
+ theme: resolvedTheme,
85
+ colorScheme,
86
+ colorMode,
87
+ resolvedColorMode,
88
+ resolvedColorScheme,
89
+ dayScheme,
90
+ nightScheme,
91
+ setColorMode,
92
+ setDayScheme,
93
+ setNightScheme
94
+ },
83
95
  children: /*#__PURE__*/jsxs(ThemeProvider$1, {
84
96
  theme: resolvedTheme,
85
97
  children: [children, props.preventSSRMismatch ? /*#__PURE__*/jsx("script", {
@@ -103,20 +115,48 @@ function useColorSchemeVar(values, fallback) {
103
115
  } = useTheme();
104
116
  return values[colorScheme] ?? fallback;
105
117
  }
106
- function subscribeToSystemColorMode(callback) {
107
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
108
- const media = window?.matchMedia?.('(prefers-color-scheme: dark)');
109
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
110
- media?.addEventListener('change', callback);
111
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
112
- return () => media?.removeEventListener('change', callback);
113
- }
114
118
  function useSystemColorMode() {
115
- return React.useSyncExternalStore(subscribeToSystemColorMode, getSystemColorMode, () => 'day');
119
+ const [systemColorMode, setSystemColorMode] = React.useState(getSystemColorMode);
120
+ React.useEffect(() => {
121
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
122
+ const media = window?.matchMedia?.('(prefers-color-scheme: dark)');
123
+ function matchesMediaToColorMode(matches) {
124
+ return matches ? 'night' : 'day';
125
+ }
126
+ function handleChange(event) {
127
+ const isNight = event.matches;
128
+ setSystemColorMode(matchesMediaToColorMode(isNight));
129
+ }
130
+
131
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
132
+ if (media) {
133
+ // just in case the preference changed before the event listener was attached
134
+ const isNight = media.matches;
135
+ setSystemColorMode(matchesMediaToColorMode(isNight));
136
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
137
+ if (media.addEventListener !== undefined) {
138
+ media.addEventListener('change', handleChange);
139
+ return function cleanup() {
140
+ media.removeEventListener('change', handleChange);
141
+ };
142
+ }
143
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
144
+ else if (media.addListener !== undefined) {
145
+ media.addListener(handleChange);
146
+ return function cleanup() {
147
+ media.removeListener(handleChange);
148
+ };
149
+ }
150
+ }
151
+ }, []);
152
+ return systemColorMode;
116
153
  }
117
154
  function getSystemColorMode() {
118
155
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
119
- return window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? 'night' : 'day';
156
+ if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)')?.matches) {
157
+ return 'night';
158
+ }
159
+ return 'day';
120
160
  }
121
161
  function resolveColorMode(colorMode, systemColorMode) {
122
162
  switch (colorMode) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primer/styled-react",
3
- "version": "0.0.0-20260326162136",
3
+ "version": "0.0.0-20260326174604",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -46,7 +46,7 @@
46
46
  "@babel/preset-react": "^7.28.5",
47
47
  "@babel/preset-typescript": "^7.28.5",
48
48
  "@primer/primitives": "10.x || 11.x",
49
- "@primer/react": "0.0.0-20260326162136",
49
+ "@primer/react": "0.0.0-20260326174604",
50
50
  "@rollup/plugin-babel": "^6.1.0",
51
51
  "@storybook/react-vite": "^10.1.11",
52
52
  "@types/react": "18.3.11",
@@ -67,7 +67,7 @@
67
67
  "typescript": "^5.9.2"
68
68
  },
69
69
  "peerDependencies": {
70
- "@primer/react": "0.0.0-20260326162136",
70
+ "@primer/react": "0.0.0-20260326174604",
71
71
  "@types/react": "18.x || 19.x",
72
72
  "@types/react-dom": "18.x || 19.x",
73
73
  "@types/react-is": "18.x || 19.x",