Skip to content

Commit 9695810

Browse files
authored
Improvement(ui/ux): signup, command-list, cursors, search modal, workflow runs, usage indicator (#1998)
* improvement: signup loading, command-list, cursors, search modal ordering * improvement: workflow runs, search modal * improvement(usage-indicator): ui/ux
1 parent 6f29e24 commit 9695810

File tree

16 files changed

+756
-332
lines changed

16 files changed

+756
-332
lines changed

apps/sim/app/(auth)/signup/signup-form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ function SignupFormContent({
513513
disabled={isLoading}
514514
>
515515
<span className='flex items-center gap-1'>
516-
{isLoading ? 'Creating account...' : 'Create account'}
516+
{isLoading ? 'Creating account' : 'Create account'}
517517
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
518518
{isButtonHovered ? (
519519
<ArrowRight className='h-4 w-4' aria-hidden='true' />

apps/sim/app/globals.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,30 @@
7474
animation: dash-animation 1.5s linear infinite !important;
7575
}
7676

77+
/**
78+
* Active block ring animation - cycles through gray tones using box-shadow
79+
*/
80+
@keyframes ring-pulse-colors {
81+
0%,
82+
100% {
83+
box-shadow: 0 0 0 4px var(--surface-14);
84+
}
85+
33% {
86+
box-shadow: 0 0 0 4px var(--surface-12);
87+
}
88+
66% {
89+
box-shadow: 0 0 0 4px var(--surface-15);
90+
}
91+
}
92+
93+
.dark .animate-ring-pulse {
94+
animation: ring-pulse-colors 2s ease-in-out infinite !important;
95+
}
96+
97+
.light .animate-ring-pulse {
98+
animation: ring-pulse-colors 2s ease-in-out infinite !important;
99+
}
100+
77101
/**
78102
* Dark color tokens - single source of truth for all colors (dark-only)
79103
*/

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
'use client'
22

3+
import { useCallback } from 'react'
34
import { Layout, LibraryBig, Search } from 'lucide-react'
45
import Image from 'next/image'
6+
import { useParams, useRouter } from 'next/navigation'
57
import { Button } from '@/components/emcn'
68
import { AgentIcon } from '@/components/icons'
9+
import { createLogger } from '@/lib/logs/console/logger'
710
import { cn } from '@/lib/utils'
11+
import { useSearchModalStore } from '@/stores/search-modal/store'
12+
13+
const logger = createLogger('WorkflowCommandList')
814

915
/**
1016
* Command item data structure
@@ -49,13 +55,131 @@ const commands: CommandItem[] = [
4955
* Centered on the screen for empty workflows
5056
*/
5157
export function CommandList() {
58+
const params = useParams()
59+
const router = useRouter()
60+
const { open: openSearchModal } = useSearchModalStore()
61+
62+
const workspaceId = params.workspaceId as string | undefined
63+
64+
/**
65+
* Handle click on a command row.
66+
*
67+
* Mirrors the behavior of the corresponding global keyboard shortcuts:
68+
* - Templates: navigate to workspace templates
69+
* - New Agent: add an agent block to the canvas
70+
* - Logs: navigate to workspace logs
71+
* - Search Blocks: open the universal search modal
72+
*
73+
* @param label - Command label that was clicked.
74+
*/
75+
const handleCommandClick = useCallback(
76+
(label: string) => {
77+
try {
78+
switch (label) {
79+
case 'Templates': {
80+
if (!workspaceId) {
81+
logger.warn('No workspace ID found, cannot navigate to templates from command list')
82+
return
83+
}
84+
router.push(`/workspace/${workspaceId}/templates`)
85+
return
86+
}
87+
case 'New Agent': {
88+
const event = new CustomEvent('add-block-from-toolbar', {
89+
detail: { type: 'agent', enableTriggerMode: false },
90+
})
91+
window.dispatchEvent(event)
92+
return
93+
}
94+
case 'Logs': {
95+
if (!workspaceId) {
96+
logger.warn('No workspace ID found, cannot navigate to logs from command list')
97+
return
98+
}
99+
router.push(`/workspace/${workspaceId}/logs`)
100+
return
101+
}
102+
case 'Search Blocks': {
103+
openSearchModal()
104+
return
105+
}
106+
default:
107+
logger.warn('Unknown command label clicked in command list', { label })
108+
}
109+
} catch (error) {
110+
logger.error('Failed to handle command click in command list', { error, label })
111+
}
112+
},
113+
[router, workspaceId, openSearchModal]
114+
)
115+
116+
/**
117+
* Handle drag-over events from the toolbar.
118+
*
119+
* When a toolbar item is dragged over the command list, mark the drop as valid
120+
* so the browser shows the appropriate drop cursor. Only reacts to toolbar
121+
* drags that carry the expected JSON payload.
122+
*
123+
* @param event - Drag event from the browser.
124+
*/
125+
const handleDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
126+
if (!event.dataTransfer?.types.includes('application/json')) {
127+
return
128+
}
129+
event.preventDefault()
130+
event.dataTransfer.dropEffect = 'move'
131+
}, [])
132+
133+
/**
134+
* Handle drops of toolbar items onto the command list.
135+
*
136+
* This forwards the drop information (block type and cursor position)
137+
* to the workflow canvas via a custom event. The workflow component
138+
* then reuses its existing drop logic to place the block precisely
139+
* under the cursor, including container/subflow handling.
140+
*
141+
* @param event - Drop event from the browser.
142+
*/
143+
const handleDrop = useCallback((event: React.DragEvent<HTMLDivElement>) => {
144+
if (!event.dataTransfer?.types.includes('application/json')) {
145+
return
146+
}
147+
148+
event.preventDefault()
149+
150+
try {
151+
const raw = event.dataTransfer.getData('application/json')
152+
if (!raw) return
153+
154+
const data = JSON.parse(raw) as { type?: string; enableTriggerMode?: boolean }
155+
if (!data?.type || data.type === 'connectionBlock') return
156+
157+
const overlayDropEvent = new CustomEvent('toolbar-drop-on-empty-workflow-overlay', {
158+
detail: {
159+
type: data.type,
160+
enableTriggerMode: data.enableTriggerMode ?? false,
161+
clientX: event.clientX,
162+
clientY: event.clientY,
163+
},
164+
})
165+
166+
window.dispatchEvent(overlayDropEvent)
167+
} catch (error) {
168+
logger.error('Failed to handle drop on command list', { error })
169+
}
170+
}, [])
171+
52172
return (
53173
<div
54174
className={cn(
55175
'pointer-events-none absolute inset-0 mb-[50px] flex items-center justify-center'
56176
)}
57177
>
58-
<div className='pointer-events-none flex flex-col gap-[8px]'>
178+
<div
179+
className='pointer-events-auto flex flex-col gap-[8px]'
180+
onDragOver={handleDragOver}
181+
onDrop={handleDrop}
182+
>
59183
{/* Logo */}
60184
<div className='mb-[20px] flex justify-center'>
61185
<Image
@@ -79,6 +203,7 @@ export function CommandList() {
79203
<div
80204
key={command.label}
81205
className='group flex cursor-pointer items-center justify-between gap-[60px]'
206+
onClick={() => handleCommandClick(command.label)}
82207
>
83208
{/* Left side: Icon and Label */}
84209
<div className='flex items-center gap-[8px]'>
@@ -91,15 +216,15 @@ export function CommandList() {
91216
{/* Right side: Keyboard Shortcut */}
92217
<div className='flex items-center gap-[4px]'>
93218
<Button
94-
className='group-hover:-translate-y-0.5 w-[26px] py-[3px] text-[12px] hover:translate-y-0 hover:text-[var(--text-tertiary)] hover:shadow-[0_2px_0_0] group-hover:text-[var(--text-primary)] group-hover:shadow-[0_4px_0_0]'
219+
className='group-hover:-translate-y-0.5 w-[26px] py-[3px] text-[12px] hover:translate-y-0 hover:text-[var(--text-tertiary)] hover:shadow-[0_2px_0_0_rgba(48,48,48,1)] group-hover:text-[var(--text-primary)] group-hover:shadow-[0_4px_0_0_rgba(48,48,48,1)]'
95220
variant='3d'
96221
>
97222
<span></span>
98223
</Button>
99224
{shortcuts.map((key, index) => (
100225
<Button
101226
key={index}
102-
className='group-hover:-translate-y-0.5 w-[26px] py-[3px] text-[12px] hover:translate-y-0 hover:text-[var(--text-tertiary)] hover:shadow-[0_2px_0_0] group-hover:text-[var(--text-primary)] group-hover:shadow-[0_4px_0_0]'
227+
className='group-hover:-translate-y-0.5 w-[26px] py-[3px] text-[12px] hover:translate-y-0 hover:text-[var(--text-tertiary)] hover:shadow-[0_2px_0_0_rgba(48,48,48,1)] group-hover:text-[var(--text-primary)] group-hover:shadow-[0_4px_0_0_rgba(48,48,48,1)]'
103228
variant='3d'
104229
>
105230
{key}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/cursors/cursors.tsx

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,6 @@ interface CursorRenderData {
1818
color: string
1919
}
2020

21-
const POINTER_OFFSET = {
22-
x: 0,
23-
y: 0,
24-
}
25-
2621
const CursorsComponent = () => {
2722
const { presenceUsers } = useSocket()
2823
const viewport = useViewport()
@@ -60,23 +55,15 @@ const CursorsComponent = () => {
6055
transition: 'transform 0.12s ease-out',
6156
}}
6257
>
63-
<div
64-
className='relative'
65-
style={{ transform: `translate(${-POINTER_OFFSET.x}px, ${-POINTER_OFFSET.y}px)` }}
66-
>
67-
{/* Simple cursor pointer */}
68-
<svg width={16} height={18} viewBox='0 0 16 18' fill='none'>
69-
<path
70-
d='M0.5 0.5L0.5 12L4 9L6.5 15L8.5 14L6 8L12 8L0.5 0.5Z'
71-
fill={color}
72-
stroke='rgba(0,0,0,0.3)'
73-
strokeWidth={1}
74-
/>
58+
<div className='relative flex items-start'>
59+
{/* Filled mouse pointer cursor */}
60+
<svg className='-mt-[18px]' width={24} height={24} viewBox='0 0 24 24' fill={color}>
61+
<path d='M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z' />
7562
</svg>
7663

77-
{/* Name tag underneath and to the right */}
64+
{/* Name tag to the right, background tightly wrapping text */}
7865
<div
79-
className='absolute top-[18px] left-[4px] h-[21px] w-[140px] truncate whitespace-nowrap rounded-[2px] p-[6px] font-medium text-[11px] text-[var(--surface-1)]'
66+
className='ml-[-4px] inline-flex max-w-[160px] truncate whitespace-nowrap rounded-[2px] px-1.5 py-[2px] font-medium text-[11px] text-[var(--surface-1)]'
8067
style={{ backgroundColor: color }}
8168
>
8269
{name}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-core.ts

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useCallback, useMemo } from 'react'
22
import { cn } from '@/lib/utils'
3+
import { useExecutionStore } from '@/stores/execution/store'
34
import { usePanelEditorStore } from '@/stores/panel-new/editor/store'
45
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
56
import { useBlockState } from '../components/workflow-block/hooks'
@@ -28,6 +29,10 @@ export function useBlockCore({ blockId, data, isPending = false }: UseBlockCoreO
2829
data
2930
)
3031

32+
// Run path state (from last execution)
33+
const lastRunPath = useExecutionStore((state) => state.lastRunPath)
34+
const runPathStatus = lastRunPath.get(blockId)
35+
3136
// Focus management
3237
const setCurrentBlockId = usePanelEditorStore((state) => state.setCurrentBlockId)
3338
const currentBlockId = usePanelEditorStore((state) => state.currentBlockId)
@@ -38,27 +43,60 @@ export function useBlockCore({ blockId, data, isPending = false }: UseBlockCoreO
3843
}, [blockId, setCurrentBlockId])
3944

4045
// Ring styling based on all states
46+
// Priority: active (animated) > pending > focused > deleted > diff > run path
4147
const { hasRing, ringStyles } = useMemo(() => {
4248
const hasRing =
4349
isActive ||
4450
isPending ||
4551
isFocused ||
4652
diffStatus === 'new' ||
4753
diffStatus === 'edited' ||
48-
isDeletedBlock
54+
isDeletedBlock ||
55+
!!runPathStatus
4956

5057
const ringStyles = cn(
51-
hasRing && 'ring-[1.75px]',
52-
isActive && 'ring-[#8C10FF] animate-pulse-ring',
53-
isPending && 'ring-[var(--warning)]',
54-
isFocused && 'ring-[var(--brand-secondary)]',
55-
diffStatus === 'new' && 'ring-[#22C55F]',
56-
diffStatus === 'edited' && 'ring-[var(--warning)]',
57-
isDeletedBlock && 'ring-[var(--text-error)]'
58+
// Executing block: animated ring cycling through gray tones (animation handles all styling)
59+
isActive && 'animate-ring-pulse',
60+
// Non-active states use standard ring utilities
61+
!isActive && hasRing && 'ring-[1.75px]',
62+
// Pending state: warning ring
63+
!isActive && isPending && 'ring-[var(--warning)]',
64+
// Focused (selected) state: brand ring
65+
!isActive && !isPending && isFocused && 'ring-[var(--brand-secondary)]',
66+
// Deleted state (highest priority after active/pending/focused)
67+
!isActive && !isPending && !isFocused && isDeletedBlock && 'ring-[var(--text-error)]',
68+
// Diff states
69+
!isActive &&
70+
!isPending &&
71+
!isFocused &&
72+
!isDeletedBlock &&
73+
diffStatus === 'new' &&
74+
'ring-[#22C55E]',
75+
!isActive &&
76+
!isPending &&
77+
!isFocused &&
78+
!isDeletedBlock &&
79+
diffStatus === 'edited' &&
80+
'ring-[var(--warning)]',
81+
// Run path states (lowest priority - only show if no other states active)
82+
!isActive &&
83+
!isPending &&
84+
!isFocused &&
85+
!isDeletedBlock &&
86+
!diffStatus &&
87+
runPathStatus === 'success' &&
88+
'ring-[var(--surface-14)]',
89+
!isActive &&
90+
!isPending &&
91+
!isFocused &&
92+
!isDeletedBlock &&
93+
!diffStatus &&
94+
runPathStatus === 'error' &&
95+
'ring-[var(--text-error)]'
5896
)
5997

6098
return { hasRing, ringStyles }
61-
}, [isActive, isPending, isFocused, diffStatus, isDeletedBlock])
99+
}, [isActive, isPending, isFocused, diffStatus, isDeletedBlock, runPathStatus])
62100

63101
return {
64102
// Workflow context

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export function useWorkflowExecution() {
9999
setExecutor,
100100
setDebugContext,
101101
setActiveBlocks,
102+
setBlockRunStatus,
102103
} = useExecutionStore()
103104
const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null)
104105
const executionStream = useExecutionStream()
@@ -900,6 +901,9 @@ export function useWorkflowExecution() {
900901
// Create a new Set to trigger React re-render
901902
setActiveBlocks(new Set(activeBlocksSet))
902903

904+
// Track successful block execution in run path
905+
setBlockRunStatus(data.blockId, 'success')
906+
903907
// Add to console
904908
addConsole({
905909
input: data.input || {},
@@ -932,6 +936,9 @@ export function useWorkflowExecution() {
932936
// Create a new Set to trigger React re-render
933937
setActiveBlocks(new Set(activeBlocksSet))
934938

939+
// Track failed block execution in run path
940+
setBlockRunStatus(data.blockId, 'error')
941+
935942
// Add error to console
936943
addConsole({
937944
input: data.input || {},

0 commit comments

Comments
 (0)