feat: blue theme migration and chat input UI polish

- Migrate purple/violet theme to blue across entire codebase (globals.css,
  ToolCallBlock, ChatListPanel, SkillEditor, SkillListItem, e2e tests)
- Increase input box border radius (rounded-md → rounded-2xl) and shadow
- Add arrow flip animation on mode/model selector dropdowns
- Change send button to circular with up-arrow icon
- Increase global Hugeicons stroke weight (1.5 → 2)
- Remove theme-colored focus ring on input box

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-09 19:48:06 +08:00
1 parent ee65c62d62
commit 9635585ab6
11 files changed
+54 -35

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "codepilot",
"version": "0.6.1",
"version": "0.6.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codepilot",
"version": "0.6.1",
"version": "0.6.2",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.33",
"@hugeicons/core-free-icons": "^3.1.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "codepilot",
"version": "0.6.1",
"version": "0.6.2",
"private": true,
"main": "dist-electron/main.js",
"scripts": {
+5 -5
View File
@@ -30,7 +30,7 @@ test.describe('Chat UI Enhanced (V2)', () => {
// Wait for assistant response -- V2 uses gradient avatar instead of text label
// Detect assistant response via the gradient avatar
await expect(
page.locator('.from-violet-500')
page.locator('[data-role="assistant"]')
).toBeVisible({ timeout: 30_000 });
// Wait for streaming to complete
@@ -50,7 +50,7 @@ test.describe('Chat UI Enhanced (V2)', () => {
await sendMessage(page, 'Show me a Python hello world');
await expect(
page.locator('.from-violet-500')
page.locator('[data-role="assistant"]')
).toBeVisible({ timeout: 30_000 });
await waitForStreamingEnd(page);
@@ -103,9 +103,9 @@ test.describe('Chat UI Enhanced (V2)', () => {
await goToChat(page);
await sendMessage(page, 'Hi there');
// V2: Assistant avatar uses gradient bg (from-violet-500 to-purple-600)
// Detect assistant response via data-role attribute
await expect(
page.locator('.from-violet-500')
page.locator('[data-role="assistant"]')
).toBeVisible({ timeout: 30_000 });
});
@@ -125,7 +125,7 @@ test.describe('Chat UI Enhanced (V2)', () => {
// V2: Assistant avatar is gradient, not bg-primary
await expect(
page.locator('.from-violet-500:has(svg)')
page.locator('[data-role="assistant"]')
).toBeVisible({ timeout: 30_000 });
});
});
+2 -2
View File
@@ -116,9 +116,9 @@ test.describe('Chat Page', () => {
await goToChat(page);
await sendMessage(page, 'Say hello');
// V2: Wait for assistant response -- gradient avatar appears
// Wait for assistant response
await expect(
page.locator('.from-violet-500')
page.locator('[data-role="assistant"]')
).toBeVisible({ timeout: 10_000 });
});
+25 -15
View File
@@ -54,7 +54,7 @@
--card-foreground: oklch(0.147 0.004 49.25);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.147 0.004 49.25);
--primary: oklch(0.457 0.24 277.023);
--primary: oklch(0.546 0.245 262.881);
--primary-foreground: oklch(0.985 0.001 106.423);
--secondary: oklch(0.97 0.001 106.424);
--secondary-foreground: oklch(0.216 0.006 56.043);
@@ -65,20 +65,20 @@
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.923 0.003 48.717);
--input: oklch(0.923 0.003 48.717);
--ring: oklch(0.457 0.24 277.023);
--chart-1: oklch(0.457 0.24 277.023);
--ring: oklch(0.546 0.245 262.881);
--chart-1: oklch(0.546 0.245 262.881);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0.001 106.423);
--sidebar-foreground: oklch(0.147 0.004 49.25);
--sidebar-primary: oklch(0.457 0.24 277.023);
--sidebar-primary: oklch(0.546 0.245 262.881);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.97 0.001 106.424);
--sidebar-accent-foreground: oklch(0.216 0.006 56.043);
--sidebar-border: oklch(0.923 0.003 48.717);
--sidebar-ring: oklch(0.457 0.24 277.023);
--sidebar-ring: oklch(0.546 0.245 262.881);
}
.dark {
@@ -88,7 +88,7 @@
--card-foreground: oklch(0.985 0.001 106.423);
--popover: oklch(0.216 0.006 56.043);
--popover-foreground: oklch(0.985 0.001 106.423);
--primary: oklch(0.585 0.233 277.117);
--primary: oklch(0.623 0.214 259.815);
--primary-foreground: oklch(0.985 0.001 106.423);
--secondary: oklch(0.268 0.007 34.298);
--secondary-foreground: oklch(0.985 0.001 106.423);
@@ -99,20 +99,20 @@
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.585 0.233 277.117);
--chart-1: oklch(0.585 0.233 277.117);
--ring: oklch(0.623 0.214 259.815);
--chart-1: oklch(0.623 0.214 259.815);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.216 0.006 56.043);
--sidebar-foreground: oklch(0.985 0.001 106.423);
--sidebar-primary: oklch(0.585 0.233 277.117);
--sidebar-primary: oklch(0.623 0.214 259.815);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.268 0.007 34.298);
--sidebar-accent-foreground: oklch(0.985 0.001 106.423);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.585 0.233 277.117);
--sidebar-ring: oklch(0.623 0.214 259.815);
}
@layer base {
@@ -125,10 +125,10 @@
/* Selection color */
::selection {
background: oklch(0.7 0.15 280 / 30%);
background: oklch(0.7 0.15 260 / 30%);
}
.dark ::selection {
background: oklch(0.6 0.2 280 / 35%);
background: oklch(0.6 0.2 260 / 35%);
}
/* Custom scrollbar */
@@ -179,16 +179,16 @@
margin-bottom: 0.25em;
}
.prose blockquote {
border-left-color: oklch(0.7 0.15 280 / 40%);
border-left-color: oklch(0.7 0.15 260 / 40%);
font-style: normal;
}
.prose a {
color: oklch(0.55 0.2 260);
color: oklch(0.55 0.2 250);
text-decoration: underline;
text-underline-offset: 2px;
}
.dark .prose a {
color: oklch(0.7 0.18 260);
color: oklch(0.7 0.18 250);
}
.prose hr {
border-color: var(--border);
@@ -197,3 +197,13 @@
font-weight: 600;
}
}
/* Global icon stroke weight — make Hugeicons strokes slightly thicker */
svg.hugeicons,
svg.hugeicons path,
svg.hugeicons line,
svg.hugeicons circle,
svg.hugeicons polyline,
svg.hugeicons rect {
stroke-width: 2;
}
+12 -3
View File
@@ -9,6 +9,7 @@ import {
ClipboardIcon,
HelpCircleIcon,
ArrowDown01Icon,
ArrowUp02Icon,
CommandLineIcon,
Attachment01Icon,
Cancel01Icon,
@@ -32,6 +33,7 @@ import {
PromptInputSubmit,
usePromptInputAttachments,
} from '@/components/ai-elements/prompt-input';
import { SquareIcon } from 'lucide-react';
import type { ChatStatus } from 'ai';
import type { FileAttachment } from '@/types';
import { nanoid } from 'nanoid';
@@ -221,7 +223,14 @@ function FileAwareSubmitButton({
status={status}
onStop={onStop}
disabled={disabled || (!isStreaming && !inputValue.trim() && !hasBadge && !hasFiles)}
/>
className="rounded-full"
>
{isStreaming ? (
<SquareIcon className="size-4" />
) : (
<HugeiconsIcon icon={ArrowUp02Icon} className="h-4 w-4" strokeWidth={2} />
)}
</PromptInputSubmit>
);
}
@@ -887,7 +896,7 @@ export function MessageInput({
>
<HugeiconsIcon icon={currentMode.icon} className="h-3.5 w-3.5" />
<span className="text-xs">{currentMode.label}</span>
<HugeiconsIcon icon={ArrowDown01Icon} className="h-2.5 w-2.5" />
<HugeiconsIcon icon={ArrowDown01Icon} className={cn("h-2.5 w-2.5 transition-transform duration-200", modeMenuOpen && "rotate-180")} />
</PromptInputButton>
{/* Mode dropdown */}
@@ -931,7 +940,7 @@ export function MessageInput({
onClick={() => setModelMenuOpen((prev) => !prev)}
>
<span className="text-xs font-mono">{currentModelOption.label}</span>
<HugeiconsIcon icon={ArrowDown01Icon} className="h-2.5 w-2.5" />
<HugeiconsIcon icon={ArrowDown01Icon} className={cn("h-2.5 w-2.5 transition-transform duration-200", modelMenuOpen && "rotate-180")} />
</PromptInputButton>
{modelMenuOpen && (
+1 -1
View File
@@ -312,7 +312,7 @@ export function ToolCallBlock({
category === 'read' && "text-blue-500",
category === 'write' && "text-amber-500",
category === 'bash' && "text-green-500",
category === 'search' && "text-purple-500",
category === 'search' && "text-indigo-500",
category === 'other' && "text-zinc-500",
)} />
<span className="font-mono text-xs truncate flex-1 text-foreground/80">{summary}</span>
+1 -1
View File
@@ -65,7 +65,7 @@ function groupSessionsByDate(
const MODE_BADGE_CONFIG = {
code: { label: "Code", className: "bg-blue-500/10 text-blue-500" },
plan: { label: "Plan", className: "bg-purple-500/10 text-purple-500" },
plan: { label: "Plan", className: "bg-sky-500/10 text-sky-500" },
ask: { label: "Ask", className: "bg-green-500/10 text-green-500" },
} as const;
+1 -1
View File
@@ -124,7 +124,7 @@ export function SkillEditor({ skill, onSave, onDelete }: SkillEditorProps) {
: skill.source === "installed"
? "border-orange-500/40 text-orange-600 dark:text-orange-400"
: skill.source === "plugin"
? "border-purple-500/40 text-purple-600 dark:text-purple-400"
? "border-indigo-500/40 text-indigo-600 dark:text-indigo-400"
: "border-blue-500/40 text-blue-600 dark:text-blue-400"
)}
>
+1 -1
View File
@@ -77,7 +77,7 @@ export function SkillListItem({
: skill.source === "installed"
? "border-orange-500/40 text-orange-600 dark:text-orange-400"
: skill.source === "plugin"
? "border-purple-500/40 text-purple-600 dark:text-purple-400"
? "border-indigo-500/40 text-indigo-600 dark:text-indigo-400"
: "border-blue-500/40 text-blue-600 dark:text-blue-400"
)}
>
+3 -3
View File
@@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
data-slot="input-group"
role="group"
className={cn(
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-2xl border shadow-md transition-[color,box-shadow] outline-none",
"h-9 min-w-0 has-[>textarea]:h-auto",
// Variants based on alignment.
@@ -23,8 +23,8 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
// Focus state.
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
// Focus state — neutral border only, no colored ring.
"has-[[data-slot=input-group-control]:focus-visible]:border-border",
// Error state.
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",