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:
@@ -1,9 +1,12 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" class="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Tusk</title>
|
<title>Tusk</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default function App() {
|
|||||||
}, [handleNewQuery, handleCloseTab]);
|
}, [handleNewQuery, handleCloseTab]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen flex-col">
|
<div className="tusk-noise flex h-screen flex-col">
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<ResizablePanelGroup orientation="horizontal">
|
<ResizablePanelGroup orientation="horizontal">
|
||||||
|
|||||||
@@ -52,21 +52,21 @@ export function AiBar({ connectionId, onSqlGenerated, onClose, onExecute }: Prop
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 border-b bg-muted/50 px-2 py-1">
|
<div className="tusk-ai-bar flex items-center gap-2 px-2 py-1.5 tusk-fade-in">
|
||||||
<Sparkles className="h-3.5 w-3.5 shrink-0 text-purple-500" />
|
<Sparkles className="h-3.5 w-3.5 shrink-0 tusk-ai-icon" />
|
||||||
<Input
|
<Input
|
||||||
value={prompt}
|
value={prompt}
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Describe the query you want..."
|
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
|
autoFocus
|
||||||
disabled={generateMutation.isPending}
|
disabled={generateMutation.isPending}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 gap-1 text-xs"
|
className="gap-1 text-[11px] text-tusk-purple hover:bg-tusk-purple/10 hover:text-tusk-purple"
|
||||||
onClick={handleGenerate}
|
onClick={handleGenerate}
|
||||||
disabled={generateMutation.isPending || !prompt.trim()}
|
disabled={generateMutation.isPending || !prompt.trim()}
|
||||||
>
|
>
|
||||||
@@ -78,23 +78,23 @@ export function AiBar({ connectionId, onSqlGenerated, onClose, onExecute }: Prop
|
|||||||
</Button>
|
</Button>
|
||||||
{prompt.trim() && (
|
{prompt.trim() && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="icon-xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 w-6 p-0"
|
|
||||||
onClick={() => setPrompt("")}
|
onClick={() => setPrompt("")}
|
||||||
title="Clear prompt"
|
title="Clear prompt"
|
||||||
disabled={generateMutation.isPending}
|
disabled={generateMutation.isPending}
|
||||||
|
className="text-muted-foreground"
|
||||||
>
|
>
|
||||||
<Eraser className="h-3 w-3" />
|
<Eraser className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<AiSettingsPopover />
|
<AiSettingsPopover />
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="icon-xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 w-6 p-0"
|
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
title="Close AI bar"
|
title="Close AI bar"
|
||||||
|
className="text-muted-foreground"
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -36,24 +36,24 @@ export function ReadOnlyToggle() {
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="xs"
|
||||||
className={`h-7 gap-1.5 text-xs font-medium ${
|
className={`gap-1.5 font-medium ${
|
||||||
isReadOnly
|
isReadOnly
|
||||||
? "text-yellow-600 dark:text-yellow-500"
|
? "text-amber-500 hover:bg-amber-500/10 hover:text-amber-500"
|
||||||
: "text-green-600 dark:text-green-500"
|
: "text-emerald-500 hover:bg-emerald-500/10 hover:text-emerald-500"
|
||||||
}`}
|
}`}
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
disabled={toggleMutation.isPending}
|
disabled={toggleMutation.isPending}
|
||||||
>
|
>
|
||||||
{isReadOnly ? (
|
{isReadOnly ? (
|
||||||
<>
|
<>
|
||||||
<Lock className="h-3.5 w-3.5" />
|
<Lock className="h-3 w-3" />
|
||||||
Read-Only
|
<span className="text-[11px] tracking-wide">Read-Only</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<LockOpen className="h-3.5 w-3.5" />
|
<LockOpen className="h-3 w-3" />
|
||||||
Read-Write
|
<span className="text-[11px] tracking-wide">Read-Write</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { SchemaTree } from "@/components/schema/SchemaTree";
|
|||||||
import { HistoryPanel } from "@/components/history/HistoryPanel";
|
import { HistoryPanel } from "@/components/history/HistoryPanel";
|
||||||
import { SavedQueriesPanel } from "@/components/saved-queries/SavedQueriesPanel";
|
import { SavedQueriesPanel } from "@/components/saved-queries/SavedQueriesPanel";
|
||||||
import { AdminPanel } from "@/components/management/AdminPanel";
|
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";
|
type SidebarView = "schema" | "history" | "saved" | "admin";
|
||||||
|
|
||||||
@@ -20,6 +20,13 @@ const SCHEMA_QUERY_KEYS = [
|
|||||||
"functions", "sequences", "completionSchema", "column-details",
|
"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() {
|
export function Sidebar() {
|
||||||
const [view, setView] = useState<SidebarView>("schema");
|
const [view, setView] = useState<SidebarView>("schema");
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -32,58 +39,33 @@ export function Sidebar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col bg-card">
|
<div className="flex h-full flex-col" style={{ background: "var(--sidebar)" }}>
|
||||||
<div className="flex border-b text-xs">
|
{/* Sidebar navigation tabs */}
|
||||||
<button
|
<div className="flex border-b border-border/50">
|
||||||
className={`flex-1 px-3 py-1.5 font-medium ${
|
{SIDEBAR_TABS.map((tab) => (
|
||||||
view === "schema"
|
<button
|
||||||
? "bg-background text-foreground"
|
key={tab.id}
|
||||||
: "text-muted-foreground hover:text-foreground"
|
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
|
||||||
onClick={() => setView("schema")}
|
? "text-foreground tusk-sidebar-tab-active"
|
||||||
>
|
: "text-muted-foreground hover:text-foreground/70"
|
||||||
Schema
|
}`}
|
||||||
</button>
|
onClick={() => setView(tab.id)}
|
||||||
<button
|
>
|
||||||
className={`flex-1 px-3 py-1.5 font-medium ${
|
{tab.icon}
|
||||||
view === "history"
|
<span className="hidden min-[220px]:inline">{tab.label}</span>
|
||||||
? "bg-background text-foreground"
|
</button>
|
||||||
: "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>
|
</div>
|
||||||
|
|
||||||
{view === "schema" ? (
|
{view === "schema" ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-1 p-2">
|
<div className="flex items-center gap-1 p-2">
|
||||||
<div className="relative flex-1">
|
<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
|
<Input
|
||||||
placeholder="Search objects..."
|
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}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -94,6 +76,7 @@ export function Sidebar() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-xs"
|
size="icon-xs"
|
||||||
onClick={handleRefreshSchema}
|
onClick={handleRefreshSchema}
|
||||||
|
className="text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-3.5 w-3.5" />
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useAppStore } from "@/stores/app-store";
|
import { useAppStore } from "@/stores/app-store";
|
||||||
import { useConnections } from "@/hooks/use-connections";
|
import { useConnections } from "@/hooks/use-connections";
|
||||||
import { useMcpStatus } from "@/hooks/use-settings";
|
import { useMcpStatus } from "@/hooks/use-settings";
|
||||||
import { Circle } from "lucide-react";
|
|
||||||
import { EnvironmentBadge } from "@/components/connections/EnvironmentBadge";
|
import { EnvironmentBadge } from "@/components/connections/EnvironmentBadge";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
@@ -31,51 +30,67 @@ export function StatusBar({ rowCount, executionTime }: Props) {
|
|||||||
: false;
|
: false;
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex items-center gap-3">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1.5">
|
||||||
{activeConn?.color ? (
|
{activeConn?.color ? (
|
||||||
<span
|
<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 }}
|
style={{ backgroundColor: activeConn.color }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Circle
|
<span
|
||||||
className={`h-2 w-2 ${isConnected ? "fill-green-500 text-green-500" : "fill-muted text-muted"}`}
|
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" />
|
<EnvironmentBadge environment={activeConn?.environment} size="sm" />
|
||||||
</span>
|
</span>
|
||||||
{isConnected && activeConnectionId && (
|
{isConnected && activeConnectionId && (
|
||||||
<span
|
<span
|
||||||
className={`font-semibold ${
|
className={`rounded px-1 py-px text-[10px] font-semibold tracking-wide ${
|
||||||
(readOnlyMap[activeConnectionId] ?? true)
|
(readOnlyMap[activeConnectionId] ?? true)
|
||||||
? "text-yellow-600 dark:text-yellow-500"
|
? "bg-amber-500/10 text-amber-500"
|
||||||
: "text-green-600 dark:text-green-500"
|
: "bg-emerald-500/10 text-emerald-500"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{(readOnlyMap[activeConnectionId] ?? true) ? "RO" : "RW"}
|
{(readOnlyMap[activeConnectionId] ?? true) ? "READ" : "WRITE"}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{pgVersion && (
|
{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>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{rowCount != null && <span>{rowCount.toLocaleString()} rows</span>}
|
{rowCount != null && (
|
||||||
{executionTime != null && <span>{executionTime} ms</span>}
|
<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>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span className="flex items-center gap-1 cursor-default">
|
<span className="flex items-center gap-1.5 cursor-default">
|
||||||
<span
|
<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
|
mcpStatus?.running
|
||||||
? "bg-green-500"
|
? "bg-emerald-500 shadow-[0_0_4px_theme(--color-emerald-500/40)]"
|
||||||
: "bg-muted-foreground/30"
|
: "bg-muted-foreground/20"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span>MCP</span>
|
<span className="tracking-wide">MCP</span>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top">
|
<TooltipContent side="top">
|
||||||
|
|||||||
@@ -23,48 +23,55 @@ export function TabBar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b bg-card">
|
<div className="border-b border-border/40" style={{ background: "var(--card)" }}>
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => {
|
||||||
<div
|
const isActive = activeTabId === tab.id;
|
||||||
key={tab.id}
|
const tabColor = connections?.find((c) => c.id === tab.connectionId)?.color;
|
||||||
className={`group flex h-8 cursor-pointer items-center gap-1.5 border-r px-3 text-xs ${
|
|
||||||
activeTabId === tab.id
|
return (
|
||||||
? "bg-background text-foreground"
|
<div
|
||||||
: "text-muted-foreground hover:text-foreground"
|
key={tab.id}
|
||||||
}`}
|
className={`group relative flex h-8 cursor-pointer items-center gap-1.5 px-3 text-xs transition-colors ${
|
||||||
onClick={() => setActiveTabId(tab.id)}
|
isActive
|
||||||
>
|
? "bg-background text-foreground tusk-tab-active"
|
||||||
{(() => {
|
: "text-muted-foreground hover:bg-accent/30 hover:text-foreground/80"
|
||||||
const tabColor = connections?.find((c) => c.id === tab.connectionId)?.color;
|
}`}
|
||||||
return tabColor ? (
|
onClick={() => setActiveTabId(tab.id)}
|
||||||
|
>
|
||||||
|
{tabColor && (
|
||||||
<span
|
<span
|
||||||
className="h-2 w-2 shrink-0 rounded-full"
|
className="h-2 w-2 shrink-0 rounded-full"
|
||||||
style={{ backgroundColor: tabColor }}
|
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>
|
<span className="opacity-60">{iconMap[tab.type]}</span>
|
||||||
<button
|
<span className="max-w-[150px] truncate font-medium">
|
||||||
className="ml-1 rounded-sm opacity-0 hover:bg-accent group-hover:opacity-100"
|
{tab.title}
|
||||||
onClick={(e) => {
|
{tab.database && (
|
||||||
e.stopPropagation();
|
<span className="ml-1 text-[10px] font-normal text-muted-foreground/60">
|
||||||
closeTab(tab.id);
|
{tab.database}
|
||||||
}}
|
</span>
|
||||||
>
|
)}
|
||||||
<X className="h-3 w-3" />
|
</span>
|
||||||
</button>
|
<button
|
||||||
</div>
|
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>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { ConnectionSelector } from "@/components/connections/ConnectionSelector";
|
import { ConnectionSelector } from "@/components/connections/ConnectionSelector";
|
||||||
import { ConnectionList } from "@/components/connections/ConnectionList";
|
import { ConnectionList } from "@/components/connections/ConnectionList";
|
||||||
import { ConnectionDialog } from "@/components/connections/ConnectionDialog";
|
import { ConnectionDialog } from "@/components/connections/ConnectionDialog";
|
||||||
@@ -61,70 +60,73 @@ export function Toolbar() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="flex h-10 items-center gap-2 border-b px-3 bg-card"
|
className="tusk-toolbar tusk-conn-strip flex h-10 items-center gap-1.5 px-3"
|
||||||
style={{ borderLeftWidth: activeColor ? 3 : 0, borderLeftColor: activeColor }}
|
style={{
|
||||||
|
"--strip-width": activeColor ? "3px" : "0px",
|
||||||
|
"--strip-color": activeColor ?? "transparent",
|
||||||
|
} as React.CSSProperties}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="xs"
|
||||||
className="h-7 gap-1.5"
|
className="gap-1.5 text-muted-foreground hover:text-foreground"
|
||||||
onClick={() => setListOpen(true)}
|
onClick={() => setListOpen(true)}
|
||||||
>
|
>
|
||||||
<Database className="h-3.5 w-3.5" />
|
<Database className="h-3.5 w-3.5" />
|
||||||
Connections
|
<span className="text-xs font-medium">Connections</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Separator orientation="vertical" className="h-5" />
|
<div className="mx-1 h-4 w-px bg-border" />
|
||||||
|
|
||||||
<ConnectionSelector />
|
<ConnectionSelector />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="icon-xs"
|
||||||
className="h-7 w-7 p-0"
|
|
||||||
onClick={handleReconnect}
|
onClick={handleReconnect}
|
||||||
disabled={!activeConnectionId || reconnectMutation.isPending}
|
disabled={!activeConnectionId || reconnectMutation.isPending}
|
||||||
title="Reconnect"
|
title="Reconnect"
|
||||||
|
className="text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<RefreshCw className={`h-3.5 w-3.5 ${reconnectMutation.isPending ? "animate-spin" : ""}`} />
|
<RefreshCw className={`h-3.5 w-3.5 ${reconnectMutation.isPending ? "animate-spin" : ""}`} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Separator orientation="vertical" className="h-5" />
|
<div className="mx-1 h-4 w-px bg-border" />
|
||||||
|
|
||||||
<ReadOnlyToggle />
|
<ReadOnlyToggle />
|
||||||
|
|
||||||
<Separator orientation="vertical" className="h-5" />
|
<div className="mx-1 h-4 w-px bg-border" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="xs"
|
||||||
className="h-7 gap-1.5"
|
className="gap-1.5 text-muted-foreground hover:text-foreground"
|
||||||
onClick={handleNewQuery}
|
onClick={handleNewQuery}
|
||||||
disabled={!activeConnectionId}
|
disabled={!activeConnectionId}
|
||||||
>
|
>
|
||||||
<Plus className="h-3.5 w-3.5" />
|
<Plus className="h-3.5 w-3.5" />
|
||||||
New Query
|
<span className="text-xs font-medium">New Query</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="xs"
|
||||||
className="h-7 gap-1.5"
|
className="gap-1.5 text-muted-foreground hover:text-foreground"
|
||||||
onClick={handleNewLookup}
|
onClick={handleNewLookup}
|
||||||
disabled={!activeConnectionId}
|
disabled={!activeConnectionId}
|
||||||
>
|
>
|
||||||
<Search className="h-3.5 w-3.5" />
|
<Search className="h-3.5 w-3.5" />
|
||||||
Entity Lookup
|
<span className="text-xs font-medium">Lookup</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="icon-xs"
|
||||||
className="h-7 w-7 p-0"
|
|
||||||
onClick={() => setSettingsOpen(true)}
|
onClick={() => setSettingsOpen(true)}
|
||||||
title="Settings"
|
title="Settings"
|
||||||
|
className="text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<Settings className="h-3.5 w-3.5" />
|
<Settings className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -55,11 +55,11 @@ export function ResultsTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`truncate px-2 py-1 ${
|
className={`truncate px-2 py-1 font-mono text-[12px] ${
|
||||||
value === null
|
value === null
|
||||||
? "italic text-muted-foreground"
|
? "tusk-grid-cell-null"
|
||||||
: isHighlighted
|
: isHighlighted
|
||||||
? "bg-yellow-900/30"
|
? "tusk-grid-cell-highlight"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
onDoubleClick={() =>
|
onDoubleClick={() =>
|
||||||
@@ -108,7 +108,6 @@ export function ResultsTable({
|
|||||||
(colName: string, defaultHandler: ((e: unknown) => void) | undefined) =>
|
(colName: string, defaultHandler: ((e: unknown) => void) | undefined) =>
|
||||||
(e: unknown) => {
|
(e: unknown) => {
|
||||||
if (externalSort) {
|
if (externalSort) {
|
||||||
// Cycle: none → ASC → DESC → none
|
|
||||||
if (externalSort.column !== colName) {
|
if (externalSort.column !== colName) {
|
||||||
externalSort.onSort(colName, "ASC");
|
externalSort.onSort(colName, "ASC");
|
||||||
} else if (externalSort.direction === "ASC") {
|
} else if (externalSort.direction === "ASC") {
|
||||||
@@ -141,16 +140,16 @@ export function ResultsTable({
|
|||||||
return (
|
return (
|
||||||
<div ref={parentRef} className="h-full select-text overflow-auto">
|
<div ref={parentRef} className="h-full select-text overflow-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="sticky top-0 z-10 flex bg-card border-b">
|
<div className="tusk-grid-header sticky top-0 z-10 flex">
|
||||||
{table.getHeaderGroups().map((headerGroup) =>
|
{table.getHeaderGroups().map((headerGroup) =>
|
||||||
headerGroup.headers.map((header) => (
|
headerGroup.headers.map((header) => (
|
||||||
<div
|
<div
|
||||||
key={header.id}
|
key={header.id}
|
||||||
className="relative shrink-0 select-none border-r px-2 py-1.5 text-left text-xs font-medium text-muted-foreground"
|
className="relative shrink-0 select-none border-r border-border/30 px-2 py-1.5 text-left text-[11px] font-semibold tracking-wide text-muted-foreground uppercase"
|
||||||
style={{ width: header.getSize(), minWidth: header.getSize() }}
|
style={{ width: header.getSize(), minWidth: header.getSize() }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex cursor-pointer items-center gap-1 hover:text-foreground"
|
className="flex cursor-pointer items-center gap-1 transition-colors hover:text-foreground"
|
||||||
onClick={handleHeaderClick(header.column.id, header.column.getToggleSortingHandler())}
|
onClick={handleHeaderClick(header.column.id, header.column.getToggleSortingHandler())}
|
||||||
>
|
>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
@@ -158,10 +157,10 @@ export function ResultsTable({
|
|||||||
header.getContext()
|
header.getContext()
|
||||||
)}
|
)}
|
||||||
{getIsSorted(header.column.id, header.column.getIsSorted()) === "asc" && (
|
{getIsSorted(header.column.id, header.column.getIsSorted()) === "asc" && (
|
||||||
<ArrowUp className="h-3 w-3" />
|
<ArrowUp className="h-3 w-3 text-primary" />
|
||||||
)}
|
)}
|
||||||
{getIsSorted(header.column.id, header.column.getIsSorted()) === "desc" && (
|
{getIsSorted(header.column.id, header.column.getIsSorted()) === "desc" && (
|
||||||
<ArrowDown className="h-3 w-3" />
|
<ArrowDown className="h-3 w-3 text-primary" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Resize handle */}
|
{/* Resize handle */}
|
||||||
@@ -169,7 +168,7 @@ export function ResultsTable({
|
|||||||
onMouseDown={header.getResizeHandler()}
|
onMouseDown={header.getResizeHandler()}
|
||||||
onTouchStart={header.getResizeHandler()}
|
onTouchStart={header.getResizeHandler()}
|
||||||
onDoubleClick={() => header.column.resetSize()}
|
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" : ""
|
header.column.getIsResizing() ? "bg-primary" : ""
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
@@ -190,7 +189,7 @@ export function ResultsTable({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={row.id}
|
key={row.id}
|
||||||
className="absolute left-0 flex hover:bg-accent/50"
|
className="tusk-grid-row absolute left-0 flex transition-colors"
|
||||||
style={{
|
style={{
|
||||||
top: `${virtualRow.start}px`,
|
top: `${virtualRow.start}px`,
|
||||||
height: `${virtualRow.size}px`,
|
height: `${virtualRow.size}px`,
|
||||||
@@ -201,7 +200,7 @@ export function ResultsTable({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
className="shrink-0 border-b border-r text-xs"
|
className="shrink-0 border-b border-r border-border/20 text-xs"
|
||||||
style={{ width: w, minWidth: w }}
|
style={{ width: w, minWidth: w }}
|
||||||
>
|
>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function TableSizeInfo({ item }: { item: SchemaObject }) {
|
|||||||
if (item.row_count != null) parts.push(formatCount(item.row_count));
|
if (item.row_count != null) parts.push(formatCount(item.row_count));
|
||||||
if (item.size_bytes != null) parts.push(formatSize(item.size_bytes));
|
if (item.size_bytes != null) parts.push(formatSize(item.size_bytes));
|
||||||
return (
|
return (
|
||||||
<span className="ml-auto text-[10px] text-muted-foreground shrink-0">
|
<span className="ml-auto shrink-0 font-mono text-[10px] text-muted-foreground/50">
|
||||||
{parts.join(", ")}
|
{parts.join(", ")}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -71,15 +71,18 @@ export function SchemaTree() {
|
|||||||
|
|
||||||
if (!activeConnectionId) {
|
if (!activeConnectionId) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 text-sm text-muted-foreground">
|
<div className="flex flex-col items-center gap-2 p-6 text-center">
|
||||||
Connect to a database to browse schema.
|
<Database className="h-8 w-8 text-muted-foreground/20" />
|
||||||
|
<p className="text-sm text-muted-foreground/60">
|
||||||
|
Connect to a database to browse schema
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!databases || databases.length === 0) {
|
if (!databases || databases.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 text-sm text-muted-foreground">
|
<div className="p-4 text-sm text-muted-foreground/60">
|
||||||
No databases found.
|
No databases found.
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -218,22 +221,26 @@ function DatabaseNode({
|
|||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger>
|
<ContextMenuTrigger>
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-1 rounded-sm px-1 py-0.5 text-sm hover:bg-accent cursor-pointer select-none font-medium ${
|
className={`flex items-center gap-1.5 rounded-md px-1.5 py-1 text-sm transition-colors hover:bg-accent/50 cursor-pointer select-none font-medium ${
|
||||||
isActive ? "text-primary" : "text-muted-foreground"
|
isActive ? "text-foreground" : "text-muted-foreground"
|
||||||
}`}
|
}`}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
{expanded ? (
|
<span className="text-muted-foreground/50">
|
||||||
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
{expanded ? (
|
||||||
) : (
|
<ChevronDown className="h-3.5 w-3.5" />
|
||||||
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
) : (
|
||||||
)}
|
<ChevronRight className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
<HardDrive
|
<HardDrive
|
||||||
className={`h-3.5 w-3.5 shrink-0 ${isActive ? "text-primary" : "text-muted-foreground"}`}
|
className={`h-3.5 w-3.5 shrink-0 ${isActive ? "text-primary" : "text-muted-foreground/50"}`}
|
||||||
/>
|
/>
|
||||||
<span className="truncate">{name}</span>
|
<span className="truncate">{name}</span>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<span className="ml-auto text-[10px] text-primary">active</span>
|
<span className="ml-auto rounded-sm bg-primary/10 px-1 py-px text-[9px] font-semibold tracking-wider text-primary uppercase">
|
||||||
|
active
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
@@ -316,7 +323,7 @@ function DatabaseNode({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{expanded && !isActive && (
|
{expanded && !isActive && (
|
||||||
<div className="ml-6 py-1 text-xs text-muted-foreground">
|
<div className="ml-6 py-1 text-xs text-muted-foreground/50">
|
||||||
{isSwitching ? "Switching..." : "Click to switch to this database"}
|
{isSwitching ? "Switching..." : "Click to switch to this database"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -339,7 +346,7 @@ function SchemasForCurrentDb({
|
|||||||
|
|
||||||
if (!schemas || schemas.length === 0) {
|
if (!schemas || schemas.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="py-1 text-xs text-muted-foreground">No schemas found.</div>
|
<div className="py-1 text-xs text-muted-foreground/50">No schemas found.</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,18 +386,20 @@ function SchemaNode({
|
|||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger>
|
<ContextMenuTrigger>
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-1 rounded-sm px-1 py-0.5 text-sm hover:bg-accent cursor-pointer select-none font-medium"
|
className="flex items-center gap-1.5 rounded-md px-1.5 py-1 text-sm transition-colors hover:bg-accent/50 cursor-pointer select-none font-medium"
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
>
|
>
|
||||||
|
<span className="text-muted-foreground/50">
|
||||||
|
{expanded ? (
|
||||||
|
<ChevronDown className="h-3.5 w-3.5" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
{expanded ? (
|
{expanded ? (
|
||||||
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
<FolderOpen className="h-3.5 w-3.5 text-amber-500/70" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
|
<Database className="h-3.5 w-3.5 text-muted-foreground/50" />
|
||||||
)}
|
|
||||||
{expanded ? (
|
|
||||||
<FolderOpen className="h-3.5 w-3.5 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<Database className="h-3.5 w-3.5 text-muted-foreground" />
|
|
||||||
)}
|
)}
|
||||||
<span>{schema}</span>
|
<span>{schema}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -442,10 +451,10 @@ function SchemaNode({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const categoryIcons = {
|
const categoryIcons = {
|
||||||
tables: <Table2 className="h-3.5 w-3.5 text-blue-400" />,
|
tables: <Table2 className="h-3.5 w-3.5 text-sky-400/80" />,
|
||||||
views: <Eye className="h-3.5 w-3.5 text-green-400" />,
|
views: <Eye className="h-3.5 w-3.5 text-emerald-400/80" />,
|
||||||
functions: <FunctionSquare className="h-3.5 w-3.5 text-purple-400" />,
|
functions: <FunctionSquare className="h-3.5 w-3.5 text-violet-400/80" />,
|
||||||
sequences: <Hash className="h-3.5 w-3.5 text-orange-400" />,
|
sequences: <Hash className="h-3.5 w-3.5 text-amber-400/80" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CategoryNode({
|
function CategoryNode({
|
||||||
@@ -498,18 +507,24 @@ function CategoryNode({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-1 rounded-sm px-1 py-0.5 text-sm hover:bg-accent cursor-pointer select-none"
|
className="flex items-center gap-1.5 rounded-md px-1.5 py-1 text-sm transition-colors hover:bg-accent/50 cursor-pointer select-none"
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
>
|
>
|
||||||
{expanded ? (
|
<span className="text-muted-foreground/50">
|
||||||
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
{expanded ? (
|
||||||
) : (
|
<ChevronDown className="h-3.5 w-3.5" />
|
||||||
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
) : (
|
||||||
)}
|
<ChevronRight className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
{icon}
|
{icon}
|
||||||
<span className="truncate">
|
<span className="truncate font-medium">
|
||||||
{label}
|
{label}
|
||||||
{items ? ` (${items.length})` : ""}
|
{items && (
|
||||||
|
<span className="ml-1 font-normal text-muted-foreground/40">
|
||||||
|
{items.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
@@ -522,12 +537,12 @@ function CategoryNode({
|
|||||||
<ContextMenu key={item.name}>
|
<ContextMenu key={item.name}>
|
||||||
<ContextMenuTrigger>
|
<ContextMenuTrigger>
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-1 rounded-sm px-1 py-0.5 text-sm hover:bg-accent cursor-pointer select-none"
|
className="flex items-center gap-1.5 rounded-md px-1.5 py-0.5 text-sm transition-colors hover:bg-accent/50 cursor-pointer select-none"
|
||||||
onDoubleClick={() => onOpenTable(item.name)}
|
onDoubleClick={() => onOpenTable(item.name)}
|
||||||
>
|
>
|
||||||
<span className="w-3.5 shrink-0" />
|
<span className="w-3.5 shrink-0" />
|
||||||
{icon}
|
{icon}
|
||||||
<span className="truncate">{item.name}</span>
|
<span className="truncate text-foreground/80">{item.name}</span>
|
||||||
{category === "tables" && <TableSizeInfo item={item} />}
|
{category === "tables" && <TableSizeInfo item={item} />}
|
||||||
</div>
|
</div>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
@@ -561,11 +576,11 @@ function CategoryNode({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.name}
|
key={item.name}
|
||||||
className="flex items-center gap-1 rounded-sm px-1 py-0.5 text-sm hover:bg-accent cursor-pointer select-none"
|
className="flex items-center gap-1.5 rounded-md px-1.5 py-0.5 text-sm transition-colors hover:bg-accent/50 cursor-pointer select-none"
|
||||||
>
|
>
|
||||||
<span className="w-3.5 shrink-0" />
|
<span className="w-3.5 shrink-0" />
|
||||||
{icon}
|
{icon}
|
||||||
<span className="truncate">{item.name}</span>
|
<span className="truncate text-foreground/80">{item.name}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -5,19 +5,19 @@ import { Slot } from "radix-ui"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
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:
|
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:
|
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:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost:
|
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",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
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",
|
"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-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
"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",
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -255,11 +255,12 @@ export function WorkspacePanel({
|
|||||||
<ResizablePanelGroup orientation="vertical">
|
<ResizablePanelGroup orientation="vertical">
|
||||||
<ResizablePanel id="editor" defaultSize="40%" minSize="15%">
|
<ResizablePanel id="editor" defaultSize="40%" minSize="15%">
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex items-center gap-2 border-b px-2 py-1">
|
{/* Editor action bar */}
|
||||||
|
<div className="flex items-center gap-1 border-b border-border/40 px-2 py-1">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 gap-1 text-xs"
|
className="gap-1 text-[11px] text-primary hover:bg-primary/10 hover:text-primary"
|
||||||
onClick={handleExecute}
|
onClick={handleExecute}
|
||||||
disabled={queryMutation.isPending || !sqlValue.trim()}
|
disabled={queryMutation.isPending || !sqlValue.trim()}
|
||||||
>
|
>
|
||||||
@@ -271,9 +272,9 @@ export function WorkspacePanel({
|
|||||||
Run
|
Run
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 gap-1 text-xs"
|
className="gap-1 text-[11px]"
|
||||||
onClick={handleExplain}
|
onClick={handleExplain}
|
||||||
disabled={queryMutation.isPending || !sqlValue.trim()}
|
disabled={queryMutation.isPending || !sqlValue.trim()}
|
||||||
>
|
>
|
||||||
@@ -285,9 +286,9 @@ export function WorkspacePanel({
|
|||||||
Explain
|
Explain
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 gap-1 text-xs"
|
className="gap-1 text-[11px]"
|
||||||
onClick={handleFormat}
|
onClick={handleFormat}
|
||||||
disabled={!sqlValue.trim()}
|
disabled={!sqlValue.trim()}
|
||||||
title="Format SQL (Shift+Alt+F)"
|
title="Format SQL (Shift+Alt+F)"
|
||||||
@@ -296,9 +297,9 @@ export function WorkspacePanel({
|
|||||||
Format
|
Format
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 gap-1 text-xs"
|
className="gap-1 text-[11px]"
|
||||||
onClick={() => setSaveDialogOpen(true)}
|
onClick={() => setSaveDialogOpen(true)}
|
||||||
disabled={!sqlValue.trim()}
|
disabled={!sqlValue.trim()}
|
||||||
title="Save query"
|
title="Save query"
|
||||||
@@ -306,20 +307,24 @@ export function WorkspacePanel({
|
|||||||
<Bookmark className="h-3 w-3" />
|
<Bookmark className="h-3 w-3" />
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<div className="mx-1 h-3.5 w-px bg-border/40" />
|
||||||
|
|
||||||
|
{/* AI actions group — purple-branded */}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="xs"
|
||||||
variant={aiBarOpen ? "secondary" : "ghost"}
|
variant={aiBarOpen ? "secondary" : "ghost"}
|
||||||
className="h-6 gap-1 text-xs"
|
className={`gap-1 text-[11px] ${aiBarOpen ? "text-tusk-purple" : ""}`}
|
||||||
onClick={() => setAiBarOpen(!aiBarOpen)}
|
onClick={() => setAiBarOpen(!aiBarOpen)}
|
||||||
title="AI SQL Generator"
|
title="AI SQL Generator"
|
||||||
>
|
>
|
||||||
<Sparkles className="h-3 w-3" />
|
<Sparkles className={`h-3 w-3 ${aiBarOpen ? "tusk-ai-icon" : ""}`} />
|
||||||
AI
|
AI
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 gap-1 text-xs"
|
className="gap-1 text-[11px]"
|
||||||
onClick={handleAiExplain}
|
onClick={handleAiExplain}
|
||||||
disabled={isAiLoading || !sqlValue.trim()}
|
disabled={isAiLoading || !sqlValue.trim()}
|
||||||
title="Explain query with AI"
|
title="Explain query with AI"
|
||||||
@@ -331,35 +336,42 @@ export function WorkspacePanel({
|
|||||||
)}
|
)}
|
||||||
AI Explain
|
AI Explain
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{result && result.columns.length > 0 && (
|
{result && result.columns.length > 0 && (
|
||||||
<DropdownMenu>
|
<>
|
||||||
<DropdownMenuTrigger asChild>
|
<div className="mx-1 h-3.5 w-px bg-border/40" />
|
||||||
<Button
|
<DropdownMenu>
|
||||||
size="sm"
|
<DropdownMenuTrigger asChild>
|
||||||
variant="ghost"
|
<Button
|
||||||
className="h-6 gap-1 text-xs"
|
size="xs"
|
||||||
>
|
variant="ghost"
|
||||||
<Download className="h-3 w-3" />
|
className="gap-1 text-[11px]"
|
||||||
Export
|
>
|
||||||
</Button>
|
<Download className="h-3 w-3" />
|
||||||
</DropdownMenuTrigger>
|
Export
|
||||||
<DropdownMenuContent align="start">
|
</Button>
|
||||||
<DropdownMenuItem onClick={() => handleExport("csv")}>
|
</DropdownMenuTrigger>
|
||||||
Export CSV
|
<DropdownMenuContent align="start">
|
||||||
</DropdownMenuItem>
|
<DropdownMenuItem onClick={() => handleExport("csv")}>
|
||||||
<DropdownMenuItem onClick={() => handleExport("json")}>
|
Export CSV
|
||||||
Export JSON
|
</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuItem onClick={() => handleExport("json")}>
|
||||||
</DropdownMenuContent>
|
Export JSON
|
||||||
</DropdownMenu>
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<span className="text-[11px] text-muted-foreground">
|
|
||||||
Ctrl+Enter to execute
|
<div className="flex-1" />
|
||||||
|
|
||||||
|
<span className="text-[10px] text-muted-foreground/50 font-mono">
|
||||||
|
{"\u2318"}Enter
|
||||||
</span>
|
</span>
|
||||||
{isReadOnly && (
|
{isReadOnly && (
|
||||||
<span className="flex items-center gap-1 rounded bg-yellow-500/10 px-1.5 py-0.5 text-[10px] font-semibold text-yellow-600 dark:text-yellow-500">
|
<span className="ml-2 flex items-center gap-1 rounded-sm bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-semibold tracking-wide text-amber-500">
|
||||||
<Lock className="h-3 w-3" />
|
<Lock className="h-2.5 w-2.5" />
|
||||||
Read-Only
|
READ
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -389,35 +401,41 @@ export function WorkspacePanel({
|
|||||||
<ResizablePanel id="results" defaultSize="60%" minSize="15%">
|
<ResizablePanel id="results" defaultSize="60%" minSize="15%">
|
||||||
<div className="flex h-full flex-col overflow-hidden">
|
<div className="flex h-full flex-col overflow-hidden">
|
||||||
{(explainData || result || error || aiExplanation) && (
|
{(explainData || result || error || aiExplanation) && (
|
||||||
<div className="flex shrink-0 items-center border-b text-xs">
|
<div className="flex shrink-0 items-center border-b border-border/40 text-xs">
|
||||||
<button
|
<button
|
||||||
className={`px-3 py-1 font-medium ${
|
className={`relative px-3 py-1.5 font-medium transition-colors ${
|
||||||
resultView === "results"
|
resultView === "results"
|
||||||
? "bg-background text-foreground"
|
? "text-foreground"
|
||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground/70"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setResultView("results")}
|
onClick={() => setResultView("results")}
|
||||||
>
|
>
|
||||||
Results
|
Results
|
||||||
|
{resultView === "results" && (
|
||||||
|
<span className="absolute bottom-0 left-1/4 right-1/4 h-0.5 rounded-t bg-primary" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
{explainData && (
|
{explainData && (
|
||||||
<button
|
<button
|
||||||
className={`px-3 py-1 font-medium ${
|
className={`relative px-3 py-1.5 font-medium transition-colors ${
|
||||||
resultView === "explain"
|
resultView === "explain"
|
||||||
? "bg-background text-foreground"
|
? "text-foreground"
|
||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground/70"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setResultView("explain")}
|
onClick={() => setResultView("explain")}
|
||||||
>
|
>
|
||||||
Explain
|
Explain
|
||||||
|
{resultView === "explain" && (
|
||||||
|
<span className="absolute bottom-0 left-1/4 right-1/4 h-0.5 rounded-t bg-primary" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{resultView === "results" && result && result.columns.length > 0 && (
|
{resultView === "results" && result && result.columns.length > 0 && (
|
||||||
<div className="ml-auto mr-2 flex items-center rounded-md border">
|
<div className="ml-auto mr-2 flex items-center overflow-hidden rounded border border-border/40">
|
||||||
<button
|
<button
|
||||||
className={`flex items-center gap-1 px-2 py-0.5 font-medium ${
|
className={`flex items-center gap-1 px-2 py-0.5 text-[11px] font-medium transition-colors ${
|
||||||
resultViewMode === "table"
|
resultViewMode === "table"
|
||||||
? "bg-muted text-foreground"
|
? "bg-accent text-foreground"
|
||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setResultViewMode("table")}
|
onClick={() => setResultViewMode("table")}
|
||||||
@@ -427,9 +445,9 @@ export function WorkspacePanel({
|
|||||||
Table
|
Table
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`flex items-center gap-1 px-2 py-0.5 font-medium ${
|
className={`flex items-center gap-1 px-2 py-0.5 text-[11px] font-medium transition-colors ${
|
||||||
resultViewMode === "json"
|
resultViewMode === "json"
|
||||||
? "bg-muted text-foreground"
|
? "bg-accent text-foreground"
|
||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setResultViewMode("json")}
|
onClick={() => setResultViewMode("json")}
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
TUSK — "Twilight" Design System
|
||||||
|
Soft dark with blue undertones and teal accents
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
@@ -43,50 +48,399 @@
|
|||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
|
/* Custom semantic tokens */
|
||||||
|
--color-tusk-teal: var(--tusk-teal);
|
||||||
|
--color-tusk-purple: var(--tusk-purple);
|
||||||
|
--color-tusk-amber: var(--tusk-amber);
|
||||||
|
--color-tusk-rose: var(--tusk-rose);
|
||||||
|
--color-tusk-surface: var(--tusk-surface);
|
||||||
|
|
||||||
|
/* Font families */
|
||||||
|
--font-sans: "Outfit", system-ui, -apple-system, sans-serif;
|
||||||
|
--font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", ui-monospace, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.625rem;
|
--radius: 0.5rem;
|
||||||
--background: oklch(0.145 0 0);
|
|
||||||
--foreground: oklch(0.985 0 0);
|
/* Soft twilight palette — comfortable, not eye-straining */
|
||||||
--card: oklch(0.205 0 0);
|
--background: oklch(0.2 0.012 250);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.9 0.005 250);
|
||||||
--popover: oklch(0.205 0 0);
|
--card: oklch(0.23 0.012 250);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.9 0.005 250);
|
||||||
--primary: oklch(0.922 0 0);
|
--popover: oklch(0.25 0.014 250);
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
--popover-foreground: oklch(0.9 0.005 250);
|
||||||
--secondary: oklch(0.269 0 0);
|
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
/* Teal primary — slightly softer for the lighter background */
|
||||||
--muted: oklch(0.269 0 0);
|
--primary: oklch(0.72 0.14 170);
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
--primary-foreground: oklch(0.18 0.015 250);
|
||||||
--accent: oklch(0.269 0 0);
|
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
/* Surfaces — gentle stepping */
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--secondary: oklch(0.27 0.012 250);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--secondary-foreground: oklch(0.85 0.008 250);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--muted: oklch(0.27 0.012 250);
|
||||||
--ring: oklch(0.556 0 0);
|
--muted-foreground: oklch(0.62 0.015 250);
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--accent: oklch(0.28 0.014 250);
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--accent-foreground: oklch(0.9 0.005 250);
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
/* Status */
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
--destructive: oklch(0.65 0.2 15);
|
||||||
--sidebar: oklch(0.205 0 0);
|
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
/* Borders & inputs — more visible, less transparent */
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--border: oklch(0.34 0.015 250 / 70%);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--input: oklch(0.36 0.015 250 / 60%);
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
--ring: oklch(0.72 0.14 170 / 40%);
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
/* Chart palette */
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
--chart-1: oklch(0.72 0.14 170);
|
||||||
|
--chart-2: oklch(0.68 0.14 200);
|
||||||
|
--chart-3: oklch(0.78 0.14 85);
|
||||||
|
--chart-4: oklch(0.62 0.18 290);
|
||||||
|
--chart-5: oklch(0.68 0.16 30);
|
||||||
|
|
||||||
|
/* Sidebar <20><> same family, slightly offset */
|
||||||
|
--sidebar: oklch(0.215 0.012 250);
|
||||||
|
--sidebar-foreground: oklch(0.9 0.005 250);
|
||||||
|
--sidebar-primary: oklch(0.72 0.14 170);
|
||||||
|
--sidebar-primary-foreground: oklch(0.9 0.005 250);
|
||||||
|
--sidebar-accent: oklch(0.28 0.014 250);
|
||||||
|
--sidebar-accent-foreground: oklch(0.9 0.005 250);
|
||||||
|
--sidebar-border: oklch(0.34 0.015 250 / 70%);
|
||||||
|
--sidebar-ring: oklch(0.72 0.14 170 / 40%);
|
||||||
|
|
||||||
|
/* Tusk semantic tokens */
|
||||||
|
--tusk-teal: oklch(0.72 0.14 170);
|
||||||
|
--tusk-purple: oklch(0.62 0.2 290);
|
||||||
|
--tusk-amber: oklch(0.78 0.14 85);
|
||||||
|
--tusk-rose: oklch(0.65 0.2 15);
|
||||||
|
--tusk-surface: oklch(0.26 0.012 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Base layer
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
|
font-family: var(--font-sans);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Monospace for code and data */
|
||||||
|
code, pre, .font-mono,
|
||||||
|
[data-slot="sql-editor"],
|
||||||
|
.cm-editor {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smoother scrollbars */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: oklch(0.42 0.015 250 / 45%);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: oklch(0.5 0.015 250 / 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Noise texture overlay — very subtle depth
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.tusk-noise::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.018;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-size: 256px 256px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Glow effects — softer for lighter background
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.tusk-glow-teal {
|
||||||
|
box-shadow: 0 0 10px oklch(0.72 0.14 170 / 12%),
|
||||||
|
0 0 3px oklch(0.72 0.14 170 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tusk-glow-purple {
|
||||||
|
box-shadow: 0 0 10px oklch(0.62 0.2 290 / 15%),
|
||||||
|
0 0 3px oklch(0.62 0.2 290 / 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tusk-glow-teal-subtle {
|
||||||
|
box-shadow: 0 0 6px oklch(0.72 0.14 170 / 6%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════<E29590><E29590>═══════
|
||||||
|
Active tab indicator — top glow bar
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.tusk-tab-active {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tusk-tab-active::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, oklch(0.72 0.14 170), oklch(0.68 0.14 200));
|
||||||
|
border-radius: 0 0 2px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
AI feature branding — purple glow language
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.tusk-ai-bar {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.62 0.2 290 / 5%) 0%,
|
||||||
|
oklch(0.62 0.2 290 / 2%) 50%,
|
||||||
|
oklch(0.72 0.14 170 / 3%) 100%
|
||||||
|
);
|
||||||
|
border-bottom: 1px solid oklch(0.62 0.2 290 / 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tusk-ai-icon {
|
||||||
|
color: oklch(0.68 0.18 290);
|
||||||
|
filter: drop-shadow(0 0 3px oklch(0.62 0.2 290 / 30%));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Transitions — smooth everything
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
button, a, [role="tab"], [role="menuitem"], [data-slot="button"] {
|
||||||
|
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Glassmorphism for floating elements
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
[data-radix-popper-content-wrapper] [role="dialog"],
|
||||||
|
[data-radix-popper-content-wrapper] [role="listbox"],
|
||||||
|
[data-radix-popper-content-wrapper] [role="menu"],
|
||||||
|
[data-state="open"][data-side] {
|
||||||
|
backdrop-filter: blur(16px) saturate(1.2);
|
||||||
|
-webkit-backdrop-filter: blur(16px) saturate(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Sidebar tab active indicator
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.tusk-sidebar-tab-active {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tusk-sidebar-tab-active::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 25%;
|
||||||
|
right: 25%;
|
||||||
|
height: 2px;
|
||||||
|
background: oklch(0.72 0.14 170);
|
||||||
|
border-radius: 2px 2px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Data grid refinements
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.tusk-grid-header {
|
||||||
|
background: oklch(0.23 0.012 250);
|
||||||
|
border-bottom: 1px solid oklch(0.34 0.015 250 / 80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tusk-grid-row:hover {
|
||||||
|
background: oklch(0.72 0.14 170 / 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tusk-grid-cell-null {
|
||||||
|
color: oklch(0.5 0.015 250);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tusk-grid-cell-highlight {
|
||||||
|
background: oklch(0.78 0.14 85 / 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Status bar
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.tusk-status-bar {
|
||||||
|
background: oklch(0.215 0.012 250);
|
||||||
|
border-top: 1px solid oklch(0.34 0.015 250 / 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Connection color accent strip
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.tusk-conn-strip {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tusk-conn-strip::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: var(--strip-width, 3px);
|
||||||
|
background: var(--strip-color, transparent);
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Toolbar
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.tusk-toolbar {
|
||||||
|
background: oklch(0.23 0.012 250);
|
||||||
|
border-bottom: 1px solid oklch(0.34 0.015 250 / 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Resizable handle
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
[data-panel-group-direction="horizontal"] > [data-resize-handle] {
|
||||||
|
width: 1px !important;
|
||||||
|
background: oklch(0.34 0.015 250 / 50%);
|
||||||
|
transition: background 200ms, width 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-panel-group-direction="horizontal"] > [data-resize-handle]:hover,
|
||||||
|
[data-panel-group-direction="horizontal"] > [data-resize-handle][data-resize-handle-active] {
|
||||||
|
width: 3px !important;
|
||||||
|
background: oklch(0.72 0.14 170 / 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-panel-group-direction="vertical"] > [data-resize-handle] {
|
||||||
|
height: 1px !important;
|
||||||
|
background: oklch(0.34 0.015 250 / 50%);
|
||||||
|
transition: background 200ms, height 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-panel-group-direction="vertical"] > [data-resize-handle]:hover,
|
||||||
|
[data-panel-group-direction="vertical"] > [data-resize-handle][data-resize-handle-active] {
|
||||||
|
height: 3px !important;
|
||||||
|
background: oklch(0.72 0.14 170 / 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
CodeMirror theme overrides
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.cm-editor {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor .cm-gutters {
|
||||||
|
background: oklch(0.215 0.012 250);
|
||||||
|
border-right: 1px solid oklch(0.34 0.015 250 / 50%);
|
||||||
|
color: oklch(0.48 0.012 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor .cm-activeLineGutter {
|
||||||
|
background: oklch(0.72 0.14 170 / 8%);
|
||||||
|
color: oklch(0.65 0.015 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor .cm-activeLine {
|
||||||
|
background: oklch(0.72 0.14 170 / 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor .cm-cursor {
|
||||||
|
border-left-color: oklch(0.72 0.14 170);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor .cm-selectionBackground {
|
||||||
|
background: oklch(0.72 0.14 170 / 15%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor .cm-tooltip-autocomplete {
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
background: oklch(0.25 0.014 250 / 95%);
|
||||||
|
border: 1px solid oklch(0.34 0.015 250 / 70%);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 8px 32px oklch(0 0 0 / 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Sonner toast styling
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
[data-sonner-toaster] [data-sonner-toast] {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Utility animations
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
@keyframes tusk-fade-in {
|
||||||
|
from { opacity: 0; transform: translateY(4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tusk-pulse-glow {
|
||||||
|
0%, 100% { opacity: 0.6; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tusk-fade-in {
|
||||||
|
animation: tusk-fade-in 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tusk-pulse-glow {
|
||||||
|
animation: tusk-pulse-glow 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Selection styling
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: oklch(0.72 0.14 170 / 25%);
|
||||||
|
color: oklch(0.95 0.005 250);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user