@@ -48,6 +48,8 @@ import {
4848 unwrapArrowFunctionToPropsExpr ,
4949 hasThemeAccessInArrowFn ,
5050 buildTemplateWithStaticParts ,
51+ inlineArrowFunctionBody ,
52+ collectPropsFromArrowFn ,
5153} from "./inline-styles.js" ;
5254import { extractStaticPartsForDecl } from "./interpolations.js" ;
5355import {
@@ -70,6 +72,7 @@ export function processDeclRules(ctx: DeclProcessingState): void {
7072 localVarValues,
7173 cssHelperPropValues,
7274 getComposedDefaultValue,
75+ inlineStyleProps,
7376 } = ctx ;
7477 const {
7578 j,
@@ -681,6 +684,23 @@ export function processDeclRules(ctx: DeclProcessingState): void {
681684 { bailOnUnresolved : true } ,
682685 ) ;
683686 if ( forwardResult === "bail" ) {
687+ // Try CSS variable bridge: forward prop-based interpolations via CSS custom
688+ // properties set on the parent component's inline style.
689+ if ( ! crossFileUsage ) {
690+ const bridgeResult = tryForwardCssVarBridge (
691+ rule ,
692+ bucket ,
693+ j ,
694+ decl ,
695+ overrideStyleKey ,
696+ resolveThemeValue ,
697+ resolveThemeValueFromFn ,
698+ inlineStyleProps ,
699+ ) ;
700+ if ( bridgeResult !== "bail" ) {
701+ continue ;
702+ }
703+ }
684704 state . markBail ( ) ;
685705 warnings . push ( {
686706 severity : "warning" ,
@@ -1219,6 +1239,102 @@ export function processDeclRules(ctx: DeclProcessingState): void {
12191239
12201240// --- Non-exported helpers ---
12211241
1242+ /**
1243+ * Attempts to resolve unresolvable interpolations in a forward descendant selector
1244+ * by bridging them via CSS custom properties. The parent component sets the CSS
1245+ * variable as an inline style, and the child's override style references it via `var()`.
1246+ *
1247+ * Only handles single-slot interpolations that are arrow function prop accesses.
1248+ * Returns "bail" if the bridge can't be applied.
1249+ */
1250+ function tryForwardCssVarBridge (
1251+ rule : { declarations : CssDeclarationIR [ ] } ,
1252+ bucket : Record < string , unknown > ,
1253+ j : DeclProcessingState [ "state" ] [ "j" ] ,
1254+ decl : StyledDecl ,
1255+ overrideStyleKey : string ,
1256+ resolveThemeValue : ( expr : unknown ) => unknown ,
1257+ resolveThemeValueFromFn : ( expr : unknown ) => unknown ,
1258+ parentInlineStyleProps : Array < { prop : string ; expr : ExpressionKind ; jsxProp ?: string } > ,
1259+ ) : Set < string > | "bail" {
1260+ const writtenProps = new Set < string > ( ) ;
1261+
1262+ for ( const d of rule . declarations ) {
1263+ // Static and theme-resolvable declarations use the shared helper
1264+ const sharedResult = writeResolvedDeclaration (
1265+ d ,
1266+ bucket ,
1267+ j ,
1268+ decl ,
1269+ resolveThemeValue ,
1270+ resolveThemeValueFromFn ,
1271+ writtenProps ,
1272+ ) ;
1273+ if ( sharedResult === "written" || sharedResult === "skip" ) {
1274+ continue ;
1275+ }
1276+
1277+ // sharedResult === "unresolved" — try CSS variable bridge for prop-based expressions
1278+ if ( d . value . kind !== "interpolated" || ! d . property ) {
1279+ return "bail" ;
1280+ }
1281+ const parts = ( d . value as { parts ?: Array < { kind : string ; slotId ?: number } > } ) . parts ;
1282+ if ( ! parts ) {
1283+ return "bail" ;
1284+ }
1285+ const slotParts = parts . filter ( ( p ) => p . kind === "slot" && p . slotId !== undefined ) ;
1286+ // Only handle single-slot interpolations for now
1287+ if ( slotParts . length !== 1 || slotParts [ 0 ] ! . slotId === undefined ) {
1288+ return "bail" ;
1289+ }
1290+ const slotId = slotParts [ 0 ] ! . slotId ;
1291+ const expr = decl . templateExpressions [ slotId ] ;
1292+ if (
1293+ ! expr ||
1294+ typeof expr !== "object" ||
1295+ ( ( expr as { type ?: string } ) . type !== "ArrowFunctionExpression" &&
1296+ ( expr as { type ?: string } ) . type !== "FunctionExpression" )
1297+ ) {
1298+ return "bail" ;
1299+ }
1300+
1301+ // Reject theme accesses — they should be handled by resolveThemeValueFromFn
1302+ if ( hasThemeAccessInArrowFn ( expr ) ) {
1303+ return "bail" ;
1304+ }
1305+
1306+ // Inline the arrow function body to get a wrapper-scope expression
1307+ const inlinedExpr = inlineArrowFunctionBody ( j , expr ) ;
1308+ if ( ! inlinedExpr ) {
1309+ return "bail" ;
1310+ }
1311+
1312+ // Collect and register used props so the wrapper destructures them
1313+ for ( const propName of collectPropsFromArrowFn ( expr ) ) {
1314+ ensureShouldForwardPropDrop ( decl , propName ) ;
1315+ }
1316+
1317+ // Generate a CSS variable name from the override style key and CSS property
1318+ const varName = `--${ overrideStyleKey } -${ kebabToCamelCase ( d . property ) } ` ;
1319+
1320+ // Set bucket value(s) to var(--name) — shorthand expansion produces the right outputs
1321+ for ( const out of cssDeclarationToStylexDeclarations ( d ) ) {
1322+ if ( out . value . kind === "static" ) {
1323+ bucket [ out . prop ] = cssValueToJs ( out . value , d . important , out . prop ) ;
1324+ } else {
1325+ bucket [ out . prop ] = `var(${ varName } )` ;
1326+ }
1327+ writtenProps . add ( out . prop ) ;
1328+ }
1329+
1330+ // Add CSS variable assignment to the parent's inline style props
1331+ parentInlineStyleProps . push ( { prop : varName , expr : inlinedExpr } ) ;
1332+ decl . needsWrapperComponent = true ;
1333+ }
1334+
1335+ return writtenProps ;
1336+ }
1337+
12221338/**
12231339 * Processes rule declarations into a relation override bucket, handling both static
12241340 * and interpolated (theme-resolved) values. Returns "bail" if any interpolated
@@ -1235,40 +1351,70 @@ function processDeclarationsIntoBucket(
12351351) : Set < string > | "bail" {
12361352 const writtenProps = new Set < string > ( ) ;
12371353 for ( const d of rule . declarations ) {
1238- if ( d . value . kind === "static" ) {
1239- for ( const out of cssDeclarationToStylexDeclarations ( d ) ) {
1240- if ( out . value . kind !== "static" ) {
1241- continue ;
1242- }
1243- const v = cssValueToJs ( out . value , d . important , out . prop ) ;
1244- bucket [ out . prop ] = v ;
1245- writtenProps . add ( out . prop ) ;
1246- }
1247- } else if ( d . value . kind === "interpolated" && d . property ) {
1248- const resolveResult = resolveAllSlots ( d , decl , resolveThemeValue , resolveThemeValueFromFn ) ;
1249- if ( resolveResult === "bail" ) {
1250- if ( options ?. bailOnUnresolved ) {
1251- return "bail" ;
1252- }
1354+ const result = writeResolvedDeclaration (
1355+ d ,
1356+ bucket ,
1357+ j ,
1358+ decl ,
1359+ resolveThemeValue ,
1360+ resolveThemeValueFromFn ,
1361+ writtenProps ,
1362+ ) ;
1363+ if ( result === "written" || result === "skip" ) {
1364+ continue ;
1365+ }
1366+ // result === "unresolved"
1367+ if ( options ?. bailOnUnresolved ) {
1368+ return "bail" ;
1369+ }
1370+ }
1371+ return writtenProps ;
1372+ }
1373+
1374+ /**
1375+ * Shared logic for processing a single declaration into a bucket.
1376+ * Handles static values and theme-resolvable interpolations.
1377+ * Returns "written" if handled, "skip" for non-interpolated non-static,
1378+ * or "unresolved" if the interpolation couldn't be resolved.
1379+ */
1380+ function writeResolvedDeclaration (
1381+ d : CssDeclarationIR ,
1382+ bucket : Record < string , unknown > ,
1383+ j : DeclProcessingState [ "state" ] [ "j" ] ,
1384+ decl : { templateExpressions : unknown [ ] } ,
1385+ resolveThemeValue : ( expr : unknown ) => unknown ,
1386+ resolveThemeValueFromFn : ( expr : unknown ) => unknown ,
1387+ writtenProps : Set < string > ,
1388+ ) : "written" | "skip" | "unresolved" {
1389+ if ( d . value . kind === "static" ) {
1390+ for ( const out of cssDeclarationToStylexDeclarations ( d ) ) {
1391+ if ( out . value . kind !== "static" ) {
12531392 continue ;
12541393 }
1255- if ( resolveResult ) {
1256- for ( const out of cssDeclarationToStylexDeclarations ( d ) ) {
1257- if ( out . value . kind === "static" ) {
1258- // Shorthand expansion produced a static component (e.g., borderWidth from border)
1259- const v = cssValueToJs ( out . value , d . important , out . prop ) ;
1260- bucket [ out . prop ] = v ;
1261- } else {
1262- // Build interpolated value using the output's parts (may differ from original d
1263- // when shorthand expansion strips the static prefix, e.g., border → borderColor)
1264- bucket [ out . prop ] = buildInterpolatedValue ( j , { value : out . value } , resolveResult ) ;
1265- }
1266- writtenProps . add ( out . prop ) ;
1267- }
1268- }
1394+ bucket [ out . prop ] = cssValueToJs ( out . value , d . important , out . prop ) ;
1395+ writtenProps . add ( out . prop ) ;
12691396 }
1397+ return "written" ;
12701398 }
1271- return writtenProps ;
1399+
1400+ if ( d . value . kind !== "interpolated" || ! d . property ) {
1401+ return "skip" ;
1402+ }
1403+
1404+ const resolveResult = resolveAllSlots ( d , decl , resolveThemeValue , resolveThemeValueFromFn ) ;
1405+ if ( ! resolveResult || resolveResult === "bail" ) {
1406+ return "unresolved" ;
1407+ }
1408+
1409+ for ( const out of cssDeclarationToStylexDeclarations ( d ) ) {
1410+ if ( out . value . kind === "static" ) {
1411+ bucket [ out . prop ] = cssValueToJs ( out . value , d . important , out . prop ) ;
1412+ } else {
1413+ bucket [ out . prop ] = buildInterpolatedValue ( j , { value : out . value } , resolveResult ) ;
1414+ }
1415+ writtenProps . add ( out . prop ) ;
1416+ }
1417+ return "written" ;
12721418}
12731419
12741420/**
0 commit comments