feat: enhance CoordinatorEmptyState and ThemeSelector with gradient utilities and improved layout

This commit is contained in:
catlog22
2026-02-04 23:06:00 +08:00
parent de989aa038
commit 369b470969
6 changed files with 79 additions and 58 deletions

View File

@@ -49,27 +49,18 @@ export function CoordinatorEmptyState({
{/* Animated Gradient Orbs - Using gradient utility classes */} {/* Animated Gradient Orbs - Using gradient utility classes */}
<div className="absolute top-20 left-20 w-72 h-72 rounded-full blur-3xl animate-pulse bg-gradient-primary opacity-15" /> <div className="absolute top-20 left-20 w-72 h-72 rounded-full blur-3xl animate-pulse bg-gradient-primary opacity-15" />
<div <div
className="absolute bottom-20 right-20 w-96 h-96 rounded-full blur-3xl animate-pulse opacity-15" className="absolute bottom-20 right-20 w-96 h-96 rounded-full blur-3xl animate-pulse opacity-15 bg-gradient-secondary"
style={{ style={{ animationDelay: '1s' }}
background: 'radial-gradient(circle, hsl(var(--secondary)) 0%, transparent 70%)',
animationDelay: '1s',
}}
/> />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-80 h-80 rounded-full blur-3xl animate-pulse bg-gradient-primary opacity-10" style={{ animationDelay: '2s' }} /> <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-80 h-80 rounded-full blur-3xl animate-pulse bg-gradient-primary opacity-10" style={{ animationDelay: '2s' }} />
</div> </div>
{/* Main Content */} {/* Main Content */}
<div className="relative z-10 max-w-2xl mx-auto px-8 text-center"> <div className="relative z-10 max-w-2xl mx-auto px-8 text-center">
{/* Hero Icon */} {/* Hero Icon - Using gradient brand background */}
<div className="relative mb-8 inline-block"> <div className="relative mb-8 inline-block">
<div <div className="absolute inset-0 rounded-full blur-2xl opacity-40 animate-pulse bg-gradient-brand" />
className="absolute inset-0 rounded-full blur-2xl opacity-40 animate-pulse" <div className="relative p-6 rounded-full shadow-2xl text-white bg-primary hover-glow-primary">
style={{ background: 'hsl(var(--primary))' }}
/>
<div
className="relative p-6 rounded-full shadow-2xl text-white"
style={{ background: 'hsl(var(--primary))' }}
>
<Rocket className="w-16 h-16" strokeWidth={2} /> <Rocket className="w-16 h-16" strokeWidth={2} />
</div> </div>
</div> </div>
@@ -84,23 +75,15 @@ export function CoordinatorEmptyState({
{formatMessage({ id: 'coordinator.emptyState.subtitle' })} {formatMessage({ id: 'coordinator.emptyState.subtitle' })}
</p> </p>
{/* Start Button - Using primary theme color */} {/* Start Button - Using gradient and glow utilities */}
<Button <Button
size="lg" size="lg"
onClick={onStart} onClick={onStart}
disabled={disabled} disabled={disabled}
className="group relative px-8 py-6 text-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-300" className="group relative px-8 py-6 text-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-300 bg-primary text-primary-foreground hover-glow-primary"
style={{
background: 'hsl(var(--primary))',
color: 'hsl(var(--primary-foreground))',
}}
> >
<Play className="w-6 h-6 mr-2 group-hover:scale-110 transition-transform" /> <Play className="w-6 h-6 mr-2 group-hover:scale-110 transition-transform" />
{formatMessage({ id: 'coordinator.emptyState.startButton' })} {formatMessage({ id: 'coordinator.emptyState.startButton' })}
<div
className="absolute inset-0 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity blur-xl"
style={{ background: 'hsl(var(--primary) / 0.3)' }}
/>
</Button> </Button>
{/* Feature Cards */} {/* Feature Cards */}
@@ -175,38 +158,26 @@ export function CoordinatorEmptyState({
{/* Quick Start Guide */} {/* Quick Start Guide */}
<div className="mt-12 text-left bg-card/50 backdrop-blur-sm rounded-xl p-6 border border-border"> <div className="mt-12 text-left bg-card/50 backdrop-blur-sm rounded-xl p-6 border border-border">
<h3 className="font-semibold mb-4 text-foreground flex items-center gap-2"> <h3 className="font-semibold mb-4 text-foreground flex items-center gap-2">
<span <span className="w-6 h-6 rounded-full flex items-center justify-center text-primary-foreground text-xs font-semibold bg-primary">
className="w-6 h-6 rounded-full flex items-center justify-center text-white text-xs font-semibold" ok
style={{ background: 'hsl(var(--primary))' }}
>
</span> </span>
{formatMessage({ id: 'coordinator.emptyState.quickStart.title' })} {formatMessage({ id: 'coordinator.emptyState.quickStart.title' })}
</h3> </h3>
<div className="space-y-3 text-sm text-muted-foreground"> <div className="space-y-3 text-sm text-muted-foreground">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<span <span className="w-5 h-5 rounded-full flex items-center justify-center text-xs font-semibold shrink-0 mt-0.5 text-white bg-primary">
className="w-5 h-5 rounded-full flex items-center justify-center text-xs font-semibold shrink-0 mt-0.5 text-white"
style={{ background: 'hsl(var(--primary))' }}
>
1 1
</span> </span>
<p>{formatMessage({ id: 'coordinator.emptyState.quickStart.step1' })}</p> <p>{formatMessage({ id: 'coordinator.emptyState.quickStart.step1' })}</p>
</div> </div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<span <span className="w-5 h-5 rounded-full flex items-center justify-center text-xs font-semibold shrink-0 mt-0.5 text-white bg-secondary">
className="w-5 h-5 rounded-full flex items-center justify-center text-xs font-semibold shrink-0 mt-0.5 text-white"
style={{ background: 'hsl(var(--secondary))' }}
>
2 2
</span> </span>
<p>{formatMessage({ id: 'coordinator.emptyState.quickStart.step2' })}</p> <p>{formatMessage({ id: 'coordinator.emptyState.quickStart.step2' })}</p>
</div> </div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<span <span className="w-5 h-5 rounded-full flex items-center justify-center text-xs font-semibold shrink-0 mt-0.5 text-white bg-accent">
className="w-5 h-5 rounded-full flex items-center justify-center text-xs font-semibold shrink-0 mt-0.5 text-white"
style={{ background: 'hsl(var(--accent))' }}
>
3 3
</span> </span>
<p>{formatMessage({ id: 'coordinator.emptyState.quickStart.step3' })}</p> <p>{formatMessage({ id: 'coordinator.emptyState.quickStart.step3' })}</p>

View File

@@ -192,25 +192,31 @@ export function ThemeSelector() {
/> />
{/* Preview Swatches */} {/* Preview Swatches */}
<div className="flex gap-2 items-center"> <div className="flex gap-3 items-end">
<span className="text-xs text-text-secondary mr-2"> <span className="text-xs text-text-secondary pb-1">
{formatMessage({ id: 'theme.preview' })}: {formatMessage({ id: 'theme.preview' })}:
</span> </span>
<div className="flex flex-col items-center gap-1">
<div <div
className="w-10 h-10 rounded border-2 border-border shadow-sm" className="w-10 h-10 rounded border-2 border-border shadow-sm"
style={{ backgroundColor: getPreviewColor('--bg') }} style={{ backgroundColor: getPreviewColor('--bg') }}
title="Background"
/> />
<span className="text-[10px] text-text-tertiary">{formatMessage({ id: 'theme.preview.background' })}</span>
</div>
<div className="flex flex-col items-center gap-1">
<div <div
className="w-10 h-10 rounded border-2 border-border shadow-sm" className="w-10 h-10 rounded border-2 border-border shadow-sm"
style={{ backgroundColor: getPreviewColor('--surface') }} style={{ backgroundColor: getPreviewColor('--surface') }}
title="Surface"
/> />
<span className="text-[10px] text-text-tertiary">{formatMessage({ id: 'theme.preview.surface' })}</span>
</div>
<div className="flex flex-col items-center gap-1">
<div <div
className="w-10 h-10 rounded border-2 border-border shadow-sm" className="w-10 h-10 rounded border-2 border-border shadow-sm"
style={{ backgroundColor: getPreviewColor('--accent') }} style={{ backgroundColor: getPreviewColor('--accent') }}
title="Accent"
/> />
<span className="text-[10px] text-text-tertiary">{formatMessage({ id: 'theme.preview.accent' })}</span>
</div>
</div> </div>
{/* Save and Reset Buttons */} {/* Save and Reset Buttons */}

View File

@@ -22,6 +22,9 @@
"current": "Current theme: {name}", "current": "Current theme: {name}",
"hueValue": "Hue: {value}°", "hueValue": "Hue: {value}°",
"preview": "Preview", "preview": "Preview",
"preview.background": "Background",
"preview.surface": "Card",
"preview.accent": "Accent",
"save": "Save Custom Theme", "save": "Save Custom Theme",
"reset": "Reset to Preset" "reset": "Reset to Preset"
} }

View File

@@ -22,6 +22,9 @@
"current": "当前主题: {name}", "current": "当前主题: {name}",
"hueValue": "色调: {value}°", "hueValue": "色调: {value}°",
"preview": "预览", "preview": "预览",
"preview.background": "背景",
"preview.surface": "卡片",
"preview.accent": "强调色",
"save": "保存自定义主题", "save": "保存自定义主题",
"reset": "重置为预设" "reset": "重置为预设"
} }

View File

@@ -8,6 +8,9 @@ const gradientPlugin = plugin(function({ addUtilities, addComponents }) {
'.bg-gradient-primary': { '.bg-gradient-primary': {
backgroundImage: 'radial-gradient(circle, hsl(var(--accent)) 0%, transparent 70%)', backgroundImage: 'radial-gradient(circle, hsl(var(--accent)) 0%, transparent 70%)',
}, },
'.bg-gradient-secondary': {
backgroundImage: 'radial-gradient(circle, hsl(var(--secondary)) 0%, transparent 70%)',
},
'.bg-gradient-brand': { '.bg-gradient-brand': {
backgroundImage: 'linear-gradient(to right, hsl(var(--primary)), hsl(var(--secondary)))', backgroundImage: 'linear-gradient(to right, hsl(var(--primary)), hsl(var(--secondary)))',
}, },
@@ -17,6 +20,19 @@ const gradientPlugin = plugin(function({ addUtilities, addComponents }) {
'.bg-gradient-conic': { '.bg-gradient-conic': {
backgroundImage: 'conic-gradient(var(--tw-gradient-stops))', backgroundImage: 'conic-gradient(var(--tw-gradient-stops))',
}, },
// Hover glow effect utilities
'.hover-glow': {
transition: 'box-shadow 0.3s ease-in-out',
'&:hover': {
boxShadow: '0 0 40px 10px hsl(var(--accent) / 0.7)',
},
},
'.hover-glow-primary': {
transition: 'box-shadow 0.3s ease-in-out',
'&:hover': {
boxShadow: '0 0 40px 10px hsl(var(--primary) / 0.5)',
},
},
}); });
// 2. Gradient border component // 2. Gradient border component

View File

@@ -8,9 +8,13 @@
import { afterEach, describe, it, mock } from 'node:test'; import { afterEach, describe, it, mock } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import http from 'node:http'; import http from 'node:http';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const cliCommandPath = new URL('../dist/commands/cli.js', import.meta.url).href; const cliCommandPath = new URL('../dist/commands/cli.js', import.meta.url).href;
const cliExecutorPath = new URL('../dist/tools/cli-executor.js', import.meta.url).href; const cliExecutorPath = new URL('../dist/tools/cli-executor.js', import.meta.url).href;
const historyStorePath = new URL('../dist/tools/cli-history-store.js', import.meta.url).href;
function stubHttpRequest() { function stubHttpRequest() {
mock.method(http, 'request', () => { mock.method(http, 'request', () => {
@@ -35,8 +39,17 @@ describe('ccw cli exec --final', async () => {
it('writes only finalOutput to stdout (no banner/summary)', async () => { it('writes only finalOutput to stdout (no banner/summary)', async () => {
stubHttpRequest(); stubHttpRequest();
const testHome = mkdtempSync(join(tmpdir(), 'ccw-cli-final-only-'));
const prevHome = process.env.CCW_DATA_DIR;
process.env.CCW_DATA_DIR = testHome;
// Ensure the CLI doesn't wait for stdin in Node's test runner environment.
const prevStdinIsTty = process.stdin.isTTY;
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
const cliModule = await import(cliCommandPath); const cliModule = await import(cliCommandPath);
const cliExecutorModule = await import(cliExecutorPath); const cliExecutorModule = await import(cliExecutorPath);
const historyStoreModule = await import(historyStorePath);
const stdoutWrites = []; const stdoutWrites = [];
mock.method(process.stdout, 'write', (chunk) => { mock.method(process.stdout, 'write', (chunk) => {
@@ -72,5 +85,14 @@ describe('ccw cli exec --final', async () => {
await cliModule.cliCommand('exec', [], { prompt: 'Hello', tool: 'gemini', final: true }); await cliModule.cliCommand('exec', [], { prompt: 'Hello', tool: 'gemini', final: true });
assert.equal(stdoutWrites.join(''), 'FINAL'); assert.equal(stdoutWrites.join(''), 'FINAL');
try {
historyStoreModule?.closeAllStores?.();
} catch {
// ignore
}
Object.defineProperty(process.stdin, 'isTTY', { value: prevStdinIsTty, configurable: true });
process.env.CCW_DATA_DIR = prevHome;
rmSync(testHome, { recursive: true, force: true });
}); });
}); });