Skip to content

Commit 8d3b6f8

Browse files
committed
fix: generating theme tokens in panda spec output
1 parent 3ac138b commit 8d3b6f8

File tree

7 files changed

+531
-53
lines changed

7 files changed

+531
-53
lines changed

.changeset/spec-themes-support.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@pandacss/generator': patch
3+
'@pandacss/types': patch
4+
---
5+
6+
Add support for generating theme tokens in `panda spec` output.
7+
8+
Previously, tokens defined in the `themes` config were excluded from the spec output because they are registered as virtual tokens. Now, `panda spec` generates a `themes.json` file containing tokens and semantic tokens for each configured theme.
Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
import { createContext } from '@pandacss/fixture'
2+
import { describe, expect, test } from 'vitest'
3+
import { generateThemesSpec } from '../src/spec/themes'
4+
5+
describe('generateThemesSpec', () => {
6+
test('should return undefined when no themes are configured', () => {
7+
const ctx = createContext({
8+
eject: true,
9+
theme: {
10+
tokens: {
11+
colors: { red: { value: '#ff0000' } },
12+
},
13+
},
14+
})
15+
16+
expect(generateThemesSpec(ctx)).toBeUndefined()
17+
})
18+
19+
test('should generate spec for themes with tokens', () => {
20+
const ctx = createContext({
21+
eject: true,
22+
theme: {
23+
tokens: {
24+
colors: {
25+
red: { value: '#ff0000' },
26+
blue: { value: '#0000ff' },
27+
},
28+
},
29+
},
30+
themes: {
31+
dark: {
32+
tokens: {
33+
colors: {
34+
red: { value: '#cc0000' },
35+
blue: { value: '#0000cc' },
36+
},
37+
},
38+
},
39+
},
40+
})
41+
42+
const spec = generateThemesSpec(ctx)
43+
44+
expect(spec).toMatchInlineSnapshot(`
45+
{
46+
"data": [
47+
{
48+
"name": "dark",
49+
"semanticTokens": [],
50+
"tokens": [
51+
{
52+
"functionExamples": [
53+
"css({ color: 'red' })",
54+
],
55+
"jsxExamples": [
56+
"<Box color="red" />",
57+
],
58+
"tokenFunctionExamples": [
59+
"token('colors.red')",
60+
"token.var('colors.red')",
61+
],
62+
"type": "colors",
63+
"values": [
64+
{
65+
"cssVar": "var(--colors-red)",
66+
"deprecated": undefined,
67+
"description": undefined,
68+
"name": "red",
69+
"values": [
70+
{
71+
"condition": "dark",
72+
"value": "#cc0000",
73+
},
74+
],
75+
},
76+
{
77+
"cssVar": "var(--colors-blue)",
78+
"deprecated": undefined,
79+
"description": undefined,
80+
"name": "blue",
81+
"values": [
82+
{
83+
"condition": "dark",
84+
"value": "#0000cc",
85+
},
86+
],
87+
},
88+
],
89+
},
90+
],
91+
},
92+
],
93+
"type": "themes",
94+
}
95+
`)
96+
})
97+
98+
test('should generate spec for themes with semantic tokens', () => {
99+
const ctx = createContext({
100+
eject: true,
101+
theme: {
102+
tokens: {
103+
colors: {
104+
red: { value: '#ff0000' },
105+
blue: { value: '#0000ff' },
106+
},
107+
},
108+
semanticTokens: {
109+
colors: {
110+
primary: { value: '{colors.blue}' },
111+
},
112+
},
113+
},
114+
themes: {
115+
dark: {
116+
semanticTokens: {
117+
colors: {
118+
primary: { value: '{colors.red}' },
119+
},
120+
},
121+
},
122+
},
123+
})
124+
125+
const spec = generateThemesSpec(ctx)
126+
127+
expect(spec).toMatchInlineSnapshot(`
128+
{
129+
"data": [
130+
{
131+
"name": "dark",
132+
"semanticTokens": [
133+
{
134+
"functionExamples": [
135+
"css({ color: 'primary' })",
136+
],
137+
"jsxExamples": [
138+
"<Box color="primary" />",
139+
],
140+
"tokenFunctionExamples": [
141+
"token('colors.primary')",
142+
"token.var('colors.primary')",
143+
],
144+
"type": "colors",
145+
"values": [
146+
{
147+
"cssVar": "var(--colors-primary)",
148+
"deprecated": undefined,
149+
"description": undefined,
150+
"name": "primary",
151+
"values": [
152+
{
153+
"condition": "dark",
154+
"value": "{colors.red}",
155+
},
156+
],
157+
},
158+
],
159+
},
160+
],
161+
"tokens": [],
162+
},
163+
],
164+
"type": "themes",
165+
}
166+
`)
167+
})
168+
169+
test('should generate spec for multiple themes', () => {
170+
const ctx = createContext({
171+
eject: true,
172+
theme: {
173+
tokens: {
174+
colors: {
175+
red: { value: '#ff0000' },
176+
blue: { value: '#0000ff' },
177+
},
178+
},
179+
},
180+
themes: {
181+
dark: {
182+
tokens: {
183+
colors: {
184+
red: { value: '#cc0000' },
185+
},
186+
},
187+
},
188+
brand: {
189+
tokens: {
190+
colors: {
191+
red: { value: '#ee0000' },
192+
},
193+
},
194+
},
195+
},
196+
})
197+
198+
const spec = generateThemesSpec(ctx)
199+
200+
expect(spec!.data.map((t) => t.name)).toMatchInlineSnapshot(`
201+
[
202+
"dark",
203+
"brand",
204+
]
205+
`)
206+
207+
expect(spec!.data.find((t) => t.name === 'dark')!.tokens).toMatchInlineSnapshot(`
208+
[
209+
{
210+
"functionExamples": [
211+
"css({ color: 'red' })",
212+
],
213+
"jsxExamples": [
214+
"<Box color="red" />",
215+
],
216+
"tokenFunctionExamples": [
217+
"token('colors.red')",
218+
"token.var('colors.red')",
219+
],
220+
"type": "colors",
221+
"values": [
222+
{
223+
"cssVar": "var(--colors-red)",
224+
"deprecated": undefined,
225+
"description": undefined,
226+
"name": "red",
227+
"values": [
228+
{
229+
"condition": "dark",
230+
"value": "#cc0000",
231+
},
232+
],
233+
},
234+
],
235+
},
236+
]
237+
`)
238+
239+
expect(spec!.data.find((t) => t.name === 'brand')!.tokens).toMatchInlineSnapshot(`
240+
[
241+
{
242+
"functionExamples": [
243+
"css({ color: 'red' })",
244+
],
245+
"jsxExamples": [
246+
"<Box color="red" />",
247+
],
248+
"tokenFunctionExamples": [
249+
"token('colors.red')",
250+
"token.var('colors.red')",
251+
],
252+
"type": "colors",
253+
"values": [
254+
{
255+
"cssVar": "var(--colors-red)",
256+
"deprecated": undefined,
257+
"description": undefined,
258+
"name": "red",
259+
"values": [
260+
{
261+
"condition": "brand",
262+
"value": "#ee0000",
263+
},
264+
],
265+
},
266+
],
267+
},
268+
]
269+
`)
270+
})
271+
272+
test('should generate spec with both tokens and semantic tokens in a theme', () => {
273+
const ctx = createContext({
274+
eject: true,
275+
theme: {
276+
tokens: {
277+
colors: {
278+
red: { value: '#ff0000' },
279+
blue: { value: '#0000ff' },
280+
},
281+
},
282+
semanticTokens: {
283+
colors: {
284+
primary: { value: '{colors.blue}' },
285+
},
286+
},
287+
},
288+
themes: {
289+
dark: {
290+
tokens: {
291+
colors: {
292+
red: { value: '#cc0000' },
293+
},
294+
},
295+
semanticTokens: {
296+
colors: {
297+
primary: { value: '{colors.red}' },
298+
},
299+
},
300+
},
301+
},
302+
})
303+
304+
const spec = generateThemesSpec(ctx)
305+
const dark = spec!.data[0]
306+
307+
expect(dark.tokens.map((g) => ({ type: g.type, count: g.values.length }))).toMatchInlineSnapshot(`
308+
[
309+
{
310+
"count": 1,
311+
"type": "colors",
312+
},
313+
]
314+
`)
315+
316+
expect(dark.semanticTokens.map((g) => ({ type: g.type, count: g.values.length }))).toMatchInlineSnapshot(`
317+
[
318+
{
319+
"count": 1,
320+
"type": "colors",
321+
},
322+
]
323+
`)
324+
})
325+
326+
test('should be included in getSpec output', () => {
327+
const ctx = createContext({
328+
eject: true,
329+
theme: {
330+
tokens: {
331+
colors: {
332+
red: { value: '#ff0000' },
333+
},
334+
},
335+
},
336+
themes: {
337+
dark: {
338+
tokens: {
339+
colors: {
340+
red: { value: '#cc0000' },
341+
},
342+
},
343+
},
344+
},
345+
})
346+
347+
const specs = ctx.getSpec()
348+
const themesSpec = specs.find((s) => s.type === 'themes')
349+
expect(themesSpec!.type).toBe('themes')
350+
})
351+
})

