feat: add icon components and update ImageLightbox with icons and Button

This commit is contained in:
Ilia Mashkov
2026-05-22 12:46:47 +03:00
parent eeb7d6b4a6
commit 7a06d42d20
5 changed files with 102 additions and 9 deletions
+30
View File
@@ -0,0 +1,30 @@
type Props = {
/**
* CSS classes on the svg element
*/
className?: string;
};
/**
* Close / X icon (Lucide).
*/
export function CloseIcon({ className }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden={true}
className={className}
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
+30
View File
@@ -0,0 +1,30 @@
type Props = {
/**
* CSS classes on the svg element
*/
className?: string;
};
/**
* Magnify / search icon (Lucide).
*/
export function MagnifyIcon({ className }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden={true}
className={className}
>
<path d="m21 21-4.34-4.34" />
<circle cx="11" cy="11" r="8" />
</svg>
);
}
+2
View File
@@ -0,0 +1,2 @@
export { CloseIcon } from './CloseIcon';
export { MagnifyIcon } from './MagnifyIcon';
@@ -0,0 +1,25 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { ImageLightbox } from './ImageLightbox';
const meta: Meta<typeof ImageLightbox> = {
title: 'Shared/ImageLightbox',
component: ImageLightbox,
};
export default meta;
type Story = StoryObj<typeof ImageLightbox>;
export const Default: Story = {
args: {
src: 'https://picsum.photos/800/450',
alt: 'Sample project image',
},
decorators: [
(Story) => (
<div className="p-8 max-w-2xl">
<Story />
</div>
),
],
};
@@ -2,7 +2,9 @@
import Image from 'next/image';
import { useRef } from 'react';
import { CloseIcon, MagnifyIcon } from '$shared/assets/icons';
import { cn } from '$shared/lib';
import { Button } from '$shared/ui/Button';
type Props = {
/**
@@ -33,6 +35,11 @@ export function ImageLightbox({ src, alt, className }: Props) {
dialogRef.current?.close();
}
/**
* Closes the dialog when the user clicks the backdrop area directly.
* Comparing target to currentTarget distinguishes a click on the <dialog>
* element itself (the backdrop) from a click on its content children.
*/
function handleBackdropClick(e: React.MouseEvent<HTMLDialogElement>) {
if (e.target === e.currentTarget) {
close();
@@ -59,6 +66,10 @@ export function ImageLightbox({ src, alt, className }: Props) {
className={cn('relative block w-full aspect-video overflow-hidden cursor-zoom-in', className)}
>
<Image src={src} alt={alt} fill className="object-cover" />
{/* Magnify hint — pointer-events-none so clicks pass through to the button */}
<span aria-hidden={true} className="absolute bottom-2 right-2 bg-cream text-blue p-1 pointer-events-none">
<MagnifyIcon />
</span>
</button>
<dialog
@@ -66,20 +77,15 @@ export function ImageLightbox({ src, alt, className }: Props) {
aria-label={alt}
onClick={handleBackdropClick}
onKeyUp={handleBackdropKeyUp}
className="lightbox bg-blue brutal-border shadow-brutal-xl p-0 max-w-5xl w-full"
className="lightbox relative bg-blue brutal-border shadow-brutal-xl p-0 max-w-5xl w-full"
>
<div className="relative aspect-video w-full">
{/* aria-hidden: the dialog element itself carries the accessible label */}
<Image src={src} alt={alt} fill className="object-contain" aria-hidden={true} />
</div>
<button
type="button"
onClick={close}
aria-label="Close image"
className="absolute top-3 right-3 bg-blue text-cream brutal-border px-3 py-1 text-sm font-bold"
>
Close
</button>
<Button variant="ghost" size="sm" onClick={close} aria-label="Close image" className="absolute top-3 right-3">
<CloseIcon />
</Button>
</dialog>
</>
);