From 1b7e044bf52cdcaafbe5650860b0cce3e149efb1 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:30:15 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=A9=20style:=20DialogImage,=20Update?= =?UTF-8?q?=20Stylesheet,=20and=20Improve=20Accessibility=20(#8014)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 fix: Adjust typography and border styles for improved readability in markdown components * 🔧 fix: Enhance code block styling in markdown for better visibility and consistency * 🔧 fix: Adjust margins and line heights for improved readability in markdown elements * 🔧 fix: Adjust spacing for horizontal rules in markdown for improved consistency * 🔧 fix: Refactor DialogImage component for improved quality styling and layout consistency * 🔧 fix: Enhance zoom and pan functionality in DialogImage component with improved controls and user experience * 🔧 fix: Improve zoom and pan functionality in DialogImage component with enhanced controls and reset zoom feature --- .../Chat/Messages/Content/DialogImage.tsx | 246 +++++++++++++++--- client/src/locales/en/translation.json | 3 +- client/src/style.css | 146 +++++++---- 3 files changed, 314 insertions(+), 81 deletions(-) diff --git a/client/src/components/Chat/Messages/Content/DialogImage.tsx b/client/src/components/Chat/Messages/Content/DialogImage.tsx index 907902f4ed..0711757df1 100644 --- a/client/src/components/Chat/Messages/Content/DialogImage.tsx +++ b/client/src/components/Chat/Messages/Content/DialogImage.tsx @@ -1,14 +1,33 @@ -import { useState, useEffect } from 'react'; -import { X, ArrowDownToLine, PanelLeftOpen, PanelLeftClose } from 'lucide-react'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import { X, ArrowDownToLine, PanelLeftOpen, PanelLeftClose, RotateCcw } from 'lucide-react'; import { Button, OGDialog, OGDialogContent, TooltipAnchor } from '~/components'; import { useLocalize } from '~/hooks'; +const getQualityStyles = (quality: string): string => { + if (quality === 'high') { + return 'bg-green-100 text-green-800'; + } + if (quality === 'low') { + return 'bg-orange-100 text-orange-800'; + } + return 'bg-gray-100 text-gray-800'; +}; + export default function DialogImage({ isOpen, onOpenChange, src = '', downloadImage, args }) { const localize = useLocalize(); const [isPromptOpen, setIsPromptOpen] = useState(false); const [imageSize, setImageSize] = useState(null); - const getImageSize = async (url: string) => { + // Zoom and pan state + const [zoom, setZoom] = useState(1); + const [panX, setPanX] = useState(0); + const [panY, setPanY] = useState(0); + const [isDragging, setIsDragging] = useState(false); + const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); + + const containerRef = useRef(null); + + const getImageSize = useCallback(async (url: string) => { try { const response = await fetch(url, { method: 'HEAD' }); const contentLength = response.headers.get('Content-Length'); @@ -25,7 +44,7 @@ export default function DialogImage({ isOpen, onOpenChange, src = '', downloadIm console.error('Error getting image size:', error); return null; } - }; + }, []); const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 Bytes'; @@ -37,11 +56,129 @@ export default function DialogImage({ isOpen, onOpenChange, src = '', downloadIm return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; + const getImageMaxWidth = () => { + // On mobile (when panel overlays), use full width minus padding + // On desktop, account for the side panel width + if (isPromptOpen) { + return window.innerWidth >= 640 ? 'calc(100vw - 22rem)' : 'calc(100vw - 2rem)'; + } + return 'calc(100vw - 2rem)'; + }; + + const resetZoom = useCallback(() => { + setZoom(1); + setPanX(0); + setPanY(0); + }, []); + + const getCursor = () => { + if (zoom <= 1) return 'default'; + return isDragging ? 'grabbing' : 'grab'; + }; + + const handleDoubleClick = useCallback(() => { + if (zoom > 1) { + resetZoom(); + } else { + // Zoom in to 2x on double click when at normal zoom + setZoom(2); + } + }, [zoom, resetZoom]); + + const handleWheel = useCallback( + (e: React.WheelEvent) => { + e.preventDefault(); + if (!containerRef.current) return; + + const rect = containerRef.current.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + // Calculate zoom factor + const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; + const newZoom = Math.min(Math.max(zoom * zoomFactor, 1), 5); + + if (newZoom === zoom) return; + + // If zooming back to 1, reset pan to center the image + if (newZoom === 1) { + setZoom(1); + setPanX(0); + setPanY(0); + return; + } + + // Calculate the zoom center relative to the current viewport + const containerCenterX = rect.width / 2; + const containerCenterY = rect.height / 2; + + // Calculate new pan position to zoom towards mouse cursor + const zoomRatio = newZoom / zoom; + const deltaX = (mouseX - containerCenterX - panX) * (zoomRatio - 1); + const deltaY = (mouseY - containerCenterY - panY) * (zoomRatio - 1); + + setZoom(newZoom); + setPanX(panX - deltaX); + setPanY(panY - deltaY); + }, + [zoom, panX, panY], + ); + + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + if (zoom <= 1) return; + setIsDragging(true); + setDragStart({ + x: e.clientX - panX, + y: e.clientY - panY, + }); + }, + [zoom, panX, panY], + ); + + const handleMouseMove = useCallback( + (e: React.MouseEvent) => { + if (!isDragging || zoom <= 1) return; + const newPanX = e.clientX - dragStart.x; + const newPanY = e.clientY - dragStart.y; + setPanX(newPanX); + setPanY(newPanY); + }, + [isDragging, dragStart, zoom], + ); + const handleMouseUp = useCallback(() => { + setIsDragging(false); + }, []); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => e.key === 'Escape' && resetZoom(); + document.addEventListener('keydown', onKey); + return () => document.removeEventListener('keydown', onKey); + }, [resetZoom]); + useEffect(() => { if (isOpen && src) { getImageSize(src).then(setImageSize); + resetZoom(); } - }, [isOpen, src]); + }, [isOpen, src, getImageSize, resetZoom]); + + // Ensure image is centered when zoom changes to 1 + useEffect(() => { + if (zoom === 1) { + setPanX(0); + setPanY(0); + } + }, [zoom]); + + // Reset pan when panel opens/closes to maintain centering + useEffect(() => { + if (zoom === 1) { + setPanX(0); + setPanY(0); + } + }, [isPromptOpen, zoom]); return ( @@ -52,7 +189,7 @@ export default function DialogImage({ isOpen, onOpenChange, src = '', downloadIm overlayClassName="bg-surface-primary opacity-95 z-50" >
- + } /> -
+
+ {zoom > 1 && ( + + + + } + /> + )} {isPromptOpen ? ( - + ) : ( - + )} } @@ -100,36 +247,81 @@ export default function DialogImage({ isOpen, onOpenChange, src = '', downloadIm {/* Main content area with image */}
-
- Image 1 ? 'hidden' : 'visible', + minHeight: 0, // Allow flexbox to shrink + }} + > +
+ > + Image +
{/* Side Panel */}
-
-
+ {/* Mobile pull handle - removed for cleaner look */} + +
+ {/* Mobile close button */} +
+

+ {localize('com_ui_image_details')} +

+ +
+ +

{localize('com_ui_image_details')}

-
+
{/* Prompt Section */}

@@ -157,13 +349,7 @@ export default function DialogImage({ isOpen, onOpenChange, src = '', downloadIm
{localize('com_ui_quality')}: {args?.quality || 'Standard'} diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 34ef2a7448..b875644a20 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -1054,6 +1054,7 @@ "com_ui_x_selected": "{{0}} selected", "com_ui_yes": "Yes", "com_ui_zoom": "Zoom", + "com_ui_reset_zoom": "Reset Zoom", "com_user_message": "You", "com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint." -} \ No newline at end of file +} diff --git a/client/src/style.css b/client/src/style.css index 438f197a55..4139dfd043 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -818,14 +818,14 @@ pre { max-width: 65ch; font-size: var(--markdown-font-size, var(--font-size-base)); line-height: calc( - 28px * var(--markdown-font-size, var(--font-size-base)) / var(--font-size-base) + 22px * var(--markdown-font-size, var(--font-size-base)) / var(--font-size-base) ); } .prose :where([class~='lead']):not(:where([class~='not-prose'] *)) { color: var(--tw-prose-lead); font-size: 1.25em; - line-height: 1.6; + line-height: 1.3; margin-bottom: 1.2em; margin-top: 1.2em; } @@ -853,8 +853,8 @@ pre { .prose :where(hr):not(:where([class~='not-prose'] *)) { border-color: var(--tw-prose-hr); border-top-width: 1px; - margin-bottom: 3em; - margin-top: 3em; + margin-bottom: 0.8em; + margin-top: 0.8em; } .prose :where(blockquote):not(:where([class~='not-prose'] *)) { border-left-color: var(--tw-prose-quote-borders); @@ -878,9 +878,9 @@ pre { color: var(--tw-prose-headings); font-size: 2.25em; font-weight: 800; - line-height: 1.1111111; - margin-bottom: 0.8888889em; - margin-top: 0; + line-height: 1; + margin-bottom: 0.4em; + margin-top: 0.6em; } .prose :where(h1 strong):not(:where([class~='not-prose'] *)) { color: inherit; @@ -890,9 +890,9 @@ pre { color: var(--tw-prose-headings); font-size: 1.5em; font-weight: 700; - line-height: 1.3333333; - margin-bottom: 1em; - margin-top: 2em; + line-height: 1.1; + margin-bottom: 0.4em; + margin-top: 0.8em; } .prose :where(h2 strong):not(:where([class~='not-prose'] *)) { color: inherit; @@ -902,9 +902,9 @@ pre { color: var(--tw-prose-headings); font-size: 1.25em; font-weight: 600; - line-height: 1.6; - margin-bottom: 0.6em; - margin-top: 1.6em; + line-height: 1.3; + margin-bottom: 0.3em; + margin-top: 0.6em; } .prose :where(h3 strong):not(:where([class~='not-prose'] *)) { color: inherit; @@ -913,9 +913,9 @@ pre { .prose :where(h4):not(:where([class~='not-prose'] *)) { color: var(--tw-prose-headings); font-weight: 600; - line-height: 1.5; - margin-bottom: 0.5em; - margin-top: 1.5em; + line-height: 1.2; + margin-bottom: 0.3em; + margin-top: 0.5em; } .prose :where(h4 strong):not(:where([class~='not-prose'] *)) { color: inherit; @@ -932,19 +932,19 @@ pre { .prose :where(figcaption):not(:where([class~='not-prose'] *)) { color: var(--tw-prose-captions); font-size: 0.875em; - line-height: 1.4285714; + line-height: 1.2; margin-top: 0.8571429em; } .prose :where(code):not(:where([class~='not-prose'] *)) { color: var(--tw-prose-code); font-size: 0.875em; font-weight: 600; + background-color: var(--gray-200); + padding: 0.125rem 0.25rem; + border-radius: 0.35rem; } -.prose :where(code):not(:where([class~='not-prose'] *)):before { - content: '`'; -} -.prose :where(code):not(:where([class~='not-prose'] *)):after { - content: '`'; +.dark .prose :where(code):not(:where([class~='not-prose'] *)):not(:where(pre *)) { + background-color: var(--gray-600); } .prose :where(a code):not(:where([class~='not-prose'] *)) { color: inherit; @@ -971,11 +971,11 @@ pre { } .prose :where(pre):not(:where([class~='not-prose'] *)) { background-color: transparent; - border-radius: 0.375rem; + border-radius: 0.75rem; color: currentColor; font-size: 0.875em; font-weight: 400; - line-height: 1.7142857; + line-height: 1.4; margin: 0; overflow-x: auto; padding: 0; @@ -999,7 +999,7 @@ pre { } .prose :where(table):not(:where([class~='not-prose'] *)) { font-size: 0.875em; - line-height: 1.7142857; + line-height: 1.4; margin-bottom: 2em; margin-top: 2em; table-layout: auto; @@ -1036,14 +1036,14 @@ pre { vertical-align: top; } .prose { - --tw-prose-body: #374151; + --tw-prose-body: #424242; --tw-prose-headings: #111827; --tw-prose-lead: #4b5563; --tw-prose-links: #0066cc; --tw-prose-bold: #111827; --tw-prose-counters: #6b7280; --tw-prose-bullets: #d1d5db; - --tw-prose-hr: #e5e7eb; + --tw-prose-hr: #cdcdcd; --tw-prose-quotes: #111827; --tw-prose-quote-borders: #e5e7eb; --tw-prose-captions: #6b7280; @@ -1059,17 +1059,17 @@ pre { --tw-prose-invert-bold: #fff; --tw-prose-invert-counters: #9ca3af; --tw-prose-invert-bullets: #4b5563; - --tw-prose-invert-hr: #374151; + --tw-prose-invert-hr: #424242; --tw-prose-invert-quotes: #f3f4f6; - --tw-prose-invert-quote-borders: #374151; + --tw-prose-invert-quote-borders: #424242; --tw-prose-invert-captions: #9ca3af; --tw-prose-invert-code: #fff; --tw-prose-invert-pre-code: #d1d5db; --tw-prose-invert-pre-bg: rgba(0, 0, 0, 0.5); --tw-prose-invert-th-borders: #4b5563; - --tw-prose-invert-td-borders: #374151; + --tw-prose-invert-td-borders: #424242; font-size: 1rem; - line-height: 1.75; + line-height: 1.4; } .prose :where(p):not(:where([class~='not-prose'] *)) { margin-bottom: 1.25em; @@ -1112,6 +1112,13 @@ pre { .prose :where(h4 + *):not(:where([class~='not-prose'] *)) { margin-top: 0; } +/* Ensure symmetrical spacing around hr */ +.prose :where(* + hr):not(:where([class~='not-prose'] *)) { + margin-top: 0.8em; +} +.prose :where(hr + h1, hr + h2, hr + h3, hr + h4):not(:where([class~='not-prose'] *)) { + margin-top: 0.4em; +} .prose :where(thead th:first-child):not(:where([class~='not-prose'] *)) { padding-left: 0; } @@ -1213,6 +1220,14 @@ pre { .prose-2xl :where(.prose > :last-child):not(:where([class~='not-prose'] *)) { margin-bottom: 0; } +.prose :where(ul > li):has(input[type='checkbox']):not(:where([class~='not-prose'] *)) { + margin-bottom: 0; + margin-top: 0; +} +.prose :where(ul > li):has(input[type='checkbox']) p:not(:where([class~='not-prose'] *)) { + margin-bottom: 0; + margin-top: 0; +} code, pre { @@ -1484,7 +1499,7 @@ html { max-width: none; font-size: var(--markdown-font-size, var(--font-size-base)); line-height: calc( - 28px * var(--markdown-font-size, var(--font-size-base)) / var(--font-size-base) + 22px * var(--markdown-font-size, var(--font-size-base)) / var(--font-size-base) ); } @@ -1496,8 +1511,8 @@ html { } .markdown h2 { - margin-bottom: 1rem; - margin-top: 2rem; + margin-bottom: 0.4rem; + margin-top: 0.8rem; } .markdown h3 { @@ -1507,8 +1522,8 @@ html { .markdown h3, .markdown h4 { - margin-bottom: 0.5rem; - margin-top: 1rem; + margin-bottom: 0.3rem; + margin-top: 0.6rem; } .markdown h4 { @@ -1523,7 +1538,7 @@ html { .markdown blockquote { --tw-border-opacity: 1; - border-color: rgba(142, 142, 160, var(--tw-border-opacity)); + border-color: var(--gray-400); border-left-width: 2px; line-height: 1rem; padding-left: 1rem; @@ -1551,6 +1566,7 @@ html { .markdown th:last-child { border-right-width: 1px; + border-color: #d1d5db; border-top-right-radius: 0.375rem; } @@ -1751,16 +1767,16 @@ html { font-weight: 600; } .markdown h2 { - margin-bottom: 1rem; - margin-top: 2rem; + margin-bottom: 0.4rem; + margin-top: 0.8rem; } .markdown h3 { font-weight: 600; } .markdown h3, .markdown h4 { - margin-bottom: 0.5rem; - margin-top: 1rem; + margin-bottom: 0.3rem; + margin-top: 0.6rem; } .markdown h4 { font-weight: 400; @@ -1770,45 +1786,63 @@ html { } .markdown blockquote { --tw-border-opacity: 1; - border-color: rgba(142, 142, 160, var(--tw-border-opacity)); + border-color: var(--gray-300); border-left-width: 2px; line-height: 1rem; padding-left: 1rem; } +.dark .markdown blockquote { + border-color: var(--gray-600); +} .markdown table { --tw-border-spacing-x: 0px; --tw-border-spacing-y: 0px; border-collapse: separate; border-spacing: var(--tw-border-spacing-x) var(--tw-border-spacing-y); width: 100%; + border-color: var(--gray-300); } .markdown th { - background-color: rgba(236, 236, 241, 0.2); + background-color: var(--gray-100); border-bottom-width: 1px; border-left-width: 1px; border-top-width: 1px; + border-color: var(--gray-300); padding: 0.25rem 0.75rem; + font-weight: 600; +} +.dark .markdown th { + border-color: var(--gray-600); + background-color: var(--gray-600); } .markdown th:first-child { - border-top-left-radius: 0.375rem; + border-top-left-radius: 0.75rem; } .markdown th:last-child { border-right-width: 1px; - border-top-right-radius: 0.375rem; + border-top-right-radius: 0.75rem; } .markdown td { border-bottom-width: 1px; border-left-width: 1px; + border-color: var(--gray-300); padding: 0.25rem 0.75rem; } .markdown td:last-child { border-right-width: 1px; + border-color: var(--gray-300); +} +.dark .markdown td { + border-color: var(--gray-600); +} +.dark .markdown td:last-child { + border-color: var(--gray-600); } .markdown tbody tr:last-child td:first-child { - border-bottom-left-radius: 0.375rem; + border-bottom-left-radius: 0.75rem; } .markdown tbody tr:last-child td:last-child { - border-bottom-right-radius: 0.375rem; + border-bottom-right-radius: 0.75rem; } .markdown a { text-decoration-line: underline; @@ -2011,7 +2045,7 @@ html { .dark .assistant-item:after { --tw-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25); - --tw-shadow-colored: inset 0 0 0 1px var(--tw-shadow-color); + --tw-shadow-colored: inset 0 0 0 0 1px var(--tw-shadow-color); } .result-streaming > :not(ol):not(ul):not(pre):last-child:after, @@ -2248,7 +2282,13 @@ html { /* Nested unordered lists */ .prose ul ul, .markdown ul ul { - list-style-type: circle; + list-style-type: disc; +} + +.prose ul ul > li::marker, +.markdown ul ul > li::marker { + color: var(--tw-prose-bullets); + font-size: 0.8em; } .prose ul ul ul, @@ -2256,6 +2296,12 @@ html { list-style-type: square; } +.prose ul ul ul > li::marker, +.markdown ul ul ul > li::marker { + color: var(--tw-prose-bullets); + font-size: 0.7em; +} + /* Nested lists */ .prose ol ol, .prose ul ul, @@ -2450,7 +2496,7 @@ html { .message-content { font-size: var(--markdown-font-size, var(--font-size-base)); - line-height: 1.75; + line-height: 1.4; } .message-content pre code {