packages/generator/src/generator.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { generateLayerStylesSpec } from './spec/layer-styles'
1818
import { generatePatternsSpec } from './spec/patterns'
1919
import { generateRecipesSpec } from './spec/recipes'
2020
import { generateTextStylesSpec } from './spec/text-styles'
21+
import { generateThemesSpec } from './spec/themes'
2122
import { generateSemanticTokensSpec, generateTokensSpec } from './spec/tokens'
2223

2324
export interface SplitCssArtifact {
@@ -223,12 +224,17 @@ export class Generator extends Context {
223224
specs.push(colorPaletteSpec)
224225
}
225226

227+
const themesSpec = generateThemesSpec(this)
228+
if (themesSpec) {
229+
specs.push(themesSpec)
230+
}
231+
226232
return specs
227233
}
228234

229235
getSpecOfType = <T extends SpecType>(
230236
type: T,
231-
): T extends 'color-palette' ? SpecTypeMap[T] | undefined : SpecTypeMap[T] => {
237+
): T extends 'color-palette' | 'themes' ? SpecTypeMap[T] | undefined : SpecTypeMap[T] => {
232238
const spec = (() => {
233239
switch (type) {
234240
case 'tokens':
@@ -251,8 +257,10 @@ export class Generator extends Context {
251257
return generateAnimationStylesSpec(this)
252258
case 'color-palette':
253259
return generateColorPaletteSpec(this) ?? undefined
260+
case 'themes':
261+
return generateThemesSpec(this) ?? undefined
254262
}
255263
})()
256-
return spec as T extends 'color-palette' ? SpecTypeMap[T] | undefined : SpecTypeMap[T]
264+
return spec as T extends 'color-palette' | 'themes' ? SpecTypeMap[T] | undefined : SpecTypeMap[T]
257265
}
258266
}

0 commit comments

Comments
 (0)