Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
031bf0ee45 | ||
|
|
0efe80b06d | ||
|
|
3fb7c6dd21 | ||
|
|
c7e4ac82ca | ||
|
|
d5e29598d3 | ||
|
|
fca7782634 | ||
|
|
42b23a9faa | ||
|
|
06011d01d6 | ||
|
|
4bf4e65df8 | ||
|
|
45e62ed43e |
@@ -514,6 +514,9 @@ export const enTerminalMessages: Messages = {
|
||||
'snippets.field.packagePlaceholder': 'Select or create package',
|
||||
'snippets.field.createPackage': 'Create Package',
|
||||
'snippets.field.scriptRequired': 'Script *',
|
||||
'snippets.scriptEditor.expand': 'Open in dialog',
|
||||
'snippets.scriptEditor.resize': 'Resize editor height',
|
||||
'snippets.scriptEditor.modalTitle': 'Edit script',
|
||||
'snippets.targets.title': 'Targets',
|
||||
'snippets.targets.add': 'Add targets',
|
||||
'snippets.history.title': 'Shell History',
|
||||
|
||||
@@ -532,6 +532,20 @@ export const ruTerminalMessages: Messages = {
|
||||
'snippets.field.packagePlaceholder': 'Выберите или создайте пакет',
|
||||
'snippets.field.createPackage': 'Создать пакет',
|
||||
'snippets.field.scriptRequired': 'Скрипт *',
|
||||
'snippets.scriptEditor.expand': 'Открыть в окне',
|
||||
'snippets.scriptEditor.resize': 'Изменить высоту редактора',
|
||||
'snippets.scriptEditor.modalTitle': 'Редактировать скрипт',
|
||||
'snippets.variables.dialogTitle': 'Переменные сниппета',
|
||||
'snippets.variables.dialogDesc': 'Заполните значения для "{label}" перед запуском.',
|
||||
'snippets.variables.hint': 'Значения вставляются в скрипт как есть (без shell-экранирования).',
|
||||
'snippets.variables.preview': 'Предпросмотр',
|
||||
'snippets.variables.placeholder': 'Введите значение',
|
||||
'snippets.variables.placeholderDefault': 'По умолчанию: {value}',
|
||||
'snippets.variables.required': 'Эта переменная обязательна',
|
||||
'snippets.variables.run': 'Запустить',
|
||||
'snippets.field.variablesHelp': 'Используйте {{name}} или {{name:default}} для плейсхолдеров в скрипте.',
|
||||
'snippets.field.variablesDetected': 'Переменные',
|
||||
'snippets.field.variableDefault': 'по умолчанию {value}',
|
||||
'snippets.targets.title': 'Цели',
|
||||
'snippets.targets.add': 'Добавить цели',
|
||||
'snippets.history.title': 'История оболочки',
|
||||
|
||||
@@ -495,6 +495,9 @@ export const zhCNTerminalMessages: Messages = {
|
||||
'snippets.field.packagePlaceholder': '选择或创建代码包',
|
||||
'snippets.field.createPackage': '创建代码包',
|
||||
'snippets.field.scriptRequired': '脚本 *',
|
||||
'snippets.scriptEditor.expand': '弹窗编辑',
|
||||
'snippets.scriptEditor.resize': '调整编辑器高度',
|
||||
'snippets.scriptEditor.modalTitle': '编辑脚本',
|
||||
'snippets.targets.title': '目标主机',
|
||||
'snippets.targets.add': '添加目标主机',
|
||||
'snippets.history.title': 'Shell 历史',
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import {
|
||||
BadgeCheck,
|
||||
ChevronDown,
|
||||
Copy,
|
||||
Edit2,
|
||||
ExternalLink,
|
||||
Key,
|
||||
LayoutGrid,
|
||||
List as ListIcon,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
Shield,
|
||||
Trash2,
|
||||
@@ -838,9 +839,35 @@ echo $3 >> "$FILE"`);
|
||||
</AsideActionMenuItem>
|
||||
</AsideActionMenu>
|
||||
) : panel.type === "view" ? (
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<MoreHorizontal size={16} />
|
||||
</Button>
|
||||
<AsideActionMenu>
|
||||
{panel.key.publicKey ? (
|
||||
<AsideActionMenuItem
|
||||
icon={<Copy size={14} />}
|
||||
onClick={() => copyPublicKey(panel.key)}
|
||||
>
|
||||
{t("action.copyPublicKey")}
|
||||
</AsideActionMenuItem>
|
||||
) : null}
|
||||
<AsideActionMenuItem
|
||||
icon={<ExternalLink size={14} />}
|
||||
onClick={() => openKeyExport(panel.key)}
|
||||
>
|
||||
{t("action.keyExport")}
|
||||
</AsideActionMenuItem>
|
||||
<AsideActionMenuItem
|
||||
icon={<Edit2 size={14} />}
|
||||
onClick={() => openKeyEdit(panel.key)}
|
||||
>
|
||||
{t("action.edit")}
|
||||
</AsideActionMenuItem>
|
||||
<AsideActionMenuItem
|
||||
variant="destructive"
|
||||
icon={<Trash2 size={14} />}
|
||||
onClick={() => handleDelete(panel.key.id)}
|
||||
>
|
||||
{t("action.delete")}
|
||||
</AsideActionMenuItem>
|
||||
</AsideActionMenu>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from './ui/dialog';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { CodeTextarea } from './ui/code-textarea';
|
||||
import { SnippetScriptEditor } from './snippets/SnippetScriptEditor';
|
||||
|
||||
export interface QuickAddSnippetDialogProps {
|
||||
snippets: Snippet[];
|
||||
@@ -148,8 +148,11 @@ export const QuickAddSnippetDialog: React.FC<QuickAddSnippetDialogProps> = ({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-md" onKeyDown={handleKeyDown}>
|
||||
<DialogHeader>
|
||||
<DialogContent
|
||||
className="max-w-md max-h-[min(90vh,720px)] flex flex-col overflow-hidden"
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<DialogHeader className="shrink-0">
|
||||
<DialogTitle>
|
||||
{t(editing ? 'snippets.panel.editTitle' : 'snippets.panel.newTitle')}
|
||||
</DialogTitle>
|
||||
@@ -158,7 +161,7 @@ export const QuickAddSnippetDialog: React.FC<QuickAddSnippetDialogProps> = ({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="min-h-0 space-y-3 overflow-y-auto pr-1">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="quick-add-snippet-label" className="text-xs">
|
||||
{t('snippets.field.description')}
|
||||
@@ -174,18 +177,13 @@ export const QuickAddSnippetDialog: React.FC<QuickAddSnippetDialogProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="quick-add-snippet-command" className="text-xs">
|
||||
{t('snippets.field.scriptRequired')}
|
||||
</Label>
|
||||
<CodeTextarea
|
||||
id="quick-add-snippet-command"
|
||||
value={command}
|
||||
onChange={(e) => setCommand(e.target.value)}
|
||||
placeholder="echo hello"
|
||||
className="min-h-[120px]"
|
||||
/>
|
||||
</div>
|
||||
<SnippetScriptEditor
|
||||
id="quick-add-snippet-command"
|
||||
label={t('snippets.field.scriptRequired')}
|
||||
value={command}
|
||||
onChange={setCommand}
|
||||
placeholder="echo hello"
|
||||
/>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs flex items-center gap-1.5">
|
||||
@@ -203,7 +201,7 @@ export const QuickAddSnippetDialog: React.FC<QuickAddSnippetDialogProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogFooter className="shrink-0">
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AsidePanel, AsidePanelContent, AsidePanelFooter } from './ui/aside-pane
|
||||
import { Button } from './ui/button';
|
||||
import { Card } from './ui/card';
|
||||
import { Input } from './ui/input';
|
||||
import { CodeTextarea } from './ui/code-textarea';
|
||||
import { SnippetScriptEditor } from './snippets/SnippetScriptEditor';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||
import { Combobox } from './ui/combobox';
|
||||
import { DistroAvatar } from './DistroAvatar';
|
||||
@@ -165,12 +165,11 @@ export const SnippetsRightPanel: React.FC<SnippetsRightPanelProps> = ({
|
||||
|
||||
{/* Script */}
|
||||
<Card className="p-3 space-y-2 bg-card border-border/80">
|
||||
<p className="text-xs font-semibold text-muted-foreground">{t('snippets.field.scriptRequired')}</p>
|
||||
<CodeTextarea
|
||||
<SnippetScriptEditor
|
||||
label={t('snippets.field.scriptRequired')}
|
||||
placeholder="ls -l"
|
||||
className="min-h-[120px]"
|
||||
value={editingSnippet.command || ''}
|
||||
onChange={(e) => setEditingSnippet({ ...editingSnippet, command: e.target.value })}
|
||||
onChange={(command) => setEditingSnippet({ ...editingSnippet, command })}
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
||||
{t('snippets.field.variablesHelp')}
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
* View Key Panel - Display SSH key details
|
||||
*/
|
||||
|
||||
import { Copy,Info } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { Check, Copy, Info } from 'lucide-react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useI18n } from '../../application/i18n/I18nProvider';
|
||||
import { SSHKey } from '../../types';
|
||||
import { Button } from '../ui/button';
|
||||
import { Label } from '../ui/label';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||
import { copyToClipboard } from './utils';
|
||||
|
||||
interface ViewKeyPanelProps {
|
||||
@@ -20,6 +21,15 @@ export const ViewKeyPanel: React.FC<ViewKeyPanelProps> = ({
|
||||
onExport,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopyPublicKey = useCallback(async () => {
|
||||
const ok = await copyToClipboard(keyItem.publicKey || '');
|
||||
if (!ok) return;
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}, [keyItem.publicKey]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
@@ -30,18 +40,34 @@ export const ViewKeyPanel: React.FC<ViewKeyPanelProps> = ({
|
||||
{keyItem.publicKey && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-muted-foreground">{t('keychain.field.publicKey')}</Label>
|
||||
<div className="relative">
|
||||
<div className="p-3 bg-card border border-border/80 rounded-lg font-mono text-xs break-all max-h-32 overflow-y-auto">
|
||||
<div className="flex rounded-lg border border-border/80 bg-card overflow-hidden">
|
||||
<div className="flex-1 min-w-0 p-3 font-mono text-xs break-all max-h-32 overflow-y-auto">
|
||||
{keyItem.publicKey}
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 h-7 w-7"
|
||||
onClick={() => copyToClipboard(keyItem.publicKey || '')}
|
||||
>
|
||||
<Copy size={12} />
|
||||
</Button>
|
||||
<div className="shrink-0 flex flex-col border-l border-border/60 p-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7"
|
||||
onClick={() => void handleCopyPublicKey()}
|
||||
aria-label={
|
||||
copied
|
||||
? t('cloudSync.githubFlow.copied')
|
||||
: t('action.copyPublicKey')
|
||||
}
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
{copied
|
||||
? t('cloudSync.githubFlow.copied')
|
||||
: t('action.copyPublicKey')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
174
components/snippets/SnippetScriptEditor.tsx
Normal file
174
components/snippets/SnippetScriptEditor.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { Maximize2 } from 'lucide-react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useI18n } from '../../application/i18n/I18nProvider';
|
||||
import { STORAGE_KEY_SNIPPET_SCRIPT_EDITOR_HEIGHT } from '@/infrastructure/config/storageKeys.ts';
|
||||
import { localStorageAdapter } from '@/infrastructure/persistence/localStorageAdapter.ts';
|
||||
import { cn } from '@/lib/utils.ts';
|
||||
import { Button } from '../ui/button';
|
||||
import { CodeTextarea } from '../ui/code-textarea';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '../ui/dialog';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||
|
||||
const DEFAULT_HEIGHT = 120;
|
||||
const MIN_HEIGHT = 80;
|
||||
const MAX_HEIGHT = 520;
|
||||
|
||||
function clampHeight(height: number): number {
|
||||
return Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, height));
|
||||
}
|
||||
|
||||
function readStoredHeight(): number {
|
||||
const stored = localStorageAdapter.readNumber(STORAGE_KEY_SNIPPET_SCRIPT_EDITOR_HEIGHT);
|
||||
if (stored === null) return DEFAULT_HEIGHT;
|
||||
return clampHeight(stored);
|
||||
}
|
||||
|
||||
const editorFillClass =
|
||||
'[&>div]:h-full [&_textarea]:h-full [&_textarea]:min-h-0 [&_textarea]:overflow-auto';
|
||||
|
||||
export interface SnippetScriptEditorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
id?: string;
|
||||
/** Shown on the same row as the expand button (e.g. "Script *"). */
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const SnippetScriptEditor: React.FC<SnippetScriptEditorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
id,
|
||||
label,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [height, setHeight] = useState(readStoredHeight);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const dragRef = useRef<{ startY: number; startHeight: number } | null>(null);
|
||||
const heightRef = useRef(height);
|
||||
heightRef.current = height;
|
||||
|
||||
const handleResizeStart = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
dragRef.current = { startY: e.clientY, startHeight: heightRef.current };
|
||||
document.body.style.cursor = 'ns-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onMove = (e: MouseEvent) => {
|
||||
if (!dragRef.current) return;
|
||||
const delta = e.clientY - dragRef.current.startY;
|
||||
setHeight(clampHeight(dragRef.current.startHeight + delta));
|
||||
};
|
||||
const onUp = () => {
|
||||
if (dragRef.current) {
|
||||
localStorageAdapter.writeNumber(
|
||||
STORAGE_KEY_SNIPPET_SCRIPT_EDITOR_HEIGHT,
|
||||
heightRef.current,
|
||||
);
|
||||
}
|
||||
dragRef.current = null;
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mouseup', onUp);
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', onMove);
|
||||
window.removeEventListener('mouseup', onUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between gap-2 min-h-7">
|
||||
{label ? (
|
||||
id ? (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="text-xs font-semibold text-muted-foreground shrink-0 cursor-text"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
) : (
|
||||
<p className="text-xs font-semibold text-muted-foreground shrink-0">{label}</p>
|
||||
)
|
||||
) : (
|
||||
<span className="flex-1" aria-hidden />
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 shrink-0 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setModalOpen(true)}
|
||||
aria-label={t('snippets.scriptEditor.expand')}
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
{t('snippets.scriptEditor.expand')}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('snippets.scriptEditor.expand')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="relative rounded-md" style={{ height }}>
|
||||
<div className={cn('h-full min-h-0 overflow-hidden', editorFillClass)}>
|
||||
<CodeTextarea
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="min-h-0"
|
||||
wrapperClassName="h-full"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
role="separator"
|
||||
aria-orientation="horizontal"
|
||||
aria-label={t('snippets.scriptEditor.resize')}
|
||||
className="absolute bottom-0 left-0 right-0 z-10 flex h-2.5 cursor-ns-resize items-center justify-center rounded-b-md hover:bg-muted/40"
|
||||
onMouseDown={handleResizeStart}
|
||||
>
|
||||
<div className="h-0.5 w-10 rounded-full bg-border/80" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DialogContent className="max-w-4xl w-[min(90vw,56rem)] h-[min(85vh,640px)] flex flex-col gap-0 p-0">
|
||||
<DialogHeader className="px-6 pt-6 pb-3 shrink-0">
|
||||
<DialogTitle>{t('snippets.scriptEditor.modalTitle')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className={cn('flex-1 min-h-0 px-6 pb-3 overflow-hidden', editorFillClass)}>
|
||||
<CodeTextarea
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="min-h-0"
|
||||
wrapperClassName="h-full"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="px-6 pb-6 pt-2 shrink-0">
|
||||
<Button type="button" onClick={() => setModalOpen(false)}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,8 @@ import { cn } from "@/lib/utils.ts";
|
||||
export interface CodeTextareaProps
|
||||
extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "spellCheck"> {
|
||||
showLineNumbers?: boolean;
|
||||
/** Applied to the outer bordered container when line numbers are shown. */
|
||||
wrapperClassName?: string;
|
||||
}
|
||||
|
||||
function countLines(value: string): number {
|
||||
@@ -12,7 +14,7 @@ function countLines(value: string): number {
|
||||
}
|
||||
|
||||
const CodeTextarea = React.forwardRef<HTMLTextAreaElement, CodeTextareaProps>(
|
||||
({ className, value, showLineNumbers = true, onScroll, ...props }, ref) => {
|
||||
({ className, wrapperClassName, value, showLineNumbers = true, onScroll, ...props }, ref) => {
|
||||
const gutterRef = React.useRef<HTMLDivElement>(null);
|
||||
const text = typeof value === "string" ? value : String(value ?? "");
|
||||
const lineCount = countLines(text);
|
||||
@@ -61,8 +63,9 @@ const CodeTextarea = React.forwardRef<HTMLTextAreaElement, CodeTextareaProps>(
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full overflow-hidden rounded-md border border-input bg-background",
|
||||
"focus-within:ring-1 focus-within:ring-ring",
|
||||
"flex w-full overflow-hidden rounded-md border border-input bg-background transition-colors",
|
||||
"focus-within:border-ring",
|
||||
wrapperClassName,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -90,7 +90,7 @@ export function VaultViewLayout({ ctx }: { ctx: VaultViewLayoutContext }) {
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className={cn("space-y-1", sidebarCollapsed ? "px-1.5" : "px-3")}>
|
||||
<div className={cn("space-y-1", sidebarCollapsed ? "px-1.5" : "px-2.5")}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<RippleButton
|
||||
@@ -228,7 +228,7 @@ export function VaultViewLayout({ ctx }: { ctx: VaultViewLayoutContext }) {
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className={cn("mt-auto pb-4 space-y-2", sidebarCollapsed ? "px-1.5" : "px-3")}>
|
||||
<div className={cn("mt-auto pb-4 space-y-2", sidebarCollapsed ? "px-1.5" : "px-2.5")}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
@@ -261,7 +261,7 @@ export function VaultViewLayout({ ctx }: { ctx: VaultViewLayoutContext }) {
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
|
||||
<div className="flex min-w-0 flex-1 p-2 pl-1" data-section="vault-stage">
|
||||
<div className="flex min-w-0 flex-1 p-2 pl-0" data-section="vault-stage">
|
||||
<div
|
||||
className="relative flex min-h-0 flex-1 overflow-hidden rounded-xl border border-border/60 bg-background shadow-sm"
|
||||
data-section="vault-surface"
|
||||
|
||||
@@ -159,7 +159,6 @@
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
opacity: 0;
|
||||
transform: translateX(22px);
|
||||
}
|
||||
55% {
|
||||
opacity: 0.88;
|
||||
@@ -168,13 +167,12 @@
|
||||
width: var(--aside-inline-width);
|
||||
min-width: var(--aside-inline-width);
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.split-panel-enter {
|
||||
animation: split-panel-enter 220ms cubic-bezier(0.24, 0.84, 0.32, 1) both;
|
||||
will-change: width, opacity, transform;
|
||||
will-change: width, opacity;
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@@ -41,6 +41,8 @@ export const STORAGE_KEY_VAULT_SIDEBAR_WIDTH = 'netcatty_vault_sidebar_width_v1'
|
||||
export const STORAGE_KEY_VAULT_KEYS_VIEW_MODE = 'netcatty_vault_keys_view_mode_v1';
|
||||
export const STORAGE_KEY_VAULT_PROXY_PROFILES_VIEW_MODE = 'netcatty_vault_proxy_profiles_view_mode_v1';
|
||||
export const STORAGE_KEY_VAULT_SNIPPETS_VIEW_MODE = 'netcatty_vault_snippets_view_mode_v1';
|
||||
/** Inline snippet script editor height (px) in vault edit panel. */
|
||||
export const STORAGE_KEY_SNIPPET_SCRIPT_EDITOR_HEIGHT = 'netcatty_snippet_script_editor_height_v1';
|
||||
export const STORAGE_KEY_VAULT_KNOWN_HOSTS_VIEW_MODE = 'netcatty_vault_known_hosts_view_mode_v1';
|
||||
|
||||
// Update check
|
||||
|
||||
Reference in New Issue
Block a user