Skip to content

Commit a19ef94

Browse files
author
catlog22
committed
feat: add quick template functionality to NodePalette and enhance node creation experience
1 parent 0664937 commit a19ef94

9 files changed

Lines changed: 293 additions & 38 deletions

File tree

ccw/frontend/src/components/shared/NavGroup.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ export function NavGroup({
6565
to={item.path}
6666
onClick={onNavClick}
6767
className={cn(
68-
'flex items-center justify-center gap-3 px-2 py-2.5 rounded-md text-sm transition-colors',
69-
'hover:bg-hover hover:text-foreground',
68+
'flex items-center justify-center gap-3 px-2 py-2.5 rounded-md text-sm transition-all duration-200',
69+
'hover:bg-hover hover:text-foreground hover-glow',
7070
isActive
7171
? 'bg-primary/10 text-primary font-medium'
7272
: 'text-muted-foreground'
@@ -107,8 +107,8 @@ export function NavGroup({
107107
to={item.path}
108108
onClick={onNavClick}
109109
className={cn(
110-
'flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors pl-6',
111-
'hover:bg-hover hover:text-foreground',
110+
'flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-all duration-200 pl-6',
111+
'hover:bg-hover hover:text-foreground hover-glow',
112112
(isActive && !searchParams) || isQueryParamActive
113113
? 'bg-primary/10 text-primary font-medium'
114114
: 'text-muted-foreground'

ccw/frontend/src/components/ui/Button.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const buttonVariants = cva(
1919
"hover:bg-accent hover:text-accent-foreground",
2020
link:
2121
"text-primary underline-offset-4 hover:underline",
22+
gradient:
23+
"bg-gradient-brand text-primary-foreground hover-glow",
24+
gradientPrimary:
25+
"bg-gradient-primary text-primary-foreground hover-glow-primary",
2226
},
2327
size: {
2428
default: "h-10 px-4 py-2",

ccw/frontend/src/components/ui/Card.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,27 @@ const CardFooter = React.forwardRef<
7575
));
7676
CardFooter.displayName = "CardFooter";
7777

78+
const CardGradientBorder = React.forwardRef<
79+
HTMLDivElement,
80+
React.HTMLAttributes<HTMLDivElement>
81+
>(({ className, ...props }, ref) => (
82+
<div
83+
ref={ref}
84+
className={cn(
85+
"rounded-lg bg-card text-card-foreground shadow-sm border-gradient-brand",
86+
className
87+
)}
88+
{...props}
89+
/>
90+
));
91+
CardGradientBorder.displayName = "CardGradientBorder";
92+
7893
export {
7994
Card,
8095
CardHeader,
8196
CardFooter,
8297
CardTitle,
8398
CardDescription,
8499
CardContent,
100+
CardGradientBorder,
85101
};

ccw/frontend/src/index.css

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,3 +535,49 @@
535535
animation: none;
536536
background-size: 100% 100%;
537537
}
538+
539+
/* ===========================
540+
Global Ambient Gradient Background
541+
Always visible behind content
542+
=========================== */
543+
544+
/* Standard ambient gradient - subtle */
545+
[data-gradient="standard"] body::before {
546+
content: '';
547+
position: fixed;
548+
top: -50%;
549+
left: -50%;
550+
width: 200%;
551+
height: 200%;
552+
background: radial-gradient(circle at 30% 30%, hsla(var(--accent), 0.03) 0%, transparent 50%),
553+
radial-gradient(circle at 70% 70%, hsla(var(--primary), 0.03) 0%, transparent 50%);
554+
pointer-events: none;
555+
z-index: -1;
556+
}
557+
558+
/* Enhanced ambient gradient - more vibrant with animation */
559+
[data-gradient="enhanced"] body::before {
560+
content: '';
561+
position: fixed;
562+
top: -50%;
563+
left: -50%;
564+
width: 200%;
565+
height: 200%;
566+
background: radial-gradient(circle at 20% 20%, hsla(var(--accent), 0.08) 0%, transparent 40%),
567+
radial-gradient(circle at 80% 80%, hsla(var(--primary), 0.08) 0%, transparent 40%),
568+
radial-gradient(circle at 50% 50%, hsla(var(--secondary), 0.05) 0%, transparent 50%);
569+
pointer-events: none;
570+
z-index: -1;
571+
animation: ambient-shift 20s ease-in-out infinite;
572+
}
573+
574+
/* Disable ambient gradient when off */
575+
[data-gradient="off"] body::before {
576+
display: none;
577+
}
578+
579+
/* Ambient shift animation for enhanced gradient */
580+
@keyframes ambient-shift {
581+
0%, 100% { transform: translate(0, 0); }
582+
50% { transform: translate(-2%, -2%); }
583+
}

ccw/frontend/src/pages/orchestrator/FlowCanvas.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ function FlowCanvasInner({ className }: FlowCanvasProps) {
4242
const setNodes = useFlowStore((state) => state.setNodes);
4343
const setEdges = useFlowStore((state) => state.setEdges);
4444
const addNode = useFlowStore((state) => state.addNode);
45+
const addNodeFromTemplate = useFlowStore((state) => state.addNodeFromTemplate);
4546
const setSelectedNodeId = useFlowStore((state) => state.setSelectedNodeId);
4647
const setSelectedEdgeId = useFlowStore((state) => state.setSelectedEdgeId);
4748
const markModified = useFlowStore((state) => state.markModified);
@@ -127,10 +128,17 @@ function FlowCanvasInner({ className }: FlowCanvasProps) {
127128
y: event.clientY,
128129
});
129130

130-
// Add prompt-template node at drop position
131-
addNode(position);
131+
// Check if a template ID is provided
132+
const templateId = event.dataTransfer.getData('application/reactflow-template-id');
133+
if (templateId) {
134+
// Use quick template
135+
addNodeFromTemplate(templateId, position);
136+
} else {
137+
// Use basic empty node
138+
addNode(position);
139+
}
132140
},
133-
[screenToFlowPosition, addNode]
141+
[screenToFlowPosition, addNode, addNodeFromTemplate]
134142
);
135143

136144
return (

ccw/frontend/src/pages/orchestrator/NodePalette.tsx

Lines changed: 136 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,113 @@
11
// ========================================
22
// Node Palette Component
33
// ========================================
4-
// Draggable node palette for creating new nodes
4+
// Draggable node palette with quick templates for creating nodes
55

66
import { DragEvent, useState } from 'react';
77
import { useIntl } from 'react-intl';
8-
import { MessageSquare, ChevronDown, ChevronRight, GripVertical } from 'lucide-react';
8+
import {
9+
MessageSquare, ChevronDown, ChevronRight, GripVertical,
10+
Search, Code, FileOutput, GitBranch, GitFork, GitMerge, Plus, Terminal
11+
} from 'lucide-react';
912
import { cn } from '@/lib/utils';
1013
import { Button } from '@/components/ui/Button';
1114
import { useFlowStore } from '@/stores';
12-
import { NODE_TYPE_CONFIGS } from '@/types/flow';
15+
import { NODE_TYPE_CONFIGS, QUICK_TEMPLATES } from '@/types/flow';
1316

1417
interface NodePaletteProps {
1518
className?: string;
1619
}
1720

1821
/**
19-
* Draggable card for the unified Prompt Template node type
22+
* Icon mapping for quick templates
2023
*/
21-
function PromptTemplateCard() {
24+
const TEMPLATE_ICONS: Record<string, React.ElementType> = {
25+
'slash-command-main': Terminal,
26+
'slash-command-async': Terminal,
27+
analysis: Search,
28+
implementation: Code,
29+
'file-operation': FileOutput,
30+
conditional: GitBranch,
31+
parallel: GitFork,
32+
merge: GitMerge,
33+
};
34+
35+
/**
36+
* Draggable card for a quick template
37+
*/
38+
function QuickTemplateCard({
39+
template,
40+
}: {
41+
template: typeof QUICK_TEMPLATES[number];
42+
}) {
43+
const Icon = TEMPLATE_ICONS[template.id] || MessageSquare;
44+
45+
// Handle drag start - store template ID
46+
const onDragStart = (event: DragEvent<HTMLDivElement>) => {
47+
event.dataTransfer.setData('application/reactflow-node-type', 'prompt-template');
48+
event.dataTransfer.setData('application/reactflow-template-id', template.id);
49+
event.dataTransfer.effectAllowed = 'move';
50+
};
51+
52+
// Handle double-click to add node at default position
53+
const onDoubleClick = () => {
54+
const position = { x: 100 + Math.random() * 200, y: 100 + Math.random() * 200 };
55+
useFlowStore.getState().addNodeFromTemplate(template.id, position);
56+
};
57+
58+
return (
59+
<div
60+
draggable
61+
onDragStart={onDragStart}
62+
onDoubleClick={onDoubleClick}
63+
className={cn(
64+
'group flex items-center gap-3 p-3 rounded-lg border-2 bg-card cursor-grab transition-all',
65+
'hover:shadow-md hover:scale-[1.02] active:cursor-grabbing active:scale-[0.98]',
66+
`border-${template.color.replace('bg-', '')}`
67+
)}
68+
>
69+
<div className={cn('p-2 rounded-md text-white', template.color, `hover:${template.color}`)}>
70+
<Icon className="w-4 h-4" />
71+
</div>
72+
<div className="flex-1 min-w-0">
73+
<div className="text-sm font-medium text-foreground">{template.label}</div>
74+
<div className="text-xs text-muted-foreground truncate">{template.description}</div>
75+
</div>
76+
<GripVertical className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
77+
</div>
78+
);
79+
}
80+
81+
/**
82+
* Basic empty prompt template card
83+
*/
84+
function BasicTemplateCard() {
2285
const config = NODE_TYPE_CONFIGS['prompt-template'];
2386

24-
// Handle drag start
2587
const onDragStart = (event: DragEvent<HTMLDivElement>) => {
2688
event.dataTransfer.setData('application/reactflow-node-type', 'prompt-template');
2789
event.dataTransfer.effectAllowed = 'move';
2890
};
2991

92+
const onDoubleClick = () => {
93+
const position = { x: 100 + Math.random() * 200, y: 100 + Math.random() * 200 };
94+
useFlowStore.getState().addNode(position);
95+
};
96+
3097
return (
3198
<div
3299
draggable
33100
onDragStart={onDragStart}
101+
onDoubleClick={onDoubleClick}
34102
className={cn(
35103
'group flex items-center gap-3 p-3 rounded-lg border-2 bg-card cursor-grab transition-all',
36104
'hover:shadow-md hover:scale-[1.02] active:cursor-grabbing active:scale-[0.98]',
105+
'border-dashed border-muted-foreground/50 hover:border-primary',
37106
'border-blue-500'
38107
)}
39108
>
40109
<div className="p-2 rounded-md text-white bg-blue-500 hover:bg-blue-600">
41-
<MessageSquare className="w-4 h-4" />
110+
<Plus className="w-4 h-4" />
42111
</div>
43112
<div className="flex-1 min-w-0">
44113
<div className="text-sm font-medium text-foreground">{config.label}</div>
@@ -49,9 +118,41 @@ function PromptTemplateCard() {
49118
);
50119
}
51120

121+
/**
122+
* Category section with expand/collapse
123+
*/
124+
function TemplateCategory({
125+
title,
126+
children,
127+
defaultExpanded = true,
128+
}: {
129+
title: string;
130+
children: React.ReactNode;
131+
defaultExpanded?: boolean;
132+
}) {
133+
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
134+
135+
return (
136+
<div>
137+
<button
138+
onClick={() => setIsExpanded(!isExpanded)}
139+
className="flex items-center gap-2 w-full text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2 hover:text-foreground transition-colors"
140+
>
141+
{isExpanded ? (
142+
<ChevronDown className="w-3 h-3" />
143+
) : (
144+
<ChevronRight className="w-3 h-3" />
145+
)}
146+
{title}
147+
</button>
148+
149+
{isExpanded && <div className="space-y-2">{children}</div>}
150+
</div>
151+
);
152+
}
153+
52154
export function NodePalette({ className }: NodePaletteProps) {
53155
const { formatMessage } = useIntl();
54-
const [isExpanded, setIsExpanded] = useState(true);
55156
const isPaletteOpen = useFlowStore((state) => state.isPaletteOpen);
56157
const setIsPaletteOpen = useFlowStore((state) => state.setIsPaletteOpen);
57158

@@ -91,34 +192,39 @@ export function NodePalette({ className }: NodePaletteProps) {
91192
{formatMessage({ id: 'orchestrator.palette.instructions' })}
92193
</div>
93194

94-
{/* Node Type Categories */}
195+
{/* Template Categories */}
95196
<div className="flex-1 overflow-y-auto p-4 space-y-4">
96-
{/* Execution Nodes */}
97-
<div>
98-
<button
99-
onClick={() => setIsExpanded(!isExpanded)}
100-
className="flex items-center gap-2 w-full text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2"
101-
>
102-
{isExpanded ? (
103-
<ChevronDown className="w-3 h-3" />
104-
) : (
105-
<ChevronRight className="w-3 h-3" />
106-
)}
107-
{formatMessage({ id: 'orchestrator.palette.nodeTypes' })}
108-
</button>
109-
110-
{isExpanded && (
111-
<div className="space-y-2">
112-
<PromptTemplateCard />
113-
</div>
114-
)}
115-
</div>
197+
{/* Basic / Empty Template */}
198+
<TemplateCategory title="Basic" defaultExpanded={false}>
199+
<BasicTemplateCard />
200+
</TemplateCategory>
201+
202+
{/* Slash Commands */}
203+
<TemplateCategory title="Slash Commands" defaultExpanded={true}>
204+
{QUICK_TEMPLATES.filter(t => t.id.startsWith('slash-command')).map((template) => (
205+
<QuickTemplateCard key={template.id} template={template} />
206+
))}
207+
</TemplateCategory>
208+
209+
{/* CLI Tools */}
210+
<TemplateCategory title="CLI Tools" defaultExpanded={true}>
211+
{QUICK_TEMPLATES.filter(t => ['analysis', 'implementation'].includes(t.id)).map((template) => (
212+
<QuickTemplateCard key={template.id} template={template} />
213+
))}
214+
</TemplateCategory>
215+
216+
{/* Flow Control */}
217+
<TemplateCategory title="Flow Control" defaultExpanded={true}>
218+
{QUICK_TEMPLATES.filter(t => ['file-operation', 'conditional', 'parallel', 'merge'].includes(t.id)).map((template) => (
219+
<QuickTemplateCard key={template.id} template={template} />
220+
))}
221+
</TemplateCategory>
116222
</div>
117223

118224
{/* Footer */}
119225
<div className="px-4 py-3 border-t border-border bg-muted/30">
120226
<div className="text-xs text-muted-foreground">
121-
<span className="font-medium">{formatMessage({ id: 'orchestrator.palette.tipLabel' })}</span> {formatMessage({ id: 'orchestrator.palette.tip' })}
227+
<span className="font-medium">Tip:</span> Drag to canvas or double-click to add
122228
</div>
123229
</div>
124230
</div>

ccw/frontend/src/stores/appStore.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,18 @@ if (typeof window !== 'undefined') {
364364
);
365365
}
366366
});
367+
368+
// Apply initial theme immediately (before localStorage rehydration)
369+
// This ensures gradient attributes are set from the start
370+
const state = useAppStore.getState();
371+
applyThemeToDocument(
372+
state.resolvedTheme,
373+
state.colorScheme,
374+
state.customHue,
375+
state.gradientLevel,
376+
state.enableHoverGlow,
377+
state.enableBackgroundAnimation
378+
);
367379
}
368380

369381
// Selectors for common access patterns

0 commit comments

Comments
 (0)