diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index ca64a4858..889f6ce8e 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -23,12 +23,13 @@ import { getBearSettings } from '@plannotator/ui/utils/bear'; import { getDefaultNotesApp } from '@plannotator/ui/utils/defaultNotesApp'; import { getAgentSwitchSettings, getEffectiveAgentName } from '@plannotator/ui/utils/agentSwitch'; import { getPlanSaveSettings } from '@plannotator/ui/utils/planSave'; -import { getUIPreferences, needsUIFeaturesSetup, type UIPreferences } from '@plannotator/ui/utils/uiPreferences'; +import { getUIPreferences, needsUIFeaturesSetup, type UIPreferences, type PlanWidth } from '@plannotator/ui/utils/uiPreferences'; import { getEditorMode, saveEditorMode } from '@plannotator/ui/utils/editorMode'; import { getInputMethod, saveInputMethod } from '@plannotator/ui/utils/inputMethod'; import { useInputMethodSwitch } from '@plannotator/ui/hooks/useInputMethodSwitch'; import { useResizablePanel } from '@plannotator/ui/hooks/useResizablePanel'; import { ResizeHandle } from '@plannotator/ui/components/ResizeHandle'; +import { MobileMenu } from '@plannotator/ui/components/MobileMenu'; import { getPermissionModeSettings, needsPermissionModeSetup, @@ -401,7 +402,8 @@ const App: React.FC = () => { const [showClaudeCodeWarning, setShowClaudeCodeWarning] = useState(false); const [showAgentWarning, setShowAgentWarning] = useState(false); const [agentWarningMessage, setAgentWarningMessage] = useState(''); - const [isPanelOpen, setIsPanelOpen] = useState(true); + const [isPanelOpen, setIsPanelOpen] = useState(() => window.innerWidth >= 768); + const [mobileSettingsOpen, setMobileSettingsOpen] = useState(false); const [editorMode, setEditorMode] = useState(getEditorMode); const [inputMethod, setInputMethod] = useState(getInputMethod); const [taterMode, setTaterMode] = useState(() => { @@ -966,6 +968,12 @@ const App: React.FC = () => { setIsPanelOpen(true); }; + // Stable reference — the Viewer's highlighter useEffect depends on this + const handleSelectAnnotation = React.useCallback((id: string | null) => { + setSelectedAnnotationId(id); + if (id && window.innerWidth < 768) setIsPanelOpen(true); + }, []); + const handleDeleteAnnotation = (id: string) => { viewerRef.current?.removeHighlight(id); setAnnotations(prev => prev.filter(a => a.id !== id)); @@ -1121,14 +1129,14 @@ const App: React.FC = () => { // Close export dropdown on click outside useEffect(() => { if (!showExportDropdown) return; - const handleClickOutside = (e: MouseEvent) => { + const handleClickOutside = (e: PointerEvent) => { const target = e.target as HTMLElement; if (!target.closest('[data-export-dropdown]')) { setShowExportDropdown(false); } }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); + document.addEventListener('pointerdown', handleClickOutside); + return () => document.removeEventListener('pointerdown', handleClickOutside); }, [showExportDropdown]); const agentName = useMemo(() => { @@ -1138,13 +1146,18 @@ const App: React.FC = () => { return 'Coding Agent'; }, [origin]); + const planMaxWidth = useMemo(() => { + const widths: Record = { compact: 832, default: 1040, wide: 1280 }; + return widths[uiPrefs.planWidth] ?? 832; + }, [uiPrefs.planWidth]); + return (
{/* Tater sprites */} {taterMode && } {/* Minimal Header */} -
+
{ )} - - {!linkedDocHook.isActive && } - - + {/* Desktop buttons — hidden on mobile */} +
+ + {!linkedDocHook.isActive && setMobileSettingsOpen(false)} />} -
- - {showExportDropdown && ( -
- {sharingEnabled && ( - - )} - - {isApiMode && isObsidianConfigured() && ( - - )} - {isApiMode && getBearSettings().enabled && ( +
+ + + + {showExportDropdown && ( +
+ {sharingEnabled && ( + + )} - )} - {isApiMode && !isObsidianConfigured() && !getBearSettings().enabled && ( -
- No notes apps configured. -
- )} - {sharingEnabled && ( - <> -
+ {isApiMode && isObsidianConfigured() && ( - - )} -
- )} + )} + {isApiMode && getBearSettings().enabled && ( + + )} + {isApiMode && !isObsidianConfigured() && !getBearSettings().enabled && ( +
+ No notes apps configured. +
+ )} + {sharingEnabled && ( + <> +
+ + + )} +
+ )} +
+ + {/* Mobile hamburger menu */} + setIsPanelOpen(!isPanelOpen)} + annotationCount={annotations.length + editorAnnotations.length} + onOpenExport={() => { setInitialExportTab(undefined); setShowExport(true); }} + onOpenSettings={() => setMobileSettingsOpen(true)} + onDownloadAnnotations={handleDownloadAnnotations} + onCopyShareLink={async () => { + try { + await navigator.clipboard.writeText(shareUrl); + setNoteSaveToast({ type: 'success', message: 'Share link copied' }); + } catch { + setNoteSaveToast({ type: 'error', message: 'Failed to copy' }); + } + setTimeout(() => setNoteSaveToast(null), 3000); + }} + onOpenImport={() => setShowImport(true)} + sharingEnabled={sharingEnabled} + />
@@ -1380,7 +1415,7 @@ const App: React.FC = () => { )} {/* Main Content */} -
+
{/* Left Sidebar: collapsed tab flags (when sidebar is closed) */} {!sidebar.isOpen && ( { cancelText="Dismiss" showCancel /> -
+
{/* Annotation Toolstrip (hidden during plan diff) */} {!isPlanDiffActive && ( -
+
{ repoInfo={repoInfo} baseVersionLabel={planDiff.diffBaseVersion != null ? `v${planDiff.diffBaseVersion}` : undefined} baseVersion={planDiff.diffBaseVersion ?? undefined} + maxWidth={planMaxWidth} /> ) : ( { frontmatter={frontmatter} annotations={annotations} onAddAnnotation={handleAddAnnotation} - onSelectAnnotation={setSelectedAnnotationId} + onSelectAnnotation={handleSelectAnnotation} selectedAnnotationId={selectedAnnotationId} mode={editorMode} inputMethod={inputMethod} @@ -1491,6 +1527,7 @@ const App: React.FC = () => { onPlanDiffToggle={() => setIsPlanDiffActive(!isPlanDiffActive)} hasPreviousVersion={!linkedDocHook.isActive && planDiff.hasPreviousVersion} showDemoBadge={!isApiMode && !isLoadingShared && !isSharedSession} + maxWidth={planMaxWidth} onOpenLinkedDoc={handleOpenLinkedDoc} linkedDocInfo={linkedDocHook.isActive ? { filepath: linkedDocHook.filepath!, onBack: handleLinkedDocBack, label: vaultBrowser.activeFile ? 'Vault File' : undefined } : null} /> @@ -1499,7 +1536,7 @@ const App: React.FC = () => { {/* Resize Handle */} - {isPanelOpen && } + {isPanelOpen && } {/* Annotation Panel */} { width={panelResize.width} editorAnnotations={editorAnnotations} onDeleteEditorAnnotation={deleteEditorAnnotation} + onClose={() => setIsPanelOpen(false)} />
diff --git a/packages/ui/components/AnnotationPanel.tsx b/packages/ui/components/AnnotationPanel.tsx index c78d80820..c63093171 100644 --- a/packages/ui/components/AnnotationPanel.tsx +++ b/packages/ui/components/AnnotationPanel.tsx @@ -3,6 +3,7 @@ import { Annotation, AnnotationType, Block, type EditorAnnotation } from '../typ import { isCurrentUser } from '../utils/identity'; import { ImageThumbnail } from './ImageThumbnail'; import { EditorAnnotationCard } from './EditorAnnotationCard'; +import { useIsMobile } from '../hooks/useIsMobile'; interface PanelProps { isOpen: boolean; @@ -17,6 +18,7 @@ interface PanelProps { width?: number; editorAnnotations?: EditorAnnotation[]; onDeleteEditorAnnotation?: (id: string) => void; + onClose?: () => void; } export const AnnotationPanel: React.FC = ({ @@ -32,7 +34,9 @@ export const AnnotationPanel: React.FC = ({ width, editorAnnotations, onDeleteEditorAnnotation, + onClose, }) => { + const isMobile = useIsMobile(); const [copied, setCopied] = useState(false); const listRef = useRef(null); const sortedAnnotations = [...annotations].sort((a, b) => a.createdA - b.createdA); @@ -60,17 +64,35 @@ export const AnnotationPanel: React.FC = ({ if (!isOpen) return null; - return ( -