Files
tusk/src/components/layout/Toolbar.tsx
Aleksey Shakhmatov da0001e77e feat(ui): "Graphite & Honey" redesign — warm dark, monospace-first
- new design system in globals.css: warm graphite surfaces, ivory text, honey
  accent; semantic status/data-type/syntax tokens replacing hardcoded colors
- IBM Plex Mono as the universal UI font (sans + mono), tabular numerals
- custom CodeMirror SQL theme (src/lib/editor-theme.ts) matching the palette
- data grid: zebra striping + honey row hover, stronger sticky header
- route status dots, JSON syntax, EXPLAIN cost, schema-tree icons and the
  read/write toggle through the new tokens
- TUSK wordmark in the toolbar
2026-05-23 15:02:19 +03:00

170 lines
5.2 KiB
TypeScript

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { ConnectionSelector } from "@/components/connections/ConnectionSelector";
import { ConnectionList } from "@/components/connections/ConnectionList";
import { ConnectionDialog } from "@/components/connections/ConnectionDialog";
import { ReadOnlyToggle } from "@/components/layout/ReadOnlyToggle";
import { useAppStore } from "@/stores/app-store";
import { useConnections, useReconnect } from "@/hooks/use-connections";
import { toast } from "sonner";
import { Database, Plus, RefreshCw, Settings, Sparkles } from "lucide-react";
import type { ConnectionConfig, Tab } from "@/types";
import { getEnvironment } from "@/lib/environment";
import { AppSettingsSheet } from "@/components/settings/AppSettingsSheet";
export function Toolbar() {
const [listOpen, setListOpen] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const [editingConn, setEditingConn] = useState<ConnectionConfig | null>(null);
const { activeConnectionId, currentDatabase, addTab } = useAppStore();
const { data: connections } = useConnections();
const reconnectMutation = useReconnect();
const activeConn = connections?.find((c) => c.id === activeConnectionId);
const activeEnv = getEnvironment(activeConn?.environment);
const activeColor = activeEnv?.color ?? activeConn?.color;
const handleReconnect = () => {
if (!activeConn) return;
reconnectMutation.mutate(activeConn, {
onSuccess: () => toast.success("Reconnected"),
onError: (err) => toast.error("Reconnect failed", { description: String(err) }),
});
};
const handleNewQuery = () => {
if (!activeConnectionId) return;
const tab: Tab = {
id: crypto.randomUUID(),
type: "query",
title: "New Query",
connectionId: activeConnectionId,
database: currentDatabase ?? undefined,
sql: "",
};
addTab(tab);
};
const handleNewChat = () => {
if (!activeConnectionId) return;
const tab: Tab = {
id: crypto.randomUUID(),
type: "chat",
title: "Chat",
connectionId: activeConnectionId,
database: currentDatabase ?? undefined,
};
addTab(tab);
};
return (
<>
<div
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}
>
<span
className="tusk-wordmark select-none px-1 text-[12px] text-primary"
style={{ textShadow: "0 0 10px oklch(0.808 0.124 82 / 35%)" }}
>
TUSK
</span>
<div className="mx-1 h-4 w-px bg-border" />
<Button
variant="ghost"
size="xs"
className="gap-1.5 text-muted-foreground hover:text-foreground"
onClick={() => setListOpen(true)}
>
<Database className="h-3.5 w-3.5" />
<span className="text-xs font-medium">Connections</span>
</Button>
<div className="mx-1 h-4 w-px bg-border" />
<ConnectionSelector />
<Button
variant="ghost"
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>
<div className="mx-1 h-4 w-px bg-border" />
<ReadOnlyToggle />
<div className="mx-1 h-4 w-px bg-border" />
<Button
variant="ghost"
size="xs"
className="gap-1.5 text-muted-foreground hover:text-foreground"
onClick={handleNewChat}
disabled={!activeConnectionId}
>
<Sparkles className="h-3.5 w-3.5" />
<span className="text-xs font-medium">Ask AI</span>
</Button>
<Button
variant="ghost"
size="xs"
className="gap-1.5 text-muted-foreground hover:text-foreground"
onClick={handleNewQuery}
disabled={!activeConnectionId}
>
<Plus className="h-3.5 w-3.5" />
<span className="text-xs font-medium">New Query</span>
</Button>
<div className="flex-1" />
<Button
variant="ghost"
size="icon-xs"
onClick={() => setSettingsOpen(true)}
title="Settings"
className="text-muted-foreground hover:text-foreground"
>
<Settings className="h-3.5 w-3.5" />
</Button>
</div>
<ConnectionList
open={listOpen}
onOpenChange={setListOpen}
onEdit={(conn) => {
setEditingConn(conn);
setDialogOpen(true);
}}
onNew={() => {
setEditingConn(null);
setDialogOpen(true);
}}
/>
<ConnectionDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
connection={editingConn}
/>
<AppSettingsSheet
open={settingsOpen}
onOpenChange={setSettingsOpen}
/>
</>
);
}