Skip to content

Commit 9af6a12

Browse files
committed
fix(studio): theme selector
1 parent 1bf8fa8 commit 9af6a12

26 files changed

+635
-167
lines changed

.changeset/studio-theme-tokens.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@pandacss/studio': patch
3+
---
4+
5+
### Fixed
6+
7+
- Fix semantic tokens defined in `defineTheme` not showing in Panda Studio. We now show a theme selector in the token
8+
pages for the theme-aware tokens.

packages/astro-plugin-studio/src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ function vitePlugin(configPath: string): PluginOption {
1111
let config: PandaContext['config']
1212
const textStyleMap: Record<string, string> = {}
1313
const layerStyleMap: Record<string, string> = {}
14+
const themesMap: Record<string, any> = {}
1415

1516
async function loadPandaConfig() {
1617
const ctx = await loadConfigAndCreateContext({ configPath })
@@ -22,6 +23,14 @@ function vitePlugin(configPath: string): PluginOption {
2223
const utility = ctx.utility.transform('layerStyle', key)
2324
layerStyleMap[key] = stringifyPanda(utility.styles)
2425
}
26+
27+
// Process themes if they exist
28+
if (ctx.config.themes) {
29+
for (const [themeName, themeConfig] of Object.entries(ctx.config.themes)) {
30+
themesMap[themeName] = themeConfig
31+
}
32+
}
33+
2534
config = ctx.config
2635
}
2736

@@ -52,6 +61,7 @@ function vitePlugin(configPath: string): PluginOption {
5261
`export const config = ${stringify(config)}`,
5362
`export const textStyles = ${stringify(textStyleMap)}`,
5463
`export const layerStyles = ${stringify(layerStyleMap)}`,
64+
`export const themes = ${stringify(themesMap)}`,
5565
].join('\n\n'),
5666
}
5767
}

packages/studio/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,22 @@
5555
"license": "MIT",
5656
"dependencies": {
5757
"@astrojs/react": "4.4.2",
58+
"@nanostores/react": "^1.0.0",
59+
"@pandacss/astro-plugin-studio": "workspace:*",
5860
"@pandacss/config": "workspace:*",
5961
"@pandacss/logger": "workspace:*",
6062
"@pandacss/shared": "workspace:*",
6163
"@pandacss/token-dictionary": "workspace:*",
6264
"@pandacss/types": "workspace:*",
63-
"@pandacss/astro-plugin-studio": "workspace:*",
6465
"astro": "5.16.2",
66+
"nanostores": "^1.1.0",
6567
"react": "19.2.0",
6668
"react-dom": "19.2.0",
6769
"vite": "7.2.4"
6870
},
6971
"devDependencies": {
72+
"@testing-library/react": "16.3.0",
7073
"@types/react": "19.2.7",
71-
"@types/react-dom": "19.2.3",
72-
"@testing-library/react": "16.3.0"
74+
"@types/react-dom": "19.2.3"
7375
}
7476
}

packages/studio/src/components/colors.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,13 @@ export function SemanticToken(props: SemanticTokenProps) {
6161
)
6262
}
6363

