Skip to content

Commit a7f3ff3

Browse files
committed
feat(sidebar): blocks and triggers ui/ux updated
1 parent 2237700 commit a7f3ff3

File tree

9 files changed

+784
-149
lines changed

9 files changed

+784
-149
lines changed

apps/sim/app/globals.css

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,35 @@
1717
*/
1818
:root {
1919
--sidebar-width: 232px;
20+
--triggers-height: 200px;
21+
--blocks-height: 200px;
2022
}
2123

2224
.sidebar-container {
2325
width: var(--sidebar-width);
2426
}
2527

28+
.triggers-container {
29+
height: var(--triggers-height);
30+
}
31+
32+
.blocks-container {
33+
height: var(--blocks-height);
34+
}
35+
36+
/* Scrollable area constraints for sidebar panels */
37+
.workflows-section {
38+
padding-bottom: var(--triggers-height);
39+
}
40+
41+
.triggers-scrollable {
42+
max-height: calc(var(--triggers-height) - 28px - var(--blocks-height));
43+
}
44+
45+
.blocks-scrollable {
46+
max-height: calc(var(--blocks-height) - 28px);
47+
}
48+
2649
/* ==========================================================================
2750
WORKFLOW COMPONENT Z-INDEX FIXES
2851
========================================================================== */
@@ -233,6 +256,10 @@
233256
overscroll-behavior-x: none;
234257
}
235258

259+
*:focus {
260+
outline: none;
261+
}
262+
236263
body {
237264
@apply bg-background text-foreground;
238265
overscroll-behavior-x: none;
@@ -269,6 +296,15 @@
269296
background-color: hsl(var(--muted-foreground) / 0.3);
270297
}
271298

