1- import React , { memo , useEffect , useState } from 'react' ;
1+ import React , { memo , useState } from 'react' ;
22import type { ThemeVars } from '@coinbase/cds-common/core/theme' ;
3- import { useTheme } from '@coinbase/cds-web' ;
4- import { Button , IconButton } from '@coinbase/cds-web/buttons' ;
5- import { Card , MessagingCard } from '@coinbase/cds-web/cards' ;
3+ import type { TabValue } from '@coinbase/cds-common/tabs/useTabs' ;
64import { Box , HStack , VStack } from '@coinbase/cds-web/layout' ;
7- import { Interactable } from '@coinbase/cds-web/system' ;
85import { ThemeProvider } from '@coinbase/cds-web/system/ThemeProvider' ;
96import { SegmentedTabs } from '@coinbase/cds-web/tabs' ;
10- import { Tag } from '@coinbase/cds-web/tag' ;
117import { Text } from '@coinbase/cds-web/typography' ;
12- import { LineChart , Scrubber , SolidLine } from '@coinbase/cds-web-visualization' ;
138import { useColorMode } from '@docusaurus/theme-common' ;
149
1510import { useDocsTheme } from '../../../theme/Layout/Provider/UnifiedThemeContext' ;
1611
17- import { aaTextColor } from './colorUtils ' ;
12+ import { PlaygroundContent } from './PlaygroundContent ' ;
1813import styles from './ResultCard.module.css' ;
19- import { darkSpectrum , lightSpectrum } from './tokens' ;
2014
21- const PLAYGROUND_TABS = [
22- { id : 'light' as const , label : 'Light' } ,
23- { id : 'dark' as const , label : 'Dark' } ,
15+ const PLAYGROUND_TABS : TabValue [ ] = [
16+ { id : 'light' , label : 'Light' } ,
17+ { id : 'dark' , label : 'Dark' } ,
2418] ;
2519
26- const CHART_DATA_PRIMARY = [ 10 , 22 , 29 , 45 , 98 , 45 , 22 , 52 , 21 , 4 , 68 , 20 , 21 , 58 ] ;
27- const CHART_DATA_SECONDARY = [ 5 , 18 , 35 , 28 , 55 , 70 , 48 , 62 , 38 , 15 , 42 , 55 , 30 , 45 ] ;
28-
29- // ── PlaygroundContent ──────────────────────────────────────────────────────────
30- // Must be rendered *inside* the ThemeProvider so useTheme() reads the nested
31- // theme and resolves spectrum tokens to actual RGB values for the selected mode.
32-
33- type PlaygroundContentProps = {
34- selectedToken : string ;
35- selectedHex : string ;
36- selectedMode : 'light' | 'dark' ;
37- imgSrc : string | null ;
38- } ;
39-
40- const PlaygroundContent = memo ( function PlaygroundContent ( {
41- selectedToken,
42- selectedHex,
43- selectedMode,
44- imgSrc,
45- } : PlaygroundContentProps ) {
46- const theme = useTheme ( ) ;
47-
48- // theme.spectrum[token] returns a comma-separated "r,g,b" string for the active
49- // color scheme. Wrap it with rgb() to produce a valid CSS color value.
50- const spectrumRgb = theme . spectrum [ selectedToken as ThemeVars . SpectrumColor ] ;
51- const pColor = spectrumRgb ? `rgb(${ spectrumRgb } )` : selectedHex ;
52-
53- const pText = aaTextColor ( selectedHex ) ;
54- const pButtonBg = pText ;
55- const pButtonText = aaTextColor ( pButtonBg ) ;
56-
57- const spectrum = selectedMode === 'light' ? lightSpectrum : darkSpectrum ;
58- const checkerSvg = `data:image/svg+xml,${ encodeURIComponent (
59- `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect width="20" height="20" fill="rgb(${ spectrum . gray15 } )"/><rect x="20" y="20" width="20" height="20" fill="rgb(${ spectrum . gray15 } )"/><rect x="20" width="20" height="20" fill="rgb(${ spectrum . gray10 } )"/><rect y="20" width="20" height="20" fill="rgb(${ spectrum . gray10 } )"/></svg>` ,
60- ) } `;
61-
62- return (
63- < Box
64- background = "bgAlternate"
65- borderRadius = { 200 }
66- padding = { 3 }
67- style = { { transition : 'background 0.2s ease' } }
68- >
69- < VStack gap = { 3 } width = "100%" >
70- < HStack alignItems = "center" flexWrap = "wrap" gap = { 2 } >
71- < Interactable
72- as = "button"
73- blendStyles = { { background : pColor , borderColor : pColor } }
74- borderRadius = { 1000 }
75- paddingX = { 3 }
76- paddingY = { 1 }
77- style = { { color : pText , border : 'none' , fontWeight : 600 , fontSize : 15 } }
78- >
79- Button
80- </ Interactable >
81- < IconButton
82- accessibilityLabel = "Add"
83- name = "add"
84- style = { { background : pColor , color : pText , borderColor : pColor } }
85- />
86- < Tag
87- emphasis = "high"
88- intent = "promotional"
89- style = { {
90- background : pColor ,
91- color : pText ,
92- [ '--cds-fg' as string ] : pText ,
93- [ '--cds-fgPrimary' as string ] : pText ,
94- } }
95- >
96- < span style = { { color : pText } } > Promo Tag</ span >
97- </ Tag >
98- </ HStack >
99-
100- < HStack
101- className = { styles . cardsRow }
102- gap = { 2 }
103- style = { { alignItems : 'stretch' , height : 200 } }
104- width = "100%"
105- >
106- < Box className = { styles . cardBox } display = "flex" style = { { flex : '1 1 0' , minWidth : 0 } } >
107- < MessagingCard
108- action = {
109- < Button
110- compact
111- onClick = { ( ) => { } }
112- style = { { background : pButtonBg , color : pButtonText , borderColor : pButtonBg } }
113- variant = "secondary"
114- >
115- < span className = { styles . ctaLabelFull } > Learn more</ span >
116- < span className = { styles . ctaLabelShort } > CTA</ span >
117- </ Button >
118- }
119- description = "Add up to 3 lines of body copy. Be concise."
120- media = {
121- < img
122- alt = ""
123- src = { imgSrc || checkerSvg }
124- style = { { width : '100%' , height : '100%' , objectFit : 'cover' } }
125- />
126- }
127- mediaPlacement = "end"
128- styles = { {
129- root : {
130- height : '100%' ,
131- background : pColor ,
132- [ '--color-fgInverse' as string ] : pText ,
133- [ '--color-bgPrimary' as string ] : pColor ,
134- } ,
135- contentContainer : {
136- justifyContent : 'space-between' ,
137- flex : '1 1 0' ,
138- minWidth : 0 ,
139- } ,
140- mediaContainer : { maxWidth : '45%' } ,
141- } }
142- title = "Title"
143- type = "upsell"
144- width = "100%"
145- />
146- </ Box >
147-
148- < Box className = { styles . cardBox } display = "flex" style = { { flex : '1 1 0' , minWidth : 0 } } >
149- < Card
150- background = "bgSecondary"
151- borderRadius = { 500 }
152- style = { { height : '100%' , width : '100%' } }
153- >
154- < VStack gap = { 1 } >
155- < Box padding = { 2 } paddingBottom = { 0 } >
156- < HStack alignItems = "center" gap = { 1 } >
157- < Box
158- borderRadius = { 1000 }
159- height = { 36 }
160- style = { { background : pColor , flexShrink : 0 , overflow : 'hidden' } }
161- width = { 36 }
162- >
163- { imgSrc && (
164- < img
165- alt = ""
166- src = { imgSrc }
167- style = { { width : '100%' , height : '100%' , objectFit : 'cover' } }
168- />
169- ) }
170- </ Box >
171- < VStack gap = { 0 } >
172- < Text color = "fgMuted" font = "legal" >
173- Subtitle
174- </ Text >
175- < HStack alignItems = "center" gap = { 0.5 } >
176- < Text color = "fg" font = "headline" >
177- Title
178- </ Text >
179- < Text color = "fgPositive" font = "label2" >
180- ↑ 25.25%
181- </ Text >
182- </ HStack >
183- </ VStack >
184- </ HStack >
185- </ Box >
186- < LineChart
187- enableScrubbing
188- height = { 110 }
189- series = { [
190- { id : 'primary' , data : CHART_DATA_PRIMARY , color : pColor } ,
191- { id : 'secondary' , data : CHART_DATA_SECONDARY , color : theme . color [ 'fg' ] } ,
192- ] }
193- xAxis = { { range : ( { min, max } ) => ( { min, max : max - 8 } ) } }
194- >
195- < Scrubber hideOverlay idlePulse LineComponent = { SolidLine } />
196- </ LineChart >
197- </ VStack >
198- </ Card >
199- </ Box >
200- </ HStack >
201- </ VStack >
202- </ Box >
203- ) ;
204- } ) ;
205-
206- // ── ComponentPlayground ────────────────────────────────────────────────────────
207- // Manages the light/dark tab state. Renders the header+tabs outside ThemeProvider
208- // (so they stay in the docs color mode), then wraps PlaygroundContent in a
209- // ThemeProvider scoped to the selected mode.
210-
21120type ComponentPlaygroundProps = {
21221 /** CDS spectrum token name for the light-mode brand color (e.g. "blue40"). */
213- lightToken : string ;
22+ lightToken : ThemeVars . SpectrumColor ;
21423 /** CDS spectrum token name for the dark-mode brand color (e.g. "blue50"). */
215- darkToken : string ;
24+ darkToken : ThemeVars . SpectrumColor ;
21625 /** Hex value of lightToken — used for contrast ratio calculation only. */
21726 lightHex : string ;
21827 /** Hex value of darkToken — used for contrast ratio calculation only. */
@@ -234,18 +43,19 @@ export const ComponentPlayground = memo(function ComponentPlayground({
23443 colorMode === 'light' ? 'light' : 'dark' ,
23544 ) ;
23645
237- // Sync the playground tab when the global docs color mode changes
238- useEffect ( ( ) => {
46+ const [ prevColorMode , setPrevColorMode ] = useState ( colorMode ) ;
47+ if ( prevColorMode !== colorMode ) {
48+ setPrevColorMode ( colorMode ) ;
23949 setSelectedMode ( colorMode === 'light' ? 'light' : 'dark' ) ;
240- } , [ colorMode ] ) ;
50+ }
24151
24252 const activeTab = PLAYGROUND_TABS . find ( ( t ) => t . id === selectedMode ) ?? PLAYGROUND_TABS [ 0 ] ;
24353
24454 const selectedToken = selectedMode === 'light' ? lightToken : darkToken ;
24555 const selectedHex = selectedMode === 'light' ? lightHex : darkHex ;
24656
24757 return (
248- < Box style = { { padding : '24px 32px 32px' } } width = "100%" >
58+ < Box padding = { 4 } paddingTop = { 3 } width = "100%" >
24959 < VStack gap = { 2 } width = "100%" >
25060 < HStack
25161 alignItems = "center"
@@ -256,7 +66,6 @@ export const ComponentPlayground = memo(function ComponentPlayground({
25666 < Text as = "h3" font = "title3" >
25767 Color match to components
25868 </ Text >
259- { /* SegmentedTabs outside ThemeProvider — stays in the docs color mode */ }
26069 < SegmentedTabs
26170 accessibilityLabel = "Switch light or dark mode preview"
26271 activeTab = { activeTab }
@@ -267,9 +76,6 @@ export const ComponentPlayground = memo(function ComponentPlayground({
26776 />
26877 </ HStack >
26978
270- { /* ThemeProvider scopes the selected color scheme to PlaygroundContent only.
271- PlaygroundContent calls useTheme() inside this boundary to resolve
272- spectrum tokens to actual rgb() values for the active mode. */ }
27379 < ThemeProvider activeColorScheme = { selectedMode } display = "contents" theme = { theme } >
27480 < PlaygroundContent
27581 imgSrc = { imgSrc }
0 commit comments