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:
2026-04-06 10:27:20 +03:00
parent 64e27f79a4
commit 4e5714b291
14 changed files with 674 additions and 278 deletions

View File

@@ -49,7 +49,7 @@ export default function App() {
}, [handleNewQuery, handleCloseTab]);
return (
<div className="flex h-screen flex-col">
<div className="tusk-noise flex h-screen flex-col">
<Toolbar />
<div className="flex-1 overflow-hidden">
<ResizablePanelGroup orientation="horizontal">

View File

@@ -52,21 +52,21 @@ export function AiBar({ connectionId, onSqlGenerated, onClose, onExecute }: Prop
};
return (
<div className="flex items-center gap-2 border-b bg-muted/50 px-2 py-1">
<Sparkles className="h-3.5 w-3.5 shrink-0 text-purple-500" />
<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 tusk-ai-icon" />
<Input
value={prompt}
onChange={(e) => 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}
/>
<Button
size="sm"
size="xs"
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}
disabled={generateMutation.isPending || !prompt.trim()}
>
@@ -78,23 +78,23 @@ export function AiBar({ connectionId, onSqlGenerated, onClose, onExecute }: Prop
</Button>
{prompt.trim() && (
<Button
size="sm"
size="icon-xs"
variant="ghost"
className="h-6 w-6 p-0"
onClick={() => setPrompt("")}
title="Clear prompt"
disabled={generateMutation.isPending}
className="text-muted-foreground"
>
<Eraser className="h-3 w-3" />
</Button>
)}
<AiSettingsPopover />
<Button
size="sm"
size="icon-xs"
variant="ghost"
className="h-6 w-6 p-0"
onClick={onClose}
title="Close AI bar"
className="text-muted-foreground"
>
<X className="h-3 w-3" />
</Button>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -55,11 +55,11 @@ export function ResultsTable({
return (
<div
className={`truncate px-2 py-1 ${
className={`truncate px-2 py-1 font-mono text-[12px] ${
value === null
? "italic text-muted-foreground"
? "tusk-grid-cell-null"
: isHighlighted
? "bg-yellow-900/30"
? "tusk-grid-cell-highlight"
: ""
}`}
onDoubleClick={() =>
@@ -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 (
<div ref={parentRef} className="h-full select-text overflow-auto">
{/* 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) =>
headerGroup.headers.map((header) => (
<div
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() }}
>
<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())}
>
{flexRender(
@@ -158,10 +157,10 @@ export function ResultsTable({
header.getContext()
)}
{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" && (
<ArrowDown className="h-3 w-3" />
<ArrowDown className="h-3 w-3 text-primary" />
)}
</div>
{/* 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 (
<div
key={row.id}
className="absolute left-0 flex hover:bg-accent/50"
className="tusk-grid-row absolute left-0 flex transition-colors"
style={{
top: `${virtualRow.start}px`,
height: `${virtualRow.size}px`,
@@ -201,7 +200,7 @@ export function ResultsTable({
return (
<div
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 }}
>
{flexRender(

View File

@@ -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 (
<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(", ")}
</span>
);
@@ -71,15 +71,18 @@ export function SchemaTree() {
if (!activeConnectionId) {
return (
<div className="p-4 text-sm text-muted-foreground">
Connect to a database to browse schema.
<div className="flex flex-col items-center gap-2 p-6 text-center">
<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>
);
}
if (!databases || databases.length === 0) {
return (
<div className="p-4 text-sm text-muted-foreground">
<div className="p-4 text-sm text-muted-foreground/60">
No databases found.
</div>
);
@@ -218,22 +221,26 @@ function DatabaseNode({
<ContextMenu>
<ContextMenuTrigger>
<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 ${
isActive ? "text-primary" : "text-muted-foreground"
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-foreground" : "text-muted-foreground"
}`}
onClick={handleClick}
>
{expanded ? (
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
) : (
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
)}
<span className="text-muted-foreground/50">
{expanded ? (
<ChevronDown className="h-3.5 w-3.5" />
) : (
<ChevronRight className="h-3.5 w-3.5" />
)}
</span>
<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>
{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>
</ContextMenuTrigger>
@@ -316,7 +323,7 @@ function DatabaseNode({
</div>
)}
{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"}
</div>
)}
@@ -339,7 +346,7 @@ function SchemasForCurrentDb({
if (!schemas || schemas.length === 0) {
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>
<ContextMenuTrigger>
<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)}
>
<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 ? (
<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" />
)}
{expanded ? (
<FolderOpen className="h-3.5 w-3.5 text-muted-foreground" />
) : (
<Database className="h-3.5 w-3.5 text-muted-foreground" />
<Database className="h-3.5 w-3.5 text-muted-foreground/50" />
)}
<span>{schema}</span>
</div>
@@ -442,10 +451,10 @@ function SchemaNode({
}
const categoryIcons = {
tables: <Table2 className="h-3.5 w-3.5 text-blue-400" />,
views: <Eye className="h-3.5 w-3.5 text-green-400" />,
functions: <FunctionSquare className="h-3.5 w-3.5 text-purple-400" />,
sequences: <Hash className="h-3.5 w-3.5 text-orange-400" />,
tables: <Table2 className="h-3.5 w-3.5 text-sky-400/80" />,
views: <Eye className="h-3.5 w-3.5 text-emerald-400/80" />,
functions: <FunctionSquare className="h-3.5 w-3.5 text-violet-400/80" />,
sequences: <Hash className="h-3.5 w-3.5 text-amber-400/80" />,
};
function CategoryNode({
@@ -498,18 +507,24 @@ function CategoryNode({
return (
<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)}
>
{expanded ? (
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
) : (
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
)}
<span className="text-muted-foreground/50">
{expanded ? (
<ChevronDown className="h-3.5 w-3.5" />
) : (
<ChevronRight className="h-3.5 w-3.5" />
)}
</span>
{icon}
<span className="truncate">
<span className="truncate font-medium">
{label}
{items ? ` (${items.length})` : ""}
{items && (
<span className="ml-1 font-normal text-muted-foreground/40">
{items.length}
</span>
)}
</span>
</div>
{expanded && (
@@ -522,12 +537,12 @@ function CategoryNode({
<ContextMenu key={item.name}>
<ContextMenuTrigger>
<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)}
>
<span className="w-3.5 shrink-0" />
{icon}
<span className="truncate">{item.name}</span>
<span className="truncate text-foreground/80">{item.name}</span>
{category === "tables" && <TableSizeInfo item={item} />}
</div>
</ContextMenuTrigger>
@@ -561,11 +576,11 @@ function CategoryNode({
return (
<div
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" />
{icon}
<span className="truncate">{item.name}</span>
<span className="truncate text-foreground/80">{item.name}</span>
</div>
);
})}

View File

@@ -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: {

View File

@@ -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
)}

View File

@@ -255,11 +255,12 @@ export function WorkspacePanel({
<ResizablePanelGroup orientation="vertical">
<ResizablePanel id="editor" defaultSize="40%" minSize="15%">
<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
size="sm"
size="xs"
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}
disabled={queryMutation.isPending || !sqlValue.trim()}
>
@@ -271,9 +272,9 @@ export function WorkspacePanel({
Run
</Button>
<Button
size="sm"
size="xs"
variant="ghost"
className="h-6 gap-1 text-xs"
className="gap-1 text-[11px]"
onClick={handleExplain}
disabled={queryMutation.isPending || !sqlValue.trim()}
>
@@ -285,9 +286,9 @@ export function WorkspacePanel({
Explain
</Button>
<Button
size="sm"
size="xs"
variant="ghost"
className="h-6 gap-1 text-xs"
className="gap-1 text-[11px]"
onClick={handleFormat}
disabled={!sqlValue.trim()}
title="Format SQL (Shift+Alt+F)"
@@ -296,9 +297,9 @@ export function WorkspacePanel({
Format
</Button>
<Button
size="sm"
size="xs"
variant="ghost"
className="h-6 gap-1 text-xs"
className="gap-1 text-[11px]"
onClick={() => setSaveDialogOpen(true)}
disabled={!sqlValue.trim()}
title="Save query"
@@ -306,20 +307,24 @@ export function WorkspacePanel({
<Bookmark className="h-3 w-3" />
Save
</Button>
<div className="mx-1 h-3.5 w-px bg-border/40" />
{/* AI actions group — purple-branded */}
<Button
size="sm"
size="xs"
variant={aiBarOpen ? "secondary" : "ghost"}
className="h-6 gap-1 text-xs"
className={`gap-1 text-[11px] ${aiBarOpen ? "text-tusk-purple" : ""}`}
onClick={() => setAiBarOpen(!aiBarOpen)}
title="AI SQL Generator"
>
<Sparkles className="h-3 w-3" />
<Sparkles className={`h-3 w-3 ${aiBarOpen ? "tusk-ai-icon" : ""}`} />
AI
</Button>
<Button
size="sm"
size="xs"
variant="ghost"
className="h-6 gap-1 text-xs"
className="gap-1 text-[11px]"
onClick={handleAiExplain}
disabled={isAiLoading || !sqlValue.trim()}
title="Explain query with AI"
@@ -331,35 +336,42 @@ export function WorkspacePanel({
)}
AI Explain
</Button>
{result && result.columns.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="sm"
variant="ghost"
className="h-6 gap-1 text-xs"
>
<Download className="h-3 w-3" />
Export
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => handleExport("csv")}>
Export CSV
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport("json")}>
Export JSON
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<>
<div className="mx-1 h-3.5 w-px bg-border/40" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="xs"
variant="ghost"
className="gap-1 text-[11px]"
>
<Download className="h-3 w-3" />
Export
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => handleExport("csv")}>
Export CSV
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleExport("json")}>
Export JSON
</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>
{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">
<Lock className="h-3 w-3" />
Read-Only
<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-2.5 w-2.5" />
READ
</span>
)}
</div>
@@ -389,35 +401,41 @@ export function WorkspacePanel({
<ResizablePanel id="results" defaultSize="60%" minSize="15%">
<div className="flex h-full flex-col overflow-hidden">
{(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
className={`px-3 py-1 font-medium ${
className={`relative px-3 py-1.5 font-medium transition-colors ${
resultView === "results"
? "bg-background text-foreground"
: "text-muted-foreground hover:text-foreground"
? "text-foreground"
: "text-muted-foreground hover:text-foreground/70"
}`}
onClick={() => setResultView("results")}
>
Results
{resultView === "results" && (
<span className="absolute bottom-0 left-1/4 right-1/4 h-0.5 rounded-t bg-primary" />
)}
</button>
{explainData && (
<button
className={`px-3 py-1 font-medium ${
className={`relative px-3 py-1.5 font-medium transition-colors ${
resultView === "explain"
? "bg-background text-foreground"
: "text-muted-foreground hover:text-foreground"
? "text-foreground"
: "text-muted-foreground hover:text-foreground/70"
}`}
onClick={() => setResultView("explain")}
>
Explain
{resultView === "explain" && (
<span className="absolute bottom-0 left-1/4 right-1/4 h-0.5 rounded-t bg-primary" />
)}
</button>
)}
{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
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"
? "bg-muted text-foreground"
? "bg-accent text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
onClick={() => setResultViewMode("table")}
@@ -427,9 +445,9 @@ export function WorkspacePanel({
Table
</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"
? "bg-muted text-foreground"
? "bg-accent text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
onClick={() => setResultViewMode("json")}

View File

@@ -4,6 +4,11 @@
@custom-variant dark (&:is(.dark *));
/* ═══════════════════════════════════════════════════════
TUSK — "Twilight" Design System
Soft dark with blue undertones and teal accents
═══════════════════════════════════════════════════════ */
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
@@ -43,50 +48,399 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--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 {
--radius: 0.625rem;
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--radius: 0.5rem;
/* Soft twilight palette — comfortable, not eye-straining */
--background: oklch(0.2 0.012 250);
--foreground: oklch(0.9 0.005 250);
--card: oklch(0.23 0.012 250);
--card-foreground: oklch(0.9 0.005 250);
--popover: oklch(0.25 0.014 250);
--popover-foreground: oklch(0.9 0.005 250);
/* Teal primary — slightly softer for the lighter background */
--primary: oklch(0.72 0.14 170);
--primary-foreground: oklch(0.18 0.015 250);
/* Surfaces — gentle stepping */
--secondary: oklch(0.27 0.012 250);
--secondary-foreground: oklch(0.85 0.008 250);
--muted: oklch(0.27 0.012 250);
--muted-foreground: oklch(0.62 0.015 250);
--accent: oklch(0.28 0.014 250);
--accent-foreground: oklch(0.9 0.005 250);
/* Status */
--destructive: oklch(0.65 0.2 15);
/* Borders & inputs — more visible, less transparent */
--border: oklch(0.34 0.015 250 / 70%);
--input: oklch(0.36 0.015 250 / 60%);
--ring: oklch(0.72 0.14 170 / 40%);
/* Chart palette */
--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 {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
font-family: var(--font-sans);
overflow: hidden;
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);
}