mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-12 13:19:43 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
533d589132 | ||
|
|
d1f310161f | ||
|
|
b853e8cda1 | ||
|
|
1610de1f22 | ||
|
|
cbb18fb010 | ||
|
|
d8d754274a | ||
|
|
eb12aef641 | ||
|
|
5e574dbdec | ||
|
|
af9e9eec02 | ||
|
|
8c3ee770c3 | ||
|
|
58108c083c | ||
|
|
af0ad6b4b6 | ||
|
|
36d9f47e29 | ||
|
|
1f25f1e79b | ||
|
|
d7ed1de1cb | ||
|
|
9be54233d0 | ||
|
|
c8bcad71e7 | ||
|
|
a3f504aed2 | ||
|
|
f766ac1517 | ||
|
|
680d8f6fb1 | ||
|
|
1820f3bfa8 | ||
|
|
376e055443 | ||
|
|
f4becdc218 | ||
|
|
70b421e5a8 | ||
|
|
771f7e4d7b | ||
|
|
cab13a1534 | ||
|
|
1eba594418 | ||
|
|
ed5374a1bd | ||
|
|
573a04d2e5 | ||
|
|
79981693f3 | ||
|
|
fb1117a999 | ||
|
|
40b8737732 | ||
|
|
15b95c4d08 | ||
|
|
3ff1db0331 | ||
|
|
dab0068d8f | ||
|
|
11b2ff588a | ||
|
|
975e4b04a6 |
21
.release-it.json
Normal file
21
.release-it.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"git": {
|
||||||
|
"commitMessage": "Release ${version}",
|
||||||
|
"tagName": "v${version}",
|
||||||
|
"changelog": "git log --pretty=format:\"* %s (%h)\" ${from}...${to}"
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"publish": true
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"release": true,
|
||||||
|
"releaseName": "Claude Code UI v${version}",
|
||||||
|
"releaseNotes": {
|
||||||
|
"commit": "* ${commit.subject} (${sha}){ - thanks @${author.login}!}",
|
||||||
|
"excludeMatches": ["viper151"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hooks": {
|
||||||
|
"before:init": ["npm run build"]
|
||||||
|
}
|
||||||
|
}
|
||||||
12
README.md
12
README.md
@@ -59,7 +59,17 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla
|
|||||||
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and configured, and/or
|
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and configured, and/or
|
||||||
- [Cursor CLI](https://docs.cursor.com/en/cli/overview) installed and configured
|
- [Cursor CLI](https://docs.cursor.com/en/cli/overview) installed and configured
|
||||||
|
|
||||||
### Installation
|
### One-click Operation (Recommended)
|
||||||
|
|
||||||
|
No installation required, direct operation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @siteboon/claude-code-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
Your default browser will automatically open the Claude Code UI interface.
|
||||||
|
|
||||||
|
### Local Development Installation
|
||||||
|
|
||||||
1. **Clone the repository:**
|
1. **Clone the repository:**
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
<title>Claude Code UI</title>
|
<title>Claude Code UI</title>
|
||||||
|
|
||||||
<!-- PWA Manifest -->
|
<!-- PWA Manifest -->
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<!-- iOS Safari PWA Meta Tags -->
|
<!-- iOS Safari PWA Meta Tags -->
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Claude UI" />
|
<meta name="apple-mobile-web-app-title" content="Claude UI" />
|
||||||
|
|
||||||
<!-- iOS Safari Icons -->
|
<!-- iOS Safari Icons -->
|
||||||
|
|||||||
2285
package-lock.json
generated
2285
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -1,23 +1,40 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-code-ui",
|
"name": "@siteboon/claude-code-ui",
|
||||||
"version": "1.8.1",
|
"version": "1.8.10",
|
||||||
"description": "A web-based UI for Claude Code CLI",
|
"description": "A web-based UI for Claude Code CLI",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "server/index.js",
|
"main": "server/index.js",
|
||||||
|
"bin": {
|
||||||
|
"claude-code-ui": "server/index.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"server/",
|
||||||
|
"dist/",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"homepage": "https://claudecodeui.siteboon.ai",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/siteboon/claudecodeui.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/siteboon/claudecodeui/issues"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently --kill-others \"npm run server\" \"npm run client\"",
|
"dev": "concurrently --kill-others \"npm run server\" \"npm run client\"",
|
||||||
"server": "node server/index.js",
|
"server": "node server/index.js",
|
||||||
"client": "vite --host",
|
"client": "vite --host",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"start": "npm run build && npm run server"
|
"start": "npm run build && npm run server",
|
||||||
|
"release": "release-it"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude",
|
"claude coode",
|
||||||
"ai",
|
"ai",
|
||||||
"code",
|
"anthropic",
|
||||||
"ui",
|
"ui",
|
||||||
"assistant"
|
"mobile"
|
||||||
],
|
],
|
||||||
"author": "Claude Code UI Contributors",
|
"author": "Claude Code UI Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -63,10 +80,12 @@
|
|||||||
"@types/react": "^18.2.43",
|
"@types/react": "^18.2.43",
|
||||||
"@types/react-dom": "^18.2.17",
|
"@types/react-dom": "^18.2.17",
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
|
"auto-changelog": "^2.5.0",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"node-gyp": "^10.0.0",
|
"node-gyp": "^10.0.0",
|
||||||
"postcss": "^8.4.32",
|
"postcss": "^8.4.32",
|
||||||
|
"release-it": "^19.0.5",
|
||||||
"sharp": "^0.34.2",
|
"sharp": "^0.34.2",
|
||||||
"tailwindcss": "^3.4.0",
|
"tailwindcss": "^3.4.0",
|
||||||
"vite": "^7.0.4"
|
"vite": "^7.0.4"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
// Load environment variables from .env file
|
// Load environment variables from .env file
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -506,7 +507,7 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
|
|||||||
return res.status(404).json({ error: `Project path not found: ${actualPath}` });
|
return res.status(404).json({ error: `Project path not found: ${actualPath}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = await getFileTree(actualPath, 3, 0, true);
|
const files = await getFileTree(actualPath, 10, 0, true);
|
||||||
const hiddenFiles = files.filter(f => f.name.startsWith('.'));
|
const hiddenFiles = files.filter(f => f.name.startsWith('.'));
|
||||||
res.json(files);
|
res.json(files);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -899,23 +899,17 @@ async function addProjectManually(projectPath, displayName = null) {
|
|||||||
// Generate project name (encode path for use as directory name)
|
// Generate project name (encode path for use as directory name)
|
||||||
const projectName = absolutePath.replace(/\//g, '-');
|
const projectName = absolutePath.replace(/\//g, '-');
|
||||||
|
|
||||||
// Check if project already exists in config or as a folder
|
// Check if project already exists in config
|
||||||
const config = await loadProjectConfig();
|
const config = await loadProjectConfig();
|
||||||
const projectDir = path.join(process.env.HOME, '.claude', 'projects', projectName);
|
const projectDir = path.join(process.env.HOME, '.claude', 'projects', projectName);
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.access(projectDir);
|
|
||||||
throw new Error(`Project already exists for path: ${absolutePath}`);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== 'ENOENT') {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config[projectName]) {
|
if (config[projectName]) {
|
||||||
throw new Error(`Project already configured for path: ${absolutePath}`);
|
throw new Error(`Project already configured for path: ${absolutePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow adding projects even if the directory exists - this enables tracking
|
||||||
|
// existing Claude Code or Cursor projects in the UI
|
||||||
|
|
||||||
// Add to config as manually added project
|
// Add to config as manually added project
|
||||||
config[projectName] = {
|
config[projectName] = {
|
||||||
manuallyAdded: true,
|
manuallyAdded: true,
|
||||||
|
|||||||
38
src/App.jsx
38
src/App.jsx
@@ -78,6 +78,37 @@ function AppContent() {
|
|||||||
|
|
||||||
const { ws, sendMessage, messages } = useWebSocketContext();
|
const { ws, sendMessage, messages } = useWebSocketContext();
|
||||||
|
|
||||||
|
// Detect if running as PWA
|
||||||
|
const [isPWA, setIsPWA] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if running in standalone mode (PWA)
|
||||||
|
const checkPWA = () => {
|
||||||
|
const isStandalone = window.matchMedia('(display-mode: standalone)').matches ||
|
||||||
|
window.navigator.standalone ||
|
||||||
|
document.referrer.includes('android-app://');
|
||||||
|
setIsPWA(isStandalone);
|
||||||
|
|
||||||
|
// Add class to html and body for CSS targeting
|
||||||
|
if (isStandalone) {
|
||||||
|
document.documentElement.classList.add('pwa-mode');
|
||||||
|
document.body.classList.add('pwa-mode');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('pwa-mode');
|
||||||
|
document.body.classList.remove('pwa-mode');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkPWA();
|
||||||
|
|
||||||
|
// Listen for changes
|
||||||
|
window.matchMedia('(display-mode: standalone)').addEventListener('change', checkPWA);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.matchMedia('(display-mode: standalone)').removeEventListener('change', checkPWA);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkMobile = () => {
|
const checkMobile = () => {
|
||||||
setIsMobile(window.innerWidth < 768);
|
setIsMobile(window.innerWidth < 768);
|
||||||
@@ -561,6 +592,8 @@ function AppContent() {
|
|||||||
latestVersion={latestVersion}
|
latestVersion={latestVersion}
|
||||||
currentVersion={currentVersion}
|
currentVersion={currentVersion}
|
||||||
onShowVersionModal={() => setShowVersionModal(true)}
|
onShowVersionModal={() => setShowVersionModal(true)}
|
||||||
|
isPWA={isPWA}
|
||||||
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -607,13 +640,15 @@ function AppContent() {
|
|||||||
latestVersion={latestVersion}
|
latestVersion={latestVersion}
|
||||||
currentVersion={currentVersion}
|
currentVersion={currentVersion}
|
||||||
onShowVersionModal={() => setShowVersionModal(true)}
|
onShowVersionModal={() => setShowVersionModal(true)}
|
||||||
|
isPWA={isPWA}
|
||||||
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main Content Area - Flexible */}
|
{/* Main Content Area - Flexible */}
|
||||||
<div className="flex-1 flex flex-col min-w-0">
|
<div className={`flex-1 flex flex-col min-w-0 ${isMobile && !isInputFocused ? 'pb-16' : ''}`}>
|
||||||
<MainContent
|
<MainContent
|
||||||
selectedProject={selectedProject}
|
selectedProject={selectedProject}
|
||||||
selectedSession={selectedSession}
|
selectedSession={selectedSession}
|
||||||
@@ -623,6 +658,7 @@ function AppContent() {
|
|||||||
sendMessage={sendMessage}
|
sendMessage={sendMessage}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
isPWA={isPWA}
|
||||||
onMenuClick={() => setSidebarOpen(true)}
|
onMenuClick={() => setSidebarOpen(true)}
|
||||||
isLoading={isLoadingProjects}
|
isLoading={isLoadingProjects}
|
||||||
onInputFocusChange={setIsInputFocused}
|
onInputFocusChange={setIsInputFocused}
|
||||||
|
|||||||
@@ -158,8 +158,10 @@ const safeLocalStorage = {
|
|||||||
// Memoized message component to prevent unnecessary re-renders
|
// Memoized message component to prevent unnecessary re-renders
|
||||||
const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, autoExpandTools, showRawParameters }) => {
|
const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, autoExpandTools, showRawParameters }) => {
|
||||||
const isGrouped = prevMessage && prevMessage.type === message.type &&
|
const isGrouped = prevMessage && prevMessage.type === message.type &&
|
||||||
prevMessage.type === 'assistant' &&
|
((prevMessage.type === 'assistant') ||
|
||||||
!prevMessage.isToolUse && !message.isToolUse;
|
(prevMessage.type === 'user') ||
|
||||||
|
(prevMessage.type === 'tool') ||
|
||||||
|
(prevMessage.type === 'error'));
|
||||||
const messageRef = React.useRef(null);
|
const messageRef = React.useRef(null);
|
||||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -3217,7 +3219,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
|
|
||||||
{/* Input Area - Fixed Bottom */}
|
{/* Input Area - Fixed Bottom */}
|
||||||
<div className={`p-2 sm:p-4 md:p-4 flex-shrink-0 ${
|
<div className={`p-2 sm:p-4 md:p-4 flex-shrink-0 ${
|
||||||
isInputFocused ? 'pb-2 sm:pb-4 md:pb-6' : 'pb-16 sm:pb-4 md:pb-6'
|
isInputFocused ? 'pb-2 sm:pb-4 md:pb-6' : 'pb-2 sm:pb-4 md:pb-6'
|
||||||
}`}>
|
}`}>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|||||||
41
src/components/DiffViewer.jsx
Normal file
41
src/components/DiffViewer.jsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function DiffViewer({ diff, fileName, isMobile, wrapText }) {
|
||||||
|
if (!diff) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 text-center text-gray-500 dark:text-gray-400 text-sm">
|
||||||
|
No diff available
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderDiffLine = (line, index) => {
|
||||||
|
const isAddition = line.startsWith('+') && !line.startsWith('+++');
|
||||||
|
const isDeletion = line.startsWith('-') && !line.startsWith('---');
|
||||||
|
const isHeader = line.startsWith('@@');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`font-mono text-xs p-2 ${
|
||||||
|
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' :
|
||||||
|
isDeletion ? 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300' :
|
||||||
|
isHeader ? 'bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300' :
|
||||||
|
'text-gray-600 dark:text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{line}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="diff-viewer">
|
||||||
|
{diff.split('\n').map((line, index) => renderDiffLine(line, index))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DiffViewer;
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { ScrollArea } from './ui/scroll-area';
|
import { ScrollArea } from './ui/scroll-area';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { Folder, FolderOpen, File, FileText, FileCode, List, TableProperties, Eye } from 'lucide-react';
|
import { Input } from './ui/input';
|
||||||
|
import { Folder, FolderOpen, File, FileText, FileCode, List, TableProperties, Eye, Search, X } 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';
|
||||||
@@ -14,6 +15,8 @@ function FileTree({ selectedProject }) {
|
|||||||
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'); // 'simple', 'detailed', 'compact'
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [filteredFiles, setFilteredFiles] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedProject) {
|
if (selectedProject) {
|
||||||
@@ -29,6 +32,51 @@ function FileTree({ selectedProject }) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Filter files based on search query
|
||||||
|
useEffect(() => {
|
||||||
|
if (!searchQuery.trim()) {
|
||||||
|
setFilteredFiles(files);
|
||||||
|
} else {
|
||||||
|
const filtered = filterFiles(files, searchQuery.toLowerCase());
|
||||||
|
setFilteredFiles(filtered);
|
||||||
|
|
||||||
|
// Auto-expand directories that contain matches
|
||||||
|
const expandMatches = (items) => {
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.type === 'directory' && item.children && item.children.length > 0) {
|
||||||
|
setExpandedDirs(prev => new Set(prev.add(item.path)));
|
||||||
|
expandMatches(item.children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
expandMatches(filtered);
|
||||||
|
}
|
||||||
|
}, [files, searchQuery]);
|
||||||
|
|
||||||
|
// Recursively filter files and directories based on search query
|
||||||
|
const filterFiles = (items, query) => {
|
||||||
|
return items.reduce((filtered, item) => {
|
||||||
|
const matchesName = item.name.toLowerCase().includes(query);
|
||||||
|
let filteredChildren = [];
|
||||||
|
|
||||||
|
if (item.type === 'directory' && item.children) {
|
||||||
|
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) {
|
||||||
|
filtered.push({
|
||||||
|
...item,
|
||||||
|
children: filteredChildren
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
const fetchFiles = async () => {
|
const fetchFiles = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -308,42 +356,67 @@ function FileTree({ selectedProject }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col bg-card">
|
<div className="h-full flex flex-col bg-card">
|
||||||
{/* View Mode Toggle */}
|
{/* Header with Search and View Mode Toggle */}
|
||||||
<div className="p-4 border-b border-border flex items-center justify-between">
|
<div className="p-4 border-b border-border space-y-3">
|
||||||
<h3 className="text-sm font-medium text-foreground">Files</h3>
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex gap-1">
|
<h3 className="text-sm font-medium text-foreground">Files</h3>
|
||||||
<Button
|
<div className="flex gap-1">
|
||||||
variant={viewMode === 'simple' ? 'default' : 'ghost'}
|
<Button
|
||||||
size="sm"
|
variant={viewMode === 'simple' ? 'default' : 'ghost'}
|
||||||
className="h-8 w-8 p-0"
|
size="sm"
|
||||||
onClick={() => changeViewMode('simple')}
|
className="h-8 w-8 p-0"
|
||||||
title="Simple view"
|
onClick={() => changeViewMode('simple')}
|
||||||
>
|
title="Simple view"
|
||||||
<List className="w-4 h-4" />
|
>
|
||||||
</Button>
|
<List className="w-4 h-4" />
|
||||||
<Button
|
</Button>
|
||||||
variant={viewMode === 'compact' ? 'default' : 'ghost'}
|
<Button
|
||||||
size="sm"
|
variant={viewMode === 'compact' ? 'default' : 'ghost'}
|
||||||
className="h-8 w-8 p-0"
|
size="sm"
|
||||||
onClick={() => changeViewMode('compact')}
|
className="h-8 w-8 p-0"
|
||||||
title="Compact view"
|
onClick={() => changeViewMode('compact')}
|
||||||
>
|
title="Compact view"
|
||||||
<Eye className="w-4 h-4" />
|
>
|
||||||
</Button>
|
<Eye className="w-4 h-4" />
|
||||||
<Button
|
</Button>
|
||||||
variant={viewMode === 'detailed' ? 'default' : 'ghost'}
|
<Button
|
||||||
size="sm"
|
variant={viewMode === 'detailed' ? 'default' : 'ghost'}
|
||||||
className="h-8 w-8 p-0"
|
size="sm"
|
||||||
onClick={() => changeViewMode('detailed')}
|
className="h-8 w-8 p-0"
|
||||||
title="Detailed view"
|
onClick={() => changeViewMode('detailed')}
|
||||||
>
|
title="Detailed view"
|
||||||
<TableProperties className="w-4 h-4" />
|
>
|
||||||
</Button>
|
<TableProperties className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Bar */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search files and folders..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-8 pr-8 h-8 text-sm"
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-6 w-6 p-0 hover:bg-accent"
|
||||||
|
onClick={() => setSearchQuery('')}
|
||||||
|
title="Clear search"
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Column Headers for Detailed View */}
|
{/* Column Headers for Detailed View */}
|
||||||
{viewMode === 'detailed' && files.length > 0 && (
|
{viewMode === 'detailed' && filteredFiles.length > 0 && (
|
||||||
<div className="px-4 pt-2 pb-1 border-b border-border">
|
<div className="px-4 pt-2 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-2 text-xs font-medium text-muted-foreground">
|
||||||
<div className="col-span-5">Name</div>
|
<div className="col-span-5">Name</div>
|
||||||
@@ -365,11 +438,21 @@ function FileTree({ selectedProject }) {
|
|||||||
Check if the project path is accessible
|
Check if the project path is accessible
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
) : filteredFiles.length === 0 && searchQuery ? (
|
||||||
|
<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">
|
||||||
|
<Search className="w-6 h-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<h4 className="font-medium text-foreground mb-1">No matches found</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Try a different search term or clear the search
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={viewMode === 'detailed' ? '' : 'space-y-1'}>
|
<div className={viewMode === 'detailed' ? '' : 'space-y-1'}>
|
||||||
{viewMode === 'simple' && renderFileTree(files)}
|
{viewMode === 'simple' && renderFileTree(filteredFiles)}
|
||||||
{viewMode === 'compact' && renderCompactView(files)}
|
{viewMode === 'compact' && renderCompactView(filteredFiles)}
|
||||||
{viewMode === 'detailed' && renderDetailedView(files)}
|
{viewMode === 'detailed' && renderDetailedView(filteredFiles)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||||||
import { GitBranch, GitCommit, Plus, Minus, RefreshCw, Check, X, ChevronDown, ChevronRight, Info, History, FileText, Mic, MicOff, Sparkles, Download, RotateCcw, Trash2, AlertTriangle, Upload } from 'lucide-react';
|
import { GitBranch, GitCommit, Plus, Minus, RefreshCw, Check, X, ChevronDown, ChevronRight, Info, History, FileText, Mic, MicOff, Sparkles, Download, RotateCcw, Trash2, AlertTriangle, Upload } from 'lucide-react';
|
||||||
import { MicButton } from './MicButton.jsx';
|
import { MicButton } from './MicButton.jsx';
|
||||||
import { authenticatedFetch } from '../utils/api';
|
import { authenticatedFetch } from '../utils/api';
|
||||||
|
import DiffViewer from './DiffViewer.jsx';
|
||||||
|
|
||||||
function GitPanel({ selectedProject, isMobile }) {
|
function GitPanel({ selectedProject, isMobile }) {
|
||||||
const [gitStatus, setGitStatus] = useState(null);
|
const [gitStatus, setGitStatus] = useState(null);
|
||||||
@@ -523,27 +524,6 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderDiffLine = (line, index) => {
|
|
||||||
const isAddition = line.startsWith('+') && !line.startsWith('+++');
|
|
||||||
const isDeletion = line.startsWith('-') && !line.startsWith('---');
|
|
||||||
const isHeader = line.startsWith('@@');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`font-mono text-xs ${
|
|
||||||
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' :
|
|
||||||
isDeletion ? 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300' :
|
|
||||||
isHeader ? 'bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300' :
|
|
||||||
'text-gray-600 dark:text-gray-400'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{line}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusLabel = (status) => {
|
const getStatusLabel = (status) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -590,7 +570,7 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
<div className="text-xs font-mono text-gray-600 dark:text-gray-400 mb-2">
|
<div className="text-xs font-mono text-gray-600 dark:text-gray-400 mb-2">
|
||||||
{commit.stats}
|
{commit.stats}
|
||||||
</div>
|
</div>
|
||||||
{diff.split('\n').map((line, index) => renderDiffLine(line, index))}
|
<DiffViewer diff={diff} fileName="commit" isMobile={isMobile} wrapText={wrapText} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -705,8 +685,8 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-96 overflow-y-auto p-2">
|
<div className="max-h-96 overflow-y-auto">
|
||||||
{diff && diff.split('\n').map((line, index) => renderDiffLine(line, index))}
|
{diff && <DiffViewer diff={diff} fileName={filePath} isMobile={isMobile} wrapText={wrapText} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ function MainContent({
|
|||||||
sendMessage,
|
sendMessage,
|
||||||
messages,
|
messages,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
isPWA,
|
||||||
onMenuClick,
|
onMenuClick,
|
||||||
isLoading,
|
isLoading,
|
||||||
onInputFocusChange,
|
onInputFocusChange,
|
||||||
@@ -152,10 +153,12 @@ function MainContent({
|
|||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
{/* Header with menu button for mobile */}
|
{/* Header with menu button for mobile */}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 flex-shrink-0">
|
<div
|
||||||
|
className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 pwa-header-safe flex-shrink-0"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onClick={onMenuClick}
|
onClick={onMenuClick}
|
||||||
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
|
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 pwa-menu-button"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||||
@@ -188,10 +191,12 @@ function MainContent({
|
|||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
{/* Header with menu button for mobile */}
|
{/* Header with menu button for mobile */}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 flex-shrink-0">
|
<div
|
||||||
|
className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 pwa-header-safe flex-shrink-0"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onClick={onMenuClick}
|
onClick={onMenuClick}
|
||||||
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
|
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 pwa-menu-button"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||||
@@ -224,7 +229,9 @@ function MainContent({
|
|||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
{/* Header with tabs */}
|
{/* Header with tabs */}
|
||||||
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 flex-shrink-0">
|
<div
|
||||||
|
className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 pwa-header-safe flex-shrink-0"
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-2 sm:space-x-3">
|
<div className="flex items-center space-x-2 sm:space-x-3">
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
@@ -234,7 +241,7 @@ function MainContent({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onMenuClick();
|
onMenuClick();
|
||||||
}}
|
}}
|
||||||
className="p-2.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 touch-manipulation active:scale-95"
|
className="p-2.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 touch-manipulation active:scale-95 pwa-menu-button"
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
|||||||
@@ -1992,8 +1992,8 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
|||||||
|
|
||||||
{/* Login Modal */}
|
{/* Login Modal */}
|
||||||
{showLoginModal && (
|
{showLoginModal && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 max-md:items-stretch max-md:justify-stretch">
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-4xl h-3/4 flex flex-col">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-4xl h-3/4 flex flex-col md:max-w-4xl md:h-3/4 md:rounded-lg md:m-4 max-md:max-w-none max-md:h-full max-md:rounded-none max-md:m-0">
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
{loginProvider === 'claude' ? 'Claude CLI Login' : 'Cursor CLI Login'}
|
{loginProvider === 'claude' ? 'Claude CLI Login' : 'Cursor CLI Login'}
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ function Sidebar({
|
|||||||
updateAvailable,
|
updateAvailable,
|
||||||
latestVersion,
|
latestVersion,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
onShowVersionModal
|
onShowVersionModal,
|
||||||
|
isPWA,
|
||||||
|
isMobile
|
||||||
}) {
|
}) {
|
||||||
const [expandedProjects, setExpandedProjects] = useState(new Set());
|
const [expandedProjects, setExpandedProjects] = useState(new Set());
|
||||||
const [editingProject, setEditingProject] = useState(null);
|
const [editingProject, setEditingProject] = useState(null);
|
||||||
@@ -562,7 +564,10 @@ function Sidebar({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col bg-card md:select-none">
|
<div
|
||||||
|
className="h-full flex flex-col bg-card md:select-none"
|
||||||
|
style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
|
||||||
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="md:p-4 md:border-b md:border-border">
|
<div className="md:p-4 md:border-b md:border-border">
|
||||||
{/* Desktop Header */}
|
{/* Desktop Header */}
|
||||||
@@ -607,7 +612,10 @@ function Sidebar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Header */}
|
{/* Mobile Header */}
|
||||||
<div className="md:hidden p-3 border-b border-border">
|
<div
|
||||||
|
className="md:hidden p-3 border-b border-border"
|
||||||
|
style={isPWA && isMobile ? { paddingTop: '16px' } : {}}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
||||||
|
|||||||
@@ -43,6 +43,22 @@
|
|||||||
--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;
|
||||||
|
|
||||||
|
/* Safe area CSS variables */
|
||||||
|
--safe-area-inset-top: env(safe-area-inset-top);
|
||||||
|
--safe-area-inset-right: env(safe-area-inset-right);
|
||||||
|
--safe-area-inset-bottom: env(safe-area-inset-bottom);
|
||||||
|
--safe-area-inset-left: env(safe-area-inset-left);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback for older iOS versions */
|
||||||
|
@supports (padding-top: constant(safe-area-inset-top)) {
|
||||||
|
:root {
|
||||||
|
--safe-area-inset-top: constant(safe-area-inset-top);
|
||||||
|
--safe-area-inset-right: constant(safe-area-inset-right);
|
||||||
|
--safe-area-inset-bottom: constant(safe-area-inset-bottom);
|
||||||
|
--safe-area-inset-left: constant(safe-area-inset-left);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -82,12 +98,60 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body, #root {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Root element with safe area padding for PWA */
|
||||||
|
#root {
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply safe area padding in standalone mode */
|
||||||
|
@supports (padding-top: env(safe-area-inset-top)) {
|
||||||
|
@media (display-mode: standalone) {
|
||||||
|
#root {
|
||||||
|
padding-top: var(--safe-area-inset-top);
|
||||||
|
padding-left: var(--safe-area-inset-left);
|
||||||
|
padding-right: var(--safe-area-inset-right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PWA mode detected by JavaScript - more reliable */
|
||||||
|
html.pwa-mode,
|
||||||
|
body.pwa-mode {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: rgb(255 255 255); /* white - same as bg-white for safe area */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.pwa-mode #root {
|
||||||
|
padding-top: calc(env(safe-area-inset-top, 0px) + 8px);
|
||||||
|
padding-left: env(safe-area-inset-left);
|
||||||
|
padding-right: env(safe-area-inset-right);
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust fixed inset positioning in PWA mode */
|
||||||
|
body.pwa-mode .fixed.inset-0 {
|
||||||
|
top: calc(env(safe-area-inset-top, 0px) + 8px);
|
||||||
|
left: env(safe-area-inset-left);
|
||||||
|
right: env(safe-area-inset-right);
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode safe area background */
|
||||||
|
html.dark body.pwa-mode {
|
||||||
|
background-color: rgb(31 41 55); /* gray-800 - matches header color */
|
||||||
|
}
|
||||||
|
|
||||||
/* Global transition defaults */
|
/* Global transition defaults */
|
||||||
button,
|
button,
|
||||||
a,
|
a,
|
||||||
@@ -577,6 +641,25 @@
|
|||||||
padding-bottom: max(env(safe-area-inset-bottom), 12px);
|
padding-bottom: max(env(safe-area-inset-bottom), 12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* PWA specific header adjustments for iOS */
|
||||||
|
.pwa-header-safe {
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When PWA mode is detected by JavaScript */
|
||||||
|
body.pwa-mode .pwa-header-safe {
|
||||||
|
/* Reset padding since #root already handles safe area */
|
||||||
|
padding-top: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For mobile PWA, ensure proper header spacing */
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
body.pwa-mode .pwa-header-safe {
|
||||||
|
padding-top: 4px !important;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.chat-input-mobile {
|
.chat-input-mobile {
|
||||||
padding-bottom: calc(60px + max(env(safe-area-inset-bottom), 12px));
|
padding-bottom: calc(60px + max(env(safe-area-inset-bottom), 12px));
|
||||||
|
|||||||
Reference in New Issue
Block a user