@@ -60,6 +60,10 @@ const gradientFunctionNames: Record<
6060 } ,
6161} ;
6262
63+ /**
64+ * Checks if a CSS value string starts with a gradient function of the specified type.
65+ * Handles both base and repeating gradient functions (e.g., "linear-gradient" and "repeating-linear-gradient").
66+ */
6367const startsWithGradientFunction = ( value : string , type : GradientType ) => {
6468 const normalized = value . trim ( ) . toLowerCase ( ) ;
6569 const { base, repeating } = gradientFunctionNames [ type ] ;
@@ -87,23 +91,16 @@ export const percentUnitOptions: UnitOption[] = [
8791 } ,
8892] ;
8993
90- const createKeywordValue = ( value : string ) : KeywordValue => ( {
91- type : "keyword" ,
92- value,
93- } ) ;
94-
95- const createCenterKeyword = ( ) => createKeywordValue ( "center" ) ;
96-
9794export const gradientPositionXOptions : KeywordValue [ ] = [
98- createCenterKeyword ( ) ,
99- createKeywordValue ( " left") ,
100- createKeywordValue ( " right") ,
95+ { type : "keyword" , value : "center" } ,
96+ { type : "keyword" , value : " left" } ,
97+ { type : "keyword" , value : " right" } ,
10198] ;
10299
103100export const gradientPositionYOptions : KeywordValue [ ] = [
104- createCenterKeyword ( ) ,
105- createKeywordValue ( " top") ,
106- createKeywordValue ( " bottom") ,
101+ { type : "keyword" , value : "center" } ,
102+ { type : "keyword" , value : " top" } ,
103+ { type : "keyword" , value : " bottom" } ,
107104] ;
108105
109106const getAxisPositionValue = (
@@ -120,11 +117,15 @@ const getAxisPositionValue = (
120117 return parsed ;
121118} ;
122119
120+ /**
121+ * Parses a gradient position string into separate x and y StyleValues.
122+ * Defaults to "center center" if position is undefined or parsing fails.
123+ */
123124export const parseGradientPositionValues = ( position ?: string ) => {
124125 if ( position === undefined ) {
125126 return {
126- xValue : createCenterKeyword ( ) ,
127- yValue : createCenterKeyword ( ) ,
127+ xValue : { type : "keyword" as const , value : "center" } ,
128+ yValue : { type : "keyword" as const , value : "center" } ,
128129 } as const ;
129130 }
130131 try {
@@ -134,27 +135,33 @@ export const parseGradientPositionValues = (position?: string) => {
134135 // Use the real background-position longhand when parsing so we can reuse
135136 // its CSS syntax rules, but assign the result to the gradient-specific
136137 // custom property downstream.
137- xValue :
138- getAxisPositionValue ( backgroundPositionXLonghand , xLonghand ?. [ 1 ] ) ??
139- createCenterKeyword ( ) ,
140- yValue :
141- getAxisPositionValue ( backgroundPositionYLonghand , yLonghand ?. [ 1 ] ) ??
142- createCenterKeyword ( ) ,
138+ xValue : getAxisPositionValue (
139+ backgroundPositionXLonghand ,
140+ xLonghand ?. [ 1 ]
141+ ) ?? { type : "keyword" as const , value : "center" } ,
142+ yValue : getAxisPositionValue (
143+ backgroundPositionYLonghand ,
144+ yLonghand ?. [ 1 ]
145+ ) ?? { type : "keyword" as const , value : "center" } ,
143146 } as const ;
144147 } catch {
145148 return {
146- xValue : createCenterKeyword ( ) ,
147- yValue : createCenterKeyword ( ) ,
149+ xValue : { type : "keyword" as const , value : "center" } ,
150+ yValue : { type : "keyword" as const , value : "center" } ,
148151 } as const ;
149152 }
150153} ;
151154
155+ /**
156+ * Formats x and y position values into a CSS position string.
157+ * Omits "center center" as it's the default. Returns just x if y is center.
158+ */
152159export const formatGradientPositionValues = (
153160 xValue ?: StyleValue ,
154161 yValue ?: StyleValue
155162) => {
156- const x = toValue ( xValue ?? createCenterKeyword ( ) ) ;
157- const y = toValue ( yValue ?? createCenterKeyword ( ) ) ;
163+ const x = toValue ( xValue ?? { type : "keyword" as const , value : "center" } ) ;
164+ const y = toValue ( yValue ?? { type : "keyword" as const , value : "center" } ) ;
158165 if ( x === "center" && y === "center" ) {
159166 return ;
160167 }
@@ -190,6 +197,11 @@ const angleUnitToDegrees = (value: UnitValue): number | undefined => {
190197 }
191198} ;
192199
200+ /**
201+ * Converts a UnitValue to a PercentUnitValue.
202+ * For percent units, clamps to 0-100. For angle units, converts to percent (0-100 representing 0-360deg).
203+ * Returns undefined for unsupported unit types.
204+ */
193205const toPercentUnitValue = ( value : UnitValue ) : PercentUnitValue | undefined => {
194206 if ( value . unit === "%" ) {
195207 return {
@@ -286,7 +298,7 @@ export const isRadialGradient = (
286298) : gradient is ParsedRadialGradient => gradient . type === "radial" ;
287299
288300/**
289- * Get the default angle for a gradient according to CSS spec.
301+ * Returns the CSS spec default angle for each gradient type:
290302 * - linear-gradient: 180deg (to bottom)
291303 * - conic-gradient: 0deg (from top)
292304 * - radial-gradient: undefined (no angle)
@@ -330,6 +342,10 @@ export const getPercentUnit = (
330342 }
331343} ;
332344
345+ /**
346+ * Converts repeating-*-gradient to *-gradient while preserving leading whitespace.
347+ * Returns both the normalized string and whether it was originally repeating.
348+ */
333349export const normalizeGradientInput = (
334350 gradientString : string ,
335351 gradientType : GradientType
@@ -401,6 +417,10 @@ export const sideOrCornerToAngle = (
401417 }
402418} ;
403419
420+ /**
421+ * Interpolates missing stop positions proportionally between defined positions.
422+ * Only works with percent units - returns original gradient for non-percent units.
423+ */
404424export const fillMissingStopPositions = < T extends ParsedGradient > (
405425 gradient : T
406426) : T => {
@@ -501,11 +521,9 @@ export const fillMissingStopPositions = <T extends ParsedGradient>(
501521 } as T ;
502522} ;
503523
504- const cloneVarValue = ( value : VarValue ) : VarValue => ( {
505- ...value ,
506- fallback : value . fallback && { ...value . fallback } ,
507- } ) ;
508-
524+ /**
525+ * Clones stop values, deep cloning var fallbacks to avoid shared references.
526+ */
509527const cloneGradientStopValue = <
510528 Value extends GradientStop [ "position" ] | GradientStop [ "hint" ] ,
511529> (
@@ -525,50 +543,48 @@ const cloneGradientStopValue = <
525543 return { ...value } ;
526544} ;
527545
546+ /**
547+ * Clones colors, deep cloning var fallbacks to avoid shared references.
548+ */
528549const cloneGradientStopColor = (
529550 color : GradientStop [ "color" ] | undefined
530551) : GradientStop [ "color" ] => {
531552 if ( color === undefined ) {
532- return { ...fallbackStopColor } satisfies GradientStop [ "color" ] ;
553+ return { ...fallbackStopColor } ;
533554 }
534555 if ( color . type === "var" ) {
535556 return {
536557 ...color ,
537558 fallback : color . fallback && { ...color . fallback } ,
538- } satisfies GradientStop [ "color" ] ;
539- }
540- if ( color . type === "rgb" ) {
541- return { ...color } satisfies GradientStop [ "color" ] ;
559+ } ;
542560 }
543- return { ...color } satisfies GradientStop [ "color" ] ;
544- } ;
545-
546- const createSolidGradientStops = ( color : GradientStop [ "color" ] ) => {
547- const firstColor = cloneGradientStopColor ( color ) ;
548- const secondColor = cloneGradientStopColor ( color ) ;
549- return [
550- {
551- color : firstColor ,
552- position : { type : "unit" , unit : "%" , value : 0 } ,
553- } ,
554- {
555- color : secondColor ,
556- position : { type : "unit" , unit : "%" , value : 100 } ,
557- } ,
558- ] satisfies GradientStop [ ] ;
561+ return { ...color } ;
559562} ;
560563
564+ /**
565+ * Creates a solid color gradient (two identical stops at 0% and 100%).
566+ */
561567export const createSolidLinearGradient = (
562568 color : GradientStop [ "color" ] ,
563569 base ?: ParsedLinearGradient
564570) : ParsedLinearGradient => {
565- const stops = createSolidGradientStops ( color ) ;
571+ const firstColor = cloneGradientStopColor ( color ) ;
572+ const secondColor = cloneGradientStopColor ( color ) ;
566573 return {
567574 type : "linear" ,
568575 angle : base ?. angle ,
569576 sideOrCorner : base ?. sideOrCorner ,
570- stops,
571- } satisfies ParsedLinearGradient ;
577+ stops : [
578+ {
579+ color : firstColor ,
580+ position : { type : "unit" , unit : "%" , value : 0 } ,
581+ } ,
582+ {
583+ color : secondColor ,
584+ position : { type : "unit" , unit : "%" , value : 100 } ,
585+ } ,
586+ ] ,
587+ } ;
572588} ;
573589
574590type AngleUnitValue = UnitValue & { unit : AngleUnit } ;
@@ -582,14 +598,17 @@ const resolveAnglePrimitive = (
582598 }
583599
584600 if ( value . type === "var" ) {
585- return cloneVarValue ( value ) ;
601+ return {
602+ ...value ,
603+ fallback : value . fallback && { ...value . fallback } ,
604+ } ;
586605 }
587606
588607 if ( value . type === "unit" && isAngleUnit ( value . unit ) ) {
589608 return {
590609 ...value ,
591610 unit : value . unit ,
592- } satisfies AngleUnitValue ;
611+ } ;
593612 }
594613
595614 return ;
@@ -689,6 +708,9 @@ const normalizeStopsForPicker = <T extends ParsedGradient>(gradient: T): T => {
689708 } as T ;
690709} ;
691710
711+ /**
712+ * Converts conic angle units (deg, turn, etc.) to percent units for picker UI.
713+ */
692714const convertConicStopsToPercent = < T extends ParsedGradient > (
693715 gradient : T
694716) : T => {
@@ -748,6 +770,9 @@ const convertConicStopsToPercent = <T extends ParsedGradient>(
748770 } as T ;
749771} ;
750772
773+ /**
774+ * Prepares gradient for picker UI: converts angles to percent, resolves vars, fills positions, applies hint overrides.
775+ */
751776export const resolveGradientForPicker = < T extends ParsedGradient > (
752777 gradient : T ,
753778 hintOverrides : ReadonlyMap < number , PercentUnitValue >
@@ -782,6 +807,9 @@ export const resolveGradientForPicker = <T extends ParsedGradient>(
782807 } as T ;
783808} ;
784809
810+ /**
811+ * Returns same map reference if unchanged for referential equality.
812+ */
785813export const removeHintOverride = (
786814 overrides : Map < number , PercentUnitValue > ,
787815 stopIndex : number
@@ -794,6 +822,9 @@ export const removeHintOverride = (
794822 return next ;
795823} ;
796824
825+ /**
826+ * Returns same map reference if unchanged for referential equality.
827+ */
797828export const setHintOverride = (
798829 overrides : Map < number , PercentUnitValue > ,
799830 stopIndex : number ,
@@ -817,6 +848,9 @@ export const setHintOverride = (
817848 return next ;
818849} ;
819850
851+ /**
852+ * Returns same map reference if unchanged for referential equality.
853+ */
820854export const pruneHintOverrides = (
821855 overrides : Map < number , PercentUnitValue > ,
822856 stopCount : number
@@ -835,13 +869,11 @@ export const pruneHintOverrides = (
835869 return changed ? next : overrides ;
836870} ;
837871
838- // Helper to get stop position as a number (0-100) for sorting and calculations
839872export const getStopPosition = ( stop : GradientStop ) : number =>
840873 stop . position ?. type === "unit" && stop . position . unit === "%"
841874 ? stop . position . value
842875 : 0 ;
843876
844- // Reindex hint overrides after a stop is deleted
845877export const reindexHintOverrides = (
846878 overrides : Map < number , PercentUnitValue > ,
847879 deletedIndex : number
@@ -857,7 +889,6 @@ export const reindexHintOverrides = (
857889 return reindexed ;
858890} ;
859891
860- // Sort gradient stops by position and reindex hint overrides to match
861892export const sortGradientStops = (
862893 gradient : ParsedGradient ,
863894 hintOverrides : Map < number , PercentUnitValue >
@@ -905,6 +936,9 @@ export type ReverseStopsResolution<T extends ParsedGradient> =
905936 }
906937 | { type : "none" } ;
907938
939+ /**
940+ * Reverses stops and mirrors percent positions (0% becomes 100%, etc.).
941+ */
908942export const resolveReverseStops = < T extends ParsedGradient > (
909943 gradient : T ,
910944 selectedStopIndex : number
@@ -1208,7 +1242,9 @@ type GradientByType<T extends GradientType> = Extract<
12081242 { type : T }
12091243> ;
12101244
1211- // Cache for parsed gradients - avoids re-parsing the same gradient string
1245+ /**
1246+ * Cached to avoid re-parsing the same string.
1247+ */
12121248const parsedGradientCache = new Map < string , ParsedGradient | undefined > ( ) ;
12131249
12141250export const parseAnyGradient = ( value : string ) : ParsedGradient | undefined => {
@@ -1252,6 +1288,11 @@ export const convertGradientToTarget = <Target extends GradientType>(
12521288 return converted as GradientByType < Target > ;
12531289} ;
12541290
1291+ /**
1292+ * Formats a gradient for a specific background type.
1293+ * Handles solid color conversion and gradient type conversions.
1294+ * Returns the formatted CSS gradient string.
1295+ */
12551296export const formatGradientForType = (
12561297 styleValue : StyleValue | undefined ,
12571298 target : Exclude < BackgroundType , "image" >
@@ -1273,6 +1314,11 @@ export const formatGradientForType = (
12731314 return formatRadialGradient ( parsed ) ;
12741315} ;
12751316
1317+ /**
1318+ * Detects the background type from a StyleValue.
1319+ * Returns "solid" for uniform linear gradients, specific gradient types for gradients,
1320+ * and "image" for non-gradient values or unparseable gradients.
1321+ */
12761322export const detectBackgroundType = (
12771323 styleValue ?: StyleValue
12781324) : BackgroundType => {
@@ -1323,6 +1369,11 @@ export const detectBackgroundType = (
13231369 return "image" ;
13241370} ;
13251371
1372+ /**
1373+ * Gets a specific background layer item from a ComputedStyleDecl.
1374+ * For index 0, returns the first layer or the cascaded value itself.
1375+ * For index > 0, delegates to getRepeatedStyleItem.
1376+ */
13261377export const getBackgroundStyleItem = (
13271378 styleDecl : ComputedStyleDecl ,
13281379 index : number
0 commit comments