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 */}
<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 bottom-20 right-20 w-96 h-96 rounded-full blur-3xl animate-pulse opacity-15"
style={{
background: 'radial-gradient(circle, hsl(var(--secondary)) 0%, transparent 70%)',
animationDelay: '1s',
}}
className="absolute bottom-20 right-20 w-96 h-96 rounded-full blur-3xl animate-pulse opacity-15 bg-gradient-secondary"
style={{ 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>
{/* Main Content */}
<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="absolute inset-0 rounded-full blur-2xl opacity-40 animate-pulse"
style={{ background: 'hsl(var(--primary))' }}
/>
<div
className="relative p-6 rounded-full shadow-2xl text-white"
style={{ background: 'hsl(var(--primary))' }}
>
<div className="absolute inset-0 rounded-full blur-2xl opacity-40 animate-pulse bg-gradient-brand" />
<div className="relative p-6 rounded-full shadow-2xl text-white bg-primary hover-glow-primary">
<Rocket className="w-16 h-16" strokeWidth={2} />
</div>
</div>
@@ -84,23 +75,15 @@ export function CoordinatorEmptyState({
{formatMessage({ id: 'coordinator.emptyState.subtitle' })}
</p>
{/* Start Button - Using primary theme color */}
{/* Start Button - Using gradient and glow utilities */}
<Button
size="lg"
onClick={onStart}
disabled={disabled}
className="group relative px-8 py-6 text-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-300"
style={{
background: 'hsl(var(--primary))',
color: 'hsl(var(--primary-foreground))',
}}
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"
>
<Play className="w-6 h-6 mr-2 group-hover:scale-110 transition-transform" />
{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>
{/* Feature Cards */}
@@ -175,38 +158,26 @@ export function CoordinatorEmptyState({
{/* Quick Start Guide */}
<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">
<span
className="w-6 h-6 rounded-full flex items-center justify-center text-white text-xs font-semibold"
style={{ background: 'hsl(var(--primary))' }}
>
<span className="w-6 h-6 rounded-full flex items-center justify-center text-primary-foreground text-xs font-semibold bg-primary">
ok
</span>
{formatMessage({ id: 'coordinator.emptyState.quickStart.title' })}
</h3>
<div className="space-y-3 text-sm text-muted-foreground">
<div className="flex items-start gap-3">
<span
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))' }}
>
<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">
1
</span>
<p>{formatMessage({ id: 'coordinator.emptyState.quickStart.step1' })}</p>
</div>
<div className="flex items-start gap-3">
<span
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))' }}
>
<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">
2
</span>
<p>{formatMessage({ id: 'coordinator.emptyState.quickStart.step2' })}</p>
</div>
<div className="flex items-start gap-3">
<span
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))' }}
>
<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">
3
</span>
<p>{formatMessage({ id: 'coordinator.emptyState.quickStart.step3' })}</p>

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,9 @@ const gradientPlugin = plugin(function({ addUtilities, addComponents }) {
'.bg-gradient-primary': {
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': {
backgroundImage: 'linear-gradient(to right, hsl(var(--primary)), hsl(var(--secondary)))',
},
@@ -17,6 +20,19 @@ const gradientPlugin = plugin(function({ addUtilities, addComponents }) {
'.bg-gradient-conic': {
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

View File

@@ -8,9 +8,13 @@
import { afterEach, describe, it, mock } from 'node:test';
import assert from 'node:assert/strict';
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 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() {
mock.method(http, 'request', () => {
@@ -35,8 +39,17 @@ describe('ccw cli exec --final', async () => {
it('writes only finalOutput to stdout (no banner/summary)', async () => {
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 cliExecutorModule = await import(cliExecutorPath);
const historyStoreModule = await import(historyStorePath);
const stdoutWrites = [];
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 });
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 });
});
});