Skip to content

Commit 9cd60f1

Browse files
feat: apply image background to component (#2508)
* feat: input image background * update tw merge
1 parent a0eb9c6 commit 9cd60f1

File tree

15 files changed

+1112
-117
lines changed

15 files changed

+1112
-117
lines changed

apps/web/client/src/app/project/[id]/_components/editor-bar/div-selected.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ import { FontWeightSelector } from './text-inputs/font/font-weight';
2121
import { TextColor } from './text-inputs/text-color';
2222
import { AdvancedTypography } from './text-inputs/advanced-typography';
2323
import { TextAlignSelector } from './text-inputs/text-align';
24+
import { InputImage } from './inputs/input-image';
2425

2526
// Group definitions for the div-selected toolbar
2627
export const DIV_SELECTED_GROUPS = [
2728

2829
{
2930
key: 'base',
3031
label: 'Base',
31-
components: [<ColorBackground />, <Border />, <BorderColor />, <Radius />],
32+
components: [<ColorBackground />, <InputImage />, <Border />, <BorderColor />, <Radius />],
3233
},
3334
{
3435
key: 'layout',
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import type { EditorEngine } from '@/components/store/editor/engine';
2+
import { toast } from '@onlook/ui/sonner';
3+
import { useCallback, useEffect, useMemo, useState } from 'react';
4+
5+
export enum ImageFit {
6+
FILL = 'fill',
7+
FIT = 'fit',
8+
STRETCH = 'stretch',
9+
CENTER = 'center',
10+
TILE = 'tile',
11+
AUTO = 'auto',
12+
}
13+
14+
export const IMAGE_FIT_OPTIONS = [
15+
{ value: ImageFit.FILL, label: 'Fill' },
16+
{ value: ImageFit.FIT, label: 'Fit' },
17+
{ value: ImageFit.STRETCH, label: 'Stretch' },
18+
{ value: ImageFit.CENTER, label: 'Center' },
19+
{ value: ImageFit.TILE, label: 'Tile' },
20+
{ value: ImageFit.AUTO, label: 'Auto' },
21+
] as const;
22+
23+
const FitToStyle: Record<ImageFit, Record<string, string>> = {
24+
[ImageFit.FILL]: {
25+
backgroundSize: 'cover',
26+
backgroundPosition: 'center',
27+
backgroundRepeat: 'no-repeat',
28+
},
29+
[ImageFit.FIT]: {
30+
backgroundSize: 'contain',
31+
backgroundPosition: 'center',
32+
backgroundRepeat: 'no-repeat',
33+
},
34+
[ImageFit.STRETCH]: {
35+
backgroundSize: '100% 100%',
36+
backgroundPosition: 'center',
37+
backgroundRepeat: 'no-repeat',
38+
},
39+
[ImageFit.CENTER]: {
40+
backgroundSize: 'auto',
41+
backgroundPosition: 'center',
42+
backgroundRepeat: 'no-repeat',
43+
},
44+
[ImageFit.TILE]: {
45+
backgroundSize: 'auto',
46+
backgroundPosition: 'center',
47+
backgroundRepeat: 'repeat',
48+
},
49+
[ImageFit.AUTO]: {
50+
backgroundSize: 'auto',
51+
backgroundPosition: 'center',
52+
backgroundRepeat: 'no-repeat',
53+
},
54+
};
55+
56+
const cssToImageFit = (backgroundSize: string, backgroundRepeat: string): ImageFit => {
57+
if (backgroundSize === 'cover') return ImageFit.FILL;
58+
if (backgroundSize === 'contain') return ImageFit.FIT;
59+
if (backgroundSize === '100% 100%') return ImageFit.STRETCH;
60+
if (backgroundSize === 'auto' && backgroundRepeat === 'repeat') return ImageFit.TILE;
61+
if (backgroundSize === 'auto') return ImageFit.CENTER;
62+
return ImageFit.AUTO;
63+
};
64+
65+
export const useBackgroundImage = (editorEngine: EditorEngine) => {
66+
const [fillOption, setFillOption] = useState<ImageFit>(ImageFit.FILL);
67+
68+
const currentBackgroundImage = useMemo(() => {
69+
const selectedImage = editorEngine.style.selectedStyle?.styles.computed.backgroundImage;
70+
if (selectedImage && selectedImage !== 'none') {
71+
return selectedImage;
72+
}
73+
return null;
74+
}, [editorEngine.style.selectedStyle?.styles.computed.backgroundImage]);
75+
76+
const currentBackgroundSize = useMemo(() => {
77+
const selectedStyle = editorEngine.style.selectedStyle?.styles.computed.backgroundSize;
78+
const selectedRepeat = editorEngine.style.selectedStyle?.styles.computed.backgroundRepeat;
79+
80+
if (!selectedStyle) return null;
81+
82+
return cssToImageFit(selectedStyle, selectedRepeat ?? 'no-repeat');
83+
}, [
84+
editorEngine.style.selectedStyle?.styles.computed.backgroundSize,
85+
editorEngine.style.selectedStyle?.styles.computed.backgroundRepeat,
86+
]);
87+
88+
const applyFillOption = useCallback(
89+
(fillOptionValue: ImageFit) => {
90+
try {
91+
const selected = editorEngine.elements.selected;
92+
93+
if (!selected || selected.length === 0) {
94+
console.warn('No elements selected to apply fill option');
95+
return;
96+
}
97+
98+
const cssStyles = FitToStyle[fillOptionValue];
99+
editorEngine.style.updateMultiple(cssStyles);
100+
} catch (error) {
101+
console.error('Failed to apply fill option:', error);
102+
toast.error('Failed to apply fill option', {
103+
description: error instanceof Error ? error.message : String(error),
104+
});
105+
}
106+
},
107+
[],
108+
);
109+
110+
const handleFillOptionChange = useCallback(
111+
(option: ImageFit) => {
112+
setFillOption(option);
113+
applyFillOption(option);
114+
},
115+
[applyFillOption],
116+
);
117+
118+
const removeBackground = useCallback(async () => {
119+
try {
120+
const styles = {
121+
backgroundImage: 'none',
122+
backgroundSize: 'auto',
123+
backgroundRepeat: 'repeat',
124+
backgroundPosition: 'auto',
125+
};
126+
127+
editorEngine.style.updateMultiple(styles);
128+
editorEngine.image.setSelectedImage(null);
129+
editorEngine.image.setPreviewImage(null);
130+
} catch (error) {
131+
console.error('Failed to remove background:', error);
132+
toast.error('Failed to remove background', {
133+
description: error instanceof Error ? error.message : String(error),
134+
});
135+
}
136+
}, [editorEngine]);
137+
138+
useEffect(() => {
139+
if (currentBackgroundSize) {
140+
setFillOption(currentBackgroundSize);
141+
}
142+
}, [currentBackgroundSize]);
143+
144+
useEffect(() => {
145+
return () => {
146+
if (editorEngine.image.isSelectingImage) {
147+
editorEngine.image.setIsSelectingImage(false);
148+
editorEngine.image.setSelectedImage(null);
149+
editorEngine.image.setPreviewImage(null);
150+
}
151+
};
152+
}, [editorEngine]);
153+
154+
return {
155+
fillOption,
156+
currentBackgroundImage,
157+
handleFillOptionChange,
158+
removeBackground,
159+
ImageFit,
160+
IMAGE_FIT_OPTIONS,
161+
};
162+
};

0 commit comments

Comments
 (0)