mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-21 16:17:34 +00:00
Feat: Refine design language and use theme tokens across most pages.
This commit is contained in:
@@ -538,7 +538,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|||||||
return { behavior: 'deny', message: decision.message ?? 'User denied tool use' };
|
return { behavior: 'deny', message: decision.message ?? 'User denied tool use' };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set stream-close timeout for interactive tools (Query constructor reads it synchronously)
|
// Set stream-close timeout for interactive tools (Query constructor reads it synchronously). Claude Agent SDK has a default of 5s and this overrides it
|
||||||
const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
|
const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
|
||||||
process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '300000';
|
process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '300000';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
function DiffViewer({ diff, fileName, isMobile, wrapText }) {
|
function DiffViewer({ diff, fileName, isMobile, wrapText }) {
|
||||||
if (!diff) {
|
if (!diff) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 text-center text-gray-500 dark:text-gray-400 text-sm">
|
<div className="p-4 text-center text-muted-foreground text-sm">
|
||||||
No diff available
|
No diff available
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -17,13 +17,13 @@ function DiffViewer({ diff, fileName, isMobile, wrapText }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`font-mono text-xs p-2 ${
|
className={`font-mono text-xs px-3 py-0.5 ${
|
||||||
isMobile && wrapText ? 'whitespace-pre-wrap break-all' : 'whitespace-pre overflow-x-auto'
|
isMobile && wrapText ? 'whitespace-pre-wrap break-all' : 'whitespace-pre overflow-x-auto'
|
||||||
} ${
|
} ${
|
||||||
isAddition ? 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300' :
|
isAddition ? 'bg-green-50 dark:bg-green-950/50 text-green-700 dark:text-green-300' :
|
||||||
isDeletion ? 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300' :
|
isDeletion ? 'bg-red-50 dark:bg-red-950/50 text-red-700 dark:text-red-300' :
|
||||||
isHeader ? 'bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300' :
|
isHeader ? 'bg-primary/5 text-primary' :
|
||||||
'text-gray-600 dark:text-gray-400'
|
'text-muted-foreground/70'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{line}
|
{line}
|
||||||
|
|||||||
@@ -3,12 +3,263 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { ScrollArea } from './ui/scroll-area';
|
import { ScrollArea } from './ui/scroll-area';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { Input } from './ui/input';
|
import { Input } from './ui/input';
|
||||||
import { Folder, FolderOpen, File, FileText, FileCode, List, TableProperties, Eye, Search, X } from 'lucide-react';
|
import {
|
||||||
|
Folder, FolderOpen, File, FileText, FileCode, List, TableProperties, Eye, Search, X,
|
||||||
|
ChevronRight,
|
||||||
|
FileJson, FileType, FileSpreadsheet, FileArchive,
|
||||||
|
Hash, Braces, Terminal, Database, Globe, Palette, Music2, Video, Archive,
|
||||||
|
Lock, Shield, Settings, Image, BookOpen, Cpu, Box, Gem, Coffee,
|
||||||
|
Flame, Hexagon, FileCode2, Code2, Cog, FileWarning, Binary, SquareFunction,
|
||||||
|
Scroll, FlaskConical, NotebookPen, FileCheck, Workflow, Blocks
|
||||||
|
} from 'lucide-react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import CodeEditor from './CodeEditor';
|
import CodeEditor from './CodeEditor';
|
||||||
import ImageViewer from './ImageViewer';
|
import ImageViewer from './ImageViewer';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
|
||||||
|
// ─── File Icon Registry ──────────────────────────────────────────────
|
||||||
|
// Maps file extensions (and special filenames) to { icon, colorClass } pairs.
|
||||||
|
// Uses lucide-react icons mapped semantically to file types.
|
||||||
|
|
||||||
|
const ICON_SIZE = 'w-4 h-4 flex-shrink-0';
|
||||||
|
|
||||||
|
const FILE_ICON_MAP = {
|
||||||
|
// ── JavaScript / TypeScript ──
|
||||||
|
js: { icon: FileCode, color: 'text-yellow-500' },
|
||||||
|
jsx: { icon: FileCode, color: 'text-yellow-500' },
|
||||||
|
mjs: { icon: FileCode, color: 'text-yellow-500' },
|
||||||
|
cjs: { icon: FileCode, color: 'text-yellow-500' },
|
||||||
|
ts: { icon: FileCode2, color: 'text-blue-500' },
|
||||||
|
tsx: { icon: FileCode2, color: 'text-blue-500' },
|
||||||
|
mts: { icon: FileCode2, color: 'text-blue-500' },
|
||||||
|
|
||||||
|
// ── Python ──
|
||||||
|
py: { icon: Code2, color: 'text-emerald-500' },
|
||||||
|
pyw: { icon: Code2, color: 'text-emerald-500' },
|
||||||
|
pyi: { icon: Code2, color: 'text-emerald-400' },
|
||||||
|
ipynb:{ icon: NotebookPen, color: 'text-orange-500' },
|
||||||
|
|
||||||
|
// ── Rust ──
|
||||||
|
rs: { icon: Cog, color: 'text-orange-600' },
|
||||||
|
toml: { icon: Settings, color: 'text-gray-500' },
|
||||||
|
|
||||||
|
// ── Go ──
|
||||||
|
go: { icon: Hexagon, color: 'text-cyan-500' },
|
||||||
|
|
||||||
|
// ── Ruby ──
|
||||||
|
rb: { icon: Gem, color: 'text-red-500' },
|
||||||
|
erb: { icon: Gem, color: 'text-red-400' },
|
||||||
|
|
||||||
|
// ── PHP ──
|
||||||
|
php: { icon: Blocks, color: 'text-violet-500' },
|
||||||
|
|
||||||
|
// ── Java / Kotlin ──
|
||||||
|
java: { icon: Coffee, color: 'text-red-600' },
|
||||||
|
jar: { icon: Coffee, color: 'text-red-500' },
|
||||||
|
kt: { icon: Hexagon, color: 'text-violet-500' },
|
||||||
|
kts: { icon: Hexagon, color: 'text-violet-400' },
|
||||||
|
|
||||||
|
// ── C / C++ ──
|
||||||
|
c: { icon: Cpu, color: 'text-blue-600' },
|
||||||
|
h: { icon: Cpu, color: 'text-blue-400' },
|
||||||
|
cpp: { icon: Cpu, color: 'text-blue-700' },
|
||||||
|
hpp: { icon: Cpu, color: 'text-blue-500' },
|
||||||
|
cc: { icon: Cpu, color: 'text-blue-700' },
|
||||||
|
|
||||||
|
// ── C# ──
|
||||||
|
cs: { icon: Hexagon, color: 'text-purple-600' },
|
||||||
|
|
||||||
|
// ── Swift ──
|
||||||
|
swift:{ icon: Flame, color: 'text-orange-500' },
|
||||||
|
|
||||||
|
// ── Lua ──
|
||||||
|
lua: { icon: SquareFunction, color: 'text-blue-500' },
|
||||||
|
|
||||||
|
// ── R ──
|
||||||
|
r: { icon: FlaskConical, color: 'text-blue-600' },
|
||||||
|
|
||||||
|
// ── Web ──
|
||||||
|
html: { icon: Globe, color: 'text-orange-600' },
|
||||||
|
htm: { icon: Globe, color: 'text-orange-600' },
|
||||||
|
css: { icon: Hash, color: 'text-blue-500' },
|
||||||
|
scss: { icon: Hash, color: 'text-pink-500' },
|
||||||
|
sass: { icon: Hash, color: 'text-pink-400' },
|
||||||
|
less: { icon: Hash, color: 'text-indigo-500' },
|
||||||
|
vue: { icon: FileCode2, color: 'text-emerald-500' },
|
||||||
|
svelte:{ icon: FileCode2, color: 'text-orange-500' },
|
||||||
|
|
||||||
|
// ── Data / Config ──
|
||||||
|
json: { icon: Braces, color: 'text-yellow-600' },
|
||||||
|
jsonc:{ icon: Braces, color: 'text-yellow-500' },
|
||||||
|
json5:{ icon: Braces, color: 'text-yellow-500' },
|
||||||
|
yaml: { icon: Settings, color: 'text-purple-400' },
|
||||||
|
yml: { icon: Settings, color: 'text-purple-400' },
|
||||||
|
xml: { icon: FileCode, color: 'text-orange-500' },
|
||||||
|
csv: { icon: FileSpreadsheet, color: 'text-green-600' },
|
||||||
|
tsv: { icon: FileSpreadsheet, color: 'text-green-500' },
|
||||||
|
sql: { icon: Database, color: 'text-blue-500' },
|
||||||
|
graphql:{ icon: Workflow, color: 'text-pink-500' },
|
||||||
|
gql: { icon: Workflow, color: 'text-pink-500' },
|
||||||
|
proto:{ icon: Box, color: 'text-green-500' },
|
||||||
|
env: { icon: Shield, color: 'text-yellow-600' },
|
||||||
|
|
||||||
|
// ── Documents ──
|
||||||
|
md: { icon: BookOpen, color: 'text-blue-500' },
|
||||||
|
mdx: { icon: BookOpen, color: 'text-blue-400' },
|
||||||
|
txt: { icon: FileText, color: 'text-gray-500' },
|
||||||
|
doc: { icon: FileText, color: 'text-blue-600' },
|
||||||
|
docx: { icon: FileText, color: 'text-blue-600' },
|
||||||
|
pdf: { icon: FileCheck, color: 'text-red-600' },
|
||||||
|
rtf: { icon: FileText, color: 'text-gray-500' },
|
||||||
|
tex: { icon: Scroll, color: 'text-teal-600' },
|
||||||
|
rst: { icon: FileText, color: 'text-gray-400' },
|
||||||
|
|
||||||
|
// ── Shell / Scripts ──
|
||||||
|
sh: { icon: Terminal, color: 'text-green-500' },
|
||||||
|
bash: { icon: Terminal, color: 'text-green-500' },
|
||||||
|
zsh: { icon: Terminal, color: 'text-green-400' },
|
||||||
|
fish: { icon: Terminal, color: 'text-green-400' },
|
||||||
|
ps1: { icon: Terminal, color: 'text-blue-400' },
|
||||||
|
bat: { icon: Terminal, color: 'text-gray-500' },
|
||||||
|
cmd: { icon: Terminal, color: 'text-gray-500' },
|
||||||
|
|
||||||
|
// ── Images ──
|
||||||
|
png: { icon: Image, color: 'text-purple-500' },
|
||||||
|
jpg: { icon: Image, color: 'text-purple-500' },
|
||||||
|
jpeg: { icon: Image, color: 'text-purple-500' },
|
||||||
|
gif: { icon: Image, color: 'text-purple-400' },
|
||||||
|
webp: { icon: Image, color: 'text-purple-400' },
|
||||||
|
ico: { icon: Image, color: 'text-purple-400' },
|
||||||
|
bmp: { icon: Image, color: 'text-purple-400' },
|
||||||
|
tiff: { icon: Image, color: 'text-purple-400' },
|
||||||
|
svg: { icon: Palette, color: 'text-amber-500' },
|
||||||
|
|
||||||
|
// ── Audio ──
|
||||||
|
mp3: { icon: Music2, color: 'text-pink-500' },
|
||||||
|
wav: { icon: Music2, color: 'text-pink-500' },
|
||||||
|
ogg: { icon: Music2, color: 'text-pink-400' },
|
||||||
|
flac: { icon: Music2, color: 'text-pink-400' },
|
||||||
|
aac: { icon: Music2, color: 'text-pink-400' },
|
||||||
|
m4a: { icon: Music2, color: 'text-pink-400' },
|
||||||
|
|
||||||
|
// ── Video ──
|
||||||
|
mp4: { icon: Video, color: 'text-rose-500' },
|
||||||
|
mov: { icon: Video, color: 'text-rose-500' },
|
||||||
|
avi: { icon: Video, color: 'text-rose-500' },
|
||||||
|
webm: { icon: Video, color: 'text-rose-400' },
|
||||||
|
mkv: { icon: Video, color: 'text-rose-400' },
|
||||||
|
|
||||||
|
// ── Fonts ──
|
||||||
|
ttf: { icon: FileType, color: 'text-red-500' },
|
||||||
|
otf: { icon: FileType, color: 'text-red-500' },
|
||||||
|
woff: { icon: FileType, color: 'text-red-400' },
|
||||||
|
woff2:{ icon: FileType, color: 'text-red-400' },
|
||||||
|
eot: { icon: FileType, color: 'text-red-400' },
|
||||||
|
|
||||||
|
// ── Archives ──
|
||||||
|
zip: { icon: Archive, color: 'text-amber-600' },
|
||||||
|
tar: { icon: Archive, color: 'text-amber-600' },
|
||||||
|
gz: { icon: Archive, color: 'text-amber-600' },
|
||||||
|
bz2: { icon: Archive, color: 'text-amber-600' },
|
||||||
|
rar: { icon: Archive, color: 'text-amber-500' },
|
||||||
|
'7z': { icon: Archive, color: 'text-amber-500' },
|
||||||
|
|
||||||
|
// ── Lock files ──
|
||||||
|
lock: { icon: Lock, color: 'text-gray-500' },
|
||||||
|
|
||||||
|
// ── Binary / Executable ──
|
||||||
|
exe: { icon: Binary, color: 'text-gray-500' },
|
||||||
|
bin: { icon: Binary, color: 'text-gray-500' },
|
||||||
|
dll: { icon: Binary, color: 'text-gray-400' },
|
||||||
|
so: { icon: Binary, color: 'text-gray-400' },
|
||||||
|
dylib:{ icon: Binary, color: 'text-gray-400' },
|
||||||
|
wasm: { icon: Binary, color: 'text-purple-500' },
|
||||||
|
|
||||||
|
// ── Misc config ──
|
||||||
|
ini: { icon: Settings, color: 'text-gray-500' },
|
||||||
|
cfg: { icon: Settings, color: 'text-gray-500' },
|
||||||
|
conf: { icon: Settings, color: 'text-gray-500' },
|
||||||
|
log: { icon: Scroll, color: 'text-gray-400' },
|
||||||
|
map: { icon: File, color: 'text-gray-400' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Special full-filename matches (highest priority)
|
||||||
|
const FILENAME_ICON_MAP = {
|
||||||
|
'Dockerfile': { icon: Box, color: 'text-blue-500' },
|
||||||
|
'docker-compose.yml': { icon: Box, color: 'text-blue-500' },
|
||||||
|
'docker-compose.yaml': { icon: Box, color: 'text-blue-500' },
|
||||||
|
'.dockerignore': { icon: Box, color: 'text-gray-500' },
|
||||||
|
'.gitignore': { icon: Settings, color: 'text-gray-500' },
|
||||||
|
'.gitmodules': { icon: Settings, color: 'text-gray-500' },
|
||||||
|
'.gitattributes': { icon: Settings, color: 'text-gray-500' },
|
||||||
|
'.editorconfig': { icon: Settings, color: 'text-gray-500' },
|
||||||
|
'.prettierrc': { icon: Settings, color: 'text-pink-400' },
|
||||||
|
'.prettierignore': { icon: Settings, color: 'text-gray-500' },
|
||||||
|
'.eslintrc': { icon: Settings, color: 'text-violet-500' },
|
||||||
|
'.eslintrc.js': { icon: Settings, color: 'text-violet-500' },
|
||||||
|
'.eslintrc.json': { icon: Settings, color: 'text-violet-500' },
|
||||||
|
'.eslintrc.cjs': { icon: Settings, color: 'text-violet-500' },
|
||||||
|
'eslint.config.js': { icon: Settings, color: 'text-violet-500' },
|
||||||
|
'eslint.config.mjs':{ icon: Settings, color: 'text-violet-500' },
|
||||||
|
'.env': { icon: Shield, color: 'text-yellow-600' },
|
||||||
|
'.env.local': { icon: Shield, color: 'text-yellow-600' },
|
||||||
|
'.env.development': { icon: Shield, color: 'text-yellow-500' },
|
||||||
|
'.env.production': { icon: Shield, color: 'text-yellow-600' },
|
||||||
|
'.env.example': { icon: Shield, color: 'text-yellow-400' },
|
||||||
|
'package.json': { icon: Braces, color: 'text-green-500' },
|
||||||
|
'package-lock.json':{ icon: Lock, color: 'text-gray-500' },
|
||||||
|
'yarn.lock': { icon: Lock, color: 'text-blue-400' },
|
||||||
|
'pnpm-lock.yaml': { icon: Lock, color: 'text-orange-400' },
|
||||||
|
'bun.lockb': { icon: Lock, color: 'text-gray-400' },
|
||||||
|
'Cargo.toml': { icon: Cog, color: 'text-orange-600' },
|
||||||
|
'Cargo.lock': { icon: Lock, color: 'text-orange-400' },
|
||||||
|
'Gemfile': { icon: Gem, color: 'text-red-500' },
|
||||||
|
'Gemfile.lock': { icon: Lock, color: 'text-red-400' },
|
||||||
|
'Makefile': { icon: Terminal, color: 'text-gray-500' },
|
||||||
|
'CMakeLists.txt': { icon: Cog, color: 'text-blue-500' },
|
||||||
|
'tsconfig.json': { icon: Braces, color: 'text-blue-500' },
|
||||||
|
'jsconfig.json': { icon: Braces, color: 'text-yellow-500' },
|
||||||
|
'vite.config.ts': { icon: Flame, color: 'text-purple-500' },
|
||||||
|
'vite.config.js': { icon: Flame, color: 'text-purple-500' },
|
||||||
|
'webpack.config.js':{ icon: Cog, color: 'text-blue-500' },
|
||||||
|
'tailwind.config.js':{ icon: Hash, color: 'text-cyan-500' },
|
||||||
|
'tailwind.config.ts':{ icon: Hash, color: 'text-cyan-500' },
|
||||||
|
'postcss.config.js':{ icon: Cog, color: 'text-red-400' },
|
||||||
|
'babel.config.js': { icon: Settings, color: 'text-yellow-500' },
|
||||||
|
'.babelrc': { icon: Settings, color: 'text-yellow-500' },
|
||||||
|
'README.md': { icon: BookOpen, color: 'text-blue-500' },
|
||||||
|
'LICENSE': { icon: FileCheck, color: 'text-gray-500' },
|
||||||
|
'LICENSE.md': { icon: FileCheck, color: 'text-gray-500' },
|
||||||
|
'CHANGELOG.md': { icon: Scroll, color: 'text-blue-400' },
|
||||||
|
'requirements.txt': { icon: FileText, color: 'text-emerald-400' },
|
||||||
|
'go.mod': { icon: Hexagon, color: 'text-cyan-500' },
|
||||||
|
'go.sum': { icon: Lock, color: 'text-cyan-400' },
|
||||||
|
};
|
||||||
|
|
||||||
|
function getFileIconData(filename) {
|
||||||
|
// 1. Exact filename match
|
||||||
|
if (FILENAME_ICON_MAP[filename]) {
|
||||||
|
return FILENAME_ICON_MAP[filename];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check for .env prefix pattern
|
||||||
|
if (filename.startsWith('.env')) {
|
||||||
|
return { icon: Shield, color: 'text-yellow-600' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Extension-based lookup
|
||||||
|
const ext = filename.split('.').pop()?.toLowerCase();
|
||||||
|
if (ext && FILE_ICON_MAP[ext]) {
|
||||||
|
return FILE_ICON_MAP[ext];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fallback
|
||||||
|
return { icon: File, color: 'text-muted-foreground' };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
function FileTree({ selectedProject }) {
|
function FileTree({ selectedProject }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [files, setFiles] = useState([]);
|
const [files, setFiles] = useState([]);
|
||||||
@@ -16,7 +267,7 @@ function FileTree({ selectedProject }) {
|
|||||||
const [expandedDirs, setExpandedDirs] = useState(new Set());
|
const [expandedDirs, setExpandedDirs] = useState(new Set());
|
||||||
const [selectedFile, setSelectedFile] = useState(null);
|
const [selectedFile, setSelectedFile] = useState(null);
|
||||||
const [selectedImage, setSelectedImage] = useState(null);
|
const [selectedImage, setSelectedImage] = useState(null);
|
||||||
const [viewMode, setViewMode] = useState('detailed'); // 'simple', 'detailed', 'compact'
|
const [viewMode, setViewMode] = useState('detailed');
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [filteredFiles, setFilteredFiles] = useState([]);
|
const [filteredFiles, setFilteredFiles] = useState([]);
|
||||||
|
|
||||||
@@ -26,7 +277,6 @@ function FileTree({ selectedProject }) {
|
|||||||
}
|
}
|
||||||
}, [selectedProject]);
|
}, [selectedProject]);
|
||||||
|
|
||||||
// Load view mode preference from localStorage
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedViewMode = localStorage.getItem('file-tree-view-mode');
|
const savedViewMode = localStorage.getItem('file-tree-view-mode');
|
||||||
if (savedViewMode && ['simple', 'detailed', 'compact'].includes(savedViewMode)) {
|
if (savedViewMode && ['simple', 'detailed', 'compact'].includes(savedViewMode)) {
|
||||||
@@ -34,7 +284,6 @@ function FileTree({ selectedProject }) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Filter files based on search query
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchQuery.trim()) {
|
if (!searchQuery.trim()) {
|
||||||
setFilteredFiles(files);
|
setFilteredFiles(files);
|
||||||
@@ -42,7 +291,6 @@ function FileTree({ selectedProject }) {
|
|||||||
const filtered = filterFiles(files, searchQuery.toLowerCase());
|
const filtered = filterFiles(files, searchQuery.toLowerCase());
|
||||||
setFilteredFiles(filtered);
|
setFilteredFiles(filtered);
|
||||||
|
|
||||||
// Auto-expand directories that contain matches
|
|
||||||
const expandMatches = (items) => {
|
const expandMatches = (items) => {
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
if (item.type === 'directory' && item.children && item.children.length > 0) {
|
if (item.type === 'directory' && item.children && item.children.length > 0) {
|
||||||
@@ -55,7 +303,6 @@ function FileTree({ selectedProject }) {
|
|||||||
}
|
}
|
||||||
}, [files, searchQuery]);
|
}, [files, searchQuery]);
|
||||||
|
|
||||||
// Recursively filter files and directories based on search query
|
|
||||||
const filterFiles = (items, query) => {
|
const filterFiles = (items, query) => {
|
||||||
return items.reduce((filtered, item) => {
|
return items.reduce((filtered, item) => {
|
||||||
const matchesName = item.name.toLowerCase().includes(query);
|
const matchesName = item.name.toLowerCase().includes(query);
|
||||||
@@ -65,9 +312,6 @@ function FileTree({ selectedProject }) {
|
|||||||
filteredChildren = filterFiles(item.children, query);
|
filteredChildren = filterFiles(item.children, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include item if:
|
|
||||||
// 1. It matches the search query, or
|
|
||||||
// 2. It's a directory with matching children
|
|
||||||
if (matchesName || filteredChildren.length > 0) {
|
if (matchesName || filteredChildren.length > 0) {
|
||||||
filtered.push({
|
filtered.push({
|
||||||
...item,
|
...item,
|
||||||
@@ -83,14 +327,14 @@ function FileTree({ selectedProject }) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await api.getFiles(selectedProject.name);
|
const response = await api.getFiles(selectedProject.name);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
console.error('❌ File fetch failed:', response.status, errorText);
|
console.error('❌ File fetch failed:', response.status, errorText);
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setFiles(data);
|
setFiles(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -111,13 +355,11 @@ function FileTree({ selectedProject }) {
|
|||||||
setExpandedDirs(newExpanded);
|
setExpandedDirs(newExpanded);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Change view mode and save preference
|
|
||||||
const changeViewMode = (mode) => {
|
const changeViewMode = (mode) => {
|
||||||
setViewMode(mode);
|
setViewMode(mode);
|
||||||
localStorage.setItem('file-tree-view-mode', mode);
|
localStorage.setItem('file-tree-view-mode', mode);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format file size
|
|
||||||
const formatFileSize = (bytes) => {
|
const formatFileSize = (bytes) => {
|
||||||
if (!bytes || bytes === 0) return '0 B';
|
if (!bytes || bytes === 0) return '0 B';
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
@@ -126,7 +368,6 @@ function FileTree({ selectedProject }) {
|
|||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format date as relative time
|
|
||||||
const formatRelativeTime = (date) => {
|
const formatRelativeTime = (date) => {
|
||||||
if (!date) return '-';
|
if (!date) return '-';
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -140,65 +381,6 @@ function FileTree({ selectedProject }) {
|
|||||||
return past.toLocaleDateString();
|
return past.toLocaleDateString();
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderFileTree = (items, level = 0) => {
|
|
||||||
return items.map((item) => (
|
|
||||||
<div key={item.path} className="select-none">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className={cn(
|
|
||||||
"w-full justify-start p-2 h-auto font-normal text-left hover:bg-accent",
|
|
||||||
)}
|
|
||||||
style={{ paddingLeft: `${level * 16 + 12}px` }}
|
|
||||||
onClick={() => {
|
|
||||||
if (item.type === 'directory') {
|
|
||||||
toggleDirectory(item.path);
|
|
||||||
} else if (isImageFile(item.name)) {
|
|
||||||
// Open image in viewer
|
|
||||||
setSelectedImage({
|
|
||||||
name: item.name,
|
|
||||||
path: item.path,
|
|
||||||
projectPath: selectedProject.path,
|
|
||||||
projectName: selectedProject.name
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Open file in editor
|
|
||||||
setSelectedFile({
|
|
||||||
name: item.name,
|
|
||||||
path: item.path,
|
|
||||||
projectPath: selectedProject.path,
|
|
||||||
projectName: selectedProject.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 min-w-0 w-full">
|
|
||||||
{item.type === 'directory' ? (
|
|
||||||
expandedDirs.has(item.path) ? (
|
|
||||||
<FolderOpen className="w-4 h-4 text-blue-500 flex-shrink-0" />
|
|
||||||
) : (
|
|
||||||
<Folder className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
getFileIcon(item.name)
|
|
||||||
)}
|
|
||||||
<span className="text-sm truncate text-foreground">
|
|
||||||
{item.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{item.type === 'directory' &&
|
|
||||||
expandedDirs.has(item.path) &&
|
|
||||||
item.children &&
|
|
||||||
item.children.length > 0 && (
|
|
||||||
<div>
|
|
||||||
{renderFileTree(item.children, level + 1)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
const isImageFile = (filename) => {
|
const isImageFile = (filename) => {
|
||||||
const ext = filename.split('.').pop()?.toLowerCase();
|
const ext = filename.split('.').pop()?.toLowerCase();
|
||||||
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp'];
|
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp'];
|
||||||
@@ -206,208 +388,289 @@ function FileTree({ selectedProject }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getFileIcon = (filename) => {
|
const getFileIcon = (filename) => {
|
||||||
const ext = filename.split('.').pop()?.toLowerCase();
|
const { icon: Icon, color } = getFileIconData(filename);
|
||||||
|
return <Icon className={cn(ICON_SIZE, color)} />;
|
||||||
const codeExtensions = ['js', 'jsx', 'ts', 'tsx', 'py', 'java', 'cpp', 'c', 'php', 'rb', 'go', 'rs'];
|
};
|
||||||
const docExtensions = ['md', 'txt', 'doc', 'pdf'];
|
|
||||||
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp'];
|
// ── Click handler shared across all view modes ──
|
||||||
|
const handleItemClick = (item) => {
|
||||||
if (codeExtensions.includes(ext)) {
|
if (item.type === 'directory') {
|
||||||
return <FileCode className="w-4 h-4 text-green-500 flex-shrink-0" />;
|
toggleDirectory(item.path);
|
||||||
} else if (docExtensions.includes(ext)) {
|
} else if (isImageFile(item.name)) {
|
||||||
return <FileText className="w-4 h-4 text-blue-500 flex-shrink-0" />;
|
setSelectedImage({
|
||||||
} else if (imageExtensions.includes(ext)) {
|
name: item.name,
|
||||||
return <File className="w-4 h-4 text-purple-500 flex-shrink-0" />;
|
path: item.path,
|
||||||
|
projectPath: selectedProject.path,
|
||||||
|
projectName: selectedProject.name
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return <File className="w-4 h-4 text-muted-foreground flex-shrink-0" />;
|
setSelectedFile({
|
||||||
|
name: item.name,
|
||||||
|
path: item.path,
|
||||||
|
projectPath: selectedProject.path,
|
||||||
|
projectName: selectedProject.name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render detailed view with table-like layout
|
// ── Indent guide + folder/file icon rendering ──
|
||||||
|
const renderIndentGuides = (level) => {
|
||||||
|
if (level === 0) return null;
|
||||||
|
return (
|
||||||
|
<span className="flex items-center flex-shrink-0" aria-hidden="true">
|
||||||
|
{Array.from({ length: level }).map((_, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className="inline-block w-4 h-full border-l border-border/50"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderItemIcons = (item) => {
|
||||||
|
const isDir = item.type === 'directory';
|
||||||
|
const isOpen = expandedDirs.has(item.path);
|
||||||
|
|
||||||
|
if (isDir) {
|
||||||
|
return (
|
||||||
|
<span className="flex items-center gap-0.5 flex-shrink-0">
|
||||||
|
<ChevronRight
|
||||||
|
className={cn(
|
||||||
|
'w-3.5 h-3.5 text-muted-foreground/70 transition-transform duration-150',
|
||||||
|
isOpen && 'rotate-90'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{isOpen ? (
|
||||||
|
<FolderOpen className="w-4 h-4 text-blue-500 flex-shrink-0" />
|
||||||
|
) : (
|
||||||
|
<Folder className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="flex items-center flex-shrink-0 ml-[18px]">
|
||||||
|
{getFileIcon(item.name)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Simple (Tree) View ────────────────────────────────────────────
|
||||||
|
const renderFileTree = (items, level = 0) => {
|
||||||
|
return items.map((item) => {
|
||||||
|
const isDir = item.type === 'directory';
|
||||||
|
const isOpen = isDir && expandedDirs.has(item.path);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={item.path} className="select-none">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'group flex items-center gap-1.5 py-[3px] pr-2 cursor-pointer rounded-sm',
|
||||||
|
'hover:bg-accent/60 transition-colors duration-100',
|
||||||
|
isDir && isOpen && 'border-l-2 border-primary/30',
|
||||||
|
isDir && !isOpen && 'border-l-2 border-transparent',
|
||||||
|
!isDir && 'border-l-2 border-transparent',
|
||||||
|
)}
|
||||||
|
style={{ paddingLeft: `${level * 16 + 4}px` }}
|
||||||
|
onClick={() => handleItemClick(item)}
|
||||||
|
>
|
||||||
|
{renderItemIcons(item)}
|
||||||
|
<span className={cn(
|
||||||
|
'text-[13px] leading-tight truncate',
|
||||||
|
isDir ? 'font-medium text-foreground' : 'text-foreground/90'
|
||||||
|
)}>
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isDir && isOpen && item.children && item.children.length > 0 && (
|
||||||
|
<div className="relative">
|
||||||
|
<span
|
||||||
|
className="absolute top-0 bottom-0 border-l border-border/40"
|
||||||
|
style={{ left: `${level * 16 + 14}px` }}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{renderFileTree(item.children, level + 1)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Detailed View ────────────────────────────────────────────────
|
||||||
const renderDetailedView = (items, level = 0) => {
|
const renderDetailedView = (items, level = 0) => {
|
||||||
return items.map((item) => (
|
return items.map((item) => {
|
||||||
<div key={item.path} className="select-none">
|
const isDir = item.type === 'directory';
|
||||||
<div
|
const isOpen = isDir && expandedDirs.has(item.path);
|
||||||
className={cn(
|
|
||||||
"grid grid-cols-12 gap-2 p-2 hover:bg-accent cursor-pointer items-center",
|
return (
|
||||||
)}
|
<div key={item.path} className="select-none">
|
||||||
style={{ paddingLeft: `${level * 16 + 12}px` }}
|
<div
|
||||||
onClick={() => {
|
className={cn(
|
||||||
if (item.type === 'directory') {
|
'group grid grid-cols-12 gap-2 py-[3px] pr-2 hover:bg-accent/60 cursor-pointer items-center rounded-sm transition-colors duration-100',
|
||||||
toggleDirectory(item.path);
|
isDir && isOpen && 'border-l-2 border-primary/30',
|
||||||
} else if (isImageFile(item.name)) {
|
isDir && !isOpen && 'border-l-2 border-transparent',
|
||||||
setSelectedImage({
|
!isDir && 'border-l-2 border-transparent',
|
||||||
name: item.name,
|
|
||||||
path: item.path,
|
|
||||||
projectPath: selectedProject.path,
|
|
||||||
projectName: selectedProject.name
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setSelectedFile({
|
|
||||||
name: item.name,
|
|
||||||
path: item.path,
|
|
||||||
projectPath: selectedProject.path,
|
|
||||||
projectName: selectedProject.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="col-span-5 flex items-center gap-2 min-w-0">
|
|
||||||
{item.type === 'directory' ? (
|
|
||||||
expandedDirs.has(item.path) ? (
|
|
||||||
<FolderOpen className="w-4 h-4 text-blue-500 flex-shrink-0" />
|
|
||||||
) : (
|
|
||||||
<Folder className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
getFileIcon(item.name)
|
|
||||||
)}
|
)}
|
||||||
<span className="text-sm truncate text-foreground">
|
style={{ paddingLeft: `${level * 16 + 4}px` }}
|
||||||
{item.name}
|
onClick={() => handleItemClick(item)}
|
||||||
</span>
|
>
|
||||||
</div>
|
<div className="col-span-5 flex items-center gap-1.5 min-w-0">
|
||||||
<div className="col-span-2 text-sm text-muted-foreground">
|
{renderItemIcons(item)}
|
||||||
{item.type === 'file' ? formatFileSize(item.size) : '-'}
|
<span className={cn(
|
||||||
</div>
|
'text-[13px] leading-tight truncate',
|
||||||
<div className="col-span-3 text-sm text-muted-foreground">
|
isDir ? 'font-medium text-foreground' : 'text-foreground/90'
|
||||||
{formatRelativeTime(item.modified)}
|
)}>
|
||||||
</div>
|
{item.name}
|
||||||
<div className="col-span-2 text-sm text-muted-foreground font-mono">
|
</span>
|
||||||
{item.permissionsRwx || '-'}
|
</div>
|
||||||
|
<div className="col-span-2 text-xs text-muted-foreground tabular-nums">
|
||||||
|
{item.type === 'file' ? formatFileSize(item.size) : ''}
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3 text-xs text-muted-foreground">
|
||||||
|
{formatRelativeTime(item.modified)}
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 text-xs text-muted-foreground font-mono">
|
||||||
|
{item.permissionsRwx || ''}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isDir && isOpen && item.children && (
|
||||||
|
<div className="relative">
|
||||||
|
<span
|
||||||
|
className="absolute top-0 bottom-0 border-l border-border/40"
|
||||||
|
style={{ left: `${level * 16 + 14}px` }}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{renderDetailedView(item.children, level + 1)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
{item.type === 'directory' &&
|
});
|
||||||
expandedDirs.has(item.path) &&
|
|
||||||
item.children &&
|
|
||||||
renderDetailedView(item.children, level + 1)}
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render compact view with inline details
|
// ─── Compact View ──────────────────────────────────────────────────
|
||||||
const renderCompactView = (items, level = 0) => {
|
const renderCompactView = (items, level = 0) => {
|
||||||
return items.map((item) => (
|
return items.map((item) => {
|
||||||
<div key={item.path} className="select-none">
|
const isDir = item.type === 'directory';
|
||||||
<div
|
const isOpen = isDir && expandedDirs.has(item.path);
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-between p-2 hover:bg-accent cursor-pointer",
|
return (
|
||||||
|
<div key={item.path} className="select-none">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'group flex items-center justify-between py-[3px] pr-2 hover:bg-accent/60 cursor-pointer rounded-sm transition-colors duration-100',
|
||||||
|
isDir && isOpen && 'border-l-2 border-primary/30',
|
||||||
|
isDir && !isOpen && 'border-l-2 border-transparent',
|
||||||
|
!isDir && 'border-l-2 border-transparent',
|
||||||
|
)}
|
||||||
|
style={{ paddingLeft: `${level * 16 + 4}px` }}
|
||||||
|
onClick={() => handleItemClick(item)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5 min-w-0">
|
||||||
|
{renderItemIcons(item)}
|
||||||
|
<span className={cn(
|
||||||
|
'text-[13px] leading-tight truncate',
|
||||||
|
isDir ? 'font-medium text-foreground' : 'text-foreground/90'
|
||||||
|
)}>
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 text-xs text-muted-foreground flex-shrink-0 ml-2">
|
||||||
|
{item.type === 'file' && (
|
||||||
|
<>
|
||||||
|
<span className="tabular-nums">{formatFileSize(item.size)}</span>
|
||||||
|
<span className="font-mono">{item.permissionsRwx}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isDir && isOpen && item.children && (
|
||||||
|
<div className="relative">
|
||||||
|
<span
|
||||||
|
className="absolute top-0 bottom-0 border-l border-border/40"
|
||||||
|
style={{ left: `${level * 16 + 14}px` }}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{renderCompactView(item.children, level + 1)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
style={{ paddingLeft: `${level * 16 + 12}px` }}
|
|
||||||
onClick={() => {
|
|
||||||
if (item.type === 'directory') {
|
|
||||||
toggleDirectory(item.path);
|
|
||||||
} else if (isImageFile(item.name)) {
|
|
||||||
setSelectedImage({
|
|
||||||
name: item.name,
|
|
||||||
path: item.path,
|
|
||||||
projectPath: selectedProject.path,
|
|
||||||
projectName: selectedProject.name
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setSelectedFile({
|
|
||||||
name: item.name,
|
|
||||||
path: item.path,
|
|
||||||
projectPath: selectedProject.path,
|
|
||||||
projectName: selectedProject.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
|
||||||
{item.type === 'directory' ? (
|
|
||||||
expandedDirs.has(item.path) ? (
|
|
||||||
<FolderOpen className="w-4 h-4 text-blue-500 flex-shrink-0" />
|
|
||||||
) : (
|
|
||||||
<Folder className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
getFileIcon(item.name)
|
|
||||||
)}
|
|
||||||
<span className="text-sm truncate text-foreground">
|
|
||||||
{item.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
|
||||||
{item.type === 'file' && (
|
|
||||||
<>
|
|
||||||
<span>{formatFileSize(item.size)}</span>
|
|
||||||
<span className="font-mono">{item.permissionsRwx}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
{item.type === 'directory' &&
|
});
|
||||||
expandedDirs.has(item.path) &&
|
|
||||||
item.children &&
|
|
||||||
renderCompactView(item.children, level + 1)}
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── Loading state ─────────────────────────────────────────────────
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex items-center justify-center">
|
<div className="h-full flex items-center justify-center">
|
||||||
<div className="text-gray-500 dark:text-gray-400">
|
<div className="text-muted-foreground text-sm">
|
||||||
{t('fileTree.loading')}
|
{t('fileTree.loading')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Main render ───────────────────────────────────────────────────
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col bg-card">
|
<div className="h-full flex flex-col bg-card">
|
||||||
{/* Header with Search and View Mode Toggle */}
|
{/* Header */}
|
||||||
<div className="p-4 border-b border-border space-y-3">
|
<div className="px-3 pt-3 pb-2 border-b border-border space-y-2">
|
||||||
<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-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||||
<div className="flex gap-1">
|
{t('fileTree.files')}
|
||||||
|
</h3>
|
||||||
|
<div className="flex gap-0.5">
|
||||||
<Button
|
<Button
|
||||||
variant={viewMode === 'simple' ? 'default' : 'ghost'}
|
variant={viewMode === 'simple' ? 'default' : 'ghost'}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 w-8 p-0"
|
className="h-7 w-7 p-0"
|
||||||
onClick={() => changeViewMode('simple')}
|
onClick={() => changeViewMode('simple')}
|
||||||
title={t('fileTree.simpleView')}
|
title={t('fileTree.simpleView')}
|
||||||
>
|
>
|
||||||
<List className="w-4 h-4" />
|
<List className="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={viewMode === 'compact' ? 'default' : 'ghost'}
|
variant={viewMode === 'compact' ? 'default' : 'ghost'}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 w-8 p-0"
|
className="h-7 w-7 p-0"
|
||||||
onClick={() => changeViewMode('compact')}
|
onClick={() => changeViewMode('compact')}
|
||||||
title={t('fileTree.compactView')}
|
title={t('fileTree.compactView')}
|
||||||
>
|
>
|
||||||
<Eye className="w-4 h-4" />
|
<Eye className="w-3.5 h-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={viewMode === 'detailed' ? 'default' : 'ghost'}
|
variant={viewMode === 'detailed' ? 'default' : 'ghost'}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 w-8 p-0"
|
className="h-7 w-7 p-0"
|
||||||
onClick={() => changeViewMode('detailed')}
|
onClick={() => changeViewMode('detailed')}
|
||||||
title={t('fileTree.detailedView')}
|
title={t('fileTree.detailedView')}
|
||||||
>
|
>
|
||||||
<TableProperties className="w-4 h-4" />
|
<TableProperties className="w-3.5 h-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 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('fileTree.searchPlaceholder')}
|
placeholder={t('fileTree.searchPlaceholder')}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="pl-8 pr-8 h-8 text-sm"
|
className="pl-7 pr-7 h-7 text-xs"
|
||||||
/>
|
/>
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-6 w-6 p-0 hover:bg-accent"
|
className="absolute right-0.5 top-1/2 transform -translate-y-1/2 h-5 w-5 p-0 hover:bg-accent"
|
||||||
onClick={() => setSearchQuery('')}
|
onClick={() => setSearchQuery('')}
|
||||||
title={t('fileTree.clearSearch')}
|
title={t('fileTree.clearSearch')}
|
||||||
>
|
>
|
||||||
@@ -419,8 +682,8 @@ function FileTree({ selectedProject }) {
|
|||||||
|
|
||||||
{/* Column Headers for Detailed View */}
|
{/* Column Headers for Detailed View */}
|
||||||
{viewMode === 'detailed' && filteredFiles.length > 0 && (
|
{viewMode === 'detailed' && filteredFiles.length > 0 && (
|
||||||
<div className="px-4 pt-2 pb-1 border-b border-border">
|
<div className="px-3 pt-1.5 pb-1 border-b border-border">
|
||||||
<div className="grid grid-cols-12 gap-2 px-2 text-xs font-medium text-muted-foreground">
|
<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>
|
||||||
<div className="col-span-3">{t('fileTree.modified')}</div>
|
<div className="col-span-3">{t('fileTree.modified')}</div>
|
||||||
@@ -429,7 +692,7 @@ function FileTree({ selectedProject }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ScrollArea className="flex-1 p-4">
|
<ScrollArea className="flex-1 px-2 py-1">
|
||||||
{files.length === 0 ? (
|
{files.length === 0 ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-3">
|
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
@@ -451,14 +714,14 @@ function FileTree({ selectedProject }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={viewMode === 'detailed' ? '' : 'space-y-1'}>
|
<div>
|
||||||
{viewMode === 'simple' && renderFileTree(filteredFiles)}
|
{viewMode === 'simple' && renderFileTree(filteredFiles)}
|
||||||
{viewMode === 'compact' && renderCompactView(filteredFiles)}
|
{viewMode === 'compact' && renderCompactView(filteredFiles)}
|
||||||
{viewMode === 'detailed' && renderDetailedView(filteredFiles)}
|
{viewMode === 'detailed' && renderDetailedView(filteredFiles)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
{/* Code Editor Modal */}
|
{/* Code Editor Modal */}
|
||||||
{selectedFile && (
|
{selectedFile && (
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
@@ -467,7 +730,7 @@ function FileTree({ selectedProject }) {
|
|||||||
projectPath={selectedFile.projectPath}
|
projectPath={selectedFile.projectPath}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Image Viewer Modal */}
|
{/* Image Viewer Modal */}
|
||||||
{selectedImage && (
|
{selectedImage && (
|
||||||
<ImageViewer
|
<ImageViewer
|
||||||
@@ -479,4 +742,4 @@ function FileTree({ selectedProject }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FileTree;
|
export default FileTree;
|
||||||
|
|||||||
@@ -640,36 +640,36 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
const renderCommitItem = (commit) => {
|
const renderCommitItem = (commit) => {
|
||||||
const isExpanded = expandedCommits.has(commit.hash);
|
const isExpanded = expandedCommits.has(commit.hash);
|
||||||
const diff = commitDiffs[commit.hash];
|
const diff = commitDiffs[commit.hash];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={commit.hash} className="border-b border-gray-200 dark:border-gray-700 last:border-0">
|
<div key={commit.hash} className="border-b border-border last:border-0">
|
||||||
<div
|
<div
|
||||||
className="flex items-start p-3 hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
|
className="flex items-start p-3 hover:bg-accent/50 cursor-pointer transition-colors"
|
||||||
onClick={() => toggleCommitExpanded(commit.hash)}
|
onClick={() => toggleCommitExpanded(commit.hash)}
|
||||||
>
|
>
|
||||||
<div className="mr-2 mt-1 p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 rounded">
|
<div className="mr-2 mt-1 p-0.5 hover:bg-accent rounded">
|
||||||
{isExpanded ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
{isExpanded ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<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="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
<p className="text-sm font-medium text-foreground truncate">
|
||||||
{commit.message}
|
{commit.message}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
{commit.author} • {commit.date}
|
{commit.author} • {commit.date}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-mono text-gray-400 dark:text-gray-500 flex-shrink-0">
|
<span className="text-xs font-mono text-muted-foreground/60 flex-shrink-0">
|
||||||
{commit.hash.substring(0, 7)}
|
{commit.hash.substring(0, 7)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isExpanded && diff && (
|
{isExpanded && diff && (
|
||||||
<div className="bg-gray-50 dark:bg-gray-900">
|
<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-xs font-mono text-gray-600 dark:text-gray-400 mb-2">
|
<div className="text-xs font-mono text-muted-foreground mb-2">
|
||||||
{commit.stats}
|
{commit.stats}
|
||||||
</div>
|
</div>
|
||||||
<DiffViewer diff={diff} fileName="commit" isMobile={isMobile} wrapText={wrapText} />
|
<DiffViewer diff={diff} fileName="commit" isMobile={isMobile} wrapText={wrapText} />
|
||||||
@@ -684,22 +684,20 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
const isExpanded = expandedFiles.has(filePath);
|
const isExpanded = expandedFiles.has(filePath);
|
||||||
const isSelected = selectedFiles.has(filePath);
|
const isSelected = selectedFiles.has(filePath);
|
||||||
const diff = gitDiff[filePath];
|
const diff = gitDiff[filePath];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={filePath} className="border-b border-gray-200 dark:border-gray-700 last:border-0">
|
<div key={filePath} className="border-b border-border last:border-0">
|
||||||
<div className={`flex items-center hover:bg-gray-50 dark:hover:bg-gray-800 ${isMobile ? 'px-2 py-1.5' : 'px-3 py-2'}`}>
|
<div className={`flex items-center hover:bg-accent/50 transition-colors ${isMobile ? 'px-2 py-1.5' : 'px-3 py-2'}`}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={() => toggleFileSelected(filePath)}
|
onChange={() => toggleFileSelected(filePath)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={`rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600 ${isMobile ? 'mr-1.5' : 'mr-2'}`}
|
className={`rounded border-border text-primary focus:ring-primary/40 bg-background checked:bg-primary ${isMobile ? 'mr-1.5' : 'mr-2'}`}
|
||||||
/>
|
/>
|
||||||
<div
|
<div className="flex items-center flex-1">
|
||||||
className="flex items-center flex-1"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={`p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 rounded cursor-pointer ${isMobile ? 'mr-1' : 'mr-2'}`}
|
className={`p-0.5 hover:bg-accent rounded cursor-pointer ${isMobile ? 'mr-1' : 'mr-2'}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleFileExpanded(filePath);
|
toggleFileExpanded(filePath);
|
||||||
@@ -708,7 +706,7 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
<ChevronRight className={`w-3 h-3 transition-transform duration-200 ease-in-out ${isExpanded ? 'rotate-90' : 'rotate-0'}`} />
|
<ChevronRight className={`w-3 h-3 transition-transform duration-200 ease-in-out ${isExpanded ? 'rotate-90' : 'rotate-0'}`} />
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`flex-1 truncate ${isMobile ? 'text-xs' : 'text-sm'} cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 hover:underline`}
|
className={`flex-1 truncate ${isMobile ? 'text-xs' : 'text-sm'} cursor-pointer hover:text-primary hover:underline`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleFileOpen(filePath);
|
handleFileOpen(filePath);
|
||||||
@@ -722,16 +720,16 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setConfirmAction({
|
setConfirmAction({
|
||||||
type: 'discard',
|
type: 'discard',
|
||||||
file: filePath,
|
file: filePath,
|
||||||
message: `Discard all changes to "${filePath}"? This action cannot be undone.`
|
message: `Discard all changes to "${filePath}"? This action cannot be undone.`
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className={`${isMobile ? 'px-2 py-1 text-xs' : 'p-1'} hover:bg-red-100 dark:hover:bg-red-900 rounded text-red-600 dark:text-red-400 font-medium flex items-center gap-1`}
|
className={`${isMobile ? 'px-2 py-1 text-xs' : 'p-1'} hover:bg-destructive/10 rounded text-destructive font-medium flex items-center gap-1`}
|
||||||
title="Discard changes"
|
title="Discard changes"
|
||||||
>
|
>
|
||||||
<Trash2 className={`${isMobile ? 'w-3 h-3' : 'w-3 h-3'}`} />
|
<Trash2 className="w-3 h-3" />
|
||||||
{isMobile && <span>Discard</span>}
|
{isMobile && <span>Discard</span>}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -739,25 +737,25 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setConfirmAction({
|
setConfirmAction({
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
file: filePath,
|
file: filePath,
|
||||||
message: `Delete untracked file "${filePath}"? This action cannot be undone.`
|
message: `Delete untracked file "${filePath}"? This action cannot be undone.`
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className={`${isMobile ? 'px-2 py-1 text-xs' : 'p-1'} hover:bg-red-100 dark:hover:bg-red-900 rounded text-red-600 dark:text-red-400 font-medium flex items-center gap-1`}
|
className={`${isMobile ? 'px-2 py-1 text-xs' : 'p-1'} hover:bg-destructive/10 rounded text-destructive font-medium flex items-center gap-1`}
|
||||||
title="Delete untracked file"
|
title="Delete untracked file"
|
||||||
>
|
>
|
||||||
<Trash2 className={`${isMobile ? 'w-3 h-3' : 'w-3 h-3'}`} />
|
<Trash2 className="w-3 h-3" />
|
||||||
{isMobile && <span>Delete</span>}
|
{isMobile && <span>Delete</span>}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold border ${
|
className={`inline-flex items-center justify-center w-5 h-5 rounded text-[10px] font-bold border ${
|
||||||
status === 'M' ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800' :
|
status === 'M' ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800/50' :
|
||||||
status === 'A' ? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300 border-green-200 dark:border-green-800' :
|
status === 'A' ? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300 border-green-200 dark:border-green-800/50' :
|
||||||
status === 'D' ? 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300 border-red-200 dark:border-red-800' :
|
status === 'D' ? 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300 border-red-200 dark:border-red-800/50' :
|
||||||
'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 border-gray-300 dark:border-gray-600'
|
'bg-muted text-muted-foreground border-border'
|
||||||
}`}
|
}`}
|
||||||
title={getStatusLabel(status)}
|
title={getStatusLabel(status)}
|
||||||
>
|
>
|
||||||
@@ -766,25 +764,25 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`bg-gray-50 dark:bg-gray-900 transition-all duration-400 ease-in-out overflow-hidden ${
|
<div className={`bg-muted/50 transition-all duration-400 ease-in-out overflow-hidden ${
|
||||||
isExpanded && diff
|
isExpanded && diff
|
||||||
? 'max-h-[600px] opacity-100 translate-y-0'
|
? 'max-h-[600px] opacity-100 translate-y-0'
|
||||||
: 'max-h-0 opacity-0 -translate-y-1'
|
: 'max-h-0 opacity-0 -translate-y-1'
|
||||||
}`}>
|
}`}>
|
||||||
{/* Operation header */}
|
{/* Operation header */}
|
||||||
<div className="flex items-center justify-between p-2 border-b border-gray-200 dark:border-gray-700">
|
<div className="flex items-center justify-between p-2 border-b border-border">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold border ${
|
className={`inline-flex items-center justify-center w-5 h-5 rounded text-[10px] font-bold border ${
|
||||||
status === 'M' ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800' :
|
status === 'M' ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800/50' :
|
||||||
status === 'A' ? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300 border-green-200 dark:border-green-800' :
|
status === 'A' ? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300 border-green-200 dark:border-green-800/50' :
|
||||||
status === 'D' ? 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300 border-red-200 dark:border-red-800' :
|
status === 'D' ? 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300 border-red-200 dark:border-red-800/50' :
|
||||||
'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 border-gray-300 dark:border-gray-600'
|
'bg-muted text-muted-foreground border-border'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{status}
|
{status}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
<span className="text-sm font-medium text-foreground">
|
||||||
{getStatusLabel(status)}
|
{getStatusLabel(status)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -794,7 +792,7 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setWrapText(!wrapText);
|
setWrapText(!wrapText);
|
||||||
}}
|
}}
|
||||||
className="text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||||
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'}
|
||||||
@@ -811,7 +809,7 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
|
|
||||||
if (!selectedProject) {
|
if (!selectedProject) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex items-center justify-center text-gray-500 dark:text-gray-400">
|
<div className="h-full flex 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>
|
||||||
);
|
);
|
||||||
@@ -820,13 +818,13 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col bg-background">
|
<div className="h-full flex flex-col bg-background">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className={`flex items-center justify-between border-b border-gray-200 dark:border-gray-700 ${isMobile ? 'px-3 py-2' : 'px-4 py-3'}`}>
|
<div className={`flex items-center justify-between border-b border-border/60 ${isMobile ? 'px-3 py-2' : 'px-4 py-3'}`}>
|
||||||
<div className="relative" ref={dropdownRef}>
|
<div className="relative" ref={dropdownRef}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowBranchDropdown(!showBranchDropdown)}
|
onClick={() => setShowBranchDropdown(!showBranchDropdown)}
|
||||||
className={`flex items-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors ${isMobile ? 'space-x-1 px-2 py-1' : 'space-x-2 px-3 py-1.5'}`}
|
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'}`}
|
||||||
>
|
>
|
||||||
<GitBranch className={`text-gray-600 dark:text-gray-400 ${isMobile ? 'w-3 h-3' : 'w-4 h-4'}`} />
|
<GitBranch className={`text-muted-foreground ${isMobile ? 'w-3 h-3' : 'w-4 h-4'}`} />
|
||||||
<div className="flex items-center gap-1">
|
<div 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>
|
||||||
{/* Remote status indicators */}
|
{/* Remote status indicators */}
|
||||||
@@ -838,47 +836,47 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{remoteStatus.behind > 0 && (
|
{remoteStatus.behind > 0 && (
|
||||||
<span className="text-blue-600 dark:text-blue-400" title={`${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} behind`}>
|
<span className="text-primary" title={`${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} behind`}>
|
||||||
↓{remoteStatus.behind}
|
↓{remoteStatus.behind}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{remoteStatus.isUpToDate && (
|
{remoteStatus.isUpToDate && (
|
||||||
<span className="text-gray-500 dark:text-gray-400" title="Up to date with remote">
|
<span className="text-muted-foreground" title="Up to date with remote">
|
||||||
✓
|
✓
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ChevronDown className={`w-3 h-3 text-gray-500 transition-transform ${showBranchDropdown ? 'rotate-180' : ''}`} />
|
<ChevronDown className={`w-3 h-3 text-muted-foreground transition-transform ${showBranchDropdown ? 'rotate-180' : ''}`} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Branch Dropdown */}
|
{/* Branch Dropdown */}
|
||||||
{showBranchDropdown && (
|
{showBranchDropdown && (
|
||||||
<div className="absolute top-full left-0 mt-1 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-50">
|
<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="py-1 max-h-64 overflow-y-auto">
|
<div className="py-1 max-h-64 overflow-y-auto">
|
||||||
{branches.map(branch => (
|
{branches.map(branch => (
|
||||||
<button
|
<button
|
||||||
key={branch}
|
key={branch}
|
||||||
onClick={() => switchBranch(branch)}
|
onClick={() => switchBranch(branch)}
|
||||||
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${
|
className={`w-full text-left px-4 py-2 text-sm hover:bg-accent transition-colors ${
|
||||||
branch === currentBranch ? 'bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100' : 'text-gray-700 dark:text-gray-300'
|
branch === currentBranch ? 'bg-accent/50 text-foreground' : 'text-muted-foreground'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{branch === currentBranch && <Check className="w-3 h-3 text-green-600 dark:text-green-400" />}
|
{branch === currentBranch && <Check className="w-3 h-3 text-primary" />}
|
||||||
<span className={branch === currentBranch ? 'font-medium' : ''}>{branch}</span>
|
<span className={branch === currentBranch ? 'font-medium' : ''}>{branch}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 py-1">
|
<div className="border-t border-border py-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowNewBranchModal(true);
|
setShowNewBranchModal(true);
|
||||||
setShowBranchDropdown(false);
|
setShowBranchDropdown(false);
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center space-x-2"
|
className="w-full text-left px-4 py-2 text-sm hover:bg-accent transition-colors flex items-center space-x-2"
|
||||||
>
|
>
|
||||||
<Plus className="w-3 h-3" />
|
<Plus className="w-3 h-3" />
|
||||||
<span>Create new branch</span>
|
<span>Create new branch</span>
|
||||||
@@ -895,12 +893,12 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
{/* Publish button - show when branch doesn't exist on remote */}
|
{/* Publish button - show when branch doesn't exist on remote */}
|
||||||
{!remoteStatus?.hasUpstream && (
|
{!remoteStatus?.hasUpstream && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmAction({
|
onClick={() => setConfirmAction({
|
||||||
type: 'publish',
|
type: 'publish',
|
||||||
message: `Publish branch "${currentBranch}" to ${remoteStatus.remoteName}?`
|
message: `Publish branch "${currentBranch}" to ${remoteStatus.remoteName}?`
|
||||||
})}
|
})}
|
||||||
disabled={isPublishing}
|
disabled={isPublishing}
|
||||||
className="px-2 py-1 text-xs bg-purple-600 text-white rounded hover:bg-purple-700 disabled:opacity-50 flex items-center gap-1"
|
className="px-2.5 py-1 text-xs bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 flex items-center gap-1 transition-colors"
|
||||||
title={`Publish branch "${currentBranch}" to ${remoteStatus.remoteName}`}
|
title={`Publish branch "${currentBranch}" to ${remoteStatus.remoteName}`}
|
||||||
>
|
>
|
||||||
<Upload className={`w-3 h-3 ${isPublishing ? 'animate-pulse' : ''}`} />
|
<Upload className={`w-3 h-3 ${isPublishing ? 'animate-pulse' : ''}`} />
|
||||||
@@ -914,41 +912,41 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
{/* Pull button - show when behind (primary action) */}
|
{/* Pull button - show when behind (primary action) */}
|
||||||
{remoteStatus.behind > 0 && (
|
{remoteStatus.behind > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmAction({
|
onClick={() => setConfirmAction({
|
||||||
type: 'pull',
|
type: 'pull',
|
||||||
message: `Pull ${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} from ${remoteStatus.remoteName}?`
|
message: `Pull ${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} from ${remoteStatus.remoteName}?`
|
||||||
})}
|
})}
|
||||||
disabled={isPulling}
|
disabled={isPulling}
|
||||||
className="px-2 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 flex items-center gap-1"
|
className="px-2.5 py-1 text-xs bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 flex items-center gap-1 transition-colors"
|
||||||
title={`Pull ${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} from ${remoteStatus.remoteName}`}
|
title={`Pull ${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} from ${remoteStatus.remoteName}`}
|
||||||
>
|
>
|
||||||
<Download className={`w-3 h-3 ${isPulling ? 'animate-pulse' : ''}`} />
|
<Download className={`w-3 h-3 ${isPulling ? 'animate-pulse' : ''}`} />
|
||||||
<span>{isPulling ? 'Pulling...' : `Pull ${remoteStatus.behind}`}</span>
|
<span>{isPulling ? 'Pulling...' : `Pull ${remoteStatus.behind}`}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Push button - show when ahead (primary action when ahead only) */}
|
{/* Push button - show when ahead (primary action when ahead only) */}
|
||||||
{remoteStatus.ahead > 0 && (
|
{remoteStatus.ahead > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmAction({
|
onClick={() => setConfirmAction({
|
||||||
type: 'push',
|
type: 'push',
|
||||||
message: `Push ${remoteStatus.ahead} commit${remoteStatus.ahead !== 1 ? 's' : ''} to ${remoteStatus.remoteName}?`
|
message: `Push ${remoteStatus.ahead} commit${remoteStatus.ahead !== 1 ? 's' : ''} to ${remoteStatus.remoteName}?`
|
||||||
})}
|
})}
|
||||||
disabled={isPushing}
|
disabled={isPushing}
|
||||||
className="px-2 py-1 text-xs bg-orange-600 text-white rounded hover:bg-orange-700 disabled:opacity-50 flex items-center gap-1"
|
className="px-2.5 py-1 text-xs bg-orange-600 text-white rounded-lg hover:bg-orange-700 disabled:opacity-50 flex items-center gap-1 transition-colors"
|
||||||
title={`Push ${remoteStatus.ahead} commit${remoteStatus.ahead !== 1 ? 's' : ''} to ${remoteStatus.remoteName}`}
|
title={`Push ${remoteStatus.ahead} commit${remoteStatus.ahead !== 1 ? 's' : ''} to ${remoteStatus.remoteName}`}
|
||||||
>
|
>
|
||||||
<Upload className={`w-3 h-3 ${isPushing ? 'animate-pulse' : ''}`} />
|
<Upload className={`w-3 h-3 ${isPushing ? 'animate-pulse' : ''}`} />
|
||||||
<span>{isPushing ? 'Pushing...' : `Push ${remoteStatus.ahead}`}</span>
|
<span>{isPushing ? 'Pushing...' : `Push ${remoteStatus.ahead}`}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Fetch button - show when ahead only or when diverged (secondary action) */}
|
{/* Fetch button - show when ahead only or when diverged (secondary action) */}
|
||||||
{(remoteStatus.ahead > 0 || (remoteStatus.behind > 0 && remoteStatus.ahead > 0)) && (
|
{(remoteStatus.ahead > 0 || (remoteStatus.behind > 0 && remoteStatus.ahead > 0)) && (
|
||||||
<button
|
<button
|
||||||
onClick={handleFetch}
|
onClick={handleFetch}
|
||||||
disabled={isFetching}
|
disabled={isFetching}
|
||||||
className="px-2 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 flex items-center gap-1"
|
className="px-2.5 py-1 text-xs bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 flex items-center gap-1 transition-colors"
|
||||||
title={`Fetch from ${remoteStatus.remoteName}`}
|
title={`Fetch from ${remoteStatus.remoteName}`}
|
||||||
>
|
>
|
||||||
<RefreshCw className={`w-3 h-3 ${isFetching ? 'animate-spin' : ''}`} />
|
<RefreshCw className={`w-3 h-3 ${isFetching ? 'animate-spin' : ''}`} />
|
||||||
@@ -967,42 +965,43 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
fetchRemoteStatus();
|
fetchRemoteStatus();
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className={`hover:bg-gray-100 dark:hover:bg-gray-800 rounded ${isMobile ? 'p-1' : 'p-1.5'}`}
|
className={`hover:bg-accent rounded-lg transition-colors ${isMobile ? 'p-1' : 'p-1.5'}`}
|
||||||
>
|
>
|
||||||
<RefreshCw className={`${isLoading ? 'animate-spin' : ''} ${isMobile ? 'w-3 h-3' : 'w-4 h-4'}`} />
|
<RefreshCw className={`text-muted-foreground ${isLoading ? 'animate-spin' : ''} ${isMobile ? 'w-3 h-3' : 'w-4 h-4'}`} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Git Repository Not Found Message */}
|
{/* Git Repository Not Found Message */}
|
||||||
{gitStatus?.error ? (
|
{gitStatus?.error ? (
|
||||||
<div className="flex-1 flex flex-col items-center justify-center text-gray-500 dark:text-gray-400 px-6 py-12">
|
<div className="flex-1 flex flex-col items-center justify-center text-muted-foreground px-6 py-12">
|
||||||
<GitBranch className="w-20 h-20 mb-6 opacity-30" />
|
<div className="w-16 h-16 rounded-2xl bg-muted/50 flex items-center justify-center mb-6">
|
||||||
<h3 className="text-xl font-medium mb-3 text-center">{gitStatus.error}</h3>
|
<GitBranch className="w-8 h-8 opacity-40" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium mb-3 text-center text-foreground">{gitStatus.error}</h3>
|
||||||
{gitStatus.details && (
|
{gitStatus.details && (
|
||||||
<p className="text-sm text-center leading-relaxed mb-6 max-w-md">{gitStatus.details}</p>
|
<p className="text-sm text-center leading-relaxed mb-6 max-w-md">{gitStatus.details}</p>
|
||||||
)}
|
)}
|
||||||
{/* // ! This can be a custom component that can be reused for " Tip: Create a new project..." as well */}
|
<div className="p-4 bg-primary/5 rounded-xl border border-primary/10 max-w-md">
|
||||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 max-w-md">
|
<p className="text-sm text-primary text-center">
|
||||||
<p className="text-sm text-blue-700 dark:text-blue-300 text-center">
|
<strong>Tip:</strong> Run <code className="bg-primary/10 px-2 py-1 rounded-md font-mono text-xs">git init</code> in your project directory to initialize git source control.
|
||||||
<strong>Tip:</strong> Run <code className="bg-blue-100 dark:bg-blue-900 px-2 py-1 rounded font-mono text-xs">git init</code> in your project directory to initialize git source control.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Tab Navigation - Only show when git is available and no files expanded */}
|
{/* Tab Navigation - Only show when git is available and no files expanded */}
|
||||||
<div className={`flex border-b border-gray-200 dark:border-gray-700 transition-all duration-300 ease-in-out ${
|
<div className={`flex border-b border-border/60 transition-all duration-300 ease-in-out ${
|
||||||
expandedFiles.size === 0
|
expandedFiles.size === 0
|
||||||
? 'max-h-16 opacity-100 translate-y-0'
|
? 'max-h-16 opacity-100 translate-y-0'
|
||||||
: 'max-h-0 opacity-0 -translate-y-2 overflow-hidden'
|
: 'max-h-0 opacity-0 -translate-y-2 overflow-hidden'
|
||||||
}`}>
|
}`}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveView('changes')}
|
onClick={() => setActiveView('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-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400'
|
? 'text-primary border-b-2 border-primary'
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
@@ -1014,8 +1013,8 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
onClick={() => setActiveView('history')}
|
onClick={() => setActiveView('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-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400'
|
? 'text-primary border-b-2 border-primary'
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
@@ -1035,10 +1034,10 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
: 'max-h-0 opacity-0 -translate-y-2 overflow-hidden'
|
: 'max-h-0 opacity-0 -translate-y-2 overflow-hidden'
|
||||||
}`}>
|
}`}>
|
||||||
{isMobile && isCommitAreaCollapsed ? (
|
{isMobile && isCommitAreaCollapsed ? (
|
||||||
<div className="px-4 py-2 border-b border-gray-200 dark:border-gray-700">
|
<div className="px-4 py-2 border-b border-border/60">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCommitAreaCollapsed(false)}
|
onClick={() => setIsCommitAreaCollapsed(false)}
|
||||||
className="w-full flex items-center justify-center gap-2 px-3 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
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"
|
||||||
>
|
>
|
||||||
<GitCommit className="w-4 h-4" />
|
<GitCommit className="w-4 h-4" />
|
||||||
<span>Commit {selectedFiles.size} file{selectedFiles.size !== 1 ? 's' : ''}</span>
|
<span>Commit {selectedFiles.size} file{selectedFiles.size !== 1 ? 's' : ''}</span>
|
||||||
@@ -1048,27 +1047,27 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Commit Message Input */}
|
{/* Commit Message Input */}
|
||||||
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
<div className="px-4 py-3 border-b border-border/60">
|
||||||
{/* Mobile collapse button */}
|
{/* Mobile collapse button */}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-sm font-medium">Commit Changes</span>
|
<span className="text-sm font-medium text-foreground">Commit Changes</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCommitAreaCollapsed(true)}
|
onClick={() => setIsCommitAreaCollapsed(true)}
|
||||||
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
|
className="p-1 hover:bg-accent rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<ChevronDown className="w-4 h-4 rotate-180" />
|
<ChevronDown className="w-4 h-4 rotate-180" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={commitMessage}
|
value={commitMessage}
|
||||||
onChange={(e) => setCommitMessage(e.target.value)}
|
onChange={(e) => setCommitMessage(e.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-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 resize-none pr-20"
|
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"
|
||||||
rows="3"
|
rows="3"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||||
@@ -1080,7 +1079,7 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
<button
|
<button
|
||||||
onClick={generateCommitMessage}
|
onClick={generateCommitMessage}
|
||||||
disabled={selectedFiles.size === 0 || isGeneratingMessage}
|
disabled={selectedFiles.size === 0 || isGeneratingMessage}
|
||||||
className="p-1.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="p-1.5 text-muted-foreground hover:text-foreground disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
title="Generate commit message"
|
title="Generate commit message"
|
||||||
>
|
>
|
||||||
{isGeneratingMessage ? (
|
{isGeneratingMessage ? (
|
||||||
@@ -1099,16 +1098,16 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between mt-2">
|
<div className="flex items-center justify-between mt-2">
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-muted-foreground">
|
||||||
{selectedFiles.size} file{selectedFiles.size !== 1 ? 's' : ''} selected
|
{selectedFiles.size} file{selectedFiles.size !== 1 ? 's' : ''} selected
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmAction({
|
onClick={() => setConfirmAction({
|
||||||
type: 'commit',
|
type: 'commit',
|
||||||
message: `Commit ${selectedFiles.size} file${selectedFiles.size !== 1 ? 's' : ''} with message: "${commitMessage.trim()}"?`
|
message: `Commit ${selectedFiles.size} file${selectedFiles.size !== 1 ? 's' : ''} with message: "${commitMessage.trim()}"?`
|
||||||
})}
|
})}
|
||||||
disabled={!commitMessage.trim() || selectedFiles.size === 0 || isCommitting}
|
disabled={!commitMessage.trim() || selectedFiles.size === 0 || isCommitting}
|
||||||
className="px-3 py-1 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
|
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"
|
||||||
>
|
>
|
||||||
<Check className="w-3 h-3" />
|
<Check className="w-3 h-3" />
|
||||||
<span>{isCommitting ? 'Committing...' : 'Commit'}</span>
|
<span>{isCommitting ? 'Committing...' : 'Commit'}</span>
|
||||||
@@ -1123,12 +1122,12 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
|
|
||||||
{/* File Selection Controls - Only show in changes view and when git is working and no files expanded */}
|
{/* File Selection Controls - Only show in changes view and when git is working and no files expanded */}
|
||||||
{activeView === 'changes' && gitStatus && !gitStatus.error && (
|
{activeView === 'changes' && gitStatus && !gitStatus.error && (
|
||||||
<div className={`border-b border-gray-200 dark:border-gray-700 flex items-center justify-between transition-all duration-300 ease-in-out ${isMobile ? 'px-3 py-1.5' : 'px-4 py-2'} ${
|
<div className={`border-b border-border/60 flex items-center justify-between transition-all duration-300 ease-in-out ${isMobile ? 'px-3 py-1.5' : 'px-4 py-2'} ${
|
||||||
expandedFiles.size === 0
|
expandedFiles.size === 0
|
||||||
? 'max-h-16 opacity-100 translate-y-0'
|
? 'max-h-16 opacity-100 translate-y-0'
|
||||||
: 'max-h-0 opacity-0 -translate-y-2 overflow-hidden'
|
: 'max-h-0 opacity-0 -translate-y-2 overflow-hidden'
|
||||||
}`}>
|
}`}>
|
||||||
<span className={`text-gray-600 dark:text-gray-400 ${isMobile ? 'text-xs' : 'text-xs'}`}>
|
<span className="text-xs text-muted-foreground">
|
||||||
{selectedFiles.size} of {(gitStatus?.modified?.length || 0) + (gitStatus?.added?.length || 0) + (gitStatus?.deleted?.length || 0) + (gitStatus?.untracked?.length || 0)} {isMobile ? '' : 'files'} selected
|
{selectedFiles.size} of {(gitStatus?.modified?.length || 0) + (gitStatus?.added?.length || 0) + (gitStatus?.deleted?.length || 0) + (gitStatus?.untracked?.length || 0)} {isMobile ? '' : 'files'} selected
|
||||||
</span>
|
</span>
|
||||||
<div className={`flex ${isMobile ? 'gap-1' : 'gap-2'}`}>
|
<div className={`flex ${isMobile ? 'gap-1' : 'gap-2'}`}>
|
||||||
@@ -1142,14 +1141,14 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
]);
|
]);
|
||||||
setSelectedFiles(allFiles);
|
setSelectedFiles(allFiles);
|
||||||
}}
|
}}
|
||||||
className={`text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 ${isMobile ? 'text-xs' : 'text-xs'}`}
|
className="text-xs text-primary hover:text-primary/80 transition-colors"
|
||||||
>
|
>
|
||||||
{isMobile ? 'All' : 'Select All'}
|
{isMobile ? 'All' : 'Select All'}
|
||||||
</button>
|
</button>
|
||||||
<span className="text-gray-300 dark:text-gray-600">|</span>
|
<span className="text-border">|</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedFiles(new Set())}
|
onClick={() => setSelectedFiles(new Set())}
|
||||||
className={`text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 ${isMobile ? 'text-xs' : 'text-xs'}`}
|
className="text-xs text-primary hover:text-primary/80 transition-colors"
|
||||||
>
|
>
|
||||||
{isMobile ? 'None' : 'Deselect All'}
|
{isMobile ? 'None' : 'Deselect All'}
|
||||||
</button>
|
</button>
|
||||||
@@ -1159,42 +1158,42 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
|
|
||||||
{/* Status Legend Toggle - Hide on mobile by default */}
|
{/* Status Legend Toggle - Hide on mobile by default */}
|
||||||
{!gitStatus?.error && !isMobile && (
|
{!gitStatus?.error && !isMobile && (
|
||||||
<div className="border-b border-gray-200 dark:border-gray-700">
|
<div className="border-b border-border/60">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowLegend(!showLegend)}
|
onClick={() => setShowLegend(!showLegend)}
|
||||||
className="w-full px-4 py-2 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-750 text-xs text-gray-600 dark:text-gray-400 flex items-center justify-center gap-1"
|
className="w-full px-4 py-2 bg-muted/30 hover:bg-muted/50 text-xs text-muted-foreground flex items-center justify-center gap-1 transition-colors"
|
||||||
>
|
>
|
||||||
<Info className="w-3 h-3" />
|
<Info className="w-3 h-3" />
|
||||||
<span>File Status Guide</span>
|
<span>File Status Guide</span>
|
||||||
{showLegend ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
{showLegend ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{showLegend && (
|
{showLegend && (
|
||||||
<div className="px-4 py-3 bg-gray-50 dark:bg-gray-800 text-xs">
|
<div className="px-4 py-3 bg-muted/30 text-xs">
|
||||||
<div className={`${isMobile ? 'grid grid-cols-2 gap-3 justify-items-center' : 'flex justify-center gap-6'}`}>
|
<div className={`${isMobile ? 'grid grid-cols-2 gap-3 justify-items-center' : 'flex justify-center gap-6'}`}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="inline-flex items-center justify-center w-5 h-5 bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300 rounded border border-yellow-200 dark:border-yellow-800 font-bold text-xs">
|
<span className="inline-flex items-center justify-center w-5 h-5 bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300 rounded border border-yellow-200 dark:border-yellow-800/50 font-bold text-[10px]">
|
||||||
M
|
M
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-600 dark:text-gray-400 italic">Modified</span>
|
<span className="text-muted-foreground italic">Modified</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="inline-flex items-center justify-center w-5 h-5 bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300 rounded border border-green-200 dark:border-green-800 font-bold text-xs">
|
<span className="inline-flex items-center justify-center w-5 h-5 bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300 rounded border border-green-200 dark:border-green-800/50 font-bold text-[10px]">
|
||||||
A
|
A
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-600 dark:text-gray-400 italic">Added</span>
|
<span className="text-muted-foreground italic">Added</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="inline-flex items-center justify-center w-5 h-5 bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300 rounded border border-red-200 dark:border-red-800 font-bold text-xs">
|
<span className="inline-flex items-center justify-center w-5 h-5 bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300 rounded border border-red-200 dark:border-red-800/50 font-bold text-[10px]">
|
||||||
D
|
D
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-600 dark:text-gray-400 italic">Deleted</span>
|
<span className="text-muted-foreground italic">Deleted</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="inline-flex items-center justify-center w-5 h-5 bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300 rounded border border-gray-300 dark:border-gray-600 font-bold text-xs">
|
<span className="inline-flex items-center justify-center w-5 h-5 bg-muted text-muted-foreground rounded border border-border font-bold text-[10px]">
|
||||||
U
|
U
|
||||||
</span>
|
</span>
|
||||||
<span className="text-gray-600 dark:text-gray-400 italic">Untracked</span>
|
<span className="text-muted-foreground italic">Untracked</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1209,19 +1208,21 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
<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 items-center justify-center h-32">
|
||||||
<RefreshCw className="w-6 h-6 animate-spin text-gray-400" />
|
<RefreshCw className="w-5 h-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">
|
||||||
<GitBranch className="w-16 h-16 mb-4 opacity-30 text-gray-400 dark:text-gray-500" />
|
<div className="w-14 h-14 rounded-2xl bg-muted/50 flex items-center justify-center mb-4">
|
||||||
<h3 className="text-lg font-medium mb-2 text-gray-900 dark:text-white">No commits yet</h3>
|
<GitBranch className="w-7 h-7 text-muted-foreground/50" />
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6 max-w-md">
|
</div>
|
||||||
|
<h3 className="text-lg font-medium mb-2 text-foreground">No commits yet</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-6 max-w-md">
|
||||||
This repository doesn't have any commits yet. Create your first commit to start tracking changes.
|
This repository doesn't have any commits yet. Create your first commit to start tracking changes.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={createInitialCommit}
|
onClick={createInitialCommit}
|
||||||
disabled={isCreatingInitialCommit}
|
disabled={isCreatingInitialCommit}
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
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"
|
||||||
>
|
>
|
||||||
{isCreatingInitialCommit ? (
|
{isCreatingInitialCommit ? (
|
||||||
<>
|
<>
|
||||||
@@ -1237,8 +1238,8 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : !gitStatus || (!gitStatus.modified?.length && !gitStatus.added?.length && !gitStatus.deleted?.length && !gitStatus.untracked?.length) ? (
|
) : !gitStatus || (!gitStatus.modified?.length && !gitStatus.added?.length && !gitStatus.deleted?.length && !gitStatus.untracked?.length) ? (
|
||||||
<div className="flex flex-col items-center justify-center h-32 text-gray-500 dark:text-gray-400">
|
<div className="flex flex-col items-center justify-center h-32 text-muted-foreground">
|
||||||
<GitCommit className="w-12 h-12 mb-2 opacity-50" />
|
<GitCommit className="w-10 h-10 mb-2 opacity-40" />
|
||||||
<p className="text-sm">No changes detected</p>
|
<p className="text-sm">No changes detected</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -1257,11 +1258,11 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
<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 items-center justify-center h-32">
|
||||||
<RefreshCw className="w-6 h-6 animate-spin text-gray-400" />
|
<RefreshCw className="w-5 h-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-gray-500 dark:text-gray-400">
|
<div className="flex flex-col items-center justify-center h-32 text-muted-foreground">
|
||||||
<History className="w-12 h-12 mb-2 opacity-50" />
|
<History className="w-10 h-10 mb-2 opacity-40" />
|
||||||
<p className="text-sm">No commits found</p>
|
<p className="text-sm">No commits found</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -1275,12 +1276,12 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
{/* New Branch Modal */}
|
{/* New Branch Modal */}
|
||||||
{showNewBranchModal && (
|
{showNewBranchModal && (
|
||||||
<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 bg-opacity-50" onClick={() => setShowNewBranchModal(false)} />
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setShowNewBranchModal(false)} />
|
||||||
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full">
|
<div className="relative bg-card border border-border rounded-xl shadow-2xl max-w-md w-full overflow-hidden">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">Create New Branch</h3>
|
<h3 className="text-lg font-semibold text-foreground mb-4">Create New Branch</h3>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-foreground/80 mb-2">
|
||||||
Branch Name
|
Branch Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -1293,11 +1294,11 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="feature/new-feature"
|
placeholder="feature/new-feature"
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
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"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 mb-4">
|
<div className="text-xs text-muted-foreground mb-4">
|
||||||
This will create a new branch from the current branch ({currentBranch})
|
This will create a new branch from the current branch ({currentBranch})
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end space-x-3">
|
<div className="flex justify-end space-x-3">
|
||||||
@@ -1306,14 +1307,14 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
setShowNewBranchModal(false);
|
setShowNewBranchModal(false);
|
||||||
setNewBranchName('');
|
setNewBranchName('');
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md"
|
className="px-4 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={createBranch}
|
onClick={createBranch}
|
||||||
disabled={!newBranchName.trim() || isCreatingBranch}
|
disabled={!newBranchName.trim() || isCreatingBranch}
|
||||||
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
|
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"
|
||||||
>
|
>
|
||||||
{isCreatingBranch ? (
|
{isCreatingBranch ? (
|
||||||
<>
|
<>
|
||||||
@@ -1336,44 +1337,44 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
|||||||
{/* Confirmation Modal */}
|
{/* Confirmation Modal */}
|
||||||
{confirmAction && (
|
{confirmAction && (
|
||||||
<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 bg-opacity-50" onClick={() => setConfirmAction(null)} />
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setConfirmAction(null)} />
|
||||||
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full">
|
<div className="relative bg-card border border-border rounded-xl shadow-2xl max-w-md w-full overflow-hidden">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
<div className={`p-2 rounded-full mr-3 ${
|
<div className={`p-2 rounded-full mr-3 ${
|
||||||
(confirmAction.type === 'discard' || confirmAction.type === 'delete') ? 'bg-red-100 dark:bg-red-900' : 'bg-yellow-100 dark:bg-yellow-900'
|
(confirmAction.type === 'discard' || confirmAction.type === 'delete') ? 'bg-red-100 dark:bg-red-900/30' : 'bg-yellow-100 dark:bg-yellow-900/30'
|
||||||
}`}>
|
}`}>
|
||||||
<AlertTriangle className={`w-5 h-5 ${
|
<AlertTriangle className={`w-5 h-5 ${
|
||||||
(confirmAction.type === 'discard' || confirmAction.type === 'delete') ? 'text-red-600 dark:text-red-400' : 'text-yellow-600 dark:text-yellow-400'
|
(confirmAction.type === 'discard' || confirmAction.type === 'delete') ? 'text-red-600 dark:text-red-400' : 'text-yellow-600 dark:text-yellow-400'
|
||||||
}`} />
|
}`} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold">
|
<h3 className="text-lg font-semibold text-foreground">
|
||||||
{confirmAction.type === 'discard' ? 'Discard Changes' :
|
{confirmAction.type === 'discard' ? 'Discard Changes' :
|
||||||
confirmAction.type === 'delete' ? 'Delete File' :
|
confirmAction.type === 'delete' ? 'Delete File' :
|
||||||
confirmAction.type === 'commit' ? 'Confirm Commit' :
|
confirmAction.type === 'commit' ? 'Confirm Commit' :
|
||||||
confirmAction.type === 'pull' ? 'Confirm Pull' :
|
confirmAction.type === 'pull' ? 'Confirm Pull' :
|
||||||
confirmAction.type === 'publish' ? 'Publish Branch' : 'Confirm Push'}
|
confirmAction.type === 'publish' ? 'Publish Branch' : 'Confirm Push'}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
<p className="text-sm text-muted-foreground mb-6">
|
||||||
{confirmAction.message}
|
{confirmAction.message}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-3">
|
<div className="flex justify-end space-x-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmAction(null)}
|
onClick={() => setConfirmAction(null)}
|
||||||
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md"
|
className="px-4 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={confirmAndExecute}
|
onClick={confirmAndExecute}
|
||||||
className={`px-4 py-2 text-sm text-white rounded-md ${
|
className={`px-4 py-2 text-sm text-white rounded-lg transition-colors ${
|
||||||
(confirmAction.type === 'discard' || confirmAction.type === 'delete')
|
(confirmAction.type === 'discard' || confirmAction.type === 'delete')
|
||||||
? 'bg-red-600 hover:bg-red-700'
|
? 'bg-red-600 hover:bg-red-700'
|
||||||
: confirmAction.type === 'commit'
|
: confirmAction.type === 'commit'
|
||||||
? 'bg-blue-600 hover:bg-blue-700'
|
? 'bg-primary hover:bg-primary/90'
|
||||||
: confirmAction.type === 'pull'
|
: confirmAction.type === 'pull'
|
||||||
? 'bg-green-600 hover:bg-green-700'
|
? 'bg-green-600 hover:bg-green-700'
|
||||||
: confirmAction.type === 'publish'
|
: confirmAction.type === 'publish'
|
||||||
|
|||||||
@@ -1,74 +1,90 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MessageSquare, Folder, Terminal, GitBranch, Globe, CheckSquare } from 'lucide-react';
|
import { MessageSquare, Folder, Terminal, GitBranch, ClipboardCheck } from 'lucide-react';
|
||||||
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
||||||
|
import { useTaskMaster } from '../contexts/TaskMasterContext';
|
||||||
|
|
||||||
function MobileNav({ activeTab, setActiveTab, isInputFocused }) {
|
function MobileNav({ activeTab, setActiveTab, isInputFocused }) {
|
||||||
const { tasksEnabled } = useTasksSettings();
|
const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings();
|
||||||
|
const shouldShowTasksTab = Boolean(tasksEnabled && isTaskMasterInstalled);
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{
|
{
|
||||||
id: 'chat',
|
id: 'chat',
|
||||||
icon: MessageSquare,
|
icon: MessageSquare,
|
||||||
|
label: 'Chat',
|
||||||
onClick: () => setActiveTab('chat')
|
onClick: () => setActiveTab('chat')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'shell',
|
id: 'shell',
|
||||||
icon: Terminal,
|
icon: Terminal,
|
||||||
|
label: 'Shell',
|
||||||
onClick: () => setActiveTab('shell')
|
onClick: () => setActiveTab('shell')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'files',
|
id: 'files',
|
||||||
icon: Folder,
|
icon: Folder,
|
||||||
|
label: 'Files',
|
||||||
onClick: () => setActiveTab('files')
|
onClick: () => setActiveTab('files')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'git',
|
id: 'git',
|
||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
|
label: 'Git',
|
||||||
onClick: () => setActiveTab('git')
|
onClick: () => setActiveTab('git')
|
||||||
},
|
},
|
||||||
// Conditionally add tasks tab if enabled
|
...(shouldShowTasksTab ? [{
|
||||||
...(tasksEnabled ? [{
|
|
||||||
id: 'tasks',
|
id: 'tasks',
|
||||||
icon: CheckSquare,
|
icon: ClipboardCheck,
|
||||||
|
label: 'Tasks',
|
||||||
onClick: () => setActiveTab('tasks')
|
onClick: () => setActiveTab('tasks')
|
||||||
}] : [])
|
}] : [])
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`fixed bottom-0 left-0 right-0 bg-background border-t border-border z-50 ios-bottom-safe transform transition-transform duration-300 ease-in-out shadow-lg ${
|
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'
|
isInputFocused ? 'translate-y-full' : 'translate-y-0'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-around py-1">
|
<div className="nav-glass mobile-nav-float rounded-2xl border border-border/30">
|
||||||
{navItems.map((item) => {
|
<div className="flex items-center justify-around px-1 py-1.5 gap-0.5">
|
||||||
const Icon = item.icon;
|
{navItems.map((item) => {
|
||||||
const isActive = activeTab === item.id;
|
const Icon = item.icon;
|
||||||
|
const isActive = activeTab === item.id;
|
||||||
return (
|
|
||||||
<button
|
return (
|
||||||
key={item.id}
|
<button
|
||||||
onClick={item.onClick}
|
key={item.id}
|
||||||
onTouchStart={(e) => {
|
onClick={item.onClick}
|
||||||
e.preventDefault();
|
onTouchStart={(e) => {
|
||||||
item.onClick();
|
e.preventDefault();
|
||||||
}}
|
item.onClick();
|
||||||
className={`flex items-center justify-center p-2 rounded-lg min-h-[40px] min-w-[40px] relative touch-manipulation ${
|
}}
|
||||||
isActive
|
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 ${
|
||||||
? 'text-blue-600 dark:text-blue-400'
|
isActive
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
? 'text-primary'
|
||||||
}`}
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
aria-label={item.id}
|
}`}
|
||||||
>
|
aria-label={item.label}
|
||||||
<Icon className="w-5 h-5" />
|
aria-current={isActive ? 'page' : undefined}
|
||||||
{isActive && (
|
>
|
||||||
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-6 h-0.5 bg-blue-600 dark:bg-blue-400 rounded-full" />
|
{isActive && (
|
||||||
)}
|
<div className="absolute inset-0 bg-primary/8 dark:bg-primary/12 rounded-xl" />
|
||||||
</button>
|
)}
|
||||||
);
|
<Icon
|
||||||
})}
|
className={`relative z-10 transition-all duration-200 ${isActive ? 'w-5 h-5' : 'w-[18px] h-[18px]'}`}
|
||||||
|
strokeWidth={isActive ? 2.4 : 1.8}
|
||||||
|
/>
|
||||||
|
<span className={`relative z-10 text-[10px] font-medium transition-all duration-200 ${isActive ? 'opacity-100' : 'opacity-60'}`}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MobileNav;
|
export default MobileNav;
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export default function AppContent() {
|
|||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex bg-background">
|
<div className="fixed inset-0 flex bg-background">
|
||||||
{!isMobile ? (
|
{!isMobile ? (
|
||||||
<div className="h-full flex-shrink-0 border-r border-border bg-card">
|
<div className="h-full flex-shrink-0 border-r border-border/50">
|
||||||
<Sidebar {...sidebarSharedProps} />
|
<Sidebar {...sidebarSharedProps} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -83,7 +83,7 @@ export default function AppContent() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="fixed inset-0 bg-background/80 backdrop-blur-sm transition-opacity duration-150 ease-out"
|
className="fixed inset-0 bg-background/60 backdrop-blur-sm transition-opacity duration-150 ease-out"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setSidebarOpen(false);
|
setSidebarOpen(false);
|
||||||
@@ -96,7 +96,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 transform transition-transform duration-150 ease-out ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
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'
|
||||||
}`}
|
}`}
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
onTouchStart={(event) => event.stopPropagation()}
|
onTouchStart={(event) => event.stopPropagation()}
|
||||||
|
|||||||
@@ -254,8 +254,8 @@ function ChatInterface({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400">
|
<div className="text-center text-muted-foreground">
|
||||||
<p>
|
<p className="text-sm">
|
||||||
{t('projectSelection.startChatWithProvider', {
|
{t('projectSelection.startChatWithProvider', {
|
||||||
provider: selectedProviderLabel,
|
provider: selectedProviderLabel,
|
||||||
defaultValue: 'Select a project to start chatting with {{provider}}',
|
defaultValue: 'Select a project to start chatting with {{provider}}',
|
||||||
|
|||||||
@@ -201,9 +201,9 @@ export default function ChatComposer({
|
|||||||
|
|
||||||
{!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 max-w-4xl mx-auto">
|
||||||
{isDragActive && (
|
{isDragActive && (
|
||||||
<div className="absolute inset-0 bg-blue-500/20 border-2 border-dashed border-blue-500 rounded-lg flex items-center justify-center z-50">
|
<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="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-lg">
|
<div className="bg-card rounded-xl p-4 shadow-lg border border-border/30">
|
||||||
<svg className="w-8 h-8 text-blue-500 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-8 h-8 text-primary mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
@@ -217,7 +217,7 @@ export default function ChatComposer({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{attachedImages.length > 0 && (
|
{attachedImages.length > 0 && (
|
||||||
<div className="mb-2 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
<div className="mb-2 p-2 bg-muted/40 rounded-xl">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{attachedImages.map((file, index) => (
|
{attachedImages.map((file, index) => (
|
||||||
<ImageAttachment
|
<ImageAttachment
|
||||||
@@ -233,14 +233,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-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg max-h-48 overflow-y-auto z-50 backdrop-blur-sm">
|
<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">
|
||||||
{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-gray-100 dark:border-gray-700 last:border-b-0 touch-manipulation ${
|
className={`px-4 py-3 cursor-pointer border-b border-border/30 last:border-b-0 touch-manipulation ${
|
||||||
index === selectedFileIndex
|
index === selectedFileIndex
|
||||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
? 'bg-primary/8 text-primary'
|
||||||
: 'hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'
|
: 'hover:bg-accent/50 text-foreground'
|
||||||
}`}
|
}`}
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -253,7 +253,7 @@ export default function ChatComposer({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="font-medium text-sm">{file.name}</div>
|
<div className="font-medium text-sm">{file.name}</div>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 font-mono">{file.path}</div>
|
<div className="text-xs text-muted-foreground font-mono">{file.path}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -271,7 +271,7 @@ export default function ChatComposer({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
{...getRootProps()}
|
{...getRootProps()}
|
||||||
className={`relative bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 focus-within:ring-2 focus-within:ring-blue-500 dark:focus-within:ring-blue-500 focus-within:border-blue-500 transition-all duration-200 overflow-hidden ${
|
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 ${
|
||||||
isTextareaExpanded ? 'chat-input-expanded' : ''
|
isTextareaExpanded ? 'chat-input-expanded' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -296,17 +296,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-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 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 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"
|
||||||
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-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
className="absolute left-2 top-1/2 transform -translate-y-1/2 p-2 hover:bg-accent/60 rounded-xl transition-colors"
|
||||||
title={t('input.attachImages')}
|
title={t('input.attachImages')}
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
@@ -331,15 +331,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-12 h-12 sm:w-12 sm:h-12 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed rounded-full flex items-center justify-center transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:ring-offset-gray-800"
|
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"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-white transform rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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-gray-400 dark:text-gray-500 pointer-events-none hidden sm:block transition-opacity duration-200 ${
|
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 ${
|
||||||
input.trim() ? 'opacity-0' : 'opacity-100'
|
input.trim() ? 'opacity-0' : 'opacity-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -38,31 +38,31 @@ export default function ChatInputControls({
|
|||||||
const { t } = useTranslation('chat');
|
const { t } = useTranslation('chat');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center gap-3">
|
<div className="flex items-center justify-center gap-2 sm:gap-3 flex-wrap">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onModeSwitch}
|
onClick={onModeSwitch}
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-all duration-200 ${
|
className={`px-2.5 py-1 sm:px-3 sm:py-1.5 rounded-lg text-xs sm:text-sm font-medium border transition-all duration-200 ${
|
||||||
permissionMode === 'default'
|
permissionMode === 'default'
|
||||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600'
|
? 'bg-muted/50 text-muted-foreground border-border/60 hover:bg-muted'
|
||||||
: permissionMode === 'acceptEdits'
|
: permissionMode === 'acceptEdits'
|
||||||
? 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border-green-300 dark:border-green-600 hover:bg-green-100 dark:hover:bg-green-900/30'
|
? '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'
|
||||||
: permissionMode === 'bypassPermissions'
|
: permissionMode === 'bypassPermissions'
|
||||||
? 'bg-orange-50 dark:bg-orange-900/20 text-orange-700 dark:text-orange-300 border-orange-300 dark:border-orange-600 hover:bg-orange-100 dark:hover:bg-orange-900/30'
|
? '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'
|
||||||
: 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900/30'
|
: 'bg-primary/5 text-primary border-primary/20 hover:bg-primary/10'
|
||||||
}`}
|
}`}
|
||||||
title={t('input.clickToChangeMode')}
|
title={t('input.clickToChangeMode')}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-1.5">
|
||||||
<div
|
<div
|
||||||
className={`w-2 h-2 rounded-full ${
|
className={`w-1.5 h-1.5 rounded-full ${
|
||||||
permissionMode === 'default'
|
permissionMode === 'default'
|
||||||
? 'bg-gray-500'
|
? 'bg-muted-foreground'
|
||||||
: permissionMode === 'acceptEdits'
|
: permissionMode === 'acceptEdits'
|
||||||
? 'bg-green-500'
|
? 'bg-green-500'
|
||||||
: permissionMode === 'bypassPermissions'
|
: permissionMode === 'bypassPermissions'
|
||||||
? 'bg-orange-500'
|
? 'bg-orange-500'
|
||||||
: 'bg-blue-500'
|
: 'bg-primary'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
@@ -83,10 +83,10 @@ export default function ChatInputControls({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onToggleCommandMenu}
|
onClick={onToggleCommandMenu}
|
||||||
className="relative w-8 h-8 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-full flex items-center justify-center transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:ring-offset-gray-800"
|
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"
|
||||||
title={t('input.showAllCommands')}
|
title={t('input.showAllCommands')}
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
@@ -96,8 +96,7 @@ export default function ChatInputControls({
|
|||||||
</svg>
|
</svg>
|
||||||
{slashCommandsCount > 0 && (
|
{slashCommandsCount > 0 && (
|
||||||
<span
|
<span
|
||||||
className="absolute -top-1 -right-1 bg-blue-600 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center"
|
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"
|
||||||
style={{ fontSize: '10px' }}
|
|
||||||
>
|
>
|
||||||
{slashCommandsCount}
|
{slashCommandsCount}
|
||||||
</span>
|
</span>
|
||||||
@@ -108,11 +107,11 @@ export default function ChatInputControls({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClearInput}
|
onClick={onClearInput}
|
||||||
className="w-8 h-8 bg-white dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-full flex items-center justify-center transition-all duration-200 group shadow-sm"
|
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"
|
||||||
title={t('input.clearInput', { defaultValue: 'Clear input' })}
|
title={t('input.clearInput', { defaultValue: 'Clear input' })}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 text-gray-600 dark:text-gray-300 group-hover:text-gray-800 dark:group-hover:text-gray-100 transition-colors"
|
className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-muted-foreground group-hover:text-foreground transition-colors"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -125,10 +124,10 @@ export default function ChatInputControls({
|
|||||||
{isUserScrolledUp && hasMessages && (
|
{isUserScrolledUp && hasMessages && (
|
||||||
<button
|
<button
|
||||||
onClick={onScrollToBottom}
|
onClick={onScrollToBottom}
|
||||||
className="w-8 h-8 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-200 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:ring-offset-gray-800"
|
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"
|
||||||
title={t('input.scrollToBottom', { defaultValue: 'Scroll to bottom' })}
|
title={t('input.scrollToBottom', { defaultValue: 'Scroll to bottom' })}
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-3.5 h-3.5 sm:w-4 sm:h-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>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Check, ChevronDown } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import SessionProviderLogo from '../../../SessionProviderLogo';
|
import SessionProviderLogo from '../../../SessionProviderLogo';
|
||||||
import NextTaskBanner from '../../../NextTaskBanner.jsx';
|
import NextTaskBanner from '../../../NextTaskBanner.jsx';
|
||||||
@@ -23,6 +24,54 @@ interface ProviderSelectionEmptyStateProps {
|
|||||||
setInput: React.Dispatch<React.SetStateAction<string>>;
|
setInput: React.Dispatch<React.SetStateAction<string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProviderDef = {
|
||||||
|
id: SessionProvider;
|
||||||
|
name: string;
|
||||||
|
infoKey: string;
|
||||||
|
accent: string;
|
||||||
|
ring: string;
|
||||||
|
check: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROVIDERS: ProviderDef[] = [
|
||||||
|
{
|
||||||
|
id: 'claude',
|
||||||
|
name: 'Claude Code',
|
||||||
|
infoKey: 'providerSelection.providerInfo.anthropic',
|
||||||
|
accent: 'border-primary',
|
||||||
|
ring: 'ring-primary/15',
|
||||||
|
check: 'bg-primary text-primary-foreground',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cursor',
|
||||||
|
name: 'Cursor',
|
||||||
|
infoKey: 'providerSelection.providerInfo.cursorEditor',
|
||||||
|
accent: 'border-violet-500 dark:border-violet-400',
|
||||||
|
ring: 'ring-violet-500/15',
|
||||||
|
check: 'bg-violet-500 text-white',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'codex',
|
||||||
|
name: 'Codex',
|
||||||
|
infoKey: 'providerSelection.providerInfo.openai',
|
||||||
|
accent: 'border-emerald-600 dark:border-emerald-400',
|
||||||
|
ring: 'ring-emerald-600/15',
|
||||||
|
check: 'bg-emerald-600 dark:bg-emerald-500 text-white',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function getModelConfig(p: SessionProvider) {
|
||||||
|
if (p === 'claude') return CLAUDE_MODELS;
|
||||||
|
if (p === 'codex') return CODEX_MODELS;
|
||||||
|
return CURSOR_MODELS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModelValue(p: SessionProvider, c: string, cu: string, co: string) {
|
||||||
|
if (p === 'claude') return c;
|
||||||
|
if (p === 'codex') return co;
|
||||||
|
return cu;
|
||||||
|
}
|
||||||
|
|
||||||
export default function ProviderSelectionEmptyState({
|
export default function ProviderSelectionEmptyState({
|
||||||
selectedSession,
|
selectedSession,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
@@ -41,185 +90,133 @@ export default function ProviderSelectionEmptyState({
|
|||||||
setInput,
|
setInput,
|
||||||
}: ProviderSelectionEmptyStateProps) {
|
}: ProviderSelectionEmptyStateProps) {
|
||||||
const { t } = useTranslation('chat');
|
const { t } = useTranslation('chat');
|
||||||
// Reuse one translated prompt so task-start behavior stays consistent across empty and session states.
|
|
||||||
const nextTaskPrompt = t('tasks.nextTaskPrompt', { defaultValue: 'Start the next task' });
|
const nextTaskPrompt = t('tasks.nextTaskPrompt', { defaultValue: 'Start the next task' });
|
||||||
|
|
||||||
const selectProvider = (nextProvider: SessionProvider) => {
|
const selectProvider = (next: SessionProvider) => {
|
||||||
setProvider(nextProvider);
|
setProvider(next);
|
||||||
localStorage.setItem('selected-provider', nextProvider);
|
localStorage.setItem('selected-provider', next);
|
||||||
setTimeout(() => textareaRef.current?.focus(), 100);
|
setTimeout(() => textareaRef.current?.focus(), 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const handleModelChange = (value: string) => {
|
||||||
<div className="flex items-center justify-center h-full">
|
if (provider === 'claude') { setClaudeModel(value); localStorage.setItem('claude-model', value); }
|
||||||
{!selectedSession && !currentSessionId && (
|
else if (provider === 'codex') { setCodexModel(value); localStorage.setItem('codex-model', value); }
|
||||||
<div className="text-center px-6 sm:px-4 py-8">
|
else { setCursorModel(value); localStorage.setItem('cursor-model', value); }
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-3">{t('providerSelection.title')}</h2>
|
};
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-8">{t('providerSelection.description')}</p>
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-8">
|
const modelConfig = getModelConfig(provider);
|
||||||
<button
|
const currentModel = getModelValue(provider, claudeModel, cursorModel, codexModel);
|
||||||
onClick={() => selectProvider('claude')}
|
|
||||||
className={`group relative w-64 h-32 bg-white dark:bg-gray-800 rounded-xl border-2 transition-all duration-200 hover:scale-105 hover:shadow-xl ${
|
|
||||||
provider === 'claude'
|
|
||||||
? 'border-blue-500 shadow-lg ring-2 ring-blue-500/20'
|
|
||||||
: 'border-gray-200 dark:border-gray-700 hover:border-blue-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center justify-center h-full gap-3">
|
|
||||||
<SessionProviderLogo provider="claude" className="w-10 h-10" />
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold text-gray-900 dark:text-white">Claude Code</p>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{t('providerSelection.providerInfo.anthropic')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{provider === 'claude' && (
|
|
||||||
<div className="absolute top-2 right-2">
|
|
||||||
<div className="w-5 h-5 bg-blue-500 rounded-full flex items-center justify-center">
|
|
||||||
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
/* ── New session — provider picker ── */
|
||||||
onClick={() => selectProvider('cursor')}
|
if (!selectedSession && !currentSessionId) {
|
||||||
className={`group relative w-64 h-32 bg-white dark:bg-gray-800 rounded-xl border-2 transition-all duration-200 hover:scale-105 hover:shadow-xl ${
|
return (
|
||||||
provider === 'cursor'
|
<div className="flex items-center justify-center h-full px-4">
|
||||||
? 'border-purple-500 shadow-lg ring-2 ring-purple-500/20'
|
<div className="w-full max-w-md">
|
||||||
: 'border-gray-200 dark:border-gray-700 hover:border-purple-400'
|
{/* Heading */}
|
||||||
}`}
|
<div className="text-center mb-8">
|
||||||
>
|
<h2 className="text-lg sm:text-xl font-semibold text-foreground tracking-tight">
|
||||||
<div className="flex flex-col items-center justify-center h-full gap-3">
|
{t('providerSelection.title')}
|
||||||
<SessionProviderLogo provider="cursor" className="w-10 h-10" />
|
</h2>
|
||||||
<div>
|
<p className="text-[13px] text-muted-foreground mt-1">
|
||||||
<p className="font-semibold text-gray-900 dark:text-white">Cursor</p>
|
{t('providerSelection.description')}
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{t('providerSelection.providerInfo.cursorEditor')}</p>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{provider === 'cursor' && (
|
|
||||||
<div className="absolute top-2 right-2">
|
|
||||||
<div className="w-5 h-5 bg-purple-500 rounded-full flex items-center justify-center">
|
|
||||||
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => selectProvider('codex')}
|
|
||||||
className={`group relative w-64 h-32 bg-white dark:bg-gray-800 rounded-xl border-2 transition-all duration-200 hover:scale-105 hover:shadow-xl ${
|
|
||||||
provider === 'codex'
|
|
||||||
? 'border-gray-800 dark:border-gray-300 shadow-lg ring-2 ring-gray-800/20 dark:ring-gray-300/20'
|
|
||||||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-500 dark:hover:border-gray-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center justify-center h-full gap-3">
|
|
||||||
<SessionProviderLogo provider="codex" className="w-10 h-10" />
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold text-gray-900 dark:text-white">Codex</p>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">{t('providerSelection.providerInfo.openai')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{provider === 'codex' && (
|
|
||||||
<div className="absolute top-2 right-2">
|
|
||||||
<div className="w-5 h-5 bg-gray-800 dark:bg-gray-300 rounded-full flex items-center justify-center">
|
|
||||||
<svg className="w-3 h-3 text-white dark:text-gray-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`mb-6 transition-opacity duration-200 ${provider ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}>
|
{/* Provider cards — horizontal row, equal width */}
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{t('providerSelection.selectModel')}</label>
|
<div className="grid grid-cols-3 gap-2 sm:gap-2.5 mb-6">
|
||||||
{provider === 'claude' ? (
|
{PROVIDERS.map((p) => {
|
||||||
<select
|
const active = provider === p.id;
|
||||||
value={claudeModel}
|
return (
|
||||||
onChange={(e) => {
|
<button
|
||||||
const newModel = e.target.value;
|
key={p.id}
|
||||||
setClaudeModel(newModel);
|
onClick={() => selectProvider(p.id)}
|
||||||
localStorage.setItem('claude-model', newModel);
|
className={`
|
||||||
}}
|
relative flex flex-col items-center gap-2.5 pt-5 pb-4 px-2
|
||||||
className="pl-4 pr-10 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 min-w-[140px]"
|
rounded-xl border-[1.5px] transition-all duration-150
|
||||||
>
|
active:scale-[0.97]
|
||||||
{CLAUDE_MODELS.OPTIONS.map(({ value, label }) => (
|
${active
|
||||||
<option key={value} value={value}>
|
? `${p.accent} ${p.ring} ring-2 bg-card shadow-sm`
|
||||||
{label}
|
: 'border-border bg-card/60 hover:bg-card hover:border-border/80'
|
||||||
</option>
|
}
|
||||||
))}
|
`}
|
||||||
</select>
|
>
|
||||||
) : provider === 'codex' ? (
|
<SessionProviderLogo
|
||||||
<select
|
provider={p.id}
|
||||||
value={codexModel}
|
className={`w-9 h-9 transition-transform duration-150 ${active ? 'scale-110' : ''}`}
|
||||||
onChange={(e) => {
|
/>
|
||||||
const newModel = e.target.value;
|
<div className="text-center">
|
||||||
setCodexModel(newModel);
|
<p className="text-[13px] font-semibold text-foreground leading-none">{p.name}</p>
|
||||||
localStorage.setItem('codex-model', newModel);
|
<p className="text-[10px] text-muted-foreground mt-1 leading-tight">{t(p.infoKey)}</p>
|
||||||
}}
|
</div>
|
||||||
className="pl-4 pr-10 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-gray-500 focus:border-gray-500 min-w-[140px]"
|
{/* Check badge */}
|
||||||
>
|
{active && (
|
||||||
{CODEX_MODELS.OPTIONS.map(({ value, label }) => (
|
<div className={`absolute -top-1 -right-1 w-[18px] h-[18px] rounded-full ${p.check} flex items-center justify-center shadow-sm`}>
|
||||||
<option key={value} value={value}>
|
<Check className="w-2.5 h-2.5" strokeWidth={3} />
|
||||||
{label}
|
</div>
|
||||||
</option>
|
)}
|
||||||
))}
|
</button>
|
||||||
</select>
|
);
|
||||||
) : (
|
})}
|
||||||
<select
|
|
||||||
value={cursorModel}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newModel = e.target.value;
|
|
||||||
setCursorModel(newModel);
|
|
||||||
localStorage.setItem('cursor-model', newModel);
|
|
||||||
}}
|
|
||||||
className="pl-4 pr-10 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 min-w-[140px]"
|
|
||||||
disabled={provider !== 'cursor'}
|
|
||||||
>
|
|
||||||
{CURSOR_MODELS.OPTIONS.map(({ value, label }) => (
|
|
||||||
<option key={value} value={value}>
|
|
||||||
{label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
{/* Model picker — appears after provider is chosen */}
|
||||||
{provider === 'claude'
|
<div className={`transition-all duration-200 ${provider ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-1 pointer-events-none'}`}>
|
||||||
? t('providerSelection.readyPrompt.claude', { model: claudeModel })
|
<div className="flex items-center justify-center gap-2 mb-5">
|
||||||
: provider === 'cursor'
|
<span className="text-xs text-muted-foreground">{t('providerSelection.selectModel')}</span>
|
||||||
? t('providerSelection.readyPrompt.cursor', { model: cursorModel })
|
<div className="relative">
|
||||||
: provider === 'codex'
|
<select
|
||||||
? t('providerSelection.readyPrompt.codex', { model: codexModel })
|
value={currentModel}
|
||||||
: t('providerSelection.readyPrompt.default')}
|
onChange={(e) => handleModelChange(e.target.value)}
|
||||||
</p>
|
tabIndex={-1}
|
||||||
|
className="appearance-none pl-3 pr-7 py-1.5 text-xs 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"
|
||||||
|
>
|
||||||
|
{modelConfig.OPTIONS.map(({ value, label }: { value: string; label: string }) => (
|
||||||
|
<option key={value} value={value}>{label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-3 h-3 text-muted-foreground pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-center text-xs text-muted-foreground/70">
|
||||||
|
{provider === 'claude'
|
||||||
|
? t('providerSelection.readyPrompt.claude', { model: claudeModel })
|
||||||
|
: provider === 'cursor'
|
||||||
|
? t('providerSelection.readyPrompt.cursor', { model: cursorModel })
|
||||||
|
: provider === 'codex'
|
||||||
|
? t('providerSelection.readyPrompt.codex', { model: codexModel })
|
||||||
|
: t('providerSelection.readyPrompt.default')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Task banner */}
|
||||||
{provider && tasksEnabled && isTaskMasterInstalled && (
|
{provider && tasksEnabled && isTaskMasterInstalled && (
|
||||||
<div className="mt-4 px-4 sm:px-0">
|
<div className="mt-5">
|
||||||
<NextTaskBanner onStartTask={() => setInput(nextTaskPrompt)} onShowAllTasks={onShowAllTasks} />
|
<NextTaskBanner onStartTask={() => setInput(nextTaskPrompt)} onShowAllTasks={onShowAllTasks} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
{selectedSession && (
|
);
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400 px-6 sm:px-4">
|
}
|
||||||
<p className="font-bold text-lg sm:text-xl mb-3">{t('session.continue.title')}</p>
|
|
||||||
<p className="text-sm sm:text-base leading-relaxed">{t('session.continue.description')}</p>
|
/* ── Existing session — continue prompt ── */
|
||||||
|
if (selectedSession) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-center px-6 max-w-md">
|
||||||
|
<p className="text-lg font-semibold text-foreground mb-1.5">{t('session.continue.title')}</p>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">{t('session.continue.description')}</p>
|
||||||
|
|
||||||
{tasksEnabled && isTaskMasterInstalled && (
|
{tasksEnabled && isTaskMasterInstalled && (
|
||||||
<div className="mt-4 px-4 sm:px-0">
|
<div className="mt-5">
|
||||||
<NextTaskBanner onStartTask={() => setInput(nextTaskPrompt)} onShowAllTasks={onShowAllTasks} />
|
<NextTaskBanner onStartTask={() => setInput(nextTaskPrompt)} onShowAllTasks={onShowAllTasks} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ export default function MainContentHeader({
|
|||||||
onMenuClick,
|
onMenuClick,
|
||||||
}: MainContentHeaderProps) {
|
}: MainContentHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-background border-b border-border p-2 sm:p-3 pwa-header-safe flex-shrink-0">
|
<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="flex items-center justify-between relative">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="flex items-center space-x-2 min-w-0 flex-1">
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||||
{isMobile && <MobileMenuButton onMenuClick={onMenuClick} />}
|
{isMobile && <MobileMenuButton onMenuClick={onMenuClick} />}
|
||||||
<MainContentTitle
|
<MainContentTitle
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Folder } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import MobileMenuButton from './MobileMenuButton';
|
import MobileMenuButton from './MobileMenuButton';
|
||||||
import type { MainContentStateViewProps } from '../../types/types';
|
import type { MainContentStateViewProps } from '../../types/types';
|
||||||
@@ -10,17 +11,17 @@ export default function MainContentStateView({ mode, isMobile, onMenuClick }: Ma
|
|||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<div className="bg-background border-b border-border p-2 sm:p-3 pwa-header-safe flex-shrink-0">
|
<div className="bg-background/80 backdrop-blur-sm border-b border-border/50 p-2 sm:p-3 pwa-header-safe flex-shrink-0">
|
||||||
<MobileMenuButton onMenuClick={onMenuClick} compact />
|
<MobileMenuButton onMenuClick={onMenuClick} compact />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex-1 flex items-center justify-center">
|
<div className="flex-1 flex items-center justify-center">
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400">
|
<div className="text-center text-muted-foreground">
|
||||||
<div className="w-12 h-12 mx-auto mb-4">
|
<div className="w-10 h-10 mx-auto mb-4">
|
||||||
<div
|
<div
|
||||||
className="w-full h-full rounded-full border-4 border-gray-200 border-t-blue-500"
|
className="w-full h-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',
|
||||||
@@ -28,22 +29,20 @@ export default function MainContentStateView({ mode, isMobile, onMenuClick }: Ma
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-xl font-semibold mb-2">{t('mainContent.loading')}</h2>
|
<h2 className="text-lg font-semibold text-foreground mb-1">{t('mainContent.loading')}</h2>
|
||||||
<p>{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-1 flex items-center justify-center">
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400 max-w-md mx-auto px-6">
|
<div className="text-center max-w-md mx-auto px-6">
|
||||||
<div className="w-16 h-16 mx-auto mb-6 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
|
<div className="w-14 h-14 mx-auto mb-5 bg-muted/50 rounded-2xl flex items-center justify-center">
|
||||||
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<Folder className="w-7 h-7 text-muted-foreground" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-5l-2-2H5a2 2 0 00-2 2z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-semibold mb-3 text-gray-900 dark:text-white">{t('mainContent.chooseProject')}</h2>
|
<h2 className="text-xl font-semibold mb-2 text-foreground">{t('mainContent.chooseProject')}</h2>
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">{t('mainContent.selectProjectDescription')}</p>
|
<p className="text-sm text-muted-foreground mb-5 leading-relaxed">{t('mainContent.selectProjectDescription')}</p>
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
|
<div className="bg-primary/5 rounded-xl p-3.5 border border-primary/10">
|
||||||
<p className="text-sm text-blue-700 dark:text-blue-300">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { MessageSquare, Terminal, Folder, GitBranch, ClipboardCheck, type LucideIcon } from 'lucide-react';
|
||||||
import Tooltip from '../../../Tooltip';
|
import Tooltip from '../../../Tooltip';
|
||||||
import type { AppTab } from '../../../../types/app';
|
import type { AppTab } from '../../../../types/app';
|
||||||
import type { Dispatch, SetStateAction } from 'react';
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
@@ -12,50 +13,22 @@ type MainContentTabSwitcherProps = {
|
|||||||
type TabDefinition = {
|
type TabDefinition = {
|
||||||
id: AppTab;
|
id: AppTab;
|
||||||
labelKey: string;
|
labelKey: string;
|
||||||
iconPath: string;
|
icon: LucideIcon;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BASE_TABS: TabDefinition[] = [
|
const BASE_TABS: TabDefinition[] = [
|
||||||
{
|
{ id: 'chat', labelKey: 'tabs.chat', icon: MessageSquare },
|
||||||
id: 'chat',
|
{ id: 'shell', labelKey: 'tabs.shell', icon: Terminal },
|
||||||
labelKey: 'tabs.chat',
|
{ id: 'files', labelKey: 'tabs.files', icon: Folder },
|
||||||
iconPath:
|
{ id: 'git', labelKey: 'tabs.git', icon: GitBranch },
|
||||||
'M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'shell',
|
|
||||||
labelKey: 'tabs.shell',
|
|
||||||
iconPath: 'M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'files',
|
|
||||||
labelKey: 'tabs.files',
|
|
||||||
iconPath: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-5l-2-2H5a2 2 0 00-2 2z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'git',
|
|
||||||
labelKey: 'tabs.git',
|
|
||||||
iconPath: 'M13 10V3L4 14h7v7l9-11h-7z',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const TASKS_TAB: TabDefinition = {
|
const TASKS_TAB: TabDefinition = {
|
||||||
id: 'tasks',
|
id: 'tasks',
|
||||||
labelKey: 'tabs.tasks',
|
labelKey: 'tabs.tasks',
|
||||||
iconPath:
|
icon: ClipboardCheck,
|
||||||
'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getButtonClasses(tabId: AppTab, activeTab: AppTab) {
|
|
||||||
const base = 'relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200';
|
|
||||||
|
|
||||||
if (tabId === activeTab) {
|
|
||||||
return `${base} bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${base} text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MainContentTabSwitcher({
|
export default function MainContentTabSwitcher({
|
||||||
activeTab,
|
activeTab,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
@@ -66,19 +39,27 @@ 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="relative flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
|
<div className="inline-flex items-center bg-muted/60 rounded-lg p-[3px] gap-[2px]">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => {
|
||||||
<Tooltip key={tab.id} content={t(tab.labelKey)} position="bottom">
|
const Icon = tab.icon;
|
||||||
<button onClick={() => setActiveTab(tab.id)} className={getButtonClasses(tab.id, activeTab)}>
|
const isActive = tab.id === activeTab;
|
||||||
<span className="flex items-center gap-1 sm:gap-1.5">
|
|
||||||
<svg className="w-3 sm:w-3.5 h-3 sm:h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
return (
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={tab.iconPath} />
|
<Tooltip key={tab.id} content={t(tab.labelKey)} position="bottom">
|
||||||
</svg>
|
<button
|
||||||
<span className="hidden md:hidden lg:inline">{t(tab.labelKey)}</span>
|
onClick={() => setActiveTab(tab.id)}
|
||||||
</span>
|
className={`relative flex items-center gap-1.5 px-2.5 py-[5px] text-xs font-medium rounded-md transition-all duration-150 ${
|
||||||
</button>
|
isActive
|
||||||
</Tooltip>
|
? 'bg-background text-foreground shadow-sm'
|
||||||
))}
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="w-3.5 h-3.5" strokeWidth={isActive ? 2.2 : 1.8} />
|
||||||
|
<span className="hidden lg:inline">{t(tab.labelKey)}</span>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,22 +55,22 @@ export default function MainContentTitle({
|
|||||||
<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 sm:text-base font-semibold text-gray-900 dark:text-white whitespace-nowrap overflow-x-auto scrollbar-hide">
|
<h2 className="text-sm font-semibold text-foreground whitespace-nowrap overflow-x-auto scrollbar-hide leading-tight">
|
||||||
{getSessionTitle(selectedSession)}
|
{getSessionTitle(selectedSession)}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">{selectedProject.displayName}</div>
|
<div className="text-[11px] text-muted-foreground truncate leading-tight">{selectedProject.displayName}</div>
|
||||||
</div>
|
</div>
|
||||||
) : showChatNewSession ? (
|
) : showChatNewSession ? (
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h2 className="text-sm sm:text-base font-semibold text-gray-900 dark:text-white">{t('mainContent.newSession')}</h2>
|
<h2 className="text-sm font-semibold text-foreground leading-tight">{t('mainContent.newSession')}</h2>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">{selectedProject.displayName}</div>
|
<div className="text-[11px] text-muted-foreground truncate leading-tight">{selectedProject.displayName}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h2 className="text-sm sm:text-base font-semibold text-gray-900 dark:text-white">
|
<h2 className="text-sm font-semibold text-foreground leading-tight">
|
||||||
{getTabTitle(activeTab, shouldShowTasksTab, t)}
|
{getTabTitle(activeTab, shouldShowTasksTab, t)}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">{selectedProject.displayName}</div>
|
<div className="text-[11px] text-muted-foreground truncate leading-tight">{selectedProject.displayName}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ export default function MobileMenuButton({ onMenuClick, compact = false }: Mobil
|
|||||||
const { handleMobileMenuClick, handleMobileMenuTouchEnd } = useMobileMenuHandlers(onMenuClick);
|
const { handleMobileMenuClick, handleMobileMenuTouchEnd } = useMobileMenuHandlers(onMenuClick);
|
||||||
|
|
||||||
const buttonClasses = compact
|
const buttonClasses = compact
|
||||||
? '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-700 pwa-menu-button'
|
? 'p-1.5 text-muted-foreground hover:text-foreground rounded-lg hover:bg-accent/60 pwa-menu-button'
|
||||||
: '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-700 touch-manipulation active:scale-95 pwa-menu-button flex-shrink-0';
|
: 'p-1.5 text-muted-foreground hover:text-foreground rounded-lg hover:bg-accent/60 touch-manipulation active:scale-95 pwa-menu-button flex-shrink-0';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Settings, Sparkles } from 'lucide-react';
|
import { Settings, Sparkles, PanelLeftOpen } from 'lucide-react';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
|
|
||||||
type SidebarCollapsedProps = {
|
type SidebarCollapsedProps = {
|
||||||
@@ -17,41 +17,39 @@ export default function SidebarCollapsed({
|
|||||||
t,
|
t,
|
||||||
}: SidebarCollapsedProps) {
|
}: SidebarCollapsedProps) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col items-center py-4 gap-4 bg-card">
|
<div className="h-full flex flex-col items-center py-3 gap-1 bg-background/80 backdrop-blur-sm w-12">
|
||||||
|
{/* Expand button with brand logo */}
|
||||||
<button
|
<button
|
||||||
onClick={onExpand}
|
onClick={onExpand}
|
||||||
className="p-2 hover:bg-accent rounded-md transition-colors duration-200 group"
|
className="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-accent/80 transition-colors group"
|
||||||
aria-label={t('common:versionUpdate.ariaLabels.showSidebar')}
|
aria-label={t('common:versionUpdate.ariaLabels.showSidebar')}
|
||||||
title={t('common:versionUpdate.ariaLabels.showSidebar')}
|
title={t('common:versionUpdate.ariaLabels.showSidebar')}
|
||||||
>
|
>
|
||||||
<svg
|
<PanelLeftOpen className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||||
className="w-5 h-5 text-foreground group-hover:scale-110 transition-transform"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div className="nav-divider w-6 my-1" />
|
||||||
|
|
||||||
|
{/* Settings */}
|
||||||
<button
|
<button
|
||||||
onClick={onShowSettings}
|
onClick={onShowSettings}
|
||||||
className="p-2 hover:bg-accent rounded-md transition-colors duration-200"
|
className="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-accent/80 transition-colors group"
|
||||||
aria-label={t('actions.settings')}
|
aria-label={t('actions.settings')}
|
||||||
title={t('actions.settings')}
|
title={t('actions.settings')}
|
||||||
>
|
>
|
||||||
<Settings className="w-5 h-5 text-muted-foreground hover:text-foreground transition-colors" />
|
<Settings className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Update indicator */}
|
||||||
{updateAvailable && (
|
{updateAvailable && (
|
||||||
<button
|
<button
|
||||||
onClick={onShowVersionModal}
|
onClick={onShowVersionModal}
|
||||||
className="relative p-2 hover:bg-accent rounded-md transition-colors duration-200"
|
className="relative w-8 h-8 rounded-lg flex items-center justify-center hover:bg-accent/80 transition-colors"
|
||||||
aria-label={t('common:versionUpdate.ariaLabels.updateAvailable')}
|
aria-label={t('common:versionUpdate.ariaLabels.updateAvailable')}
|
||||||
title={t('common:versionUpdate.ariaLabels.updateAvailable')}
|
title={t('common:versionUpdate.ariaLabels.updateAvailable')}
|
||||||
>
|
>
|
||||||
<Sparkles className="w-5 h-5 text-blue-500" />
|
<Sparkles className="w-4 h-4 text-blue-500" />
|
||||||
<span className="absolute top-1 right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
<span className="absolute top-1.5 right-1.5 w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default function SidebarContent({
|
|||||||
}: SidebarContentProps) {
|
}: SidebarContentProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-full flex flex-col bg-card md:select-none md:w-80"
|
className="h-full flex flex-col bg-background/80 backdrop-blur-sm md:select-none md:w-72"
|
||||||
style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
|
style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
|
||||||
>
|
>
|
||||||
<SidebarHeader
|
<SidebarHeader
|
||||||
@@ -67,7 +67,7 @@ export default function SidebarContent({
|
|||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ScrollArea className="flex-1 md:px-2 md:py-3 overflow-y-auto overscroll-contain">
|
<ScrollArea className="flex-1 md:px-1.5 md:py-2 overflow-y-auto overscroll-contain">
|
||||||
<SidebarProjectList {...projectListProps} />
|
<SidebarProjectList {...projectListProps} />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Settings } from 'lucide-react';
|
import { Settings, ArrowUpCircle } from 'lucide-react';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import type { ReleaseInfo } from '../../../../types/sharedTypes';
|
import type { ReleaseInfo } from '../../../../types/sharedTypes';
|
||||||
import { Button } from '../../../ui/button';
|
|
||||||
|
|
||||||
type SidebarFooterProps = {
|
type SidebarFooterProps = {
|
||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
@@ -21,74 +20,81 @@ export default function SidebarFooter({
|
|||||||
t,
|
t,
|
||||||
}: SidebarFooterProps) {
|
}: SidebarFooterProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex-shrink-0">
|
||||||
|
{/* Update banner */}
|
||||||
{updateAvailable && (
|
{updateAvailable && (
|
||||||
<div className="md:p-2 border-t border-border/50 flex-shrink-0">
|
<>
|
||||||
<div className="hidden md:block">
|
<div className="nav-divider" />
|
||||||
<Button
|
{/* Desktop update */}
|
||||||
variant="ghost"
|
<div className="hidden md:block px-2 py-1.5">
|
||||||
className="w-full justify-start gap-3 p-3 h-auto font-normal text-left hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors duration-200 border border-blue-200 dark:border-blue-700 rounded-lg mb-2"
|
<button
|
||||||
|
className="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-left hover:bg-blue-50/80 dark:hover:bg-blue-900/15 transition-colors group"
|
||||||
onClick={onShowVersionModal}
|
onClick={onShowVersionModal}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative flex-shrink-0">
|
||||||
<svg className="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<ArrowUpCircle className="w-4 h-4 text-blue-500 dark:text-blue-400" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
|
<span className="absolute -top-0.5 -right-0.5 w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse" />
|
||||||
</svg>
|
|
||||||
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">
|
<span className="text-xs font-medium text-blue-600 dark:text-blue-300 truncate block">
|
||||||
{releaseInfo?.title || `Version ${latestVersion}`}
|
{releaseInfo?.title || `v${latestVersion}`}
|
||||||
</div>
|
</span>
|
||||||
<div className="text-xs text-blue-600 dark:text-blue-400">{t('version.updateAvailable')}</div>
|
<span className="text-[10px] text-blue-500/70 dark:text-blue-400/60">
|
||||||
</div>
|
{t('version.updateAvailable')}
|
||||||
</Button>
|
</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="md:hidden p-3 pb-2">
|
|
||||||
<button
|
|
||||||
className="w-full h-12 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700 rounded-xl flex items-center justify-start gap-3 px-4 active:scale-[0.98] transition-all duration-150"
|
|
||||||
onClick={onShowVersionModal}
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
|
|
||||||
</svg>
|
|
||||||
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1 text-left">
|
|
||||||
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">
|
|
||||||
{releaseInfo?.title || `Version ${latestVersion}`}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-blue-600 dark:text-blue-400">{t('version.updateAvailable')}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Mobile update */}
|
||||||
|
<div className="md:hidden px-3 py-2">
|
||||||
|
<button
|
||||||
|
className="w-full h-11 bg-blue-50/80 dark:bg-blue-900/15 border border-blue-200/60 dark:border-blue-700/40 rounded-xl flex items-center gap-3 px-3.5 active:scale-[0.98] transition-all"
|
||||||
|
onClick={onShowVersionModal}
|
||||||
|
>
|
||||||
|
<div className="relative flex-shrink-0">
|
||||||
|
<ArrowUpCircle className="w-4.5 h-4.5 text-blue-500 dark:text-blue-400" />
|
||||||
|
<span className="absolute -top-0.5 -right-0.5 w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1 text-left">
|
||||||
|
<span className="text-sm font-medium text-blue-600 dark:text-blue-300 truncate block">
|
||||||
|
{releaseInfo?.title || `v${latestVersion}`}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-blue-500/70 dark:text-blue-400/60">
|
||||||
|
{t('version.updateAvailable')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="md:p-2 md:border-t md:border-border flex-shrink-0">
|
{/* Settings */}
|
||||||
<div className="md:hidden p-4 pb-20 border-t border-border/50">
|
<div className="nav-divider" />
|
||||||
<button
|
|
||||||
className="w-full h-14 bg-muted/50 hover:bg-muted/70 rounded-2xl flex items-center justify-start gap-4 px-4 active:scale-[0.98] transition-all duration-150"
|
|
||||||
onClick={onShowSettings}
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 rounded-2xl bg-background/80 flex items-center justify-center">
|
|
||||||
<Settings className="w-5 h-5 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<span className="text-lg font-medium text-foreground">{t('actions.settings')}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
{/* Desktop settings */}
|
||||||
variant="ghost"
|
<div className="hidden md:block px-2 py-1.5">
|
||||||
className="hidden md:flex w-full justify-start gap-2 p-2 h-auto font-normal text-muted-foreground hover:text-foreground hover:bg-accent transition-colors duration-200"
|
<button
|
||||||
|
className="w-full flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent/60 transition-colors"
|
||||||
onClick={onShowSettings}
|
onClick={onShowSettings}
|
||||||
>
|
>
|
||||||
<Settings className="w-3 h-3" />
|
<Settings className="w-3.5 h-3.5" />
|
||||||
<span className="text-xs">{t('actions.settings')}</span>
|
<span className="text-xs">{t('actions.settings')}</span>
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
|
{/* Mobile settings */}
|
||||||
|
<div className="md:hidden p-3 pb-20">
|
||||||
|
<button
|
||||||
|
className="w-full h-12 bg-muted/40 hover:bg-muted/60 rounded-xl flex items-center gap-3.5 px-4 active:scale-[0.98] transition-all"
|
||||||
|
onClick={onShowSettings}
|
||||||
|
>
|
||||||
|
<div className="w-8 h-8 rounded-xl bg-background/80 flex items-center justify-center">
|
||||||
|
<Settings className="w-4.5 h-4.5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<span className="text-base font-medium text-foreground">{t('actions.settings')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FolderPlus, MessageSquare, RefreshCw, Search, X } from 'lucide-react';
|
import { FolderPlus, Plus, RefreshCw, Search, X, PanelLeftClose } from 'lucide-react';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import { Button } from '../../../ui/button';
|
import { Button } from '../../../ui/button';
|
||||||
import { Input } from '../../../ui/input';
|
import { Input } from '../../../ui/input';
|
||||||
@@ -33,155 +33,159 @@ export default function SidebarHeader({
|
|||||||
onCollapseSidebar,
|
onCollapseSidebar,
|
||||||
t,
|
t,
|
||||||
}: SidebarHeaderProps) {
|
}: SidebarHeaderProps) {
|
||||||
|
const LogoBlock = () => (
|
||||||
|
<div className="flex items-center gap-2.5 min-w-0">
|
||||||
|
<div className="w-7 h-7 bg-primary/90 rounded-lg flex items-center justify-center shadow-sm flex-shrink-0">
|
||||||
|
<svg className="w-3.5 h-3.5 text-primary-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-sm font-semibold text-foreground tracking-tight truncate">{t('app.title')}</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex-shrink-0">
|
||||||
|
{/* Desktop header */}
|
||||||
<div
|
<div
|
||||||
className="md:p-4 md:border-b md:border-border"
|
className="hidden md:block px-3 pt-3 pb-2"
|
||||||
style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
|
style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
|
||||||
>
|
>
|
||||||
<div className="hidden md:flex items-center justify-between">
|
<div className="flex items-center justify-between gap-2">
|
||||||
{IS_PLATFORM ? (
|
{IS_PLATFORM ? (
|
||||||
<a
|
<a
|
||||||
href="https://cloudcli.ai/dashboard"
|
href="https://cloudcli.ai/dashboard"
|
||||||
className="flex items-center gap-3 hover:opacity-80 transition-opacity group"
|
className="flex items-center gap-2.5 min-w-0 hover:opacity-80 transition-opacity"
|
||||||
title={t('tooltips.viewEnvironments')}
|
title={t('tooltips.viewEnvironments')}
|
||||||
>
|
>
|
||||||
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center shadow-sm group-hover:shadow-md transition-shadow">
|
<LogoBlock />
|
||||||
<MessageSquare className="w-4 h-4 text-primary-foreground" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-lg font-bold text-foreground">{t('app.title')}</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">{t('app.subtitle')}</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-3">
|
<LogoBlock />
|
||||||
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center shadow-sm">
|
|
||||||
<MessageSquare className="w-4 h-4 text-primary-foreground" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-lg font-bold text-foreground">{t('app.title')}</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">{t('app.subtitle')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<div className="flex items-center gap-0.5 flex-shrink-0">
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 w-8 px-0 hover:bg-accent transition-colors duration-200"
|
|
||||||
onClick={onCollapseSidebar}
|
|
||||||
title={t('tooltips.hideSidebar')}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
||||||
</svg>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="md:hidden p-3 border-b border-border"
|
|
||||||
style={isPWA && isMobile ? { paddingTop: '16px' } : {}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{IS_PLATFORM ? (
|
|
||||||
<a
|
|
||||||
href="https://cloudcli.ai/dashboard"
|
|
||||||
className="flex items-center gap-3 active:opacity-70 transition-opacity"
|
|
||||||
title={t('tooltips.viewEnvironments')}
|
|
||||||
>
|
|
||||||
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
|
||||||
<MessageSquare className="w-4 h-4 text-primary-foreground" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-lg font-semibold text-foreground">{t('app.title')}</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">{t('projects.title')}</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
|
||||||
<MessageSquare className="w-4 h-4 text-primary-foreground" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-lg font-semibold text-foreground">{t('app.title')}</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">{t('projects.title')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
className="w-8 h-8 rounded-md bg-background border border-border flex items-center justify-center active:scale-95 transition-all duration-150"
|
|
||||||
onClick={onRefresh}
|
|
||||||
disabled={isRefreshing}
|
|
||||||
>
|
|
||||||
<RefreshCw className={`w-4 h-4 text-foreground ${isRefreshing ? 'animate-spin' : ''}`} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="w-8 h-8 rounded-md bg-primary text-primary-foreground flex items-center justify-center active:scale-95 transition-all duration-150"
|
|
||||||
onClick={onCreateProject}
|
|
||||||
>
|
|
||||||
<FolderPlus className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isLoading && !isMobile && (
|
|
||||||
<div className="px-3 md:px-4 py-2 border-b border-border">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex-1 h-8 text-xs bg-primary hover:bg-primary/90 transition-all duration-200"
|
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground hover:bg-accent/80 rounded-lg"
|
||||||
onClick={onCreateProject}
|
|
||||||
title={t('tooltips.createProject')}
|
|
||||||
>
|
|
||||||
<FolderPlus className="w-3.5 h-3.5 mr-1.5" />
|
|
||||||
{t('projects.newProject')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 w-8 px-0 hover:bg-accent transition-colors duration-200 group"
|
|
||||||
onClick={onRefresh}
|
onClick={onRefresh}
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing}
|
||||||
title={t('tooltips.refresh')}
|
title={t('tooltips.refresh')}
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`w-3.5 h-3.5 ${
|
className={`w-3.5 h-3.5 ${
|
||||||
isRefreshing ? 'animate-spin' : 'group-hover:rotate-180 transition-transform duration-300'
|
isRefreshing ? 'animate-spin' : ''
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground hover:bg-accent/80 rounded-lg"
|
||||||
|
onClick={onCreateProject}
|
||||||
|
title={t('tooltips.createProject')}
|
||||||
|
>
|
||||||
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground hover:bg-accent/80 rounded-lg"
|
||||||
|
onClick={onCollapseSidebar}
|
||||||
|
title={t('tooltips.hideSidebar')}
|
||||||
|
>
|
||||||
|
<PanelLeftClose className="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{projectsCount > 0 && !isLoading && (
|
{/* Search bar */}
|
||||||
<div className="px-3 md:px-4 py-2 border-b border-border">
|
{projectsCount > 0 && !isLoading && (
|
||||||
<div className="relative">
|
<div className="relative mt-2.5">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground/50 pointer-events-none" />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('projects.searchPlaceholder')}
|
placeholder={t('projects.searchPlaceholder')}
|
||||||
value={searchFilter}
|
value={searchFilter}
|
||||||
onChange={(event) => onSearchFilterChange(event.target.value)}
|
onChange={(event) => onSearchFilterChange(event.target.value)}
|
||||||
className="pl-9 h-9 text-sm bg-muted/50 border-0 focus:bg-background focus:ring-1 focus:ring-primary/20"
|
className="nav-search-input pl-9 pr-8 h-9 text-xs rounded-xl border-0 placeholder:text-muted-foreground/40 focus-visible:ring-0 focus-visible:ring-offset-0 transition-all duration-200"
|
||||||
/>
|
/>
|
||||||
{searchFilter && (
|
{searchFilter && (
|
||||||
<button
|
<button
|
||||||
onClick={onClearSearchFilter}
|
onClick={onClearSearchFilter}
|
||||||
className="absolute right-2 top-1/2 transform -translate-y-1/2 p-1 hover:bg-accent rounded"
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 p-0.5 hover:bg-accent rounded-md"
|
||||||
>
|
>
|
||||||
<X className="w-3 h-3 text-muted-foreground" />
|
<X className="w-3 h-3 text-muted-foreground" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop divider */}
|
||||||
|
<div className="hidden md:block nav-divider" />
|
||||||
|
|
||||||
|
{/* Mobile header */}
|
||||||
|
<div
|
||||||
|
className="md:hidden p-3 pb-2"
|
||||||
|
style={isPWA && isMobile ? { paddingTop: '16px' } : {}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
{IS_PLATFORM ? (
|
||||||
|
<a
|
||||||
|
href="https://cloudcli.ai/dashboard"
|
||||||
|
className="flex items-center gap-2.5 active:opacity-70 transition-opacity min-w-0"
|
||||||
|
title={t('tooltips.viewEnvironments')}
|
||||||
|
>
|
||||||
|
<LogoBlock />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<LogoBlock />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-1.5 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center active:scale-95 transition-all"
|
||||||
|
onClick={onRefresh}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 text-muted-foreground ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-8 h-8 rounded-lg bg-primary/90 text-primary-foreground flex items-center justify-center active:scale-95 transition-all"
|
||||||
|
onClick={onCreateProject}
|
||||||
|
>
|
||||||
|
<FolderPlus className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</>
|
{/* Mobile search */}
|
||||||
|
{projectsCount > 0 && !isLoading && (
|
||||||
|
<div className="relative mt-2.5">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground/50 pointer-events-none" />
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={t('projects.searchPlaceholder')}
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={(event) => onSearchFilterChange(event.target.value)}
|
||||||
|
className="nav-search-input pl-10 pr-9 h-10 text-sm rounded-xl border-0 placeholder:text-muted-foreground/40 focus-visible:ring-0 focus-visible:ring-offset-0 transition-all duration-200"
|
||||||
|
/>
|
||||||
|
{searchFilter && (
|
||||||
|
<button
|
||||||
|
onClick={onClearSearchFilter}
|
||||||
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 p-1 hover:bg-accent rounded-md"
|
||||||
|
>
|
||||||
|
<X className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile divider */}
|
||||||
|
<div className="md:hidden nav-divider" />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,12 +84,16 @@ export default function SidebarModals({
|
|||||||
document.body,
|
document.body,
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TypedSettings
|
{showSettings &&
|
||||||
isOpen={showSettings}
|
ReactDOM.createPortal(
|
||||||
onClose={onCloseSettings}
|
<TypedSettings
|
||||||
projects={settingsProjects}
|
isOpen={showSettings}
|
||||||
initialTab={settingsInitialTab}
|
onClose={onCloseSettings}
|
||||||
/>
|
projects={settingsProjects}
|
||||||
|
initialTab={settingsInitialTab}
|
||||||
|
/>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
|
||||||
{deleteConfirmation &&
|
{deleteConfirmation &&
|
||||||
ReactDOM.createPortal(
|
ReactDOM.createPortal(
|
||||||
|
|||||||
@@ -43,7 +43,19 @@
|
|||||||
--input: 214.3 31.8% 91.4%;
|
--input: 214.3 31.8% 91.4%;
|
||||||
--ring: 221.2 83.2% 53.3%;
|
--ring: 221.2 83.2% 53.3%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
|
/* Nav design tokens */
|
||||||
|
--nav-glass-bg: 0 0% 100% / 0.7;
|
||||||
|
--nav-glass-blur: 20px;
|
||||||
|
--nav-glass-saturate: 1.8;
|
||||||
|
--nav-tab-glow: 221.2 83.2% 53.3% / 0.18;
|
||||||
|
--nav-tab-ring: 221.2 83.2% 53.3% / 0.10;
|
||||||
|
--nav-float-shadow: 0 0% 0% / 0.06;
|
||||||
|
--nav-float-ring: 214.3 31.8% 91.4% / 0.5;
|
||||||
|
--nav-divider-color: 214.3 31.8% 91.4% / 0.5;
|
||||||
|
--nav-input-bg: 210 40% 96.1% / 0.5;
|
||||||
|
--nav-input-focus-ring: 221.2 83.2% 53.3% / 0.22;
|
||||||
|
|
||||||
/* Safe area CSS variables */
|
/* Safe area CSS variables */
|
||||||
--safe-area-inset-top: env(safe-area-inset-top);
|
--safe-area-inset-top: env(safe-area-inset-top);
|
||||||
--safe-area-inset-right: env(safe-area-inset-right);
|
--safe-area-inset-right: env(safe-area-inset-right);
|
||||||
@@ -51,9 +63,10 @@
|
|||||||
--safe-area-inset-left: env(safe-area-inset-left);
|
--safe-area-inset-left: env(safe-area-inset-left);
|
||||||
|
|
||||||
/* Mobile navigation dimensions - Single source of truth */
|
/* Mobile navigation dimensions - Single source of truth */
|
||||||
--mobile-nav-height: 60px;
|
/* Floating nav: ~52px bar + 8px bottom margin + 12px px-3 top spacing */
|
||||||
--mobile-nav-padding: 12px;
|
--mobile-nav-height: 52px;
|
||||||
--mobile-nav-total: calc(var(--mobile-nav-height) + env(safe-area-inset-bottom, 0px));
|
--mobile-nav-padding: 20px;
|
||||||
|
--mobile-nav-total: calc(var(--mobile-nav-height) + var(--mobile-nav-padding) + env(safe-area-inset-bottom, 0px));
|
||||||
|
|
||||||
/* Header safe area dimensions */
|
/* Header safe area dimensions */
|
||||||
--header-safe-area-top: env(safe-area-inset-top, 0px);
|
--header-safe-area-top: env(safe-area-inset-top, 0px);
|
||||||
@@ -91,6 +104,18 @@
|
|||||||
--border: 217.2 32.6% 17.5%;
|
--border: 217.2 32.6% 17.5%;
|
||||||
--input: 220 13% 46%;
|
--input: 220 13% 46%;
|
||||||
--ring: 217.2 91.2% 59.8%;
|
--ring: 217.2 91.2% 59.8%;
|
||||||
|
|
||||||
|
/* Nav design tokens — dark overrides */
|
||||||
|
--nav-glass-bg: 217.2 91.2% 8% / 0.55;
|
||||||
|
--nav-glass-blur: 24px;
|
||||||
|
--nav-glass-saturate: 1.6;
|
||||||
|
--nav-tab-glow: 217.2 91.2% 59.8% / 0.25;
|
||||||
|
--nav-tab-ring: 217.2 91.2% 59.8% / 0.15;
|
||||||
|
--nav-float-shadow: 0 0% 0% / 0.35;
|
||||||
|
--nav-float-ring: 217.2 32.6% 17.5% / 0.3;
|
||||||
|
--nav-divider-color: 217.2 32.6% 17.5% / 0.5;
|
||||||
|
--nav-input-bg: 217.2 32.6% 17.5% / 0.5;
|
||||||
|
--nav-input-focus-ring: 217.2 91.2% 59.8% / 0.25;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,6 +265,42 @@
|
|||||||
transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1),
|
transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
opacity 300ms ease-in-out;
|
opacity 300ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Nav glass surface — uses theme tokens */
|
||||||
|
.nav-glass {
|
||||||
|
background: hsl(var(--nav-glass-bg));
|
||||||
|
backdrop-filter: blur(var(--nav-glass-blur)) saturate(var(--nav-glass-saturate));
|
||||||
|
-webkit-backdrop-filter: blur(var(--nav-glass-blur)) saturate(var(--nav-glass-saturate));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nav tab active pill glow — uses theme tokens */
|
||||||
|
.nav-tab-active {
|
||||||
|
box-shadow: 0 1px 8px hsl(var(--nav-tab-glow)),
|
||||||
|
0 0 0 1px hsl(var(--nav-tab-ring));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating mobile nav bar — uses theme tokens */
|
||||||
|
.mobile-nav-float {
|
||||||
|
box-shadow: 0 -1px 20px hsl(var(--nav-float-shadow)),
|
||||||
|
0 0 0 1px hsl(var(--nav-float-ring));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle sidebar divider — uses theme tokens */
|
||||||
|
.nav-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, hsl(var(--nav-divider-color)) 20%, hsl(var(--nav-divider-color)) 80%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nav search input surface — uses theme tokens */
|
||||||
|
.nav-search-input {
|
||||||
|
background: hsl(var(--nav-input-bg));
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-search-input:focus-within {
|
||||||
|
background: hsl(var(--background));
|
||||||
|
box-shadow: 0 0 0 2px hsl(var(--nav-input-focus-ring));
|
||||||
|
}
|
||||||
|
|
||||||
/* Modal and dropdown transitions */
|
/* Modal and dropdown transitions */
|
||||||
.modal-transition {
|
.modal-transition {
|
||||||
|
|||||||
Reference in New Issue
Block a user