From df9655bb10c6f2a330d33b80530183c03de46ecd Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sun, 7 Dec 2025 12:44:48 -0800 Subject: [PATCH] feat: add StandardizedToolWrapper and fix map interactions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wrap map component in StandardizedToolWrapper with header bar - Add onPointerDown={stopPropagation} to all sidebar interactive elements - Add handleMapWheel that forwards wheel zoom to map component - Add pinnedToView, tags, isMinimized props for consistency - Fix TypeScript type for stopPropagation handler ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/shapes/MapShapeUtil.tsx | 1019 ++++++++++++++++++----------------- 1 file changed, 528 insertions(+), 491 deletions(-) diff --git a/src/shapes/MapShapeUtil.tsx b/src/shapes/MapShapeUtil.tsx index f3c1e98..1352456 100644 --- a/src/shapes/MapShapeUtil.tsx +++ b/src/shapes/MapShapeUtil.tsx @@ -15,9 +15,10 @@ */ import { BaseBoxShapeUtil, TLBaseShape, HTMLContainer, TLResizeInfo, resizeBox, T } from 'tldraw'; -import { useRef, useEffect, useState, useCallback, useMemo } from 'react'; +import { useRef, useEffect, useState, useCallback } from 'react'; import maplibregl from 'maplibre-gl'; import 'maplibre-gl/dist/maplibre-gl.css'; +import { StandardizedToolWrapper } from '../components/StandardizedToolWrapper'; // ============================================================================= // Types @@ -76,6 +77,9 @@ export type IMapShape = TLBaseShape< waypoints: Coordinate[]; collaborators: CollaboratorPresence[]; showSidebar: boolean; + pinnedToView: boolean; + tags: string[]; + isMinimized: boolean; // Legacy compatibility properties interactive: boolean; showGPS: boolean; @@ -168,6 +172,9 @@ type StyleKey = keyof typeof MAP_STYLES; export class MapShape extends BaseBoxShapeUtil { static override type = 'Map' as const; + // Map theme color: Blue (consistent with mapping/navigation) + static readonly PRIMARY_COLOR = '#4890E8'; + static override props = { w: T.number, h: T.number, @@ -180,6 +187,9 @@ export class MapShape extends BaseBoxShapeUtil { waypoints: T.any, collaborators: T.any, showSidebar: T.boolean, + pinnedToView: T.boolean, + tags: T.any, + isMinimized: T.boolean, // Legacy compatibility properties interactive: T.boolean, showGPS: T.boolean, @@ -202,6 +212,9 @@ export class MapShape extends BaseBoxShapeUtil { waypoints: [], collaborators: [], showSidebar: true, + pinnedToView: false, + tags: ['map'], + isMinimized: false, // Legacy compatibility defaults interactive: true, showGPS: false, @@ -220,11 +233,13 @@ export class MapShape extends BaseBoxShapeUtil { } indicator(shape: IMapShape) { - return ; + const height = shape.props.isMinimized ? 40 : shape.props.h + 40; + return ; } component(shape: IMapShape) { - return ; + const isSelected = this.editor.getSelectedShapeIds().includes(shape.id); + return ; } } @@ -303,7 +318,7 @@ const styles = { // Map Component // ============================================================================= -function MapComponent({ shape, editor }: { shape: IMapShape; editor: MapShape['editor'] }) { +function MapComponent({ shape, editor, isSelected }: { shape: IMapShape; editor: MapShape['editor']; isSelected: boolean }) { const containerRef = useRef(null); const mapRef = useRef(null); const markersRef = useRef>(new Map()); @@ -315,10 +330,9 @@ function MapComponent({ shape, editor }: { shape: IMapShape; editor: MapShape['e const [showColorPicker, setShowColorPicker] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); - const [nearbyPlaces, setNearbyPlaces] = useState([]); + const [_nearbyPlaces, setNearbyPlaces] = useState([]); const [isSearching, setIsSearching] = useState(false); const [observingUser, setObservingUser] = useState(null); - const [editingTitle, setEditingTitle] = useState(false); const styleKey = (shape.props.styleKey || 'voyager') as StyleKey; const currentStyle = MAP_STYLES[styleKey] || MAP_STYLES.voyager; @@ -775,36 +789,74 @@ function MapComponent({ shape, editor }: { shape: IMapShape; editor: MapShape['e // Event Handlers // ========================================================================== - const stopPropagation = useCallback((e: React.SyntheticEvent) => { + const stopPropagation = useCallback((e: { stopPropagation: () => void }) => { e.stopPropagation(); }, []); - // Prevent browser zoom when over the map - use map zoom instead - const handleWheel = useCallback((e: React.WheelEvent) => { + // Handle wheel events on map container - forward delta to map for zooming + const handleMapWheel = useCallback((e: React.WheelEvent) => { e.stopPropagation(); e.preventDefault(); - // The map will handle zooming via its own wheel handler + // Forward wheel event to the map for zooming + if (mapRef.current) { + const map = mapRef.current; + const delta = e.deltaY > 0 ? -1 : 1; + const currentZoom = map.getZoom(); + map.easeTo({ + zoom: currentZoom + delta * 0.5, + duration: 150, + }); + } }, []); - // Capture all pointer events to prevent tldraw from intercepting - const capturePointerEvents = useCallback((e: React.PointerEvent) => { - e.stopPropagation(); - }, []); + // Close handler for StandardizedToolWrapper + const handleClose = useCallback(() => { + editor.deleteShape(shape.id); + }, [editor, shape.id]); + + // Minimize handler + const handleMinimize = useCallback(() => { + editor.updateShape({ + id: shape.id, + type: 'Map', + props: { isMinimized: !shape.props.isMinimized }, + }); + }, [editor, shape.id, shape.props.isMinimized]); + + // Pin handler + const handlePinToggle = useCallback(() => { + editor.updateShape({ + id: shape.id, + type: 'Map', + props: { pinnedToView: !shape.props.pinnedToView }, + }); + }, [editor, shape.id, shape.props.pinnedToView]); + + // Tags handler + const handleTagsChange = useCallback((newTags: string[]) => { + editor.updateShape({ + id: shape.id, + type: 'Map', + props: { tags: newTags }, + }); + }, [editor, shape.id]); // ========================================================================== // Render // ========================================================================== + const contentHeight = shape.props.h; + return ( - + -
- {/* Left Sidebar */} - {shape.props.showSidebar && ( -
- {/* Map Details */} -
- updateTitle(e.target.value)} - onFocus={() => setEditingTitle(true)} - onBlur={() => setEditingTitle(false)} - style={{ - width: '100%', - border: 'none', - fontSize: 15, - fontWeight: 600, - color: '#222', - background: 'transparent', - borderBottom: editingTitle ? '2px solid #E8E8E8' : '2px solid transparent', - outline: 'none', - padding: 0, - marginBottom: 4, - }} - /> - updateDescription(e.target.value)} - style={{ - width: '100%', - border: 'none', - fontSize: 13, - color: '#626C72', - background: 'transparent', - outline: 'none', - padding: 0, - }} - placeholder="Add a description..." - /> -
+
+ {/* Left Sidebar */} + {shape.props.showSidebar && ( +
+ {/* Search */} +
+
+ setSearchQuery(e.target.value)} + onKeyDown={(e) => { + e.stopPropagation(); + if (e.key === 'Enter') searchPlaces(); + }} + onPointerDown={stopPropagation} + placeholder="Search for a place..." + style={{ + flex: 1, + padding: '10px 12px', + border: '1px solid #E8E8E8', + borderRadius: 6, + fontSize: 13, + outline: 'none', + }} + /> + +
- {/* Search */} -
-
- setSearchQuery(e.target.value)} - onKeyDown={(e) => { - e.stopPropagation(); - if (e.key === 'Enter') searchPlaces(); - }} - placeholder="Search for a place..." - style={{ - flex: 1, - padding: '10px 12px', - border: '1px solid #E8E8E8', - borderRadius: 6, - fontSize: 13, - outline: 'none', - }} - /> - + {/* Search Results */} + {searchResults.length > 0 && ( +
+ {searchResults.map((result, i) => ( +
selectSearchResult(result)} + onPointerDown={stopPropagation} + style={{ + padding: '10px 8px', + cursor: 'pointer', + fontSize: 12, + borderRadius: 4, + }} + > + ๐Ÿ“ {result.name.slice(0, 50)}... +
+ ))} +
+ )}
- {/* Search Results */} - {searchResults.length > 0 && ( -
- {searchResults.map((result, i) => ( + {/* Find Nearby */} +
+
Find nearby
+
+ {NEARBY_CATEGORIES.map((cat) => (
selectSearchResult(result)} + key={cat.key} + className="mapus-category" + onClick={() => findNearby(cat)} + onPointerDown={stopPropagation} style={{ - padding: '10px 8px', + textAlign: 'center', + padding: '10px 4px', + borderRadius: 6, cursor: 'pointer', - fontSize: 12, - borderRadius: 4, + fontSize: 11, + color: '#626C72', }} > - ๐Ÿ“ {result.name.slice(0, 50)}... +
{cat.icon}
+ {cat.label}
))}
- )} -
- - {/* Find Nearby */} -
-
Find nearby
-
- {NEARBY_CATEGORIES.map((cat) => ( -
findNearby(cat)} - style={{ - textAlign: 'center', - padding: '10px 4px', - borderRadius: 6, - cursor: 'pointer', - fontSize: 11, - color: '#626C72', - }} - > -
{cat.icon}
- {cat.label} -
- ))} -
-
- - {/* Collaborators */} - {shape.props.collaborators.length > 0 && ( -
-
People ({shape.props.collaborators.length})
- {shape.props.collaborators.map((collab: CollaboratorPresence) => ( -
observeUser(observingUser === collab.id ? null : collab.id)} - style={{ - display: 'flex', - alignItems: 'center', - gap: 10, - padding: '8px 6px', - borderRadius: 6, - cursor: 'pointer', - background: observingUser === collab.id ? '#f0fdf4' : 'transparent', - }} - > -
- {collab.name[0].toUpperCase()} -
-
-
{collab.name}
- {observingUser === collab.id && ( -
๐Ÿ‘๏ธ Observing
- )} -
-
- ))} -
- )} - - {/* Annotations */} -
-
-
Annotations
-
- {shape.props.annotations.length === 0 ? ( -
- No annotations yet.
Use the tools below to add some! -
- ) : ( -
- {shape.props.annotations.map((ann: Annotation) => ( + {/* Collaborators */} + {shape.props.collaborators.length > 0 && ( +
+
People ({shape.props.collaborators.length})
+ {shape.props.collaborators.map((collab: CollaboratorPresence) => (
observeUser(observingUser === collab.id ? null : collab.id)} + onPointerDown={stopPropagation} style={{ display: 'flex', alignItems: 'center', - gap: 8, + gap: 10, padding: '8px 6px', borderRadius: 6, cursor: 'pointer', - opacity: ann.visible ? 1 : 0.5, + background: observingUser === collab.id ? '#f0fdf4' : 'transparent', }} >
-
- {ann.name} + background: collab.color, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: 12, + color: 'white', + fontWeight: 600, + }}> + {collab.name[0].toUpperCase()} +
+
+
{collab.name}
+ {observingUser === collab.id && ( +
๐Ÿ‘๏ธ Observing
+ )}
- -
))}
)} -
- {/* Attribution */} -
- ยฉ OpenStreetMap -
-
- )} - - {/* Map Container */} -
-
- - {/* Sidebar Toggle */} - - - {/* Style Picker */} -
- -
- - {/* Zoom Controls */} -
- - - -
- - {/* Drawing Toolbar (Mapus-style) */} -
- {/* Cursor Tool */} - - - {/* Marker Tool */} - - - {/* Line Tool */} - - - {/* Area Tool */} - - - {/* Eraser */} - - - {/* Divider */} -
- - {/* Color Picker */} -
- - - {showColorPicker && ( -
- {COLORS.map((color) => ( -
{ setSelectedColor(color); setShowColorPicker(false); }} - style={{ - width: 28, - height: 28, - borderRadius: '50%', - background: color, - cursor: 'pointer', - transition: 'transform 0.15s', - }} - /> - ))} + {/* Annotations */} +
+
+
Annotations
+
- )} -
-
- {/* Observing Indicator */} - {observingUser && ( -
c.id === observingUser)?.color || '#3b82f6'}`, - borderRadius: 8, - }}> -
c.id === observingUser)?.color || '#3b82f6', - color: '#fff', - padding: '4px 10px', - borderRadius: 4, - fontSize: 12, - fontWeight: 500, - }}> - ๐Ÿ‘๏ธ Observing {shape.props.collaborators.find((c: CollaboratorPresence) => c.id === observingUser)?.name} + {shape.props.annotations.length === 0 ? ( +
+ No annotations yet.
Use the tools below to add some! +
+ ) : ( +
+ {shape.props.annotations.map((ann: Annotation) => ( +
+
+
+ {ann.name} +
+ + +
+ ))} +
+ )} +
+ + {/* Attribution */} +
+ ยฉ OpenStreetMap
)} + + {/* Map Container */} +
+
+ + {/* Sidebar Toggle */} + + + {/* Style Picker */} +
+ +
+ + {/* Zoom Controls */} +
+ + + +
+ + {/* Drawing Toolbar (Mapus-style) */} +
+ {/* Cursor Tool */} + + + {/* Marker Tool */} + + + {/* Line Tool */} + + + {/* Area Tool */} + + + {/* Eraser */} + + + {/* Divider */} +
+ + {/* Color Picker */} +
+ + + {showColorPicker && ( +
+ {COLORS.map((color) => ( +
{ setSelectedColor(color); setShowColorPicker(false); }} + style={{ + width: 28, + height: 28, + borderRadius: '50%', + background: color, + cursor: 'pointer', + transition: 'transform 0.15s', + }} + /> + ))} +
+ )} +
+
+ + {/* Observing Indicator */} + {observingUser && ( +
c.id === observingUser)?.color || '#3b82f6'}`, + borderRadius: 8, + }}> +
c.id === observingUser)?.color || '#3b82f6', + color: '#fff', + padding: '4px 10px', + borderRadius: 4, + fontSize: 12, + fontWeight: 500, + }}> + ๐Ÿ‘๏ธ Observing {shape.props.collaborators.find((c: CollaboratorPresence) => c.id === observingUser)?.name} +
+
+ )} +
-
+ ); }