Skip to content

Commit b3857ab

Browse files
committed
refactor: spec command
1 parent c00f85a commit b3857ab

File tree

9 files changed

+143
-128
lines changed

9 files changed

+143
-128
lines changed

.changeset/add-spec-command.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
'@pandacss/generator': minor
44
'@pandacss/node': minor
55
'@pandacss/types': minor
6+
'@pandacss/cli': patch
67
---
78

89
Add `panda spec` command to generate specification files for your theme (useful for documentation). This command
@@ -12,17 +13,28 @@ generates JSON specification files containing metadata, examples, and usage info
1213
# Generate all spec files
1314
panda spec
1415

15-
# Generate with filter (filters across all spec types)
16-
panda spec --filter "button*"
17-
1816
# Custom output directory
1917
panda spec --outdir custom/specs
18+
```
19+
20+
**Token Spec Structure:**
2021

21-
# Include spec entrypoint in package.json
22-
panda emit-pkg --spec
22+
```json
23+
{
24+
"type": "tokens",
25+
"data": [
26+
{
27+
"type": "aspectRatios",
28+
"values": [{ "name": "square", "value": "1 / 1", "cssVar": "var(--aspect-ratios-square)" }],
29+
"tokenFunctionExamples": ["token('aspectRatios.square')"],
30+
"functionExamples": ["css({ aspectRatio: 'square' })"],
31+
"jsxExamples": ["<Box aspectRatio=\"square\" />"]
32+
}
33+
]
34+
}
2335
```
2436

25-
Spec files can be consumed via:
37+
**Spec Usage:**
2638

2739
```javascript
2840
import tokens from 'styled-system/specs/tokens'

packages/cli/src/cli-main.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,10 @@ export async function main() {
312312
.command('spec', 'Generate spec files for your theme (useful for documentation)')
313313
.option('--silent', "Don't print any logs")
314314
.option('--outdir <dir>', 'Output directory for spec files')
315-
.option('--filter <pattern>', 'Filter specifications by name/pattern')
316315
.option('-c, --config <path>', 'Path to panda config file')
317316
.option('--cwd <cwd>', 'Current working directory', { default: cwd })
318317
.action(async (flags: SpecCommandFlags) => {
319-
const { silent, config: configPath, outdir, filter } = flags
318+
const { silent, config: configPath, outdir } = flags
320319
const cwd = resolve(flags.cwd ?? '')
321320

322321
if (silent) {
@@ -329,10 +328,7 @@ export async function main() {
329328
config: { cwd },
330329
})
331330

332-
await spec(ctx, {
333-
outdir,
334-
filter,
335-
})
331+
await spec(ctx, { outdir })
336332
})
337333

338334
cli

packages/cli/src/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,4 @@ export interface SpecCommandFlags {
103103
outdir?: string
104104
cwd?: string
105105
config?: string
106-
filter?: string
107106
}

packages/generator/src/generator.ts

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Context, type StyleDecoder, type Stylesheet } from '@pandacss/core'
22
import { dashCase, PandaError } from '@pandacss/shared'
3-
import type { ArtifactId, CssArtifactType, LoadConfigResult, SpecType } from '@pandacss/types'
3+
import type { ArtifactId, CssArtifactType, LoadConfigResult, SpecFile } from '@pandacss/types'
44
import { match } from 'ts-pattern'
55
import { generateArtifacts } from './artifacts'
66
import { generateGlobalCss } from './artifacts/css/global-css'
@@ -17,8 +17,8 @@ import { generateKeyframesSpec } from './spec/keyframes'
1717
import { generateLayerStylesSpec } from './spec/layer-styles'
1818
import { generatePatternsSpec } from './spec/patterns'
1919
import { generateRecipesSpec } from './spec/recipes'
20-
import { generateSemanticTokensSpec, generateTokensSpec } from './spec/tokens'
2120
import { generateTextStylesSpec } from './spec/text-styles'
21+
import { generateSemanticTokensSpec, generateTokensSpec } from './spec/tokens'
2222

2323
export interface SplitCssArtifact {
2424
type: 'layer' | 'recipe' | 'theme'
@@ -205,33 +205,24 @@ export class Generator extends Context {
205205
}
206206
}
207207

