diff --git a/src/components/modals/CardDetailModal.tsx b/src/components/modals/CardDetailModal.tsx index 170d3264..6c1f33d5 100644 --- a/src/components/modals/CardDetailModal.tsx +++ b/src/components/modals/CardDetailModal.tsx @@ -40,14 +40,13 @@ export function CardDetailModal({ // Since WorkspaceCard now subscribes directly to its own isSelected state, // changing the selection will only re-render the affected card, not all cards const toggleCardSelection = useUIStore((state) => state.toggleCardSelection); - const selectedCardIds = useUIStore((state) => state.selectedCardIds); - // Auto-select card when modal opens + // Auto-select card when modal opens (read selection state imperatively to avoid infinite loops) useEffect(() => { if (!isOpen || !item?.id) return; // Check if card was already selected at the time of opening - const wasAlreadySelected = selectedCardIds.has(item.id); + const wasAlreadySelected = useUIStore.getState().selectedCardIds.has(item.id); // If not already selected, select it now if (!wasAlreadySelected) { @@ -62,8 +61,6 @@ export function CardDetailModal({ return undefined; }, [isOpen, item?.id]); // eslint-disable-line react-hooks/exhaustive-deps - // eslint-disable-next-line react-hooks/exhaustive-deps - // Handle escape key const handleEscape = useCallback( (e: KeyboardEvent) => { diff --git a/src/components/modals/PDFViewerModal.tsx b/src/components/modals/PDFViewerModal.tsx index e523ebc7..acbfb338 100644 --- a/src/components/modals/PDFViewerModal.tsx +++ b/src/components/modals/PDFViewerModal.tsx @@ -1,13 +1,12 @@ "use client"; import { X } from "lucide-react"; -import { useEffect, useMemo } from "react"; +import { useEffect } from "react"; import ItemHeader from "@/components/workspace-canvas/ItemHeader"; import SpotlightModal from "@/components/SpotlightModal"; import { getCardColorCSS, getCardAccentColor, getWhiteTintedColor } from "@/lib/workspace-state/colors"; import type { Item, PdfData } from "@/lib/workspace-state/types"; -import { useUIStore, selectSelectedCardIdsArray } from "@/lib/stores/ui-store"; -import { useShallow } from "zustand/react/shallow"; +import { useUIStore } from "@/lib/stores/ui-store"; import { formatKeyboardShortcut } from "@/lib/utils/keyboard-shortcut"; import { ItemPanelContent } from "@/components/workspace-canvas/ItemPanelContent"; @@ -33,19 +32,12 @@ export function PDFViewerModal({ const setIsChatExpanded = useUIStore((state) => state.setIsChatExpanded); const toggleCardSelection = useUIStore((state) => state.toggleCardSelection); - // Use array selector with shallow comparison to prevent unnecessary re-renders and SSR issues - const selectedCardIdsArray = useUIStore( - useShallow(selectSelectedCardIdsArray) - ); - const selectedCardIds = useMemo(() => new Set(selectedCardIdsArray), [selectedCardIdsArray]); - - // Track whether we selected the card (so we know whether to deselect on cleanup) + // Auto-select card when modal opens (read selection state imperatively to avoid infinite loops) useEffect(() => { - // Only run when modal is open and we have an item if (!isOpen || !item?.id) return; // Check if card was already selected at the time of opening - const wasAlreadySelected = selectedCardIds.has(item.id); + const wasAlreadySelected = useUIStore.getState().selectedCardIds.has(item.id); // If not already selected, select it now (adds it to context) if (!wasAlreadySelected) { @@ -59,7 +51,7 @@ export function PDFViewerModal({ // If it was already selected, don't change anything on cleanup return undefined; - }, [isOpen, item?.id, selectedCardIds, toggleCardSelection]); + }, [isOpen, item?.id]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { const handleEscape = (e: KeyboardEvent) => { diff --git a/src/components/workspace-canvas/FolderCard.tsx b/src/components/workspace-canvas/FolderCard.tsx index dbc8f377..8ec7c852 100644 --- a/src/components/workspace-canvas/FolderCard.tsx +++ b/src/components/workspace-canvas/FolderCard.tsx @@ -85,7 +85,6 @@ function FolderCardComponent({ const [isDragHover, setIsDragHover] = useState(false); const [selectedCount, setSelectedCount] = useState(null); const [isEditingTitle, setIsEditingTitle] = useState(false); - const [shouldAutoFocus, setShouldAutoFocus] = useState(false); // Subscribe directly to this folder's selection state from the store const isSelected = useUIStore( @@ -103,19 +102,8 @@ function FolderCardComponent({ const folderColor = item.color || "#6366F1"; // Default to indigo - // Auto-focus and scroll into view for newly created folders (name is "New Folder") - useEffect(() => { - if (item.name === "New Folder") { - setShouldAutoFocus(true); - // Scroll the folder card into view - const element = document.getElementById(`item-${item.id}`); - if (element) { - setTimeout(() => { - element.scrollIntoView({ behavior: 'smooth', block: 'center' }); - }, 100); - } - } - }, [item.id, item.name]); + // Note: Auto-focus removed - user must click to edit title + // Scroll behavior is handled by useReactiveNavigation hook // Listen for drag hover events useEffect(() => { @@ -406,19 +394,13 @@ function FolderCardComponent({ subtitle="" description="" onNameChange={handleNameChange} - onNameCommit={(value) => { - handleNameCommit(value); - // Clear auto-focus after first commit - if (shouldAutoFocus) { - setShouldAutoFocus(false); - } - }} + onNameCommit={handleNameCommit} onSubtitleChange={() => { }} onTitleFocus={() => setIsEditingTitle(true)} onTitleBlur={() => setIsEditingTitle(false)} readOnly={false} noMargin={true} - autoFocus={shouldAutoFocus} + autoFocus={false} /> {/* Item count as subtext */}

diff --git a/src/components/workspace-canvas/SelectionActionBar.tsx b/src/components/workspace-canvas/SelectionActionBar.tsx index 71b31440..edc665de 100644 --- a/src/components/workspace-canvas/SelectionActionBar.tsx +++ b/src/components/workspace-canvas/SelectionActionBar.tsx @@ -23,8 +23,8 @@ export default function SelectionActionBar({ return (

{/* Selection count */} - + {isCompactMode ? ( -
- + + {selectedCount} -
+
) : ( `${selectedCount} ${selectedCount === 1 ? 'item' : 'items'} selected` )}
{/* Separator */} -
+
{/* New Folder Button */} @@ -53,7 +53,7 @@ export default function SelectionActionBar({ type="button" onClick={onCreateFolderFromSelection} className={cn( - "inline-flex items-center gap-2 px-2 py-2 rounded-md", + "inline-flex items-center gap-2 px-2 py-2 rounded-md shrink-0", "text-sm font-medium text-amber-400", "bg-amber-500/10 border border-amber-500/20", "hover:bg-amber-500/20 hover:border-amber-500/30", @@ -75,7 +75,7 @@ export default function SelectionActionBar({ type="button" onClick={onMoveSelected} className={cn( - "inline-flex items-center gap-2 px-2 py-2 rounded-md", + "inline-flex items-center gap-2 px-2 py-2 rounded-md shrink-0", "text-sm font-medium text-blue-400", "bg-blue-500/10 border border-blue-500/20", "hover:bg-blue-500/20 hover:border-blue-500/30", @@ -97,7 +97,7 @@ export default function SelectionActionBar({ type="button" onClick={onDeleteSelected} className={cn( - "inline-flex items-center gap-2 px-2 py-2 rounded-md", + "inline-flex items-center gap-2 px-2 py-2 rounded-md shrink-0", "text-sm font-medium text-red-400", "bg-red-500/10 border border-red-500/20", "hover:bg-red-500/20 hover:border-red-500/30", @@ -113,7 +113,7 @@ export default function SelectionActionBar({ {/* Separator before Clear */} -
+
{/* Clear Selection Button */} @@ -122,7 +122,7 @@ export default function SelectionActionBar({ type="button" onClick={onClearSelection} className={cn( - "inline-flex items-center justify-center p-2 rounded-md", + "inline-flex items-center justify-center p-2 rounded-md shrink-0", "text-foreground/60 dark:text-white/60", "hover:text-foreground/90 hover:bg-foreground/5 dark:hover:text-white/90 dark:hover:bg-white/5", "transition-all duration-200" diff --git a/src/components/workspace-canvas/WorkspaceGrid.tsx b/src/components/workspace-canvas/WorkspaceGrid.tsx index 57fa7a66..53cc855a 100644 --- a/src/components/workspace-canvas/WorkspaceGrid.tsx +++ b/src/components/workspace-canvas/WorkspaceGrid.tsx @@ -859,6 +859,7 @@ export function WorkspaceGrid({ `} {mounted && ( {workspaceSplitViewActive ? ( @@ -833,7 +833,7 @@ export default function WorkspaceHeader({ - {workspaceSplitViewActive ? "Focus on this item" : "Split View"} + {workspaceSplitViewActive ? "Focus on this item" : "Split"} @@ -962,7 +962,14 @@ export default function WorkspaceHeader({ } } }, - onCreateFolder: () => { if (addItem) addItem("folder"); }, + onCreateFolder: () => { + if (addItem) { + const itemId = addItem("folder"); + if (onItemCreated && itemId) { + onItemCreated([itemId]); + } + } + }, onUpload: () => { setShowUploadDialog(true); setIsNewMenuOpen(false); }, onAudio: () => { openAudioDialog(); setIsNewMenuOpen(false); }, onYouTube: () => { setShowYouTubeDialog(true); setIsNewMenuOpen(false); }, diff --git a/src/components/workspace-canvas/WorkspaceSection.tsx b/src/components/workspace-canvas/WorkspaceSection.tsx index e21ca7a0..79d5b115 100644 --- a/src/components/workspace-canvas/WorkspaceSection.tsx +++ b/src/components/workspace-canvas/WorkspaceSection.tsx @@ -1,4 +1,5 @@ import React, { RefObject, useState, useMemo, useCallback } from "react"; +import { useElementWidth } from "@/hooks/use-element-width"; import { useSearchParams } from "next/navigation"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; @@ -164,6 +165,9 @@ export function WorkspaceSection({ const setSelectedActions = useUIStore((state) => state.setSelectedActions); const { data: session } = useSession(); + // Measure container width for responsive SelectionActionBar + const containerWidth = useElementWidth(scrollAreaRef); + // Assistant API for Deep Research action // Note: WorkspaceSection is inside WorkspaceRuntimeProvider in DashboardLayout, so this hook works const aui = useAui(); @@ -416,7 +420,8 @@ export function WorkspaceSection({ // Clear the selection clearCardSelection(); - // Note: FolderCard auto-focuses the title when name is "New Folder" + // Navigate to the newly created folder + handleCreatedItems([folderId]); }; // Handle PDF upload from BottomActionBar @@ -645,12 +650,15 @@ export function WorkspaceSection({ onCreateNote: () => { if (addItem) { const itemId = addItem("note"); - if (handleCreatedItems && itemId) { - handleCreatedItems([itemId]); - } + handleCreatedItems([itemId]); + } + }, + onCreateFolder: () => { + if (addItem) { + const itemId = addItem("folder"); + handleCreatedItems([itemId]); } }, - onCreateFolder: () => { if (addItem) addItem("folder"); }, onUpload: () => handleUploadMenuItemClick(), onAudio: () => openAudioDialog(), onYouTube: () => setShowYouTubeDialog(true), @@ -658,9 +666,7 @@ export function WorkspaceSection({ onFlashcards: () => { if (addItem) { const itemId = addItem("flashcard"); - if (handleCreatedItems && itemId) { - handleCreatedItems([itemId]); - } + handleCreatedItems([itemId]); } }, onQuiz: () => { @@ -690,7 +696,7 @@ export function WorkspaceSection({ onDeleteSelected={handleDeleteRequest} onCreateFolderFromSelection={handleCreateFolderFromSelection} onMoveSelected={handleMoveSelected} - isCompactMode={isItemPanelOpen && isChatExpanded} + isCompactMode={containerWidth !== undefined && containerWidth < 400} /> )} {/* Move To Dialog */} diff --git a/src/hooks/ui/use-reactive-navigation.ts b/src/hooks/ui/use-reactive-navigation.ts index 7a8caf5f..154cd503 100644 --- a/src/hooks/ui/use-reactive-navigation.ts +++ b/src/hooks/ui/use-reactive-navigation.ts @@ -1,28 +1,23 @@ import { useState, useEffect, useCallback } from "react"; -import { useUIStore } from "@/lib/stores/ui-store"; import { useNavigateToItem } from "./use-navigate-to-item"; import type { AgentState } from "@/lib/workspace-state/types"; /** - * Hook to handle navigation and selection after item creation. + * Hook to handle navigation after item creation. * It waits for the item to appear in the workspace state before attempting to scroll to it, * solving race conditions and stale closure issues. */ export function useReactiveNavigation(workspaceState: AgentState) { const [pendingNavigationId, setPendingNavigationId] = useState(null); const navigateToItem = useNavigateToItem(); - const selectMultipleCards = useUIStore((state) => state.selectMultipleCards); const handleCreatedItems = useCallback((createdIds: string[]) => { - // Select the newly created items - selectMultipleCards(createdIds); - // Set pending navigation to trigger in useEffect once item is available in state if (createdIds.length > 0) { setPendingNavigationId(createdIds[0]); } - }, [selectMultipleCards]); + }, []); // Effect to handle navigation once item appears in state useEffect(() => { diff --git a/src/lib/stores/ui-store.ts b/src/lib/stores/ui-store.ts index f959a6e4..1d20f05c 100644 --- a/src/lib/stores/ui-store.ts +++ b/src/lib/stores/ui-store.ts @@ -201,6 +201,7 @@ export const useUIStore = create()( activeFolderId: folderId, openPanelIds: [], maximizedItemId: null, + workspaceSplitViewActive: false, selectedCardIds: newSelectedCardIds, panelAutoSelectedCardIds: new Set(), }; @@ -217,6 +218,7 @@ export const useUIStore = create()( activeFolderId: null, openPanelIds: [], maximizedItemId: null, + workspaceSplitViewActive: false, selectedCardIds: newSelectedCardIds, panelAutoSelectedCardIds: new Set(), }; @@ -236,6 +238,7 @@ export const useUIStore = create()( return { openPanelIds: [], maximizedItemId: null, + workspaceSplitViewActive: false, selectedCardIds: newSelectedCardIds, panelAutoSelectedCardIds: new Set(), }; @@ -288,6 +291,7 @@ export const useUIStore = create()( return { openPanelIds: [], maximizedItemId: null, + workspaceSplitViewActive: false, selectedCardIds: newSelectedCardIds, panelAutoSelectedCardIds: new Set(), }; @@ -512,6 +516,7 @@ export const useUIStore = create()( openPanelIds: [], itemPrompt: null, maximizedItemId: null, + workspaceSplitViewActive: false, showVersionHistory: false, showCreateWorkspaceModal: false, showSheetModal: false,