diff options
| author | bndw <ben@bdw.to> | 2026-02-28 21:49:29 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-28 21:49:29 -0800 |
| commit | 454453788e759fa16442e755434fbb842fa1acab (patch) | |
| tree | 38cff99c9cd9c625f549d7f05d435d0b3fd04479 /renderer | |
| parent | 0484d97dfbc3b8a2e7878d3ab35a9895decdf467 (diff) | |
feat: **`globals.css`** — Edit `.chat-pane`: remove `width: 3… (+7 more)
- ✅ **`globals.css`** — Edit `.chat-pane`: remove `width: 380px`, add `min-width: 0`, `overflow: hidden`, `transition: width 0.2s ease`
- ✅ **`globals.css`** — Insert `.chat-resize-handle`, `.chat-header`, `.chat-collapse-btn` rules after `.chat-pane` block
- ✅ **`globals.css`** — Append `::-webkit-scrollbar` rules at end of file
- ✅ **`ChatPane.tsx`** — Full rewrite: new props interface, header strip, conditional body, inline width style
- ✅ **`App.tsx`** — Add `chatWidth` + `chatCollapsed` state with lazy localStorage initialisers
- ✅ **`App.tsx`** — Add two `useEffect` persistence hooks
- ✅ **`App.tsx`** — Add `handleResizeMouseDown` function
- ✅ **`App.tsx`** — Update `.main-content` JSX: insert conditional resize handle div, pass new props to `<ChatPane>`
Diffstat (limited to 'renderer')
| -rw-r--r-- | renderer/src/App.tsx | 42 | ||||
| -rw-r--r-- | renderer/src/components/ChatPane.tsx | 80 | ||||
| -rw-r--r-- | renderer/src/styles/globals.css | 77 |
3 files changed, 169 insertions, 30 deletions
diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx index 7d75196..c3eafd4 100644 --- a/renderer/src/App.tsx +++ b/renderer/src/App.tsx | |||
| @@ -63,15 +63,47 @@ export function App() { | |||
| 63 | () => (localStorage.getItem("cf-theme") as Theme) ?? "dark" | 63 | () => (localStorage.getItem("cf-theme") as Theme) ?? "dark" |
| 64 | ); | 64 | ); |
| 65 | 65 | ||
| 66 | const [chatWidth, setChatWidth] = useState<number>( | ||
| 67 | () => Number(localStorage.getItem("cf-chat-width")) || 380 | ||
| 68 | ); | ||
| 69 | const [chatCollapsed, setChatCollapsed] = useState<boolean>( | ||
| 70 | () => localStorage.getItem("cf-chat-collapsed") === "true" | ||
| 71 | ); | ||
| 72 | |||
| 66 | // Keep document.documentElement in sync and persist to localStorage | 73 | // Keep document.documentElement in sync and persist to localStorage |
| 67 | useEffect(() => { | 74 | useEffect(() => { |
| 68 | document.documentElement.setAttribute("data-theme", theme); | 75 | document.documentElement.setAttribute("data-theme", theme); |
| 69 | localStorage.setItem("cf-theme", theme); | 76 | localStorage.setItem("cf-theme", theme); |
| 70 | }, [theme]); | 77 | }, [theme]); |
| 71 | 78 | ||
| 79 | useEffect(() => { | ||
| 80 | localStorage.setItem("cf-chat-width", String(chatWidth)); | ||
| 81 | }, [chatWidth]); | ||
| 82 | |||
| 83 | useEffect(() => { | ||
| 84 | localStorage.setItem("cf-chat-collapsed", String(chatCollapsed)); | ||
| 85 | }, [chatCollapsed]); | ||
| 86 | |||
| 72 | const handleToggleTheme = () => | 87 | const handleToggleTheme = () => |
| 73 | setTheme((t) => (t === "dark" ? "light" : "dark")); | 88 | setTheme((t) => (t === "dark" ? "light" : "dark")); |
| 74 | 89 | ||
| 90 | const handleResizeMouseDown = (e: React.MouseEvent) => { | ||
| 91 | e.preventDefault(); | ||
| 92 | const startX = e.clientX; | ||
| 93 | const startWidth = chatWidth; | ||
| 94 | const onMove = (ev: MouseEvent) => { | ||
| 95 | const delta = startX - ev.clientX; // drag left = wider chat | ||
| 96 | const next = Math.max(180, Math.min(700, startWidth + delta)); | ||
| 97 | setChatWidth(next); | ||
| 98 | }; | ||
| 99 | const onUp = () => { | ||
| 100 | document.removeEventListener("mousemove", onMove); | ||
| 101 | document.removeEventListener("mouseup", onUp); | ||
| 102 | }; | ||
| 103 | document.addEventListener("mousemove", onMove); | ||
| 104 | document.addEventListener("mouseup", onUp); | ||
| 105 | }; | ||
| 106 | |||
| 75 | const hasChanges = documentContent !== originalContent; | 107 | const hasChanges = documentContent !== originalContent; |
| 76 | 108 | ||
| 77 | // Clear error after 5 seconds | 109 | // Clear error after 5 seconds |
| @@ -380,6 +412,13 @@ export function App() { | |||
| 380 | theme={theme} | 412 | theme={theme} |
| 381 | /> | 413 | /> |
| 382 | 414 | ||
| 415 | {!chatCollapsed && ( | ||
| 416 | <div | ||
| 417 | className="chat-resize-handle" | ||
| 418 | onMouseDown={handleResizeMouseDown} | ||
| 419 | /> | ||
| 420 | )} | ||
| 421 | |||
| 383 | <ChatPane | 422 | <ChatPane |
| 384 | messages={messages} | 423 | messages={messages} |
| 385 | onSend={handleSendMessage} | 424 | onSend={handleSendMessage} |
| @@ -390,6 +429,9 @@ export function App() { | |||
| 390 | ? `Chat with Claude (${selectedSession.phase})...` | 429 | ? `Chat with Claude (${selectedSession.phase})...` |
| 391 | : "Select a session to start" | 430 | : "Select a session to start" |
| 392 | } | 431 | } |
| 432 | collapsed={chatCollapsed} | ||
| 433 | chatWidth={chatWidth} | ||
| 434 | onToggleCollapse={() => setChatCollapsed((c) => !c)} | ||
| 393 | /> | 435 | /> |
| 394 | </div> | 436 | </div> |
| 395 | 437 | ||
diff --git a/renderer/src/components/ChatPane.tsx b/renderer/src/components/ChatPane.tsx index 917d462..40e682c 100644 --- a/renderer/src/components/ChatPane.tsx +++ b/renderer/src/components/ChatPane.tsx | |||
| @@ -7,6 +7,9 @@ interface ChatPaneProps { | |||
| 7 | isLoading: boolean; | 7 | isLoading: boolean; |
| 8 | disabled: boolean; | 8 | disabled: boolean; |
| 9 | placeholder: string; | 9 | placeholder: string; |
| 10 | collapsed: boolean; | ||
| 11 | chatWidth: number; | ||
| 12 | onToggleCollapse: () => void; | ||
| 10 | } | 13 | } |
| 11 | 14 | ||
| 12 | export function ChatPane({ | 15 | export function ChatPane({ |
| @@ -15,6 +18,9 @@ export function ChatPane({ | |||
| 15 | isLoading, | 18 | isLoading, |
| 16 | disabled, | 19 | disabled, |
| 17 | placeholder, | 20 | placeholder, |
| 21 | collapsed, | ||
| 22 | chatWidth, | ||
| 23 | onToggleCollapse, | ||
| 18 | }: ChatPaneProps) { | 24 | }: ChatPaneProps) { |
| 19 | const [input, setInput] = useState(""); | 25 | const [input, setInput] = useState(""); |
| 20 | const messagesEndRef = useRef<HTMLDivElement>(null); | 26 | const messagesEndRef = useRef<HTMLDivElement>(null); |
| @@ -30,37 +36,53 @@ export function ChatPane({ | |||
| 30 | }; | 36 | }; |
| 31 | 37 | ||
| 32 | return ( | 38 | return ( |
| 33 | <div className="chat-pane"> | 39 | <div |
| 34 | <div className="chat-messages"> | 40 | className="chat-pane" |
| 35 | {messages.map((msg) => ( | 41 | style={{ width: collapsed ? 28 : chatWidth }} |
| 36 | <div key={msg.id} className={`message ${msg.role}`}> | 42 | > |
| 37 | <div className="message-content">{msg.content}</div> | 43 | <div className="chat-header"> |
| 38 | </div> | 44 | {!collapsed && <span>Chat</span>} |
| 39 | ))} | 45 | <button className="chat-collapse-btn" onClick={onToggleCollapse}> |
| 40 | {isLoading && ( | 46 | {collapsed ? "⟨" : "⟩"} |
| 41 | <div className="message assistant loading"> | ||
| 42 | <div className="message-content">Thinking...</div> | ||
| 43 | </div> | ||
| 44 | )} | ||
| 45 | <div ref={messagesEndRef} /> | ||
| 46 | </div> | ||
| 47 | |||
| 48 | <div className="chat-input"> | ||
| 49 | <input | ||
| 50 | type="text" | ||
| 51 | value={input} | ||
| 52 | onChange={(e) => setInput(e.target.value)} | ||
| 53 | onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && handleSend()} | ||
| 54 | placeholder={placeholder} | ||
| 55 | disabled={disabled || isLoading} | ||
| 56 | /> | ||
| 57 | <button | ||
| 58 | onClick={handleSend} | ||
| 59 | disabled={disabled || isLoading || !input.trim()} | ||
| 60 | > | ||
| 61 | Send | ||
| 62 | </button> | 47 | </button> |
| 63 | </div> | 48 | </div> |
| 49 | |||
| 50 | {!collapsed && ( | ||
| 51 | <> | ||
| 52 | <div className="chat-messages"> | ||
| 53 | {messages.map((msg) => ( | ||
| 54 | <div key={msg.id} className={`message ${msg.role}`}> | ||
| 55 | <div className="message-content">{msg.content}</div> | ||
| 56 | </div> | ||
| 57 | ))} | ||
| 58 | {isLoading && ( | ||
| 59 | <div className="message assistant loading"> | ||
| 60 | <div className="message-content">Thinking...</div> | ||
| 61 | </div> | ||
| 62 | )} | ||
| 63 | <div ref={messagesEndRef} /> | ||
| 64 | </div> | ||
| 65 | |||
| 66 | <div className="chat-input"> | ||
| 67 | <input | ||
| 68 | type="text" | ||
| 69 | value={input} | ||
| 70 | onChange={(e) => setInput(e.target.value)} | ||
| 71 | onKeyDown={(e) => | ||
| 72 | e.key === "Enter" && !e.shiftKey && handleSend() | ||
| 73 | } | ||
| 74 | placeholder={placeholder} | ||
| 75 | disabled={disabled || isLoading} | ||
| 76 | /> | ||
| 77 | <button | ||
| 78 | onClick={handleSend} | ||
| 79 | disabled={disabled || isLoading || !input.trim()} | ||
| 80 | > | ||
| 81 | Send | ||
| 82 | </button> | ||
| 83 | </div> | ||
| 84 | </> | ||
| 85 | )} | ||
| 64 | </div> | 86 | </div> |
| 65 | ); | 87 | ); |
| 66 | } | 88 | } |
diff --git a/renderer/src/styles/globals.css b/renderer/src/styles/globals.css index 8e43f3a..03c7443 100644 --- a/renderer/src/styles/globals.css +++ b/renderer/src/styles/globals.css | |||
| @@ -408,10 +408,57 @@ html[data-theme="light"] .session-rename-input { | |||
| 408 | 408 | ||
| 409 | /* ── Chat Pane ───────────────────────────────────────────────── */ | 409 | /* ── Chat Pane ───────────────────────────────────────────────── */ |
| 410 | .chat-pane { | 410 | .chat-pane { |
| 411 | width: 380px; | ||
| 412 | display: flex; | 411 | display: flex; |
| 413 | flex-direction: column; | 412 | flex-direction: column; |
| 414 | background: var(--bg-secondary); | 413 | background: var(--bg-secondary); |
| 414 | min-width: 0; | ||
| 415 | overflow: hidden; | ||
| 416 | transition: width 0.2s ease; | ||
| 417 | } | ||
| 418 | |||
| 419 | /* ── Chat Resize Handle ──────────────────────────────────────── */ | ||
| 420 | .chat-resize-handle { | ||
| 421 | width: 5px; | ||
| 422 | background: transparent; | ||
| 423 | cursor: col-resize; | ||
| 424 | flex-shrink: 0; | ||
| 425 | transition: background 0.15s; | ||
| 426 | } | ||
| 427 | |||
| 428 | .chat-resize-handle:hover { | ||
| 429 | background: var(--accent); | ||
| 430 | } | ||
| 431 | |||
| 432 | /* ── Chat Header Strip ───────────────────────────────────────── */ | ||
| 433 | .chat-header { | ||
| 434 | display: flex; | ||
| 435 | justify-content: space-between; | ||
| 436 | align-items: center; | ||
| 437 | padding: 7px 12px; | ||
| 438 | background: var(--bg-secondary); | ||
| 439 | border-bottom: 1px solid var(--border); | ||
| 440 | font-size: 11px; | ||
| 441 | letter-spacing: 0.07em; | ||
| 442 | text-transform: uppercase; | ||
| 443 | color: var(--text-secondary); | ||
| 444 | flex-shrink: 0; | ||
| 445 | } | ||
| 446 | |||
| 447 | .chat-collapse-btn { | ||
| 448 | background: transparent; | ||
| 449 | border: 1px solid var(--border); | ||
| 450 | border-radius: 2px; | ||
| 451 | color: var(--text-secondary); | ||
| 452 | cursor: pointer; | ||
| 453 | font-size: 12px; | ||
| 454 | padding: 2px 7px; | ||
| 455 | font-family: inherit; | ||
| 456 | transition: background 0.15s, color 0.15s; | ||
| 457 | } | ||
| 458 | |||
| 459 | .chat-collapse-btn:hover { | ||
| 460 | background: var(--bg-tertiary); | ||
| 461 | color: var(--text-primary); | ||
| 415 | } | 462 | } |
| 416 | 463 | ||
| 417 | .chat-messages { | 464 | .chat-messages { |
| @@ -998,3 +1045,31 @@ html[data-theme="light"] .settings-textarea:focus { | |||
| 998 | background: var(--bg-tertiary); | 1045 | background: var(--bg-tertiary); |
| 999 | color: var(--text-primary); | 1046 | color: var(--text-primary); |
| 1000 | } | 1047 | } |
| 1048 | |||
| 1049 | /* ── Scrollbars ──────────────────────────────────────────────── */ | ||
| 1050 | .chat-messages::-webkit-scrollbar, | ||
| 1051 | .document-content::-webkit-scrollbar, | ||
| 1052 | .cm-scroller::-webkit-scrollbar { | ||
| 1053 | width: 8px; | ||
| 1054 | height: 8px; | ||
| 1055 | } | ||
| 1056 | |||
| 1057 | .chat-messages::-webkit-scrollbar-track, | ||
| 1058 | .document-content::-webkit-scrollbar-track, | ||
| 1059 | .cm-scroller::-webkit-scrollbar-track { | ||
| 1060 | background: var(--bg-primary); | ||
| 1061 | } | ||
| 1062 | |||
| 1063 | .chat-messages::-webkit-scrollbar-thumb, | ||
| 1064 | .document-content::-webkit-scrollbar-thumb, | ||
| 1065 | .cm-scroller::-webkit-scrollbar-thumb { | ||
| 1066 | background: var(--bg-tertiary); | ||
| 1067 | border-radius: 4px; | ||
| 1068 | border: 2px solid var(--bg-primary); | ||
| 1069 | } | ||
| 1070 | |||
| 1071 | .chat-messages::-webkit-scrollbar-thumb:hover, | ||
| 1072 | .document-content::-webkit-scrollbar-thumb:hover, | ||
| 1073 | .cm-scroller::-webkit-scrollbar-thumb:hover { | ||
| 1074 | background: var(--border); | ||
| 1075 | } | ||
