mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
fix(ui): full-bleed image preview with quiet zoom controls (#57318)
This commit is contained in:
committed by
GitHub
Unverified
parent
71a1b10ba4
commit
1b5ce90cc7
@@ -2,9 +2,8 @@
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* Only the open preview takes clicks, so nothing left on screen while it closes blocks the page. */
|
||||
pointer-events: none;
|
||||
--image-preview-shadow:
|
||||
0 15px 45px 0 rgba(19, 16, 16, 0.35), 0 3.35px 10.051px 0 rgba(19, 16, 16, 0.25),
|
||||
0 0.998px 2.993px 0 rgba(19, 16, 16, 0.2);
|
||||
@@ -17,14 +16,12 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* While the image flies, the panel's surface flies with it, from the image's place on the page to the panel; its
|
||||
shadow rises as it lifts off the page. */
|
||||
/* While the image flies, a shadow flies under it, deepest mid-flight as it lifts between the page and the preview,
|
||||
where the image lies flat on the backdrop. */
|
||||
[data-slot="image-preview-surface"] {
|
||||
display: none;
|
||||
position: fixed;
|
||||
/* Under the panel, whose controls ride it, and over the backdrop. */
|
||||
z-index: 49;
|
||||
background: var(--surface-raised-stronger-non-alpha);
|
||||
pointer-events: none;
|
||||
|
||||
&::before {
|
||||
@@ -33,11 +30,11 @@
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
box-shadow: var(--image-preview-shadow);
|
||||
opacity: var(--image-preview-lift, 1);
|
||||
opacity: calc(4 * var(--image-preview-lift, 0) * (1 - var(--image-preview-lift, 0)));
|
||||
}
|
||||
}
|
||||
|
||||
/* The origin's outline, carried over the flying image until it becomes the panel. */
|
||||
/* The origin's outline, carried over the flying image until it lifts off the page. */
|
||||
[data-slot="image-preview-edge"] {
|
||||
display: none;
|
||||
position: fixed;
|
||||
@@ -68,96 +65,87 @@
|
||||
[data-slot="image-preview-image"] {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* The surface stands in for the panel, so only its controls show, fading in as it lands. */
|
||||
[data-slot="image-preview-container"] [data-slot="image-preview-content"] {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
/* The image is the preview: no panel around it, fitted to the window with room for the controls under it. */
|
||||
[data-slot="image-preview-content"] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
pointer-events: auto;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Kobalte unmounts closed content once its CSS animation ends, so this holds it for the closing flight. Held, it
|
||||
must not take the page's clicks; Kobalte's inline style is otherwise auto. */
|
||||
&[data-closed] {
|
||||
animation: image-preview-exit var(--image-preview-exit);
|
||||
pointer-events: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="image-preview-container"] {
|
||||
position: relative;
|
||||
z-index: 50;
|
||||
width: min(calc(100vw - 32px), 90vw);
|
||||
max-width: 1200px;
|
||||
height: min(calc(100vh - 32px), 90vh);
|
||||
[data-slot="image-preview-body"] {
|
||||
position: absolute;
|
||||
inset: 44px clamp(8px, 3vw, 36px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
/* A zoomed image fades out in the padding instead of being cut at the edge; a fitted one never reaches it. */
|
||||
mask-image:
|
||||
linear-gradient(to right, transparent, #000 12px, #000 calc(100% - 12px), transparent),
|
||||
linear-gradient(to bottom, transparent, #000 12px, #000 calc(100% - 12px), transparent);
|
||||
mask-composite: intersect;
|
||||
|
||||
[data-slot="image-preview-content"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--surface-raised-stronger-non-alpha);
|
||||
box-shadow: var(--image-preview-shadow);
|
||||
overflow: hidden;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Kobalte unmounts closed content once its CSS animation ends, so this holds it for the closing flight. Held, it
|
||||
must not take the page's clicks; Kobalte's inline style is otherwise auto. */
|
||||
&[data-closed] {
|
||||
animation: image-preview-exit var(--image-preview-exit);
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
[data-slot="image-preview-header"] {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 8px 0;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
[data-slot="image-preview-zoom"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
[data-slot="image-preview-zoom-level"] {
|
||||
min-width: 52px;
|
||||
justify-content: center;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
[data-slot="image-preview-body"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
|
||||
&[data-zoomed] {
|
||||
cursor: grab;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
|
||||
&[data-dragging] {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="image-preview-image"] {
|
||||
max-width: 100%;
|
||||
max-height: calc(90vh - 100px);
|
||||
object-fit: contain;
|
||||
border-radius: var(--radius-md);
|
||||
transform-origin: center;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
&[data-zoomed] {
|
||||
cursor: grab;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
|
||||
&[data-dragging] {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="image-preview-image"] {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: var(--radius-md);
|
||||
transform-origin: center;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
[data-slot="image-preview-controls"] {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
inset-inline: 0;
|
||||
width: fit-content;
|
||||
margin-inline: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
border-radius: 8px;
|
||||
background: var(--v2-background-bg-layer-01);
|
||||
box-shadow: var(--v2-elevation-floating);
|
||||
}
|
||||
|
||||
[data-slot="image-preview-zoom-level"] {
|
||||
min-width: 52px;
|
||||
justify-content: center;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
[data-slot="image-preview-divider"] {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
margin-inline: 2px;
|
||||
background: var(--v2-border-border-base);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,14 @@ Use for full-size image inspection; keep images optimized.
|
||||
- Optional: \`alt\` text, and \`origin\`, the image on the page that the preview grows out of and shrinks back into.
|
||||
|
||||
### Variants and states
|
||||
- Single layout with zoom controls and a close action.
|
||||
- Single layout: the image fills the window without a panel, with zoom controls and a close action in one strip under it.
|
||||
|
||||
### Behavior
|
||||
- Intended to be used via \`useDialog().show\`.
|
||||
- With an \`origin\`, the image flies from its place on the page to the preview while the page dims, and back on close.
|
||||
- Pinch or Cmd/Ctrl + wheel zooms at the cursor; wheel and drag pan while zoomed.
|
||||
- Double-click toggles between fit and 100%; Cmd/Ctrl + =, - and 0 zoom in, out and to fit.
|
||||
- A click on the backdrop around the image closes it, unless it ends a pan.
|
||||
|
||||
### Accessibility
|
||||
- Uses localized aria-label for close button.
|
||||
|
||||
@@ -65,32 +65,32 @@ export function ImagePreview(props: ImagePreviewProps) {
|
||||
let image!: HTMLImageElement
|
||||
let standIn!: HTMLImageElement
|
||||
let surface!: HTMLDivElement
|
||||
let header!: HTMLDivElement
|
||||
let edge!: HTMLDivElement
|
||||
let close: HTMLButtonElement | undefined
|
||||
// Nothing shows until the image has loaded and the preview knows where it lands; while flying, a stand-in image and
|
||||
// the panel's surface travel above the dialog in place of the real ones.
|
||||
// its shadow travel above the dialog in place of the real one.
|
||||
const [stage, setStage] = createSignal<"pending" | "flying" | "rest">("pending")
|
||||
|
||||
// The flight under way: where the stand-in is laid out (the flight only transforms and clips it), the panel at rest
|
||||
// and whether its controls sit at its left, the springs, and what happens when it lands.
|
||||
let course:
|
||||
| { at: Box; panel: Box; rtl: boolean; flight: ReturnType<typeof createFlight>; landed: () => void }
|
||||
| undefined
|
||||
// The flight under way: where the stand-in is laid out (the flight only transforms and clips it), the springs, and
|
||||
// what happens when it lands.
|
||||
let course: { at: Box; flight: ReturnType<typeof createFlight>; landed: () => void } | undefined
|
||||
|
||||
const dim = Spring.fromDuration(0, FLIGHT_SPRING)
|
||||
// Once a close lands, the stand-in fades out over the image the page shows again, so whatever the page draws over
|
||||
// its own image (a fade under a sticky bar, say) comes in softly instead of in one frame.
|
||||
const veil = Spring.fromDuration(1, FLIGHT_SPRING)
|
||||
|
||||
// How far the flight has become the panel, as last painted: the origin's outline fades out as it rises.
|
||||
// How far the flight has lifted off the page, as last painted: the origin's outline fades out as it rises.
|
||||
let lift = 0
|
||||
// Without a flight (reduced motion, or nothing on the page to fly to) the panel only fades with the backdrop.
|
||||
// Without a flight (reduced motion, or nothing on the page to fly to) the preview only fades with the backdrop.
|
||||
let plain = false
|
||||
let stop: (() => void) | undefined
|
||||
const [view, setView] = createSignal(FIT_VIEW)
|
||||
const [fitRatio, setFitRatio] = createSignal(1)
|
||||
const [drag, setDrag] = createSignal<{ id: number; x: number; y: number }>()
|
||||
// Whether the press that ends in a click began on the backdrop around the image and stayed put. A pan captures the
|
||||
// pointer, so its click lands on the backdrop wherever it began.
|
||||
let backdrop = false
|
||||
// Where an eased zoom is heading, so repeated steps build on the destination rather than the frame in flight.
|
||||
let goal = FIT_VIEW
|
||||
let animation: AnimationPlaybackControls | undefined
|
||||
@@ -164,7 +164,7 @@ export function ImagePreview(props: ImagePreviewProps) {
|
||||
const flight = createFlight(from)
|
||||
const rest = restFrame()
|
||||
flight.to(rest, KICK.open, tuning())
|
||||
course = { at: place(fitted()), ...panel(rest), flight, landed: () => setStage("rest") }
|
||||
course = { at: place(fitted()), flight, landed: () => setStage("rest") }
|
||||
hideOrigin()
|
||||
paint()
|
||||
setStage("flying")
|
||||
@@ -187,7 +187,7 @@ export function ImagePreview(props: ImagePreviewProps) {
|
||||
// A close mid-open turns the flight around where it is, keeping its velocity.
|
||||
if (!course) {
|
||||
const rest = restFrame()
|
||||
course = { at: place(fitted()), ...panel(rest), flight: createFlight(rest), landed: showOrigin }
|
||||
course = { at: place(fitted()), flight: createFlight(rest), landed: showOrigin }
|
||||
}
|
||||
|
||||
course.flight.to(to, KICK.close, tuning())
|
||||
@@ -234,18 +234,14 @@ export function ImagePreview(props: ImagePreviewProps) {
|
||||
const paint = () => {
|
||||
scrim.style.opacity = String(dim.value)
|
||||
standIn.style.opacity = String(plain ? dim.value : veil.value)
|
||||
// Landed, the surface lies under the stand-in's own outline, so it leaves at once rather than fading in between.
|
||||
surface.style.opacity = course ? "1" : "0"
|
||||
|
||||
if (course) {
|
||||
const frame = course.flight.frame()
|
||||
// The controls ride the panel's top corner on their side, fading in as it lifts.
|
||||
const side = course.rtl ? frame.surface.x - course.panel.x : right(frame.surface) - right(course.panel)
|
||||
header.style.transform = `translate(${side}px, ${frame.surface.y - course.panel.y}px)`
|
||||
const style = flightStyle(course.at, frame)
|
||||
standIn.style.transform = style.transform
|
||||
standIn.style.clipPath = style.clipPath
|
||||
// The surface and the outline are leaf boxes, so sizing them every frame stays cheap; scaling them instead would
|
||||
// The shadow and the outline are leaf boxes, so sizing them every frame stays cheap; scaling them instead would
|
||||
// stretch their corners and shadow.
|
||||
layout(surface, frame.surface, frame.surfaceRadius)
|
||||
surface.style.setProperty("--image-preview-lift", String(frame.lift))
|
||||
@@ -253,8 +249,7 @@ export function ImagePreview(props: ImagePreviewProps) {
|
||||
lift = frame.lift
|
||||
}
|
||||
|
||||
if (!course) header.style.transform = ""
|
||||
// Flying, the panel's controls arrive with the surface; without a flight they fade with the backdrop.
|
||||
// Flying, the controls arrive as the image lifts; without a flight they fade with the backdrop.
|
||||
content.style.opacity = String(plain ? dim.value : course ? lift ** 2 : dim.value ** 2)
|
||||
edge.style.opacity = String((1 - lift) * veil.value)
|
||||
}
|
||||
@@ -270,24 +265,16 @@ export function ImagePreview(props: ImagePreviewProps) {
|
||||
return box
|
||||
}
|
||||
|
||||
// The image as shown now, zoomed and panned and cropped to the viewport, in its panel.
|
||||
// The image as shown now, zoomed and panned and cropped to the viewport. There is no panel: the image is the surface.
|
||||
const restFrame = (): FlightFrame => {
|
||||
const shown = image.getBoundingClientRect()
|
||||
const clip = intersect(shown, viewport.getBoundingClientRect())
|
||||
const cropped = clip.width < shown.width - 0.5 || clip.height < shown.height - 0.5
|
||||
const radius = cropped ? 0 : parseFloat(getComputedStyle(image).borderTopLeftRadius) * view().scale
|
||||
|
||||
return {
|
||||
image: shown,
|
||||
clip,
|
||||
radius: cropped ? 0 : parseFloat(getComputedStyle(image).borderTopLeftRadius) * view().scale,
|
||||
surface: content.getBoundingClientRect(),
|
||||
surfaceRadius: parseFloat(getComputedStyle(content).borderTopLeftRadius) || 0,
|
||||
lift: 1,
|
||||
}
|
||||
return { image: shown, clip, radius, surface: clip, surfaceRadius: radius, lift: 1 }
|
||||
}
|
||||
|
||||
const panel = (rest: FlightFrame) => ({ panel: rest.surface, rtl: getComputedStyle(content).direction === "rtl" })
|
||||
|
||||
// The origin's frame, cropped to the part the page shows, or nothing when it is gone or out of sight.
|
||||
const originFrame = () => {
|
||||
const origin = props.origin
|
||||
@@ -430,97 +417,103 @@ export function ImagePreview(props: ImagePreviewProps) {
|
||||
style={{ "--image-preview-exit": `${exit()}ms` }}
|
||||
>
|
||||
<div ref={scrim} data-slot="image-preview-scrim" />
|
||||
<div data-slot="image-preview-container">
|
||||
<Kobalte.Content
|
||||
ref={content}
|
||||
data-slot="image-preview-content"
|
||||
onOpenAutoFocus={(event) => {
|
||||
// Open with focus on Close, as before, rather than on the zoom controls that now precede it.
|
||||
<Kobalte.Content
|
||||
ref={content}
|
||||
data-slot="image-preview-content"
|
||||
onOpenAutoFocus={(event) => {
|
||||
// Open with focus on Close, as before, rather than on the zoom controls that precede it.
|
||||
event.preventDefault()
|
||||
close?.focus()
|
||||
}}
|
||||
onPointerDown={(event: PointerEvent) => {
|
||||
backdrop = event.target === content || event.target === viewport
|
||||
}}
|
||||
onClick={(event: MouseEvent) => {
|
||||
// The preview fills the window, so a click on the backdrop around the image lands here rather than outside.
|
||||
if (!backdrop || (event.target !== content && event.target !== viewport)) return
|
||||
dialog.close()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={viewport}
|
||||
data-slot="image-preview-body"
|
||||
data-zoomed={view().scale > 1 ? "" : undefined}
|
||||
data-dragging={drag() ? "" : undefined}
|
||||
onDblClick={(event) => {
|
||||
if (goal.scale > 1) return ease(FIT_VIEW)
|
||||
ease(zoomAt(goal, detailScale(fitRatio()), point(event), bounds()))
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
if (event.button !== 0 || view().scale <= 1) return
|
||||
event.preventDefault()
|
||||
close?.focus()
|
||||
viewport.setPointerCapture(event.pointerId)
|
||||
setDrag({ id: event.pointerId, x: event.clientX, y: event.clientY })
|
||||
}}
|
||||
onPointerMove={(event) => {
|
||||
const current = drag()
|
||||
|
||||
if (current?.id !== event.pointerId) return
|
||||
backdrop = false
|
||||
setDrag({ id: current.id, x: event.clientX, y: event.clientY })
|
||||
follow(panBy(view(), { x: event.clientX - current.x, y: event.clientY - current.y }, bounds()))
|
||||
}}
|
||||
onPointerUp={(event) => {
|
||||
if (drag()?.id === event.pointerId) setDrag(undefined)
|
||||
}}
|
||||
onPointerCancel={(event) => {
|
||||
if (drag()?.id === event.pointerId) setDrag(undefined)
|
||||
}}
|
||||
>
|
||||
<div ref={header} data-slot="image-preview-header">
|
||||
<div data-slot="image-preview-zoom">
|
||||
<IconButton
|
||||
data-slot="image-preview-zoom-out"
|
||||
icon={<Icon name="minus" />}
|
||||
variant="ghost"
|
||||
aria-label={i18n.t("ui.imagePreview.zoomOut")}
|
||||
onClick={() => step(1 / STEP)}
|
||||
/>
|
||||
<Button
|
||||
data-slot="image-preview-zoom-level"
|
||||
variant="ghost-muted"
|
||||
size="small"
|
||||
aria-label={i18n.t("ui.imagePreview.zoomToFit")}
|
||||
title={i18n.t("ui.imagePreview.zoomToFit")}
|
||||
onClick={() => ease(FIT_VIEW)}
|
||||
>
|
||||
{percent().format(view().scale * fitRatio())}
|
||||
</Button>
|
||||
<IconButton
|
||||
data-slot="image-preview-zoom-in"
|
||||
icon={<Icon name="plus" />}
|
||||
variant="ghost"
|
||||
aria-label={i18n.t("ui.imagePreview.zoomIn")}
|
||||
onClick={() => step(STEP)}
|
||||
/>
|
||||
</div>
|
||||
<Kobalte.CloseButton
|
||||
ref={close}
|
||||
data-slot="image-preview-close"
|
||||
as={IconButton}
|
||||
icon={<Icon name="close" />}
|
||||
variant="ghost"
|
||||
aria-label={i18n.t("ui.common.close")}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={viewport}
|
||||
data-slot="image-preview-body"
|
||||
data-zoomed={view().scale > 1 ? "" : undefined}
|
||||
data-dragging={drag() ? "" : undefined}
|
||||
onDblClick={(event) => {
|
||||
if (goal.scale > 1) return ease(FIT_VIEW)
|
||||
ease(zoomAt(goal, detailScale(fitRatio()), point(event), bounds()))
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
if (event.button !== 0 || view().scale <= 1) return
|
||||
event.preventDefault()
|
||||
viewport.setPointerCapture(event.pointerId)
|
||||
setDrag({ id: event.pointerId, x: event.clientX, y: event.clientY })
|
||||
}}
|
||||
onPointerMove={(event) => {
|
||||
const current = drag()
|
||||
|
||||
if (current?.id !== event.pointerId) return
|
||||
setDrag({ id: current.id, x: event.clientX, y: event.clientY })
|
||||
follow(panBy(view(), { x: event.clientX - current.x, y: event.clientY - current.y }, bounds()))
|
||||
}}
|
||||
onPointerUp={(event) => {
|
||||
if (drag()?.id === event.pointerId) setDrag(undefined)
|
||||
}}
|
||||
onPointerCancel={(event) => {
|
||||
if (drag()?.id === event.pointerId) setDrag(undefined)
|
||||
<img
|
||||
ref={image}
|
||||
src={props.src}
|
||||
alt={props.alt ?? i18n.t("ui.imagePreview.alt")}
|
||||
data-slot="image-preview-image"
|
||||
draggable={false}
|
||||
onLoad={() => {
|
||||
measure()
|
||||
void reveal()
|
||||
}}
|
||||
onError={() => void reveal()}
|
||||
style={{ transform: `translate(${view().x}px, ${view().y}px) scale(${view().scale})` }}
|
||||
/>
|
||||
</div>
|
||||
<div data-slot="image-preview-controls">
|
||||
<IconButton
|
||||
data-slot="image-preview-zoom-out"
|
||||
icon={<Icon name="minus" />}
|
||||
variant="ghost"
|
||||
aria-label={i18n.t("ui.imagePreview.zoomOut")}
|
||||
onClick={() => step(1 / STEP)}
|
||||
/>
|
||||
<Button
|
||||
data-slot="image-preview-zoom-level"
|
||||
variant="ghost-muted"
|
||||
size="small"
|
||||
aria-label={i18n.t("ui.imagePreview.zoomToFit")}
|
||||
title={i18n.t("ui.imagePreview.zoomToFit")}
|
||||
onClick={() => ease(FIT_VIEW)}
|
||||
>
|
||||
<img
|
||||
ref={image}
|
||||
src={props.src}
|
||||
alt={props.alt ?? i18n.t("ui.imagePreview.alt")}
|
||||
data-slot="image-preview-image"
|
||||
draggable={false}
|
||||
onLoad={() => {
|
||||
measure()
|
||||
void reveal()
|
||||
}}
|
||||
onError={() => void reveal()}
|
||||
style={{ transform: `translate(${view().x}px, ${view().y}px) scale(${view().scale})` }}
|
||||
/>
|
||||
</div>
|
||||
</Kobalte.Content>
|
||||
</div>
|
||||
{percent().format(view().scale * fitRatio())}
|
||||
</Button>
|
||||
<IconButton
|
||||
data-slot="image-preview-zoom-in"
|
||||
icon={<Icon name="plus" />}
|
||||
variant="ghost"
|
||||
aria-label={i18n.t("ui.imagePreview.zoomIn")}
|
||||
onClick={() => step(STEP)}
|
||||
/>
|
||||
<div data-slot="image-preview-divider" />
|
||||
<Kobalte.CloseButton
|
||||
ref={close}
|
||||
data-slot="image-preview-close"
|
||||
as={IconButton}
|
||||
icon={<Icon name="close" />}
|
||||
variant="ghost"
|
||||
aria-label={i18n.t("ui.common.close")}
|
||||
/>
|
||||
</div>
|
||||
</Kobalte.Content>
|
||||
<div data-slot="image-preview-surface" ref={surface} />
|
||||
<img data-slot="image-preview-flight" src={props.src} alt="" aria-hidden="true" ref={standIn} />
|
||||
<div data-slot="image-preview-edge" ref={edge} />
|
||||
@@ -675,10 +668,6 @@ function covered(rows: number, shows: (row: number) => boolean) {
|
||||
return low
|
||||
}
|
||||
|
||||
function right(box: Box) {
|
||||
return box.x + box.width
|
||||
}
|
||||
|
||||
function layout(element: HTMLElement, frame: Box, radius: number) {
|
||||
Object.assign(element.style, {
|
||||
left: `${frame.x}px`,
|
||||
|
||||
Reference in New Issue
Block a user