From fe5453a44e291e181ecdd5b0275a0f95748e6bf8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Mar 2026 18:13:59 +0000 Subject: [PATCH] Add resizable minimap with edge/corner resize gestures - Add resize handles (north, east, northeast corner) to minimap - Store minimap size in localStorage for persistence - Use same resize interaction pattern as sidebar (pointer capture, idle/pointing/resizing states) - No visible handles - invisible resize zones at edges - Double-click on handles to reset to default size - Respects min/max size constraints (100x80 to 500x400) --- packages/tldraw/src/lib/ui.css | 43 +++++- .../ui/components/Minimap/DefaultMinimap.tsx | 136 +++++++++++++++++- 2 files changed, 176 insertions(+), 3 deletions(-) diff --git a/packages/tldraw/src/lib/ui.css b/packages/tldraw/src/lib/ui.css index 69fe0bd7480e..fada5b651df6 100644 --- a/packages/tldraw/src/lib/ui.css +++ b/packages/tldraw/src/lib/ui.css @@ -1159,20 +1159,59 @@ tldraw? probably. /* Minimap */ .tlui-minimap { - width: 100%; + position: relative; + width: 140px; height: 96px; - min-height: 96px; + min-width: 100px; + min-height: 80px; + max-width: 500px; + max-height: 400px; overflow: hidden; padding: var(--tl-space-3); padding-top: 0px; } +.tlui-minimap[data-resizing='true'] { + pointer-events: none; +} + .tlui-minimap__canvas { position: relative; width: 100%; height: 100%; } +/* Minimap resize handles - invisible like sidebar resizer */ +.tlui-minimap__resize-handle { + position: absolute; + z-index: 1; + pointer-events: all; +} + +.tlui-minimap__resize-handle--n { + top: -3px; + left: 0; + right: 8px; + height: 6px; + cursor: ns-resize; +} + +.tlui-minimap__resize-handle--e { + top: 6px; + right: -3px; + bottom: 0; + width: 6px; + cursor: ew-resize; +} + +.tlui-minimap__resize-handle--ne { + top: -3px; + right: -3px; + width: 12px; + height: 12px; + cursor: nesw-resize; +} + /* --------------------- Toolbar -------------------- */ /* Wide container */ diff --git a/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx b/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx index bb6f1fcae56c..7d597c0cba50 100644 --- a/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx +++ b/packages/tldraw/src/lib/ui/components/Minimap/DefaultMinimap.tsx @@ -2,6 +2,7 @@ import { Box, TLPointerEventInfo, Vec, + clamp, getPointerInfo, isAccelKey, normalizeWheel, @@ -12,9 +13,37 @@ import { useIsDarkMode, } from '@tldraw/editor' import * as React from 'react' +import { useLocalStorageState } from '../../hooks/useLocalStorageState' import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { MinimapManager } from './MinimapManager' +const MIN_MINIMAP_WIDTH = 100 +const MIN_MINIMAP_HEIGHT = 80 +const MAX_MINIMAP_WIDTH = 500 +const MAX_MINIMAP_HEIGHT = 400 +const DEFAULT_MINIMAP_WIDTH = 140 +const DEFAULT_MINIMAP_HEIGHT = 96 + +type ResizeEdge = 'n' | 'e' | 'ne' +type ResizeState = + | { name: 'idle' } + | { + name: 'pointing' + edge: ResizeEdge + startX: number + startY: number + startWidth: number + startHeight: number + } + | { + name: 'resizing' + edge: ResizeEdge + startX: number + startY: number + startWidth: number + startHeight: number + } + /** @public @react */ export function DefaultMinimap() { const editor = useEditor() @@ -23,6 +52,13 @@ export function DefaultMinimap() { const rCanvas = React.useRef(null!) const rPointing = React.useRef(false) + const rMinimapContainer = React.useRef(null!) + const rResizeState = React.useRef({ name: 'idle' }) + + const [minimapSize, setMinimapSize] = useLocalStorageState('tldraw_minimap_size', { + width: DEFAULT_MINIMAP_WIDTH, + height: DEFAULT_MINIMAP_HEIGHT, + }) const minimapRef = React.useRef() @@ -199,8 +235,84 @@ export function DefaultMinimap() { }) }, [isDarkMode, editor]) + const handleResizePointerDown = React.useCallback( + (e: React.PointerEvent, edge: ResizeEdge) => { + e.currentTarget.setPointerCapture(e.pointerId) + e.stopPropagation() + + rResizeState.current = { + name: 'pointing', + edge, + startX: e.clientX, + startY: e.clientY, + startWidth: minimapSize.width, + startHeight: minimapSize.height, + } + }, + [minimapSize] + ) + + const handleResizePointerMove = React.useCallback( + (e: React.PointerEvent) => { + const state = rResizeState.current + if (state.name === 'idle') return + + if (state.name === 'pointing') { + const dx = Math.abs(e.clientX - state.startX) + const dy = Math.abs(e.clientY - state.startY) + if (dx < 3 && dy < 3) return + + rResizeState.current = { ...state, name: 'resizing' } + rMinimapContainer.current?.setAttribute('data-resizing', 'true') + } + + if (rResizeState.current.name === 'resizing') { + const { edge, startX, startY, startWidth, startHeight } = rResizeState.current + + let newWidth = startWidth + let newHeight = startHeight + + if (edge === 'e' || edge === 'ne') { + newWidth = clamp(startWidth + (e.clientX - startX), MIN_MINIMAP_WIDTH, MAX_MINIMAP_WIDTH) + } + + if (edge === 'n' || edge === 'ne') { + newHeight = clamp( + startHeight - (e.clientY - startY), + MIN_MINIMAP_HEIGHT, + MAX_MINIMAP_HEIGHT + ) + } + + setMinimapSize({ width: Math.floor(newWidth), height: Math.floor(newHeight) }) + } + }, + [setMinimapSize] + ) + + const handleResizePointerUp = React.useCallback((e: React.PointerEvent) => { + e.currentTarget.releasePointerCapture(e.pointerId) + rResizeState.current = { name: 'idle' } + rMinimapContainer.current?.removeAttribute('data-resizing') + }, []) + + const handleResizeDoubleClick = React.useCallback( + (e: React.MouseEvent) => { + e.stopPropagation() + setMinimapSize({ width: DEFAULT_MINIMAP_WIDTH, height: DEFAULT_MINIMAP_HEIGHT }) + }, + [setMinimapSize] + ) + return ( -
+
+ {/* Resize handles - invisible, like sidebar resizer */} +
handleResizePointerDown(e, 'n')} + onPointerMove={handleResizePointerMove} + onLostPointerCapture={handleResizePointerUp} + onDoubleClick={handleResizeDoubleClick} + /> +
handleResizePointerDown(e, 'e')} + onPointerMove={handleResizePointerMove} + onLostPointerCapture={handleResizePointerUp} + onDoubleClick={handleResizeDoubleClick} + /> +
handleResizePointerDown(e, 'ne')} + onPointerMove={handleResizePointerMove} + onLostPointerCapture={handleResizePointerUp} + onDoubleClick={handleResizeDoubleClick} + />
) }