Files
deepseek-harness/packages/client/ui-attachment/src/ImageLightbox.tsx
T
creatixchu 2c9036b102 feat(web): whole-page image drop, projected intake limits, and thumbnail tiling
Second alignment step for #2248: document-level drag intake behind the new
DropOverlay atom, lightbox close icon and shared dialog mask, DeepSeek Chat
thumbnail rules (single 240px long edge with ratio clamp, 64px tiles, merged
consecutive assistant images), image limits raised to 20/10MiB/100MiB and
published to clients as the imageLimits projection, whole-batch intake
pre-check with product-copy banners, and attachment-error reasons mapped to
localized copy.
2026-08-12 12:31:52 +08:00

65 lines
2.2 KiB
TypeScript

import { useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'
import { IconCloseOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
import css from './ImageLightbox.module.css'
/** Lightbox strings the owner resolves from its own locale namespace. */
export interface ImageLightboxLabels {
/** Accessible name of the preview dialog. */
dialog: string
/** Accessible label of the close control. */
close: string
}
/**
* Document-level original-image preview opened by clicking a thumbnail.
* Closes on Escape, backdrop press, or the close control, and restores focus
* to the opener on unmount. Rendered through a body portal: an opener inside
* a transformed or filtered ancestor would otherwise trap the fixed backdrop
* in that ancestor's box instead of covering the viewport.
*
* @param props.src - the original image URL.
* @param props.alt - the image's alt text.
* @param props.labels - dialog and close-control strings.
* @param props.onClose - dismiss callback owned by the opener.
* @returns the modal preview dialog.
*/
export function ImageLightbox({ src, alt, labels, onClose }: {
src: string
alt: string
labels: ImageLightboxLabels
onClose: () => void
}) {
const closeRef = useRef<HTMLButtonElement | null>(null)
const restoreRef = useRef<HTMLElement | null>(null)
useEffect(() => {
restoreRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null
closeRef.current?.focus()
const onKeyDown = (event: globalThis.KeyboardEvent): void => {
if (event.key === 'Escape') onClose()
}
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
restoreRef.current?.focus()
}
}, [onClose])
return createPortal(
<div
className={css.backdrop}
role="dialog"
aria-modal="true"
aria-label={labels.dialog}
>
<div className={css.mask} aria-hidden="true" onMouseDown={onClose} />
<img className={css.image} src={src} alt={alt} />
<button ref={closeRef} type="button" className={css.close} aria-label={labels.close} onClick={onClose}>
<IconCloseOutline16 size={16} />
</button>
</div>,
document.body,
)
}