33 formatHexAlpha ,
44 parseBackgroundShorthand ,
55 preprocessCssValue ,
6- stripFallback
6+ stripFallback ,
7+ normalizeFigmaVarName
78} from '@/utils/css'
89
910const BG_URL_LIGHTGRAY_RE = / u r l \( .* ?\) \s + l i g h t g r a y / i
@@ -19,6 +20,12 @@ export function cleanFigmaSpecificStyles(
1920 const bgValue = processed . background
2021 const normalized = stripFallback ( preprocessCssValue ( bgValue ) ) . trim ( )
2122
23+ const gradient = resolveGradientWithOpacity ( normalized , node )
24+ if ( gradient ) {
25+ processed . background = gradient
26+ return processed
27+ }
28+
2229 if ( isSolidBackground ( normalized ) ) {
2330 processed [ 'background-color' ] = normalized
2431 delete processed . background
@@ -62,6 +69,131 @@ export function cleanFigmaSpecificStyles(
6269 return processed
6370}
6471
72+ function resolveGradientWithOpacity ( value : string , node : SceneNode ) : string | null {
73+ if ( ! value ) return null
74+ if ( ! / g r a d i e n t \( / i. test ( value ) ) return null
75+ if ( ! ( 'fills' in node ) || ! Array . isArray ( node . fills ) ) return null
76+
77+ const fill = node . fills . find ( ( f ) => f && f . visible !== false && f . type === 'GRADIENT_LINEAR' ) as
78+ | GradientPaint
79+ | undefined
80+ if ( ! fill || ! Array . isArray ( fill . gradientStops ) ) return null
81+
82+ const fillOpacity = typeof fill . opacity === 'number' ? fill . opacity : 1
83+ const hasStopAlpha = fill . gradientStops . some ( ( stop ) => ( stop . color ?. a ?? 1 ) < 1 )
84+ if ( fillOpacity >= 0.99 && ! hasStopAlpha ) return null
85+
86+ const parsed = parseGradient ( value )
87+ if ( ! parsed ) return null
88+
89+ const angle = parsed . args [ 0 ] ?. trim ( )
90+ const hasAngle =
91+ ! ! angle &&
92+ ( angle . endsWith ( 'deg' ) ||
93+ angle . endsWith ( 'rad' ) ||
94+ angle . endsWith ( 'turn' ) ||
95+ angle . startsWith ( 'to ' ) )
96+ const stops = fill . gradientStops . map ( ( stop ) => {
97+ const pct = formatPercent ( stop . position )
98+ const color = formatGradientStopColor ( stop , fillOpacity )
99+ return `${ color } ${ pct } `
100+ } )
101+
102+ const args = hasAngle ? [ angle , ...stops ] : stops
103+ return `${ parsed . fn } (${ args . join ( ', ' ) } )`
104+ }
105+
106+ function parseGradient ( value : string ) : { fn : string ; args : string [ ] } | null {
107+ const match = value . match ( / ( l i n e a r - g r a d i e n t | r a d i a l - g r a d i e n t | c o n i c - g r a d i e n t ) \s * \( / i)
108+ if ( ! match || match . index == null ) return null
109+ const fn = match [ 1 ]
110+ const start = value . indexOf ( '(' , match . index )
111+ if ( start < 0 ) return null
112+
113+ let depth = 0
114+ let end = - 1
115+ for ( let i = start ; i < value . length ; i += 1 ) {
116+ const ch = value [ i ]
117+ if ( ch === '(' ) depth += 1
118+ else if ( ch === ')' ) {
119+ depth -= 1
120+ if ( depth === 0 ) {
121+ end = i
122+ break
123+ }
124+ }
125+ }
126+ if ( end < 0 ) return null
127+
128+ const inner = value . slice ( start + 1 , end )
129+ return { fn, args : splitTopLevel ( inner ) }
130+ }
131+
132+ function splitTopLevel ( input : string ) : string [ ] {
133+ const out : string [ ] = [ ]
134+ let depth = 0
135+ let quote : '"' | "'" | null = null
136+ let buf = ''
137+
138+ for ( let i = 0 ; i < input . length ; i += 1 ) {
139+ const ch = input [ i ]
140+
141+ if ( quote ) {
142+ if ( ch === '\\' ) {
143+ buf += ch
144+ i += 1
145+ if ( i < input . length ) buf += input [ i ]
146+ continue
147+ }
148+ if ( ch === quote ) quote = null
149+ buf += ch
150+ continue
151+ }
152+
153+ if ( ch === '"' || ch === "'" ) {
154+ quote = ch
155+ buf += ch
156+ continue
157+ }
158+
159+ if ( ch === '(' ) depth += 1
160+ if ( ch === ')' ) depth = Math . max ( 0 , depth - 1 )
161+
162+ if ( ch === ',' && depth === 0 ) {
163+ out . push ( buf . trim ( ) )
164+ buf = ''
165+ continue
166+ }
167+
168+ buf += ch
169+ }
170+
171+ if ( buf . trim ( ) ) out . push ( buf . trim ( ) )
172+ return out
173+ }
174+
175+ function formatPercent ( pos : number ) : string {
176+ const pct = Math . round ( pos * 10000 ) / 100
177+ return `${ pct } %`
178+ }
179+
180+ function formatGradientStopColor ( stop : ColorStop , fillOpacity : number ) : string {
181+ const baseAlpha = stop . color ?. a ?? 1
182+ const alpha = Math . max ( 0 , Math . min ( 1 , baseAlpha * fillOpacity ) )
183+
184+ const bound = stop . boundVariables ?. color
185+ if ( bound && typeof bound === 'object' && 'id' in bound && bound . id ) {
186+ const v = figma . variables . getVariableById ( bound . id )
187+ const name = normalizeFigmaVarName ( v ?. name ?? '' )
188+ const expr = `var(${ name } )`
189+ if ( alpha >= 0.99 ) return expr
190+ const pct = Math . round ( alpha * 10000 ) / 100
191+ return `color-mix(in srgb, ${ expr } ${ pct } %, transparent)`
192+ }
193+
194+ return formatHexAlpha ( stop . color , alpha )
195+ }
196+
65197function isSolidBackground ( value : string ) : boolean {
66198 if ( ! value ) return false
67199 const trimmed = value . trim ( )
0 commit comments