import React from "react" import { CustomMainMenu } from "./CustomMainMenu" import { CustomToolbar } from "./CustomToolbar" 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, TLComponents, TldrawUiMenuItem, useTools, useActions, useEditor, useValue, } from "tldraw" import { SlidesPanel } from "@/slides/SlidesPanel" // 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(['drive']) 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]) const totalUsers = collaborators.length + 1 return (
{/* Clickable avatar stack */} {/* Dropdown with user names */} {showDropdown && (
Participants ({totalUsers})
{/* Current user */}
{myUserName.charAt(0).toUpperCase()}
{myUserName} (you)
{/* Other users */} {collaborators.map((presence) => (
{(presence.userName || 'A').charAt(0).toUpperCase()}
{presence.userName || 'Anonymous'}
))} {/* 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 && (
setShowDropdown(false)} /> )}
) } // Custom SharePanel that shows people menu and help button function CustomSharePanel() { const tools = useTools() const actions = useActions() const [showShortcuts, setShowShortcuts] = React.useState(false) // Helper to extract label string from tldraw label (can be string or {default, menu} object) const getLabelString = (label: any, fallback: string): string => { if (typeof label === 'string') return label if (label && typeof label === 'object' && 'default' in label) return label.default return fallback } // Collect all tools and actions with keyboard shortcuts const allShortcuts = React.useMemo(() => { 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: getLabelString(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: getLabelString(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: getLabelString(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: getLabelString(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) => (
{shortcut.name.replace('tool.', '').replace('action.', '')} {shortcut.kbd.toUpperCase()}
))}
))}
)}
) } // Combined InFrontOfCanvas component for floating UI elements function CustomInFrontOfCanvas() { return ( <> ) } export const components: TLComponents = { Toolbar: CustomToolbar, MainMenu: CustomMainMenu, ContextMenu: CustomContextMenu, HelperButtons: SlidesPanel, SharePanel: CustomSharePanel, InFrontOfTheCanvas: CustomInFrontOfCanvas, KeyboardShortcutsDialog: (props: any) => { const tools = useTools() const actions = useActions() // Get all custom tools with keyboard shortcuts const customTools = [ tools["VideoChat"], tools["ChatBox"], tools["Embed"], tools["Slide"], tools["Markdown"], tools["MycrozineTemplate"], tools["Prompt"], tools["ObsidianNote"], tools["Transcription"], tools["Holon"], tools["FathomMeetings"], tools["ImageGen"], tools["VideoGen"], tools["Multmux"], // MycelialIntelligence moved to permanent floating bar ].filter(tool => tool && tool.kbd) // Get all custom actions with keyboard shortcuts const customActions = [ actions["zoom-in"], actions["zoom-out"], actions["zoom-to-selection"], actions["copy-link-to-current-view"], actions["copy-focus-link"], actions["unlock-camera-focus"], actions["revert-camera"], actions["lock-element"], actions["save-to-pdf"], actions["search-shapes"], actions["llm"], actions["open-obsidian-browser"], ].filter(action => action && action.kbd) return ( {/* Custom Tools */} {customTools.map(tool => ( ))} {/* Custom Actions */} {customActions.map(action => ( ))} {/* Default content (includes standard TLDraw shortcuts) */} ) }, }