refact(front): optimize front style (#1007)
This commit is contained in:
@@ -62,7 +62,7 @@ interface ChannelConfigModalProps {
|
||||
onChannelSaved?: (channelType: ChannelType) => void | Promise<void>;
|
||||
}
|
||||
|
||||
const inputClasses = 'h-[44px] rounded-xl font-mono text-meta bg-surface-input border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:border-blue-500 shadow-sm transition-all text-foreground placeholder:text-foreground/40';
|
||||
const inputClasses = 'h-[44px] rounded-xl font-mono text-meta bg-transparent border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:border-blue-500 shadow-sm transition-all text-foreground placeholder:text-foreground/40';
|
||||
const labelClasses = 'text-sm text-foreground/80 font-bold';
|
||||
const outlineButtonClasses = 'h-9 text-meta font-medium rounded-full px-4 border-black/10 dark:border-white/10 bg-transparent hover:bg-black/5 dark:hover:bg-white/5 shadow-none text-foreground/80 hover:text-foreground';
|
||||
const primaryButtonClasses = 'h-9 text-meta font-medium rounded-full px-4 shadow-none';
|
||||
@@ -539,7 +539,7 @@ export function ChannelConfigModal({
|
||||
key={type}
|
||||
onClick={() => setSelectedType(type)}
|
||||
className={cn(
|
||||
'group flex items-start gap-4 p-4 rounded-2xl transition-all text-left border relative overflow-hidden bg-surface-input shadow-sm',
|
||||
'group flex items-start gap-4 p-4 rounded-2xl transition-all text-left border relative overflow-hidden bg-transparent shadow-sm',
|
||||
isConfigured
|
||||
? 'border-green-500/40 bg-green-500/5 dark:bg-green-500/10'
|
||||
: 'border-black/5 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/5'
|
||||
@@ -578,7 +578,7 @@ export function ChannelConfigModal({
|
||||
</div>
|
||||
) : qrCode ? (
|
||||
<div className="text-center space-y-6">
|
||||
<div className="bg-surface-input p-4 rounded-3xl inline-block shadow-sm border border-black/10 dark:border-white/10">
|
||||
<div className="bg-transparent p-4 rounded-3xl inline-block shadow-sm border border-black/10 dark:border-white/10">
|
||||
{qrCode.startsWith('data:image') || qrCode.startsWith('http://') || qrCode.startsWith('https://') ? (
|
||||
<img src={qrCode} alt="Scan QR Code" className="w-64 h-64 object-contain rounded-2xl" />
|
||||
) : (
|
||||
@@ -604,7 +604,7 @@ export function ChannelConfigModal({
|
||||
</div>
|
||||
</div>
|
||||
) : loadingConfig ? (
|
||||
<div className="flex items-center justify-center py-10 rounded-2xl bg-surface-input border border-black/10 dark:border-white/10">
|
||||
<div className="flex items-center justify-center py-10 rounded-2xl bg-transparent border border-black/10 dark:border-white/10">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-sm text-muted-foreground">{t('dialog.loadingConfig')}</span>
|
||||
</div>
|
||||
@@ -617,7 +617,7 @@ export function ChannelConfigModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-surface-input p-4 rounded-2xl space-y-4 shadow-sm border border-black/10 dark:border-white/10">
|
||||
<div className="bg-transparent p-4 rounded-2xl space-y-4 shadow-sm border border-black/10 dark:border-white/10">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className={labelClasses}>{t('dialog.howToConnect')}</p>
|
||||
@@ -851,7 +851,7 @@ function ConfigField({ field, value, onChange, showSecret, onToggleSecret }: Con
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={onToggleSecret}
|
||||
className="h-[44px] w-[44px] rounded-xl bg-surface-input border-black/10 dark:border-white/10 text-muted-foreground hover:text-foreground shrink-0 shadow-sm"
|
||||
className="h-[44px] w-[44px] rounded-xl bg-transparent border-black/10 dark:border-white/10 text-muted-foreground hover:text-foreground shrink-0 shadow-sm"
|
||||
>
|
||||
{showSecret ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</Button>
|
||||
|
||||
@@ -138,7 +138,7 @@ function SkillFileSection({ title, description, files, onOpen }: SkillFileSectio
|
||||
type="button"
|
||||
onClick={() => onOpen(file)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-xl border border-black/5 bg-surface-input px-3 py-2 text-left transition-colors',
|
||||
'flex items-center gap-2 rounded-xl border border-black/5 bg-transparent px-3 py-2 text-left transition-colors',
|
||||
'hover:border-primary/40 hover:bg-primary/5',
|
||||
'dark:border-white/5 dark:hover:bg-white/10',
|
||||
)}
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
/**
|
||||
* Main Layout Component
|
||||
* TitleBar at top, then sidebar + content below.
|
||||
* Platform-aware application shell.
|
||||
*/
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { TitleBar } from './TitleBar';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function MainLayout() {
|
||||
const platform = window.electron?.platform;
|
||||
const isMac = platform === 'darwin';
|
||||
|
||||
return (
|
||||
<div data-testid="main-layout" className="flex h-screen flex-col overflow-hidden bg-background">
|
||||
{/* Title bar: drag region on macOS, icon + controls on Windows */}
|
||||
<div
|
||||
data-testid="main-layout"
|
||||
data-platform={platform}
|
||||
className={cn(
|
||||
'flex h-screen overflow-hidden bg-background',
|
||||
isMac ? 'flex-row' : 'flex-col',
|
||||
)}
|
||||
>
|
||||
<TitleBar />
|
||||
|
||||
{/* Below the title bar: sidebar + content */}
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden bg-surface-sidebar">
|
||||
<Sidebar />
|
||||
<main data-testid="main-content" className="min-h-0 flex-1 overflow-auto p-6">
|
||||
<main
|
||||
data-testid="main-content"
|
||||
className="min-h-0 flex-1 overflow-auto rounded-tl-2xl border-l border-t border-border/60 bg-background p-6"
|
||||
>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Navigation sidebar with menu items.
|
||||
* No longer fixed - sits inside the flex layout below the title bar.
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Network,
|
||||
@@ -52,7 +52,7 @@ function NavItem({ to, icon, label, badge, collapsed, onClick, testId }: NavItem
|
||||
data-testid={testId}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium transition-colors',
|
||||
'sidebar-nav-text flex items-center gap-2 rounded-lg px-2.5 py-1.5 transition-colors',
|
||||
'hover:bg-black/5 dark:hover:bg-white/5 text-foreground/80',
|
||||
isActive
|
||||
? 'bg-black/5 dark:bg-white/10 text-foreground'
|
||||
@@ -61,23 +61,21 @@ function NavItem({ to, icon, label, badge, collapsed, onClick, testId }: NavItem
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<div className={cn("flex shrink-0 items-center justify-center", isActive ? "text-foreground" : "text-muted-foreground")}>
|
||||
{icon}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<>
|
||||
<span className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap">{label}</span>
|
||||
{badge && (
|
||||
<Badge variant="secondary" className="ml-auto shrink-0">
|
||||
{badge}
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<div className="flex shrink-0 items-center justify-center text-current [&_svg]:size-4">
|
||||
{icon}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<>
|
||||
<span className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap">{label}</span>
|
||||
{badge && (
|
||||
<Badge variant="secondary" className="ml-auto shrink-0">
|
||||
{badge}
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
@@ -91,9 +89,14 @@ function getAgentIdFromSessionKey(sessionKey: string): string {
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
const isMac = window.electron?.platform === 'darwin';
|
||||
const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
|
||||
const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed);
|
||||
const sidebarWidth = useSettingsStore((state) => state.sidebarWidth);
|
||||
const setSidebarWidth = useSettingsStore((state) => state.setSidebarWidth);
|
||||
const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const stopResizeRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const sessions = useChatStore((s) => s.sessions);
|
||||
const currentSessionKey = useChatStore((s) => s.currentSessionKey);
|
||||
@@ -169,6 +172,45 @@ export function Sidebar() {
|
||||
void fetchAgents();
|
||||
}, [fetchAgents]);
|
||||
|
||||
const stopResizing = useCallback(() => {
|
||||
stopResizeRef.current?.();
|
||||
stopResizeRef.current = null;
|
||||
setIsResizing(false);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
}, []);
|
||||
|
||||
const handleResizePointerDown = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (sidebarCollapsed) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
try {
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
} catch {
|
||||
// Window listeners below keep dragging reliable even if capture is unavailable.
|
||||
}
|
||||
|
||||
const onMove = (moveEvent: PointerEvent) => {
|
||||
setSidebarWidth(moveEvent.clientX);
|
||||
};
|
||||
const onUp = () => stopResizing();
|
||||
|
||||
stopResizeRef.current = () => {
|
||||
window.removeEventListener('pointermove', onMove);
|
||||
window.removeEventListener('pointerup', onUp);
|
||||
};
|
||||
window.addEventListener('pointermove', onMove);
|
||||
window.addEventListener('pointerup', onUp);
|
||||
setIsResizing(true);
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
},
|
||||
[setSidebarWidth, sidebarCollapsed, stopResizing],
|
||||
);
|
||||
|
||||
useEffect(() => stopResizing, [stopResizing]);
|
||||
|
||||
const agentNameById = useMemo(
|
||||
() => Object.fromEntries((agents ?? []).map((agent) => [agent.id, agent.name])),
|
||||
[agents],
|
||||
@@ -200,13 +242,13 @@ export function Sidebar() {
|
||||
const extraNavItems = rendererExtensionRegistry.getExtraNavItems();
|
||||
|
||||
const coreNavItems = [
|
||||
{ to: '/models', icon: <Cpu className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.models'), testId: 'sidebar-nav-models' },
|
||||
{ to: '/agents', icon: <Bot className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.agents'), testId: 'sidebar-nav-agents' },
|
||||
{ to: '/channels', icon: <Network className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.channels'), testId: 'sidebar-nav-channels' },
|
||||
{ to: '/skills', icon: <Puzzle className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.skills'), testId: 'sidebar-nav-skills' },
|
||||
{ to: '/cron', icon: <Clock className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.cronTasks'), testId: 'sidebar-nav-cron' },
|
||||
{ to: '/models', icon: <Cpu className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.models'), testId: 'sidebar-nav-models' },
|
||||
{ to: '/agents', icon: <Bot className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.agents'), testId: 'sidebar-nav-agents' },
|
||||
{ to: '/channels', icon: <Network className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.channels'), testId: 'sidebar-nav-channels' },
|
||||
{ to: '/skills', icon: <Puzzle className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.skills'), testId: 'sidebar-nav-skills' },
|
||||
{ to: '/cron', icon: <Clock className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.cronTasks'), testId: 'sidebar-nav-cron' },
|
||||
...(devModeUnlocked
|
||||
? [{ to: '/dreams', icon: <Moon className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('common:sidebar.openClawDreams'), testId: 'sidebar-nav-dreams' }]
|
||||
? [{ to: '/dreams', icon: <Moon className="h-4 w-4" strokeWidth={2} />, label: t('common:sidebar.openClawDreams'), testId: 'sidebar-nav-dreams' }]
|
||||
: []),
|
||||
];
|
||||
|
||||
@@ -214,7 +256,7 @@ export function Sidebar() {
|
||||
...coreNavItems.filter((item) => !hiddenRoutes.has(item.to)),
|
||||
...extraNavItems.map((item) => ({
|
||||
to: item.to,
|
||||
icon: <item.icon className="h-[18px] w-[18px]" strokeWidth={2} />,
|
||||
icon: <item.icon className="h-4 w-4" strokeWidth={2} />,
|
||||
label: item.labelI18nKey ? t(item.labelI18nKey) : item.label,
|
||||
testId: item.testId,
|
||||
})),
|
||||
@@ -224,12 +266,19 @@ export function Sidebar() {
|
||||
<aside
|
||||
data-testid="sidebar"
|
||||
className={cn(
|
||||
'flex min-h-0 shrink-0 flex-col overflow-hidden border-r bg-surface-sidebar/60 transition-all duration-300',
|
||||
sidebarCollapsed ? 'w-16' : 'w-64'
|
||||
'relative flex min-h-0 shrink-0 flex-col overflow-hidden bg-surface-sidebar',
|
||||
isResizing ? 'transition-none' : 'transition-[width] duration-300',
|
||||
)}
|
||||
style={{ width: sidebarCollapsed ? 64 : sidebarWidth }}
|
||||
>
|
||||
{/* Top Header Toggle */}
|
||||
<div className={cn("flex items-center p-2 h-12", sidebarCollapsed ? "justify-center" : "justify-between")}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center p-2 h-12',
|
||||
isMac && 'drag-region h-[4.75rem] items-end pt-10',
|
||||
sidebarCollapsed ? 'justify-center' : 'justify-between',
|
||||
)}
|
||||
>
|
||||
{!sidebarCollapsed && (
|
||||
<div className="flex items-center gap-2 px-2 overflow-hidden">
|
||||
<img src={logoSvg} alt="ClawX" className="h-5 w-auto shrink-0" />
|
||||
@@ -241,7 +290,7 @@ export function Sidebar() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0 text-muted-foreground hover:bg-black/5 dark:hover:bg-white/10"
|
||||
className="no-drag h-8 w-8 shrink-0 text-muted-foreground hover:bg-black/5 dark:hover:bg-white/10"
|
||||
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||
>
|
||||
{sidebarCollapsed ? (
|
||||
@@ -253,7 +302,7 @@ export function Sidebar() {
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex flex-col px-2 gap-0.5">
|
||||
<nav className="flex flex-col gap-0 px-2">
|
||||
<button
|
||||
data-testid="sidebar-new-chat"
|
||||
onClick={() => {
|
||||
@@ -262,13 +311,13 @@ export function Sidebar() {
|
||||
navigate('/');
|
||||
}}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium transition-colors mb-2',
|
||||
'bg-black/5 dark:bg-accent shadow-none border border-transparent text-foreground',
|
||||
'sidebar-nav-text mb-1 flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 transition-colors',
|
||||
'border border-transparent text-foreground/80 shadow-none hover:bg-black/5 hover:text-foreground dark:hover:bg-white/5',
|
||||
sidebarCollapsed && 'justify-center px-0',
|
||||
)}
|
||||
>
|
||||
<div className="flex shrink-0 items-center justify-center text-foreground/80">
|
||||
<Plus className="h-[18px] w-[18px]" strokeWidth={2} />
|
||||
<div className="flex shrink-0 items-center justify-center text-current [&_svg]:size-4">
|
||||
<Plus className="h-4 w-4" strokeWidth={2} />
|
||||
</div>
|
||||
{!sidebarCollapsed && <span className="flex-1 text-left overflow-hidden text-ellipsis whitespace-nowrap">{t('sidebar.newChat')}</span>}
|
||||
</button>
|
||||
@@ -353,21 +402,19 @@ export function Sidebar() {
|
||||
data-testid="sidebar-nav-settings"
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium transition-colors',
|
||||
'sidebar-nav-text flex items-center gap-2 rounded-lg px-2.5 py-1.5 transition-colors',
|
||||
'hover:bg-black/5 dark:hover:bg-white/5 text-foreground/80',
|
||||
isActive && 'bg-black/5 dark:bg-white/10 text-foreground',
|
||||
sidebarCollapsed ? 'justify-center px-0' : ''
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<div className={cn("flex shrink-0 items-center justify-center", isActive ? "text-foreground" : "text-muted-foreground")}>
|
||||
<SettingsIcon className="h-[18px] w-[18px]" strokeWidth={2} />
|
||||
</div>
|
||||
{!sidebarCollapsed && <span className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap">{t('sidebar.settings')}</span>}
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<div className="flex shrink-0 items-center justify-center text-current [&_svg]:size-4">
|
||||
<SettingsIcon className="h-4 w-4" strokeWidth={2} />
|
||||
</div>
|
||||
{!sidebarCollapsed && <span className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap">{t('sidebar.settings')}</span>}
|
||||
</>
|
||||
</NavLink>
|
||||
|
||||
{devModeUnlocked && (
|
||||
@@ -375,25 +422,44 @@ export function Sidebar() {
|
||||
data-testid="sidebar-open-dev-console"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'flex items-center gap-2.5 rounded-lg px-2.5 py-2 h-auto text-sm font-medium transition-colors w-full mt-1',
|
||||
'sidebar-nav-text mt-0.5 flex h-auto w-full items-center gap-2 rounded-lg px-2.5 py-1.5 transition-colors',
|
||||
'hover:bg-black/5 dark:hover:bg-white/5 text-foreground/80',
|
||||
sidebarCollapsed ? 'justify-center px-0' : 'justify-start'
|
||||
)}
|
||||
onClick={openDevConsole}
|
||||
>
|
||||
<div className="flex shrink-0 items-center justify-center text-muted-foreground">
|
||||
<Terminal className="h-[18px] w-[18px]" strokeWidth={2} />
|
||||
<div className="flex shrink-0 items-center justify-center text-current [&_svg]:size-4">
|
||||
<Terminal className="h-4 w-4" strokeWidth={2} />
|
||||
</div>
|
||||
{!sidebarCollapsed && (
|
||||
<>
|
||||
<span className="flex-1 text-left overflow-hidden text-ellipsis whitespace-nowrap">{t('common:sidebar.openClawPage')}</span>
|
||||
<ExternalLink className="h-3 w-3 shrink-0 ml-auto opacity-50 text-muted-foreground" />
|
||||
<ExternalLink className="ml-auto h-3 w-3 shrink-0 opacity-50 text-current" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!sidebarCollapsed && (
|
||||
<div
|
||||
data-testid="sidebar-resize-handle"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-valuemin={220}
|
||||
aria-valuemax={420}
|
||||
aria-valuenow={sidebarWidth}
|
||||
title="Drag to resize sidebar"
|
||||
onPointerDown={handleResizePointerDown}
|
||||
className="no-drag group absolute inset-y-0 right-0 z-20 w-2 translate-x-1/2 cursor-col-resize select-none"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-transparent transition-colors group-hover:bg-primary/40"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!sessionToDelete}
|
||||
title={t('common:actions.confirm')}
|
||||
|
||||
@@ -12,8 +12,8 @@ export function TitleBar() {
|
||||
const platform = window.electron?.platform;
|
||||
|
||||
if (platform === 'darwin') {
|
||||
// macOS: just a drag region, traffic lights are native
|
||||
return <div className="drag-region h-10 shrink-0 border-b bg-background" />;
|
||||
// macOS traffic lights live inside the sidebar area; keep the shell left/right.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Linux keeps the native frame/title bar for better IME compatibility.
|
||||
@@ -51,7 +51,10 @@ function WindowsTitleBar() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="drag-region flex h-10 shrink-0 items-center justify-end border-b bg-background">
|
||||
<div
|
||||
data-testid="windows-titlebar"
|
||||
className="drag-region flex h-10 shrink-0 items-center justify-end bg-background"
|
||||
>
|
||||
|
||||
{/* Right: Window Controls */}
|
||||
<div className="no-drag flex h-full">
|
||||
|
||||
@@ -55,7 +55,7 @@ import { useSettingsStore } from '@/stores/settings';
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import { subscribeHostEvent } from '@/lib/host-events';
|
||||
|
||||
const inputClasses = 'h-[44px] rounded-xl font-mono text-meta bg-surface-input border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:border-blue-500 shadow-sm transition-all text-foreground placeholder:text-foreground/40';
|
||||
const inputClasses = 'h-[44px] rounded-xl font-mono text-meta bg-transparent border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:border-blue-500 shadow-sm transition-all text-foreground placeholder:text-foreground/40';
|
||||
const labelClasses = 'text-sm text-foreground/80 font-bold';
|
||||
type ArkMode = 'apikey' | 'codeplan';
|
||||
|
||||
@@ -790,7 +790,7 @@ function ProviderCard({
|
||||
placeholder={t('aiProviders.dialog.fallbackModelIdsPlaceholder')}
|
||||
className={isDefault
|
||||
? "min-h-24 w-full rounded-xl border border-black/10 dark:border-white/10 bg-white dark:bg-card px-3 py-2 text-meta font-mono outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 shadow-sm"
|
||||
: "min-h-24 w-full rounded-xl border border-black/10 dark:border-white/10 bg-surface-input px-3 py-2 text-meta font-mono outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:border-blue-500 shadow-sm transition-all text-foreground placeholder:text-foreground/40"}
|
||||
: "min-h-24 w-full rounded-xl border border-black/10 dark:border-white/10 bg-transparent px-3 py-2 text-meta font-mono outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:border-blue-500 shadow-sm transition-all text-foreground placeholder:text-foreground/40"}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('aiProviders.dialog.fallbackModelIdsHelp')}
|
||||
@@ -801,7 +801,7 @@ function ProviderCard({
|
||||
{fallbackOptions.length === 0 ? (
|
||||
<p className="text-meta text-muted-foreground">{t('aiProviders.dialog.noFallbackOptions')}</p>
|
||||
) : (
|
||||
<div className={cn("space-y-2 rounded-xl border border-black/10 dark:border-white/10 p-3 shadow-sm", isDefault ? "bg-white dark:bg-card" : "bg-surface-input")}>
|
||||
<div className={cn("space-y-2 rounded-xl border border-black/10 dark:border-white/10 p-3 shadow-sm", isDefault ? "bg-white dark:bg-card" : "bg-transparent")}>
|
||||
{fallbackOptions.map((candidate) => (
|
||||
<label key={candidate.account.id} className="flex items-center gap-3 text-meta cursor-pointer group/label">
|
||||
<input
|
||||
@@ -883,7 +883,7 @@ function ProviderCard({
|
||||
"rounded-xl px-4 border-black/10 dark:border-white/10",
|
||||
isDefault
|
||||
? "h-[40px] bg-white dark:bg-card hover:bg-black/5 dark:hover:bg-white/10"
|
||||
: "h-[44px] bg-surface-input hover:bg-black/5 dark:hover:bg-white/10 shadow-sm"
|
||||
: "h-[44px] bg-transparent hover:bg-black/5 dark:hover:bg-white/10 shadow-sm"
|
||||
)}
|
||||
disabled={
|
||||
validating
|
||||
@@ -913,7 +913,7 @@ function ProviderCard({
|
||||
"p-0 rounded-xl",
|
||||
isDefault
|
||||
? "h-[40px] w-[40px] hover:bg-black/5 dark:hover:bg-white/10"
|
||||
: "h-[44px] w-[44px] bg-surface-input border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/10 shadow-sm text-muted-foreground hover:text-foreground"
|
||||
: "h-[44px] w-[44px] bg-transparent border border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/10 shadow-sm text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -1314,7 +1314,7 @@ function AddProviderDialog({
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3 p-4 rounded-2xl bg-white dark:bg-card border border-black/5 dark:border-white/5 shadow-sm">
|
||||
<div className="flex items-center gap-3 p-4 rounded-2xl bg-transparent border border-black/5 dark:border-white/5 shadow-sm">
|
||||
<div className="h-10 w-10 shrink-0 flex items-center justify-center bg-black/5 dark:bg-white/5 rounded-xl">
|
||||
{getProviderIconUrl(selectedType!) ? (
|
||||
<img src={getProviderIconUrl(selectedType!)} alt={typeInfo?.name} className={cn('h-6 w-6', shouldInvertInDark(selectedType!) && 'dark:invert')} />
|
||||
@@ -1370,7 +1370,7 @@ function AddProviderDialog({
|
||||
|
||||
{/* Auth mode toggle for providers supporting both */}
|
||||
{isOAuth && supportsApiKey && (
|
||||
<div className="flex rounded-xl border border-black/10 dark:border-white/10 overflow-hidden text-meta font-medium shadow-sm bg-surface-input p-1 gap-1">
|
||||
<div className="flex rounded-xl border border-black/10 dark:border-white/10 overflow-hidden text-meta font-medium shadow-sm bg-transparent p-1 gap-1">
|
||||
<button
|
||||
onClick={() => setAuthMode('oauth')}
|
||||
className={cn(
|
||||
@@ -1662,7 +1662,7 @@ function AddProviderDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-3 p-4 bg-surface-input border border-black/5 dark:border-white/5 rounded-xl shadow-inner">
|
||||
<div className="flex items-center justify-center gap-3 p-4 bg-transparent border border-black/5 dark:border-white/5 rounded-xl shadow-inner">
|
||||
<code className="text-3xl font-mono tracking-[0.2em] font-bold text-foreground">
|
||||
{oauthData.userCode}
|
||||
</code>
|
||||
|
||||
@@ -332,8 +332,8 @@ function AgentCard({
|
||||
);
|
||||
}
|
||||
|
||||
const inputClasses = 'h-[44px] rounded-xl font-mono text-meta bg-surface-input border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:border-blue-500 shadow-sm transition-all text-foreground placeholder:text-foreground/40';
|
||||
const selectClasses = 'h-[44px] w-full rounded-xl font-mono text-meta bg-surface-input border border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:border-blue-500 shadow-sm transition-all text-foreground px-3';
|
||||
const inputClasses = 'h-[44px] rounded-xl font-mono text-meta bg-transparent border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:border-blue-500 shadow-sm transition-all text-foreground placeholder:text-foreground/40';
|
||||
const selectClasses = 'h-[44px] w-full rounded-xl font-mono text-meta bg-transparent border border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-blue-500/50 focus-visible:border-blue-500 shadow-sm transition-all text-foreground px-3';
|
||||
const labelClasses = 'text-sm text-foreground/80 font-bold';
|
||||
|
||||
function ChannelLogo({ type }: { type: ChannelType }) {
|
||||
@@ -541,7 +541,7 @@ function AgentSettingsModal({
|
||||
variant="outline"
|
||||
onClick={() => void handleSaveName()}
|
||||
disabled={savingName || !name.trim() || name.trim() === agent.name}
|
||||
className="h-[44px] text-meta font-medium rounded-xl px-4 border-black/10 dark:border-white/10 bg-surface-input hover:bg-black/5 dark:hover:bg-white/5 shadow-none text-foreground/80 hover:text-foreground"
|
||||
className="h-[44px] text-meta font-medium rounded-xl px-4 border-black/10 dark:border-white/10 bg-transparent hover:bg-black/5 dark:hover:bg-white/5 shadow-none text-foreground/80 hover:text-foreground"
|
||||
>
|
||||
{savingName ? (
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
|
||||
@@ -671,11 +671,21 @@ export function Chat() {
|
||||
}
|
||||
}, [userRunCards, messages, currentSessionKey]);
|
||||
|
||||
const platform = window.electron?.platform;
|
||||
const isMac = platform === 'darwin';
|
||||
const isWindows = platform === 'win32';
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={splitContainerRef}
|
||||
className={cn('relative flex min-h-0 -m-6 transition-colors duration-500 dark:bg-background')}
|
||||
style={{ height: 'calc(100vh - 2.5rem)' }}
|
||||
data-testid="chat-page"
|
||||
className={cn(
|
||||
'relative flex min-h-0 -m-6 overflow-hidden transition-colors duration-500',
|
||||
'bg-background',
|
||||
isMac && 'rounded-tl-2xl shadow-[inset_1px_1px_0_hsl(var(--border)/0.55)]',
|
||||
isWindows && 'rounded-tl-2xl',
|
||||
)}
|
||||
style={{ height: isMac ? '100vh' : 'calc(100vh - 2.5rem)' }}
|
||||
>
|
||||
{/* Left column: chat */}
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
|
||||
@@ -442,7 +442,7 @@ function TaskDialog({ job, configuredChannels, onClose, onSave }: TaskDialogProp
|
||||
placeholder={t('dialog.taskNamePlaceholder')}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="h-[44px] rounded-xl font-mono text-meta bg-surface-input border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:border-primary shadow-sm transition-all text-foreground placeholder:text-foreground/40"
|
||||
className="h-[44px] rounded-xl font-mono text-meta bg-transparent border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:border-primary shadow-sm transition-all text-foreground placeholder:text-foreground/40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -455,7 +455,7 @@ function TaskDialog({ job, configuredChannels, onClose, onSave }: TaskDialogProp
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
rows={3}
|
||||
className="rounded-xl font-mono text-meta bg-surface-input border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:border-primary shadow-sm transition-all text-foreground placeholder:text-foreground/40 resize-none"
|
||||
className="rounded-xl font-mono text-meta bg-transparent border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:border-primary shadow-sm transition-all text-foreground placeholder:text-foreground/40 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -468,7 +468,7 @@ function TaskDialog({ job, configuredChannels, onClose, onSave }: TaskDialogProp
|
||||
onChange={(e) => {
|
||||
setSelectedAgentId(e.target.value);
|
||||
}}
|
||||
className="h-[44px] rounded-xl border-black/10 dark:border-white/10 bg-surface-input text-meta"
|
||||
className="h-[44px] rounded-xl border-black/10 dark:border-white/10 bg-transparent text-meta"
|
||||
>
|
||||
{agents.map((agent) => (
|
||||
<option key={agent.id} value={agent.id}>
|
||||
@@ -494,7 +494,7 @@ function TaskDialog({ job, configuredChannels, onClose, onSave }: TaskDialogProp
|
||||
"justify-start h-10 rounded-xl font-medium text-meta transition-all",
|
||||
schedule === preset.value
|
||||
? "bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm border-transparent"
|
||||
: "bg-surface-input border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/5 text-foreground/80 hover:text-foreground"
|
||||
: "bg-transparent border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/5 text-foreground/80 hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<Timer className="h-4 w-4 mr-2 opacity-70" />
|
||||
@@ -507,7 +507,7 @@ function TaskDialog({ job, configuredChannels, onClose, onSave }: TaskDialogProp
|
||||
placeholder={t('dialog.cronPlaceholder')}
|
||||
value={customSchedule}
|
||||
onChange={(e) => setCustomSchedule(e.target.value)}
|
||||
className="h-[44px] rounded-xl font-mono text-meta bg-surface-input border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:border-primary shadow-sm transition-all text-foreground placeholder:text-foreground/40"
|
||||
className="h-[44px] rounded-xl font-mono text-meta bg-transparent border-black/10 dark:border-white/10 focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:border-primary shadow-sm transition-all text-foreground placeholder:text-foreground/40"
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
@@ -543,7 +543,7 @@ function TaskDialog({ job, configuredChannels, onClose, onSave }: TaskDialogProp
|
||||
'justify-start h-auto min-h-12 rounded-xl px-4 py-3 text-left whitespace-normal',
|
||||
deliveryMode === 'none'
|
||||
? 'bg-primary hover:bg-primary/90 text-primary-foreground border-transparent'
|
||||
: 'bg-surface-input border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/5 text-foreground/80 hover:text-foreground',
|
||||
: 'bg-transparent border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/5 text-foreground/80 hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
@@ -560,7 +560,7 @@ function TaskDialog({ job, configuredChannels, onClose, onSave }: TaskDialogProp
|
||||
'justify-start h-auto min-h-12 rounded-xl px-4 py-3 text-left whitespace-normal',
|
||||
deliveryMode === 'announce'
|
||||
? 'bg-primary hover:bg-primary/90 text-primary-foreground border-transparent'
|
||||
: 'bg-surface-input border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/5 text-foreground/80 hover:text-foreground',
|
||||
: 'bg-transparent border-black/10 dark:border-white/10 hover:bg-black/5 dark:hover:bg-white/5 text-foreground/80 hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
@@ -571,7 +571,7 @@ function TaskDialog({ job, configuredChannels, onClose, onSave }: TaskDialogProp
|
||||
</div>
|
||||
|
||||
{deliveryMode === 'announce' && (
|
||||
<div className="space-y-3 rounded-2xl border border-black/5 dark:border-white/5 bg-surface-input p-4 shadow-sm">
|
||||
<div className="space-y-3 rounded-2xl border border-black/5 dark:border-white/5 bg-transparent p-4 shadow-sm">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="delivery-channel" className="text-meta text-foreground/80 font-bold">
|
||||
{t('dialog.deliveryChannel')}
|
||||
@@ -662,7 +662,7 @@ function TaskDialog({ job, configuredChannels, onClose, onSave }: TaskDialogProp
|
||||
</div>
|
||||
|
||||
{/* Enabled */}
|
||||
<div className="flex items-center justify-between bg-surface-input p-4 rounded-2xl shadow-sm border border-black/5 dark:border-white/5">
|
||||
<div className="flex items-center justify-between bg-transparent p-4 rounded-2xl shadow-sm border border-black/5 dark:border-white/5">
|
||||
<div>
|
||||
<Label className="text-sm text-foreground/80 font-bold">{t('dialog.enableImmediately')}</Label>
|
||||
<p className="text-meta text-muted-foreground mt-0.5">
|
||||
|
||||
@@ -129,12 +129,19 @@ function buildDreamingEnabledPatchRaw(enabled: boolean): string {
|
||||
},
|
||||
});
|
||||
}
|
||||
const PANEL_CLASS = 'border-black/10 bg-surface-modal shadow-sm dark:border-white/10';
|
||||
const INSET_CLASS = 'border-black/10 bg-surface-input dark:border-white/10';
|
||||
const QUIET_BUTTON_CLASS = 'border-black/10 bg-surface-input text-foreground/80 shadow-none hover:bg-black/5 hover:text-foreground dark:border-white/10 dark:hover:bg-white/5';
|
||||
const PANEL_CLASS = 'rounded-2xl border-black/10 bg-surface-modal shadow-sm dark:border-white/10';
|
||||
const INSET_CLASS = 'rounded-xl border-black/10 bg-transparent dark:border-white/10';
|
||||
const QUIET_BUTTON_CLASS = 'border-black/10 bg-transparent text-foreground/80 shadow-none hover:bg-black/5 hover:text-foreground dark:border-white/10 dark:hover:bg-white/5';
|
||||
const STATUS_BADGE_CLASS = 'border-black/10 bg-black/5 text-foreground/80 dark:border-white/10 dark:bg-white/10 dark:text-foreground/80';
|
||||
const SUCCESS_NOTICE_CLASS = 'border-black/10 bg-black/5 text-foreground/80 dark:border-white/10 dark:bg-white/10';
|
||||
|
||||
// Header pill buttons — mirrors the Agents/Cron page header style so
|
||||
// the top-right action cluster looks consistent across pages. Outline
|
||||
// is used for secondary actions (refresh, disable, open-full-UI); the
|
||||
// primary is the brand-filled pill (enable / new task / add agent).
|
||||
const HEADER_PILL_OUTLINE_CLASS = 'h-9 rounded-full px-4 text-meta font-medium border-black/10 dark:border-white/10 bg-transparent hover:bg-black/5 dark:hover:bg-white/5 shadow-none text-foreground/80 hover:text-foreground transition-colors';
|
||||
const HEADER_PILL_PRIMARY_CLASS = 'h-9 rounded-full px-4 text-meta font-medium shadow-none';
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value != null && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
@@ -411,38 +418,35 @@ export function Dreams() {
|
||||
</div>
|
||||
<p className="mt-2 text-subtitle font-medium text-foreground/60">{t('subtitle')}</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<div className="flex shrink-0 items-center gap-3">
|
||||
<Button
|
||||
data-testid={dreaming?.enabled ? 'dreams-disable' : 'dreams-enable'}
|
||||
variant={dreaming?.enabled ? 'outline' : 'default'}
|
||||
size="sm"
|
||||
onClick={() => void setDreamingEnabled(!dreaming?.enabled)}
|
||||
disabled={!dreamsReady || busy || loading}
|
||||
className={dreaming?.enabled ? QUIET_BUTTON_CLASS : undefined}
|
||||
className={dreaming?.enabled ? HEADER_PILL_OUTLINE_CLASS : HEADER_PILL_PRIMARY_CLASS}
|
||||
>
|
||||
{runningToggle ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Power className="mr-2 h-4 w-4" />}
|
||||
{runningToggle ? <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" /> : <Power className="mr-2 h-3.5 w-3.5" />}
|
||||
{dreaming?.enabled ? t('actions.disable') : t('actions.enable')}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="dreams-refresh"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void refreshAll({ force: true })}
|
||||
disabled={!dreamsReady}
|
||||
className={QUIET_BUTTON_CLASS}
|
||||
className={HEADER_PILL_OUTLINE_CLASS}
|
||||
>
|
||||
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCw className="mr-2 h-4 w-4" />}
|
||||
{loading ? <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="mr-2 h-3.5 w-3.5" />}
|
||||
{t('common:actions.refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="dreams-open-full-ui"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => void openFullDreams()}
|
||||
disabled={openingFullUi || !gatewayRunning}
|
||||
className="border border-black/10 bg-card text-foreground shadow-sm hover:bg-black/5 dark:border-white/10 dark:bg-card dark:hover:bg-white/5"
|
||||
className={HEADER_PILL_OUTLINE_CLASS}
|
||||
>
|
||||
{openingFullUi ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <ExternalLink className="mr-2 h-4 w-4" />}
|
||||
{openingFullUi ? <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" /> : <ExternalLink className="mr-2 h-3.5 w-3.5" />}
|
||||
{t('openFullUi')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -450,19 +454,19 @@ export function Dreams() {
|
||||
|
||||
<main className="min-h-0 flex-1 overflow-auto px-10 pb-10">
|
||||
{!dreamsReady && (
|
||||
<div className="mb-4 rounded-lg border border-black/10 bg-surface-input px-4 py-3 text-sm text-foreground/70 dark:border-white/10">
|
||||
<div className="mb-4 rounded-xl border border-black/10 bg-transparent px-4 py-3 text-sm text-foreground/70 dark:border-white/10">
|
||||
{t('gatewayNotReady')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div data-testid="dreams-error" className="mb-4 rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||
<div data-testid="dreams-error" className="mb-4 rounded-xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lastActionMessage && (
|
||||
<div data-testid="dreams-action-message" className={cn('mb-4 rounded-lg border px-4 py-3 text-sm', SUCCESS_NOTICE_CLASS)}>
|
||||
<div data-testid="dreams-action-message" className={cn('mb-4 rounded-xl border px-4 py-3 text-sm', SUCCESS_NOTICE_CLASS)}>
|
||||
{lastActionMessage}
|
||||
</div>
|
||||
)}
|
||||
@@ -473,7 +477,7 @@ export function Dreams() {
|
||||
return (
|
||||
<Card key={metric.label} className={PANEL_CLASS}>
|
||||
<CardContent className="flex items-center gap-3 p-4">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-surface-input">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-transparent">
|
||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
@@ -499,12 +503,12 @@ export function Dreams() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 p-4 pt-0">
|
||||
{diaryEntries.length === 0 ? (
|
||||
<div data-testid="dreams-empty-diary" className="rounded-md border border-dashed p-6 text-center text-sm text-muted-foreground">
|
||||
<div data-testid="dreams-empty-diary" className="rounded-xl border border-dashed p-6 text-center text-sm text-muted-foreground">
|
||||
{t('diary.empty')}
|
||||
</div>
|
||||
) : (
|
||||
diaryEntries.map((entry) => (
|
||||
<article key={entry.id} className={cn('rounded-md border p-3', INSET_CLASS)}>
|
||||
<article key={entry.id} className={cn('border p-3', INSET_CLASS)}>
|
||||
<div className="mb-1 flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>{entry.date || t('diary.undated')}</span>
|
||||
@@ -586,7 +590,7 @@ export function Dreams() {
|
||||
{phases.map((phase) => {
|
||||
const value = dreaming?.phases?.[phase.key];
|
||||
return (
|
||||
<div key={phase.key} className={cn('flex items-center justify-between gap-3 rounded-md border px-3 py-2', INSET_CLASS)}>
|
||||
<div key={phase.key} className={cn('flex items-center justify-between gap-3 border px-3 py-2', INSET_CLASS)}>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium">{phase.label}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">{value?.cron || t('phases.noSchedule')}</div>
|
||||
@@ -613,17 +617,17 @@ export function Dreams() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 p-4 pt-0">
|
||||
{(dreaming?.storeError || dreaming?.phaseSignalError) && (
|
||||
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
<div className="rounded-xl border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
{dreaming.storeError || dreaming.phaseSignalError}
|
||||
</div>
|
||||
)}
|
||||
{recentSignals.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed p-6 text-center text-sm text-muted-foreground">
|
||||
<div className="rounded-xl border border-dashed p-6 text-center text-sm text-muted-foreground">
|
||||
{t('signals.empty')}
|
||||
</div>
|
||||
) : (
|
||||
recentSignals.map((entry, index) => (
|
||||
<div key={`${entry.key || entry.path || 'signal'}-${index}`} className={cn('rounded-md border p-3', INSET_CLASS)}>
|
||||
<div key={`${entry.key || entry.path || 'signal'}-${index}`} className={cn('border p-3', INSET_CLASS)}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="min-w-0 truncate text-xs text-muted-foreground">
|
||||
{entry.path || entry.key || t('signals.unknownSource')}
|
||||
|
||||
@@ -429,13 +429,13 @@ export function Models() {
|
||||
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1.5 text-meta font-medium text-muted-foreground">
|
||||
{entry.usageStatus === 'available' || entry.usageStatus === undefined ? (
|
||||
<>
|
||||
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-sky-500"></div>{t('dashboard:recentTokenHistory.input', { value: formatTokenCount(entry.inputTokens) })}</span>
|
||||
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-violet-500"></div>{t('dashboard:recentTokenHistory.output', { value: formatTokenCount(entry.outputTokens) })}</span>
|
||||
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-usage-input"></div>{t('dashboard:recentTokenHistory.input', { value: formatTokenCount(entry.inputTokens) })}</span>
|
||||
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-usage-output"></div>{t('dashboard:recentTokenHistory.output', { value: formatTokenCount(entry.outputTokens) })}</span>
|
||||
{entry.cacheReadTokens > 0 && (
|
||||
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-amber-500"></div>{t('dashboard:recentTokenHistory.cacheRead', { value: formatTokenCount(entry.cacheReadTokens) })}</span>
|
||||
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-usage-cache"></div>{t('dashboard:recentTokenHistory.cacheRead', { value: formatTokenCount(entry.cacheReadTokens) })}</span>
|
||||
)}
|
||||
{entry.cacheWriteTokens > 0 && (
|
||||
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-amber-500"></div>{t('dashboard:recentTokenHistory.cacheWrite', { value: formatTokenCount(entry.cacheWriteTokens) })}</span>
|
||||
<span className="flex items-center gap-1.5"><div className="w-2 h-2 rounded-full bg-usage-cache"></div>{t('dashboard:recentTokenHistory.cacheWrite', { value: formatTokenCount(entry.cacheWriteTokens) })}</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
@@ -572,15 +572,15 @@ function UsageBarChart({
|
||||
<div className="space-y-4 bg-transparent p-5 rounded-2xl border border-black/10 dark:border-white/10">
|
||||
<div className="flex flex-wrap gap-4 text-meta font-medium text-muted-foreground mb-2">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-sky-500" />
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-usage-input" />
|
||||
{inputLabel}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-violet-500" />
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-usage-output" />
|
||||
{outputLabel}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-amber-500" />
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-usage-cache" />
|
||||
{cacheLabel}
|
||||
</span>
|
||||
</div>
|
||||
@@ -603,19 +603,19 @@ function UsageBarChart({
|
||||
>
|
||||
{group.inputTokens > 0 && (
|
||||
<div
|
||||
className="h-full bg-sky-500"
|
||||
className="h-full bg-usage-input"
|
||||
style={{ width: `${(group.inputTokens / group.totalTokens) * 100}%` }}
|
||||
/>
|
||||
)}
|
||||
{group.outputTokens > 0 && (
|
||||
<div
|
||||
className="h-full bg-violet-500"
|
||||
className="h-full bg-usage-output"
|
||||
style={{ width: `${(group.outputTokens / group.totalTokens) * 100}%` }}
|
||||
/>
|
||||
)}
|
||||
{group.cacheTokens > 0 && (
|
||||
<div
|
||||
className="h-full bg-amber-500"
|
||||
className="h-full bg-usage-cache"
|
||||
style={{ width: `${(group.cacheTokens / group.totalTokens) * 100}%` }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -177,7 +177,7 @@ function SkillDetailDialog({ skill, isOpen, onClose, onToggle, onUninstall, onOp
|
||||
<Input
|
||||
value={skill.baseDir || t('detail.pathUnavailable')}
|
||||
readOnly
|
||||
className="h-[38px] font-mono text-xs bg-surface-input border-black/10 dark:border-white/10 rounded-xl text-foreground/70"
|
||||
className="h-[38px] font-mono text-xs bg-transparent border-black/10 dark:border-white/10 rounded-xl text-foreground/70"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -36,6 +36,7 @@ interface SettingsState {
|
||||
|
||||
// UI State
|
||||
sidebarCollapsed: boolean;
|
||||
sidebarWidth: number;
|
||||
devModeUnlocked: boolean;
|
||||
|
||||
// Setup
|
||||
@@ -60,6 +61,7 @@ interface SettingsState {
|
||||
setAutoCheckUpdate: (value: boolean) => void;
|
||||
setAutoDownloadUpdate: (value: boolean) => void;
|
||||
setSidebarCollapsed: (value: boolean) => void;
|
||||
setSidebarWidth: (value: number) => void;
|
||||
setDevModeUnlocked: (value: boolean) => void;
|
||||
markSetupComplete: () => void;
|
||||
resetSettings: () => void;
|
||||
@@ -83,10 +85,13 @@ const defaultSettings = {
|
||||
autoCheckUpdate: true,
|
||||
autoDownloadUpdate: false,
|
||||
sidebarCollapsed: false,
|
||||
sidebarWidth: 280,
|
||||
devModeUnlocked: false,
|
||||
setupComplete: false,
|
||||
};
|
||||
|
||||
const clampSidebarWidth = (value: number) => Math.min(420, Math.max(220, Math.round(value)));
|
||||
|
||||
export const useSettingsStore = create<SettingsState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
@@ -102,6 +107,9 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
...state,
|
||||
...settings,
|
||||
...(resolvedLanguage ? { language: resolvedLanguage } : {}),
|
||||
...(typeof settings.sidebarWidth === 'number'
|
||||
? { sidebarWidth: clampSidebarWidth(settings.sidebarWidth) }
|
||||
: {}),
|
||||
}));
|
||||
if (resolvedLanguage) {
|
||||
i18n.changeLanguage(resolvedLanguage);
|
||||
@@ -167,6 +175,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
setAutoCheckUpdate: (autoCheckUpdate) => set({ autoCheckUpdate }),
|
||||
setAutoDownloadUpdate: (autoDownloadUpdate) => set({ autoDownloadUpdate }),
|
||||
setSidebarCollapsed: (sidebarCollapsed) => set({ sidebarCollapsed }),
|
||||
setSidebarWidth: (sidebarWidth) => set({ sidebarWidth: clampSidebarWidth(sidebarWidth) }),
|
||||
setDevModeUnlocked: (devModeUnlocked) => {
|
||||
set({ devModeUnlocked });
|
||||
void hostApiFetch('/api/settings/devModeUnlocked', {
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
@layer base {
|
||||
:root {
|
||||
/* ── shadcn semantic tokens (light) ─────────────────────────── */
|
||||
/* #f1ede1 — ClawX cream page background; gentler than pure white
|
||||
/* Lightened ClawX cream page background; gentler than pure white
|
||||
* for long sessions in front of the editor. */
|
||||
--background: 45 36.4% 91.4%;
|
||||
--background: 45 30% 96.6%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
@@ -64,16 +64,37 @@
|
||||
* heavy shadows. Pixel values are extracted from the hex literals
|
||||
* that used to be sprinkled across components.
|
||||
*
|
||||
* --surface-modal = #f3f1e9 large rounded modal panels (top)
|
||||
* was: bg-[#f3f1e9] dark:bg-card
|
||||
* --surface-input = #eeece3 inputs / selects / code panes
|
||||
* was: bg-[#eeece3] dark:bg-muted
|
||||
* --surface-sidebar = #eae8e1 left navigation rail (often /60)
|
||||
* was: bg-[#eae8e1]/60 dark:bg-background
|
||||
* --surface-modal = lifted cream large rounded modal panels (top);
|
||||
* slightly lighter than --background
|
||||
* so dialogs feel raised
|
||||
* --surface-input = recessed cream inputs / selects / code panes;
|
||||
* slightly darker than --background
|
||||
* so fields feel inset
|
||||
* --surface-sidebar = sidebar cream left navigation rail (often /60)
|
||||
* ─────────────────────────────────────────────────────────── */
|
||||
--surface-modal: 48 29% 93%;
|
||||
--surface-input: 49 24% 91%;
|
||||
--surface-sidebar: 47 18% 90%;
|
||||
--surface-modal: 45 30% 97.6%;
|
||||
--surface-input: 45 22% 94%;
|
||||
--surface-sidebar: 45 16% 94%;
|
||||
|
||||
/* ── ClawX usage accents (light) ─────────────────────────────
|
||||
*
|
||||
* Three semantic accents reused everywhere that we visualise
|
||||
* token usage / activity. Mirrors the Cron page convention so
|
||||
* the Models bar chart, legend dots, and any future "input /
|
||||
* output / cache" indicator pulls from the same palette:
|
||||
*
|
||||
* --usage-input = blue (matches Cron "Total" tile / primary)
|
||||
* --usage-output = green (matches Cron "Active" tile / `bg-green-500`)
|
||||
* --usage-cache = yellow (matches Cron "Paused" tile)
|
||||
*
|
||||
* Anchored to Tailwind's *-500 shades so chart bars read at the
|
||||
* same brightness as the `bg-green-500` "configured" status dot
|
||||
* and the Cron stat-tile icon halos. Going darker (*-600/*-700)
|
||||
* makes the bars look duller than the dots they sit next to.
|
||||
* ─────────────────────────────────────────────────────────── */
|
||||
--usage-input: 217 91% 60%;
|
||||
--usage-output: 142 71% 45%;
|
||||
--usage-cache: 48 96% 53%;
|
||||
}
|
||||
|
||||
/* Neutral deep-gray dark palette */
|
||||
@@ -109,6 +130,13 @@
|
||||
--surface-modal: 240 3% 14%;
|
||||
--surface-input: 240 3% 18%;
|
||||
--surface-sidebar: 240 4% 11%;
|
||||
|
||||
/* Usage accents — lifted *-400-ish shades so bars stay vivid
|
||||
* against the deep-gray dark surfaces (see light block above
|
||||
* for semantic mapping). */
|
||||
--usage-input: 217 91% 65%;
|
||||
--usage-output: 142 71% 52%;
|
||||
--usage-cache: 48 96% 60%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +151,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.sidebar-nav-text.sidebar-nav-text {
|
||||
@apply text-sm font-normal leading-4;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar: keep scrollbars visually quiet until the user hovers a scroll area. */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
|
||||
@@ -254,6 +254,17 @@ module.exports = {
|
||||
input: 'hsl(var(--surface-input) / <alpha-value>)',
|
||||
sidebar: 'hsl(var(--surface-sidebar) / <alpha-value>)',
|
||||
},
|
||||
|
||||
// ── D. ClawX usage accents ──────────────────────────────────
|
||||
// Semantic chart palette shared by the Models token-usage
|
||||
// visualisation and any future input/output/cache indicator.
|
||||
// Mirrors Cron's stat-tile palette (blue / green / yellow).
|
||||
// Values live in globals.css; dark mode brightens each one.
|
||||
usage: {
|
||||
input: 'hsl(var(--usage-input) / <alpha-value>)',
|
||||
output: 'hsl(var(--usage-output) / <alpha-value>)',
|
||||
cache: 'hsl(var(--usage-cache) / <alpha-value>)',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
|
||||
@@ -8,6 +8,10 @@ test.describe('ClawX main navigation without setup flow', () => {
|
||||
const page = await getStableWindow(app);
|
||||
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
await expect(page.getByTestId('chat-page')).toBeVisible();
|
||||
await expect(page.getByTestId('main-content')).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-resize-handle')).toBeVisible();
|
||||
await expect(page.getByTestId('main-content')).toHaveCSS('border-top-left-radius', '16px');
|
||||
|
||||
await page.getByTestId('sidebar-nav-models').click();
|
||||
await expect(page.getByTestId('models-page')).toBeVisible();
|
||||
|
||||
29
tests/unit/main-layout.test.tsx
Normal file
29
tests/unit/main-layout.test.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MainLayout } from '@/components/layout/MainLayout';
|
||||
|
||||
vi.mock('@/components/layout/Sidebar', () => ({
|
||||
Sidebar: () => <aside data-testid="sidebar" />,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/layout/TitleBar', () => ({
|
||||
TitleBar: () => <div data-testid="titlebar" />,
|
||||
}));
|
||||
|
||||
describe('MainLayout platform layout', () => {
|
||||
it('uses a left/right shell on macOS', () => {
|
||||
window.electron.platform = 'darwin';
|
||||
|
||||
render(<MainLayout />);
|
||||
|
||||
expect(screen.getByTestId('main-layout')).toHaveClass('flex-row');
|
||||
});
|
||||
|
||||
it('keeps a top titlebar column shell on Windows', () => {
|
||||
window.electron.platform = 'win32';
|
||||
|
||||
render(<MainLayout />);
|
||||
|
||||
expect(screen.getByTestId('main-layout')).toHaveClass('flex-col');
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ describe('Settings Store', () => {
|
||||
theme: 'system',
|
||||
language: 'en',
|
||||
sidebarCollapsed: false,
|
||||
sidebarWidth: 280,
|
||||
devModeUnlocked: false,
|
||||
gatewayAutoStart: true,
|
||||
gatewayPort: 18789,
|
||||
@@ -41,6 +42,19 @@ describe('Settings Store', () => {
|
||||
setSidebarCollapsed(true);
|
||||
expect(useSettingsStore.getState().sidebarCollapsed).toBe(true);
|
||||
});
|
||||
|
||||
it('should clamp sidebar width', () => {
|
||||
const { setSidebarWidth } = useSettingsStore.getState();
|
||||
|
||||
setSidebarWidth(320);
|
||||
expect(useSettingsStore.getState().sidebarWidth).toBe(320);
|
||||
|
||||
setSidebarWidth(100);
|
||||
expect(useSettingsStore.getState().sidebarWidth).toBe(220);
|
||||
|
||||
setSidebarWidth(600);
|
||||
expect(useSettingsStore.getState().sidebarWidth).toBe(420);
|
||||
});
|
||||
|
||||
it('should unlock dev mode', () => {
|
||||
const invoke = vi.mocked(window.electron.ipcRenderer.invoke);
|
||||
|
||||
@@ -14,12 +14,12 @@ describe('TitleBar platform behavior', () => {
|
||||
invokeIpcMock.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
it('renders macOS drag region', () => {
|
||||
it('does not render a standalone title bar on macOS', () => {
|
||||
window.electron.platform = 'darwin';
|
||||
|
||||
const { container } = render(<TitleBar />);
|
||||
|
||||
expect(container.querySelector('.drag-region')).toBeInTheDocument();
|
||||
expect(container.firstChild).toBeNull();
|
||||
expect(screen.queryByTitle('Minimize')).not.toBeInTheDocument();
|
||||
expect(invokeIpcMock).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -32,6 +32,7 @@ describe('TitleBar platform behavior', () => {
|
||||
expect(screen.getByTitle('Minimize')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Maximize')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Close')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('windows-titlebar')).not.toHaveClass('border-b');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(invokeIpcMock).toHaveBeenCalledWith('window:isMaximized');
|
||||
|
||||
Reference in New Issue
Block a user