Skip to content

Commit 812800a

Browse files
authored
Merge pull request #168 from ShipSecAI/betterclever/ux-fixes-3
feat(ui): enhance workflow design, edge styling, and terminal UX
2 parents b6f2b36 + d7660d1 commit 812800a

File tree

14 files changed

+979
-440
lines changed

14 files changed

+979
-440
lines changed

backend/src/workflows/dto/workflow-graph.dto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const WorkflowEdgeSchema = z.object({
3535
target: z.string(),
3636
sourceHandle: z.string().optional(),
3737
targetHandle: z.string().optional(),
38+
type: z.enum(['default', 'smoothstep', 'step', 'straight', 'bezier']).optional(),
3839
});
3940

4041
export const WorkflowGraphSchema = z.object({

frontend/src/components/layout/Sidebar.tsx

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import type { ComponentMetadata } from '@/schemas/component'
1313
import { cn } from '@/lib/utils'
1414
import { env } from '@/config/env'
1515
import { Skeleton } from '@/components/ui/skeleton'
16+
import {
17+
type ComponentCategory,
18+
getCategorySeparatorColor
19+
} from '@/utils/categoryColors'
20+
import { useThemeStore } from '@/store/themeStore'
1621

1722
// Use backend-provided category configuration
1823
// The frontend will no longer categorize components - it will use backend data
@@ -166,10 +171,17 @@ export function Sidebar({ canManageWorkflows = true }: SidebarProps) {
166171
const { getAllComponents, fetchComponents, loading, error } = useComponentStore()
167172
const [searchQuery, setSearchQuery] = useState('')
168173
const [viewMode, setViewMode] = useState<ViewMode>('list')
174+
const theme = useThemeStore((state) => state.theme)
175+
const isDarkMode = theme === 'dark'
169176
const frontendBranch = env.VITE_FRONTEND_BRANCH.trim()
170177
const backendBranch = env.VITE_BACKEND_BRANCH.trim()
171178
const hasBranchInfo = Boolean(frontendBranch || backendBranch)
172179

180+
// Get category accent color (for left border) - uses separator colors for brightness
181+
const getCategoryAccentColor = (category: string): string | undefined => {
182+
return getCategorySeparatorColor(category as ComponentCategory, isDarkMode)
183+
}
184+
173185
// Custom scrollbar state
174186
const scrollContainerRef = useRef<HTMLDivElement>(null)
175187
const [scrollbarVisible, setScrollbarVisible] = useState(false)
@@ -289,7 +301,7 @@ export function Sidebar({ canManageWorkflows = true }: SidebarProps) {
289301
// eslint-disable-next-line react-hooks/exhaustive-deps
290302
}, [searchQuery])
291303

292-
// Custom scrollbar logic
304+
// Custom scrollbar logic - setup event listeners (only once)
293305
useEffect(() => {
294306
const container = scrollContainerRef.current
295307
if (!container) return
@@ -349,7 +361,43 @@ export function Sidebar({ canManageWorkflows = true }: SidebarProps) {
349361
clearTimeout(scrollTimeoutRef.current)
350362
}
351363
}
352-
}, [filteredComponentsByCategory, loading])
364+
// Only setup event listeners once - don't depend on content changes
365+
}, [])
366+
367+
// Update scrollbar when content changes (without re-setting up listeners)
368+
// Calculate a stable value for component count to use as dependency
369+
const componentCount = useMemo(() => {
370+
return Object.values(filteredComponentsByCategory).reduce(
371+
(total, components) => total + components.length,
372+
0
373+
)
374+
}, [filteredComponentsByCategory])
375+
376+
useEffect(() => {
377+
const container = scrollContainerRef.current
378+
if (!container) return
379+
380+
// Use requestAnimationFrame to ensure DOM has updated after content changes
381+
requestAnimationFrame(() => {
382+
const { scrollTop, scrollHeight, clientHeight } = container
383+
const maxScroll = scrollHeight - clientHeight
384+
385+
if (maxScroll <= 0) {
386+
setScrollbarVisible(false)
387+
setScrollbarHeight(0)
388+
setScrollbarPosition(0)
389+
return
390+
}
391+
392+
// Calculate scrollbar thumb position and height
393+
const thumbHeight = Math.max((clientHeight / scrollHeight) * clientHeight, 30)
394+
const thumbPosition = (scrollTop / maxScroll) * (clientHeight - thumbHeight)
395+
396+
setScrollbarHeight(thumbHeight)
397+
setScrollbarPosition(thumbPosition)
398+
// Don't show scrollbar automatically when content changes - only on scroll
399+
})
400+
}, [componentCount, loading]) // Use stable componentCount instead of filteredComponentsByCategory object
353401

