Skip to content

Commit c0511b8

Browse files
authored
fix(generator): forward variant props via forwardProps in withProvider (#3549)
* fix(generator): forward variant props via forwardProps in withProvider withProvider split variant props out before passing them to the wrapped component, so props listed in `forwardProps` were silently dropped. Now a variant prop named in `forwardProps` still drives the slot styles and is forwarded to the component, across the React, Preact, Vue, and Solid outputs. The Solid implementation forwards via getters to preserve reactivity. Also documents `forwardProps` on the styled factory and createStyleContext. * docs: add git and writing conventions to CLAUDE.md No co-author trailer on commits, and write commits, PRs, and comments in plain human language (no em-dashes, no AI filler).
1 parent 3730779 commit c0511b8

13 files changed

Lines changed: 196 additions & 4 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@pandacss/generator': patch
3+
---
4+
5+
Fix `forwardProps` being ignored by `withProvider` in `createStyleContext`.
6+
7+
Previously, `withProvider` split out variant props before handing them to the wrapped component, so any variant prop
8+
listed in `forwardProps` never reached the component (it was silently dropped). Now variant props named in
9+
`forwardProps` still drive the slot styles **and** are forwarded to the component.
10+
11+
```tsx
12+
const { withProvider } = createStyleContext(tabs)
13+
14+
function TabsRoot({ orientation, ...rest }) {
15+
// `orientation` is now defined here instead of `undefined`
16+
return <div aria-orientation={orientation} {...rest} />
17+
}
18+
19+
export const Tabs = withProvider(TabsRoot, 'root', { forwardProps: ['orientation'] })
20+
```
21+
22+
This is fixed across the React, Preact, Vue, and Solid outputs. The Solid implementation forwards the props through
23+
getters so reactivity is preserved.

CLAUDE.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,24 @@ Brief description of the change and its impact.
139139
- `minor`: New features, backwards-compatible changes
140140
- `major`: Breaking changes
141141

142+
## Git & Writing Conventions
143+
144+
### No co-author trailer on commits
145+
146+
Do NOT add a `Co-Authored-By` line (or any "Generated with" / tool attribution) to commit messages. Write the commit
147+
as if a developer on the team wrote it. This overrides any default that appends a co-author trailer.
148+
149+
### Write like a human, not like AI
150+
151+
Commit messages, PR descriptions, and GitHub/issue comments should read like a normal developer wrote them. Keep it
152+
plain and direct so an average developer understands it on the first read.
153+
154+
- No em-dashes (``). Use a period, comma, or parentheses instead.
155+
- Skip the AI tics: "delve", "seamless", "robust", "leverage", "comprehensive", "it's worth noting", and similar filler.
156+
- Don't over-format. Avoid walls of bold text, emoji, and a bullet list for every thought. Use prose where prose works.
157+
- Say what changed and why. Drop the marketing tone and the wrap-up paragraph that just restates the title.
158+
- Match the length to the change. A one-line fix gets a one-line message, not an essay.
159+
142160
## Important Files & Patterns
143161

144162
### Configuration Flow

packages/generator/src/artifacts/preact-jsx/create-style-context.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ export function generatePreactCreateStyleContext(ctx: Context) {
8282
8383
const WithProvider = forwardRef(function WithProvider(props, ref) {
8484
const [variantProps, restProps] = svaFn.splitVariantProps(props)
85-
85+
options?.forwardProps?.forEach((key) => {
86+
if (key in variantProps) restProps[key] = variantProps[key]
87+
})
88+
8689
const slotStyles = isConfigRecipe ? svaFn(variantProps) : svaFn.raw(variantProps)
8790
slotStyles._classNameMap = svaFn.classNameMap
8891

packages/generator/src/artifacts/react-jsx/create-style-context.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ export function generateReactCreateStyleContext(ctx: Context) {
8080
8181
const WithProvider = forwardRef((props, ref) => {
8282
const [variantProps, restProps] = svaFn.splitVariantProps(props)
83-
83+
options?.forwardProps?.forEach((key) => {
84+
if (key in variantProps) restProps[key] = variantProps[key]
85+
})
86+
8487
const slotStyles = isConfigRecipe ? svaFn(variantProps) : svaFn.raw(variantProps)
8588
slotStyles._classNameMap = svaFn.classNameMap
8689

packages/generator/src/artifacts/solid-jsx/create-style-context.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ export function generateSolidCreateStyleContext(ctx: Context) {
101101
const [variantProps, restProps] = svaFn.splitVariantProps(props)
102102
const [local, propsWithoutChildren] = splitProps(restProps, ["children"])
103103
104+
// forward selected variant props to the component without losing reactivity
105+
const forwardedProps = {}
106+
options?.forwardProps?.forEach((key) => {
107+
if (key in variantProps) {
108+
Object.defineProperty(forwardedProps, key, { get: () => variantProps[key], enumerable: true })
109+
}
110+
})
111+
104112
const slotStyles = createMemo(() => {
105113
const styles = isConfigRecipe ? svaFn(variantProps) : svaFn.raw(variantProps)
106114
styles._classNameMap = svaFn.classNameMap
@@ -124,7 +132,7 @@ export function generateSolidCreateStyleContext(ctx: Context) {
124132
get children() {
125133
return createComponent(
126134
StyledComponent,
127-
mergeProps(resolvedProps, {
135+
mergeProps(resolvedProps, forwardedProps, {
128136
get children() {
129137
return local.children
130138
},

packages/generator/src/artifacts/vue-jsx/create-style-context.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ export function generateVueCreateStyleContext(ctx: Context) {
9090
})
9191
const res = computed(() => {
9292
const [variantProps, restProps] = svaFn.splitVariantProps(props.value)
93+
options?.forwardProps?.forEach((key) => {
94+
if (key in variantProps) restProps[key] = variantProps[key]
95+
})
9396
return { variantProps, restProps }
9497
})
9598

packages/studio/styled-system/jsx/create-style-context.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ export function createStyleContext(recipe) {
6767

6868
const WithProvider = forwardRef((props, ref) => {
6969
const [variantProps, restProps] = svaFn.splitVariantProps(props)
70-
70+
options?.forwardProps?.forEach((key) => {
71+
if (key in variantProps) restProps[key] = variantProps[key]
72+
})
73+
7174
const slotStyles = isConfigRecipe ? svaFn(variantProps) : svaFn.raw(variantProps)
7275
slotStyles._classNameMap = svaFn.classNameMap
7376

sandbox/codegen/__tests__/frameworks/preact.style-context.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,12 @@ describe('style context - preact', () => {
5151
`"<div data-testid="button-root"><span class="slot-button__root slot-button__root--visual_solid">Click me</span></div>"`,
5252
)
5353
})
54+
55+
test('forwardProps exposes the variant to the component', () => {
56+
const RootWithForward = withProvider('div', 'root', { forwardProps: ['visual'] })
57+
58+
const { container } = render(<RootWithForward visual="outline" />)
59+
60+
expect(container.firstElementChild?.outerHTML).toMatchInlineSnapshot(`"<div visual="outline" class="slot-button__root slot-button__root--visual_outline"></div>"`)
61+
})
5462
})

sandbox/codegen/__tests__/frameworks/react.style-context.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,29 @@ describe('style context - react', () => {
5757
`)
5858
})
5959

60+
test('forwardProps', () => {
61+
type RootProps = React.ComponentProps<'div'> & { visual?: string }
62+
const ForwardRoot = React.forwardRef<HTMLDivElement, RootProps>(function ForwardRoot(
63+
{ visual, ...rest },
64+
ref,
65+
) {
66+
return <div ref={ref} data-visual={visual} {...rest} />
67+
})
68+
69+
const RootWithForward = withProvider(ForwardRoot, 'root', { forwardProps: ['visual'] })
70+
71+
const { container } = render(<RootWithForward visual="outline">Hello</RootWithForward>)
72+
73+
expect(container.firstChild).toMatchInlineSnapshot(`
74+
<div
75+
class="slot-button__root slot-button__root--visual_outline"
76+
data-visual="outline"
77+
>
78+
Hello
79+
</div>
80+
`)
81+
})
82+
6083
test('default props', () => {
6184
const RootWithDefaults = withProvider('div', 'root', { defaultProps: { 'data-testid': 'button-root' } })
6285

sandbox/codegen/__tests__/frameworks/solid.style-context.test.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** @jsxImportSource solid-js */
22
import { render } from '@solidjs/testing-library'
33
import { describe, expect, test } from 'vitest'
4+
import { createSignal } from 'solid-js'
45
import { createStyleContext } from '../../styled-system-solid/jsx/create-style-context'
56
import { slotButton } from '../../styled-system-solid/recipes'
67

@@ -108,4 +109,30 @@ describe('style context - solid', () => {
108109
</div>
109110
`)
110111
})
112+
113+
test('forwardProps exposes the variant to the component', () => {
114+
const RootWithForward = withProvider('div', 'root', { forwardProps: ['visual'] })
115+
116+
const { container } = render(() => <RootWithForward visual="outline" />)
117+
118+
expect(container.firstChild).toMatchInlineSnapshot(`
119+
<div
120+
class="slot-button__root slot-button__root--visual_outline"
121+
visual="outline"
122+
/>
123+
`)
124+
})
125+
126+
test('forwardProps stays reactive', () => {
127+
const [visual, setVisual] = createSignal<'outline' | 'solid'>('outline')
128+
const RootWithForward = withProvider('div', 'root', { forwardProps: ['visual'] })
129+
130+
const { container } = render(() => <RootWithForward visual={visual()} />)
131+
const div = () => container.firstChild as HTMLElement
132+
133+
expect(div().getAttribute('visual')).toBe('outline')
134+
135+
setVisual('solid')
136+
expect(div().getAttribute('visual')).toBe('solid')
137+
})
111138
})

0 commit comments

Comments
 (0)