@@ -4,13 +4,14 @@ import type { TailwindColor } from '@onlook/models/style';
4
4
import {
5
5
ColorPicker ,
6
6
Gradient ,
7
- type GradientState ,
8
- type GradientStop ,
7
+ type GradientState
9
8
} from '@onlook/ui/color-picker' ;
9
+ import { parseGradientFromCSS } from '@onlook/ui/color-picker/Gradient' ;
10
10
import { Icons } from '@onlook/ui/icons' ;
11
11
import { Input } from '@onlook/ui/input' ;
12
12
import { Separator } from '@onlook/ui/separator' ;
13
13
import { Tabs , TabsContent , TabsList , TabsTrigger } from '@onlook/ui/tabs' ;
14
+ import { cn } from '@onlook/ui/utils' ;
14
15
import { Color , toNormalCase , type Palette } from '@onlook/utility' ;
15
16
import { useCallback , useEffect , useRef , useState } from 'react' ;
16
17
import { useGradientUpdate } from '../hooks/use-gradient-update' ;
@@ -43,7 +44,7 @@ const ColorGroup = ({
43
44
if ( selectedRef . current ) {
44
45
selectedRef . current . scrollIntoView ( { block : 'center' } ) ;
45
46
}
46
- } , [ expanded ] ) ;
47
+ } , [ expanded ] ) ;
47
48
48
49
return (
49
50
< div className = "w-full group" >
@@ -103,6 +104,7 @@ interface ColorPickerProps {
103
104
onChangeEnd : ( color : Color | TailwindColor ) => void ;
104
105
backgroundImage ?: string ;
105
106
isCreatingNewColor ?: boolean ;
107
+ hideGradient ?: boolean ;
106
108
}
107
109
108
110
export const ColorPickerContent : React . FC < ColorPickerProps > = ( {
@@ -111,6 +113,7 @@ export const ColorPickerContent: React.FC<ColorPickerProps> = ({
111
113
onChangeEnd,
112
114
backgroundImage,
113
115
isCreatingNewColor,
116
+ hideGradient = false ,
114
117
} ) => {
115
118
const [ viewMode , setViewMode ] = useState < 'grid' | 'list' > ( 'grid' ) ;
116
119
const [ palette , setPalette ] = useState < Palette > ( color . palette ) ;
@@ -136,119 +139,6 @@ export const ColorPickerContent: React.FC<ColorPickerProps> = ({
136
139
) ;
137
140
138
141
139
- const parseGradientFromCSS = useCallback ( ( cssValue : string ) : GradientState | null => {
140
- try {
141
- const normalized = cssValue . trim ( ) ;
142
-
143
- const linearMatch = / l i n e a r - g r a d i e n t \( ( [ ^ ) ] + ) \) / . exec ( normalized ) ;
144
- const radialMatch = / r a d i a l - g r a d i e n t \( ( [ ^ ) ] + ) \) / . exec ( normalized ) ;
145
- const conicMatch = / c o n i c - g r a d i e n t \( ( [ ^ ) ] + ) \) / . exec ( normalized ) ;
146
-
147
- let type : GradientState [ 'type' ] = 'linear' ;
148
- let angle = 90 ;
149
- let stopsString = '' ;
150
-
151
- if ( linearMatch ?. [ 1 ] ) {
152
- type = 'linear' ;
153
- const params = linearMatch [ 1 ] ;
154
- const angleMatch = / ( \d + ) d e g / . exec ( params ) ;
155
- if ( angleMatch ?. [ 1 ] ) {
156
- angle = parseInt ( angleMatch [ 1 ] ) ;
157
- stopsString = params . replace ( / ^ \d + d e g , ? \s * / , '' ) ;
158
- } else {
159
- stopsString = params ;
160
- }
161
- } else if ( radialMatch ?. [ 1 ] ) {
162
- const params = radialMatch [ 1 ] ;
163
- // Check if it's a diamond gradient (ellipse 80% 80% pattern)
164
- if ( params . includes ( 'ellipse 80% 80%' ) ) {
165
- type = 'diamond' ;
166
- stopsString = params . replace ( / ^ e l l i p s e \s + 8 0 % \s + 8 0 % \s + a t \s + c e n t e r , ? \s * / , '' ) ;
167
- } else {
168
- type = 'radial' ;
169
- stopsString = params . replace ( / ^ ( c i r c l e | e l l i p s e ) .* ?, ? \s * / , '' ) ;
170
- }
171
- } else if ( conicMatch ?. [ 1 ] ) {
172
- const params = conicMatch [ 1 ] ;
173
- const angleMatch = / f r o m \s + ( \d + ) d e g / . exec ( params ) ;
174
-
175
- if ( angleMatch ?. [ 1 ] ) {
176
- angle = parseInt ( angleMatch [ 1 ] ) ;
177
- stopsString = params . replace ( / ^ f r o m \s + \d + d e g , ? \s * / , '' ) ;
178
- } else {
179
- stopsString = params ;
180
- }
181
-
182
- // Parse stops first to check for angular pattern
183
- const stopMatches = stopsString . split ( / , (? ! [ ^ ( ) ] * \) ) / ) ;
184
- const tempStops : { color : string ; position : number } [ ] = [ ] ;
185
-
186
- stopMatches . forEach ( ( stop , index ) => {
187
- const trimmed = stop . trim ( ) ;
188
- const match = / ^ ( # [ 0 - 9 a - f A - F ] { 3 , 8 } | r g b \( [ ^ ) ] + \) | r g b a \( [ ^ ) ] + \) | h s l \( [ ^ ) ] + \) | h s l a \( [ ^ ) ] + \) | [ a - z A - Z ] + ) \s * ( \d + (?: \. \d + ) ? ) ? % ? / . exec ( trimmed ) ;
189
- if ( match ?. [ 1 ] ) {
190
- const color = match [ 1 ] ;
191
- const position = match [ 2 ]
192
- ? parseFloat ( match [ 2 ] )
193
- : ( index / Math . max ( 1 , stopMatches . length - 1 ) ) * 100 ;
194
- tempStops . push ( { color, position } ) ;
195
- }
196
- } ) ;
197
-
198
- // Check if it's an angular gradient (has duplicate end color at 100%)
199
- const firstStop = tempStops [ 0 ] ;
200
- const lastStop = tempStops [ tempStops . length - 1 ] ;
201
- const isAngular = tempStops . length >= 3 &&
202
- firstStop && lastStop &&
203
- firstStop . color === lastStop . color &&
204
- Math . abs ( lastStop . position - 100 ) < 1 ;
205
-
206
- if ( isAngular ) {
207
- type = 'angular' ;
208
- // Remove the duplicate end color for angular gradients
209
- tempStops . pop ( ) ;
210
- // Reconstruct stopsString without the duplicate
211
- stopsString = tempStops . map ( stop =>
212
- stop . position === Math . round ( stop . position )
213
- ? `${ stop . color } ${ Math . round ( stop . position ) } %`
214
- : `${ stop . color } ${ stop . position } %`
215
- ) . join ( ', ' ) ;
216
- } else {
217
- type = 'conic' ;
218
- }
219
- } else {
220
- return null ;
221
- }
222
-
223
- const stops : GradientStop [ ] = [ ] ;
224
- const stopMatches = stopsString . split ( / , (? ! [ ^ ( ) ] * \) ) / ) ;
225
-
226
- stopMatches . forEach ( ( stop , index ) => {
227
- const trimmed = stop . trim ( ) ;
228
- const match = / ^ ( # [ 0 - 9 a - f A - F ] { 3 , 8 } | r g b \( [ ^ ) ] + \) | r g b a \( [ ^ ) ] + \) | h s l \( [ ^ ) ] + \) | h s l a \( [ ^ ) ] + \) | [ a - z A - Z ] + ) \s * ( \d + (?: \. \d + ) ? ) ? % ? / . exec ( trimmed ) ;
229
- if ( match ?. [ 1 ] ) {
230
- const color = match [ 1 ] ;
231
- const position = match [ 2 ]
232
- ? parseFloat ( match [ 2 ] )
233
- : ( index / Math . max ( 1 , stopMatches . length - 1 ) ) * 100 ;
234
- stops . push ( {
235
- id : `stop-${ index + 1 } ` ,
236
- color,
237
- position : Math . round ( position ) ,
238
- opacity : 100 , // Default opacity for parsed gradients
239
- } ) ;
240
- }
241
- } ) ;
242
-
243
- if ( stops . length < 2 ) return null ;
244
-
245
- return { type, angle, stops } ;
246
- } catch ( error ) {
247
- console . warn ( 'Failed to parse gradient:' , error ) ;
248
- return null ;
249
- }
250
- } , [ ] ) ;
251
-
252
142
const isColorRemoved = ( colorToCheck : Color ) => colorToCheck . isEqual ( Color . from ( 'transparent' ) ) ;
253
143
254
144
interface PresetGradient {
@@ -463,7 +353,7 @@ export const ColorPickerContent: React.FC<ColorPickerProps> = ({
463
353
? editorEngine . style . selectedStyle ?. styles . computed . backgroundImage
464
354
: undefined ;
465
355
466
- const activeGradientSource = computedBackgroundImage || backgroundImage ;
356
+ const activeGradientSource = computedBackgroundImage ?? backgroundImage ;
467
357
468
358
if ( hasGradient ( activeGradientSource ) ) {
469
359
const parsed = parseGradientFromCSS ( activeGradientSource ! ) ;
@@ -744,35 +634,39 @@ export const ColorPickerContent: React.FC<ColorPickerProps> = ({
744
634
>
745
635
Custom
746
636
</ TabsTrigger >
747
- < TabsTrigger
748
- value = { TabValue . GRADIENT }
749
- className = "flex items-center justify-center px-1.5 py-1 text-xs rounded-md bg-transparent hover:bg-background-secondary hover:text-foreground-primary transition-colors"
750
- >
751
- Gradient
752
- </ TabsTrigger >
637
+ { ! hideGradient && (
638
+ < TabsTrigger
639
+ value = { TabValue . GRADIENT }
640
+ className = "flex items-center justify-center px-1.5 py-1 text-xs rounded-md bg-transparent hover:bg-background-secondary hover:text-foreground-primary transition-colors"
641
+ >
642
+ Gradient
643
+ </ TabsTrigger >
644
+ ) }
753
645
</ div >
754
646
{ ! isCreatingNewColor && (
755
647
< HoverOnlyTooltip
756
- content = "Remove Background Color"
648
+ content = "Remove Color"
757
649
side = "bottom"
758
650
className = "mt-1"
759
651
hideArrow
760
652
disabled = { isColorRemoved ( color ) }
761
653
>
762
654
< button
763
- className = { `p-1 rounded transition-colors ${
655
+ className = { cn (
656
+ 'p-1 rounded transition-colors' ,
764
657
isColorRemoved ( color )
765
658
? 'bg-background-secondary'
766
659
: 'hover:bg-background-tertiary'
767
- } ` }
660
+ ) }
768
661
onClick = { handleRemoveColor }
769
662
>
770
663
< Icons . SquareX
771
- className = { `h-4 w-4 ${
664
+ className = { cn (
665
+ 'h-4 w-4' ,
772
666
isColorRemoved ( color )
773
667
? 'text-foreground-primary'
774
668
: 'text-foreground-tertiary'
775
- } ` }
669
+ ) }
776
670
/>
777
671
</ button >
778
672
</ HoverOnlyTooltip >
@@ -875,11 +769,10 @@ export const ColorPickerContent: React.FC<ColorPickerProps> = ({
875
769
< div className = "flex flex-row items-center justify-between w-full px-2 py-1" >
876
770
< span className = "text-foreground-secondary text-small" > Presets</ span >
877
771
< button
878
- className = { `px-1 py-1 text-xs transition-colors w-6 h-6 flex items-center justify-center rounded ${
879
- viewMode === 'grid'
880
- ? 'text-foreground-secondary hover:text-foreground-primary hover:bg-background-hover'
881
- : 'text-foreground-primary bg-background-secondary'
882
- } `}
772
+ className = { `px-1 py-1 text-xs transition-colors w-6 h-6 flex items-center justify-center rounded ${ viewMode === 'grid'
773
+ ? 'text-foreground-secondary hover:text-foreground-primary hover:bg-background-hover'
774
+ : 'text-foreground-primary bg-background-secondary'
775
+ } `}
883
776
onClick = { ( ) => setViewMode ( viewMode === 'grid' ? 'list' : 'grid' ) }
884
777
title = "Toggle view mode"
885
778
>
0 commit comments