From b29e130f267399eebfa73948dd0d8a47efd7caa1 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Wed, 26 Aug 2026 02:24:02 +0100 Subject: [PATCH] fix: enhance lightbox (#2429) * fix: close on click outside; smaller icon size * fix: make ALT more visible --- .../components/common/lightbox-view.tsx | 28 +++++++--- .../src/features/editor/styles/media.css | 54 +++++++++++++++++++ packages/editor-ext/src/lib/drawio.ts | 6 ++- packages/editor-ext/src/lib/excalidraw.ts | 6 ++- packages/editor-ext/src/lib/image/image.ts | 6 ++- packages/editor-ext/src/lib/media-utils.ts | 21 ++++++++ 6 files changed, 110 insertions(+), 11 deletions(-) diff --git a/apps/client/src/features/editor/components/common/lightbox-view.tsx b/apps/client/src/features/editor/components/common/lightbox-view.tsx index 88aec56e1..9c8e9bf6c 100644 --- a/apps/client/src/features/editor/components/common/lightbox-view.tsx +++ b/apps/client/src/features/editor/components/common/lightbox-view.tsx @@ -4,8 +4,6 @@ import Lightbox, { type Slide } from "yet-another-react-lightbox"; import type { LightboxRequest } from "@/features/editor/atoms/editor-atoms"; import { getFileUrl } from "@/lib/config.ts"; import "yet-another-react-lightbox/styles.css"; -import "yet-another-react-lightbox/plugins/captions.css"; -import Captions from "yet-another-react-lightbox/plugins/captions"; import Download from "yet-another-react-lightbox/plugins/download"; import Fullscreen from "yet-another-react-lightbox/plugins/fullscreen"; import Video from "yet-another-react-lightbox/plugins/video"; @@ -53,13 +51,11 @@ function getFilename(src: string) { function getMedia(rawSrc: string, type?: string, alt?: string): Slide { const src = getFileUrl(rawSrc); const filename = getFilename(rawSrc); - const caption = alt || filename; if (type === "video") { return { type: "video", sources: [{ src, type: getVideoMimeType(rawSrc) }], - title: caption, download: { url: src, filename }, }; } else { @@ -67,7 +63,6 @@ function getMedia(rawSrc: string, type?: string, alt?: string): Slide { type: "image", src, alt: alt || undefined, - title: caption, download: { url: src, filename }, }; } @@ -124,6 +119,11 @@ export default function LightboxView({ const [pageSlides, setPageSlides] = useState([]); const [loadedMediaKey, setLoadedMediaKey] = useState(null); + const [isFullscreen, setIsFullscreen] = useState(false); + + useEffect(() => { + if (!open) setIsFullscreen(false); + }, [open]); useEffect(() => { if (!open) return; @@ -161,9 +161,21 @@ export default function LightboxView({ close={onClose} index={index} slides={slides} - plugins={[Captions, Download, Fullscreen, Video, Zoom]} - styles={{ container: { backgroundColor: "rgba(0, 0, 0, 0.8)" } }} - captions={{ descriptionTextAlign: "center" }} + plugins={[Download, Fullscreen, Video, Zoom]} + styles={{ + container: { backgroundColor: "rgba(0, 0, 0, 0.8)" }, + icon: { width: 24, height: 24 }, + toolbar: { + margin: 8, + borderRadius: 8, + backgroundColor: "rgba(0, 0, 0, 0.5)", + }, + }} + controller={{ closeOnBackdropClick: !isFullscreen }} + on={{ + enterFullscreen: () => setIsFullscreen(true), + exitFullscreen: () => setIsFullscreen(false), + }} video={{ controls: true, playsInline: true }} zoom={{ scrollToZoom: true, diff --git a/apps/client/src/features/editor/styles/media.css b/apps/client/src/features/editor/styles/media.css index 0b02cdbeb..14725b9fe 100644 --- a/apps/client/src/features/editor/styles/media.css +++ b/apps/client/src/features/editor/styles/media.css @@ -38,6 +38,60 @@ line-height: var(--mantine-line-height-md); } + .media-alt-badge { + position: absolute; + right: 8px; + bottom: 8px; + z-index: 1; + padding: 2px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.03em; + cursor: default; + user-select: none; + background-color: var(--mantine-color-body); + + @mixin light { + border: 1px solid var(--mantine-color-gray-3); + color: var(--mantine-color-gray-7); + } + + @mixin dark { + border: 1px solid var(--mantine-color-dark-4); + color: var(--mantine-color-dark-1); + } + + @media print { + display: none; + } + + &::after { + content: attr(data-alt); + display: none; + position: absolute; + right: 0; + bottom: calc(100% + 6px); + width: max-content; + max-width: 320px; + padding: 6px 10px; + border-radius: var(--mantine-radius-default); + font-size: var(--mantine-font-size-sm); + font-weight: 400; + letter-spacing: normal; + line-height: var(--mantine-line-height-sm); + white-space: pre-wrap; + overflow-wrap: break-word; + text-align: start; + background-color: var(--mantine-color-gray-9); + color: var(--mantine-color-white); + } + + &:hover::after { + display: block; + } + } + .media-pulse { animation: media-pulse 1.2s ease-in-out infinite; diff --git a/packages/editor-ext/src/lib/drawio.ts b/packages/editor-ext/src/lib/drawio.ts index 14d70f394..63e2f3460 100644 --- a/packages/editor-ext/src/lib/drawio.ts +++ b/packages/editor-ext/src/lib/drawio.ts @@ -2,7 +2,7 @@ import { Node, mergeAttributes } from "@tiptap/core"; import { ResizableNodeView } from "./resizable-nodeview"; import type { ResizableNodeViewDirection } from "./resizable-nodeview"; import { ReactNodeViewRenderer } from "@tiptap/react"; -import { normalizeFileUrl } from "./media-utils"; +import { normalizeFileUrl, syncAltBadge } from "./media-utils"; export type DrawioResizeOptions = { enabled: boolean; @@ -293,6 +293,8 @@ export const Drawio = Node.create({ const container = nodeView.dom as HTMLElement; applyAlignment(container, align); + syncAltBadge(nodeView.wrapper, updatedNode.attrs.alt); + currentNode = updatedNode; return true; }, @@ -310,6 +312,8 @@ export const Drawio = Node.create({ const dom = nodeView.dom as HTMLElement; + syncAltBadge(nodeView.wrapper, node.attrs.alt); + applyAlignment(dom, node.attrs.align || "center"); // Handle percentage width backward compat diff --git a/packages/editor-ext/src/lib/excalidraw.ts b/packages/editor-ext/src/lib/excalidraw.ts index 59b28f7fa..de35bde0f 100644 --- a/packages/editor-ext/src/lib/excalidraw.ts +++ b/packages/editor-ext/src/lib/excalidraw.ts @@ -2,7 +2,7 @@ import { Node, mergeAttributes } from "@tiptap/core"; import { ResizableNodeView } from "./resizable-nodeview"; import type { ResizableNodeViewDirection } from "./resizable-nodeview"; import { ReactNodeViewRenderer } from "@tiptap/react"; -import { normalizeFileUrl } from "./media-utils"; +import { normalizeFileUrl, syncAltBadge } from "./media-utils"; export type ExcalidrawResizeOptions = { enabled: boolean; @@ -293,6 +293,8 @@ export const Excalidraw = Node.create({ const container = nodeView.dom as HTMLElement; applyAlignment(container, align); + syncAltBadge(nodeView.wrapper, updatedNode.attrs.alt); + currentNode = updatedNode; return true; }, @@ -310,6 +312,8 @@ export const Excalidraw = Node.create({ const dom = nodeView.dom as HTMLElement; + syncAltBadge(nodeView.wrapper, node.attrs.alt); + applyAlignment(dom, node.attrs.align || "center"); // Handle percentage width backward compat diff --git a/packages/editor-ext/src/lib/image/image.ts b/packages/editor-ext/src/lib/image/image.ts index 6a9219264..bafdf82b8 100644 --- a/packages/editor-ext/src/lib/image/image.ts +++ b/packages/editor-ext/src/lib/image/image.ts @@ -7,7 +7,7 @@ import { } from "@tiptap/core"; import { ResizableNodeView } from "../resizable-nodeview"; import type { ResizableNodeViewDirection } from "../resizable-nodeview"; -import { normalizeFileUrl } from "../media-utils"; +import { normalizeFileUrl, syncAltBadge } from "../media-utils"; export type ImageResizeOptions = { enabled: boolean; @@ -316,6 +316,8 @@ export const TiptapImage = Image.extend({ const container = nodeView.dom as HTMLElement; applyAlignment(container, align); + syncAltBadge(nodeView.wrapper, updatedNode.attrs.alt); + currentNode = updatedNode; return true; }, @@ -333,6 +335,8 @@ export const TiptapImage = Image.extend({ const dom = nodeView.dom as HTMLElement; + syncAltBadge(nodeView.wrapper, node.attrs.alt); + // Apply initial alignment applyAlignment(dom, node.attrs.align || "center"); diff --git a/packages/editor-ext/src/lib/media-utils.ts b/packages/editor-ext/src/lib/media-utils.ts index d18bcb69e..dfec750ca 100644 --- a/packages/editor-ext/src/lib/media-utils.ts +++ b/packages/editor-ext/src/lib/media-utils.ts @@ -7,6 +7,27 @@ export function normalizeFileUrl(src: string): string { return src || ""; } +export function syncAltBadge(wrapper: HTMLElement, alt: unknown): void { + const existing = wrapper.querySelector( + ":scope > .media-alt-badge", + ); + + if (typeof alt !== "string" || !alt.trim()) { + existing?.remove(); + return; + } + + const badge = existing ?? document.createElement("span"); + badge.dataset.alt = alt; + + if (!existing) { + badge.className = "media-alt-badge"; + badge.textContent = "ALT"; + badge.setAttribute("aria-hidden", "true"); + wrapper.appendChild(badge); + } +} + export type UploadFn = ( file: File, editor: Editor,