299+
/* Dark Mode Sidebar Scrollbar - Match search div background */
300+
.dark .sidebar-container ::-webkit-scrollbar-track {
301+
background: #272727;
302+
}
303+
304+
.dark .sidebar-container {
305+
scrollbar-color: hsl(var(--muted-foreground) / 0.3) #272727;
306+
}
307+
272308
/* For Firefox */
273309
* {
274310
scrollbar-width: thin;

apps/sim/app/layout.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
8787
<meta name='format-detection' content='telephone=no' />
8888
<meta httpEquiv='x-ua-compatible' content='ie=edge' />
8989

90-
{/* Blocking script to prevent sidebar width flash on page load */}
90+
{/* Blocking script to prevent sidebar dimensions flash on page load */}
9191
<script
9292
dangerouslySetInnerHTML={{
9393
__html: `
@@ -96,13 +96,31 @@ export default function RootLayout({ children }: { children: React.ReactNode })
9696
var stored = localStorage.getItem('sidebar-state');
9797
if (stored) {
9898
var parsed = JSON.parse(stored);
99-
var width = parsed?.state?.sidebarWidth;
99+
var state = parsed?.state;
100+
101+
// Set sidebar width
102+
var width = state?.sidebarWidth;
100103
if (width >= 232 && width <= 400) {
101104
document.documentElement.style.setProperty('--sidebar-width', width + 'px');
102105
}
106+
107+
// Set triggers height with constraint validation
108+
var triggersHeight = state?.triggersHeight;
109+
var blocksHeight = state?.blocksHeight;
110+
111+
if (blocksHeight !== undefined && blocksHeight >= 28 && blocksHeight <= 500) {
112+
document.documentElement.style.setProperty('--blocks-height', blocksHeight + 'px');
113+
}
114+
115+
if (triggersHeight !== undefined && triggersHeight >= 28 && triggersHeight <= 500) {
116+
// Ensure triggers height respects blocks constraint
117+
var minTriggersHeight = (blocksHeight || 200) + 28;
118+
var validTriggersHeight = Math.max(triggersHeight, minTriggersHeight);
119+
document.documentElement.style.setProperty('--triggers-height', validTriggersHeight + 'px');
120+
}
103121
}
104122
} catch (e) {
105-
// Fallback handled by CSS default
123+
// Fallback handled by CSS defaults
106124
}
107125
})();
108126
`,
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
'use client'
2+
3+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4+
import clsx from 'clsx'
5+
import { getBlocksForSidebar } from '@/lib/workflows/trigger-utils'
6+
import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config'
7+
import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config'
8+
import type { BlockConfig } from '@/blocks/types'
9+
import { useSidebarStore } from '@/stores/sidebar/store'
10+
11+
interface BlocksProps {
12+
disabled?: boolean
13+
}
14+
15+
interface BlockItem {
16+
name: string
17+
type: string
18+
isSpecial: boolean
19+
config?: BlockConfig
20+
icon?: any
21+
bgColor?: string
22+
}
23+
24+
/**
25+
* Constants for blocks panel sizing
26+
*/
27+
const DEFAULT_HEIGHT = 200
28+
const MIN_HEIGHT = 28
29+
const HEADER_HEIGHT = 28
30+
31+
export function Blocks({ disabled = false }: BlocksProps) {
32+
const [isResizing, setIsResizing] = useState(false)
33+
const { blocksHeight, setBlocksHeight, setTriggersHeight } = useSidebarStore()
34+
const startYRef = useRef<number>(0)
35+
const startHeightRef = useRef<number>(0)
36+
const containerRef = useRef<HTMLDivElement>(null)
37+
38+
const blocks = useMemo(() => {
39+
const allBlocks = getBlocksForSidebar()
40+
41+
// Separate blocks by category
42+
const regularBlockConfigs = allBlocks.filter((block) => block.category === 'blocks')
43+
const toolConfigs = allBlocks.filter((block) => block.category === 'tools')
44+
45+
// Create regular block items
46+
const regularBlockItems: BlockItem[] = regularBlockConfigs.map((block) => ({
47+
name: block.name,
48+
type: block.type,
49+
config: block,
50+
icon: block.icon,
51+
bgColor: block.bgColor,
52+
isSpecial: false,
53+
}))
54+
55+
// Add Loop and Parallel to blocks
56+
regularBlockItems.push({
57+
name: LoopTool.name,
58+
type: LoopTool.type,
59+
icon: LoopTool.icon,
60+
bgColor: LoopTool.bgColor,
61+
isSpecial: true,
62+
})
63+
64+
regularBlockItems.push({
65+
name: ParallelTool.name,
66+
type: ParallelTool.type,
67+
icon: ParallelTool.icon,
68+
bgColor: ParallelTool.bgColor,
69+
isSpecial: true,
70+
})
71+
72+
// Create tool items
73+
const toolItems: BlockItem[] = toolConfigs.map((block) => ({
74+
name: block.name,
75+
type: block.type,
76+
config: block,
77+
icon: block.icon,
78+
bgColor: block.bgColor,
79+
isSpecial: false,
80+
}))
81+
82+
// Sort each group alphabetically
83+
regularBlockItems.sort((a, b) => a.name.localeCompare(b.name))
84+
toolItems.sort((a, b) => a.name.localeCompare(b.name))
85+
86+
// Return blocks first, then tools
87+
return [...regularBlockItems, ...toolItems]
88+
}, [])
89+
90+
const handleDragStart = (e: React.DragEvent, item: BlockItem) => {
91+
if (disabled) {
92+
e.preventDefault()
93+
return
94+
}
95+
e.dataTransfer.setData(
96+
'application/json',
97+
JSON.stringify({
98+
type: item.type,
99+
enableTriggerMode: false,
100+
})
101+
)
102+
e.dataTransfer.effectAllowed = 'move'
103+
}
104+
105+
const handleClick = (item: BlockItem) => {
106+
if (item.type === 'connectionBlock' || disabled) return
107+
108+
const event = new CustomEvent('add-block-from-toolbar', {
109+
detail: {
110+
type: item.type,
111+
enableTriggerMode: false,
112+
},
113+
})
114+
window.dispatchEvent(event)
115+
}
116+
117+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
118+
setIsResizing(true)
119+
startYRef.current = e.clientY
120+
const currentHeight = Number.parseInt(
121+
getComputedStyle(document.documentElement).getPropertyValue('--blocks-height')
122+
)
123+
startHeightRef.current = currentHeight
124+
}, [])
125+
126+
const handleToggle = useCallback(() => {
127+
if (blocksHeight <= MIN_HEIGHT) {
128+
// Expanding: set to default height, and ensure triggers has enough space for both sections
129+
const currentTriggersHeight = Number.parseInt(
130+
getComputedStyle(document.documentElement).getPropertyValue('--triggers-height')
131+
)
132+
133+
// Calculate what blocks height we want
134+
const desiredBlocksHeight = DEFAULT_HEIGHT
135+
136+
// Calculate minimum triggers height needed to show blocks content
137+
const minRequiredTriggersHeight = desiredBlocksHeight + HEADER_HEIGHT
138+
139+
// Calculate ideal triggers height to show both blocks and triggers content reasonably
140+
// This gives DEFAULT_HEIGHT visible space for triggers content above blocks
141+
const idealTriggersHeight = desiredBlocksHeight + HEADER_HEIGHT + DEFAULT_HEIGHT
142+
143+
// If current triggers height is below ideal, expand triggers to show both sections properly
144+
if (currentTriggersHeight < idealTriggersHeight) {
145+
if (containerRef.current?.parentElement) {
146+
const parentHeight = containerRef.current.parentElement.getBoundingClientRect().height
147+
setTriggersHeight(Math.min(idealTriggersHeight, parentHeight))
148+
} else {
149+
setTriggersHeight(idealTriggersHeight)
150+
}
151+
}
152+
153+
// Now expand blocks (store constraints will handle the sizing)
154+
setBlocksHeight(desiredBlocksHeight)
155+
} else {
156+
// Collapsing: simply collapse to minimum
157+
setBlocksHeight(MIN_HEIGHT)
158+
}
159+
}, [blocksHeight, setBlocksHeight, setTriggersHeight])
160+
161+
/**
162+
* Setup resize event listeners and body styles when resizing
163+
* Event handlers are defined inline to avoid stale closure issues
164+
*/
165+
useEffect(() => {
166+
if (!isResizing || !containerRef.current) return
167+
168+
const handleMouseMove = (e: MouseEvent) => {
169+
const deltaY = startYRef.current - e.clientY
170+
let newHeight = startHeightRef.current + deltaY
171+
172+
const parentContainer = containerRef.current?.parentElement
173+
if (parentContainer) {
174+
const parentHeight = parentContainer.getBoundingClientRect().height
175+
const currentTriggersHeight = Number.parseInt(
176+
getComputedStyle(document.documentElement).getPropertyValue('--triggers-height')
177+
)
178+
const currentBlocksHeight = Number.parseInt(
179+
getComputedStyle(document.documentElement).getPropertyValue('--blocks-height')
180+
)
181+
182+
const maxAllowedHeight = currentTriggersHeight - HEADER_HEIGHT
183+
184+
// Special case: if blocks is at max height and user is expanding blocks (deltaY > 0)
185+
// then expand both blocks and triggers together
186+
const isAtMaxHeight = Math.abs(currentBlocksHeight - maxAllowedHeight) <= 2
187+
const isExpandingBlocks = deltaY > 0
188+
189+
if (isAtMaxHeight && isExpandingBlocks) {
190+
// Calculate how much more the user wants to expand
191+
const requestedIncrease = newHeight - currentBlocksHeight
192+
193+
// Expand triggers by the same amount (respecting parent height limit)
194+
const newTriggersHeight = Math.min(
195+
currentTriggersHeight + requestedIncrease,
196+
parentHeight
197+
)
198+
setTriggersHeight(newTriggersHeight)
199+
200+
// Blocks will expand proportionally through the store constraint
201+
setBlocksHeight(newHeight)
202+
} else {
203+
// Normal behavior: constrain blocks within current triggers space
204+
newHeight = Math.min(newHeight, maxAllowedHeight)
205+
newHeight = Math.max(newHeight, MIN_HEIGHT)
206+
setBlocksHeight(newHeight)
207+
}
208+
}
209+
}
210+
211+
const handleMouseUp = () => {
212+
setIsResizing(false)
213+
}
214+
215+
document.addEventListener('mousemove', handleMouseMove)
216+
document.addEventListener('mouseup', handleMouseUp)
217+
document.body.style.cursor = 'ns-resize'
218+
document.body.style.userSelect = 'none'
219+
220+
return () => {
221+
document.removeEventListener('mousemove', handleMouseMove)
222+
document.removeEventListener('mouseup', handleMouseUp)
223+
document.body.style.cursor = ''
224+
document.body.style.userSelect = ''
225+
}
226+
}, [isResizing, setBlocksHeight, setTriggersHeight])
227+
228+
return (
229+
<div
230+
ref={containerRef}
231+
className='blocks-container absolute right-0 bottom-0 left-0 z-20 flex flex-col border-[#2C2C2C] border-t bg-[#1E1E1E] dark:border-[#2C2C2C] dark:bg-[#1E1E1E]'
232+
>
233+
<div
234+
className='absolute top-[-4px] right-0 left-0 z-30 h-[8px] cursor-ns-resize'
235+
onMouseDown={handleMouseDown}
236+
/>
237+
<div
238+
className='flex-shrink-0 cursor-pointer px-[14px] pt-[3px] pb-[5px]'
239+
onClick={handleToggle}
240+
>
241+
<div className='font-medium text-[#AEAEAE] text-small dark:text-[#AEAEAE]'>Blocks</div>
242+
</div>
243+
244+
<div className='blocks-scrollable flex-1 overflow-y-auto overflow-x-hidden px-[8px]'>
245+
<div className='space-y-[4px] pb-[8px]'>
246+
{blocks.map((block) => {
247+
const Icon = block.icon
248+
return (
249+
<div
250+
key={block.type}
251+
draggable={!disabled}
252+
onDragStart={(e) => handleDragStart(e, block)}
253+
onClick={() => handleClick(block)}
254+
className={clsx(
255+
'group flex h-[25px] items-center gap-[8px] rounded-[8px] px-[5px] text-[14px]',
256+
disabled
257+
? 'cursor-not-allowed opacity-60'
258+
: 'cursor-pointer hover:bg-[#2C2C2C] active:cursor-grabbing dark:hover:bg-[#2C2C2C]'
259+
)}
260+
>
261+
<div
262+
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
263+
style={{ backgroundColor: block.bgColor }}
264+
>
265+
{Icon && (
266+
<Icon
267+
className={clsx(
268+
'text-white transition-transform duration-200',
269+
!disabled && 'group-hover:scale-110',
270+
'!h-[10px] !w-[10px]'
271+
)}
272+
/>
273+
)}
274+
</div>
275+
<span
276+
className={clsx(
277+
'truncate font-medium',
278+
'text-[#AEAEAE] group-hover:text-[#E6E6E6] dark:text-[#AEAEAE] dark:group-hover:text-[#E6E6E6]'
279+
)}
280+
>
281+
{block.name}
282+
</span>
283+
</div>
284+
)
285+
})}
286+
</div>
287+
</div>
288+
</div>
289+
)
290+
}

0 commit comments

Comments
 (0)