feat: redesign UI with Twilight design system
Outfit + JetBrains Mono typography, soft dark palette with blue undertones, electric teal primary, purple-branded AI features, noise texture, glow effects, glassmorphism, and refined grid/tree.
This commit is contained in:
@@ -36,24 +36,24 @@ export function ReadOnlyToggle() {
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={`h-7 gap-1.5 text-xs font-medium ${
|
||||
size="xs"
|
||||
className={`gap-1.5 font-medium ${
|
||||
isReadOnly
|
||||
? "text-yellow-600 dark:text-yellow-500"
|
||||
: "text-green-600 dark:text-green-500"
|
||||
? "text-amber-500 hover:bg-amber-500/10 hover:text-amber-500"
|
||||
: "text-emerald-500 hover:bg-emerald-500/10 hover:text-emerald-500"
|
||||
}`}
|
||||
onClick={handleToggle}
|
||||
disabled={toggleMutation.isPending}
|
||||
>
|
||||
{isReadOnly ? (
|
||||
<>
|
||||
<Lock className="h-3.5 w-3.5" />
|
||||
Read-Only
|
||||
<Lock className="h-3 w-3" />
|
||||
<span className="text-[11px] tracking-wide">Read-Only</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LockOpen className="h-3.5 w-3.5" />
|
||||
Read-Write
|
||||
<LockOpen className="h-3 w-3" />
|
||||
<span className="text-[11px] tracking-wide">Read-Write</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -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: <Layers className="h-3.5 w-3.5" /> },
|
||||
{ id: "history", label: "History", icon: <Clock className="h-3.5 w-3.5" /> },
|
||||
{ id: "saved", label: "Saved", icon: <Bookmark className="h-3.5 w-3.5" /> },
|
||||
{ id: "admin", label: "Admin", icon: <Shield className="h-3.5 w-3.5" /> },
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
const [view, setView] = useState<SidebarView>("schema");
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -32,58 +39,33 @@ export function Sidebar() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-card">
|
||||
<div className="flex border-b text-xs">
|
||||
<button
|
||||
className={`flex-1 px-3 py-1.5 font-medium ${
|
||||
view === "schema"
|
||||
? "bg-background text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setView("schema")}
|
||||
>
|
||||
Schema
|
||||
</button>
|
||||
<button
|
||||
className={`flex-1 px-3 py-1.5 font-medium ${
|
||||
view === "history"
|
||||
? "bg-background text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setView("history")}
|
||||
>
|
||||
History
|
||||
</button>
|
||||
<button
|
||||
className={`flex-1 px-3 py-1.5 font-medium ${
|
||||
view === "saved"
|
||||
? "bg-background text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setView("saved")}
|
||||
>
|
||||
Saved
|
||||
</button>
|
||||
<button
|
||||
className={`flex-1 px-3 py-1.5 font-medium ${
|
||||
view === "admin"
|
||||
? "bg-background text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setView("admin")}
|
||||
>
|
||||
Admin
|
||||
</button>
|
||||
<div className="flex h-full flex-col" style={{ background: "var(--sidebar)" }}>
|
||||
{/* Sidebar navigation tabs */}
|
||||
<div className="flex border-b border-border/50">
|
||||
{SIDEBAR_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`relative flex flex-1 items-center justify-center gap-1.5 px-2 py-2 text-[11px] font-medium tracking-wide transition-colors ${
|
||||
view === tab.id
|
||||
? "text-foreground tusk-sidebar-tab-active"
|
||||
: "text-muted-foreground hover:text-foreground/70"
|
||||
}`}
|
||||
onClick={() => setView(tab.id)}
|
||||
>
|
||||
{tab.icon}
|
||||
<span className="hidden min-[220px]:inline">{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{view === "schema" ? (
|
||||
<>
|
||||
<div className="flex items-center gap-1 p-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Search className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/60" />
|
||||
<Input
|
||||
placeholder="Search objects..."
|
||||
className="h-7 pl-7 text-xs"
|
||||
className="h-7 border-border/40 bg-background/50 pl-7 text-xs placeholder:text-muted-foreground/40 focus:border-primary/40 focus:ring-primary/20"
|
||||
value={search}
|
||||
onChange={(e) => 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"
|
||||
>
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex h-6 items-center justify-between border-t bg-card px-3 text-[11px] text-muted-foreground">
|
||||
<div className="tusk-status-bar flex h-6 items-center justify-between px-3 text-[11px] text-muted-foreground">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="flex items-center gap-1.5">
|
||||
{activeConn?.color ? (
|
||||
<span
|
||||
className="inline-block h-2 w-2 rounded-full"
|
||||
className="inline-block h-2 w-2 rounded-full ring-1 ring-white/10"
|
||||
style={{ backgroundColor: activeConn.color }}
|
||||
/>
|
||||
) : (
|
||||
<Circle
|
||||
className={`h-2 w-2 ${isConnected ? "fill-green-500 text-green-500" : "fill-muted text-muted"}`}
|
||||
<span
|
||||
className={`inline-block h-2 w-2 rounded-full ${
|
||||
isConnected
|
||||
? "bg-emerald-500 shadow-[0_0_6px_theme(--color-emerald-500/40)]"
|
||||
: "bg-muted-foreground/30"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
{activeConn ? activeConn.name : "No connection"}
|
||||
<span className="font-medium">
|
||||
{activeConn ? activeConn.name : "No connection"}
|
||||
</span>
|
||||
<EnvironmentBadge environment={activeConn?.environment} size="sm" />
|
||||
</span>
|
||||
{isConnected && activeConnectionId && (
|
||||
<span
|
||||
className={`font-semibold ${
|
||||
className={`rounded px-1 py-px text-[10px] font-semibold tracking-wide ${
|
||||
(readOnlyMap[activeConnectionId] ?? true)
|
||||
? "text-yellow-600 dark:text-yellow-500"
|
||||
: "text-green-600 dark:text-green-500"
|
||||
? "bg-amber-500/10 text-amber-500"
|
||||
: "bg-emerald-500/10 text-emerald-500"
|
||||
}`}
|
||||
>
|
||||
{(readOnlyMap[activeConnectionId] ?? true) ? "RO" : "RW"}
|
||||
{(readOnlyMap[activeConnectionId] ?? true) ? "READ" : "WRITE"}
|
||||
</span>
|
||||
)}
|
||||
{pgVersion && (
|
||||
<span className="hidden sm:inline">{formatDbVersion(pgVersion)}</span>
|
||||
<span className="hidden text-muted-foreground/60 sm:inline font-mono text-[10px]">
|
||||
{formatDbVersion(pgVersion)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{rowCount != null && <span>{rowCount.toLocaleString()} rows</span>}
|
||||
{executionTime != null && <span>{executionTime} ms</span>}
|
||||
{rowCount != null && (
|
||||
<span className="font-mono">
|
||||
{rowCount.toLocaleString()} <span className="text-muted-foreground/50">rows</span>
|
||||
</span>
|
||||
)}
|
||||
{executionTime != null && (
|
||||
<span className="font-mono">
|
||||
{executionTime} <span className="text-muted-foreground/50">ms</span>
|
||||
</span>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="flex items-center gap-1 cursor-default">
|
||||
<span className="flex items-center gap-1.5 cursor-default">
|
||||
<span
|
||||
className={`inline-block h-1.5 w-1.5 rounded-full ${
|
||||
className={`inline-block h-1.5 w-1.5 rounded-full transition-colors ${
|
||||
mcpStatus?.running
|
||||
? "bg-green-500"
|
||||
: "bg-muted-foreground/30"
|
||||
? "bg-emerald-500 shadow-[0_0_4px_theme(--color-emerald-500/40)]"
|
||||
: "bg-muted-foreground/20"
|
||||
}`}
|
||||
/>
|
||||
<span>MCP</span>
|
||||
<span className="tracking-wide">MCP</span>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
|
||||
@@ -23,48 +23,55 @@ export function TabBar() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-b bg-card">
|
||||
<div className="border-b border-border/40" style={{ background: "var(--card)" }}>
|
||||
<ScrollArea>
|
||||
<div className="flex">
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={`group flex h-8 cursor-pointer items-center gap-1.5 border-r px-3 text-xs ${
|
||||
activeTabId === tab.id
|
||||
? "bg-background text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => 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 (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={`group relative flex h-8 cursor-pointer items-center gap-1.5 px-3 text-xs transition-colors ${
|
||||
isActive
|
||||
? "bg-background text-foreground tusk-tab-active"
|
||||
: "text-muted-foreground hover:bg-accent/30 hover:text-foreground/80"
|
||||
}`}
|
||||
onClick={() => setActiveTabId(tab.id)}
|
||||
>
|
||||
{tabColor && (
|
||||
<span
|
||||
className="h-2 w-2 shrink-0 rounded-full"
|
||||
style={{ backgroundColor: tabColor }}
|
||||
/>
|
||||
) : null;
|
||||
})()}
|
||||
{iconMap[tab.type]}
|
||||
<span className="max-w-[150px] truncate">
|
||||
{tab.title}
|
||||
{tab.database && (
|
||||
<span className="ml-1 text-[10px] text-muted-foreground">
|
||||
{tab.database}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
className="ml-1 rounded-sm opacity-0 hover:bg-accent group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
closeTab(tab.id);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<span className="opacity-60">{iconMap[tab.type]}</span>
|
||||
<span className="max-w-[150px] truncate font-medium">
|
||||
{tab.title}
|
||||
{tab.database && (
|
||||
<span className="ml-1 text-[10px] font-normal text-muted-foreground/60">
|
||||
{tab.database}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
className="ml-1 rounded-sm p-0.5 opacity-0 transition-opacity hover:bg-accent group-hover:opacity-60 hover:!opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
closeTab(tab.id);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
{/* Right separator between tabs */}
|
||||
{!isActive && (
|
||||
<div className="absolute right-0 top-1.5 bottom-1.5 w-px bg-border/30" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div
|
||||
className="flex h-10 items-center gap-2 border-b px-3 bg-card"
|
||||
style={{ borderLeftWidth: activeColor ? 3 : 0, borderLeftColor: activeColor }}
|
||||
className="tusk-toolbar tusk-conn-strip flex h-10 items-center gap-1.5 px-3"
|
||||
style={{
|
||||
"--strip-width": activeColor ? "3px" : "0px",
|
||||
"--strip-color": activeColor ?? "transparent",
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 gap-1.5"
|
||||
size="xs"
|
||||
className="gap-1.5 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setListOpen(true)}
|
||||
>
|
||||
<Database className="h-3.5 w-3.5" />
|
||||
Connections
|
||||
<span className="text-xs font-medium">Connections</span>
|
||||
</Button>
|
||||
|
||||
<Separator orientation="vertical" className="h-5" />
|
||||
<div className="mx-1 h-4 w-px bg-border" />
|
||||
|
||||
<ConnectionSelector />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0"
|
||||
size="icon-xs"
|
||||
onClick={handleReconnect}
|
||||
disabled={!activeConnectionId || reconnectMutation.isPending}
|
||||
title="Reconnect"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<RefreshCw className={`h-3.5 w-3.5 ${reconnectMutation.isPending ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
|
||||
<Separator orientation="vertical" className="h-5" />
|
||||
<div className="mx-1 h-4 w-px bg-border" />
|
||||
|
||||
<ReadOnlyToggle />
|
||||
|
||||
<Separator orientation="vertical" className="h-5" />
|
||||
<div className="mx-1 h-4 w-px bg-border" />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 gap-1.5"
|
||||
size="xs"
|
||||
className="gap-1.5 text-muted-foreground hover:text-foreground"
|
||||
onClick={handleNewQuery}
|
||||
disabled={!activeConnectionId}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
New Query
|
||||
<span className="text-xs font-medium">New Query</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 gap-1.5"
|
||||
size="xs"
|
||||
className="gap-1.5 text-muted-foreground hover:text-foreground"
|
||||
onClick={handleNewLookup}
|
||||
disabled={!activeConnectionId}
|
||||
>
|
||||
<Search className="h-3.5 w-3.5" />
|
||||
Entity Lookup
|
||||
<span className="text-xs font-medium">Lookup</span>
|
||||
</Button>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0"
|
||||
size="icon-xs"
|
||||
onClick={() => setSettingsOpen(true)}
|
||||
title="Settings"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user