feat: add icon components and update ImageLightbox with icons and Button
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user