Skip to content

Commit 336c065

Browse files
authored
fix(viewer): image pan/zoom, sort fixes, sidebar dot fixes (#3836)
* feat(file-viewer): add pan and zoom to image preview * fix(viewer): fix sort key mapping, disable load-more on sort, hide status dots when menu open * fix(file-viewer): prevent scroll bleed and zoom button micro-pans * fix(file-viewer): use exponential zoom formula to prevent zero/negative multiplier
1 parent b371364 commit 336c065

File tree

4 files changed

+118
-13
lines changed

4 files changed

+118
-13
lines changed

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { memo, useCallback, useEffect, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
5+
import { ZoomIn, ZoomOut } from 'lucide-react'
56
import { Skeleton } from '@/components/emcn'
67
import { cn } from '@/lib/core/utils/cn'
78
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
@@ -432,17 +433,120 @@ const IframePreview = memo(function IframePreview({ file }: { file: WorkspaceFil
432433
)
433434
})
434435

436+
const ZOOM_MIN = 0.25
437+
const ZOOM_MAX = 4
438+
const ZOOM_WHEEL_SENSITIVITY = 0.005
439+
const ZOOM_BUTTON_FACTOR = 1.2
440+
441+
const clampZoom = (z: number) => Math.min(Math.max(z, ZOOM_MIN), ZOOM_MAX)
442+
435443
const ImagePreview = memo(function ImagePreview({ file }: { file: WorkspaceFileRecord }) {
436444
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace`
445+
const [zoom, setZoom] = useState(1)
446+
const [offset, setOffset] = useState({ x: 0, y: 0 })
447+
const isDragging = useRef(false)
448+
const dragStart = useRef({ x: 0, y: 0 })
449+
const offsetAtDragStart = useRef({ x: 0, y: 0 })
450+
const offsetRef = useRef(offset)
451+
offsetRef.current = offset
452+
453+
const containerRef = useRef<HTMLDivElement>(null)
454+
455+
const zoomIn = useCallback(() => setZoom((z) => clampZoom(z * ZOOM_BUTTON_FACTOR)), [])
456+
const zoomOut = useCallback(() => setZoom((z) => clampZoom(z / ZOOM_BUTTON_FACTOR)), [])
457+
458+
useEffect(() => {
459+
const el = containerRef.current
460+
if (!el) return
461+
const onWheel = (e: WheelEvent) => {
462+
e.preventDefault()
463+
if (e.ctrlKey || e.metaKey) {
464+
setZoom((z) => clampZoom(z * Math.exp(-e.deltaY * ZOOM_WHEEL_SENSITIVITY)))
465+
} else {
466+
setOffset((o) => ({ x: o.x - e.deltaX, y: o.y - e.deltaY }))
467+
}
468+
}
469+
el.addEventListener('wheel', onWheel, { passive: false })
470+
return () => el.removeEventListener('wheel', onWheel)
471+
}, [])
472+
473+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
474+
if (e.button !== 0) return
475+
isDragging.current = true
476+
dragStart.current = { x: e.clientX, y: e.clientY }
477+
offsetAtDragStart.current = offsetRef.current
478+
if (containerRef.current) containerRef.current.style.cursor = 'grabbing'
479+
e.preventDefault()
480+
}, [])
481+
482+
const handleMouseMove = useCallback((e: React.MouseEvent) => {
483+
if (!isDragging.current) return
484+
setOffset({
485+
x: offsetAtDragStart.current.x + (e.clientX - dragStart.current.x),
486+
y: offsetAtDragStart.current.y + (e.clientY - dragStart.current.y),
487+
})
488+
}, [])
489+
490+
const handleMouseUp = useCallback(() => {
491+
isDragging.current = false
492+
if (containerRef.current) containerRef.current.style.cursor = 'grab'
493+
}, [])
494+
495+
useEffect(() => {
496+
setZoom(1)
497+
setOffset({ x: 0, y: 0 })
498+
}, [file.key])
437499

438500
return (
439-
<div className='flex flex-1 items-center justify-center overflow-auto bg-[var(--surface-1)] p-6'>
440-
<img
441-
src={serveUrl}
442-
alt={file.name}
443-
className='max-h-full max-w-full rounded-md object-contain'
444-
loading='eager'
445-
/>
501+
<div
502+
ref={containerRef}
503+
className='relative flex flex-1 cursor-grab overflow-hidden bg-[var(--surface-1)]'
504+
onMouseDown={handleMouseDown}
505+
onMouseMove={handleMouseMove}
506+
onMouseUp={handleMouseUp}
507+
onMouseLeave={handleMouseUp}
508+
>
509+
<div
510+
className='pointer-events-none absolute inset-0 flex items-center justify-center'
511+
style={{
512+
transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom})`,
513+
transformOrigin: 'center center',
514+
}}
515+
>
516+
<img
517+
src={serveUrl}
518+
alt={file.name}
519+
className='max-h-full max-w-full select-none rounded-md object-contain'
520+
draggable={false}
521+
loading='eager'
522+
/>
523+
</div>
524+
<div
525+
className='absolute right-4 bottom-4 flex items-center gap-1 rounded-md border border-[var(--border)] bg-[var(--surface-2)] px-2 py-1 shadow-sm'
526+
onMouseDown={(e) => e.stopPropagation()}
527+
>
528+
<button
529+
type='button'
530+
onClick={zoomOut}
531+
disabled={zoom <= ZOOM_MIN}
532+
className='flex h-6 w-6 items-center justify-center rounded text-[var(--text-secondary)] transition-colors hover:bg-[var(--surface-3)] hover:text-[var(--text-primary)] disabled:cursor-not-allowed disabled:opacity-40'
533+
aria-label='Zoom out'
534+
>
535+
<ZoomOut className='h-3.5 w-3.5' />
536+
</button>
537+
<span className='min-w-[3rem] text-center text-[11px] text-[var(--text-secondary)]'>
538+
{Math.round(zoom * 100)}%
539+
</span>
540+
<button
541+
type='button'
542+
onClick={zoomIn}
543+
disabled={zoom >= ZOOM_MAX}
544+
className='flex h-6 w-6 items-center justify-center rounded text-[var(--text-secondary)] transition-colors hover:bg-[var(--surface-3)] hover:text-[var(--text-primary)] disabled:cursor-not-allowed disabled:opacity-40'
545+
aria-label='Zoom in'
546+
>
547+
<ZoomIn className='h-3.5 w-3.5' />
548+
</button>
549+
</div>
446550
</div>
447551
)
448552
})

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export function Document({
185185
? 'tokenCount'
186186
: activeSort?.column === 'status'
187187
? 'enabled'
188-
: activeSort
188+
: activeSort?.column === 'index'
189189
? 'chunkIndex'
190190
: undefined,
191191
activeSort?.direction

apps/sim/app/workspace/[workspaceId]/logs/logs.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -640,11 +640,12 @@ export default function Logs() {
640640
}, [initializeFromURL])
641641

642642
const loadMoreLogs = useCallback(() => {
643+
if (activeSort) return
643644
const { isFetching, hasNextPage, fetchNextPage } = logsQueryRef.current
644645
if (!isFetching && hasNextPage) {
645646
fetchNextPage()
646647
}
647-
}, [])
648+
}, [activeSort])
648649