64-
export default function Colors() {
64+
interface ColorsProps {
65+
theme?: string
66+
}
67+
68+
export function Colors({ theme }: ColorsProps) {
6569
const { filterQuery, setFilterQuery, semanticTokens, hasResults, uncategorizedColors, categorizedColors } =
66-
useColorDocs()
70+
useColorDocs(theme)
6771

6872
return (
6973
<TokenGroup>

packages/studio/src/components/font-family.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import * as React from 'react'
22
import { Flex, HStack, Square, Stack, panda } from '../../styled-system/jsx'
3-
import * as context from '../lib/panda-context'
43
import { EmptyState } from './empty-state'
54
import { TypographyIcon } from './icons'
6-
7-
const fonts = context.getTokens('fonts')
5+
import type { useThemeTokens } from '../lib/use-theme-tokens'
86

97
const letters = Array.from({ length: 26 }, (_, i) => String.fromCharCode(65 + i))
108
const symbols = Array.from({ length: 10 }, (_, i) => String.fromCharCode(48 + i))
119
const specials = ['@', '#', '$', '%', '&', '!', '?', '+', '-']
1210

13-
export const FontFamily = () => {
11+
type Token = ReturnType<typeof useThemeTokens>[number]
12+
13+
interface FontFamilyProps {
14+
fonts: Token[]
15+
}
16+
17+
export function FontFamily({ fonts }: FontFamilyProps) {
1418
if (fonts.length === 0) {
1519
return (
1620
<EmptyState title="No Tokens" icon={<TypographyIcon />}>
Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,47 @@
11
import * as React from 'react'
22
import { toPx } from '@pandacss/shared'
33
import { Grid, panda, Stack } from '../../styled-system/jsx'
4-
import * as context from '../lib/panda-context'
54
import { getSortedSizes } from '../lib/sizes-sort'
65
import { TokenGroup } from './token-group'
6+
import { EmptyState } from './empty-state'
7+
import { SizesIcon } from './icons'
8+
import type { useThemeTokens } from '../lib/use-theme-tokens'
79

8-
const radii = context.getTokens('radii')
10+
type Token = ReturnType<typeof useThemeTokens>[number]
11+
12+
interface RadiiProps {
13+
radii: Token[]
14+
}
15+
16+
export function Radii({ radii }: RadiiProps) {
17+
if (radii.length === 0) {
18+
return (
19+
<EmptyState title="No Tokens" icon={<SizesIcon />}>
20+
The panda config does not contain any `radii` tokens
21+
</EmptyState>
22+
)
23+
}
924

10-
export default function Radii() {
1125
return (
1226
<TokenGroup>
13-
{radii && (
14-
<Grid display="grid" minChildWidth="10rem" gap="10">
15-
{getSortedSizes([...radii.values()])
16-
.sort((a, b) => parseFloat(toPx(a.value)!) - parseFloat(toPx(b.value)!))
17-
.map((size, index) => (
18-
<Stack direction="column" key={index}>
19-
<panda.div
20-
width="80px"
21-
height="80px"
22-
background="rgba(255, 192, 203, 0.5)"
23-
style={{ borderRadius: size.value }}
24-
/>
25-
<Stack gap="1">
26-
<b>{size.extensions.prop}</b>
27-
<panda.span opacity="0.7">{size.value}</panda.span>
28-
</Stack>
27+
<Grid display="grid" minChildWidth="10rem" gap="10">
28+
{getSortedSizes(radii)
29+
.sort((a, b) => parseFloat(toPx(a.value)!) - parseFloat(toPx(b.value)!))
30+
.map((size, index) => (
31+
<Stack direction="column" key={index}>
32+
<panda.div
33+
width="80px"
34+
height="80px"
35+
background="rgba(255, 192, 203, 0.5)"
36+
style={{ borderRadius: size.value }}
37+
/>
38+
<Stack gap="1">
39+
<b>{size.extensions.prop}</b>
40+
<panda.span opacity="0.7">{size.value}</panda.span>
2941
</Stack>
30-
))}
31-
</Grid>
32-
)}
42+
</Stack>
43+
))}
44+
</Grid>
3345
</TokenGroup>
3446
)
3547
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React, { useEffect, useState } from 'react'
2+
import { useStore } from '@nanostores/react'
3+
import { panda, Stack } from '../../styled-system/jsx'
4+
import { availableThemes } from '../lib/panda-context'
5+
import { currentThemeStore } from '../lib/theme-store'
6+
7+
const titleCase = (str: string) => str.replace(/[-_]/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase())
8+
9+
export function ThemeSelector() {
10+
const currentTheme = useStore(currentThemeStore)
11+
const [isHydrated, setIsHydrated] = useState(false)
12+
13+
useEffect(() => {
14+
setIsHydrated(true)
15+
}, [])
16+
17+
// Only show theme selector when themes are defined in the configuration
18+
if (availableThemes.length === 0) {
19+
return null
20+
}
21+
22+
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
23+
const value = e.target.value
24+
currentThemeStore.set(value || undefined)
25+
}
26+
27+
// Show a simple fallback until hydrated to prevent hydration mismatch
28+
if (!isHydrated) {
29+
return (
30+
<Stack gap="2">
31+
<panda.label fontWeight="bold" fontSize="small" opacity="0.7">
32+
THEME
33+
</panda.label>
34+
<panda.select
35+
px="3"
36+
py="2"
37+
borderRadius="md"
38+
borderWidth="1"
39+
borderColor="gray.200"
40+
bg="white"
41+
_dark={{ bg: 'gray.800', borderColor: 'gray.600' }}
42+
fontSize="sm"
43+
disabled
44+
>
45+
<option>Loading...</option>
46+
</panda.select>
47+
</Stack>
48+
)
49+
}
50+
51+
return (
52+
<Stack gap="2">
53+
<panda.label fontWeight="bold" fontSize="small" opacity="0.7">
54+
THEME
55+
</panda.label>
56+
<panda.select
57+
id="theme-selector"
58+
px="3"
59+
py="2"
60+
borderRadius="md"
61+
borderWidth="1"
62+
borderColor="gray.200"
63+
bg="white"
64+
_dark={{ bg: 'gray.800', borderColor: 'gray.600' }}
65+
fontSize="sm"
66+
cursor="pointer"
67+
value={currentTheme || ''}
68+
onChange={handleChange}
69+
>
70+
<option value="">Default</option>
71+
{availableThemes.map((theme) => (
72+
<option key={theme} value={theme}>
73+
{titleCase(theme)}
74+
</option>
75+
))}
76+
</panda.select>
77+
</Stack>
78+
)
79+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { useStore } from '@nanostores/react'
2+
import type { TokenDataTypes } from '@pandacss/types'
3+
import React, { useEffect, useState } from 'react'
4+
import { css } from '../../styled-system/css'
5+
import { Center } from '../../styled-system/jsx'
6+
import { availableThemes } from '../lib/panda-context'
7+
import { currentThemeStore } from '../lib/theme-store'
8+
import { useThemeTokens } from '../lib/use-theme-tokens'
9+
import { Colors } from './colors'
10+
import { FontFamily } from './font-family'
11+
import FontTokens from './font-tokens'
12+
import { Radii } from './radii'
13+
import Sizes from './sizes'
14+
15+
type TokenCategory = keyof TokenDataTypes
16+
17+
const hasThemes = availableThemes.length > 0
18+
19+
const loadingStyles = css({
20+
width: '40px',
21+
height: '40px',
22+
border: '2px solid #FFF',
23+
borderColor: 'yellow.400',
24+
borderBottomColor: 'transparent',
25+
borderRadius: '50%',
26+
display: 'inline-block',
27+
boxSizing: 'border-box',
28+
animation: 'spin 0.6s linear infinite',
29+
})
30+
31+
function Loader() {
32+
return (
33+
<Center height="400px">
34+
<span className={loadingStyles} />
35+
</Center>
36+
)
37+
}
38+
39+
function ThemeLoading({ children }: { children: React.ReactNode }) {
40+
const [isHydrated, setIsHydrated] = useState(false)
41+
useStore(currentThemeStore)
42+
43+
useEffect(() => {
44+
setIsHydrated(true)
45+
}, [])
46+
47+
if (!isHydrated) {
48+
return <Loader />
49+
}
50+
51+
return <>{children}</>
52+
}
53+
54+
function createTokenPage<T extends TokenCategory>(
55+
tokenType: T,
56+
render: (tokens: ReturnType<typeof useThemeTokens>) => React.ReactNode,
57+
) {
58+
return function TokenPage() {
59+
const tokens = useThemeTokens(tokenType)
60+
return hasThemes ? <ThemeLoading>{render(tokens)}</ThemeLoading> : <>{render(tokens)}</>
61+
}
62+
}
63+
64+
export const SizesPage = createTokenPage('sizes', (tokens) => <Sizes sizes={tokens} name="sizes" />)
65+
66+
export const SpacingPage = createTokenPage('spacing', (tokens) => <Sizes sizes={tokens} name="spacing" />)
67+
68+
export const FontSizesPage = createTokenPage('fontSizes', (tokens) => (
69+
<FontTokens fontTokens={tokens} token="fontSize" />
70+
))
71+
72+
export const FontWeightsPage = createTokenPage('fontWeights', (tokens) => (
73+
<FontTokens fontTokens={tokens} token="fontWeight" />
74+
))
75+
76+
export const LetterSpacingsPage = createTokenPage('letterSpacings', (tokens) => (
77+
<FontTokens fontTokens={tokens} token="letterSpacing" text="The quick brown fox jumps over the lazy dog." />
78+
))
79+
80+
export const LineHeightsPage = createTokenPage('lineHeights', (tokens) => (
81+
<FontTokens
82+
fontTokens={tokens}
83+
token="lineHeight"
84+
largeText
85+
text="Panda design system lineHeight specifies the vertical distance between two lines of text. You can preview this visually here."
86+
/>
87+
))
88+
89+
export const RadiiPage = createTokenPage('radii', (tokens) => <Radii radii={tokens} />)
90+
91+
export const FontsPage = createTokenPage('fonts', (tokens) => <FontFamily fonts={tokens} />)
92+
93+
export function ColorsPage() {
94+
const theme = hasThemes ? useStore(currentThemeStore) : undefined
95+
return hasThemes ? (
96+
<ThemeLoading>
97+
<Colors theme={theme} />
98+
</ThemeLoading>
99+
) : (
100+
<Colors />
101+
)
102+
}

packages/studio/src/layouts/Sidebar.astro

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Container, panda, Stack } from '../../styled-system/jsx'
33
import '../../styled-system/styles.css'
44
import SideNav from '../components/side-nav.astro'
55
import ThemeToggle from '../components/theme-toggle.astro'
6+
import { ThemeSelector } from '../components/theme-selector'
67
import { Logo } from '../icons/logo'
78
89
interface Props {
@@ -20,6 +21,9 @@ const { title } = Astro.props
2021
<panda.div mt="4">
2122
<ThemeToggle />
2223
</panda.div>
24+
<panda.div mt="4">
25+
<ThemeSelector client:load />
26+
</panda.div>
2327
<SideNav />
2428
</Stack>
2529

0 commit comments

Comments
 (0)