354402
return (
355403
<div className="h-full w-full max-w-[320px] border-r bg-background flex flex-col">
@@ -480,16 +528,24 @@ export function Sidebar({ canManageWorkflows = true }: SidebarProps) {
480528

481529
const categoryConfig = components[0]?.categoryConfig
482530

531+
const categoryAccentColor = getCategoryAccentColor(category)
532+
483533
return (
484534
<AccordionItem
485535
key={category}
486536
value={category}
487537
className="border border-border/50 rounded-sm px-3 py-1 transition-colors"
488538
>
489-
<AccordionTrigger className={cn(
490-
'py-3 px-0 hover:no-underline hover:bg-muted/50 rounded-sm -mx-3 -my-1 px-3 [&[data-state=open]]:text-foreground',
491-
'group transition-colors'
492-
)}>
539+
<AccordionTrigger
540+
className={cn(
541+
'py-3 hover:no-underline hover:bg-muted/50 rounded-sm -mx-3 -my-1 px-3 [&[data-state=open]]:text-foreground',
542+
'group transition-colors relative'
543+
)}
544+
style={{
545+
borderLeftWidth: categoryAccentColor ? '3px' : undefined,
546+
borderLeftColor: categoryAccentColor || undefined,
547+
}}
548+
>
493549
<div className="flex flex-col items-start gap-0.5 w-full">
494550
<div className="flex items-center justify-between w-full">
495551
<div className="flex items-center gap-2">

frontend/src/components/layout/TopBar.tsx

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Download,
1313
CheckCircle2,
1414
Loader2,
15+
Pencil,
1516
} from 'lucide-react'
1617
import { useWorkflowStore } from '@/store/workflowStore'
1718
import { useWorkflowUiStore } from '@/store/workflowUiStore'
@@ -40,7 +41,10 @@ export function TopBar({
4041
const [isSaving, setIsSaving] = useState(false)
4142
const [isImporting, setIsImporting] = useState(false)
4243
const [tempWorkflowName, setTempWorkflowName] = useState('')
44+
const [isEditingTitle, setIsEditingTitle] = useState(false)
45+
const [showPencil, setShowPencil] = useState(false)
4346
const fileInputRef = useRef<HTMLInputElement | null>(null)
47+
const titleInputRef = useRef<HTMLInputElement | null>(null)
4448

4549
const { metadata, isDirty, setWorkflowName } = useWorkflowStore()
4650
const { mode, setMode } = useWorkflowUiStore()
@@ -51,11 +55,32 @@ export function TopBar({
5155
if (!trimmed) {
5256
setWorkflowName(DEFAULT_WORKFLOW_NAME)
5357
setTempWorkflowName(DEFAULT_WORKFLOW_NAME)
58+
setIsEditingTitle(false)
5459
return
5560
}
5661
if (trimmed !== metadata.name) {
5762
setWorkflowName(trimmed)
5863
}
64+
setIsEditingTitle(false)
65+
}
66+
67+
const handleStartEditing = () => {
68+
if (!canEdit) return
69+
setIsEditingTitle(true)
70+
// Focus the input after a brief delay to ensure it's rendered
71+
setTimeout(() => {
72+
titleInputRef.current?.focus()
73+
titleInputRef.current?.select()
74+
}, 0)
75+
}
76+
77+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
78+
if (e.key === 'Enter') {
79+
handleChangeWorkflowName()
80+
} else if (e.key === 'Escape') {
81+
setTempWorkflowName(metadata.name || DEFAULT_WORKFLOW_NAME)
82+
setIsEditingTitle(false)
83+
}
5984
}
6085

6186
const handleSave = async () => {
@@ -216,18 +241,50 @@ export function TopBar({
216241

217242
<div className="flex-1 min-w-0 w-full">
218243
<div className="grid w-full gap-3 sm:gap-4 items-center sm:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]">
219-
<div className="flex items-center justify-start">
220-
<div className="flex items-center gap-2 rounded-lg border border-border/60 bg-muted/40 px-3 py-1.5 shadow-sm min-w-[220px] max-w-[360px] w-full">
221-
<Input
222-
value={tempWorkflowName}
223-
onChange={(e) => setTempWorkflowName(e.target.value)}
224-
onBlur={handleChangeWorkflowName}
225-
readOnly={!canEdit}
226-
aria-readonly={!canEdit}
227-
className="font-semibold bg-transparent border-none shadow-none h-7 px-0 py-0 text-base focus-visible:ring-0 focus-visible:ring-offset-0"
228-
placeholder="Workflow name"
229-
/>
244+
<div className="flex items-center justify-start gap-2">
245+
<div
246+
className={cn(
247+
'flex items-center gap-2 min-w-[220px] max-w-[360px]',
248+
isEditingTitle
249+
? 'rounded-lg border border-border/60 bg-muted/40 px-3 py-1.5 shadow-sm'
250+
: 'group relative'
251+
)}
252+
onMouseEnter={() => canEdit && !isEditingTitle && setShowPencil(true)}
253+
onMouseLeave={() => setShowPencil(false)}
254+
>
255+
{isEditingTitle ? (
256+
<Input
257+
ref={titleInputRef}
258+
value={tempWorkflowName}
259+
onChange={(e) => setTempWorkflowName(e.target.value)}
260+
onBlur={handleChangeWorkflowName}
261+
onKeyDown={handleKeyDown}
262+
className="font-semibold bg-transparent border-none shadow-none h-7 px-0 py-0 text-base focus-visible:ring-0 focus-visible:ring-offset-0"
263+
placeholder="Workflow name"
264+
/>
265+
) : (
266+
<>
267+
<h1 className="font-semibold text-base text-foreground pr-6">
268+
{metadata.name || DEFAULT_WORKFLOW_NAME}
269+
</h1>
270+
{canEdit && showPencil && (
271+
<button
272+
type="button"
273+
onClick={handleStartEditing}
274+
className="absolute right-0 top-1/2 -translate-y-1/2 p-1 rounded hover:bg-muted/80 transition-colors"
275+
aria-label="Edit workflow name"
276+
>
277+
<Pencil className="h-3.5 w-3.5 text-muted-foreground" />
278+
</button>
279+
)}
280+
</>
281+
)}
230282
</div>
283+
{metadata.currentVersion !== null && metadata.currentVersion !== undefined && (
284+
<span className="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium bg-muted text-muted-foreground border border-border/60">
285+
v{metadata.currentVersion}
286+
</span>
287+
)}
231288
</div>
232289
<div className="flex justify-center">{modeToggle}</div>
233290
<div className="flex items-center justify-end gap-3">

0 commit comments

Comments
 (0)