@@ -149,20 +149,34 @@ export const tailwindGradientFromFills = (
149149 return tailwindGradient ( fill ) ;
150150 }
151151
152- // Use arbitrary values with HTML-based gradient syntax for other gradient types
153- if ( fill . type === "GRADIENT_ANGULAR" ) {
154- return tailwindArbitraryGradient ( htmlAngularGradient ( fill ) ) ;
155- }
152+ // Tailwind 4 has built-in support for radial and conic gradients
153+ if ( localTailwindSettings . useTailwind4 ) {
154+ if ( fill . type === "GRADIENT_RADIAL" ) {
155+ return tailwindRadialGradient ( fill ) ;
156+ }
157+ if ( fill . type === "GRADIENT_ANGULAR" ) {
158+ return tailwindConicGradient ( fill ) ;
159+ }
160+ // Diamond is still too complex for direct Tailwind support
161+ if ( fill . type === "GRADIENT_DIAMOND" ) {
162+ return "" ;
163+ }
164+ } else {
165+ // Use arbitrary values with HTML-based gradient syntax for other gradient types
166+ if ( fill . type === "GRADIENT_ANGULAR" ) {
167+ return tailwindArbitraryGradient ( htmlAngularGradient ( fill ) ) ;
168+ }
156169
157- if ( fill . type === "GRADIENT_RADIAL" ) {
158- return tailwindArbitraryGradient ( htmlRadialGradient ( fill ) ) ;
159- }
170+ if ( fill . type === "GRADIENT_RADIAL" ) {
171+ return tailwindArbitraryGradient ( htmlRadialGradient ( fill ) ) ;
172+ }
160173
161- if ( fill . type === "GRADIENT_DIAMOND" ) {
162- // Diamond is too complex, it is going to create 3 linear gradients, which gets too weird in Tailwind.
163- return "" ;
174+ if ( fill . type === "GRADIENT_DIAMOND" ) {
175+ // Diamond is too complex, it is going to create 3 linear gradients, which gets too weird in Tailwind.
176+ return "" ;
177+ }
164178 }
165-
179+
166180 return "" ;
167181} ;
168182
@@ -177,59 +191,208 @@ const tailwindArbitraryGradient = (cssGradient: string): string => {
177191 return `bg-[${ tailwindValue } ]` ;
178192} ;
179193
180- export const tailwindGradient = ( fill : GradientPaint ) : string => {
181- const direction = gradientDirection ( gradientAngle ( fill ) ) ;
194+ /**
195+ * Maps an angle to a gradient direction class for both Tailwind 3 and 4
196+ * @param angle The angle in degrees
197+ * @param useTailwind4 Whether to use Tailwind 4 syntax
198+ * @returns The appropriate gradient direction class
199+ */
200+ const directionMap : Record < number , { tailwind3 : string ; tailwind4 : string } > = {
201+ 0 : { tailwind3 : "bg-gradient-to-r" , tailwind4 : "bg-linear-to-r" } ,
202+ 45 : { tailwind3 : "bg-gradient-to-br" , tailwind4 : "bg-linear-to-br" } ,
203+ 90 : { tailwind3 : "bg-gradient-to-b" , tailwind4 : "bg-linear-to-b" } ,
204+ 135 : { tailwind3 : "bg-gradient-to-bl" , tailwind4 : "bg-linear-to-bl" } ,
205+ "-45" : { tailwind3 : "bg-gradient-to-tr" , tailwind4 : "bg-linear-to-tr" } ,
206+ "-90" : { tailwind3 : "bg-gradient-to-t" , tailwind4 : "bg-linear-to-t" } ,
207+ "-135" : { tailwind3 : "bg-gradient-to-tl" , tailwind4 : "bg-linear-to-tl" } ,
208+ 180 : { tailwind3 : "bg-gradient-to-l" , tailwind4 : "bg-linear-to-l" } ,
209+ } ;
210+
211+ function getGradientDirectionClass ( angle : number , useTailwind4 : boolean ) : string {
212+ let snappedAngle = nearestValue ( angle , [
213+ 0 , 45 , 90 , 135 , 180 , - 45 , - 90 , - 135 , - 180 ,
214+ ] ) ;
215+ if ( snappedAngle === - 180 ) snappedAngle = 180 ;
216+
217+ // Check if angle is in the map
218+ const entry = directionMap [ snappedAngle ] ;
219+ if ( entry ) {
220+ return useTailwind4 ? entry . tailwind4 : entry . tailwind3 ;
221+ }
222+
223+ // For non-standard angles in Tailwind 4, use exact angle
224+ if ( useTailwind4 ) {
225+ const exactAngle = Math . round ( ( ( angle % 360 ) + 360 ) % 360 ) ;
226+ return `bg-linear-${ exactAngle } ` ;
227+ }
228+
229+ // Fallback for Tailwind 3 (nearest standard direction)
230+ return snappedAngle === 180 ? "bg-gradient-to-l" : "bg-gradient-to-r" ;
231+ }
232+
233+ /**
234+ * Check if a stop position needs a position override
235+ * @param actual The actual position (0-1)
236+ * @param expected The expected default position (0-1)
237+ * @returns True if position needs to be specified
238+ */
239+ const needsPositionOverride = ( actual : number , expected : number ) : boolean => {
240+ // Only include position if it deviates by more than 5% from expected
241+ return Math . abs ( actual - expected ) > 0.05 ;
242+ } ;
243+
244+ /**
245+ * Gets position modifier string for a gradient stop if needed
246+ * @param stopPosition The stop position (0-1)
247+ * @param expectedPosition The expected default position (0-1)
248+ * @param unit The unit to use (%, deg)
249+ * @param multiplier Multiplier for the position value
250+ * @returns Position string or empty string
251+ */
252+ const getStopPositionModifier = (
253+ stopPosition : number ,
254+ expectedPosition : number ,
255+ unit : string = "%" ,
256+ multiplier : number = 100
257+ ) : string => {
258+ if ( needsPositionOverride ( stopPosition , expectedPosition ) ) {
259+ const position = Math . round ( stopPosition * multiplier ) ;
260+ return ` ${ position } ${ unit } ` ;
261+ }
262+ return "" ;
263+ } ;
264+
265+ /**
266+ * Generates a complete gradient stop with position if needed
267+ * @param prefix The stop prefix (from-, via-, to-)
268+ * @param stop The gradient stop
269+ * @param globalOpacity The global opacity
270+ * @param expectedPosition The expected default position (0-1)
271+ * @param unit The unit to use (%, deg)
272+ * @param multiplier Multiplier for the position value
273+ * @returns Complete gradient stop string
274+ */
275+ function generateGradientStop (
276+ prefix : string ,
277+ stop : ColorStop ,
278+ globalOpacity : number = 1.0 ,
279+ expectedPosition : number ,
280+ unit : string = "%" ,
281+ multiplier : number = 100
282+ ) : string {
283+ const colorValue = tailwindGradientStop ( stop , globalOpacity ) ;
284+ const colorPart = `${ prefix } -${ colorValue } ` ;
182285
183- // Get the overall fill opacity
184- const globalOpacity = fill . opacity !== undefined ? fill . opacity : 1.0 ;
286+ if ( ! localTailwindSettings . useTailwind4 ) {
287+ return colorPart ;
288+ }
289+
290+ // Only add position if it significantly differs from the default
291+ const positionModifier = getStopPositionModifier (
292+ stop . position ,
293+ expectedPosition ,
294+ unit ,
295+ multiplier
296+ ) ;
297+ return positionModifier ? `${ colorPart } ${ prefix } ${ positionModifier } ` : colorPart ;
298+ }
299+
300+ export const tailwindGradient = ( fill : GradientPaint ) : string => {
301+ const globalOpacity = fill . opacity ?? 1.0 ;
302+ const direction = getGradientDirectionClass (
303+ gradientAngle ( fill ) ,
304+ localTailwindSettings . useTailwind4
305+ ) ;
185306
186307 if ( fill . gradientStops . length === 1 ) {
187- const fromColor = tailwindGradientStop (
188- fill . gradientStops [ 0 ] ,
189- globalOpacity ,
190- ) ;
191- return `${ direction } from-${ fromColor } ` ;
308+ const fromStop = generateGradientStop ( "from" , fill . gradientStops [ 0 ] , globalOpacity , 0 ) ;
309+ return [ direction , fromStop ] . filter ( Boolean ) . join ( " " ) ;
192310 } else if ( fill . gradientStops . length === 2 ) {
193- const fromColor = tailwindGradientStop (
194- fill . gradientStops [ 0 ] ,
195- globalOpacity ,
196- ) ;
197- const toColor = tailwindGradientStop ( fill . gradientStops [ 1 ] , globalOpacity ) ;
198- return `${ direction } from-${ fromColor } to-${ toColor } ` ;
311+ const firstStop = generateGradientStop ( "from" , fill . gradientStops [ 0 ] , globalOpacity , 0 ) ;
312+ const lastStop = generateGradientStop ( "to" , fill . gradientStops [ 1 ] , globalOpacity , 1 ) ;
313+ return [ direction , firstStop , lastStop ] . filter ( Boolean ) . join ( " " ) ;
199314 } else {
200- const fromColor = tailwindGradientStop (
201- fill . gradientStops [ 0 ] ,
315+ const firstStop = generateGradientStop ( "from" , fill . gradientStops [ 0 ] , globalOpacity , 0 ) ;
316+ const viaStop = generateGradientStop ( "via" , fill . gradientStops [ 1 ] , globalOpacity , 0.5 ) ;
317+ const lastStop = generateGradientStop (
318+ "to" ,
319+ fill . gradientStops [ fill . gradientStops . length - 1 ] ,
202320 globalOpacity ,
321+ 1
203322 ) ;
204- // middle (second color)
205- const viaColor = tailwindGradientStop ( fill . gradientStops [ 1 ] , globalOpacity ) ;
206- // last
207- const toColor = tailwindGradientStop (
323+ return [ direction , firstStop , viaStop , lastStop ] . filter ( Boolean ) . join ( " " ) ;
324+ }
325+ } ;
326+
327+ /**
328+ * Generate Tailwind 4 radial gradient
329+ */
330+ const tailwindRadialGradient = ( fill : GradientPaint ) : string => {
331+ const globalOpacity = fill . opacity ?? 1.0 ;
332+ const [ center ] = fill . gradientHandlePositions ;
333+ const cx = Math . round ( center . x * 100 ) ;
334+ const cy = Math . round ( center . y * 100 ) ;
335+ const isCustomPosition = Math . abs ( cx - 50 ) > 5 || Math . abs ( cy - 50 ) > 5 ;
336+ const baseClass = isCustomPosition ? `bg-radial-[at_${ cx } %_${ cy } %]` : "bg-radial" ;
337+
338+ if ( fill . gradientStops . length === 1 ) {
339+ const fromStop = generateGradientStop ( "from" , fill . gradientStops [ 0 ] , globalOpacity , 0 ) ;
340+ return [ baseClass , fromStop ] . filter ( Boolean ) . join ( " " ) ;
341+ } else if ( fill . gradientStops . length === 2 ) {
342+ const firstStop = generateGradientStop ( "from" , fill . gradientStops [ 0 ] , globalOpacity , 0 ) ;
343+ const lastStop = generateGradientStop ( "to" , fill . gradientStops [ 1 ] , globalOpacity , 1 ) ;
344+ return [ baseClass , firstStop , lastStop ] . filter ( Boolean ) . join ( " " ) ;
345+ } else {
346+ const firstStop = generateGradientStop ( "from" , fill . gradientStops [ 0 ] , globalOpacity , 0 ) ;
347+ const viaStop = generateGradientStop ( "via" , fill . gradientStops [ 1 ] , globalOpacity , 0.5 ) ;
348+ const lastStop = generateGradientStop (
349+ "to" ,
208350 fill . gradientStops [ fill . gradientStops . length - 1 ] ,
209351 globalOpacity ,
352+ 1
210353 ) ;
211- return ` ${ direction } from- ${ fromColor } via- ${ viaColor } to- ${ toColor } ` ;
354+ return [ baseClass , firstStop , viaStop , lastStop ] . filter ( Boolean ) . join ( " " ) ;
212355 }
213356} ;
214357
215- const gradientDirection = ( angle : number ) : string => {
216- switch ( nearestValue ( angle , [ - 180 , - 135 , - 90 , - 45 , 0 , 45 , 90 , 135 , 180 ] ) ) {
217- case 0 :
218- return "bg-gradient-to-r" ;
219- case 45 :
220- return "bg-gradient-to-br" ;
221- case 90 :
222- return "bg-gradient-to-b" ;
223- case 135 :
224- return "bg-gradient-to-bl" ;
225- case - 45 :
226- return "bg-gradient-to-tr" ;
227- case - 90 :
228- return "bg-gradient-to-t" ;
229- case - 135 :
230- return "bg-gradient-to-tl" ;
231- default :
232- // 180 and -180
233- return "bg-gradient-to-l" ;
358+ /**
359+ * Generate Tailwind 4 conic gradient
360+ */
361+ const tailwindConicGradient = ( fill : GradientPaint ) : string => {
362+ const [ center , , startDirection ] = fill . gradientHandlePositions ;
363+ const globalOpacity = fill . opacity ?? 1.0 ;
364+ const dx = startDirection . x - center . x ;
365+ const dy = startDirection . y - center . y ;
366+ let angle = Math . atan2 ( dy , dx ) * ( 180 / Math . PI ) ;
367+ angle = ( angle + 360 ) % 360 ;
368+ const normalizedAngle = Math . round ( angle ) ;
369+ const cx = Math . round ( center . x * 100 ) ;
370+ const cy = Math . round ( center . y * 100 ) ;
371+ const isCustomPosition = Math . abs ( cx - 50 ) > 5 || Math . abs ( cy - 50 ) > 5 ;
372+ let baseClass = `bg-conic-${ normalizedAngle } ` ;
373+
374+ if ( isCustomPosition ) {
375+ baseClass = `bg-conic-[from_${ normalizedAngle } deg_at_${ cx } %_${ cy } %]` ;
376+ }
377+
378+ if ( fill . gradientStops . length === 1 ) {
379+ const fromStop = generateGradientStop ( "from" , fill . gradientStops [ 0 ] , globalOpacity , 0 , "deg" , 360 ) ;
380+ return [ baseClass , fromStop ] . filter ( Boolean ) . join ( " " ) ;
381+ } else if ( fill . gradientStops . length === 2 ) {
382+ const firstStop = generateGradientStop ( "from" , fill . gradientStops [ 0 ] , globalOpacity , 0 , "deg" , 360 ) ;
383+ const lastStop = generateGradientStop ( "to" , fill . gradientStops [ 1 ] , globalOpacity , 1 , "deg" , 360 ) ;
384+ return [ baseClass , firstStop , lastStop ] . filter ( Boolean ) . join ( " " ) ;
385+ } else {
386+ const firstStop = generateGradientStop ( "from" , fill . gradientStops [ 0 ] , globalOpacity , 0 , "deg" , 360 ) ;
387+ const viaStop = generateGradientStop ( "via" , fill . gradientStops [ 1 ] , globalOpacity , 0.5 , "deg" , 360 ) ;
388+ const lastStop = generateGradientStop (
389+ "to" ,
390+ fill . gradientStops [ fill . gradientStops . length - 1 ] ,
391+ globalOpacity ,
392+ 1 ,
393+ "deg" ,
394+ 360
395+ ) ;
396+ return [ baseClass , firstStop , viaStop , lastStop ] . filter ( Boolean ) . join ( " " ) ;
234397 }
235398} ;
0 commit comments