authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-02-09 01:27:01-08:00
committergravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-02-09 01:27:01-08:00
log1ab32562049fdb51de7ffc3a7cc8af19f721a306
treea2d0e4dd5a28405cec241ec11b5b2ebaf63c719f
parent377d21211a6b2873437e488590da13d81fa9e926
signature Commit is signed but in an unrecognized format.

light mode round 2


10 files changed, 394 insertions(+), 97 deletions(-)

apps/renderer/index.html+1-1
...@@ -5,7 +5,7 @@...@@ -5,7 +5,7 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>Electron Todo</title>6 <title>Electron Todo</title>
7 </head>7 </head>
8 <body class="bg-slate-100">8 <body>
9 <div id="root"></div>9 <div id="root"></div>
10 <script type="module" src="/src/main.tsx"></script>10 <script type="module" src="/src/main.tsx"></script>
11 </body>11 </body>
apps/renderer/package.json+1
...@@ -13,6 +13,7 @@...@@ -13,6 +13,7 @@
13 "dependencies": {13 "dependencies": {
14 "@acme/contracts": "workspace:*",14 "@acme/contracts": "workspace:*",
15 "highlight.js": "^11.11.1",15 "highlight.js": "^11.11.1",
16 "lucide-react": "^0.563.0",
16 "react": "^19.0.0",17 "react": "^19.0.0",
17 "react-dom": "^19.0.0",18 "react-dom": "^19.0.0",
18 "react-markdown": "^10.1.0",19 "react-markdown": "^10.1.0",
apps/renderer/src/App.tsx+3-3
...@@ -31,10 +31,10 @@ function Layout() {...@@ -31,10 +31,10 @@ function Layout() {
3131
32 if (!api) {32 if (!api) {
33 return (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 <div className="drag-region h-[52px] shrink-0" />35 <div className="drag-region h-[52px] shrink-0" />
36 <div className="flex flex-1 items-center justify-center">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 Native bridge unavailable. Launch through Electron.38 Native bridge unavailable. Launch through Electron.
39 </p>39 </p>
40 </div>40 </div>
...@@ -43,7 +43,7 @@ function Layout() {...@@ -43,7 +43,7 @@ function Layout() {
43 }43 }
4444
45 return (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 <EventRouter />47 <EventRouter />
48 <Sidebar />48 <Sidebar />
49 <ChatView />49 <ChatView />
apps/renderer/src/components/ChatMarkdown.tsx+1-1
...@@ -15,7 +15,7 @@ const markdownComponents: Components = {...@@ -15,7 +15,7 @@ const markdownComponents: Components = {
1515
16export default function ChatMarkdown({ text }: ChatMarkdownProps) {16export default function ChatMarkdown({ text }: ChatMarkdownProps) {
17 return (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 <ReactMarkdown19 <ReactMarkdown
20 remarkPlugins={[remarkGfm]}20 remarkPlugins={[remarkGfm]}
21 rehypePlugins={[[rehypeHighlight, { detect: true, ignoreMissing: true }]]}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,10 +47,10 @@ function editorLabel(editor: (typeof EDITORS)[number]): string {
47const LAST_EDITOR_KEY = "codething:last-editor";47const LAST_EDITOR_KEY = "codething:last-editor";
4848
49function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {49function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {
50 if (tone === "error") return "text-rose-300/50";50 if (tone === "error") return "text-rose-300/50 dark:text-rose-300/50";
51 if (tone === "tool") return "text-[#8a8a8a]";51 if (tone === "tool") return "text-muted-foreground/70";
52 if (tone === "thinking") return "text-[#707070]";52 if (tone === "thinking") return "text-muted-foreground/50";
53 return "text-[#606060]";53 return "text-muted-foreground/40";
54}54}
5555
56interface PendingApprovalCard {56interface PendingApprovalCard {
...@@ -488,7 +488,7 @@ export default function ChatView() {...@@ -488,7 +488,7 @@ export default function ChatView() {
488 // Empty state: no active thread488 // Empty state: no active thread
489 if (!activeThread) {489 if (!activeThread) {
490 return (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 <div className="drag-region h-[52px] shrink-0" />492 <div className="drag-region h-[52px] shrink-0" />
493 <div className="flex flex-1 items-center justify-center">493 <div className="flex flex-1 items-center justify-center">
494 <div className="text-center">494 <div className="text-center">
...@@ -500,11 +500,11 @@ export default function ChatView() {...@@ -500,11 +500,11 @@ export default function ChatView() {
500 }500 }
501501
502 return (502 return (
503 <div className="flex flex-1 flex-col bg-[#0c0c0c]">503 <div className="flex flex-1 flex-col bg-background">
504 {/* Top bar */}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 <div className="flex items-center gap-3">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 {activeThread.title}508 {activeThread.title}
509 </h2>509 </h2>
510 </div>510 </div>
...@@ -514,23 +514,23 @@ export default function ChatView() {...@@ -514,23 +514,23 @@ export default function ChatView() {
514 <div className="relative" ref={editorMenuRef}>514 <div className="relative" ref={editorMenuRef}>
515 <button515 <button
516 type="button"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 onClick={() => setIsEditorMenuOpen((v) => !v)}518 onClick={() => setIsEditorMenuOpen((v) => !v)}
519 >519 >
520 Open in&hellip;520 Open in&hellip;
521 </button>521 </button>
522 {isEditorMenuOpen && (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 {EDITORS.map((editor) => (524 {EDITORS.map((editor) => (
525 <button525 <button
526 key={editor.id}526 key={editor.id}
527 type="button"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 onClick={() => openInEditor(editor.id)}529 onClick={() => openInEditor(editor.id)}
530 >530 >
531 {editorLabel(editor)}531 {editorLabel(editor)}
532 {editor.id === lastEditor && (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 {navigator.platform.includes("Mac")534 {navigator.platform.includes("Mac")
535 ? "\u2318O"535 ? "\u2318O"
536 : "Ctrl+O"}536 : "Ctrl+O"}
...@@ -547,8 +547,8 @@ export default function ChatView() {...@@ -547,8 +547,8 @@ export default function ChatView() {
547 type="button"547 type="button"
548 className={`rounded-md px-2 py-1 text-[10px] transition-colors duration-150 ${548 className={`rounded-md px-2 py-1 text-[10px] transition-colors duration-150 ${
549 state.diffOpen549 state.diffOpen
550 ? "bg-white/10 text-white"550 ? "bg-accent text-foreground"
551 : "text-[#a0a0a0]/40 hover:text-[#a0a0a0]/60"551 : "text-muted-foreground/40 hover:text-muted-foreground/60"
552 }`}552 }`}
553 onClick={() => dispatch({ type: "TOGGLE_DIFF" })}553 onClick={() => dispatch({ type: "TOGGLE_DIFF" })}
554 >554 >
...@@ -591,7 +591,7 @@ export default function ChatView() {...@@ -591,7 +591,7 @@ export default function ChatView() {
591 <div className="mt-2 flex flex-wrap gap-1.5">591 <div className="mt-2 flex flex-wrap gap-1.5">
592 <button592 <button
593 type="button"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 disabled={isResponding}595 disabled={isResponding}
596 onClick={() =>596 onClick={() =>
597 void onRespondToApproval(approval.requestId, "accept")597 void onRespondToApproval(approval.requestId, "accept")
...@@ -614,7 +614,7 @@ export default function ChatView() {...@@ -614,7 +614,7 @@ export default function ChatView() {
614 </button>614 </button>
615 <button615 <button
616 type="button"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 disabled={isResponding}618 disabled={isResponding}
619 onClick={() =>619 onClick={() =>
620 void onRespondToApproval(approval.requestId, "decline")620 void onRespondToApproval(approval.requestId, "decline")
...@@ -643,7 +643,7 @@ export default function ChatView() {...@@ -643,7 +643,7 @@ export default function ChatView() {
643 <div className="flex-1 overflow-y-auto px-5 py-4">643 <div className="flex-1 overflow-y-auto px-5 py-4">
644 {activeThread.messages.length === 0 && !isWorking ? (644 {activeThread.messages.length === 0 && !isWorking ? (
645 <div className="flex h-full items-center justify-center">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 </div>647 </div>
648 ) : (648 ) : (
649 <div className="mx-auto max-w-3xl space-y-4">649 <div className="mx-auto max-w-3xl space-y-4">
...@@ -654,18 +654,18 @@ export default function ChatView() {...@@ -654,18 +654,18 @@ export default function ChatView() {
654 (completionDividerBeforeEntryId === timelineEntry.id ||654 (completionDividerBeforeEntryId === timelineEntry.id ||
655 timelineEntries[index - 1]?.kind === "work") && (655 timelineEntries[index - 1]?.kind === "work") && (
656 <div className="my-3 flex items-center gap-3">656 <div className="my-3 flex items-center gap-3">
657 <span className="h-px flex-1 bg-white/[0.1]" />657 <span className="h-px flex-1 bg-border" />
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">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 {completionSummary659 {completionSummary
660 ? `Response • ${completionSummary}`660 ? `Response • ${completionSummary}`
661 : "Response"}661 : "Response"}
662 </span>662 </span>
663 <span className="h-px flex-1 bg-white/[0.1]" />663 <span className="h-px flex-1 bg-border" />
664 </div>664 </div>
665 )}665 )}
666 {timelineEntry.kind === "work" ? (666 {timelineEntry.kind === "work" ? (
667 <div className="flex items-start gap-2 py-0.5 pl-1.5">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 <p669 <p
670 className={`py-[2px] text-[11px] leading-relaxed ${workToneClass(timelineEntry.entry.tone)}`}670 className={`py-[2px] text-[11px] leading-relaxed ${workToneClass(timelineEntry.entry.tone)}`}
671 >671 >
...@@ -686,11 +686,11 @@ export default function ChatView() {...@@ -686,11 +686,11 @@ export default function ChatView() {
686 </div>686 </div>
687 ) : timelineEntry.message.role === "user" ? (687 ) : timelineEntry.message.role === "user" ? (
688 <div className="flex justify-end">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">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-[#e0e0e0]">690 <pre className="whitespace-pre-wrap break-words font-mono text-sm leading-relaxed text-foreground">
691 {timelineEntry.message.text}691 {timelineEntry.message.text}
692 </pre>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 {formatTimestamp(timelineEntry.message.createdAt)}694 {formatTimestamp(timelineEntry.message.createdAt)}
695 </p>695 </p>
696 </div>696 </div>
...@@ -717,7 +717,7 @@ export default function ChatView() {...@@ -717,7 +717,7 @@ export default function ChatView() {
717 </span>717 </span>
718 </div>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 {formatMessageMeta(721 {formatMessageMeta(
722 timelineEntry.message.createdAt,722 timelineEntry.message.createdAt,
723 timelineEntry.message.streaming723 timelineEntry.message.streaming
...@@ -739,12 +739,12 @@ export default function ChatView() {...@@ -739,12 +739,12 @@ export default function ChatView() {
739 ))}739 ))}
740 {isWorking && (740 {isWorking && (
741 <div className="flex items-center gap-2 py-0.5 pl-1.5">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 <div className="flex items-center pt-1">743 <div className="flex items-center pt-1">
744 <span className="inline-flex items-center gap-[3px]">744 <span className="inline-flex items-center gap-[3px]">
745 <span className="h-1 w-1 rounded-full bg-white/20 animate-pulse" />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-white/20 animate-pulse [animation-delay:200ms]" />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-white/20 animate-pulse [animation-delay:400ms]" />747 <span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse [animation-delay:400ms]" />
748 </span>748 </span>
749 </div>749 </div>
750 </div>750 </div>
...@@ -757,12 +757,12 @@ export default function ChatView() {...@@ -757,12 +757,12 @@ export default function ChatView() {
757 {/* Input bar */}757 {/* Input bar */}
758 <div className="px-5 pb-4 pt-2">758 <div className="px-5 pb-4 pt-2">
759 <form onSubmit={onSend} className="mx-auto max-w-3xl">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 {/* Textarea area */}761 {/* Textarea area */}
762 <div className="px-4 pt-4 pb-2">762 <div className="px-4 pt-4 pb-2">
763 <textarea763 <textarea
764 ref={textareaRef}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 rows={2}766 rows={2}
767 value={prompt}767 value={prompt}
768 onChange={(e) => setPrompt(e.target.value)}768 onChange={(e) => setPrompt(e.target.value)}
...@@ -781,7 +781,7 @@ export default function ChatView() {...@@ -781,7 +781,7 @@ export default function ChatView() {
781 <div className="relative" ref={modelMenuRef}>781 <div className="relative" ref={modelMenuRef}>
782 <button782 <button
783 type="button"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 onClick={() => setIsModelMenuOpen((open) => !open)}785 onClick={() => setIsModelMenuOpen((open) => !open)}
786 >786 >
787 <span className="max-w-[180px] truncate">{selectedModel}</span>787 <span className="max-w-[180px] truncate">{selectedModel}</span>
...@@ -803,8 +803,8 @@ export default function ChatView() {...@@ -803,8 +803,8 @@ export default function ChatView() {
803 </svg>803 </svg>
804 </button>804 </button>
805 {isModelMenuOpen && (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">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-[#a0a0a0]/70">Select model</p>807 <p className="px-2 py-1 text-[11px] text-muted-foreground/70">Select model</p>
808 <div className="max-h-72 overflow-y-auto">808 <div className="max-h-72 overflow-y-auto">
809 {modelOptions.map((model) => {809 {modelOptions.map((model) => {
810 const isSelected = model === selectedModel;810 const isSelected = model === selectedModel;
...@@ -814,15 +814,15 @@ export default function ChatView() {...@@ -814,15 +814,15 @@ export default function ChatView() {
814 type="button"814 type="button"
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 ${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 isSelected816 isSelected
817 ? "bg-white/8 text-white"817 ? "bg-accent text-foreground"
818 : "text-[#d4d4d4] hover:bg-white/5"818 : "text-foreground/90 hover:bg-accent"
819 }`}819 }`}
820 onClick={() => onModelSelect(model)}820 onClick={() => onModelSelect(model)}
821 >821 >
822 <span className="truncate">{model}</span>822 <span className="truncate">{model}</span>
823 <span823 <span
824 className={`pt-0.5 text-sm ${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,11 +836,11 @@ export default function ChatView() {
836 </div>836 </div>
837837
838 {/* Divider */}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" />
840840
841 {/* Reasoning effort */}841 {/* Reasoning effort */}
842 <label842 <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 htmlFor="reasoning-effort"844 htmlFor="reasoning-effort"
845 >845 >
846 <span>{selectedEffort.charAt(0).toUpperCase() + selectedEffort.slice(1)}</span>846 <span>{selectedEffort.charAt(0).toUpperCase() + selectedEffort.slice(1)}</span>
...@@ -851,7 +851,7 @@ export default function ChatView() {...@@ -851,7 +851,7 @@ export default function ChatView() {
851 onChange={(event) => setSelectedEffort(event.target.value)}851 onChange={(event) => setSelectedEffort(event.target.value)}
852 >852 >
853 {REASONING_OPTIONS.map((effort) => (853 {REASONING_OPTIONS.map((effort) => (
854 <option key={effort} value={effort} className="bg-[#1b1b1d]">854 <option key={effort} value={effort} className="bg-popover">
855 {effort}855 {effort}
856 {effort === DEFAULT_REASONING ? " (default)" : ""}856 {effort === DEFAULT_REASONING ? " (default)" : ""}
857 </option>857 </option>
...@@ -876,12 +876,12 @@ export default function ChatView() {...@@ -876,12 +876,12 @@ export default function ChatView() {
876 </label>876 </label>
877877
878 {/* Divider */}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" />
880880
881 {/* Runtime mode toggle */}881 {/* Runtime mode toggle */}
882 <button882 <button
883 type="button"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 disabled={isSwitchingRuntimeMode}885 disabled={isSwitchingRuntimeMode}
886 onClick={() =>886 onClick={() =>
887 void handleRuntimeModeChange(887 void handleRuntimeModeChange(
...@@ -975,7 +975,7 @@ export default function ChatView() {...@@ -975,7 +975,7 @@ export default function ChatView() {
975 ) : (975 ) : (
976 <button976 <button
977 type="submit"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 disabled={isSending || isConnecting || !prompt.trim()}979 disabled={isSending || isConnecting || !prompt.trim()}
980 aria-label={980 aria-label={
981 isConnecting ? "Connecting" : isSending ? "Sending" : "Send message"981 isConnecting ? "Connecting" : isSending ? "Sending" : "Send message"
apps/renderer/src/components/DiffPanel.tsx+9-9
...@@ -4,13 +4,13 @@ export default function DiffPanel() {...@@ -4,13 +4,13 @@ export default function DiffPanel() {
4 const { dispatch } = useStore();4 const { dispatch } = useStore();
55
6 return (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 {/* Header */}8 {/* Header */}
9 <div className="drag-region flex items-center justify-between border-b border-white/[0.08] px-4 pt-[28px] pb-3">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-[#e0e0e0]">Uncommitted changes</h3>10 <h3 className="text-xs font-medium text-foreground">Uncommitted changes</h3>
11 <button11 <button
12 type="button"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 onClick={() => dispatch({ type: "TOGGLE_DIFF" })}14 onClick={() => dispatch({ type: "TOGGLE_DIFF" })}
15 >15 >
16 <span className="text-sm">&times;</span>16 <span className="text-sm">&times;</span>
...@@ -20,12 +20,12 @@ export default function DiffPanel() {...@@ -20,12 +20,12 @@ export default function DiffPanel() {
20 {/* Placeholder content */}20 {/* Placeholder content */}
21 <div className="flex-1 overflow-y-auto p-4">21 <div className="flex-1 overflow-y-auto p-4">
22 <div className="space-y-3">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 <div className="mb-2 flex items-center gap-2">24 <div className="mb-2 flex items-center gap-2">
25 <span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[9px] font-medium text-emerald-400">25 <span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[9px] font-medium text-emerald-400">
26 M26 M
27 </span>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 </div>29 </div>
30 <div className="space-y-0.5 font-mono text-[11px]">30 <div className="space-y-0.5 font-mono text-[11px]">
31 <div className="text-red-400/60">- const oldValue = "hello";</div>31 <div className="text-red-400/60">- const oldValue = "hello";</div>
...@@ -33,17 +33,17 @@ export default function DiffPanel() {...@@ -33,17 +33,17 @@ export default function DiffPanel() {
33 </div>33 </div>
34 </div>34 </div>
3535
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 <div className="flex items-center gap-2">37 <div className="flex items-center gap-2">
38 <span className="rounded bg-blue-500/20 px-1.5 py-0.5 text-[9px] font-medium text-blue-400">38 <span className="rounded bg-blue-500/20 px-1.5 py-0.5 text-[9px] font-medium text-blue-400">
39 A39 A
40 </span>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 </div>42 </div>
43 </div>43 </div>
44 </div>44 </div>
4545
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 Diff integration coming soon47 Diff integration coming soon
48 </p>48 </p>
49 </div>49 </div>
apps/renderer/src/components/Sidebar.tsx+46-24
...@@ -1,9 +1,13 @@...@@ -1,9 +1,13 @@
1import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
1import { useCallback, useEffect, useMemo, useState } from "react";2import { useCallback, useEffect, useMemo, useState } from "react";
3import { useTheme } from "../hooks/useTheme";
2import { DEFAULT_MODEL } from "../model-logic";4import { DEFAULT_MODEL } from "../model-logic";
3import { readNativeApi } from "../session-logic";5import { readNativeApi } from "../session-logic";
4import { useStore } from "../store";6import { useStore } from "../store";
5import type { Project } from "../types";7import type { Project } from "../types";
68
9const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const;
10
711
8function formatRelativeTime(iso: string): string {12function formatRelativeTime(iso: string): string {
9 const diff = Date.now() - new Date(iso).getTime();13 const diff = Date.now() - new Date(iso).getTime();
...@@ -26,6 +30,7 @@ function threadStatusLabel(...@@ -26,6 +30,7 @@ function threadStatusLabel(
26export default function Sidebar() {30export default function Sidebar() {
27 const { state, dispatch } = useStore();31 const { state, dispatch } = useStore();
28 const api = useMemo(() => readNativeApi(), []);32 const api = useMemo(() => readNativeApi(), []);
33 const { theme, setTheme } = useTheme();
29 const [addingProject, setAddingProject] = useState(false);34 const [addingProject, setAddingProject] = useState(false);
30 const [newCwd, setNewCwd] = useState("");35 const [newCwd, setNewCwd] = useState("");
31 const [isPickingFolder, setIsPickingFolder] = useState(false);36 const [isPickingFolder, setIsPickingFolder] = useState(false);
...@@ -120,22 +125,39 @@ export default function Sidebar() {...@@ -120,22 +125,39 @@ export default function Sidebar() {
120 }, [handleNewThread, state.activeThreadId, state.projects, state.threads]);125 }, [handleNewThread, state.activeThreadId, state.projects, state.threads]);
121126
122 return (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 {/* Drag region / traffic light space */}129 {/* Drag region / traffic light space */}
125 <div className="drag-region h-[52px] shrink-0" />130 <div className="drag-region h-[52px] shrink-0" />
126 {/* Branding */}131 {/* Branding */}
127 <div className="flex items-center gap-2 px-4 pb-4">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 CT134 CT
130 </div>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 </div>154 </div>
133155
134 {/* New thread (global) */}156 {/* New thread (global) */}
135 <div className="px-3 pb-3">157 <div className="px-3 pb-3">
136 <button158 <button
137 type="button"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 onClick={() => {161 onClick={() => {
140 if (state.projects.length === 0) {162 if (state.projects.length === 0) {
141 setAddingProject(true);163 setAddingProject(true);
...@@ -145,7 +167,7 @@ export default function Sidebar() {...@@ -145,7 +167,7 @@ export default function Sidebar() {
145 if (firstProject) handleNewThread(firstProject.id);167 if (firstProject) handleNewThread(firstProject.id);
146 }}168 }}
147 >169 >
148 <span className="text-white">+</span>170 <span className="text-foreground">+</span>
149 New thread171 New thread
150 </button>172 </button>
151 </div>173 </div>
...@@ -167,21 +189,21 @@ export default function Sidebar() {...@@ -167,21 +189,21 @@ export default function Sidebar() {
167 {/* Project header */}189 {/* Project header */}
168 <button190 <button
169 type="button"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 onClick={() => dispatch({ type: "TOGGLE_PROJECT", projectId: project.id })}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 {project.expanded ? "â–¼" : "â–¶"}196 {project.expanded ? "â–¼" : "â–¶"}
175 </span>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 {project.name}199 {project.name}
178 </span>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 </button>202 </button>
181203
182 {/* Threads */}204 {/* Threads */}
183 {project.expanded && (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 {threads.map((thread) => {207 {threads.map((thread) => {
186 const isActive = state.activeThreadId === thread.id;208 const isActive = state.activeThreadId === thread.id;
187 const threadStatus = threadStatusLabel(209 const threadStatus = threadStatusLabel(
...@@ -192,7 +214,7 @@ export default function Sidebar() {...@@ -192,7 +214,7 @@ export default function Sidebar() {
192 key={thread.id}214 key={thread.id}
193 type="button"215 type="button"
194 className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 ${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 onClick={() =>219 onClick={() =>
198 dispatch({220 dispatch({
...@@ -203,8 +225,8 @@ export default function Sidebar() {...@@ -203,8 +225,8 @@ export default function Sidebar() {
203 >225 >
204 <div className="flex min-w-0 flex-1 items-center gap-1.5">226 <div className="flex min-w-0 flex-1 items-center gap-1.5">
205 {threadStatus && (227 {threadStatus && (
206 <span className="inline-flex items-center gap-1 text-[10px] text-[#7ed8ff]/80">228 <span className="inline-flex items-center gap-1 text-[10px] text-sky-600 dark:text-sky-300/80">
207 <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-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 <span className="hidden xl:inline">230 <span className="hidden xl:inline">
209 {threadStatus}231 {threadStatus}
210 </span>232 </span>
...@@ -214,7 +236,7 @@ export default function Sidebar() {...@@ -214,7 +236,7 @@ export default function Sidebar() {
214 {thread.title}236 {thread.title}
215 </span>237 </span>
216 </div>238 </div>
217 <span className="shrink-0 text-[10px] text-[#a0a0a0]/30">239 <span className="shrink-0 text-[10px] text-muted-foreground/40">
218 {formatRelativeTime(thread.createdAt)}240 {formatRelativeTime(thread.createdAt)}
219 </span>241 </span>
220 </button>242 </button>
...@@ -224,7 +246,7 @@ export default function Sidebar() {...@@ -224,7 +246,7 @@ export default function Sidebar() {
224 {/* New thread within project */}246 {/* New thread within project */}
225 <button247 <button
226 type="button"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 onClick={() => handleNewThread(project.id)}250 onClick={() => handleNewThread(project.id)}
229 >251 >
230 <span>+</span> New thread252 <span>+</span> New thread
...@@ -236,7 +258,7 @@ export default function Sidebar() {...@@ -236,7 +258,7 @@ export default function Sidebar() {
236 })}258 })}
237259
238 {state.projects.length === 0 && !addingProject && (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 No projects yet.262 No projects yet.
241 <br />263 <br />
242 Add one to get started.264 Add one to get started.
...@@ -246,12 +268,12 @@ export default function Sidebar() {...@@ -246,12 +268,12 @@ export default function Sidebar() {
246268
247 {/* Add project form */}269 {/* Add project form */}
248 {addingProject ? (270 {addingProject ? (
249 <div className="border-t border-white/8 p-3">271 <div className="border-t border-border p-3">
250 <p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-[#a0a0a0]/50">272 <p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70">
251 Add project273 Add project
252 </p>274 </p>
253 <input275 <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 placeholder="/path/to/project"277 placeholder="/path/to/project"
256 value={newCwd}278 value={newCwd}
257 onChange={(e) => setNewCwd(e.target.value)}279 onChange={(e) => setNewCwd(e.target.value)}
...@@ -263,7 +285,7 @@ export default function Sidebar() {...@@ -263,7 +285,7 @@ export default function Sidebar() {
263 {api && (285 {api && (
264 <button286 <button
265 type="button"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 onClick={() => void handlePickFolder()}289 onClick={() => void handlePickFolder()}
268 disabled={isPickingFolder}290 disabled={isPickingFolder}
269 >291 >
...@@ -273,14 +295,14 @@ export default function Sidebar() {...@@ -273,14 +295,14 @@ export default function Sidebar() {
273 <div className="flex gap-2">295 <div className="flex gap-2">
274 <button296 <button
275 type="button"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 onClick={handleAddProject}299 onClick={handleAddProject}
278 >300 >
279 Add301 Add
280 </button>302 </button>
281 <button303 <button
282 type="button"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 onClick={() => setAddingProject(false)}306 onClick={() => setAddingProject(false)}
285 >307 >
286 Cancel308 Cancel
...@@ -288,10 +310,10 @@ export default function Sidebar() {...@@ -288,10 +310,10 @@ export default function Sidebar() {
288 </div>310 </div>
289 </div>311 </div>
290 ) : (312 ) : (
291 <div className="border-t border-white/8 p-3">313 <div className="border-t border-border p-3">
292 <button314 <button
293 type="button"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 onClick={() => setAddingProject(true)}317 onClick={() => setAddingProject(true)}
296 >318 >
297 + Add project319 + Add project
apps/renderer/src/hooks/useTheme.ts created+90
...@@ -0,0 +1,90 @@
1import { useCallback, useEffect, useSyncExternalStore } from "react";
2
3type Theme = "light" | "dark" | "system";
4
5const STORAGE_KEY = "codething:theme";
6const MEDIA_QUERY = "(prefers-color-scheme: dark)";
7
8let listeners: Array<() => void> = [];
9function emitChange() {
10 for (const listener of listeners) listener();
11}
12
13function getSystemDark(): boolean {
14 return window.matchMedia(MEDIA_QUERY).matches;
15}
16
17function getStored(): Theme {
18 const raw = localStorage.getItem(STORAGE_KEY);
19 if (raw === "light" || raw === "dark" || raw === "system") return raw;
20 return "system";
21}
22
23function 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
39applyTheme(getStored());
40
41function getSnapshot(): Theme {
42 return getStored();
43}
44
45function 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
72export 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,7 +1,178 @@
1@import "tailwindcss";1@import "tailwindcss";
22
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:root {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}
6177
7body {178body {
...@@ -10,8 +181,6 @@ body {...@@ -10,8 +181,6 @@ body {
10 margin: 0;181 margin: 0;
11 padding: 0;182 padding: 0;
12 min-height: 100vh;183 min-height: 100vh;
13 color: #e0e0e0;
14 background: #0c0c0c;
15}184}
16185
17body::after {186body::after {
...@@ -55,15 +224,23 @@ input {...@@ -55,15 +224,23 @@ input {
55}224}
56225
57::-webkit-scrollbar-thumb {226::-webkit-scrollbar-thumb {
58 background: rgba(255, 255, 255, 0.1);227 background: rgba(0, 0, 0, 0.15);
59 border-radius: 3px;228 border-radius: 3px;
60}229}
61230
62::-webkit-scrollbar-thumb:hover {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 background: rgba(255, 255, 255, 0.18);240 background: rgba(255, 255, 255, 0.18);
64}241}
65242
66/* Reasoning select — clickable label surface */243/* Reasoning select -- clickable label surface */
67label:has(> select#reasoning-effort) {244label:has(> select#reasoning-effort) {
68 position: relative;245 position: relative;
69}246}
...@@ -108,34 +285,38 @@ label:has(> select#reasoning-effort) select {...@@ -108,34 +285,38 @@ label:has(> select#reasoning-effort) select {
108}285}
109286
110.chat-markdown a {287.chat-markdown a {
111 color: #93c5fd;288 color: var(--info-foreground);
112 text-decoration: underline;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}
115296
116.chat-markdown a:hover {297.chat-markdown a:hover {
117 color: #bfdbfe;298 opacity: 0.8;
118}299}
119300
120.chat-markdown blockquote {301.chat-markdown blockquote {
121 border-left: 2px solid rgba(255, 255, 255, 0.2);302 border-left: 2px solid var(--border);
122 padding-left: 0.8rem;303 padding-left: 0.8rem;
123 color: rgba(224, 224, 224, 0.75);304 color: var(--muted-foreground);
124}305}
125306
126.chat-markdown :not(pre) > code {307.chat-markdown :not(pre) > code {
127 border: 1px solid rgba(255, 255, 255, 0.12);308 border: 1px solid var(--border);
128 border-radius: 0.375rem;309 border-radius: 0.375rem;
129 background: rgba(255, 255, 255, 0.07);310 background: var(--muted);
130 padding: 0.1rem 0.35rem;311 padding: 0.1rem 0.35rem;
131 color: #f4f4f5;312 color: var(--foreground);
132}313}
133314
134.chat-markdown pre {315.chat-markdown pre {
135 overflow-x: auto;316 overflow-x: auto;
136 border: 1px solid rgba(255, 255, 255, 0.12);317 border: 1px solid var(--border);
137 border-radius: 0.75rem;318 border-radius: 0.75rem;
138 background: rgba(0, 0, 0, 0.3);319 background: var(--muted);
139 padding: 0.8rem 0.9rem;320 padding: 0.8rem 0.9rem;
140}321}
141322
...@@ -153,7 +334,7 @@ label:has(> select#reasoning-effort) select {...@@ -153,7 +334,7 @@ label:has(> select#reasoning-effort) select {
153334
154.chat-markdown th,335.chat-markdown th,
155.chat-markdown td {336.chat-markdown td {
156 border: 1px solid rgba(255, 255, 255, 0.12);337 border: 1px solid var(--border);
157 padding: 0.35rem 0.45rem;338 padding: 0.35rem 0.45rem;
158 text-align: left;339 text-align: left;
159}340}
bun.lock+3
...@@ -35,6 +35,7 @@...@@ -35,6 +35,7 @@
35 "dependencies": {35 "dependencies": {
36 "@acme/contracts": "workspace:*",36 "@acme/contracts": "workspace:*",
37 "highlight.js": "^11.11.1",37 "highlight.js": "^11.11.1",
38 "lucide-react": "^0.563.0",
38 "react": "^19.0.0",39 "react": "^19.0.0",
39 "react-dom": "^19.0.0",40 "react-dom": "^19.0.0",
40 "react-markdown": "^10.1.0",41 "react-markdown": "^10.1.0",
...@@ -754,6 +755,8 @@...@@ -754,6 +755,8 @@
754755
755 "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],756 "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
756757
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 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],760 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
758761
759 "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=="],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=="],