Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/sim/app/_shell/providers/tooltip-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client'

import { Tooltip } from '@/components/emcn'

interface TooltipProviderProps {
children: React.ReactNode
}

export function TooltipProvider({ children }: TooltipProviderProps) {
return <Tooltip.Provider>{children}</Tooltip.Provider>
}
19 changes: 19 additions & 0 deletions apps/sim/app/_styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,25 @@
pointer-events: none !important;
}

/**
* Workflow canvas cursor styles
* Override React Flow's default selection cursor based on canvas mode
*/
.workflow-container.canvas-mode-cursor .react-flow__pane,
.workflow-container.canvas-mode-cursor .react-flow__selectionpane {
cursor: default !important;
}

.workflow-container.canvas-mode-hand .react-flow__pane,
.workflow-container.canvas-mode-hand .react-flow__selectionpane {
cursor: grab !important;
}

.workflow-container.canvas-mode-hand .react-flow__pane:active,
.workflow-container.canvas-mode-hand .react-flow__selectionpane:active {
cursor: grabbing !important;
}

/**
* Selected node ring indicator
* Uses a pseudo-element overlay to match the original behavior (absolute inset-0 z-40)
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/app/api/users/me/settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const SettingsSchema = z.object({
superUserModeEnabled: z.boolean().optional(),
errorNotificationsEnabled: z.boolean().optional(),
snapToGridSize: z.number().min(0).max(50).optional(),
showActionBar: z.boolean().optional(),
})

const defaultSettings = {
Expand All @@ -39,6 +40,7 @@ const defaultSettings = {
superUserModeEnabled: false,
errorNotificationsEnabled: true,
snapToGridSize: 0,
showActionBar: true,
}

export async function GET() {
Expand Down Expand Up @@ -73,6 +75,7 @@ export async function GET() {
superUserModeEnabled: userSettings.superUserModeEnabled ?? true,
errorNotificationsEnabled: userSettings.errorNotificationsEnabled ?? true,
snapToGridSize: userSettings.snapToGridSize ?? 0,
showActionBar: userSettings.showActionBar ?? true,
},
},
{ status: 200 }
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { HydrationErrorHandler } from '@/app/_shell/hydration-error-handler'
import { QueryProvider } from '@/app/_shell/providers/query-provider'
import { SessionProvider } from '@/app/_shell/providers/session-provider'
import { ThemeProvider } from '@/app/_shell/providers/theme-provider'
import { TooltipProvider } from '@/app/_shell/providers/tooltip-provider'
import { season } from '@/app/_styles/fonts/season/season'

export const viewport: Viewport = {
Expand Down Expand Up @@ -194,7 +195,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<ThemeProvider>
<QueryProvider>
<SessionProvider>
<BrandedLayout>{children}</BrandedLayout>
<TooltipProvider>
<BrandedLayout>{children}</BrandedLayout>
</TooltipProvider>
</SessionProvider>
</QueryProvider>
</ThemeProvider>
Expand Down
6 changes: 6 additions & 0 deletions apps/sim/app/playground/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ import {
Combobox,
Connections,
Copy,
Cursor,
DatePicker,
DocumentAttachment,
Duplicate,
Expand,
Eye,
FolderCode,
FolderPlus,
Hand,
HexSimple,
Input,
Key as KeyIcon,
Expand Down Expand Up @@ -979,11 +982,14 @@ export default function PlaygroundPage() {
{ Icon: ChevronDown, name: 'ChevronDown' },
{ Icon: Connections, name: 'Connections' },
{ Icon: Copy, name: 'Copy' },
{ Icon: Cursor, name: 'Cursor' },
{ Icon: DocumentAttachment, name: 'DocumentAttachment' },
{ Icon: Duplicate, name: 'Duplicate' },
{ Icon: Expand, name: 'Expand' },
{ Icon: Eye, name: 'Eye' },
{ Icon: FolderCode, name: 'FolderCode' },
{ Icon: FolderPlus, name: 'FolderPlus' },
{ Icon: Hand, name: 'Hand' },
{ Icon: HexSimple, name: 'HexSimple' },
{ Icon: KeyIcon, name: 'Key' },
{ Icon: Layout, name: 'Layout' },
Expand Down
11 changes: 4 additions & 7 deletions apps/sim/app/templates/layout-client.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
'use client'

import { Tooltip } from '@/components/emcn'
import { season } from '@/app/_styles/fonts/season/season'

export default function TemplatesLayoutClient({ children }: { children: React.ReactNode }) {
return (
<Tooltip.Provider delayDuration={600} skipDelayDuration={0}>
<div className={`${season.variable} relative flex min-h-screen flex-col font-season`}>
<div className='-z-50 pointer-events-none fixed inset-0 bg-white' />
{children}
</div>
</Tooltip.Provider>
<div className={`${season.variable} relative flex min-h-screen flex-col font-season`}>
<div className='-z-50 pointer-events-none fixed inset-0 bg-white' />
{children}
</div>
)
}
19 changes: 8 additions & 11 deletions apps/sim/app/workspace/[workspaceId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client'

import { Tooltip } from '@/components/emcn'
import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
import { ProviderModelsLoader } from '@/app/workspace/[workspaceId]/providers/provider-models-loader'
import { SettingsLoader } from '@/app/workspace/[workspaceId]/providers/settings-loader'
Expand All @@ -13,16 +12,14 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
<SettingsLoader />
<ProviderModelsLoader />
<GlobalCommandsProvider>
<Tooltip.Provider delayDuration={600} skipDelayDuration={0}>
<div className='flex h-screen w-full bg-[var(--bg)]'>
<WorkspacePermissionsProvider>
<div className='shrink-0' suppressHydrationWarning>
<Sidebar />
</div>
{children}
</WorkspacePermissionsProvider>
</div>
</Tooltip.Provider>
<div className='flex h-screen w-full bg-[var(--bg)]'>
<WorkspacePermissionsProvider>
<div className='shrink-0' suppressHydrationWarning>
<Sidebar />
</div>
{children}
</WorkspacePermissionsProvider>
</div>
</GlobalCommandsProvider>
</>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
'use client'

import { useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { useReactFlow } from 'reactflow'
import {
Button,
ChevronDown,
Cursor,
Expand,
Hand,
Popover,
PopoverAnchor,
PopoverContent,
PopoverItem,
PopoverTrigger,
Redo,
Tooltip,
Undo,
ZoomIn,
ZoomOut,
} from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client'
import { useUpdateGeneralSetting } from '@/hooks/queries/general-settings'
import { useCanvasViewport } from '@/hooks/use-canvas-viewport'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useCanvasModeStore } from '@/stores/canvas-mode'
import { useGeneralStore } from '@/stores/settings/general'
import { useUndoRedoStore } from '@/stores/undo-redo'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'

const logger = createLogger('ActionBar')

export function ActionBar() {
const reactFlowInstance = useReactFlow()
const { zoomIn, zoomOut } = reactFlowInstance
const { fitViewToBounds } = useCanvasViewport(reactFlowInstance)
const { mode, setMode } = useCanvasModeStore()
const { undo, redo } = useCollaborativeWorkflow()
const showActionBar = useGeneralStore((s) => s.showActionBar)
const updateSetting = useUpdateGeneralSetting()

const { activeWorkflowId } = useWorkflowRegistry()
const { data: session } = useSession()
const userId = session?.user?.id || 'unknown'
const stacks = useUndoRedoStore((s) => s.stacks)
const key = activeWorkflowId && userId ? `${activeWorkflowId}:${userId}` : ''
const stack = (key && stacks[key]) || { undo: [], redo: [] }
const canUndo = stack.undo.length > 0
const canRedo = stack.redo.length > 0

const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null)
const [isCanvasModeOpen, setIsCanvasModeOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)

const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault()
setContextMenu({ x: e.clientX, y: e.clientY })
}

const handleHide = async () => {
try {
await updateSetting.mutateAsync({ key: 'showActionBar', value: false })
} catch (error) {
logger.error('Failed to hide action bar', error)
} finally {
setContextMenu(null)
}
}

if (!showActionBar) {
return null
}

return (
<>
<div
className='-translate-x-1/2 fixed bottom-[calc(var(--terminal-height)+16px)] left-[calc((100vw+var(--sidebar-width)-var(--panel-width))/2)] z-10 flex h-[36px] items-center gap-[2px] rounded-[8px] border border-[var(--border)] bg-[var(--surface-1)] p-[4px] shadow-sm transition-[left,bottom] duration-100 ease-out'
onContextMenu={handleContextMenu}
>
{/* Canvas Mode Selector */}
<Popover
open={isCanvasModeOpen}
onOpenChange={setIsCanvasModeOpen}
variant='secondary'
size='sm'
>
<PopoverTrigger asChild>
<div className='flex cursor-pointer items-center gap-[4px]'>
<Button className='h-[28px] w-[28px] rounded-[6px] p-0' variant='active'>
{mode === 'hand' ? (
<Hand className='h-[14px] w-[14px]' />
) : (
<Cursor className='h-[14px] w-[14px]' />
)}
</Button>
<Button className='!p-[2px] group' variant='ghost'>
<ChevronDown className='h-[8px] w-[10px] text-[var(--text-muted)] group-hover:text-[var(--text-secondary)]' />
</Button>
</div>
</PopoverTrigger>
<PopoverContent align='center' side='top' sideOffset={8} maxWidth={100} minWidth={100}>
<PopoverItem
onClick={() => {
setMode('cursor')
setIsCanvasModeOpen(false)
}}
>
<Cursor className='h-3 w-3' />
<span>Pointer</span>
</PopoverItem>
<PopoverItem
onClick={() => {
setMode('hand')
setIsCanvasModeOpen(false)
}}
>
<Hand className='h-3 w-3' />
<span>Mover</span>
</PopoverItem>
</PopoverContent>
</Popover>

