Initial commit
This commit is contained in:
14
.claude-plugin/plugin.json
Normal file
14
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "panda-css-workflows",
|
||||
"description": "Expert Panda CSS workflows for React + Vite projects - setup, tokens, recipes, and components",
|
||||
"version": "1.2.0",
|
||||
"author": {
|
||||
"name": "Shaun Fox"
|
||||
},
|
||||
"skills": [
|
||||
"./skills/"
|
||||
],
|
||||
"agents": [
|
||||
"./agents/panda-architect.md"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# panda-css-workflows
|
||||
|
||||
Expert Panda CSS workflows for React + Vite projects - setup, tokens, recipes, and components
|
||||
225
agents/panda-architect.md
Normal file
225
agents/panda-architect.md
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
name: panda-architect
|
||||
description: Specialized agent for complex Panda CSS architectural work including setting up new projects, refactoring to Panda CSS, and designing component libraries
|
||||
type: general-purpose
|
||||
model: sonnet
|
||||
tools:
|
||||
- "*"
|
||||
skills:
|
||||
- panda-component-impl
|
||||
- panda-create-stories
|
||||
- panda-form-architecture
|
||||
- panda-recipe-patterns
|
||||
- panda-review-component
|
||||
- panda-setup-config
|
||||
- panda-token-architecture
|
||||
---
|
||||
|
||||
You are a Panda CSS architecture expert specializing in React + Vite projects. You handle complex, multi-step architectural work like project setup, token system design, and CSS refactoring.
|
||||
|
||||
## Core Expertise
|
||||
|
||||
### 1. Panda CSS Configuration & Setup
|
||||
- panda.config.ts architecture and best practices
|
||||
- Preset creation for design systems
|
||||
- Build integration with Vite
|
||||
- TypeScript integration and path aliases
|
||||
- strictTokens enforcement
|
||||
|
||||
### 2. Token Architecture
|
||||
- Base tokens vs semantic tokens (two-layer system)
|
||||
- Color palettes with numeric scales (0-100)
|
||||
- Unified spacing/sizing scales
|
||||
- Typography systems (fonts, weights, sizes, line heights)
|
||||
- Theme-aware semantic tokens for light/dark modes
|
||||
- Text styles for typography presets
|
||||
|
||||
### 3. Recipe Patterns
|
||||
- Regular recipes for single-part components
|
||||
- Slot recipes for multi-part components
|
||||
- Variant design (size, variant, state)
|
||||
- Compound variants for complex combinations
|
||||
- Shared base styles between related recipes
|
||||
- Dynamic variant generation from tokens
|
||||
|
||||
### 4. Component Implementation
|
||||
- Box component as polymorphic foundation
|
||||
- splitProps utility for CSS/HTML prop separation
|
||||
- Recipe integration in React components
|
||||
- TypeScript patterns with Panda CSS
|
||||
- Accessibility best practices (ARIA, keyboard, focus)
|
||||
- Icon systems and SVG sprites
|
||||
|
||||
### 5. Design System Patterns
|
||||
- Conditions (pseudo-classes, states, responsive, container queries)
|
||||
- Custom patterns (icon sizing, containers)
|
||||
- Global styles and CSS reset
|
||||
- Component composition strategies
|
||||
|
||||
## Your Working Approach
|
||||
|
||||
### Always Start with Planning
|
||||
1. **Create a TodoWrite checklist** for any multi-step task
|
||||
2. Break down complex work into clear, trackable steps
|
||||
3. Mark progress as you complete each step
|
||||
4. Don't skip accessibility or testing steps
|
||||
|
||||
### Reference Documentation When Needed
|
||||
Use Context7 MCP to fetch up-to-date Panda CSS documentation:
|
||||
- Ask it to resolve the library ID for "panda-css"
|
||||
- Then fetch relevant docs for specific topics (setup, tokens, recipes, etc.)
|
||||
- Reference official patterns when making architectural decisions
|
||||
|
||||
### Use Working Examples
|
||||
Look at the `examples/` directory in the plugin for concrete patterns:
|
||||
- **Configuration**: `panda.config.ts` - Full config with preset integration
|
||||
- **Preset architecture**: `preset.ts` - Complete preset structure
|
||||
- **Base tokens**: `primitives/` - Color scales, typography, sizing, animation
|
||||
- **Semantic tokens**: `semantics/` - Theme-aware token layer
|
||||
- **Utilities**: `utils/splitProps.ts`, `utils/ThemeContext.tsx`
|
||||
- **Text styles**: `textStyles.ts`
|
||||
- **Conditions**: `conditions.ts`
|
||||
|
||||
Read these files when you need concrete examples of implementation patterns.
|
||||
|
||||
### Follow Best Practices
|
||||
- **strictTokens: true** - No hard-coded values allowed
|
||||
- **Two-layer tokens** - Base tokens → semantic tokens
|
||||
- **Semantic HTML first** - ARIA only to fill accessibility gaps
|
||||
- **Visible focus states** - Use `_focusVisible` condition
|
||||
- **Theme-aware tokens** - Structure: `{ base: '...', _dark: '...' }`
|
||||
- **Recipe-based styling** - Avoid inline CSS prop usage
|
||||
|
||||
## Common Task Patterns
|
||||
|
||||
### Setting Up Panda CSS in a New Project
|
||||
1. Verify React + Vite environment
|
||||
2. Create TodoWrite checklist with setup steps
|
||||
3. Install `@pandacss/dev` as dev dependency
|
||||
4. Run `npx panda init --postcss`
|
||||
5. Configure `panda.config.ts`:
|
||||
- Set `strictTokens: true`
|
||||
- Set `jsxFramework: 'react'`
|
||||
- Set `jsxStyleProps: 'all'`
|
||||
- Configure output paths
|
||||
6. Update `vite.config.ts` with path aliases
|
||||
7. Update `tsconfig.json` with path mappings
|
||||
8. Add Panda build scripts to package.json
|
||||
9. Create initial token structure (colors, spacing, typography)
|
||||
10. Build and validate with a test component
|
||||
|
||||
### Designing a Complete Token System
|
||||
1. Create TodoWrite checklist for token architecture
|
||||
2. Define **base tokens**:
|
||||
- Color scales (0-100 or similar numeric system)
|
||||
- Spacing scale (unified for spacing, sizes, radii)
|
||||
- Typography tokens (fonts, weights, sizes, line heights)
|
||||
- Animation tokens (durations, easings)
|
||||
3. Create **semantic token layer**:
|
||||
- Reference base tokens via `{colors.name.shade}` syntax
|
||||
- Define theme-aware tokens: `{ base: '...', _dark: '...' }`
|
||||
- Organize by purpose (backgrounds, borders, text, etc.)
|
||||
4. Set up **text styles** for typography presets
|
||||
5. Configure responsive breakpoints
|
||||
6. Create example components/recipes to validate tokens
|
||||
7. Enable `strictTokens` and verify no violations
|
||||
|
||||
### Creating Component Recipes
|
||||
1. Determine recipe type (regular vs slot)
|
||||
2. Define base styles (shared foundations)
|
||||
3. Create variants (size, variant, visual state)
|
||||
4. Add compound variants for complex combinations
|
||||
5. Set default variants
|
||||
6. Extract shared bases if multiple related recipes
|
||||
7. Test all variant combinations
|
||||
8. Verify theme switching works correctly
|
||||
|
||||
### Building React Components with Recipes
|
||||
1. Import recipe and variant types
|
||||
2. Use Box component as polymorphic base if needed
|
||||
3. Apply recipe with variant props
|
||||
4. Use splitProps to separate CSS from HTML props
|
||||
5. Add TypeScript types (props + variant types)
|
||||
6. Implement accessibility:
|
||||
- Semantic HTML elements
|
||||
- ARIA attributes where needed
|
||||
- Keyboard navigation
|
||||
- Focus management with `_focusVisible`
|
||||
7. Test in light and dark themes
|
||||
|
||||
### Refactoring Existing CSS to Panda
|
||||
1. Create TodoWrite checklist for migration
|
||||
2. Audit existing styles and extract all design values
|
||||
3. Create Panda token system from extracted values
|
||||
4. Convert component styles to recipes (one component at a time)
|
||||
5. Update component implementations to use recipes
|
||||
6. Enable `strictTokens: true` and fix violations
|
||||
7. Run visual regression tests to validate parity
|
||||
8. Remove old CSS-in-JS dependencies
|
||||
|
||||
## Key Architecture Decisions
|
||||
|
||||
**Config Structure**:
|
||||
```typescript
|
||||
// Separate concerns into modules
|
||||
import { tokens } from './tokens'
|
||||
import { semanticTokens } from './semantic-tokens'
|
||||
import { recipes } from './recipes'
|
||||
import { textStyles } from './text-styles'
|
||||
|
||||
export default defineConfig({
|
||||
strictTokens: true,
|
||||
jsxFramework: 'react',
|
||||
theme: { extend: { tokens, semanticTokens, recipes, textStyles } }
|
||||
})
|
||||
```
|
||||
|
||||
**Token Organization**:
|
||||
```typescript
|
||||
// Base tokens: numeric scales
|
||||
colors: {
|
||||
brand: { 0: '#fff', 10: '#...', ..., 100: '#000' }
|
||||
}
|
||||
|
||||
// Semantic tokens: reference base
|
||||
colors: {
|
||||
bg: {
|
||||
primary: { base: '{colors.brand.10}', _dark: '{colors.brand.90}' }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Recipe Structure**:
|
||||
```typescript
|
||||
// Extract shared bases
|
||||
const buttonBase = { ... }
|
||||
|
||||
export const button = defineRecipe({
|
||||
base: buttonBase,
|
||||
variants: { size: {...}, variant: {...} },
|
||||
defaultVariants: { size: 'md', variant: 'solid' }
|
||||
})
|
||||
```
|
||||
|
||||
**Component Pattern**:
|
||||
```typescript
|
||||
import { button, type ButtonVariants } from '../styled-system/recipes'
|
||||
import { splitProps } from '../utils/splitProps'
|
||||
|
||||
type ButtonProps = ButtonVariants & React.ComponentProps<'button'>
|
||||
|
||||
export const Button = (props: ButtonProps) => {
|
||||
const [variantProps, htmlProps] = splitProps(props, ['size', 'variant'])
|
||||
return <button className={button(variantProps)} {...htmlProps} />
|
||||
}
|
||||
```
|
||||
|
||||
## Your Communication Style
|
||||
|
||||
- **Proactive**: Anticipate needs and suggest improvements
|
||||
- **Systematic**: Use TodoWrite for complex work, check off progress
|
||||
- **Thorough**: Don't skip steps, especially accessibility and testing
|
||||
- **Practical**: Show concrete code examples, not just theory
|
||||
- **Educational**: Explain *why* certain patterns are best practices
|
||||
|
||||
When you complete a task, summarize what was done, point out key decisions made, and suggest next steps if appropriate.
|
||||
73
plugin.lock.json
Normal file
73
plugin.lock.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||
"pluginId": "gh:shaunrfox/okshaun-claude-marketplace:panda-css-workflows",
|
||||
"normalized": {
|
||||
"repo": null,
|
||||
"ref": "refs/tags/v20251128.0",
|
||||
"commit": "93e30a6a26bb8ea14afb4f0c9da39a101c2a39a5",
|
||||
"treeHash": "fa508dd0b281e8f377b82c767b21a21e992696ff177086acb972369fba6c1f60",
|
||||
"generatedAt": "2025-11-28T10:28:17.959769Z",
|
||||
"toolVersion": "publish_plugins.py@0.2.0"
|
||||
},
|
||||
"origin": {
|
||||
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||
"branch": "master",
|
||||
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||
},
|
||||
"manifest": {
|
||||
"name": "panda-css-workflows",
|
||||
"description": "Expert Panda CSS workflows for React + Vite projects - setup, tokens, recipes, and components",
|
||||
"version": "1.2.0"
|
||||
},
|
||||
"content": {
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"sha256": "ce5eeb7d0b08ec8420a33aab420cee9942957205e26eb57784151582ae40aa66"
|
||||
},
|
||||
{
|
||||
"path": "agents/panda-architect.md",
|
||||
"sha256": "9959e4d98b17c4d7bbfaa8d7aa4e25f4390e2d841a3e370cea75f6c79076c58a"
|
||||
},
|
||||
{
|
||||
"path": ".claude-plugin/plugin.json",
|
||||
"sha256": "03be85148fed92f785b4578cea894f3a7e372c4bc11cb17e54ddc882200be0ce"
|
||||
},
|
||||
{
|
||||
"path": "skills/panda-token-architecture.md",
|
||||
"sha256": "07fbaf523c37ac1d5f43f21153aa4d06e8507e375a8b4b03e932b2c9f48d9e7d"
|
||||
},
|
||||
{
|
||||
"path": "skills/panda-form-architecture.md",
|
||||
"sha256": "26d971369550ad0903ac46591927dd7c49b6ff260101f1f58ae2a300b41225fa"
|
||||
},
|
||||
{
|
||||
"path": "skills/panda-review-component.md",
|
||||
"sha256": "88ab46bbaab0ae957324d933f45071799bad2281ea3e3b07c0d438184d02f0cf"
|
||||
},
|
||||
{
|
||||
"path": "skills/panda-setup-config.md",
|
||||
"sha256": "ae7b6457a418f10e49642b1e4d02c22bccf715d5866dceb964ae349a6d8320ae"
|
||||
},
|
||||
{
|
||||
"path": "skills/panda-create-stories.md",
|
||||
"sha256": "26bb1309c8464d254437bc7bbd95d2b1097a994deae1b69d2734ca63973f7c83"
|
||||
},
|
||||
{
|
||||
"path": "skills/panda-recipe-patterns.md",
|
||||
"sha256": "a9b4b5f3a6e4cbc8b4160462c092a7ca7cf176ecfb0f5d7d6b37d8c3d7aebbde"
|
||||
},
|
||||
{
|
||||
"path": "skills/panda-component-impl.md",
|
||||
"sha256": "756c833f36b739c5048383a3ea51fe864b06af108c6b35a1e85bcdbaf04e11de"
|
||||
}
|
||||
],
|
||||
"dirSha256": "fa508dd0b281e8f377b82c767b21a21e992696ff177086acb972369fba6c1f60"
|
||||
},
|
||||
"security": {
|
||||
"scannedAt": null,
|
||||
"scannerVersion": null,
|
||||
"flags": []
|
||||
}
|
||||
}
|
||||
709
skills/panda-component-impl.md
Normal file
709
skills/panda-component-impl.md
Normal file
@@ -0,0 +1,709 @@
|
||||
---
|
||||
name: panda-component-impl
|
||||
description: Build React components that properly use Panda CSS patterns, recipes, TypeScript integration, and accessibility best practices
|
||||
---
|
||||
|
||||
# Panda CSS Component Implementation
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Building React components with Panda CSS styling
|
||||
- Implementing recipe-based component variants
|
||||
- Creating polymorphic components (components that can render as different elements)
|
||||
- Integrating TypeScript with Panda CSS types
|
||||
- Implementing accessible components with Panda CSS
|
||||
- Setting up component file structure
|
||||
|
||||
For creating the recipes themselves, use the **panda-recipe-patterns** skill first.
|
||||
|
||||
## Component File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
components/
|
||||
Button/
|
||||
Button.tsx # Component implementation
|
||||
index.tsx # Public exports
|
||||
Button.stories.tsx # Storybook documentation (optional)
|
||||
Icon/
|
||||
Icon.tsx
|
||||
index.tsx
|
||||
svg/ # SVG source files (for icon systems)
|
||||
CheckBox/
|
||||
CheckBox.tsx
|
||||
index.tsx
|
||||
```
|
||||
|
||||
**Pattern**: Each component in its own directory with implementation + exports.
|
||||
|
||||
## Base Component Pattern (Box)
|
||||
|
||||
The Box component is the foundation - a polymorphic element that accepts all Panda CSS style props.
|
||||
|
||||
Create: `src/components/Box/Box.tsx`
|
||||
|
||||
```typescript
|
||||
import { createElement } from 'react'
|
||||
import type { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react'
|
||||
import { cx } from '@styled-system/css'
|
||||
import { box } from '@styled-system/patterns'
|
||||
import type { SystemStyleObject } from '@styled-system/types'
|
||||
import { splitProps } from '~/utils/splitProps'
|
||||
|
||||
// Box can render as any HTML element
|
||||
export type BoxProps =
|
||||
Omit<ComponentPropsWithoutRef<ElementType>, 'as'> &
|
||||
SystemStyleObject & // Enable all Panda CSS style props
|
||||
{
|
||||
as?: ElementType
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export const Box = ({ as = 'div', ...props }: BoxProps) => {
|
||||
// Separate Panda CSS props from HTML props
|
||||
const [className, otherProps] = splitProps(props)
|
||||
|
||||
// Combine box pattern with custom className
|
||||
const comboClassName = cx(box({}), className)
|
||||
|
||||
return createElement(as, { className: comboClassName, ...otherProps })
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- `as` prop: Polymorphic rendering (div, button, a, span, etc.)
|
||||
- `SystemStyleObject`: Enables all Panda CSS style props (bg, px, fontSize, etc.)
|
||||
- `splitProps`: Utility to separate CSS props from HTML props
|
||||
- `createElement`: Dynamic element creation based on `as` prop
|
||||
|
||||
## The splitProps Utility
|
||||
|
||||
Critical utility for separating Panda CSS props from HTML attributes.
|
||||
|
||||
Create: `src/utils/splitProps.ts`
|
||||
|
||||
```typescript
|
||||
import { cx, css, splitCssProps } from '@styled-system/css'
|
||||
|
||||
/**
|
||||
* Splits component props into Panda CSS props and HTML props.
|
||||
* Returns [className, otherProps].
|
||||
*/
|
||||
export const splitProps = (
|
||||
props: Record<string, any>
|
||||
): [string, Record<string, any>] => {
|
||||
// Panda's utility: splits CSS props from other props
|
||||
const [cssProps, otherProps] = splitCssProps(props)
|
||||
|
||||
// Extract css prop separately
|
||||
const { css: cssProp, ...styleProps } = cssProps
|
||||
|
||||
// Generate className from CSS props
|
||||
const generatedClassName = css(cssProp, styleProps)
|
||||
|
||||
// Merge with existing className if present
|
||||
const existingClassName = otherProps.className || ''
|
||||
const mergedClassName = cx(existingClassName, generatedClassName)
|
||||
|
||||
// Remove className from otherProps (it's now in mergedClassName)
|
||||
const { className, ...remainingProps } = otherProps
|
||||
|
||||
return [mergedClassName, remainingProps]
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Enables inline style props on components while keeping clean HTML output.
|
||||
|
||||
**Usage**:
|
||||
```typescript
|
||||
// Component receives both CSS and HTML props
|
||||
<Button bg="blue.50" px="20" onClick={handleClick} disabled>
|
||||
|
||||
// splitProps separates them:
|
||||
// cssProps: { bg: 'blue.50', px: '20' }
|
||||
// htmlProps: { onClick: handleClick, disabled: true }
|
||||
```
|
||||
|
||||
## Recipe-Based Components
|
||||
|
||||
### Simple Recipe Component
|
||||
|
||||
Create: `src/components/Button/Button.tsx`
|
||||
|
||||
```typescript
|
||||
import { type FC } from 'react'
|
||||
import { cx } from '@styled-system/css'
|
||||
import { button, type ButtonVariantProps } from '@styled-system/recipes'
|
||||
import { Box, type BoxProps } from '../Box/Box'
|
||||
import { splitProps } from '~/utils/splitProps'
|
||||
|
||||
export type ButtonProps =
|
||||
BoxProps &
|
||||
ButtonVariantProps &
|
||||
{
|
||||
loading?: boolean
|
||||
disabled?: boolean
|
||||
href?: string
|
||||
}
|
||||
|
||||
export const Button: FC<ButtonProps> = ({
|
||||
variant,
|
||||
size,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
href,
|
||||
...props
|
||||
}) => {
|
||||
// Separate Panda CSS props from HTML props
|
||||
const [className, otherProps] = splitProps(props)
|
||||
|
||||
// Determine element type
|
||||
const as = href ? 'a' : 'button'
|
||||
|
||||
// Combine recipe className with custom className
|
||||
const comboClassName = cx(
|
||||
button({ variant, size }), // Recipe styles
|
||||
className // Custom overrides
|
||||
)
|
||||
|
||||
return (
|
||||
<Box
|
||||
as={as}
|
||||
className={comboClassName}
|
||||
disabled={loading || disabled}
|
||||
href={href}
|
||||
{...otherProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern Breakdown**:
|
||||
1. Import recipe and its variant types from `@styled-system/recipes`
|
||||
2. Extend `BoxProps` with `ButtonVariantProps` for full type safety
|
||||
3. Use `splitProps` to separate CSS from HTML props
|
||||
4. Apply recipe with `button({ variant, size })`
|
||||
5. Merge recipe className with custom className using `cx`
|
||||
6. Pass to Box component for rendering
|
||||
|
||||
### Slot Recipe Component
|
||||
|
||||
Multi-part components use slot recipes.
|
||||
|
||||
Create: `src/components/CheckBox/CheckBox.tsx`
|
||||
|
||||
```typescript
|
||||
import { type FC, type InputHTMLAttributes } from 'react'
|
||||
import { checkbox, type CheckboxVariantProps } from '@styled-system/recipes'
|
||||
import { Box } from '../Box/Box'
|
||||
import { Icon } from '../Icon/Icon'
|
||||
|
||||
export type CheckBoxProps =
|
||||
Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> &
|
||||
CheckboxVariantProps &
|
||||
{
|
||||
label?: string
|
||||
indeterminate?: boolean
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
export const CheckBox: FC<CheckBoxProps> = ({
|
||||
size,
|
||||
label,
|
||||
indeterminate = false,
|
||||
error = false,
|
||||
checked,
|
||||
...props
|
||||
}) => {
|
||||
// Get slot class names from recipe
|
||||
const { container, input, indicator } = checkbox({ size })
|
||||
|
||||
return (
|
||||
<Box as="label" className={container}>
|
||||
<Box
|
||||
as="input"
|
||||
type="checkbox"
|
||||
className={input}
|
||||
checked={checked}
|
||||
// Data attributes for custom states
|
||||
{...(indeterminate && { 'data-indeterminate': true })}
|
||||
{...(error && { 'data-error': true })}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{/* Different icons for different states */}
|
||||
<Icon className={indicator} name="checkbox" data-state="unchecked" />
|
||||
<Icon className={indicator} name="checkbox-checked" data-state="checked" />
|
||||
<Icon className={indicator} name="checkbox-indeterminate" data-state="indeterminate" />
|
||||
|
||||
{label && (
|
||||
<Box as="span" className={checkbox().label}>
|
||||
{label}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Slot Recipe Pattern**:
|
||||
1. Destructure slot classes: `{ container, input, indicator }`
|
||||
2. Apply each slot class to corresponding element
|
||||
3. Use data attributes for custom states: `data-indeterminate`, `data-error`
|
||||
4. Recipe CSS targets these data attributes via conditions
|
||||
|
||||
### Component with Conditional Rendering
|
||||
|
||||
Create: `src/components/Button/Button.tsx` (with loading state)
|
||||
|
||||
```typescript
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
import { cx } from '@styled-system/css'
|
||||
import { button, type ButtonVariantProps } from '@styled-system/recipes'
|
||||
import { Box, type BoxProps } from '../Box/Box'
|
||||
import { Spinner } from '../Spinner/Spinner'
|
||||
import { splitProps } from '~/utils/splitProps'
|
||||
|
||||
export type ButtonProps =
|
||||
BoxProps &
|
||||
ButtonVariantProps &
|
||||
{
|
||||
loading?: boolean
|
||||
leftIcon?: ReactNode
|
||||
rightIcon?: ReactNode
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const Button: FC<ButtonProps> = ({
|
||||
variant,
|
||||
size,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const [className, otherProps] = splitProps(props)
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="button"
|
||||
className={cx(button({ variant, size }), className)}
|
||||
disabled={loading || disabled}
|
||||
aria-busy={loading} // Accessibility: announce loading state
|
||||
{...otherProps}
|
||||
>
|
||||
{/* Show spinner when loading */}
|
||||
{loading && <Spinner size={size} />}
|
||||
|
||||
{/* Show left icon if not loading */}
|
||||
{!loading && leftIcon}
|
||||
|
||||
{/* Button text */}
|
||||
<span>{children}</span>
|
||||
|
||||
{/* Right icon */}
|
||||
{!loading && rightIcon}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Patterns
|
||||
|
||||
### Extract Recipe Types
|
||||
|
||||
```typescript
|
||||
import { button, type ButtonVariantProps } from '@styled-system/recipes'
|
||||
|
||||
// ButtonVariantProps includes:
|
||||
// - variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
|
||||
// - size?: 'small' | 'medium' | 'large'
|
||||
```
|
||||
|
||||
**Pattern**: Always use generated variant types for prop types.
|
||||
|
||||
### Omit Conflicting Props
|
||||
|
||||
```typescript
|
||||
import { text, type TextVariantProps } from '@styled-system/recipes'
|
||||
|
||||
// Avoid prop conflicts between Box and recipe
|
||||
export type TextProps =
|
||||
Omit<BoxProps, keyof TextVariantProps> & // Remove conflicts
|
||||
TextVariantProps &
|
||||
{
|
||||
children: ReactNode
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Prevents TypeScript errors when Box and recipe define same props.
|
||||
|
||||
### Conditional Value Types
|
||||
|
||||
For responsive/theme-aware props:
|
||||
|
||||
```typescript
|
||||
import { type ConditionalValue } from '@styled-system/types'
|
||||
import { type ColorToken } from '@styled-system/tokens'
|
||||
|
||||
export type IconProps = {
|
||||
fill?: ConditionalValue<ColorToken> // Enables: fill="blue.50" or fill={{ base: 'blue.50', _dark: 'blue.40' }}
|
||||
}
|
||||
```
|
||||
|
||||
### Component Props Pattern
|
||||
|
||||
```typescript
|
||||
import { type ComponentPropsWithoutRef } from 'react'
|
||||
|
||||
// Get all props for a specific HTML element
|
||||
export type InputProps = ComponentPropsWithoutRef<'input'> & {
|
||||
// Custom props
|
||||
}
|
||||
|
||||
// For polymorphic components
|
||||
export type BoxProps = ComponentPropsWithoutRef<ElementType> & {
|
||||
as?: ElementType
|
||||
}
|
||||
```
|
||||
|
||||
## Accessibility Patterns
|
||||
|
||||
### Always Include focusVisible
|
||||
|
||||
In recipes:
|
||||
```typescript
|
||||
base: {
|
||||
_focusVisible: {
|
||||
outlineWidth: '2',
|
||||
outlineOffset: '1',
|
||||
outlineColor: { base: 'blue.50', _dark: 'blue.40' }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Provides visible focus indication for keyboard navigation.
|
||||
|
||||
### ARIA Attributes
|
||||
|
||||
```typescript
|
||||
export const Button: FC<ButtonProps> = ({ loading, disabled, ...props }) => {
|
||||
return (
|
||||
<button
|
||||
disabled={loading || disabled}
|
||||
aria-disabled={disabled}
|
||||
aria-busy={loading}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Common ARIA Attributes**:
|
||||
- `aria-label`: Label for screen readers
|
||||
- `aria-disabled`: Disabled state
|
||||
- `aria-busy`: Loading state
|
||||
- `aria-checked`: Checkbox/radio state
|
||||
- `aria-expanded`: Collapsed/expanded state
|
||||
- `aria-pressed`: Toggle button state
|
||||
|
||||
### Keyboard Interaction
|
||||
|
||||
```typescript
|
||||
export const MenuItem: FC<MenuItemProps> = ({ onClick, ...props }) => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === ' ' || e.key === 'Spacebar' || e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
onClick?.(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={onClick}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Ensure keyboard users can interact with custom components.
|
||||
|
||||
### Match Multiple State Selectors
|
||||
|
||||
In recipes, support both native and custom states:
|
||||
|
||||
```typescript
|
||||
conditions: {
|
||||
checked: '&:is(:checked, [data-checked], [aria-checked=true], [data-state="checked"])'
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Works with native inputs AND custom components.
|
||||
|
||||
## Icon Component Pattern
|
||||
|
||||
Icons often need special handling for sizing and color.
|
||||
|
||||
Create: `src/components/Icon/Icon.tsx`
|
||||
|
||||
```typescript
|
||||
import { type FC } from 'react'
|
||||
import { cx } from '@styled-system/css'
|
||||
import { icon } from '@styled-system/patterns'
|
||||
import type { ConditionalValue } from '@styled-system/types'
|
||||
import type { ColorToken } from '@styled-system/tokens'
|
||||
import { Box, type BoxProps } from '../Box/Box'
|
||||
import { splitProps } from '~/utils/splitProps'
|
||||
import { numericSizes } from '~/styles/tokens'
|
||||
|
||||
// Constrain size to numeric tokens only
|
||||
export type AllowedIconSizes = keyof typeof numericSizes
|
||||
|
||||
export type IconProps =
|
||||
Omit<BoxProps, 'size'> & // Remove BoxProps size
|
||||
{
|
||||
name: string // Icon identifier
|
||||
size?: AllowedIconSizes
|
||||
fill?: ConditionalValue<ColorToken>
|
||||
}
|
||||
|
||||
export const Icon: FC<IconProps> = ({
|
||||
name,
|
||||
size = '24',
|
||||
fill = 'currentColor',
|
||||
...props
|
||||
}) => {
|
||||
const [className, otherProps] = splitProps(props)
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="svg"
|
||||
viewBox="0 0 24 24"
|
||||
className={cx(icon({ size }), className)} // icon pattern sets width + height
|
||||
fill="none"
|
||||
stroke={fill === 'currentColor' ? fill : undefined}
|
||||
{...otherProps}
|
||||
>
|
||||
{/* SVG sprite reference */}
|
||||
<use xlinkHref={`/sprite.svg#${name}`} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern**: Custom icon pattern enforces square sizing via tokens.
|
||||
|
||||
## Responsive Components
|
||||
|
||||
### Responsive Props
|
||||
|
||||
Use object syntax for breakpoint-based props:
|
||||
|
||||
```typescript
|
||||
<Box
|
||||
px={{ base: '16', md: '20', lg: '24' }}
|
||||
fontSize={{ base: 'sm', md: 'md' }}
|
||||
display={{ base: 'block', lg: 'flex' }}
|
||||
/>
|
||||
```
|
||||
|
||||
### Container Queries
|
||||
|
||||
For component-level responsive design:
|
||||
|
||||
```typescript
|
||||
<Box
|
||||
containerType="inline-size" // Enable container queries
|
||||
width="full"
|
||||
>
|
||||
<Box
|
||||
// Responsive based on CONTAINER size, not viewport
|
||||
p={{ base: '12', '@container(min-width: 400px)': '20' }}
|
||||
/>
|
||||
</Box>
|
||||
```
|
||||
|
||||
## Component Composition
|
||||
|
||||
### Compose with Box
|
||||
|
||||
```typescript
|
||||
export const Card: FC<BoxProps> = (props) => {
|
||||
return (
|
||||
<Box
|
||||
bg={{ base: 'white', _dark: 'slate.90' }}
|
||||
borderRadius="8"
|
||||
boxShadow="md"
|
||||
p="20"
|
||||
{...props} // Allow overrides
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern**: Provide sensible defaults, allow prop overrides.
|
||||
|
||||
### Compound Components
|
||||
|
||||
```typescript
|
||||
// Menu.tsx
|
||||
export const Menu: FC<MenuProps> = ({ children, ...props }) => {
|
||||
const { container } = menu()
|
||||
return <Box className={container} {...props}>{children}</Box>
|
||||
}
|
||||
|
||||
// MenuItem.tsx
|
||||
export const MenuItem: FC<MenuItemProps> = ({ children, ...props }) => {
|
||||
const { item } = menu() // Access same recipe
|
||||
return <Box className={item} {...props}>{children}</Box>
|
||||
}
|
||||
|
||||
// Usage
|
||||
<Menu>
|
||||
<MenuItem>Item 1</MenuItem>
|
||||
<MenuItem>Item 2</MenuItem>
|
||||
</Menu>
|
||||
```
|
||||
|
||||
## Best Practices Checklist
|
||||
|
||||
Create TodoWrite items when building components:
|
||||
|
||||
- [ ] Use Box as foundation for polymorphic components
|
||||
- [ ] Apply splitProps to separate CSS from HTML props
|
||||
- [ ] Import and use recipe variant types for TypeScript
|
||||
- [ ] Include ARIA attributes for accessibility
|
||||
- [ ] Add keyboard interaction for custom interactive elements
|
||||
- [ ] Use _focusVisible for visible focus states
|
||||
- [ ] Test component in light AND dark themes
|
||||
- [ ] Validate all variant combinations work correctly
|
||||
- [ ] Test with keyboard-only navigation
|
||||
- [ ] Test with screen reader (basic check)
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Avoid: Mixing CSS Approaches
|
||||
|
||||
```typescript
|
||||
// BAD: Mixing inline styles, Panda props, and classes
|
||||
<Box
|
||||
style={{ backgroundColor: 'red' }} // Inline style (avoid)
|
||||
bg="blue.50" // Panda CSS (good)
|
||||
className="custom-class" // External CSS (avoid)
|
||||
/>
|
||||
|
||||
// GOOD: Use Panda CSS exclusively
|
||||
<Box bg="blue.50" px="20" />
|
||||
```
|
||||
|
||||
### Avoid: Not Using Recipe Types
|
||||
|
||||
```typescript
|
||||
// BAD: Manual prop types (out of sync with recipe)
|
||||
type ButtonProps = {
|
||||
variant?: 'primary' | 'secondary'
|
||||
}
|
||||
|
||||
// GOOD: Use generated types
|
||||
import { type ButtonVariantProps } from '@styled-system/recipes'
|
||||
type ButtonProps = ButtonVariantProps
|
||||
```
|
||||
|
||||
### Avoid: Missing Accessibility
|
||||
|
||||
```typescript
|
||||
// BAD: No keyboard support, no ARIA
|
||||
<div onClick={handleClick}>Click me</div>
|
||||
|
||||
// GOOD: Proper semantics and keyboard support
|
||||
<button onClick={handleClick} aria-label="Action button">
|
||||
Click me
|
||||
</button>
|
||||
```
|
||||
|
||||
### Avoid: Over-wrapping
|
||||
|
||||
```typescript
|
||||
// BAD: Unnecessary div wrappers
|
||||
<Box>
|
||||
<Box>
|
||||
<Box>Content</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
// GOOD: Minimal, semantic structure
|
||||
<Box>Content</Box>
|
||||
```
|
||||
|
||||
## Accessing Official Panda CSS Docs
|
||||
|
||||
For component implementation patterns:
|
||||
|
||||
1. **Resolve library ID**: `mcp__MCP_DOCKER__resolve-library-id` with `libraryName: "panda-css"`
|
||||
2. **Fetch docs**: `mcp__MCP_DOCKER__get-library-docs` with:
|
||||
- `topic: "recipes"` - Using recipes in components
|
||||
- `topic: "typescript"` - TypeScript patterns
|
||||
- `topic: "patterns"` - Built-in patterns
|
||||
|
||||
## Exporting Components
|
||||
|
||||
Create: `src/components/Button/index.tsx`
|
||||
|
||||
```typescript
|
||||
export { Button } from './Button'
|
||||
export type { ButtonProps } from './Button'
|
||||
```
|
||||
|
||||
Create: `src/index.ts` (main library export)
|
||||
|
||||
```typescript
|
||||
// Components
|
||||
export { Box } from './components/Box'
|
||||
export { Button, IconButton } from './components/Button'
|
||||
export { Icon } from './components/Icon'
|
||||
export { CheckBox } from './components/CheckBox'
|
||||
// ... more components
|
||||
|
||||
// Types
|
||||
export type { BoxProps } from './components/Box'
|
||||
export type { ButtonProps, IconButtonProps } from './components/Button'
|
||||
export type { IconProps } from './components/Icon'
|
||||
// ... more types
|
||||
```
|
||||
|
||||
**Pattern**: Export both components and their prop types for consuming projects.
|
||||
|
||||
## Working Examples
|
||||
|
||||
Reference these files in the `examples/` directory for production-tested patterns:
|
||||
|
||||
**Utility Functions:**
|
||||
- `examples/utils/splitProps.ts` - CSS/HTML prop separation utility
|
||||
```typescript
|
||||
// Separates Panda CSS props from HTML props
|
||||
const [className, htmlProps] = splitProps(props);
|
||||
// Uses splitCssProps, css(), and cx() to merge classNames
|
||||
```
|
||||
- `examples/utils/ThemeContext.tsx` - Theme provider with localStorage persistence
|
||||
```typescript
|
||||
// Manages light/dark theme with system preference detection
|
||||
// Persists user preference to localStorage
|
||||
// Applies theme class to document root
|
||||
```
|
||||
|
||||
**Token & Configuration Integration:**
|
||||
- `examples/preset.ts` - Shows how to integrate tokens with components via preset
|
||||
- `examples/conditions.ts` - Custom conditions for component states
|
||||
- `examples/textStyles.ts` - Typography presets for text components
|
||||
|
||||
**For Complete Component Examples:**
|
||||
While this plugin focuses on architecture patterns, you can reference:
|
||||
- The skills themselves (this file, panda-recipe-patterns) contain inline component examples
|
||||
- Use the **panda-architect** agent for full component implementations
|
||||
1323
skills/panda-create-stories.md
Normal file
1323
skills/panda-create-stories.md
Normal file
File diff suppressed because it is too large
Load Diff
930
skills/panda-form-architecture.md
Normal file
930
skills/panda-form-architecture.md
Normal file
@@ -0,0 +1,930 @@
|
||||
---
|
||||
name: panda-form-architecture
|
||||
description: Design and implement composable form component architectures using atomic design principles with Panda CSS
|
||||
---
|
||||
|
||||
# Panda CSS Form Architecture
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Building a form component system from scratch
|
||||
- Refactoring existing forms to use composable patterns
|
||||
- Creating form field wrappers for consistent accessibility and error handling
|
||||
- Implementing a design system's form components
|
||||
- Establishing form component hierarchy and composition patterns
|
||||
|
||||
For implementing individual form components (buttons, inputs), also reference **panda-component-impl**.
|
||||
For creating recipes for these components, use **panda-recipe-patterns**.
|
||||
|
||||
## Form Component Composability Philosophy
|
||||
|
||||
Form components should follow a progressive composition model where simpler components combine into more complex ones. This atomic design approach creates:
|
||||
- **Reusability**: Foundational components work in multiple contexts
|
||||
- **Consistency**: Shared primitives ensure visual and behavioral uniformity
|
||||
- **Flexibility**: Compose components differently for different use cases
|
||||
- **Maintainability**: Changes to primitives cascade to all consumers
|
||||
|
||||
## Three-Layer Architecture
|
||||
|
||||
### Layer 1: Atomic Components (Primitives)
|
||||
|
||||
The foundational styled elements that map directly to HTML form controls.
|
||||
|
||||
**Characteristics:**
|
||||
- Single responsibility (one HTML element)
|
||||
- No internal composition
|
||||
- Accept Panda CSS style props for customization
|
||||
- Minimal logic (mostly styling)
|
||||
|
||||
**Components:**
|
||||
```typescript
|
||||
<Box> // Polymorphic base (any HTML element)
|
||||
<Button> // Styled <button> or <a>
|
||||
<TextInput> // Styled <input type="text">
|
||||
<Textarea> // Styled <textarea>
|
||||
<CheckBox> // Styled <input type="checkbox">
|
||||
<Toggle> // Styled <input type="checkbox"> for toggle switches
|
||||
<Radio> // Styled <input type="radio">
|
||||
<Text> // Styled text for labels, help text, errors
|
||||
<Label> // Styled <label> element
|
||||
```
|
||||
|
||||
**Example Implementation (Atomic):**
|
||||
|
||||
Create: `src/components/TextInput/TextInput.tsx`
|
||||
|
||||
```typescript
|
||||
import { type FC, type InputHTMLAttributes } from 'react'
|
||||
import { cx } from '@styled-system/css'
|
||||
import { textInput, type TextInputVariantProps } from '@styled-system/recipes'
|
||||
import { Box, type BoxProps } from '../Box/Box'
|
||||
import { splitProps } from '~/utils/splitProps'
|
||||
|
||||
export type TextInputProps =
|
||||
Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> &
|
||||
BoxProps &
|
||||
TextInputVariantProps &
|
||||
{
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
export const TextInput: FC<TextInputProps> = ({
|
||||
size,
|
||||
error = false,
|
||||
...props
|
||||
}) => {
|
||||
const [className, otherProps] = splitProps(props)
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="input"
|
||||
type="text"
|
||||
className={cx(textInput({ size }), className)}
|
||||
{...(error && { 'data-error': true })}
|
||||
aria-invalid={error}
|
||||
{...otherProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Simple wrapper around native input
|
||||
- Applies Panda CSS recipe for styling
|
||||
- Supports error state via data attribute
|
||||
- Includes ARIA attribute for accessibility
|
||||
- Allows style overrides via props
|
||||
|
||||
### Layer 2: Molecular Components (Composed Primitives)
|
||||
|
||||
Combine atomic components for common patterns. These handle basic composition without complex logic.
|
||||
|
||||
**Characteristics:**
|
||||
- Combine 2-3 atomic components
|
||||
- Handle common use cases (input + label)
|
||||
- Still relatively simple
|
||||
- Improve ergonomics (less boilerplate for consumers)
|
||||
|
||||
**Components:**
|
||||
```typescript
|
||||
<ToggleInput> // Toggle + Label
|
||||
<CheckboxInput> // CheckBox + Label
|
||||
<RadioInput> // Radio + Label
|
||||
```
|
||||
|
||||
**Example Implementation (Molecular):**
|
||||
|
||||
Create: `src/components/CheckboxInput/CheckboxInput.tsx`
|
||||
|
||||
```typescript
|
||||
import { type FC, type InputHTMLAttributes, type ReactNode } from 'react'
|
||||
import { cx } from '@styled-system/css'
|
||||
import { checkboxInput } from '@styled-system/recipes'
|
||||
import { CheckBox, type CheckBoxProps } from '../CheckBox/CheckBox'
|
||||
import { Box } from '../Box/Box'
|
||||
import { Text } from '../Text/Text'
|
||||
|
||||
export type CheckboxInputProps =
|
||||
Omit<CheckBoxProps, 'label'> &
|
||||
{
|
||||
label?: ReactNode
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const CheckboxInput: FC<CheckboxInputProps> = ({
|
||||
label,
|
||||
description,
|
||||
size,
|
||||
id,
|
||||
...props
|
||||
}) => {
|
||||
// Generate ID if not provided (for label/input association)
|
||||
const inputId = id || `checkbox-${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
const { container, labelText, descriptionText } = checkboxInput({ size })
|
||||
|
||||
return (
|
||||
<Box as="label" htmlFor={inputId} className={container}>
|
||||
<CheckBox
|
||||
id={inputId}
|
||||
size={size}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{label && (
|
||||
<Box className={labelText}>
|
||||
{label}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{description && (
|
||||
<Text className={descriptionText} color="gray.60">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern Breakdown:**
|
||||
1. Wraps CheckBox (atomic) with label and description
|
||||
2. Auto-generates ID for accessibility if not provided
|
||||
3. Uses slot recipe for layout styling
|
||||
4. Accepts ReactNode for flexible label content
|
||||
5. Optional description for additional context
|
||||
|
||||
**When to Use Molecular vs Atomic:**
|
||||
- Use **atomic** when you need maximum flexibility and custom layouts
|
||||
- Use **molecular** for standard form layouts (label beside/above input)
|
||||
- Both should be available in your component library
|
||||
|
||||
### Layer 3: Organism Components (Complex Wrappers)
|
||||
|
||||
Higher-level components that provide structure, accessibility features, and common patterns like error handling.
|
||||
|
||||
**Characteristics:**
|
||||
- Orchestrate multiple components
|
||||
- Provide consistent patterns (labels, help text, errors)
|
||||
- Handle accessibility concerns (ARIA attributes, ID linking)
|
||||
- Accept children for maximum flexibility
|
||||
|
||||
**Primary Component: FormField**
|
||||
|
||||
`FormField` is a critical wrapper that provides:
|
||||
- Consistent label/input/error layout
|
||||
- Automatic accessibility (aria-describedby, aria-invalid)
|
||||
- Error and help text display
|
||||
- Required field indication
|
||||
|
||||
**Example Implementation (Organism):**
|
||||
|
||||
Create: `src/components/FormField/FormField.tsx`
|
||||
|
||||
```typescript
|
||||
import { type FC, type ReactNode, type ReactElement, cloneElement, isValidElement } from 'react'
|
||||
import { formField } from '@styled-system/recipes'
|
||||
import { Box } from '../Box/Box'
|
||||
import { Label } from '../Label/Label'
|
||||
import { Text } from '../Text/Text'
|
||||
|
||||
export type FormFieldProps = {
|
||||
label: string
|
||||
helpText?: string
|
||||
errorText?: string
|
||||
required?: boolean
|
||||
children: ReactNode
|
||||
htmlFor?: string
|
||||
}
|
||||
|
||||
export const FormField: FC<FormFieldProps> = ({
|
||||
label,
|
||||
helpText,
|
||||
errorText,
|
||||
required = false,
|
||||
children,
|
||||
htmlFor,
|
||||
}) => {
|
||||
// Generate IDs for accessibility linking
|
||||
const fieldId = htmlFor || `field-${Math.random().toString(36).substr(2, 9)}`
|
||||
const helpTextId = `${fieldId}-help`
|
||||
const errorTextId = `${fieldId}-error`
|
||||
|
||||
const { container, labelSlot, helpTextSlot, errorTextSlot } = formField()
|
||||
|
||||
// Clone children to inject ARIA attributes
|
||||
const enhancedChildren = isValidElement(children)
|
||||
? cloneElement(children as ReactElement<any>, {
|
||||
id: fieldId,
|
||||
'aria-describedby': [
|
||||
helpText ? helpTextId : null,
|
||||
errorText ? errorTextId : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ') || undefined,
|
||||
'aria-invalid': !!errorText,
|
||||
'aria-required': required,
|
||||
})
|
||||
: children
|
||||
|
||||
return (
|
||||
<Box className={container}>
|
||||
<Label htmlFor={fieldId} className={labelSlot}>
|
||||
{label}
|
||||
{required && (
|
||||
<Text as="span" color="red.50" aria-label="required">
|
||||
{' '}*
|
||||
</Text>
|
||||
)}
|
||||
</Label>
|
||||
|
||||
{helpText && (
|
||||
<Text id={helpTextId} className={helpTextSlot} color="gray.60">
|
||||
{helpText}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{enhancedChildren}
|
||||
|
||||
{errorText && (
|
||||
<Text
|
||||
id={errorTextId}
|
||||
className={errorTextSlot}
|
||||
color="red.50"
|
||||
role="alert"
|
||||
>
|
||||
{errorText}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Advanced Pattern Breakdown:**
|
||||
1. **Auto-generates IDs**: Ensures proper label/input/error association
|
||||
2. **Clones children**: Injects ARIA attributes into child input
|
||||
3. **aria-describedby**: Links input to help text and errors
|
||||
4. **aria-invalid**: Marks input as invalid when error present
|
||||
5. **aria-required**: Indicates required fields to screen readers
|
||||
6. **role="alert"**: Announces errors to screen readers immediately
|
||||
|
||||
**Accessibility Features:**
|
||||
- Proper label/input association via `htmlFor` and `id`
|
||||
- Help text linked via `aria-describedby`
|
||||
- Error text linked via `aria-describedby` and marked as `role="alert"`
|
||||
- Required fields indicated both visually (*) and semantically
|
||||
- Screen reader support through proper ARIA attributes
|
||||
|
||||
## Full Form Implementation Example
|
||||
|
||||
Here's how all three layers compose into a complete, accessible form:
|
||||
|
||||
Create: `src/pages/UserProfileForm.tsx`
|
||||
|
||||
```typescript
|
||||
import { type FC, type FormEvent, useState } from 'react'
|
||||
import { Box } from '~/components/Box/Box'
|
||||
import { FormField } from '~/components/FormField/FormField'
|
||||
import { TextInput } from '~/components/TextInput/TextInput'
|
||||
import { Textarea } from '~/components/Textarea/Textarea'
|
||||
import { RadioInput } from '~/components/RadioInput/RadioInput'
|
||||
import { CheckboxInput } from '~/components/CheckboxInput/CheckboxInput'
|
||||
import { Button } from '~/components/Button/Button'
|
||||
|
||||
export const UserProfileForm: FC = () => {
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
|
||||
const favColors = ['blue', 'red', 'yellow', 'green']
|
||||
|
||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
|
||||
// Form validation logic here
|
||||
const formData = new FormData(e.currentTarget)
|
||||
|
||||
// Example validation
|
||||
const firstName = formData.get('firstName') as string
|
||||
if (!firstName) {
|
||||
setErrors({ firstName: 'First name is required' })
|
||||
return
|
||||
}
|
||||
|
||||
// Submit form...
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="form"
|
||||
onSubmit={handleSubmit}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
gap="24"
|
||||
maxWidth="600px"
|
||||
margin="0 auto"
|
||||
p="32"
|
||||
>
|
||||
<FormField
|
||||
label="First name"
|
||||
required
|
||||
errorText={errors.firstName}
|
||||
helpText="Enter your legal first name"
|
||||
>
|
||||
<TextInput
|
||||
name="firstName"
|
||||
placeholder="John"
|
||||
error={!!errors.firstName}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Last name"
|
||||
required
|
||||
errorText={errors.lastName}
|
||||
>
|
||||
<TextInput
|
||||
name="lastName"
|
||||
placeholder="Doe"
|
||||
error={!!errors.lastName}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Bio"
|
||||
helpText="Tell us about yourself (optional)"
|
||||
>
|
||||
<Textarea
|
||||
name="bio"
|
||||
placeholder="I'm a developer who loves..."
|
||||
rows={4}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Favorite color" required>
|
||||
<Box display="flex" flexDirection="column" gap="12">
|
||||
{favColors.map((color) => (
|
||||
<RadioInput
|
||||
key={color}
|
||||
name="favoriteColor"
|
||||
value={color}
|
||||
label={color.charAt(0).toUpperCase() + color.slice(1)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</FormField>
|
||||
|
||||
<CheckboxInput
|
||||
name="newsletter"
|
||||
label="Subscribe to newsletter"
|
||||
description="Receive updates about new features and releases"
|
||||
/>
|
||||
|
||||
<CheckboxInput
|
||||
name="terms"
|
||||
label="I agree to the terms and conditions"
|
||||
required
|
||||
/>
|
||||
|
||||
<Box display="flex" justifyContent="flex-end" gap="12">
|
||||
<Button type="button" variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="primary">
|
||||
Save Profile
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Form Pattern Highlights:**
|
||||
1. **FormField wrapper**: Consistent layout for all fields
|
||||
2. **Error handling**: Centralized error state, passed to FormField
|
||||
3. **Help text**: Contextual guidance for users
|
||||
4. **Required indicators**: Visual and semantic marking
|
||||
5. **Accessible structure**: Proper labels, ARIA attributes, semantic HTML
|
||||
6. **Flexible composition**: Mix atomic and molecular components as needed
|
||||
7. **Layout control**: Panda CSS props for spacing and arrangement
|
||||
|
||||
## Recipe Architecture for Form Components
|
||||
|
||||
### Atomic Component Recipes
|
||||
|
||||
Each atomic component should have its own recipe:
|
||||
|
||||
Create: `src/styles/recipes/text-input.recipe.ts`
|
||||
|
||||
```typescript
|
||||
import { defineRecipe } from '@pandacss/dev'
|
||||
|
||||
export const textInputRecipe = defineRecipe({
|
||||
className: 'textInput',
|
||||
description: 'Text input field styles',
|
||||
|
||||
base: {
|
||||
width: 'full',
|
||||
px: '12',
|
||||
py: '8',
|
||||
fontSize: 'md',
|
||||
fontFamily: 'body',
|
||||
borderWidth: '1',
|
||||
borderColor: { base: 'gray.30', _dark: 'gray.70' },
|
||||
borderRadius: '6',
|
||||
bg: { base: 'white', _dark: 'slate.90' },
|
||||
color: { base: 'gray.90', _dark: 'gray.10' },
|
||||
transition: 'all 0.2s',
|
||||
|
||||
_placeholder: {
|
||||
color: { base: 'gray.50', _dark: 'gray.60' },
|
||||
},
|
||||
|
||||
_focus: {
|
||||
outline: 'none',
|
||||
borderColor: { base: 'blue.50', _dark: 'blue.40' },
|
||||
boxShadow: '0 0 0 3px token(colors.blue.20)',
|
||||
},
|
||||
|
||||
_disabled: {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed',
|
||||
bg: { base: 'gray.10', _dark: 'gray.80' },
|
||||
},
|
||||
|
||||
// Error state
|
||||
'&[data-error=true]': {
|
||||
borderColor: { base: 'red.50', _dark: 'red.40' },
|
||||
_focus: {
|
||||
boxShadow: '0 0 0 3px token(colors.red.20)',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
variants: {
|
||||
size: {
|
||||
sm: {
|
||||
px: '8',
|
||||
py: '6',
|
||||
fontSize: 'sm',
|
||||
},
|
||||
md: {
|
||||
px: '12',
|
||||
py: '8',
|
||||
fontSize: 'md',
|
||||
},
|
||||
lg: {
|
||||
px: '16',
|
||||
py: '12',
|
||||
fontSize: 'lg',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**Recipe Best Practices:**
|
||||
- Use data attributes for custom states (`data-error`)
|
||||
- Include all interactive states (_focus, _disabled, _hover)
|
||||
- Support both light and dark themes
|
||||
- Use semantic token references
|
||||
- Provide size variants
|
||||
|
||||
### Molecular Component Slot Recipes
|
||||
|
||||
Molecular components need layout coordination between primitives:
|
||||
|
||||
Create: `src/styles/recipes/checkbox-input.recipe.ts`
|
||||
|
||||
```typescript
|
||||
import { defineSlotRecipe } from '@pandacss/dev'
|
||||
|
||||
export const checkboxInputRecipe = defineSlotRecipe({
|
||||
className: 'checkboxInput',
|
||||
description: 'Checkbox with label composition',
|
||||
|
||||
slots: ['container', 'labelText', 'descriptionText'],
|
||||
|
||||
base: {
|
||||
container: {
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '12',
|
||||
cursor: 'pointer',
|
||||
|
||||
_hover: {
|
||||
'& input': {
|
||||
borderColor: { base: 'blue.40', _dark: 'blue.50' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
labelText: {
|
||||
fontSize: 'md',
|
||||
fontWeight: 'medium',
|
||||
color: { base: 'gray.90', _dark: 'gray.10' },
|
||||
lineHeight: '1.5',
|
||||
userSelect: 'none',
|
||||
},
|
||||
|
||||
descriptionText: {
|
||||
fontSize: 'sm',
|
||||
color: { base: 'gray.60', _dark: 'gray.50' },
|
||||
mt: '4',
|
||||
},
|
||||
},
|
||||
|
||||
variants: {
|
||||
size: {
|
||||
sm: {
|
||||
container: { gap: '8' },
|
||||
labelText: { fontSize: 'sm' },
|
||||
descriptionText: { fontSize: 'xs' },
|
||||
},
|
||||
md: {
|
||||
container: { gap: '12' },
|
||||
labelText: { fontSize: 'md' },
|
||||
descriptionText: { fontSize: 'sm' },
|
||||
},
|
||||
lg: {
|
||||
container: { gap: '16' },
|
||||
labelText: { fontSize: 'lg' },
|
||||
descriptionText: { fontSize: 'md' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**Slot Recipe Pattern:**
|
||||
- Define all component parts as slots
|
||||
- Coordinate sizing across slots with variants
|
||||
- Include hover states that affect children
|
||||
- Use userSelect: 'none' on labels for better UX
|
||||
|
||||
### Organism Component Slot Recipes
|
||||
|
||||
FormField needs comprehensive slot management:
|
||||
|
||||
Create: `src/styles/recipes/form-field.recipe.ts`
|
||||
|
||||
```typescript
|
||||
import { defineSlotRecipe } from '@pandacss/dev'
|
||||
|
||||
export const formFieldRecipe = defineSlotRecipe({
|
||||
className: 'formField',
|
||||
description: 'Form field wrapper with label, help text, and error text',
|
||||
|
||||
slots: ['container', 'labelSlot', 'helpTextSlot', 'errorTextSlot'],
|
||||
|
||||
base: {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8',
|
||||
width: 'full',
|
||||
},
|
||||
|
||||
labelSlot: {
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'semibold',
|
||||
color: { base: 'gray.90', _dark: 'gray.10' },
|
||||
mb: '4',
|
||||
},
|
||||
|
||||
helpTextSlot: {
|
||||
fontSize: 'sm',
|
||||
color: { base: 'gray.60', _dark: 'gray.50' },
|
||||
mt: '4',
|
||||
},
|
||||
|
||||
errorTextSlot: {
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: { base: 'red.60', _dark: 'red.40' },
|
||||
mt: '4',
|
||||
|
||||
// Icon support
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6',
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Best Practices Checklist
|
||||
|
||||
When building form architecture, create TodoWrite items for:
|
||||
|
||||
- [ ] Create atomic components (TextInput, CheckBox, Radio, etc.)
|
||||
- [ ] Create recipes for atomic components with all variants
|
||||
- [ ] Implement molecular compositions (CheckboxInput, RadioInput, etc.)
|
||||
- [ ] Create slot recipes for molecular components
|
||||
- [ ] Build FormField organism with accessibility features
|
||||
- [ ] Create FormField slot recipe
|
||||
- [ ] Test all components with keyboard navigation
|
||||
- [ ] Test all components with screen reader
|
||||
- [ ] Verify ARIA attributes are properly applied
|
||||
- [ ] Test error states and error announcements
|
||||
- [ ] Verify required field indicators work
|
||||
- [ ] Test form submission and validation flow
|
||||
- [ ] Verify light and dark theme support
|
||||
- [ ] Test responsive behavior on mobile devices
|
||||
- [ ] Create Storybook stories for all form components
|
||||
- [ ] Document composition patterns and usage examples
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Avoid: Skipping the FormField Wrapper
|
||||
|
||||
```typescript
|
||||
// BAD: Manual label/error handling (inconsistent, inaccessible)
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<TextInput id="email" />
|
||||
{error && <span style={{ color: 'red' }}>{error}</span>}
|
||||
</div>
|
||||
|
||||
// GOOD: Use FormField for consistency
|
||||
<FormField label="Email" errorText={error}>
|
||||
<TextInput />
|
||||
</FormField>
|
||||
```
|
||||
|
||||
**Why**: FormField ensures consistent accessibility, ARIA attributes, and visual design.
|
||||
|
||||
### Avoid: Not Linking Help Text and Errors
|
||||
|
||||
```typescript
|
||||
// BAD: Screen readers can't connect help text to input
|
||||
<Label>First name</Label>
|
||||
<Text>Enter your legal name</Text>
|
||||
<TextInput />
|
||||
|
||||
// GOOD: FormField automatically links via aria-describedby
|
||||
<FormField label="First name" helpText="Enter your legal name">
|
||||
<TextInput />
|
||||
</FormField>
|
||||
```
|
||||
|
||||
**Why**: Proper ARIA linking helps screen reader users understand context.
|
||||
|
||||
### Avoid: Over-composing Too Early
|
||||
|
||||
```typescript
|
||||
// BAD: Creating rigid mega-components
|
||||
<MagicFormInput
|
||||
label="Email"
|
||||
type="email"
|
||||
helpText="..."
|
||||
errorText="..."
|
||||
icon="email"
|
||||
suffix="@company.com"
|
||||
tooltip="..."
|
||||
/>
|
||||
|
||||
// GOOD: Compose flexibly from primitives
|
||||
<FormField label="Email" helpText="..." errorText="...">
|
||||
<Box display="flex" alignItems="center">
|
||||
<Icon name="email" />
|
||||
<TextInput type="email" />
|
||||
<Text>@company.com</Text>
|
||||
</Box>
|
||||
</FormField>
|
||||
```
|
||||
|
||||
**Why**: Flexible composition beats rigid mega-components. Keep primitives simple, compose as needed.
|
||||
|
||||
### Avoid: Inconsistent Error Handling
|
||||
|
||||
```typescript
|
||||
// BAD: Different error patterns across forms
|
||||
<TextInput className={error ? 'error' : ''} />
|
||||
<CheckBox style={{ borderColor: error ? 'red' : 'gray' }} />
|
||||
|
||||
// GOOD: Consistent error prop
|
||||
<TextInput error={!!errors.email} />
|
||||
<CheckBox error={!!errors.terms} />
|
||||
```
|
||||
|
||||
**Why**: Consistent error APIs make forms predictable and maintainable.
|
||||
|
||||
## Form Validation Integration
|
||||
|
||||
FormField works seamlessly with form libraries:
|
||||
|
||||
### React Hook Form Example
|
||||
|
||||
```typescript
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
export const ProfileForm = () => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm()
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
label="Email"
|
||||
required
|
||||
errorText={errors.email?.message}
|
||||
>
|
||||
<TextInput
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
pattern: {
|
||||
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
message: 'Invalid email address',
|
||||
},
|
||||
})}
|
||||
error={!!errors.email}
|
||||
/>
|
||||
</FormField>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Formik Example
|
||||
|
||||
```typescript
|
||||
import { Formik, Form } from 'formik'
|
||||
|
||||
export const ProfileForm = () => {
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{ email: '' }}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ errors, touched, values, handleChange }) => (
|
||||
<Form>
|
||||
<FormField
|
||||
label="Email"
|
||||
required
|
||||
errorText={touched.email && errors.email}
|
||||
>
|
||||
<TextInput
|
||||
name="email"
|
||||
value={values.email}
|
||||
onChange={handleChange}
|
||||
error={!!(touched.email && errors.email)}
|
||||
/>
|
||||
</FormField>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Progressive Enhancement
|
||||
|
||||
Start simple, add complexity as needed:
|
||||
|
||||
**Phase 1: Atomic components only**
|
||||
```typescript
|
||||
<Box as="form">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<TextInput id="email" name="email" />
|
||||
<Button type="submit">Submit</Button>
|
||||
</Box>
|
||||
```
|
||||
|
||||
**Phase 2: Add molecular compositions**
|
||||
```typescript
|
||||
<Box as="form">
|
||||
<CheckboxInput name="terms" label="I agree" />
|
||||
<Button type="submit">Submit</Button>
|
||||
</Box>
|
||||
```
|
||||
|
||||
**Phase 3: Add organism wrapper**
|
||||
```typescript
|
||||
<Box as="form">
|
||||
<FormField label="Email" helpText="...">
|
||||
<TextInput name="email" />
|
||||
</FormField>
|
||||
<Button type="submit">Submit</Button>
|
||||
</Box>
|
||||
```
|
||||
|
||||
**Phase 4: Full validation and error handling**
|
||||
```typescript
|
||||
<Box as="form" onSubmit={handleSubmit}>
|
||||
<FormField
|
||||
label="Email"
|
||||
required
|
||||
helpText="We'll never share your email"
|
||||
errorText={errors.email}
|
||||
>
|
||||
<TextInput
|
||||
name="email"
|
||||
error={!!errors.email}
|
||||
/>
|
||||
</FormField>
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
</Box>
|
||||
```
|
||||
|
||||
## Testing Form Components
|
||||
|
||||
### Accessibility Testing
|
||||
|
||||
```typescript
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { axe, toHaveNoViolations } from 'jest-axe'
|
||||
import { FormField } from './FormField'
|
||||
import { TextInput } from '../TextInput/TextInput'
|
||||
|
||||
expect.extend(toHaveNoViolations)
|
||||
|
||||
test('FormField has no accessibility violations', async () => {
|
||||
const { container } = render(
|
||||
<FormField label="Email" helpText="Enter your email">
|
||||
<TextInput />
|
||||
</FormField>
|
||||
)
|
||||
|
||||
const results = await axe(container)
|
||||
expect(results).toHaveNoViolations()
|
||||
})
|
||||
|
||||
test('FormField properly links label and input', () => {
|
||||
render(
|
||||
<FormField label="Email">
|
||||
<TextInput />
|
||||
</FormField>
|
||||
)
|
||||
|
||||
const input = screen.getByLabelText('Email')
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('FormField announces errors to screen readers', () => {
|
||||
render(
|
||||
<FormField label="Email" errorText="Email is required">
|
||||
<TextInput />
|
||||
</FormField>
|
||||
)
|
||||
|
||||
const error = screen.getByRole('alert')
|
||||
expect(error).toHaveTextContent('Email is required')
|
||||
|
||||
const input = screen.getByLabelText('Email')
|
||||
expect(input).toHaveAttribute('aria-invalid', 'true')
|
||||
})
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
The form architecture follows a clear hierarchy:
|
||||
|
||||
**Atomic → Molecular → Organism**
|
||||
|
||||
1. **Atomic**: Individual styled form controls (TextInput, CheckBox, Button)
|
||||
2. **Molecular**: Simple compositions (CheckboxInput = CheckBox + Label)
|
||||
3. **Organism**: Complex wrappers (FormField = Label + HelpText + Input + ErrorText + ARIA)
|
||||
|
||||
**Key Principles:**
|
||||
- Compose upward, never downward
|
||||
- Provide both atomic and molecular variants
|
||||
- Use FormField for consistent accessibility
|
||||
- Keep primitives simple and flexible
|
||||
- Test accessibility thoroughly
|
||||
- Integrate with form libraries as needed
|
||||
|
||||
This architecture ensures your forms are accessible, maintainable, and consistent across your application.
|
||||
875
skills/panda-recipe-patterns.md
Normal file
875
skills/panda-recipe-patterns.md
Normal file
@@ -0,0 +1,875 @@
|
||||
---
|
||||
name: panda-recipe-patterns
|
||||
description: Create and organize recipes (regular + slot recipes), compound variants, and understand when to use recipes vs patterns vs inline CSS
|
||||
---
|
||||
|
||||
# Panda CSS Recipe Patterns
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Creating reusable component styles with variants
|
||||
- Building multi-part component styling (checkboxes, tooltips, menus)
|
||||
- Defining compound variants (multiple conditions)
|
||||
- Organizing component style libraries
|
||||
- Deciding between recipes, patterns, and inline CSS
|
||||
|
||||
For implementing these recipes in React components, use the **panda-component-impl** skill.
|
||||
|
||||
## When to Use What
|
||||
|
||||
### Use Recipes When:
|
||||
- Component has multiple style variants (e.g., button: primary, secondary, outline)
|
||||
- Component has size variants (small, medium, large)
|
||||
- Styles are reused across multiple instances
|
||||
- Need compound variants (combining multiple variant conditions)
|
||||
- Want to auto-apply styles to JSX components
|
||||
- Building a design system with consistent component APIs
|
||||
|
||||
### Use Patterns When:
|
||||
- Need computed/transformed styles (e.g., icon sizing that sets width=height)
|
||||
- Creating reusable layout primitives (stack, grid, container)
|
||||
- Want a props-based API for common styling tasks
|
||||
- Need to enforce constraints (e.g., size must be a valid token)
|
||||
|
||||
### Use Inline CSS When:
|
||||
- One-off styles specific to a single usage
|
||||
- Dynamic values from props or state
|
||||
- Component-specific overrides of recipe defaults
|
||||
- Rapid prototyping before extracting to recipe
|
||||
|
||||
**Example Decision Tree**:
|
||||
```
|
||||
Button component with variants? → Recipe
|
||||
Icon with size prop? → Pattern
|
||||
Unique spacing on one div? → Inline CSS
|
||||
Reusable card layout? → Recipe
|
||||
```
|
||||
|
||||
## Regular Recipes (Single-Part Components)
|
||||
|
||||
### Basic Recipe Structure
|
||||
|
||||
Create: `src/recipes/button.ts`
|
||||
|
||||
```typescript
|
||||
import { defineRecipe } from '@pandacss/dev';
|
||||
|
||||
const buttonBase = {
|
||||
position: 'relative',
|
||||
appearance: 'none',
|
||||
minWidth: '0',
|
||||
transitionDuration: 'fast',
|
||||
transitionProperty: 'background, border-color, color, box-shadow',
|
||||
transitionTimingFunction: 'default',
|
||||
userSelect: 'none',
|
||||
verticalAlign: 'middle',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4',
|
||||
fontFamily: 'body',
|
||||
fontSize: '16',
|
||||
fontWeight: 'medium',
|
||||
lineHeight: 'default',
|
||||
borderWidth: '1',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'transparent',
|
||||
borderRadius: '4',
|
||||
outlineWidth: '2',
|
||||
outlineStyle: 'solid',
|
||||
outlineColor: 'transparent',
|
||||
outlineOffset: '1',
|
||||
textDecoration: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: 'pointer',
|
||||
_disabled: {
|
||||
opacity: 0.4,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
_focusVisible: {
|
||||
outlineColor: { base: 'slate.80', _dark: 'slate.5' },
|
||||
},
|
||||
'& svg': {
|
||||
fill: 'current',
|
||||
},
|
||||
};
|
||||
|
||||
const buttonVariants = {
|
||||
variant: {
|
||||
primary: {
|
||||
bg: { base: 'slate.90', _dark: 'slate.5' },
|
||||
color: { base: 'slate.0', _dark: 'slate.90' },
|
||||
_hover: {
|
||||
bg: { base: 'slate.70', _dark: 'slate.10' },
|
||||
},
|
||||
_active: {
|
||||
bg: { base: 'slate.100', _dark: 'slate.20' },
|
||||
},
|
||||
_disabled: {
|
||||
_hover: {
|
||||
bg: { base: 'slate.90', _dark: 'slate.5' },
|
||||
},
|
||||
},
|
||||
_selected: {
|
||||
bg: { base: 'slate.5', _dark: 'slate.90' },
|
||||
color: { base: 'slate.90', _dark: 'slate.0' },
|
||||
},
|
||||
},
|
||||
standard: {
|
||||
bg: { base: 'slate.5', _dark: 'slate.70' },
|
||||
color: { base: 'slate.90', _dark: 'slate.0' },
|
||||
_hover: {
|
||||
bg: { base: 'slate.10', _dark: 'slate.60' },
|
||||
},
|
||||
_active: {
|
||||
bg: { base: 'slate.20', _dark: 'slate.80' },
|
||||
},
|
||||
_disabled: {
|
||||
_hover: {
|
||||
bg: { base: 'slate.5', _dark: 'slate.70' },
|
||||
},
|
||||
},
|
||||
_selected: {
|
||||
bg: { base: 'slate.90', _dark: 'slate.5' },
|
||||
color: { base: 'slate.0', _dark: 'slate.90' },
|
||||
},
|
||||
},
|
||||
hollow: {
|
||||
bg: 'transparent',
|
||||
borderColor: { base: 'slate.30', _dark: 'slate.60' },
|
||||
color: { base: 'slate.90', _dark: 'slate.0' },
|
||||
_hover: {
|
||||
bg: { base: 'slate.10', _dark: 'slate.60' },
|
||||
borderColor: { base: 'slate.10', _dark: 'slate.60' },
|
||||
},
|
||||
_active: {
|
||||
bg: { base: 'slate.20', _dark: 'slate.80' },
|
||||
borderColor: { base: 'slate.20', _dark: 'slate.80' },
|
||||
},
|
||||
_disabled: {
|
||||
_hover: {
|
||||
bg: 'transparent',
|
||||
},
|
||||
},
|
||||
_selected: {
|
||||
bg: { base: 'slate.90', _dark: 'slate.5' },
|
||||
color: { base: 'slate.0', _dark: 'slate.90' },
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
},
|
||||
ghost: {
|
||||
bg: 'transparent',
|
||||
color: { base: 'slate.90', _dark: 'slate.0' },
|
||||
_hover: {
|
||||
bg: { base: 'slate.10', _dark: 'slate.60' },
|
||||
},
|
||||
_active: {
|
||||
bg: { base: 'slate.20', _dark: 'slate.70' },
|
||||
},
|
||||
_disabled: {
|
||||
_hover: {
|
||||
bg: 'transparent',
|
||||
},
|
||||
},
|
||||
_selected: {
|
||||
bg: { base: 'slate.90', _dark: 'slate.5' },
|
||||
color: { base: 'slate.0', _dark: 'slate.90' },
|
||||
},
|
||||
},
|
||||
cta: {
|
||||
bg: { base: 'gold.20', _dark: 'gold.30' },
|
||||
color: 'slate.90',
|
||||
_hover: {
|
||||
bg: { base: 'gold.10', _dark: 'gold.20' },
|
||||
},
|
||||
_active: {
|
||||
bg: { base: 'gold.30', _dark: 'gold.40' },
|
||||
},
|
||||
_disabled: {
|
||||
_hover: {
|
||||
bg: { base: 'gold.20', _dark: 'gold.30' },
|
||||
},
|
||||
},
|
||||
},
|
||||
danger: {
|
||||
bg: 'red.50',
|
||||
color: 'slate.0',
|
||||
_hover: {
|
||||
bg: 'red.40',
|
||||
},
|
||||
_active: {
|
||||
bg: 'red.60',
|
||||
},
|
||||
_disabled: {
|
||||
_hover: {
|
||||
bg: 'red.50',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const buttonRecipe = defineRecipe({
|
||||
className: 'button',
|
||||
jsx: ['Button'],
|
||||
base: buttonBase,
|
||||
variants: {
|
||||
...buttonVariants,
|
||||
size: {
|
||||
medium: {
|
||||
fontSize: '16',
|
||||
py: '3',
|
||||
px: '10',
|
||||
},
|
||||
large: {
|
||||
fontSize: '16',
|
||||
py: '7',
|
||||
px: '12',
|
||||
},
|
||||
small: {
|
||||
fontSize: '14',
|
||||
py: '0',
|
||||
px: '8',
|
||||
'& svg': {
|
||||
mt: '-1',
|
||||
mb: '-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'standard',
|
||||
size: 'medium',
|
||||
},
|
||||
});
|
||||
|
||||
export const iconButtonRecipe = defineRecipe({
|
||||
className: 'icon-button',
|
||||
jsx: ['IconButton'],
|
||||
base: buttonBase,
|
||||
variants: {
|
||||
...buttonVariants,
|
||||
size: {
|
||||
medium: {
|
||||
fontSize: '16',
|
||||
p: '3',
|
||||
},
|
||||
large: {
|
||||
fontSize: '16',
|
||||
p: '7',
|
||||
},
|
||||
small: {
|
||||
fontSize: '14',
|
||||
p: '0',
|
||||
'& svg': {
|
||||
mt: '-1',
|
||||
mb: '-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'standard',
|
||||
size: 'medium',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Extract Base Styles for DRY Code
|
||||
|
||||
**Pattern**: Share base styles between related recipes:
|
||||
|
||||
```typescript
|
||||
// Shared base for button and iconButton
|
||||
const buttonBase = {
|
||||
appearance: 'none',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transitionDuration: 'fast',
|
||||
_disabled: { opacity: 0.4, cursor: 'not-allowed' },
|
||||
_focusVisible: { outlineWidth: '2', outlineColor: 'blue.50' }
|
||||
}
|
||||
|
||||
// Shared variant styles
|
||||
const buttonVariants = {
|
||||
variant: {
|
||||
primary: { /* ... */ },
|
||||
secondary: { /* ... */ }
|
||||
}
|
||||
}
|
||||
|
||||
// Button recipe
|
||||
export const buttonRecipe = defineRecipe({
|
||||
className: 'button',
|
||||
jsx: ['Button'],
|
||||
base: buttonBase,
|
||||
variants: {
|
||||
...buttonVariants,
|
||||
size: { /* button sizes */ }
|
||||
},
|
||||
defaultVariants: { variant: 'primary', size: 'medium' }
|
||||
})
|
||||
|
||||
// IconButton recipe (reuses base and variants)
|
||||
export const iconButtonRecipe = defineRecipe({
|
||||
className: 'iconButton',
|
||||
jsx: ['IconButton'],
|
||||
base: buttonBase,
|
||||
variants: {
|
||||
...buttonVariants,
|
||||
size: {
|
||||
small: { width: '32', height: '32' },
|
||||
medium: { width: '40', height: '40' },
|
||||
large: { width: '48', height: '48' }
|
||||
}
|
||||
},
|
||||
defaultVariants: { variant: 'primary', size: 'medium' }
|
||||
})
|
||||
```
|
||||
|
||||
**Why**: Keeps related components visually consistent, reduces duplication.
|
||||
|
||||
### Dynamic Variants from Tokens
|
||||
|
||||
**Pattern**: Generate variants from design tokens:
|
||||
|
||||
```typescript
|
||||
import { tokens } from '../styles/tokens'
|
||||
|
||||
// Get fontSize tokens
|
||||
const fontSizeTokens = tokens.fontSizes
|
||||
|
||||
type FontSizeKey = keyof typeof fontSizeTokens
|
||||
|
||||
// Generate fontSize variants dynamically
|
||||
const fontSizes = (Object.keys(fontSizeTokens) as FontSizeKey[]).reduce(
|
||||
(accumulator, currentKey) => {
|
||||
accumulator[currentKey] = { fontSize: currentKey }
|
||||
return accumulator
|
||||
},
|
||||
{} as Record<FontSizeKey, Record<'fontSize', string>>
|
||||
)
|
||||
|
||||
export const textRecipe = defineRecipe({
|
||||
className: 'text',
|
||||
jsx: ['Text'],
|
||||
variants: {
|
||||
size: fontSizes // All token sizes available as variants
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Why**: Automatically sync recipe variants with token changes.
|
||||
|
||||
## Slot Recipes (Multi-Part Components)
|
||||
|
||||
Use slot recipes for components with multiple styled parts (checkbox + label, tooltip + arrow, menu + items).
|
||||
|
||||
### Basic Slot Recipe Structure
|
||||
|
||||
Create: `src/recipes/checkbox.ts`
|
||||
|
||||
```typescript
|
||||
import { defineSlotRecipe } from '@pandacss/dev'
|
||||
|
||||
export const checkBoxRecipe = defineSlotRecipe({
|
||||
className: 'checkbox',
|
||||
description: 'Checkbox component with label',
|
||||
jsx: ['CheckBox'],
|
||||
|
||||
// Define named slots for component parts
|
||||
slots: ['container', 'input', 'indicator', 'label'],
|
||||
|
||||
// Base styles for each slot
|
||||
base: {
|
||||
container: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8',
|
||||
cursor: 'pointer',
|
||||
position: 'relative'
|
||||
},
|
||||
|
||||
input: {
|
||||
// Visually hidden but accessible
|
||||
position: 'absolute',
|
||||
opacity: 0,
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
|
||||
// Show different indicator icons based on state
|
||||
_checked: {
|
||||
"& ~ [data-part='indicator'][data-state='unchecked']": {
|
||||
display: 'none'
|
||||
},
|
||||
"& ~ [data-part='indicator'][data-state='checked']": {
|
||||
display: 'inline-flex'
|
||||
}
|
||||
},
|
||||
|
||||
_indeterminate: {
|
||||
"& ~ [data-part='indicator'][data-state='indeterminate']": {
|
||||
display: 'inline-flex'
|
||||
}
|
||||
},
|
||||
|
||||
_disabled: {
|
||||
"& ~ [data-part='indicator']": {
|
||||
opacity: 0.4,
|
||||
cursor: 'not-allowed'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
indicator: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
width: '20',
|
||||
height: '20',
|
||||
borderWidth: '1',
|
||||
borderRadius: '4',
|
||||
borderColor: { base: 'slate.40', _dark: 'slate.60' },
|
||||
bg: { base: 'white', _dark: 'slate.90' },
|
||||
color: { base: 'blue.50', _dark: 'blue.40' }
|
||||
},
|
||||
|
||||
label: {
|
||||
fontSize: 'md',
|
||||
color: { base: 'slate.90', _dark: 'slate.10' },
|
||||
userSelect: 'none'
|
||||
}
|
||||
},
|
||||
|
||||
// Variants apply to specific slots
|
||||
variants: {
|
||||
size: {
|
||||
small: {
|
||||
indicator: { width: '16', height: '16' },
|
||||
label: { fontSize: 'sm' }
|
||||
},
|
||||
medium: {
|
||||
indicator: { width: '20', height: '20' },
|
||||
label: { fontSize: 'md' }
|
||||
},
|
||||
large: {
|
||||
indicator: { width: '24', height: '24' },
|
||||
label: { fontSize: 'lg' }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
defaultVariants: {
|
||||
size: 'medium'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Complex State Handling
|
||||
|
||||
**Pattern**: Use sibling selectors for state-based styling:
|
||||
|
||||
```typescript
|
||||
base: {
|
||||
input: {
|
||||
// When checked, show checked icon, hide unchecked icon
|
||||
_checked: {
|
||||
"& ~ [data-part='indicator']": {
|
||||
bg: { base: 'blue.50', _dark: 'blue.40' },
|
||||
borderColor: { base: 'blue.50', _dark: 'blue.40' },
|
||||
color: 'white'
|
||||
}
|
||||
},
|
||||
|
||||
// When focused, add focus ring to indicator
|
||||
_focusVisible: {
|
||||
"& ~ [data-part='indicator']": {
|
||||
outlineWidth: '2',
|
||||
outlineOffset: '1',
|
||||
outlineColor: { base: 'blue.50', _dark: 'blue.40' }
|
||||
}
|
||||
},
|
||||
|
||||
// Error state
|
||||
_invalid: {
|
||||
"& ~ [data-part='indicator']": {
|
||||
borderColor: { base: 'red.50', _dark: 'red.40' }
|
||||
},
|
||||
"& ~ [data-part='label']": {
|
||||
color: { base: 'red.50', _dark: 'red.40' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Slot Recipe: Tooltip
|
||||
|
||||
Create: `src/recipes/tooltip.ts`
|
||||
|
||||
```typescript
|
||||
import { defineSlotRecipe } from '@pandacss/dev'
|
||||
|
||||
export const tooltipRecipe = defineSlotRecipe({
|
||||
className: 'tooltip',
|
||||
jsx: ['Tooltip'],
|
||||
slots: ['trigger', 'content', 'arrow'],
|
||||
|
||||
base: {
|
||||
trigger: {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
|
||||
content: {
|
||||
position: 'absolute',
|
||||
zIndex: 50,
|
||||
px: '12',
|
||||
py: '8',
|
||||
fontSize: 'sm',
|
||||
borderRadius: '6',
|
||||
bg: { base: 'slate.90', _dark: 'slate.10' },
|
||||
color: { base: 'white', _dark: 'slate.90' },
|
||||
boxShadow: 'lg',
|
||||
maxWidth: '320',
|
||||
|
||||
// Fade in/out animation
|
||||
opacity: 0,
|
||||
transitionProperty: 'opacity',
|
||||
transitionDuration: 'fast',
|
||||
_open: {
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
|
||||
arrow: {
|
||||
position: 'absolute',
|
||||
width: '8',
|
||||
height: '8',
|
||||
bg: { base: 'slate.90', _dark: 'slate.10' },
|
||||
transform: 'rotate(45deg)'
|
||||
}
|
||||
},
|
||||
|
||||
variants: {
|
||||
position: {
|
||||
top: {
|
||||
content: { bottom: 'full', left: '50%', transform: 'translateX(-50%)' },
|
||||
arrow: { bottom: '-4', left: '50%', transform: 'translateX(-50%) rotate(45deg)' }
|
||||
},
|
||||
'top-start': {
|
||||
content: { bottom: 'full', left: '0' },
|
||||
arrow: { bottom: '-4', left: '12' }
|
||||
},
|
||||
'top-end': {
|
||||
content: { bottom: 'full', right: '0' },
|
||||
arrow: { bottom: '-4', right: '12' }
|
||||
},
|
||||
bottom: {
|
||||
content: { top: 'full', left: '50%', transform: 'translateX(-50%)' },
|
||||
arrow: { top: '-4', left: '50%', transform: 'translateX(-50%) rotate(45deg)' }
|
||||
},
|
||||
left: {
|
||||
content: { right: 'full', top: '50%', transform: 'translateY(-50%)' },
|
||||
arrow: { right: '-4', top: '50%', transform: 'translateY(-50%) rotate(45deg)' }
|
||||
},
|
||||
right: {
|
||||
content: { left: 'full', top: '50%', transform: 'translateY(-50%)' },
|
||||
arrow: { left: '-4', top: '50%', transform: 'translateY(-50%) rotate(45deg)' }
|
||||
}
|
||||
},
|
||||
|
||||
// Option to hide arrow
|
||||
caret: {
|
||||
true: {},
|
||||
false: {
|
||||
arrow: { display: 'none' }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Compound variants: combine multiple variant conditions
|
||||
compoundVariants: [
|
||||
{
|
||||
position: ['top', 'top-start', 'top-end'],
|
||||
caret: true,
|
||||
css: {
|
||||
content: { mb: '12' } // Add margin when arrow is shown on top
|
||||
}
|
||||
},
|
||||
{
|
||||
position: ['bottom', 'bottom-start', 'bottom-end'],
|
||||
caret: true,
|
||||
css: {
|
||||
content: { mt: '12' }
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
defaultVariants: {
|
||||
position: 'top',
|
||||
caret: true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Compound Variants
|
||||
|
||||
Use compound variants when combining multiple variant values requires unique styling.
|
||||
|
||||
**Pattern**: Conditional styling based on variant combinations:
|
||||
|
||||
```typescript
|
||||
export const buttonRecipe = defineRecipe({
|
||||
variants: {
|
||||
variant: {
|
||||
primary: { /* ... */ },
|
||||
outline: { /* ... */ }
|
||||
},
|
||||
size: {
|
||||
small: { /* ... */ },
|
||||
large: { /* ... */ }
|
||||
},
|
||||
loading: {
|
||||
true: { cursor: 'wait' }
|
||||
}
|
||||
},
|
||||
|
||||
// Special styles when specific variants combine
|
||||
compoundVariants: [
|
||||
{
|
||||
variant: 'primary',
|
||||
loading: true,
|
||||
css: {
|
||||
bg: { base: 'blue.40', _dark: 'blue.30' }, // Lighter when loading
|
||||
_hover: { bg: { base: 'blue.40', _dark: 'blue.30' } } // Disable hover
|
||||
}
|
||||
},
|
||||
{
|
||||
variant: 'outline',
|
||||
size: 'small',
|
||||
css: {
|
||||
borderWidth: '1', // Thinner border for small outline
|
||||
fontWeight: 'medium'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
## Recipe Organization
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
recipes/
|
||||
index.ts # Export all recipes
|
||||
button.ts # Regular recipe
|
||||
input.ts # Regular recipe
|
||||
checkbox.ts # Slot recipe
|
||||
radio.ts # Slot recipe
|
||||
tooltip.ts # Slot recipe
|
||||
menu.ts # Slot recipe
|
||||
```
|
||||
|
||||
### Export Pattern
|
||||
|
||||
`src/recipes/index.ts`:
|
||||
|
||||
```typescript
|
||||
// Regular recipes
|
||||
export { buttonRecipe } from './button'
|
||||
export { iconButtonRecipe } from './button'
|
||||
export { inputRecipe } from './input'
|
||||
export { textRecipe } from './text'
|
||||
|
||||
// Slot recipes
|
||||
export { checkBoxRecipe } from './checkbox'
|
||||
export { radioRecipe } from './radio'
|
||||
export { tooltipRecipe } from './tooltip'
|
||||
export { menuRecipe } from './menu'
|
||||
```
|
||||
|
||||
### Register in Config
|
||||
|
||||
`panda.config.ts`:
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from '@pandacss/dev'
|
||||
import * as allRecipes from './src/recipes'
|
||||
|
||||
// Separate regular and slot recipes
|
||||
const {
|
||||
checkBoxRecipe,
|
||||
radioRecipe,
|
||||
tooltipRecipe,
|
||||
menuRecipe,
|
||||
...regularRecipes
|
||||
} = allRecipes
|
||||
|
||||
// Transform keys: remove 'Recipe' suffix
|
||||
const recipes = Object.fromEntries(
|
||||
Object.entries(regularRecipes).map(([key, value]) => [
|
||||
key.replace(/Recipe$/, ''), // buttonRecipe → button
|
||||
value
|
||||
])
|
||||
)
|
||||
|
||||
const slotRecipes = {
|
||||
checkbox: checkBoxRecipe,
|
||||
radio: radioRecipe,
|
||||
tooltip: tooltipRecipe,
|
||||
menu: menuRecipe
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
theme: {
|
||||
extend: {
|
||||
recipes,
|
||||
slotRecipes
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Why**: Clean separation, automatic recipe registration.
|
||||
|
||||
## Responsive Recipes
|
||||
|
||||
**Pattern**: Use object syntax for responsive variants:
|
||||
|
||||
```typescript
|
||||
export const cardRecipe = defineRecipe({
|
||||
base: {
|
||||
p: { base: '16', md: '20', lg: '24' }, // Responsive padding
|
||||
borderRadius: { base: '6', md: '8' },
|
||||
fontSize: { base: 'sm', md: 'md' }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Pattern**: Container queries for component-level responsive:
|
||||
|
||||
```typescript
|
||||
export const menuRecipe = defineSlotRecipe({
|
||||
base: {
|
||||
container: {
|
||||
// Change layout based on container size (not viewport)
|
||||
width: { base: 'full', '@container(min-width: 768px)': '260' },
|
||||
position: { base: 'fixed', '@container(min-width: 768px)': 'relative' }
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Best Practices Checklist
|
||||
|
||||
Create TodoWrite items when creating recipes:
|
||||
|
||||
- [ ] Extract shared base styles for related components
|
||||
- [ ] Use semantic tokens (not raw colors) in recipes
|
||||
- [ ] Define sensible defaultVariants
|
||||
- [ ] Include common state styles (_hover, _focus, _disabled, _active)
|
||||
- [ ] Add _focusVisible for accessibility
|
||||
- [ ] Use slot recipes for multi-part components
|
||||
- [ ] Document recipe purpose in description field
|
||||
- [ ] Use compound variants for complex combinations
|
||||
- [ ] Test all variant combinations
|
||||
- [ ] Validate recipes work in light AND dark themes
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Avoid: Hard-coded Values
|
||||
|
||||
```typescript
|
||||
// BAD: Hard-coded hex colors, px values
|
||||
variants: {
|
||||
primary: {
|
||||
bg: '#3B82F6',
|
||||
padding: '12px'
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Use design tokens
|
||||
variants: {
|
||||
primary: {
|
||||
bg: { base: 'blue.50', _dark: 'blue.40' },
|
||||
padding: '12'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid: Missing Default Variants
|
||||
|
||||
```typescript
|
||||
// BAD: No defaults, components render unstyled
|
||||
variants: {
|
||||
size: { small: {...}, large: {...} }
|
||||
}
|
||||
|
||||
// GOOD: Always provide defaults
|
||||
variants: {
|
||||
size: { small: {...}, medium: {...}, large: {...} }
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'medium'
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid: Over-nesting in Slot Recipes
|
||||
|
||||
```typescript
|
||||
// BAD: Deep nesting, hard to maintain
|
||||
base: {
|
||||
container: {
|
||||
'& > div > span > button': { /* ... */ }
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Use slots for structure
|
||||
slots: ['container', 'wrapper', 'label', 'button'],
|
||||
base: {
|
||||
container: { /* ... */ },
|
||||
button: { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid: Duplicate Logic Across Recipes
|
||||
|
||||
```typescript
|
||||
// BAD: Copy-paste same styles
|
||||
const button = { _disabled: { opacity: 0.4 } }
|
||||
const input = { _disabled: { opacity: 0.4 } }
|
||||
|
||||
// GOOD: Extract to shared constant
|
||||
const disabledStyles = { opacity: 0.4, cursor: 'not-allowed' }
|
||||
|
||||
const button = { _disabled: disabledStyles }
|
||||
const input = { _disabled: disabledStyles }
|
||||
```
|
||||
|
||||
## Accessing Official Panda CSS Docs
|
||||
|
||||
For recipe-specific documentation:
|
||||
|
||||
1. **Resolve library ID**: `mcp__MCP_DOCKER__resolve-library-id` with `libraryName: "panda-css"`
|
||||
2. **Fetch docs**: `mcp__MCP_DOCKER__get-library-docs` with `topic: "recipes"` or `topic: "slot-recipes"`
|
||||
|
||||
## Next Steps
|
||||
|
||||
After creating recipes:
|
||||
|
||||
1. **Implement in components**: Use **panda-component-impl** skill for React integration
|
||||
2. **Test variants**: Verify all combinations work visually
|
||||
3. **Document in Storybook**: Create stories showing all variants
|
||||
|
||||
## Reference Files from Best Practices Repo
|
||||
|
||||
- Regular recipe: `src/recipes/button.ts`
|
||||
- Slot recipe: `src/recipes/checkbox.ts`, `src/recipes/tooltip.ts`, `src/recipes/menu.ts`
|
||||
- Shared base: `src/recipes/button.ts` (buttonBase constant)
|
||||
- Dynamic variants: `src/recipes/text.ts` (fontSize generation)
|
||||
- Config registration: `panda.config.ts`, `cetec-preset.ts`
|
||||
533
skills/panda-review-component.md
Normal file
533
skills/panda-review-component.md
Normal file
@@ -0,0 +1,533 @@
|
||||
---
|
||||
name: panda-review-component
|
||||
description: Systematically assess an existing component against Panda CSS best practices, create prioritized recommendations, and implement approved changes
|
||||
---
|
||||
|
||||
# Panda CSS Component Review
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Reviewing an existing component for Panda CSS best practices compliance
|
||||
- Auditing components before production deployment
|
||||
- Refactoring components to use Panda CSS patterns
|
||||
- Onboarding legacy components to a Panda CSS design system
|
||||
- Identifying technical debt in component styling
|
||||
- Learning from existing implementations to improve other components
|
||||
|
||||
**Important**: This skill creates recommendations and presents them for approval BEFORE making any changes.
|
||||
|
||||
## Review Process Overview
|
||||
|
||||
The review follows a systematic four-phase approach:
|
||||
|
||||
1. **Discovery Phase**: Gather component code and context
|
||||
2. **Assessment Phase**: Evaluate against best practices using comprehensive checklist
|
||||
3. **Recommendation Phase**: Generate prioritized improvement list with explanations
|
||||
4. **Implementation Phase**: Execute approved changes (only after user confirmation)
|
||||
|
||||
## Phase 1: Discovery - Gather Component Context
|
||||
|
||||
### Step 1: Identify Component Files
|
||||
|
||||
Create TodoWrite items for discovery:
|
||||
- [ ] Locate component implementation file(s)
|
||||
- [ ] Find associated recipe/pattern definitions
|
||||
- [ ] Check for TypeScript type definitions
|
||||
- [ ] Identify any test files or stories
|
||||
- [ ] Review import statements and dependencies
|
||||
|
||||
Ask the user:
|
||||
- Component name or file path
|
||||
- Is this a standalone component or part of a component library?
|
||||
- Are there related components that should be reviewed together?
|
||||
|
||||
### Step 2: Read Component Code
|
||||
|
||||
Use Read tool to examine:
|
||||
- Component implementation (`.tsx`, `.jsx`)
|
||||
- Recipe definitions (if separate file)
|
||||
- Exported types and interfaces
|
||||
- Related utilities (splitProps, theme context, etc.)
|
||||
|
||||
### Step 3: Access Official Panda CSS Documentation
|
||||
|
||||
For accurate assessment, reference latest Panda CSS best practices:
|
||||
|
||||
```typescript
|
||||
// 1. Resolve Panda CSS library ID
|
||||
mcp__MCP_DOCKER__resolve-library-id({ libraryName: "panda-css" })
|
||||
|
||||
// 2. Fetch relevant documentation
|
||||
mcp__MCP_DOCKER__get-library-docs({
|
||||
context7CompatibleLibraryID: "/cschroeter/park-ui", // or resolved ID
|
||||
topic: "recipes" // or "patterns", "typescript", "styling"
|
||||
})
|
||||
```
|
||||
|
||||
**Topics to reference**:
|
||||
- `recipes` - Recipe patterns and slot recipes
|
||||
- `patterns` - Built-in patterns usage
|
||||
- `typescript` - Type integration
|
||||
- `styling` - Style prop patterns
|
||||
|
||||
## Phase 2: Assessment - Comprehensive Checklist
|
||||
|
||||
Create TodoWrite items for each assessment category:
|
||||
|
||||
### Architecture & Structure Assessment
|
||||
- [ ] **Component file organization**: Is the component in its own directory with proper exports?
|
||||
- [ ] **Import structure**: Are imports from correct Panda CSS packages?
|
||||
- [ ] **Base component usage**: Does it use Box as foundation for polymorphic behavior?
|
||||
- [ ] **Recipe/pattern usage**: Are recipes/patterns properly imported and applied?
|
||||
- [ ] **File separation**: Are recipes defined in proper location (separate file in `src/recipes/` or inline)?
|
||||
|
||||
### TypeScript Integration Assessment
|
||||
- [ ] **Recipe types**: Are generated variant types imported and used (e.g., `ButtonVariantProps`)?
|
||||
- [ ] **Prop type composition**: Are types properly composed (BoxProps + VariantProps)?
|
||||
- [ ] **Prop conflicts**: Are conflicting props properly omitted using `Omit`?
|
||||
- [ ] **Conditional types**: Are `ConditionalValue` and token types used for responsive/theme-aware props?
|
||||
- [ ] **Type exports**: Are prop types exported for consuming code?
|
||||
|
||||
### Styling Pattern Assessment
|
||||
- [ ] **Recipe application**: Is the recipe correctly applied with variant props?
|
||||
- [ ] **Slot recipes**: For multi-part components, are all slots properly destructured and applied?
|
||||
- [ ] **className merging**: Is `cx()` used to merge recipe + custom classes?
|
||||
- [ ] **splitProps usage**: Is `splitProps` utility used to separate CSS from HTML props?
|
||||
- [ ] **Style prop support**: Does component accept Panda CSS style props (bg, px, etc.)?
|
||||
- [ ] **Theme awareness**: Are colors/tokens using theme-aware values with `_dark` conditions?
|
||||
- [ ] **No style mixing**: Avoid mixing inline styles, external CSS, and Panda CSS?
|
||||
|
||||
### State & Interaction Assessment
|
||||
- [ ] **Pseudo-states**: Are hover, active, focus states defined in recipe?
|
||||
- [ ] **Focus visibility**: Is `_focusVisible` used for keyboard navigation?
|
||||
- [ ] **Disabled state**: Is `_disabled` properly styled and handled?
|
||||
- [ ] **Loading state**: If applicable, is loading state visually indicated?
|
||||
- [ ] **Data attributes**: Are custom states using data attributes (e.g., `data-indeterminate`)?
|
||||
- [ ] **Condition matching**: Do conditions match multiple state selectors (native + custom)?
|
||||
|
||||
### Accessibility Assessment
|
||||
- [ ] **Semantic HTML**: Is the correct HTML element used (button, input, etc.)?
|
||||
- [ ] **ARIA attributes**: Are proper ARIA attributes included (aria-label, aria-busy, etc.)?
|
||||
- [ ] **Keyboard interaction**: Can the component be used with keyboard only?
|
||||
- [ ] **Focus management**: Is focus properly managed in interactive components?
|
||||
- [ ] **Screen reader support**: Will screen readers announce the component correctly?
|
||||
- [ ] **Color contrast**: Do color combinations meet WCAG contrast requirements?
|
||||
|
||||
### Responsive & Adaptive Assessment
|
||||
- [ ] **Responsive props**: Are responsive values using object syntax correctly?
|
||||
- [ ] **Breakpoint usage**: Are breakpoints from theme tokens used consistently?
|
||||
- [ ] **Container queries**: If needed, are container queries implemented properly?
|
||||
- [ ] **Mobile-first**: Are base styles mobile-first with progressive enhancement?
|
||||
|
||||
### Token & Design System Assessment
|
||||
- [ ] **Semantic tokens**: Are semantic tokens used instead of primitive values?
|
||||
- [ ] **Spacing tokens**: Is spacing using token values (not arbitrary px values)?
|
||||
- [ ] **Color tokens**: Are colors from token system (not hardcoded hex/rgb)?
|
||||
- [ ] **Typography tokens**: Are font sizes, weights, line heights from tokens?
|
||||
- [ ] **Animation tokens**: Are transitions using token durations and easings?
|
||||
|
||||
### Performance & Best Practices Assessment
|
||||
- [ ] **Recipe optimization**: Are compound variants used efficiently?
|
||||
- [ ] **Unnecessary wrapping**: Is there unnecessary Box wrapping?
|
||||
- [ ] **Conditional rendering**: Is conditional rendering implemented efficiently?
|
||||
- [ ] **Prop spreading**: Is prop spreading used appropriately (not spreading into recipe)?
|
||||
- [ ] **Recipe size**: Is recipe definition reasonably sized (not overly complex)?
|
||||
|
||||
### Documentation & Developer Experience Assessment
|
||||
- [ ] **Component exports**: Are component and types properly exported?
|
||||
- [ ] **Prop documentation**: Are props clear and self-documenting?
|
||||
- [ ] **Usage examples**: Are there examples or stories showing usage?
|
||||
- [ ] **Default props**: Are sensible defaults provided?
|
||||
|
||||
## Phase 3: Recommendations - Generate Prioritized List
|
||||
|
||||
After completing the assessment, generate a structured recommendation report.
|
||||
|
||||
### Priority Levels
|
||||
|
||||
**P0 (Critical)**: Blocks functionality, security issues, major accessibility violations
|
||||
- Examples: Missing keyboard support, broken TypeScript types, non-functional styles
|
||||
|
||||
**P1 (High)**: Significant best practice violations, maintainability issues
|
||||
- Examples: Not using recipe types, missing theme awareness, no splitProps usage
|
||||
|
||||
**P2 (Medium)**: Optimization opportunities, minor best practice gaps
|
||||
- Examples: Inefficient compound variants, missing responsive props, token misuse
|
||||
|
||||
**P3 (Low)**: Nice-to-haves, polish items, documentation improvements
|
||||
- Examples: Component documentation, additional variants, example refinements
|
||||
|
||||
### Recommendation Format
|
||||
|
||||
For each recommendation, provide:
|
||||
|
||||
1. **Priority Level**: P0, P1, P2, or P3
|
||||
2. **Category**: Architecture, TypeScript, Styling, Accessibility, etc.
|
||||
3. **Issue Description**: What's wrong or missing
|
||||
4. **Impact**: Why it matters (performance, maintainability, accessibility, etc.)
|
||||
5. **Solution**: Specific implementation approach
|
||||
6. **Code Example**: Show before/after if applicable
|
||||
7. **Effort Estimate**: Small (< 15 min), Medium (15-45 min), Large (> 45 min)
|
||||
|
||||
### Example Recommendation
|
||||
|
||||
```markdown
|
||||
**P1 - TypeScript Integration**
|
||||
|
||||
**Issue**: Component not using generated recipe variant types
|
||||
|
||||
**Impact**:
|
||||
- TypeScript types can drift out of sync with recipe definition
|
||||
- No autocomplete for variant values
|
||||
- Manual maintenance of prop types
|
||||
|
||||
**Solution**: Import and use `ButtonVariantProps` from generated recipe types
|
||||
|
||||
**Before**:
|
||||
```typescript
|
||||
type ButtonProps = {
|
||||
variant?: 'primary' | 'secondary' | 'outline'
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
}
|
||||
```
|
||||
|
||||
**After**:
|
||||
```typescript
|
||||
import { type ButtonVariantProps } from '@styled-system/recipes'
|
||||
|
||||
type ButtonProps = BoxProps & ButtonVariantProps & {
|
||||
loading?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
**Effort**: Small (5 minutes)
|
||||
```
|
||||
|
||||
## Phase 4: Present Recommendations for Approval
|
||||
|
||||
### Create Summary Report
|
||||
|
||||
Before showing recommendations, create a summary:
|
||||
|
||||
```markdown
|
||||
# Component Review: [ComponentName]
|
||||
|
||||
## Overview
|
||||
- **File**: path/to/Component.tsx
|
||||
- **Type**: [Recipe-based | Pattern-based | Inline CSS]
|
||||
- **Issues Found**: [Total count]
|
||||
- **Critical (P0)**: [count]
|
||||
- **High (P1)**: [count]
|
||||
- **Medium (P2)**: [count]
|
||||
- **Low (P3)**: [count]
|
||||
|
||||
## Overall Assessment
|
||||
[Brief 2-3 sentence assessment of component health]
|
||||
|
||||
## Strengths
|
||||
- [What the component does well]
|
||||
- [Existing best practices being followed]
|
||||
|
||||
## Areas for Improvement
|
||||
[High-level summary of main issues]
|
||||
```
|
||||
|
||||
### Present Recommendations
|
||||
|
||||
Display recommendations grouped by priority:
|
||||
|
||||
```markdown
|
||||
## Recommendations
|
||||
|
||||
### Critical Priority (P0)
|
||||
[List P0 items with full detail as shown above]
|
||||
|
||||
### High Priority (P1)
|
||||
[List P1 items with full detail]
|
||||
|
||||
### Medium Priority (P2)
|
||||
[List P2 items with full detail]
|
||||
|
||||
### Low Priority (P3)
|
||||
[List P3 items with full detail]
|
||||
```
|
||||
|
||||
### Request User Approval
|
||||
|
||||
After presenting recommendations, explicitly ask:
|
||||
|
||||
**"Which recommendations would you like me to implement?"**
|
||||
|
||||
Options:
|
||||
- "All recommendations" - Implement everything
|
||||
- "Only P0 and P1" - Focus on critical and high priority
|
||||
- "Let me select specific ones" - User chooses from list
|
||||
- "None, I just wanted the review" - Stop here
|
||||
|
||||
**DO NOT proceed with implementation until user responds.**
|
||||
|
||||
## Phase 5: Implementation - Execute Approved Changes
|
||||
|
||||
Only after user approval, proceed with changes.
|
||||
|
||||
### Create Implementation TodoWrite Items
|
||||
|
||||
For each approved recommendation, create a specific todo:
|
||||
|
||||
```typescript
|
||||
TodoWrite({
|
||||
todos: [
|
||||
{ content: "Update Button types to use ButtonVariantProps", status: "pending", activeForm: "Updating Button types to use ButtonVariantProps" },
|
||||
{ content: "Add splitProps utility to Button component", status: "pending", activeForm: "Adding splitProps utility to Button component" },
|
||||
{ content: "Implement _focusVisible styles in button recipe", status: "pending", activeForm: "Implementing _focusVisible styles in button recipe" },
|
||||
// ... more items
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### Implementation Guidelines
|
||||
|
||||
1. **Make one change at a time**: Mark todo as in_progress, implement, mark completed
|
||||
2. **Test after each change**: Verify component still works
|
||||
3. **Group related changes**: When multiple changes affect same code block, batch them
|
||||
4. **Preserve functionality**: Don't change behavior, only improve implementation
|
||||
5. **Follow existing patterns**: Match the code style and patterns in the codebase
|
||||
|
||||
### Post-Implementation Verification
|
||||
|
||||
After implementing changes, create verification todos:
|
||||
|
||||
- [ ] Component renders correctly
|
||||
- [ ] All variants still work
|
||||
- [ ] TypeScript types are correct
|
||||
- [ ] No console errors or warnings
|
||||
- [ ] Accessibility features work
|
||||
- [ ] Responsive behavior intact
|
||||
|
||||
## Using This Skill - Quick Reference
|
||||
|
||||
### Typical Workflow
|
||||
|
||||
```bash
|
||||
# User requests review
|
||||
"Review the Button component for best practices"
|
||||
|
||||
# 1. Discovery
|
||||
- Read component file(s)
|
||||
- Identify recipe/pattern usage
|
||||
- Note dependencies and utilities
|
||||
|
||||
# 2. Assessment
|
||||
- Create TodoWrite items for each assessment category
|
||||
- Systematically check each item
|
||||
- Reference Panda CSS docs via MCP as needed
|
||||
- Document findings
|
||||
|
||||
# 3. Generate Recommendations
|
||||
- Prioritize issues (P0 > P1 > P2 > P3)
|
||||
- Format each recommendation with detail
|
||||
- Estimate effort for each
|
||||
|
||||
# 4. Present for Approval
|
||||
- Show summary report
|
||||
- Display recommendations by priority
|
||||
- Ask user which to implement
|
||||
- WAIT for response
|
||||
|
||||
# 5. Implement (only after approval)
|
||||
- Create implementation todos
|
||||
- Execute changes one by one
|
||||
- Verify after each change
|
||||
- Mark todos complete
|
||||
```
|
||||
|
||||
## Accessing Official Panda CSS Documentation
|
||||
|
||||
Throughout the review process, reference official docs for accuracy:
|
||||
|
||||
### Recipe Patterns
|
||||
```typescript
|
||||
mcp__MCP_DOCKER__get-library-docs({
|
||||
context7CompatibleLibraryID: "/cschroeter/park-ui",
|
||||
topic: "recipes"
|
||||
})
|
||||
```
|
||||
|
||||
### TypeScript Integration
|
||||
```typescript
|
||||
mcp__MCP_DOCKER__get-library-docs({
|
||||
context7CompatibleLibraryID: "/cschroeter/park-ui",
|
||||
topic: "typescript"
|
||||
})
|
||||
```
|
||||
|
||||
### Styling Best Practices
|
||||
```typescript
|
||||
mcp__MCP_DOCKER__get-library-docs({
|
||||
context7CompatibleLibraryID: "/cschroeter/park-ui",
|
||||
topic: "styling"
|
||||
})
|
||||
```
|
||||
|
||||
## Common Review Findings
|
||||
|
||||
### Most Common Issues (seen in practice)
|
||||
|
||||
1. **Not using generated recipe types** (P1)
|
||||
- Fix: Import and use `*VariantProps` types
|
||||
|
||||
2. **Missing splitProps utility** (P1)
|
||||
- Fix: Add splitProps to separate CSS from HTML props
|
||||
|
||||
3. **No _focusVisible styles** (P0 for interactive components)
|
||||
- Fix: Add focus styles in recipe
|
||||
|
||||
4. **Hardcoded colors instead of tokens** (P2)
|
||||
- Fix: Replace with semantic token references
|
||||
|
||||
5. **Missing ARIA attributes** (P0 for complex components)
|
||||
- Fix: Add appropriate aria-* attributes
|
||||
|
||||
6. **Not using Box as foundation** (P1)
|
||||
- Fix: Refactor to use Box with `as` prop
|
||||
|
||||
7. **Mixing CSS approaches** (P1)
|
||||
- Fix: Remove inline styles and external CSS, use only Panda
|
||||
|
||||
8. **No theme awareness (_dark conditions)** (P2)
|
||||
- Fix: Add _dark variants for all colors
|
||||
|
||||
## Best Practices for Conducting Reviews
|
||||
|
||||
### DO:
|
||||
- Be thorough but not pedantic
|
||||
- Prioritize issues that impact users (accessibility, functionality)
|
||||
- Provide clear, actionable recommendations
|
||||
- Show code examples for complex changes
|
||||
- Reference official docs for validation
|
||||
- Acknowledge what's already done well
|
||||
- Estimate effort realistically
|
||||
- Wait for user approval before changing code
|
||||
|
||||
### DON'T:
|
||||
- Make changes without approval
|
||||
- Recommend changes without explaining why
|
||||
- Suggest subjective style preferences as "best practices"
|
||||
- Overwhelm with low-priority nitpicks
|
||||
- Skip the assessment checklist
|
||||
- Forget to create TodoWrite items for tracking
|
||||
- Implement everything as one large change
|
||||
- Sacrifice readability for minor optimizations
|
||||
|
||||
## Example Review Session
|
||||
|
||||
### User Request
|
||||
```
|
||||
"Review my Button component at src/components/Button/Button.tsx"
|
||||
```
|
||||
|
||||
### Your Response
|
||||
```
|
||||
I'll systematically review your Button component against Panda CSS best practices. Let me start by reading the component and gathering context.
|
||||
|
||||
[Use Read tool to examine component]
|
||||
[Create discovery TodoWrite items]
|
||||
[Read recipe if separate]
|
||||
[Access Panda CSS docs for recipe patterns]
|
||||
|
||||
[Complete assessment checklist with TodoWrite items]
|
||||
[Document findings in each category]
|
||||
|
||||
# Component Review: Button
|
||||
|
||||
## Overview
|
||||
- **File**: src/components/Button/Button.tsx
|
||||
- **Type**: Recipe-based component
|
||||
- **Issues Found**: 5
|
||||
- **Critical (P0)**: 1
|
||||
- **High (P1)**: 2
|
||||
- **Medium (P2)**: 2
|
||||
- **Low (P3)**: 0
|
||||
|
||||
## Overall Assessment
|
||||
The Button component follows basic Panda CSS patterns but has several opportunities for improvement, particularly around TypeScript integration and prop handling.
|
||||
|
||||
## Strengths
|
||||
- Uses recipe for variant management
|
||||
- Implements hover and active states
|
||||
- Has clear variant names
|
||||
|
||||
## Areas for Improvement
|
||||
- Missing focus visible styles (accessibility concern)
|
||||
- Not using generated recipe types
|
||||
- No splitProps utility for prop separation
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Critical Priority (P0)
|
||||
|
||||
**P0 - Accessibility: Missing focus visible styles**
|
||||
[Full recommendation detail...]
|
||||
|
||||
### High Priority (P1)
|
||||
|
||||
**P1 - TypeScript: Not using generated recipe types**
|
||||
[Full recommendation detail...]
|
||||
|
||||
**P1 - Architecture: Missing splitProps utility**
|
||||
[Full recommendation detail...]
|
||||
|
||||
[... more recommendations ...]
|
||||
|
||||
---
|
||||
|
||||
Which recommendations would you like me to implement?
|
||||
- "All recommendations" - Implement everything
|
||||
- "Only P0 and P1" - Focus on critical and high priority
|
||||
- "Let me select specific ones" - Choose from list
|
||||
- "None, I just wanted the review" - Stop here
|
||||
```
|
||||
|
||||
### After User Approves
|
||||
```
|
||||
Great! I'll implement the P0 and P1 recommendations. Let me create a plan:
|
||||
|
||||
[Create implementation TodoWrite items]
|
||||
|
||||
Now I'll start with the critical accessibility fix...
|
||||
[Implement changes one by one, marking todos complete]
|
||||
```
|
||||
|
||||
## Integration with Other Skills
|
||||
|
||||
This review skill works well with:
|
||||
|
||||
- **panda-component-impl**: Reference for proper component patterns
|
||||
- **panda-recipe-patterns**: Reference for recipe best practices
|
||||
- **panda-token-architecture**: Reference for token usage
|
||||
- **panda-setup-config**: Reference for configuration patterns
|
||||
|
||||
When recommendations involve significant refactoring, consider suggesting:
|
||||
- Creating a new component following best practices
|
||||
- Incremental migration approach
|
||||
- Using the panda-architect agent for complex architectural changes
|
||||
|
||||
## Skill Usage Checklist
|
||||
|
||||
When using this skill, ensure you:
|
||||
|
||||
- [ ] Create TodoWrite items for discovery phase
|
||||
- [ ] Read all relevant component files
|
||||
- [ ] Reference official Panda CSS documentation via MCP
|
||||
- [ ] Create TodoWrite items for assessment checklist
|
||||
- [ ] Complete all assessment categories systematically
|
||||
- [ ] Prioritize findings (P0, P1, P2, P3)
|
||||
- [ ] Format recommendations with full detail
|
||||
- [ ] Present summary report before recommendations
|
||||
- [ ] Explicitly wait for user approval
|
||||
- [ ] DO NOT implement anything without approval
|
||||
- [ ] Create implementation TodoWrite items after approval
|
||||
- [ ] Make changes incrementally
|
||||
- [ ] Mark todos complete as you go
|
||||
- [ ] Verify functionality after implementation
|
||||
431
skills/panda-setup-config.md
Normal file
431
skills/panda-setup-config.md
Normal file
@@ -0,0 +1,431 @@
|
||||
---
|
||||
name: panda-setup-config
|
||||
description: Guide Panda CSS initial setup, configuration, preset architecture, and build integration for new projects
|
||||
---
|
||||
|
||||
# Panda CSS Setup & Configuration
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Starting a new Panda CSS project from scratch
|
||||
- Integrating Panda CSS into an existing React + Vite project
|
||||
- Creating a reusable Panda CSS preset for multiple projects
|
||||
- Configuring build tools and import aliases
|
||||
- Setting up the foundation for a design system
|
||||
|
||||
For complex multi-project architecture or refactoring large codebases, use the **panda-architect** agent instead.
|
||||
|
||||
## Prerequisites Check
|
||||
|
||||
Before starting, verify:
|
||||
- [ ] Project uses React 18+ and Vite 4+
|
||||
- [ ] TypeScript is configured (recommended but not required)
|
||||
- [ ] You understand the project's design token needs
|
||||
|
||||
## Installation Checklist
|
||||
|
||||
Create TodoWrite items for each step:
|
||||
|
||||
### 1. Install Panda CSS
|
||||
|
||||
```bash
|
||||
npm install -D @pandacss/dev
|
||||
```
|
||||
|
||||
### 2. Initialize Panda CSS
|
||||
|
||||
```bash
|
||||
npx panda init
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `panda.config.ts` - Main configuration file
|
||||
- `styled-system/` - Generated CSS and utilities (add to .gitignore)
|
||||
|
||||
### 3. Configure panda.config.ts
|
||||
|
||||
**Critical Settings:**
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from '@pandacss/dev'
|
||||
|
||||
export default defineConfig({
|
||||
// STRICT MODE: Prevents hard-coded values (enforce tokens-only)
|
||||
strictTokens: true,
|
||||
strictPropertyValues: true,
|
||||
|
||||
// Source files to scan for Panda CSS usage
|
||||
include: [
|
||||
'./src/**/*.{js,jsx,ts,tsx}',
|
||||
'./pages/**/*.{js,jsx,ts,tsx}'
|
||||
],
|
||||
|
||||
// Files to ignore
|
||||
exclude: [],
|
||||
|
||||
// React integration
|
||||
jsxFramework: 'react',
|
||||
jsxStyleProps: 'all', // Enable style props on all components
|
||||
|
||||
// Generated code location
|
||||
outdir: 'styled-system',
|
||||
|
||||
// Optional: Namespace your CSS classes
|
||||
prefix: 'app',
|
||||
|
||||
// Optional: Custom import path
|
||||
importMap: '@styled-system',
|
||||
|
||||
// Theme configuration
|
||||
theme: {
|
||||
extend: {
|
||||
// tokens, recipes, etc. go here
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Key Decision Points:**
|
||||
|
||||
- **strictTokens**: Set to `true` to enforce design system consistency
|
||||
- **prefix**: Use for avoiding CSS class conflicts (especially in design systems)
|
||||
- **importMap**: Customize if you want cleaner imports (e.g., `@styled-system` instead of `../styled-system`)
|
||||
|
||||
### 4. Add Import Alias (Vite)
|
||||
|
||||
Update `vite.config.ts`:
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@styled-system': path.resolve(__dirname, './styled-system'),
|
||||
'~': path.resolve(__dirname, './src') // Optional: src alias
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Update `tsconfig.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@styled-system/*": ["./styled-system/*"],
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Add Build Scripts
|
||||
|
||||
Update `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"prepare": "panda codegen",
|
||||
"dev": "panda --watch & vite",
|
||||
"build": "panda codegen && vite build",
|
||||
"lint": "eslint ."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern**: Always run `panda codegen` before builds and in watch mode during dev.
|
||||
|
||||
### 6. Import Global Styles
|
||||
|
||||
In your app entry point (e.g., `src/main.tsx` or `src/App.tsx`):
|
||||
|
||||
```typescript
|
||||
import '@styled-system/styles.css'
|
||||
```
|
||||
|
||||
This imports the generated CSS including:
|
||||
- CSS reset
|
||||
- Design tokens as CSS variables
|
||||
- Utility classes
|
||||
- Recipe styles
|
||||
|
||||
### 7. Add styled-system to .gitignore
|
||||
|
||||
```
|
||||
# Panda CSS generated files
|
||||
styled-system/
|
||||
```
|
||||
|
||||
**Why**: Generated code should not be committed (similar to node_modules).
|
||||
|
||||
## Preset Architecture (For Design Systems)
|
||||
|
||||
If you're building a **reusable design system**, create a separate preset file:
|
||||
|
||||
### Create a Preset File
|
||||
|
||||
**File**: `panda-preset.ts`
|
||||
|
||||
```typescript
|
||||
import { definePreset } from '@pandacss/dev'
|
||||
import type { Preset } from '@pandacss/types'
|
||||
|
||||
const customPreset: Preset = definePreset({
|
||||
theme: {
|
||||
extend: {
|
||||
tokens: {
|
||||
// Your design tokens
|
||||
},
|
||||
semanticTokens: {
|
||||
// Theme-aware tokens
|
||||
},
|
||||
recipes: {
|
||||
// Component recipes
|
||||
},
|
||||
slotRecipes: {
|
||||
// Multi-part component recipes
|
||||
}
|
||||
}
|
||||
},
|
||||
conditions: {
|
||||
// Custom conditions (pseudo-classes, states, etc.)
|
||||
},
|
||||
patterns: {
|
||||
// Custom patterns
|
||||
},
|
||||
utilities: {
|
||||
// Custom utility functions
|
||||
}
|
||||
})
|
||||
|
||||
export default customPreset
|
||||
```
|
||||
|
||||
### Use the Preset
|
||||
|
||||
In consuming projects' `panda.config.ts`:
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from '@pandacss/dev'
|
||||
import customPreset from './panda-preset'
|
||||
// Or: import customPreset from 'your-design-system/preset'
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
'@pandacss/preset-base', // Always include base preset
|
||||
customPreset
|
||||
],
|
||||
|
||||
// Project-specific config
|
||||
include: ['./src/**/*.{ts,tsx}'],
|
||||
strictTokens: true,
|
||||
|
||||
// Optionally override preset values
|
||||
theme: {
|
||||
extend: {
|
||||
// Project-specific tokens
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Pattern**: Preset provides defaults, consuming projects can extend/override.
|
||||
|
||||
## Configuration Best Practices
|
||||
|
||||
### 1. Separate Concerns
|
||||
|
||||
```
|
||||
src/
|
||||
styles/
|
||||
tokens.ts # Base design tokens
|
||||
semanticTokens.ts # Theme-aware tokens
|
||||
conditions.ts # Custom conditions
|
||||
globalStyles.ts # Global CSS
|
||||
recipes/
|
||||
index.ts # Export all recipes
|
||||
button.ts # Individual recipe files
|
||||
input.ts
|
||||
patterns/
|
||||
index.ts # Custom patterns
|
||||
```
|
||||
|
||||
Import in `panda.config.ts`:
|
||||
|
||||
```typescript
|
||||
import { tokens } from './src/styles/tokens'
|
||||
import { semanticTokens } from './src/styles/semanticTokens'
|
||||
import { conditions } from './src/styles/conditions'
|
||||
import * as recipes from './src/recipes'
|
||||
|
||||
export default defineConfig({
|
||||
theme: {
|
||||
extend: {
|
||||
tokens,
|
||||
semanticTokens,
|
||||
recipes: {
|
||||
...recipes
|
||||
}
|
||||
}
|
||||
},
|
||||
conditions
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Enable Useful Features
|
||||
|
||||
```typescript
|
||||
export default defineConfig({
|
||||
// Generate JSDoc comments for better autocomplete
|
||||
emitTokensOnly: false,
|
||||
|
||||
// Hash class names in production
|
||||
hash: process.env.NODE_ENV === 'production',
|
||||
|
||||
// Minify output
|
||||
minify: process.env.NODE_ENV === 'production',
|
||||
|
||||
// Optimize build
|
||||
optimize: true,
|
||||
|
||||
// Enable container queries
|
||||
containerQueries: true
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Configure Output Paths for Distribution
|
||||
|
||||
If building a **library**:
|
||||
|
||||
```typescript
|
||||
export default defineConfig({
|
||||
outdir: 'styled-system',
|
||||
|
||||
// Generate separate token files
|
||||
outExtension: 'js',
|
||||
|
||||
// Export as ES modules
|
||||
emitPackage: true
|
||||
})
|
||||
```
|
||||
|
||||
In `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"exports": {
|
||||
"./preset": "./panda-preset.js",
|
||||
"./styles.css": "./styled-system/styles.css"
|
||||
},
|
||||
"files": [
|
||||
"styled-system",
|
||||
"panda-preset.js"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Accessing Official Panda CSS Docs
|
||||
|
||||
When you need up-to-date information about Panda CSS features, configuration options, or API changes:
|
||||
|
||||
### Use MCP Tools (Recommended)
|
||||
|
||||
1. **Resolve the library ID:**
|
||||
```
|
||||
Use mcp__MCP_DOCKER__resolve-library-id with libraryName: "panda-css"
|
||||
```
|
||||
|
||||
2. **Fetch documentation:**
|
||||
```
|
||||
Use mcp__MCP_DOCKER__get-library-docs with:
|
||||
- context7CompatibleLibraryID: (from step 1)
|
||||
- topic: "configuration" | "recipes" | "patterns" | etc.
|
||||
```
|
||||
|
||||
### Topics to Search
|
||||
|
||||
- "configuration" - Config options, presets
|
||||
- "tokens" - Design tokens, semantic tokens
|
||||
- "recipes" - Component recipes, variants
|
||||
- "patterns" - Built-in and custom patterns
|
||||
- "conditions" - Responsive, state, pseudo-classes
|
||||
- "utilities" - Utility functions and utilities
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: "Token not found" errors with strictTokens
|
||||
|
||||
**Solution**: Either add the token to your tokens config, or disable strictTokens (not recommended).
|
||||
|
||||
```typescript
|
||||
// Add missing token
|
||||
tokens: {
|
||||
colors: {
|
||||
brand: { value: '#FF0000' }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Issue: Styles not updating during development
|
||||
|
||||
**Solution**: Ensure Panda watch mode is running:
|
||||
|
||||
```bash
|
||||
panda --watch
|
||||
```
|
||||
|
||||
Or update dev script:
|
||||
|
||||
```json
|
||||
"dev": "panda --watch & vite"
|
||||
```
|
||||
|
||||
### Issue: Import errors for @styled-system
|
||||
|
||||
**Solution**:
|
||||
1. Run `npm run prepare` to generate files
|
||||
2. Verify `tsconfig.json` has correct path mapping
|
||||
3. Check Vite alias configuration
|
||||
|
||||
### Issue: CSS not loading
|
||||
|
||||
**Solution**: Ensure you imported styles in app entry:
|
||||
|
||||
```typescript
|
||||
import '@styled-system/styles.css'
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
After setup is complete:
|
||||
|
||||
1. **Design tokens**: Use the **panda-token-architecture** skill to organize your design system
|
||||
2. **Recipes**: Use the **panda-recipe-patterns** skill to create component styles
|
||||
3. **Components**: Use the **panda-component-impl** skill to build React components
|
||||
|
||||
For complex architectural decisions or refactoring, launch the **panda-architect** agent.
|
||||
|
||||
## Working Examples
|
||||
|
||||
Reference these files in the `examples/` directory for production-tested patterns:
|
||||
|
||||
**Configuration:**
|
||||
- `examples/panda.config.ts` - Complete Panda config with preset integration
|
||||
- `examples/preset.ts` - Full preset architecture (myPreset) with tokens, semantic tokens, conditions, patterns
|
||||
|
||||
**Token Architecture:**
|
||||
- `examples/tokens.ts` - Base token files organized by category
|
||||
- `examples/semanticTokens.ts/` - Semantic token layer referencing primitives
|
||||
|
||||
**Additional Patterns:**
|
||||
- `examples/textStyles.ts` - Typography preset definitions
|
||||
- `examples/conditions.ts` - Custom conditions and pseudo-classes
|
||||
- `examples/utils/splitProps.ts` - CSS/HTML prop separation utility
|
||||
- `examples/utils/ThemeContext.tsx` - Theme provider pattern
|
||||
693
skills/panda-token-architecture.md
Normal file
693
skills/panda-token-architecture.md
Normal file
@@ -0,0 +1,693 @@
|
||||
---
|
||||
name: panda-token-architecture
|
||||
description: Design token systems, semantic tokens, theme structures, and responsive design tokens following best practices
|
||||
---
|
||||
|
||||
# Panda CSS Token Architecture
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Designing a design system's token architecture
|
||||
- Organizing color palettes, spacing, typography, and other design tokens
|
||||
- Setting up theme switching (light/dark modes)
|
||||
- Creating semantic token layers for intent-based naming
|
||||
- Establishing responsive design token patterns
|
||||
|
||||
For implementing these tokens in recipes or components, use **panda-recipe-patterns** or **panda-component-impl** skills.
|
||||
|
||||
## Token Architecture Principles
|
||||
|
||||
### Two-Layer Token System
|
||||
|
||||
**Layer 1: Base Tokens** - Raw design values
|
||||
- Color palettes with numeric scales
|
||||
- Sizing and spacing scales
|
||||
- Typography scales
|
||||
- Static values that rarely change
|
||||
|
||||
**Layer 2: Semantic Tokens** - Context-aware aliases
|
||||
- Theme-dependent (light/dark mode)
|
||||
- Intent-based naming (success, error, brand)
|
||||
- References to base tokens
|
||||
- Changes based on context/theme
|
||||
|
||||
**Why**: Separation enables theme switching without redefining entire palettes.
|
||||
|
||||
## Base Tokens
|
||||
|
||||
Create: `src/styles/tokens.ts`
|
||||
|
||||
### Color Palettes
|
||||
|
||||
**Pattern**: Use numeric scales (0-100 or 1-90) for lightness/darkness:
|
||||
|
||||
```typescript
|
||||
import { defineTokens } from '@pandacss/dev'
|
||||
|
||||
export const tokens = defineTokens({
|
||||
colors: {
|
||||
// Grayscale: 0 = lightest, 100 = darkest
|
||||
slate: {
|
||||
0: { value: '#FFFFFF' },
|
||||
5: { value: '#F8F9FA' },
|
||||
10: { value: '#F1F3F5' },
|
||||
20: { value: '#E9ECEF' },
|
||||
30: { value: '#DEE2E6' },
|
||||
40: { value: '#CED4DA' },
|
||||
50: { value: '#ADB5BD' },
|
||||
60: { value: '#868E96' },
|
||||
70: { value: '#495057' },
|
||||
80: { value: '#343A40' },
|
||||
90: { value: '#212529' },
|
||||
100: { value: '#000000' }
|
||||
},
|
||||
|
||||
// Brand colors with tints/shades
|
||||
blue: {
|
||||
5: { value: '#E7F5FF' },
|
||||
10: { value: '#D0EBFF' },
|
||||
20: { value: '#A5D8FF' },
|
||||
30: { value: '#74C0FC' },
|
||||
40: { value: '#4DABF7' },
|
||||
50: { value: '#339AF0' }, // Base brand blue
|
||||
60: { value: '#228BE6' },
|
||||
70: { value: '#1C7ED6' },
|
||||
80: { value: '#1971C2' },
|
||||
90: { value: '#1864AB' }
|
||||
},
|
||||
|
||||
// Semantic palette colors
|
||||
green: { /* success shades */ },
|
||||
red: { /* error shades */ },
|
||||
yellow: { /* warning shades */ },
|
||||
cyan: { /* info shades */ }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Naming Convention**:
|
||||
- 0-100 scale: 0 = lightest, 100 = darkest
|
||||
- Or 1-90 scale: 1 = lightest, 90 = darkest
|
||||
- Choose one convention and stick to it
|
||||
|
||||
### Spacing & Sizing
|
||||
|
||||
**Pattern**: Unified scale for both spacing and sizing:
|
||||
|
||||
```typescript
|
||||
const sizes = {
|
||||
// Utility sizes
|
||||
0: { value: '0' },
|
||||
auto: { value: 'auto' },
|
||||
full: { value: '100%' },
|
||||
min: { value: 'min-content' },
|
||||
max: { value: 'max-content' },
|
||||
fit: { value: 'fit-content' },
|
||||
prose: { value: '65ch' },
|
||||
|
||||
// Numeric scale in rem (16px base)
|
||||
1: { value: '0.0625rem' }, // 1px
|
||||
2: { value: '0.125rem' }, // 2px
|
||||
4: { value: '0.25rem' }, // 4px
|
||||
6: { value: '0.375rem' }, // 6px
|
||||
8: { value: '0.5rem' }, // 8px
|
||||
12: { value: '0.75rem' }, // 12px
|
||||
16: { value: '1rem' }, // 16px
|
||||
20: { value: '1.25rem' }, // 20px
|
||||
24: { value: '1.5rem' }, // 24px
|
||||
32: { value: '2rem' }, // 32px
|
||||
40: { value: '2.5rem' }, // 40px
|
||||
48: { value: '3rem' }, // 48px
|
||||
64: { value: '4rem' }, // 64px
|
||||
80: { value: '5rem' }, // 80px
|
||||
96: { value: '6rem' }, // 96px
|
||||
// ... extend as needed
|
||||
}
|
||||
|
||||
export const tokens = defineTokens({
|
||||
sizes,
|
||||
spacing: sizes // Reuse same scale for spacing
|
||||
})
|
||||
```
|
||||
|
||||
**Why**: Unified scale ensures consistency between width/height and padding/margin.
|
||||
|
||||
### Container Sizes
|
||||
|
||||
**Pattern**: Named breakpoint containers:
|
||||
|
||||
```typescript
|
||||
export const tokens = defineTokens({
|
||||
sizes: {
|
||||
// ... numeric sizes above ...
|
||||
|
||||
// Named container sizes
|
||||
'2xs': { value: '16rem' }, // 256px
|
||||
xs: { value: '20rem' }, // 320px
|
||||
sm: { value: '24rem' }, // 384px
|
||||
md: { value: '28rem' }, // 448px
|
||||
lg: { value: '32rem' }, // 512px
|
||||
xl: { value: '36rem' }, // 576px
|
||||
'2xl': { value: '42rem' }, // 672px
|
||||
'3xl': { value: '48rem' }, // 768px
|
||||
'4xl': { value: '56rem' }, // 896px
|
||||
'5xl': { value: '64rem' }, // 1024px
|
||||
'6xl': { value: '72rem' }, // 1152px
|
||||
'7xl': { value: '80rem' }, // 1280px
|
||||
'8xl': { value: '90rem' } // 1440px
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Typography
|
||||
|
||||
**Pattern**: Separate font families, weights, sizes, and line heights:
|
||||
|
||||
```typescript
|
||||
export const tokens = defineTokens({
|
||||
fonts: {
|
||||
body: { value: 'Inter, -apple-system, sans-serif' },
|
||||
heading: { value: 'Inter, -apple-system, sans-serif' },
|
||||
mono: { value: 'JetBrains Mono, Consolas, monospace' }
|
||||
},
|
||||
|
||||
fontWeights: {
|
||||
thin: { value: '100' },
|
||||
light: { value: '300' },
|
||||
normal: { value: '400' },
|
||||
medium: { value: '500' },
|
||||
semibold: { value: '600' },
|
||||
bold: { value: '700' },
|
||||
extrabold: { value: '800' }
|
||||
},
|
||||
|
||||
fontSizes: {
|
||||
'2xs': { value: '0.625rem' }, // 10px
|
||||
xs: { value: '0.75rem' }, // 12px
|
||||
sm: { value: '0.875rem' }, // 14px
|
||||
md: { value: '1rem' }, // 16px
|
||||
lg: { value: '1.125rem' }, // 18px
|
||||
xl: { value: '1.25rem' }, // 20px
|
||||
'2xl': { value: '1.5rem' }, // 24px
|
||||
'3xl': { value: '1.875rem' }, // 30px
|
||||
'4xl': { value: '2.25rem' }, // 36px
|
||||
'5xl': { value: '3rem' }, // 48px
|
||||
'6xl': { value: '3.75rem' }, // 60px
|
||||
'7xl': { value: '4.5rem' } // 72px
|
||||
},
|
||||
|
||||
lineHeights: {
|
||||
none: { value: '1' },
|
||||
tight: { value: '1.25' },
|
||||
snug: { value: '1.375' },
|
||||
normal: { value: '1.5' },
|
||||
relaxed: { value: '1.625' },
|
||||
loose: { value: '2' }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Other Design Tokens
|
||||
|
||||
```typescript
|
||||
export const tokens = defineTokens({
|
||||
radii: {
|
||||
none: { value: '0' },
|
||||
sm: { value: '0.125rem' }, // 2px
|
||||
base: { value: '0.25rem' }, // 4px
|
||||
md: { value: '0.375rem' }, // 6px
|
||||
lg: { value: '0.5rem' }, // 8px
|
||||
xl: { value: '0.75rem' }, // 12px
|
||||
'2xl': { value: '1rem' }, // 16px
|
||||
full: { value: '9999px' }
|
||||
},
|
||||
|
||||
shadows: {
|
||||
xs: { value: '0 1px 2px 0 rgb(0 0 0 / 0.05)' },
|
||||
sm: { value: '0 1px 3px 0 rgb(0 0 0 / 0.1)' },
|
||||
md: { value: '0 4px 6px -1px rgb(0 0 0 / 0.1)' },
|
||||
lg: { value: '0 10px 15px -3px rgb(0 0 0 / 0.1)' },
|
||||
xl: { value: '0 20px 25px -5px rgb(0 0 0 / 0.1)' },
|
||||
inner: { value: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)' }
|
||||
},
|
||||
|
||||
easings: {
|
||||
default: { value: 'cubic-bezier(0.4, 0, 0.2, 1)' },
|
||||
linear: { value: 'linear' },
|
||||
in: { value: 'cubic-bezier(0.4, 0, 1, 1)' },
|
||||
out: { value: 'cubic-bezier(0, 0, 0.2, 1)' },
|
||||
inOut: { value: 'cubic-bezier(0.4, 0, 0.2, 1)' }
|
||||
},
|
||||
|
||||
durations: {
|
||||
fast: { value: '150ms' },
|
||||
normal: { value: '250ms' },
|
||||
slow: { value: '350ms' },
|
||||
slower: { value: '500ms' }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Semantic Tokens
|
||||
|
||||
Create: `src/styles/semanticTokens.ts`
|
||||
|
||||
### Purpose
|
||||
|
||||
Semantic tokens provide:
|
||||
- Theme-aware values (light/dark mode)
|
||||
- Intent-based naming (success, error, warning)
|
||||
- Contextual meaning (background, foreground, border)
|
||||
- Easier token updates without touching components
|
||||
|
||||
### Pattern: Theme-Aware Colors
|
||||
|
||||
```typescript
|
||||
import { defineSemanticTokens } from '@pandacss/dev'
|
||||
|
||||
export const semanticTokens = defineSemanticTokens({
|
||||
colors: {
|
||||
// Background colors
|
||||
bg: {
|
||||
canvas: {
|
||||
value: { base: '{colors.slate.0}', _dark: '{colors.slate.90}' }
|
||||
},
|
||||
surface: {
|
||||
value: { base: '{colors.slate.5}', _dark: '{colors.slate.80}' }
|
||||
},
|
||||
overlay: {
|
||||
value: { base: '{colors.slate.10}', _dark: '{colors.slate.70}' }
|
||||
}
|
||||
},
|
||||
|
||||
// Foreground/text colors
|
||||
fg: {
|
||||
default: {
|
||||
value: { base: '{colors.slate.90}', _dark: '{colors.slate.10}' }
|
||||
},
|
||||
muted: {
|
||||
value: { base: '{colors.slate.60}', _dark: '{colors.slate.40}' }
|
||||
},
|
||||
subtle: {
|
||||
value: { base: '{colors.slate.50}', _dark: '{colors.slate.50}' }
|
||||
}
|
||||
},
|
||||
|
||||
// Border colors
|
||||
border: {
|
||||
default: {
|
||||
value: { base: '{colors.slate.20}', _dark: '{colors.slate.70}' }
|
||||
},
|
||||
muted: {
|
||||
value: { base: '{colors.slate.10}', _dark: '{colors.slate.80}' }
|
||||
}
|
||||
},
|
||||
|
||||
// Intent-based colors
|
||||
success: {
|
||||
default: {
|
||||
value: { base: '{colors.green.40}', _dark: '{colors.green.30}' }
|
||||
},
|
||||
emphasis: {
|
||||
value: { base: '{colors.green.50}', _dark: '{colors.green.40}' }
|
||||
},
|
||||
muted: {
|
||||
value: { base: '{colors.green.10}', _dark: '{colors.green.80}' }
|
||||
}
|
||||
},
|
||||
|
||||
error: {
|
||||
default: {
|
||||
value: { base: '{colors.red.40}', _dark: '{colors.red.30}' }
|
||||
},
|
||||
emphasis: {
|
||||
value: { base: '{colors.red.50}', _dark: '{colors.red.40}' }
|
||||
},
|
||||
muted: {
|
||||
value: { base: '{colors.red.10}', _dark: '{colors.red.80}' }
|
||||
}
|
||||
},
|
||||
|
||||
warning: {
|
||||
default: {
|
||||
value: { base: '{colors.yellow.40}', _dark: '{colors.yellow.30}' }
|
||||
},
|
||||
emphasis: {
|
||||
value: { base: '{colors.yellow.50}', _dark: '{colors.yellow.40}' }
|
||||
},
|
||||
muted: {
|
||||
value: { base: '{colors.yellow.10}', _dark: '{colors.yellow.80}' }
|
||||
}
|
||||
},
|
||||
|
||||
// Brand colors
|
||||
brand: {
|
||||
default: {
|
||||
value: { base: '{colors.blue.50}', _dark: '{colors.blue.40}' }
|
||||
},
|
||||
emphasis: {
|
||||
value: { base: '{colors.blue.60}', _dark: '{colors.blue.30}' }
|
||||
},
|
||||
muted: {
|
||||
value: { base: '{colors.blue.10}', _dark: '{colors.blue.80}' }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Token Reference Syntax
|
||||
|
||||
Reference base tokens using `{category.name.shade}`:
|
||||
|
||||
```typescript
|
||||
value: { base: '{colors.blue.50}', _dark: '{colors.blue.40}' }
|
||||
// ^-- Reference to base token
|
||||
```
|
||||
|
||||
**Why**: References enable token changes to cascade through semantic layer.
|
||||
|
||||
### Nesting Semantic Tokens
|
||||
|
||||
Create hierarchies for better organization:
|
||||
|
||||
```typescript
|
||||
export const semanticTokens = defineSemanticTokens({
|
||||
colors: {
|
||||
button: {
|
||||
primary: {
|
||||
bg: {
|
||||
value: { base: '{colors.brand.default}', _dark: '{colors.brand.default}' }
|
||||
},
|
||||
fg: {
|
||||
value: { base: '{colors.slate.0}', _dark: '{colors.slate.0}' }
|
||||
},
|
||||
border: {
|
||||
value: { base: '{colors.brand.emphasis}', _dark: '{colors.brand.emphasis}' }
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
bg: {
|
||||
value: { base: '{colors.bg.surface}', _dark: '{colors.bg.surface}' }
|
||||
},
|
||||
fg: {
|
||||
value: { base: '{colors.fg.default}', _dark: '{colors.fg.default}' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Usage in recipes:
|
||||
|
||||
```typescript
|
||||
bg: 'button.primary.bg',
|
||||
color: 'button.primary.fg',
|
||||
borderColor: 'button.primary.border'
|
||||
```
|
||||
|
||||
## Text Styles
|
||||
|
||||
**Pattern**: Define complete typography presets:
|
||||
|
||||
```typescript
|
||||
import { defineTextStyles } from '@pandacss/dev'
|
||||
|
||||
export const textStyles = defineTextStyles({
|
||||
// Display text
|
||||
display: {
|
||||
lg: {
|
||||
value: {
|
||||
fontSize: '5xl',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 'tight',
|
||||
letterSpacing: '-0.02em'
|
||||
}
|
||||
},
|
||||
md: {
|
||||
value: {
|
||||
fontSize: '4xl',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 'tight'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Headings
|
||||
heading: {
|
||||
lg: {
|
||||
value: {
|
||||
fontSize: '3xl',
|
||||
fontWeight: 'semibold',
|
||||
lineHeight: 'tight'
|
||||
}
|
||||
},
|
||||
md: {
|
||||
value: {
|
||||
fontSize: '2xl',
|
||||
fontWeight: 'semibold',
|
||||
lineHeight: 'snug'
|
||||
}
|
||||
},
|
||||
sm: {
|
||||
value: {
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'semibold',
|
||||
lineHeight: 'snug'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Body text
|
||||
body: {
|
||||
lg: {
|
||||
value: {
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'normal',
|
||||
lineHeight: 'normal'
|
||||
}
|
||||
},
|
||||
md: {
|
||||
value: {
|
||||
fontSize: 'md',
|
||||
fontWeight: 'normal',
|
||||
lineHeight: 'normal'
|
||||
}
|
||||
},
|
||||
sm: {
|
||||
value: {
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'normal',
|
||||
lineHeight: 'normal'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```typescript
|
||||
// In recipes or components
|
||||
textStyle: 'heading.lg'
|
||||
textStyle: 'body.md'
|
||||
```
|
||||
|
||||
## Responsive Token Patterns
|
||||
|
||||
### Breakpoint Tokens
|
||||
|
||||
Define in config (not as tokens):
|
||||
|
||||
```typescript
|
||||
// In panda.config.ts
|
||||
export default defineConfig({
|
||||
theme: {
|
||||
extend: {
|
||||
breakpoints: {
|
||||
sm: '640px',
|
||||
md: '768px',
|
||||
lg: '1024px',
|
||||
xl: '1280px',
|
||||
'2xl': '1536px'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Usage with responsive object syntax:
|
||||
|
||||
```typescript
|
||||
fontSize: { base: 'sm', md: 'md', lg: 'lg' }
|
||||
px: { base: '16', md: '20', lg: '24' }
|
||||
```
|
||||
|
||||
### Container Query Tokens
|
||||
|
||||
For component-based responsive design:
|
||||
|
||||
```typescript
|
||||
// In panda.config.ts
|
||||
export default defineConfig({
|
||||
theme: {
|
||||
extend: {
|
||||
containerSizes: {
|
||||
sm: '384px',
|
||||
md: '448px',
|
||||
lg: '512px',
|
||||
xl: '576px'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Integration with panda.config.ts
|
||||
|
||||
Import and extend theme:
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from '@pandacss/dev'
|
||||
import { tokens } from './src/styles/tokens'
|
||||
import { semanticTokens } from './src/styles/semanticTokens'
|
||||
import { textStyles } from './src/styles/textStyles'
|
||||
|
||||
export default defineConfig({
|
||||
// ... other config ...
|
||||
|
||||
theme: {
|
||||
extend: {
|
||||
tokens,
|
||||
semanticTokens,
|
||||
textStyles
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Best Practices Checklist
|
||||
|
||||
Create TodoWrite items when organizing tokens:
|
||||
|
||||
- [ ] Separate base tokens from semantic tokens (different files)
|
||||
- [ ] Use consistent numeric scales (0-100 or 1-90, pick one)
|
||||
- [ ] Create semantic tokens for all theme-dependent values
|
||||
- [ ] Use intent-based naming (success, error, warning, not green, red, yellow)
|
||||
- [ ] Unify spacing and sizing scales
|
||||
- [ ] Define text styles for common typography patterns
|
||||
- [ ] Reference base tokens in semantic tokens (don't duplicate values)
|
||||
- [ ] Document token purpose and usage in comments
|
||||
- [ ] Test tokens in both light and dark themes
|
||||
- [ ] Validate strictTokens mode catches hard-coded values
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Avoid: Duplicating Values
|
||||
|
||||
```typescript
|
||||
// BAD: Duplicates color value
|
||||
semanticTokens: {
|
||||
colors: {
|
||||
success: {
|
||||
value: { base: '#22C55E', _dark: '#4ADE80' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: References base token
|
||||
semanticTokens: {
|
||||
colors: {
|
||||
success: {
|
||||
value: { base: '{colors.green.40}', _dark: '{colors.green.30}' }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid: Too Many Token Layers
|
||||
|
||||
Keep it simple: Base → Semantic → Components (2-3 layers max)
|
||||
|
||||
```typescript
|
||||
// BAD: Too many indirection levels
|
||||
semantic → alias → component → variant → state
|
||||
|
||||
// GOOD: Clear, direct references
|
||||
base → semantic → component
|
||||
```
|
||||
|
||||
### Avoid: Mixing Concerns
|
||||
|
||||
```typescript
|
||||
// BAD: Mixing spacing into colors
|
||||
tokens: {
|
||||
colors: {
|
||||
buttonPadding: { value: '12px' } // Wrong category!
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Correct categorization
|
||||
tokens: {
|
||||
spacing: {
|
||||
buttonPadding: { value: '12' }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Accessing Official Panda CSS Docs
|
||||
|
||||
For token-specific documentation:
|
||||
|
||||
1. **Resolve library ID**: `mcp__MCP_DOCKER__resolve-library-id` with `libraryName: "panda-css"`
|
||||
2. **Fetch docs**: `mcp__MCP_DOCKER__get-library-docs` with `topic: "tokens"` or `topic: "semantic-tokens"`
|
||||
|
||||
## Next Steps
|
||||
|
||||
After organizing tokens:
|
||||
|
||||
1. **Create recipes**: Use **panda-recipe-patterns** skill to build component styles
|
||||
2. **Implement components**: Use **panda-component-impl** skill for React components
|
||||
3. **Validate design system**: Ensure all components use tokens (strictTokens catches violations)
|
||||
|
||||
## Working Examples
|
||||
|
||||
Reference these files in the `examples/` directory for production-tested token patterns:
|
||||
|
||||
**Base Tokens (Primitives):**
|
||||
- `examples/tokens.ts`
|
||||
- - Color scales (10-100) for all hues
|
||||
```typescript
|
||||
lime: {
|
||||
"10": { value: "#EFFFD6" },
|
||||
"50": { value: "#82B536" },
|
||||
"100": { value: "#28311B" }
|
||||
}
|
||||
```
|
||||
- - Unified spacing/sizing numeric scale
|
||||
- - Font families, sizes, weights, line heights
|
||||
- - Durations, easings, keyframes
|
||||
- - Borders, radii, opacity, aspect ratios
|
||||
|
||||
**Semantic Tokens:**
|
||||
- `examples/semanticTokens.ts`
|
||||
- - Theme-aware color system
|
||||
```typescript
|
||||
background: {
|
||||
accent: {
|
||||
blue: {
|
||||
bolder: {
|
||||
value: { base: "{colors.blue.70}", _dark: "{colors.blue.40}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- - Shadow and depth tokens
|
||||
- - Semantic utility tokens
|
||||
|
||||
**Preset Integration:**
|
||||
- `examples/preset.ts` - Complete preset showing how to integrate primitives and semantics using `definePreset`, `defineTokens`, and `defineSemanticTokens`
|
||||
- `examples/textStyles.ts` - Typography presets combining font tokens
|
||||
Reference in New Issue
Block a user