Skip to content

Commit b375997

Browse files
fix: update gradient color (#2571)
* update gradient color * fix generate function
1 parent 7577e4b commit b375997

File tree

5 files changed

+796
-137
lines changed

5 files changed

+796
-137
lines changed

apps/web/client/src/app/project/[id]/_components/editor-bar/dropdowns/color-background.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
'use client';
22

33
import { useEditorEngine } from '@/components/store/editor';
4-
import { Button } from '@onlook/ui/button';
5-
import { ToolbarButton } from '../toolbar-button';
64
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@onlook/ui/dropdown-menu';
75
import { Icons } from '@onlook/ui/icons';
86
import { observer } from 'mobx-react-lite';
@@ -11,6 +9,7 @@ import { useColorUpdate } from '../hooks/use-color-update';
119
import { useDropdownControl } from '../hooks/use-dropdown-manager';
1210
import { HoverOnlyTooltip } from '../hover-tooltip';
1311
import { ColorPickerContent } from '../inputs/color-picker';
12+
import { ToolbarButton } from '../toolbar-button';
1413
import { hasGradient } from '../utils/gradient';
1514

1615
export const ColorBackground = observer(() => {

apps/web/client/src/app/project/[id]/_components/editor-bar/inputs/color-picker.tsx

Lines changed: 26 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import type { TailwindColor } from '@onlook/models/style';
44
import {
55
ColorPicker,
66
Gradient,
7-
type GradientState,
8-
type GradientStop,
7+
type GradientState
98
} from '@onlook/ui/color-picker';
9+
import { parseGradientFromCSS } from '@onlook/ui/color-picker/Gradient';
1010
import { Icons } from '@onlook/ui/icons';
1111
import { Input } from '@onlook/ui/input';
1212
import { Separator } from '@onlook/ui/separator';
1313
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@onlook/ui/tabs';
14+
import { cn } from '@onlook/ui/utils';
1415
import { Color, toNormalCase, type Palette } from '@onlook/utility';
1516
import { useCallback, useEffect, useRef, useState } from 'react';
1617
import { useGradientUpdate } from '../hooks/use-gradient-update';
@@ -43,7 +44,7 @@ const ColorGroup = ({
4344
if (selectedRef.current) {
4445
selectedRef.current.scrollIntoView({ block: 'center' });
4546
}
46-
}, [ expanded]);
47+
}, [expanded]);
4748

4849
return (
4950
<div className="w-full group">
@@ -103,6 +104,7 @@ interface ColorPickerProps {
103104
onChangeEnd: (color: Color | TailwindColor) => void;
104105
backgroundImage?: string;
105106
isCreatingNewColor?: boolean;
107+
hideGradient?: boolean;
106108
}
107109

108110
export const ColorPickerContent: React.FC<ColorPickerProps> = ({
@@ -111,6 +113,7 @@ export const ColorPickerContent: React.FC<ColorPickerProps> = ({
111113
onChangeEnd,
112114
backgroundImage,
113115
isCreatingNewColor,
116+
hideGradient = false,
114117
}) => {
115118
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
116119
const [palette, setPalette] = useState<Palette>(color.palette);
@@ -136,119 +139,6 @@ export const ColorPickerContent: React.FC<ColorPickerProps> = ({
136139
);
137140

138141

139-
const parseGradientFromCSS = useCallback((cssValue: string): GradientState | null => {
140-
try {
141-
const normalized = cssValue.trim();
142-
143-
const linearMatch = /linear-gradient\(([^)]+)\)/.exec(normalized);
144-
const radialMatch = /radial-gradient\(([^)]+)\)/.exec(normalized);
145-
const conicMatch = /conic-gradient\(([^)]+)\)/.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+)deg/.exec(params);
155-
if (angleMatch?.[1]) {
156-
angle = parseInt(angleMatch[1]);
157-
stopsString = params.replace(/^\d+deg,?\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(/^ellipse\s+80%\s+80%\s+at\s+center,?\s*/, '');
167-
} else {
168-
type = 'radial';
169-
stopsString = params.replace(/^(circle|ellipse).*?,?\s*/, '');
170-
}
171-
} else if (conicMatch?.[1]) {
172-
const params = conicMatch[1];
173-
const angleMatch = /from\s+(\d+)deg/.exec(params);
174-
175-
if (angleMatch?.[1]) {
176-
angle = parseInt(angleMatch[1]);
177-
stopsString = params.replace(/^from\s+\d+deg,?\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-9a-fA-F]{3,8}|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|hsla\([^)]+\)|[a-zA-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-9a-fA-F]{3,8}|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|hsla\([^)]+\)|[a-zA-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-
252142
const isColorRemoved = (colorToCheck: Color) => colorToCheck.isEqual(Color.from('transparent'));
253143

254144
interface PresetGradient {
@@ -463,7 +353,7 @@ export const ColorPickerContent: React.FC<ColorPickerProps> = ({
463353
? editorEngine.style.selectedStyle?.styles.computed.backgroundImage
464354
: undefined;
465355

466-
const activeGradientSource = computedBackgroundImage || backgroundImage;
356+
const activeGradientSource = computedBackgroundImage ?? backgroundImage;
467357

468358
if (hasGradient(activeGradientSource)) {
469359
const parsed = parseGradientFromCSS(activeGradientSource!);
@@ -744,35 +634,39 @@ export const ColorPickerContent: React.FC<ColorPickerProps> = ({
744634
>
745635
Custom
746636
</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+
)}
753645
</div>
754646
{!isCreatingNewColor && (
755647
<HoverOnlyTooltip
756-
content="Remove Background Color"
648+
content="Remove Color"
757649
side="bottom"
758650
className="mt-1"
759651
hideArrow
760652
disabled={isColorRemoved(color)}
761653
>
762654
<button
763-
className={`p-1 rounded transition-colors ${
655+
className={cn(
656+
'p-1 rounded transition-colors',
764657
isColorRemoved(color)
765658
? 'bg-background-secondary'
766659
: 'hover:bg-background-tertiary'
767-
}`}
660+
)}
768661
onClick={handleRemoveColor}
769662
>
770663
<Icons.SquareX
771-
className={`h-4 w-4 ${
664+
className={cn(
665+
'h-4 w-4',
772666
isColorRemoved(color)
773667
? 'text-foreground-primary'
774668
: 'text-foreground-tertiary'
775-
}`}
669+
)}
776670
/>
777671
</button>
778672
</HoverOnlyTooltip>
@@ -875,11 +769,10 @@ export const ColorPickerContent: React.FC<ColorPickerProps> = ({
875769
<div className="flex flex-row items-center justify-between w-full px-2 py-1">
876770
<span className="text-foreground-secondary text-small">Presets</span>
877771
<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+
}`}
883776
onClick={() => setViewMode(viewMode === 'grid' ? 'list' : 'grid')}
884777
title="Toggle view mode"
885778
>

apps/web/client/src/app/project/[id]/_components/editor-bar/text-inputs/text-color.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use client';
22

3-
import { Button } from '@onlook/ui/button';
43
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@onlook/ui/dropdown-menu';
54
import { Icons } from '@onlook/ui/icons';
65
import { observer } from 'mobx-react-lite';
@@ -55,6 +54,7 @@ export const TextColor = observer(
5554
color={tempColor}
5655
onChange={handleColorUpdate}
5756
onChangeEnd={handleColorUpdateEnd}
57+
hideGradient={true}
5858
/>
5959
</DropdownMenuContent>
6060
</DropdownMenu>

0 commit comments

Comments
 (0)