Skip to content

Commit 86b30b1

Browse files
committed
feat: add spec command
1 parent 59568d0 commit 86b30b1

34 files changed

+1009
-35
lines changed

.changeset/add-spec-command.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@pandacss/cli': minor
3+
'@pandacss/generator': minor
4+
'@pandacss/node': minor
5+
'@pandacss/types': minor
6+
---
7+
8+
Add `panda spec` command to generate specification files for your theme (useful for documentation). This command
9+
generates JSON specification files containing metadata, examples, and usage information.
10+
11+
```bash
12+
# Generate all spec files
13+
panda spec
14+
15+
# Generate with filter (filters across all spec types)
16+
panda spec --filter "button*"
17+
18+
# Custom output directory
19+
panda spec --outdir custom/specs
20+
21+
# Include spec entrypoint in package.json
22+
panda emit-pkg --spec
23+
```
24+
25+
Spec files can be consumed via:
26+
27+
```javascript
28+
import tokens from 'styled-system/specs/tokens'
29+
import recipes from 'styled-system/specs/recipes'
30+
```

packages/cli/src/cli-main.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
setupConfig,
1414
setupGitIgnore,
1515
setupPostcss,
16+
spec,
1617
startProfiling,
1718
type CssGenOptions,
1819
} from '@pandacss/node'
@@ -31,6 +32,7 @@ import type {
3132
InitCommandFlags,
3233
MainCommandFlags,
3334
ShipCommandFlags,
35+
SpecCommandFlags,
3436
StudioCommandFlags,
3537
} from './types'
3638

@@ -306,6 +308,33 @@ export async function main() {
306308
}
307309
})
308310

311+
cli
312+
.command('spec', 'Generate spec files for your theme (useful for documentation)')
313+
.option('--silent', "Don't print any logs")
314+
.option('--outdir <dir>', 'Output directory for spec files')
315+
.option('--filter <pattern>', 'Filter specifications by name/pattern')
316+
.option('-c, --config <path>', 'Path to panda config file')
317+
.option('--cwd <cwd>', 'Current working directory', { default: cwd })
318+
.action(async (flags: SpecCommandFlags) => {
319+
const { silent, config: configPath, outdir, filter } = flags
320+
const cwd = resolve(flags.cwd ?? '')
321+
322+
if (silent) {
323+
logger.level = 'silent'
324+
}
325+
326+
const ctx = await loadConfigAndCreateContext({
327+
cwd,
328+
configPath,
329+
config: { cwd },
330+
})
331+
332+
await spec(ctx, {
333+
outdir,
334+
filter,
335+
})
336+
})
337+
309338
cli
310339
.command('studio', 'Realtime documentation for your design tokens')
311340
.option('--build', 'Build')

packages/cli/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,11 @@ export interface EmitPackageCommandFlags {
9797
cwd: string
9898
base?: string
9999
}
100+
101+
export interface SpecCommandFlags {
102+
silent?: boolean
103+
outdir?: string
104+
cwd?: string
105+
config?: string
106+
filter?: string
107+
}

packages/core/src/path.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,8 @@ export class PathEngine {
5050
get themes() {
5151
return this.getFilePath('themes')
5252
}
53+
54+
get specs() {
55+
return this.getFilePath('specs')
56+
}
5357
}

packages/core/src/patterns.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1-
import { capitalize, createRegex, dashCase, getPatternStyles, isObject, memo, uncapitalize } from '@pandacss/shared'
1+
import {
2+
capitalize,
3+
createRegex,
4+
dashCase,
5+
getPatternStyles,
6+
isObject,
7+
memo,
8+
uncapitalize,
9+
unionType,
10+
} from '@pandacss/shared'
211
import type { TokenDictionary } from '@pandacss/token-dictionary'
3-
import type { ArtifactFilters, Dict, PatternConfig, PatternHelpers, UserConfig } from '@pandacss/types'
12+
import type { ArtifactFilters, Dict, PatternConfig, PatternHelpers, PatternProperty, UserConfig } from '@pandacss/types'
413
import type { Utility } from './utility'
514

615
interface PatternOptions {
@@ -134,6 +143,37 @@ export class Patterns {
134143
}
135144
}
136145

