Remove more inline styles and run npm updates
This commit is contained in:
1873
package-lock.json
generated
1873
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -488,6 +488,7 @@
|
||||
"messageParseError": "Failed to parse server message",
|
||||
"websocketError": "WebSocket connection error",
|
||||
"reconnecting": "Reconnecting... ({{attempt}}/{{max}})",
|
||||
"reconnected": "Reconnected successfully",
|
||||
"maxReconnectAttemptsReached": "Maximum reconnection attempts reached"
|
||||
},
|
||||
"fileManager": {
|
||||
|
||||
@@ -535,20 +535,24 @@ router.post('/file_manager/recent', authenticateJWT, async (req: Request, res: R
|
||||
});
|
||||
|
||||
// Route: Remove recent file (requires JWT)
|
||||
// DELETE /ssh/file_manager/recent/:id
|
||||
router.delete('/file_manager/recent/:id', authenticateJWT, async (req: Request, res: Response) => {
|
||||
// DELETE /ssh/file_manager/recent
|
||||
router.delete('/file_manager/recent', authenticateJWT, async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const id = req.params.id;
|
||||
const { hostId, path, name } = req.body;
|
||||
|
||||
if (!isNonEmptyString(userId) || !id) {
|
||||
sshLogger.warn('Invalid userId or id for recent file deletion');
|
||||
return res.status(400).json({error: 'Invalid userId or id'});
|
||||
if (!isNonEmptyString(userId) || !hostId || !path) {
|
||||
sshLogger.warn('Invalid data for recent file deletion');
|
||||
return res.status(400).json({error: 'Invalid data'});
|
||||
}
|
||||
|
||||
try {
|
||||
await db
|
||||
.delete(fileManagerRecent)
|
||||
.where(and(eq(fileManagerRecent.id, Number(id)), eq(fileManagerRecent.userId, userId)));
|
||||
.where(and(
|
||||
eq(fileManagerRecent.userId, userId),
|
||||
eq(fileManagerRecent.hostId, hostId),
|
||||
eq(fileManagerRecent.path, path)
|
||||
));
|
||||
|
||||
res.json({message: 'Recent file removed'});
|
||||
} catch (err) {
|
||||
@@ -629,20 +633,24 @@ router.post('/file_manager/pinned', authenticateJWT, async (req: Request, res: R
|
||||
});
|
||||
|
||||
// Route: Remove pinned file (requires JWT)
|
||||
// DELETE /ssh/file_manager/pinned/:id
|
||||
router.delete('/file_manager/pinned/:id', authenticateJWT, async (req: Request, res: Response) => {
|
||||
// DELETE /ssh/file_manager/pinned
|
||||
router.delete('/file_manager/pinned', authenticateJWT, async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const id = req.params.id;
|
||||
const { hostId, path, name } = req.body;
|
||||
|
||||
if (!isNonEmptyString(userId) || !id) {
|
||||
sshLogger.warn('Invalid userId or id for pinned file deletion');
|
||||
return res.status(400).json({error: 'Invalid userId or id'});
|
||||
if (!isNonEmptyString(userId) || !hostId || !path) {
|
||||
sshLogger.warn('Invalid data for pinned file deletion');
|
||||
return res.status(400).json({error: 'Invalid data'});
|
||||
}
|
||||
|
||||
try {
|
||||
await db
|
||||
.delete(fileManagerPinned)
|
||||
.where(and(eq(fileManagerPinned.id, Number(id)), eq(fileManagerPinned.userId, userId)));
|
||||
.where(and(
|
||||
eq(fileManagerPinned.userId, userId),
|
||||
eq(fileManagerPinned.hostId, hostId),
|
||||
eq(fileManagerPinned.path, path)
|
||||
));
|
||||
|
||||
res.json({message: 'Pinned file removed'});
|
||||
} catch (err) {
|
||||
@@ -723,20 +731,24 @@ router.post('/file_manager/shortcuts', authenticateJWT, async (req: Request, res
|
||||
});
|
||||
|
||||
// Route: Remove shortcut (requires JWT)
|
||||
// DELETE /ssh/file_manager/shortcuts/:id
|
||||
router.delete('/file_manager/shortcuts/:id', authenticateJWT, async (req: Request, res: Response) => {
|
||||
// DELETE /ssh/file_manager/shortcuts
|
||||
router.delete('/file_manager/shortcuts', authenticateJWT, async (req: Request, res: Response) => {
|
||||
const userId = (req as any).userId;
|
||||
const id = req.params.id;
|
||||
const { hostId, path, name } = req.body;
|
||||
|
||||
if (!isNonEmptyString(userId) || !id) {
|
||||
sshLogger.warn('Invalid userId or id for shortcut deletion');
|
||||
return res.status(400).json({error: 'Invalid userId or id'});
|
||||
if (!isNonEmptyString(userId) || !hostId || !path) {
|
||||
sshLogger.warn('Invalid data for shortcut deletion');
|
||||
return res.status(400).json({error: 'Invalid data'});
|
||||
}
|
||||
|
||||
try {
|
||||
await db
|
||||
.delete(fileManagerShortcuts)
|
||||
.where(and(eq(fileManagerShortcuts.id, Number(id)), eq(fileManagerShortcuts.userId, userId)));
|
||||
.where(and(
|
||||
eq(fileManagerShortcuts.userId, userId),
|
||||
eq(fileManagerShortcuts.hostId, hostId),
|
||||
eq(fileManagerShortcuts.path, path)
|
||||
));
|
||||
|
||||
res.json({message: 'Shortcut removed'});
|
||||
} catch (err) {
|
||||
|
||||
@@ -25,13 +25,13 @@ export function LanguageSwitcher() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 relative" style={{ zIndex: 99999 }}>
|
||||
<div className="flex items-center gap-2 relative z-[99999]">
|
||||
<Globe className="h-4 w-4 text-muted-foreground" />
|
||||
<Select value={i18n.language} onValueChange={handleLanguageChange}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder={t('placeholders.language')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent style={{ zIndex: 99999 }}>
|
||||
<SelectContent className="z-[99999]">
|
||||
{languages.map((lang) => (
|
||||
<SelectItem key={lang.code} value={lang.code}>
|
||||
{lang.nativeName}
|
||||
|
||||
@@ -37,13 +37,13 @@ function ResizableHandle({
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
data-slot="resizable-handle"
|
||||
className={cn(
|
||||
"relative flex w-1 items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-1 data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90 bg-[#434345] hover:bg-[#2a2a2c] active:bg-[#1a1a1c] transition-colors duration-150",
|
||||
"relative flex w-1 items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-1 data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90 bg-dark-border-hover hover:bg-dark-active active:bg-dark-pressed transition-colors duration-150",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="bg-[#434345] hover:bg-[#2a2a2c] active:bg-[#1a1a1c] z-10 flex h-4 w-3 items-center justify-center rounded-xs border transition-colors duration-150">
|
||||
<div className="bg-dark-border-hover hover:bg-dark-active active:bg-dark-pressed z-10 flex h-4 w-3 items-center justify-center rounded-xs border transition-colors duration-150">
|
||||
<GripVerticalIcon className="size-2.5" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -86,6 +86,29 @@
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
/* Custom dark theme colors - exact hex matches */
|
||||
--color-dark-bg: #18181b;
|
||||
--color-dark-bg-darker: #0e0e10;
|
||||
--color-dark-bg-darkest: #09090b;
|
||||
--color-dark-bg-input: #222225;
|
||||
--color-dark-bg-button: #23232a;
|
||||
--color-dark-bg-active: #1d1d1f;
|
||||
--color-dark-bg-header: #131316;
|
||||
--color-dark-border: #303032;
|
||||
--color-dark-border-active: #2d2d30;
|
||||
--color-dark-border-hover: #434345;
|
||||
--color-dark-hover: #2d2d30;
|
||||
--color-dark-active: #2a2a2c;
|
||||
--color-dark-pressed: #1a1a1c;
|
||||
--color-dark-hover-alt: #2a2a2d;
|
||||
--color-dark-border-light: #5a5a5d;
|
||||
--color-dark-bg-light: #141416;
|
||||
--color-dark-border-medium: #373739;
|
||||
--color-dark-bg-very-light: #101014;
|
||||
--color-dark-bg-panel: #1b1b1e;
|
||||
--color-dark-border-panel: #222224;
|
||||
--color-dark-bg-panel-hover: #232327;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {Button} from "@/components/ui/button.tsx";
|
||||
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx";
|
||||
import {Checkbox} from "@/components/ui/checkbox.tsx";
|
||||
import {Input} from "@/components/ui/input.tsx";
|
||||
import {PasswordInput} from "@/components/ui/password-input.tsx";
|
||||
import {Label} from "@/components/ui/label.tsx";
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx";
|
||||
import {
|
||||
@@ -219,7 +220,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
||||
|
||||
return (
|
||||
<div style={wrapperStyle}
|
||||
className="bg-[#18181b] text-white rounded-lg border-2 border-[#303032] overflow-hidden">
|
||||
className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden">
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<div className="flex items-center justify-between px-3 pt-2 pb-2">
|
||||
<h1 className="font-bold text-lg">{t('admin.title')}</h1>
|
||||
@@ -228,7 +229,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
||||
|
||||
<div className="px-6 py-4 overflow-auto">
|
||||
<Tabs defaultValue="registration" className="w-full">
|
||||
<TabsList className="mb-4 bg-[#18181b] border-2 border-[#303032]">
|
||||
<TabsList className="mb-4 bg-dark-bg border-2 border-dark-border">
|
||||
<TabsTrigger value="registration" className="flex items-center gap-2">
|
||||
<Users className="h-4 w-4"/>
|
||||
{t('admin.general')}
|
||||
@@ -279,7 +280,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="client_secret">{t('admin.clientSecret')}</Label>
|
||||
<Input id="client_secret" type="password" value={oidcConfig.client_secret}
|
||||
<PasswordInput id="client_secret" value={oidcConfig.client_secret}
|
||||
onChange={(e) => handleOIDCConfigChange('client_secret', e.target.value)}
|
||||
placeholder={t('placeholders.clientSecret')} required/>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { PasswordInput } from "@/components/ui/password-input"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
@@ -390,7 +391,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
||||
{folderDropdownOpen && filteredFolders.length > 0 && (
|
||||
<div
|
||||
ref={folderDropdownRef}
|
||||
className="absolute top-full left-0 z-50 mt-1 w-full bg-[#18181b] border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1"
|
||||
className="absolute top-full left-0 z-50 mt-1 w-full bg-dark-bg border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-1 p-0">
|
||||
{filteredFolders.map((folder) => (
|
||||
@@ -420,7 +421,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
||||
<FormLabel>{t('credentials.tags')}</FormLabel>
|
||||
<FormControl>
|
||||
<div
|
||||
className="flex flex-wrap items-center gap-1 border border-input rounded-md px-3 py-2 bg-[#222225] focus-within:ring-2 ring-ring min-h-[40px]">
|
||||
className="flex flex-wrap items-center gap-1 border border-input rounded-md px-3 py-2 bg-dark-bg-input focus-within:ring-2 ring-ring min-h-[40px]">
|
||||
{(field.value || []).map((tag: string, idx: number) => (
|
||||
<span key={`${tag}-${idx}`}
|
||||
className="flex items-center bg-gray-200 text-gray-800 rounded-full px-2 py-0.5 text-xs">
|
||||
@@ -509,7 +510,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
||||
<FormItem>
|
||||
<FormLabel>{t('credentials.password')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder={t('placeholders.password')} {...field} />
|
||||
<PasswordInput placeholder={t('placeholders.password')} {...field} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -576,9 +577,8 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
||||
<FormItem className="col-span-8">
|
||||
<FormLabel>{t('credentials.keyPassword')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
<PasswordInput
|
||||
placeholder={t('placeholders.keyPassword')}
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -597,7 +597,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
||||
ref={keyTypeButtonRef}
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full justify-start text-left rounded-md px-2 py-2 bg-[#18181b] border border-input text-foreground"
|
||||
className="w-full justify-start text-left rounded-md px-2 py-2 bg-dark-bg border border-input text-foreground"
|
||||
onClick={() => setKeyTypeDropdownOpen((open) => !open)}
|
||||
>
|
||||
{keyTypeOptions.find((opt) => opt.value === field.value)?.label || t('credentials.keyTypeRSA')}
|
||||
@@ -605,7 +605,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
||||
{keyTypeDropdownOpen && (
|
||||
<div
|
||||
ref={keyTypeDropdownRef}
|
||||
className="absolute bottom-full left-0 z-50 mb-1 w-full bg-[#18181b] border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1"
|
||||
className="absolute bottom-full left-0 z-50 mb-1 w-full bg-dark-bg border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-1 p-0">
|
||||
{keyTypeOptions.map((opt) => (
|
||||
@@ -614,7 +614,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start text-left rounded-md px-2 py-1.5 bg-[#18181b] text-foreground hover:bg-white/15 focus:bg-white/20 focus:outline-none"
|
||||
className="w-full justify-start text-left rounded-md px-2 py-1.5 bg-dark-bg text-foreground hover:bg-white/15 focus:bg-white/20 focus:outline-none"
|
||||
onClick={() => {
|
||||
field.onChange(opt.value);
|
||||
setKeyTypeDropdownOpen(false);
|
||||
@@ -659,9 +659,8 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
||||
<FormItem className="col-span-8">
|
||||
<FormLabel>{t('credentials.keyPassword')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
<PasswordInput
|
||||
placeholder={t('placeholders.keyPassword')}
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -680,7 +679,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
||||
ref={keyTypeButtonRef}
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full justify-start text-left rounded-md px-2 py-2 bg-[#18181b] border border-input text-foreground"
|
||||
className="w-full justify-start text-left rounded-md px-2 py-2 bg-dark-bg border border-input text-foreground"
|
||||
onClick={() => setKeyTypeDropdownOpen((open) => !open)}
|
||||
>
|
||||
{keyTypeOptions.find((opt) => opt.value === field.value)?.label || t('credentials.keyTypeRSA')}
|
||||
@@ -688,7 +687,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
||||
{keyTypeDropdownOpen && (
|
||||
<div
|
||||
ref={keyTypeDropdownRef}
|
||||
className="absolute bottom-full left-0 z-50 mb-1 w-full bg-[#18181b] border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1"
|
||||
className="absolute bottom-full left-0 z-50 mb-1 w-full bg-dark-bg border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-1 p-0">
|
||||
{keyTypeOptions.map((opt) => (
|
||||
@@ -697,7 +696,7 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start text-left rounded-md px-2 py-1.5 bg-[#18181b] text-foreground hover:bg-white/15 focus:bg-white/20 focus:outline-none"
|
||||
className="w-full justify-start text-left rounded-md px-2 py-1.5 bg-dark-bg text-foreground hover:bg-white/15 focus:bg-white/20 focus:outline-none"
|
||||
onClick={() => {
|
||||
field.onChange(opt.value);
|
||||
setKeyTypeDropdownOpen(false);
|
||||
@@ -725,12 +724,9 @@ export function CredentialEditor({ editingCredential, onFormSubmit }: Credential
|
||||
<footer className="shrink-0 w-full pb-0">
|
||||
<Separator className="p-0.25"/>
|
||||
<Button
|
||||
className=""
|
||||
className="translate-y-2"
|
||||
type="submit"
|
||||
variant="outline"
|
||||
style={{
|
||||
transform: 'translateY(8px)'
|
||||
}}
|
||||
>
|
||||
{editingCredential ? t('credentials.updateCredential') : t('credentials.addCredential')}
|
||||
</Button>
|
||||
|
||||
@@ -438,7 +438,7 @@ export function CredentialsManager({ onEditCredential }: CredentialsManagerProps
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, credential)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className={`bg-[#222225] border border-input rounded-lg cursor-pointer hover:shadow-lg hover:border-blue-400/50 hover:bg-[#2a2a2d] transition-all duration-200 p-3 group relative ${
|
||||
className={`bg-dark-bg-input border border-input rounded-lg cursor-pointer hover:shadow-lg hover:border-blue-400/50 hover:bg-dark-hover-alt transition-all duration-200 p-3 group relative ${
|
||||
draggedCredential?.id === credential.id ? 'opacity-50 scale-95' : ''
|
||||
}`}
|
||||
onClick={() => handleEdit(credential)}
|
||||
|
||||
@@ -28,10 +28,11 @@ import {
|
||||
} from '@/ui/main-axios.ts';
|
||||
import type { SSHHost, Tab, FileManagerProps } from '../../../types/index.js';
|
||||
|
||||
export function FileManager({onSelectView, embedded = false, initialHost = null}: {
|
||||
export function FileManager({onSelectView, embedded = false, initialHost = null, onClose}: {
|
||||
onSelectView?: (view: string) => void,
|
||||
embedded?: boolean,
|
||||
initialHost?: SSHHost | null
|
||||
initialHost?: SSHHost | null,
|
||||
onClose?: () => void
|
||||
}): React.ReactElement {
|
||||
const {t} = useTranslation();
|
||||
const [tabs, setTabs] = useState<Tab[]>([]);
|
||||
@@ -75,12 +76,6 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
}
|
||||
}, [currentHost]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'home' && currentHost) {
|
||||
fetchHomeData();
|
||||
}
|
||||
}, [activeTab, currentHost]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'home' && currentHost) {
|
||||
const interval = setInterval(() => {
|
||||
@@ -130,6 +125,10 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
console.error('Failed to fetch home data:', err);
|
||||
const {toast} = await import('sonner');
|
||||
toast.error(t('fileManager.failedToFetchHomeData'));
|
||||
// Close the file manager tab on connection failure
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +184,6 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
sshSessionId: currentSshSessionId,
|
||||
hostId: currentHost?.id
|
||||
});
|
||||
fetchHomeData();
|
||||
} catch (err: any) {
|
||||
const errorMessage = formatErrorMessage(err, t('fileManager.cannotReadFile'));
|
||||
toast.error(errorMessage);
|
||||
@@ -218,7 +216,6 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
sshSessionId: file.sshSessionId,
|
||||
hostId: currentHost?.id
|
||||
});
|
||||
fetchHomeData();
|
||||
if (sidebarRef.current && sidebarRef.current.fetchFiles) {
|
||||
sidebarRef.current.fetchFiles();
|
||||
}
|
||||
@@ -235,7 +232,6 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
sshSessionId: file.sshSessionId,
|
||||
hostId: currentHost?.id
|
||||
});
|
||||
fetchHomeData();
|
||||
if (sidebarRef.current && sidebarRef.current.fetchFiles) {
|
||||
sidebarRef.current.fetchFiles();
|
||||
}
|
||||
@@ -275,7 +271,6 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
sshSessionId: currentHost?.id.toString(),
|
||||
hostId: currentHost?.id
|
||||
});
|
||||
fetchHomeData();
|
||||
} catch (err) {
|
||||
}
|
||||
};
|
||||
@@ -289,7 +284,6 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
sshSessionId: currentHost?.id.toString(),
|
||||
hostId: currentHost?.id
|
||||
});
|
||||
fetchHomeData();
|
||||
} catch (err) {
|
||||
}
|
||||
};
|
||||
@@ -302,9 +296,6 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
if (newTabs.length > 0) setActiveTab(newTabs[Math.max(0, idx - 1)].id);
|
||||
else setActiveTab('home');
|
||||
}
|
||||
if (currentHost) {
|
||||
fetchHomeData();
|
||||
}
|
||||
};
|
||||
|
||||
const setTabContent = (tabId: string | number, content: string) => {
|
||||
@@ -401,13 +392,6 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
console.error('Failed to add recent file:', recentErr);
|
||||
}
|
||||
})(),
|
||||
(async () => {
|
||||
try {
|
||||
await fetchHomeData();
|
||||
} catch (refreshErr) {
|
||||
console.error('Failed to refresh home data:', refreshErr);
|
||||
}
|
||||
})()
|
||||
]).then(() => {
|
||||
});
|
||||
|
||||
@@ -435,9 +419,6 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
if (sidebarRef.current && sidebarRef.current.fetchFiles) {
|
||||
sidebarRef.current.fetchFiles();
|
||||
}
|
||||
if (currentHost) {
|
||||
fetchHomeData();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSuccess = (message: string) => {
|
||||
@@ -479,8 +460,8 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
|
||||
if (!currentHost) {
|
||||
return (
|
||||
<div style={{position: 'absolute', inset: 0, overflow: 'hidden'}} className="rounded-md">
|
||||
<div style={{position: 'absolute', top: 0, left: 0, width: 256, height: '100%', zIndex: 20}}>
|
||||
<div className="absolute inset-0 overflow-hidden rounded-md">
|
||||
<div className="absolute top-0 left-0 w-64 h-full z-[20]">
|
||||
<FileManagerLeftSidebar
|
||||
onSelectView={onSelectView || (() => {
|
||||
})}
|
||||
@@ -494,17 +475,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
onPathChange={updateCurrentPath}
|
||||
/>
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 256,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#09090b'
|
||||
}}>
|
||||
<div className="absolute top-0 left-64 right-0 bottom-0 flex items-center justify-center bg-dark-bg-darkest">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold text-white mb-2">{t('fileManager.connectToServer')}</h2>
|
||||
<p className="text-muted-foreground">{t('fileManager.selectServerToEdit')}</p>
|
||||
@@ -515,8 +486,8 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{position: 'absolute', inset: 0, overflow: 'hidden'}} className="rounded-md">
|
||||
<div style={{position: 'absolute', top: 0, left: 0, width: 256, height: '100%', zIndex: 20}}>
|
||||
<div className="absolute inset-0 overflow-hidden rounded-md">
|
||||
<div className="absolute top-0 left-0 w-64 h-full z-[20]">
|
||||
<FileManagerLeftSidebar
|
||||
onSelectView={onSelectView || (() => {
|
||||
})}
|
||||
@@ -531,10 +502,10 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
onDeleteItem={handleDeleteFromSidebar}
|
||||
/>
|
||||
</div>
|
||||
<div style={{position: 'absolute', top: 0, left: 256, right: 0, height: 50, zIndex: 30}}>
|
||||
<div className="flex items-center w-full bg-[#18181b] border-b-2 border-[#303032] h-[50px] relative">
|
||||
<div className="absolute top-0 left-64 right-0 h-[50px] z-[30]">
|
||||
<div className="flex items-center w-full bg-dark-bg border-b-2 border-dark-border h-[50px] relative">
|
||||
<div
|
||||
className="h-full p-1 pr-2 border-r-2 border-[#303032] w-[calc(100%-6rem)] flex items-center overflow-x-auto overflow-y-hidden gap-2 thin-scrollbar">
|
||||
className="h-full p-1 pr-2 border-r-2 border-dark-border w-[calc(100%-6rem)] flex items-center overflow-x-auto overflow-y-hidden gap-2 thin-scrollbar">
|
||||
<FIleManagerTopNavbar
|
||||
tabs={tabs.map(t => ({id: t.id, title: t.title}))}
|
||||
activeTab={activeTab}
|
||||
@@ -542,9 +513,6 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
closeTab={closeTab}
|
||||
onHomeClick={() => {
|
||||
setActiveTab('home');
|
||||
if (currentHost) {
|
||||
fetchHomeData();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -554,13 +522,13 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
onClick={() => setShowOperations(!showOperations)}
|
||||
className={cn(
|
||||
'w-[30px] h-[30px]',
|
||||
showOperations ? 'bg-[#2d2d30] border-[#434345]' : ''
|
||||
showOperations ? 'bg-dark-hover border-dark-border-hover' : ''
|
||||
)}
|
||||
title={t('fileManager.fileOperations')}
|
||||
>
|
||||
<Settings className="h-4 w-4"/>
|
||||
</Button>
|
||||
<div className="p-0.25 w-px h-[30px] bg-[#303032]"></div>
|
||||
<div className="p-0.25 w-px h-[30px] bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
@@ -578,18 +546,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 44,
|
||||
left: 256,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
overflow: 'hidden',
|
||||
zIndex: 10,
|
||||
background: '#101014',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}>
|
||||
<div className="absolute top-[44px] left-64 right-0 bottom-0 overflow-hidden z-[10] bg-dark-bg-very-light flex flex-col">
|
||||
<div className="flex h-full">
|
||||
<div className="flex-1">
|
||||
{activeTab === 'home' ? (
|
||||
@@ -610,7 +567,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
const tab = tabs.find(t => t.id === activeTab);
|
||||
if (!tab) return null;
|
||||
return (
|
||||
<div className="flex flex-col h-full" style={{flex: 1, minHeight: 0}}>
|
||||
<div className="flex flex-col h-full flex-1 min-h-0">
|
||||
<div className="flex-1 min-h-0">
|
||||
<FileManagerFileEditor
|
||||
content={tab.content}
|
||||
@@ -624,7 +581,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
)}
|
||||
</div>
|
||||
{showOperations && (
|
||||
<div className="w-80 border-l-2 border-[#303032] bg-[#09090b] overflow-y-auto">
|
||||
<div className="w-80 border-l-2 border-dark-border bg-dark-bg-darkest overflow-y-auto">
|
||||
<FileManagerOperations
|
||||
currentPath={currentPath}
|
||||
sshSessionId={currentHost?.id.toString() || null}
|
||||
@@ -642,7 +599,7 @@ export function FileManager({onSelectView, embedded = false, initialHost = null}
|
||||
<div className="absolute inset-0 bg-black/60"></div>
|
||||
|
||||
<div className="relative h-full flex items-center justify-center">
|
||||
<div className="bg-[#18181b] border-2 border-[#303032] rounded-lg p-6 max-w-md mx-4 shadow-2xl">
|
||||
<div className="bg-dark-bg border-2 border-dark-border rounded-lg p-6 max-w-md mx-4 shadow-2xl">
|
||||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<Trash2 className="w-5 h-5 text-red-400"/>
|
||||
{t('fileManager.confirmDelete')}
|
||||
|
||||
@@ -304,24 +304,9 @@ export function FileManagerFileEditor({content, fileName, onContentChange}: File
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}>
|
||||
<div className="w-full h-full relative overflow-hidden flex flex-col">
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
className="config-codemirror-scroll-wrapper"
|
||||
className="w-full h-full overflow-auto flex-1 flex flex-col config-codemirror-scroll-wrapper"
|
||||
>
|
||||
<CodeMirror
|
||||
value={content}
|
||||
@@ -331,10 +316,10 @@ export function FileManagerFileEditor({content, fileName, onContentChange}: File
|
||||
oneDark,
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
backgroundColor: '#09090b !important',
|
||||
backgroundColor: 'var(--color-dark-bg-darkest) !important',
|
||||
},
|
||||
'.cm-gutters': {
|
||||
backgroundColor: '#18181b !important',
|
||||
backgroundColor: 'var(--color-dark-bg) !important',
|
||||
},
|
||||
})
|
||||
]}
|
||||
@@ -342,7 +327,7 @@ export function FileManagerFileEditor({content, fileName, onContentChange}: File
|
||||
theme={undefined}
|
||||
height="100%"
|
||||
basicSetup={{lineNumbers: true}}
|
||||
style={{minHeight: '100%', minWidth: '100%', flex: 1}}
|
||||
className="min-h-full min-w-full flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {Tabs, TabsList, TabsTrigger, TabsContent} from '@/components/ui/tabs.tsx
|
||||
import {Input} from '@/components/ui/input.tsx';
|
||||
import {useState} from 'react';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import type { FileItem, ShortcutItem } from '../../../types/index.js';
|
||||
import type { FileItem, ShortcutItem } from '../../../types/index';
|
||||
|
||||
interface FileManagerHomeViewProps {
|
||||
recent: FileItem[];
|
||||
@@ -39,7 +39,7 @@ export function FileManagerHomeView({
|
||||
|
||||
const renderFileCard = (file: FileItem, onRemove: () => void, onPin?: () => void, isPinned = false) => (
|
||||
<div key={file.path}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-[#18181b] border-2 border-[#303032] rounded hover:border-[#434345] transition-colors">
|
||||
className="flex items-center gap-2 px-3 py-2 bg-dark-bg border-2 border-dark-border rounded hover:border-dark-border-hover transition-colors">
|
||||
<div
|
||||
className="flex items-center gap-2 flex-1 cursor-pointer min-w-0"
|
||||
onClick={() => onOpenFile(file)}
|
||||
@@ -59,7 +59,7 @@ export function FileManagerHomeView({
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 px-1.5 bg-[#23232a] hover:bg-[#2d2d30] rounded-md"
|
||||
className="h-6 px-1.5 bg-dark-bg-button hover:bg-dark-hover rounded-md"
|
||||
onClick={onPin}
|
||||
>
|
||||
<Pin
|
||||
@@ -70,7 +70,7 @@ export function FileManagerHomeView({
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 px-1.5 bg-[#23232a] hover:bg-[#2d2d30] rounded-md"
|
||||
className="h-6 px-1.5 bg-dark-bg-button hover:bg-dark-hover rounded-md"
|
||||
onClick={onRemove}
|
||||
>
|
||||
<Trash2 className="w-3 h-3 text-red-500"/>
|
||||
@@ -82,7 +82,7 @@ export function FileManagerHomeView({
|
||||
|
||||
const renderShortcutCard = (shortcut: ShortcutItem) => (
|
||||
<div key={shortcut.path}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-[#18181b] border-2 border-[#303032] rounded hover:border-[#434345] transition-colors">
|
||||
className="flex items-center gap-2 px-3 py-2 bg-dark-bg border-2 border-dark-border rounded hover:border-dark-border-hover transition-colors">
|
||||
<div
|
||||
className="flex items-center gap-2 flex-1 cursor-pointer min-w-0"
|
||||
onClick={() => onOpenShortcut(shortcut)}
|
||||
@@ -98,7 +98,7 @@ export function FileManagerHomeView({
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 px-1.5 bg-[#23232a] hover:bg-[#2d2d30] rounded-md"
|
||||
className="h-6 px-1.5 bg-dark-bg-button hover:bg-dark-hover rounded-md"
|
||||
onClick={() => onRemoveShortcut(shortcut)}
|
||||
>
|
||||
<Trash2 className="w-3 h-3 text-red-500"/>
|
||||
@@ -108,12 +108,12 @@ export function FileManagerHomeView({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-4 flex flex-col gap-4 h-full bg-[#09090b]">
|
||||
<div className="p-4 flex flex-col gap-4 h-full bg-dark-bg-darkest">
|
||||
<Tabs value={tab} onValueChange={v => setTab(v as 'recent' | 'pinned' | 'shortcuts')} className="w-full">
|
||||
<TabsList className="mb-4 bg-[#18181b] border-2 border-[#303032]">
|
||||
<TabsTrigger value="recent" className="data-[state=active]:bg-[#23232a]">{t('fileManager.recent')}</TabsTrigger>
|
||||
<TabsTrigger value="pinned" className="data-[state=active]:bg-[#23232a]">{t('fileManager.pinned')}</TabsTrigger>
|
||||
<TabsTrigger value="shortcuts" className="data-[state=active]:bg-[#23232a]">{t('fileManager.folderShortcuts')}</TabsTrigger>
|
||||
<TabsList className="mb-4 bg-dark-bg border-2 border-dark-border">
|
||||
<TabsTrigger value="recent" className="data-[state=active]:bg-dark-bg-button">{t('fileManager.recent')}</TabsTrigger>
|
||||
<TabsTrigger value="pinned" className="data-[state=active]:bg-dark-bg-button">{t('fileManager.pinned')}</TabsTrigger>
|
||||
<TabsTrigger value="shortcuts" className="data-[state=active]:bg-dark-bg-button">{t('fileManager.folderShortcuts')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="recent" className="mt-0">
|
||||
@@ -153,12 +153,12 @@ export function FileManagerHomeView({
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="shortcuts" className="mt-0">
|
||||
<div className="flex items-center gap-3 mb-4 p-3 bg-[#18181b] border-2 border-[#303032] rounded-lg">
|
||||
<div className="flex items-center gap-3 mb-4 p-3 bg-dark-bg border-2 border-dark-border rounded-lg">
|
||||
<Input
|
||||
placeholder={t('fileManager.enterFolderPath')}
|
||||
value={newShortcut}
|
||||
onChange={e => setNewShortcut(e.target.value)}
|
||||
className="flex-1 bg-[#23232a] border-2 border-[#303032] text-white placeholder:text-muted-foreground"
|
||||
className="flex-1 bg-dark-bg-button border-2 border-dark-border text-white placeholder:text-muted-foreground"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && newShortcut.trim()) {
|
||||
onAddShortcut(newShortcut.trim());
|
||||
@@ -169,7 +169,7 @@ export function FileManagerHomeView({
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 px-2 bg-[#23232a] border-2 !border-[#303032] hover:bg-[#2d2d30] rounded-md"
|
||||
className="h-8 px-2 bg-dark-bg-button border-2 !border-dark-border hover:bg-dark-hover rounded-md"
|
||||
onClick={() => {
|
||||
if (newShortcut.trim()) {
|
||||
onAddShortcut(newShortcut.trim());
|
||||
|
||||
@@ -358,16 +358,16 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-[256px]" style={{maxWidth: 256}}>
|
||||
<div className="flex flex-col h-full w-[256px] max-w-[256px]">
|
||||
<div className="flex flex-col flex-grow min-h-0">
|
||||
<div className="flex-1 w-full h-full flex flex-col bg-[#09090b] border-r-2 border-[#303032] overflow-hidden p-0 relative min-h-0">
|
||||
<div className="flex-1 w-full h-full flex flex-col bg-dark-bg-darkest border-r-2 border-dark-border overflow-hidden p-0 relative min-h-0">
|
||||
{host && (
|
||||
<div className="flex flex-col h-full w-full" style={{maxWidth: 260}}>
|
||||
<div className="flex items-center gap-2 px-2 py-1.5 border-b-2 border-[#303032] bg-[#18181b] z-20" style={{maxWidth: 260}}>
|
||||
<div className="flex flex-col h-full w-full max-w-[260px]">
|
||||
<div className="flex items-center gap-2 px-2 py-1.5 border-b-2 border-dark-border bg-dark-bg z-20 max-w-[260px]">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="h-9 w-9 bg-[#18181b] border-2 border-[#303032] rounded-md hover:bg-[#2d2d30] focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
className="h-9 w-9 bg-dark-bg border-2 border-dark-border rounded-md hover:bg-dark-hover focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
onClick={() => {
|
||||
let path = currentPath;
|
||||
if (path && path !== '/' && path !== '') {
|
||||
@@ -387,20 +387,20 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
||||
</Button>
|
||||
<Input ref={pathInputRef} value={currentPath}
|
||||
onChange={e => handlePathChange(e.target.value)}
|
||||
className="flex-1 bg-[#18181b] border-2 border-[#434345] text-white truncate rounded-md px-2 py-1 focus:outline-none focus:ring-2 focus:ring-ring hover:border-[#5a5a5d]"
|
||||
className="flex-1 bg-dark-bg border-2 border-dark-border-hover text-white truncate rounded-md px-2 py-1 focus:outline-none focus:ring-2 focus:ring-ring hover:border-dark-border-light"
|
||||
/>
|
||||
</div>
|
||||
<div className="px-2 py-2 border-b-1 border-[#303032] bg-[#18181b]">
|
||||
<div className="px-2 py-2 border-b-1 border-dark-border bg-dark-bg">
|
||||
<Input
|
||||
placeholder={t('fileManager.searchFilesAndFolders')}
|
||||
className="w-full h-7 text-sm bg-[#23232a] border-2 border-[#434345] text-white placeholder:text-muted-foreground rounded-md"
|
||||
className="w-full h-7 text-sm bg-dark-bg-button border-2 border-dark-border-hover text-white placeholder:text-muted-foreground rounded-md"
|
||||
autoComplete="off"
|
||||
value={fileSearch}
|
||||
onChange={e => setFileSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 w-full bg-[#09090b] border-t-1 border-[#303032]">
|
||||
<ScrollArea className="h-full w-full bg-[#09090b]">
|
||||
<div className="flex-1 min-h-0 w-full bg-dark-bg-darkest border-t-1 border-dark-border">
|
||||
<ScrollArea className="h-full w-full bg-dark-bg-darkest">
|
||||
<div className="p-2 pb-0">
|
||||
{connectingSSH || filesLoading ? (
|
||||
<div className="text-xs text-muted-foreground">{t('common.loading')}</div>
|
||||
@@ -417,10 +417,9 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
||||
<div
|
||||
key={item.path}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-2 bg-[#18181b] border-2 border-[#303032] rounded group max-w-full relative",
|
||||
"flex items-center gap-2 px-3 py-2 bg-dark-bg border-2 border-dark-border rounded group max-w-[220px] mb-2 relative",
|
||||
isOpen && "opacity-60 cursor-not-allowed pointer-events-none"
|
||||
)}
|
||||
style={{maxWidth: 220, marginBottom: 8}}
|
||||
onContextMenu={(e) => !isOpen && handleContextMenu(e, item)}
|
||||
>
|
||||
{isRenaming ? (
|
||||
@@ -431,7 +430,7 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
||||
<Input
|
||||
value={renamingItem.newName}
|
||||
onChange={(e) => setRenamingItem(prev => prev ? {...prev, newName: e.target.value} : null)}
|
||||
className="flex-1 h-6 text-sm bg-[#23232a] border border-[#434345] text-white"
|
||||
className="flex-1 h-6 text-sm bg-dark-bg-button border border-dark-border-hover text-white"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -527,21 +526,21 @@ const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
||||
|
||||
{contextMenu.visible && contextMenu.item && (
|
||||
<div
|
||||
className="fixed z-[99998] bg-[#18181b] border-2 border-[#303032] rounded-lg shadow-xl py-1 min-w-[160px]"
|
||||
className="fixed z-[99998] bg-dark-bg border-2 border-dark-border rounded-lg shadow-xl py-1 min-w-[160px]"
|
||||
style={{
|
||||
left: contextMenu.x,
|
||||
top: contextMenu.y,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className="w-full px-3 py-2 text-left text-sm text-white hover:bg-[#2d2d30] flex items-center gap-2"
|
||||
className="w-full px-3 py-2 text-left text-sm text-white hover:bg-dark-hover flex items-center gap-2"
|
||||
onClick={() => startRename(contextMenu.item)}
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
Rename
|
||||
</button>
|
||||
<button
|
||||
className="w-full px-3 py-2 text-left text-sm text-red-400 hover:bg-[#2d2d30] flex items-center gap-2"
|
||||
className="w-full px-3 py-2 text-left text-sm text-red-400 hover:bg-dark-hover flex items-center gap-2"
|
||||
onClick={() => startDelete(contextMenu.item)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
|
||||
@@ -66,7 +66,7 @@ export function FileManagerLeftSidebarFileViewer({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-1 bg-[#09090b] p-2 overflow-y-auto">
|
||||
<div className="flex-1 bg-dark-bg-darkest p-2 overflow-y-auto">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span
|
||||
className="text-xs text-muted-foreground font-semibold">{isSSHMode ? t('common.sshPath') : t('common.localPath')}</span>
|
||||
@@ -80,7 +80,7 @@ export function FileManagerLeftSidebarFileViewer({
|
||||
<div className="flex flex-col gap-1">
|
||||
{files.map((item) => (
|
||||
<Card key={item.path}
|
||||
className="flex items-center gap-2 px-2 py-1 bg-[#18181b] border-2 border-[#303032] rounded">
|
||||
className="flex items-center gap-2 px-2 py-1 bg-dark-bg border-2 border-dark-border rounded">
|
||||
<div className="flex items-center gap-2 flex-1 cursor-pointer"
|
||||
onClick={() => item.type === 'directory' ? onOpenFolder(item) : onOpenFile(item)}>
|
||||
{item.type === 'directory' ? <Folder className="w-4 h-4 text-blue-400"/> :
|
||||
|
||||
@@ -301,7 +301,7 @@ export function FileManagerOperations({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowUpload(true)}
|
||||
className="h-10 bg-[#18181b] border-2 border-[#303032] hover:border-[#434345] hover:bg-[#2d2d30]"
|
||||
className="h-10 bg-dark-bg border-2 border-dark-border hover:border-dark-border-hover hover:bg-dark-hover"
|
||||
title={t('fileManager.uploadFile')}
|
||||
>
|
||||
<Upload className={cn("w-4 h-4", showTextLabels ? "mr-2" : "")}/>
|
||||
@@ -311,7 +311,7 @@ export function FileManagerOperations({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowCreateFile(true)}
|
||||
className="h-10 bg-[#18181b] border-2 border-[#303032] hover:border-[#434345] hover:bg-[#2d2d30]"
|
||||
className="h-10 bg-dark-bg border-2 border-dark-border hover:border-dark-border-hover hover:bg-dark-hover"
|
||||
title={t('fileManager.newFile')}
|
||||
>
|
||||
<FilePlus className={cn("w-4 h-4", showTextLabels ? "mr-2" : "")}/>
|
||||
@@ -321,7 +321,7 @@ export function FileManagerOperations({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowCreateFolder(true)}
|
||||
className="h-10 bg-[#18181b] border-2 border-[#303032] hover:border-[#434345] hover:bg-[#2d2d30]"
|
||||
className="h-10 bg-dark-bg border-2 border-dark-border hover:border-dark-border-hover hover:bg-dark-hover"
|
||||
title={t('fileManager.newFolder')}
|
||||
>
|
||||
<FolderPlus className={cn("w-4 h-4", showTextLabels ? "mr-2" : "")}/>
|
||||
@@ -331,7 +331,7 @@ export function FileManagerOperations({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowRename(true)}
|
||||
className="h-10 bg-[#18181b] border-2 border-[#303032] hover:border-[#434345] hover:bg-[#2d2d30]"
|
||||
className="h-10 bg-dark-bg border-2 border-dark-border hover:border-dark-border-hover hover:bg-dark-hover"
|
||||
title={t('fileManager.rename')}
|
||||
>
|
||||
<Edit3 className={cn("w-4 h-4", showTextLabels ? "mr-2" : "")}/>
|
||||
@@ -341,7 +341,7 @@ export function FileManagerOperations({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowDelete(true)}
|
||||
className="h-10 bg-[#18181b] border-2 border-[#303032] hover:border-[#434345] hover:bg-[#2d2d30] col-span-2"
|
||||
className="h-10 bg-dark-bg border-2 border-dark-border hover:border-dark-border-hover hover:bg-dark-hover col-span-2"
|
||||
title={t('fileManager.deleteItem')}
|
||||
>
|
||||
<Trash2 className={cn("w-4 h-4", showTextLabels ? "mr-2" : "")}/>
|
||||
@@ -349,7 +349,7 @@ export function FileManagerOperations({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#141416] border-2 border-[#373739] rounded-md p-3">
|
||||
<div className="bg-dark-bg-light border-2 border-dark-border-medium rounded-md p-3">
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<Folder className="w-4 h-4 text-blue-400 flex-shrink-0 mt-0.5"/>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -359,10 +359,10 @@ export function FileManagerOperations({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="p-0.25 bg-[#303032]"/>
|
||||
<Separator className="p-0.25 bg-dark-border"/>
|
||||
|
||||
{showUpload && (
|
||||
<Card className="bg-[#18181b] border-2 border-[#303032] p-3 sm:p-4">
|
||||
<Card className="bg-dark-bg border-2 border-dark-border p-3 sm:p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-base sm:text-lg font-semibold text-white flex items-center gap-2 mb-1">
|
||||
@@ -384,7 +384,7 @@ export function FileManagerOperations({
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="border-2 border-dashed border-[#434345] rounded-lg p-4 text-center">
|
||||
<div className="border-2 border-dashed border-dark-border-hover rounded-lg p-4 text-center">
|
||||
{uploadFile ? (
|
||||
<div className="space-y-3">
|
||||
<FileText className="w-12 h-12 text-blue-400 mx-auto"/>
|
||||
@@ -447,7 +447,7 @@ export function FileManagerOperations({
|
||||
)}
|
||||
|
||||
{showCreateFile && (
|
||||
<Card className="bg-[#18181b] border-2 border-[#303032] p-3 sm:p-4">
|
||||
<Card className="bg-dark-bg border-2 border-dark-border p-3 sm:p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-base sm:text-lg font-semibold text-white flex items-center gap-2">
|
||||
@@ -474,7 +474,7 @@ export function FileManagerOperations({
|
||||
value={newFileName}
|
||||
onChange={(e) => setNewFileName(e.target.value)}
|
||||
placeholder={t('placeholders.fileName')}
|
||||
className="bg-[#23232a] border-2 border-[#434345] text-white text-sm"
|
||||
className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCreateFile()}
|
||||
/>
|
||||
</div>
|
||||
@@ -501,7 +501,7 @@ export function FileManagerOperations({
|
||||
)}
|
||||
|
||||
{showCreateFolder && (
|
||||
<Card className="bg-[#18181b] border-2 border-[#303032] p-3">
|
||||
<Card className="bg-dark-bg border-2 border-dark-border p-3">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-base font-semibold text-white flex items-center gap-2">
|
||||
@@ -528,7 +528,7 @@ export function FileManagerOperations({
|
||||
value={newFolderName}
|
||||
onChange={(e) => setNewFolderName(e.target.value)}
|
||||
placeholder={t('placeholders.folderName')}
|
||||
className="bg-[#23232a] border-2 border-[#434345] text-white text-sm"
|
||||
className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCreateFolder()}
|
||||
/>
|
||||
</div>
|
||||
@@ -555,7 +555,7 @@ export function FileManagerOperations({
|
||||
)}
|
||||
|
||||
{showDelete && (
|
||||
<Card className="bg-[#18181b] border-2 border-[#303032] p-3">
|
||||
<Card className="bg-dark-bg border-2 border-dark-border p-3">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-base font-semibold text-white flex items-center gap-2">
|
||||
@@ -589,7 +589,7 @@ export function FileManagerOperations({
|
||||
value={deletePath}
|
||||
onChange={(e) => setDeletePath(e.target.value)}
|
||||
placeholder={t('placeholders.fullPath')}
|
||||
className="bg-[#23232a] border-2 border-[#434345] text-white text-sm"
|
||||
className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -599,7 +599,7 @@ export function FileManagerOperations({
|
||||
id="deleteIsDirectory"
|
||||
checked={deleteIsDirectory}
|
||||
onChange={(e) => setDeleteIsDirectory(e.target.checked)}
|
||||
className="rounded border-[#434345] bg-[#23232a] mt-0.5 flex-shrink-0"
|
||||
className="rounded border-dark-border-hover bg-dark-bg-button mt-0.5 flex-shrink-0"
|
||||
/>
|
||||
<label htmlFor="deleteIsDirectory" className="text-sm text-white break-words">
|
||||
{t('fileManager.thisIsDirectory')}
|
||||
@@ -629,7 +629,7 @@ export function FileManagerOperations({
|
||||
)}
|
||||
|
||||
{showRename && (
|
||||
<Card className="bg-[#18181b] border-2 border-[#303032] p-3">
|
||||
<Card className="bg-dark-bg border-2 border-dark-border p-3">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-base font-semibold text-white flex items-center gap-2">
|
||||
@@ -656,7 +656,7 @@ export function FileManagerOperations({
|
||||
value={renamePath}
|
||||
onChange={(e) => setRenamePath(e.target.value)}
|
||||
placeholder={t('placeholders.currentPath')}
|
||||
className="bg-[#23232a] border-2 border-[#434345] text-white text-sm"
|
||||
className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -668,7 +668,7 @@ export function FileManagerOperations({
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
placeholder={t('placeholders.newName')}
|
||||
className="bg-[#23232a] border-2 border-[#434345] text-white text-sm"
|
||||
className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleRename()}
|
||||
/>
|
||||
</div>
|
||||
@@ -679,7 +679,7 @@ export function FileManagerOperations({
|
||||
id="renameIsDirectory"
|
||||
checked={renameIsDirectory}
|
||||
onChange={(e) => setRenameIsDirectory(e.target.checked)}
|
||||
className="rounded border-[#434345] bg-[#23232a] mt-0.5 flex-shrink-0"
|
||||
className="rounded border-dark-border-hover bg-dark-bg-button mt-0.5 flex-shrink-0"
|
||||
/>
|
||||
<label htmlFor="renameIsDirectory" className="text-sm text-white break-words">
|
||||
{t('fileManager.thisIsDirectoryRename')}
|
||||
|
||||
@@ -21,7 +21,7 @@ export function FileManagerTabList({tabs, activeTab, setActiveTab, closeTab, onH
|
||||
<Button
|
||||
onClick={onHomeClick}
|
||||
variant="outline"
|
||||
className={`ml-1 h-8 rounded-md flex items-center !px-2 border-1 border-[#303032] ${activeTab === 'home' ? '!bg-[#1d1d1f] !text-white !border-[#2d2d30] !hover:bg-[#1d1d1f] !active:bg-[#1d1d1f] !focus:bg-[#1d1d1f] !hover:text-white !active:text-white !focus:text-white' : ''}`}
|
||||
className={`ml-1 h-8 rounded-md flex items-center !px-2 border-1 border-dark-border ${activeTab === 'home' ? '!bg-dark-bg-active !text-white !border-dark-border-active !hover:bg-dark-bg-active !active:bg-dark-bg-active !focus:bg-dark-bg-active !hover:text-white !active:text-white !focus:text-white' : ''}`}
|
||||
>
|
||||
<Home className="w-4 h-4"/>
|
||||
</Button>
|
||||
@@ -32,7 +32,7 @@ export function FileManagerTabList({tabs, activeTab, setActiveTab, closeTab, onH
|
||||
<Button
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
variant="outline"
|
||||
className={`h-8 rounded-r-none !px-2 border-1 border-[#303032] ${isActive ? '!bg-[#1d1d1f] !text-white !border-[#2d2d30] !hover:bg-[#1d1d1f] !active:bg-[#1d1d1f] !focus:bg-[#1d1d1f] !hover:text-white !active:text-white !focus:text-white' : ''}`}
|
||||
className={`h-8 rounded-r-none !px-2 border-1 border-dark-border ${isActive ? '!bg-dark-bg-active !text-white !border-dark-border-active !hover:bg-dark-bg-active !active:bg-dark-bg-active !focus:bg-dark-bg-active !hover:text-white !active:text-white !focus:text-white' : ''}`}
|
||||
>
|
||||
{tab.title}
|
||||
</Button>
|
||||
@@ -40,7 +40,7 @@ export function FileManagerTabList({tabs, activeTab, setActiveTab, closeTab, onH
|
||||
<Button
|
||||
onClick={() => closeTab(tab.id)}
|
||||
variant="outline"
|
||||
className="h-8 rounded-l-none p-0 !w-9 border-1 border-[#303032]"
|
||||
className="h-8 rounded-l-none p-0 !w-9 border-1 border-dark-border"
|
||||
>
|
||||
<X className="!w-4 !h-4" strokeWidth={2}/>
|
||||
</Button>
|
||||
|
||||
@@ -57,7 +57,7 @@ export function HostManager({onSelectView, isTopbarOpen}: HostManagerProps): Rea
|
||||
<div>
|
||||
<div className="w-full">
|
||||
<div
|
||||
className="bg-[#18181b] text-white p-4 pt-0 rounded-lg border-2 border-[#303032] flex flex-col min-h-0 overflow-hidden"
|
||||
className="bg-dark-bg text-white p-4 pt-0 rounded-lg border-2 border-dark-border flex flex-col min-h-0 overflow-hidden"
|
||||
style={{
|
||||
marginLeft: leftMarginPx,
|
||||
marginRight: 17,
|
||||
@@ -68,12 +68,12 @@ export function HostManager({onSelectView, isTopbarOpen}: HostManagerProps): Rea
|
||||
>
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange}
|
||||
className="flex-1 flex flex-col h-full min-h-0">
|
||||
<TabsList className="bg-[#18181b] border-2 border-[#303032] mt-1.5">
|
||||
<TabsList className="bg-dark-bg border-2 border-dark-border mt-1.5">
|
||||
<TabsTrigger value="host_viewer">{t('hosts.hostViewer')}</TabsTrigger>
|
||||
<TabsTrigger value="add_host">
|
||||
{editingHost ? t('hosts.editHost') : t('hosts.addHost')}
|
||||
</TabsTrigger>
|
||||
<div className="h-6 w-px bg-[#303032] mx-1"></div>
|
||||
<div className="h-6 w-px bg-dark-border mx-1"></div>
|
||||
<TabsTrigger value="credentials">{t('credentials.credentialsViewer')}</TabsTrigger>
|
||||
<TabsTrigger value="add_credential">
|
||||
{editingCredential ? t('credentials.editCredential') : t('credentials.addCredential')}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form.tsx";
|
||||
import {Input} from "@/components/ui/input.tsx";
|
||||
import {PasswordInput} from "@/components/ui/password-input.tsx";
|
||||
import {ScrollArea} from "@/components/ui/scroll-area.tsx"
|
||||
import {Separator} from "@/components/ui/separator.tsx";
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx";
|
||||
@@ -650,7 +651,7 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
||||
{folderDropdownOpen && filteredFolders.length > 0 && (
|
||||
<div
|
||||
ref={folderDropdownRef}
|
||||
className="absolute top-full left-0 z-50 mt-1 w-full bg-[#18181b] border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1"
|
||||
className="absolute top-full left-0 z-50 mt-1 w-full bg-dark-bg border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-1 p-0">
|
||||
{filteredFolders.map((folder) => (
|
||||
@@ -680,7 +681,7 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
||||
<FormLabel>{t('hosts.tags')}</FormLabel>
|
||||
<FormControl>
|
||||
<div
|
||||
className="flex flex-wrap items-center gap-1 border border-input rounded-md px-3 py-2 bg-[#222225] focus-within:ring-2 ring-ring min-h-[40px]">
|
||||
className="flex flex-wrap items-center gap-1 border border-input rounded-md px-3 py-2 bg-dark-bg-input focus-within:ring-2 ring-ring min-h-[40px]">
|
||||
{field.value.map((tag: string, idx: number) => (
|
||||
<span key={tag + idx}
|
||||
className="flex items-center bg-gray-200 text-gray-800 rounded-full px-2 py-0.5 text-xs">
|
||||
@@ -776,7 +777,7 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
||||
<FormItem>
|
||||
<FormLabel>{t('hosts.password')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder={t('placeholders.password')} {...field} />
|
||||
<PasswordInput placeholder={t('placeholders.password')} {...field} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -864,9 +865,8 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
||||
<FormItem className="col-span-8">
|
||||
<FormLabel>{t('hosts.keyPassword')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
<PasswordInput
|
||||
placeholder={t('placeholders.keyPassword')}
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -885,7 +885,7 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
||||
ref={keyTypeButtonRef}
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full justify-start text-left rounded-md px-2 py-2 bg-[#18181b] border border-input text-foreground"
|
||||
className="w-full justify-start text-left rounded-md px-2 py-2 bg-dark-bg border border-input text-foreground"
|
||||
onClick={() => setKeyTypeDropdownOpen((open) => !open)}
|
||||
>
|
||||
{keyTypeOptions.find((opt) => opt.value === field.value)?.label || t('hosts.autoDetect')}
|
||||
@@ -893,7 +893,7 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
||||
{keyTypeDropdownOpen && (
|
||||
<div
|
||||
ref={keyTypeDropdownRef}
|
||||
className="absolute bottom-full left-0 z-50 mb-1 w-full bg-[#18181b] border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1"
|
||||
className="absolute bottom-full left-0 z-50 mb-1 w-full bg-dark-bg border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-1 p-0">
|
||||
{keyTypeOptions.map((opt) => (
|
||||
@@ -902,7 +902,7 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start text-left rounded-md px-2 py-1.5 bg-[#18181b] text-foreground hover:bg-white/15 focus:bg-white/20 focus:outline-none"
|
||||
className="w-full justify-start text-left rounded-md px-2 py-1.5 bg-dark-bg text-foreground hover:bg-white/15 focus:bg-white/20 focus:outline-none"
|
||||
onClick={() => {
|
||||
field.onChange(opt.value);
|
||||
setKeyTypeDropdownOpen(false);
|
||||
@@ -1115,7 +1115,7 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
||||
ref={(el) => {
|
||||
sshConfigDropdownRefs.current[index] = el;
|
||||
}}
|
||||
className="absolute top-full left-0 z-50 mt-1 w-full bg-[#18181b] border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1"
|
||||
className="absolute top-full left-0 z-50 mt-1 w-full bg-dark-bg border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1"
|
||||
>
|
||||
<div
|
||||
className="grid grid-cols-1 gap-1 p-0">
|
||||
@@ -1269,12 +1269,9 @@ export function HostManagerEditor({editingHost, onFormSubmit}: SSHManagerHostEdi
|
||||
<footer className="shrink-0 w-full pb-0">
|
||||
<Separator className="p-0.25"/>
|
||||
<Button
|
||||
className=""
|
||||
className="translate-y-2"
|
||||
type="submit"
|
||||
variant="outline"
|
||||
style={{
|
||||
transform: 'translateY(8px)'
|
||||
}}
|
||||
>
|
||||
{editingHost ? t('hosts.updateHost') : t('hosts.addHost')}
|
||||
</Button>
|
||||
|
||||
@@ -568,7 +568,7 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleJsonImport}
|
||||
style={{display: 'none'}}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-center flex-1">
|
||||
@@ -722,7 +722,7 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleJsonImport}
|
||||
style={{display: 'none'}}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
<div className="relative mb-3">
|
||||
@@ -837,7 +837,7 @@ export function HostManagerViewer({onEditHost}: SSHManagerHostViewerProps) {
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, host)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className={`bg-[#222225] border border-input rounded-lg cursor-pointer hover:shadow-lg hover:border-blue-400/50 hover:bg-[#2a2a2d] transition-all duration-200 p-3 group relative ${
|
||||
className={`bg-dark-bg-input border border-input rounded-lg cursor-pointer hover:shadow-lg hover:border-blue-400/50 hover:bg-dark-hover-alt transition-all duration-200 p-3 group relative ${
|
||||
draggedHost?.id === host.id ? 'opacity-50 scale-95' : ''
|
||||
}`}
|
||||
onClick={() => handleEdit(host)}
|
||||
|
||||
@@ -99,7 +99,9 @@ export function Server({
|
||||
if (!currentHostConfig?.id) return;
|
||||
try {
|
||||
const data = await getServerMetricsById(currentHostConfig.id);
|
||||
if (!cancelled) setMetrics(data);
|
||||
if (!cancelled) {
|
||||
setMetrics(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch server metrics:', error);
|
||||
if (!cancelled) {
|
||||
@@ -151,7 +153,7 @@ export function Server({
|
||||
|
||||
const containerClass = embedded
|
||||
? "h-full w-full text-white overflow-hidden bg-transparent"
|
||||
: "bg-[#18181b] text-white rounded-lg border-2 border-[#303032] overflow-hidden";
|
||||
: "bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden";
|
||||
|
||||
return (
|
||||
<div style={wrapperStyle} className={containerClass}>
|
||||
@@ -213,7 +215,7 @@ export function Server({
|
||||
<Separator className="p-0.25 w-full"/>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="rounded-lg border-2 border-[#303032] m-3 bg-[#0e0e10] flex flex-row items-stretch">
|
||||
<div className="rounded-lg border-2 border-dark-border m-3 bg-dark-bg-darker flex flex-row items-stretch">
|
||||
{/* CPU */}
|
||||
<div className="flex-1 min-w-0 px-2 py-2">
|
||||
<h1 className="font-bold xt-lg flex flex-row gap-2 mb-2">
|
||||
@@ -278,7 +280,7 @@ export function Server({
|
||||
{/* SSH Tunnels */}
|
||||
{(currentHostConfig?.tunnelConnections && currentHostConfig.tunnelConnections.length > 0) && (
|
||||
<div
|
||||
className="rounded-lg border-2 border-[#303032] m-3 bg-[#0e0e10] h-[360px] overflow-hidden flex flex-col min-h-0">
|
||||
className="rounded-lg border-2 border-dark-border m-3 bg-dark-bg-darker h-[360px] overflow-hidden flex flex-col min-h-0">
|
||||
<Tunnel
|
||||
filterHostKey={(currentHostConfig?.name && currentHostConfig.name.trim() !== '') ? currentHostConfig.name : `${currentHostConfig?.username}@${currentHostConfig?.ip}`}/>
|
||||
</div>
|
||||
|
||||
@@ -13,10 +13,11 @@ interface SSHTerminalProps {
|
||||
title?: string;
|
||||
showTitle?: boolean;
|
||||
splitScreen?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
{hostConfig, isVisible, splitScreen = false},
|
||||
{hostConfig, isVisible, splitScreen = false, onClose},
|
||||
ref
|
||||
) {
|
||||
const {t} = useTranslation();
|
||||
@@ -33,6 +34,8 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const reconnectAttempts = useRef(0);
|
||||
const maxReconnectAttempts = 3;
|
||||
const isUnmountingRef = useRef(false);
|
||||
const shouldNotReconnectRef = useRef(false);
|
||||
|
||||
|
||||
const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||
@@ -71,6 +74,8 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
disconnect: () => {
|
||||
isUnmountingRef.current = true;
|
||||
shouldNotReconnectRef.current = true;
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current);
|
||||
pingIntervalRef.current = null;
|
||||
@@ -130,6 +135,11 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
}
|
||||
|
||||
function attemptReconnection() {
|
||||
// Don't attempt reconnection if component is unmounting or if we shouldn't reconnect
|
||||
if (isUnmountingRef.current || shouldNotReconnectRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reconnectAttempts.current >= maxReconnectAttempts) {
|
||||
toast.error(t('terminal.maxReconnectAttemptsReached'));
|
||||
return;
|
||||
@@ -139,7 +149,13 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
toast.info(t('terminal.reconnecting', { attempt: reconnectAttempts.current, max: maxReconnectAttempts }));
|
||||
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
// Check again if component is still mounted and should reconnect
|
||||
if (isUnmountingRef.current || shouldNotReconnectRef.current) {
|
||||
return;
|
||||
}
|
||||
if (terminal && hostConfig) {
|
||||
// Clear terminal before reconnecting
|
||||
terminal.clear();
|
||||
const cols = terminal.cols;
|
||||
const rows = terminal.rows;
|
||||
connectToHost(cols, rows);
|
||||
@@ -163,6 +179,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
webSocketRef.current = ws;
|
||||
wasDisconnectedBySSH.current = false;
|
||||
setConnectionError(null);
|
||||
shouldNotReconnectRef.current = false; // Reset reconnection flag
|
||||
|
||||
setupWebSocketListeners(ws, cols, rows);
|
||||
}
|
||||
@@ -172,8 +189,11 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) {
|
||||
ws.addEventListener('open', () => {
|
||||
setIsConnected(true);
|
||||
// Show reconnected toast if this was a reconnection attempt
|
||||
if (reconnectAttempts.current > 0) {
|
||||
toast.success(t('terminal.reconnected'));
|
||||
}
|
||||
reconnectAttempts.current = 0; // Reset on successful connection
|
||||
toast.success(t('terminal.connected'));
|
||||
|
||||
ws.send(JSON.stringify({type: 'connectToHost', data: {cols, rows, hostConfig}}));
|
||||
terminal.onData((data) => {
|
||||
@@ -200,12 +220,20 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
if (errorMessage.toLowerCase().includes('auth') ||
|
||||
errorMessage.toLowerCase().includes('password') ||
|
||||
errorMessage.toLowerCase().includes('permission') ||
|
||||
errorMessage.toLowerCase().includes('denied')) {
|
||||
errorMessage.toLowerCase().includes('denied') ||
|
||||
errorMessage.toLowerCase().includes('invalid') ||
|
||||
errorMessage.toLowerCase().includes('failed') ||
|
||||
errorMessage.toLowerCase().includes('incorrect')) {
|
||||
toast.error(t('terminal.authError', { message: errorMessage }));
|
||||
shouldNotReconnectRef.current = true; // Don't reconnect on auth errors
|
||||
// Close terminal on auth errors
|
||||
if (webSocketRef.current) {
|
||||
webSocketRef.current.close();
|
||||
}
|
||||
// Close the terminal tab immediately
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -223,13 +251,13 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
toast.error(t('terminal.error', { message: errorMessage }));
|
||||
} else if (msg.type === 'connected') {
|
||||
setIsConnected(true);
|
||||
toast.success(t('terminal.sshConnected'));
|
||||
} else if (msg.type === 'disconnected') {
|
||||
wasDisconnectedBySSH.current = true;
|
||||
setIsConnected(false);
|
||||
toast.info(t('terminal.disconnected', { message: msg.message }));
|
||||
// Attempt reconnection for disconnections
|
||||
attemptReconnection();
|
||||
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
||||
attemptReconnection();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(t('terminal.messageParseError'));
|
||||
@@ -238,8 +266,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
|
||||
ws.addEventListener('close', (event) => {
|
||||
setIsConnected(false);
|
||||
if (!wasDisconnectedBySSH.current) {
|
||||
toast.warning(t('terminal.connectionClosed'));
|
||||
if (!wasDisconnectedBySSH.current && !isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
||||
// Attempt reconnection for unexpected disconnections
|
||||
attemptReconnection();
|
||||
}
|
||||
@@ -248,9 +275,10 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
ws.addEventListener('error', (event) => {
|
||||
setIsConnected(false);
|
||||
setConnectionError(t('terminal.websocketError'));
|
||||
toast.error(t('terminal.websocketError'));
|
||||
// Attempt reconnection for WebSocket errors
|
||||
attemptReconnection();
|
||||
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
||||
attemptReconnection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -295,7 +323,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
scrollback: 10000,
|
||||
fontSize: 14,
|
||||
fontFamily: '"JetBrains Mono Nerd Font", "MesloLGS NF", "FiraCode Nerd Font", "Cascadia Code", "JetBrains Mono", Consolas, "Courier New", monospace',
|
||||
theme: {background: '#18181b', foreground: '#f7f7f7'},
|
||||
theme: {background: 'var(--color-dark-bg)', foreground: '#f7f7f7'},
|
||||
allowTransparency: true,
|
||||
convertEol: true,
|
||||
windowsMode: false,
|
||||
@@ -385,6 +413,8 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
});
|
||||
|
||||
return () => {
|
||||
isUnmountingRef.current = true;
|
||||
shouldNotReconnectRef.current = true;
|
||||
resizeObserver.disconnect();
|
||||
element?.removeEventListener('contextmenu', handleContextMenu);
|
||||
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
||||
@@ -430,29 +460,15 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
}, [splitScreen, isVisible, terminal]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full relative">
|
||||
{/* Connection Status Indicator */}
|
||||
{!isConnected && (
|
||||
<div className="absolute top-2 right-2 z-10 bg-red-500 text-white px-2 py-1 rounded text-xs">
|
||||
{t('terminal.disconnected')}
|
||||
</div>
|
||||
)}
|
||||
{isConnected && (
|
||||
<div className="absolute top-2 right-2 z-10 bg-green-500 text-white px-2 py-1 rounded text-xs">
|
||||
{t('terminal.connected')}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={xtermRef}
|
||||
className="h-full w-full m-1"
|
||||
style={{opacity: visible && isVisible ? 1 : 0, overflow: 'hidden'}}
|
||||
onClick={() => {
|
||||
if (terminal && !splitScreen) {
|
||||
terminal.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={xtermRef}
|
||||
className={`h-full w-full m-1 transition-opacity duration-200 ${visible && isVisible ? 'opacity-100' : 'opacity-0'} overflow-hidden`}
|
||||
onClick={() => {
|
||||
if (terminal && !splitScreen) {
|
||||
terminal.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -128,29 +128,13 @@ function AppContent() {
|
||||
username={username}
|
||||
>
|
||||
<div
|
||||
className="h-screen w-full"
|
||||
style={{
|
||||
visibility: showTerminalView ? "visible" : "hidden",
|
||||
pointerEvents: showTerminalView ? "auto" : "none",
|
||||
height: showTerminalView ? "100vh" : 0,
|
||||
width: showTerminalView ? "100%" : 0,
|
||||
position: showTerminalView ? "static" : "absolute",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
className={`h-screen w-full ${showTerminalView ? "visible pointer-events-auto static overflow-hidden" : "invisible pointer-events-none absolute h-0 w-0"}`}
|
||||
>
|
||||
<AppView isTopbarOpen={isTopbarOpen} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="h-screen w-full"
|
||||
style={{
|
||||
visibility: showHome ? "visible" : "hidden",
|
||||
pointerEvents: showHome ? "auto" : "none",
|
||||
height: showHome ? "100vh" : 0,
|
||||
width: showHome ? "100%" : 0,
|
||||
position: showHome ? "static" : "absolute",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
className={`h-screen w-full ${showHome ? "visible pointer-events-auto static overflow-hidden" : "invisible pointer-events-none absolute h-0 w-0"}`}
|
||||
>
|
||||
<Homepage
|
||||
onSelectView={handleSelectView}
|
||||
@@ -162,43 +146,19 @@ function AppContent() {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="h-screen w-full"
|
||||
style={{
|
||||
visibility: showSshManager ? "visible" : "hidden",
|
||||
pointerEvents: showSshManager ? "auto" : "none",
|
||||
height: showSshManager ? "100vh" : 0,
|
||||
width: showSshManager ? "100%" : 0,
|
||||
position: showSshManager ? "static" : "absolute",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
className={`h-screen w-full ${showSshManager ? "visible pointer-events-auto static overflow-hidden" : "invisible pointer-events-none absolute h-0 w-0"}`}
|
||||
>
|
||||
<HostManager onSelectView={handleSelectView} isTopbarOpen={isTopbarOpen} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="h-screen w-full"
|
||||
style={{
|
||||
visibility: showAdmin ? "visible" : "hidden",
|
||||
pointerEvents: showAdmin ? "auto" : "none",
|
||||
height: showAdmin ? "100vh" : 0,
|
||||
width: showAdmin ? "100%" : 0,
|
||||
position: showAdmin ? "static" : "absolute",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
className={`h-screen w-full ${showAdmin ? "visible pointer-events-auto static overflow-hidden" : "invisible pointer-events-none absolute h-0 w-0"}`}
|
||||
>
|
||||
<AdminSettings isTopbarOpen={isTopbarOpen} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="h-screen w-full"
|
||||
style={{
|
||||
visibility: showProfile ? "visible" : "hidden",
|
||||
pointerEvents: showProfile ? "auto" : "none",
|
||||
height: showProfile ? "100vh" : 0,
|
||||
width: showProfile ? "100%" : 0,
|
||||
position: showProfile ? "static" : "absolute",
|
||||
overflow: "auto",
|
||||
}}
|
||||
className={`h-screen w-full ${showProfile ? "visible pointer-events-auto static overflow-auto" : "invisible pointer-events-none absolute h-0 w-0"}`}
|
||||
>
|
||||
<UserProfile isTopbarOpen={isTopbarOpen} />
|
||||
</div>
|
||||
|
||||
@@ -107,64 +107,51 @@ export function Homepage({
|
||||
}}>
|
||||
<div className="flex flex-row items-center justify-center gap-8 relative z-10">
|
||||
<div className="flex flex-col items-center gap-6 w-[400px]">
|
||||
<div
|
||||
className="text-center bg-[#18181b] border-2 border-[#303032] rounded-lg p-6 w-full shadow-lg">
|
||||
<h3 className="text-xl font-bold mb-3 text-white">{t('homepage.loggedInTitle')}</h3>
|
||||
<p className="text-gray-300 leading-relaxed">
|
||||
{t('homepage.loggedInMessage')}
|
||||
</p>
|
||||
</div>
|
||||
<HomepageUpdateLog
|
||||
loggedIn={loggedIn}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-[#303032] text-gray-300 hover:text-white hover:bg-[#18181b] transition-colors"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() => window.open('https://github.com/LukeGus/Termix', '_blank')}
|
||||
>
|
||||
GitHub
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-[#303032]"></div>
|
||||
<div className="w-px h-4 bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-[#303032] text-gray-300 hover:text-white hover:bg-[#18181b] transition-colors"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() => window.open('https://github.com/LukeGus/Termix/issues/new', '_blank')}
|
||||
>
|
||||
Feedback
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-[#303032]"></div>
|
||||
<div className="w-px h-4 bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-[#303032] text-gray-300 hover:text-white hover:bg-[#18181b] transition-colors"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() => window.open('https://discord.com/invite/jVQGdvHDrf', '_blank')}
|
||||
>
|
||||
Discord
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-[#303032]"></div>
|
||||
<div className="w-px h-4 bg-dark-border"></div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-sm border-[#303032] text-gray-300 hover:text-white hover:bg-[#18181b] transition-colors"
|
||||
className="text-sm border-dark-border text-gray-300 hover:text-white hover:bg-dark-bg transition-colors"
|
||||
onClick={() => window.open('https://github.com/sponsors/LukeGus', '_blank')}
|
||||
>
|
||||
Donate
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HomepageUpdateLog
|
||||
loggedIn={loggedIn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<HomepageAlertManager
|
||||
userId={userId}
|
||||
loggedIn={loggedIn}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import {Eye, EyeOff} from "lucide-react";
|
||||
import {cn} from "@/lib/utils.ts";
|
||||
import {Button} from "@/components/ui/button.tsx";
|
||||
import {Input} from "@/components/ui/input.tsx";
|
||||
import {PasswordInput} from "@/components/ui/password-input.tsx";
|
||||
import {Label} from "@/components/ui/label.tsx";
|
||||
import {Alert, AlertTitle, AlertDescription} from "@/components/ui/alert.tsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
@@ -411,7 +412,7 @@ export function HomepageAuth({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-[420px] max-w-full p-6 flex flex-col bg-[#18181b] border-2 border-[#303032] rounded-md ${className || ''}`}
|
||||
className={`w-[420px] max-w-full p-6 flex flex-col bg-dark-bg border-2 border-dark-border rounded-md ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{dbError && (
|
||||
@@ -686,51 +687,27 @@ export function HomepageAuth({
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="new-password">{t('auth.newPassword')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="new-password"
|
||||
type={visibility.resetNew ? "text" : "password"}
|
||||
required
|
||||
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200 pr-10"
|
||||
value={newPassword}
|
||||
onChange={e => setNewPassword(e.target.value)}
|
||||
disabled={resetLoading}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => toggleVisibility('resetNew')}
|
||||
className="absolute right-0 top-0 h-full px-3 py-2"
|
||||
>
|
||||
{visibility.resetNew ? <EyeOff className="h-4 w-4 text-muted-foreground" /> : <Eye className="h-4 w-4 text-muted-foreground" />}
|
||||
</Button>
|
||||
</div>
|
||||
<PasswordInput
|
||||
id="new-password"
|
||||
required
|
||||
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200"
|
||||
value={newPassword}
|
||||
onChange={e => setNewPassword(e.target.value)}
|
||||
disabled={resetLoading}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="confirm-password">{t('auth.confirmNewPassword')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="confirm-password"
|
||||
type={visibility.resetConfirm ? "text" : "password"}
|
||||
required
|
||||
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200 pr-10"
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
disabled={resetLoading}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => toggleVisibility('resetConfirm')}
|
||||
className="absolute right-0 top-0 h-full px-3 py-2"
|
||||
>
|
||||
{visibility.resetConfirm ? <EyeOff className="h-4 w-4 text-muted-foreground" /> : <Eye className="h-4 w-4 text-muted-foreground" />}
|
||||
</Button>
|
||||
</div>
|
||||
<PasswordInput
|
||||
id="confirm-password"
|
||||
required
|
||||
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200"
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
disabled={resetLoading}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -775,48 +752,24 @@ export function HomepageAuth({
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="password">{t('common.password')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={visibility.password ? "text" : "password"}
|
||||
required
|
||||
className="h-11 text-base pr-10"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
disabled={loading || internalLoggedIn}/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => toggleVisibility('password')}
|
||||
className="absolute right-0 top-0 h-full px-3 py-2"
|
||||
>
|
||||
{visibility.password ? <EyeOff className="h-4 w-4 text-muted-foreground" /> : <Eye className="h-4 w-4 text-muted-foreground" />}
|
||||
</Button>
|
||||
</div>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
required
|
||||
className="h-11 text-base"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
disabled={loading || internalLoggedIn}/>
|
||||
</div>
|
||||
{tab === "signup" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="signup-confirm-password">{t('common.confirmPassword')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="signup-confirm-password"
|
||||
type={visibility.signupConfirm ? "text" : "password"}
|
||||
required
|
||||
className="h-11 text-base pr-10"
|
||||
value={signupConfirmPassword}
|
||||
onChange={e => setSignupConfirmPassword(e.target.value)}
|
||||
disabled={loading || internalLoggedIn}/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => toggleVisibility('signupConfirm')}
|
||||
className="absolute right-0 top-0 h-full px-3 py-2"
|
||||
>
|
||||
{visibility.signupConfirm ? <EyeOff className="h-4 w-4 text-muted-foreground" /> : <Eye className="h-4 w-4 text-muted-foreground" />}
|
||||
</Button>
|
||||
</div>
|
||||
<PasswordInput
|
||||
id="signup-confirm-password"
|
||||
required
|
||||
className="h-11 text-base"
|
||||
value={signupConfirmPassword}
|
||||
onChange={e => setSignupConfirmPassword(e.target.value)}
|
||||
disabled={loading || internalLoggedIn}/>
|
||||
</div>
|
||||
)}
|
||||
<Button type="submit" className="w-full h-11 mt-2 text-base font-semibold"
|
||||
@@ -839,7 +792,7 @@ export function HomepageAuth({
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="mt-6 pt-4 border-t border-[#303032]">
|
||||
<div className="mt-6 pt-4 border-t border-dark-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-sm text-muted-foreground">{t('common.language')}</Label>
|
||||
|
||||
@@ -90,14 +90,14 @@ export function HomepageUpdateLog({loggedIn}: HomepageUpdateLogProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-[400px] h-[600px] flex flex-col border-2 border-[#303032] rounded-lg bg-[#18181b] p-4 shadow-lg">
|
||||
<div className="w-[400px] h-[600px] flex flex-col border-2 border-dark-border rounded-lg bg-dark-bg p-4 shadow-lg">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-3 text-white">{t('common.updatesAndReleases')}</h3>
|
||||
|
||||
<Separator className="p-0.25 mt-3 mb-3 bg-[#303032]"/>
|
||||
<Separator className="p-0.25 mt-3 mb-3 bg-dark-border"/>
|
||||
|
||||
{versionInfo && versionInfo.status === 'requires_update' && (
|
||||
<Alert className="bg-[#0e0e10] border-[#303032] text-white">
|
||||
<Alert className="bg-dark-bg-darker border-dark-border text-white">
|
||||
<AlertTitle className="text-white">{t('common.updateAvailable')}</AlertTitle>
|
||||
<AlertDescription className="text-gray-300">
|
||||
{t('common.newVersionAvailable', { version: versionInfo.version })}
|
||||
@@ -107,7 +107,7 @@ export function HomepageUpdateLog({loggedIn}: HomepageUpdateLogProps) {
|
||||
</div>
|
||||
|
||||
{versionInfo && versionInfo.status === 'requires_update' && (
|
||||
<Separator className="p-0.25 mt-3 mb-3 bg-[#303032]"/>
|
||||
<Separator className="p-0.25 mt-3 mb-3 bg-dark-border"/>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto space-y-3 pr-2">
|
||||
@@ -127,7 +127,7 @@ export function HomepageUpdateLog({loggedIn}: HomepageUpdateLogProps) {
|
||||
{releases?.items.map((release) => (
|
||||
<div
|
||||
key={release.id}
|
||||
className="border border-[#303032] rounded-lg p-3 hover:bg-[#0e0e10] transition-colors cursor-pointer bg-[#0e0e10]/50"
|
||||
className="border border-dark-border rounded-lg p-3 hover:bg-dark-bg-darker transition-colors cursor-pointer bg-dark-bg-darker/50"
|
||||
onClick={() => window.open(release.link, '_blank')}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
@@ -159,7 +159,7 @@ export function HomepageUpdateLog({loggedIn}: HomepageUpdateLogProps) {
|
||||
))}
|
||||
|
||||
{releases && releases.items.length === 0 && !loading && (
|
||||
<Alert className="bg-[#0e0e10] border-[#303032] text-gray-300">
|
||||
<Alert className="bg-dark-bg-darker border-dark-border text-gray-300">
|
||||
<AlertTitle className="text-gray-300">{t('common.noReleases')}</AlertTitle>
|
||||
<AlertDescription className="text-gray-400">
|
||||
{t('common.noReleasesFound')}
|
||||
|
||||
@@ -14,7 +14,7 @@ interface TerminalViewProps {
|
||||
}
|
||||
|
||||
export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactElement {
|
||||
const {tabs, currentTab, allSplitScreenTab} = useTabs() as any;
|
||||
const {tabs, currentTab, allSplitScreenTab, removeTab} = useTabs() as any;
|
||||
const {state: sidebarState} = useSidebar();
|
||||
|
||||
const terminalTabs = tabs.filter((tab: any) => tab.type === 'terminal' || tab.type === 'server' || tab.type === 'file_manager');
|
||||
@@ -141,7 +141,7 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{position: 'absolute', inset: 0, zIndex: 1}}>
|
||||
<div className="absolute inset-0 z-[1]">
|
||||
{terminalTabs.map((t: any) => {
|
||||
const hasStyle = !!styles[t.id];
|
||||
const isVisible = hasStyle || (allSplitScreenTab.length === 0 && t.id === currentTab);
|
||||
@@ -155,7 +155,7 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
||||
const effectiveVisible = isVisible && ready;
|
||||
return (
|
||||
<div key={t.id} style={finalStyle}>
|
||||
<div className="absolute inset-0 rounded-md bg-[#18181b]">
|
||||
<div className="absolute inset-0 rounded-md bg-dark-bg">
|
||||
{t.type === 'terminal' ? (
|
||||
<Terminal
|
||||
ref={t.terminalRef}
|
||||
@@ -164,6 +164,7 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
||||
title={t.title}
|
||||
showTitle={false}
|
||||
splitScreen={allSplitScreenTab.length > 0}
|
||||
onClose={() => removeTab(t.id)}
|
||||
/>
|
||||
) : t.type === 'server' ? (
|
||||
<ServerView
|
||||
@@ -177,6 +178,7 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
||||
<FileManager
|
||||
embedded
|
||||
initialHost={t.hostConfig}
|
||||
onClose={() => removeTab(t.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -193,7 +195,7 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
||||
variant="ghost"
|
||||
onClick={onClick}
|
||||
aria-label="Reset split sizes"
|
||||
className="absolute top-0 right-0 h-[28px] w-[28px] !rounded-none border-l-1 border-b-1 border-[#222224] bg-[#1b1b1e] hover:bg-[#232327] text-white flex items-center justify-center p-0"
|
||||
className="absolute top-0 right-0 h-[28px] w-[28px] !rounded-none border-l-1 border-b-1 border-dark-border-panel bg-dark-bg-panel hover:bg-dark-bg-panel-hover text-white flex items-center justify-center p-0"
|
||||
>
|
||||
<RefreshCcw className="h-4 w-4"/>
|
||||
</Button>
|
||||
@@ -210,41 +212,21 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
||||
const layoutTabs = [mainTab, ...splitTabs.filter((t: any) => t && t.id !== (mainTab && (mainTab as any).id))].filter(Boolean) as any[];
|
||||
if (allSplitScreenTab.length === 0) return null;
|
||||
|
||||
const handleStyle = {pointerEvents: 'auto', zIndex: 12, background: '#303032'} as React.CSSProperties;
|
||||
const handleStyle = {pointerEvents: 'auto', zIndex: 12, background: 'var(--color-dark-border)'} as React.CSSProperties;
|
||||
const commonGroupProps = {onLayout: scheduleMeasureAndFit, onResize: scheduleMeasureAndFit} as any;
|
||||
|
||||
if (layoutTabs.length === 2) {
|
||||
const [a, b] = layoutTabs as any[];
|
||||
return (
|
||||
<div style={{position: 'absolute', inset: 0, zIndex: 10, pointerEvents: 'none'}}>
|
||||
<div className="absolute inset-0 z-[10] pointer-events-none">
|
||||
<ResizablePrimitive.PanelGroup key={resetKey} direction="horizontal"
|
||||
className="h-full w-full" {...commonGroupProps}>
|
||||
<ResizablePanel defaultSize={50} minSize={20} className="!overflow-hidden h-full w-full"
|
||||
id={`panel-${a.id}`} order={1}>
|
||||
<div ref={el => {
|
||||
panelRefs.current[String(a.id)] = el;
|
||||
}} style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'transparent',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div style={{
|
||||
background: '#1b1b1e',
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
height: HEADER_H,
|
||||
lineHeight: `${HEADER_H}px`,
|
||||
padding: '0 10px',
|
||||
borderBottom: '1px solid #222224',
|
||||
letterSpacing: 1,
|
||||
margin: 0,
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 11,
|
||||
position: 'relative'
|
||||
}}>{a.title}</div>
|
||||
}} className="h-full w-full flex flex-col bg-transparent relative">
|
||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{a.title}</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle style={handleStyle}/>
|
||||
@@ -252,28 +234,8 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
||||
id={`panel-${b.id}`} order={2}>
|
||||
<div ref={el => {
|
||||
panelRefs.current[String(b.id)] = el;
|
||||
}} style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'transparent',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div style={{
|
||||
background: '#1b1b1e',
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
height: HEADER_H,
|
||||
lineHeight: `${HEADER_H}px`,
|
||||
padding: '0 10px',
|
||||
borderBottom: '1px solid #222224',
|
||||
letterSpacing: 1,
|
||||
margin: 0,
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 11,
|
||||
position: 'relative'
|
||||
}}>
|
||||
}} className="h-full w-full flex flex-col bg-transparent relative">
|
||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
|
||||
{b.title}
|
||||
<ResetButton onClick={handleReset}/>
|
||||
</div>
|
||||
@@ -286,7 +248,7 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
||||
if (layoutTabs.length === 3) {
|
||||
const [a, b, c] = layoutTabs as any[];
|
||||
return (
|
||||
<div style={{position: 'absolute', inset: 0, zIndex: 10, pointerEvents: 'none'}}>
|
||||
<div className="absolute inset-0 z-[10] pointer-events-none">
|
||||
<ResizablePrimitive.PanelGroup key={resetKey} direction="vertical" className="h-full w-full"
|
||||
id="main-vertical" {...commonGroupProps}>
|
||||
<ResizablePanel defaultSize={50} minSize={20} className="!overflow-hidden h-full w-full"
|
||||
@@ -297,27 +259,8 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
||||
id={`panel-${a.id}`} order={1}>
|
||||
<div ref={el => {
|
||||
panelRefs.current[String(a.id)] = el;
|
||||
}} style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div style={{
|
||||
background: '#1b1b1e',
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
height: HEADER_H,
|
||||
lineHeight: `${HEADER_H}px`,
|
||||
padding: '0 10px',
|
||||
borderBottom: '1px solid #222224',
|
||||
letterSpacing: 1,
|
||||
margin: 0,
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 11,
|
||||
position: 'relative'
|
||||
}}>{a.title}</div>
|
||||
}} className="h-full w-full flex flex-col relative">
|
||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{a.title}</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle style={handleStyle}/>
|
||||
@@ -325,27 +268,8 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
||||
id={`panel-${b.id}`} order={2}>
|
||||
<div ref={el => {
|
||||
panelRefs.current[String(b.id)] = el;
|
||||
}} style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div style={{
|
||||
background: '#1b1b1e',
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
height: HEADER_H,
|
||||
lineHeight: `${HEADER_H}px`,
|
||||
padding: '0 10px',
|
||||
borderBottom: '1px solid #222224',
|
||||
letterSpacing: 1,
|
||||
margin: 0,
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 11,
|
||||
position: 'relative'
|
||||
}}>
|
||||
}} className="h-full w-full flex flex-col relative">
|
||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
|
||||
{b.title}
|
||||
<ResetButton onClick={handleReset}/>
|
||||
</div>
|
||||
@@ -358,27 +282,8 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
||||
id="bottom-panel" order={2}>
|
||||
<div ref={el => {
|
||||
panelRefs.current[String(c.id)] = el;
|
||||
}} style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div style={{
|
||||
background: '#1b1b1e',
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
height: HEADER_H,
|
||||
lineHeight: `${HEADER_H}px`,
|
||||
padding: '0 10px',
|
||||
borderBottom: '1px solid #222224',
|
||||
letterSpacing: 1,
|
||||
margin: 0,
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 11,
|
||||
position: 'relative'
|
||||
}}>{c.title}</div>
|
||||
}} className="h-full w-full flex flex-col relative">
|
||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{c.title}</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePrimitive.PanelGroup>
|
||||
@@ -388,66 +293,28 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
||||
if (layoutTabs.length === 4) {
|
||||
const [a, b, c, d] = layoutTabs as any[];
|
||||
return (
|
||||
<div style={{position: 'absolute', inset: 0, zIndex: 10, pointerEvents: 'none'}}>
|
||||
<div className="absolute inset-0 z-[10] pointer-events-none">
|
||||
<ResizablePrimitive.PanelGroup key={resetKey} direction="vertical" className="h-full w-full"
|
||||
id="main-vertical" {...commonGroupProps}>
|
||||
<ResizablePanel defaultSize={50} minSize={20} className="!overflow-hidden h-full w-full"
|
||||
id="top-panel" order={1}>
|
||||
<ResizablePanelGroup key={`top-${resetKey}`} direction="horizontal"
|
||||
className="h-full w-full" id="top-horizontal" {...commonGroupProps}>
|
||||
<ResizablePanel defaultSize={50} minSize={20} className="!overflow-hidden h_full w_full"
|
||||
<ResizablePanel defaultSize={50} minSize={20} className="!overflow-hidden h-full w-full"
|
||||
id={`panel-${a.id}`} order={1}>
|
||||
<div ref={el => {
|
||||
panelRefs.current[String(a.id)] = el;
|
||||
}} style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div style={{
|
||||
background: '#1b1b1e',
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
height: HEADER_H,
|
||||
lineHeight: `${HEADER_H}px`,
|
||||
padding: '0 10px',
|
||||
borderBottom: '1px solid #222224',
|
||||
letterSpacing: 1,
|
||||
margin: 0,
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 11,
|
||||
position: 'relative'
|
||||
}}>{a.title}</div>
|
||||
}} className="h-full w-full flex flex-col relative">
|
||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{a.title}</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle style={handleStyle}/>
|
||||
<ResizablePanel defaultSize={50} minSize={20} className="!overflow-hidden h_full w_full"
|
||||
<ResizablePanel defaultSize={50} minSize={20} className="!overflow-hidden h-full w-full"
|
||||
id={`panel-${b.id}`} order={2}>
|
||||
<div ref={el => {
|
||||
panelRefs.current[String(b.id)] = el;
|
||||
}} style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div style={{
|
||||
background: '#1b1b1e',
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
height: HEADER_H,
|
||||
lineHeight: `${HEADER_H}px`,
|
||||
padding: '0 10px',
|
||||
borderBottom: '1px solid #222224',
|
||||
letterSpacing: 1,
|
||||
margin: 0,
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 11,
|
||||
position: 'relative'
|
||||
}}>
|
||||
}} className="h-full w-full flex flex-col relative">
|
||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">
|
||||
{b.title}
|
||||
<ResetButton onClick={handleReset}/>
|
||||
</div>
|
||||
@@ -456,63 +323,25 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
||||
</ResizablePanelGroup>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle style={handleStyle}/>
|
||||
<ResizablePanel defaultSize={50} minSize={20} className="!overflow-hidden h_full w_full"
|
||||
<ResizablePanel defaultSize={50} minSize={20} className="!overflow-hidden h-full w-full"
|
||||
id="bottom-panel" order={2}>
|
||||
<ResizablePanelGroup key={`bottom-${resetKey}`} direction="horizontal"
|
||||
className="h-full w-full" id="bottom-horizontal" {...commonGroupProps}>
|
||||
<ResizablePanel defaultSize={50} minSize={20} className="!overflow-hidden h_full w_full"
|
||||
<ResizablePanel defaultSize={50} minSize={20} className="!overflow-hidden h-full w-full"
|
||||
id={`panel-${c.id}`} order={1}>
|
||||
<div ref={el => {
|
||||
panelRefs.current[String(c.id)] = el;
|
||||
}} style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div style={{
|
||||
background: '#1b1b1e',
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
height: HEADER_H,
|
||||
lineHeight: `${HEADER_H}px`,
|
||||
padding: '0 10px',
|
||||
borderBottom: '1px solid #222224',
|
||||
letterSpacing: 1,
|
||||
margin: 0,
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 11,
|
||||
position: 'relative'
|
||||
}}>{c.title}</div>
|
||||
}} className="h-full w-full flex flex-col relative">
|
||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{c.title}</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle style={handleStyle}/>
|
||||
<ResizablePanel defaultSize={50} minSize={20} className="!overflow-hidden h_full w_full"
|
||||
<ResizablePanel defaultSize={50} minSize={20} className="!overflow-hidden h-full w-full"
|
||||
id={`panel-${d.id}`} order={2}>
|
||||
<div ref={el => {
|
||||
panelRefs.current[String(d.id)] = el;
|
||||
}} style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div style={{
|
||||
background: '#1b1b1e',
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
height: HEADER_H,
|
||||
lineHeight: `${HEADER_H}px`,
|
||||
padding: '0 10px',
|
||||
borderBottom: '1px solid #222224',
|
||||
letterSpacing: 1,
|
||||
margin: 0,
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 11,
|
||||
position: 'relative'
|
||||
}}>{d.title}</div>
|
||||
}} className="h-full w-full flex flex-col relative">
|
||||
<div className="bg-dark-bg-panel text-white text-[13px] h-[28px] leading-[28px] px-[10px] border-b border-dark-border-panel tracking-[1px] m-0 pointer-events-auto z-[11] relative">{d.title}</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
@@ -535,10 +364,9 @@ export function AppView({isTopbarOpen = true}: TerminalViewProps): React.ReactEl
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="border-2 border-[#303032] rounded-lg overflow-hidden overflow-x-hidden"
|
||||
className="border-2 border-dark-border rounded-lg overflow-hidden overflow-x-hidden relative"
|
||||
style={{
|
||||
position: 'relative',
|
||||
background: (isFileManager && !isSplitScreen) ? '#09090b' : '#18181b',
|
||||
background: (isFileManager && !isSplitScreen) ? 'var(--color-dark-bg-darkest)' : 'var(--color-dark-bg)',
|
||||
marginLeft: leftMarginPx,
|
||||
marginRight: 17,
|
||||
marginTop: topMarginPx,
|
||||
|
||||
@@ -43,9 +43,8 @@ export function FolderCard({folderName, hosts, isFirst, isLast}: FolderCardProps
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-[#0e0e10] border-2 border-[#303032] rounded-lg overflow-hidden"
|
||||
style={{padding: '0', margin: '0'}}>
|
||||
<div className={`px-4 py-3 relative ${isExpanded ? 'border-b-2' : ''} bg-[#131316]`}>
|
||||
<div className="bg-dark-bg-darker border-2 border-dark-border rounded-lg overflow-hidden p-0 m-0">
|
||||
<div className={`px-4 py-3 relative ${isExpanded ? 'border-b-2' : ''} bg-dark-bg-header`}>
|
||||
<div className="flex gap-2 pr-10">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<Folder size={16} strokeWidth={3}/>
|
||||
|
||||
@@ -58,13 +58,13 @@ export function Host({host}: HostProps): React.ReactElement {
|
||||
{host.name || host.ip}
|
||||
</p>
|
||||
<ButtonGroup className="flex-shrink-0">
|
||||
<Button variant="outline" className="!px-2 border-1 border-[#303032]" onClick={handleServerClick}>
|
||||
<Button variant="outline" className="!px-2 border-1 border-dark-border" onClick={handleServerClick}>
|
||||
<Server/>
|
||||
</Button>
|
||||
{host.enableTerminal && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="!px-2 border-1 border-[#303032]"
|
||||
className="!px-2 border-1 border-dark-border"
|
||||
onClick={handleTerminalClick}
|
||||
>
|
||||
<Terminal/>
|
||||
@@ -75,7 +75,7 @@ export function Host({host}: HostProps): React.ReactElement {
|
||||
{hasTags && (
|
||||
<div className="flex flex-wrap items-center gap-2 mt-1">
|
||||
{tags.map((tag: string) => (
|
||||
<div key={tag} className="bg-[#18181b] border-1 border-[#303032] pl-2 pr-2 rounded-[10px]">
|
||||
<div key={tag} className="bg-dark-bg border-1 border-dark-border pl-2 pr-2 rounded-[10px]">
|
||||
<p className="text-sm">{tag}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from "@/components/ui/sheet.tsx";
|
||||
import {Checkbox} from "@/components/ui/checkbox.tsx";
|
||||
import {Input} from "@/components/ui/input.tsx";
|
||||
import {PasswordInput} from "@/components/ui/password-input.tsx";
|
||||
import {Label} from "@/components/ui/label.tsx";
|
||||
import {Button} from "@/components/ui/button.tsx";
|
||||
import {Alert, AlertTitle, AlertDescription} from "@/components/ui/alert.tsx";
|
||||
@@ -316,7 +317,7 @@ export function LeftSidebar({
|
||||
<Separator className="p-0.25"/>
|
||||
<SidebarContent>
|
||||
<SidebarGroup className="!m-0 !p-0 !-mb-2">
|
||||
<Button className="m-2 flex flex-row font-semibold border-2 !border-[#303032]" variant="outline"
|
||||
<Button className="m-2 flex flex-row font-semibold border-2 !border-dark-border" variant="outline"
|
||||
onClick={openSshManagerTab} disabled={!!sshManagerTab || isSplitScreenActive}
|
||||
title={sshManagerTab ? t('interface.sshManagerAlreadyOpen') : isSplitScreenActive ? t('interface.disabledDuringSplitScreen') : undefined}>
|
||||
<HardDrive strokeWidth="2.5"/>
|
||||
@@ -325,12 +326,12 @@ export function LeftSidebar({
|
||||
</SidebarGroup>
|
||||
<Separator className="p-0.25"/>
|
||||
<SidebarGroup className="flex flex-col gap-y-2 !-mt-2">
|
||||
<div className="!bg-[#222225] rounded-lg">
|
||||
<div className="!bg-dark-bg-input rounded-lg">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder={t('placeholders.searchHostsAny')}
|
||||
className="w-full h-8 text-sm border-2 !bg-[#222225] border-[#303032] rounded-md"
|
||||
className="w-full h-8 text-sm border-2 !bg-dark-bg-input border-dark-border rounded-md"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
@@ -371,7 +372,6 @@ export function LeftSidebar({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
className="data-[state=open]:opacity-90 w-full"
|
||||
style={{width: '100%'}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<User2/> {username ? username : t('common.logout')}
|
||||
@@ -437,40 +437,28 @@ export function LeftSidebar({
|
||||
{!isSidebarOpen && (
|
||||
<div
|
||||
onClick={() => setIsSidebarOpen(true)}
|
||||
className="absolute top-0 left-0 w-[10px] h-full bg-[#18181b] cursor-pointer z-20 flex items-center justify-center rounded-tr-md rounded-br-md">
|
||||
className="absolute top-0 left-0 w-[10px] h-full bg-dark-bg cursor-pointer z-20 flex items-center justify-center rounded-tr-md rounded-br-md">
|
||||
<ChevronRight size={10}/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{deleteAccountOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-[999999] flex"
|
||||
className="fixed top-0 left-0 right-0 bottom-0 z-[999999] pointer-events-auto isolate"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 999999,
|
||||
pointerEvents: 'auto',
|
||||
isolation: 'isolate',
|
||||
transform: 'translateZ(0)',
|
||||
willChange: 'z-index'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-[400px] h-full bg-[#18181b] border-r-2 border-[#303032] flex flex-col shadow-2xl"
|
||||
className="w-[400px] h-full bg-dark-bg border-r-2 border-dark-border flex flex-col shadow-2xl relative isolate z-[9999999]"
|
||||
style={{
|
||||
backgroundColor: '#18181b',
|
||||
boxShadow: '4px 0 20px rgba(0, 0, 0, 0.5)',
|
||||
zIndex: 9999999,
|
||||
position: 'relative',
|
||||
isolation: 'isolate',
|
||||
transform: 'translateZ(0)'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 border-b border-[#303032]">
|
||||
<div className="flex items-center justify-between p-4 border-b border-dark-border">
|
||||
<h2 className="text-lg font-semibold text-white">{t('leftSidebar.deleteAccount')}</h2>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -510,9 +498,8 @@ export function LeftSidebar({
|
||||
<form onSubmit={handleDeleteAccount} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="delete-password">{t('leftSidebar.confirmPassword')}</Label>
|
||||
<Input
|
||||
<PasswordInput
|
||||
id="delete-password"
|
||||
type="password"
|
||||
value={deletePassword}
|
||||
onChange={(e) => setDeletePassword(e.target.value)}
|
||||
placeholder={t('placeholders.confirmPassword')}
|
||||
@@ -553,7 +540,7 @@ export function LeftSidebar({
|
||||
setDeletePassword("");
|
||||
setDeleteError(null);
|
||||
}}
|
||||
style={{cursor: 'pointer'}}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -43,7 +43,7 @@ export function Tab({
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`!px-2 border-1 border-[#303032] ${isActive ? '!bg-[#1d1d1f] !text-white !border-[#2d2d30]' : ''}`}
|
||||
className={`!px-2 border-1 border-dark-border ${isActive ? '!bg-dark-bg-active !text-white !border-dark-border-active' : ''}`}
|
||||
onClick={onActivate}
|
||||
disabled={disableActivate}
|
||||
>
|
||||
@@ -59,7 +59,7 @@ export function Tab({
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`!px-2 border-1 border-[#303032] ${isActive ? '!bg-[#1d1d1f] !text-white !border-[#2d2d30]' : ''}`}
|
||||
className={`!px-2 border-1 border-dark-border ${isActive ? '!bg-dark-bg-active !text-white !border-dark-border-active' : ''}`}
|
||||
onClick={onActivate}
|
||||
disabled={disableActivate}
|
||||
>
|
||||
@@ -70,7 +70,7 @@ export function Tab({
|
||||
{canSplit && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="!px-2 border-1 border-[#303032]"
|
||||
className="!px-2 border-1 border-dark-border"
|
||||
onClick={onSplit}
|
||||
disabled={disableSplit}
|
||||
title={disableSplit ? t('nav.cannotSplitTab') : t('nav.splitScreen')}
|
||||
@@ -81,7 +81,7 @@ export function Tab({
|
||||
{canClose && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="!px-2 border-1 border-[#303032]"
|
||||
className="!px-2 border-1 border-dark-border"
|
||||
onClick={onClose}
|
||||
disabled={disableClose}
|
||||
>
|
||||
@@ -97,7 +97,7 @@ export function Tab({
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`!px-2 border-1 border-[#303032] ${isActive ? '!bg-[#1d1d1f] !text-white !border-[#2d2d30]' : ''}`}
|
||||
className={`!px-2 border-1 border-dark-border ${isActive ? '!bg-dark-bg-active !text-white !border-dark-border-active' : ''}`}
|
||||
onClick={onActivate}
|
||||
disabled={disableActivate}
|
||||
>
|
||||
@@ -105,7 +105,7 @@ export function Tab({
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="!px-2 border-1 border-[#303032]"
|
||||
className="!px-2 border-1 border-dark-border"
|
||||
onClick={onClose}
|
||||
disabled={disableClose}
|
||||
>
|
||||
@@ -120,7 +120,7 @@ export function Tab({
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`!px-2 border-1 border-[#303032] ${isActive ? '!bg-[#1d1d1f] !text-white !border-[#2d2d30]' : ''}`}
|
||||
className={`!px-2 border-1 border-dark-border ${isActive ? '!bg-dark-bg-active !text-white !border-dark-border-active' : ''}`}
|
||||
onClick={onActivate}
|
||||
disabled={disableActivate}
|
||||
>
|
||||
@@ -128,7 +128,7 @@ export function Tab({
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="!px-2 border-1 border-[#303032]"
|
||||
className="!px-2 border-1 border-dark-border"
|
||||
onClick={onClose}
|
||||
disabled={disableClose}
|
||||
>
|
||||
|
||||
@@ -73,7 +73,7 @@ export function TabDropdown(): React.ReactElement {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-[30px] h-[30px] border-[#303032]"
|
||||
className="w-[30px] h-[30px] border-dark-border"
|
||||
title={t('nav.tabNavigation', { defaultValue: 'Tab Navigation' })}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
@@ -81,7 +81,7 @@ export function TabDropdown(): React.ReactElement {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-56 bg-[#18181b] border-[#303032] text-white"
|
||||
className="w-56 bg-dark-bg border-dark-border text-white"
|
||||
>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.id === currentTab;
|
||||
@@ -91,8 +91,8 @@ export function TabDropdown(): React.ReactElement {
|
||||
onClick={() => handleTabSwitch(tab.id)}
|
||||
className={`flex items-center gap-2 cursor-pointer px-3 py-2 ${
|
||||
isActive
|
||||
? 'bg-[#1d1d1f] text-white'
|
||||
: 'hover:bg-[#2d2d30] text-gray-300'
|
||||
? 'bg-dark-bg-active text-white'
|
||||
: 'hover:bg-dark-hover text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{getTabIcon(tab.type)}
|
||||
|
||||
@@ -217,19 +217,15 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="fixed z-10 h-[50px] bg-[#18181b] border-2 border-[#303032] rounded-lg transition-all duration-200 ease-linear flex flex-row"
|
||||
className="fixed z-10 h-[50px] bg-dark-bg border-2 border-dark-border rounded-lg transition-all duration-200 ease-linear flex flex-row transform-none m-0 p-0"
|
||||
style={{
|
||||
top: isTopbarOpen ? "0.5rem" : "-3rem",
|
||||
left: leftPosition,
|
||||
right: "17px",
|
||||
position: "fixed",
|
||||
transform: "none",
|
||||
margin: "0",
|
||||
padding: "0"
|
||||
right: "17px"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-full p-1 pr-2 border-r-2 border-[#303032] w-[calc(100%-6rem)] flex items-center overflow-x-auto overflow-y-hidden gap-2 thin-scrollbar">
|
||||
className="h-full p-1 pr-2 border-r-2 border-dark-border w-[calc(100%-6rem)] flex items-center overflow-x-auto overflow-y-hidden gap-2 thin-scrollbar">
|
||||
{tabs.map((tab: any) => {
|
||||
const isActive = tab.id === currentTab;
|
||||
const isSplit = Array.isArray(allSplitScreenTab) && allSplitScreenTab.includes(tab.id);
|
||||
@@ -287,45 +283,32 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
|
||||
{!isTopbarOpen && (
|
||||
<div
|
||||
onClick={() => setIsTopbarOpen(true)}
|
||||
className="absolute top-0 left-0 w-full h-[10px] bg-[#18181b] cursor-pointer z-20 flex items-center justify-center rounded-bl-md rounded-br-md">
|
||||
className="absolute top-0 left-0 w-full h-[10px] bg-dark-bg cursor-pointer z-20 flex items-center justify-center rounded-bl-md rounded-br-md">
|
||||
<ChevronDown size={10}/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{toolsSheetOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-[999999] flex justify-end"
|
||||
className="fixed top-0 left-0 right-0 bottom-0 z-[999999] flex justify-end pointer-events-auto isolate"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 999999,
|
||||
pointerEvents: 'auto',
|
||||
isolation: 'isolate',
|
||||
transform: 'translateZ(0)'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex-1"
|
||||
className="flex-1 cursor-pointer"
|
||||
onClick={() => setToolsSheetOpen(false)}
|
||||
style={{cursor: 'pointer'}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="w-[400px] h-full bg-[#18181b] border-l-2 border-[#303032] flex flex-col shadow-2xl"
|
||||
className="w-[400px] h-full bg-dark-bg border-l-2 border-dark-border flex flex-col shadow-2xl relative isolate z-[999999]"
|
||||
style={{
|
||||
backgroundColor: '#18181b',
|
||||
boxShadow: '-4px 0 20px rgba(0, 0, 0, 0.5)',
|
||||
zIndex: 999999,
|
||||
position: 'relative',
|
||||
isolation: 'isolate',
|
||||
transform: 'translateZ(0)'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 border-b border-[#303032]">
|
||||
<div className="flex items-center justify-between p-4 border-b border-dark-border">
|
||||
<h2 className="text-lg font-semibold text-white">{t('sshTools.title')}</h2>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, {useState} from "react";
|
||||
import {completePasswordReset, initiatePasswordReset, verifyPasswordResetCode} from "@/ui/main-axios.ts";
|
||||
import {Label} from "@/components/ui/label.tsx";
|
||||
import {Input} from "@/components/ui/input.tsx";
|
||||
import {PasswordInput} from "@/components/ui/password-input.tsx";
|
||||
import {Button} from "@/components/ui/button.tsx";
|
||||
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx";
|
||||
import {toast} from "sonner";
|
||||
@@ -182,9 +183,8 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="new-password">{t('common.newPassword')}</Label>
|
||||
<Input
|
||||
<PasswordInput
|
||||
id="new-password"
|
||||
type="password"
|
||||
required
|
||||
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200"
|
||||
value={newPassword}
|
||||
@@ -195,9 +195,8 @@ export function PasswordReset({userInfo}: PasswordResetProps) {
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="confirm-password">{t('common.confirmPassword')}</Label>
|
||||
<Input
|
||||
<PasswordInput
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
required
|
||||
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200"
|
||||
value={confirmPassword}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState } from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { PasswordInput } from "@/components/ui/password-input.tsx";
|
||||
import { Label } from "@/components/ui/label.tsx";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs.tsx";
|
||||
@@ -165,9 +166,8 @@ export function TOTPSetup({ isEnabled: initialEnabled, onStatusChange }: TOTPSet
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="disable-password">{t('auth.passwordOrTotpCode')}</Label>
|
||||
<Input
|
||||
<PasswordInput
|
||||
id="disable-password"
|
||||
type="password"
|
||||
placeholder={t('placeholders.enterPassword')}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
@@ -199,9 +199,8 @@ export function TOTPSetup({ isEnabled: initialEnabled, onStatusChange }: TOTPSet
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="backup-password">{t('auth.passwordOrTotpCode')}</Label>
|
||||
<Input
|
||||
<PasswordInput
|
||||
id="backup-password"
|
||||
type="password"
|
||||
placeholder={t('placeholders.enterPassword')}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
|
||||
@@ -97,9 +97,8 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container max-w-4xl mx-auto p-6 overflow-y-auto" style={{
|
||||
<div className="container max-w-4xl mx-auto p-6 overflow-y-auto transition-[margin-top] duration-300 ease-in-out" style={{
|
||||
marginTop: isTopbarOpen ? '60px' : '0',
|
||||
transition: 'margin-top 0.3s ease',
|
||||
maxHeight: 'calc(100vh - 60px)'
|
||||
}}>
|
||||
<div className="mb-6">
|
||||
|
||||
@@ -11,7 +11,7 @@ export function BottomNavbar({onSidebarOpenClick}: MenuProps) {
|
||||
const {tabs, currentTab, setCurrentTab, removeTab} = useTabs();
|
||||
|
||||
return (
|
||||
<div className="w-full h-[80px] bg-[#18181B] flex items-center p-2 gap-2">
|
||||
<div className="w-full h-[80px] bg-dark-bg flex items-center p-2 gap-2">
|
||||
<Button className="w-[40px] h-[40px] flex-shrink-0" variant="outline" onClick={onSidebarOpenClick}>
|
||||
<Menu/>
|
||||
</Button>
|
||||
@@ -22,8 +22,8 @@ export function BottomNavbar({onSidebarOpenClick}: MenuProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-10 rounded-r-none !px-3 border-1 border-[#303032]",
|
||||
tab.id === currentTab && '!bg-[#09090b] !text-white'
|
||||
"h-10 rounded-r-none !px-3 border-1 border-dark-border",
|
||||
tab.id === currentTab && '!bg-dark-bg-darkest !text-white'
|
||||
)}
|
||||
onClick={() => setCurrentTab(tab.id)}
|
||||
>
|
||||
@@ -32,7 +32,7 @@ export function BottomNavbar({onSidebarOpenClick}: MenuProps) {
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-10 rounded-l-none !px-2 border-1 border-[#303032]"
|
||||
className="h-10 rounded-l-none !px-2 border-1 border-dark-border"
|
||||
onClick={() => removeTab(tab.id)}
|
||||
>
|
||||
<X className="h-4 w-4"/>
|
||||
|
||||
@@ -42,9 +42,8 @@ export function FolderCard({folderName, hosts, onHostConnect}: FolderCardProps):
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-[#0e0e10] border-2 border-[#303032] rounded-lg overflow-hidden"
|
||||
style={{padding: '0', margin: '0'}}>
|
||||
<div className={`px-4 py-3 relative ${isExpanded ? 'border-b-2' : ''} bg-[#131316]`}>
|
||||
<div className="bg-dark-bg-darker border-2 border-dark-border rounded-lg overflow-hidden p-0 m-0">
|
||||
<div className={`px-4 py-3 relative ${isExpanded ? 'border-b-2' : ''} bg-dark-bg-header`}>
|
||||
<div className="flex gap-2 pr-10">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<Folder size={16} strokeWidth={3}/>
|
||||
|
||||
@@ -85,7 +85,7 @@ export function Host({host, onHostConnect}: HostProps): React.ReactElement {
|
||||
{host.enableTerminal && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="!px-2 border-1 w-[60px] border-[#303032]"
|
||||
className="!px-2 border-1 w-[60px] border-dark-border"
|
||||
onClick={handleTerminalClick}
|
||||
>
|
||||
<Terminal/>
|
||||
@@ -96,7 +96,7 @@ export function Host({host, onHostConnect}: HostProps): React.ReactElement {
|
||||
{hasTags && (
|
||||
<div className="flex flex-wrap items-center gap-2 mt-1">
|
||||
{tags.map((tag: string) => (
|
||||
<div key={tag} className="bg-[#18181b] border-1 border-[#303032] pl-2 pr-2 rounded-[10px]">
|
||||
<div key={tag} className="bg-dark-bg border-1 border-dark-border pl-2 pr-2 rounded-[10px]">
|
||||
<p className="text-sm">{tag}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -153,12 +153,12 @@ export function LeftSidebar({isSidebarOpen, setIsSidebarOpen, onHostConnect, dis
|
||||
</SidebarHeader>
|
||||
<Separator/>
|
||||
<SidebarContent className="px-2 py-2">
|
||||
<div className="!bg-[#222225] rounded-lg mb-2">
|
||||
<div className="!bg-dark-bg-input rounded-lg mb-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder={t('placeholders.searchHostsAny')}
|
||||
className="w-full h-8 text-sm border-2 !bg-[#222225] border-[#303032] rounded-md"
|
||||
className="w-full h-8 text-sm border-2 !bg-dark-bg-input border-dark-border rounded-md"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
@@ -196,7 +196,6 @@ export function LeftSidebar({isSidebarOpen, setIsSidebarOpen, onHostConnect, dis
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
className="data-[state=open]:opacity-90 w-full"
|
||||
style={{width: '100%'}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<User2/> {username ? username : t('common.logout')}
|
||||
|
||||
@@ -152,7 +152,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
scrollback: 10000,
|
||||
fontSize: 14,
|
||||
fontFamily: '"JetBrains Mono Nerd Font", "MesloLGS NF", "FiraCode Nerd Font", "Cascadia Code", "JetBrains Mono", Consolas, "Courier New", monospace',
|
||||
theme: {background: '#09090b', foreground: '#f7f7f7'},
|
||||
theme: {background: 'var(--color-dark-bg-darkest)', foreground: '#f7f7f7'},
|
||||
allowTransparency: true,
|
||||
convertEol: true,
|
||||
windowsMode: false,
|
||||
@@ -265,8 +265,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
return (
|
||||
<div
|
||||
ref={xtermRef}
|
||||
className="h-full w-full m-1"
|
||||
style={{opacity: visible && isVisible ? 1 : 0, overflow: 'hidden'}}
|
||||
className={`h-full w-full m-1 transition-opacity duration-200 ${visible && isVisible ? 'opacity-100' : 'opacity-0'} overflow-hidden`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -401,7 +401,7 @@ export function HomepageAuth({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-full max-w-md flex flex-col bg-[#18181b] ${className || ''}`}
|
||||
className={`w-full max-w-md flex flex-col bg-dark-bg ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{dbError && (
|
||||
@@ -676,9 +676,8 @@ export function HomepageAuth({
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="new-password">{t('auth.newPassword')}</Label>
|
||||
<Input
|
||||
<PasswordInput
|
||||
id="new-password"
|
||||
type="password"
|
||||
required
|
||||
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200"
|
||||
value={newPassword}
|
||||
@@ -690,9 +689,8 @@ export function HomepageAuth({
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label
|
||||
htmlFor="confirm-password">{t('auth.confirmNewPassword')}</Label>
|
||||
<Input
|
||||
<PasswordInput
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
required
|
||||
className="h-11 text-base focus:ring-2 focus:ring-primary/50 transition-all duration-200"
|
||||
value={confirmPassword}
|
||||
@@ -744,14 +742,14 @@ export function HomepageAuth({
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="password">{t('common.password')}</Label>
|
||||
<PasswordInput id="password" type="password" required className="h-55 text-base"
|
||||
<PasswordInput id="password" required className="h-55 text-base"
|
||||
value={password} onChange={e => setPassword(e.target.value)}
|
||||
disabled={loading || internalLoggedIn}/>
|
||||
</div>
|
||||
{tab === "signup" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="signup-confirm-password">{t('common.confirmPassword')}</Label>
|
||||
<PasswordInput id="signup-confirm-password" type="password" required
|
||||
<PasswordInput id="signup-confirm-password" required
|
||||
className="h-11 text-base"
|
||||
value={signupConfirmPassword}
|
||||
onChange={e => setSignupConfirmPassword(e.target.value)}
|
||||
@@ -778,7 +776,7 @@ export function HomepageAuth({
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="mt-6 pt-4 border-t border-[#303032]">
|
||||
<div className="mt-6 pt-4 border-t border-dark-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-sm text-muted-foreground">{t('common.language')}</Label>
|
||||
|
||||
@@ -17,7 +17,7 @@ function getCookie(name: string) {
|
||||
|
||||
const AppContent: FC = () => {
|
||||
const {t} = useTranslation();
|
||||
const {tabs, currentTab, getTab} = useTabs();
|
||||
const {tabs, currentTab, getTab, removeTab} = useTabs();
|
||||
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
|
||||
const [ready, setReady] = React.useState(true);
|
||||
|
||||
@@ -108,7 +108,7 @@ const AppContent: FC = () => {
|
||||
|
||||
if (authLoading) {
|
||||
return (
|
||||
<div className="h-screen w-screen flex items-center justify-center bg-[#09090b]">
|
||||
<div className="h-screen w-screen flex items-center justify-center bg-dark-bg-darkest">
|
||||
<p className="text-white">{t('common.loading')}</p>
|
||||
</div>
|
||||
)
|
||||
@@ -116,7 +116,7 @@ const AppContent: FC = () => {
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="h-screen w-screen flex items-center justify-center bg-[#18181b] p-4">
|
||||
<div className="h-screen w-screen flex items-center justify-center bg-dark-bg p-4">
|
||||
<HomepageAuth
|
||||
setLoggedIn={setIsAuthenticated}
|
||||
setIsAdmin={setIsAdmin}
|
||||
@@ -135,21 +135,18 @@ const AppContent: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen flex flex-col bg-[#09090b] overflow-y-hidden overflow-x-hidden relative">
|
||||
<div className="h-screen w-screen flex flex-col bg-dark-bg-darkest overflow-y-hidden overflow-x-hidden relative">
|
||||
<div className="flex-1 min-h-0 relative">
|
||||
{tabs.map(tab => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className="absolute inset-0 mb-2"
|
||||
style={{
|
||||
visibility: tab.id === currentTab ? 'visible' : 'hidden',
|
||||
opacity: ready ? 1 : 0,
|
||||
}}
|
||||
className={`absolute inset-0 mb-2 ${tab.id === currentTab ? 'visible' : 'invisible'} ${ready ? 'opacity-100' : 'opacity-0'}`}
|
||||
>
|
||||
<Terminal
|
||||
ref={tab.terminalRef}
|
||||
hostConfig={tab.hostConfig}
|
||||
isVisible={tab.id === currentTab}
|
||||
onClose={() => removeTab(tab.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -11,7 +11,7 @@ export function BottomNavbar({onSidebarOpenClick}: MenuProps) {
|
||||
const {tabs, currentTab, setCurrentTab, removeTab} = useTabs();
|
||||
|
||||
return (
|
||||
<div className="w-full h-[50px] bg-[#18181B] items-center p-1">
|
||||
<div className="w-full h-[50px] bg-dark-bg items-center p-1">
|
||||
<div className="flex gap-2 !mb-0.5">
|
||||
<Button className="w-[40px] h-[40px] flex-shrink-0" variant="outline" onClick={onSidebarOpenClick}>
|
||||
<Menu/>
|
||||
@@ -23,8 +23,8 @@ export function BottomNavbar({onSidebarOpenClick}: MenuProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-10 rounded-r-none !px-3 border-1 border-[#303032]",
|
||||
tab.id === currentTab && '!bg-[#09090b] !text-white'
|
||||
"h-10 rounded-r-none !px-3 border-1 border-dark-border",
|
||||
tab.id === currentTab && '!bg-dark-bg-darkest !text-white'
|
||||
)}
|
||||
onClick={() => setCurrentTab(tab.id)}
|
||||
>
|
||||
@@ -33,7 +33,7 @@ export function BottomNavbar({onSidebarOpenClick}: MenuProps) {
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-10 rounded-l-none !px-2 border-1 border-[#303032]"
|
||||
className="h-10 rounded-l-none !px-2 border-1 border-dark-border"
|
||||
onClick={() => removeTab(tab.id)}
|
||||
>
|
||||
<X className="h-4 w-4"/>
|
||||
|
||||
@@ -42,9 +42,8 @@ export function FolderCard({folderName, hosts, onHostConnect}: FolderCardProps):
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-[#0e0e10] border-2 border-[#303032] rounded-lg overflow-hidden"
|
||||
style={{padding: '0', margin: '0'}}>
|
||||
<div className={`px-4 py-3 relative ${isExpanded ? 'border-b-2' : ''} bg-[#131316]`}>
|
||||
<div className="bg-dark-bg-darker border-2 border-dark-border rounded-lg overflow-hidden p-0 m-0">
|
||||
<div className={`px-4 py-3 relative ${isExpanded ? 'border-b-2' : ''} bg-dark-bg-header`}>
|
||||
<div className="flex gap-2 pr-10">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<Folder size={16} strokeWidth={3}/>
|
||||
|
||||
@@ -85,7 +85,7 @@ export function Host({host, onHostConnect}: HostProps): React.ReactElement {
|
||||
{host.enableTerminal && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="!px-2 border-1 w-[60px] border-[#303032]"
|
||||
className="!px-2 border-1 w-[60px] border-dark-border"
|
||||
onClick={handleTerminalClick}
|
||||
>
|
||||
<Terminal/>
|
||||
@@ -96,7 +96,7 @@ export function Host({host, onHostConnect}: HostProps): React.ReactElement {
|
||||
{hasTags && (
|
||||
<div className="flex flex-wrap items-center gap-2 mt-1">
|
||||
{tags.map((tag: string) => (
|
||||
<div key={tag} className="bg-[#18181b] border-1 border-[#303032] pl-2 pr-2 rounded-[10px]">
|
||||
<div key={tag} className="bg-dark-bg border-1 border-dark-border pl-2 pr-2 rounded-[10px]">
|
||||
<p className="text-sm">{tag}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -153,12 +153,12 @@ export function LeftSidebar({isSidebarOpen, setIsSidebarOpen, onHostConnect, dis
|
||||
</SidebarHeader>
|
||||
<Separator/>
|
||||
<SidebarContent className="px-2 py-2">
|
||||
<div className="!bg-[#222225] rounded-lg">
|
||||
<div className="!bg-dark-bg-input rounded-lg">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder={t('placeholders.searchHostsAny')}
|
||||
className="w-full h-8 text-sm border-2 !bg-[#222225] border-[#303032] rounded-md"
|
||||
className="w-full h-8 text-sm border-2 !bg-dark-bg-input border-dark-border rounded-md"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
@@ -196,7 +196,6 @@ export function LeftSidebar({isSidebarOpen, setIsSidebarOpen, onHostConnect, dis
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
className="data-[state=open]:opacity-90 w-full"
|
||||
style={{width: '100%'}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<User2/> {username ? username : t('common.logout')}
|
||||
|
||||
Reference in New Issue
Block a user