From d79cad4c52cc637eb06602a3af3cd669befae58d Mon Sep 17 00:00:00 2001 From: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com> Date: Fri, 9 May 2025 22:49:54 -0700 Subject: [PATCH] feat(sidebar-controls): added ability to expand on hover (#343) --- .../app/api/workflows/[id]/status/route.ts | 8 +- apps/sim/app/globals.css | 51 +++++---- .../components/control-bar/control-bar.tsx | 1 - .../app/w/[id]/components/toolbar/toolbar.tsx | 20 ++-- apps/sim/app/w/[id]/workflow.tsx | 7 +- .../sidebar-control/sidebar-control.tsx | 102 ++++++++++++++++++ .../workspace-header/workspace-header.tsx | 53 ++++++++- apps/sim/app/w/components/sidebar/sidebar.tsx | 89 +++++++++++---- apps/sim/app/w/logs/logs.tsx | 4 +- apps/sim/lib/usage-monitor.ts | 2 - apps/sim/stores/sidebar/store.ts | 36 +++++-- 11 files changed, 301 insertions(+), 72 deletions(-) create mode 100644 apps/sim/app/w/components/sidebar/components/sidebar-control/sidebar-control.tsx diff --git a/apps/sim/app/api/workflows/[id]/status/route.ts b/apps/sim/app/api/workflows/[id]/status/route.ts index 5222aa670..8b853ee37 100644 --- a/apps/sim/app/api/workflows/[id]/status/route.ts +++ b/apps/sim/app/api/workflows/[id]/status/route.ts @@ -26,13 +26,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ validation.workflow.deployedState as any ) } - - logger.info(`[${requestId}] Retrieved status for workflow: ${id}`, { - isDeployed: validation.workflow.isDeployed, - isPublished: validation.workflow.isPublished, - needsRedeployment, - }) - + return createSuccessResponse({ isDeployed: validation.workflow.isDeployed, deployedAt: validation.workflow.deployedAt, diff --git a/apps/sim/app/globals.css b/apps/sim/app/globals.css index a5964c3fe..f1d16a55f 100644 --- a/apps/sim/app/globals.css +++ b/apps/sim/app/globals.css @@ -140,6 +140,7 @@ /* Custom Animations */ @layer utilities { + /* Animation containment to avoid layout shifts */ .animation-container { contain: paint layout style; @@ -150,9 +151,11 @@ 0% { box-shadow: 0 0 0 0 hsl(var(--border)); } + 50% { box-shadow: 0 0 0 8px hsl(var(--border)); } + 100% { box-shadow: 0 0 0 0 hsl(var(--border)); } @@ -195,6 +198,7 @@ 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } @@ -202,12 +206,11 @@ @keyframes orbit { 0% { - transform: rotate(calc(var(--angle) * 1deg)) translateY(calc(var(--radius) * 1px)) - rotate(calc(var(--angle) * -1deg)); + transform: rotate(calc(var(--angle) * 1deg)) translateY(calc(var(--radius) * 1px)) rotate(calc(var(--angle) * -1deg)); } + 100% { - transform: rotate(calc(var(--angle) * 1deg + 360deg)) translateY(calc(var(--radius) * 1px)) - rotate(calc((var(--angle) * -1deg) - 360deg)); + transform: rotate(calc(var(--angle) * 1deg + 360deg)) translateY(calc(var(--radius) * 1px)) rotate(calc((var(--angle) * -1deg) - 360deg)); } } @@ -215,14 +218,17 @@ from { transform: translateX(0); } + to { transform: translateX(calc(-100% - var(--gap))); } } + @keyframes marquee-vertical { from { transform: translateY(0); } + to { transform: translateY(calc(-100% - var(--gap))); } @@ -234,30 +240,27 @@ .streaming-effect::after { content: ''; - @apply pointer-events-none absolute top-0 left-0 h-full w-full; - background: linear-gradient( - 90deg, - rgba(128, 128, 128, 0) 0%, - rgba(128, 128, 128, 0.1) 50%, - rgba(128, 128, 128, 0) 100% - ); + @apply pointer-events-none absolute left-0 top-0 h-full w-full; + background: linear-gradient(90deg, + rgba(128, 128, 128, 0) 0%, + rgba(128, 128, 128, 0.1) 50%, + rgba(128, 128, 128, 0) 100%); animation: code-shimmer 1.5s infinite; z-index: 10; } .dark .streaming-effect::after { - background: linear-gradient( - 90deg, - rgba(180, 180, 180, 0) 0%, - rgba(180, 180, 180, 0.1) 50%, - rgba(180, 180, 180, 0) 100% - ); + background: linear-gradient(90deg, + rgba(180, 180, 180, 0) 0%, + rgba(180, 180, 180, 0.1) 50%, + rgba(180, 180, 180, 0) 100%); } @keyframes fadeIn { from { opacity: 0; } + to { opacity: 1; } @@ -270,8 +273,10 @@ /* Dark mode error badge styling */ .dark .error-badge { - background-color: hsl(0, 70%, 20%) !important; /* Darker red background for dark mode */ - color: hsl(0, 0%, 100%) !important; /* Pure white text for better contrast */ + background-color: hsl(0, 70%, 20%) !important; + /* Darker red background for dark mode */ + color: hsl(0, 0%, 100%) !important; + /* Pure white text for better contrast */ } /* Input Overrides */ @@ -293,10 +298,12 @@ input[type='search']::-ms-clear { /* Code Prompt Bar Placeholder Animation */ @keyframes placeholder-pulse { + 0%, 100% { opacity: 0.5; } + 50% { opacity: 0.8; } @@ -319,3 +326,9 @@ input[type='search']::-ms-clear { .font-geist-mono { font-family: var(--font-geist-mono); } + +/* Sidebar overlay styles */ +.main-content-overlay { + z-index: 40; + /* Higher z-index to appear above content */ +} \ No newline at end of file diff --git a/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx b/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx index cd2045c7d..9cf1e03d3 100644 --- a/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx @@ -76,7 +76,6 @@ const RUN_COUNT_OPTIONS = [1, 5, 10, 25, 50, 100] export function ControlBar() { const router = useRouter() const { data: session } = useSession() - const { isCollapsed: isSidebarCollapsed } = useSidebarStore() // Store hooks const { diff --git a/apps/sim/app/w/[id]/components/toolbar/toolbar.tsx b/apps/sim/app/w/[id]/components/toolbar/toolbar.tsx index 349efe854..703675353 100644 --- a/apps/sim/app/w/[id]/components/toolbar/toolbar.tsx +++ b/apps/sim/app/w/[id]/components/toolbar/toolbar.tsx @@ -14,8 +14,13 @@ import { ToolbarTabs } from './components/toolbar-tabs/toolbar-tabs' export function Toolbar() { const [activeTab, setActiveTab] = useState('blocks') const [searchQuery, setSearchQuery] = useState('') - const [isCollapsed, setIsCollapsed] = useState(false) - const { isCollapsed: isSidebarCollapsed } = useSidebarStore() + const { mode, isExpanded } = useSidebarStore() + // In hover mode, act as if sidebar is always collapsed for layout purposes + const isSidebarCollapsed = + mode === 'expanded' ? !isExpanded : mode === 'collapsed' || mode === 'hover' + + // State to track if toolbar is open - independent of sidebar state + const [isToolbarOpen, setIsToolbarOpen] = useState(true) const blocks = useMemo(() => { const filteredBlocks = !searchQuery.trim() ? getBlocksByCategory(activeTab) : getAllBlocks() @@ -31,13 +36,14 @@ export function Toolbar() { }) }, [searchQuery, activeTab]) - if (isCollapsed) { + // Show toolbar button when it's closed, regardless of sidebar state + if (!isToolbarOpen) { return ( + + +
+

Sidebar control

+
+
+
+ + + +
+
+
+ + ) +} diff --git a/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx b/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx index fb6994a71..9771f404a 100644 --- a/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx +++ b/apps/sim/app/w/components/sidebar/components/workspace-header/workspace-header.tsx @@ -39,6 +39,7 @@ import { Skeleton } from '@/components/ui/skeleton' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useSession } from '@/lib/auth-client' import { cn } from '@/lib/utils' +import { useSidebarStore } from '@/stores/sidebar/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' interface Workspace { @@ -51,6 +52,7 @@ interface Workspace { interface WorkspaceHeaderProps { onCreateWorkflow: () => void isCollapsed?: boolean + onDropdownOpenChange?: (isOpen: boolean) => void } // New WorkspaceModal component @@ -225,8 +227,16 @@ function WorkspaceEditModal({ ) } -export function WorkspaceHeader({ onCreateWorkflow, isCollapsed }: WorkspaceHeaderProps) { - const [isOpen, setIsOpen] = useState(false) +export function WorkspaceHeader({ + onCreateWorkflow, + isCollapsed, + onDropdownOpenChange, +}: WorkspaceHeaderProps) { + // Get sidebar store state to check current mode + const { mode, workspaceDropdownOpen, setAnyModalOpen } = useSidebarStore() + + // Keep local isOpen state in sync with the store (for internal component use) + const [isOpen, setIsOpen] = useState(workspaceDropdownOpen) const { data: sessionData, isPending } = useSession() const [plan, setPlan] = useState('Free Plan') // Use client-side loading instead of isPending to avoid hydration mismatch @@ -419,6 +429,32 @@ export function WorkspaceHeader({ onCreateWorkflow, isCollapsed }: WorkspaceHead // Determine URL for workspace links const workspaceUrl = activeWorkspace ? `/w/${activeWorkspace.id}` : '/w' + // Notify parent component when dropdown opens/closes + const handleDropdownOpenChange = (open: boolean) => { + setIsOpen(open) + // Inform the parent component about the dropdown state change + if (onDropdownOpenChange) { + onDropdownOpenChange(open) + } + } + + // Special handling for click interactions in hover mode + const handleTriggerClick = (e: React.MouseEvent) => { + // When in hover mode, explicitly prevent bubbling for the trigger + if (mode === 'hover') { + e.stopPropagation() + e.preventDefault() + // Toggle dropdown state + handleDropdownOpenChange(!isOpen) + } + } + + // Handle modal open/close state + useEffect(() => { + // Update the modal state in the store + setAnyModalOpen(isWorkspaceModalOpen || isEditModalOpen || isDeleting) + }, [isWorkspaceModalOpen, isEditModalOpen, isDeleting, setAnyModalOpen]) + return (
{/* Workspace Modal */} @@ -436,9 +472,15 @@ export function WorkspaceHeader({ onCreateWorkflow, isCollapsed }: WorkspaceHead workspace={editingWorkspace} /> - +
{ + // In hover mode, prevent clicks on the container from collapsing the sidebar + if (mode === 'hover') { + e.stopPropagation() + } + }} > {/* Hover background with consistent padding - only when not collapsed */} {!isCollapsed &&
} @@ -456,7 +498,10 @@ export function WorkspaceHeader({ onCreateWorkflow, isCollapsed }: WorkspaceHead ) : (
-
+
{ @@ -56,6 +68,18 @@ export function Sidebar() { } }, [activeWorkspaceId]) + // Update modal state in the store when settings or help modals open/close + useEffect(() => { + setAnyModalOpen(showSettings || showHelp) + }, [showSettings, showHelp, setAnyModalOpen]) + + // Reset explicit mouse enter state when modal state changes + useEffect(() => { + if (isAnyModalOpen) { + setExplicitMouseEnter(false) + } + }, [isAnyModalOpen]) + // Separate regular workflows from temporary marketplace workflows const { regularWorkflows, tempWorkflows } = useMemo(() => { const regular: WorkflowMetadata[] = [] @@ -129,16 +153,49 @@ export function Sidebar() { } } + // Calculate sidebar visibility states + // When in hover mode, sidebar is collapsed until hovered or workspace dropdown is open + // When in expanded/collapsed mode, sidebar follows isExpanded state + const isCollapsed = + mode === 'collapsed' || + (mode === 'hover' && + ((!isHovered && !workspaceDropdownOpen) || isAnyModalOpen || !explicitMouseEnter)) + // Only show overlay effect when in hover mode and actually being hovered or dropdown is open + const showOverlay = + mode === 'hover' && + ((isHovered && !isAnyModalOpen && explicitMouseEnter) || workspaceDropdownOpen) + return (
) : (
- {/* Help button on left */} + {/* Sidebar control on left */} + + + {/* Help button on right */}
setShowHelp(true)} - className="flex items-center justify-center rounded-md px-1 py-1 text-sm font-medium text-muted-foreground hover:bg-accent/50 cursor-pointer" + className="flex items-center justify-center rounded-md w-8 h-8 text-sm font-medium text-muted-foreground hover:bg-accent/50 cursor-pointer" > Help
- - {/* Collapse/Expand button on right */} -
- - Collapse -
)}
diff --git a/apps/sim/app/w/logs/logs.tsx b/apps/sim/app/w/logs/logs.tsx index 2b3d051e0..121ef895e 100644 --- a/apps/sim/app/w/logs/logs.tsx +++ b/apps/sim/app/w/logs/logs.tsx @@ -62,7 +62,9 @@ export default function Logs() { const [selectedLogIndex, setSelectedLogIndex] = useState(-1) const [isSidebarOpen, setIsSidebarOpen] = useState(false) const selectedRowRef = useRef(null) - const { isCollapsed: isSidebarCollapsed } = useSidebarStore() + const { mode, isExpanded } = useSidebarStore() + const isSidebarCollapsed = + mode === 'expanded' ? !isExpanded : mode === 'collapsed' || mode === 'hover' // Group logs by executionId to identify the last log in each group const executionGroups = useMemo(() => { diff --git a/apps/sim/lib/usage-monitor.ts b/apps/sim/lib/usage-monitor.ts index e9dfa9900..9a6d56f7c 100644 --- a/apps/sim/lib/usage-monitor.ts +++ b/apps/sim/lib/usage-monitor.ts @@ -83,8 +83,6 @@ async function getTeamSeats(userId: string): Promise { */ export async function checkUsageStatus(userId: string): Promise { try { - logger.info('Starting usage status check for user', { userId }) - // In development, always return permissive limits if (!isProd) { // Get actual usage from the database for display purposes diff --git a/apps/sim/stores/sidebar/store.ts b/apps/sim/stores/sidebar/store.ts index 3217444a0..4c2517894 100644 --- a/apps/sim/stores/sidebar/store.ts +++ b/apps/sim/stores/sidebar/store.ts @@ -1,21 +1,43 @@ import { create } from 'zustand' import { persist } from 'zustand/middleware' +export type SidebarMode = 'expanded' | 'collapsed' | 'hover' + interface SidebarState { - isCollapsed: boolean - toggleCollapsed: () => void - setCollapsed: (collapsed: boolean) => void + mode: SidebarMode + isExpanded: boolean + // Track workspace dropdown state + workspaceDropdownOpen: boolean + // Track if any modal is open + isAnyModalOpen: boolean + setMode: (mode: SidebarMode) => void + toggleExpanded: () => void + // Control workspace dropdown state + setWorkspaceDropdownOpen: (isOpen: boolean) => void + // Control modal state + setAnyModalOpen: (isOpen: boolean) => void + // Force sidebar expanded state without triggering loops + forceExpanded: (expanded: boolean) => void } export const useSidebarStore = create()( persist( (set) => ({ - isCollapsed: false, - toggleCollapsed: () => set((state) => ({ isCollapsed: !state.isCollapsed })), - setCollapsed: (collapsed) => set({ isCollapsed: collapsed }), + mode: 'expanded', // Default to expanded mode + isExpanded: true, // Default to expanded state + workspaceDropdownOpen: false, // Track if workspace dropdown is open + isAnyModalOpen: false, // Track if any modal is open + setMode: (mode) => set({ mode }), + toggleExpanded: () => set((state) => ({ isExpanded: !state.isExpanded })), + // Only update dropdown state without changing isExpanded + setWorkspaceDropdownOpen: (isOpen) => set({ workspaceDropdownOpen: isOpen }), + // Update modal state + setAnyModalOpen: (isOpen) => set({ isAnyModalOpen: isOpen }), + // Separate function to control expanded state + forceExpanded: (expanded) => set({ isExpanded: expanded }), }), { name: 'sidebar-state', } ) -) +) \ No newline at end of file