feat: add visual style picker and contextual previews

This commit is contained in:
2026-05-19 17:51:46 +08:00
parent ab4625abb7
commit 265d7c9f5e
15 changed files with 356 additions and 121 deletions

View File

@@ -0,0 +1,75 @@
'use client';
import { useState } from 'react';
import type { PointerEvent } from 'react';
type PreviewState = {
left: number;
top: number;
width: number;
};
function parseRatio(aspectRatio?: string) {
if (!aspectRatio || aspectRatio === 'long') return aspectRatio === 'long' ? 1 / 3 : 1;
const [w, h] = aspectRatio.split(':').map(Number);
return w && h ? w / h : 1;
}
function nextPreviewState(event: PointerEvent<HTMLElement>, aspectRatio?: string): PreviewState {
const gap = 18;
const margin = 12;
const ratio = parseRatio(aspectRatio);
const width = Math.min(620, Math.max(280, window.innerWidth * 0.42));
const height = Math.min(window.innerHeight * 0.82, width / ratio);
let left = event.clientX + gap;
let top = event.clientY + gap;
if (left + width > window.innerWidth - margin) {
left = event.clientX - width - gap;
}
if (top + height > window.innerHeight - margin) {
top = window.innerHeight - height - margin;
}
return {
left: Math.max(margin, left),
top: Math.max(margin, top),
width,
};
}
export function HoverImagePreview({
src,
alt,
imageClassName,
aspectRatio,
}: {
src: string;
alt: string;
imageClassName?: string;
aspectRatio?: string;
}) {
const [preview, setPreview] = useState<PreviewState | null>(null);
return (
<>
<img
src={src}
alt={alt}
className={imageClassName}
onPointerMove={event => {
if (event.pointerType === 'touch') return;
setPreview(nextPreviewState(event, aspectRatio));
}}
onPointerLeave={() => setPreview(null)}
/>
{preview && (
<div
className="pointer-events-none fixed z-[90] rounded-[8px] bg-white p-2 shadow-2xl ring-1 ring-white/20"
style={{ left: preview.left, top: preview.top, width: preview.width }}
>
<img src={src} alt="" className="max-h-[82vh] w-full object-contain" style={{ aspectRatio: parseRatio(aspectRatio) }} />
</div>
)}
</>
);
}