mirror of
https://github.com/YuzuZensai/Termix.git
synced 2026-09-14 02:59:06 +00:00
v1.10.0 (#471)
* fix select edit host but not update view (#438) * fix: Checksum issue with chocolatey * fix: Remove homebrew old stuff * Add Korean translation (#439) Co-authored-by: 송준우 <2484@coreit.co.kr> * feat: Automate flatpak * fix: Add imagemagik to electron builder to resolve build error * fix: Build error with runtime repo flag * fix: Flatpak runtime error and install freedesktop ver warning * fix: Flatpak runtime error and install freedesktop ver warning * feat: Re-add homebrew cask and move scripts to backend * fix: No sandbox flag issue * fix: Change name for electron macos cask output * fix: Sandbox error with Linux * fix: Remove comming soon for app stores in readme * Adding Comment at the end of the public_key on the host on deploy (#440) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * -Add New Interface for Credential DB -Add Credential Name as a comment into the server authorized_key file --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Sudo auto fill password (#441) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Feature Sudo password auto-fill; * Fix locale json shema; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Added Italian Language; (#445) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Auto collapse snippet folders (#448) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * feat: Add collapsable snippets (customizable in user profile) * Translations (#447) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; * Fix translations; Removed duplicate keys, synchronised other languages using English as the source, translated added keys, fixed inaccurate translations. --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Remove PTY-level keepalive (#449) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Remove PTY-level keepalive to prevent unwanted terminal output; use SSH-level keepalive instead --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation * fix: finalize adding docker to db * feat: Add docker management support (local squash) * Fix RBAC role system bugs and improve UX (#446) * Fix RBAC role system bugs and improve UX - Fix user list dropdown selection in host sharing - Fix role sharing permissions to include role-based access - Fix translation template interpolation for success messages - Standardize system roles to admin and user only - Auto-assign user role to new registrations - Remove blocking confirmation dialogs in modal contexts - Add missing i18n keys for common actions - Fix button type to prevent unintended form submissions * Enhance RBAC system with UI improvements and security fixes - Move role assignment to Users tab with per-user role management - Protect system roles (admin/user) from editing and manual assignment - Simplify permission system: remove Use level, keep View and Manage - Hide Update button and Sharing tab for view-only/shared hosts - Prevent users from sharing hosts with themselves - Unify table and modal styling across admin panels - Auto-assign system roles on user registration - Add permission metadata to host interface * Add empty state message for role assignment - Display helpful message when no custom roles available - Clarify that system roles are auto-assigned - Add noCustomRolesToAssign translation in English and Chinese * fix: Prevent credential sharing errors for shared hosts - Skip credential resolution for shared hosts with credential authentication to prevent decryption errors (credentials are encrypted per-user) - Add warning alert in sharing tab when host uses credential authentication - Inform users that shared users cannot connect to credential-based hosts - Add translations for credential sharing warning (EN/ZH) This prevents authentication failures when sharing hosts configured with credential authentication while maintaining security by keeping credentials isolated per user. * feat: Improve rbac UI and fixes some bugs --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> * SOCKS5 support (#452) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * SOCKS5 support Adding single and chain socks5 proxy support * fix: cleanup files --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> * Notes and Expiry fields add (#453) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Notes and Expiry add * fix: cleanup files --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> * fix: ssh host types * fix: sudo incorrect styling and remove expiration date * feat: add sudo password and add diagonal bg's * fix: snippet running on enter key * fix: base64 decoding * fix: improve server stats / rbac * fix: wrap ssh host json export in hosts array * feat: auto trim host inputs, fix file manager jump hosts, dashboard prevent duplicates, file manager terminal not size updating, improve left sidebar sorting, hide/show tags, add apperance user profile tab, add new host manager tabs. * feat: improve terminal connection speed * fix: sqlite constriant errors and support non-root user (nginx perm issue) * feat: add beta syntax highlighing to terminal * feat: update imports and improve admin settings user management * chore: update translations * chore: update translations * feat: Complete light mode implementation with semantic theme system (#450) - Add comprehensive light/dark mode CSS variables with semantic naming - Implement theme-aware scrollbars using CSS variables - Add light mode backgrounds: --bg-base, --bg-elevated, --bg-surface, etc. - Add theme-aware borders: --border-base, --border-panel, --border-subtle - Add semantic text colors: --foreground-secondary, --foreground-subtle - Convert oklch colors to hex for better compatibility - Add theme awareness to CodeMirror editors - Update dark mode colors for consistency (background, sidebar, card, muted, input) - Add Tailwind color mappings for semantic classes Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: syntax errors * chore: updating/match themes and split admin settings * feat: add translation workflow and remove old translation.json * fix: translation workflow error * fix: translation workflow error * feat: improve translation system and update workflow * fix: wrong path for translations * fix: change translation to flat files * fix: gh rule error * chore: auto-translate to multiple languages (#458) * chore: improve organization and made a few styling changes in host manager * feat: improve terminal stability and split out the host manager * fix: add unnversiioned files * chore: migrate all to use the new theme system * fix: wrong animation line colors * fix: rbac implementation general issues (local squash) * fix: remove unneeded files * feat: add 10 new langs * chore: update gitnore * chore: auto-translate to multiple languages (#459) * fix: improve tunnel system * fix: properly split tabs, still need to fix up the host manager * chore: cleanup files (possible RC) * feat: add norwegian * chore: auto-translate to multiple languages (#461) * fix: small qol fixes and began readme update * fix: run cleanup script * feat: add docker docs button * feat: general bug fixes and readme updates * fix: translations * chore: auto-translate to multiple languages (#462) * fix: cleanup files * fix: test new translation issue and add better server-stats support * fix: fix translate error * chore: auto-translate to multiple languages (#463) * fix: fix translate mismatching text * chore: auto-translate to multiple languages (#465) * fix: fix translate mismatching text * fix: fix translate mismatching text * chore: auto-translate to multiple languages (#466) * fix: fix translate mismatching text * fix: fix translate mismatching text * fix: fix translate mismatching text * chore: auto-translate to multiple languages (#467) * fix: fix translate mismatching text * chore: auto-translate to multiple languages (#468) * feat: add to readme, a few qol changes, and improve server stats in general * chore: auto-translate to multiple languages (#469) * feat: turned disk uage into graph and fixed issue with termina console * fix: electron build error and hide icons when shared * chore: run clean * fix: general server stats issues, file manager decoding, ui qol * fix: add dashboard line breaks * fix: docker console error * fix: docker console not loading and mismatched stripped background for electron * fix: docker console not loading * chore: docker console not loading in docker * chore: translate readme to chinese * chore: match package lock to package json * chore: nginx config issue for dokcer console * chore: auto-translate to multiple languages (#470) --------- Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Wesley Reid <starhound@lostsouls.org> Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> Co-authored-by: Denis <38875137+Medvedinca@users.noreply.github.com> Co-authored-by: Peet McKinney <68706879+PeetMcK@users.noreply.github.com>
This commit is contained in:
co-authored by
Tran Trung Kien
junu
송준우
SlimGary
Nunzio Marfè
Wesley Reid
ZacharyZcR
Denis
Peet McKinney
parent
7139290d14
commit
ad86c2040b
@@ -197,9 +197,12 @@ export function SSHToolsSidebar({
|
||||
);
|
||||
const [draggedSnippet, setDraggedSnippet] = useState<Snippet | null>(null);
|
||||
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
|
||||
const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(() => {
|
||||
const shouldCollapse =
|
||||
localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false";
|
||||
return shouldCollapse ? new Set() : new Set();
|
||||
});
|
||||
const [snippetSearchQuery, setSnippetSearchQuery] = useState("");
|
||||
const [showFolderDialog, setShowFolderDialog] = useState(false);
|
||||
const [editingFolder, setEditingFolder] = useState<SnippetFolder | null>(
|
||||
null,
|
||||
@@ -243,9 +246,10 @@ export function SSHToolsSidebar({
|
||||
const splittableTabs = tabs.filter(
|
||||
(tab: TabData) =>
|
||||
tab.type === "terminal" ||
|
||||
tab.type === "server" ||
|
||||
tab.type === "server_stats" ||
|
||||
tab.type === "file_manager" ||
|
||||
tab.type === "user_profile",
|
||||
tab.type === "tunnel" ||
|
||||
tab.type === "docker",
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -281,9 +285,9 @@ export function SSHToolsSidebar({
|
||||
console.error("Failed to fetch command history", err);
|
||||
const errorMessage =
|
||||
err?.response?.status === 401
|
||||
? "Authentication required. Please refresh the page."
|
||||
? t("commandHistory.authRequiredRefresh")
|
||||
: err?.response?.status === 403
|
||||
? "Data access locked. Please re-authenticate."
|
||||
? t("commandHistory.dataAccessLockedReauth")
|
||||
: err?.message || "Failed to load command history";
|
||||
|
||||
setHistoryError(errorMessage);
|
||||
@@ -351,6 +355,55 @@ export function SSHToolsSidebar({
|
||||
}
|
||||
}, [isOpen, activeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (snippetFolders.length > 0) {
|
||||
const shouldCollapse =
|
||||
localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false";
|
||||
if (shouldCollapse) {
|
||||
const allFolderNames = new Set(snippetFolders.map((f) => f.name));
|
||||
const uncategorizedSnippets = snippets.filter(
|
||||
(s) => !s.folder || s.folder === "",
|
||||
);
|
||||
if (uncategorizedSnippets.length > 0) {
|
||||
allFolderNames.add("");
|
||||
}
|
||||
setCollapsedFolders(allFolderNames);
|
||||
} else {
|
||||
setCollapsedFolders(new Set());
|
||||
}
|
||||
}
|
||||
}, [snippetFolders, snippets]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleSettingChange = () => {
|
||||
const shouldCollapse =
|
||||
localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false";
|
||||
if (shouldCollapse) {
|
||||
const allFolderNames = new Set(snippetFolders.map((f) => f.name));
|
||||
const uncategorizedSnippets = snippets.filter(
|
||||
(s) => !s.folder || s.folder === "",
|
||||
);
|
||||
if (uncategorizedSnippets.length > 0) {
|
||||
allFolderNames.add("");
|
||||
}
|
||||
setCollapsedFolders(allFolderNames);
|
||||
} else {
|
||||
setCollapsedFolders(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
"defaultSnippetFoldersCollapsedChanged",
|
||||
handleSettingChange,
|
||||
);
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"defaultSnippetFoldersCollapsedChanged",
|
||||
handleSettingChange,
|
||||
);
|
||||
};
|
||||
}, [snippetFolders, snippets]);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsResizing(true);
|
||||
@@ -674,6 +727,11 @@ export function SSHToolsSidebar({
|
||||
onSnippetExecute(snippet.content);
|
||||
toast.success(t("snippets.executeSuccess", { name: snippet.name }));
|
||||
}
|
||||
|
||||
// Remove focus from any active element in the sidebar to prevent accidental re-execution
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = (snippet: Snippet) => {
|
||||
@@ -715,7 +773,22 @@ export function SSHToolsSidebar({
|
||||
}
|
||||
});
|
||||
|
||||
snippets.forEach((snippet) => {
|
||||
const filteredSnippets = snippetSearchQuery
|
||||
? snippets.filter(
|
||||
(snippet) =>
|
||||
snippet.name
|
||||
.toLowerCase()
|
||||
.includes(snippetSearchQuery.toLowerCase()) ||
|
||||
snippet.content
|
||||
.toLowerCase()
|
||||
.includes(snippetSearchQuery.toLowerCase()) ||
|
||||
snippet.description
|
||||
?.toLowerCase()
|
||||
.includes(snippetSearchQuery.toLowerCase()),
|
||||
)
|
||||
: snippets;
|
||||
|
||||
filteredSnippets.forEach((snippet) => {
|
||||
const folderName = snippet.folder || "";
|
||||
if (!grouped.has(folderName)) {
|
||||
grouped.set(folderName, []);
|
||||
@@ -757,11 +830,7 @@ export function SSHToolsSidebar({
|
||||
const targetFolder = targetSnippet.folder || "";
|
||||
|
||||
if (sourceFolder !== targetFolder) {
|
||||
toast.error(
|
||||
t("snippets.reorderSameFolder", {
|
||||
defaultValue: "Can only reorder snippets within the same folder",
|
||||
}),
|
||||
);
|
||||
toast.error(t("snippets.reorderSameFolder"));
|
||||
setDraggedSnippet(null);
|
||||
setDragOverFolder(null);
|
||||
return;
|
||||
@@ -796,18 +865,10 @@ export function SSHToolsSidebar({
|
||||
|
||||
try {
|
||||
await reorderSnippets(updates);
|
||||
toast.success(
|
||||
t("snippets.reorderSuccess", {
|
||||
defaultValue: "Snippets reordered successfully",
|
||||
}),
|
||||
);
|
||||
toast.success(t("snippets.reorderSuccess"));
|
||||
fetchSnippets();
|
||||
} catch {
|
||||
toast.error(
|
||||
t("snippets.reorderFailed", {
|
||||
defaultValue: "Failed to reorder snippets",
|
||||
}),
|
||||
);
|
||||
toast.error(t("snippets.reorderFailed"));
|
||||
}
|
||||
|
||||
setDraggedSnippet(null);
|
||||
@@ -845,23 +906,14 @@ export function SSHToolsSidebar({
|
||||
confirmWithToast(
|
||||
t("snippets.deleteFolderConfirm", {
|
||||
name: folderName,
|
||||
defaultValue: `Delete folder "${folderName}"? All snippets will be moved to Uncategorized.`,
|
||||
}),
|
||||
async () => {
|
||||
try {
|
||||
await deleteSnippetFolder(folderName);
|
||||
toast.success(
|
||||
t("snippets.deleteFolderSuccess", {
|
||||
defaultValue: "Folder deleted successfully",
|
||||
}),
|
||||
);
|
||||
toast.success(t("snippets.deleteFolderSuccess"));
|
||||
fetchSnippets();
|
||||
} catch {
|
||||
toast.error(
|
||||
t("snippets.deleteFolderFailed", {
|
||||
defaultValue: "Failed to delete folder",
|
||||
}),
|
||||
);
|
||||
toast.error(t("snippets.deleteFolderFailed"));
|
||||
}
|
||||
},
|
||||
"destructive",
|
||||
@@ -888,22 +940,14 @@ export function SSHToolsSidebar({
|
||||
color: folderFormData.color || undefined,
|
||||
icon: folderFormData.icon || undefined,
|
||||
});
|
||||
toast.success(
|
||||
t("snippets.updateFolderSuccess", {
|
||||
defaultValue: "Folder updated successfully",
|
||||
}),
|
||||
);
|
||||
toast.success(t("snippets.updateFolderSuccess"));
|
||||
} else {
|
||||
await createSnippetFolder({
|
||||
name: folderFormData.name,
|
||||
color: folderFormData.color || undefined,
|
||||
icon: folderFormData.icon || undefined,
|
||||
});
|
||||
toast.success(
|
||||
t("snippets.createFolderSuccess", {
|
||||
defaultValue: "Folder created successfully",
|
||||
}),
|
||||
);
|
||||
toast.success(t("snippets.createFolderSuccess"));
|
||||
}
|
||||
|
||||
setShowFolderDialog(false);
|
||||
@@ -911,12 +955,8 @@ export function SSHToolsSidebar({
|
||||
} catch {
|
||||
toast.error(
|
||||
editingFolder
|
||||
? t("snippets.updateFolderFailed", {
|
||||
defaultValue: "Failed to update folder",
|
||||
})
|
||||
: t("snippets.createFolderFailed", {
|
||||
defaultValue: "Failed to create folder",
|
||||
}),
|
||||
? t("snippets.updateFolderFailed")
|
||||
: t("snippets.createFolderFailed"),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -985,11 +1025,7 @@ export function SSHToolsSidebar({
|
||||
}
|
||||
|
||||
if (splitAssignments.size === 0) {
|
||||
toast.error(
|
||||
t("splitScreen.error.noAssignments", {
|
||||
defaultValue: "Please drag tabs to cells before applying",
|
||||
}),
|
||||
);
|
||||
toast.error(t("splitScreen.error.noAssignments"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -998,7 +1034,6 @@ export function SSHToolsSidebar({
|
||||
if (splitAssignments.size < requiredSlots) {
|
||||
toast.error(
|
||||
t("splitScreen.error.fillAllSlots", {
|
||||
defaultValue: `Please fill all ${requiredSlots} layout spots before applying`,
|
||||
count: requiredSlots,
|
||||
}),
|
||||
);
|
||||
@@ -1026,11 +1061,7 @@ export function SSHToolsSidebar({
|
||||
setCurrentTab(orderedTabIds[0]);
|
||||
}
|
||||
|
||||
toast.success(
|
||||
t("splitScreen.success", {
|
||||
defaultValue: "Split screen applied",
|
||||
}),
|
||||
);
|
||||
toast.success(t("splitScreen.success"));
|
||||
};
|
||||
|
||||
const handleClearSplit = () => {
|
||||
@@ -1042,11 +1073,7 @@ export function SSHToolsSidebar({
|
||||
setSplitAssignments(new Map());
|
||||
setPreviewKey((prev) => prev + 1);
|
||||
|
||||
toast.success(
|
||||
t("splitScreen.cleared", {
|
||||
defaultValue: "Split screen cleared",
|
||||
}),
|
||||
);
|
||||
toast.success(t("splitScreen.cleared"));
|
||||
};
|
||||
|
||||
const handleResetToSingle = () => {
|
||||
@@ -1064,17 +1091,9 @@ export function SSHToolsSidebar({
|
||||
try {
|
||||
await deleteCommandFromHistory(activeTerminalHostId, command);
|
||||
setCommandHistory((prev) => prev.filter((c) => c !== command));
|
||||
toast.success(
|
||||
t("commandHistory.deleteSuccess", {
|
||||
defaultValue: "Command deleted from history",
|
||||
}),
|
||||
);
|
||||
toast.success(t("commandHistory.deleteSuccess"));
|
||||
} catch {
|
||||
toast.error(
|
||||
t("commandHistory.deleteFailed", {
|
||||
defaultValue: "Failed to delete command.",
|
||||
}),
|
||||
);
|
||||
toast.error(t("commandHistory.deleteFailed"));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1096,14 +1115,14 @@ export function SSHToolsSidebar({
|
||||
className="pointer-events-auto"
|
||||
>
|
||||
<SidebarHeader>
|
||||
<SidebarGroupLabel className="text-lg font-bold text-white">
|
||||
<SidebarGroupLabel className="text-lg font-bold text-foreground">
|
||||
{t("nav.tools")}
|
||||
<div className="absolute right-5 flex gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setSidebarWidth(400)}
|
||||
className="w-[28px] h-[28px]"
|
||||
title="Reset sidebar width"
|
||||
title={t("common.resetSidebarWidth")}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -1133,15 +1152,15 @@ export function SSHToolsSidebar({
|
||||
{t("snippets.title")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="command-history">
|
||||
{t("commandHistory.title", { defaultValue: "History" })}
|
||||
{t("commandHistory.title")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="split-screen">
|
||||
{t("splitScreen.title", { defaultValue: "Split Screen" })}
|
||||
{t("splitScreen.title")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="ssh-tools" className="space-y-4">
|
||||
<h3 className="font-semibold text-white">
|
||||
<h3 className="font-semibold text-foreground">
|
||||
{t("sshTools.keyRecording")}
|
||||
</h3>
|
||||
|
||||
@@ -1169,10 +1188,10 @@ export function SSHToolsSidebar({
|
||||
{isRecording && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{t("sshTools.selectTerminals")}
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto">
|
||||
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto thin-scrollbar">
|
||||
{terminalTabs.map((tab) => (
|
||||
<Button
|
||||
key={tab.id}
|
||||
@@ -1181,8 +1200,8 @@ export function SSHToolsSidebar({
|
||||
size="sm"
|
||||
className={`rounded-full px-3 py-1 text-xs flex items-center gap-1 ${
|
||||
selectedTabIds.includes(tab.id)
|
||||
? "text-white bg-gray-700"
|
||||
: "text-gray-500"
|
||||
? "text-foreground bg-surface"
|
||||
: "text-foreground-subtle"
|
||||
}`}
|
||||
onClick={() => handleTabToggle(tab.id)}
|
||||
>
|
||||
@@ -1193,7 +1212,7 @@ export function SSHToolsSidebar({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{t("sshTools.typeCommands")}
|
||||
</label>
|
||||
<Input
|
||||
@@ -1217,7 +1236,7 @@ export function SSHToolsSidebar({
|
||||
|
||||
<Separator />
|
||||
|
||||
<h3 className="font-semibold text-white">
|
||||
<h3 className="font-semibold text-foreground">
|
||||
{t("sshTools.settings")}
|
||||
</h3>
|
||||
|
||||
@@ -1229,7 +1248,7 @@ export function SSHToolsSidebar({
|
||||
/>
|
||||
<label
|
||||
htmlFor="enable-copy-paste"
|
||||
className="text-sm font-medium leading-none text-white cursor-pointer"
|
||||
className="text-sm font-medium leading-none text-foreground cursor-pointer"
|
||||
>
|
||||
{t("sshTools.enableRightClickCopyPaste")}
|
||||
</label>
|
||||
@@ -1244,23 +1263,17 @@ export function SSHToolsSidebar({
|
||||
{terminalTabs.length > 0 && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white">
|
||||
{t("snippets.selectTerminals", {
|
||||
defaultValue: "Select Terminals (optional)",
|
||||
})}
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{t("snippets.selectTerminals")}
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{selectedSnippetTabIds.length > 0
|
||||
? t("snippets.executeOnSelected", {
|
||||
defaultValue: `Execute on ${selectedSnippetTabIds.length} selected terminal(s)`,
|
||||
count: selectedSnippetTabIds.length,
|
||||
})
|
||||
: t("snippets.executeOnCurrent", {
|
||||
defaultValue:
|
||||
"Execute on current terminal (click to select multiple)",
|
||||
})}
|
||||
: t("snippets.executeOnCurrent")}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto">
|
||||
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto thin-scrollbar">
|
||||
{terminalTabs.map((tab) => (
|
||||
<Button
|
||||
key={tab.id}
|
||||
@@ -1269,8 +1282,8 @@ export function SSHToolsSidebar({
|
||||
size="sm"
|
||||
className={`rounded-full px-3 py-1 text-xs flex items-center gap-1 ${
|
||||
selectedSnippetTabIds.includes(tab.id)
|
||||
? "text-white bg-gray-700"
|
||||
: "text-gray-500"
|
||||
? "text-foreground bg-surface"
|
||||
: "text-foreground-subtle"
|
||||
}`}
|
||||
onClick={() => handleSnippetTabToggle(tab.id)}
|
||||
>
|
||||
@@ -1283,6 +1296,28 @@ export function SSHToolsSidebar({
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t("snippets.searchSnippets")}
|
||||
value={snippetSearchQuery}
|
||||
onChange={(e) => {
|
||||
setSnippetSearchQuery(e.target.value);
|
||||
}}
|
||||
className="pl-10 pr-10"
|
||||
/>
|
||||
{snippetSearchQuery && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-7 w-7 p-0"
|
||||
onClick={() => setSnippetSearchQuery("")}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
@@ -1298,9 +1333,7 @@ export function SSHToolsSidebar({
|
||||
variant="outline"
|
||||
>
|
||||
<FolderPlus className="w-4 h-4 mr-2" />
|
||||
{t("snippets.newFolder", {
|
||||
defaultValue: "New Folder",
|
||||
})}
|
||||
{t("snippets.newFolder")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1318,7 +1351,7 @@ export function SSHToolsSidebar({
|
||||
</div>
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
<div className="space-y-3 overflow-y-auto flex-1 min-h-0">
|
||||
<div className="space-y-3 overflow-y-auto flex-1 min-h-0 thin-scrollbar">
|
||||
{Array.from(groupSnippetsByFolder()).map(
|
||||
([folderName, folderSnippets]) => {
|
||||
const folderMetadata = snippetFolders.find(
|
||||
@@ -1329,7 +1362,7 @@ export function SSHToolsSidebar({
|
||||
|
||||
return (
|
||||
<div key={folderName || "uncategorized"}>
|
||||
<div className="flex items-center gap-2 mb-2 hover:bg-dark-hover-alt p-2 rounded-lg transition-colors group/folder">
|
||||
<div className="flex items-center gap-2 mb-2 hover:bg-hover-alt p-2 rounded-lg transition-colors group/folder">
|
||||
<div
|
||||
className="flex items-center gap-2 flex-1 cursor-pointer"
|
||||
onClick={() => toggleFolder(folderName)}
|
||||
@@ -1420,7 +1453,7 @@ export function SSHToolsSidebar({
|
||||
}
|
||||
onDrop={(e) => handleDrop(e, snippet)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className={`bg-dark-bg-input border border-input rounded-lg cursor-move hover:shadow-lg hover:border-gray-400/50 hover:bg-dark-hover-alt transition-all duration-200 p-3 group ${
|
||||
className={`bg-field border border-input rounded-lg cursor-move hover:shadow-lg hover:border-edge-hover hover:bg-hover-alt transition-all duration-200 p-3 group ${
|
||||
draggedSnippet?.id === snippet.id
|
||||
? "opacity-50"
|
||||
: ""
|
||||
@@ -1429,7 +1462,7 @@ export function SSHToolsSidebar({
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<GripVertical className="h-4 w-4 text-muted-foreground flex-shrink-0 opacity-50 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-medium text-white mb-1">
|
||||
<h3 className="text-sm font-medium text-foreground mb-1">
|
||||
{snippet.name}
|
||||
</h3>
|
||||
{snippet.description && (
|
||||
@@ -1550,9 +1583,7 @@ export function SSHToolsSidebar({
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t("commandHistory.searchPlaceholder", {
|
||||
defaultValue: "Search commands...",
|
||||
})}
|
||||
placeholder={t("commandHistory.searchPlaceholder")}
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
@@ -1571,10 +1602,7 @@ export function SSHToolsSidebar({
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground bg-muted/30 px-2 py-1.5 rounded">
|
||||
{t("commandHistory.tabHint", {
|
||||
defaultValue:
|
||||
"Use Tab in Terminal to autocomplete from command history",
|
||||
})}
|
||||
{t("commandHistory.tabHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1583,9 +1611,7 @@ export function SSHToolsSidebar({
|
||||
<div className="text-center py-8">
|
||||
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-4 mb-4">
|
||||
<p className="text-destructive font-medium mb-2">
|
||||
{t("commandHistory.error", {
|
||||
defaultValue: "Error loading history",
|
||||
})}
|
||||
{t("commandHistory.error")}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{historyError}
|
||||
@@ -1597,31 +1623,24 @@ export function SSHToolsSidebar({
|
||||
}
|
||||
variant="outline"
|
||||
>
|
||||
{t("common.retry", { defaultValue: "Retry" })}
|
||||
{t("common.retry")}
|
||||
</Button>
|
||||
</div>
|
||||
) : !activeTerminal ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
<Terminal className="h-12 w-12 mb-4 opacity-20 mx-auto" />
|
||||
<p className="mb-2 font-medium">
|
||||
{t("commandHistory.noTerminal", {
|
||||
defaultValue: "No active terminal",
|
||||
})}
|
||||
{t("commandHistory.noTerminal")}{" "}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{t("commandHistory.noTerminalHint", {
|
||||
defaultValue:
|
||||
"Open a terminal to see its command history.",
|
||||
})}
|
||||
{t("commandHistory.noTerminalHint")}
|
||||
</p>
|
||||
</div>
|
||||
) : isHistoryLoading && commandHistory.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
<Loader2 className="h-12 w-12 mb-4 opacity-20 mx-auto animate-spin" />
|
||||
<p className="mb-2 font-medium">
|
||||
{t("commandHistory.loading", {
|
||||
defaultValue: "Loading command history...",
|
||||
})}
|
||||
{t("commandHistory.loading")}{" "}
|
||||
</p>
|
||||
</div>
|
||||
) : filteredCommands.length === 0 ? (
|
||||
@@ -1630,13 +1649,10 @@ export function SSHToolsSidebar({
|
||||
<>
|
||||
<Search className="h-12 w-12 mb-2 opacity-20 mx-auto" />
|
||||
<p className="mb-2 font-medium">
|
||||
{t("commandHistory.noResults", {
|
||||
defaultValue: "No commands found",
|
||||
})}
|
||||
{t("commandHistory.noResults")}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{t("commandHistory.noResultsHint", {
|
||||
defaultValue: `No commands matching "${searchQuery}"`,
|
||||
query: searchQuery,
|
||||
})}
|
||||
</p>
|
||||
@@ -1644,15 +1660,10 @@ export function SSHToolsSidebar({
|
||||
) : (
|
||||
<>
|
||||
<p className="mb-2 font-medium">
|
||||
{t("commandHistory.empty", {
|
||||
defaultValue: "No command history yet",
|
||||
})}
|
||||
{t("commandHistory.empty")}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{t("commandHistory.emptyHint", {
|
||||
defaultValue:
|
||||
"Execute commands in the active terminal to build its history.",
|
||||
})}
|
||||
{t("commandHistory.emptyHint")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
@@ -1660,16 +1671,16 @@ export function SSHToolsSidebar({
|
||||
) : (
|
||||
<div
|
||||
ref={commandHistoryScrollRef}
|
||||
className="space-y-2 overflow-y-auto h-full"
|
||||
className="space-y-2 overflow-y-auto h-full thin-scrollbar"
|
||||
>
|
||||
{filteredCommands.map((command, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-dark-bg border-2 border-dark-border rounded-md px-3 py-2.5 hover:bg-dark-hover-alt hover:border-gray-600 transition-all duration-200 group h-12 flex items-center"
|
||||
className="bg-canvas border-2 border-edge rounded-md px-3 py-2.5 hover:bg-hover-alt hover:border-edge-hover transition-all duration-200 group h-12 flex items-center"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 w-full min-w-0">
|
||||
<span
|
||||
className="flex-1 font-mono text-sm cursor-pointer text-white truncate"
|
||||
className="flex-1 font-mono text-sm cursor-pointer text-foreground truncate"
|
||||
onClick={() => handleCommandSelect(command)}
|
||||
title={command}
|
||||
>
|
||||
@@ -1683,9 +1694,7 @@ export function SSHToolsSidebar({
|
||||
e.stopPropagation();
|
||||
handleCommandDelete(command);
|
||||
}}
|
||||
title={t("commandHistory.deleteTooltip", {
|
||||
defaultValue: "Delete command",
|
||||
})}
|
||||
title={t("commandHistory.deleteTooltip")}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
@@ -1701,7 +1710,7 @@ export function SSHToolsSidebar({
|
||||
value="split-screen"
|
||||
className="flex flex-col flex-1 overflow-hidden"
|
||||
>
|
||||
<div className="space-y-4 flex-1 overflow-y-auto overflow-x-hidden pb-4">
|
||||
<div className="space-y-4 flex-1 overflow-y-auto overflow-x-hidden pb-4 thin-scrollbar">
|
||||
<Tabs
|
||||
value={splitMode}
|
||||
onValueChange={(value) =>
|
||||
@@ -1713,22 +1722,16 @@ export function SSHToolsSidebar({
|
||||
>
|
||||
<TabsList className="w-full grid grid-cols-4">
|
||||
<TabsTrigger value="none">
|
||||
{t("splitScreen.none", { defaultValue: "None" })}
|
||||
{t("splitScreen.none")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="2">
|
||||
{t("splitScreen.twoSplit", {
|
||||
defaultValue: "2-Split",
|
||||
})}
|
||||
{t("splitScreen.twoSplit")}{" "}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="3">
|
||||
{t("splitScreen.threeSplit", {
|
||||
defaultValue: "3-Split",
|
||||
})}
|
||||
{t("splitScreen.threeSplit")}{" "}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="4">
|
||||
{t("splitScreen.fourSplit", {
|
||||
defaultValue: "4-Split",
|
||||
})}
|
||||
{t("splitScreen.fourSplit")}{" "}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
@@ -1738,18 +1741,13 @@ export function SSHToolsSidebar({
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white">
|
||||
{t("splitScreen.availableTabs", {
|
||||
defaultValue: "Available Tabs",
|
||||
})}
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{t("splitScreen.availableTabs")}
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
{t("splitScreen.dragTabsHint", {
|
||||
defaultValue:
|
||||
"Drag tabs into the grid below to position them",
|
||||
})}
|
||||
{t("splitScreen.dragTabsHint")}
|
||||
</p>
|
||||
<div className="space-y-1 max-h-[200px] overflow-y-auto">
|
||||
<div className="space-y-1 max-h-[200px] overflow-y-auto thin-scrollbar">
|
||||
{splittableTabs.map((tab) => {
|
||||
const isAssigned = Array.from(
|
||||
splitAssignments.values(),
|
||||
@@ -1768,8 +1766,8 @@ export function SSHToolsSidebar({
|
||||
px-3 py-2 rounded-md text-sm cursor-move transition-all
|
||||
${
|
||||
isAssigned
|
||||
? "bg-dark-bg/50 text-muted-foreground cursor-not-allowed opacity-50"
|
||||
: "bg-dark-bg border border-dark-border hover:border-gray-400 hover:bg-dark-bg-input"
|
||||
? "bg-canvas/50 text-muted-foreground cursor-not-allowed opacity-50"
|
||||
: "bg-canvas border border-edge hover:border-edge-hover hover:bg-field"
|
||||
}
|
||||
${isDragging ? "opacity-50" : ""}
|
||||
`}
|
||||
@@ -1786,13 +1784,11 @@ export function SSHToolsSidebar({
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white">
|
||||
{t("splitScreen.layout", {
|
||||
defaultValue: "Layout",
|
||||
})}
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{t("splitScreen.layout")}
|
||||
</label>
|
||||
<div
|
||||
className={`grid gap-2 ${
|
||||
className={`grid gap-2 mt-2 ${
|
||||
splitMode === "2"
|
||||
? "grid-cols-2"
|
||||
: splitMode === "3"
|
||||
@@ -1806,7 +1802,9 @@ export function SSHToolsSidebar({
|
||||
const assignedTabId =
|
||||
splitAssignments.get(idx);
|
||||
const assignedTab = assignedTabId
|
||||
? tabs.find((t) => t.id === assignedTabId)
|
||||
? splittableTabs.find(
|
||||
(t) => t.id === assignedTabId,
|
||||
)
|
||||
: null;
|
||||
const isHovered = dragOverCellIndex === idx;
|
||||
const isEmpty = !assignedTabId;
|
||||
@@ -1820,24 +1818,24 @@ export function SSHToolsSidebar({
|
||||
onDragLeave={handleTabDragLeave}
|
||||
onDrop={() => handleTabDrop(idx)}
|
||||
className={`
|
||||
relative bg-dark-bg border-2 rounded-md p-3 min-h-[100px]
|
||||
relative bg-canvas border-2 rounded-md p-3 min-h-[100px]
|
||||
flex flex-col items-center justify-center transition-all
|
||||
${splitMode === "3" && idx === 2 ? "col-span-2" : ""}
|
||||
${
|
||||
isEmpty
|
||||
? "border-dashed border-dark-border"
|
||||
: "border-solid border-gray-400 bg-gray-500/10"
|
||||
? "border-dashed border-edge"
|
||||
: "border-solid border-edge-hover bg-surface"
|
||||
}
|
||||
${
|
||||
isHovered && draggedTabId
|
||||
? "border-gray-500 bg-gray-500/20 ring-2 ring-gray-500/50"
|
||||
? "border-edge-hover bg-surface ring-2 ring-edge-hover"
|
||||
: ""
|
||||
}
|
||||
`}
|
||||
>
|
||||
{assignedTab ? (
|
||||
<>
|
||||
<span className="text-sm text-white truncate w-full text-center mb-2">
|
||||
<span className="text-sm text-foreground truncate w-full text-center mb-2">
|
||||
{assignedTab.title}
|
||||
</span>
|
||||
<Button
|
||||
@@ -1848,14 +1846,12 @@ export function SSHToolsSidebar({
|
||||
}
|
||||
className="h-6 text-xs hover:bg-red-500/20"
|
||||
>
|
||||
Remove
|
||||
{t("common.remove")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("splitScreen.dropHere", {
|
||||
defaultValue: "Drop tab here",
|
||||
})}
|
||||
{t("splitScreen.dropHere")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -1871,18 +1867,14 @@ export function SSHToolsSidebar({
|
||||
className="flex-1"
|
||||
disabled={splitAssignments.size === 0}
|
||||
>
|
||||
{t("splitScreen.apply", {
|
||||
defaultValue: "Apply Split",
|
||||
})}
|
||||
{t("splitScreen.apply")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClearSplit}
|
||||
className="flex-1"
|
||||
>
|
||||
{t("splitScreen.clear", {
|
||||
defaultValue: "Clear",
|
||||
})}
|
||||
{t("splitScreen.clear")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -1892,16 +1884,10 @@ export function SSHToolsSidebar({
|
||||
<div className="text-center py-8">
|
||||
<LayoutGrid className="h-12 w-12 mb-4 opacity-20 mx-auto" />
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
{t("splitScreen.selectMode", {
|
||||
defaultValue:
|
||||
"Select a split mode to get started",
|
||||
})}
|
||||
{t("splitScreen.selectMode")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("splitScreen.helpText", {
|
||||
defaultValue:
|
||||
"Choose how many tabs you want to display at once",
|
||||
})}
|
||||
{t("splitScreen.helpText")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -1914,16 +1900,16 @@ export function SSHToolsSidebar({
|
||||
className="absolute top-0 h-full cursor-col-resize z-[60]"
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{
|
||||
left: "-8px",
|
||||
width: "18px",
|
||||
left: "-4px",
|
||||
width: "8px",
|
||||
backgroundColor: isResizing
|
||||
? "var(--dark-active)"
|
||||
? "var(--bg-active)"
|
||||
: "transparent",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isResizing) {
|
||||
e.currentTarget.style.backgroundColor =
|
||||
"var(--dark-border-hover)";
|
||||
"var(--border-hover)";
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
@@ -1931,7 +1917,7 @@ export function SSHToolsSidebar({
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
}
|
||||
}}
|
||||
title="Drag to resize sidebar"
|
||||
title={t("common.dragToResizeSidebar")}
|
||||
/>
|
||||
)}
|
||||
</Sidebar>
|
||||
@@ -1945,11 +1931,11 @@ export function SSHToolsSidebar({
|
||||
onClick={() => setShowDialog(false)}
|
||||
>
|
||||
<div
|
||||
className="bg-dark-bg border-2 border-dark-border rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto"
|
||||
className="bg-canvas border-2 border-edge rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto thin-scrollbar"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
<h2 className="text-xl font-semibold text-foreground">
|
||||
{editingSnippet ? t("snippets.edit") : t("snippets.create")}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
@@ -1961,7 +1947,7 @@ export function SSHToolsSidebar({
|
||||
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white flex items-center gap-1">
|
||||
<label className="text-sm font-medium text-foreground flex items-center gap-1">
|
||||
{t("snippets.name")}
|
||||
<span className="text-destructive">*</span>
|
||||
</label>
|
||||
@@ -1982,7 +1968,7 @@ export function SSHToolsSidebar({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{t("snippets.description")}
|
||||
<span className="text-muted-foreground ml-1">
|
||||
({t("common.optional")})
|
||||
@@ -1998,9 +1984,9 @@ export function SSHToolsSidebar({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white flex items-center gap-2">
|
||||
<label className="text-sm font-medium text-foreground flex items-center gap-2">
|
||||
<Folder className="h-4 w-4" />
|
||||
{t("snippets.folder", { defaultValue: "Folder" })}
|
||||
{t("snippets.folder")}
|
||||
<span className="text-muted-foreground">
|
||||
({t("common.optional")})
|
||||
</span>
|
||||
@@ -2015,17 +2001,11 @@ export function SSHToolsSidebar({
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t("snippets.selectFolder", {
|
||||
defaultValue: "Select a folder or leave empty",
|
||||
})}
|
||||
/>
|
||||
<SelectValue placeholder={t("snippets.selectFolder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__no_folder__">
|
||||
{t("snippets.noFolder", {
|
||||
defaultValue: "No folder (Uncategorized)",
|
||||
})}
|
||||
{t("snippets.noFolder")}
|
||||
</SelectItem>
|
||||
{snippetFolders.map((folder) => {
|
||||
const FolderIcon = getFolderIcon(folder.name);
|
||||
@@ -2048,7 +2028,7 @@ export function SSHToolsSidebar({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white flex items-center gap-1">
|
||||
<label className="text-sm font-medium text-foreground flex items-center gap-1">
|
||||
{t("snippets.content")}
|
||||
<span className="text-destructive">*</span>
|
||||
</label>
|
||||
@@ -2093,32 +2073,26 @@ export function SSHToolsSidebar({
|
||||
onClick={() => setShowFolderDialog(false)}
|
||||
>
|
||||
<div
|
||||
className="bg-dark-bg border-2 border-dark-border rounded-lg p-6 max-w-lg w-full mx-4"
|
||||
className="bg-canvas border-2 border-edge rounded-lg p-6 max-w-lg w-full mx-4"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
<h2 className="text-xl font-semibold text-foreground">
|
||||
{editingFolder
|
||||
? t("snippets.editFolder", { defaultValue: "Edit Folder" })
|
||||
: t("snippets.createFolder", {
|
||||
defaultValue: "Create Folder",
|
||||
})}
|
||||
? t("snippets.editFolder")
|
||||
: t("snippets.createFolder")}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{editingFolder
|
||||
? t("snippets.editFolderDescription", {
|
||||
defaultValue: "Customize your snippet folder",
|
||||
})
|
||||
: t("snippets.createFolderDescription", {
|
||||
defaultValue: "Organize your snippets into folders",
|
||||
})}
|
||||
? t("snippets.editFolderDescription")
|
||||
: t("snippets.createFolderDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white flex items-center gap-1">
|
||||
{t("snippets.folderName", { defaultValue: "Folder Name" })}
|
||||
<label className="text-sm font-medium text-foreground flex items-center gap-1">
|
||||
{t("snippets.folderName")}
|
||||
<span className="text-destructive">*</span>
|
||||
</label>
|
||||
<Input
|
||||
@@ -2129,24 +2103,20 @@ export function SSHToolsSidebar({
|
||||
name: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t("snippets.folderNamePlaceholder", {
|
||||
defaultValue: "e.g., System Commands, Docker Scripts",
|
||||
})}
|
||||
placeholder={t("sshTools.scripts.inputPlaceholder")}
|
||||
className={`${folderFormErrors.name ? "border-destructive focus-visible:ring-destructive" : ""}`}
|
||||
autoFocus
|
||||
/>
|
||||
{folderFormErrors.name && (
|
||||
<p className="text-xs text-destructive mt-1">
|
||||
{t("snippets.folderNameRequired", {
|
||||
defaultValue: "Folder name is required",
|
||||
})}
|
||||
{t("snippets.folderNameRequired")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-base font-semibold text-white">
|
||||
{t("snippets.folderColor", { defaultValue: "Folder Color" })}
|
||||
<Label className="text-base font-semibold text-foreground">
|
||||
{t("snippets.folderColor")}
|
||||
</Label>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{AVAILABLE_COLORS.map((color) => (
|
||||
@@ -2156,7 +2126,7 @@ export function SSHToolsSidebar({
|
||||
className={`h-12 rounded-md border-2 transition-all hover:scale-105 ${
|
||||
folderFormData.color === color.value
|
||||
? "border-white shadow-lg scale-105"
|
||||
: "border-dark-border"
|
||||
: "border-edge"
|
||||
}`}
|
||||
style={{ backgroundColor: color.value }}
|
||||
onClick={() =>
|
||||
@@ -2172,8 +2142,8 @@ export function SSHToolsSidebar({
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-base font-semibold text-white">
|
||||
{t("snippets.folderIcon", { defaultValue: "Folder Icon" })}
|
||||
<Label className="text-base font-semibold text-foreground">
|
||||
{t("snippets.folderIcon")}
|
||||
</Label>
|
||||
<div className="grid grid-cols-5 gap-3">
|
||||
{AVAILABLE_ICONS.map(({ value, label, Icon }) => (
|
||||
@@ -2183,7 +2153,7 @@ export function SSHToolsSidebar({
|
||||
className={`h-14 rounded-md border-2 transition-all hover:scale-105 flex items-center justify-center ${
|
||||
folderFormData.icon === value
|
||||
? "border-primary bg-primary/10"
|
||||
: "border-dark-border bg-dark-bg-darker"
|
||||
: "border-edge bg-elevated"
|
||||
}`}
|
||||
onClick={() =>
|
||||
setFolderFormData({ ...folderFormData, icon: value })
|
||||
@@ -2197,10 +2167,10 @@ export function SSHToolsSidebar({
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-base font-semibold text-white">
|
||||
{t("snippets.preview", { defaultValue: "Preview" })}
|
||||
<Label className="text-base font-semibold text-foreground">
|
||||
{t("snippets.preview")}
|
||||
</Label>
|
||||
<div className="flex items-center gap-3 p-4 rounded-md bg-dark-bg-darker border border-dark-border">
|
||||
<div className="flex items-center gap-3 p-4 rounded-md bg-elevated border border-edge">
|
||||
{(() => {
|
||||
const IconComponent =
|
||||
AVAILABLE_ICONS.find(
|
||||
@@ -2214,8 +2184,7 @@ export function SSHToolsSidebar({
|
||||
);
|
||||
})()}
|
||||
<span className="font-medium">
|
||||
{folderFormData.name ||
|
||||
t("snippets.folderName", { defaultValue: "Folder Name" })}
|
||||
{folderFormData.name || t("snippets.folderName")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2233,12 +2202,8 @@ export function SSHToolsSidebar({
|
||||
</Button>
|
||||
<Button onClick={handleFolderSubmit} className="flex-1">
|
||||
{editingFolder
|
||||
? t("snippets.updateFolder", {
|
||||
defaultValue: "Update Folder",
|
||||
})
|
||||
: t("snippets.createFolder", {
|
||||
defaultValue: "Create Folder",
|
||||
})}
|
||||
? t("snippets.updateFolder")
|
||||
: t("snippets.createFolder")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user