146+
getPropertyType = (prop: PatternProperty): string => {
147+
switch (prop.type) {
148+
case 'enum':
149+
// TypeScript union style for enums: "a" | "b" | "c"
150+
return unionType(prop.value)
151+
152+
case 'token': {
153+
// Token reference with optional CSS property fallback
154+
const tokenType = `Tokens["${prop.value}"]`
155+
if (prop.property) {
156+
return `ConditionalValue<${tokenType} | Properties["${prop.property}"]>`
157+
}
158+
return `ConditionalValue<${tokenType}>`
159+
}
160+
161+
case 'property':
162+
// System property type
163+
return `SystemProperties["${prop.value}"]`
164+
165+
case 'string':
166+
case 'number':
167+
case 'boolean':
168+
// Primitive types with ConditionalValue wrapper
169+
return `ConditionalValue<${prop.type}>`
170+
171+
default:
172+
// For any other type, return ConditionalValue wrapper
173+
return `ConditionalValue<${(prop as any).type || 'unknown'}>`
174+
}
175+
}
176+
137177
static isValidNode = (node: unknown): node is PatternNode => {
138178
return isObject(node) && 'type' in node && node.type === 'recipe'
139179
}

packages/generator/__tests__/generate-pattern.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ test('should generate pattern', () => {
753753
import type { Tokens } from '../tokens/index';
754754
755755
export interface DividerProperties {
756-
orientation?: ConditionalValue<"horizontal" | "vertical">
756+
orientation?: "horizontal" | "vertical"
757757
thickness?: ConditionalValue<Tokens["sizes"] | Properties["borderWidth"]>
758758
color?: ConditionalValue<Tokens["colors"] | Properties["borderColor"]>
759759
}
@@ -806,7 +806,7 @@ test('should generate pattern', () => {
806806
offsetX?: ConditionalValue<Tokens["spacing"] | Properties["left"]>
807807
offsetY?: ConditionalValue<Tokens["spacing"] | Properties["top"]>
808808
offset?: ConditionalValue<Tokens["spacing"] | Properties["top"]>
809-
placement?: ConditionalValue<"bottom-end" | "bottom-start" | "top-end" | "top-start" | "bottom-center" | "top-center" | "middle-center" | "middle-end" | "middle-start">
809+
placement?: "bottom-end" | "bottom-start" | "top-end" | "top-start" | "bottom-center" | "top-center" | "middle-center" | "middle-end" | "middle-start"
810810
}
811811
812812
interface FloatStyles extends FloatProperties, DistributiveOmit<SystemStyleObject, keyof FloatProperties > {}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"content": "import type { CssProperty, SystemStyleObject } from './system-types'\nimport type { TokenCategory } from '../tokens'\n\ntype Primitive = string | number | boolean | null | undefined\ntype LiteralUnion<T, K extends Primitive = string> = T | (K & Record<never, never>)\n\nexport type PatternProperty =\n | { type: 'property'; value: CssProperty }\n | { type: 'enum'; value: string[] }\n | { type: 'token'; value: TokenCategory; property?: CssProperty }\n | { type: 'string' | 'boolean' | 'number' }\n\nexport interface PatternHelpers {\n map: (value: any, fn: (value: string) => string | undefined) => any\n isCssUnit: (value: any) => boolean\n isCssVar: (value: any) => boolean\n isCssFunction: (value: any) => boolean\n}\n\nexport interface PatternProperties {\n [key: string]: PatternProperty\n}\n\ntype InferProps<T> = Record<LiteralUnion<keyof T>, any>\n\nexport type PatternDefaultValue<T> = Partial<InferProps<T>>\n\nexport type PatternDefaultValueFn<T> = (props: InferProps<T>) => PatternDefaultValue<T>\n\nexport interface PatternConfig<T extends PatternProperties = PatternProperties> {\n /**\n * The description of the pattern. This will be used in the JSDoc comment.\n */\n description?: string\n /**\n * The JSX element rendered by the pattern\n * @default 'div'\n */\n jsxElement?: string\n /**\n * The properties of the pattern.\n */\n properties?: T\n /**\n * The default values of the pattern.\n */\n defaultValues?: PatternDefaultValue<T> | PatternDefaultValueFn<T>\n /**\n * The css object this pattern will generate.\n */\n transform?: (props: InferProps<T>, helpers: PatternHelpers) => SystemStyleObject\n /**\n * Whether the pattern is deprecated.\n */\n deprecated?: boolean | string\n /**\n * The jsx element name this pattern will generate.\n */\n jsxName?: string\n /**\n * The jsx elements to track for this pattern. Can be string or Regexp.\n *\n * @default capitalize(pattern.name)\n * @example ['Button', 'Link', /Button$/]\n */\n jsx?: Array<string | RegExp>\n /**\n * Whether to only generate types for the specified properties.\n * This will disallow css properties\n */\n strict?: boolean\n /**\n * @experimental\n * Disallow certain css properties for this pattern\n */\n blocklist?: LiteralUnion<CssProperty>[]\n}\n"
2+
"content": "import type { CssProperty, SystemStyleObject } from './system-types'\nimport type { TokenCategory } from '../tokens'\n\ntype Primitive = string | number | boolean | null | undefined\ntype LiteralUnion<T, K extends Primitive = string> = T | (K & Record<never, never>)\n\nexport type PatternProperty =\n | { type: 'property'; value: CssProperty; description?: string }\n | { type: 'enum'; value: string[]; description?: string }\n | { type: 'token'; value: TokenCategory; property?: CssProperty; description?: string }\n | { type: 'string' | 'boolean' | 'number'; description?: string }\n\nexport interface PatternHelpers {\n map: (value: any, fn: (value: string) => string | undefined) => any\n isCssUnit: (value: any) => boolean\n isCssVar: (value: any) => boolean\n isCssFunction: (value: any) => boolean\n}\n\nexport interface PatternProperties {\n [key: string]: PatternProperty\n}\n\ntype InferProps<T> = Record<LiteralUnion<keyof T>, any>\n\nexport type PatternDefaultValue<T> = Partial<InferProps<T>>\n\nexport type PatternDefaultValueFn<T> = (props: InferProps<T>) => PatternDefaultValue<T>\n\nexport interface PatternConfig<T extends PatternProperties = PatternProperties> {\n /**\n * The description of the pattern. This will be used in the JSDoc comment.\n */\n description?: string\n /**\n * The JSX element rendered by the pattern\n * @default 'div'\n */\n jsxElement?: string\n /**\n * The properties of the pattern.\n */\n properties?: T\n /**\n * The default values of the pattern.\n */\n defaultValues?: PatternDefaultValue<T> | PatternDefaultValueFn<T>\n /**\n * The css object this pattern will generate.\n */\n transform?: (props: InferProps<T>, helpers: PatternHelpers) => SystemStyleObject\n /**\n * Whether the pattern is deprecated.\n */\n deprecated?: boolean | string\n /**\n * The jsx element name this pattern will generate.\n */\n jsxName?: string\n /**\n * The jsx elements to track for this pattern. Can be string or Regexp.\n *\n * @default capitalize(pattern.name)\n * @example ['Button', 'Link', /Button$/]\n */\n jsx?: Array<string | RegExp>\n /**\n * Whether to only generate types for the specified properties.\n * This will disallow css properties\n */\n strict?: boolean\n /**\n * @experimental\n * Disallow certain css properties for this pattern\n */\n blocklist?: LiteralUnion<CssProperty>[]\n}\n"
33
}

packages/generator/src/artifacts/js/pattern.ts

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import type { Context } from '@pandacss/core'
2-
import { compact, unionType } from '@pandacss/shared'
2+
import { compact } from '@pandacss/shared'
33
import type { ArtifactFilters } from '@pandacss/types'
44
import { stringify } from 'javascript-stringify'
55
import { outdent } from 'outdent'
6-
import { match } from 'ts-pattern'
76

87
export function generatePattern(ctx: Context, filters?: ArtifactFilters) {
98
if (ctx.patterns.isEmpty()) return
@@ -38,22 +37,8 @@ export function generatePattern(ctx: Context, filters?: ArtifactFilters) {
3837
${Object.keys(properties ?? {})
3938
.map((key) => {
4039
const value = properties![key]
41-
return match(value)
42-
.with({ type: 'property' }, (value) => {
43-
return `${key}?: SystemProperties["${value.value}"]`
44-
})
45-
.with({ type: 'token' }, (value) => {
46-
if (value.property) {
47-
return `${key}?: ConditionalValue<Tokens["${value.value}"] | Properties["${value.property}"]>`
48-
}
49-
return `${key}?: ConditionalValue<Tokens["${value.value}"]>`
50-
})
51-
.with({ type: 'enum' }, (value) => {
52-
return `${key}?: ConditionalValue<${unionType(value.value)}>`
53-
})
54-
.otherwise(() => {
55-
return `${key}?: ConditionalValue<${value.type}>`
56-
})
40+
const typeString = ctx.patterns.getPropertyType(value)
41+
return `${key}?: ${typeString}`
5742
})
5843
.join('\n\t')}
5944
}

packages/generator/src/generator.ts

Lines changed: 40 additions & 1 deletion
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 } from '@pandacss/types'
3+
import type { ArtifactId, CssArtifactType, LoadConfigResult, SpecType } from '@pandacss/types'
44
import { match } from 'ts-pattern'
55
import { generateArtifacts } from './artifacts'
66
import { generateGlobalCss } from './artifacts/css/global-css'
@@ -10,6 +10,15 @@ import { generateResetCss } from './artifacts/css/reset-css'
1010
import { generateStaticCss } from './artifacts/css/static-css'
1111
import { generateTokenCss } from './artifacts/css/token-css'
1212
import { getThemeCss } from './artifacts/js/themes'
13+
import { generateAnimationStylesSpec } from './spec/animation-styles'
14+
import { generateColorPaletteSpec } from './spec/color-palette'
15+
import { generateConditionsSpec } from './spec/conditions'
16+
import { generateKeyframesSpec } from './spec/keyframes'
17+
import { generateLayerStylesSpec } from './spec/layer-styles'
18+
import { generatePatternsSpec } from './spec/patterns'
19+
import { generateRecipesSpec } from './spec/recipes'
20+
import { generateSemanticTokensSpec, generateTokensSpec } from './spec/tokens'
21+
import { generateTextStylesSpec } from './spec/text-styles'
1322

1423
export interface SplitCssArtifact {
1524
type: 'layer' | 'recipe' | 'theme'
@@ -195,4 +204,34 @@ export class Generator extends Context {
195204
index: imports.join('\n'),
196205
}
197206
}
207+
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+
}
222+
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),
235+
}
236+
}
198237
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { Context } from '@pandacss/core'
2+
import { isObject, walkObject } from '@pandacss/shared'
3+
import type { AnimationStyleSpec } from '@pandacss/types'
4+
5+
const collectAnimationStyles = (values: Record<string, any>): Array<{ name: string; description?: string }> => {
6+
const result: Array<{ name: string; description?: string }> = []
7+
8+
walkObject(
9+
values,
10+
(token, paths) => {
11+
if (token && isObject(token) && 'value' in token) {
12+
const filteredPaths = paths.filter((item) => item !== 'DEFAULT')
13+
result.push({
14+
name: filteredPaths.join('.'),
15+
description: token.description,
16+
})
17+
}
18+
},
19+
{
20+
stop: (v) => isObject(v) && 'value' in v,
21+
},
22+
)
23+
24+
return result
25+
}
26+
27+
export const generateAnimationStylesSpec = (ctx: Context): AnimationStyleSpec => {
28+
const jsxStyleProps = ctx.config.jsxStyleProps ?? 'all'
29+
const animationStyles = collectAnimationStyles(ctx.config.theme?.animationStyles ?? {})
30+
31+
const animationStylesSpec = animationStyles.map((style) => {
32+
const functionExamples: string[] = [`css({ animationStyle: '${style.name}' })`]
33+
const jsxExamples: string[] = []
34+
35+
if (jsxStyleProps === 'all') {
36+
jsxExamples.push(`<Box animationStyle="${style.name}" />`)
37+
} else if (jsxStyleProps === 'minimal') {
38+
jsxExamples.push(`<Box css={{ animationStyle: '${style.name}' }} />`)
39+
}
40+
// 'none' - no JSX examples
41+
42+
return {
43+
name: style.name,
44+
description: style.description,
45+
functionExamples,
46+
jsxExamples,
47+
}
48+
})
49+
50+
return {
51+
type: 'animation-styles',
52+
data: animationStylesSpec,
53+
}
54+
}

0 commit comments

Comments
 (0)