chore: reformat files after running npm run lint:fix

This commit is contained in:
Haileyesus
2026-03-05 17:30:21 +03:00
parent 28e9bf7eb0
commit 6e55bd2d75
162 changed files with 1685 additions and 1693 deletions

View File

@@ -1,15 +1,13 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Sidebar from '../sidebar/view/Sidebar'; import Sidebar from '../sidebar/view/Sidebar';
import MainContent from '../main-content/view/MainContent'; import MainContent from '../main-content/view/MainContent';
import MobileNav from './MobileNav';
import { useWebSocket } from '../../contexts/WebSocketContext'; import { useWebSocket } from '../../contexts/WebSocketContext';
import { useDeviceSettings } from '../../hooks/useDeviceSettings'; import { useDeviceSettings } from '../../hooks/useDeviceSettings';
import { useSessionProtection } from '../../hooks/useSessionProtection'; import { useSessionProtection } from '../../hooks/useSessionProtection';
import { useProjectsState } from '../../hooks/useProjectsState'; import { useProjectsState } from '../../hooks/useProjectsState';
import MobileNav from './MobileNav';
export default function AppContent() { export default function AppContent() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -98,7 +96,7 @@ export default function AppContent() {
</div> </div>
) : ( ) : (
<div <div
className={`fixed inset-0 z-50 flex transition-all duration-150 ease-out ${sidebarOpen ? 'opacity-100 visible' : 'opacity-0 invisible' className={`fixed inset-0 z-50 flex transition-all duration-150 ease-out ${sidebarOpen ? 'visible opacity-100' : 'invisible opacity-0'
}`} }`}
> >
<button <button
@@ -115,7 +113,7 @@ export default function AppContent() {
aria-label={t('versionUpdate.ariaLabels.closeSidebar')} aria-label={t('versionUpdate.ariaLabels.closeSidebar')}
/> />
<div <div
className={`relative w-[85vw] max-w-sm sm:w-80 h-full bg-card border-r border-border/40 transform transition-transform duration-150 ease-out ${sidebarOpen ? 'translate-x-0' : '-translate-x-full' className={`relative h-full w-[85vw] max-w-sm transform border-r border-border/40 bg-card transition-transform duration-150 ease-out sm:w-80 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`} }`}
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
onTouchStart={(event) => event.stopPropagation()} onTouchStart={(event) => event.stopPropagation()}
@@ -125,7 +123,7 @@ export default function AppContent() {
</div> </div>
)} )}
<div className={`flex-1 flex flex-col min-w-0 ${isMobile ? 'pb-mobile-nav' : ''}`}> <div className={`flex min-w-0 flex-1 flex-col ${isMobile ? 'pb-mobile-nav' : ''}`}>
<MainContent <MainContent
selectedProject={selectedProject} selectedProject={selectedProject}
selectedSession={selectedSession} selectedSession={selectedSession}

View File

