11import React from "react" ;
22import type { Preview } from "@storybook/react-vite" ;
3- import {
4- ThemeProvider ,
5- type ThemeMode ,
6- } from "../src/browser/contexts/ThemeContext" ;
3+ import { ThemeProvider , type ThemeMode } from "../src/browser/contexts/ThemeContext" ;
4+ import { getStorageChangeEvent } from "@/common/constants/events" ;
75import { UI_THEME_KEY } from "@/common/constants/storage" ;
86import "../src/browser/styles/globals.css" ;
97
10- const syncStorybookTheme = ( mode ? : ThemeMode ) => {
11- if ( typeof window === "undefined" || ! mode ) {
8+ const applyStorybookTheme = ( mode : ThemeMode ) => {
9+ if ( typeof window === "undefined" ) {
1210 return ;
1311 }
1412
1513 try {
16- window . localStorage . setItem ( UI_THEME_KEY , JSON . stringify ( mode ) ) ;
14+ const serialized = JSON . stringify ( mode ) ;
15+ window . localStorage . setItem ( UI_THEME_KEY , serialized ) ;
1716 const root = document . documentElement ;
1817 root . dataset . theme = mode ;
1918 root . style . colorScheme = mode ;
19+
20+ const event = new CustomEvent ( getStorageChangeEvent ( UI_THEME_KEY ) , {
21+ detail : { key : UI_THEME_KEY , newValue : mode , origin : "storybook" } ,
22+ } ) ;
23+ window . dispatchEvent ( event ) ;
2024 } catch ( error ) {
21- console . warn ( "Failed to sync Storybook theme:" , error ) ;
25+ console . warn ( "Failed to write Storybook theme:" , error ) ;
26+ }
27+ } ;
28+
29+ const syncStorybookTheme = ( mode ?: ThemeMode ) : ThemeMode => {
30+ if ( typeof window === "undefined" ) {
31+ return mode ?? "dark" ;
32+ }
33+
34+ const stored = window . localStorage . getItem ( UI_THEME_KEY ) ;
35+ const persisted = stored ? ( JSON . parse ( stored ) as ThemeMode ) : undefined ;
36+ const resolved = mode ?? persisted ?? "dark" ;
37+ applyStorybookTheme ( resolved ) ;
38+ return resolved ;
39+ } ;
40+
41+ const StorybookThemeToggle : React . FC < { initialTheme : ThemeMode } > = ( { initialTheme } ) => {
42+ const [ theme , setTheme ] = React . useState ( initialTheme ) ;
43+
44+ React . useEffect ( ( ) => {
45+ setTheme ( initialTheme ) ;
46+ } , [ initialTheme ] ) ;
47+
48+ const handleToggle = ( ) => {
49+ const next = theme === "dark" ? "light" : "dark" ;
50+ applyStorybookTheme ( next ) ;
51+ setTheme ( next ) ;
52+ } ;
53+
54+ if ( typeof window === "undefined" ) {
55+ return null ;
2256 }
57+
58+ return (
59+ < button
60+ onClick = { handleToggle }
61+ style = { {
62+ position : "fixed" ,
63+ bottom : "1rem" ,
64+ right : "1rem" ,
65+ zIndex : 9999 ,
66+ padding : "0.5rem 1rem" ,
67+ background : theme === "dark" ? "#f5f6f8" : "#1e1e1e" ,
68+ color : theme === "dark" ? "#1e1e1e" : "#f5f6f8" ,
69+ border : "1px solid #777" ,
70+ borderRadius : "4px" ,
71+ cursor : "pointer" ,
72+ fontFamily : "system-ui, sans-serif" ,
73+ fontSize : "12px" ,
74+ boxShadow : "0 2px 5px rgba(0,0,0,0.2)" ,
75+ } }
76+ >
77+ { theme === "dark" ? "☀️ Light" : "🌙 Dark" }
78+ </ button >
79+ ) ;
2380} ;
2481
2582const preview : Preview = {
@@ -40,10 +97,11 @@ const preview: Preview = {
4097 decorators : [
4198 ( Story , context ) => {
4299 const mode = context . globals . theme as ThemeMode | undefined ;
43- syncStorybookTheme ( mode ) ;
100+ const resolved = syncStorybookTheme ( mode ) ;
44101 return (
45102 < ThemeProvider >
46103 < Story />
104+ { ! mode && < StorybookThemeToggle initialTheme = { resolved } /> }
47105 </ ThemeProvider >
48106 ) ;
49107 } ,
0 commit comments