208-
getSpecOfType = (type: SpecType) => {
209-
return match(type)
210-
.with('tokens', () => generateTokensSpec(this))
211-
.with('recipes', () => generateRecipesSpec(this))
212-
.with('patterns', () => generatePatternsSpec(this))
213-
.with('conditions', () => generateConditionsSpec(this))
214-
.with('keyframes', () => generateKeyframesSpec(this))
215-
.with('semantic-tokens', () => generateSemanticTokensSpec(this))
216-
.with('text-styles', () => generateTextStylesSpec(this))
217-
.with('layer-styles', () => generateLayerStylesSpec(this))
218-
.with('animation-styles', () => generateAnimationStylesSpec(this))
219-
.with('color-palette', () => generateColorPaletteSpec(this))
220-
.exhaustive()
221-
}
208+
getSpec = (): SpecFile[] => {
209+
const specs: SpecFile[] = [
210+
generateTokensSpec(this),
211+
generateRecipesSpec(this),
212+
generatePatternsSpec(this),
213+
generateConditionsSpec(this),
214+
generateKeyframesSpec(this),
215+
generateSemanticTokensSpec(this),
216+
generateTextStylesSpec(this),
217+
generateLayerStylesSpec(this),
218+
generateAnimationStylesSpec(this),
219+
]
222220

223-
getSpec = () => {
224-
return {
225-
tokens: generateTokensSpec(this),
226-
recipes: generateRecipesSpec(this),
227-
patterns: generatePatternsSpec(this),
228-
conditions: generateConditionsSpec(this),
229-
keyframes: generateKeyframesSpec(this),
230-
'semantic-tokens': generateSemanticTokensSpec(this),
231-
'text-styles': generateTextStylesSpec(this),
232-
'layer-styles': generateLayerStylesSpec(this),
233-
'animation-styles': generateAnimationStylesSpec(this),
234-
'color-palette': generateColorPaletteSpec(this),
221+
const colorPaletteSpec = generateColorPaletteSpec(this)
222+
if (colorPaletteSpec) {
223+
specs.push(colorPaletteSpec)
235224
}
225+
226+
return specs
236227
}
237228
}

packages/generator/src/spec/color-palette.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
import type { Context } from '@pandacss/core'
22
import type { ColorPaletteSpec } from '@pandacss/types'
33

4-
export const generateColorPaletteSpec = (ctx: Context): ColorPaletteSpec => {
4+
export const generateColorPaletteSpec = (ctx: Context): ColorPaletteSpec | null => {
5+
// Don't emit color-palette spec if colorPalette is disabled
6+
const colorPaletteConfig = ctx.config.theme?.colorPalette
7+
if (colorPaletteConfig?.enabled === false) {
8+
return null
9+
}
10+
511
const jsxStyleProps = ctx.config.jsxStyleProps ?? 'all'
612
const values = Array.from(ctx.tokens.view.colorPalettes.keys()).sort()
713

14+
// If there are no color palettes, don't emit the spec
15+
if (!values.length) {
16+
return null
17+
}
18+
819
const functionExamples: string[] = [
920
`css({ colorPalette: 'blue' })`,
1021
`css({ colorPalette: 'blue', bg: 'colorPalette.500', color: 'colorPalette.50' })`,

packages/generator/src/spec/tokens.ts

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import type { Context } from '@pandacss/core'
22
import { walkObject } from '@pandacss/shared'
33
import type { Token } from '@pandacss/token-dictionary'
4-
import type { SemanticTokenSpec, SemanticTokenSpecDefinition, TokenSpec, TokenSpecDefinition } from '@pandacss/types'
4+
import type {
5+
SemanticTokenSpec,
6+
SemanticTokenGroupDefinition,
7+
SemanticTokenValue,
8+
TokenSpec,
9+
TokenGroupDefinition,
10+
TokenValue,
11+
} from '@pandacss/types'
512

613
const CATEGORY_PROPERTY_MAP: Record<string, string> = {
714
colors: 'color',
@@ -56,62 +63,96 @@ const generateTokenExamples = (token: Token, jsxStyleProps: 'all' | 'minimal' |
5663

5764
export const generateTokensSpec = (ctx: Context): TokenSpec => {
5865
const jsxStyleProps = ctx.config.jsxStyleProps ?? 'all'
59-
const tokens = ctx.tokens.allTokens
60-
.filter(
61-
(token) =>
62-
!token.extensions.isSemantic &&
63-
!token.extensions.isVirtual &&
64-
!token.extensions.conditions &&
65-
!token.extensions.isNegative,
66-
)
67-
.map((token): TokenSpecDefinition => {
68-
const { functionExamples, tokenFunctionExamples, jsxExamples } = generateTokenExamples(token, jsxStyleProps)
69-
return {
70-
name: token.name,
66+
67+
// Create grouped data structure using tokens.view.categoryMap
68+
const groupedData: TokenGroupDefinition[] = Array.from(ctx.tokens.view.categoryMap.entries())
69+
.map(([category, tokenMap]) => {
70+
// Convert Map values to array and filter
71+
const typeTokens = Array.from(tokenMap.values()).filter(
72+
(token) =>
73+
!token.extensions.isSemantic &&
74+
!token.extensions.isVirtual &&
75+
!token.extensions.conditions &&
76+
!token.extensions.isNegative,
77+
)
78+
79+
// Skip if no tokens after filtering
80+
if (!typeTokens.length) return null
81+
82+
// Get examples from first token of this type (they'll be the same for all)
83+
const firstToken = typeTokens[0]
84+
const { functionExamples, tokenFunctionExamples, jsxExamples } = generateTokenExamples(firstToken, jsxStyleProps)
85+
86+
const values: TokenValue[] = typeTokens.map((token) => ({
87+
name: token.extensions.prop || token.name,
7188
value: token.value,
72-
type: token.extensions.category,
7389
description: token.description,
7490
deprecated: token.deprecated,
7591
cssVar: token.extensions.varRef,
92+
}))
93+
94+
return {
95+
type: category,
96+
values,
7697
tokenFunctionExamples,
7798
functionExamples,
7899
jsxExamples,
79100
}
80101
})
102+
.filter(Boolean) as TokenGroupDefinition[]
81103

82104
return {
83105
type: 'tokens',
84-
data: tokens,
106+
data: groupedData,
85107
}
86108
}
87109

88110
export const generateSemanticTokensSpec = (ctx: Context): SemanticTokenSpec => {
89111
const jsxStyleProps = ctx.config.jsxStyleProps ?? 'all'
90-
const semanticTokens = ctx.tokens.allTokens
91-
.filter((token) => (token.extensions.isSemantic || token.extensions.conditions) && !token.extensions.isVirtual)
92-
.map((token): SemanticTokenSpecDefinition => {
93-
const { functionExamples, tokenFunctionExamples, jsxExamples } = generateTokenExamples(token, jsxStyleProps)
94-
const values: Array<{ value: string; condition?: string }> = []
95-
96-
walkObject(token.extensions.rawValue, (value, path) => {
97-
values.push({ value, condition: path.map((p) => p.replace(/^_/, '')).join('.') })
112+
113+
// Create grouped data structure using tokens.view.categoryMap
114+
const groupedData: SemanticTokenGroupDefinition[] = Array.from(ctx.tokens.view.categoryMap.entries())
115+
.map(([category, tokenMap]) => {
116+
// Convert Map values to array and filter for semantic tokens
117+
const typeTokens = Array.from(tokenMap.values()).filter(
118+
(token) => (token.extensions.isSemantic || token.extensions.conditions) && !token.extensions.isVirtual,
119+
)
120+
121+
// Skip if no tokens after filtering
122+
if (!typeTokens.length) return null
123+
124+
// Get examples from first token of this type
125+
const firstToken = typeTokens[0]
126+
const { functionExamples, tokenFunctionExamples, jsxExamples } = generateTokenExamples(firstToken, jsxStyleProps)
127+
128+
const values: SemanticTokenValue[] = typeTokens.map((token) => {
129+
const conditions: Array<{ value: string; condition?: string }> = []
130+
131+
walkObject(token.extensions.rawValue, (value, path) => {
132+
conditions.push({ value, condition: path.map((p) => p.replace(/^_/, '')).join('.') })
133+
})
134+
135+
return {
136+
name: token.extensions.prop || token.name,
137+
values: conditions,
138+
description: token.description,
139+
deprecated: token.deprecated,
140+
cssVar: token.extensions.varRef,
141+
}
98142
})
99143

100144
return {
101-
name: token.name,
145+
type: category,
102146
values,
103-
type: token.extensions.category,
104-
description: token.description,
105-
deprecated: token.deprecated,
106-
cssVar: token.extensions.varRef,
107147
tokenFunctionExamples,
108148
functionExamples,
109149
jsxExamples,
110150
}
111151
})
152+
.filter(Boolean) as SemanticTokenGroupDefinition[]
112153

113154
return {
114155
type: 'semantic-tokens',
115-
data: semanticTokens,
156+
data: groupedData,
116157
}
117158
}

packages/node/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ export { setupGitIgnore } from './git-ignore'
1212
export { setLogStream } from './logstream'
1313
export { parseDependency } from './parse-dependency'
1414
export { setupConfig, setupPostcss } from './setup-config'
15-
export { spec, isValidSpecType, VALID_SPEC_TYPES } from './spec'
15+
export { spec } from './spec'

packages/node/src/spec.ts

Lines changed: 7 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,31 @@
11
import { logger } from '@pandacss/logger'
2-
import type { ArtifactId, SpecType } from '@pandacss/types'
3-
import picomatch from 'picomatch'
2+
import type { ArtifactId, SpecFile } from '@pandacss/types'
43
import type { PandaContext } from './create-context'
54

6-
export const VALID_SPEC_TYPES: SpecType[] = [
7-
'tokens',
8-
'recipes',
9-
'patterns',
10-
'conditions',
11-
'keyframes',
12-
'semantic-tokens',
13-
'text-styles',
14-
'layer-styles',
15-
'animation-styles',
16-
'color-palette',
17-
]
18-
19-
export const isValidSpecType = (value: unknown): value is SpecType => {
20-
return typeof value === 'string' && VALID_SPEC_TYPES.includes(value as SpecType)
21-
}
22-
235
export interface SpecOptions {
246
outdir?: string
25-
filter?: string
267
}
278

289
export async function spec(ctx: PandaContext, options: SpecOptions) {
29-
const { outdir, filter } = options
10+
const { outdir } = options
3011

3112
const specs = ctx.getSpec()
32-
const matcher = filter ? picomatch(filter) : null
33-
34-
const filterContent = (content: any, specType: string) => {
35-
if (!matcher) return content
36-
const clone = { ...content }
37-
38-
if (specType === 'color-palette') {
39-
// color-palette has a different structure: data.values is an array
40-
clone.data = {
41-
...clone.data,
42-
values: clone.data.values.filter((v: string) => matcher(v)),
43-
}
44-
} else {
45-
// All other specs have data as an array of objects with a 'name' property
46-
clone.data = clone.data.filter((item: any) => matcher(item.name))
47-
}
48-
return clone
49-
}
5013

5114
// Use ctx.paths.specs (includes configured outdir) or override with provided outdir
5215
const specDir = outdir ? [ctx.config.cwd, outdir] : ctx.paths.specs
5316
const specDirPath = ctx.runtime.path.join(...specDir)
5417

55-
const writeSpec = async (name: string, content: any) => {
56-
const filteredContent = filterContent(content, name)
57-
18+
const writeSpec = async (spec: SpecFile) => {
5819
await ctx.output.write({
59-
id: `spec-${name}` as ArtifactId,
20+
id: `spec-${spec.type}` as ArtifactId,
6021
dir: specDir,
61-
files: [{ file: `${name}.json`, code: JSON.stringify(filteredContent, null, 2) }],
22+
files: [{ file: `${spec.type}.json`, code: JSON.stringify(spec, null, 2) }],
6223
})
6324
}
6425

65-
await Promise.all(Object.entries(specs as Record<string, any>).map(([key, content]) => writeSpec(key, content)))
66-
67-
const specTypes = Object.keys(specs as Record<string, any>)
68-
const typesList = specTypes.map((t) => `${t}.json`).join(', ')
26+
await Promise.all(specs.map(writeSpec))
6927

28+
const specTypes = specs.map((s) => s.type)
7029
logger.info('spec', `Generated ${specTypes.length} spec file(s) → ${specDirPath}`)
71-
logger.info('spec', ` Types: ${typesList}`)
72-
7330
return specs
7431
}

0 commit comments

Comments
 (0)