<div className='mx-[4px] h-[20px] w-[1px] bg-[var(--border)]' />

<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
className='h-[28px] w-[28px] rounded-[6px] p-0 hover:bg-[var(--surface-5)]'
onClick={undo}
disabled={!canUndo}
>
<Undo className='h-[16px] w-[16px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<Tooltip.Shortcut keys='⌘Z'>Undo</Tooltip.Shortcut>
</Tooltip.Content>
</Tooltip.Root>

<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
className='h-[28px] w-[28px] rounded-[6px] p-0 hover:bg-[var(--surface-5)]'
onClick={redo}
disabled={!canRedo}
>
<Redo className='h-[16px] w-[16px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<Tooltip.Shortcut keys='⌘⇧Z'>Redo</Tooltip.Shortcut>
</Tooltip.Content>
</Tooltip.Root>

<div className='mx-[4px] h-[20px] w-[1px] bg-[var(--border)]' />

<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
className='h-[28px] w-[28px] rounded-[6px] p-0 hover:bg-[var(--surface-5)]'
onClick={() => zoomOut()}
>
<ZoomOut className='h-[16px] w-[16px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>Zoom out</Tooltip.Content>
</Tooltip.Root>

<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
className='h-[28px] w-[28px] rounded-[6px] p-0 hover:bg-[var(--surface-5)]'
onClick={() => zoomIn()}
>
<ZoomIn className='h-[16px] w-[16px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>Zoom in</Tooltip.Content>
</Tooltip.Root>

<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
className='h-[28px] w-[28px] rounded-[6px] p-0 hover:bg-[var(--surface-5)]'
onClick={() => fitViewToBounds({ padding: 0.1, duration: 300 })}
>
<Expand className='h-[16px] w-[16px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>Zoom to fit</Tooltip.Content>
</Tooltip.Root>
</div>

<Popover
open={contextMenu !== null}
onOpenChange={(open) => !open && setContextMenu(null)}
variant='secondary'
size='sm'
colorScheme='inverted'
>
<PopoverAnchor
style={{
position: 'fixed',
left: `${contextMenu?.x ?? 0}px`,
top: `${contextMenu?.y ?? 0}px`,
width: '1px',
height: '1px',
}}
/>
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
<PopoverItem onClick={handleHide}>Hide canvas controls</PopoverItem>
</PopoverContent>
</Popover>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ActionBar } from './action-bar'
Loading