From d4a0950effeab16f1f957cdded370bfd623fce01 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 4 Dec 2025 19:21:21 -0800 Subject: [PATCH] feat: add Google integration to user dropdown and keyboard shortcuts panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Google Workspace integration directly in user dropdown (CustomPeopleMenu) - Shows connection status (Connected/Not Connected) - Connect button to trigger OAuth flow - Browse Data button to open GoogleExportBrowser modal - Add toggleable keyboard shortcuts panel (? icon) - Shows full names of tools and actions with their shortcuts - Organized by category: Tools, Custom Tools, Actions, Custom Actions - Toggle on/off by clicking, closes when clicking outside - Import GoogleExportBrowser component for data browsing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/ui/components.tsx | 401 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 398 insertions(+), 3 deletions(-) diff --git a/src/ui/components.tsx b/src/ui/components.tsx index e280c55..0fb9cd8 100644 --- a/src/ui/components.tsx +++ b/src/ui/components.tsx @@ -5,6 +5,8 @@ import { CustomContextMenu } from "./CustomContextMenu" import { FocusLockIndicator } from "./FocusLockIndicator" import { MycelialIntelligenceBar } from "./MycelialIntelligenceBar" import { CommandPalette } from "./CommandPalette" +import { UserSettingsModal } from "./UserSettingsModal" +import { GoogleExportBrowser } from "../components/GoogleExportBrowser" import { DefaultKeyboardShortcutsDialog, DefaultKeyboardShortcutsDialogContent, @@ -17,15 +19,77 @@ import { } from "tldraw" import { SlidesPanel } from "@/slides/SlidesPanel" -// Custom People Menu component for showing connected users +// Custom People Menu component for showing connected users and integrations function CustomPeopleMenu() { const editor = useEditor() const [showDropdown, setShowDropdown] = React.useState(false) + const [showGoogleBrowser, setShowGoogleBrowser] = React.useState(false) + const [googleConnected, setGoogleConnected] = React.useState(false) + const [googleLoading, setGoogleLoading] = React.useState(false) + + // Detect dark mode + const isDarkMode = typeof document !== 'undefined' && + document.documentElement.classList.contains('dark') // Get current user info const myUserColor = useValue('myColor', () => editor.user.getColor(), [editor]) const myUserName = useValue('myName', () => editor.user.getName() || 'You', [editor]) + // Check Google connection on mount + React.useEffect(() => { + const checkGoogleStatus = async () => { + try { + const { GoogleDataService } = await import('../lib/google') + const service = GoogleDataService.getInstance() + const isAuthed = await service.isAuthenticated() + setGoogleConnected(isAuthed) + } catch (error) { + console.warn('Failed to check Google status:', error) + } + } + checkGoogleStatus() + }, []) + + const handleGoogleConnect = async () => { + setGoogleLoading(true) + try { + const { GoogleDataService } = await import('../lib/google') + const service = GoogleDataService.getInstance() + await service.authenticate() + setGoogleConnected(true) + } catch (error) { + console.error('Google auth failed:', error) + } finally { + setGoogleLoading(false) + } + } + + const handleOpenGoogleBrowser = () => { + setShowDropdown(false) + setShowGoogleBrowser(true) + } + + const handleAddToCanvas = async (items: any[], position: { x: number; y: number }) => { + try { + const { createGoogleItemProps } = await import('../shapes/GoogleItemShapeUtil') + + // Create shapes for each selected item + items.forEach((item, index) => { + const props = createGoogleItemProps(item, 'local') + editor.createShape({ + type: 'GoogleItem', + x: position.x + (index % 3) * 240, + y: position.y + Math.floor(index / 3) * 160, + props, + }) + }) + + setShowGoogleBrowser(false) + } catch (error) { + console.error('Failed to add items to canvas:', error) + } + } + // Get all collaborators (other users in the session) const collaborators = useValue('collaborators', () => editor.getCollaborators(), [editor]) @@ -199,9 +263,128 @@ function CustomPeopleMenu() { ))} + + {/* Separator */} +
+ + {/* Google Workspace Section */} +
+ Integrations +
+ +
+
+ G +
+
+
+ Google Workspace +
+
+ {googleConnected ? 'Connected' : 'Not connected'} +
+
+ {googleConnected ? ( + + ) : null} +
+ + {/* Google action buttons */} +
+ {!googleConnected ? ( + + ) : ( + + )} +
)} + {/* Google Export Browser Modal */} + {showGoogleBrowser && ( + setShowGoogleBrowser(false)} + onAddToCanvas={handleAddToCanvas} + isDarkMode={isDarkMode} + /> + )} + {/* Click outside to close */} {showDropdown && (
{ + const shortcuts: { name: string; kbd: string; category: string }[] = [] + + // Built-in tools + const builtInTools = ['select', 'hand', 'draw', 'eraser', 'arrow', 'text', 'note', 'frame', 'geo', 'line', 'highlight', 'laser'] + builtInTools.forEach(toolId => { + const tool = tools[toolId] + if (tool?.kbd) { + shortcuts.push({ + name: tool.label || toolId, + kbd: tool.kbd, + category: 'Tools' + }) + } + }) + + // Custom tools + const customToolIds = ['VideoChat', 'ChatBox', 'Embed', 'Slide', 'Markdown', 'MycrozineTemplate', 'Prompt', 'ObsidianNote', 'Transcription', 'Holon', 'FathomMeetings', 'ImageGen', 'VideoGen', 'Multmux'] + customToolIds.forEach(toolId => { + const tool = tools[toolId] + if (tool?.kbd) { + shortcuts.push({ + name: tool.label || toolId, + kbd: tool.kbd, + category: 'Custom Tools' + }) + } + }) + + // Built-in actions + const builtInActionIds = ['undo', 'redo', 'cut', 'copy', 'paste', 'delete', 'select-all', 'duplicate', 'group', 'ungroup', 'bring-to-front', 'send-to-back', 'zoom-in', 'zoom-out', 'zoom-to-fit', 'zoom-to-100', 'toggle-grid'] + builtInActionIds.forEach(actionId => { + const action = actions[actionId] + if (action?.kbd) { + shortcuts.push({ + name: action.label || actionId, + kbd: action.kbd, + category: 'Actions' + }) + } + }) + + // Custom actions + const customActionIds = ['copy-link-to-current-view', 'copy-focus-link', 'unlock-camera-focus', 'revert-camera', 'lock-element', 'save-to-pdf', 'search-shapes', 'llm', 'open-obsidian-browser'] + customActionIds.forEach(actionId => { + const action = actions[actionId] + if (action?.kbd) { + shortcuts.push({ + name: action.label || actionId, + kbd: action.kbd, + category: 'Custom Actions' + }) + } + }) + + return shortcuts + }, [tools, actions]) + + // Group shortcuts by category + const groupedShortcuts = React.useMemo(() => { + const groups: Record = {} + allShortcuts.forEach(shortcut => { + if (!groups[shortcut.category]) { + groups[shortcut.category] = [] + } + groups[shortcut.category].push(shortcut) + }) + return groups + }, [allShortcuts]) + return ( -
+
+ {/* Help/Keyboard shortcuts button */} + + + {/* Keyboard shortcuts panel */} + {showShortcuts && ( + <> +
setShowShortcuts(false)} + /> +
+
+ Keyboard Shortcuts + +
+ + {Object.entries(groupedShortcuts).map(([category, shortcuts]) => ( +
+
+ {category} +
+ {shortcuts.map((shortcut, idx) => ( +
+ + {typeof shortcut.name === 'string' ? shortcut.name.replace('tool.', '').replace('action.', '') : shortcut.name} + + + {shortcut.kbd.toUpperCase()} + +
+ ))} +
+ ))} +
+ + )} +
)