649650
useEffect(() => {
650651
const handleKeyDown = (e: KeyboardEvent) => {
@@ -1144,7 +1145,7 @@ export default function Logs() {
11441145
onRowContextMenu={handleLogContextMenu}
11451146
isLoading={!logsQuery.data}
11461147
onLoadMore={loadMoreLogs}
1147-
hasMore={logsQuery.hasNextPage ?? false}
1148+
hasMore={!activeSort && (logsQuery.hasNextPage ?? false)}
11481149
isLoadingMore={logsQuery.isFetchingNextPage}
11491150
emptyMessage='No logs found'
11501151
overlay={sidebarOverlay}

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,13 @@ const SidebarTaskItem = memo(function SidebarTaskItem({
183183
<div className='min-w-0 flex-1 truncate font-base text-[var(--text-body)]'>{task.name}</div>
184184
{task.id !== 'new' && (
185185
<div className='relative flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center'>
186-
{isActive && !isCurrentRoute && (
186+
{isActive && !isCurrentRoute && !isMenuOpen && (
187187
<span className='absolute h-[7px] w-[7px] animate-ping rounded-full bg-amber-400 opacity-30 group-hover:hidden' />
188188
)}
189-
{isActive && !isCurrentRoute && (
189+
{isActive && !isCurrentRoute && !isMenuOpen && (
190190
<span className='absolute h-[7px] w-[7px] rounded-full bg-amber-400 group-hover:hidden' />
191191
)}
192-
{!isActive && isUnread && !isCurrentRoute && (
192+
{!isActive && isUnread && !isCurrentRoute && !isMenuOpen && (
193193
<span className='absolute h-[7px] w-[7px] rounded-full bg-[var(--brand-accent)] group-hover:hidden' />
194194
)}
195195
<button

0 commit comments

Comments
 (0)