@open-mercato/ui 0.6.8-develop.6912.1.36161dc9ac → 0.6.8-develop.6914.1.6c7a5dcb6b

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,5 +1,5 @@
1
1
  "use client";
2
- import { Fragment, jsx } from "react/jsx-runtime";
2
+ import { jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { createContext, useContext } from "react";
5
5
  import { createLogger } from "@open-mercato/shared/lib/logger";
@@ -35,14 +35,12 @@ function applyTheme(resolvedTheme) {
35
35
  function ThemeProvider({ children }) {
36
36
  const [theme, setThemeState] = React.useState("system");
37
37
  const [resolvedTheme, setResolvedTheme] = React.useState("light");
38
- const [mounted, setMounted] = React.useState(false);
39
38
  React.useEffect(() => {
40
39
  const stored = getStoredTheme();
41
40
  setThemeState(stored);
42
41
  const resolved = stored === "system" ? getSystemTheme() : stored;
43
42
  setResolvedTheme(resolved);
44
43
  applyTheme(resolved);
45
- setMounted(true);
46
44
  }, []);
47
45
  React.useEffect(() => {
48
46
  if (typeof window === "undefined") return;
@@ -74,9 +72,6 @@ function ThemeProvider({ children }) {
74
72
  () => ({ theme, resolvedTheme, setTheme }),
75
73
  [theme, resolvedTheme, setTheme]
76
74
  );
77
- if (!mounted) {
78
- return /* @__PURE__ */ jsx(Fragment, { children });
79
- }
80
75
  return /* @__PURE__ */ jsx(ThemeContext.Provider, { value, children });
81
76
  }
82
77
  function useTheme() {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/theme/ThemeProvider.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { createContext, useContext } from 'react'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { THEME_STORAGE_KEY } from './theme-init-script'\n\nconst logger = createLogger('ui').child({ component: 'ThemeProvider' })\n\nexport type Theme = 'light' | 'dark' | 'system'\n\ntype ThemeContextValue = {\n theme: Theme\n resolvedTheme: 'light' | 'dark'\n setTheme: (theme: Theme) => void\n}\n\nconst ThemeContext = createContext<ThemeContextValue | undefined>(undefined)\n\nfunction getSystemTheme(): 'light' | 'dark' {\n if (typeof window === 'undefined') return 'light'\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n}\n\nfunction getStoredTheme(): Theme {\n if (typeof window === 'undefined') return 'system'\n try {\n const stored = localStorage.getItem(THEME_STORAGE_KEY)\n if (stored === 'light' || stored === 'dark' || stored === 'system') {\n return stored\n }\n } catch (error) {\n // localStorage may be unavailable in private browsing, iframes, or restricted contexts\n // Theme will default to system preference - this is expected graceful degradation\n if (process.env.NODE_ENV === 'development') {\n logger.warn('localStorage read failed', { err: error })\n }\n }\n return 'system'\n}\n\nfunction applyTheme(resolvedTheme: 'light' | 'dark') {\n const root = document.documentElement\n if (resolvedTheme === 'dark') {\n root.classList.add('dark')\n } else {\n root.classList.remove('dark')\n }\n}\n\nexport function ThemeProvider({ children }: { children: React.ReactNode }) {\n const [theme, setThemeState] = React.useState<Theme>('system')\n const [resolvedTheme, setResolvedTheme] = React.useState<'light' | 'dark'>('light')\n const [mounted, setMounted] = React.useState(false)\n\n // Initialize theme from localStorage on mount\n React.useEffect(() => {\n const stored = getStoredTheme()\n setThemeState(stored)\n const resolved = stored === 'system' ? getSystemTheme() : stored\n setResolvedTheme(resolved)\n applyTheme(resolved)\n setMounted(true)\n }, [])\n\n // Listen for system theme changes\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')\n const handleChange = () => {\n if (theme === 'system') {\n const newResolved = getSystemTheme()\n setResolvedTheme(newResolved)\n applyTheme(newResolved)\n }\n }\n\n mediaQuery.addEventListener('change', handleChange)\n return () => mediaQuery.removeEventListener('change', handleChange)\n }, [theme])\n\n const setTheme = React.useCallback((newTheme: Theme) => {\n setThemeState(newTheme)\n try {\n localStorage.setItem(THEME_STORAGE_KEY, newTheme)\n } catch (error) {\n // localStorage may be unavailable - theme still works for this session, just won't persist\n if (process.env.NODE_ENV === 'development') {\n logger.warn('localStorage write failed', { err: error })\n }\n }\n const resolved = newTheme === 'system' ? getSystemTheme() : newTheme\n setResolvedTheme(resolved)\n applyTheme(resolved)\n }, [])\n\n const value = React.useMemo(\n () => ({ theme, resolvedTheme, setTheme }),\n [theme, resolvedTheme, setTheme]\n )\n\n // Prevent flash of wrong theme during hydration\n if (!mounted) {\n return <>{children}</>\n }\n\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>\n}\n\nexport function useTheme(): ThemeContextValue {\n const context = useContext(ThemeContext)\n if (context === undefined) {\n // Return safe defaults when not in provider (e.g., server render)\n return {\n theme: 'system',\n resolvedTheme: 'light',\n setTheme: () => {},\n }\n }\n return context\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { createContext, useContext } from 'react'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { THEME_STORAGE_KEY } from './theme-init-script'\n\nconst logger = createLogger('ui').child({ component: 'ThemeProvider' })\n\nexport type Theme = 'light' | 'dark' | 'system'\n\ntype ThemeContextValue = {\n theme: Theme\n resolvedTheme: 'light' | 'dark'\n setTheme: (theme: Theme) => void\n}\n\nconst ThemeContext = createContext<ThemeContextValue | undefined>(undefined)\n\nfunction getSystemTheme(): 'light' | 'dark' {\n if (typeof window === 'undefined') return 'light'\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n}\n\nfunction getStoredTheme(): Theme {\n if (typeof window === 'undefined') return 'system'\n try {\n const stored = localStorage.getItem(THEME_STORAGE_KEY)\n if (stored === 'light' || stored === 'dark' || stored === 'system') {\n return stored\n }\n } catch (error) {\n // localStorage may be unavailable in private browsing, iframes, or restricted contexts\n // Theme will default to system preference - this is expected graceful degradation\n if (process.env.NODE_ENV === 'development') {\n logger.warn('localStorage read failed', { err: error })\n }\n }\n return 'system'\n}\n\nfunction applyTheme(resolvedTheme: 'light' | 'dark') {\n const root = document.documentElement\n if (resolvedTheme === 'dark') {\n root.classList.add('dark')\n } else {\n root.classList.remove('dark')\n }\n}\n\nexport function ThemeProvider({ children }: { children: React.ReactNode }) {\n const [theme, setThemeState] = React.useState<Theme>('system')\n const [resolvedTheme, setResolvedTheme] = React.useState<'light' | 'dark'>('light')\n\n // Initialize theme from localStorage on mount\n React.useEffect(() => {\n const stored = getStoredTheme()\n setThemeState(stored)\n const resolved = stored === 'system' ? getSystemTheme() : stored\n setResolvedTheme(resolved)\n applyTheme(resolved)\n }, [])\n\n // Listen for system theme changes\n React.useEffect(() => {\n if (typeof window === 'undefined') return\n\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')\n const handleChange = () => {\n if (theme === 'system') {\n const newResolved = getSystemTheme()\n setResolvedTheme(newResolved)\n applyTheme(newResolved)\n }\n }\n\n mediaQuery.addEventListener('change', handleChange)\n return () => mediaQuery.removeEventListener('change', handleChange)\n }, [theme])\n\n const setTheme = React.useCallback((newTheme: Theme) => {\n setThemeState(newTheme)\n try {\n localStorage.setItem(THEME_STORAGE_KEY, newTheme)\n } catch (error) {\n // localStorage may be unavailable - theme still works for this session, just won't persist\n if (process.env.NODE_ENV === 'development') {\n logger.warn('localStorage write failed', { err: error })\n }\n }\n const resolved = newTheme === 'system' ? getSystemTheme() : newTheme\n setResolvedTheme(resolved)\n applyTheme(resolved)\n }, [])\n\n const value = React.useMemo(\n () => ({ theme, resolvedTheme, setTheme }),\n [theme, resolvedTheme, setTheme]\n )\n\n // The element type rendered here must not depend on `mounted`. Swapping a\n // Fragment for the Provider once the first effect runs makes React unmount and\n // remount everything below this point, discarding the whole page's component\n // state. Flash of the wrong theme is prevented before first paint by\n // THEME_INIT_SCRIPT in the root layout, not by this branch.\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>\n}\n\nexport function useTheme(): ThemeContextValue {\n const context = useContext(ThemeContext)\n if (context === undefined) {\n // Return safe defaults when not in provider (e.g., server render)\n return {\n theme: 'system',\n resolvedTheme: 'light',\n setTheme: () => {},\n }\n }\n return context\n}\n"],
5
+ "mappings": ";AAyGS;AAvGT,YAAY,WAAW;AACvB,SAAS,eAAe,kBAAkB;AAC1C,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAElC,MAAM,SAAS,aAAa,IAAI,EAAE,MAAM,EAAE,WAAW,gBAAgB,CAAC;AAUtE,MAAM,eAAe,cAA6C,MAAS;AAE3E,SAAS,iBAAmC;AAC1C,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,SAAO,OAAO,WAAW,8BAA8B,EAAE,UAAU,SAAS;AAC9E;AAEA,SAAS,iBAAwB;AAC/B,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,UAAM,SAAS,aAAa,QAAQ,iBAAiB;AACrD,QAAI,WAAW,WAAW,WAAW,UAAU,WAAW,UAAU;AAClE,aAAO;AAAA,IACT;AAAA,EACF,SAAS,OAAO;AAGd,QAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,aAAO,KAAK,4BAA4B,EAAE,KAAK,MAAM,CAAC;AAAA,IACxD;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,WAAW,eAAiC;AACnD,QAAM,OAAO,SAAS;AACtB,MAAI,kBAAkB,QAAQ;AAC5B,SAAK,UAAU,IAAI,MAAM;AAAA,EAC3B,OAAO;AACL,SAAK,UAAU,OAAO,MAAM;AAAA,EAC9B;AACF;AAEO,SAAS,cAAc,EAAE,SAAS,GAAkC;AACzE,QAAM,CAAC,OAAO,aAAa,IAAI,MAAM,SAAgB,QAAQ;AAC7D,QAAM,CAAC,eAAe,gBAAgB,IAAI,MAAM,SAA2B,OAAO;AAGlF,QAAM,UAAU,MAAM;AACpB,UAAM,SAAS,eAAe;AAC9B,kBAAc,MAAM;AACpB,UAAM,WAAW,WAAW,WAAW,eAAe,IAAI;AAC1D,qBAAiB,QAAQ;AACzB,eAAW,QAAQ;AAAA,EACrB,GAAG,CAAC,CAAC;AAGL,QAAM,UAAU,MAAM;AACpB,QAAI,OAAO,WAAW,YAAa;AAEnC,UAAM,aAAa,OAAO,WAAW,8BAA8B;AACnE,UAAM,eAAe,MAAM;AACzB,UAAI,UAAU,UAAU;AACtB,cAAM,cAAc,eAAe;AACnC,yBAAiB,WAAW;AAC5B,mBAAW,WAAW;AAAA,MACxB;AAAA,IACF;AAEA,eAAW,iBAAiB,UAAU,YAAY;AAClD,WAAO,MAAM,WAAW,oBAAoB,UAAU,YAAY;AAAA,EACpE,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,WAAW,MAAM,YAAY,CAAC,aAAoB;AACtD,kBAAc,QAAQ;AACtB,QAAI;AACF,mBAAa,QAAQ,mBAAmB,QAAQ;AAAA,IAClD,SAAS,OAAO;AAEd,UAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,eAAO,KAAK,6BAA6B,EAAE,KAAK,MAAM,CAAC;AAAA,MACzD;AAAA,IACF;AACA,UAAM,WAAW,aAAa,WAAW,eAAe,IAAI;AAC5D,qBAAiB,QAAQ;AACzB,eAAW,QAAQ;AAAA,EACrB,GAAG,CAAC,CAAC;AAEL,QAAM,QAAQ,MAAM;AAAA,IAClB,OAAO,EAAE,OAAO,eAAe,SAAS;AAAA,IACxC,CAAC,OAAO,eAAe,QAAQ;AAAA,EACjC;AAOA,SAAO,oBAAC,aAAa,UAAb,EAAsB,OAAe,UAAS;AACxD;AAEO,SAAS,WAA8B;AAC5C,QAAM,UAAU,WAAW,YAAY;AACvC,MAAI,YAAY,QAAW;AAEzB,WAAO;AAAA,MACL,OAAO;AAAA,MACP,eAAe;AAAA,MACf,UAAU,MAAM;AAAA,MAAC;AAAA,IACnB;AAAA,EACF;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.6.8-develop.6912.1.36161dc9ac",
3
+ "version": "0.6.8-develop.6914.1.6c7a5dcb6b",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -155,14 +155,14 @@
155
155
  "remark-gfm": "^4.0.1"
156
156
  },
157
157
  "peerDependencies": {
158
- "@open-mercato/shared": "0.6.8-develop.6912.1.36161dc9ac",
158
+ "@open-mercato/shared": "0.6.8-develop.6914.1.6c7a5dcb6b",
159
159
  "react": ">=18.0.0",
160
160
  "react-dom": ">=18.0.0",
161
161
  "react-is": ">=18.0.0"
162
162
  },
163
163
  "devDependencies": {
164
164
  "@figma/code-connect": "^1.3.4",
165
- "@open-mercato/shared": "0.6.8-develop.6912.1.36161dc9ac",
165
+ "@open-mercato/shared": "0.6.8-develop.6914.1.6c7a5dcb6b",
166
166
  "@testing-library/dom": "^10.4.1",
167
167
  "@testing-library/jest-dom": "^7.0.0",
168
168
  "@testing-library/react": "^16.3.1",
@@ -51,7 +51,6 @@ function applyTheme(resolvedTheme: 'light' | 'dark') {
51
51
  export function ThemeProvider({ children }: { children: React.ReactNode }) {
52
52
  const [theme, setThemeState] = React.useState<Theme>('system')
53
53
  const [resolvedTheme, setResolvedTheme] = React.useState<'light' | 'dark'>('light')
54
- const [mounted, setMounted] = React.useState(false)
55
54
 
56
55
  // Initialize theme from localStorage on mount
57
56
  React.useEffect(() => {
@@ -60,7 +59,6 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
60
59
  const resolved = stored === 'system' ? getSystemTheme() : stored
61
60
  setResolvedTheme(resolved)
62
61
  applyTheme(resolved)
63
- setMounted(true)
64
62
  }, [])
65
63
 
66
64
  // Listen for system theme changes
@@ -100,11 +98,11 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
100
98
  [theme, resolvedTheme, setTheme]
101
99
  )
102
100
 
103
- // Prevent flash of wrong theme during hydration
104
- if (!mounted) {
105
- return <>{children}</>
106
- }
107
-
101
+ // The element type rendered here must not depend on `mounted`. Swapping a
102
+ // Fragment for the Provider once the first effect runs makes React unmount and
103
+ // remount everything below this point, discarding the whole page's component
104
+ // state. Flash of the wrong theme is prevented before first paint by
105
+ // THEME_INIT_SCRIPT in the root layout, not by this branch.
108
106
  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
109
107
  }
110
108
 
@@ -0,0 +1,105 @@
1
+ import * as React from 'react'
2
+ import { act, render, screen } from '@testing-library/react'
3
+ import { ThemeProvider, useTheme } from '../ThemeProvider'
4
+ import { THEME_STORAGE_KEY } from '../theme-init-script'
5
+
6
+ function mockSystemTheme(prefersDark: boolean) {
7
+ Object.defineProperty(window, 'matchMedia', {
8
+ writable: true,
9
+ configurable: true,
10
+ value: (query: string) => ({
11
+ matches: query.includes('prefers-color-scheme: dark') ? prefersDark : false,
12
+ media: query,
13
+ addEventListener: () => {},
14
+ removeEventListener: () => {},
15
+ }),
16
+ })
17
+ }
18
+
19
+ let mountCount = 0
20
+
21
+ function MountCounter() {
22
+ React.useEffect(() => {
23
+ mountCount += 1
24
+ }, [])
25
+ return <div data-testid="child" />
26
+ }
27
+
28
+ function ThemeReadout() {
29
+ const { theme, resolvedTheme, setTheme } = useTheme()
30
+ return (
31
+ <div>
32
+ <span data-testid="theme">{theme}</span>
33
+ <span data-testid="resolved">{resolvedTheme}</span>
34
+ <button type="button" onClick={() => setTheme('dark')}>
35
+ go dark
36
+ </button>
37
+ </div>
38
+ )
39
+ }
40
+
41
+ describe('ThemeProvider', () => {
42
+ beforeEach(() => {
43
+ mountCount = 0
44
+ document.documentElement.classList.remove('dark')
45
+ window.localStorage.clear()
46
+ mockSystemTheme(false)
47
+ })
48
+
49
+ it('does not remount its subtree when the theme initializes', () => {
50
+ render(
51
+ <ThemeProvider>
52
+ <MountCounter />
53
+ </ThemeProvider>,
54
+ )
55
+
56
+ expect(screen.getByTestId('child')).toBeTruthy()
57
+ // A `mounted` gate that swapped a Fragment for the context Provider used to
58
+ // tear this subtree down and rebuild it, losing every child's state.
59
+ expect(mountCount).toBe(1)
60
+ })
61
+
62
+ it('keeps children mounted across a theme change', () => {
63
+ window.localStorage.setItem(THEME_STORAGE_KEY, 'light')
64
+ render(
65
+ <ThemeProvider>
66
+ <MountCounter />
67
+ <ThemeReadout />
68
+ </ThemeProvider>,
69
+ )
70
+
71
+ act(() => {
72
+ screen.getByRole('button', { name: 'go dark' }).click()
73
+ })
74
+
75
+ expect(screen.getByTestId('theme').textContent).toBe('dark')
76
+ expect(mountCount).toBe(1)
77
+ })
78
+
79
+ it('exposes the stored theme to consumers after initialization', () => {
80
+ window.localStorage.setItem(THEME_STORAGE_KEY, 'dark')
81
+
82
+ render(
83
+ <ThemeProvider>
84
+ <ThemeReadout />
85
+ </ThemeProvider>,
86
+ )
87
+
88
+ expect(screen.getByTestId('theme').textContent).toBe('dark')
89
+ expect(screen.getByTestId('resolved').textContent).toBe('dark')
90
+ expect(document.documentElement.classList.contains('dark')).toBe(true)
91
+ })
92
+
93
+ it('resolves the system preference when nothing is stored', () => {
94
+ mockSystemTheme(true)
95
+
96
+ render(
97
+ <ThemeProvider>
98
+ <ThemeReadout />
99
+ </ThemeProvider>,
100
+ )
101
+
102
+ expect(screen.getByTestId('theme').textContent).toBe('system')
103
+ expect(screen.getByTestId('resolved').textContent).toBe('dark')
104
+ })
105
+ })