| author | |
| committer | |
| log | 1ab32562049fdb51de7ffc3a7cc8af19f721a306 |
| tree | a2d0e4dd5a28405cec241ec11b5b2ebaf63c719f |
| parent | 377d21211a6b2873437e488590da13d81fa9e926 |
| signature | Commit is signed but in an unrecognized format. |
10 files changed, 394 insertions(+), 97 deletions(-)
apps/renderer/index.html+1-1| ... | ... | @@ -5,7 +5,7 @@ |
| 5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| 6 | 6 | <title>Electron Todo</title> |
| 7 | 7 | </head> |
| 8 | <body class="bg-slate-100"> | |
| 8 | <body> | |
| 9 | 9 | <div id="root"></div> |
| 10 | 10 | <script type="module" src="/src/main.tsx"></script> |
| 11 | 11 | </body> |
apps/renderer/package.json+1| ... | ... | @@ -13,6 +13,7 @@ |
| 13 | 13 | "dependencies": { |
| 14 | 14 | "@acme/contracts": "workspace:*", |
| 15 | 15 | "highlight.js": "^11.11.1", |
| 16 | "lucide-react": "^0.563.0", | |
| 16 | 17 | "react": "^19.0.0", |
| 17 | 18 | "react-dom": "^19.0.0", |
| 18 | 19 | "react-markdown": "^10.1.0", |
apps/renderer/src/App.tsx+3-3| ... | ... | @@ -31,10 +31,10 @@ function Layout() { |
| 31 | 31 | |
| 32 | 32 | if (!api) { |
| 33 | 33 | return ( |
| 34 | <div className="flex h-screen flex-col bg-[#0c0c0c] text-[#e0e0e0]"> | |
| 34 | <div className="flex h-screen flex-col bg-background text-foreground"> | |
| 35 | 35 | <div className="drag-region h-[52px] shrink-0" /> |
| 36 | 36 | <div className="flex flex-1 items-center justify-center"> |
| 37 | <p className="text-sm text-[#a0a0a0]/60"> | |
| 37 | <p className="text-sm text-muted-foreground"> | |
| 38 | 38 | Native bridge unavailable. Launch through Electron. |
| 39 | 39 | </p> |
| 40 | 40 | </div> |
| ... | ... | @@ -43,7 +43,7 @@ function Layout() { |
| 43 | 43 | } |
| 44 | 44 | |
| 45 | 45 | return ( |
| 46 | <div className="flex h-screen overflow-hidden bg-[#0c0c0c] text-[#e0e0e0]"> | |
| 46 | <div className="flex h-screen overflow-hidden bg-background text-foreground"> | |
| 47 | 47 | <EventRouter /> |
| 48 | 48 | <Sidebar /> |
| 49 | 49 | <ChatView /> |
apps/renderer/src/components/ChatMarkdown.tsx+1-1| ... | ... | @@ -15,7 +15,7 @@ const markdownComponents: Components = { |
| 15 | 15 | |
| 16 | 16 | export default function ChatMarkdown({ text }: ChatMarkdownProps) { |
| 17 | 17 | return ( |
| 18 | <div className="chat-markdown text-sm leading-relaxed text-[#d0d0d0]"> | |
| 18 | <div className="chat-markdown text-sm leading-relaxed text-foreground/80"> | |
| 19 | 19 | <ReactMarkdown |
| 20 | 20 | remarkPlugins={[remarkGfm]} |
| 21 | 21 | rehypePlugins={[[rehypeHighlight, { detect: true, ignoreMissing: true }]]} |
apps/renderer/src/components/ChatView.tsx+43-43| ... | ... | @@ -47,10 +47,10 @@ function editorLabel(editor: (typeof EDITORS)[number]): string { |
| 47 | 47 | const LAST_EDITOR_KEY = "codething:last-editor"; |
| 48 | 48 | |
| 49 | 49 | function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string { |
| 50 | if (tone === "error") return "text-rose-300/50"; | |
| 51 | if (tone === "tool") return "text-[#8a8a8a]"; | |
| 52 | if (tone === "thinking") return "text-[#707070]"; | |
| 53 | return "text-[#606060]"; | |
| 50 | if (tone === "error") return "text-rose-300/50 dark:text-rose-300/50"; | |
| 51 | if (tone === "tool") return "text-muted-foreground/70"; | |
| 52 | if (tone === "thinking") return "text-muted-foreground/50"; | |
| 53 | return "text-muted-foreground/40"; | |
| 54 | 54 | } |
| 55 | 55 | |
| 56 | 56 | interface PendingApprovalCard { |
| ... | ... | @@ -488,7 +488,7 @@ export default function ChatView() { |
| 488 | 488 | // Empty state: no active thread |
| 489 | 489 | if (!activeThread) { |
| 490 | 490 | return ( |
| 491 | <div className="flex flex-1 flex-col bg-[#0c0c0c] text-[#a0a0a0]/40"> | |
| 491 | <div className="flex flex-1 flex-col bg-background text-muted-foreground/40"> | |
| 492 | 492 | <div className="drag-region h-[52px] shrink-0" /> |
| 493 | 493 | <div className="flex flex-1 items-center justify-center"> |
| 494 | 494 | <div className="text-center"> |
| ... | ... | @@ -500,11 +500,11 @@ export default function ChatView() { |
| 500 | 500 | } |
| 501 | 501 | |
| 502 | 502 | return ( |
| 503 | <div className="flex flex-1 flex-col bg-[#0c0c0c]"> | |
| 503 | <div className="flex flex-1 flex-col bg-background"> | |
| 504 | 504 | {/* Top bar */} |
| 505 | <header className="drag-region flex items-center justify-between border-b border-white/8 px-5 pt-[28px] pb-3"> | |
| 505 | <header className="drag-region flex items-center justify-between border-b border-border px-5 pt-[28px] pb-3"> | |
| 506 | 506 | <div className="flex items-center gap-3"> |
| 507 | <h2 className="text-sm font-medium text-[#e0e0e0]"> | |
| 507 | <h2 className="text-sm font-medium text-foreground"> | |
| 508 | 508 | {activeThread.title} |
| 509 | 509 | </h2> |
| 510 | 510 | </div> |
| ... | ... | @@ -514,23 +514,23 @@ export default function ChatView() { |
| 514 | 514 | <div className="relative" ref={editorMenuRef}> |
| 515 | 515 | <button |
| 516 | 516 | type="button" |
| 517 | className="rounded-md px-2 py-1 text-[10px] text-[#a0a0a0]/40 transition-colors duration-150 hover:text-[#a0a0a0]/60" | |
| 517 | className="rounded-md px-2 py-1 text-[10px] text-muted-foreground/40 transition-colors duration-150 hover:text-muted-foreground/60" | |
| 518 | 518 | onClick={() => setIsEditorMenuOpen((v) => !v)} |
| 519 | 519 | > |
| 520 | 520 | Open in&hellip; |
| 521 | 521 | </button> |
| 522 | 522 | {isEditorMenuOpen && ( |
| 523 | <div className="absolute right-0 top-full z-50 mt-1 min-w-[120px] rounded-md border border-white/[0.08] bg-[#1b1b1d] py-1 shadow-xl"> | |
| 523 | <div className="absolute right-0 top-full z-50 mt-1 min-w-[120px] rounded-md border border-border bg-popover py-1 shadow-xl"> | |
| 524 | 524 | {EDITORS.map((editor) => ( |
| 525 | 525 | <button |
| 526 | 526 | key={editor.id} |
| 527 | 527 | type="button" |
| 528 | className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[11px] text-[#e0e0e0] hover:bg-white/[0.06]" | |
| 528 | className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[11px] text-foreground hover:bg-accent" | |
| 529 | 529 | onClick={() => openInEditor(editor.id)} |
| 530 | 530 | > |
| 531 | 531 | {editorLabel(editor)} |
| 532 | 532 | {editor.id === lastEditor && ( |
| 533 | <kbd className="ml-auto text-[9px] text-[#a0a0a0]/40"> | |
| 533 | <kbd className="ml-auto text-[9px] text-muted-foreground/40"> | |
| 534 | 534 | {navigator.platform.includes("Mac") |
| 535 | 535 | ? "\u2318O" |
| 536 | 536 | : "Ctrl+O"} |
| ... | ... | @@ -547,8 +547,8 @@ export default function ChatView() { |
| 547 | 547 | type="button" |
| 548 | 548 | className={`rounded-md px-2 py-1 text-[10px] transition-colors duration-150 ${ |
| 549 | 549 | state.diffOpen |
| 550 | ? "bg-white/10 text-white" | |
| 551 | : "text-[#a0a0a0]/40 hover:text-[#a0a0a0]/60" | |
| 550 | ? "bg-accent text-foreground" | |
| 551 | : "text-muted-foreground/40 hover:text-muted-foreground/60" | |
| 552 | 552 | }`} |
| 553 | 553 | onClick={() => dispatch({ type: "TOGGLE_DIFF" })} |
| 554 | 554 | > |
| ... | ... | @@ -591,7 +591,7 @@ export default function ChatView() { |
| 591 | 591 | <div className="mt-2 flex flex-wrap gap-1.5"> |
| 592 | 592 | <button |
| 593 | 593 | type="button" |
| 594 | className="rounded-md border border-white/[0.15] bg-white/[0.08] px-2 py-1 text-[11px] text-[#e8e8e8] transition-colors duration-150 hover:bg-white/[0.13] disabled:cursor-not-allowed disabled:opacity-50" | |
| 594 | className="rounded-md border border-border bg-accent px-2 py-1 text-[11px] text-foreground transition-colors duration-150 hover:bg-accent/80 disabled:cursor-not-allowed disabled:opacity-50" | |
| 595 | 595 | disabled={isResponding} |
| 596 | 596 | onClick={() => |
| 597 | 597 | void onRespondToApproval(approval.requestId, "accept") |
| ... | ... | @@ -614,7 +614,7 @@ export default function ChatView() { |
| 614 | 614 | </button> |
| 615 | 615 | <button |
| 616 | 616 | type="button" |
| 617 | className="rounded-md border border-white/[0.15] px-2 py-1 text-[11px] text-[#d8d8d8] transition-colors duration-150 hover:bg-white/[0.06] disabled:cursor-not-allowed disabled:opacity-50" | |
| 617 | className="rounded-md border border-border px-2 py-1 text-[11px] text-foreground/90 transition-colors duration-150 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50" | |
| 618 | 618 | disabled={isResponding} |
| 619 | 619 | onClick={() => |
| 620 | 620 | void onRespondToApproval(approval.requestId, "decline") |
| ... | ... | @@ -643,7 +643,7 @@ export default function ChatView() { |
| 643 | 643 | <div className="flex-1 overflow-y-auto px-5 py-4"> |
| 644 | 644 | {activeThread.messages.length === 0 && !isWorking ? ( |
| 645 | 645 | <div className="flex h-full items-center justify-center"> |
| 646 | <p className="text-sm text-[#a0a0a0]/30">Send a message to start the conversation.</p> | |
| 646 | <p className="text-sm text-muted-foreground/30">Send a message to start the conversation.</p> | |
| 647 | 647 | </div> |
| 648 | 648 | ) : ( |
| 649 | 649 | <div className="mx-auto max-w-3xl space-y-4"> |
| ... | ... | @@ -654,18 +654,18 @@ export default function ChatView() { |
| 654 | 654 | (completionDividerBeforeEntryId === timelineEntry.id || |
| 655 | 655 | timelineEntries[index - 1]?.kind === "work") && ( |
| 656 | 656 | <div className="my-3 flex items-center gap-3"> |
| 657 | <span className="h-px flex-1 bg-white/[0.1]" /> | |
| 658 | <span className="rounded-full border border-white/[0.12] bg-[#121212] px-2.5 py-1 text-[10px] uppercase tracking-[0.14em] text-[#9a9a9a]/80"> | |
| 657 | <span className="h-px flex-1 bg-border" /> | |
| 658 | <span className="rounded-full border border-border bg-background px-2.5 py-1 text-[10px] uppercase tracking-[0.14em] text-muted-foreground/80"> | |
| 659 | 659 | {completionSummary |
| 660 | 660 | ? `Response • ${completionSummary}` |
| 661 | 661 | : "Response"} |
| 662 | 662 | </span> |
| 663 | <span className="h-px flex-1 bg-white/[0.1]" /> | |
| 663 | <span className="h-px flex-1 bg-border" /> | |
| 664 | 664 | </div> |
| 665 | 665 | )} |
| 666 | 666 | {timelineEntry.kind === "work" ? ( |
| 667 | 667 | <div className="flex items-start gap-2 py-0.5 pl-1.5"> |
| 668 | <span className="mt-[7px] h-1.5 w-1.5 shrink-0 rounded-full bg-white/[0.18]" /> | |
| 668 | <span className="mt-[7px] h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/30" /> | |
| 669 | 669 | <p |
| 670 | 670 | className={`py-[2px] text-[11px] leading-relaxed ${workToneClass(timelineEntry.entry.tone)}`} |
| 671 | 671 | > |
| ... | ... | @@ -686,11 +686,11 @@ export default function ChatView() { |
| 686 | 686 | </div> |
| 687 | 687 | ) : timelineEntry.message.role === "user" ? ( |
| 688 | 688 | <div className="flex justify-end"> |
| 689 | <div className="max-w-[80%] rounded-2xl rounded-br-sm border border-white/[0.08] bg-white/[0.05] px-4 py-3"> | |
| 690 | <pre className="whitespace-pre-wrap break-words font-mono text-sm leading-relaxed text-[#e0e0e0]"> | |
| 689 | <div className="max-w-[80%] rounded-2xl rounded-br-sm border border-border bg-secondary px-4 py-3"> | |
| 690 | <pre className="whitespace-pre-wrap break-words font-mono text-sm leading-relaxed text-foreground"> | |
| 691 | 691 | {timelineEntry.message.text} |
| 692 | 692 | </pre> |
| 693 | <p className="mt-1.5 text-right text-[10px] text-[#a0a0a0]/30"> | |
| 693 | <p className="mt-1.5 text-right text-[10px] text-muted-foreground/30"> | |
| 694 | 694 | {formatTimestamp(timelineEntry.message.createdAt)} |
| 695 | 695 | </p> |
| 696 | 696 | </div> |
| ... | ... | @@ -717,7 +717,7 @@ export default function ChatView() { |
| 717 | 717 | </span> |
| 718 | 718 | </div> |
| 719 | 719 | )} |
| 720 | <p className="mt-1.5 text-[10px] text-[#a0a0a0]/30"> | |
| 720 | <p className="mt-1.5 text-[10px] text-muted-foreground/30"> | |
| 721 | 721 | {formatMessageMeta( |
| 722 | 722 | timelineEntry.message.createdAt, |
| 723 | 723 | timelineEntry.message.streaming |
| ... | ... | @@ -739,12 +739,12 @@ export default function ChatView() { |
| 739 | 739 | ))} |
| 740 | 740 | {isWorking && ( |
| 741 | 741 | <div className="flex items-center gap-2 py-0.5 pl-1.5"> |
| 742 | <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-white/[0.18]" /> | |
| 742 | <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/30" /> | |
| 743 | 743 | <div className="flex items-center pt-1"> |
| 744 | 744 | <span className="inline-flex items-center gap-[3px]"> |
| 745 | <span className="h-1 w-1 rounded-full bg-white/20 animate-pulse" /> | |
| 746 | <span className="h-1 w-1 rounded-full bg-white/20 animate-pulse [animation-delay:200ms]" /> | |
| 747 | <span className="h-1 w-1 rounded-full bg-white/20 animate-pulse [animation-delay:400ms]" /> | |
| 745 | <span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse" /> | |
| 746 | <span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse [animation-delay:200ms]" /> | |
| 747 | <span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse [animation-delay:400ms]" /> | |
| 748 | 748 | </span> |
| 749 | 749 | </div> |
| 750 | 750 | </div> |
| ... | ... | @@ -757,12 +757,12 @@ export default function ChatView() { |
| 757 | 757 | {/* Input bar */} |
| 758 | 758 | <div className="px-5 pb-4 pt-2"> |
| 759 | 759 | <form onSubmit={onSend} className="mx-auto max-w-3xl"> |
| 760 | <div className="group rounded-[20px] border border-white/8 bg-[#141416] transition-colors duration-200 focus-within:border-white/16"> | |
| 760 | <div className="group rounded-[20px] border border-border bg-card transition-colors duration-200 focus-within:border-ring"> | |
| 761 | 761 | {/* Textarea area */} |
| 762 | 762 | <div className="px-4 pt-4 pb-2"> |
| 763 | 763 | <textarea |
| 764 | 764 | ref={textareaRef} |
| 765 | className="w-full resize-none bg-transparent text-[14px] leading-relaxed text-[#e0e0e0] placeholder:text-[#a0a0a0]/35 focus:outline-none" | |
| 765 | className="w-full resize-none bg-transparent text-[14px] leading-relaxed text-foreground placeholder:text-muted-foreground/35 focus:outline-none" | |
| 766 | 766 | rows={2} |
| 767 | 767 | value={prompt} |
| 768 | 768 | onChange={(e) => setPrompt(e.target.value)} |
| ... | ... | @@ -781,7 +781,7 @@ export default function ChatView() { |
| 781 | 781 | <div className="relative" ref={modelMenuRef}> |
| 782 | 782 | <button |
| 783 | 783 | type="button" |
| 784 | className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/6 hover:text-[#d0d0d0]" | |
| 784 | className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-muted-foreground/70 transition-colors duration-150 hover:bg-accent hover:text-foreground/80" | |
| 785 | 785 | onClick={() => setIsModelMenuOpen((open) => !open)} |
| 786 | 786 | > |
| 787 | 787 | <span className="max-w-[180px] truncate">{selectedModel}</span> |
| ... | ... | @@ -803,8 +803,8 @@ export default function ChatView() { |
| 803 | 803 | </svg> |
| 804 | 804 | </button> |
| 805 | 805 | {isModelMenuOpen && ( |
| 806 | <div className="absolute bottom-full left-0 z-20 mb-2 w-[320px] rounded-2xl border border-white/10 bg-[#1b1b1d]/95 p-2 shadow-[0_16px_40px_rgba(0,0,0,0.55)] backdrop-blur"> | |
| 807 | <p className="px-2 py-1 text-[11px] text-[#a0a0a0]/70">Select model</p> | |
| 806 | <div className="absolute bottom-full left-0 z-20 mb-2 w-[320px] rounded-2xl border border-border bg-popover/95 p-2 shadow-[0_16px_40px_rgba(0,0,0,0.55)] backdrop-blur"> | |
| 807 | <p className="px-2 py-1 text-[11px] text-muted-foreground/70">Select model</p> | |
| 808 | 808 | <div className="max-h-72 overflow-y-auto"> |
| 809 | 809 | {modelOptions.map((model) => { |
| 810 | 810 | const isSelected = model === selectedModel; |
| ... | ... | @@ -814,15 +814,15 @@ export default function ChatView() { |
| 814 | 814 | type="button" |
| 815 | 815 | className={`mb-0.5 flex w-full items-center justify-between gap-2 rounded-xl px-2 py-2 text-left font-mono text-sm transition-colors duration-150 ${ |
| 816 | 816 | isSelected |
| 817 | ? "bg-white/8 text-white" | |
| 818 | : "text-[#d4d4d4] hover:bg-white/5" | |
| 817 | ? "bg-accent text-foreground" | |
| 818 | : "text-foreground/90 hover:bg-accent" | |
| 819 | 819 | }`} |
| 820 | 820 | onClick={() => onModelSelect(model)} |
| 821 | 821 | > |
| 822 | 822 | <span className="truncate">{model}</span> |
| 823 | 823 | <span |
| 824 | 824 | className={`pt-0.5 text-sm ${ |
| 825 | isSelected ? "text-white" : "text-transparent" | |
| 825 | isSelected ? "text-foreground" : "text-transparent" | |
| 826 | 826 | }`} |
| 827 | 827 | > |
| 828 | 828 | ✓ |
| ... | ... | @@ -836,11 +836,11 @@ export default function ChatView() { |
| 836 | 836 | </div> |
| 837 | 837 | |
| 838 | 838 | {/* Divider */} |
| 839 | <div className="mx-0.5 h-4 w-px bg-white/8" /> | |
| 839 | <div className="mx-0.5 h-4 w-px bg-border" /> | |
| 840 | 840 | |
| 841 | 841 | {/* Reasoning effort */} |
| 842 | 842 | <label |
| 843 | className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/6 hover:text-[#d0d0d0]" | |
| 843 | className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-muted-foreground/70 transition-colors duration-150 hover:bg-accent hover:text-foreground/80" | |
| 844 | 844 | htmlFor="reasoning-effort" |
| 845 | 845 | > |
| 846 | 846 | <span>{selectedEffort.charAt(0).toUpperCase() + selectedEffort.slice(1)}</span> |
| ... | ... | @@ -851,7 +851,7 @@ export default function ChatView() { |
| 851 | 851 | onChange={(event) => setSelectedEffort(event.target.value)} |
| 852 | 852 | > |
| 853 | 853 | {REASONING_OPTIONS.map((effort) => ( |
| 854 | <option key={effort} value={effort} className="bg-[#1b1b1d]"> | |
| 854 | <option key={effort} value={effort} className="bg-popover"> | |
| 855 | 855 | {effort} |
| 856 | 856 | {effort === DEFAULT_REASONING ? " (default)" : ""} |
| 857 | 857 | </option> |
| ... | ... | @@ -876,12 +876,12 @@ export default function ChatView() { |
| 876 | 876 | </label> |
| 877 | 877 | |
| 878 | 878 | {/* Divider */} |
| 879 | <div className="mx-0.5 h-4 w-px bg-white/[0.08]" /> | |
| 879 | <div className="mx-0.5 h-4 w-px bg-border" /> | |
| 880 | 880 | |
| 881 | 881 | {/* Runtime mode toggle */} |
| 882 | 882 | <button |
| 883 | 883 | type="button" |
| 884 | className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/[0.06] hover:text-[#d0d0d0]" | |
| 884 | className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-muted-foreground/70 transition-colors duration-150 hover:bg-accent hover:text-foreground/80" | |
| 885 | 885 | disabled={isSwitchingRuntimeMode} |
| 886 | 886 | onClick={() => |
| 887 | 887 | void handleRuntimeModeChange( |
| ... | ... | @@ -975,7 +975,7 @@ export default function ChatView() { |
| 975 | 975 | ) : ( |
| 976 | 976 | <button |
| 977 | 977 | type="submit" |
| 978 | className="flex h-8 w-8 items-center justify-center rounded-full bg-white/90 text-[#0c0c0c] transition-all duration-150 hover:bg-white hover:scale-105 disabled:opacity-30 disabled:hover:scale-100" | |
| 978 | className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/90 text-primary-foreground transition-all duration-150 hover:bg-primary hover:scale-105 disabled:opacity-30 disabled:hover:scale-100" | |
| 979 | 979 | disabled={isSending || isConnecting || !prompt.trim()} |
| 980 | 980 | aria-label={ |
| 981 | 981 | isConnecting ? "Connecting" : isSending ? "Sending" : "Send message" |
apps/renderer/src/components/DiffPanel.tsx+9-9| ... | ... | @@ -4,13 +4,13 @@ export default function DiffPanel() { |
| 4 | 4 | const { dispatch } = useStore(); |
| 5 | 5 | |
| 6 | 6 | return ( |
| 7 | <aside className="flex h-full w-[400px] shrink-0 flex-col border-l border-white/[0.08] bg-[#141414]"> | |
| 7 | <aside className="flex h-full w-[400px] shrink-0 flex-col border-l border-border bg-card"> | |
| 8 | 8 | {/* Header */} |
| 9 | <div className="drag-region flex items-center justify-between border-b border-white/[0.08] px-4 pt-[28px] pb-3"> | |
| 10 | <h3 className="text-xs font-medium text-[#e0e0e0]">Uncommitted changes</h3> | |
| 9 | <div className="drag-region flex items-center justify-between border-b border-border px-4 pt-[28px] pb-3"> | |
| 10 | <h3 className="text-xs font-medium text-foreground">Uncommitted changes</h3> | |
| 11 | 11 | <button |
| 12 | 12 | type="button" |
| 13 | className="text-[#a0a0a0]/40 transition-colors duration-150 hover:text-[#a0a0a0]/70" | |
| 13 | className="text-muted-foreground/60 transition-colors duration-150 hover:text-muted-foreground" | |
| 14 | 14 | onClick={() => dispatch({ type: "TOGGLE_DIFF" })} |
| 15 | 15 | > |
| 16 | 16 | <span className="text-sm">&times;</span> |
| ... | ... | @@ -20,12 +20,12 @@ export default function DiffPanel() { |
| 20 | 20 | {/* Placeholder content */} |
| 21 | 21 | <div className="flex-1 overflow-y-auto p-4"> |
| 22 | 22 | <div className="space-y-3"> |
| 23 | <div className="rounded-lg border border-white/[0.06] bg-white/[0.02] p-3"> | |
| 23 | <div className="rounded-lg border border-border/80 bg-muted p-3"> | |
| 24 | 24 | <div className="mb-2 flex items-center gap-2"> |
| 25 | 25 | <span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[9px] font-medium text-emerald-400"> |
| 26 | 26 | M |
| 27 | 27 | </span> |
| 28 | <span className="font-mono text-xs text-[#a0a0a0]/60">src/App.tsx</span> | |
| 28 | <span className="font-mono text-xs text-muted-foreground/80">src/App.tsx</span> | |
| 29 | 29 | </div> |
| 30 | 30 | <div className="space-y-0.5 font-mono text-[11px]"> |
| 31 | 31 | <div className="text-red-400/60">- const oldValue = "hello";</div> |
| ... | ... | @@ -33,17 +33,17 @@ export default function DiffPanel() { |
| 33 | 33 | </div> |
| 34 | 34 | </div> |
| 35 | 35 | |
| 36 | <div className="rounded-lg border border-white/[0.06] bg-white/[0.02] p-3"> | |
| 36 | <div className="rounded-lg border border-border/80 bg-muted p-3"> | |
| 37 | 37 | <div className="flex items-center gap-2"> |
| 38 | 38 | <span className="rounded bg-blue-500/20 px-1.5 py-0.5 text-[9px] font-medium text-blue-400"> |
| 39 | 39 | A |
| 40 | 40 | </span> |
| 41 | <span className="font-mono text-xs text-[#a0a0a0]/60">src/utils.ts</span> | |
| 41 | <span className="font-mono text-xs text-muted-foreground/80">src/utils.ts</span> | |
| 42 | 42 | </div> |
| 43 | 43 | </div> |
| 44 | 44 | </div> |
| 45 | 45 | |
| 46 | <p className="mt-6 text-center text-[10px] text-[#a0a0a0]/25"> | |
| 46 | <p className="mt-6 text-center text-[10px] text-muted-foreground/30"> | |
| 47 | 47 | Diff integration coming soon |
| 48 | 48 | </p> |
| 49 | 49 | </div> |
apps/renderer/src/components/Sidebar.tsx+46-24| ... | ... | @@ -1,9 +1,13 @@ |
| 1 | import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react"; | |
| 1 | 2 | import { useCallback, useEffect, useMemo, useState } from "react"; |
| 3 | import { useTheme } from "../hooks/useTheme"; | |
| 2 | 4 | import { DEFAULT_MODEL } from "../model-logic"; |
| 3 | 5 | import { readNativeApi } from "../session-logic"; |
| 4 | 6 | import { useStore } from "../store"; |
| 5 | 7 | import type { Project } from "../types"; |
| 6 | 8 | |
| 9 | const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; | |
| 10 | ||
| 7 | 11 | |
| 8 | 12 | function formatRelativeTime(iso: string): string { |
| 9 | 13 | const diff = Date.now() - new Date(iso).getTime(); |
| ... | ... | @@ -26,6 +30,7 @@ function threadStatusLabel( |
| 26 | 30 | export default function Sidebar() { |
| 27 | 31 | const { state, dispatch } = useStore(); |
| 28 | 32 | const api = useMemo(() => readNativeApi(), []); |
| 33 | const { theme, setTheme } = useTheme(); | |
| 29 | 34 | const [addingProject, setAddingProject] = useState(false); |
| 30 | 35 | const [newCwd, setNewCwd] = useState(""); |
| 31 | 36 | const [isPickingFolder, setIsPickingFolder] = useState(false); |
| ... | ... | @@ -120,22 +125,39 @@ export default function Sidebar() { |
| 120 | 125 | }, [handleNewThread, state.activeThreadId, state.projects, state.threads]); |
| 121 | 126 | |
| 122 | 127 | return ( |
| 123 | <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-white/8 bg-[#141414]"> | |
| 128 | <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-border bg-card"> | |
| 124 | 129 | {/* Drag region / traffic light space */} |
| 125 | 130 | <div className="drag-region h-[52px] shrink-0" /> |
| 126 | 131 | {/* Branding */} |
| 127 | 132 | <div className="flex items-center gap-2 px-4 pb-4"> |
| 128 | <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-white text-xs font-bold text-[#0c0c0c]"> | |
| 133 | <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-xs font-bold text-primary-foreground"> | |
| 129 | 134 | CT |
| 130 | 135 | </div> |
| 131 | <span className="text-sm font-semibold tracking-tight text-[#e0e0e0]">CodeThing</span> | |
| 136 | <span className="flex-1 text-sm font-semibold tracking-tight text-foreground"> | |
| 137 | CodeThing | |
| 138 | </span> | |
| 139 | <button | |
| 140 | type="button" | |
| 141 | className="rounded-md p-1.5 text-muted-foreground/80 transition-colors duration-150 hover:bg-accent hover:text-muted-foreground" | |
| 142 | onClick={() => setTheme(THEME_CYCLE[theme])} | |
| 143 | aria-label={`Theme: ${theme}`} | |
| 144 | title={`Theme: ${theme}`} | |
| 145 | > | |
| 146 | {theme === "system" ? ( | |
| 147 | <MonitorIcon className="size-3.5" /> | |
| 148 | ) : theme === "light" ? ( | |
| 149 | <SunIcon className="size-3.5" /> | |
| 150 | ) : ( | |
| 151 | <MoonIcon className="size-3.5" /> | |
| 152 | )} | |
| 153 | </button> | |
| 132 | 154 | </div> |
| 133 | 155 | |
| 134 | 156 | {/* New thread (global) */} |
| 135 | 157 | <div className="px-3 pb-3"> |
| 136 | 158 | <button |
| 137 | 159 | type="button" |
| 138 | className="flex w-full items-center gap-2 rounded-lg border border-white/8 bg-white/4 px-3 py-2 text-xs text-[#a0a0a0]/80 transition-colors duration-150 hover:bg-white/[0.07]" | |
| 160 | className="flex w-full items-center gap-2 rounded-lg border border-border bg-secondary px-3 py-2 text-xs text-muted-foreground transition-colors duration-150 hover:bg-accent" | |
| 139 | 161 | onClick={() => { |
| 140 | 162 | if (state.projects.length === 0) { |
| 141 | 163 | setAddingProject(true); |
| ... | ... | @@ -145,7 +167,7 @@ export default function Sidebar() { |
| 145 | 167 | if (firstProject) handleNewThread(firstProject.id); |
| 146 | 168 | }} |
| 147 | 169 | > |
| 148 | <span className="text-white">+</span> | |
| 170 | <span className="text-foreground">+</span> | |
| 149 | 171 | New thread |
| 150 | 172 | </button> |
| 151 | 173 | </div> |
| ... | ... | @@ -167,21 +189,21 @@ export default function Sidebar() { |
| 167 | 189 | {/* Project header */} |
| 168 | 190 | <button |
| 169 | 191 | type="button" |
| 170 | className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 hover:bg-white/5" | |
| 192 | className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 hover:bg-accent" | |
| 171 | 193 | onClick={() => dispatch({ type: "TOGGLE_PROJECT", projectId: project.id })} |
| 172 | 194 | > |
| 173 | <span className="text-[10px] text-[#a0a0a0]/50"> | |
| 195 | <span className="text-[10px] text-muted-foreground/70"> | |
| 174 | 196 | {project.expanded ? "▼" : "▶"} |
| 175 | 197 | </span> |
| 176 | <span className="flex-1 truncate text-xs font-medium text-[#e0e0e0]/90"> | |
| 198 | <span className="flex-1 truncate text-xs font-medium text-foreground/90"> | |
| 177 | 199 | {project.name} |
| 178 | 200 | </span> |
| 179 | <span className="text-[10px] text-[#a0a0a0]/40">{threads.length}</span> | |
| 201 | <span className="text-[10px] text-muted-foreground/60">{threads.length}</span> | |
| 180 | 202 | </button> |
| 181 | 203 | |
| 182 | 204 | {/* Threads */} |
| 183 | 205 | {project.expanded && ( |
| 184 | <div className="ml-2 border-l border-white/6 pl-2"> | |
| 206 | <div className="ml-2 border-l border-border/80 pl-2"> | |
| 185 | 207 | {threads.map((thread) => { |
| 186 | 208 | const isActive = state.activeThreadId === thread.id; |
| 187 | 209 | const threadStatus = threadStatusLabel( |
| ... | ... | @@ -192,7 +214,7 @@ export default function Sidebar() { |
| 192 | 214 | key={thread.id} |
| 193 | 215 | type="button" |
| 194 | 216 | className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 ${ |
| 195 | isActive ? "bg-white/10 text-white" : "text-[#a0a0a0]/70 hover:bg-white/4" | |
| 217 | isActive ? "bg-accent text-foreground" : "text-muted-foreground hover:bg-secondary" | |
| 196 | 218 | }`} |
| 197 | 219 | onClick={() => |
| 198 | 220 | dispatch({ |
| ... | ... | @@ -203,8 +225,8 @@ export default function Sidebar() { |
| 203 | 225 | > |
| 204 | 226 | <div className="flex min-w-0 flex-1 items-center gap-1.5"> |
| 205 | 227 | {threadStatus && ( |
| 206 | <span className="inline-flex items-center gap-1 text-[10px] text-[#7ed8ff]/80"> | |
| 207 | <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-300/80" /> | |
| 228 | <span className="inline-flex items-center gap-1 text-[10px] text-sky-600 dark:text-sky-300/80"> | |
| 229 | <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-500 dark:bg-sky-300/80" /> | |
| 208 | 230 | <span className="hidden xl:inline"> |
| 209 | 231 | {threadStatus} |
| 210 | 232 | </span> |
| ... | ... | @@ -214,7 +236,7 @@ export default function Sidebar() { |
| 214 | 236 | {thread.title} |
| 215 | 237 | </span> |
| 216 | 238 | </div> |
| 217 | <span className="shrink-0 text-[10px] text-[#a0a0a0]/30"> | |
| 239 | <span className="shrink-0 text-[10px] text-muted-foreground/40"> | |
| 218 | 240 | {formatRelativeTime(thread.createdAt)} |
| 219 | 241 | </span> |
| 220 | 242 | </button> |
| ... | ... | @@ -224,7 +246,7 @@ export default function Sidebar() { |
| 224 | 246 | {/* New thread within project */} |
| 225 | 247 | <button |
| 226 | 248 | type="button" |
| 227 | className="flex w-full items-center gap-1 px-2 py-1 text-[10px] text-[#a0a0a0]/40 transition-colors duration-150 hover:text-[#a0a0a0]/60" | |
| 249 | className="flex w-full items-center gap-1 px-2 py-1 text-[10px] text-muted-foreground/60 transition-colors duration-150 hover:text-muted-foreground/80" | |
| 228 | 250 | onClick={() => handleNewThread(project.id)} |
| 229 | 251 | > |
| 230 | 252 | <span>+</span> New thread |
| ... | ... | @@ -236,7 +258,7 @@ export default function Sidebar() { |
| 236 | 258 | })} |
| 237 | 259 | |
| 238 | 260 | {state.projects.length === 0 && !addingProject && ( |
| 239 | <div className="px-2 pt-4 text-center text-xs text-[#a0a0a0]/40"> | |
| 261 | <div className="px-2 pt-4 text-center text-xs text-muted-foreground/60"> | |
| 240 | 262 | No projects yet. |
| 241 | 263 | <br /> |
| 242 | 264 | Add one to get started. |
| ... | ... | @@ -246,12 +268,12 @@ export default function Sidebar() { |
| 246 | 268 | |
| 247 | 269 | {/* Add project form */} |
| 248 | 270 | {addingProject ? ( |
| 249 | <div className="border-t border-white/8 p-3"> | |
| 250 | <p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-[#a0a0a0]/50"> | |
| 271 | <div className="border-t border-border p-3"> | |
| 272 | <p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70"> | |
| 251 | 273 | Add project |
| 252 | 274 | </p> |
| 253 | 275 | <input |
| 254 | className="mb-2 w-full rounded-md border border-white/10 bg-white/4 px-2 py-1.5 font-mono text-xs text-[#e0e0e0] placeholder:text-[#a0a0a0]/30 focus:border-white/30 focus:outline-none" | |
| 276 | className="mb-2 w-full rounded-md border border-border bg-secondary px-2 py-1.5 font-mono text-xs text-foreground placeholder:text-muted-foreground/40 focus:border-ring focus:outline-none" | |
| 255 | 277 | placeholder="/path/to/project" |
| 256 | 278 | value={newCwd} |
| 257 | 279 | onChange={(e) => setNewCwd(e.target.value)} |
| ... | ... | @@ -263,7 +285,7 @@ export default function Sidebar() { |
| 263 | 285 | {api && ( |
| 264 | 286 | <button |
| 265 | 287 | type="button" |
| 266 | className="mb-2 flex w-full items-center justify-center rounded-md border border-white/10 px-2 py-1.5 text-xs text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/4 disabled:cursor-not-allowed disabled:opacity-60" | |
| 288 | className="mb-2 flex w-full items-center justify-center rounded-md border border-border px-2 py-1.5 text-xs text-muted-foreground transition-colors duration-150 hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60" | |
| 267 | 289 | onClick={() => void handlePickFolder()} |
| 268 | 290 | disabled={isPickingFolder} |
| 269 | 291 | > |
| ... | ... | @@ -273,14 +295,14 @@ export default function Sidebar() { |
| 273 | 295 | <div className="flex gap-2"> |
| 274 | 296 | <button |
| 275 | 297 | type="button" |
| 276 | className="flex-1 rounded-md bg-white px-2 py-1 text-xs font-medium text-[#0c0c0c] transition-colors duration-150 hover:bg-white/90" | |
| 298 | className="flex-1 rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors duration-150 hover:bg-primary/90" | |
| 277 | 299 | onClick={handleAddProject} |
| 278 | 300 | > |
| 279 | 301 | Add |
| 280 | 302 | </button> |
| 281 | 303 | <button |
| 282 | 304 | type="button" |
| 283 | className="flex-1 rounded-md border border-white/10 px-2 py-1 text-xs text-[#a0a0a0]/60 transition-colors duration-150 hover:bg-white/4" | |
| 305 | className="flex-1 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground/80 transition-colors duration-150 hover:bg-secondary" | |
| 284 | 306 | onClick={() => setAddingProject(false)} |
| 285 | 307 | > |
| 286 | 308 | Cancel |
| ... | ... | @@ -288,10 +310,10 @@ export default function Sidebar() { |
| 288 | 310 | </div> |
| 289 | 311 | </div> |
| 290 | 312 | ) : ( |
| 291 | <div className="border-t border-white/8 p-3"> | |
| 313 | <div className="border-t border-border p-3"> | |
| 292 | 314 | <button |
| 293 | 315 | type="button" |
| 294 | className="flex w-full items-center justify-center gap-1 rounded-md border border-dashed border-white/12 py-2 text-xs text-[#a0a0a0]/50 transition-colors duration-150 hover:border-white/20 hover:text-[#a0a0a0]/70" | |
| 316 | className="flex w-full items-center justify-center gap-1 rounded-md border border-dashed border-border py-2 text-xs text-muted-foreground/70 transition-colors duration-150 hover:border-ring hover:text-muted-foreground" | |
| 295 | 317 | onClick={() => setAddingProject(true)} |
| 296 | 318 | > |
| 297 | 319 | + Add project |
apps/renderer/src/hooks/useTheme.ts created+90| ... | ... | @@ -0,0 +1,90 @@ |
| 1 | import { useCallback, useEffect, useSyncExternalStore } from "react"; | |
| 2 | ||
| 3 | type Theme = "light" | "dark" | "system"; | |
| 4 | ||
| 5 | const STORAGE_KEY = "codething:theme"; | |
| 6 | const MEDIA_QUERY = "(prefers-color-scheme: dark)"; | |
| 7 | ||
| 8 | let listeners: Array<() => void> = []; | |
| 9 | function emitChange() { | |
| 10 | for (const listener of listeners) listener(); | |
| 11 | } | |
| 12 | ||
| 13 | function getSystemDark(): boolean { | |
| 14 | return window.matchMedia(MEDIA_QUERY).matches; | |
| 15 | } | |
| 16 | ||
| 17 | function getStored(): Theme { | |
| 18 | const raw = localStorage.getItem(STORAGE_KEY); | |
| 19 | if (raw === "light" || raw === "dark" || raw === "system") return raw; | |
| 20 | return "system"; | |
| 21 | } | |
| 22 | ||
| 23 | function applyTheme(theme: Theme, suppressTransitions = false) { | |
| 24 | if (suppressTransitions) { | |
| 25 | document.documentElement.classList.add("no-transitions"); | |
| 26 | } | |
| 27 | const isDark = theme === "dark" || (theme === "system" && getSystemDark()); | |
| 28 | document.documentElement.classList.toggle("dark", isDark); | |
| 29 | if (suppressTransitions) { | |
| 30 | // Force a reflow so the no-transitions class takes effect before removal | |
| 31 | document.documentElement.offsetHeight; | |
| 32 | requestAnimationFrame(() => { | |
| 33 | document.documentElement.classList.remove("no-transitions"); | |
| 34 | }); | |
| 35 | } | |
| 36 | } | |
| 37 | ||
| 38 | // Apply immediately on module load to prevent flash | |
| 39 | applyTheme(getStored()); | |
| 40 | ||
| 41 | function getSnapshot(): Theme { | |
| 42 | return getStored(); | |
| 43 | } | |
| 44 | ||
| 45 | function subscribe(listener: () => void): () => void { | |
| 46 | listeners.push(listener); | |
| 47 | ||
| 48 | // Listen for system preference changes | |
| 49 | const mq = window.matchMedia(MEDIA_QUERY); | |
| 50 | const handleChange = () => { | |
| 51 | if (getStored() === "system") applyTheme("system", true); | |
| 52 | emitChange(); | |
| 53 | }; | |
| 54 | mq.addEventListener("change", handleChange); | |
| 55 | ||
| 56 | // Listen for storage changes from other tabs | |
| 57 | const handleStorage = (e: StorageEvent) => { | |
| 58 | if (e.key === STORAGE_KEY) { | |
| 59 | applyTheme(getStored(), true); | |
| 60 | emitChange(); | |
| 61 | } | |
| 62 | }; | |
| 63 | window.addEventListener("storage", handleStorage); | |
| 64 | ||
| 65 | return () => { | |
| 66 | listeners = listeners.filter((l) => l !== listener); | |
| 67 | mq.removeEventListener("change", handleChange); | |
| 68 | window.removeEventListener("storage", handleStorage); | |
| 69 | }; | |
| 70 | } | |
| 71 | ||
| 72 | export function useTheme() { | |
| 73 | const theme = useSyncExternalStore(subscribe, getSnapshot); | |
| 74 | ||
| 75 | const resolvedTheme: "light" | "dark" = | |
| 76 | theme === "system" ? (getSystemDark() ? "dark" : "light") : theme; | |
| 77 | ||
| 78 | const setTheme = useCallback((next: Theme) => { | |
| 79 | localStorage.setItem(STORAGE_KEY, next); | |
| 80 | applyTheme(next, true); | |
| 81 | emitChange(); | |
| 82 | }, []); | |
| 83 | ||
| 84 | // Keep DOM in sync on mount/change | |
| 85 | useEffect(() => { | |
| 86 | applyTheme(theme); | |
| 87 | }, [theme]); | |
| 88 | ||
| 89 | return { theme, setTheme, resolvedTheme } as const; | |
| 90 | } |
apps/renderer/src/index.css+197-16| ... | ... | @@ -1,7 +1,178 @@ |
| 1 | 1 | @import "tailwindcss"; |
| 2 | 2 | |
| 3 | @custom-variant dark (&:is(.dark, .dark *)); | |
| 4 | ||
| 5 | @theme inline { | |
| 6 | --animate-skeleton: skeleton 2s -1s infinite linear; | |
| 7 | --color-warning-foreground: var(--warning-foreground); | |
| 8 | --color-warning: var(--warning); | |
| 9 | --color-success-foreground: var(--success-foreground); | |
| 10 | --color-success: var(--success); | |
| 11 | --color-info-foreground: var(--info-foreground); | |
| 12 | --color-info: var(--info); | |
| 13 | --color-destructive-foreground: var(--destructive-foreground); | |
| 14 | --color-sidebar-ring: var(--sidebar-ring); | |
| 15 | --color-sidebar-border: var(--sidebar-border); | |
| 16 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); | |
| 17 | --color-sidebar-accent: var(--sidebar-accent); | |
| 18 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); | |
| 19 | --color-sidebar-primary: var(--sidebar-primary); | |
| 20 | --color-sidebar-foreground: var(--sidebar-foreground); | |
| 21 | --color-sidebar: var(--sidebar); | |
| 22 | --color-chart-5: var(--chart-5); | |
| 23 | --color-chart-4: var(--chart-4); | |
| 24 | --color-chart-3: var(--chart-3); | |
| 25 | --color-chart-2: var(--chart-2); | |
| 26 | --color-chart-1: var(--chart-1); | |
| 27 | --color-ring: var(--ring); | |
| 28 | --color-input: var(--input); | |
| 29 | --color-border: var(--border); | |
| 30 | --color-destructive: var(--destructive); | |
| 31 | --color-accent-foreground: var(--accent-foreground); | |
| 32 | --color-accent: var(--accent); | |
| 33 | --color-muted-foreground: var(--muted-foreground); | |
| 34 | --color-muted: var(--muted); | |
| 35 | --color-secondary-foreground: var(--secondary-foreground); | |
| 36 | --color-secondary: var(--secondary); | |
| 37 | --color-primary-foreground: var(--primary-foreground); | |
| 38 | --color-primary: var(--primary); | |
| 39 | --color-popover-foreground: var(--popover-foreground); | |
| 40 | --color-popover: var(--popover); | |
| 41 | --color-card-foreground: var(--card-foreground); | |
| 42 | --color-card: var(--card); | |
| 43 | --color-foreground: var(--foreground); | |
| 44 | --color-background: var(--background); | |
| 45 | --radius-sm: calc(var(--radius) - 4px); | |
| 46 | --radius-md: calc(var(--radius) - 2px); | |
| 47 | --radius-lg: var(--radius); | |
| 48 | --radius-xl: calc(var(--radius) + 4px); | |
| 49 | --radius-2xl: calc(var(--radius) + 8px); | |
| 50 | --radius-3xl: calc(var(--radius) + 12px); | |
| 51 | --radius-4xl: calc(var(--radius) + 16px); | |
| 52 | @keyframes skeleton { | |
| 53 | to { | |
| 54 | background-position: -200% 0; | |
| 55 | } | |
| 56 | } | |
| 57 | } | |
| 58 | ||
| 59 | @layer base { | |
| 60 | * { | |
| 61 | @apply border-border outline-ring/50; | |
| 62 | } | |
| 63 | body { | |
| 64 | @apply bg-background text-foreground; | |
| 65 | } | |
| 66 | } | |
| 67 | ||
| 68 | /* Suppress all transitions during theme changes */ | |
| 69 | .no-transitions, | |
| 70 | .no-transitions *, | |
| 71 | .no-transitions *::before, | |
| 72 | .no-transitions *::after { | |
| 73 | transition-duration: 0s !important; | |
| 74 | animation-duration: 0s !important; | |
| 75 | } | |
| 76 | ||
| 3 | 77 | :root { |
| 4 | color-scheme: dark; | |
| 78 | color-scheme: light; | |
| 79 | --radius: 0.625rem; | |
| 80 | --background: var(--color-white); | |
| 81 | --foreground: var(--color-neutral-800); | |
| 82 | --card: var(--color-white); | |
| 83 | --card-foreground: var(--color-neutral-800); | |
| 84 | --popover: var(--color-white); | |
| 85 | --popover-foreground: var(--color-neutral-800); | |
| 86 | --primary: var(--color-neutral-800); | |
| 87 | --primary-foreground: var(--color-neutral-50); | |
| 88 | --secondary: --alpha(var(--color-black) / 4%); | |
| 89 | --secondary-foreground: var(--color-neutral-800); | |
| 90 | --muted: --alpha(var(--color-black) / 4%); | |
| 91 | --muted-foreground: color-mix( | |
| 92 | in srgb, | |
| 93 | var(--color-neutral-500) 90%, | |
| 94 | var(--color-black) | |
| 95 | ); | |
| 96 | --accent: --alpha(var(--color-black) / 4%); | |
| 97 | --accent-foreground: var(--color-neutral-800); | |
| 98 | --destructive: var(--color-red-500); | |
| 99 | --border: --alpha(var(--color-black) / 8%); | |
| 100 | --input: --alpha(var(--color-black) / 10%); | |
| 101 | --ring: var(--color-neutral-400); | |
| 102 | --chart-1: oklch(0.646 0.222 41.116); | |
| 103 | --chart-2: oklch(0.6 0.118 184.704); | |
| 104 | --chart-3: oklch(0.398 0.07 227.392); | |
| 105 | --chart-4: oklch(0.828 0.189 84.429); | |
| 106 | --chart-5: oklch(0.769 0.188 70.08); | |
| 107 | --sidebar: oklch(0.985 0 0); | |
| 108 | --sidebar-foreground: oklch(0.141 0.005 285.823); | |
| 109 | --sidebar-primary: oklch(0.21 0.006 285.885); | |
| 110 | --sidebar-primary-foreground: oklch(0.985 0 0); | |
| 111 | --sidebar-accent: oklch(0.967 0.001 286.375); | |
| 112 | --sidebar-accent-foreground: oklch(0.21 0.006 285.885); | |
| 113 | --sidebar-border: oklch(0.92 0.004 286.32); | |
| 114 | --sidebar-ring: oklch(0.705 0.015 286.067); | |
| 115 | --destructive-foreground: var(--color-red-700); | |
| 116 | --info: var(--color-blue-500); | |
| 117 | --info-foreground: var(--color-blue-700); | |
| 118 | --success: var(--color-emerald-500); | |
| 119 | --success-foreground: var(--color-emerald-700); | |
| 120 | --warning: var(--color-amber-500); | |
| 121 | --warning-foreground: var(--color-amber-700); | |
| 122 | ||
| 123 | @variant dark { | |
| 124 | color-scheme: dark; | |
| 125 | --background: color-mix( | |
| 126 | in srgb, | |
| 127 | var(--color-neutral-950) 95%, | |
| 128 | var(--color-white) | |
| 129 | ); | |
| 130 | --foreground: var(--color-neutral-100); | |
| 131 | --card: color-mix(in srgb, var(--background) 98%, var(--color-white)); | |
| 132 | --card-foreground: var(--color-neutral-100); | |
| 133 | --popover: color-mix(in srgb, var(--background) 98%, var(--color-white)); | |
| 134 | --popover-foreground: var(--color-neutral-100); | |
| 135 | --primary: var(--color-neutral-100); | |
| 136 | --primary-foreground: var(--color-neutral-800); | |
| 137 | --secondary: --alpha(var(--color-white) / 4%); | |
| 138 | --secondary-foreground: var(--color-neutral-100); | |
| 139 | --muted: --alpha(var(--color-white) / 4%); | |
| 140 | --muted-foreground: color-mix( | |
| 141 | in srgb, | |
| 142 | var(--color-neutral-500) 90%, | |
| 143 | var(--color-white) | |
| 144 | ); | |
| 145 | --accent: --alpha(var(--color-white) / 4%); | |
| 146 | --accent-foreground: var(--color-neutral-100); | |
| 147 | --destructive: color-mix( | |
| 148 | in srgb, | |
| 149 | var(--color-red-500) 90%, | |
| 150 | var(--color-white) | |
| 151 | ); | |
| 152 | --border: --alpha(var(--color-white) / 6%); | |
| 153 | --input: --alpha(var(--color-white) / 8%); | |
| 154 | --ring: var(--color-neutral-500); | |
| 155 | --chart-1: oklch(0.488 0.243 264.376); | |
| 156 | --chart-2: oklch(0.696 0.17 162.48); | |
| 157 | --chart-3: oklch(0.769 0.188 70.08); | |
| 158 | --chart-4: oklch(0.627 0.265 303.9); | |
| 159 | --chart-5: oklch(0.645 0.246 16.439); | |
| 160 | --sidebar: oklch(0.21 0.006 285.885); | |
| 161 | --sidebar-foreground: oklch(0.985 0 0); | |
| 162 | --sidebar-primary: oklch(0.488 0.243 264.376); | |
| 163 | --sidebar-primary-foreground: oklch(0.985 0 0); | |
| 164 | --sidebar-accent: oklch(0.274 0.006 286.033); | |
| 165 | --sidebar-accent-foreground: oklch(0.985 0 0); | |
| 166 | --sidebar-border: oklch(1 0 0 / 10%); | |
| 167 | --sidebar-ring: oklch(0.552 0.016 285.938); | |
| 168 | --destructive-foreground: var(--color-red-400); | |
| 169 | --info: var(--color-blue-500); | |
| 170 | --info-foreground: var(--color-blue-400); | |
| 171 | --success: var(--color-emerald-500); | |
| 172 | --success-foreground: var(--color-emerald-400); | |
| 173 | --warning: var(--color-amber-500); | |
| 174 | --warning-foreground: var(--color-amber-400); | |
| 175 | } | |
| 5 | 176 | } |
| 6 | 177 | |
| 7 | 178 | body { |
| ... | ... | @@ -10,8 +181,6 @@ body { |
| 10 | 181 | margin: 0; |
| 11 | 182 | padding: 0; |
| 12 | 183 | min-height: 100vh; |
| 13 | color: #e0e0e0; | |
| 14 | background: #0c0c0c; | |
| 15 | 184 | } |
| 16 | 185 | |
| 17 | 186 | body::after { |
| ... | ... | @@ -55,15 +224,23 @@ input { |
| 55 | 224 | } |
| 56 | 225 | |
| 57 | 226 | ::-webkit-scrollbar-thumb { |
| 58 | background: rgba(255, 255, 255, 0.1); | |
| 227 | background: rgba(0, 0, 0, 0.15); | |
| 59 | 228 | border-radius: 3px; |
| 60 | 229 | } |
| 61 | 230 | |
| 62 | 231 | ::-webkit-scrollbar-thumb:hover { |
| 232 | background: rgba(0, 0, 0, 0.25); | |
| 233 | } | |
| 234 | ||
| 235 | .dark ::-webkit-scrollbar-thumb { | |
| 236 | background: rgba(255, 255, 255, 0.1); | |
| 237 | } | |
| 238 | ||
| 239 | .dark ::-webkit-scrollbar-thumb:hover { | |
| 63 | 240 | background: rgba(255, 255, 255, 0.18); |
| 64 | 241 | } |
| 65 | 242 | |
| 66 | /* Reasoning select — clickable label surface */ | |
| 243 | /* Reasoning select -- clickable label surface */ | |
| 67 | 244 | label:has(> select#reasoning-effort) { |
| 68 | 245 | position: relative; |
| 69 | 246 | } |
| ... | ... | @@ -108,34 +285,38 @@ label:has(> select#reasoning-effort) select { |
| 108 | 285 | } |
| 109 | 286 | |
| 110 | 287 | .chat-markdown a { |
| 111 | color: #93c5fd; | |
| 288 | color: var(--info-foreground); | |
| 112 | 289 | text-decoration: underline; |
| 113 | text-decoration-color: rgba(147, 197, 253, 0.4); | |
| 290 | text-decoration-color: color-mix( | |
| 291 | in srgb, | |
| 292 | var(--info-foreground) 40%, | |
| 293 | transparent | |
| 294 | ); | |
| 114 | 295 | } |
| 115 | 296 | |
| 116 | 297 | .chat-markdown a:hover { |
| 117 | color: #bfdbfe; | |
| 298 | opacity: 0.8; | |
| 118 | 299 | } |
| 119 | 300 | |
| 120 | 301 | .chat-markdown blockquote { |
| 121 | border-left: 2px solid rgba(255, 255, 255, 0.2); | |
| 302 | border-left: 2px solid var(--border); | |
| 122 | 303 | padding-left: 0.8rem; |
| 123 | color: rgba(224, 224, 224, 0.75); | |
| 304 | color: var(--muted-foreground); | |
| 124 | 305 | } |
| 125 | 306 | |
| 126 | 307 | .chat-markdown :not(pre) > code { |
| 127 | border: 1px solid rgba(255, 255, 255, 0.12); | |
| 308 | border: 1px solid var(--border); | |
| 128 | 309 | border-radius: 0.375rem; |
| 129 | background: rgba(255, 255, 255, 0.07); | |
| 310 | background: var(--muted); | |
| 130 | 311 | padding: 0.1rem 0.35rem; |
| 131 | color: #f4f4f5; | |
| 312 | color: var(--foreground); | |
| 132 | 313 | } |
| 133 | 314 | |
| 134 | 315 | .chat-markdown pre { |
| 135 | 316 | overflow-x: auto; |
| 136 | border: 1px solid rgba(255, 255, 255, 0.12); | |
| 317 | border: 1px solid var(--border); | |
| 137 | 318 | border-radius: 0.75rem; |
| 138 | background: rgba(0, 0, 0, 0.3); | |
| 319 | background: var(--muted); | |
| 139 | 320 | padding: 0.8rem 0.9rem; |
| 140 | 321 | } |
| 141 | 322 | |
| ... | ... | @@ -153,7 +334,7 @@ label:has(> select#reasoning-effort) select { |
| 153 | 334 | |
| 154 | 335 | .chat-markdown th, |
| 155 | 336 | .chat-markdown td { |
| 156 | border: 1px solid rgba(255, 255, 255, 0.12); | |
| 337 | border: 1px solid var(--border); | |
| 157 | 338 | padding: 0.35rem 0.45rem; |
| 158 | 339 | text-align: left; |
| 159 | 340 | } |
bun.lock+3| ... | ... | @@ -35,6 +35,7 @@ |
| 35 | 35 | "dependencies": { |
| 36 | 36 | "@acme/contracts": "workspace:*", |
| 37 | 37 | "highlight.js": "^11.11.1", |
| 38 | "lucide-react": "^0.563.0", | |
| 38 | 39 | "react": "^19.0.0", |
| 39 | 40 | "react-dom": "^19.0.0", |
| 40 | 41 | "react-markdown": "^10.1.0", |
| ... | ... | @@ -754,6 +755,8 @@ |
| 754 | 755 | |
| 755 | 756 | "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], |
| 756 | 757 | |
| 758 | "lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="], | |
| 759 | ||
| 757 | 760 | "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], |
| 758 | 761 | |
| 759 | 762 | "make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="], |