Explorar o código

feat(ui): 重构应用样式并优化布局结构

- 引入完整的设计系统变量,统一颜色、间距、阴影等样式
- 重构 TopBar 组件样式,使用组合类名替代独立类名
- 调整应用布局结构,将 TopBar 移至独立容器
- 优化流程图样式,增加网格背景和现代化视觉设计
- 改进详情面板样式,提升信息层次和可读性
- 修复无效节点与折叠边的关联逻辑,确保隐藏状态同步
- 扩展右侧面板最大宽度至 800px,提供更大操作空间
max_liu hai 1 semana
pai
achega
722c967403

+ 17 - 15
frontend/react-template/src/App.tsx

@@ -55,7 +55,7 @@ function App() {
       const rect = bodyRef.current?.getBoundingClientRect();
       if (!rect) return;
       const next = rect.right - event.clientX;
-      const clamped = Math.min(500, Math.max(240, next));
+      const clamped = Math.min(800, Math.max(240, next));
       setRightWidth(clamped);
     };
     const handleUp = () => {
@@ -71,23 +71,25 @@ function App() {
 
   return (
     <div className="app">
+      <div className="app-top">
+        <TopBar
+          selectedTraceId={selectedTraceId}
+          selectedNode={selectedNode}
+          title={selectedTraceTitle}
+          onTraceSelect={(id, title) => {
+            setSelectedTraceId(id);
+            if (title) setSelectedTraceTitle(title);
+          }}
+          onTraceCreated={() => setRefreshTrigger((t) => t + 1)}
+          onMessageInserted={() => setMessageRefreshTrigger((t) => t + 1)}
+        />
+      </div>
       <div
         className="app-body"
         ref={bodyRef}
         style={{ userSelect: isDragging ? "none" : "auto" }}
       >
         <div className="app-main">
-          <TopBar
-            selectedTraceId={selectedTraceId}
-            selectedNode={selectedNode}
-            title={selectedTraceTitle}
-            onTraceSelect={(id, title) => {
-              setSelectedTraceId(id);
-              if (title) setSelectedTraceTitle(title);
-            }}
-            onTraceCreated={() => setRefreshTrigger((t) => t + 1)}
-            onMessageInserted={() => setMessageRefreshTrigger((t) => t + 1)}
-          />
           <MainContent
             traceId={selectedTraceId}
             onNodeClick={handleNodeClick}
@@ -108,16 +110,16 @@ function App() {
               aria-orientation="vertical"
               aria-valuenow={rightWidth}
               aria-valuemin={240}
-              aria-valuemax={500}
+              aria-valuemax={800}
             />
             <div
               className="app-right"
               style={{ width: rightWidth }}
             >
               <DetailPanel
-                node={selectedNode && isGoalNode(selectedNode) ? (selectedNode as Goal) : null}
+                node={selectedNode}
                 edge={selectedEdge}
-                messages={selectedMessages}
+                messages={selectedMessages as Message[]}
                 onClose={handleCloseDetail}
               />
             </div>

+ 67 - 76
frontend/react-template/src/components/DetailPanel/DetailPanel.module.css

@@ -1,141 +1,132 @@
 .panel {
   width: 100%;
   height: 100%;
-  /* border-left: 1px solid var(--border-color, #e0e0e0); */
-  background: #ffffff;
+  background: var(--bg-panel);
   display: flex;
   flex-direction: column;
+  box-shadow: -1px 0 0 0 var(--border-light);
 }
 
 .header {
-  padding: 12px 16px;
-  border-bottom: 1px solid var(--border-color, #e0e0e0);
+  height: var(--topbar-height);
+  padding: 0 var(--space-lg);
+  border-bottom: 1px solid var(--border-light);
   display: flex;
   align-items: center;
   justify-content: space-between;
+  flex-shrink: 0;
 }
 
 .title {
-  font-size: 14px;
+  font-size: 16px;
   font-weight: 600;
-  color: var(--text-primary, #333);
+  color: var(--text-primary);
 }
 
 .close {
   border: none;
   background: transparent;
-  font-size: 16px;
+  width: 28px;
+  height: 28px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 20px;
+  color: var(--text-tertiary);
   cursor: pointer;
+  border-radius: var(--radius-sm);
+  transition: all var(--transition-fast);
+}
+
+.close:hover {
+  background: var(--bg-surface-hover);
+  color: var(--text-secondary);
 }
 
 .content {
-  padding: 16px;
-  overflow: auto;
+  padding: var(--space-lg);
+  overflow-y: auto;
+  overflow-x: hidden;
   flex: 1;
 }
 
 .sectionTitle {
-  font-size: 16px;
+  font-size: 14px;
   font-weight: 600;
-  color: #333;
-  margin-bottom: 12px;
+  color: var(--text-secondary);
+  margin: var(--space-lg) 0 var(--space-md);
+  text-transform: uppercase;
+  letter-spacing: 0.05em;
   display: flex;
   align-items: center;
-  gap: 8px;
 }
 
-.sectionTitle::before {
-  content: "";
-  display: block;
-  width: 4px;
-  height: 16px;
-  background: #2d72d2;
-  border-radius: 2px;
+.sectionTitle:first-child {
+  margin-top: 0;
 }
 
 .section {
-  margin-bottom: 16px;
+  margin-bottom: var(--space-lg);
 }
 
 .label {
   font-size: 12px;
-  color: #666;
-  margin-bottom: 6px;
+  font-weight: 500;
+  color: var(--text-tertiary);
+  margin-bottom: var(--space-xs);
 }
 
 .value {
-  font-size: 13px;
-  color: #333;
-  word-break: break-all;
+  font-size: 14px;
+  line-height: 1.6;
+  color: var(--text-primary);
+  word-break: break-word;
+  background: var(--bg-surface-hover);
+  padding: var(--space-sm) var(--space-md);
+  border-radius: var(--radius-md);
+  border: 1px solid var(--border-light);
 }
 
 .toolCalls {
-  margin-top: 4px;
+  display: flex;
+  flex-direction: column;
+  gap: var(--space-sm);
 }
 
 .toolCall {
-  background: #f5f5f5;
-  border-radius: 4px;
-  padding: 8px;
-  margin-bottom: 8px;
-}
-
-.toolCall:last-child {
-  margin-bottom: 0;
+  background: var(--bg-node-goal);
+  border: 1px solid var(--color-primary);
+  border-radius: var(--radius-md);
+  padding: var(--space-sm);
 }
 
 .toolName {
-  font-size: 12px;
   font-weight: 600;
-  color: #555;
-  margin-bottom: 4px;
+  color: var(--color-primary);
+  margin-bottom: var(--space-xs);
+  font-size: 13px;
 }
 
 .toolArgs {
-  font-family: monospace;
-  font-size: 11px;
-  color: #666;
-  white-space: pre-wrap;
-  word-break: break-all;
+  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+  font-size: 12px;
+  background: rgba(255, 255, 255, 0.5);
+  padding: var(--space-xs) var(--space-sm);
+  border-radius: var(--radius-sm);
   margin: 0;
-  background: rgba(0, 0, 0, 0.03);
-  padding: 4px;
-  border-radius: 2px;
+  white-space: pre-wrap;
+  color: var(--text-secondary);
 }
 
 .messages {
-  margin-top: 24px;
-  border-top: 1px solid #eee;
-  padding-top: 16px;
-}
-
-.messageList {
-  /* max-height removed to allow full content scrolling */
-}
-
-/* Custom scrollbar for message list */
-.messageList::-webkit-scrollbar {
-  width: 4px;
-}
-
-.messageList::-webkit-scrollbar-thumb {
-  background: #ccc;
-  border-radius: 2px;
-}
-
-.messageList::-webkit-scrollbar-track {
-  background: transparent;
+  display: flex;
+  flex-direction: column;
+  gap: var(--space-md);
 }
 
 .messageItem {
-  background: #fff;
-  border: 1px solid #eee;
-  border-radius: 8px;
-  padding: 12px;
-  margin-bottom: 12px;
-  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+  border-left: 2px solid var(--border-medium);
+  padding-left: var(--space-md);
 }
 
-.messageItem:last-child {
-  margin-bottom: 0;
-}
+/* Removed old styles */

+ 47 - 10
frontend/react-template/src/components/FlowChart/FlowChart.tsx

@@ -506,6 +506,9 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
         }
       });
 
+      // Map to store invalid nodes by their anchor parent ID
+      const invalidNodesByAnchor = new Map<string, LayoutNode[]>();
+
       invalidBranches.forEach((branch) => {
         if (branch.length === 0) return;
         const firstMsg = branch[0];
@@ -517,6 +520,7 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
           if (parentNode) {
             let currentParent = parentNode;
             const X_OFFSET = -200; // 向左偏移
+            const currentBranchNodes: LayoutNode[] = [];
 
             branch.forEach((msg, idx) => {
               const nodeId = `invalid-${msg.id || Math.random()}`;
@@ -531,6 +535,7 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
                 isInvalid: true,
               };
               nodes.push(node);
+              currentBranchNodes.push(node);
 
               edges.push({
                 id: `edge-${currentParent.id}-${node.id}`,
@@ -545,6 +550,28 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
 
               currentParent = node;
             });
+
+            // Store in map
+            if (!invalidNodesByAnchor.has(parentNode.id)) {
+              invalidNodesByAnchor.set(parentNode.id, []);
+            }
+            invalidNodesByAnchor.get(parentNode.id)!.push(...currentBranchNodes);
+          }
+        }
+      });
+
+      // Associate invalid nodes with collapsible edges
+      // If a parent node is hidden (part of a collapsed edge), its invalid children should also be hidden
+      edges.forEach((edge) => {
+        if (edge.collapsible && edge.children) {
+          const extraChildren: LayoutNode[] = [];
+          edge.children.forEach((child) => {
+            if (invalidNodesByAnchor.has(child.id)) {
+              extraChildren.push(...invalidNodesByAnchor.get(child.id)!);
+            }
+          });
+          if (extraChildren.length > 0) {
+            edge.children.push(...extraChildren);
           }
         }
       });
@@ -899,13 +926,13 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
 
                 if (sourceIsMessage || targetIsMessage) {
                   // msgGroup 相关的连接线用灰色
-                  color = "#9E9E9E";
+                  color = "#94a3b8"; // Slate 400
                 } else if (sourceIsMainGoal && targetIsMainGoal) {
                   // 主节点之间的连接线用绿色
-                  color = "#4CAF50";
+                  color = "#10b981"; // Emerald 500
                 } else {
                   // sub_goals 之间的连接线用蓝色
-                  color = "#2196F3";
+                  color = "#3b82f6"; // Blue 500
                 }
 
                 return (
@@ -913,10 +940,10 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
                     <path
                       d={path}
                       fill="none"
-                      stroke={edge.isInvalid ? "#9E9E9E" : color} // 失效边使用灰色
+                      stroke={edge.isInvalid ? "#cbd5e1" : color} // 失效边使用浅灰色 (Slate 300)
                       strokeWidth={strokeWidth}
                       strokeDasharray={edge.isInvalid ? "5,5" : undefined} // 失效边使用虚线
-                      markerEnd="url(#arrow-default)" // 箭头
+                      markerEnd={edge.isInvalid ? undefined : "url(#arrow-default)"} // 失效边不显示箭头
                       style={{ cursor: edge.collapsible ? "pointer" : "default" }} // 可折叠的显示手型光标
                       onClick={() => edge.collapsible && toggleCollapse(edge.id)} // 点击切换折叠状态
                     />
@@ -962,11 +989,15 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
                 const data = node.data as Goal;
                 const text = isGoal ? data.description : (node.data as Message).description || "";
 
-                let textColor = "#2196F3"; // 默认蓝色
+                let textColor = "#3b82f6"; // Blue 500
                 if (node.type === "message") {
-                  textColor = "#9E9E9E"; // 消息节点灰色
+                  textColor = "#64748b"; // Slate 500
                 } else if (node.type === "goal" && node.level === 0) {
-                  textColor = "#4CAF50"; // 主节点绿色
+                  textColor = "#10b981"; // Emerald 500
+                }
+
+                if (node.isInvalid) {
+                  textColor = "#94a3b8"; // Slate 400
                 }
 
                 return (
@@ -983,10 +1014,16 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
                       width={150}
                       height={50}
                       rx={8}
-                      fill={isGoal ? "#E3F2FD" : "#F5F5F5"} // 目标节点浅蓝色,消息节点灰色
-                      stroke={selectedNodeId === node.id ? "#2196F3" : node.isInvalid ? "#9E9E9E" : "#BDBDBD"} // 失效节点边框灰色
+                      fill={isGoal ? "#eff6ff" : "#f8fafc"} // Blue 50 / Slate 50
+                      stroke={selectedNodeId === node.id ? "#3b82f6" : node.isInvalid ? "#cbd5e1" : "#e2e8f0"} // Selected: Blue 500, Invalid: Slate 300, Default: Slate 200
                       strokeWidth={selectedNodeId === node.id ? 2 : 1}
                       strokeDasharray={node.isInvalid ? "5,5" : undefined} // 失效节点虚线边框
+                      style={{
+                        filter:
+                          selectedNodeId === node.id
+                            ? "drop-shadow(0 4px 6px rgb(59 130 246 / 0.3))"
+                            : "drop-shadow(0 1px 2px rgb(0 0 0 / 0.05))",
+                      }}
                     />
                     {/* 节点文本(带 Tooltip) */}
                     <foreignObject

+ 35 - 18
frontend/react-template/src/components/FlowChart/styles/FlowChart.module.css

@@ -1,8 +1,9 @@
 .container {
   width: 100%;
   height: 100%;
-  background: #fafafa;
+  background: var(--bg-app);
   position: relative;
+  overflow: hidden;
 }
 
 .scrollContainer {
@@ -13,46 +14,62 @@
 .svg {
   width: 100%;
   height: 100%;
-  background: #fff;
+  background-image: radial-gradient(var(--border-light) 1px, transparent 1px);
+  background-size: 20px 20px;
+  background-color: var(--bg-app);
   cursor: grab;
   overscroll-behavior: contain;
 }
+
 .panning {
   cursor: grabbing;
 }
+
 .links path {
-  transition: all 0.2s ease;
+  transition: all var(--transition-normal);
 }
+
 .nodes text {
   pointer-events: none;
 }
 
 .controls {
   position: absolute;
-  right: 16px;
-  bottom: 16px;
+  right: var(--space-lg);
+  bottom: var(--space-lg);
   display: flex;
-  gap: 8px;
-  background: rgba(255, 255, 255, 0.9);
-  border: 1px solid #e5e7eb;
-  border-radius: 10px;
-  padding: 6px;
-  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
+  gap: var(--space-sm);
+  background: var(--bg-surface);
+  border: 1px solid var(--border-light);
+  border-radius: var(--radius-lg);
+  padding: var(--space-xs);
+  box-shadow: var(--shadow-lg);
   z-index: 10;
 }
 
 .controlButton {
-  min-width: 36px;
+  min-width: 32px;
   height: 32px;
-  border: 1px solid #d0d7de;
-  border-radius: 8px;
-  background: #fff;
-  color: #333;
+  padding: 0 var(--space-sm);
+  border: 1px solid transparent;
+  border-radius: var(--radius-md);
+  background: transparent;
+  color: var(--text-secondary);
   font-size: 14px;
+  font-weight: 500;
   cursor: pointer;
-  position: relative;
+  transition: all var(--transition-fast);
+  display: flex;
+  align-items: center;
+  justify-content: center;
 }
 
 .controlButton:hover {
-  background: #f5f7fb;
+  background: var(--bg-surface-hover);
+  color: var(--text-primary);
+}
+
+.controlButton:active {
+  background: var(--border-light);
 }
+/* Removed old styles */

+ 45 - 44
frontend/react-template/src/components/MainContent/MainContent.module.css

@@ -3,86 +3,87 @@
   display: flex;
   flex-direction: column;
   overflow: hidden;
+  background: var(--bg-app);
 }
 
 .header {
-  padding: 12px 16px;
-  border-bottom: 1px solid var(--border-color, #e0e0e0);
+  height: var(--topbar-height);
+  padding: 0 var(--space-lg);
+  border-bottom: 1px solid var(--border-light);
   display: flex;
   align-items: center;
   justify-content: space-between;
-  background: #fff;
+  background: var(--bg-surface);
+  flex-shrink: 0;
+  box-shadow: var(--shadow-sm);
+  z-index: 5;
 }
 
 .title {
   font-size: 14px;
-  color: var(--text-primary, #333);
+  font-weight: 500;
+  color: var(--text-secondary);
 }
 
 .status {
   font-size: 12px;
-  color: #666;
+  color: var(--text-tertiary);
 }
 
 .headerRight {
   display: flex;
   align-items: center;
-  gap: 16px;
+  gap: var(--space-md);
 }
 
-.legend {
-  display: flex;
+.btn {
+  display: inline-flex;
   align-items: center;
-  gap: 12px;
+  justify-content: center;
+  height: 32px;
+  padding: 0 var(--space-md);
+  border-radius: var(--radius-md);
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all var(--transition-fast);
+  border: 1px solid var(--border-medium);
+  background: var(--bg-surface);
+  color: var(--text-secondary);
+  outline: none;
 }
 
-.legendItem {
-  display: flex;
-  align-items: center;
-  gap: 6px;
-  font-size: 12px;
-  color: #666;
+.btn:hover {
+  background: var(--bg-surface-hover);
+  color: var(--text-primary);
+  border-color: var(--border-medium);
 }
 
-.legendDot {
-  width: 10px;
-  height: 10px;
-  border-radius: 50%;
-  display: inline-block;
+.btn:active {
+  background: var(--border-light);
 }
 
 .content {
   flex: 1;
   overflow: hidden;
-  background: #fafafa;
-  display: flex;
-  flex-direction: column;
   position: relative;
+  background: var(--bg-app);
 }
 
-.empty {
-  color: #999;
+.loading {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  color: var(--text-tertiary);
   font-size: 14px;
-  margin: auto;
 }
 
-.buttons {
+.empty {
   display: flex;
-  gap: 8px;
-}
-
-.btn {
-  padding: 4px 12px;
-  border: 1px solid #d9d9d9;
-  border-radius: 4px;
-  background: #fff;
-  cursor: pointer;
-  font-size: 12px;
-  color: #333;
-  transition: all 0.3s;
-}
-
-.btn:hover {
-  color: #1890ff;
-  border-color: #1890ff;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  color: var(--text-tertiary);
+  font-size: 14px;
 }

+ 69 - 53
frontend/react-template/src/components/TopBar/TopBar.module.css

@@ -1,89 +1,105 @@
 .topbar {
-  height: 60px;
-  background: var(--bg-primary, #ffffff);
-  border-bottom: 1px solid var(--border-color, #e0e0e0);
+  height: var(--topbar-height);
+  background: var(--bg-surface);
+  border-bottom: 1px solid var(--border-light);
   display: flex;
   align-items: center;
   justify-content: space-between;
-  padding: 0 var(--spacing-lg, 24px);
+  padding: 0 var(--space-lg);
+  box-shadow: var(--shadow-sm);
+  z-index: 10;
 }
+
 .title {
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
+  display: flex;
+  align-items: center;
+  gap: var(--space-md);
   flex: 1;
+  min-width: 0;
 }
 
 .title h1 {
+  font-size: 18px;
+  font-weight: 600;
+  color: var(--text-primary);
+  margin: 0;
+  white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
-  white-space: nowrap;
-  font-size: var(--font-size-lg, 16px);
-  font-weight: 600;
-  color: var(--text-primary, #333333);
-  margin: 0 20px 0 0;
 }
 
-.filters {
+.actions {
   display: flex;
-  gap: var(--spacing-md, 16px);
+  gap: var(--space-sm);
+  align-items: center;
 }
 
-.select {
-  padding: 8px 12px;
-  border: 1px solid #ddd;
-  border-radius: 4px;
+.button {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  height: 32px;
+  padding: 0 var(--space-md);
+  border-radius: var(--radius-md);
   font-size: 14px;
-  background: white;
+  font-weight: 500;
   cursor: pointer;
-  min-width: 200px;
-}
-
-.select:disabled {
-  opacity: 0.6;
-  cursor: not-allowed;
+  transition: all var(--transition-fast);
+  border: 1px solid transparent;
+  outline: none;
+  white-space: nowrap;
 }
 
+/* Default Button (Secondary/Ghost) */
 .button {
-  padding: 8px 16px;
-  border: 1px solid #d9d9d9;
-  border-radius: 4px;
-  font-size: 14px;
-  background: #fff;
-  color: #666;
-  cursor: pointer;
-  transition: all 0.2s;
+  background: var(--bg-surface);
+  border-color: var(--border-medium);
+  color: var(--text-secondary);
 }
 
 .button:hover:not(:disabled) {
-  color: #0070f3;
-  border-color: #0070f3;
+  background: var(--bg-surface-hover);
+  color: var(--text-primary);
+  border-color: var(--border-medium);
 }
 
-.button:disabled {
-  opacity: 0.6;
-  cursor: not-allowed;
+.button:active:not(:disabled) {
+  background: var(--border-light);
+}
+
+/* Primary Button */
+.button.primary {
+  background: var(--color-primary);
+  color: var(--text-inverse);
+  border-color: transparent;
+}
+
+.button.primary:hover:not(:disabled) {
+  background: var(--color-primary-hover);
 }
 
-.buttonPrimary {
-  composes: button;
-  background: #0070f3;
-  color: white;
-  border-color: #0070f3;
+.button.primary:active:not(:disabled) {
+  background: var(--color-primary-active);
 }
 
-.buttonPrimary:hover:not(:disabled) {
-  background: #0051cc;
-  color: white;
+/* Danger Button */
+.button.danger {
+  background: var(--bg-surface);
+  color: var(--color-danger);
+  border-color: var(--color-danger);
 }
 
-.buttonDanger {
-  composes: button;
-  color: #f44336;
-  border-color: #f44336;
+.button.danger:hover:not(:disabled) {
+  background: #fef2f2; /* Red 50 */
 }
 
-.buttonDanger:hover:not(:disabled) {
-  background: #fff1f0;
-  color: #f44336;
+.button.danger:active:not(:disabled) {
+  background: #fee2e2; /* Red 100 */
+}
+
+.button:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+  pointer-events: none;
 }
+/* Removed old styles */

+ 3 - 4
frontend/react-template/src/components/TopBar/TopBar.tsx

@@ -227,9 +227,8 @@ export const TopBar: FC<TopBarProps> = ({
         title={title}
       >
         <h1>{title}</h1>
-        {/* <h1>{title.length > 50 ? `${title.slice(0, 50)}...` : title}</h1> */}
       </div>
-      <div className={styles.filters}>
+      <div className={styles.actions}>
         <button
           className={styles.button}
           onClick={handleNewTask}
@@ -237,13 +236,13 @@ export const TopBar: FC<TopBarProps> = ({
           新任务
         </button>
         <button
-          className={styles.buttonPrimary}
+          className={`${styles.button} ${styles.primary}`}
           onClick={handleRun}
         >
           插入
         </button>
         <button
-          className={styles.buttonDanger}
+          className={`${styles.button} ${styles.danger}`}
           onClick={handleStop}
         >
           停止

+ 28 - 0
frontend/react-template/src/styles/global.css

@@ -27,12 +27,19 @@ body {
   height: 100%;
   display: flex;
   flex-direction: column;
+  background-color: var(--bg-app);
+}
+
+.app-top {
+  flex: 0 0 auto;
+  z-index: 20;
 }
 
 .app-body {
   flex: 1;
   display: flex;
   overflow: hidden;
+  position: relative;
 }
 
 .app-main {
@@ -40,12 +47,33 @@ body {
   min-width: 0;
   display: flex;
   flex-direction: column;
+  position: relative;
 }
+
 .app-right {
   flex: 0 0 auto;
   min-width: 0;
   display: flex;
   flex-direction: column;
+  background: var(--bg-panel);
+  z-index: 10;
+}
+
+.app-splitter {
+  width: 1px;
+  cursor: col-resize;
+  background-color: var(--border-light);
+  transition: background-color 0.2s;
+  position: relative;
+  z-index: 20;
+}
+
+.app-splitter:hover,
+.app-splitter:active {
+  background-color: var(--color-primary);
+  width: 4px; /* Make it easier to grab visually */
+  margin-left: -1.5px; /* Center alignment adjustment */
+  margin-right: -1.5px;
 }
 
 .app-splitter {

+ 60 - 6
frontend/react-template/src/styles/variables.css

@@ -1,8 +1,62 @@
 :root {
-  --bg-primary: #ffffff;
-  --border-color: #e0e0e0;
-  --text-primary: #333333;
-  --spacing-md: 16px;
-  --spacing-lg: 24px;
-  --font-size-lg: 16px;
+  /* Brand Colors */
+  --color-primary: #3b82f6; /* Blue 500 */
+  --color-primary-hover: #2563eb; /* Blue 600 */
+  --color-primary-active: #1d4ed8; /* Blue 700 */
+  --color-secondary: #64748b; /* Slate 500 */
+
+  /* Semantic Colors */
+  --color-success: #10b981; /* Emerald 500 */
+  --color-warning: #f59e0b; /* Amber 500 */
+  --color-danger: #ef4444; /* Red 500 */
+  --color-info: #3b82f6; /* Blue 500 */
+
+  /* Background Colors */
+  --bg-app: #f8fafc; /* Slate 50 */
+  --bg-surface: #ffffff;
+  --bg-surface-hover: #f1f5f9; /* Slate 100 */
+  --bg-panel: #ffffff;
+  --bg-node: #ffffff;
+  --bg-node-goal: #eff6ff; /* Blue 50 */
+  --bg-node-message: #f8fafc; /* Slate 50 */
+
+  /* Text Colors */
+  --text-primary: #0f172a; /* Slate 900 */
+  --text-secondary: #475569; /* Slate 600 */
+  --text-tertiary: #94a3b8; /* Slate 400 */
+  --text-inverse: #ffffff;
+
+  /* Border Colors */
+  --border-light: #e2e8f0; /* Slate 200 */
+  --border-medium: #cbd5e1; /* Slate 300 */
+  --border-focus: #3b82f6; /* Blue 500 */
+
+  /* Spacing */
+  --space-xs: 4px;
+  --space-sm: 8px;
+  --space-md: 16px;
+  --space-lg: 24px;
+  --space-xl: 32px;
+  --space-2xl: 48px;
+
+  /* Radius */
+  --radius-sm: 4px;
+  --radius-md: 8px;
+  --radius-lg: 12px;
+  --radius-xl: 16px;
+  --radius-full: 9999px;
+
+  /* Shadows */
+  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
+  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
+  --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
+
+  /* Transitions */
+  --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
+  --transition-normal: 300ms cubic-bezier(0.4, 0, 0.2, 1);
+
+  /* Layout */
+  --topbar-height: 64px;
+  --panel-width: 400px;
 }