fix: refine canvas angle preview panel
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-05-08 13:34:53 +08:00
parent b78a2701bb
commit 1f8e19598c
5 changed files with 411 additions and 44 deletions

View File

@ -665,6 +665,7 @@ function resolveCanvasToolbarItemWithFallback(
? item.label
: fallback?.label || item.label,
iconUrl: item.iconUrl || fallback?.iconUrl || "",
enabled: item.enabled,
};
}

View File

@ -946,13 +946,14 @@ export function RealtimeCanvasDialog({
const resolveRealtimeModeItem = useCallback(
(
key: "realtimeModeRealtime" | "realtimeModeEnhance",
fallback: { label: string; iconUrl: string }
fallback: { label: string; iconUrl: string; enabled?: boolean }
) => {
const item = getCanvasToolbarItem(canvasToolbarConfig, key);
const defaultItem = getDefaultCanvasToolbarItem(key);
return {
label: item.label === defaultItem.label ? fallback.label : item.label,
iconUrl: item.iconUrl || fallback.iconUrl,
enabled: item.enabled ?? fallback.enabled ?? true,
};
},
[canvasToolbarConfig]

View File

@ -218,6 +218,14 @@ type CropInteractionState = {
startRect: NormalizedCropRect;
};
type AnglePreviewDragState = {
pointerId: number;
startX: number;
startY: number;
startHorizontalAngle: number;
startVerticalAngle: number;
};
interface BoxCutoutPoint {
x: number;
y: number;
@ -227,6 +235,8 @@ const BOX_CUTOUT_DEFAULT_PADDING_RATIO = 0.08;
const BOX_CUTOUT_MIN_EDGE_PX = 24;
const FLOATING_TOOLBAR_SCALE = 0.8;
const IMAGE_EDIT_PANEL_SCALE = 0.5;
const ANGLE_PREVIEW_HORIZONTAL_DRAG_MULTIPLIER = 0.72;
const ANGLE_PREVIEW_VERTICAL_DRAG_MULTIPLIER = 0.46;
const DEFAULT_CROP_PRESET_ID = "1:1";
const IMAGE_ADJUST_NOISE_DATA_URI = `data:image/svg+xml;utf8,${encodeURIComponent(
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.18' numOctaves='2' stitchTiles='stitch'/></filter><rect width='160' height='160' filter='url(#n)' opacity='1'/></svg>"
@ -485,6 +495,51 @@ function AngleEditPanelSlider({
const clampNumber = (value: number, min: number, max: number) =>
Math.min(max, Math.max(min, value));
const fitSizeWithinBox = ({
maxHeight,
maxWidth,
minHeight = 72,
minWidth = 72,
sourceHeight,
sourceWidth,
}: {
maxHeight: number;
maxWidth: number;
minHeight?: number;
minWidth?: number;
sourceHeight: number;
sourceWidth: number;
}) => {
const safeSourceWidth = sourceWidth > 0 ? sourceWidth : maxWidth;
const safeSourceHeight = sourceHeight > 0 ? sourceHeight : maxHeight;
let scale = Math.min(maxWidth / safeSourceWidth, maxHeight / safeSourceHeight);
let fittedWidth = safeSourceWidth * scale;
let fittedHeight = safeSourceHeight * scale;
if (fittedWidth < minWidth) {
scale = minWidth / safeSourceWidth;
fittedWidth = safeSourceWidth * scale;
fittedHeight = safeSourceHeight * scale;
}
if (fittedHeight < minHeight) {
scale = minHeight / safeSourceHeight;
fittedWidth = safeSourceWidth * scale;
fittedHeight = safeSourceHeight * scale;
}
if (fittedWidth > maxWidth || fittedHeight > maxHeight) {
scale = Math.min(maxWidth / fittedWidth, maxHeight / fittedHeight);
fittedWidth *= scale;
fittedHeight *= scale;
}
return {
width: Math.round(fittedWidth),
height: Math.round(fittedHeight),
};
};
const createNormalizedRect = (
start: BoxCutoutPoint,
end: BoxCutoutPoint
@ -1498,6 +1553,7 @@ export function ImageNode({
const [angleVerticalAngle, setAngleVerticalAngle] = useState(-30);
const [angleZoom, setAngleZoom] = useState(50);
const [isApplyingAngleEdit, setIsApplyingAngleEdit] = useState(false);
const [isAnglePreviewDragging, setIsAnglePreviewDragging] = useState(false);
const [sceneFusionDialogOpen, setSceneFusionDialogOpen] = useState(false);
const [resizeAnnotatorOpen, setResizeAnnotatorOpen] = useState(false);
const [promptDialogOpen, setPromptDialogOpen] = useState(false);
@ -1546,6 +1602,7 @@ export function ImageNode({
useState<BoxCutoutPoint | null>(null);
const [boxCutoutDragEnd, setBoxCutoutDragEnd] =
useState<BoxCutoutPoint | null>(null);
const anglePreviewDragRef = useRef<AnglePreviewDragState | null>(null);
const activeImageAdjustments = imageAdjustPanelOpen
? imageAdjustmentsDraft
: storedImageAdjustments;
@ -1569,8 +1626,8 @@ export function ImageNode({
useEffect(() => {
if (!cropPanelOpen) return;
if (cropRect) return;
const centerX = cropRect ? cropRect.x + cropRect.width / 2 : 0.5;
const centerY = cropRect ? cropRect.y + cropRect.height / 2 : 0.5;
const centerX = 0.5;
const centerY = 0.5;
const preset = getCropPresetById(cropPresetId);
if (preset) {
setCropRect(createCenteredCropRectForAspect(preset.ratio, centerX, centerY));
@ -1920,6 +1977,64 @@ export function ImageNode({
if (!source) return "";
return `${source.width} x ${source.height}`;
}, [loadedResolution, metadataResolution]);
const angleSourceResolution = useMemo(() => {
const source = metadataResolution || loadedResolution;
if (source?.width && source?.height) {
return source;
}
return {
width: Math.max(1, Math.round(node.size.width || 1)),
height: Math.max(1, Math.round(node.size.height || 1)),
};
}, [loadedResolution, metadataResolution, node.size.height, node.size.width]);
const anglePreviewFrameSize = useMemo(
() =>
fitSizeWithinBox({
sourceWidth: angleSourceResolution.width,
sourceHeight: angleSourceResolution.height,
maxWidth: anglePanelMode === "subject" ? 178 : 136,
maxHeight: anglePanelMode === "subject" ? 150 : 112,
minWidth: 74,
minHeight: 62,
}),
[anglePanelMode, angleSourceResolution.height, angleSourceResolution.width]
);
const anglePreviewDepth = useMemo(
() =>
Math.round(
clampNumber(
anglePanelMode === "subject"
? anglePreviewFrameSize.width * 0.18
: anglePreviewFrameSize.width * 0.22,
16,
36
)
),
[anglePanelMode, anglePreviewFrameSize.width]
);
const anglePreviewScale = useMemo(
() =>
Number(
(
(anglePanelMode === "subject" ? 0.9 : 0.92) +
(angleZoom / 100) * (anglePanelMode === "subject" ? 0.46 : 0.34)
).toFixed(3)
),
[anglePanelMode, angleZoom]
);
const anglePreviewImageSrc = displayImageSrc || data.src;
const angleCameraOrbitPosition = useMemo(() => {
const horizontalRadians = (angleHorizontalAngle * Math.PI) / 180;
const normalizedVertical = (angleVerticalAngle + 30) / 90;
const x = Math.sin(horizontalRadians) * 92;
const y = -normalizedVertical * 60 + 24;
const z = Math.cos(horizontalRadians) * 28;
return {
x: Number(x.toFixed(2)),
y: Number(y.toFixed(2)),
z: Number(z.toFixed(2)),
};
}, [angleHorizontalAngle, angleVerticalAngle]);
const cropImageMetrics = useMemo(() => {
if (boxCutoutMetrics) {
return {
@ -2165,6 +2280,12 @@ export function ImageNode({
}
}, [isSelected, resetBoxCutoutState]);
useEffect(() => {
if (anglePanelOpen) return;
anglePreviewDragRef.current = null;
setIsAnglePreviewDragging(false);
}, [anglePanelOpen]);
useEffect(() => {
if (!hasCompareSource || isThisNodePicking) {
setIsCompareMode(false);
@ -3390,6 +3511,64 @@ export function ImageNode({
setAngleZoom(50);
}, []);
const handleAnglePreviewPointerDown = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
if (isApplyingAngleEdit) return;
anglePreviewDragRef.current = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
startHorizontalAngle: angleHorizontalAngle,
startVerticalAngle: angleVerticalAngle,
};
setIsAnglePreviewDragging(true);
event.currentTarget.setPointerCapture?.(event.pointerId);
event.preventDefault();
event.stopPropagation();
},
[angleHorizontalAngle, angleVerticalAngle, isApplyingAngleEdit]
);
const handleAnglePreviewPointerMove = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
const dragState = anglePreviewDragRef.current;
if (!dragState || dragState.pointerId !== event.pointerId) return;
const deltaX = event.clientX - dragState.startX;
const deltaY = event.clientY - dragState.startY;
setAngleHorizontalAngle(
clampNumber(
dragState.startHorizontalAngle +
deltaX * ANGLE_PREVIEW_HORIZONTAL_DRAG_MULTIPLIER,
-180,
180
)
);
setAngleVerticalAngle(
clampNumber(
dragState.startVerticalAngle -
deltaY * ANGLE_PREVIEW_VERTICAL_DRAG_MULTIPLIER,
-30,
60
)
);
event.preventDefault();
event.stopPropagation();
},
[]
);
const handleAnglePreviewPointerEnd = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
if (anglePreviewDragRef.current?.pointerId !== event.pointerId) return;
anglePreviewDragRef.current = null;
setIsAnglePreviewDragging(false);
event.currentTarget.releasePointerCapture?.(event.pointerId);
event.preventDefault();
event.stopPropagation();
},
[]
);
const handleApplyFlipRotate = useCallback(
async (action: FlipRotateEditAction) => {
if (!data.src) {
@ -7917,86 +8096,272 @@ export function ImageNode({
</div>
<div className="relative mt-6 overflow-hidden rounded-[26px] bg-[#faf8f4] px-4 py-5">
<div className="relative flex h-[410px] items-center justify-center">
<div
className={cn(
"relative flex h-[410px] items-center justify-center overflow-hidden rounded-[22px] [perspective:1200px]",
isApplyingAngleEdit
? "cursor-default"
: isAnglePreviewDragging
? "cursor-grabbing"
: "cursor-grab"
)}
onLostPointerCapture={handleAnglePreviewPointerEnd}
onPointerCancel={handleAnglePreviewPointerEnd}
onPointerDown={handleAnglePreviewPointerDown}
onPointerMove={handleAnglePreviewPointerMove}
onPointerUp={handleAnglePreviewPointerEnd}
style={{ touchAction: "none" }}
>
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="absolute inset-[54px] rounded-full border border-[#ece8e2]" />
<div
className="absolute inset-[54px] rounded-full border border-[#efebe5]"
style={{ transform: "rotate(18deg) scaleX(1.06)" }}
/>
<div
className="absolute inset-[54px] rounded-full border border-[#efebe5]"
style={{ transform: "rotate(42deg) scaleX(0.94)" }}
/>
<div
className="absolute inset-[54px] rounded-full border border-[#efebe5]"
style={{ transform: "rotate(68deg) scaleY(0.84)" }}
/>
<div
className="absolute inset-[54px] rounded-full border border-[#efebe5]"
style={{ transform: "rotate(96deg) scaleY(0.72)" }}
/>
<div
className="absolute inset-[54px] rounded-full border border-[#f2eee8]"
style={{ transform: "rotate(132deg) scaleX(1.08) scaleY(0.76)" }}
/>
<div className="absolute inset-[90px] rounded-full border border-dashed border-[#ebe6df]" />
<svg
className="h-[300px] w-[300px]"
fill="none"
viewBox="0 0 320 320"
>
<ellipse
cx="160"
cy="160"
rx="116"
ry="116"
stroke="#ebe5dc"
strokeWidth="1.4"
/>
<ellipse
cx="160"
cy="160"
rx="116"
ry="116"
stroke="#efe8de"
strokeWidth="1.2"
transform="rotate(28 160 160)"
/>
<ellipse
cx="160"
cy="160"
rx="116"
ry="86"
stroke="#efe8de"
strokeWidth="1.2"
transform="rotate(56 160 160)"
/>
<ellipse
cx="160"
cy="160"
rx="116"
ry="76"
stroke="#efe8de"
strokeWidth="1.2"
transform="rotate(92 160 160)"
/>
<ellipse
cx="160"
cy="160"
rx="86"
ry="116"
stroke="#f2ece3"
strokeDasharray="4 6"
strokeWidth="1.1"
/>
</svg>
</div>
<button
className="absolute top-1 left-1/2 inline-flex h-8 w-8 -translate-x-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
className="absolute top-1 left-1/2 z-[3] inline-flex h-8 w-8 -translate-x-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
disabled={isApplyingAngleEdit}
onClick={() =>
setAngleVerticalAngle((prev) => clampNumber(prev + 5, -30, 60))
}
onPointerDown={(event) => event.stopPropagation()}
type="button"
>
<ChevronUp className="h-4 w-4" />
</button>
<button
className="absolute bottom-1 left-1/2 inline-flex h-8 w-8 -translate-x-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
className="absolute bottom-1 left-1/2 z-[3] inline-flex h-8 w-8 -translate-x-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
disabled={isApplyingAngleEdit}
onClick={() =>
setAngleVerticalAngle((prev) => clampNumber(prev - 5, -30, 60))
}
onPointerDown={(event) => event.stopPropagation()}
type="button"
>
<ChevronDown className="h-4 w-4" />
</button>
<button
className="absolute top-1/2 left-2 inline-flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
className="absolute top-1/2 left-2 z-[3] inline-flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
disabled={isApplyingAngleEdit}
onClick={() =>
setAngleHorizontalAngle((prev) =>
clampNumber(prev - 10, -180, 180)
)
}
onPointerDown={(event) => event.stopPropagation()}
type="button"
>
<ChevronLeft className="h-4 w-4" />
</button>
<button
className="absolute top-1/2 right-2 inline-flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
className="absolute top-1/2 right-2 z-[3] inline-flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full text-[#a6a6a6] transition-colors hover:bg-white hover:text-[#2f3133]"
disabled={isApplyingAngleEdit}
onClick={() =>
setAngleHorizontalAngle((prev) =>
clampNumber(prev + 10, -180, 180)
)
}
onPointerDown={(event) => event.stopPropagation()}
type="button"
>
<ChevronRight className="h-4 w-4" />
</button>
<div className="relative z-[1] flex h-[92px] w-[92px] items-center justify-center rounded-[24px] border border-white/80 bg-white p-2 shadow-[0_16px_34px_rgba(15,23,42,0.12)]">
<img
alt={displayTitle}
className="h-full w-full rounded-[18px] object-cover"
src={data.src}
/>
</div>
{anglePanelMode === "subject" ? (
<div
className="relative z-[2]"
style={{
width: `${anglePreviewFrameSize.width}px`,
height: `${anglePreviewFrameSize.height}px`,
transform: `rotateX(${-angleVerticalAngle * 0.46}deg) rotateY(${angleHorizontalAngle * 0.62}deg) scale(${anglePreviewScale})`,
transformStyle: "preserve-3d",
transition: isAnglePreviewDragging
? "none"
: "transform 180ms cubic-bezier(0.22, 1, 0.36, 1)",
}}
>
<div
className="absolute inset-[10%] rounded-full bg-black/12 blur-[20px]"
style={{
transform: `translate3d(0, ${anglePreviewFrameSize.height * 0.34}px, -18px) scale(${0.9 + angleZoom / 260})`,
}}
/>
<div
className="absolute inset-0 rounded-[26px] border border-white/90 bg-white shadow-[0_16px_34px_rgba(15,23,42,0.14)]"
style={{
transform: `translateZ(${anglePreviewDepth / 2}px)`,
}}
>
<div className="absolute inset-[6px] overflow-hidden rounded-[20px] bg-[#fffdfa]">
<img
alt={displayTitle}
className="h-full w-full object-contain"
draggable={false}
src={anglePreviewImageSrc}
/>
</div>
</div>
<div
className="absolute inset-0 rounded-[26px] border border-[#ebe2d8] bg-[linear-gradient(180deg,#f6efe5,#eadfce)]"
style={{
transform: `translateZ(-${anglePreviewDepth / 2}px)`,
}}
/>
<div
className="absolute left-0 top-[10px] bottom-[10px] rounded-l-[18px] border border-[#e6dccc] bg-[linear-gradient(180deg,#f3ebdf,#dccfbc)]"
style={{
width: `${anglePreviewDepth}px`,
transform: `translateX(-${anglePreviewDepth / 2}px) rotateY(-90deg)`,
transformOrigin: "center",
}}
/>
<div
className="absolute right-0 top-[10px] bottom-[10px] rounded-r-[18px] border border-[#e6dccc] bg-[linear-gradient(180deg,#f2e9dd,#dbceb9)]"
style={{
width: `${anglePreviewDepth}px`,
transform: `translateX(${anglePreviewDepth / 2}px) rotateY(90deg)`,
transformOrigin: "center",
}}
/>
<div
className="absolute left-[10px] right-[10px] top-0 rounded-t-[16px] border border-white/90 bg-[linear-gradient(180deg,#fffefe,#ece1d0)]"
style={{
height: `${anglePreviewDepth}px`,
transform: `translateY(-${anglePreviewDepth / 2}px) rotateX(90deg)`,
transformOrigin: "center",
}}
/>
<div
className="absolute left-[10px] right-[10px] bottom-0 rounded-b-[16px] border border-[#e9dfd1] bg-[linear-gradient(180deg,#e2d6c5,#d3c5b0)]"
style={{
height: `${anglePreviewDepth}px`,
transform: `translateY(${anglePreviewDepth / 2}px) rotateX(-90deg)`,
transformOrigin: "center",
}}
/>
</div>
) : (
<>
<div
className="relative z-[2] overflow-hidden rounded-[24px] border border-white/90 bg-white p-[6px] shadow-[0_16px_34px_rgba(15,23,42,0.12)]"
style={{
width: `${anglePreviewFrameSize.width}px`,
height: `${anglePreviewFrameSize.height}px`,
transform: `translateZ(${10 + angleZoom * 0.14}px) rotateX(${-angleVerticalAngle * 0.08}deg) rotateY(${angleHorizontalAngle * 0.18}deg) scale(${anglePreviewScale})`,
transformStyle: "preserve-3d",
transition: isAnglePreviewDragging
? "none"
: "transform 180ms cubic-bezier(0.22, 1, 0.36, 1)",
}}
>
<div className="h-full w-full overflow-hidden rounded-[18px] bg-[#fffdfa]">
<img
alt={displayTitle}
className="h-full w-full object-contain"
draggable={false}
src={anglePreviewImageSrc}
/>
</div>
</div>
<div className="absolute bottom-[56px] left-[34px] flex h-[48px] w-[48px] items-center justify-center rounded-[16px] border border-[#ece7df] bg-white shadow-[0_10px_24px_rgba(15,23,42,0.08)]">
<div
className="absolute z-[3]"
style={{
transform: `translate3d(${angleCameraOrbitPosition.x}px, ${angleCameraOrbitPosition.y}px, ${angleCameraOrbitPosition.z}px)`,
transformStyle: "preserve-3d",
transition: isAnglePreviewDragging
? "none"
: "transform 180ms cubic-bezier(0.22, 1, 0.36, 1)",
}}
>
<div
className="relative"
style={{
width: "54px",
height: "54px",
transform: `rotateX(${12 - angleVerticalAngle * 0.12}deg) rotateY(${angleHorizontalAngle + 26}deg)`,
transformStyle: "preserve-3d",
}}
>
<div
className="absolute inset-0 overflow-hidden rounded-[16px] border border-white/90 bg-white shadow-[0_10px_24px_rgba(15,23,42,0.12)]"
style={{ transform: "translateZ(10px)" }}
>
<img
alt=""
className="h-full w-full object-cover"
draggable={false}
src={anglePreviewImageSrc}
/>
<span className="absolute right-[6px] bottom-[6px] h-[9px] w-[9px] rounded-full border border-white/90 bg-[#2f3136]/85" />
</div>
<div
className="absolute inset-y-[4px] right-0 rounded-r-[12px] border border-[#e6dccf] bg-[linear-gradient(180deg,#f6f0e8,#e4d9ca)]"
style={{
width: "20px",
transform: "translateX(10px) rotateY(90deg)",
}}
/>
<div
className="absolute inset-x-[4px] top-0 rounded-t-[12px] border border-white/90 bg-[linear-gradient(180deg,#ffffff,#ece2d6)]"
style={{
height: "20px",
transform: "translateY(-10px) rotateX(90deg)",
}}
/>
</div>
</div>
</>
)}
<div className="pointer-events-none absolute bottom-[54px] left-[34px] z-[3] flex h-[48px] w-[48px] items-center justify-center rounded-[16px] border border-[#ece7df] bg-white shadow-[0_10px_24px_rgba(15,23,42,0.08)]">
{anglePanelMode === "subject" ? (
<Box className="h-5 w-5 text-[#949494]" />
) : (

View File

@ -122,7 +122,7 @@ export function FloatingSidebarRail({
: "border border-white/8 bg-[#17181d]/94 text-white shadow-[0_22px_60px_rgba(6,8,14,0.34),inset_0_1px_0_rgba(255,255,255,0.05)]",
isHorizontal
? "h-[56px] w-auto flex-row overflow-hidden rounded-[22px] px-3 py-2"
: "w-[60px] flex-col overflow-y-auto rounded-[28px] px-2 py-2.5",
: "w-[56px] flex-col overflow-y-auto rounded-[24px] px-2 py-2",
className
)}
>
@ -153,7 +153,7 @@ export function FloatingSidebarAction({
<TooltipTrigger asChild>
<button
className={cn(
"flex h-10 w-10 items-center justify-center rounded-full text-[#4f566b] transition-colors hover:bg-black/5 hover:text-[#111827] disabled:cursor-not-allowed disabled:opacity-45 dark:text-white/60 dark:hover:bg-white/8 dark:hover:text-white",
"flex h-10 w-10 items-center justify-center rounded-xl text-[#4f566b] transition-colors hover:bg-black/5 hover:text-[#111827] disabled:cursor-not-allowed disabled:opacity-45 dark:text-white/60 dark:hover:bg-white/8 dark:hover:text-white",
active &&
"bg-[#111827] text-white shadow-sm hover:bg-[#1f2937] hover:text-white"
)}

View File

@ -446,7 +446,7 @@ export function LovartHome({
"rounded-[24px] border border-black/8 bg-white/96 p-2.5 text-[#111827] shadow-[0_24px_64px_rgba(15,23,42,0.12)] backdrop-blur-xl";
const lovartSidebarButtonClass = (active = false) =>
cn(
"group flex h-11 w-11 items-center justify-center rounded-[16px] text-[#6b7280] transition-all duration-200 hover:bg-black/[0.04] hover:text-[#111827]",
"group flex h-10 w-10 items-center justify-center rounded-xl text-[#6b7280] transition-all duration-200 hover:bg-black/[0.04] hover:text-[#111827]",
active &&
"bg-[#111827] text-white shadow-[0_10px_22px_rgba(15,23,42,0.18)]"
);
@ -1717,13 +1717,13 @@ export function LovartHome({
<button
type="button"
onClick={openNewCanvas}
className="flex h-[60px] w-[60px] shrink-0 items-center justify-center rounded-[22px] border border-black/8 bg-white/96 shadow-[0_20px_46px_rgba(15,23,42,0.12)] backdrop-blur-xl transition-transform duration-200 hover:-translate-y-0.5 hover:bg-white"
className="flex h-[56px] w-[56px] shrink-0 items-center justify-center rounded-[20px] border border-black/8 bg-white/96 shadow-[0_20px_46px_rgba(15,23,42,0.12)] backdrop-blur-xl transition-transform duration-200 hover:-translate-y-0.5 hover:bg-white"
title={getLovartSidebarItem("newProject").label}
>
<div className="flex h-10 w-10 items-center justify-center rounded-[14px] bg-[#111827] text-white shadow-[0_10px_20px_rgba(15,23,42,0.18)]">
<div className="flex h-9 w-9 items-center justify-center rounded-[12px] bg-[#111827] text-white shadow-[0_10px_20px_rgba(15,23,42,0.18)]">
{renderLovartSidebarIcon("newProject", {
filled: true,
sizeClassName: "h-[18px] w-[18px]",
sizeClassName: "h-4 w-4",
})}
</div>
</button>