@@ -1,6 +1,6 @@
import { MessageSquare, Folder, Terminal, GitBranch, ClipboardCheck } from 'lucide-react'; import { MessageSquare, Folder, Terminal, GitBranch, ClipboardCheck } from 'lucide-react';
import { useTasksSettings } from '../../contexts/TasksSettingsContext';
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction } from 'react';
import { useTasksSettings } from '../../contexts/TasksSettingsContext';
import { AppTab } from '../../types/app'; import { AppTab } from '../../types/app';
type MobileNavProps = { type MobileNavProps = {
@@ -48,11 +48,11 @@ export default function MobileNav({ activeTab, setActiveTab, isInputFocused }: M
return ( return (
<div <div
className={`fixed bottom-0 left-0 right-0 z-50 px-3 pb-[max(8px,env(safe-area-inset-bottom))] transform transition-transform duration-300 ease-in-out ${isInputFocused ? 'translate-y-full' : 'translate-y-0' className={`fixed bottom-0 left-0 right-0 z-50 transform px-3 pb-[max(8px,env(safe-area-inset-bottom))] transition-transform duration-300 ease-in-out ${isInputFocused ? 'translate-y-full' : 'translate-y-0'
}`} }`}
> >
<div className="nav-glass mobile-nav-float rounded-2xl border border-border/30"> <div className="nav-glass mobile-nav-float rounded-2xl border border-border/30">
<div className="flex items-center justify-around px-1 py-1.5 gap-0.5"> <div className="flex items-center justify-around gap-0.5 px-1 py-1.5">
{navItems.map((item) => { {navItems.map((item) => {
const Icon = item.icon; const Icon = item.icon;
const isActive = activeTab === item.id; const isActive = activeTab === item.id;
@@ -65,7 +65,7 @@ export default function MobileNav({ activeTab, setActiveTab, isInputFocused }: M
e.preventDefault(); e.preventDefault();
item.onClick(); item.onClick();
}} }}
className={`flex flex-col items-center justify-center gap-0.5 px-3 py-2 rounded-xl flex-1 relative touch-manipulation transition-all duration-200 active:scale-95 ${isActive className={`relative flex flex-1 touch-manipulation flex-col items-center justify-center gap-0.5 rounded-xl px-3 py-2 transition-all duration-200 active:scale-95 ${isActive
? 'text-primary' ? 'text-primary'
: 'text-muted-foreground hover:text-foreground' : 'text-muted-foreground hover:text-foreground'
}`} }`}
@@ -73,10 +73,10 @@ export default function MobileNav({ activeTab, setActiveTab, isInputFocused }: M
aria-current={isActive ? 'page' : undefined} aria-current={isActive ? 'page' : undefined}
> >
{isActive && ( {isActive && (
<div className="absolute inset-0 bg-primary/8 dark:bg-primary/12 rounded-xl" /> <div className="bg-primary/8 dark:bg-primary/12 absolute inset-0 rounded-xl" />
)} )}
<Icon <Icon
className={`relative z-10 transition-all duration-200 ${isActive ? 'w-5 h-5' : 'w-[18px] h-[18px]'}`} className={`relative z-10 transition-all duration-200 ${isActive ? 'h-5 w-5' : 'h-[18px] w-[18px]'}`}
strokeWidth={isActive ? 2.4 : 1.8} strokeWidth={isActive ? 2.4 : 1.8}
/> />
<span className={`relative z-10 text-[10px] font-medium transition-all duration-200 ${isActive ? 'opacity-100' : 'opacity-60'}`}> <span className={`relative z-10 text-[10px] font-medium transition-all duration-200 ${isActive ? 'opacity-100' : 'opacity-60'}`}>

View File

@@ -8,7 +8,7 @@ export default function AuthErrorAlert({ errorMessage }: AuthErrorAlertProps) {
} }
return ( return (
<div className="p-3 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded-md"> <div className="rounded-md border border-red-300 bg-red-100 p-3 dark:border-red-800 dark:bg-red-900/20">
<p className="text-sm text-red-700 dark:text-red-400">{errorMessage}</p> <p className="text-sm text-red-700 dark:text-red-400">{errorMessage}</p>
</div> </div>
); );

View File

@@ -19,7 +19,7 @@ export default function AuthInputField({
}: AuthInputFieldProps) { }: AuthInputFieldProps) {
return ( return (
<div> <div>
<label htmlFor={id} className="block text-sm font-medium text-foreground mb-1"> <label htmlFor={id} className="mb-1 block text-sm font-medium text-foreground">
{label} {label}
</label> </label>
<input <input
@@ -27,7 +27,7 @@ export default function AuthInputField({
type={type} type={type}
value={value} value={value}
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full rounded-md border border-border bg-background px-3 py-2 text-foreground focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={placeholder} placeholder={placeholder}
required required
disabled={isDisabled} disabled={isDisabled}

View File

@@ -4,27 +4,27 @@ const loadingDotAnimationDelays = ['0s', '0.1s', '0.2s'];
export default function AuthLoadingScreen() { export default function AuthLoadingScreen() {
return ( return (
<div className="min-h-screen bg-background flex items-center justify-center p-4"> <div className="flex min-h-screen items-center justify-center bg-background p-4">
<div className="text-center"> <div className="text-center">
<div className="flex justify-center mb-4"> <div className="mb-4 flex justify-center">
<div className="w-16 h-16 bg-primary rounded-lg flex items-center justify-center shadow-sm"> <div className="flex h-16 w-16 items-center justify-center rounded-lg bg-primary shadow-sm">
<MessageSquare className="w-8 h-8 text-primary-foreground" /> <MessageSquare className="h-8 w-8 text-primary-foreground" />
</div> </div>
</div> </div>
<h1 className="text-2xl font-bold text-foreground mb-2">Claude Code UI</h1> <h1 className="mb-2 text-2xl font-bold text-foreground">Claude Code UI</h1>
<div className="flex items-center justify-center space-x-2"> <div className="flex items-center justify-center space-x-2">
{loadingDotAnimationDelays.map((delay) => ( {loadingDotAnimationDelays.map((delay) => (
<div <div
key={delay} key={delay}
className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" className="h-2 w-2 animate-bounce rounded-full bg-blue-500"
style={{ animationDelay: delay }} style={{ animationDelay: delay }}
/> />
))} ))}
</div> </div>
<p className="text-muted-foreground mt-2">Loading...</p> <p className="mt-2 text-muted-foreground">Loading...</p>
</div> </div>
</div> </div>
); );

View File

@@ -17,19 +17,19 @@ export default function AuthScreenLayout({
logo, logo,
}: AuthScreenLayoutProps) { }: AuthScreenLayoutProps) {
return ( return (
<div className="min-h-screen bg-background flex items-center justify-center p-4"> <div className="flex min-h-screen items-center justify-center bg-background p-4">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="bg-card rounded-lg shadow-lg border border-border p-8 space-y-6"> <div className="space-y-6 rounded-lg border border-border bg-card p-8 shadow-lg">
<div className="text-center"> <div className="text-center">
<div className="flex justify-center mb-4"> <div className="mb-4 flex justify-center">
{logo ?? ( {logo ?? (
<div className="w-16 h-16 bg-primary rounded-lg flex items-center justify-center shadow-sm"> <div className="flex h-16 w-16 items-center justify-center rounded-lg bg-primary shadow-sm">
<MessageSquare className="w-8 h-8 text-primary-foreground" /> <MessageSquare className="h-8 w-8 text-primary-foreground" />
</div> </div>
)} )}
</div> </div>
<h1 className="text-2xl font-bold text-foreground">{title}</h1> <h1 className="text-2xl font-bold text-foreground">{title}</h1>
<p className="text-muted-foreground mt-2">{description}</p> <p className="mt-2 text-muted-foreground">{description}</p>
</div> </div>
{children} {children}

View File

@@ -80,7 +80,7 @@ export default function LoginForm() {
<button <button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200" className="w-full rounded-md bg-blue-600 px-4 py-2 font-medium text-white transition-colors duration-200 hover:bg-blue-700 disabled:bg-blue-400"
> >
{isSubmitting ? t('login.loading') : t('login.submit')} {isSubmitting ? t('login.loading') : t('login.submit')}
</button> </button>

View File

@@ -1,9 +1,9 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { IS_PLATFORM } from '../../../constants/config'; import { IS_PLATFORM } from '../../../constants/config';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import Onboarding from '../../onboarding/view/Onboarding';
import AuthLoadingScreen from './AuthLoadingScreen'; import AuthLoadingScreen from './AuthLoadingScreen';
import LoginForm from './LoginForm'; import LoginForm from './LoginForm';
import Onboarding from '../../onboarding/view/Onboarding';
import SetupForm from './SetupForm'; import SetupForm from './SetupForm';
type ProtectedRouteProps = { type ProtectedRouteProps = {

View File

@@ -74,7 +74,7 @@ export default function SetupForm() {
title="Welcome to Claude Code UI" title="Welcome to Claude Code UI"
description="Set up your account to get started" description="Set up your account to get started"
footerText="This is a single-user system. Only one account can be created." footerText="This is a single-user system. Only one account can be created."
logo={<img src="/logo.svg" alt="CloudCLI" className="w-16 h-16" />} logo={<img src="/logo.svg" alt="CloudCLI" className="h-16 w-16" />}
> >
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<AuthInputField <AuthInputField
@@ -111,7 +111,7 @@ export default function SetupForm() {
<button <button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200" className="w-full rounded-md bg-blue-600 px-4 py-2 font-medium text-white transition-colors duration-200 hover:bg-blue-700 disabled:bg-blue-400"
> >
{isSubmitting ? 'Setting up...' : 'Create Account'} {isSubmitting ? 'Setting up...' : 'Create Account'}
</button> </button>

View File

@@ -11,9 +11,7 @@ import type {
} from 'react'; } from 'react';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import { authenticatedFetch } from '../../../utils/api'; import { authenticatedFetch } from '../../../utils/api';
import { thinkingModes } from '../constants/thinkingModes'; import { thinkingModes } from '../constants/thinkingModes';
import { grantClaudeToolPermission } from '../utils/chatPermissions'; import { grantClaudeToolPermission } from '../utils/chatPermissions';
import { safeLocalStorage } from '../utils/chatStorage'; import { safeLocalStorage } from '../utils/chatStorage';
import type { import type {
@@ -21,10 +19,10 @@ import type {
PendingPermissionRequest, PendingPermissionRequest,
PermissionMode, PermissionMode,
} from '../types/types'; } from '../types/types';
import { useFileMentions } from './useFileMentions';
import { type SlashCommand, useSlashCommands } from './useSlashCommands';
import type { Project, ProjectSession, SessionProvider } from '../../../types/app'; import type { Project, ProjectSession, SessionProvider } from '../../../types/app';
import { escapeRegExp } from '../utils/chatFormatting'; import { escapeRegExp } from '../utils/chatFormatting';
import { useFileMentions } from './useFileMentions';
import { type SlashCommand, useSlashCommands } from './useSlashCommands';
type PendingViewSession = { type PendingViewSession = {
sessionId: string | null; sessionId: string | null;

View File

@@ -1,6 +1,5 @@
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { MutableRefObject } from 'react'; import type { MutableRefObject } from 'react';
import { api, authenticatedFetch } from '../../../utils/api'; import { api, authenticatedFetch } from '../../../utils/api';
import type { ChatMessage, Provider } from '../types/types'; import type { ChatMessage, Provider } from '../types/types';
import type { Project, ProjectSession } from '../../../types/app'; import type { Project, ProjectSession } from '../../../types/app';

View File

@@ -161,7 +161,7 @@ export function useFileMentions({ selectedProject, input, setInput, textareaRef
fileMentionSet.has(part) ? ( fileMentionSet.has(part) ? (
<span <span
key={`mention-${index}`} key={`mention-${index}`}
className="bg-blue-200/70 -ml-0.5 dark:bg-blue-300/40 px-0.5 rounded-md box-decoration-clone text-transparent" className="-ml-0.5 rounded-md bg-blue-200/70 box-decoration-clone px-0.5 text-transparent dark:bg-blue-300/40"
> >
{part} {part}
</span> </span>

View File

@@ -1,8 +1,8 @@
import React, { memo, useMemo, useCallback } from 'react'; import React, { memo, useMemo, useCallback } from 'react';
import { getToolConfig } from './configs/toolConfigs';
import { OneLineDisplay, CollapsibleDisplay, ToolDiffViewer, MarkdownContent, FileListContent, TodoListContent, TaskListContent, TextContent, QuestionAnswerContent, SubagentContainer } from './components';
import type { Project } from '../../../types/app'; import type { Project } from '../../../types/app';
import type { SubagentChildTool } from '../types/types'; import type { SubagentChildTool } from '../types/types';
import { getToolConfig } from './configs/toolConfigs';
import { OneLineDisplay, CollapsibleDisplay, ToolDiffViewer, MarkdownContent, FileListContent, TodoListContent, TaskListContent, TextContent, QuestionAnswerContent, SubagentContainer } from './components';
type DiffLine = { type DiffLine = {
type: string; type: string;
@@ -202,7 +202,7 @@ export const ToolRenderer: React.FC<ToolRendererProps> = memo(({
const msg = displayConfig.getMessage?.(parsedData) || 'Success'; const msg = displayConfig.getMessage?.(parsedData) || 'Success';
contentComponent = ( contentComponent = (
<div className="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400"> <div className="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg> </svg>
{msg} {msg}

View File

@@ -43,7 +43,7 @@ export const CollapsibleDisplay: React.FC<CollapsibleDisplayProps> = ({
const borderColor = borderColorMap[toolCategory || 'default'] || borderColorMap.default; const borderColor = borderColorMap[toolCategory || 'default'] || borderColorMap.default;
return ( return (
<div className={`border-l-2 ${borderColor} pl-3 py-0.5 my-1 ${className}`}> <div className={`border-l-2 ${borderColor} my-1 py-0.5 pl-3 ${className}`}>
<CollapsibleSection <CollapsibleSection
title={title} title={title}
toolName={toolName} toolName={toolName}
@@ -54,10 +54,10 @@ export const CollapsibleDisplay: React.FC<CollapsibleDisplayProps> = ({
{children} {children}
{showRawParameters && rawContent && ( {showRawParameters && rawContent && (
<details className="relative mt-2 group/raw"> <details className="group/raw relative mt-2">
<summary className="flex items-center gap-1.5 text-[11px] text-gray-400 dark:text-gray-500 cursor-pointer hover:text-gray-600 dark:hover:text-gray-300 py-0.5"> <summary className="flex cursor-pointer items-center gap-1.5 py-0.5 text-[11px] text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300">
<svg <svg
className="w-2.5 h-2.5 transition-transform duration-150 group-open/raw:rotate-90" className="h-2.5 w-2.5 transition-transform duration-150 group-open/raw:rotate-90"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -66,7 +66,7 @@ export const CollapsibleDisplay: React.FC<CollapsibleDisplayProps> = ({
</svg> </svg>
raw params raw params
</summary> </summary>
<pre className="mt-1 text-[11px] bg-gray-50 dark:bg-gray-900/50 border border-gray-200/40 dark:border-gray-700/40 p-2 rounded whitespace-pre-wrap break-words overflow-hidden text-gray-600 dark:text-gray-400 font-mono"> <pre className="mt-1 overflow-hidden whitespace-pre-wrap break-words rounded border border-gray-200/40 bg-gray-50 p-2 font-mono text-[11px] text-gray-600 dark:border-gray-700/40 dark:bg-gray-900/50 dark:text-gray-400">
{rawContent} {rawContent}
</pre> </pre>
</details> </details>

View File

@@ -23,10 +23,10 @@ export const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
className = '' className = ''
}) => { }) => {
return ( return (
<details className={`relative group/details ${className}`} open={open}> <details className={`group/details relative ${className}`} open={open}>
<summary className="flex items-center gap-1.5 text-xs cursor-pointer py-0.5 select-none group-open/details:sticky group-open/details:top-0 group-open/details:z-10 group-open/details:bg-background group-open/details:-mx-1 group-open/details:px-1"> <summary className="flex cursor-pointer select-none items-center gap-1.5 py-0.5 text-xs group-open/details:sticky group-open/details:top-0 group-open/details:z-10 group-open/details:-mx-1 group-open/details:bg-background group-open/details:px-1">
<svg <svg
className="w-3 h-3 text-gray-400 dark:text-gray-500 transition-transform duration-150 group-open/details:rotate-90 flex-shrink-0" className="h-3 w-3 flex-shrink-0 text-gray-400 transition-transform duration-150 group-open/details:rotate-90 dark:text-gray-500"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -34,24 +34,24 @@ export const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg> </svg>
{toolName && ( {toolName && (
<span className="font-medium text-gray-500 dark:text-gray-400 flex-shrink-0">{toolName}</span> <span className="flex-shrink-0 font-medium text-gray-500 dark:text-gray-400">{toolName}</span>
)} )}
{toolName && ( {toolName && (
<span className="text-gray-300 dark:text-gray-600 text-[10px] flex-shrink-0">/</span> <span className="flex-shrink-0 text-[10px] text-gray-300 dark:text-gray-600">/</span>
)} )}
{onTitleClick ? ( {onTitleClick ? (
<button <button
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onTitleClick(); }} onClick={(e) => { e.preventDefault(); e.stopPropagation(); onTitleClick(); }}
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-mono hover:underline truncate flex-1 text-left transition-colors" className="flex-1 truncate text-left font-mono text-blue-600 transition-colors hover:text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300"
> >
{title} {title}
</button> </button>
) : ( ) : (
<span className="text-gray-600 dark:text-gray-400 truncate flex-1"> <span className="flex-1 truncate text-gray-600 dark:text-gray-400">
{title} {title}
</span> </span>
)} )}
{action && <span className="flex-shrink-0 ml-1">{action}</span>} {action && <span className="ml-1 flex-shrink-0">{action}</span>}
</summary> </summary>
<div className="mt-1.5 pl-[18px]"> <div className="mt-1.5 pl-[18px]">
{children} {children}

View File

@@ -23,11 +23,11 @@ export const FileListContent: React.FC<FileListContentProps> = ({
return ( return (
<div> <div>
{title && ( {title && (
<div className="text-[11px] text-gray-500 dark:text-gray-400 mb-1"> <div className="mb-1 text-[11px] text-gray-500 dark:text-gray-400">
{title} {title}
</div> </div>
)} )}
<div className="flex flex-wrap gap-x-1 gap-y-0.5 max-h-48 overflow-y-auto"> <div className="flex max-h-48 flex-wrap gap-x-1 gap-y-0.5 overflow-y-auto">
{files.map((file, index) => { {files.map((file, index) => {
const filePath = typeof file === 'string' ? file : file.path; const filePath = typeof file === 'string' ? file : file.path;
const fileName = filePath.split('/').pop() || filePath; const fileName = filePath.split('/').pop() || filePath;
@@ -39,13 +39,13 @@ export const FileListContent: React.FC<FileListContentProps> = ({
<span key={index} className="inline-flex items-center"> <span key={index} className="inline-flex items-center">
<button <button
onClick={handleClick} onClick={handleClick}
className="text-[11px] font-mono text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline transition-colors" className="font-mono text-[11px] text-blue-600 transition-colors hover:text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300"
title={filePath} title={filePath}
> >
{fileName} {fileName}
</button> </button>
{index < files.length - 1 && ( {index < files.length - 1 && (
<span className="text-gray-300 dark:text-gray-600 text-[10px] ml-1">,</span> <span className="ml-1 text-[10px] text-gray-300 dark:text-gray-600">,</span>
)} )}
</span> </span>
); );

View File

@@ -33,31 +33,31 @@ export const QuestionAnswerContent: React.FC<QuestionAnswerContentProps> = ({
return ( return (
<div <div
key={idx} key={idx}
className="rounded-lg border border-gray-150 dark:border-gray-700/50 bg-gray-50/50 dark:bg-gray-800/30 overflow-hidden" className="border-gray-150 overflow-hidden rounded-lg border bg-gray-50/50 dark:border-gray-700/50 dark:bg-gray-800/30"
> >
<button <button
type="button" type="button"
onClick={() => setExpandedIdx(isExpanded ? null : idx)} onClick={() => setExpandedIdx(isExpanded ? null : idx)}
className="w-full text-left px-3 py-2 flex items-start gap-2.5 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors" className="flex w-full items-start gap-2.5 px-3 py-2 text-left transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/50"
> >
<div className={`mt-0.5 flex-shrink-0 w-4 h-4 rounded-full flex items-center justify-center ${ <div className={`mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full ${
answerLabels.length > 0 answerLabels.length > 0
? 'bg-blue-100 dark:bg-blue-900/40' ? 'bg-blue-100 dark:bg-blue-900/40'
: 'bg-gray-100 dark:bg-gray-800' : 'bg-gray-100 dark:bg-gray-800'
}`}> }`}>
{answerLabels.length > 0 ? ( {answerLabels.length > 0 ? (
<svg className="w-2.5 h-2.5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={3}> <svg className="h-2.5 w-2.5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={3}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg> </svg>
) : ( ) : (
<div className="w-1.5 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600" /> <div className="h-1.5 w-1.5 rounded-full bg-gray-300 dark:bg-gray-600" />
)} )}
</div> </div>
<div className="flex-1 min-w-0"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex flex-wrap items-center gap-2">
{q.header && ( {q.header && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[9px] font-semibold uppercase tracking-wider bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 border border-blue-100/80 dark:border-blue-800/40"> <span className="inline-flex items-center rounded border border-blue-100/80 bg-blue-50 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider text-blue-600 dark:border-blue-800/40 dark:bg-blue-900/30 dark:text-blue-400">
{q.header} {q.header}
</span> </span>
)} )}
@@ -67,22 +67,22 @@ export const QuestionAnswerContent: React.FC<QuestionAnswerContentProps> = ({
</span> </span>
)} )}
</div> </div>
<div className="text-xs text-gray-600 dark:text-gray-400 mt-0.5 leading-snug"> <div className="mt-0.5 text-xs leading-snug text-gray-600 dark:text-gray-400">
{q.question} {q.question}
</div> </div>
{!isExpanded && answerLabels.length > 0 && ( {!isExpanded && answerLabels.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1.5"> <div className="mt-1.5 flex flex-wrap gap-1">
{answerLabels.map((lbl) => { {answerLabels.map((lbl) => {
const isCustom = !q.options.some(o => o.label === lbl); const isCustom = !q.options.some(o => o.label === lbl);
return ( return (
<span <span
key={lbl} key={lbl}
className="inline-flex items-center gap-1 text-[11px] px-1.5 py-0.5 rounded-md bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 font-medium" className="inline-flex items-center gap-1 rounded-md bg-blue-50 px-1.5 py-0.5 text-[11px] font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-300"
> >
{lbl} {lbl}
{isCustom && ( {isCustom && (
<span className="text-[9px] text-blue-400 dark:text-blue-500 font-normal">(custom)</span> <span className="text-[9px] font-normal text-blue-400 dark:text-blue-500">(custom)</span>
)} )}
</span> </span>
); );
@@ -91,14 +91,14 @@ export const QuestionAnswerContent: React.FC<QuestionAnswerContentProps> = ({
)} )}
{!isExpanded && skipped && hasAnyAnswer && ( {!isExpanded && skipped && hasAnyAnswer && (
<span className="inline-block mt-1 text-[10px] text-gray-400 dark:text-gray-500 italic"> <span className="mt-1 inline-block text-[10px] italic text-gray-400 dark:text-gray-500">
Skipped Skipped
</span> </span>
)} )}
</div> </div>
<svg <svg
className={`w-3.5 h-3.5 mt-0.5 text-gray-400 dark:text-gray-500 flex-shrink-0 transition-transform duration-200 ${ className={`mt-0.5 h-3.5 w-3.5 flex-shrink-0 text-gray-400 transition-transform duration-200 dark:text-gray-500 ${
isExpanded ? 'rotate-180' : '' isExpanded ? 'rotate-180' : ''
}`} }`}
fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2} fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}
@@ -108,36 +108,36 @@ export const QuestionAnswerContent: React.FC<QuestionAnswerContentProps> = ({
</button> </button>
{isExpanded && ( {isExpanded && (
<div className="px-3 pb-2.5 pt-0.5 border-t border-gray-100 dark:border-gray-700/40"> <div className="border-t border-gray-100 px-3 pb-2.5 pt-0.5 dark:border-gray-700/40">
<div className="space-y-1 ml-6.5"> <div className="ml-6.5 space-y-1">
{q.options.map((opt) => { {q.options.map((opt) => {
const wasSelected = answerLabels.includes(opt.label); const wasSelected = answerLabels.includes(opt.label);
return ( return (
<div <div
key={opt.label} key={opt.label}
className={`flex items-start gap-2 px-2.5 py-1.5 rounded-lg text-[12px] ${ className={`flex items-start gap-2 rounded-lg px-2.5 py-1.5 text-[12px] ${
wasSelected wasSelected
? 'bg-blue-50/80 dark:bg-blue-900/20 border border-blue-200/60 dark:border-blue-800/40' ? 'border border-blue-200/60 bg-blue-50/80 dark:border-blue-800/40 dark:bg-blue-900/20'
: 'text-gray-400 dark:text-gray-500' : 'text-gray-400 dark:text-gray-500'
}`} }`}
> >
<div className={`mt-0.5 flex-shrink-0 w-3.5 h-3.5 ${q.multiSelect ? 'rounded-[3px]' : 'rounded-full'} border-[1.5px] flex items-center justify-center ${ <div className={`mt-0.5 h-3.5 w-3.5 flex-shrink-0 ${q.multiSelect ? 'rounded-[3px]' : 'rounded-full'} flex items-center justify-center border-[1.5px] ${
wasSelected wasSelected
? 'border-blue-500 dark:border-blue-400 bg-blue-500 dark:bg-blue-500' ? 'border-blue-500 bg-blue-500 dark:border-blue-400 dark:bg-blue-500'
: 'border-gray-300 dark:border-gray-600' : 'border-gray-300 dark:border-gray-600'
}`}> }`}>
{wasSelected && ( {wasSelected && (
<svg className="w-2 h-2 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={3}> <svg className="h-2 w-2 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={3}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg> </svg>
)} )}
</div> </div>
<div className="flex-1 min-w-0"> <div className="min-w-0 flex-1">
<span className={wasSelected ? 'text-gray-900 dark:text-gray-100 font-medium' : ''}> <span className={wasSelected ? 'font-medium text-gray-900 dark:text-gray-100' : ''}>
{opt.label} {opt.label}
</span> </span>
{opt.description && ( {opt.description && (
<span className={`block text-[11px] mt-0.5 ${ <span className={`mt-0.5 block text-[11px] ${
wasSelected ? 'text-blue-600/70 dark:text-blue-300/70' : 'text-gray-400 dark:text-gray-600' wasSelected ? 'text-blue-600/70 dark:text-blue-300/70' : 'text-gray-400 dark:text-gray-600'
}`}> }`}>
{opt.description} {opt.description}
@@ -151,22 +151,22 @@ export const QuestionAnswerContent: React.FC<QuestionAnswerContentProps> = ({
{answerLabels.filter(lbl => !q.options.some(o => o.label === lbl)).map(lbl => ( {answerLabels.filter(lbl => !q.options.some(o => o.label === lbl)).map(lbl => (
<div <div
key={lbl} key={lbl}
className="flex items-start gap-2 px-2.5 py-1.5 rounded-lg text-[12px] bg-blue-50/80 dark:bg-blue-900/20 border border-blue-200/60 dark:border-blue-800/40" className="flex items-start gap-2 rounded-lg border border-blue-200/60 bg-blue-50/80 px-2.5 py-1.5 text-[12px] dark:border-blue-800/40 dark:bg-blue-900/20"
> >
<div className={`mt-0.5 flex-shrink-0 w-3.5 h-3.5 ${q.multiSelect ? 'rounded-[3px]' : 'rounded-full'} border-[1.5px] border-blue-500 dark:border-blue-400 bg-blue-500 dark:bg-blue-500 flex items-center justify-center`}> <div className={`mt-0.5 h-3.5 w-3.5 flex-shrink-0 ${q.multiSelect ? 'rounded-[3px]' : 'rounded-full'} flex items-center justify-center border-[1.5px] border-blue-500 bg-blue-500 dark:border-blue-400 dark:bg-blue-500`}>
<svg className="w-2 h-2 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={3}> <svg className="h-2 w-2 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={3}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg> </svg>
</div> </div>
<div className="flex-1 min-w-0"> <div className="min-w-0 flex-1">
<span className="text-gray-900 dark:text-gray-100 font-medium">{lbl}</span> <span className="font-medium text-gray-900 dark:text-gray-100">{lbl}</span>
<span className="text-[10px] text-blue-500 dark:text-blue-400 ml-1">(custom)</span> <span className="ml-1 text-[10px] text-blue-500 dark:text-blue-400">(custom)</span>
</div> </div>
</div> </div>
))} ))}
{skipped && hasAnyAnswer && ( {skipped && hasAnyAnswer && (
<div className="text-[11px] text-gray-400 dark:text-gray-500 italic px-2.5 py-1"> <div className="px-2.5 py-1 text-[11px] italic text-gray-400 dark:text-gray-500">
No answer provided No answer provided
</div> </div>
)} )}
@@ -178,7 +178,7 @@ export const QuestionAnswerContent: React.FC<QuestionAnswerContentProps> = ({
})} })}
{!hasAnyAnswer && total === 1 && ( {!hasAnyAnswer && total === 1 && (
<div className="text-[11px] text-gray-400 dark:text-gray-500 italic"> <div className="text-[11px] italic text-gray-400 dark:text-gray-500">
Skipped Skipped
</div> </div>
)} )}

View File

@@ -39,7 +39,7 @@ function parseTaskContent(content: string): TaskItem[] {
const statusConfig = { const statusConfig = {
completed: { completed: {
icon: ( icon: (
<svg className="w-3.5 h-3.5 text-green-500 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-3.5 w-3.5 text-green-500 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
), ),
@@ -48,7 +48,7 @@ const statusConfig = {
}, },
in_progress: { in_progress: {
icon: ( icon: (
<svg className="w-3.5 h-3.5 text-blue-500 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-3.5 w-3.5 text-blue-500 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
), ),
@@ -57,7 +57,7 @@ const statusConfig = {
}, },
pending: { pending: {
icon: ( icon: (
<svg className="w-3.5 h-3.5 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-3.5 w-3.5 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="9" strokeWidth={2} /> <circle cx="12" cy="12" r="9" strokeWidth={2} />
</svg> </svg>
), ),
@@ -76,7 +76,7 @@ export const TaskListContent: React.FC<TaskListContentProps> = ({ content }) =>
// If we couldn't parse any tasks, fall back to text display // If we couldn't parse any tasks, fall back to text display
if (tasks.length === 0) { if (tasks.length === 0) {
return ( return (
<pre className="text-[11px] font-mono text-gray-600 dark:text-gray-400 whitespace-pre-wrap"> <pre className="whitespace-pre-wrap font-mono text-[11px] text-gray-600 dark:text-gray-400">
{content} {content}
</pre> </pre>
); );
@@ -87,13 +87,13 @@ export const TaskListContent: React.FC<TaskListContentProps> = ({ content }) =>
return ( return (
<div> <div>
<div className="flex items-center gap-2 mb-1.5"> <div className="mb-1.5 flex items-center gap-2">
<span className="text-[11px] text-gray-500 dark:text-gray-400"> <span className="text-[11px] text-gray-500 dark:text-gray-400">
{completed}/{total} completed {completed}/{total} completed
</span> </span>
<div className="flex-1 h-1 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden"> <div className="h-1 flex-1 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
<div <div
className="h-full bg-green-500 dark:bg-green-400 rounded-full transition-all" className="h-full rounded-full bg-green-500 transition-all dark:bg-green-400"
style={{ width: `${total > 0 ? (completed / total) * 100 : 0}%` }} style={{ width: `${total > 0 ? (completed / total) * 100 : 0}%` }}
/> />
</div> </div>
@@ -104,16 +104,16 @@ export const TaskListContent: React.FC<TaskListContentProps> = ({ content }) =>
return ( return (
<div <div
key={task.id} key={task.id}
className="flex items-center gap-1.5 py-0.5 group" className="group flex items-center gap-1.5 py-0.5"
> >
<span className="flex-shrink-0">{config.icon}</span> <span className="flex-shrink-0">{config.icon}</span>
<span className="text-[11px] font-mono text-gray-400 dark:text-gray-500 flex-shrink-0"> <span className="flex-shrink-0 font-mono text-[11px] text-gray-400 dark:text-gray-500">
#{task.id} #{task.id}
</span> </span>
<span className={`text-xs truncate flex-1 ${config.textClass}`}> <span className={`flex-1 truncate text-xs ${config.textClass}`}>
{task.subject} {task.subject}
</span> </span>
<span className={`text-[10px] px-1 py-px rounded border flex-shrink-0 ${config.badgeClass}`}> <span className={`flex-shrink-0 rounded border px-1 py-px text-[10px] ${config.badgeClass}`}>
{task.status.replace('_', ' ')} {task.status.replace('_', ' ')}
</span> </span>
</div> </div>

View File

@@ -26,7 +26,7 @@ export const TextContent: React.FC<TextContentProps> = ({
} }
return ( return (
<pre className={`mt-1 text-xs bg-gray-900 dark:bg-gray-950 text-gray-100 p-2.5 rounded overflow-x-auto font-mono ${className}`}> <pre className={`mt-1 overflow-x-auto rounded bg-gray-900 p-2.5 font-mono text-xs text-gray-100 dark:bg-gray-950 ${className}`}>
{formattedJson} {formattedJson}
</pre> </pre>
); );
@@ -34,7 +34,7 @@ export const TextContent: React.FC<TextContentProps> = ({
if (format === 'code') { if (format === 'code') {
return ( return (
<pre className={`mt-1 text-xs bg-gray-50 dark:bg-gray-800/50 border border-gray-200/50 dark:border-gray-700/50 p-2 rounded whitespace-pre-wrap break-words overflow-hidden text-gray-700 dark:text-gray-300 font-mono ${className}`}> <pre className={`mt-1 overflow-hidden whitespace-pre-wrap break-words rounded border border-gray-200/50 bg-gray-50 p-2 font-mono text-xs text-gray-700 dark:border-gray-700/50 dark:bg-gray-800/50 dark:text-gray-300 ${className}`}>
{content} {content}
</pre> </pre>
); );
@@ -42,7 +42,7 @@ export const TextContent: React.FC<TextContentProps> = ({
// Plain text // Plain text
return ( return (
<div className={`mt-1 text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap ${className}`}> <div className={`mt-1 whitespace-pre-wrap text-sm text-gray-700 dark:text-gray-300 ${className}`}>
{content} {content}
</div> </div>
); );

View File

@@ -79,25 +79,25 @@ const TodoRow = memo(
const StatusIcon = statusConfig.icon; const StatusIcon = statusConfig.icon;
return ( return (
<div className="flex items-start gap-2 p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded transition-colors"> <div className="flex items-start gap-2 rounded border border-gray-200 bg-white p-2 transition-colors dark:border-gray-700 dark:bg-gray-800">
<div className="flex-shrink-0 mt-0.5"> <div className="mt-0.5 flex-shrink-0">
<StatusIcon className={statusConfig.iconClassName} /> <StatusIcon className={statusConfig.iconClassName} />
</div> </div>
<div className="flex-1 min-w-0"> <div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2 mb-0.5"> <div className="mb-0.5 flex items-start justify-between gap-2">
<p className={`text-xs font-medium ${statusConfig.textClassName}`}> <p className={`text-xs font-medium ${statusConfig.textClassName}`}>
{todo.content} {todo.content}
</p> </p>
<div className="flex gap-1 flex-shrink-0"> <div className="flex flex-shrink-0 gap-1">
<Badge <Badge
variant="outline" variant="outline"
className={`text-[10px] px-1.5 py-px ${PRIORITY_BADGE_CLASS[todo.priority]}`} className={`px-1.5 py-px text-[10px] ${PRIORITY_BADGE_CLASS[todo.priority]}`}
> >
{todo.priority} {todo.priority}
</Badge> </Badge>
<Badge <Badge
variant="outline" variant="outline"
className={`text-[10px] px-1.5 py-px ${statusConfig.badgeClassName}`} className={`px-1.5 py-px text-[10px] ${statusConfig.badgeClassName}`}
> >
{todo.status.replace('_', ' ')} {todo.status.replace('_', ' ')}
</Badge> </Badge>
@@ -136,7 +136,7 @@ const TodoList = memo(
return ( return (
<div className="space-y-1.5"> <div className="space-y-1.5">
{isResult && ( {isResult && (
<div className="text-xs font-medium text-gray-600 dark:text-gray-400 mb-1.5"> <div className="mb-1.5 text-xs font-medium text-gray-600 dark:text-gray-400">
Todo List ({normalizedTodos.length}{' '} Todo List ({normalizedTodos.length}{' '}
{normalizedTodos.length === 1 ? 'item' : 'items'}) {normalizedTodos.length === 1 ? 'item' : 'items'})
</div> </div>

View File

@@ -148,32 +148,32 @@ export const AskUserQuestionPanel: React.FC<PermissionPanelProps> = ({
tabIndex={-1} tabIndex={-1}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className={`w-full outline-none transition-all duration-500 ease-out ${ className={`w-full outline-none transition-all duration-500 ease-out ${
mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-3' mounted ? 'translate-y-0 opacity-100' : 'translate-y-3 opacity-0'
}`} }`}
> >
<div className="relative overflow-hidden rounded-2xl border border-gray-200/80 dark:border-gray-700/50 bg-white dark:bg-gray-800/90 shadow-lg dark:shadow-2xl"> <div className="relative overflow-hidden rounded-2xl border border-gray-200/80 bg-white shadow-lg dark:border-gray-700/50 dark:bg-gray-800/90 dark:shadow-2xl">
{/* Accent line */} {/* Accent line */}
<div className="absolute top-0 left-0 right-0 h-[2px] bg-gradient-to-r from-blue-500 via-cyan-400 to-teal-400" /> <div className="absolute left-0 right-0 top-0 h-[2px] bg-gradient-to-r from-blue-500 via-cyan-400 to-teal-400" />
{/* Header + Question — compact */} {/* Header + Question — compact */}
<div className="px-4 pt-3.5 pb-2"> <div className="px-4 pb-2 pt-3.5">
<div className="flex items-center gap-2.5 mb-1.5"> <div className="mb-1.5 flex items-center gap-2.5">
{/* Question icon */} {/* Question icon */}
<div className="relative flex-shrink-0"> <div className="relative flex-shrink-0">
<div className="w-6 h-6 rounded-lg bg-gradient-to-br from-blue-500/10 to-cyan-500/10 dark:from-blue-400/15 dark:to-cyan-400/15 flex items-center justify-center"> <div className="flex h-6 w-6 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500/10 to-cyan-500/10 dark:from-blue-400/15 dark:to-cyan-400/15">
<svg className="w-3.5 h-3.5 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" strokeWidth={1.75} stroke="currentColor"> <svg className="h-3.5 w-3.5 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" strokeWidth={1.75} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827m0 3h.01" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827m0 3h.01" />
</svg> </svg>
</div> </div>
<div className="absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full bg-cyan-400 dark:bg-cyan-500 animate-pulse" /> <div className="absolute -right-0.5 -top-0.5 h-2 w-2 animate-pulse rounded-full bg-cyan-400 dark:bg-cyan-500" />
</div> </div>
<div className="flex items-center gap-2 min-w-0 flex-1"> <div className="flex min-w-0 flex-1 items-center gap-2">
<span className="text-[10px] font-medium tracking-wide uppercase text-gray-400 dark:text-gray-500"> <span className="text-[10px] font-medium uppercase tracking-wide text-gray-400 dark:text-gray-500">
Claude needs your input Claude needs your input
</span> </span>
{q.header && ( {q.header && (
<span className="inline-flex items-center px-1.5 py-px rounded text-[9px] font-semibold uppercase tracking-wider bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 border border-blue-100 dark:border-blue-800/50"> <span className="inline-flex items-center rounded border border-blue-100 bg-blue-50 px-1.5 py-px text-[9px] font-semibold uppercase tracking-wider text-blue-600 dark:border-blue-800/50 dark:bg-blue-900/30 dark:text-blue-400">
{q.header} {q.header}
</span> </span>
)} )}
@@ -181,7 +181,7 @@ export const AskUserQuestionPanel: React.FC<PermissionPanelProps> = ({
{/* Step counter */} {/* Step counter */}
{!isSingle && ( {!isSingle && (
<span className="text-[10px] tabular-nums text-gray-400 dark:text-gray-500 flex-shrink-0"> <span className="flex-shrink-0 text-[10px] tabular-nums text-gray-400 dark:text-gray-500">
{currentStep + 1}/{total} {currentStep + 1}/{total}
</span> </span>
)} )}
@@ -189,7 +189,7 @@ export const AskUserQuestionPanel: React.FC<PermissionPanelProps> = ({
{/* Progress dots (multi-question) */} {/* Progress dots (multi-question) */}
{!isSingle && ( {!isSingle && (
<div className="flex items-center gap-1 mb-2"> <div className="mb-2 flex items-center gap-1">
{questions.map((_, i) => ( {questions.map((_, i) => (
<button <button
key={i} key={i}
@@ -208,7 +208,7 @@ export const AskUserQuestionPanel: React.FC<PermissionPanelProps> = ({
)} )}
{/* Question text */} {/* Question text */}
<p className="text-[14px] leading-snug font-medium text-gray-900 dark:text-gray-100"> <p className="text-[14px] font-medium leading-snug text-gray-900 dark:text-gray-100">
{q.question} {q.question}
</p> </p>
{multi && ( {multi && (
@@ -217,7 +217,7 @@ export const AskUserQuestionPanel: React.FC<PermissionPanelProps> = ({
</div> </div>
{/* Options — tight spacing */} {/* Options — tight spacing */}
<div className="px-4 pb-2 max-h-48 overflow-y-auto scrollbar-thin" role={multi ? 'group' : 'radiogroup'} aria-label={q.question}> <div className="scrollbar-thin max-h-48 overflow-y-auto px-4 pb-2" role={multi ? 'group' : 'radiogroup'} aria-label={q.question}>
<div className="space-y-1"> <div className="space-y-1">
{q.options.map((opt, optIdx) => { {q.options.map((opt, optIdx) => {
const isSelected = selected.has(opt.label); const isSelected = selected.has(opt.label);
@@ -226,25 +226,25 @@ export const AskUserQuestionPanel: React.FC<PermissionPanelProps> = ({
key={opt.label} key={opt.label}
type="button" type="button"
onClick={() => toggleOption(currentStep, opt.label, multi)} onClick={() => toggleOption(currentStep, opt.label, multi)}
className={`group w-full text-left flex items-center gap-2.5 px-3 py-2 rounded-lg border transition-all duration-150 ${ className={`group flex w-full items-center gap-2.5 rounded-lg border px-3 py-2 text-left transition-all duration-150 ${
isSelected isSelected
? 'border-blue-300 dark:border-blue-600 bg-blue-50/80 dark:bg-blue-900/25 ring-1 ring-blue-200/50 dark:ring-blue-700/30' ? 'border-blue-300 bg-blue-50/80 ring-1 ring-blue-200/50 dark:border-blue-600 dark:bg-blue-900/25 dark:ring-blue-700/30'
: 'border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50/60 dark:hover:bg-gray-750/50' : 'dark:hover:bg-gray-750/50 border-gray-200 hover:border-gray-300 hover:bg-gray-50/60 dark:border-gray-700/60 dark:hover:border-gray-600'
}`} }`}
> >
{/* Keyboard hint */} {/* Keyboard hint */}
<kbd className={`flex-shrink-0 w-5 h-5 rounded text-[10px] font-mono flex items-center justify-center transition-all duration-150 ${ <kbd className={`flex h-5 w-5 flex-shrink-0 items-center justify-center rounded font-mono text-[10px] transition-all duration-150 ${
isSelected isSelected
? 'bg-blue-500 dark:bg-blue-500 text-white font-semibold' ? 'bg-blue-500 font-semibold text-white dark:bg-blue-500'
: 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500 border border-gray-200 dark:border-gray-700 group-hover:border-gray-300 dark:group-hover:border-gray-600' : 'border border-gray-200 bg-gray-100 text-gray-400 group-hover:border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-500 dark:group-hover:border-gray-600'
}`}> }`}>
{optIdx + 1} {optIdx + 1}
</kbd> </kbd>
<div className="flex-1 min-w-0"> <div className="min-w-0 flex-1">
<div className={`text-[13px] leading-tight transition-colors duration-150 ${ <div className={`text-[13px] leading-tight transition-colors duration-150 ${
isSelected isSelected
? 'text-gray-900 dark:text-gray-100 font-medium' ? 'font-medium text-gray-900 dark:text-gray-100'
: 'text-gray-700 dark:text-gray-300' : 'text-gray-700 dark:text-gray-300'
}`}> }`}>
{opt.label} {opt.label}
@@ -262,7 +262,7 @@ export const AskUserQuestionPanel: React.FC<PermissionPanelProps> = ({
{/* Selection check */} {/* Selection check */}
{isSelected && ( {isSelected && (
<svg className="w-4 h-4 text-blue-500 dark:text-blue-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}> <svg className="h-4 w-4 flex-shrink-0 text-blue-500 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" /> <path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg> </svg>
)} )}
@@ -274,28 +274,28 @@ export const AskUserQuestionPanel: React.FC<PermissionPanelProps> = ({
<button <button
type="button" type="button"
onClick={() => toggleOther(currentStep, multi)} onClick={() => toggleOther(currentStep, multi)}
className={`group w-full text-left flex items-center gap-2.5 px-3 py-2 rounded-lg border transition-all duration-150 ${ className={`group flex w-full items-center gap-2.5 rounded-lg border px-3 py-2 text-left transition-all duration-150 ${
isOtherOn isOtherOn
? 'border-blue-300 dark:border-blue-600 bg-blue-50/80 dark:bg-blue-900/25 ring-1 ring-blue-200/50 dark:ring-blue-700/30' ? 'border-blue-300 bg-blue-50/80 ring-1 ring-blue-200/50 dark:border-blue-600 dark:bg-blue-900/25 dark:ring-blue-700/30'
: 'border-dashed border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50/60 dark:hover:bg-gray-750/50' : 'dark:hover:bg-gray-750/50 border-dashed border-gray-200 hover:border-gray-300 hover:bg-gray-50/60 dark:border-gray-700/60 dark:hover:border-gray-600'
}`} }`}
> >
<kbd className={`flex-shrink-0 w-5 h-5 rounded text-[10px] font-mono flex items-center justify-center transition-all duration-150 ${ <kbd className={`flex h-5 w-5 flex-shrink-0 items-center justify-center rounded font-mono text-[10px] transition-all duration-150 ${
isOtherOn isOtherOn
? 'bg-blue-500 dark:bg-blue-500 text-white font-semibold' ? 'bg-blue-500 font-semibold text-white dark:bg-blue-500'
: 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500 border border-gray-200 dark:border-gray-700 group-hover:border-gray-300 dark:group-hover:border-gray-600' : 'border border-gray-200 bg-gray-100 text-gray-400 group-hover:border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-500 dark:group-hover:border-gray-600'
}`}> }`}>
0 0
</kbd> </kbd>
<span className={`text-[13px] leading-tight transition-colors ${ <span className={`text-[13px] leading-tight transition-colors ${
isOtherOn isOtherOn
? 'text-gray-900 dark:text-gray-100 font-medium' ? 'font-medium text-gray-900 dark:text-gray-100'
: 'text-gray-500 dark:text-gray-400' : 'text-gray-500 dark:text-gray-400'
}`}> }`}>
Other... Other...
</span> </span>
{isOtherOn && ( {isOtherOn && (
<svg className="w-4 h-4 text-blue-500 dark:text-blue-400 flex-shrink-0 ml-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}> <svg className="ml-auto h-4 w-4 flex-shrink-0 text-blue-500 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" /> <path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg> </svg>
)} )}
@@ -320,9 +320,9 @@ export const AskUserQuestionPanel: React.FC<PermissionPanelProps> = ({
e.stopPropagation(); e.stopPropagation();
}} }}
placeholder="Type your answer..." placeholder="Type your answer..."
className="w-full text-[13px] rounded-lg border-0 bg-gray-50 dark:bg-gray-900/60 text-gray-900 dark:text-gray-100 px-3 py-1.5 outline-none ring-1 ring-gray-200 dark:ring-gray-700 focus:ring-2 focus:ring-blue-400 dark:focus:ring-blue-500 placeholder:text-gray-400 dark:placeholder:text-gray-600 transition-shadow duration-200" className="w-full rounded-lg border-0 bg-gray-50 px-3 py-1.5 text-[13px] text-gray-900 outline-none ring-1 ring-gray-200 transition-shadow duration-200 placeholder:text-gray-400 focus:ring-2 focus:ring-blue-400 dark:bg-gray-900/60 dark:text-gray-100 dark:ring-gray-700 dark:placeholder:text-gray-600 dark:focus:ring-blue-500"
/> />
<kbd className="absolute right-2 top-1/2 -translate-y-1/2 text-[9px] font-mono text-gray-300 dark:text-gray-600 bg-gray-100 dark:bg-gray-800 px-1 py-0.5 rounded border border-gray-200 dark:border-gray-700"> <kbd className="absolute right-2 top-1/2 -translate-y-1/2 rounded border border-gray-200 bg-gray-100 px-1 py-0.5 font-mono text-[9px] text-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-600">
Enter Enter
</kbd> </kbd>
</div> </div>
@@ -332,11 +332,11 @@ export const AskUserQuestionPanel: React.FC<PermissionPanelProps> = ({
</div> </div>
{/* Footer — compact */} {/* Footer — compact */}
<div className="px-4 py-2 border-t border-gray-100 dark:border-gray-700/50 bg-gray-50/50 dark:bg-gray-800/50 flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2 border-t border-gray-100 bg-gray-50/50 px-4 py-2 dark:border-gray-700/50 dark:bg-gray-800/50">
<button <button
type="button" type="button"
onClick={handleSkip} onClick={handleSkip}
className="text-[11px] text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors" className="text-[11px] text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
> >
{isSingle ? 'Skip' : 'Skip all'} {isSingle ? 'Skip' : 'Skip all'}
<span className="ml-1 text-[9px] text-gray-300 dark:text-gray-600">Esc</span> <span className="ml-1 text-[9px] text-gray-300 dark:text-gray-600">Esc</span>
@@ -347,9 +347,9 @@ export const AskUserQuestionPanel: React.FC<PermissionPanelProps> = ({
<button <button
type="button" type="button"
onClick={() => setCurrentStep(s => s - 1)} onClick={() => setCurrentStep(s => s - 1)}
className="inline-flex items-center gap-0.5 text-[11px] font-medium px-2.5 py-1.5 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700/60 transition-all duration-150" className="inline-flex items-center gap-0.5 rounded-lg px-2.5 py-1.5 text-[11px] font-medium text-gray-600 transition-all duration-150 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700/60"
> >
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}> <svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" /> <path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg> </svg>
Back Back
@@ -361,19 +361,19 @@ export const AskUserQuestionPanel: React.FC<PermissionPanelProps> = ({
type="button" type="button"
onClick={handleSubmit} onClick={handleSubmit}
disabled={!hasCurrentSelection && !Object.keys(buildAnswers()).length} disabled={!hasCurrentSelection && !Object.keys(buildAnswers()).length}
className="inline-flex items-center gap-1 text-[11px] font-semibold px-3.5 py-1.5 rounded-lg bg-gradient-to-r from-blue-600 to-blue-500 dark:from-blue-500 dark:to-blue-600 text-white shadow-sm hover:shadow-md disabled:opacity-30 disabled:cursor-not-allowed disabled:shadow-none transition-all duration-200" className="inline-flex items-center gap-1 rounded-lg bg-gradient-to-r from-blue-600 to-blue-500 px-3.5 py-1.5 text-[11px] font-semibold text-white shadow-sm transition-all duration-200 hover:shadow-md disabled:cursor-not-allowed disabled:opacity-30 disabled:shadow-none dark:from-blue-500 dark:to-blue-600"
> >
Submit Submit
<span className="text-[9px] opacity-70 font-mono ml-0.5">Enter</span> <span className="ml-0.5 font-mono text-[9px] opacity-70">Enter</span>
</button> </button>
) : ( ) : (
<button <button
type="button" type="button"
onClick={() => setCurrentStep(s => s + 1)} onClick={() => setCurrentStep(s => s + 1)}
className="inline-flex items-center gap-1 text-[11px] font-semibold px-3.5 py-1.5 rounded-lg bg-gradient-to-r from-blue-600 to-blue-500 dark:from-blue-500 dark:to-blue-600 text-white shadow-sm hover:shadow-md transition-all duration-200" className="inline-flex items-center gap-1 rounded-lg bg-gradient-to-r from-blue-600 to-blue-500 px-3.5 py-1.5 text-[11px] font-semibold text-white shadow-sm transition-all duration-200 hover:shadow-md dark:from-blue-500 dark:to-blue-600"
> >
Next Next
<span className="text-[9px] opacity-70 font-mono ml-0.5">Enter</span> <span className="ml-0.5 font-mono text-[9px] opacity-70">Enter</span>
</button> </button>
)} )}
</div> </div>

View File

@@ -68,16 +68,16 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
const renderCopyButton = () => ( const renderCopyButton = () => (
<button <button
onClick={handleAction} onClick={handleAction}
className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-all ml-1 flex-shrink-0" className="ml-1 flex-shrink-0 text-gray-400 opacity-0 transition-all hover:text-gray-600 group-hover:opacity-100 dark:hover:text-gray-200"
title="Copy to clipboard" title="Copy to clipboard"
aria-label="Copy to clipboard" aria-label="Copy to clipboard"
> >
{copied ? ( {copied ? (
<svg className="w-3 h-3 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-3 w-3 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg> </svg>
) : ( ) : (
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg> </svg>
)} )}
@@ -89,15 +89,15 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
return ( return (
<div className="group my-1"> <div className="group my-1">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<div className="flex items-center gap-1.5 flex-shrink-0 pt-0.5"> <div className="flex flex-shrink-0 items-center gap-1.5 pt-0.5">
<svg className="w-3 h-3 text-green-500 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-3 w-3 text-green-500 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg> </svg>
</div> </div>
<div className="flex-1 min-w-0 flex items-start gap-2"> <div className="flex min-w-0 flex-1 items-start gap-2">
<div className="bg-gray-900 dark:bg-black rounded px-2.5 py-1 flex-1 min-w-0"> <div className="min-w-0 flex-1 rounded bg-gray-900 px-2.5 py-1 dark:bg-black">
<code className={`text-xs text-green-400 font-mono ${wrapText ? 'whitespace-pre-wrap break-all' : 'block truncate'}`}> <code className={`font-mono text-xs text-green-400 ${wrapText ? 'whitespace-pre-wrap break-all' : 'block truncate'}`}>
<span className="text-green-600 dark:text-green-500 select-none">$ </span>{value} <span className="select-none text-green-600 dark:text-green-500">$ </span>{value}
</code> </code>
</div> </div>
{action === 'copy' && renderCopyButton()} {action === 'copy' && renderCopyButton()}
@@ -105,7 +105,7 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
</div> </div>
{secondary && ( {secondary && (
<div className="ml-7 mt-1"> <div className="ml-7 mt-1">
<span className="text-[11px] text-gray-400 dark:text-gray-500 italic"> <span className="text-[11px] italic text-gray-400 dark:text-gray-500">
{secondary} {secondary}
</span> </span>
</div> </div>
@@ -118,12 +118,12 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
if (action === 'open-file') { if (action === 'open-file') {
const displayName = value.split('/').pop() || value; const displayName = value.split('/').pop() || value;
return ( return (
<div className={`group flex items-center gap-1.5 border-l-2 ${colorScheme.border} pl-3 py-0.5 my-0.5`}> <div className={`group flex items-center gap-1.5 border-l-2 ${colorScheme.border} my-0.5 py-0.5 pl-3`}>
<span className="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">{label || toolName}</span> <span className="flex-shrink-0 text-xs text-gray-500 dark:text-gray-400">{label || toolName}</span>
<span className="text-gray-300 dark:text-gray-600 text-[10px]">/</span> <span className="text-[10px] text-gray-300 dark:text-gray-600">/</span>
<button <button
onClick={handleAction} onClick={handleAction}
className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-mono hover:underline transition-colors truncate" className="truncate font-mono text-xs text-blue-600 transition-colors hover:text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300"
title={value} title={value}
> >
{displayName} {displayName}
@@ -135,23 +135,23 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
// Search / jump-to-results style // Search / jump-to-results style
if (action === 'jump-to-results') { if (action === 'jump-to-results') {
return ( return (
<div className={`group flex items-center gap-1.5 border-l-2 ${colorScheme.border} pl-3 py-0.5 my-0.5`}> <div className={`group flex items-center gap-1.5 border-l-2 ${colorScheme.border} my-0.5 py-0.5 pl-3`}>
<span className="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">{label || toolName}</span> <span className="flex-shrink-0 text-xs text-gray-500 dark:text-gray-400">{label || toolName}</span>
<span className="text-gray-300 dark:text-gray-600 text-[10px]">/</span> <span className="text-[10px] text-gray-300 dark:text-gray-600">/</span>
<span className={`text-xs font-mono truncate flex-1 min-w-0 ${colorScheme.primary}`}> <span className={`min-w-0 flex-1 truncate font-mono text-xs ${colorScheme.primary}`}>
{value} {value}
</span> </span>
{secondary && ( {secondary && (
<span className="text-[11px] text-gray-400 dark:text-gray-500 italic flex-shrink-0"> <span className="flex-shrink-0 text-[11px] italic text-gray-400 dark:text-gray-500">
{secondary} {secondary}
</span> </span>
)} )}
{toolResult && ( {toolResult && (
<a <a
href={`#tool-result-${toolId}`} href={`#tool-result-${toolId}`}
className="flex-shrink-0 text-[11px] text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors flex items-center gap-0.5" className="flex flex-shrink-0 items-center gap-0.5 text-[11px] text-blue-600 transition-colors hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
> >
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg> </svg>
</a> </a>
@@ -162,21 +162,21 @@ export const OneLineDisplay: React.FC<OneLineDisplayProps> = ({
// Default one-line style // Default one-line style
return ( return (
<div className={`group flex items-center gap-1.5 ${colorScheme.background || ''} border-l-2 ${colorScheme.border} pl-3 py-0.5 my-0.5`}> <div className={`group flex items-center gap-1.5 ${colorScheme.background || ''} border-l-2 ${colorScheme.border} my-0.5 py-0.5 pl-3`}>
{icon && icon !== 'terminal' && ( {icon && icon !== 'terminal' && (
<span className={`${colorScheme.icon} flex-shrink-0 text-xs`}>{icon}</span> <span className={`${colorScheme.icon} flex-shrink-0 text-xs`}>{icon}</span>
)} )}
{!icon && (label || toolName) && ( {!icon && (label || toolName) && (
<span className="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">{label || toolName}</span> <span className="flex-shrink-0 text-xs text-gray-500 dark:text-gray-400">{label || toolName}</span>
)} )}
{(icon || label || toolName) && ( {(icon || label || toolName) && (
<span className="text-gray-300 dark:text-gray-600 text-[10px]">/</span> <span className="text-[10px] text-gray-300 dark:text-gray-600">/</span>
)} )}
<span className={`text-xs font-mono ${wrapText ? 'whitespace-pre-wrap break-all' : 'truncate'} flex-1 min-w-0 ${colorScheme.primary}`}> <span className={`font-mono text-xs ${wrapText ? 'whitespace-pre-wrap break-all' : 'truncate'} min-w-0 flex-1 ${colorScheme.primary}`}>
{value} {value}
</span> </span>
{secondary && ( {secondary && (
<span className={`text-[11px] ${colorScheme.secondary} italic flex-shrink-0`}> <span className={`text-[11px] ${colorScheme.secondary} flex-shrink-0 italic`}>
{secondary} {secondary}
</span> </span>
)} )}

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { CollapsibleSection } from './CollapsibleSection';
import type { SubagentChildTool } from '../../types/types'; import type { SubagentChildTool } from '../../types/types';
import { CollapsibleSection } from './CollapsibleSection';
interface SubagentContainerProps { interface SubagentContainerProps {
toolInput: unknown; toolInput: unknown;
@@ -57,7 +57,7 @@ export const SubagentContainer: React.FC<SubagentContainerProps> = ({
const title = `Subagent / ${subagentType}: ${description}`; const title = `Subagent / ${subagentType}: ${description}`;
return ( return (
<div className="border-l-2 border-l-purple-500 dark:border-l-purple-400 pl-3 py-0.5 my-1"> <div className="my-1 border-l-2 border-l-purple-500 py-0.5 pl-3 dark:border-l-purple-400">
<CollapsibleSection <CollapsibleSection
title={title} title={title}
toolName="Task" toolName="Task"
@@ -65,21 +65,21 @@ export const SubagentContainer: React.FC<SubagentContainerProps> = ({
> >
{/* Prompt/request to the subagent */} {/* Prompt/request to the subagent */}
{prompt && ( {prompt && (
<div className="text-xs text-gray-600 dark:text-gray-400 mb-2 whitespace-pre-wrap break-words line-clamp-4"> <div className="mb-2 line-clamp-4 whitespace-pre-wrap break-words text-xs text-gray-600 dark:text-gray-400">
{prompt} {prompt}
</div> </div>
)} )}
{/* Current tool indicator (while running) */} {/* Current tool indicator (while running) */}
{currentTool && !isComplete && ( {currentTool && !isComplete && (
<div className="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400 mt-1"> <div className="mt-1 flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400">
<span className="animate-pulse w-1.5 h-1.5 rounded-full bg-purple-500 dark:bg-purple-400 flex-shrink-0" /> <span className="h-1.5 w-1.5 flex-shrink-0 animate-pulse rounded-full bg-purple-500 dark:bg-purple-400" />
<span className="text-gray-400 dark:text-gray-500">Currently:</span> <span className="text-gray-400 dark:text-gray-500">Currently:</span>
<span className="font-medium text-gray-600 dark:text-gray-300">{currentTool.toolName}</span> <span className="font-medium text-gray-600 dark:text-gray-300">{currentTool.toolName}</span>
{getCompactToolDisplay(currentTool.toolName, currentTool.toolInput) && ( {getCompactToolDisplay(currentTool.toolName, currentTool.toolInput) && (
<> <>
<span className="text-gray-300 dark:text-gray-600">/</span> <span className="text-gray-300 dark:text-gray-600">/</span>
<span className="font-mono truncate text-gray-500 dark:text-gray-400"> <span className="truncate font-mono text-gray-500 dark:text-gray-400">
{getCompactToolDisplay(currentTool.toolName, currentTool.toolInput)} {getCompactToolDisplay(currentTool.toolName, currentTool.toolInput)}
</span> </span>
</> </>
@@ -89,8 +89,8 @@ export const SubagentContainer: React.FC<SubagentContainerProps> = ({
{/* Completion status */} {/* Completion status */}
{isComplete && ( {isComplete && (
<div className="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400 mt-1"> <div className="mt-1 flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400">
<svg className="w-3 h-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-3 w-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg> </svg>
<span>Completed ({childTools.length} {childTools.length === 1 ? 'tool' : 'tools'})</span> <span>Completed ({childTools.length} {childTools.length === 1 ? 'tool' : 'tools'})</span>
@@ -99,10 +99,10 @@ export const SubagentContainer: React.FC<SubagentContainerProps> = ({
{/* Tool history (collapsed) */} {/* Tool history (collapsed) */}
{childTools.length > 0 && ( {childTools.length > 0 && (
<details className="mt-2 group/history"> <details className="group/history mt-2">
<summary className="cursor-pointer text-[11px] text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 flex items-center gap-1"> <summary className="flex cursor-pointer items-center gap-1 text-[11px] text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300">
<svg <svg
className="w-2.5 h-2.5 transition-transform duration-150 group-open/history:rotate-90 flex-shrink-0" className="h-2.5 w-2.5 flex-shrink-0 transition-transform duration-150 group-open/history:rotate-90"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -111,18 +111,18 @@ export const SubagentContainer: React.FC<SubagentContainerProps> = ({
</svg> </svg>
<span>View tool history ({childTools.length})</span> <span>View tool history ({childTools.length})</span>
</summary> </summary>
<div className="mt-1 pl-3 border-l border-gray-200 dark:border-gray-700 space-y-0.5"> <div className="mt-1 space-y-0.5 border-l border-gray-200 pl-3 dark:border-gray-700">
{childTools.map((child, index) => ( {childTools.map((child, index) => (
<div key={child.toolId} className="flex items-center gap-1.5 text-[11px] text-gray-500 dark:text-gray-400"> <div key={child.toolId} className="flex items-center gap-1.5 text-[11px] text-gray-500 dark:text-gray-400">
<span className="text-gray-400 dark:text-gray-500 w-4 text-right flex-shrink-0">{index + 1}.</span> <span className="w-4 flex-shrink-0 text-right text-gray-400 dark:text-gray-500">{index + 1}.</span>
<span className="font-medium">{child.toolName}</span> <span className="font-medium">{child.toolName}</span>
{getCompactToolDisplay(child.toolName, child.toolInput) && ( {getCompactToolDisplay(child.toolName, child.toolInput) && (
<span className="font-mono truncate text-gray-400 dark:text-gray-500"> <span className="truncate font-mono text-gray-400 dark:text-gray-500">
{getCompactToolDisplay(child.toolName, child.toolInput)} {getCompactToolDisplay(child.toolName, child.toolInput)}
</span> </span>
)} )}
{child.toolResult?.isError && ( {child.toolResult?.isError && (
<span className="text-red-500 flex-shrink-0">(error)</span> <span className="flex-shrink-0 text-red-500">(error)</span>
)} )}
</div> </div>
))} ))}
@@ -163,11 +163,11 @@ export const SubagentContainer: React.FC<SubagentContainerProps> = ({
} }
return typeof content === 'string' ? ( return typeof content === 'string' ? (
<div className="whitespace-pre-wrap break-words line-clamp-6"> <div className="line-clamp-6 whitespace-pre-wrap break-words">
{content} {content}
</div> </div>
) : content ? ( ) : content ? (
<pre className="whitespace-pre-wrap break-words line-clamp-6 font-mono text-[11px]"> <pre className="line-clamp-6 whitespace-pre-wrap break-words font-mono text-[11px]">
{JSON.stringify(content, null, 2)} {JSON.stringify(content, null, 2)}
</pre> </pre>
) : null; ) : null;

View File

@@ -38,44 +38,44 @@ export const ToolDiffViewer: React.FC<ToolDiffViewerProps> = ({
); );
return ( return (
<div className="border border-gray-200/60 dark:border-gray-700/50 rounded overflow-hidden"> <div className="overflow-hidden rounded border border-gray-200/60 dark:border-gray-700/50">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-2.5 py-1 bg-gray-50/80 dark:bg-gray-800/40 border-b border-gray-200/60 dark:border-gray-700/50"> <div className="flex items-center justify-between border-b border-gray-200/60 bg-gray-50/80 px-2.5 py-1 dark:border-gray-700/50 dark:bg-gray-800/40">
{onFileClick ? ( {onFileClick ? (
<button <button
onClick={onFileClick} onClick={onFileClick}
className="text-[11px] font-mono text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 truncate cursor-pointer transition-colors" className="cursor-pointer truncate font-mono text-[11px] text-blue-600 transition-colors hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
> >
{filePath} {filePath}
</button> </button>
) : ( ) : (
<span className="text-[11px] font-mono text-gray-600 dark:text-gray-400 truncate"> <span className="truncate font-mono text-[11px] text-gray-600 dark:text-gray-400">
{filePath} {filePath}
</span> </span>
)} )}
<span className={`text-[10px] font-medium px-1.5 py-px rounded ${badgeClasses} flex-shrink-0 ml-2`}> <span className={`rounded px-1.5 py-px text-[10px] font-medium ${badgeClasses} ml-2 flex-shrink-0`}>
{badge} {badge}
</span> </span>
</div> </div>
{/* Diff lines */} {/* Diff lines */}
<div className="text-[11px] font-mono leading-[18px]"> <div className="font-mono text-[11px] leading-[18px]">
{diffLines.map((diffLine, i) => ( {diffLines.map((diffLine, i) => (
<div key={i} className="flex"> <div key={i} className="flex">
<span <span
className={`w-6 text-center select-none flex-shrink-0 ${ className={`w-6 flex-shrink-0 select-none text-center ${
diffLine.type === 'removed' diffLine.type === 'removed'
? 'bg-red-50 dark:bg-red-950/30 text-red-400 dark:text-red-500' ? 'bg-red-50 text-red-400 dark:bg-red-950/30 dark:text-red-500'
: 'bg-green-50 dark:bg-green-950/30 text-green-400 dark:text-green-500' : 'bg-green-50 text-green-400 dark:bg-green-950/30 dark:text-green-500'
}`} }`}
> >
{diffLine.type === 'removed' ? '-' : '+'} {diffLine.type === 'removed' ? '-' : '+'}
</span> </span>
<span <span
className={`px-2 flex-1 whitespace-pre-wrap ${ className={`flex-1 whitespace-pre-wrap px-2 ${
diffLine.type === 'removed' diffLine.type === 'removed'
? 'bg-red-50/50 dark:bg-red-950/20 text-red-800 dark:text-red-200' ? 'bg-red-50/50 text-red-800 dark:bg-red-950/20 dark:text-red-200'
: 'bg-green-50/50 dark:bg-green-950/20 text-green-800 dark:text-green-200' : 'bg-green-50/50 text-green-800 dark:bg-green-950/20 dark:text-green-200'
}`} }`}
> >
{diffLine.content} {diffLine.content}

View File

@@ -1,15 +1,14 @@
import React, { useCallback, useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import { QuickSettingsPanel } from '../../quick-settings-panel';
import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ChatMessagesPane from './subcomponents/ChatMessagesPane'; import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
import ChatComposer from './subcomponents/ChatComposer'; import { QuickSettingsPanel } from '../../quick-settings-panel';
import type { ChatInterfaceProps } from '../types/types'; import type { ChatInterfaceProps, Provider } from '../types/types';
import { useChatProviderState } from '../hooks/useChatProviderState'; import { useChatProviderState } from '../hooks/useChatProviderState';
import { useChatSessionState } from '../hooks/useChatSessionState'; import { useChatSessionState } from '../hooks/useChatSessionState';
import { useChatRealtimeHandlers } from '../hooks/useChatRealtimeHandlers'; import { useChatRealtimeHandlers } from '../hooks/useChatRealtimeHandlers';
import { useChatComposerState } from '../hooks/useChatComposerState'; import { useChatComposerState } from '../hooks/useChatComposerState';
import type { Provider } from '../types/types'; import ChatMessagesPane from './subcomponents/ChatMessagesPane';
import ChatComposer from './subcomponents/ChatComposer';
type PendingViewSession = { type PendingViewSession = {
@@ -259,7 +258,7 @@ function ChatInterface({
: t('messageTypes.claude'); : t('messageTypes.claude');
return ( return (
<div className="flex items-center justify-center h-full"> <div className="flex h-full items-center justify-center">
<div className="text-center text-muted-foreground"> <div className="text-center text-muted-foreground">
<p className="text-sm"> <p className="text-sm">
{t('projectSelection.startChatWithProvider', { {t('projectSelection.startChatWithProvider', {
@@ -274,7 +273,7 @@ function ChatInterface({
return ( return (
<> <>
<div className="h-full flex flex-col"> <div className="flex h-full flex-col">
<ChatMessagesPane <ChatMessagesPane
scrollContainerRef={scrollContainerRef} scrollContainerRef={scrollContainerRef}
onWheel={handleScroll} onWheel={handleScroll}

View File

@@ -10,15 +10,15 @@ export default function AssistantThinkingIndicator({ selectedProvider }: Assista
return ( return (
<div className="chat-message assistant"> <div className="chat-message assistant">
<div className="w-full"> <div className="w-full">
<div className="flex items-center space-x-3 mb-2"> <div className="mb-2 flex items-center space-x-3">
<div className="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm flex-shrink-0 p-1 bg-transparent"> <div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-transparent p-1 text-sm text-white">
<SessionProviderLogo provider={selectedProvider} className="w-full h-full" /> <SessionProviderLogo provider={selectedProvider} className="h-full w-full" />
</div> </div>
<div className="text-sm font-medium text-gray-900 dark:text-white"> <div className="text-sm font-medium text-gray-900 dark:text-white">
{selectedProvider === 'cursor' ? 'Cursor' : selectedProvider === 'codex' ? 'Codex' : selectedProvider === 'gemini' ? 'Gemini' : 'Claude'} {selectedProvider === 'cursor' ? 'Cursor' : selectedProvider === 'codex' ? 'Codex' : selectedProvider === 'gemini' ? 'Gemini' : 'Claude'}
</div> </div>
</div> </div>
<div className="w-full text-sm text-gray-500 dark:text-gray-400 pl-3 sm:pl-0"> <div className="w-full pl-3 text-sm text-gray-500 dark:text-gray-400 sm:pl-0">
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<div className="animate-pulse">.</div> <div className="animate-pulse">.</div>
<div className="animate-pulse" style={{ animationDelay: '0.2s' }}> <div className="animate-pulse" style={{ animationDelay: '0.2s' }}>

View File

@@ -1,9 +1,3 @@
import CommandMenu from './CommandMenu';
import ClaudeStatus from './ClaudeStatus';
import MicButton from '../../../mic-button/view/MicButton';
import ImageAttachment from './ImageAttachment';
import PermissionRequestsBanner from './PermissionRequestsBanner';
import ChatInputControls from './ChatInputControls';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { import type {
ChangeEvent, ChangeEvent,
@@ -17,7 +11,13 @@ import type {
SetStateAction, SetStateAction,
TouchEvent, TouchEvent,
} from 'react'; } from 'react';
import MicButton from '../../../mic-button/view/MicButton';
import type { PendingPermissionRequest, PermissionMode, Provider } from '../../types/types'; import type { PendingPermissionRequest, PermissionMode, Provider } from '../../types/types';
import CommandMenu from './CommandMenu';
import ClaudeStatus from './ClaudeStatus';
import ImageAttachment from './ImageAttachment';
import PermissionRequestsBanner from './PermissionRequestsBanner';
import ChatInputControls from './ChatInputControls';
interface MentionableFile { interface MentionableFile {
name: string; name: string;
@@ -169,7 +169,7 @@ export default function ChatComposer({
: ''; : '';
return ( return (
<div className={`p-2 sm:p-4 md:p-4 flex-shrink-0 pb-2 sm:pb-4 md:pb-6 ${mobileFloatingClass}`}> <div className={`flex-shrink-0 p-2 pb-2 sm:p-4 sm:pb-4 md:p-4 md:pb-6 ${mobileFloatingClass}`}>
{!hasQuestionPanel && ( {!hasQuestionPanel && (
<div className="flex-1"> <div className="flex-1">
<ClaudeStatus <ClaudeStatus
@@ -181,7 +181,7 @@ export default function ChatComposer({
</div> </div>
)} )}
<div className="max-w-4xl mx-auto mb-3"> <div className="mx-auto mb-3 max-w-4xl">
<PermissionRequestsBanner <PermissionRequestsBanner
pendingPermissionRequests={pendingPermissionRequests} pendingPermissionRequests={pendingPermissionRequests}
handlePermissionDecision={handlePermissionDecision} handlePermissionDecision={handlePermissionDecision}
@@ -205,11 +205,11 @@ export default function ChatComposer({
/>} />}
</div> </div>
{!hasQuestionPanel && <form onSubmit={onSubmit as (event: FormEvent<HTMLFormElement>) => void} className="relative max-w-4xl mx-auto"> {!hasQuestionPanel && <form onSubmit={onSubmit as (event: FormEvent<HTMLFormElement>) => void} className="relative mx-auto max-w-4xl">
{isDragActive && ( {isDragActive && (
<div className="absolute inset-0 bg-primary/15 border-2 border-dashed border-primary/50 rounded-2xl flex items-center justify-center z-50"> <div className="absolute inset-0 z-50 flex items-center justify-center rounded-2xl border-2 border-dashed border-primary/50 bg-primary/15">
<div className="bg-card rounded-xl p-4 shadow-lg border border-border/30"> <div className="rounded-xl border border-border/30 bg-card p-4 shadow-lg">
<svg className="w-8 h-8 text-primary mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="mx-auto mb-2 h-8 w-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@@ -223,7 +223,7 @@ export default function ChatComposer({
)} )}
{attachedImages.length > 0 && ( {attachedImages.length > 0 && (
<div className="mb-2 p-2 bg-muted/40 rounded-xl"> <div className="mb-2 rounded-xl bg-muted/40 p-2">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{attachedImages.map((file, index) => ( {attachedImages.map((file, index) => (
<ImageAttachment <ImageAttachment
@@ -239,14 +239,14 @@ export default function ChatComposer({
)} )}
{showFileDropdown && filteredFiles.length > 0 && ( {showFileDropdown && filteredFiles.length > 0 && (
<div className="absolute bottom-full left-0 right-0 mb-2 bg-card/95 backdrop-blur-md border border-border/50 rounded-xl shadow-lg max-h-48 overflow-y-auto z-50"> <div className="absolute bottom-full left-0 right-0 z-50 mb-2 max-h-48 overflow-y-auto rounded-xl border border-border/50 bg-card/95 shadow-lg backdrop-blur-md">
{filteredFiles.map((file, index) => ( {filteredFiles.map((file, index) => (
<div <div
key={file.path} key={file.path}
className={`px-4 py-3 cursor-pointer border-b border-border/30 last:border-b-0 touch-manipulation ${ className={`cursor-pointer touch-manipulation border-b border-border/30 px-4 py-3 last:border-b-0 ${
index === selectedFileIndex index === selectedFileIndex
? 'bg-primary/8 text-primary' ? 'bg-primary/8 text-primary'
: 'hover:bg-accent/50 text-foreground' : 'text-foreground hover:bg-accent/50'
}`} }`}
onMouseDown={(event) => { onMouseDown={(event) => {
event.preventDefault(); event.preventDefault();
@@ -258,8 +258,8 @@ export default function ChatComposer({
onSelectFile(file); onSelectFile(file);
}} }}
> >
<div className="font-medium text-sm">{file.name}</div> <div className="text-sm font-medium">{file.name}</div>
<div className="text-xs text-muted-foreground font-mono">{file.path}</div> <div className="font-mono text-xs text-muted-foreground">{file.path}</div>
</div> </div>
))} ))}
</div> </div>
@@ -277,13 +277,13 @@ export default function ChatComposer({
<div <div
{...getRootProps()} {...getRootProps()}
className={`relative bg-card/80 backdrop-blur-sm rounded-2xl shadow-sm border border-border/50 focus-within:shadow-md focus-within:border-primary/30 focus-within:ring-1 focus-within:ring-primary/15 transition-all duration-200 overflow-hidden ${ className={`relative overflow-hidden rounded-2xl border border-border/50 bg-card/80 shadow-sm backdrop-blur-sm transition-all duration-200 focus-within:border-primary/30 focus-within:shadow-md focus-within:ring-1 focus-within:ring-primary/15 ${
isTextareaExpanded ? 'chat-input-expanded' : '' isTextareaExpanded ? 'chat-input-expanded' : ''
}`} }`}
> >
<input {...getInputProps()} /> <input {...getInputProps()} />
<div ref={inputHighlightRef} aria-hidden="true" className="absolute inset-0 pointer-events-none overflow-hidden rounded-2xl"> <div ref={inputHighlightRef} aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden rounded-2xl">
<div className="chat-input-placeholder block w-full pl-12 pr-20 sm:pr-40 py-1.5 sm:py-4 text-transparent text-base leading-6 whitespace-pre-wrap break-words"> <div className="chat-input-placeholder block w-full whitespace-pre-wrap break-words py-1.5 pl-12 pr-20 text-base leading-6 text-transparent sm:py-4 sm:pr-40">
{renderInputWithMentions(input)} {renderInputWithMentions(input)}
</div> </div>
</div> </div>
@@ -302,17 +302,17 @@ export default function ChatComposer({
onInput={onTextareaInput} onInput={onTextareaInput}
placeholder={placeholder} placeholder={placeholder}
disabled={isLoading} disabled={isLoading}
className="chat-input-placeholder block w-full pl-12 pr-20 sm:pr-40 py-1.5 sm:py-4 bg-transparent rounded-2xl focus:outline-none text-foreground placeholder-muted-foreground/50 disabled:opacity-50 resize-none min-h-[50px] sm:min-h-[80px] max-h-[40vh] sm:max-h-[300px] overflow-y-auto text-base leading-6 transition-all duration-200" className="chat-input-placeholder block max-h-[40vh] min-h-[50px] w-full resize-none overflow-y-auto rounded-2xl bg-transparent py-1.5 pl-12 pr-20 text-base leading-6 text-foreground placeholder-muted-foreground/50 transition-all duration-200 focus:outline-none disabled:opacity-50 sm:max-h-[300px] sm:min-h-[80px] sm:py-4 sm:pr-40"
style={{ height: '50px' }} style={{ height: '50px' }}
/> />
<button <button
type="button" type="button"
onClick={openImagePicker} onClick={openImagePicker}
className="absolute left-2 top-1/2 transform -translate-y-1/2 p-2 hover:bg-accent/60 rounded-xl transition-colors" className="absolute left-2 top-1/2 -translate-y-1/2 transform rounded-xl p-2 transition-colors hover:bg-accent/60"
title={t('input.attachImages')} title={t('input.attachImages')}
> >
<svg className="w-5 h-5 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-5 w-5 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@@ -322,8 +322,8 @@ export default function ChatComposer({
</svg> </svg>
</button> </button>
<div className="absolute right-16 sm:right-16 top-1/2 transform -translate-y-1/2" style={{ display: 'none' }}> <div className="absolute right-16 top-1/2 -translate-y-1/2 transform sm:right-16" style={{ display: 'none' }}>
<MicButton onTranscript={onTranscript} className="w-10 h-10 sm:w-10 sm:h-10" /> <MicButton onTranscript={onTranscript} className="h-10 w-10 sm:h-10 sm:w-10" />
</div> </div>
<button <button
@@ -337,15 +337,15 @@ export default function ChatComposer({
event.preventDefault(); event.preventDefault();
onSubmit(event); onSubmit(event);
}} }}
className="absolute right-2 top-1/2 transform -translate-y-1/2 w-10 h-10 sm:w-11 sm:h-11 bg-primary hover:bg-primary/90 disabled:bg-muted disabled:text-muted-foreground disabled:cursor-not-allowed rounded-xl flex items-center justify-center transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:ring-offset-1 focus:ring-offset-background" className="absolute right-2 top-1/2 flex h-10 w-10 -translate-y-1/2 transform items-center justify-center rounded-xl bg-primary transition-all duration-200 hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:ring-offset-1 focus:ring-offset-background disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground sm:h-11 sm:w-11"
> >
<svg className="w-4 h-4 sm:w-[18px] sm:h-[18px] text-primary-foreground transform rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-4 w-4 rotate-90 transform text-primary-foreground sm:h-[18px] sm:w-[18px]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg> </svg>
</button> </button>
<div <div
className={`absolute bottom-1 left-12 right-14 sm:right-40 text-xs text-muted-foreground/50 pointer-events-none hidden sm:block transition-opacity duration-200 ${ className={`pointer-events-none absolute bottom-1 left-12 right-14 hidden text-xs text-muted-foreground/50 transition-opacity duration-200 sm:right-40 sm:block ${
input.trim() ? 'opacity-0' : 'opacity-100' input.trim() ? 'opacity-0' : 'opacity-100'
}`} }`}
> >

View File

@@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { PermissionMode, Provider } from '../../types/types';
import ThinkingModeSelector from './ThinkingModeSelector'; import ThinkingModeSelector from './ThinkingModeSelector';
import TokenUsagePie from './TokenUsagePie'; import TokenUsagePie from './TokenUsagePie';
import type { PermissionMode, Provider } from '../../types/types';
interface ChatInputControlsProps { interface ChatInputControlsProps {
permissionMode: PermissionMode | string; permissionMode: PermissionMode | string;
@@ -38,24 +38,24 @@ export default function ChatInputControls({
const { t } = useTranslation('chat'); const { t } = useTranslation('chat');
return ( return (
<div className="flex items-center justify-center gap-2 sm:gap-3 flex-wrap"> <div className="flex flex-wrap items-center justify-center gap-2 sm:gap-3">
<button <button
type="button" type="button"
onClick={onModeSwitch} onClick={onModeSwitch}
className={`px-2.5 py-1 sm:px-3 sm:py-1.5 rounded-lg text-sm font-medium border transition-all duration-200 ${ className={`rounded-lg border px-2.5 py-1 text-sm font-medium transition-all duration-200 sm:px-3 sm:py-1.5 ${
permissionMode === 'default' permissionMode === 'default'
? 'bg-muted/50 text-muted-foreground border-border/60 hover:bg-muted' ? 'border-border/60 bg-muted/50 text-muted-foreground hover:bg-muted'
: permissionMode === 'acceptEdits' : permissionMode === 'acceptEdits'
? 'bg-green-50 dark:bg-green-900/15 text-green-700 dark:text-green-300 border-green-300/60 dark:border-green-600/40 hover:bg-green-100 dark:hover:bg-green-900/25' ? 'border-green-300/60 bg-green-50 text-green-700 hover:bg-green-100 dark:border-green-600/40 dark:bg-green-900/15 dark:text-green-300 dark:hover:bg-green-900/25'
: permissionMode === 'bypassPermissions' : permissionMode === 'bypassPermissions'
? 'bg-orange-50 dark:bg-orange-900/15 text-orange-700 dark:text-orange-300 border-orange-300/60 dark:border-orange-600/40 hover:bg-orange-100 dark:hover:bg-orange-900/25' ? 'border-orange-300/60 bg-orange-50 text-orange-700 hover:bg-orange-100 dark:border-orange-600/40 dark:bg-orange-900/15 dark:text-orange-300 dark:hover:bg-orange-900/25'
: 'bg-primary/5 text-primary border-primary/20 hover:bg-primary/10' : 'border-primary/20 bg-primary/5 text-primary hover:bg-primary/10'
}`} }`}
title={t('input.clickToChangeMode')} title={t('input.clickToChangeMode')}
> >
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<div <div
className={`w-1.5 h-1.5 rounded-full ${ className={`h-1.5 w-1.5 rounded-full ${
permissionMode === 'default' permissionMode === 'default'
? 'bg-muted-foreground' ? 'bg-muted-foreground'
: permissionMode === 'acceptEdits' : permissionMode === 'acceptEdits'
@@ -83,10 +83,10 @@ export default function ChatInputControls({
<button <button
type="button" type="button"
onClick={onToggleCommandMenu} onClick={onToggleCommandMenu}
className="relative w-7 h-7 sm:w-8 sm:h-8 text-muted-foreground hover:text-foreground rounded-lg flex items-center justify-center transition-colors hover:bg-accent/60" className="relative flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground sm:h-8 sm:w-8"
title={t('input.showAllCommands')} title={t('input.showAllCommands')}
> >
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-4 w-4 sm:h-5 sm:w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@@ -96,7 +96,7 @@ export default function ChatInputControls({
</svg> </svg>
{slashCommandsCount > 0 && ( {slashCommandsCount > 0 && (
<span <span
className="absolute -top-1 -right-1 bg-primary text-primary-foreground text-[10px] font-bold rounded-full w-4 h-4 sm:w-5 sm:h-5 flex items-center justify-center" className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[10px] font-bold text-primary-foreground sm:h-5 sm:w-5"
> >
{slashCommandsCount} {slashCommandsCount}
</span> </span>
@@ -107,11 +107,11 @@ export default function ChatInputControls({
<button <button
type="button" type="button"
onClick={onClearInput} onClick={onClearInput}
className="w-7 h-7 sm:w-8 sm:h-8 bg-card hover:bg-accent/60 border border-border/50 rounded-lg flex items-center justify-center transition-all duration-200 group shadow-sm" className="group flex h-7 w-7 items-center justify-center rounded-lg border border-border/50 bg-card shadow-sm transition-all duration-200 hover:bg-accent/60 sm:h-8 sm:w-8"
title={t('input.clearInput', { defaultValue: 'Clear input' })} title={t('input.clearInput', { defaultValue: 'Clear input' })}
> >
<svg <svg
className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-muted-foreground group-hover:text-foreground transition-colors" className="h-3.5 w-3.5 text-muted-foreground transition-colors group-hover:text-foreground sm:h-4 sm:w-4"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -124,10 +124,10 @@ export default function ChatInputControls({
{isUserScrolledUp && hasMessages && ( {isUserScrolledUp && hasMessages && (
<button <button
onClick={onScrollToBottom} onClick={onScrollToBottom}
className="w-7 h-7 sm:w-8 sm:h-8 bg-primary hover:bg-primary/90 text-primary-foreground rounded-lg shadow-sm flex items-center justify-center transition-all duration-200 hover:scale-105" className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-primary-foreground shadow-sm transition-all duration-200 hover:scale-105 hover:bg-primary/90 sm:h-8 sm:w-8"
title={t('input.scrollToBottom', { defaultValue: 'Scroll to bottom' })} title={t('input.scrollToBottom', { defaultValue: 'Scroll to bottom' })}
> >
<svg className="w-3.5 h-3.5 sm:w-4 sm:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-3.5 w-3.5 sm:h-4 sm:w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg> </svg>
</button> </button>

View File

@@ -1,13 +1,12 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import type { Dispatch, RefObject, SetStateAction } from 'react'; import type { Dispatch, RefObject, SetStateAction } from 'react';
import MessageComponent from './MessageComponent';
import ProviderSelectionEmptyState from './ProviderSelectionEmptyState';
import type { ChatMessage } from '../../types/types'; import type { ChatMessage } from '../../types/types';
import type { Project, ProjectSession, SessionProvider } from '../../../../types/app'; import type { Project, ProjectSession, SessionProvider } from '../../../../types/app';
import AssistantThinkingIndicator from './AssistantThinkingIndicator';
import { getIntrinsicMessageKey } from '../../utils/messageKeys'; import { getIntrinsicMessageKey } from '../../utils/messageKeys';
import MessageComponent from './MessageComponent';
import ProviderSelectionEmptyState from './ProviderSelectionEmptyState';
import AssistantThinkingIndicator from './AssistantThinkingIndicator';
interface ChatMessagesPaneProps { interface ChatMessagesPaneProps {
scrollContainerRef: RefObject<HTMLDivElement>; scrollContainerRef: RefObject<HTMLDivElement>;
@@ -134,12 +133,12 @@ export default function ChatMessagesPane({
ref={scrollContainerRef} ref={scrollContainerRef}
onWheel={onWheel} onWheel={onWheel}
onTouchMove={onTouchMove} onTouchMove={onTouchMove}
className="flex-1 overflow-y-auto overflow-x-hidden px-0 py-3 sm:p-4 space-y-3 sm:space-y-4 relative" className="relative flex-1 space-y-3 overflow-y-auto overflow-x-hidden px-0 py-3 sm:space-y-4 sm:p-4"
> >
{isLoadingSessionMessages && chatMessages.length === 0 ? ( {isLoadingSessionMessages && chatMessages.length === 0 ? (
<div className="text-center text-gray-500 dark:text-gray-400 mt-8"> <div className="mt-8 text-center text-gray-500 dark:text-gray-400">
<div className="flex items-center justify-center space-x-2"> <div className="flex items-center justify-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-400" /> <div className="h-4 w-4 animate-spin rounded-full border-b-2 border-gray-400" />
<p>{t('session.loading.sessionMessages')}</p> <p>{t('session.loading.sessionMessages')}</p>
</div> </div>
</div> </div>
@@ -167,9 +166,9 @@ export default function ChatMessagesPane({
<> <>
{/* Loading indicator for older messages (hide when load-all is active) */} {/* Loading indicator for older messages (hide when load-all is active) */}
{isLoadingMoreMessages && !isLoadingAllMessages && !allMessagesLoaded && ( {isLoadingMoreMessages && !isLoadingAllMessages && !allMessagesLoaded && (
<div className="text-center text-gray-500 dark:text-gray-400 py-3"> <div className="py-3 text-center text-gray-500 dark:text-gray-400">
<div className="flex items-center justify-center space-x-2"> <div className="flex items-center justify-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-400" /> <div className="h-4 w-4 animate-spin rounded-full border-b-2 border-gray-400" />
<p className="text-sm">{t('session.loading.olderMessages')}</p> <p className="text-sm">{t('session.loading.olderMessages')}</p>
</div> </div>
</div> </div>
@@ -177,7 +176,7 @@ export default function ChatMessagesPane({
{/* Indicator showing there are more messages to load (hide when all loaded) */} {/* Indicator showing there are more messages to load (hide when all loaded) */}
{hasMoreMessages && !isLoadingMoreMessages && !allMessagesLoaded && ( {hasMoreMessages && !isLoadingMoreMessages && !allMessagesLoaded && (
<div className="text-center text-gray-500 dark:text-gray-400 text-sm py-2 border-b border-gray-200 dark:border-gray-700"> <div className="border-b border-gray-200 py-2 text-center text-sm text-gray-500 dark:border-gray-700 dark:text-gray-400">
{totalMessages > 0 && ( {totalMessages > 0 && (
<span> <span>
{t('session.messages.showingOf', { shown: sessionMessagesCount, total: totalMessages })}{' '} {t('session.messages.showingOf', { shown: sessionMessagesCount, total: totalMessages })}{' '}
@@ -189,22 +188,22 @@ export default function ChatMessagesPane({
{/* Floating "Load all messages" overlay */} {/* Floating "Load all messages" overlay */}
{(showLoadAllOverlay || isLoadingAllMessages || loadAllJustFinished) && ( {(showLoadAllOverlay || isLoadingAllMessages || loadAllJustFinished) && (
<div className="sticky top-2 z-20 flex justify-center pointer-events-none"> <div className="pointer-events-none sticky top-2 z-20 flex justify-center">
{loadAllJustFinished ? ( {loadAllJustFinished ? (
<div className="px-4 py-1.5 text-xs font-medium text-white bg-green-600 dark:bg-green-500 rounded-full shadow-lg flex items-center space-x-2"> <div className="flex items-center space-x-2 rounded-full bg-green-600 px-4 py-1.5 text-xs font-medium text-white shadow-lg dark:bg-green-500">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg> </svg>
<span>{t('session.messages.allLoaded')}</span> <span>{t('session.messages.allLoaded')}</span>
</div> </div>
) : ( ) : (
<button <button
className="pointer-events-auto px-4 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 rounded-full shadow-lg transition-all duration-200 hover:scale-105 disabled:opacity-75 disabled:cursor-wait flex items-center space-x-2" className="pointer-events-auto flex items-center space-x-2 rounded-full bg-blue-600 px-4 py-1.5 text-xs font-medium text-white shadow-lg transition-all duration-200 hover:scale-105 hover:bg-blue-700 disabled:cursor-wait disabled:opacity-75 dark:bg-blue-500 dark:hover:bg-blue-600"
onClick={loadAllMessages} onClick={loadAllMessages}
disabled={isLoadingAllMessages} disabled={isLoadingAllMessages}
> >
{isLoadingAllMessages && ( {isLoadingAllMessages && (
<div className="animate-spin rounded-full h-3 w-3 border-2 border-white/30 border-t-white" /> <div className="h-3 w-3 animate-spin rounded-full border-2 border-white/30 border-t-white" />
)} )}
<span> <span>
{isLoadingAllMessages {isLoadingAllMessages
@@ -219,21 +218,21 @@ export default function ChatMessagesPane({
{/* Performance warning when all messages are loaded */} {/* Performance warning when all messages are loaded */}
{allMessagesLoaded && ( {allMessagesLoaded && (
<div className="text-center text-amber-600 dark:text-amber-400 text-xs py-1.5 bg-amber-50 dark:bg-amber-900/20 border-b border-amber-200 dark:border-amber-800"> <div className="border-b border-amber-200 bg-amber-50 py-1.5 text-center text-xs text-amber-600 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-400">
{t('session.messages.perfWarning')} {t('session.messages.perfWarning')}
</div> </div>
)} )}
{/* Legacy message count indicator (for non-paginated view) */} {/* Legacy message count indicator (for non-paginated view) */}
{!hasMoreMessages && chatMessages.length > visibleMessageCount && ( {!hasMoreMessages && chatMessages.length > visibleMessageCount && (
<div className="text-center text-gray-500 dark:text-gray-400 text-sm py-2 border-b border-gray-200 dark:border-gray-700"> <div className="border-b border-gray-200 py-2 text-center text-sm text-gray-500 dark:border-gray-700 dark:text-gray-400">
{t('session.messages.showingLast', { count: visibleMessageCount, total: chatMessages.length })} | {t('session.messages.showingLast', { count: visibleMessageCount, total: chatMessages.length })} |
<button className="ml-1 text-blue-600 hover:text-blue-700 underline" onClick={loadEarlierMessages}> <button className="ml-1 text-blue-600 underline hover:text-blue-700" onClick={loadEarlierMessages}>
{t('session.messages.loadEarlier')} {t('session.messages.loadEarlier')}
</button> </button>
{' | '} {' | '}
<button <button
className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline" className="text-blue-600 underline hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
onClick={loadAllMessages} onClick={loadAllMessages}
> >
{t('session.messages.loadAll')} {t('session.messages.loadAll')}

View File

@@ -109,8 +109,8 @@ export default function ClaudeStatus({
: t('claudeStatus.elapsed.startingNow', { defaultValue: 'Starting now' }); : t('claudeStatus.elapsed.startingNow', { defaultValue: 'Starting now' });
return ( return (
<div className="w-full mb-3 sm:mb-6 animate-in slide-in-from-bottom duration-300"> <div className="animate-in slide-in-from-bottom mb-3 w-full duration-300 sm:mb-6">
<div className="relative max-w-4xl mx-auto overflow-hidden rounded-2xl border border-border/70 bg-card/90 shadow-md backdrop-blur-md"> <div className="relative mx-auto max-w-4xl overflow-hidden rounded-2xl border border-border/70 bg-card/90 shadow-md backdrop-blur-md">
<div className="pointer-events-none absolute inset-0 bg-gradient-to-r from-primary/10 via-transparent to-sky-500/10 dark:from-primary/20 dark:to-sky-400/20" /> <div className="pointer-events-none absolute inset-0 bg-gradient-to-r from-primary/10 via-transparent to-sky-500/10 dark:from-primary/20 dark:to-sky-400/20" />
<div className="relative px-3 py-3 sm:px-4 sm:py-3.5"> <div className="relative px-3 py-3 sm:px-4 sm:py-3.5">
@@ -160,7 +160,7 @@ export default function ClaudeStatus({
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-[11px] text-muted-foreground sm:text-xs"> <div className="mt-1 flex flex-wrap items-center gap-1.5 text-[11px] text-muted-foreground sm:text-xs">
<span <span
aria-hidden="true" aria-hidden="true"
className="inline-flex items-center -ml-2 rounded-full border border-border/70 bg-background/60 px-2 py-0.5" className="-ml-2 inline-flex items-center rounded-full border border-border/70 bg-background/60 px-2 py-0.5"
> >
{elapsedLabel} {elapsedLabel}
</span> </span>
@@ -173,7 +173,7 @@ export default function ClaudeStatus({
<button <button
type="button" type="button"
onClick={onAbort} onClick={onAbort}
className="inline-flex w-full items-center justify-center gap-2 rounded-xl bg-destructive px-3.5 py-2 text-sm font-semibold text-destructive-foreground shadow-sm ring-1 ring-destructive/40 transition-opacity hover:opacity-95 active:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/70 sm:w-auto" className="inline-flex w-full items-center justify-center gap-2 rounded-xl bg-destructive px-3.5 py-2 text-sm font-semibold text-destructive-foreground shadow-sm ring-1 ring-destructive/40 transition-opacity hover:opacity-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/70 active:opacity-90 sm:w-auto"
> >
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />

View File

@@ -17,16 +17,16 @@ const ImageAttachment = ({ file, onRemove, uploadProgress, error }: ImageAttachm
}, [file]); }, [file]);
return ( return (
<div className="relative group"> <div className="group relative">
<img src={preview} alt={file.name} className="w-20 h-20 object-cover rounded" /> <img src={preview} alt={file.name} className="h-20 w-20 rounded object-cover" />
{uploadProgress !== undefined && uploadProgress < 100 && ( {uploadProgress !== undefined && uploadProgress < 100 && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center bg-black/50">
<div className="text-white text-xs">{uploadProgress}%</div> <div className="text-xs text-white">{uploadProgress}%</div>
</div> </div>
)} )}
{error && ( {error && (
<div className="absolute inset-0 bg-red-500/50 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center bg-red-500/50">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</div> </div>
@@ -34,10 +34,10 @@ const ImageAttachment = ({ file, onRemove, uploadProgress, error }: ImageAttachm
<button <button
type="button" type="button"
onClick={onRemove} onClick={onRemove}
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 focus:opacity-100 transition-opacity" className="absolute -right-2 -top-2 rounded-full bg-red-500 p-1 text-white opacity-100 transition-opacity focus:opacity-100 sm:opacity-0 sm:group-hover:opacity-100"
aria-label="Remove image" aria-label="Remove image"
> >
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>

View File

@@ -32,7 +32,7 @@ const CodeBlock = ({ node, inline, className, children, ...props }: CodeBlockPro
if (shouldInline) { if (shouldInline) {
return ( return (
<code <code
className={`font-mono text-[0.9em] px-1.5 py-0.5 rounded-md bg-gray-100 text-gray-900 border border-gray-200 dark:bg-gray-800/60 dark:text-gray-100 dark:border-gray-700 whitespace-pre-wrap break-words ${className || '' className={`whitespace-pre-wrap break-words rounded-md border border-gray-200 bg-gray-100 px-1.5 py-0.5 font-mono text-[0.9em] text-gray-900 dark:border-gray-700 dark:bg-gray-800/60 dark:text-gray-100 ${className || ''
}`} }`}
{...props} {...props}
> >
@@ -45,9 +45,9 @@ const CodeBlock = ({ node, inline, className, children, ...props }: CodeBlockPro
const language = match ? match[1] : 'text'; const language = match ? match[1] : 'text';
return ( return (
<div className="relative group my-2"> <div className="group relative my-2">
{language && language !== 'text' && ( {language && language !== 'text' && (
<div className="absolute top-2 left-3 z-10 text-xs text-gray-400 font-medium uppercase">{language}</div> <div className="absolute left-3 top-2 z-10 text-xs font-medium uppercase text-gray-400">{language}</div>
)} )}
<button <button
@@ -60,13 +60,13 @@ const CodeBlock = ({ node, inline, className, children, ...props }: CodeBlockPro
} }
}) })
} }
className="absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 focus:opacity-100 active:opacity-100 transition-opacity text-xs px-2 py-1 rounded-md bg-gray-700/80 hover:bg-gray-700 text-white border border-gray-600" className="absolute right-2 top-2 z-10 rounded-md border border-gray-600 bg-gray-700/80 px-2 py-1 text-xs text-white opacity-0 transition-opacity hover:bg-gray-700 focus:opacity-100 active:opacity-100 group-hover:opacity-100"
title={copied ? t('codeBlock.copied') : t('codeBlock.copyCode')} title={copied ? t('codeBlock.copied') : t('codeBlock.copyCode')}
aria-label={copied ? t('codeBlock.copied') : t('codeBlock.copyCode')} aria-label={copied ? t('codeBlock.copied') : t('codeBlock.copyCode')}
> >
{copied ? ( {copied ? (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<svg className="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor"> <svg className="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
<path <path
fillRule="evenodd" fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
@@ -78,7 +78,7 @@ const CodeBlock = ({ node, inline, className, children, ...props }: CodeBlockPro
) : ( ) : (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<svg <svg
className="w-3.5 h-3.5" className="h-3.5 w-3.5"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -119,27 +119,27 @@ const CodeBlock = ({ node, inline, className, children, ...props }: CodeBlockPro
const markdownComponents = { const markdownComponents = {
code: CodeBlock, code: CodeBlock,
blockquote: ({ children }: { children?: React.ReactNode }) => ( blockquote: ({ children }: { children?: React.ReactNode }) => (
<blockquote className="border-l-4 border-gray-300 dark:border-gray-600 pl-4 italic text-gray-600 dark:text-gray-400 my-2"> <blockquote className="my-2 border-l-4 border-gray-300 pl-4 italic text-gray-600 dark:border-gray-600 dark:text-gray-400">
{children} {children}
</blockquote> </blockquote>
), ),
a: ({ href, children }: { href?: string; children?: React.ReactNode }) => ( a: ({ href, children }: { href?: string; children?: React.ReactNode }) => (
<a href={href} className="text-blue-600 dark:text-blue-400 hover:underline" target="_blank" rel="noopener noreferrer"> <a href={href} className="text-blue-600 hover:underline dark:text-blue-400" target="_blank" rel="noopener noreferrer">
{children} {children}
</a> </a>
), ),
p: ({ children }: { children?: React.ReactNode }) => <div className="mb-2 last:mb-0">{children}</div>, p: ({ children }: { children?: React.ReactNode }) => <div className="mb-2 last:mb-0">{children}</div>,
table: ({ children }: { children?: React.ReactNode }) => ( table: ({ children }: { children?: React.ReactNode }) => (
<div className="overflow-x-auto my-2"> <div className="my-2 overflow-x-auto">
<table className="min-w-full border-collapse border border-gray-200 dark:border-gray-700">{children}</table> <table className="min-w-full border-collapse border border-gray-200 dark:border-gray-700">{children}</table>
</div> </div>
), ),
thead: ({ children }: { children?: React.ReactNode }) => <thead className="bg-gray-50 dark:bg-gray-800">{children}</thead>, thead: ({ children }: { children?: React.ReactNode }) => <thead className="bg-gray-50 dark:bg-gray-800">{children}</thead>,
th: ({ children }: { children?: React.ReactNode }) => ( th: ({ children }: { children?: React.ReactNode }) => (
<th className="px-3 py-2 text-left text-sm font-semibold border border-gray-200 dark:border-gray-700">{children}</th> <th className="border border-gray-200 px-3 py-2 text-left text-sm font-semibold dark:border-gray-700">{children}</th>
), ),
td: ({ children }: { children?: React.ReactNode }) => ( td: ({ children }: { children?: React.ReactNode }) => (
<td className="px-3 py-2 align-top text-sm border border-gray-200 dark:border-gray-700">{children}</td> <td className="border border-gray-200 px-3 py-2 align-top text-sm dark:border-gray-700">{children}</td>
), ),
}; };

View File

@@ -7,12 +7,12 @@ import type {
PermissionGrantResult, PermissionGrantResult,
Provider, Provider,
} from '../../types/types'; } from '../../types/types';
import { Markdown } from './Markdown';
import { formatUsageLimitText } from '../../utils/chatFormatting'; import { formatUsageLimitText } from '../../utils/chatFormatting';
import { getClaudePermissionSuggestion } from '../../utils/chatPermissions'; import { getClaudePermissionSuggestion } from '../../utils/chatPermissions';
import { copyTextToClipboard } from '../../../../utils/clipboard'; import { copyTextToClipboard } from '../../../../utils/clipboard';
import type { Project } from '../../../../types/app'; import type { Project } from '../../../../types/app';
import { ToolRenderer, shouldHideToolResult } from '../../tools'; import { ToolRenderer, shouldHideToolResult } from '../../tools';
import { Markdown } from './Markdown';
type DiffLine = { type DiffLine = {
type: string; type: string;
@@ -100,9 +100,9 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
> >
{message.type === 'user' ? ( {message.type === 'user' ? (
/* User message bubble on the right */ /* User message bubble on the right */
<div className="flex items-end space-x-0 sm:space-x-3 w-full sm:w-auto sm:max-w-[85%] md:max-w-md lg:max-w-lg xl:max-w-xl"> <div className="flex w-full items-end space-x-0 sm:w-auto sm:max-w-[85%] sm:space-x-3 md:max-w-md lg:max-w-lg xl:max-w-xl">
<div className="bg-blue-600 text-white rounded-2xl rounded-br-md px-3 sm:px-4 py-2 shadow-sm flex-1 sm:flex-initial group"> <div className="group flex-1 rounded-2xl rounded-br-md bg-blue-600 px-3 py-2 text-white shadow-sm sm:flex-initial sm:px-4">
<div className="text-sm whitespace-pre-wrap break-words"> <div className="whitespace-pre-wrap break-words text-sm">
{message.content} {message.content}
</div> </div>
{message.images && message.images.length > 0 && ( {message.images && message.images.length > 0 && (
@@ -112,13 +112,13 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
key={img.name || idx} key={img.name || idx}
src={img.data} src={img.data}
alt={img.name} alt={img.name}
className="rounded-lg max-w-full h-auto cursor-pointer hover:opacity-90 transition-opacity" className="h-auto max-w-full cursor-pointer rounded-lg transition-opacity hover:opacity-90"
onClick={() => window.open(img.data, '_blank')} onClick={() => window.open(img.data, '_blank')}
/> />
))} ))}
</div> </div>
)} )}
<div className="flex items-center justify-end gap-1 mt-1 text-xs text-blue-100"> <div className="mt-1 flex items-center justify-end gap-1 text-xs text-blue-100">
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
@@ -134,7 +134,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
aria-label={messageCopied ? t('copyMessage.copied') : t('copyMessage.copy')} aria-label={messageCopied ? t('copyMessage.copied') : t('copyMessage.copy')}
> >
{messageCopied ? ( {messageCopied ? (
<svg className="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor"> <svg className="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
<path <path
fillRule="evenodd" fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
@@ -143,7 +143,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
</svg> </svg>
) : ( ) : (
<svg <svg
className="w-3.5 h-3.5" className="h-3.5 w-3.5"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -160,7 +160,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
</div> </div>
</div> </div>
{!isGrouped && ( {!isGrouped && (
<div className="hidden sm:flex w-8 h-8 bg-blue-600 rounded-full items-center justify-center text-white text-sm flex-shrink-0"> <div className="hidden h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-blue-600 text-sm text-white sm:flex">
U U
</div> </div>
)} )}
@@ -169,7 +169,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
/* Compact task notification on the left */ /* Compact task notification on the left */
<div className="w-full"> <div className="w-full">
<div className="flex items-center gap-2 py-0.5"> <div className="flex items-center gap-2 py-0.5">
<span className={`inline-block w-1.5 h-1.5 rounded-full flex-shrink-0 ${message.taskStatus === 'completed' ? 'bg-green-400 dark:bg-green-500' : 'bg-amber-400 dark:bg-amber-500'}`} /> <span className={`inline-block h-1.5 w-1.5 flex-shrink-0 rounded-full ${message.taskStatus === 'completed' ? 'bg-green-400 dark:bg-green-500' : 'bg-amber-400 dark:bg-amber-500'}`} />
<span className="text-xs text-gray-500 dark:text-gray-400">{message.content}</span> <span className="text-xs text-gray-500 dark:text-gray-400">{message.content}</span>
</div> </div>
</div> </div>
@@ -177,18 +177,18 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
/* Claude/Error/Tool messages on the left */ /* Claude/Error/Tool messages on the left */
<div className="w-full"> <div className="w-full">
{!isGrouped && ( {!isGrouped && (
<div className="flex items-center space-x-3 mb-2"> <div className="mb-2 flex items-center space-x-3">
{message.type === 'error' ? ( {message.type === 'error' ? (
<div className="w-8 h-8 bg-red-600 rounded-full flex items-center justify-center text-white text-sm flex-shrink-0"> <div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-red-600 text-sm text-white">
! !
</div> </div>
) : message.type === 'tool' ? ( ) : message.type === 'tool' ? (
<div className="w-8 h-8 bg-gray-600 dark:bg-gray-700 rounded-full flex items-center justify-center text-white text-sm flex-shrink-0"> <div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-gray-600 text-sm text-white dark:bg-gray-700">
🔧 🔧
</div> </div>
) : ( ) : (
<div className="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm flex-shrink-0 p-1"> <div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full p-1 text-sm text-white">
<SessionProviderLogo provider={provider} className="w-full h-full" /> <SessionProviderLogo provider={provider} className="h-full w-full" />
</div> </div>
)} )}
<div className="text-sm font-medium text-gray-900 dark:text-white"> <div className="text-sm font-medium text-gray-900 dark:text-white">
@@ -233,20 +233,20 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
// Error results - red error box with content // Error results - red error box with content
<div <div
id={`tool-result-${message.toolId}`} id={`tool-result-${message.toolId}`}
className="relative mt-2 p-3 rounded border scroll-mt-4 bg-red-50/50 dark:bg-red-950/10 border-red-200/60 dark:border-red-800/40" className="relative mt-2 scroll-mt-4 rounded border border-red-200/60 bg-red-50/50 p-3 dark:border-red-800/40 dark:bg-red-950/10"
> >
<div className="relative flex items-center gap-1.5 mb-2"> <div className="relative mb-2 flex items-center gap-1.5">
<svg className="w-4 h-4 text-red-500 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-4 w-4 text-red-500 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
<span className="text-xs font-medium text-red-700 dark:text-red-300">{t('messageTypes.error')}</span> <span className="text-xs font-medium text-red-700 dark:text-red-300">{t('messageTypes.error')}</span>
</div> </div>
<div className="relative text-sm text-red-900 dark:text-red-100"> <div className="relative text-sm text-red-900 dark:text-red-100">
<Markdown className="prose prose-sm max-w-none prose-red dark:prose-invert"> <Markdown className="prose prose-sm prose-red max-w-none dark:prose-invert">
{String(message.toolResult.content || '')} {String(message.toolResult.content || '')}
</Markdown> </Markdown>
{permissionSuggestion && ( {permissionSuggestion && (
<div className="mt-4 border-t border-red-200/60 dark:border-red-800/60 pt-3"> <div className="mt-4 border-t border-red-200/60 pt-3 dark:border-red-800/60">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<button <button
type="button" type="button"
@@ -260,9 +260,9 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
} }
}} }}
disabled={permissionSuggestion.isAllowed || permissionGrantState === 'granted'} disabled={permissionSuggestion.isAllowed || permissionGrantState === 'granted'}
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-medium border transition-colors ${permissionSuggestion.isAllowed || permissionGrantState === 'granted' className={`inline-flex items-center gap-2 rounded-md border px-3 py-1.5 text-xs font-medium transition-colors ${permissionSuggestion.isAllowed || permissionGrantState === 'granted'
? 'bg-green-100 dark:bg-green-900/30 border-green-300/70 dark:border-green-800/60 text-green-800 dark:text-green-200 cursor-default' ? 'cursor-default border-green-300/70 bg-green-100 text-green-800 dark:border-green-800/60 dark:bg-green-900/30 dark:text-green-200'
: 'bg-white/80 dark:bg-gray-900/40 border-red-300/70 dark:border-red-800/60 text-red-700 dark:text-red-200 hover:bg-white dark:hover:bg-gray-900/70' : 'border-red-300/70 bg-white/80 text-red-700 hover:bg-white dark:border-red-800/60 dark:bg-gray-900/40 dark:text-red-200 dark:hover:bg-gray-900/70'
}`} }`}
> >
{permissionSuggestion.isAllowed || permissionGrantState === 'granted' {permissionSuggestion.isAllowed || permissionGrantState === 'granted'
@@ -273,7 +273,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
<button <button
type="button" type="button"
onClick={(e) => { e.stopPropagation(); onShowSettings(); }} onClick={(e) => { e.stopPropagation(); onShowSettings(); }}
className="text-xs text-red-700 dark:text-red-200 underline hover:text-red-800 dark:hover:text-red-100" className="text-xs text-red-700 underline hover:text-red-800 dark:text-red-200 dark:hover:text-red-100"
> >
{t('permissions.openSettings')} {t('permissions.openSettings')}
</button> </button>
@@ -316,15 +316,15 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
</> </>
) : message.isInteractivePrompt ? ( ) : message.isInteractivePrompt ? (
// Special handling for interactive prompts // Special handling for interactive prompts
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4"> <div className="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="w-8 h-8 bg-amber-500 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5"> <div className="mt-0.5 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-amber-500">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-5 w-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h4 className="font-semibold text-amber-900 dark:text-amber-100 text-base mb-3"> <h4 className="mb-3 text-base font-semibold text-amber-900 dark:text-amber-100">
{t('interactive.title')} {t('interactive.title')}
</h4> </h4>
{(() => { {(() => {
@@ -348,29 +348,29 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
return ( return (
<> <>
<p className="text-sm text-amber-800 dark:text-amber-200 mb-4"> <p className="mb-4 text-sm text-amber-800 dark:text-amber-200">
{questionLine} {questionLine}
</p> </p>
{/* Option buttons */} {/* Option buttons */}
<div className="space-y-2 mb-4"> <div className="mb-4 space-y-2">
{options.map((option) => ( {options.map((option) => (
<button <button
key={option.number} key={option.number}
className={`w-full text-left px-4 py-3 rounded-lg border-2 transition-all ${option.isSelected className={`w-full rounded-lg border-2 px-4 py-3 text-left transition-all ${option.isSelected
? 'bg-amber-600 dark:bg-amber-700 text-white border-amber-600 dark:border-amber-700 shadow-md' ? 'border-amber-600 bg-amber-600 text-white shadow-md dark:border-amber-700 dark:bg-amber-700'
: 'bg-white dark:bg-gray-800 text-amber-900 dark:text-amber-100 border-amber-300 dark:border-amber-700' : 'border-amber-300 bg-white text-amber-900 dark:border-amber-700 dark:bg-gray-800 dark:text-amber-100'
} cursor-not-allowed opacity-75`} } cursor-not-allowed opacity-75`}
disabled disabled
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${option.isSelected <span className={`flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full text-sm font-bold ${option.isSelected
? 'bg-white/20' ? 'bg-white/20'
: 'bg-amber-100 dark:bg-amber-800/50' : 'bg-amber-100 dark:bg-amber-800/50'
}`}> }`}>
{option.number} {option.number}
</span> </span>
<span className="text-sm sm:text-base font-medium flex-1"> <span className="flex-1 text-sm font-medium sm:text-base">
{option.text} {option.text}
</span> </span>
{option.isSelected && ( {option.isSelected && (
@@ -381,11 +381,11 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
))} ))}
</div> </div>
<div className="bg-amber-100 dark:bg-amber-800/30 rounded-lg p-3"> <div className="rounded-lg bg-amber-100 p-3 dark:bg-amber-800/30">
<p className="text-amber-900 dark:text-amber-100 text-sm font-medium mb-1"> <p className="mb-1 text-sm font-medium text-amber-900 dark:text-amber-100">
{t('interactive.waiting')} {t('interactive.waiting')}
</p> </p>
<p className="text-amber-800 dark:text-amber-200 text-xs"> <p className="text-xs text-amber-800 dark:text-amber-200">
{t('interactive.instruction')} {t('interactive.instruction')}
</p> </p>
</div> </div>
@@ -399,14 +399,14 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
/* Thinking messages - collapsible by default */ /* Thinking messages - collapsible by default */
<div className="text-sm text-gray-700 dark:text-gray-300"> <div className="text-sm text-gray-700 dark:text-gray-300">
<details className="group"> <details className="group">
<summary className="cursor-pointer text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 font-medium flex items-center gap-2"> <summary className="flex cursor-pointer items-center gap-2 font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<svg className="w-3 h-3 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-3 w-3 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg> </svg>
<span>{t('thinking.emoji')}</span> <span>{t('thinking.emoji')}</span>
</summary> </summary>
<div className="mt-2 pl-4 border-l-2 border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 text-sm"> <div className="mt-2 border-l-2 border-gray-300 pl-4 text-sm text-gray-600 dark:border-gray-600 dark:text-gray-400">
<Markdown className="prose prose-sm max-w-none dark:prose-invert prose-gray"> <Markdown className="prose prose-sm prose-gray max-w-none dark:prose-invert">
{message.content} {message.content}
</Markdown> </Markdown>
</div> </div>
@@ -417,10 +417,10 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
{/* Thinking accordion for reasoning */} {/* Thinking accordion for reasoning */}
{showThinking && message.reasoning && ( {showThinking && message.reasoning && (
<details className="mb-3"> <details className="mb-3">
<summary className="cursor-pointer text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 font-medium"> <summary className="cursor-pointer font-medium text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
{t('thinking.emoji')} {t('thinking.emoji')}
</summary> </summary>
<div className="mt-2 pl-4 border-l-2 border-gray-300 dark:border-gray-600 italic text-gray-600 dark:text-gray-400 text-sm"> <div className="mt-2 border-l-2 border-gray-300 pl-4 text-sm italic text-gray-600 dark:border-gray-600 dark:text-gray-400">
<div className="whitespace-pre-wrap"> <div className="whitespace-pre-wrap">
{message.reasoning} {message.reasoning}
</div> </div>
@@ -441,15 +441,15 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
return ( return (
<div className="my-2"> <div className="my-2">
<div className="flex items-center gap-2 mb-2 text-sm text-gray-600 dark:text-gray-400"> <div className="mb-2 flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg> </svg>
<span className="font-medium">{t('json.response')}</span> <span className="font-medium">{t('json.response')}</span>
</div> </div>
<div className="bg-gray-800 dark:bg-gray-900 border border-gray-600/30 dark:border-gray-700 rounded-lg overflow-hidden"> <div className="overflow-hidden rounded-lg border border-gray-600/30 bg-gray-800 dark:border-gray-700 dark:bg-gray-900">
<pre className="p-4 overflow-x-auto"> <pre className="overflow-x-auto p-4">
<code className="text-gray-100 dark:text-gray-200 text-sm font-mono block whitespace-pre"> <code className="block whitespace-pre font-mono text-sm text-gray-100 dark:text-gray-200">
{formatted} {formatted}
</code> </code>
</pre> </pre>
@@ -463,7 +463,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
// Normal rendering for non-JSON content // Normal rendering for non-JSON content
return message.type === 'assistant' ? ( return message.type === 'assistant' ? (
<Markdown className="prose prose-sm max-w-none dark:prose-invert prose-gray"> <Markdown className="prose prose-sm prose-gray max-w-none dark:prose-invert">
{content} {content}
</Markdown> </Markdown>
) : ( ) : (
@@ -476,7 +476,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
)} )}
{!isGrouped && ( {!isGrouped && (
<div className="text-[11px] text-gray-400 dark:text-gray-500 mt-1"> <div className="mt-1 text-[11px] text-gray-400 dark:text-gray-500">
{formattedTime} {formattedTime}
</div> </div>
)} )}

View File

@@ -56,7 +56,7 @@ export default function PermissionRequestsBanner({
return ( return (
<div <div
key={request.requestId} key={request.requestId}
className="rounded-lg border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20 p-3 shadow-sm" className="rounded-lg border border-amber-200 bg-amber-50 p-3 shadow-sm dark:border-amber-800 dark:bg-amber-900/20"
> >
<div className="flex flex-wrap items-start justify-between gap-3"> <div className="flex flex-wrap items-start justify-between gap-3">
<div> <div>
@@ -74,10 +74,10 @@ export default function PermissionRequestsBanner({
{rawInput && ( {rawInput && (
<details className="mt-2"> <details className="mt-2">
<summary className="cursor-pointer text-xs text-amber-800 dark:text-amber-200 hover:text-amber-900 dark:hover:text-amber-100"> <summary className="cursor-pointer text-xs text-amber-800 hover:text-amber-900 dark:text-amber-200 dark:hover:text-amber-100">
View tool input View tool input
</summary> </summary>
<pre className="mt-2 max-h-40 overflow-auto rounded-md bg-white/80 dark:bg-gray-900/60 border border-amber-200/60 dark:border-amber-800/60 p-2 text-xs text-amber-900 dark:text-amber-100 whitespace-pre-wrap"> <pre className="mt-2 max-h-40 overflow-auto whitespace-pre-wrap rounded-md border border-amber-200/60 bg-white/80 p-2 text-xs text-amber-900 dark:border-amber-800/60 dark:bg-gray-900/60 dark:text-amber-100">
{rawInput} {rawInput}
</pre> </pre>
</details> </details>
@@ -87,7 +87,7 @@ export default function PermissionRequestsBanner({
<button <button
type="button" type="button"
onClick={() => handlePermissionDecision(request.requestId, { allow: true })} onClick={() => handlePermissionDecision(request.requestId, { allow: true })}
className="inline-flex items-center gap-2 rounded-md bg-amber-600 text-white text-xs font-medium px-3 py-1.5 hover:bg-amber-700 transition-colors" className="inline-flex items-center gap-2 rounded-md bg-amber-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-amber-700"
> >
Allow once Allow once
</button> </button>
@@ -99,10 +99,10 @@ export default function PermissionRequestsBanner({
} }
handlePermissionDecision(matchingRequestIds, { allow: true, rememberEntry: permissionEntry }); handlePermissionDecision(matchingRequestIds, { allow: true, rememberEntry: permissionEntry });
}} }}
className={`inline-flex items-center gap-2 rounded-md text-xs font-medium px-3 py-1.5 border transition-colors ${ className={`inline-flex items-center gap-2 rounded-md border px-3 py-1.5 text-xs font-medium transition-colors ${
permissionEntry permissionEntry
? 'border-amber-300 text-amber-800 hover:bg-amber-100 dark:border-amber-700 dark:text-amber-100 dark:hover:bg-amber-900/30' ? 'border-amber-300 text-amber-800 hover:bg-amber-100 dark:border-amber-700 dark:text-amber-100 dark:hover:bg-amber-900/30'
: 'border-gray-300 text-gray-400 cursor-not-allowed' : 'cursor-not-allowed border-gray-300 text-gray-400'
}`} }`}
disabled={!permissionEntry} disabled={!permissionEntry}
> >
@@ -111,7 +111,7 @@ export default function PermissionRequestsBanner({
<button <button
type="button" type="button"
onClick={() => handlePermissionDecision(request.requestId, { allow: false, message: 'User denied tool use' })} onClick={() => handlePermissionDecision(request.requestId, { allow: false, message: 'User denied tool use' })}
className="inline-flex items-center gap-2 rounded-md text-xs font-medium px-3 py-1.5 border border-red-300 text-red-700 hover:bg-red-50 dark:border-red-800 dark:text-red-200 dark:hover:bg-red-900/30 transition-colors" className="inline-flex items-center gap-2 rounded-md border border-red-300 px-3 py-1.5 text-xs font-medium text-red-700 transition-colors hover:bg-red-50 dark:border-red-800 dark:text-red-200 dark:hover:bg-red-900/30"
> >
Deny Deny
</button> </button>

View File

@@ -125,20 +125,20 @@ export default function ProviderSelectionEmptyState({
/* ── New session — provider picker ── */ /* ── New session — provider picker ── */
if (!selectedSession && !currentSessionId) { if (!selectedSession && !currentSessionId) {
return ( return (
<div className="flex items-center justify-center h-full px-4"> <div className="flex h-full items-center justify-center px-4">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
{/* Heading */} {/* Heading */}
<div className="text-center mb-8"> <div className="mb-8 text-center">
<h2 className="text-lg sm:text-xl font-semibold text-foreground tracking-tight"> <h2 className="text-lg font-semibold tracking-tight text-foreground sm:text-xl">
{t('providerSelection.title')} {t('providerSelection.title')}
</h2> </h2>
<p className="text-[13px] text-muted-foreground mt-1"> <p className="mt-1 text-[13px] text-muted-foreground">
{t('providerSelection.description')} {t('providerSelection.description')}
</p> </p>
</div> </div>
{/* Provider cards — horizontal row, equal width */} {/* Provider cards — horizontal row, equal width */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-2.5 mb-6"> <div className="mb-6 grid grid-cols-2 gap-2 sm:grid-cols-4 sm:gap-2.5">
{PROVIDERS.map((p) => { {PROVIDERS.map((p) => {
const active = provider === p.id; const active = provider === p.id;
return ( return (
@@ -146,27 +146,27 @@ export default function ProviderSelectionEmptyState({
key={p.id} key={p.id}
onClick={() => selectProvider(p.id)} onClick={() => selectProvider(p.id)}
className={` className={`
relative flex flex-col items-center gap-2.5 pt-5 pb-4 px-2 relative flex flex-col items-center gap-2.5 rounded-xl border-[1.5px] px-2
rounded-xl border-[1.5px] transition-all duration-150 pb-4 pt-5 transition-all duration-150
active:scale-[0.97] active:scale-[0.97]
${active ${active
? `${p.accent} ${p.ring} ring-2 bg-card shadow-sm` ? `${p.accent} ${p.ring} bg-card shadow-sm ring-2`
: 'border-border bg-card/60 hover:bg-card hover:border-border/80' : 'border-border bg-card/60 hover:border-border/80 hover:bg-card'
} }
`} `}
> >
<SessionProviderLogo <SessionProviderLogo
provider={p.id} provider={p.id}
className={`w-9 h-9 transition-transform duration-150 ${active ? 'scale-110' : ''}`} className={`h-9 w-9 transition-transform duration-150 ${active ? 'scale-110' : ''}`}
/> />
<div className="text-center"> <div className="text-center">
<p className="text-[13px] font-semibold text-foreground leading-none">{p.name}</p> <p className="text-[13px] font-semibold leading-none text-foreground">{p.name}</p>
<p className="text-[10px] text-muted-foreground mt-1 leading-tight">{t(p.infoKey)}</p> <p className="mt-1 text-[10px] leading-tight text-muted-foreground">{t(p.infoKey)}</p>
</div> </div>
{/* Check badge */} {/* Check badge */}
{active && ( {active && (
<div className={`absolute -top-1 -right-1 w-[18px] h-[18px] rounded-full ${p.check} flex items-center justify-center shadow-sm`}> <div className={`absolute -right-1 -top-1 h-[18px] w-[18px] rounded-full ${p.check} flex items-center justify-center shadow-sm`}>
<Check className="w-2.5 h-2.5" strokeWidth={3} /> <Check className="h-2.5 w-2.5" strokeWidth={3} />
</div> </div>
)} )}
</button> </button>
@@ -175,21 +175,21 @@ export default function ProviderSelectionEmptyState({
</div> </div>
{/* Model picker — appears after provider is chosen */} {/* Model picker — appears after provider is chosen */}
<div className={`transition-all duration-200 ${provider ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-1 pointer-events-none'}`}> <div className={`transition-all duration-200 ${provider ? 'translate-y-0 opacity-100' : 'pointer-events-none translate-y-1 opacity-0'}`}>
<div className="flex items-center justify-center gap-2 mb-5"> <div className="mb-5 flex items-center justify-center gap-2">
<span className="text-sm text-muted-foreground">{t('providerSelection.selectModel')}</span> <span className="text-sm text-muted-foreground">{t('providerSelection.selectModel')}</span>
<div className="relative"> <div className="relative">
<select <select
value={currentModel} value={currentModel}
onChange={(e) => handleModelChange(e.target.value)} onChange={(e) => handleModelChange(e.target.value)}
tabIndex={-1} tabIndex={-1}
className="appearance-none pl-3 pr-7 py-1.5 text-sm font-medium bg-muted/50 border border-border/60 rounded-lg text-foreground cursor-pointer hover:bg-muted transition-colors focus:outline-none focus:ring-2 focus:ring-primary/20" className="cursor-pointer appearance-none rounded-lg border border-border/60 bg-muted/50 py-1.5 pl-3 pr-7 text-sm font-medium text-foreground transition-colors hover:bg-muted focus:outline-none focus:ring-2 focus:ring-primary/20"
> >
{modelConfig.OPTIONS.map(({ value, label }: { value: string; label: string }) => ( {modelConfig.OPTIONS.map(({ value, label }: { value: string; label: string }) => (
<option key={value} value={value}>{label}</option> <option key={value} value={value}>{label}</option>
))} ))}
</select> </select>
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-3 h-3 text-muted-foreground pointer-events-none" /> <ChevronDown className="pointer-events-none absolute right-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" />
</div> </div>
</div> </div>
@@ -219,10 +219,10 @@ export default function ProviderSelectionEmptyState({
/* ── Existing session — continue prompt ── */ /* ── Existing session — continue prompt ── */
if (selectedSession) { if (selectedSession) {
return ( return (
<div className="flex items-center justify-center h-full"> <div className="flex h-full items-center justify-center">
<div className="text-center px-6 max-w-md"> <div className="max-w-md px-6 text-center">
<p className="text-lg font-semibold text-foreground mb-1.5">{t('session.continue.title')}</p> <p className="mb-1.5 text-lg font-semibold text-foreground">{t('session.continue.title')}</p>
<p className="text-sm text-muted-foreground leading-relaxed">{t('session.continue.description')}</p> <p className="text-sm leading-relaxed text-muted-foreground">{t('session.continue.description')}</p>
{tasksEnabled && isTaskMasterInstalled && ( {tasksEnabled && isTaskMasterInstalled && (
<div className="mt-5"> <div className="mt-5">

View File

@@ -1,7 +1,6 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { Brain, X } from 'lucide-react'; import { Brain, X } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { thinkingModes } from '../../constants/thinkingModes'; import { thinkingModes } from '../../constants/thinkingModes';
type ThinkingModeSelectorProps = { type ThinkingModeSelectorProps = {
@@ -53,18 +52,18 @@ function ThinkingModeSelector({ selectedMode, onModeChange, onClose, className =
<button <button
type="button" type="button"
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
className={`w-10 h-10 sm:w-10 sm:h-10 rounded-full flex items-center justify-center transition-all duration-200 ${selectedMode === 'none' className={`flex h-10 w-10 items-center justify-center rounded-full transition-all duration-200 sm:h-10 sm:w-10 ${selectedMode === 'none'
? 'bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600' ? 'bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600'
: 'bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800' : 'bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800'
}`} }`}
title={t('thinkingMode.buttonTitle', { mode: currentMode.name })} title={t('thinkingMode.buttonTitle', { mode: currentMode.name })}
> >
<IconComponent className={`w-5 h-5 ${currentMode.color}`} /> <IconComponent className={`h-5 w-5 ${currentMode.color}`} />
</button> </button>
{isOpen && ( {isOpen && (
<div className="absolute bottom-full right-0 mb-2 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 overflow-hidden"> <div className="absolute bottom-full right-0 mb-2 w-64 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-xl dark:border-gray-700 dark:bg-gray-800">
<div className="p-3 border-b border-gray-200 dark:border-gray-700"> <div className="border-b border-gray-200 p-3 dark:border-gray-700">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white"> <h3 className="text-sm font-semibold text-gray-900 dark:text-white">
{t('thinkingMode.selector.title')} {t('thinkingMode.selector.title')}
@@ -74,12 +73,12 @@ function ThinkingModeSelector({ selectedMode, onModeChange, onClose, className =
setIsOpen(false); setIsOpen(false);
if (onClose) onClose(); if (onClose) onClose();
}} }}
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" className="rounded p-1 hover:bg-gray-100 dark:hover:bg-gray-700"
> >
<X className="w-4 h-4 text-gray-500" /> <X className="h-4 w-4 text-gray-500" />
</button> </button>
</div> </div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t('thinkingMode.selector.description')} {t('thinkingMode.selector.description')}
</p> </p>
</div> </div>
@@ -97,30 +96,30 @@ function ThinkingModeSelector({ selectedMode, onModeChange, onClose, className =
setIsOpen(false); setIsOpen(false);
if (onClose) onClose(); if (onClose) onClose();
}} }}
className={`w-full px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors ${isSelected ? 'bg-gray-50 dark:bg-gray-700' : '' className={`w-full px-4 py-3 text-left transition-colors hover:bg-gray-50 dark:hover:bg-gray-700 ${isSelected ? 'bg-gray-50 dark:bg-gray-700' : ''
}`} }`}
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className={`mt-0.5 ${mode.icon ? mode.color : 'text-gray-400'}`}> <div className={`mt-0.5 ${mode.icon ? mode.color : 'text-gray-400'}`}>
{ModeIcon ? <ModeIcon className="w-5 h-5" /> : <div className="w-5 h-5" />} {ModeIcon ? <ModeIcon className="h-5 w-5" /> : <div className="h-5 w-5" />}
</div> </div>
<div className="flex-1 min-w-0"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className={`font-medium text-sm ${isSelected ? 'text-gray-900 dark:text-white' : 'text-gray-700 dark:text-gray-300' <span className={`text-sm font-medium ${isSelected ? 'text-gray-900 dark:text-white' : 'text-gray-700 dark:text-gray-300'
}`}> }`}>
{mode.name} {mode.name}
</span> </span>
{isSelected && ( {isSelected && (
<span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 px-2 py-0.5 rounded"> <span className="rounded bg-blue-100 px-2 py-0.5 text-xs text-blue-700 dark:bg-blue-900 dark:text-blue-300">
{t('thinkingMode.selector.active')} {t('thinkingMode.selector.active')}
</span> </span>
)} )}
</div> </div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> <p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{mode.description} {mode.description}
</p> </p>
{mode.prefix && ( {mode.prefix && (
<code className="text-xs bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded mt-1 inline-block"> <code className="mt-1 inline-block rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-700">
{mode.prefix} {mode.prefix}
</code> </code>
)} )}
@@ -131,7 +130,7 @@ function ThinkingModeSelector({ selectedMode, onModeChange, onClose, className =
})} })}
</div> </div>
<div className="p-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900"> <div className="border-t border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-900">
<p className="text-xs text-gray-600 dark:text-gray-400"> <p className="text-xs text-gray-600 dark:text-gray-400">
<strong>Tip:</strong> {t('thinkingMode.selector.tip')} <strong>Tip:</strong> {t('thinkingMode.selector.tip')}
</p> </p>

View File

@@ -22,7 +22,7 @@ export default function TokenUsagePie({ used, total }: TokenUsagePieProps) {
return ( return (
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400"> <div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
<svg width="24" height="24" viewBox="0 0 24 24" className="transform -rotate-90"> <svg width="24" height="24" viewBox="0 0 24 24" className="-rotate-90 transform">
{/* Background circle */} {/* Background circle */}
<circle <circle
cx="12" cx="12"

View File

@@ -220,7 +220,7 @@ export default function CodeEditor({
/> />
{saveError && ( {saveError && (
<div className="px-3 py-1.5 text-xs text-red-700 bg-red-50 border-b border-red-200 dark:bg-red-900/20 dark:text-red-300 dark:border-red-900/40"> <div className="border-b border-red-200 bg-red-50 px-3 py-1.5 text-xs text-red-700 dark:border-red-900/40 dark:bg-red-900/20 dark:text-red-300">
{saveError} {saveError}
</div> </div>
)} )}

View File

@@ -102,20 +102,20 @@ export default function EditorSidebar({
const useFlexLayout = editorExpanded || (fillSpace && !hasManualWidth); const useFlexLayout = editorExpanded || (fillSpace && !hasManualWidth);
return ( return (
<div ref={containerRef} className={`flex h-full flex-shrink-0 min-w-0 ${editorExpanded ? 'flex-1' : ''}`}> <div ref={containerRef} className={`flex h-full min-w-0 flex-shrink-0 ${editorExpanded ? 'flex-1' : ''}`}>
{!editorExpanded && ( {!editorExpanded && (
<div <div
ref={resizeHandleRef} ref={resizeHandleRef}
onMouseDown={onResizeStart} onMouseDown={onResizeStart}
className="flex-shrink-0 w-1 bg-gray-200 dark:bg-gray-700 hover:bg-blue-500 dark:hover:bg-blue-600 cursor-col-resize transition-colors relative group" className="group relative w-1 flex-shrink-0 cursor-col-resize bg-gray-200 transition-colors hover:bg-blue-500 dark:bg-gray-700 dark:hover:bg-blue-600"
title="Drag to resize" title="Drag to resize"
> >
<div className="absolute inset-y-0 left-1/2 -translate-x-1/2 w-1 bg-blue-500 dark:bg-blue-600 opacity-0 group-hover:opacity-100 transition-opacity" /> <div className="absolute inset-y-0 left-1/2 w-1 -translate-x-1/2 bg-blue-500 opacity-0 transition-opacity group-hover:opacity-100 dark:bg-blue-600" />
</div> </div>
)} )}
<div <div
className={`border-l border-gray-200 dark:border-gray-700 h-full overflow-hidden ${useFlexLayout ? 'flex-1 min-w-0' : `flex-shrink-0 min-w-[${MIN_EDITOR_WIDTH}px]`}`} className={`h-full overflow-hidden border-l border-gray-200 dark:border-gray-700 ${useFlexLayout ? 'min-w-0 flex-1' : `min-w-[ flex-shrink-0${MIN_EDITOR_WIDTH}px]`}`}
style={useFlexLayout ? undefined : { width: `${effectiveWidth}px`, minWidth: `${MIN_EDITOR_WIDTH}px` }} style={useFlexLayout ? undefined : { width: `${effectiveWidth}px`, minWidth: `${MIN_EDITOR_WIDTH}px` }}
> >
<CodeEditor <CodeEditor

View File

@@ -20,20 +20,20 @@ export default function CodeEditorBinaryFile({
message, message,
}: CodeEditorBinaryFileProps) { }: CodeEditorBinaryFileProps) {
const binaryContent = ( const binaryContent = (
<div className="w-full h-full flex flex-col items-center justify-center bg-background text-muted-foreground p-8"> <div className="flex h-full w-full flex-col items-center justify-center bg-background p-8 text-muted-foreground">
<div className="flex flex-col items-center gap-4 max-w-md text-center"> <div className="flex max-w-md flex-col items-center gap-4 text-center">
<div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center"> <div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted">
<svg className="w-8 h-8 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="h-8 w-8 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg> </svg>
</div> </div>
<div> <div>
<h3 className="text-lg font-medium text-foreground mb-2">{title}</h3> <h3 className="mb-2 text-lg font-medium text-foreground">{title}</h3>
<p className="text-sm text-muted-foreground">{message}</p> <p className="text-sm text-muted-foreground">{message}</p>
</div> </div>
<button <button
onClick={onClose} onClick={onClose}
className="mt-4 px-4 py-2 text-sm bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors" className="mt-4 rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground transition-colors hover:bg-primary/90"
> >
Close Close
</button> </button>
@@ -43,18 +43,18 @@ export default function CodeEditorBinaryFile({
if (isSidebar) { if (isSidebar) {
return ( return (
<div className="w-full h-full flex flex-col bg-background"> <div className="flex h-full w-full flex-col bg-background">
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border flex-shrink-0"> <div className="flex flex-shrink-0 items-center justify-between border-b border-border px-3 py-1.5">
<div className="flex items-center gap-2 min-w-0 flex-1"> <div className="flex min-w-0 flex-1 items-center gap-2">
<h3 className="text-sm font-medium text-gray-900 dark:text-white truncate">{file.name}</h3> <h3 className="truncate text-sm font-medium text-gray-900 dark:text-white">{file.name}</h3>
</div> </div>
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center justify-center" className="flex items-center justify-center rounded-md p-1.5 text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
title="Close" title="Close"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
@@ -75,23 +75,23 @@ export default function CodeEditorBinaryFile({
return ( return (
<div className={containerClassName}> <div className={containerClassName}>
<div className={innerClassName}> <div className={innerClassName}>
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border flex-shrink-0"> <div className="flex flex-shrink-0 items-center justify-between border-b border-border px-3 py-1.5">
<div className="flex items-center gap-2 min-w-0 flex-1"> <div className="flex min-w-0 flex-1 items-center gap-2">
<h3 className="text-sm font-medium text-gray-900 dark:text-white truncate">{file.name}</h3> <h3 className="truncate text-sm font-medium text-gray-900 dark:text-white">{file.name}</h3>
</div> </div>
<div className="flex items-center gap-0.5 shrink-0"> <div className="flex shrink-0 items-center gap-0.5">
<button <button
type="button" type="button"
onClick={onToggleFullscreen} onClick={onToggleFullscreen}
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center justify-center" className="flex items-center justify-center rounded-md p-1.5 text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'} title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
> >
{isFullscreen ? ( {isFullscreen ? (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 9V4.5M9 9H4.5M9 9L3.5 3.5M9 15v4.5M9 15H4.5M9 15l-5.5 5.5M15 9h4.5M15 9V4.5M15 9l5.5-5.5M15 15h4.5M15 15v4.5m0-4.5l5.5 5.5" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 9V4.5M9 9H4.5M9 9L3.5 3.5M9 15v4.5M9 15H4.5M9 15l-5.5 5.5M15 9h4.5M15 9V4.5M15 9l5.5-5.5M15 15h4.5M15 15v4.5m0-4.5l5.5 5.5" />
</svg> </svg>
) : ( ) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg> </svg>
)} )}
@@ -99,10 +99,10 @@ export default function CodeEditorBinaryFile({
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center justify-center" className="flex items-center justify-center rounded-md p-1.5 text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
title="Close" title="Close"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>

View File

@@ -12,7 +12,7 @@ export default function CodeEditorFooter({
shortcutsLabel, shortcutsLabel,
}: CodeEditorFooterProps) { }: CodeEditorFooterProps) {
return ( return (
<div className="flex items-center justify-between px-3 py-1.5 border-t border-border bg-muted flex-shrink-0"> <div className="flex flex-shrink-0 items-center justify-between border-t border-border bg-muted px-3 py-1.5">
<div className="flex items-center gap-3 text-xs text-gray-600 dark:text-gray-400"> <div className="flex items-center gap-3 text-xs text-gray-600 dark:text-gray-400">
<span> <span>
{linesLabel} {content.split('\n').length} {linesLabel} {content.split('\n').length}

View File

@@ -49,74 +49,74 @@ export default function CodeEditorHeader({
const saveTitle = saveSuccess ? labels.saved : saving ? labels.saving : labels.save; const saveTitle = saveSuccess ? labels.saved : saving ? labels.saving : labels.save;
return ( return (
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border flex-shrink-0 min-w-0 gap-2"> <div className="flex min-w-0 flex-shrink-0 items-center justify-between gap-2 border-b border-border px-3 py-1.5">
{/* File info - can shrink */} {/* File info - can shrink */}
<div className="flex items-center gap-2 min-w-0 flex-1 shrink"> <div className="flex min-w-0 flex-1 shrink items-center gap-2">
<div className="min-w-0 shrink"> <div className="min-w-0 shrink">
<div className="flex items-center gap-2 min-w-0"> <div className="flex min-w-0 items-center gap-2">
<h3 className="text-sm font-medium text-gray-900 dark:text-white truncate">{file.name}</h3> <h3 className="truncate text-sm font-medium text-gray-900 dark:text-white">{file.name}</h3>
{file.diffInfo && ( {file.diffInfo && (
<span className="text-[10px] bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 px-1.5 py-0.5 rounded whitespace-nowrap shrink-0"> <span className="shrink-0 whitespace-nowrap rounded bg-blue-100 px-1.5 py-0.5 text-[10px] text-blue-600 dark:bg-blue-900 dark:text-blue-300">
{labels.showingChanges} {labels.showingChanges}
</span> </span>
)} )}
</div> </div>
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">{file.path}</p> <p className="truncate text-xs text-gray-500 dark:text-gray-400">{file.path}</p>
</div> </div>
</div> </div>
{/* Buttons - don't shrink, always visible */} {/* Buttons - don't shrink, always visible */}
<div className="flex items-center gap-0.5 shrink-0"> <div className="flex shrink-0 items-center gap-0.5">
{isMarkdownFile && ( {isMarkdownFile && (
<button <button
type="button" type="button"
onClick={onToggleMarkdownPreview} onClick={onToggleMarkdownPreview}
className={`p-1.5 rounded-md flex items-center justify-center transition-colors ${ className={`flex items-center justify-center rounded-md p-1.5 transition-colors ${
markdownPreview markdownPreview
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30' ? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white'
}`} }`}
title={markdownPreview ? labels.editMarkdown : labels.previewMarkdown} title={markdownPreview ? labels.editMarkdown : labels.previewMarkdown}
> >
{markdownPreview ? <Code2 className="w-4 h-4" /> : <Eye className="w-4 h-4" />} {markdownPreview ? <Code2 className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button> </button>
)} )}
<button <button
type="button" type="button"
onClick={onOpenSettings} onClick={onOpenSettings}
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center justify-center" className="flex items-center justify-center rounded-md p-1.5 text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
title={labels.settings} title={labels.settings}
> >
<SettingsIcon className="w-4 h-4" /> <SettingsIcon className="h-4 w-4" />
</button> </button>
<button <button
type="button" type="button"
onClick={onDownload} onClick={onDownload}
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center justify-center" className="flex items-center justify-center rounded-md p-1.5 text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
title={labels.download} title={labels.download}
> >
<Download className="w-4 h-4" /> <Download className="h-4 w-4" />
</button> </button>
<button <button
type="button" type="button"
onClick={onSave} onClick={onSave}
disabled={saving} disabled={saving}
className={`p-1.5 rounded-md disabled:opacity-50 flex items-center justify-center transition-colors ${ className={`flex items-center justify-center rounded-md p-1.5 transition-colors disabled:opacity-50 ${
saveSuccess saveSuccess
? 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/30' ? 'bg-green-50 text-green-600 dark:bg-green-900/30 dark:text-green-400'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white'
}`} }`}
title={saveTitle} title={saveTitle}
> >
{saveSuccess ? ( {saveSuccess ? (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg> </svg>
) : ( ) : (
<Save className="w-4 h-4" /> <Save className="h-4 w-4" />
)} )}
</button> </button>
@@ -124,20 +124,20 @@ export default function CodeEditorHeader({
<button <button
type="button" type="button"
onClick={onToggleFullscreen} onClick={onToggleFullscreen}
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center justify-center" className="flex items-center justify-center rounded-md p-1.5 text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
title={isFullscreen ? labels.exitFullscreen : labels.fullscreen} title={isFullscreen ? labels.exitFullscreen : labels.fullscreen}
> >
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />} {isFullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
</button> </button>
)} )}
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 flex items-center justify-center" className="flex items-center justify-center rounded-md p-1.5 text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
title={labels.close} title={labels.close}
> >
<X className="w-4 h-4" /> <X className="h-4 w-4" />
</button> </button>
</div> </div>
</div> </div>

View File

@@ -15,17 +15,17 @@ export default function CodeEditorLoadingState({
<> <>
<style>{getEditorLoadingStyles(isDarkMode)}</style> <style>{getEditorLoadingStyles(isDarkMode)}</style>
{isSidebar ? ( {isSidebar ? (
<div className="w-full h-full flex items-center justify-center bg-background"> <div className="flex h-full w-full items-center justify-center bg-background">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600" /> <div className="h-6 w-6 animate-spin rounded-full border-b-2 border-blue-600" />
<span className="text-gray-900 dark:text-white">{loadingText}</span> <span className="text-gray-900 dark:text-white">{loadingText}</span>
</div> </div>
</div> </div>
) : ( ) : (
<div className="fixed inset-0 z-[9999] md:bg-black/50 md:flex md:items-center md:justify-center"> <div className="fixed inset-0 z-[9999] md:flex md:items-center md:justify-center md:bg-black/50">
<div className="code-editor-loading w-full h-full md:rounded-lg md:w-auto md:h-auto p-8 flex items-center justify-center"> <div className="code-editor-loading flex h-full w-full items-center justify-center p-8 md:h-auto md:w-auto md:rounded-lg">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600" /> <div className="h-6 w-6 animate-spin rounded-full border-b-2 border-blue-600" />
<span className="text-gray-900 dark:text-white">{loadingText}</span> <span className="text-gray-900 dark:text-white">{loadingText}</span>
</div> </div>
</div> </div>

View File

@@ -27,7 +27,7 @@ export default function CodeEditorSurface({
if (markdownPreview && isMarkdownFile) { if (markdownPreview && isMarkdownFile) {
return ( return (
<div className="h-full overflow-y-auto bg-white dark:bg-gray-900"> <div className="h-full overflow-y-auto bg-white dark:bg-gray-900">
<div className="max-w-4xl mx-auto px-8 py-6 prose prose-sm dark:prose-invert prose-headings:font-semibold prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-code:text-sm prose-pre:bg-gray-900 prose-img:rounded-lg max-w-none"> <div className="prose prose-sm mx-auto max-w-4xl max-w-none px-8 py-6 dark:prose-invert prose-headings:font-semibold prose-a:text-blue-600 prose-code:text-sm prose-pre:bg-gray-900 prose-img:rounded-lg dark:prose-a:text-blue-400">
<MarkdownPreview content={content} /> <MarkdownPreview content={content} />
</div> </div>
</div> </div>

View File

@@ -24,7 +24,7 @@ export default function MarkdownCodeBlock({
if (shouldRenderInline) { if (shouldRenderInline) {
return ( return (
<code <code
className={`font-mono text-[0.9em] px-1.5 py-0.5 rounded-md bg-gray-100 text-gray-900 border border-gray-200 dark:bg-gray-800/60 dark:text-gray-100 dark:border-gray-700 whitespace-pre-wrap break-words ${className || ''}`} className={`whitespace-pre-wrap break-words rounded-md border border-gray-200 bg-gray-100 px-1.5 py-0.5 font-mono text-[0.9em] text-gray-900 dark:border-gray-700 dark:bg-gray-800/60 dark:text-gray-100 ${className || ''}`}
{...props} {...props}
> >
{children} {children}
@@ -36,9 +36,9 @@ export default function MarkdownCodeBlock({
const language = languageMatch ? languageMatch[1] : 'text'; const language = languageMatch ? languageMatch[1] : 'text';
return ( return (
<div className="relative group my-2"> <div className="group relative my-2">
{language !== 'text' && ( {language !== 'text' && (
<div className="absolute top-2 left-3 z-10 text-xs text-gray-400 font-medium uppercase">{language}</div> <div className="absolute left-3 top-2 z-10 text-xs font-medium uppercase text-gray-400">{language}</div>
)} )}
<button <button
@@ -50,7 +50,7 @@ export default function MarkdownCodeBlock({
setTimeout(() => setCopied(false), 2000); setTimeout(() => setCopied(false), 2000);
} }
})} })}
className="absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity text-xs px-2 py-1 rounded-md bg-gray-700/80 hover:bg-gray-700 text-white border border-gray-600" className="absolute right-2 top-2 z-10 rounded-md border border-gray-600 bg-gray-700/80 px-2 py-1 text-xs text-white opacity-0 transition-opacity hover:bg-gray-700 group-hover:opacity-100"
> >
{copied ? 'Copied!' : 'Copy'} {copied ? 'Copied!' : 'Copy'}
</button> </button>

View File

@@ -13,26 +13,26 @@ type MarkdownPreviewProps = {
const markdownPreviewComponents: Components = { const markdownPreviewComponents: Components = {
code: MarkdownCodeBlock, code: MarkdownCodeBlock,
blockquote: ({ children }) => ( blockquote: ({ children }) => (
<blockquote className="border-l-4 border-gray-300 dark:border-gray-600 pl-4 italic text-gray-600 dark:text-gray-400 my-2"> <blockquote className="my-2 border-l-4 border-gray-300 pl-4 italic text-gray-600 dark:border-gray-600 dark:text-gray-400">
{children} {children}
</blockquote> </blockquote>
), ),
a: ({ href, children }) => ( a: ({ href, children }) => (
<a href={href} className="text-blue-600 dark:text-blue-400 hover:underline" target="_blank" rel="noopener noreferrer"> <a href={href} className="text-blue-600 hover:underline dark:text-blue-400" target="_blank" rel="noopener noreferrer">
{children} {children}
</a> </a>
), ),
table: ({ children }) => ( table: ({ children }) => (
<div className="overflow-x-auto my-2"> <div className="my-2 overflow-x-auto">
<table className="min-w-full border-collapse border border-gray-200 dark:border-gray-700">{children}</table> <table className="min-w-full border-collapse border border-gray-200 dark:border-gray-700">{children}</table>
</div> </div>
), ),
thead: ({ children }) => <thead className="bg-gray-50 dark:bg-gray-800">{children}</thead>, thead: ({ children }) => <thead className="bg-gray-50 dark:bg-gray-800">{children}</thead>,
th: ({ children }) => ( th: ({ children }) => (
<th className="px-3 py-2 text-left text-sm font-semibold border border-gray-200 dark:border-gray-700">{children}</th> <th className="border border-gray-200 px-3 py-2 text-left text-sm font-semibold dark:border-gray-700">{children}</th>
), ),
td: ({ children }) => ( td: ({ children }) => (
<td className="px-3 py-2 align-top text-sm border border-gray-200 dark:border-gray-700">{children}</td> <td className="border border-gray-200 px-3 py-2 align-top text-sm dark:border-gray-700">{children}</td>
), ),
}; };

View File

@@ -275,13 +275,13 @@ export default function FileContextMenu({
> >
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center py-4"> <div className="flex items-center justify-center py-4">
<RefreshCw className="w-4 h-4 animate-spin text-muted-foreground" /> <RefreshCw className="h-4 w-4 animate-spin text-muted-foreground" />
<span className="ml-2 text-sm text-muted-foreground">{t('fileTree.context.loading', 'Loading...')}</span> <span className="ml-2 text-sm text-muted-foreground">{t('fileTree.context.loading', 'Loading...')}</span>
</div> </div>
) : ( ) : (
menuActions.map((action) => ( menuActions.map((action) => (
<Fragment key={action.key}> <Fragment key={action.key}>
{action.showDividerBefore && <div className="h-px bg-border my-1 mx-2" />} {action.showDividerBefore && <div className="mx-2 my-1 h-px bg-border" />}
<button <button
role="menuitem" role="menuitem"
tabIndex={action.isDisabled ? -1 : 0} tabIndex={action.isDisabled ? -1 : 0}
@@ -298,9 +298,9 @@ export default function FileContextMenu({
isLoading && 'pointer-events-none', isLoading && 'pointer-events-none',
)} )}
> >
{action.icon && <action.icon className="w-4 h-4 flex-shrink-0" />} {action.icon && <action.icon className="h-4 w-4 flex-shrink-0" />}
<span className="flex-1">{action.label}</span> <span className="flex-1">{action.label}</span>
{action.shortcut && <span className="text-xs text-muted-foreground font-mono">{action.shortcut}</span>} {action.shortcut && <span className="font-mono text-xs text-muted-foreground">{action.shortcut}</span>}
</button> </button>
</Fragment> </Fragment>
)) ))

View File

@@ -2,7 +2,6 @@ import { useCallback, useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AlertTriangle, Check, X, Loader2, Folder, Upload } from 'lucide-react'; import { AlertTriangle, Check, X, Loader2, Folder, Upload } from 'lucide-react';
import { cn } from '../../../lib/utils'; import { cn } from '../../../lib/utils';
import ImageViewer from './ImageViewer';
import { ICON_SIZE_CLASS, getFileIconData } from '../constants/fileIcons'; import { ICON_SIZE_CLASS, getFileIconData } from '../constants/fileIcons';
import { useExpandedDirectories } from '../hooks/useExpandedDirectories'; import { useExpandedDirectories } from '../hooks/useExpandedDirectories';
import { useFileTreeData } from '../hooks/useFileTreeData'; import { useFileTreeData } from '../hooks/useFileTreeData';
@@ -12,12 +11,13 @@ import { useFileTreeViewMode } from '../hooks/useFileTreeViewMode';
import { useFileTreeUpload } from '../hooks/useFileTreeUpload'; import { useFileTreeUpload } from '../hooks/useFileTreeUpload';
import type { FileTreeImageSelection, FileTreeNode } from '../types/types'; import type { FileTreeImageSelection, FileTreeNode } from '../types/types';
import { formatFileSize, formatRelativeTime, isImageFile } from '../utils/fileTreeUtils'; import { formatFileSize, formatRelativeTime, isImageFile } from '../utils/fileTreeUtils';
import { Project } from '../../../types/app';
import { ScrollArea, Input } from '../../../shared/view/ui';
import FileTreeBody from './FileTreeBody'; import FileTreeBody from './FileTreeBody';
import FileTreeDetailedColumns from './FileTreeDetailedColumns'; import FileTreeDetailedColumns from './FileTreeDetailedColumns';
import FileTreeHeader from './FileTreeHeader'; import FileTreeHeader from './FileTreeHeader';
import FileTreeLoadingState from './FileTreeLoadingState'; import FileTreeLoadingState from './FileTreeLoadingState';
import { Project } from '../../../types/app'; import ImageViewer from './ImageViewer';
import { ScrollArea, Input } from '../../../shared/view/ui';
type FileTreeProps = { type FileTreeProps = {
@@ -123,7 +123,7 @@ export default function FileTree({ selectedProject, onFileOpen }: FileTreeProps)
return ( return (
<div <div
ref={upload.treeRef} ref={upload.treeRef}
className="h-full flex flex-col bg-background relative" className="relative flex h-full flex-col bg-background"
onDragEnter={upload.handleDragEnter} onDragEnter={upload.handleDragEnter}
onDragOver={upload.handleDragOver} onDragOver={upload.handleDragOver}
onDragLeave={upload.handleDragLeave} onDragLeave={upload.handleDragLeave}
@@ -131,9 +131,9 @@ export default function FileTree({ selectedProject, onFileOpen }: FileTreeProps)
> >
{/* Drag overlay */} {/* Drag overlay */}
{upload.isDragOver && ( {upload.isDragOver && (
<div className="absolute inset-0 z-50 bg-blue-500/10 border-2 border-dashed border-blue-500 flex items-center justify-center"> <div className="absolute inset-0 z-50 flex items-center justify-center border-2 border-dashed border-blue-500 bg-blue-500/10">
<div className="bg-background/95 px-6 py-4 rounded-lg shadow-lg flex items-center gap-3"> <div className="flex items-center gap-3 rounded-lg bg-background/95 px-6 py-4 shadow-lg">
<Upload className="w-6 h-6 text-blue-500" /> <Upload className="h-6 w-6 text-blue-500" />
<span className="text-sm font-medium">{t('fileTree.dropToUpload', 'Drop files to upload')}</span> <span className="text-sm font-medium">{t('fileTree.dropToUpload', 'Drop files to upload')}</span>
</div> </div>
</div> </div>
@@ -158,7 +158,7 @@ export default function FileTree({ selectedProject, onFileOpen }: FileTreeProps)
{/* New item input */} {/* New item input */}
{operations.isCreating && ( {operations.isCreating && (
<div <div
className="flex items-center gap-1.5 py-[3px] pr-2 mb-1" className="mb-1 flex items-center gap-1.5 py-[3px] pr-2"
style={{ paddingLeft: `${(operations.newItemParent.split('/').length - 1) * 16 + 4}px` }} style={{ paddingLeft: `${(operations.newItemParent.split('/').length - 1) * 16 + 4}px` }}
> >
{operations.newItemType === 'directory' ? ( {operations.newItemType === 'directory' ? (
@@ -181,7 +181,7 @@ export default function FileTree({ selectedProject, onFileOpen }: FileTreeProps)
if (operations.isCreating) operations.handleConfirmCreate(); if (operations.isCreating) operations.handleConfirmCreate();
}, 100); }, 100);
}} }}
className="h-6 text-sm flex-1" className="h-6 flex-1 text-sm"
disabled={operations.operationLoading} disabled={operations.operationLoading}
/> />
</div> </div>
@@ -225,10 +225,10 @@ export default function FileTree({ selectedProject, onFileOpen }: FileTreeProps)
{/* Delete Confirmation Dialog */} {/* Delete Confirmation Dialog */}
{operations.deleteConfirmation.isOpen && operations.deleteConfirmation.item && ( {operations.deleteConfirmation.isOpen && operations.deleteConfirmation.item && (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50"> <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50">
<div className="bg-background border border-border rounded-lg shadow-lg p-4 max-w-sm mx-4"> <div className="mx-4 max-w-sm rounded-lg border border-border bg-background p-4 shadow-lg">
<div className="flex items-center gap-3 mb-4"> <div className="mb-4 flex items-center gap-3">
<div className="p-2 rounded-full bg-red-100 dark:bg-red-900/30"> <div className="rounded-full bg-red-100 p-2 dark:bg-red-900/30">
<AlertTriangle className="w-5 h-5 text-red-600 dark:text-red-400" /> <AlertTriangle className="h-5 w-5 text-red-600 dark:text-red-400" />
</div> </div>
<div> <div>
<h3 className="font-medium text-foreground"> <h3 className="font-medium text-foreground">
@@ -241,7 +241,7 @@ export default function FileTree({ selectedProject, onFileOpen }: FileTreeProps)
</p> </p>
</div> </div>
</div> </div>
<p className="text-sm text-muted-foreground mb-4"> <p className="mb-4 text-sm text-muted-foreground">
{operations.deleteConfirmation.item.type === 'directory' {operations.deleteConfirmation.item.type === 'directory'
? t('fileTree.delete.folderWarning', 'This folder and all its contents will be permanently deleted.') ? t('fileTree.delete.folderWarning', 'This folder and all its contents will be permanently deleted.')
: t('fileTree.delete.fileWarning', 'This file will be permanently deleted.')} : t('fileTree.delete.fileWarning', 'This file will be permanently deleted.')}
@@ -250,16 +250,16 @@ export default function FileTree({ selectedProject, onFileOpen }: FileTreeProps)
<button <button
onClick={operations.handleCancelDelete} onClick={operations.handleCancelDelete}
disabled={operations.operationLoading} disabled={operations.operationLoading}
className="px-3 py-1.5 text-sm rounded-md hover:bg-accent transition-colors" className="rounded-md px-3 py-1.5 text-sm transition-colors hover:bg-accent"
> >
{t('common.cancel', 'Cancel')} {t('common.cancel', 'Cancel')}
</button> </button>
<button <button
onClick={operations.handleConfirmDelete} onClick={operations.handleConfirmDelete}
disabled={operations.operationLoading} disabled={operations.operationLoading}
className="px-3 py-1.5 text-sm rounded-md bg-red-600 text-white hover:bg-red-700 transition-colors disabled:opacity-50 flex items-center gap-2" className="flex items-center gap-2 rounded-md bg-red-600 px-3 py-1.5 text-sm text-white transition-colors hover:bg-red-700 disabled:opacity-50"
> >
{operations.operationLoading && <Loader2 className="w-4 h-4 animate-spin" />} {operations.operationLoading && <Loader2 className="h-4 w-4 animate-spin" />}
{t('fileTree.delete.confirm', 'Delete')} {t('fileTree.delete.confirm', 'Delete')}
</button> </button>
</div> </div>
@@ -278,9 +278,9 @@ export default function FileTree({ selectedProject, onFileOpen }: FileTreeProps)
)} )}
> >
{toast.type === 'success' ? ( {toast.type === 'success' ? (
<Check className="w-4 h-4" /> <Check className="h-4 w-4" />
) : ( ) : (
<X className="w-4 h-4" /> <X className="h-4 w-4" />
)} )}
<span className="text-sm">{toast.message}</span> <span className="text-sm">{toast.message}</span>
</div> </div>

View File

@@ -4,7 +4,7 @@ export default function FileTreeDetailedColumns() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="px-3 pt-1.5 pb-1 border-b border-border"> <div className="border-b border-border px-3 pb-1 pt-1.5">
<div className="grid grid-cols-12 gap-2 px-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70"> <div className="grid grid-cols-12 gap-2 px-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
<div className="col-span-5">{t('fileTree.name')}</div> <div className="col-span-5">{t('fileTree.name')}</div>
<div className="col-span-2">{t('fileTree.size')}</div> <div className="col-span-2">{t('fileTree.size')}</div>

View File

@@ -8,11 +8,11 @@ type FileTreeEmptyStateProps = {
export default function FileTreeEmptyState({ icon: Icon, title, description }: FileTreeEmptyStateProps) { export default function FileTreeEmptyState({ icon: Icon, title, description }: FileTreeEmptyStateProps) {
return ( return (
<div className="text-center py-8"> <div className="py-8 text-center">
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-3"> <div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-lg bg-muted">
<Icon className="w-6 h-6 text-muted-foreground" /> <Icon className="h-6 w-6 text-muted-foreground" />
</div> </div>
<h4 className="font-medium text-foreground mb-1">{title}</h4> <h4 className="mb-1 font-medium text-foreground">{title}</h4>
<p className="text-sm text-muted-foreground">{description}</p> <p className="text-sm text-muted-foreground">{description}</p>
</div> </div>
); );

View File

@@ -34,7 +34,7 @@ export default function FileTreeHeader({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="px-3 pt-3 pb-2 border-b border-border space-y-2"> <div className="space-y-2 border-b border-border px-3 pb-2 pt-3">
{/* Title and Toolbar */} {/* Title and Toolbar */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-foreground">{t('fileTree.files')}</h3> <h3 className="text-sm font-medium text-foreground">{t('fileTree.files')}</h3>
@@ -50,7 +50,7 @@ export default function FileTreeHeader({
aria-label={t('fileTree.newFile', 'New File (Cmd+N)')} aria-label={t('fileTree.newFile', 'New File (Cmd+N)')}
disabled={operationLoading} disabled={operationLoading}
> >
<FileText className="w-3.5 h-3.5" /> <FileText className="h-3.5 w-3.5" />
</Button> </Button>
)} )}
{onNewFolder && ( {onNewFolder && (
@@ -63,7 +63,7 @@ export default function FileTreeHeader({
aria-label={t('fileTree.newFolder', 'New Folder (Cmd+Shift+N)')} aria-label={t('fileTree.newFolder', 'New Folder (Cmd+Shift+N)')}
disabled={operationLoading} disabled={operationLoading}
> >
<FolderPlus className="w-3.5 h-3.5" /> <FolderPlus className="h-3.5 w-3.5" />
</Button> </Button>
)} )}
{onRefresh && ( {onRefresh && (
@@ -88,11 +88,11 @@ export default function FileTreeHeader({
title={t('fileTree.collapseAll', 'Collapse All')} title={t('fileTree.collapseAll', 'Collapse All')}
aria-label={t('fileTree.collapseAll', 'Collapse All')} aria-label={t('fileTree.collapseAll', 'Collapse All')}
> >
<ChevronDown className="w-3.5 h-3.5" /> <ChevronDown className="h-3.5 w-3.5" />
</Button> </Button>
)} )}
{/* Divider */} {/* Divider */}
<div className="w-px h-4 bg-border mx-0.5" /> <div className="mx-0.5 h-4 w-px bg-border" />
{/* View mode buttons */} {/* View mode buttons */}
<Button <Button
variant={viewMode === 'simple' ? 'default' : 'ghost'} variant={viewMode === 'simple' ? 'default' : 'ghost'}
@@ -102,7 +102,7 @@ export default function FileTreeHeader({
title={t('fileTree.simpleView')} title={t('fileTree.simpleView')}
aria-label={t('fileTree.simpleView')} aria-label={t('fileTree.simpleView')}
> >
<List className="w-3.5 h-3.5" /> <List className="h-3.5 w-3.5" />
</Button> </Button>
<Button <Button
variant={viewMode === 'compact' ? 'default' : 'ghost'} variant={viewMode === 'compact' ? 'default' : 'ghost'}
@@ -112,7 +112,7 @@ export default function FileTreeHeader({
title={t('fileTree.compactView')} title={t('fileTree.compactView')}
aria-label={t('fileTree.compactView')} aria-label={t('fileTree.compactView')}
> >
<Eye className="w-3.5 h-3.5" /> <Eye className="h-3.5 w-3.5" />
</Button> </Button>
<Button <Button
variant={viewMode === 'detailed' ? 'default' : 'ghost'} variant={viewMode === 'detailed' ? 'default' : 'ghost'}
@@ -122,31 +122,31 @@ export default function FileTreeHeader({
title={t('fileTree.detailedView')} title={t('fileTree.detailedView')}
aria-label={t('fileTree.detailedView')} aria-label={t('fileTree.detailedView')}
> >
<TableProperties className="w-3.5 h-3.5" /> <TableProperties className="h-3.5 w-3.5" />
</Button> </Button>
</div> </div>
</div> </div>
{/* Search Bar */} {/* Search Bar */}
<div className="relative"> <div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" /> <Search className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<Input <Input
type="text" type="text"
placeholder={t('fileTree.searchPlaceholder')} placeholder={t('fileTree.searchPlaceholder')}
value={searchQuery} value={searchQuery}
onChange={(event) => onSearchQueryChange(event.target.value)} onChange={(event) => onSearchQueryChange(event.target.value)}
className="pl-8 pr-8 h-8 text-sm" className="h-8 pl-8 pr-8 text-sm"
/> />
{searchQuery && ( {searchQuery && (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="absolute right-0.5 top-1/2 -translate-y-1/2 h-5 w-5 p-0 hover:bg-accent" className="absolute right-0.5 top-1/2 h-5 w-5 -translate-y-1/2 p-0 hover:bg-accent"
onClick={() => onSearchQueryChange('')} onClick={() => onSearchQueryChange('')}
title={t('fileTree.clearSearch')} title={t('fileTree.clearSearch')}
aria-label={t('fileTree.clearSearch')} aria-label={t('fileTree.clearSearch')}
> >
<X className="w-3 h-3" /> <X className="h-3 w-3" />
</Button> </Button>
)} )}
</div> </div>

View File

@@ -4,8 +4,8 @@ export default function FileTreeLoadingState() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="h-full flex items-center justify-center"> <div className="flex h-full items-center justify-center">
<div className="text-muted-foreground text-sm">{t('fileTree.loading')}</div> <div className="text-sm text-muted-foreground">{t('fileTree.loading')}</div>
</div> </div>
); );
} }

View File

@@ -1,9 +1,9 @@
import type { ReactNode, RefObject } from 'react'; import type { ReactNode, RefObject } from 'react';
import { ChevronRight, Folder, FolderOpen } from 'lucide-react'; import { ChevronRight, Folder, FolderOpen } from 'lucide-react';
import { cn } from '../../../lib/utils'; import { cn } from '../../../lib/utils';
import FileContextMenu from './FileContextMenu';
import type { FileTreeNode as FileTreeNodeType, FileTreeViewMode } from '../types/types'; import type { FileTreeNode as FileTreeNodeType, FileTreeViewMode } from '../types/types';
import { Input } from '../../../shared/view/ui'; import { Input } from '../../../shared/view/ui';
import FileContextMenu from './FileContextMenu';
type FileTreeNodeProps = { type FileTreeNodeProps = {
item: FileTreeNodeType; item: FileTreeNodeType;
@@ -40,7 +40,7 @@ type TreeItemIconProps = {
function TreeItemIcon({ item, isOpen, renderFileIcon }: TreeItemIconProps) { function TreeItemIcon({ item, isOpen, renderFileIcon }: TreeItemIconProps) {
if (item.type === 'directory') { if (item.type === 'directory') {
return ( return (
<span className="flex items-center gap-0.5 flex-shrink-0"> <span className="flex flex-shrink-0 items-center gap-0.5">
<ChevronRight <ChevronRight
className={cn( className={cn(
'w-3.5 h-3.5 text-muted-foreground/70 transition-transform duration-150', 'w-3.5 h-3.5 text-muted-foreground/70 transition-transform duration-150',
@@ -48,15 +48,15 @@ function TreeItemIcon({ item, isOpen, renderFileIcon }: TreeItemIconProps) {
)} )}
/> />
{isOpen ? ( {isOpen ? (
<FolderOpen className="w-4 h-4 text-blue-500 flex-shrink-0" /> <FolderOpen className="h-4 w-4 flex-shrink-0 text-blue-500" />
) : ( ) : (
<Folder className="w-4 h-4 text-muted-foreground flex-shrink-0" /> <Folder className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
)} )}
</span> </span>
); );
} }
return <span className="flex items-center flex-shrink-0 ml-[18px]">{renderFileIcon(item.name)}</span>; return <span className="ml-[18px] flex flex-shrink-0 items-center">{renderFileIcon(item.name)}</span>;
} }
export default function FileTreeNode({ export default function FileTreeNode({
@@ -128,7 +128,7 @@ export default function FileTreeNode({
handleConfirmRename(); handleConfirmRename();
}, 100); }, 100);
}} }}
className="h-6 text-sm flex-1" className="h-6 flex-1 text-sm"
disabled={operationLoading} disabled={operationLoading}
/> />
</div> </div>
@@ -143,23 +143,23 @@ export default function FileTreeNode({
> >
{viewMode === 'detailed' ? ( {viewMode === 'detailed' ? (
<> <>
<div className="col-span-5 flex items-center gap-1.5 min-w-0"> <div className="col-span-5 flex min-w-0 items-center gap-1.5">
<TreeItemIcon item={item} isOpen={isOpen} renderFileIcon={renderFileIcon} /> <TreeItemIcon item={item} isOpen={isOpen} renderFileIcon={renderFileIcon} />
<span className={nameClassName}>{item.name}</span> <span className={nameClassName}>{item.name}</span>
</div> </div>
<div className="col-span-2 text-sm text-muted-foreground tabular-nums"> <div className="col-span-2 text-sm tabular-nums text-muted-foreground">
{item.type === 'file' ? formatFileSize(item.size) : ''} {item.type === 'file' ? formatFileSize(item.size) : ''}
</div> </div>
<div className="col-span-3 text-sm text-muted-foreground">{formatRelativeTime(item.modified)}</div> <div className="col-span-3 text-sm text-muted-foreground">{formatRelativeTime(item.modified)}</div>
<div className="col-span-2 text-sm text-muted-foreground font-mono">{item.permissionsRwx || ''}</div> <div className="col-span-2 font-mono text-sm text-muted-foreground">{item.permissionsRwx || ''}</div>
</> </>
) : viewMode === 'compact' ? ( ) : viewMode === 'compact' ? (
<> <>
<div className="flex items-center gap-1.5 min-w-0"> <div className="flex min-w-0 items-center gap-1.5">
<TreeItemIcon item={item} isOpen={isOpen} renderFileIcon={renderFileIcon} /> <TreeItemIcon item={item} isOpen={isOpen} renderFileIcon={renderFileIcon} />
<span className={nameClassName}>{item.name}</span> <span className={nameClassName}>{item.name}</span>
</div> </div>
<div className="flex items-center gap-3 text-sm text-muted-foreground flex-shrink-0 ml-2"> <div className="ml-2 flex flex-shrink-0 items-center gap-3 text-sm text-muted-foreground">
{item.type === 'file' && ( {item.type === 'file' && (
<> <>
<span className="tabular-nums">{formatFileSize(item.size)}</span> <span className="tabular-nums">{formatFileSize(item.size)}</span>
@@ -202,7 +202,7 @@ export default function FileTreeNode({
{isDirectory && isOpen && hasChildren && ( {isDirectory && isOpen && hasChildren && (
<div className="relative"> <div className="relative">
<span <span
className="absolute top-0 bottom-0 border-l border-border/40" className="absolute bottom-0 top-0 border-l border-border/40"
style={{ left: `${level * 16 + 14}px` }} style={{ left: `${level * 16 + 14}px` }}
aria-hidden="true" aria-hidden="true"
/> />

View File

@@ -58,16 +58,16 @@ export default function ImageViewer({ file, onClose }: ImageViewerProps) {
}, [imagePath]); }, [imagePath]);
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl max-h-[90vh] w-full mx-4 overflow-hidden"> <div className="mx-4 max-h-[90vh] w-full max-w-4xl overflow-hidden rounded-lg bg-white shadow-xl dark:bg-gray-800">
<div className="flex items-center justify-between p-4 border-b"> <div className="flex items-center justify-between border-b p-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{file.name}</h3> <h3 className="text-lg font-semibold text-gray-900 dark:text-white">{file.name}</h3>
<Button variant="ghost" size="sm" onClick={onClose} className="h-8 w-8 p-0"> <Button variant="ghost" size="sm" onClick={onClose} className="h-8 w-8 p-0">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
</div> </div>
<div className="p-4 flex justify-center items-center bg-gray-50 dark:bg-gray-900 min-h-[400px]"> <div className="flex min-h-[400px] items-center justify-center bg-gray-50 p-4 dark:bg-gray-900">
{loading && ( {loading && (
<div className="text-center text-gray-500 dark:text-gray-400"> <div className="text-center text-gray-500 dark:text-gray-400">
<p>Loading image...</p> <p>Loading image...</p>
@@ -77,18 +77,18 @@ export default function ImageViewer({ file, onClose }: ImageViewerProps) {
<img <img
src={imageUrl} src={imageUrl}
alt={file.name} alt={file.name}
className="max-w-full max-h-[70vh] object-contain rounded-lg shadow-md" className="max-h-[70vh] max-w-full rounded-lg object-contain shadow-md"
/> />
)} )}
{!loading && !imageUrl && ( {!loading && !imageUrl && (
<div className="text-center text-gray-500 dark:text-gray-400"> <div className="text-center text-gray-500 dark:text-gray-400">
<p>{error || 'Unable to load image'}</p> <p>{error || 'Unable to load image'}</p>
<p className="text-sm mt-2 break-all">{file.path}</p> <p className="mt-2 break-all text-sm">{file.path}</p>
</div> </div>
)} )}
</div> </div>
<div className="p-4 border-t bg-gray-50 dark:bg-gray-800"> <div className="border-t bg-gray-50 p-4 dark:bg-gray-800">
<p className="text-sm text-gray-600 dark:text-gray-400">{file.path}</p> <p className="text-sm text-gray-600 dark:text-gray-400">{file.path}</p>
</div> </div>
</div> </div>

View File

@@ -66,14 +66,14 @@ export default function GitPanel({ selectedProject, isMobile = false, onFileOpen
if (!selectedProject) { if (!selectedProject) {
return ( return (
<div className="h-full flex items-center justify-center text-muted-foreground"> <div className="flex h-full items-center justify-center text-muted-foreground">
<p>Select a project to view source control</p> <p>Select a project to view source control</p>
</div> </div>
); );
} }
return ( return (
<div className="h-full flex flex-col bg-background"> <div className="flex h-full flex-col bg-background">
<GitPanelHeader <GitPanelHeader
isMobile={isMobile} isMobile={isMobile}
currentBranch={currentBranch} currentBranch={currentBranch}

View File

@@ -113,9 +113,9 @@ export default function GitPanelHeader({
<div className="relative" ref={dropdownRef}> <div className="relative" ref={dropdownRef}>
<button <button
onClick={() => setShowBranchDropdown((previous) => !previous)} onClick={() => setShowBranchDropdown((previous) => !previous)}
className={`flex items-center hover:bg-accent rounded-lg transition-colors ${isMobile ? 'space-x-1 px-2 py-1' : 'space-x-2 px-3 py-1.5'}`} className={`flex items-center rounded-lg transition-colors hover:bg-accent ${isMobile ? 'space-x-1 px-2 py-1' : 'space-x-2 px-3 py-1.5'}`}
> >
<GitBranch className={`text-muted-foreground ${isMobile ? 'w-3 h-3' : 'w-4 h-4'}`} /> <GitBranch className={`text-muted-foreground ${isMobile ? 'h-3 w-3' : 'h-4 w-4'}`} />
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<span className={`font-medium ${isMobile ? 'text-xs' : 'text-sm'}`}>{currentBranch}</span> <span className={`font-medium ${isMobile ? 'text-xs' : 'text-sm'}`}>{currentBranch}</span>
{remoteStatus?.hasRemote && ( {remoteStatus?.hasRemote && (
@@ -146,22 +146,22 @@ export default function GitPanelHeader({
</span> </span>
)} )}
</span> </span>
<ChevronDown className={`w-3 h-3 text-muted-foreground transition-transform ${showBranchDropdown ? 'rotate-180' : ''}`} /> <ChevronDown className={`h-3 w-3 text-muted-foreground transition-transform ${showBranchDropdown ? 'rotate-180' : ''}`} />
</button> </button>
{showBranchDropdown && ( {showBranchDropdown && (
<div className="absolute top-full left-0 mt-1 w-64 bg-card rounded-xl shadow-lg border border-border z-50 overflow-hidden"> <div className="absolute left-0 top-full z-50 mt-1 w-64 overflow-hidden rounded-xl border border-border bg-card shadow-lg">
<div className="py-1 max-h-64 overflow-y-auto"> <div className="max-h-64 overflow-y-auto py-1">
{branches.map((branch) => ( {branches.map((branch) => (
<button <button
key={branch} key={branch}
onClick={() => void handleSwitchBranch(branch)} onClick={() => void handleSwitchBranch(branch)}
className={`w-full text-left px-4 py-2 text-sm hover:bg-accent transition-colors ${ className={`w-full px-4 py-2 text-left text-sm transition-colors hover:bg-accent ${
branch === currentBranch ? 'bg-accent/50 text-foreground' : 'text-muted-foreground' branch === currentBranch ? 'bg-accent/50 text-foreground' : 'text-muted-foreground'
}`} }`}
> >
<span className="flex items-center space-x-2"> <span className="flex items-center space-x-2">
{branch === currentBranch && <Check className="w-3 h-3 text-primary" />} {branch === currentBranch && <Check className="h-3 w-3 text-primary" />}
<span className={branch === currentBranch ? 'font-medium' : ''}>{branch}</span> <span className={branch === currentBranch ? 'font-medium' : ''}>{branch}</span>
</span> </span>
</button> </button>
@@ -173,9 +173,9 @@ export default function GitPanelHeader({
setShowNewBranchModal(true); setShowNewBranchModal(true);
setShowBranchDropdown(false); setShowBranchDropdown(false);
}} }}
className="w-full text-left px-4 py-2 text-sm hover:bg-accent transition-colors flex items-center space-x-2" className="flex w-full items-center space-x-2 px-4 py-2 text-left text-sm transition-colors hover:bg-accent"
> >
<Plus className="w-3 h-3" /> <Plus className="h-3 w-3" />
<span>Create new branch</span> <span>Create new branch</span>
</button> </button>
</div> </div>
@@ -190,10 +190,10 @@ export default function GitPanelHeader({
<button <button
onClick={requestPublishConfirmation} onClick={requestPublishConfirmation}
disabled={isPublishing} disabled={isPublishing}
className="px-2.5 py-1 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 flex items-center gap-1 transition-colors" className="flex items-center gap-1 rounded-lg bg-purple-600 px-2.5 py-1 text-sm text-white transition-colors hover:bg-purple-700 disabled:opacity-50"
title={`Publish branch "${currentBranch}" to ${remoteName}`} title={`Publish branch "${currentBranch}" to ${remoteName}`}
> >
<Upload className={`w-3 h-3 ${isPublishing ? 'animate-pulse' : ''}`} /> <Upload className={`h-3 w-3 ${isPublishing ? 'animate-pulse' : ''}`} />
<span>{isPublishing ? 'Publishing...' : 'Publish'}</span> <span>{isPublishing ? 'Publishing...' : 'Publish'}</span>
</button> </button>
)} )}
@@ -204,10 +204,10 @@ export default function GitPanelHeader({
<button <button
onClick={requestPullConfirmation} onClick={requestPullConfirmation}
disabled={isPulling} disabled={isPulling}
className="px-2.5 py-1 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 flex items-center gap-1 transition-colors" className="flex items-center gap-1 rounded-lg bg-green-600 px-2.5 py-1 text-sm text-white transition-colors hover:bg-green-700 disabled:opacity-50"
title={`Pull ${behindCount} commit${behindCount !== 1 ? 's' : ''} from ${remoteName}`} title={`Pull ${behindCount} commit${behindCount !== 1 ? 's' : ''} from ${remoteName}`}
> >
<Download className={`w-3 h-3 ${isPulling ? 'animate-pulse' : ''}`} /> <Download className={`h-3 w-3 ${isPulling ? 'animate-pulse' : ''}`} />
<span>{isPulling ? 'Pulling...' : `Pull ${behindCount}`}</span> <span>{isPulling ? 'Pulling...' : `Pull ${behindCount}`}</span>
</button> </button>
)} )}
@@ -216,10 +216,10 @@ export default function GitPanelHeader({
<button <button
onClick={requestPushConfirmation} onClick={requestPushConfirmation}
disabled={isPushing} disabled={isPushing}
className="px-2.5 py-1 text-sm bg-orange-600 text-white rounded-lg hover:bg-orange-700 disabled:opacity-50 flex items-center gap-1 transition-colors" className="flex items-center gap-1 rounded-lg bg-orange-600 px-2.5 py-1 text-sm text-white transition-colors hover:bg-orange-700 disabled:opacity-50"
title={`Push ${aheadCount} commit${aheadCount !== 1 ? 's' : ''} to ${remoteName}`} title={`Push ${aheadCount} commit${aheadCount !== 1 ? 's' : ''} to ${remoteName}`}
> >
<Upload className={`w-3 h-3 ${isPushing ? 'animate-pulse' : ''}`} /> <Upload className={`h-3 w-3 ${isPushing ? 'animate-pulse' : ''}`} />
<span>{isPushing ? 'Pushing...' : `Push ${aheadCount}`}</span> <span>{isPushing ? 'Pushing...' : `Push ${aheadCount}`}</span>
</button> </button>
)} )}
@@ -228,10 +228,10 @@ export default function GitPanelHeader({
<button <button
onClick={() => void handleFetch()} onClick={() => void handleFetch()}
disabled={isFetching} disabled={isFetching}
className="px-2.5 py-1 text-sm bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 flex items-center gap-1 transition-colors" className="flex items-center gap-1 rounded-lg bg-primary px-2.5 py-1 text-sm text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
title={`Fetch from ${remoteName}`} title={`Fetch from ${remoteName}`}
> >
<RefreshCw className={`w-3 h-3 ${isFetching ? 'animate-spin' : ''}`} /> <RefreshCw className={`h-3 w-3 ${isFetching ? 'animate-spin' : ''}`} />
<span>{isFetching ? 'Fetching...' : 'Fetch'}</span> <span>{isFetching ? 'Fetching...' : 'Fetch'}</span>
</button> </button>
)} )}
@@ -243,10 +243,10 @@ export default function GitPanelHeader({
<button <button
onClick={onRefresh} onClick={onRefresh}
disabled={isLoading} disabled={isLoading}
className={`hover:bg-accent rounded-lg transition-colors ${isMobile ? 'p-1' : 'p-1.5'}`} className={`rounded-lg transition-colors hover:bg-accent ${isMobile ? 'p-1' : 'p-1.5'}`}
title="Refresh git status" title="Refresh git status"
> >
<RefreshCw className={`text-muted-foreground ${isLoading ? 'animate-spin' : ''} ${isMobile ? 'w-3 h-3' : 'w-4 h-4'}`} /> <RefreshCw className={`text-muted-foreground ${isLoading ? 'animate-spin' : ''} ${isMobile ? 'h-3 w-3' : 'h-4 w-4'}`} />
</button> </button>
</div> </div>
</div> </div>

View File

@@ -7,18 +7,18 @@ type GitRepositoryErrorStateProps = {
export default function GitRepositoryErrorState({ error, details }: GitRepositoryErrorStateProps) { export default function GitRepositoryErrorState({ error, details }: GitRepositoryErrorStateProps) {
return ( return (
<div className="flex-1 flex flex-col items-center justify-center text-muted-foreground px-6 py-12"> <div className="flex flex-1 flex-col items-center justify-center px-6 py-12 text-muted-foreground">
<div className="w-16 h-16 rounded-2xl bg-muted/50 flex items-center justify-center mb-6"> <div className="mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-muted/50">
<GitBranch className="w-8 h-8 opacity-40" /> <GitBranch className="h-8 w-8 opacity-40" />
</div> </div>
<h3 className="text-lg font-medium mb-3 text-center text-foreground">{error}</h3> <h3 className="mb-3 text-center text-lg font-medium text-foreground">{error}</h3>
{details && ( {details && (
<p className="text-sm text-center leading-relaxed mb-6 max-w-md">{details}</p> <p className="mb-6 max-w-md text-center text-sm leading-relaxed">{details}</p>
)} )}
<div className="p-4 bg-primary/5 rounded-xl border border-primary/10 max-w-md"> <div className="max-w-md rounded-xl border border-primary/10 bg-primary/5 p-4">
<p className="text-sm text-primary text-center"> <p className="text-center text-sm text-primary">
<strong>Tip:</strong> Run{' '} <strong>Tip:</strong> Run{' '}
<code className="bg-primary/10 px-2 py-1 rounded-md font-mono text-xs">git init</code>{' '} <code className="rounded-md bg-primary/10 px-2 py-1 font-mono text-xs">git init</code>{' '}
in your project directory to initialize git source control. in your project directory to initialize git source control.
</p> </p>
</div> </div>

View File

@@ -11,19 +11,19 @@ export default function GitViewTabs({ activeView, isHidden, onChange }: GitViewT
return ( return (
<div <div
className={`flex border-b border-border/60 transition-all duration-300 ease-in-out ${ className={`flex border-b border-border/60 transition-all duration-300 ease-in-out ${
isHidden ? 'max-h-0 opacity-0 -translate-y-2 overflow-hidden' : 'max-h-16 opacity-100 translate-y-0' isHidden ? 'max-h-0 -translate-y-2 overflow-hidden opacity-0' : 'max-h-16 translate-y-0 opacity-100'
}`} }`}
> >
<button <button
onClick={() => onChange('changes')} onClick={() => onChange('changes')}
className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${ className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
activeView === 'changes' activeView === 'changes'
? 'text-primary border-b-2 border-primary' ? 'border-b-2 border-primary text-primary'
: 'text-muted-foreground hover:text-foreground' : 'text-muted-foreground hover:text-foreground'
}`} }`}
> >
<span className="flex items-center justify-center gap-2"> <span className="flex items-center justify-center gap-2">
<FileText className="w-4 h-4" /> <FileText className="h-4 w-4" />
<span>Changes</span> <span>Changes</span>
</span> </span>
</button> </button>
@@ -31,12 +31,12 @@ export default function GitViewTabs({ activeView, isHidden, onChange }: GitViewT
onClick={() => onChange('history')} onClick={() => onChange('history')}
className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${ className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
activeView === 'history' activeView === 'history'
? 'text-primary border-b-2 border-primary' ? 'border-b-2 border-primary text-primary'
: 'text-muted-foreground hover:text-foreground' : 'text-muted-foreground hover:text-foreground'
}`} }`}
> >
<span className="flex items-center justify-center gap-2"> <span className="flex items-center justify-center gap-2">
<History className="w-4 h-4" /> <History className="h-4 w-4" />
<span>History</span> <span>History</span>
</span> </span>
</button> </button>

View File

@@ -153,39 +153,39 @@ export default function ChangesView({
<div className={`flex-1 overflow-y-auto ${isMobile ? 'pb-mobile-nav' : ''}`}> <div className={`flex-1 overflow-y-auto ${isMobile ? 'pb-mobile-nav' : ''}`}>
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-32"> <div className="flex h-32 items-center justify-center">
<RefreshCw className="w-5 h-5 animate-spin text-muted-foreground" /> <RefreshCw className="h-5 w-5 animate-spin text-muted-foreground" />
</div> </div>
) : gitStatus?.hasCommits === false ? ( ) : gitStatus?.hasCommits === false ? (
<div className="flex flex-col items-center justify-center p-8 text-center"> <div className="flex flex-col items-center justify-center p-8 text-center">
<div className="w-14 h-14 rounded-2xl bg-muted/50 flex items-center justify-center mb-4"> <div className="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-muted/50">
<GitBranch className="w-7 h-7 text-muted-foreground/50" /> <GitBranch className="h-7 w-7 text-muted-foreground/50" />
</div> </div>
<h3 className="text-lg font-medium mb-2 text-foreground">No commits yet</h3> <h3 className="mb-2 text-lg font-medium text-foreground">No commits yet</h3>
<p className="text-sm text-muted-foreground mb-6 max-w-md"> <p className="mb-6 max-w-md text-sm text-muted-foreground">
This repository doesn&apos;t have any commits yet. Create your first commit to start tracking changes. This repository doesn&apos;t have any commits yet. Create your first commit to start tracking changes.
</p> </p>
<button <button
onClick={() => void onCreateInitialCommit()} onClick={() => void onCreateInitialCommit()}
disabled={isCreatingInitialCommit} disabled={isCreatingInitialCommit}
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 transition-colors" className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
> >
{isCreatingInitialCommit ? ( {isCreatingInitialCommit ? (
<> <>
<RefreshCw className="w-4 h-4 animate-spin" /> <RefreshCw className="h-4 w-4 animate-spin" />
<span>Creating Initial Commit...</span> <span>Creating Initial Commit...</span>
</> </>
) : ( ) : (
<> <>
<GitCommit className="w-4 h-4" /> <GitCommit className="h-4 w-4" />
<span>Create Initial Commit</span> <span>Create Initial Commit</span>
</> </>
)} )}
</button> </button>
</div> </div>
) : !gitStatus || !hasChangedFiles(gitStatus) ? ( ) : !gitStatus || !hasChangedFiles(gitStatus) ? (
<div className="flex flex-col items-center justify-center h-32 text-muted-foreground"> <div className="flex h-32 flex-col items-center justify-center text-muted-foreground">
<GitCommit className="w-10 h-10 mb-2 opacity-40" /> <GitCommit className="mb-2 h-10 w-10 opacity-40" />
<p className="text-sm">No changes detected</p> <p className="text-sm">No changes detected</p>
</div> </div>
) : ( ) : (

View File

@@ -77,30 +77,30 @@ export default function CommitComposer({
return ( return (
<div <div
className={`transition-all duration-300 ease-in-out ${ className={`transition-all duration-300 ease-in-out ${
isHidden ? 'max-h-0 opacity-0 -translate-y-2 overflow-hidden' : 'max-h-96 opacity-100 translate-y-0' isHidden ? 'max-h-0 -translate-y-2 overflow-hidden opacity-0' : 'max-h-96 translate-y-0 opacity-100'
}`} }`}
> >
{isMobile && isCollapsed ? ( {isMobile && isCollapsed ? (
<div className="px-4 py-2 border-b border-border/60"> <div className="border-b border-border/60 px-4 py-2">
<button <button
onClick={() => setIsCollapsed(false)} onClick={() => setIsCollapsed(false)}
className="w-full flex items-center justify-center gap-2 px-3 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors" className="flex w-full items-center justify-center gap-2 rounded-lg bg-primary px-3 py-2 text-sm text-primary-foreground transition-colors hover:bg-primary/90"
> >
<GitCommit className="w-4 h-4" /> <GitCommit className="h-4 w-4" />
<span>Commit {selectedFileCount} file{selectedFileCount !== 1 ? 's' : ''}</span> <span>Commit {selectedFileCount} file{selectedFileCount !== 1 ? 's' : ''}</span>
<ChevronDown className="w-3 h-3" /> <ChevronDown className="h-3 w-3" />
</button> </button>
</div> </div>
) : ( ) : (
<div className="px-4 py-3 border-b border-border/60"> <div className="border-b border-border/60 px-4 py-3">
{isMobile && ( {isMobile && (
<div className="flex items-center justify-between mb-2"> <div className="mb-2 flex items-center justify-between">
<span className="text-sm font-medium text-foreground">Commit Changes</span> <span className="text-sm font-medium text-foreground">Commit Changes</span>
<button <button
onClick={() => setIsCollapsed(true)} onClick={() => setIsCollapsed(true)}
className="p-1 hover:bg-accent rounded-lg transition-colors" className="rounded-lg p-1 transition-colors hover:bg-accent"
> >
<ChevronDown className="w-4 h-4 rotate-180" /> <ChevronDown className="h-4 w-4 rotate-180" />
</button> </button>
</div> </div>
)} )}
@@ -110,7 +110,7 @@ export default function CommitComposer({
value={commitMessage} value={commitMessage}
onChange={(event) => setCommitMessage(event.target.value)} onChange={(event) => setCommitMessage(event.target.value)}
placeholder="Message (Ctrl+Enter to commit)" placeholder="Message (Ctrl+Enter to commit)"
className="w-full px-3 py-2 text-sm border border-border rounded-xl bg-background text-foreground placeholder:text-muted-foreground resize-none pr-20 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/30" className="w-full resize-none rounded-xl border border-border bg-background px-3 py-2 pr-20 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary/30 focus:outline-none focus:ring-2 focus:ring-primary/20"
rows={3} rows={3}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
@@ -123,13 +123,13 @@ export default function CommitComposer({
<button <button
onClick={() => void handleGenerateMessage()} onClick={() => void handleGenerateMessage()}
disabled={selectedFileCount === 0 || isGeneratingMessage} disabled={selectedFileCount === 0 || isGeneratingMessage}
className="p-1.5 text-muted-foreground hover:text-foreground disabled:opacity-50 disabled:cursor-not-allowed transition-colors" className="p-1.5 text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
title="Generate commit message" title="Generate commit message"
> >
{isGeneratingMessage ? ( {isGeneratingMessage ? (
<RefreshCw className="w-4 h-4 animate-spin" /> <RefreshCw className="h-4 w-4 animate-spin" />
) : ( ) : (
<Sparkles className="w-4 h-4" /> <Sparkles className="h-4 w-4" />
)} )}
</button> </button>
<div style={{ display: 'none' }}> <div style={{ display: 'none' }}>
@@ -142,16 +142,16 @@ export default function CommitComposer({
</div> </div>
</div> </div>
<div className="flex items-center justify-between mt-2"> <div className="mt-2 flex items-center justify-between">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{selectedFileCount} file{selectedFileCount !== 1 ? 's' : ''} selected {selectedFileCount} file{selectedFileCount !== 1 ? 's' : ''} selected
</span> </span>
<button <button
onClick={requestCommitConfirmation} onClick={requestCommitConfirmation}
disabled={!commitMessage.trim() || selectedFileCount === 0 || isCommitting} disabled={!commitMessage.trim() || selectedFileCount === 0 || isCommitting}
className="px-3 py-1.5 text-sm bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1 transition-colors" className="flex items-center space-x-1 rounded-lg bg-primary px-3 py-1.5 text-sm text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
> >
<Check className="w-3 h-3" /> <Check className="h-3 w-3" />
<span>{isCommitting ? 'Committing...' : 'Commit'}</span> <span>{isCommitting ? 'Committing...' : 'Commit'}</span>
</button> </button>
</div> </div>

View File

@@ -37,25 +37,25 @@ export default function FileChangeItem({
return ( return (
<div className="border-b border-border last:border-0"> <div className="border-b border-border last:border-0">
<div className={`flex items-center hover:bg-accent/50 transition-colors ${isMobile ? 'px-2 py-1.5' : 'px-3 py-2'}`}> <div className={`flex items-center transition-colors hover:bg-accent/50 ${isMobile ? 'px-2 py-1.5' : 'px-3 py-2'}`}>
<input <input
type="checkbox" type="checkbox"
checked={isSelected} checked={isSelected}
onChange={() => onToggleSelected(filePath)} onChange={() => onToggleSelected(filePath)}
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
className={`rounded border-border text-primary focus:ring-primary/40 bg-background checked:bg-primary ${isMobile ? 'mr-1.5' : 'mr-2'}`} className={`rounded border-border bg-background text-primary checked:bg-primary focus:ring-primary/40 ${isMobile ? 'mr-1.5' : 'mr-2'}`}
/> />
<div className="flex items-center flex-1 min-w-0"> <div className="flex min-w-0 flex-1 items-center">
<button <button
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
onToggleExpanded(filePath); onToggleExpanded(filePath);
}} }}
className={`p-0.5 hover:bg-accent rounded cursor-pointer ${isMobile ? 'mr-1' : 'mr-2'}`} className={`cursor-pointer rounded p-0.5 hover:bg-accent ${isMobile ? 'mr-1' : 'mr-2'}`}
title={isExpanded ? 'Collapse diff' : 'Expand diff'} title={isExpanded ? 'Collapse diff' : 'Expand diff'}
> >
<ChevronRight className={`w-3 h-3 transition-transform duration-200 ease-in-out ${isExpanded ? 'rotate-90' : 'rotate-0'}`} /> <ChevronRight className={`h-3 w-3 transition-transform duration-200 ease-in-out ${isExpanded ? 'rotate-90' : 'rotate-0'}`} />
</button> </button>
<span <span
@@ -76,16 +76,16 @@ export default function FileChangeItem({
event.stopPropagation(); event.stopPropagation();
onRequestFileAction(filePath, status); onRequestFileAction(filePath, status);
}} }}
className={`${isMobile ? 'px-2 py-1 text-xs' : 'p-1'} hover:bg-destructive/10 rounded text-destructive font-medium flex items-center gap-1`} className={`${isMobile ? 'px-2 py-1 text-xs' : 'p-1'} flex items-center gap-1 rounded font-medium text-destructive hover:bg-destructive/10`}
title={status === 'U' ? 'Delete untracked file' : 'Discard changes'} title={status === 'U' ? 'Delete untracked file' : 'Discard changes'}
> >
<Trash2 className="w-3 h-3" /> <Trash2 className="h-3 w-3" />
{isMobile && <span>{status === 'U' ? 'Delete' : 'Discard'}</span>} {isMobile && <span>{status === 'U' ? 'Delete' : 'Discard'}</span>}
</button> </button>
)} )}
<span <span
className={`inline-flex items-center justify-center w-5 h-5 rounded text-[10px] font-bold border ${badgeClass}`} className={`inline-flex h-5 w-5 items-center justify-center rounded border text-[10px] font-bold ${badgeClass}`}
title={statusLabel} title={statusLabel}
> >
{status} {status}
@@ -95,12 +95,12 @@ export default function FileChangeItem({
</div> </div>
<div <div
className={`bg-muted/50 transition-all duration-400 ease-in-out overflow-hidden ${isExpanded && diff ? 'max-h-[600px] opacity-100 translate-y-0' : 'max-h-0 opacity-0 -translate-y-1' className={`duration-400 overflow-hidden bg-muted/50 transition-all ease-in-out ${isExpanded && diff ? 'max-h-[600px] translate-y-0 opacity-100' : 'max-h-0 -translate-y-1 opacity-0'
}`} }`}
> >
<div className="flex items-center justify-between p-2 border-b border-border"> <div className="flex items-center justify-between border-b border-border p-2">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<span className={`inline-flex items-center justify-center w-5 h-5 rounded text-[10px] font-bold border ${badgeClass}`}> <span className={`inline-flex h-5 w-5 items-center justify-center rounded border text-[10px] font-bold ${badgeClass}`}>
{status} {status}
</span> </span>
<span className="text-sm font-medium text-foreground">{statusLabel}</span> <span className="text-sm font-medium text-foreground">{statusLabel}</span>
@@ -111,7 +111,7 @@ export default function FileChangeItem({
event.stopPropagation(); event.stopPropagation();
onToggleWrapText(); onToggleWrapText();
}} }}
className="text-sm text-muted-foreground hover:text-foreground transition-colors" className="text-sm text-muted-foreground transition-colors hover:text-foreground"
title={wrapText ? 'Switch to horizontal scroll' : 'Switch to text wrap'} title={wrapText ? 'Switch to horizontal scroll' : 'Switch to text wrap'}
> >
{wrapText ? 'Scroll' : 'Wrap'} {wrapText ? 'Scroll' : 'Wrap'}

View File

@@ -17,9 +17,9 @@ export default function FileSelectionControls({
}: FileSelectionControlsProps) { }: FileSelectionControlsProps) {
return ( return (
<div <div
className={`border-b border-border/60 flex items-center justify-between transition-all duration-300 ease-in-out ${ className={`flex items-center justify-between border-b border-border/60 transition-all duration-300 ease-in-out ${
isMobile ? 'px-3 py-1.5' : 'px-4 py-2' isMobile ? 'px-3 py-1.5' : 'px-4 py-2'
} ${isHidden ? 'max-h-0 opacity-0 -translate-y-2 overflow-hidden' : 'max-h-16 opacity-100 translate-y-0'}`} } ${isHidden ? 'max-h-0 -translate-y-2 overflow-hidden opacity-0' : 'max-h-16 translate-y-0 opacity-100'}`}
> >
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{selectedCount} of {totalCount} {isMobile ? '' : 'files'} selected {selectedCount} of {totalCount} {isMobile ? '' : 'files'} selected
@@ -27,14 +27,14 @@ export default function FileSelectionControls({
<span className={`flex ${isMobile ? 'gap-1' : 'gap-2'}`}> <span className={`flex ${isMobile ? 'gap-1' : 'gap-2'}`}>
<button <button
onClick={onSelectAll} onClick={onSelectAll}
className="text-sm text-primary hover:text-primary/80 transition-colors" className="text-sm text-primary transition-colors hover:text-primary/80"
> >
{isMobile ? 'All' : 'Select All'} {isMobile ? 'All' : 'Select All'}
</button> </button>
<span className="text-border">|</span> <span className="text-border">|</span>
<button <button
onClick={onDeselectAll} onClick={onDeselectAll}
className="text-sm text-primary hover:text-primary/80 transition-colors" className="text-sm text-primary transition-colors hover:text-primary/80"
> >
{isMobile ? 'None' : 'Deselect All'} {isMobile ? 'None' : 'Deselect All'}
</button> </button>

View File

@@ -24,24 +24,24 @@ export default function FileStatusLegend({ isMobile }: FileStatusLegendProps) {
<div className="border-b border-border/60"> <div className="border-b border-border/60">
<button <button
onClick={() => setIsOpen((previous) => !previous)} onClick={() => setIsOpen((previous) => !previous)}
className="w-full px-4 py-2 bg-muted/30 hover:bg-muted/50 text-sm text-muted-foreground flex items-center justify-center gap-1 transition-colors" className="flex w-full items-center justify-center gap-1 bg-muted/30 px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted/50"
> >
<Info className="w-3 h-3" /> <Info className="h-3 w-3" />
<span>File Status Guide</span> <span>File Status Guide</span>
{isOpen ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />} {isOpen ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
</button> </button>
{isOpen && ( {isOpen && (
<div className="px-4 py-3 bg-muted/30 text-sm"> <div className="bg-muted/30 px-4 py-3 text-sm">
<div className="flex justify-center gap-6"> <div className="flex justify-center gap-6">
{LEGEND_ITEMS.map((item) => ( {LEGEND_ITEMS.map((item) => (
<span key={item.status} className="flex items-center gap-2"> <span key={item.status} className="flex items-center gap-2">
<span <span
className={`inline-flex items-center justify-center w-5 h-5 rounded border font-bold text-[10px] ${getStatusBadgeClass(item.status)}`} className={`inline-flex h-5 w-5 items-center justify-center rounded border text-[10px] font-bold ${getStatusBadgeClass(item.status)}`}
> >
{item.status} {item.status}
</span> </span>
<span className="text-muted-foreground italic">{item.label}</span> <span className="italic text-muted-foreground">{item.label}</span>
</span> </span>
))} ))}
</div> </div>

View File

@@ -1,5 +1,4 @@
import { ChevronDown, ChevronRight } from 'lucide-react'; import { ChevronDown, ChevronRight } from 'lucide-react';
import type { GitCommitSummary } from '../../types/types'; import type { GitCommitSummary } from '../../types/types';
import GitDiffViewer from '../shared/GitDiffViewer'; import GitDiffViewer from '../shared/GitDiffViewer';
@@ -26,23 +25,23 @@ export default function CommitHistoryItem({
<button <button
type="button" type="button"
aria-expanded={isExpanded} aria-expanded={isExpanded}
className="w-full flex items-start p-3 hover:bg-accent/50 cursor-pointer transition-colors text-left bg-transparent border-0" className="flex w-full cursor-pointer items-start border-0 bg-transparent p-3 text-left transition-colors hover:bg-accent/50"
onClick={onToggle} onClick={onToggle}
> >
<span className="mr-2 mt-1 p-0.5 hover:bg-accent rounded"> <span className="mr-2 mt-1 rounded p-0.5 hover:bg-accent">
{isExpanded ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />} {isExpanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
</span> </span>
<div className="flex-1 min-w-0"> <div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0"> <div className="min-w-0 flex-1">
<p className="text-sm font-medium text-foreground truncate">{commit.message}</p> <p className="truncate text-sm font-medium text-foreground">{commit.message}</p>
<p className="text-sm text-muted-foreground mt-1"> <p className="mt-1 text-sm text-muted-foreground">
{commit.author} {commit.author}
{' \u2022 '} {' \u2022 '}
{commit.date} {commit.date}
</p> </p>
</div> </div>
<span className="text-sm font-mono text-muted-foreground/60 flex-shrink-0"> <span className="flex-shrink-0 font-mono text-sm text-muted-foreground/60">
{commit.hash.substring(0, 7)} {commit.hash.substring(0, 7)}
</span> </span>
</div> </div>
@@ -52,7 +51,7 @@ export default function CommitHistoryItem({
{isExpanded && diff && ( {isExpanded && diff && (
<div className="bg-muted/50"> <div className="bg-muted/50">
<div className="max-h-96 overflow-y-auto p-2"> <div className="max-h-96 overflow-y-auto p-2">
<div className="text-sm font-mono text-muted-foreground mb-2"> <div className="mb-2 font-mono text-sm text-muted-foreground">
{commit.stats} {commit.stats}
</div> </div>
<GitDiffViewer diff={diff} isMobile={isMobile} wrapText={wrapText} /> <GitDiffViewer diff={diff} isMobile={isMobile} wrapText={wrapText} />

View File

@@ -49,12 +49,12 @@ export default function HistoryView({
return ( return (
<div className={`flex-1 overflow-y-auto ${isMobile ? 'pb-mobile-nav' : ''}`}> <div className={`flex-1 overflow-y-auto ${isMobile ? 'pb-mobile-nav' : ''}`}>
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-32"> <div className="flex h-32 items-center justify-center">
<RefreshCw className="w-5 h-5 animate-spin text-muted-foreground" /> <RefreshCw className="h-5 w-5 animate-spin text-muted-foreground" />
</div> </div>
) : recentCommits.length === 0 ? ( ) : recentCommits.length === 0 ? (
<div className="flex flex-col items-center justify-center h-32 text-muted-foreground"> <div className="flex h-32 flex-col items-center justify-center text-muted-foreground">
<History className="w-10 h-10 mb-2 opacity-40" /> <History className="mb-2 h-10 w-10 opacity-40" />
<p className="text-sm">No commits found</p> <p className="text-sm">No commits found</p>
</div> </div>
) : ( ) : (

View File

@@ -16,18 +16,18 @@ type ConfirmActionModalProps = {
function renderConfirmActionIcon(actionType: ConfirmationRequest['type']) { function renderConfirmActionIcon(actionType: ConfirmationRequest['type']) {
if (actionType === 'discard' || actionType === 'delete') { if (actionType === 'discard' || actionType === 'delete') {
return <Trash2 className="w-4 h-4" />; return <Trash2 className="h-4 w-4" />;
} }
if (actionType === 'commit') { if (actionType === 'commit') {
return <Check className="w-4 h-4" />; return <Check className="h-4 w-4" />;
} }
if (actionType === 'pull') { if (actionType === 'pull') {
return <Download className="w-4 h-4" />; return <Download className="h-4 w-4" />;
} }
return <Upload className="w-4 h-4" />; return <Upload className="h-4 w-4" />;
} }
export default function ConfirmActionModal({ action, onCancel, onConfirm }: ConfirmActionModalProps) { export default function ConfirmActionModal({ action, onCancel, onConfirm }: ConfirmActionModalProps) {
@@ -58,14 +58,14 @@ export default function ConfirmActionModal({ action, onCancel, onConfirm }: Conf
<div className="fixed inset-0 z-50 flex items-center justify-center p-4"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm" onClick={onCancel} /> <div className="fixed inset-0 bg-black/60 backdrop-blur-sm" onClick={onCancel} />
<div <div
className="relative bg-card border border-border rounded-xl shadow-2xl max-w-md w-full overflow-hidden" className="relative w-full max-w-md overflow-hidden rounded-xl border border-border bg-card shadow-2xl"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby={titleId} aria-labelledby={titleId}
> >
<div className="p-6"> <div className="p-6">
<div className="flex items-center mb-4"> <div className="mb-4 flex items-center">
<div className={`p-2 rounded-full mr-3 ${CONFIRMATION_ICON_CONTAINER_CLASSES[action.type]}`}> <div className={`mr-3 rounded-full p-2 ${CONFIRMATION_ICON_CONTAINER_CLASSES[action.type]}`}>
{renderConfirmActionIcon(action.type)} {renderConfirmActionIcon(action.type)}
</div> </div>
<h3 id={titleId} className="text-lg font-semibold text-foreground"> <h3 id={titleId} className="text-lg font-semibold text-foreground">
@@ -73,18 +73,18 @@ export default function ConfirmActionModal({ action, onCancel, onConfirm }: Conf
</h3> </h3>
</div> </div>
<p className="text-sm text-muted-foreground mb-6">{action.message}</p> <p className="mb-6 text-sm text-muted-foreground">{action.message}</p>
<div className="flex justify-end space-x-3"> <div className="flex justify-end space-x-3">
<button <button
onClick={onCancel} onClick={onCancel}
className="px-4 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg transition-colors" className="rounded-lg px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
> >
Cancel Cancel
</button> </button>
<button <button
onClick={onConfirm} onClick={onConfirm}
className={`px-4 py-2 text-sm text-white rounded-lg transition-colors flex items-center space-x-2 ${CONFIRMATION_BUTTON_CLASSES[action.type]}`} className={`flex items-center space-x-2 rounded-lg px-4 py-2 text-sm text-white transition-colors ${CONFIRMATION_BUTTON_CLASSES[action.type]}`}
> >
{renderConfirmActionIcon(action.type)} {renderConfirmActionIcon(action.type)}
<span>{CONFIRMATION_ACTION_LABELS[action.type]}</span> <span>{CONFIRMATION_ACTION_LABELS[action.type]}</span>

View File

@@ -51,16 +51,16 @@ export default function NewBranchModal({
<div className="fixed inset-0 z-50 flex items-center justify-center p-4"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} /> <div className="fixed inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
<div <div
className="relative bg-card border border-border rounded-xl shadow-2xl max-w-md w-full overflow-hidden" className="relative w-full max-w-md overflow-hidden rounded-xl border border-border bg-card shadow-2xl"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="new-branch-title" aria-labelledby="new-branch-title"
> >
<div className="p-6"> <div className="p-6">
<h3 className="text-lg font-semibold text-foreground mb-4">Create New Branch</h3> <h3 className="mb-4 text-lg font-semibold text-foreground">Create New Branch</h3>
<div className="mb-4"> <div className="mb-4">
<label htmlFor="git-new-branch-name" className="block text-sm font-medium text-foreground/80 mb-2"> <label htmlFor="git-new-branch-name" className="mb-2 block text-sm font-medium text-foreground/80">
Branch Name Branch Name
</label> </label>
<input <input
@@ -83,35 +83,35 @@ export default function NewBranchModal({
} }
}} }}
placeholder="feature/new-feature" placeholder="feature/new-feature"
className="w-full px-3 py-2 border border-border rounded-xl bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/30" className="w-full rounded-xl border border-border bg-background px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary/30 focus:outline-none focus:ring-2 focus:ring-primary/20"
autoFocus autoFocus
/> />
</div> </div>
<p className="text-sm text-muted-foreground mb-4"> <p className="mb-4 text-sm text-muted-foreground">
This will create a new branch from the current branch ({currentBranch}) This will create a new branch from the current branch ({currentBranch})
</p> </p>
<div className="flex justify-end space-x-3"> <div className="flex justify-end space-x-3">
<button <button
onClick={onClose} onClick={onClose}
className="px-4 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg transition-colors" className="rounded-lg px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
> >
Cancel Cancel
</button> </button>
<button <button
onClick={() => void handleCreateBranch()} onClick={() => void handleCreateBranch()}
disabled={!newBranchName.trim() || isCreatingBranch} disabled={!newBranchName.trim() || isCreatingBranch}
className="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2 transition-colors" className="flex items-center space-x-2 rounded-lg bg-primary px-4 py-2 text-sm text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
> >
{isCreatingBranch ? ( {isCreatingBranch ? (
<> <>
<RefreshCw className="w-3 h-3 animate-spin" /> <RefreshCw className="h-3 w-3 animate-spin" />
<span>Creating...</span> <span>Creating...</span>
</> </>
) : ( ) : (
<> <>
<Plus className="w-3 h-3" /> <Plus className="h-3 w-3" />
<span>Create Branch</span> <span>Create Branch</span>
</> </>
)} )}

View File

@@ -7,7 +7,7 @@ type GitDiffViewerProps = {
export default function GitDiffViewer({ diff, isMobile, wrapText }: GitDiffViewerProps) { export default function GitDiffViewer({ diff, isMobile, wrapText }: GitDiffViewerProps) {
if (!diff) { if (!diff) {
return ( return (
<div className="p-4 text-center text-muted-foreground text-sm"> <div className="p-4 text-center text-sm text-muted-foreground">
No diff available No diff available
</div> </div>
); );
@@ -21,9 +21,9 @@ export default function GitDiffViewer({ diff, isMobile, wrapText }: GitDiffViewe
return ( return (
<div <div
key={index} key={index}
className={`font-mono text-xs px-3 py-0.5 ${isMobile && wrapText ? 'whitespace-pre-wrap break-all' : 'whitespace-pre overflow-x-auto' className={`px-3 py-0.5 font-mono text-xs ${isMobile && wrapText ? 'whitespace-pre-wrap break-all' : 'overflow-x-auto whitespace-pre'
} ${isAddition ? 'bg-green-50 dark:bg-green-950/50 text-green-700 dark:text-green-300' : } ${isAddition ? 'bg-green-50 text-green-700 dark:bg-green-950/50 dark:text-green-300' :
isDeletion ? 'bg-red-50 dark:bg-red-950/50 text-red-700 dark:text-red-300' : isDeletion ? 'bg-red-50 text-red-700 dark:bg-red-950/50 dark:text-red-300' :
isHeader ? 'bg-primary/5 text-primary' : isHeader ? 'bg-primary/5 text-primary' :
'text-muted-foreground/70' 'text-muted-foreground/70'
}`} }`}

View File

@@ -32,8 +32,8 @@ function ErrorFallback({
}: ErrorFallbackProps) { }: ErrorFallbackProps) {
return ( return (
<div className="flex flex-col items-center justify-center p-8 text-center"> <div className="flex flex-col items-center justify-center p-8 text-center">
<div className="bg-red-50 border border-red-200 rounded-lg p-6 max-w-md"> <div className="max-w-md rounded-lg border border-red-200 bg-red-50 p-6">
<div className="flex items-center mb-4"> <div className="mb-4 flex items-center">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor"> <svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path <path
@@ -49,8 +49,8 @@ function ErrorFallback({
<p className="mb-2">An error occurred while loading the chat interface.</p> <p className="mb-2">An error occurred while loading the chat interface.</p>
{showDetails && ( {showDetails && (
<details className="mt-4"> <details className="mt-4">
<summary className="cursor-pointer text-xs font-mono">Error Details</summary> <summary className="cursor-pointer font-mono text-xs">Error Details</summary>
<pre className="mt-2 text-xs bg-red-100 p-2 rounded overflow-auto max-h-40"> <pre className="mt-2 max-h-40 overflow-auto rounded bg-red-100 p-2 text-xs">
{formatError(error)} {formatError(error)}
{componentStack} {componentStack}
</pre> </pre>
@@ -60,7 +60,7 @@ function ErrorFallback({
<div className="mt-4"> <div className="mt-4">
<button <button
onClick={resetErrorBoundary} onClick={resetErrorBoundary}
className="bg-red-600 text-white px-4 py-2 rounded text-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500" className="rounded bg-red-600 px-4 py-2 text-sm text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500"
> >
Try Again Try Again
</button> </button>

View File

@@ -1,15 +1,9 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import ChatInterface from '../../chat/view/ChatInterface'; import ChatInterface from '../../chat/view/ChatInterface';
import FileTree from '../../file-tree/view/FileTree'; import FileTree from '../../file-tree/view/FileTree';
import StandaloneShell from '../../standalone-shell/view/StandaloneShell'; import StandaloneShell from '../../standalone-shell/view/StandaloneShell';
import GitPanel from '../../git-panel/view/GitPanel'; import GitPanel from '../../git-panel/view/GitPanel';
import ErrorBoundary from './ErrorBoundary';
import MainContentHeader from './subcomponents/MainContentHeader';
import MainContentStateView from './subcomponents/MainContentStateView';
import type { MainContentProps } from '../types/types'; import type { MainContentProps } from '../types/types';
import { useTaskMaster } from '../../../contexts/TaskMasterContext'; import { useTaskMaster } from '../../../contexts/TaskMasterContext';
import { useTasksSettings } from '../../../contexts/TasksSettingsContext'; import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
import { useUiPreferences } from '../../../hooks/useUiPreferences'; import { useUiPreferences } from '../../../hooks/useUiPreferences';
@@ -17,6 +11,9 @@ import { useEditorSidebar } from '../../code-editor/hooks/useEditorSidebar';
import EditorSidebar from '../../code-editor/view/EditorSidebar'; import EditorSidebar from '../../code-editor/view/EditorSidebar';
import type { Project } from '../../../types/app'; import type { Project } from '../../../types/app';
import { TaskMasterPanel } from '../../task-master'; import { TaskMasterPanel } from '../../task-master';
import MainContentHeader from './subcomponents/MainContentHeader';
import MainContentStateView from './subcomponents/MainContentStateView';
import ErrorBoundary from './ErrorBoundary';
type TaskMasterContextValue = { type TaskMasterContextValue = {
currentProject?: Project | null; currentProject?: Project | null;
@@ -98,7 +95,7 @@ function MainContent({
} }
return ( return (
<div className="h-full flex flex-col"> <div className="flex h-full flex-col">
<MainContentHeader <MainContentHeader
activeTab={activeTab} activeTab={activeTab}
setActiveTab={setActiveTab} setActiveTab={setActiveTab}
@@ -109,8 +106,8 @@ function MainContent({
onMenuClick={onMenuClick} onMenuClick={onMenuClick}
/> />
<div className="flex-1 flex min-h-0 overflow-hidden"> <div className="flex min-h-0 flex-1 overflow-hidden">
<div className={`flex flex-col min-h-0 min-w-[200px] overflow-hidden ${editorExpanded ? 'hidden' : ''} flex-1`}> <div className={`flex min-h-0 min-w-[200px] flex-col overflow-hidden ${editorExpanded ? 'hidden' : ''} flex-1`}>
<div className={`h-full ${activeTab === 'chat' ? 'block' : 'hidden'}`}> <div className={`h-full ${activeTab === 'chat' ? 'block' : 'hidden'}`}>
<ErrorBoundary showDetails> <ErrorBoundary showDetails>
<ChatInterface <ChatInterface

View File

@@ -1,7 +1,7 @@
import type { MainContentHeaderProps } from '../../types/types';
import MobileMenuButton from './MobileMenuButton'; import MobileMenuButton from './MobileMenuButton';
import MainContentTabSwitcher from './MainContentTabSwitcher'; import MainContentTabSwitcher from './MainContentTabSwitcher';
import MainContentTitle from './MainContentTitle'; import MainContentTitle from './MainContentTitle';
import type { MainContentHeaderProps } from '../../types/types';
export default function MainContentHeader({ export default function MainContentHeader({
activeTab, activeTab,
@@ -13,9 +13,9 @@ export default function MainContentHeader({
onMenuClick, onMenuClick,
}: MainContentHeaderProps) { }: MainContentHeaderProps) {
return ( return (
<div className="bg-background border-b border-border/60 px-3 py-1.5 sm:px-4 sm:py-2 pwa-header-safe flex-shrink-0"> <div className="pwa-header-safe flex-shrink-0 border-b border-border/60 bg-background px-3 py-1.5 sm:px-4 sm:py-2">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2 min-w-0 flex-1"> <div className="flex min-w-0 flex-1 items-center gap-2">
{isMobile && <MobileMenuButton onMenuClick={onMenuClick} />} {isMobile && <MobileMenuButton onMenuClick={onMenuClick} />}
<MainContentTitle <MainContentTitle
activeTab={activeTab} activeTab={activeTab}
@@ -25,7 +25,7 @@ export default function MainContentHeader({
/> />
</div> </div>
<div className="flex-shrink-0 hidden sm:block"> <div className="hidden flex-shrink-0 sm:block">
<MainContentTabSwitcher <MainContentTabSwitcher
activeTab={activeTab} activeTab={activeTab}
setActiveTab={setActiveTab} setActiveTab={setActiveTab}

View File

@@ -1,7 +1,7 @@
import { Folder } from 'lucide-react'; import { Folder } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import MobileMenuButton from './MobileMenuButton';
import type { MainContentStateViewProps } from '../../types/types'; import type { MainContentStateViewProps } from '../../types/types';
import MobileMenuButton from './MobileMenuButton';
export default function MainContentStateView({ mode, isMobile, onMenuClick }: MainContentStateViewProps) { export default function MainContentStateView({ mode, isMobile, onMenuClick }: MainContentStateViewProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -9,19 +9,19 @@ export default function MainContentStateView({ mode, isMobile, onMenuClick }: Ma
const isLoading = mode === 'loading'; const isLoading = mode === 'loading';
return ( return (
<div className="h-full flex flex-col"> <div className="flex h-full flex-col">
{isMobile && ( {isMobile && (
<div className="bg-background/80 backdrop-blur-sm border-b border-border/50 p-2 sm:p-3 pwa-header-safe flex-shrink-0"> <div className="pwa-header-safe flex-shrink-0 border-b border-border/50 bg-background/80 p-2 backdrop-blur-sm sm:p-3">
<MobileMenuButton onMenuClick={onMenuClick} compact /> <MobileMenuButton onMenuClick={onMenuClick} compact />
</div> </div>
)} )}
{isLoading ? ( {isLoading ? (
<div className="flex-1 flex items-center justify-center"> <div className="flex flex-1 items-center justify-center">
<div className="text-center text-muted-foreground"> <div className="text-center text-muted-foreground">
<div className="w-10 h-10 mx-auto mb-4"> <div className="mx-auto mb-4 h-10 w-10">
<div <div
className="w-full h-full rounded-full border-[3px] border-muted border-t-primary" className="h-full w-full rounded-full border-[3px] border-muted border-t-primary"
style={{ style={{
animation: 'spin 1s linear infinite', animation: 'spin 1s linear infinite',
WebkitAnimation: 'spin 1s linear infinite', WebkitAnimation: 'spin 1s linear infinite',
@@ -29,19 +29,19 @@ export default function MainContentStateView({ mode, isMobile, onMenuClick }: Ma
}} }}
/> />
</div> </div>
<h2 className="text-lg font-semibold text-foreground mb-1">{t('mainContent.loading')}</h2> <h2 className="mb-1 text-lg font-semibold text-foreground">{t('mainContent.loading')}</h2>
<p className="text-sm">{t('mainContent.settingUpWorkspace')}</p> <p className="text-sm">{t('mainContent.settingUpWorkspace')}</p>
</div> </div>
</div> </div>
) : ( ) : (
<div className="flex-1 flex items-center justify-center"> <div className="flex flex-1 items-center justify-center">
<div className="text-center max-w-md mx-auto px-6"> <div className="mx-auto max-w-md px-6 text-center">
<div className="w-14 h-14 mx-auto mb-5 bg-muted/50 rounded-2xl flex items-center justify-center"> <div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-2xl bg-muted/50">
<Folder className="w-7 h-7 text-muted-foreground" /> <Folder className="h-7 w-7 text-muted-foreground" />
</div> </div>
<h2 className="text-xl font-semibold mb-2 text-foreground">{t('mainContent.chooseProject')}</h2> <h2 className="mb-2 text-xl font-semibold text-foreground">{t('mainContent.chooseProject')}</h2>
<p className="text-sm text-muted-foreground mb-5 leading-relaxed">{t('mainContent.selectProjectDescription')}</p> <p className="mb-5 text-sm leading-relaxed text-muted-foreground">{t('mainContent.selectProjectDescription')}</p>
<div className="bg-primary/5 rounded-xl p-3.5 border border-primary/10"> <div className="rounded-xl border border-primary/10 bg-primary/5 p-3.5">
<p className="text-sm text-primary"> <p className="text-sm text-primary">
<strong>{t('mainContent.tip')}:</strong> {isMobile ? t('mainContent.createProjectMobile') : t('mainContent.createProjectDesktop')} <strong>{t('mainContent.tip')}:</strong> {isMobile ? t('mainContent.createProjectMobile') : t('mainContent.createProjectDesktop')}
</p> </p>

View File

@@ -1,8 +1,8 @@
import { MessageSquare, Terminal, Folder, GitBranch, ClipboardCheck, type LucideIcon } from 'lucide-react'; import { MessageSquare, Terminal, Folder, GitBranch, ClipboardCheck, type LucideIcon } from 'lucide-react';
import { Tooltip } from '../../../../shared/view/ui';
import type { AppTab } from '../../../../types/app';
import type { Dispatch, SetStateAction } from 'react'; import type { Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Tooltip } from '../../../../shared/view/ui';
import type { AppTab } from '../../../../types/app';
type MainContentTabSwitcherProps = { type MainContentTabSwitcherProps = {
activeTab: AppTab; activeTab: AppTab;
@@ -39,7 +39,7 @@ export default function MainContentTabSwitcher({
const tabs = shouldShowTasksTab ? [...BASE_TABS, TASKS_TAB] : BASE_TABS; const tabs = shouldShowTasksTab ? [...BASE_TABS, TASKS_TAB] : BASE_TABS;
return ( return (
<div className="inline-flex items-center bg-muted/60 rounded-lg p-[3px] gap-[2px]"> <div className="inline-flex items-center gap-[2px] rounded-lg bg-muted/60 p-[3px]">
{tabs.map((tab) => { {tabs.map((tab) => {
const Icon = tab.icon; const Icon = tab.icon;
const isActive = tab.id === activeTab; const isActive = tab.id === activeTab;
@@ -48,13 +48,13 @@ export default function MainContentTabSwitcher({
<Tooltip key={tab.id} content={t(tab.labelKey)} position="bottom"> <Tooltip key={tab.id} content={t(tab.labelKey)} position="bottom">
<button <button
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={`relative flex items-center gap-1.5 px-2.5 py-[5px] text-sm font-medium rounded-md transition-all duration-150 ${ className={`relative flex items-center gap-1.5 rounded-md px-2.5 py-[5px] text-sm font-medium transition-all duration-150 ${
isActive isActive
? 'bg-background text-foreground shadow-sm' ? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground' : 'text-muted-foreground hover:text-foreground'
}`} }`}
> >
<Icon className="w-3.5 h-3.5" strokeWidth={isActive ? 2.2 : 1.8} /> <Icon className="h-3.5 w-3.5" strokeWidth={isActive ? 2.2 : 1.8} />
<span className="hidden lg:inline">{t(tab.labelKey)}</span> <span className="hidden lg:inline">{t(tab.labelKey)}</span>
</button> </button>
</Tooltip> </Tooltip>

View File

@@ -45,32 +45,32 @@ export default function MainContentTitle({
const showChatNewSession = activeTab === 'chat' && !selectedSession; const showChatNewSession = activeTab === 'chat' && !selectedSession;
return ( return (
<div className="min-w-0 flex items-center gap-2 flex-1 overflow-x-auto scrollbar-hide"> <div className="scrollbar-hide flex min-w-0 flex-1 items-center gap-2 overflow-x-auto">
{showSessionIcon && ( {showSessionIcon && (
<div className="w-5 h-5 flex-shrink-0 flex items-center justify-center"> <div className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
<SessionProviderLogo provider={selectedSession?.__provider} className="w-4 h-4" /> <SessionProviderLogo provider={selectedSession?.__provider} className="h-4 w-4" />
</div> </div>
)} )}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
{activeTab === 'chat' && selectedSession ? ( {activeTab === 'chat' && selectedSession ? (
<div className="min-w-0"> <div className="min-w-0">
<h2 className="text-sm font-semibold text-foreground whitespace-nowrap overflow-x-auto scrollbar-hide leading-tight"> <h2 className="scrollbar-hide overflow-x-auto whitespace-nowrap text-sm font-semibold leading-tight text-foreground">
{getSessionTitle(selectedSession)} {getSessionTitle(selectedSession)}
</h2> </h2>
<div className="text-[11px] text-muted-foreground truncate leading-tight">{selectedProject.displayName}</div> <div className="truncate text-[11px] leading-tight text-muted-foreground">{selectedProject.displayName}</div>
</div> </div>
) : showChatNewSession ? ( ) : showChatNewSession ? (
<div className="min-w-0"> <div className="min-w-0">
<h2 className="text-base font-semibold text-foreground leading-tight">{t('mainContent.newSession')}</h2> <h2 className="text-base font-semibold leading-tight text-foreground">{t('mainContent.newSession')}</h2>
<div className="text-xs text-muted-foreground truncate leading-tight">{selectedProject.displayName}</div> <div className="truncate text-xs leading-tight text-muted-foreground">{selectedProject.displayName}</div>
</div> </div>
) : ( ) : (
<div className="min-w-0"> <div className="min-w-0">
<h2 className="text-sm font-semibold text-foreground leading-tight"> <h2 className="text-sm font-semibold leading-tight text-foreground">
{getTabTitle(activeTab, shouldShowTasksTab, t)} {getTabTitle(activeTab, shouldShowTasksTab, t)}
</h2> </h2>
<div className="text-[11px] text-muted-foreground truncate leading-tight">{selectedProject.displayName}</div> <div className="truncate text-[11px] leading-tight text-muted-foreground">{selectedProject.displayName}</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -15,7 +15,7 @@ export default function MobileMenuButton({ onMenuClick, compact = false }: Mobil
className={buttonClasses} className={buttonClasses}
aria-label="Open menu" aria-label="Open menu"
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg> </svg>
</button> </button>

View File

@@ -13,22 +13,22 @@ type MicButtonViewProps = {
const getButtonIcon = (state: MicButtonState, isSupported: boolean): ReactElement => { const getButtonIcon = (state: MicButtonState, isSupported: boolean): ReactElement => {
if (!isSupported) { if (!isSupported) {
return <Mic className="w-5 h-5" />; return <Mic className="h-5 w-5" />;
} }
if (state === MIC_BUTTON_STATES.TRANSCRIBING) { if (state === MIC_BUTTON_STATES.TRANSCRIBING) {
return <Loader2 className="w-5 h-5 animate-spin" />; return <Loader2 className="h-5 w-5 animate-spin" />;
} }
if (state === MIC_BUTTON_STATES.PROCESSING) { if (state === MIC_BUTTON_STATES.PROCESSING) {
return <Brain className="w-5 h-5 animate-pulse" />; return <Brain className="h-5 w-5 animate-pulse" />;
} }
if (state === MIC_BUTTON_STATES.RECORDING) { if (state === MIC_BUTTON_STATES.RECORDING) {
return <Mic className="w-5 h-5 text-white" />; return <Mic className="h-5 w-5 text-white" />;
} }
return <Mic className="w-5 h-5" />; return <Mic className="h-5 w-5" />;
}; };
export default function MicButtonView({ export default function MicButtonView({
@@ -47,12 +47,12 @@ export default function MicButtonView({
type="button" type="button"
style={{ backgroundColor: BUTTON_BACKGROUND_BY_STATE[state] }} style={{ backgroundColor: BUTTON_BACKGROUND_BY_STATE[state] }}
className={` className={`
flex items-center justify-center touch-action-manipulation flex h-12
w-12 h-12 rounded-full w-12 items-center justify-center
text-white transition-all duration-200 rounded-full text-white transition-all
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500
focus:ring-offset-2
dark:ring-offset-gray-800 dark:ring-offset-gray-800
touch-action-manipulation
${isDisabled ? 'cursor-not-allowed opacity-75' : 'cursor-pointer'} ${isDisabled ? 'cursor-not-allowed opacity-75' : 'cursor-pointer'}
${state === MIC_BUTTON_STATES.RECORDING ? 'animate-pulse' : ''} ${state === MIC_BUTTON_STATES.RECORDING ? 'animate-pulse' : ''}
hover:opacity-90 hover:opacity-90
@@ -66,20 +66,20 @@ export default function MicButtonView({
{error && ( {error && (
<div <div
className="absolute top-full mt-2 left-1/2 transform -translate-x-1/2 className="animate-fade-in absolute left-1/2 top-full z-10 mt-2
bg-red-500 text-white text-xs px-2 py-1 rounded whitespace-nowrap z-10 -translate-x-1/2 transform whitespace-nowrap rounded bg-red-500 px-2 py-1 text-xs
animate-fade-in" text-white"
> >
{error} {error}
</div> </div>
)} )}
{state === MIC_BUTTON_STATES.RECORDING && ( {state === MIC_BUTTON_STATES.RECORDING && (
<div className="absolute -inset-1 rounded-full border-2 border-red-500 animate-ping pointer-events-none" /> <div className="pointer-events-none absolute -inset-1 animate-ping rounded-full border-2 border-red-500" />
)} )}
{state === MIC_BUTTON_STATES.PROCESSING && ( {state === MIC_BUTTON_STATES.PROCESSING && (
<div className="absolute -inset-1 rounded-full border-2 border-purple-500 animate-ping pointer-events-none" /> <div className="pointer-events-none absolute -inset-1 animate-ping rounded-full border-2 border-purple-500" />
)} )}
</div> </div>
); );

View File

@@ -194,11 +194,11 @@ export default function Onboarding({ onComplete }: OnboardingProps) {
return ( return (
<> <>
<div className="min-h-screen bg-background flex items-center justify-center p-4"> <div className="flex min-h-screen items-center justify-center bg-background p-4">
<div className="w-full max-w-2xl"> <div className="w-full max-w-2xl">
<OnboardingStepProgress currentStep={currentStep} /> <OnboardingStepProgress currentStep={currentStep} />
<div className="bg-card rounded-lg shadow-lg border border-border p-8"> <div className="rounded-lg border border-border bg-card p-8 shadow-lg">
{currentStep === 0 ? ( {currentStep === 0 ? (
<GitConfigurationStep <GitConfigurationStep
gitName={gitName} gitName={gitName}
@@ -215,18 +215,18 @@ export default function Onboarding({ onComplete }: OnboardingProps) {
)} )}
{errorMessage && ( {errorMessage && (
<div className="mt-6 p-4 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded-lg"> <div className="mt-6 rounded-lg border border-red-300 bg-red-100 p-4 dark:border-red-800 dark:bg-red-900/20">
<p className="text-sm text-red-700 dark:text-red-400">{errorMessage}</p> <p className="text-sm text-red-700 dark:text-red-400">{errorMessage}</p>
</div> </div>
)} )}
<div className="flex items-center justify-between mt-8 pt-6 border-t border-border"> <div className="mt-8 flex items-center justify-between border-t border-border pt-6">
<button <button
onClick={handlePreviousStep} onClick={handlePreviousStep}
disabled={currentStep === 0 || isSubmitting} disabled={currentStep === 0 || isSubmitting}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200" className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-muted-foreground transition-colors duration-200 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
> >
<ChevronLeft className="w-4 h-4" /> <ChevronLeft className="h-4 w-4" />
Previous Previous
</button> </button>
@@ -235,17 +235,17 @@ export default function Onboarding({ onComplete }: OnboardingProps) {
<button <button
onClick={handleNextStep} onClick={handleNextStep}
disabled={!isCurrentStepValid || isSubmitting} disabled={!isCurrentStepValid || isSubmitting}
className="flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors duration-200" className="flex items-center gap-2 rounded-lg bg-blue-600 px-6 py-3 font-medium text-white transition-colors duration-200 hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-blue-400"
> >
{isSubmitting ? ( {isSubmitting ? (
<> <>
<Loader2 className="w-4 h-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
Saving... Saving...
</> </>
) : ( ) : (
<> <>
Next Next
<ChevronRight className="w-4 h-4" /> <ChevronRight className="h-4 w-4" />
</> </>
)} )}
</button> </button>
@@ -253,16 +253,16 @@ export default function Onboarding({ onComplete }: OnboardingProps) {
<button <button
onClick={handleFinish} onClick={handleFinish}
disabled={isSubmitting} disabled={isSubmitting}
className="flex items-center gap-2 px-6 py-3 bg-green-600 hover:bg-green-700 disabled:bg-green-400 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors duration-200" className="flex items-center gap-2 rounded-lg bg-green-600 px-6 py-3 font-medium text-white transition-colors duration-200 hover:bg-green-700 disabled:cursor-not-allowed disabled:bg-green-400"
> >
{isSubmitting ? ( {isSubmitting ? (
<> <>
<Loader2 className="w-4 h-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
Completing... Completing...
</> </>
) : ( ) : (
<> <>
<Check className="w-4 h-4" /> <Check className="h-4 w-4" />
Complete Setup Complete Setup
</> </>
)} )}

View File

@@ -30,17 +30,17 @@ export default function AgentConnectionCard({
: status.error || 'Not connected'; : status.error || 'Not connected';
return ( return (
<div className={`border rounded-lg p-4 transition-colors ${containerClassName}`}> <div className={`rounded-lg border p-4 transition-colors ${containerClassName}`}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${iconContainerClassName}`}> <div className={`flex h-10 w-10 items-center justify-center rounded-full ${iconContainerClassName}`}>
<SessionProviderLogo provider={provider} className="w-5 h-5" /> <SessionProviderLogo provider={provider} className="h-5 w-5" />
</div> </div>
<div> <div>
<div className="font-medium text-foreground flex items-center gap-2"> <div className="flex items-center gap-2 font-medium text-foreground">
{title} {title}
{status.authenticated && <Check className="w-4 h-4 text-green-500" />} {status.authenticated && <Check className="h-4 w-4 text-green-500" />}
</div> </div>
<div className="text-xs text-muted-foreground">{statusText}</div> <div className="text-xs text-muted-foreground">{statusText}</div>
</div> </div>
@@ -49,7 +49,7 @@ export default function AgentConnectionCard({
{!status.authenticated && !status.loading && ( {!status.authenticated && !status.loading && (
<button <button
onClick={onLogin} onClick={onLogin}
className={`${loginButtonClassName} text-white text-sm font-medium py-2 px-4 rounded-lg transition-colors`} className={`${loginButtonClassName} rounded-lg px-4 py-2 text-sm font-medium text-white transition-colors`}
> >
Login Login
</button> </button>

View File

@@ -1,5 +1,5 @@
import AgentConnectionCard from './AgentConnectionCard';
import type { CliProvider, ProviderStatusMap } from '../types'; import type { CliProvider, ProviderStatusMap } from '../types';
import AgentConnectionCard from './AgentConnectionCard';
type AgentConnectionsStepProps = { type AgentConnectionsStepProps = {
providerStatuses: ProviderStatusMap; providerStatuses: ProviderStatusMap;
@@ -43,8 +43,8 @@ export default function AgentConnectionsStep({
}: AgentConnectionsStepProps) { }: AgentConnectionsStepProps) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="text-center mb-6"> <div className="mb-6 text-center">
<h2 className="text-2xl font-bold text-foreground mb-2">Connect Your AI Agents</h2> <h2 className="mb-2 text-2xl font-bold text-foreground">Connect Your AI Agents</h2>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Login to one or more AI coding assistants. All are optional. Login to one or more AI coding assistants. All are optional.
</p> </p>
@@ -65,7 +65,7 @@ export default function AgentConnectionsStep({
))} ))}
</div> </div>
<div className="text-center text-sm text-muted-foreground pt-2"> <div className="pt-2 text-center text-sm text-muted-foreground">
<p>You can configure these later in Settings.</p> <p>You can configure these later in Settings.</p>
</div> </div>
</div> </div>

View File

@@ -17,11 +17,11 @@ export default function GitConfigurationStep({
}: GitConfigurationStepProps) { }: GitConfigurationStepProps) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="text-center mb-8"> <div className="mb-8 text-center">
<div className="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center mx-auto mb-4"> <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/30">
<GitBranch className="w-8 h-8 text-blue-600 dark:text-blue-400" /> <GitBranch className="h-8 w-8 text-blue-600 dark:text-blue-400" />
</div> </div>
<h2 className="text-2xl font-bold text-foreground mb-2">Git Configuration</h2> <h2 className="mb-2 text-2xl font-bold text-foreground">Git Configuration</h2>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Configure your git identity to ensure proper attribution for commits. Configure your git identity to ensure proper attribution for commits.
</p> </p>
@@ -29,8 +29,8 @@ export default function GitConfigurationStep({
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label htmlFor="gitName" className="flex items-center gap-2 text-sm font-medium text-foreground mb-2"> <label htmlFor="gitName" className="mb-2 flex items-center gap-2 text-sm font-medium text-foreground">
<User className="w-4 h-4" /> <User className="h-4 w-4" />
Git Name <span className="text-red-500">*</span> Git Name <span className="text-red-500">*</span>
</label> </label>
<input <input
@@ -38,7 +38,7 @@ export default function GitConfigurationStep({
id="gitName" id="gitName"
value={gitName} value={gitName}
onChange={(event) => onGitNameChange(event.target.value)} onChange={(event) => onGitNameChange(event.target.value)}
className="w-full px-4 py-3 border border-border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full rounded-lg border border-border bg-background px-4 py-3 text-foreground focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="John Doe" placeholder="John Doe"
required required
disabled={isSubmitting} disabled={isSubmitting}
@@ -47,8 +47,8 @@ export default function GitConfigurationStep({
</div> </div>
<div> <div>
<label htmlFor="gitEmail" className="flex items-center gap-2 text-sm font-medium text-foreground mb-2"> <label htmlFor="gitEmail" className="mb-2 flex items-center gap-2 text-sm font-medium text-foreground">
<Mail className="w-4 h-4" /> <Mail className="h-4 w-4" />
Git Email <span className="text-red-500">*</span> Git Email <span className="text-red-500">*</span>
</label> </label>
<input <input
@@ -56,7 +56,7 @@ export default function GitConfigurationStep({
id="gitEmail" id="gitEmail"
value={gitEmail} value={gitEmail}
onChange={(event) => onGitEmailChange(event.target.value)} onChange={(event) => onGitEmailChange(event.target.value)}
className="w-full px-4 py-3 border border-border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full rounded-lg border border-border bg-background px-4 py-3 text-foreground focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="john@example.com" placeholder="john@example.com"
required required
disabled={isSubmitting} disabled={isSubmitting}

View File

@@ -20,17 +20,17 @@ export default function OnboardingStepProgress({ currentStep }: OnboardingStepPr
return ( return (
<div key={step.title} className="contents"> <div key={step.title} className="contents">
<div className="flex flex-col items-center flex-1"> <div className="flex flex-1 flex-col items-center">
<div <div
className={`w-12 h-12 rounded-full flex items-center justify-center border-2 transition-colors duration-200 ${ className={`flex h-12 w-12 items-center justify-center rounded-full border-2 transition-colors duration-200 ${
isCompleted isCompleted
? 'bg-green-500 border-green-500 text-white' ? 'border-green-500 bg-green-500 text-white'
: isActive : isActive
? 'bg-blue-600 border-blue-600 text-white' ? 'border-blue-600 bg-blue-600 text-white'
: 'bg-background border-border text-muted-foreground' : 'border-border bg-background text-muted-foreground'
}`} }`}
> >
{isCompleted ? <Check className="w-6 h-6" /> : <Icon className="w-6 h-6" />} {isCompleted ? <Check className="h-6 w-6" /> : <Icon className="h-6 w-6" />}
</div> </div>
<div className="mt-2 text-center"> <div className="mt-2 text-center">
@@ -42,7 +42,7 @@ export default function OnboardingStepProgress({ currentStep }: OnboardingStepPr
</div> </div>
{index < onboardingSteps.length - 1 && ( {index < onboardingSteps.length - 1 && (
<div className={`flex-1 h-0.5 mx-2 transition-colors duration-200 ${isCompleted ? 'bg-green-500' : 'bg-border'}`} /> <div className={`mx-2 h-0.5 flex-1 transition-colors duration-200 ${isCompleted ? 'bg-green-500' : 'bg-border'}`} />
)} )}
</div> </div>
); );

View File

@@ -17,12 +17,12 @@ export default function GenerateTasksModal({
} }
return ( return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[300] p-4"> <div className="fixed inset-0 z-[300] flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md border border-gray-200 dark:border-gray-700"> <div className="w-full max-w-md rounded-lg border border-gray-200 bg-white shadow-xl dark:border-gray-700 dark:bg-gray-800">
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700"> <div className="flex items-center justify-between border-b border-gray-200 p-6 dark:border-gray-700">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center"> <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-900/50">
<Sparkles className="w-4 h-4 text-purple-600 dark:text-purple-400" /> <Sparkles className="h-4 w-4 text-purple-600 dark:text-purple-400" />
</div> </div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white"> <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Generate Tasks from PRD Generate Tasks from PRD
@@ -30,35 +30,35 @@ export default function GenerateTasksModal({
</div> </div>
<button <button
onClick={onClose} onClick={onClose}
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700" className="rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
> >
<X className="w-5 h-5" /> <X className="h-5 w-5" />
</button> </button>
</div> </div>
<div className="p-6 space-y-4"> <div className="space-y-4 p-6">
<div className="bg-purple-50 dark:bg-purple-900/20 rounded-lg p-4 border border-purple-200 dark:border-purple-800"> <div className="rounded-lg border border-purple-200 bg-purple-50 p-4 dark:border-purple-800 dark:bg-purple-900/20">
<h4 className="font-semibold text-purple-900 dark:text-purple-100 mb-2"> <h4 className="mb-2 font-semibold text-purple-900 dark:text-purple-100">
Ask Claude Code directly Ask Claude Code directly
</h4> </h4>
<p className="text-sm text-purple-800 dark:text-purple-200 mb-3"> <p className="mb-3 text-sm text-purple-800 dark:text-purple-200">
Save this PRD, then ask Claude Code in chat to parse the file and create your initial tasks. Save this PRD, then ask Claude Code in chat to parse the file and create your initial tasks.
</p> </p>
<div className="bg-white dark:bg-gray-800 rounded border border-purple-200 dark:border-purple-700 p-3"> <div className="rounded border border-purple-200 bg-white p-3 dark:border-purple-700 dark:bg-gray-800">
<p className="text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Example prompt</p> <p className="mb-1 text-xs font-medium text-gray-600 dark:text-gray-400">Example prompt</p>
<p className="text-xs text-gray-900 dark:text-white font-mono"> <p className="font-mono text-xs text-gray-900 dark:text-white">
I have a PRD at .taskmaster/docs/{fileName}. Parse it and create the initial tasks. I have a PRD at .taskmaster/docs/{fileName}. Parse it and create the initial tasks.
</p> </p>
</div> </div>
</div> </div>
<div className="text-center pt-4 border-t border-gray-200 dark:border-gray-700"> <div className="border-t border-gray-200 pt-4 text-center dark:border-gray-700">
<a <a
href={PRD_DOCS_URL} href={PRD_DOCS_URL}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-block text-sm text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 underline font-medium" className="inline-block text-sm font-medium text-purple-600 underline hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300"
> >
View TaskMaster documentation View TaskMaster documentation
</a> </a>
@@ -66,7 +66,7 @@ export default function GenerateTasksModal({
<button <button
onClick={onClose} onClick={onClose}
className="w-full px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors" className="w-full rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
> >
Got it Got it
</button> </button>

View File

@@ -23,16 +23,16 @@ export default function OverwriteConfirmModal({
<div className="fixed inset-0 z-[300] flex items-center justify-center p-4"> <div className="fixed inset-0 z-[300] flex items-center justify-center p-4">
<div className="fixed inset-0 bg-black/50" onClick={onCancel} /> <div className="fixed inset-0 bg-black/50" onClick={onCancel} />
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full border border-gray-200 dark:border-gray-700"> <div className="relative w-full max-w-md rounded-lg border border-gray-200 bg-white shadow-xl dark:border-gray-700 dark:bg-gray-800">
<div className="p-6"> <div className="p-6">
<div className="flex items-center mb-4"> <div className="mb-4 flex items-center">
<div className="p-2 rounded-full mr-3 bg-yellow-100 dark:bg-yellow-900"> <div className="mr-3 rounded-full bg-yellow-100 p-2 dark:bg-yellow-900">
<AlertTriangle className="w-5 h-5 text-yellow-600 dark:text-yellow-400" /> <AlertTriangle className="h-5 w-5 text-yellow-600 dark:text-yellow-400" />
</div> </div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">File Already Exists</h3> <h3 className="text-lg font-semibold text-gray-900 dark:text-white">File Already Exists</h3>
</div> </div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6"> <p className="mb-6 text-sm text-gray-600 dark:text-gray-400">
A PRD named "{fileName}" already exists. Do you want to overwrite it? A PRD named "{fileName}" already exists. Do you want to overwrite it?
</p> </p>
@@ -40,16 +40,16 @@ export default function OverwriteConfirmModal({
<button <button
onClick={onCancel} onClick={onCancel}
disabled={saving} disabled={saving}
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors" className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
> >
Cancel Cancel
</button> </button>
<button <button
onClick={onConfirm} onClick={onConfirm}
disabled={saving} disabled={saving}
className="px-4 py-2 text-sm text-white bg-yellow-600 hover:bg-yellow-700 rounded-md flex items-center gap-2 transition-colors disabled:opacity-50" className="flex items-center gap-2 rounded-md bg-yellow-600 px-4 py-2 text-sm text-white transition-colors hover:bg-yellow-700 disabled:opacity-50"
> >
<Save className="w-4 h-4" /> <Save className="h-4 w-4" />
<span>{saving ? 'Saving...' : 'Overwrite'}</span> <span>{saving ? 'Saving...' : 'Overwrite'}</span>
</button> </button>
</div> </div>

View File

@@ -27,7 +27,7 @@ export default function PrdEditorBody({
if (previewMode) { if (previewMode) {
return ( return (
<div className="h-full overflow-y-auto p-6 prose prose-gray dark:prose-invert max-w-none"> <div className="prose prose-gray h-full max-w-none overflow-y-auto p-6 dark:prose-invert">
<MarkdownPreview content={content} /> <MarkdownPreview content={content} />
</div> </div>
); );

View File

@@ -22,7 +22,7 @@ export default function PrdEditorFooter({ content }: PrdEditorFooterProps) {
const stats = useMemo(() => getContentStats(content), [content]); const stats = useMemo(() => getContentStats(content), [content]);
return ( return (
<div className="flex items-center justify-between p-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 flex-shrink-0"> <div className="flex flex-shrink-0 items-center justify-between border-t border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800">
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400"> <div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
<span>Lines: {stats.lines}</span> <span>Lines: {stats.lines}</span>
<span>Characters: {stats.characters}</span> <span>Characters: {stats.characters}</span>

View File

@@ -82,36 +82,36 @@ export default function PrdEditorHeader({
const fileNameInputRef = useRef<HTMLInputElement | null>(null); const fileNameInputRef = useRef<HTMLInputElement | null>(null);
return ( return (
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0 min-w-0"> <div className="flex min-w-0 flex-shrink-0 items-center justify-between border-b border-gray-200 p-4 dark:border-gray-700">
<div className="flex items-center gap-3 min-w-0 flex-1"> <div className="flex min-w-0 flex-1 items-center gap-3">
<div className="w-8 h-8 bg-purple-600 rounded flex items-center justify-center flex-shrink-0"> <div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded bg-purple-600">
<FileText className="w-4 h-4 text-white" /> <FileText className="h-4 w-4 text-white" />
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 min-w-0"> <div className="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center">
<div className="flex items-center gap-1 min-w-0 flex-1"> <div className="flex min-w-0 flex-1 items-center gap-1">
<div className="flex items-center min-w-0 flex-1 bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-md px-3 py-2 focus-within:ring-2 focus-within:ring-purple-500 focus-within:border-purple-500 dark:focus-within:ring-purple-400 dark:focus-within:border-purple-400"> <div className="flex min-w-0 flex-1 items-center rounded-md border border-gray-200 bg-gray-50 px-3 py-2 focus-within:border-purple-500 focus-within:ring-2 focus-within:ring-purple-500 dark:border-gray-600 dark:bg-gray-700 dark:focus-within:border-purple-400 dark:focus-within:ring-purple-400">
<input <input
ref={fileNameInputRef} ref={fileNameInputRef}
type="text" type="text"
value={fileName} value={fileName}
onChange={(event) => onFileNameChange(event.target.value)} onChange={(event) => onFileNameChange(event.target.value)}
className="font-medium text-gray-900 dark:text-white bg-transparent border-none outline-none min-w-0 flex-1 text-base sm:text-sm placeholder-gray-400 dark:placeholder-gray-500" className="min-w-0 flex-1 border-none bg-transparent text-base font-medium text-gray-900 placeholder-gray-400 outline-none dark:text-white dark:placeholder-gray-500 sm:text-sm"
placeholder="Enter PRD filename" placeholder="Enter PRD filename"
maxLength={100} maxLength={100}
/> />
<span className="text-sm sm:text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap ml-1"> <span className="ml-1 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 sm:text-xs">
.txt .txt
</span> </span>
</div> </div>
<button <button
onClick={() => fileNameInputRef.current?.focus()} onClick={() => fileNameInputRef.current?.focus()}
className="p-1 text-gray-400 hover:text-purple-600 dark:hover:text-purple-400 transition-colors" className="p-1 text-gray-400 transition-colors hover:text-purple-600 dark:hover:text-purple-400"
title="Focus filename input" title="Focus filename input"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@@ -122,36 +122,36 @@ export default function PrdEditorHeader({
</button> </button>
</div> </div>
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex flex-shrink-0 items-center gap-2">
<span className="text-xs bg-purple-100 dark:bg-purple-900 text-purple-600 dark:text-purple-300 px-2 py-1 rounded whitespace-nowrap"> <span className="whitespace-nowrap rounded bg-purple-100 px-2 py-1 text-xs text-purple-600 dark:bg-purple-900 dark:text-purple-300">
PRD PRD
</span> </span>
{isNewFile && ( {isNewFile && (
<span className="text-xs bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-300 px-2 py-1 rounded whitespace-nowrap"> <span className="whitespace-nowrap rounded bg-green-100 px-2 py-1 text-xs text-green-600 dark:bg-green-900 dark:text-green-300">
New New
</span> </span>
)} )}
</div> </div>
</div> </div>
<p className="text-xs sm:text-sm text-gray-500 dark:text-gray-400 truncate mt-1"> <p className="mt-1 truncate text-xs text-gray-500 dark:text-gray-400 sm:text-sm">
Product Requirements Document Product Requirements Document
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center gap-1 md:gap-2 flex-shrink-0"> <div className="flex flex-shrink-0 items-center gap-1 md:gap-2">
<HeaderIconButton <HeaderIconButton
title={previewMode ? 'Switch to edit mode' : 'Preview markdown'} title={previewMode ? 'Switch to edit mode' : 'Preview markdown'}
onClick={onTogglePreview} onClick={onTogglePreview}
icon={<Eye className="w-5 h-5 md:w-4 md:h-4" />} icon={<Eye className="h-5 w-5 md:h-4 md:w-4" />}
active={previewMode} active={previewMode}
/> />
<HeaderIconButton <HeaderIconButton
title={wordWrap ? 'Disable word wrap' : 'Enable word wrap'} title={wordWrap ? 'Disable word wrap' : 'Enable word wrap'}
onClick={onToggleWordWrap} onClick={onToggleWordWrap}
icon={<span className="text-sm md:text-xs font-mono font-bold">WRAP</span>} icon={<span className="font-mono text-sm font-bold md:text-xs">WRAP</span>}
active={wordWrap} active={wordWrap}
/> />
@@ -160,9 +160,9 @@ export default function PrdEditorHeader({
onClick={onToggleTheme} onClick={onToggleTheme}
icon={ icon={
isDarkMode ? ( isDarkMode ? (
<Sun className="w-5 h-5 md:w-4 md:h-4" /> <Sun className="h-5 w-5 md:h-4 md:w-4" />
) : ( ) : (
<Moon className="w-5 h-5 md:w-4 md:h-4" /> <Moon className="h-5 w-5 md:h-4 md:w-4" />
) )
} }
/> />
@@ -170,7 +170,7 @@ export default function PrdEditorHeader({
<HeaderIconButton <HeaderIconButton
title="Download PRD" title="Download PRD"
onClick={onDownload} onClick={onDownload}
icon={<Download className="w-5 h-5 md:w-4 md:h-4" />} icon={<Download className="h-5 w-5 md:h-4 md:w-4" />}
/> />
<button <button
@@ -182,7 +182,7 @@ export default function PrdEditorHeader({
)} )}
title="Generate tasks from PRD content" title="Generate tasks from PRD content"
> >
<Sparkles className="w-4 h-4" /> <Sparkles className="h-4 w-4" />
<span className="hidden md:inline">Generate Tasks</span> <span className="hidden md:inline">Generate Tasks</span>
</button> </button>
@@ -196,14 +196,14 @@ export default function PrdEditorHeader({
> >
{saveSuccess ? ( {saveSuccess ? (
<> <>
<svg className="w-5 h-5 md:w-4 md:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-5 w-5 md:h-4 md:w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg> </svg>
<span className="hidden sm:inline">Saved!</span> <span className="hidden sm:inline">Saved!</span>
</> </>
) : ( ) : (
<> <>
<Save className="w-5 h-5 md:w-4 md:h-4" /> <Save className="h-5 w-5 md:h-4 md:w-4" />
<span className="hidden sm:inline">{saving ? 'Saving...' : 'Save PRD'}</span> <span className="hidden sm:inline">{saving ? 'Saving...' : 'Save PRD'}</span>
</> </>
)} )}
@@ -211,16 +211,16 @@ export default function PrdEditorHeader({
<button <button
onClick={onToggleFullscreen} onClick={onToggleFullscreen}
className="hidden md:flex p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 items-center justify-center" className="hidden items-center justify-center rounded-md p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white md:flex"
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'} title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
> >
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />} {isFullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
</button> </button>
<HeaderIconButton <HeaderIconButton
title="Close" title="Close"
onClick={onClose} onClick={onClose}
icon={<X className="w-6 h-6 md:w-4 md:h-4" />} icon={<X className="h-6 w-6 md:h-4 md:w-4" />}
/> />
</div> </div>
</div> </div>

View File

@@ -1,9 +1,9 @@
export default function PrdEditorLoadingState() { export default function PrdEditorLoadingState() {
return ( return (
<div className="fixed inset-0 z-[200] md:bg-black/50 md:flex md:items-center md:justify-center"> <div className="fixed inset-0 z-[200] md:flex md:items-center md:justify-center md:bg-black/50">
<div className="w-full h-full md:rounded-lg md:w-auto md:h-auto p-8 flex items-center justify-center bg-white dark:bg-gray-900"> <div className="flex h-full w-full items-center justify-center bg-white p-8 dark:bg-gray-900 md:h-auto md:w-auto md:rounded-lg">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600" /> <div className="h-6 w-6 animate-spin rounded-full border-b-2 border-blue-600" />
<span className="text-gray-900 dark:text-white">Loading PRD...</span> <span className="text-gray-900 dark:text-white">Loading PRD...</span>
</div> </div>
</div> </div>

View File

@@ -65,7 +65,7 @@ export default function PrdEditorWorkspace({
)} )}
> >
{loadError && ( {loadError && (
<div className="px-4 py-3 border-b border-yellow-200 dark:border-yellow-800 bg-yellow-50 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-200 text-sm"> <div className="border-b border-yellow-200 bg-yellow-50 px-4 py-3 text-sm text-yellow-800 dark:border-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-200">
{loadError} {loadError}
</div> </div>
)} )}

View File

@@ -147,12 +147,12 @@ export default function ProjectCreationWizard({
); );
return ( return (
<div className="fixed top-0 left-0 right-0 bottom-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[60] p-0 sm:p-4"> <div className="fixed bottom-0 left-0 right-0 top-0 z-[60] flex items-center justify-center bg-black/50 p-0 backdrop-blur-sm sm:p-4">
<div className="bg-white dark:bg-gray-800 rounded-none sm:rounded-lg shadow-xl w-full h-full sm:h-auto sm:max-w-2xl border-0 sm:border border-gray-200 dark:border-gray-700 overflow-y-auto"> <div className="h-full w-full overflow-y-auto rounded-none border-0 border-gray-200 bg-white shadow-xl dark:border-gray-700 dark:bg-gray-800 sm:h-auto sm:max-w-2xl sm:rounded-lg sm:border">
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700"> <div className="flex items-center justify-between border-b border-gray-200 p-6 dark:border-gray-700">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center"> <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900/50">
<FolderPlus className="w-4 h-4 text-blue-600 dark:text-blue-400" /> <FolderPlus className="h-4 w-4 text-blue-600 dark:text-blue-400" />
</div> </div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white"> <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('projectWizard.title')} {t('projectWizard.title')}
@@ -160,16 +160,16 @@ export default function ProjectCreationWizard({
</div> </div>
<button <button
onClick={onClose} onClick={onClose}
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700" className="rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
disabled={isCreating} disabled={isCreating}
> >
<X className="w-5 h-5" /> <X className="h-5 w-5" />
</button> </button>
</div> </div>
<WizardProgress step={step} /> <WizardProgress step={step} />
<div className="p-6 space-y-6 min-h-[300px]"> <div className="min-h-[300px] space-y-6 p-6">
{error && <ErrorBanner message={error} />} {error && <ErrorBanner message={error} />}
{step === 1 && ( {step === 1 && (

View File

@@ -6,8 +6,8 @@ type ErrorBannerProps = {
export default function ErrorBanner({ message }: ErrorBannerProps) { export default function ErrorBanner({ message }: ErrorBannerProps) {
return ( return (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 flex items-start gap-3"> <div className="flex items-start gap-3 rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20">
<AlertCircle className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" /> <AlertCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-red-600 dark:text-red-400" />
<p className="text-sm text-red-800 dark:text-red-200">{message}</p> <p className="text-sm text-red-800 dark:text-red-200">{message}</p>
</div> </div>
); );

View File

@@ -97,12 +97,12 @@ export default function FolderBrowserModal({
} }
return ( return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[70] p-4"> <div className="fixed inset-0 z-[70] flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] border border-gray-200 dark:border-gray-700 flex flex-col"> <div className="flex max-h-[80vh] w-full max-w-2xl flex-col rounded-lg border border-gray-200 bg-white shadow-xl dark:border-gray-700 dark:bg-gray-800">
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700"> <div className="flex items-center justify-between border-b border-gray-200 p-4 dark:border-gray-700">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center"> <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900/50">
<FolderOpen className="w-4 h-4 text-blue-600 dark:text-blue-400" /> <FolderOpen className="h-4 w-4 text-blue-600 dark:text-blue-400" />
</div> </div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Select Folder</h3> <h3 className="text-lg font-semibold text-gray-900 dark:text-white">Select Folder</h3>
</div> </div>
@@ -110,37 +110,37 @@ export default function FolderBrowserModal({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => setShowHiddenFolders((previous) => !previous)} onClick={() => setShowHiddenFolders((previous) => !previous)}
className={`p-2 rounded-md transition-colors ${ className={`rounded-md p-2 transition-colors ${
showHiddenFolders showHiddenFolders
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30' ? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
: 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700' : 'text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300'
}`} }`}
title={showHiddenFolders ? 'Hide hidden folders' : 'Show hidden folders'} title={showHiddenFolders ? 'Hide hidden folders' : 'Show hidden folders'}
> >
{showHiddenFolders ? <Eye className="w-5 h-5" /> : <EyeOff className="w-5 h-5" />} {showHiddenFolders ? <Eye className="h-5 w-5" /> : <EyeOff className="h-5 w-5" />}
</button> </button>
<button <button
onClick={() => setShowNewFolderInput((previous) => !previous)} onClick={() => setShowNewFolderInput((previous) => !previous)}
className={`p-2 rounded-md transition-colors ${ className={`rounded-md p-2 transition-colors ${
showNewFolderInput showNewFolderInput
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30' ? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
: 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700' : 'text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300'
}`} }`}
title="Create new folder" title="Create new folder"
> >
<Plus className="w-5 h-5" /> <Plus className="h-5 w-5" />
</button> </button>
<button <button
onClick={handleClose} onClick={handleClose}
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700" className="rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-300"
> >
<X className="w-5 h-5" /> <X className="h-5 w-5" />
</button> </button>
</div> </div>
</div> </div>
{showNewFolderInput && ( {showNewFolderInput && (
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-blue-50 dark:bg-blue-900/20"> <div className="border-b border-gray-200 bg-blue-50 px-4 py-3 dark:border-gray-700 dark:bg-blue-900/20">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Input <Input
type="text" type="text"
@@ -163,7 +163,7 @@ export default function FolderBrowserModal({
onClick={handleCreateFolder} onClick={handleCreateFolder}
disabled={!newFolderName.trim() || creatingFolder} disabled={!newFolderName.trim() || creatingFolder}
> >
{creatingFolder ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Create'} {creatingFolder ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Create'}
</Button> </Button>
<Button size="sm" variant="ghost" onClick={resetNewFolderState}> <Button size="sm" variant="ghost" onClick={resetNewFolderState}>
Cancel Cancel
@@ -181,22 +181,22 @@ export default function FolderBrowserModal({
<div className="flex-1 overflow-y-auto p-4"> <div className="flex-1 overflow-y-auto p-4">
{loadingFolders ? ( {loadingFolders ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin text-gray-400" /> <Loader2 className="h-6 w-6 animate-spin text-gray-400" />
</div> </div>
) : ( ) : (
<div className="space-y-1"> <div className="space-y-1">
{parentPath && ( {parentPath && (
<button <button
onClick={() => loadFolders(parentPath)} onClick={() => loadFolders(parentPath)}
className="w-full px-4 py-3 text-left hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg flex items-center gap-3" className="flex w-full items-center gap-3 rounded-lg px-4 py-3 text-left hover:bg-gray-100 dark:hover:bg-gray-700"
> >
<FolderOpen className="w-5 h-5 text-gray-400" /> <FolderOpen className="h-5 w-5 text-gray-400" />
<span className="font-medium text-gray-700 dark:text-gray-300">..</span> <span className="font-medium text-gray-700 dark:text-gray-300">..</span>
</button> </button>
)} )}
{visibleFolders.length === 0 ? ( {visibleFolders.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400"> <div className="py-8 text-center text-gray-500 dark:text-gray-400">
No subfolders found No subfolders found
</div> </div>
) : ( ) : (
@@ -204,9 +204,9 @@ export default function FolderBrowserModal({
<div key={folder.path} className="flex items-center gap-2"> <div key={folder.path} className="flex items-center gap-2">
<button <button
onClick={() => loadFolders(folder.path)} onClick={() => loadFolders(folder.path)}
className="flex-1 px-4 py-3 text-left hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg flex items-center gap-3" className="flex flex-1 items-center gap-3 rounded-lg px-4 py-3 text-left hover:bg-gray-100 dark:hover:bg-gray-700"
> >
<FolderPlus className="w-5 h-5 text-blue-500" /> <FolderPlus className="h-5 w-5 text-blue-500" />
<span className="font-medium text-gray-900 dark:text-white"> <span className="font-medium text-gray-900 dark:text-white">
{folder.name} {folder.name}
</span> </span>
@@ -215,7 +215,7 @@ export default function FolderBrowserModal({
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onFolderSelected(folder.path, autoAdvanceOnSelect)} onClick={() => onFolderSelected(folder.path, autoAdvanceOnSelect)}
className="text-xs px-3" className="px-3 text-xs"
> >
Select Select
</Button> </Button>
@@ -227,9 +227,9 @@ export default function FolderBrowserModal({
</div> </div>
<div className="border-t border-gray-200 dark:border-gray-700"> <div className="border-t border-gray-200 dark:border-gray-700">
<div className="px-4 py-3 bg-gray-50 dark:bg-gray-900/50 flex items-center gap-2"> <div className="flex items-center gap-2 bg-gray-50 px-4 py-3 dark:bg-gray-900/50">
<span className="text-sm text-gray-600 dark:text-gray-400">Path:</span> <span className="text-sm text-gray-600 dark:text-gray-400">Path:</span>
<code className="text-sm font-mono text-gray-900 dark:text-white flex-1 truncate"> <code className="flex-1 truncate font-mono text-sm text-gray-900 dark:text-white">
{currentPath} {currentPath}
</code> </code>
</div> </div>

View File

@@ -38,11 +38,11 @@ export default function GithubAuthenticationCard({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4 border border-gray-200 dark:border-gray-700"> <div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
<div className="flex items-start gap-3 mb-4"> <div className="mb-4 flex items-start gap-3">
<Key className="w-5 h-5 text-gray-600 dark:text-gray-400 flex-shrink-0 mt-0.5" /> <Key className="mt-0.5 h-5 w-5 flex-shrink-0 text-gray-600 dark:text-gray-400" />
<div className="flex-1"> <div className="flex-1">
<h5 className="font-medium text-gray-900 dark:text-white mb-1"> <h5 className="mb-1 font-medium text-gray-900 dark:text-white">
{t('projectWizard.step2.githubAuth')} {t('projectWizard.step2.githubAuth')}
</h5> </h5>
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
@@ -53,18 +53,18 @@ export default function GithubAuthenticationCard({
{loadingTokens && ( {loadingTokens && (
<div className="flex items-center gap-2 text-sm text-gray-500"> <div className="flex items-center gap-2 text-sm text-gray-500">
<Loader2 className="w-4 h-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
{t('projectWizard.step2.loadingTokens')} {t('projectWizard.step2.loadingTokens')}
</div> </div>
)} )}
{!loadingTokens && tokenLoadError && ( {!loadingTokens && tokenLoadError && (
<p className="text-sm text-red-600 dark:text-red-400 mb-3">{tokenLoadError}</p> <p className="mb-3 text-sm text-red-600 dark:text-red-400">{tokenLoadError}</p>
)} )}
{!loadingTokens && availableTokens.length > 0 && ( {!loadingTokens && availableTokens.length > 0 && (
<> <>
<div className="grid grid-cols-3 gap-2 mb-4"> <div className="mb-4 grid grid-cols-3 gap-2">
<button <button
onClick={() => onTokenModeChange('stored')} onClick={() => onTokenModeChange('stored')}
className={getModeClassName(tokenMode, 'stored')} className={getModeClassName(tokenMode, 'stored')}
@@ -91,13 +91,13 @@ export default function GithubAuthenticationCard({
{tokenMode === 'stored' ? ( {tokenMode === 'stored' ? (
<div> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
{t('projectWizard.step2.selectToken')} {t('projectWizard.step2.selectToken')}
</label> </label>
<select <select
value={selectedGithubToken} value={selectedGithubToken}
onChange={(event) => onSelectedGithubTokenChange(event.target.value)} onChange={(event) => onSelectedGithubTokenChange(event.target.value)}
className="w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-sm" className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800"
> >
<option value="">{t('projectWizard.step2.selectTokenPlaceholder')}</option> <option value="">{t('projectWizard.step2.selectTokenPlaceholder')}</option>
{availableTokens.map((token) => ( {availableTokens.map((token) => (
@@ -109,7 +109,7 @@ export default function GithubAuthenticationCard({
</div> </div>
) : tokenMode === 'new' ? ( ) : tokenMode === 'new' ? (
<div> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
{t('projectWizard.step2.newToken')} {t('projectWizard.step2.newToken')}
</label> </label>
<Input <Input
@@ -129,14 +129,14 @@ export default function GithubAuthenticationCard({
{!loadingTokens && availableTokens.length === 0 && ( {!loadingTokens && availableTokens.length === 0 && (
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3 border border-blue-200 dark:border-blue-800"> <div className="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
<p className="text-sm text-blue-800 dark:text-blue-200"> <p className="text-sm text-blue-800 dark:text-blue-200">
{t('projectWizard.step2.publicRepoInfo')} {t('projectWizard.step2.publicRepoInfo')}
</p> </p>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
{t('projectWizard.step2.optionalTokenPublic')} {t('projectWizard.step2.optionalTokenPublic')}
</label> </label>
<Input <Input

View File

@@ -1,9 +1,9 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Input } from '../../../shared/view/ui'; import { Input } from '../../../shared/view/ui';
import GithubAuthenticationCard from './GithubAuthenticationCard';
import WorkspacePathField from './WorkspacePathField';
import { shouldShowGithubAuthentication } from '../utils/pathUtils'; import { shouldShowGithubAuthentication } from '../utils/pathUtils';
import type { GithubTokenCredential, TokenMode, WorkspaceType } from '../types'; import type { GithubTokenCredential, TokenMode, WorkspaceType } from '../types';
import GithubAuthenticationCard from './GithubAuthenticationCard';
import WorkspacePathField from './WorkspacePathField';
type StepConfigurationProps = { type StepConfigurationProps = {
workspaceType: WorkspaceType; workspaceType: WorkspaceType;
@@ -48,7 +48,7 @@ export default function StepConfiguration({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
{workspaceType === 'existing' {workspaceType === 'existing'
? t('projectWizard.step2.existingPath') ? t('projectWizard.step2.existingPath')
: t('projectWizard.step2.newPath')} : t('projectWizard.step2.newPath')}
@@ -72,7 +72,7 @@ export default function StepConfiguration({
{workspaceType === 'new' && ( {workspaceType === 'new' && (
<> <>
<div> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
{t('projectWizard.step2.githubUrl')} {t('projectWizard.step2.githubUrl')}
</label> </label>
<Input <Input

View File

@@ -36,8 +36,8 @@ export default function StepReview({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4 border border-gray-200 dark:border-gray-700"> <div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3"> <h4 className="mb-3 text-sm font-semibold text-gray-900 dark:text-white">
{t('projectWizard.step3.reviewConfig')} {t('projectWizard.step3.reviewConfig')}
</h4> </h4>
@@ -55,7 +55,7 @@ export default function StepReview({
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">{t('projectWizard.step3.path')}</span> <span className="text-gray-600 dark:text-gray-400">{t('projectWizard.step3.path')}</span>
<span className="font-mono text-xs text-gray-900 dark:text-white break-all"> <span className="break-all font-mono text-xs text-gray-900 dark:text-white">
{formState.workspacePath} {formState.workspacePath}
</span> </span>
</div> </div>
@@ -66,7 +66,7 @@ export default function StepReview({
<span className="text-gray-600 dark:text-gray-400"> <span className="text-gray-600 dark:text-gray-400">
{t('projectWizard.step3.cloneFrom')} {t('projectWizard.step3.cloneFrom')}
</span> </span>
<span className="font-mono text-xs text-gray-900 dark:text-white break-all"> <span className="break-all font-mono text-xs text-gray-900 dark:text-white">
{formState.githubUrl} {formState.githubUrl}
</span> </span>
</div> </div>
@@ -82,13 +82,13 @@ export default function StepReview({
</div> </div>
</div> </div>
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800"> <div className="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-800 dark:bg-blue-900/20">
{isCreating && cloneProgress ? ( {isCreating && cloneProgress ? (
<div className="space-y-2"> <div className="space-y-2">
<p className="text-sm font-medium text-blue-800 dark:text-blue-200"> <p className="text-sm font-medium text-blue-800 dark:text-blue-200">
{t('projectWizard.step3.cloningRepository', { defaultValue: 'Cloning repository...' })} {t('projectWizard.step3.cloningRepository', { defaultValue: 'Cloning repository...' })}
</p> </p>
<code className="block text-xs font-mono text-blue-700 dark:text-blue-300 whitespace-pre-wrap break-all"> <code className="block whitespace-pre-wrap break-all font-mono text-xs text-blue-700 dark:text-blue-300">
{cloneProgress} {cloneProgress}
</code> </code>
</div> </div>

View File

@@ -15,25 +15,25 @@ export default function StepTypeSelection({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3"> <h4 className="mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">
{t('projectWizard.step1.question')} {t('projectWizard.step1.question')}
</h4> </h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<button <button
onClick={() => onWorkspaceTypeChange('existing')} onClick={() => onWorkspaceTypeChange('existing')}
className={`p-4 border-2 rounded-lg text-left transition-all ${ className={`rounded-lg border-2 p-4 text-left transition-all ${
workspaceType === 'existing' workspaceType === 'existing'
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600' : 'border-gray-200 hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600'
}`} }`}
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="w-10 h-10 bg-green-100 dark:bg-green-900/50 rounded-lg flex items-center justify-center flex-shrink-0"> <div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-green-100 dark:bg-green-900/50">
<FolderPlus className="w-5 h-5 text-green-600 dark:text-green-400" /> <FolderPlus className="h-5 w-5 text-green-600 dark:text-green-400" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h5 className="font-semibold text-gray-900 dark:text-white mb-1"> <h5 className="mb-1 font-semibold text-gray-900 dark:text-white">
{t('projectWizard.step1.existing.title')} {t('projectWizard.step1.existing.title')}
</h5> </h5>
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
@@ -45,18 +45,18 @@ export default function StepTypeSelection({
<button <button
onClick={() => onWorkspaceTypeChange('new')} onClick={() => onWorkspaceTypeChange('new')}
className={`p-4 border-2 rounded-lg text-left transition-all ${ className={`rounded-lg border-2 p-4 text-left transition-all ${
workspaceType === 'new' workspaceType === 'new'
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600' : 'border-gray-200 hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600'
}`} }`}
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="w-10 h-10 bg-purple-100 dark:bg-purple-900/50 rounded-lg flex items-center justify-center flex-shrink-0"> <div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-900/50">
<GitBranch className="w-5 h-5 text-purple-600 dark:text-purple-400" /> <GitBranch className="h-5 w-5 text-purple-600 dark:text-purple-400" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h5 className="font-semibold text-gray-900 dark:text-white mb-1"> <h5 className="mb-1 font-semibold text-gray-900 dark:text-white">
{t('projectWizard.step1.new.title')} {t('projectWizard.step1.new.title')}
</h5> </h5>
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm text-gray-600 dark:text-gray-400">

View File

@@ -25,13 +25,13 @@ export default function WizardFooter({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="flex items-center justify-between p-6 border-t border-gray-200 dark:border-gray-700"> <div className="flex items-center justify-between border-t border-gray-200 p-6 dark:border-gray-700">
<Button variant="outline" onClick={step === 1 ? onClose : onBack} disabled={isCreating}> <Button variant="outline" onClick={step === 1 ? onClose : onBack} disabled={isCreating}>
{step === 1 ? ( {step === 1 ? (
t('projectWizard.buttons.cancel') t('projectWizard.buttons.cancel')
) : ( ) : (
<> <>
<ChevronLeft className="w-4 h-4 mr-1" /> <ChevronLeft className="mr-1 h-4 w-4" />
{t('projectWizard.buttons.back')} {t('projectWizard.buttons.back')}
</> </>
)} )}
@@ -40,20 +40,20 @@ export default function WizardFooter({
<Button onClick={step === 3 ? onCreate : onNext} disabled={isCreating}> <Button onClick={step === 3 ? onCreate : onNext} disabled={isCreating}>
{isCreating ? ( {isCreating ? (
<> <>
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
{isCloneWorkflow {isCloneWorkflow
? t('projectWizard.buttons.cloning', { defaultValue: 'Cloning...' }) ? t('projectWizard.buttons.cloning', { defaultValue: 'Cloning...' })
: t('projectWizard.buttons.creating')} : t('projectWizard.buttons.creating')}
</> </>
) : step === 3 ? ( ) : step === 3 ? (
<> <>
<Check className="w-4 h-4 mr-1" /> <Check className="mr-1 h-4 w-4" />
{t('projectWizard.buttons.createProject')} {t('projectWizard.buttons.createProject')}
</> </>
) : ( ) : (
<> <>
{t('projectWizard.buttons.next')} {t('projectWizard.buttons.next')}
<ChevronRight className="w-4 h-4 ml-1" /> <ChevronRight className="ml-1 h-4 w-4" />
</> </>
)} )}
</Button> </Button>

View File

@@ -12,23 +12,23 @@ export default function WizardProgress({ step }: WizardProgressProps) {
const steps: WizardStep[] = [1, 2, 3]; const steps: WizardStep[] = [1, 2, 3];
return ( return (
<div className="px-6 pt-4 pb-2"> <div className="px-6 pb-2 pt-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{steps.map((currentStep) => ( {steps.map((currentStep) => (
<Fragment key={currentStep}> <Fragment key={currentStep}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className={`w-8 h-8 rounded-full flex items-center justify-center font-medium text-sm ${ className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium ${
currentStep < step currentStep < step
? 'bg-green-500 text-white' ? 'bg-green-500 text-white'
: currentStep === step : currentStep === step
? 'bg-blue-500 text-white' ? 'bg-blue-500 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-500' : 'bg-gray-200 text-gray-500 dark:bg-gray-700'
}`} }`}
> >
{currentStep < step ? <Check className="w-4 h-4" /> : currentStep} {currentStep < step ? <Check className="h-4 w-4" /> : currentStep}
</div> </div>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 hidden sm:inline"> <span className="hidden text-sm font-medium text-gray-700 dark:text-gray-300 sm:inline">
{currentStep === 1 {currentStep === 1
? t('projectWizard.steps.type') ? t('projectWizard.steps.type')
: currentStep === 2 : currentStep === 2
@@ -39,7 +39,7 @@ export default function WizardProgress({ step }: WizardProgressProps) {
{currentStep < 3 && ( {currentStep < 3 && (
<div <div
className={`flex-1 h-1 mx-2 rounded ${ className={`mx-2 h-1 flex-1 rounded ${
currentStep < step ? 'bg-green-500' : 'bg-gray-200 dark:bg-gray-700' currentStep < step ? 'bg-green-500' : 'bg-gray-200 dark:bg-gray-700'
}`} }`}
/> />

View File

@@ -1,10 +1,10 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { FolderOpen } from 'lucide-react'; import { FolderOpen } from 'lucide-react';
import { Button, Input } from '../../../shared/view/ui'; import { Button, Input } from '../../../shared/view/ui';
import FolderBrowserModal from './FolderBrowserModal';
import { browseFilesystemFolders } from '../data/workspaceApi'; import { browseFilesystemFolders } from '../data/workspaceApi';
import { getSuggestionRootPath } from '../utils/pathUtils'; import { getSuggestionRootPath } from '../utils/pathUtils';
import type { FolderSuggestion, WorkspaceType } from '../types'; import type { FolderSuggestion, WorkspaceType } from '../types';
import FolderBrowserModal from './FolderBrowserModal';
type WorkspacePathFieldProps = { type WorkspacePathFieldProps = {
workspaceType: WorkspaceType; workspaceType: WorkspaceType;
@@ -83,7 +83,7 @@ export default function WorkspacePathField({
return ( return (
<> <>
<div className="relative flex gap-2"> <div className="relative flex gap-2">
<div className="flex-1 relative"> <div className="relative flex-1">
<Input <Input
type="text" type="text"
value={value} value={value}
@@ -98,12 +98,12 @@ export default function WorkspacePathField({
/> />
{showPathDropdown && pathSuggestions.length > 0 && ( {showPathDropdown && pathSuggestions.length > 0 && (
<div className="absolute z-10 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-60 overflow-y-auto"> <div className="absolute z-10 mt-1 max-h-60 w-full overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800">
{pathSuggestions.map((suggestion) => ( {pathSuggestions.map((suggestion) => (
<button <button
key={suggestion.path} key={suggestion.path}
onClick={() => handleSuggestionSelect(suggestion)} onClick={() => handleSuggestionSelect(suggestion)}
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm" className="w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700"
> >
<div className="font-medium text-gray-900 dark:text-white">{suggestion.name}</div> <div className="font-medium text-gray-900 dark:text-white">{suggestion.name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{suggestion.path}</div> <div className="text-xs text-gray-500 dark:text-gray-400">{suggestion.path}</div>
@@ -121,7 +121,7 @@ export default function WorkspacePathField({
title="Browse folders" title="Browse folders"
disabled={disabled} disabled={disabled}
> >
<FolderOpen className="w-4 h-4" /> <FolderOpen className="h-4 w-4" />
</Button> </Button>
</div> </div>

View File

@@ -100,58 +100,58 @@ export default function ProviderLoginModal({
}; };
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999] max-md:items-stretch max-md:justify-stretch"> <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black bg-opacity-50 max-md:items-stretch max-md:justify-stretch">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-4xl h-3/4 flex flex-col md:max-w-4xl md:h-3/4 md:rounded-lg md:m-4 max-md:max-w-none max-md:h-full max-md:rounded-none max-md:m-0"> <div className="flex h-3/4 w-full max-w-4xl flex-col rounded-lg bg-white shadow-xl dark:bg-gray-800 max-md:m-0 max-md:h-full max-md:max-w-none max-md:rounded-none md:m-4 md:h-3/4 md:max-w-4xl md:rounded-lg">
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700"> <div className="flex items-center justify-between border-b border-gray-200 p-4 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{title}</h3> <h3 className="text-lg font-semibold text-gray-900 dark:text-white">{title}</h3>
<button <button
onClick={onClose} onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors" className="text-gray-400 transition-colors hover:text-gray-600 dark:hover:text-gray-300"
aria-label="Close login modal" aria-label="Close login modal"
> >
<X className="w-6 h-6" /> <X className="h-6 w-6" />
</button> </button>
</div> </div>
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
{provider === 'gemini' ? ( {provider === 'gemini' ? (
<div className="flex flex-col items-center justify-center h-full p-8 text-center bg-gray-50 dark:bg-gray-900/50"> <div className="flex h-full flex-col items-center justify-center bg-gray-50 p-8 text-center dark:bg-gray-900/50">
<div className="w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center mb-6"> <div className="mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/30">
<KeyRound className="w-8 h-8 text-blue-600 dark:text-blue-400" /> <KeyRound className="h-8 w-8 text-blue-600 dark:text-blue-400" />
</div> </div>
<h4 className="text-xl font-medium text-gray-900 dark:text-white mb-3">Setup Gemini API Access</h4> <h4 className="mb-3 text-xl font-medium text-gray-900 dark:text-white">Setup Gemini API Access</h4>
<p className="text-gray-600 dark:text-gray-400 max-w-md mb-8"> <p className="mb-8 max-w-md text-gray-600 dark:text-gray-400">
The Gemini CLI requires an API key to function. Configure it in your terminal first. The Gemini CLI requires an API key to function. Configure it in your terminal first.
</p> </p>
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 max-w-lg w-full text-left shadow-sm"> <div className="w-full max-w-lg rounded-xl border border-gray-200 bg-white p-6 text-left shadow-sm dark:border-gray-700 dark:bg-gray-800">
<ol className="space-y-4"> <ol className="space-y-4">
<li className="flex gap-4"> <li className="flex gap-4">
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 flex items-center justify-center text-sm font-medium"> <div className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 text-sm font-medium text-blue-600 dark:bg-blue-900/50 dark:text-blue-400">
1 1
</div> </div>
<div> <div>
<p className="text-sm font-medium text-gray-900 dark:text-white mb-1">Get your API key</p> <p className="mb-1 text-sm font-medium text-gray-900 dark:text-white">Get your API key</p>
<a <a
href="https://aistudio.google.com/app/apikey" href="https://aistudio.google.com/app/apikey"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="text-sm text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1 inline-flex" className="flex inline-flex items-center gap-1 text-sm text-blue-600 hover:underline dark:text-blue-400"
> >
Google AI Studio <ExternalLink className="w-3 h-3" /> Google AI Studio <ExternalLink className="h-3 w-3" />
</a> </a>
</div> </div>
</li> </li>
<li className="flex gap-4"> <li className="flex gap-4">
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 flex items-center justify-center text-sm font-medium"> <div className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 text-sm font-medium text-blue-600 dark:bg-blue-900/50 dark:text-blue-400">
2 2
</div> </div>
<div> <div>
<p className="text-sm font-medium text-gray-900 dark:text-white mb-1">Run configuration</p> <p className="mb-1 text-sm font-medium text-gray-900 dark:text-white">Run configuration</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">Open your terminal and run:</p> <p className="mb-2 text-sm text-gray-600 dark:text-gray-400">Open your terminal and run:</p>
<code className="block bg-gray-100 dark:bg-gray-900 px-3 py-2 rounded text-sm text-pink-600 dark:text-pink-400 font-mono"> <code className="block rounded bg-gray-100 px-3 py-2 font-mono text-sm text-pink-600 dark:bg-gray-900 dark:text-pink-400">
gemini config set api_key YOUR_KEY gemini config set api_key YOUR_KEY
</code> </code>
</div> </div>
@@ -161,7 +161,7 @@ export default function ProviderLoginModal({
<button <button
onClick={onClose} onClick={onClose}
className="mt-8 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors" className="mt-8 rounded-lg bg-blue-600 px-6 py-2.5 font-medium text-white transition-colors hover:bg-blue-700"
> >
Done Done
</button> </button>

View File

@@ -45,7 +45,7 @@ export default function QuickSettingsContent({
); );
return ( return (
<div className={`flex-1 overflow-y-auto overflow-x-hidden p-4 space-y-6 bg-background ${isMobile ? 'pb-mobile-nav' : ''}`}> <div className={`flex-1 space-y-6 overflow-y-auto overflow-x-hidden bg-background p-4 ${isMobile ? 'pb-mobile-nav' : ''}`}>
<QuickSettingsSection title={t('quickSettings.sections.appearance')}> <QuickSettingsSection title={t('quickSettings.sections.appearance')}>
<div className={SETTING_ROW_CLASS}> <div className={SETTING_ROW_CLASS}>
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white"> <span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
@@ -71,7 +71,7 @@ export default function QuickSettingsContent({
<QuickSettingsSection title={t('quickSettings.sections.inputSettings')}> <QuickSettingsSection title={t('quickSettings.sections.inputSettings')}>
{renderToggleRows(INPUT_SETTING_TOGGLES)} {renderToggleRows(INPUT_SETTING_TOGGLES)}
<p className="text-xs text-gray-500 dark:text-gray-400 ml-3"> <p className="ml-3 text-xs text-gray-500 dark:text-gray-400">
{t('quickSettings.sendByCtrlEnterDescription')} {t('quickSettings.sendByCtrlEnterDescription')}
</p> </p>
</QuickSettingsSection> </QuickSettingsSection>

Some files were not shown because too many files have changed in this diff Show More