create-nextblock 0.2.33 → 0.2.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +67 -67
- package/templates/nextblock-template/app/[slug]/page.tsx +4 -4
- package/templates/nextblock-template/app/cms/blocks/actions.ts +5 -5
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -350
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +13 -16
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +1 -1
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +24 -42
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +6 -6
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +35 -56
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -81
- package/templates/nextblock-template/app/cms/media/actions.ts +12 -12
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +3 -3
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +1 -1
- package/templates/nextblock-template/app/cms/media/page.tsx +120 -120
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -87
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +10 -16
- package/templates/nextblock-template/app/cms/revisions/service.ts +344 -344
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +1 -1
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +0 -1
- package/templates/nextblock-template/app/providers.tsx +2 -2
- package/templates/nextblock-template/components/BlockRenderer.tsx +9 -9
- package/templates/nextblock-template/components/ResponsiveNav.tsx +22 -22
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +26 -26
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +41 -41
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +7 -7
- package/templates/nextblock-template/components/theme-switcher.tsx +78 -78
- package/templates/nextblock-template/eslint.config.mjs +35 -37
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +10 -10
- package/templates/nextblock-template/next-env.d.ts +6 -6
- package/templates/nextblock-template/package.json +1 -1
|
@@ -1,350 +1,348 @@
|
|
|
1
|
-
// app/cms/blocks/components/BackgroundSelector.tsx
|
|
2
|
-
"use client";
|
|
3
|
-
|
|
4
|
-
import React, { useState, useEffect } from "react";
|
|
5
|
-
import Image from "next/image";
|
|
6
|
-
import { Label, Select, SelectTrigger, SelectContent, SelectItem, SelectValue, Button, Input, Checkbox } from "@nextblock-cms/ui";
|
|
7
|
-
import { CustomSelectWithInput, ColorPicker } from "@nextblock-cms/ui";
|
|
8
|
-
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
|
9
|
-
import { ImageIcon, X as XIcon, Save } from "lucide-react";
|
|
10
|
-
import { cn } from "@nextblock-cms/utils";
|
|
11
|
-
import type { Database } from "@nextblock-cms/db";
|
|
12
|
-
import type { SectionBlockContent } from "@/lib/blocks/blockRegistry";
|
|
13
|
-
import MediaPickerDialog from "@/app/cms/media/components/MediaPickerDialog";
|
|
14
|
-
|
|
15
|
-
type Media = Database["public"]["Tables"]["media"]["Row"];
|
|
16
|
-
|
|
17
|
-
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
|
|
18
|
-
|
|
19
|
-
interface BackgroundSelectorProps {
|
|
20
|
-
background: SectionBlockContent["background"];
|
|
21
|
-
onChange: (newBackground: SectionBlockContent["background"]) => void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
<SelectItem value="
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
<SelectItem value="
|
|
239
|
-
<SelectItem value="
|
|
240
|
-
<SelectItem value="
|
|
241
|
-
<SelectItem value="left">Left</SelectItem>
|
|
242
|
-
<SelectItem value="right">Right</SelectItem>
|
|
243
|
-
<SelectItem value="
|
|
244
|
-
<SelectItem value="
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
{ value: "to
|
|
269
|
-
{ value: "to
|
|
270
|
-
{ value: "to
|
|
271
|
-
{ value: "to
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
{ value: "to
|
|
308
|
-
{ value: "to
|
|
309
|
-
{ value: "to
|
|
310
|
-
{ value: "to
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const {
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
};
|
|
350
|
-
}
|
|
1
|
+
// app/cms/blocks/components/BackgroundSelector.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import React, { useState, useEffect } from "react";
|
|
5
|
+
import Image from "next/image";
|
|
6
|
+
import { Label, Select, SelectTrigger, SelectContent, SelectItem, SelectValue, Button, Input, Checkbox } from "@nextblock-cms/ui";
|
|
7
|
+
import { CustomSelectWithInput, ColorPicker } from "@nextblock-cms/ui";
|
|
8
|
+
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
|
9
|
+
import { ImageIcon, X as XIcon, Save } from "lucide-react";
|
|
10
|
+
import { cn } from "@nextblock-cms/utils";
|
|
11
|
+
import type { Database } from "@nextblock-cms/db";
|
|
12
|
+
import type { SectionBlockContent } from "@/lib/blocks/blockRegistry";
|
|
13
|
+
import MediaPickerDialog from "@/app/cms/media/components/MediaPickerDialog";
|
|
14
|
+
|
|
15
|
+
type Media = Database["public"]["Tables"]["media"]["Row"];
|
|
16
|
+
|
|
17
|
+
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
|
|
18
|
+
|
|
19
|
+
interface BackgroundSelectorProps {
|
|
20
|
+
background: SectionBlockContent["background"];
|
|
21
|
+
onChange: (newBackground: SectionBlockContent["background"]) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function BackgroundSelector({ background, onChange }: BackgroundSelectorProps) {
|
|
25
|
+
|
|
26
|
+
const backgroundType = background?.type || "none";
|
|
27
|
+
const selectedImage = background?.type === "image" ? background.image : undefined;
|
|
28
|
+
const [minHeight, setMinHeight] = useState(background?.min_height || "");
|
|
29
|
+
const [imagePosition, setImagePosition] = useState<string>(selectedImage?.position || "center");
|
|
30
|
+
const [overlayDirection, setOverlayDirection] = useState(selectedImage?.overlay?.gradient?.direction || "to bottom");
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
setMinHeight(background?.min_height || "");
|
|
34
|
+
}, [background?.min_height]);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
setImagePosition(selectedImage?.position || "center");
|
|
38
|
+
setOverlayDirection(selectedImage?.overlay?.gradient?.direction || "to bottom");
|
|
39
|
+
}, [selectedImage?.position, selectedImage?.overlay?.gradient?.direction]);
|
|
40
|
+
|
|
41
|
+
const generateGradientCss = (gradient: { direction?: string; stops?: Array<{ color: string; position: number }> }) => {
|
|
42
|
+
if (!gradient || !gradient.stops || gradient.stops.length === 0) return "none";
|
|
43
|
+
const direction = gradient.direction || "to bottom";
|
|
44
|
+
const stops = gradient.stops.map((s) => `${s.color} ${s.position}%`).join(", ");
|
|
45
|
+
return `linear-gradient(${direction}, ${stops})`;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleTypeChange = (type: SectionBlockContent["background"]["type"]) => {
|
|
49
|
+
if (type === "image") {
|
|
50
|
+
onChange({
|
|
51
|
+
type: "image",
|
|
52
|
+
image: {
|
|
53
|
+
media_id: "",
|
|
54
|
+
object_key: "",
|
|
55
|
+
size: "cover",
|
|
56
|
+
position: "center",
|
|
57
|
+
overlay: undefined,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
} else if (type === "gradient") {
|
|
61
|
+
onChange({
|
|
62
|
+
type: "gradient",
|
|
63
|
+
gradient: {
|
|
64
|
+
type: "linear",
|
|
65
|
+
direction: "to right",
|
|
66
|
+
stops: [
|
|
67
|
+
{ color: "#3b82f6", position: 0 },
|
|
68
|
+
{ color: "#8b5cf6", position: 100 },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
onChange({ type });
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handleSelectMediaFromLibrary = (mediaItem: Media) => {
|
|
78
|
+
onChange({
|
|
79
|
+
type: "image",
|
|
80
|
+
image: {
|
|
81
|
+
...selectedImage,
|
|
82
|
+
media_id: mediaItem.id,
|
|
83
|
+
object_key: mediaItem.object_key,
|
|
84
|
+
width: mediaItem.width ?? undefined,
|
|
85
|
+
height: mediaItem.height ?? undefined,
|
|
86
|
+
size: selectedImage?.size || "cover",
|
|
87
|
+
position: selectedImage?.position || "center",
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleRemoveImage = () => {
|
|
93
|
+
onChange({
|
|
94
|
+
type: "image",
|
|
95
|
+
image: {
|
|
96
|
+
media_id: "",
|
|
97
|
+
object_key: "",
|
|
98
|
+
size: "cover",
|
|
99
|
+
position: "center",
|
|
100
|
+
overlay: undefined,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const handleImagePropertyChange = (prop: "size" | "position", value: string) => {
|
|
106
|
+
if (background?.type === "image" && background.image) {
|
|
107
|
+
onChange({ ...background, image: { ...background.image, [prop]: value } });
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleOverlayToggle = (checked: boolean) => {
|
|
112
|
+
if (background?.type === "image" && background.image) {
|
|
113
|
+
const newOverlay = checked
|
|
114
|
+
? {
|
|
115
|
+
type: "gradient" as const,
|
|
116
|
+
gradient: {
|
|
117
|
+
type: "linear" as const,
|
|
118
|
+
direction: "to bottom",
|
|
119
|
+
stops: [
|
|
120
|
+
{ color: "rgba(0,0,0,0.5)", position: 0 },
|
|
121
|
+
{ color: "rgba(0,0,0,0)", position: 100 },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
: undefined;
|
|
126
|
+
onChange({ ...background, image: { ...background.image, overlay: newOverlay } });
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const handleBackgroundPropertyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
131
|
+
const { name, value } = e.target;
|
|
132
|
+
onChange({ ...background, [name]: value });
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const handleOverlayGradientChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
136
|
+
const { name, value } = e.target;
|
|
137
|
+
if (background?.type === "image" && background.image) {
|
|
138
|
+
const { image } = background;
|
|
139
|
+
const overlay = image.overlay;
|
|
140
|
+
const currentGradient = overlay?.gradient || {
|
|
141
|
+
type: "linear" as const,
|
|
142
|
+
direction: "to bottom",
|
|
143
|
+
stops: [
|
|
144
|
+
{ color: "rgba(0,0,0,0.5)", position: 0 },
|
|
145
|
+
{ color: "rgba(0,0,0,0)", position: 100 },
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const updatedStops = currentGradient.stops.map((stop) => {
|
|
150
|
+
if (name === "startColor" && stop.position === 0) return { ...stop, color: value };
|
|
151
|
+
if (name === "endColor" && stop.position === 100) return { ...stop, color: value };
|
|
152
|
+
return stop;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const updatedGradient =
|
|
156
|
+
name === "direction"
|
|
157
|
+
? { ...currentGradient, direction: value }
|
|
158
|
+
: { ...currentGradient, stops: updatedStops };
|
|
159
|
+
|
|
160
|
+
onChange({ ...background, image: { ...image, overlay: { type: "gradient", gradient: updatedGradient } } });
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const hasMinHeightChanged = (background?.min_height || "") !== minHeight;
|
|
165
|
+
const imageSizeClass = selectedImage?.size === "contain" ? "object-contain" : "object-cover";
|
|
166
|
+
const hasOverlayDirectionChanged = (selectedImage?.overlay?.gradient?.direction || "to bottom") !== overlayDirection;
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<TooltipProvider>
|
|
170
|
+
<div className="space-y-4">
|
|
171
|
+
<div className="grid gap-2">
|
|
172
|
+
<Label>Background Type</Label>
|
|
173
|
+
<Select value={backgroundType} onValueChange={(v) => handleTypeChange(v as any)}>
|
|
174
|
+
<SelectTrigger className="w-full max-w-[250px]"><SelectValue placeholder="Select type" /></SelectTrigger>
|
|
175
|
+
<SelectContent>
|
|
176
|
+
<SelectItem value="none">None</SelectItem>
|
|
177
|
+
<SelectItem value="gradient">Gradient</SelectItem>
|
|
178
|
+
<SelectItem value="image">Image</SelectItem>
|
|
179
|
+
</SelectContent>
|
|
180
|
+
</Select>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div className="grid gap-2">
|
|
184
|
+
<Label htmlFor="min_height">Minimum Height (e.g., 250px)</Label>
|
|
185
|
+
<div className="flex items-center gap-2">
|
|
186
|
+
<Input id="min_height" name="min_height" value={minHeight} onChange={(e) => setMinHeight(e.target.value)} placeholder="e.g., 250px" className="max-w-[200px]" />
|
|
187
|
+
<Button type="button" variant="ghost" size="icon" onClick={() => handleBackgroundPropertyChange({ target: { name: "min_height", value: minHeight } } as any)} disabled={!hasMinHeightChanged} title="Save Minimum Height">
|
|
188
|
+
<Save className={cn("h-5 w-5", hasMinHeightChanged && "text-green-600")} />
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{backgroundType === "image" && (
|
|
194
|
+
<>
|
|
195
|
+
<div className="mt-3 p-3 border rounded-md bg-muted/30 min-h-[120px] flex flex-col items-center justify-center">
|
|
196
|
+
{selectedImage?.object_key ? (
|
|
197
|
+
<div className="relative group w-full" style={{ height: background?.min_height || "250px", overflow: "hidden" }}>
|
|
198
|
+
<Image src={`${R2_BASE_URL}/${selectedImage.object_key}`} alt="Selected background image" width={selectedImage.width || 500} height={selectedImage.height || 300} sizes="100vw" className={`w-full h-full ${imageSizeClass}`} style={{ objectPosition: selectedImage.position }} />
|
|
199
|
+
{selectedImage.overlay && (
|
|
200
|
+
<div className="absolute inset-0" style={{ background: generateGradientCss(selectedImage.overlay.gradient) }} />
|
|
201
|
+
)}
|
|
202
|
+
<Button type="button" variant="destructive" size="icon" className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity h-6 w-6" onClick={handleRemoveImage} title="Remove Image">
|
|
203
|
+
<XIcon className="h-3 w-3" />
|
|
204
|
+
</Button>
|
|
205
|
+
</div>
|
|
206
|
+
) : (
|
|
207
|
+
<ImageIcon className="h-16 w-16 text-muted-foreground" />
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
<div className="mt-3">
|
|
211
|
+
<MediaPickerDialog
|
|
212
|
+
triggerLabel={selectedImage?.object_key ? "Change Image" : "Select from Library"}
|
|
213
|
+
onSelect={handleSelectMediaFromLibrary}
|
|
214
|
+
accept={(m) => !!m.file_type?.startsWith("image/")}
|
|
215
|
+
title="Select or Upload Background Image"
|
|
216
|
+
/>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<div className="grid gap-2">
|
|
221
|
+
<Label>Image Size</Label>
|
|
222
|
+
<Select value={selectedImage?.size || "cover"} onValueChange={(v) => handleImagePropertyChange("size", v)}>
|
|
223
|
+
<SelectTrigger className="w-full max-w-[250px]"><SelectValue /></SelectTrigger>
|
|
224
|
+
<SelectContent>
|
|
225
|
+
<SelectItem value="cover">Cover</SelectItem>
|
|
226
|
+
<SelectItem value="contain">Contain</SelectItem>
|
|
227
|
+
</SelectContent>
|
|
228
|
+
</Select>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<div className="grid gap-2">
|
|
232
|
+
<Label>Image Position</Label>
|
|
233
|
+
<Select value={imagePosition} onValueChange={(v) => { setImagePosition(v); handleImagePropertyChange("position", v); }}>
|
|
234
|
+
<SelectTrigger className="w-full max-w-[250px]"><SelectValue /></SelectTrigger>
|
|
235
|
+
<SelectContent>
|
|
236
|
+
<SelectItem value="center">Center</SelectItem>
|
|
237
|
+
<SelectItem value="top">Top</SelectItem>
|
|
238
|
+
<SelectItem value="bottom">Bottom</SelectItem>
|
|
239
|
+
<SelectItem value="left">Left</SelectItem>
|
|
240
|
+
<SelectItem value="right">Right</SelectItem>
|
|
241
|
+
<SelectItem value="top left">Top Left</SelectItem>
|
|
242
|
+
<SelectItem value="top right">Top Right</SelectItem>
|
|
243
|
+
<SelectItem value="bottom left">Bottom Left</SelectItem>
|
|
244
|
+
<SelectItem value="bottom right">Bottom Right</SelectItem>
|
|
245
|
+
</SelectContent>
|
|
246
|
+
</Select>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<div className="flex items-center space-x-2 mt-2">
|
|
250
|
+
<Checkbox id="gradientOverlay" checked={!!selectedImage?.overlay} onCheckedChange={(c) => handleOverlayToggle(!!c)} />
|
|
251
|
+
<div className="grid gap-1.5 leading-none">
|
|
252
|
+
<label htmlFor="gradientOverlay" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">Add Gradient Overlay</label>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
{selectedImage?.overlay && (
|
|
257
|
+
<div className="mt-3 p-3 border rounded-md bg-muted/30 space-y-4">
|
|
258
|
+
<div className="flex items-center gap-2">
|
|
259
|
+
<div className="flex-grow">
|
|
260
|
+
<CustomSelectWithInput
|
|
261
|
+
label="Direction"
|
|
262
|
+
tooltipContent="Select a preset or enter a custom angle like '45deg' or 'to top left'. See MDN's linear-gradient docs for more options."
|
|
263
|
+
value={overlayDirection}
|
|
264
|
+
onChange={setOverlayDirection}
|
|
265
|
+
options={[
|
|
266
|
+
{ value: "to bottom", label: "To Bottom" },
|
|
267
|
+
{ value: "to top", label: "To Top" },
|
|
268
|
+
{ value: "to left", label: "To Left" },
|
|
269
|
+
{ value: "to right", label: "To Right" },
|
|
270
|
+
{ value: "to bottom right", label: "To Bottom Right" },
|
|
271
|
+
{ value: "to top left", label: "To Top Left" },
|
|
272
|
+
]}
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
<Button size="icon" variant="ghost" onClick={() => handleOverlayGradientChange({ target: { name: "direction", value: overlayDirection } } as any)} disabled={!hasOverlayDirectionChanged} title="Save Overlay Direction">
|
|
276
|
+
<Save className={cn("h-5 w-5 mt-[1.3rem]", hasOverlayDirectionChanged && "text-green-600")} />
|
|
277
|
+
</Button>
|
|
278
|
+
</div>
|
|
279
|
+
<div className="flex items-center gap-4">
|
|
280
|
+
<ColorPicker
|
|
281
|
+
label="Start Color"
|
|
282
|
+
color={selectedImage.overlay.gradient?.stops?.[0]?.color || "rgba(0,0,0,0.5)"}
|
|
283
|
+
onChange={(color) => handleOverlayGradientChange({ target: { name: "startColor", value: color } } as any)}
|
|
284
|
+
/>
|
|
285
|
+
<ColorPicker
|
|
286
|
+
label="End Color"
|
|
287
|
+
color={selectedImage.overlay.gradient?.stops?.[1]?.color || "rgba(0,0,0,0)"}
|
|
288
|
+
onChange={(color) => handleOverlayGradientChange({ target: { name: "endColor", value: color } } as any)}
|
|
289
|
+
/>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
)}
|
|
293
|
+
</>
|
|
294
|
+
)}
|
|
295
|
+
|
|
296
|
+
{backgroundType === "gradient" && (
|
|
297
|
+
<div className="mt-3 p-3 border rounded-md bg-muted/30 space-y-4">
|
|
298
|
+
<div>
|
|
299
|
+
<CustomSelectWithInput
|
|
300
|
+
label="Direction"
|
|
301
|
+
tooltipContent="Select a preset or enter a custom angle like '45deg' or 'to top left'. See MDN's linear-gradient docs for more options."
|
|
302
|
+
value={background.gradient?.direction || "to right"}
|
|
303
|
+
onChange={(value: string) => handleBackgroundGradientChange({ target: { name: "direction", value } } as any)}
|
|
304
|
+
options={[
|
|
305
|
+
{ value: "to right", label: "To Right" },
|
|
306
|
+
{ value: "to left", label: "To Left" },
|
|
307
|
+
{ value: "to top", label: "To Top" },
|
|
308
|
+
{ value: "to bottom", label: "To Bottom" },
|
|
309
|
+
{ value: "to bottom right", label: "To Bottom Right" },
|
|
310
|
+
{ value: "to top left", label: "To Top Left" },
|
|
311
|
+
]}
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
314
|
+
<div className="flex items-center gap-4">
|
|
315
|
+
<ColorPicker
|
|
316
|
+
label="Start Color"
|
|
317
|
+
color={background.gradient?.stops?.[0]?.color || "#3b82f6"}
|
|
318
|
+
onChange={(color) => handleBackgroundGradientChange({ target: { name: "startColor", value: color } } as any)}
|
|
319
|
+
/>
|
|
320
|
+
<ColorPicker
|
|
321
|
+
label="End Color"
|
|
322
|
+
color={background.gradient?.stops?.[1]?.color || "#8b5cf6"}
|
|
323
|
+
onChange={(color) => handleBackgroundGradientChange({ target: { name: "endColor", value: color } } as any)}
|
|
324
|
+
/>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
</TooltipProvider>
|
|
330
|
+
);
|
|
331
|
+
const handleBackgroundGradientChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
332
|
+
const { name, value } = e.target as any;
|
|
333
|
+
if (backgroundType !== 'gradient') return;
|
|
334
|
+
const current = background.gradient || { type: 'linear' as const, direction: 'to right', stops: [ { color: '#3b82f6', position: 0 }, { color: '#8b5cf6', position: 100 } ] };
|
|
335
|
+
if (name === 'direction') {
|
|
336
|
+
onChange({ type: 'gradient', gradient: { ...current, direction: value } });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (name === 'startColor' || name === 'endColor') {
|
|
340
|
+
const updatedStops = (current.stops || [ { color: '#3b82f6', position: 0 }, { color: '#8b5cf6', position: 100 } ]).map((s) => {
|
|
341
|
+
if (name === 'startColor' && s.position === 0) return { ...s, color: value };
|
|
342
|
+
if (name === 'endColor' && s.position === 100) return { ...s, color: value };
|
|
343
|
+
return s;
|
|
344
|
+
});
|
|
345
|
+
onChange({ type: 'gradient', gradient: { ...current, stops: updatedStops } });
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|