diff --git a/index.html b/index.html index d901d62..412e2f8 100644 --- a/index.html +++ b/index.html @@ -1,9 +1,12 @@ - + Tusk + + +
diff --git a/src/App.tsx b/src/App.tsx index b94aeb5..3725fae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -49,7 +49,7 @@ export default function App() { }, [handleNewQuery, handleCloseTab]); return ( -
+
diff --git a/src/components/ai/AiBar.tsx b/src/components/ai/AiBar.tsx index 86ecdad..5adf802 100644 --- a/src/components/ai/AiBar.tsx +++ b/src/components/ai/AiBar.tsx @@ -52,21 +52,21 @@ export function AiBar({ connectionId, onSqlGenerated, onClose, onExecute }: Prop }; return ( -
- +
+ setPrompt(e.target.value)} onKeyDown={handleKeyDown} placeholder="Describe the query you want..." - className="h-7 min-w-0 flex-1 text-xs" + className="h-7 min-w-0 flex-1 border-tusk-purple/20 bg-tusk-purple/5 text-xs placeholder:text-muted-foreground/40 focus:border-tusk-purple/40 focus:ring-tusk-purple/20" autoFocus disabled={generateMutation.isPending} /> {prompt.trim() && ( )} diff --git a/src/components/layout/ReadOnlyToggle.tsx b/src/components/layout/ReadOnlyToggle.tsx index 590cf82..cd82294 100644 --- a/src/components/layout/ReadOnlyToggle.tsx +++ b/src/components/layout/ReadOnlyToggle.tsx @@ -36,24 +36,24 @@ export function ReadOnlyToggle() { diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 06122b3..462cd33 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -11,7 +11,7 @@ import { SchemaTree } from "@/components/schema/SchemaTree"; import { HistoryPanel } from "@/components/history/HistoryPanel"; import { SavedQueriesPanel } from "@/components/saved-queries/SavedQueriesPanel"; import { AdminPanel } from "@/components/management/AdminPanel"; -import { Search, RefreshCw } from "lucide-react"; +import { Search, RefreshCw, Layers, Clock, Bookmark, Shield } from "lucide-react"; type SidebarView = "schema" | "history" | "saved" | "admin"; @@ -20,6 +20,13 @@ const SCHEMA_QUERY_KEYS = [ "functions", "sequences", "completionSchema", "column-details", ]; +const SIDEBAR_TABS: { id: SidebarView; label: string; icon: React.ReactNode }[] = [ + { id: "schema", label: "Schema", icon: }, + { id: "history", label: "History", icon: }, + { id: "saved", label: "Saved", icon: }, + { id: "admin", label: "Admin", icon: }, +]; + export function Sidebar() { const [view, setView] = useState("schema"); const [search, setSearch] = useState(""); @@ -32,58 +39,33 @@ export function Sidebar() { }; return ( -
-
- - - - +
+ {/* Sidebar navigation tabs */} +
+ {SIDEBAR_TABS.map((tab) => ( + + ))}
{view === "schema" ? ( <>
- + setSearch(e.target.value)} /> @@ -94,6 +76,7 @@ export function Sidebar() { variant="ghost" size="icon-xs" onClick={handleRefreshSchema} + className="text-muted-foreground hover:text-foreground" > diff --git a/src/components/layout/StatusBar.tsx b/src/components/layout/StatusBar.tsx index 3cfd803..be2e37c 100644 --- a/src/components/layout/StatusBar.tsx +++ b/src/components/layout/StatusBar.tsx @@ -1,7 +1,6 @@ import { useAppStore } from "@/stores/app-store"; import { useConnections } from "@/hooks/use-connections"; import { useMcpStatus } from "@/hooks/use-settings"; -import { Circle } from "lucide-react"; import { EnvironmentBadge } from "@/components/connections/EnvironmentBadge"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; @@ -31,51 +30,67 @@ export function StatusBar({ rowCount, executionTime }: Props) { : false; return ( -
+
- + {activeConn?.color ? ( ) : ( - )} - {activeConn ? activeConn.name : "No connection"} + + {activeConn ? activeConn.name : "No connection"} + {isConnected && activeConnectionId && ( - {(readOnlyMap[activeConnectionId] ?? true) ? "RO" : "RW"} + {(readOnlyMap[activeConnectionId] ?? true) ? "READ" : "WRITE"} )} {pgVersion && ( - {formatDbVersion(pgVersion)} + + {formatDbVersion(pgVersion)} + )}
- {rowCount != null && {rowCount.toLocaleString()} rows} - {executionTime != null && {executionTime} ms} + {rowCount != null && ( + + {rowCount.toLocaleString()} rows + + )} + {executionTime != null && ( + + {executionTime} ms + + )} - + - MCP + MCP diff --git a/src/components/layout/TabBar.tsx b/src/components/layout/TabBar.tsx index a538203..b66485e 100644 --- a/src/components/layout/TabBar.tsx +++ b/src/components/layout/TabBar.tsx @@ -23,48 +23,55 @@ export function TabBar() { }; return ( -
+
- {tabs.map((tab) => ( -
setActiveTabId(tab.id)} - > - {(() => { - const tabColor = connections?.find((c) => c.id === tab.connectionId)?.color; - return tabColor ? ( + {tabs.map((tab) => { + const isActive = activeTabId === tab.id; + const tabColor = connections?.find((c) => c.id === tab.connectionId)?.color; + + return ( +
setActiveTabId(tab.id)} + > + {tabColor && ( - ) : null; - })()} - {iconMap[tab.type]} - - {tab.title} - {tab.database && ( - - {tab.database} - )} - - -
- ))} + {iconMap[tab.type]} + + {tab.title} + {tab.database && ( + + {tab.database} + + )} + + + + {/* Right separator between tabs */} + {!isActive && ( +
+ )} +
+ ); + })}
diff --git a/src/components/layout/Toolbar.tsx b/src/components/layout/Toolbar.tsx index 8d192bb..435a794 100644 --- a/src/components/layout/Toolbar.tsx +++ b/src/components/layout/Toolbar.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; -import { Separator } from "@/components/ui/separator"; import { ConnectionSelector } from "@/components/connections/ConnectionSelector"; import { ConnectionList } from "@/components/connections/ConnectionList"; import { ConnectionDialog } from "@/components/connections/ConnectionDialog"; @@ -61,70 +60,73 @@ export function Toolbar() { return ( <>
- +
- +
- +
diff --git a/src/components/results/ResultsTable.tsx b/src/components/results/ResultsTable.tsx index 7986ee7..49e6c97 100644 --- a/src/components/results/ResultsTable.tsx +++ b/src/components/results/ResultsTable.tsx @@ -55,11 +55,11 @@ export function ResultsTable({ return (
@@ -108,7 +108,6 @@ export function ResultsTable({ (colName: string, defaultHandler: ((e: unknown) => void) | undefined) => (e: unknown) => { if (externalSort) { - // Cycle: none → ASC → DESC → none if (externalSort.column !== colName) { externalSort.onSort(colName, "ASC"); } else if (externalSort.direction === "ASC") { @@ -141,16 +140,16 @@ export function ResultsTable({ return (
{/* Header */} -
+
{table.getHeaderGroups().map((headerGroup) => headerGroup.headers.map((header) => (
{flexRender( @@ -158,10 +157,10 @@ export function ResultsTable({ header.getContext() )} {getIsSorted(header.column.id, header.column.getIsSorted()) === "asc" && ( - + )} {getIsSorted(header.column.id, header.column.getIsSorted()) === "desc" && ( - + )}
{/* Resize handle */} @@ -169,7 +168,7 @@ export function ResultsTable({ onMouseDown={header.getResizeHandler()} onTouchStart={header.getResizeHandler()} onDoubleClick={() => header.column.resetSize()} - className={`absolute right-0 top-0 h-full w-1 cursor-col-resize select-none touch-none hover:bg-primary ${ + className={`absolute right-0 top-0 h-full w-1 cursor-col-resize select-none touch-none transition-colors hover:bg-primary/60 ${ header.column.getIsResizing() ? "bg-primary" : "" }`} /> @@ -190,7 +189,7 @@ export function ResultsTable({ return (
{flexRender( diff --git a/src/components/schema/SchemaTree.tsx b/src/components/schema/SchemaTree.tsx index 4415831..549259b 100644 --- a/src/components/schema/SchemaTree.tsx +++ b/src/components/schema/SchemaTree.tsx @@ -55,7 +55,7 @@ function TableSizeInfo({ item }: { item: SchemaObject }) { if (item.row_count != null) parts.push(formatCount(item.row_count)); if (item.size_bytes != null) parts.push(formatSize(item.size_bytes)); return ( - + {parts.join(", ")} ); @@ -71,15 +71,18 @@ export function SchemaTree() { if (!activeConnectionId) { return ( -
- Connect to a database to browse schema. +
+ +

+ Connect to a database to browse schema +

); } if (!databases || databases.length === 0) { return ( -
+
No databases found.
); @@ -218,22 +221,26 @@ function DatabaseNode({
- {expanded ? ( - - ) : ( - - )} + + {expanded ? ( + + ) : ( + + )} + {name} {isActive && ( - active + + active + )}
@@ -316,7 +323,7 @@ function DatabaseNode({
)} {expanded && !isActive && ( -
+
{isSwitching ? "Switching..." : "Click to switch to this database"}
)} @@ -339,7 +346,7 @@ function SchemasForCurrentDb({ if (!schemas || schemas.length === 0) { return ( -
No schemas found.
+
No schemas found.
); } @@ -379,18 +386,20 @@ function SchemaNode({
setExpanded(!expanded)} > + + {expanded ? ( + + ) : ( + + )} + {expanded ? ( - + ) : ( - - )} - {expanded ? ( - - ) : ( - + )} {schema}
@@ -442,10 +451,10 @@ function SchemaNode({ } const categoryIcons = { - tables: , - views: , - functions: , - sequences: , + tables: , + views: , + functions: , + sequences: , }; function CategoryNode({ @@ -498,18 +507,24 @@ function CategoryNode({ return (
setExpanded(!expanded)} > - {expanded ? ( - - ) : ( - - )} + + {expanded ? ( + + ) : ( + + )} + {icon} - + {label} - {items ? ` (${items.length})` : ""} + {items && ( + + {items.length} + + )}
{expanded && ( @@ -522,12 +537,12 @@ function CategoryNode({
onOpenTable(item.name)} > {icon} - {item.name} + {item.name} {category === "tables" && }
@@ -561,11 +576,11 @@ function CategoryNode({ return (
{icon} - {item.name} + {item.name}
); })} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index b5ea4ab..a222f7a 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,19 +5,19 @@ import { Slot } from "radix-ui" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-150 disabled:pointer-events-none disabled:opacity-40 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-default", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", + default: "bg-primary text-primary-foreground shadow-sm shadow-primary/20 hover:bg-primary/90 hover:shadow-md hover:shadow-primary/25", destructive: - "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + "bg-destructive text-white shadow-sm shadow-destructive/20 hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + "border border-border/60 bg-transparent shadow-xs hover:bg-accent/50 hover:text-accent-foreground hover:border-border", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + "hover:bg-accent/50 hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 8916905..d0bc39a 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -8,8 +8,8 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { type={type} data-slot="input" className={cn( - "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", - "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", + "file:text-foreground placeholder:text-muted-foreground/50 selection:bg-primary/20 selection:text-foreground border-border/50 h-9 w-full min-w-0 rounded-md border bg-background/50 px-3 py-1 text-base shadow-xs transition-all duration-150 outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-40 md:text-sm", + "focus-visible:border-primary/40 focus-visible:ring-primary/15 focus-visible:ring-[3px] focus-visible:bg-background/80", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", className )} diff --git a/src/components/workspace/WorkspacePanel.tsx b/src/components/workspace/WorkspacePanel.tsx index 0c954de..c0fcf98 100644 --- a/src/components/workspace/WorkspacePanel.tsx +++ b/src/components/workspace/WorkspacePanel.tsx @@ -255,11 +255,12 @@ export function WorkspacePanel({
-
+ {/* Editor action bar */} +
+ +
+ + {/* AI actions group — purple-branded */} - - - handleExport("csv")}> - Export CSV - - handleExport("json")}> - Export JSON - - - + <> +
+ + + + + + handleExport("csv")}> + Export CSV + + handleExport("json")}> + Export JSON + + + + )} - - Ctrl+Enter to execute + +
+ + + {"\u2318"}Enter {isReadOnly && ( - - - Read-Only + + + READ )}
@@ -389,35 +401,41 @@ export function WorkspacePanel({
{(explainData || result || error || aiExplanation) && ( -
+
{explainData && ( )} {resultView === "results" && result && result.columns.length > 0 && ( -
+