+ onCellDoubleClick?.(rowIndex, i, value)
+ }
+ >
+ {value === null ? "NULL" : String(value)}
+
+ );
+ },
+ size: 150,
+ minSize: 50,
+ maxSize: 800,
+ })),
+ [colNames, onCellDoubleClick, highlightedCells]
+ );
+
+ const table = useReactTable({
+ data: rows,
+ columns,
+ state: { sorting },
+ onSortingChange: setSorting,
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ columnResizeMode,
+ enableColumnResizing: true,
+ });
+
+ const { rows: tableRows } = table.getRowModel();
+
+ const virtualizer = useVirtualizer({
+ count: tableRows.length,
+ getScrollElement: () => parentRef.current,
+ estimateSize: () => 28,
+ overscan: 20,
+ });
+
+ const columnSizing = table.getState().columnSizing;
+
+ const getColWidth = useCallback(
+ (colId: string, fallback: number) => columnSizing[colId] ?? fallback,
+ [columnSizing]
+ );
+
+ if (colNames.length === 0) return null;
+
+ return (
+
+ {/* Header */}
+
+ {table.getHeaderGroups().map((headerGroup) =>
+ headerGroup.headers.map((header) => (
+
+
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+ {header.column.getIsSorted() === "asc" && (
+
+ )}
+ {header.column.getIsSorted() === "desc" && (
+
+ )}
+
+ {/* Resize handle */}
+
header.column.resetSize()}
+ className={`absolute right-0 top-0 h-full w-1 cursor-col-resize select-none touch-none hover:bg-primary ${
+ header.column.getIsResizing() ? "bg-primary" : ""
+ }`}
+ />
+
+ ))
+ )}
+
+
+ {/* Virtual rows */}
+
+ {virtualizer.getVirtualItems().map((virtualRow) => {
+ const row = tableRows[virtualRow.index];
+ return (
+
+ {row.getVisibleCells().map((cell) => {
+ const w = getColWidth(cell.column.id, cell.column.getSize());
+ return (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+
+ );
+ })}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/components/workspace/TabContent.tsx b/src/components/workspace/TabContent.tsx
new file mode 100644
index 0000000..f9d4315
--- /dev/null
+++ b/src/components/workspace/TabContent.tsx
@@ -0,0 +1,49 @@
+import { useAppStore } from "@/stores/app-store";
+import { WorkspacePanel } from "./WorkspacePanel";
+import { TableDataView } from "@/components/table-viewer/TableDataView";
+import { TableStructure } from "@/components/table-viewer/TableStructure";
+
+export function TabContent() {
+ const { tabs, activeTabId, updateTab } = useAppStore();
+ const activeTab = tabs.find((t) => t.id === activeTabId);
+
+ if (!activeTab) {
+ return (
+
+ Open a query tab or select a table to get started.
+
+ );
+ }
+
+ switch (activeTab.type) {
+ case "query":
+ return (
+
updateTab(activeTab.id, { sql })}
+ />
+ );
+ case "table":
+ return (
+
+ );
+ case "structure":
+ return (
+
+ );
+ default:
+ return null;
+ }
+}
diff --git a/src/components/workspace/WorkspacePanel.tsx b/src/components/workspace/WorkspacePanel.tsx
new file mode 100644
index 0000000..92e2035
--- /dev/null
+++ b/src/components/workspace/WorkspacePanel.tsx
@@ -0,0 +1,102 @@
+import { useState, useCallback } from "react";
+import {
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+} from "@/components/ui/resizable";
+import { SqlEditor } from "@/components/editor/SqlEditor";
+import { ResultsPanel } from "@/components/results/ResultsPanel";
+import { useQueryExecution } from "@/hooks/use-query-execution";
+import { Button } from "@/components/ui/button";
+import { Play, Loader2 } from "lucide-react";
+import type { QueryResult } from "@/types";
+
+interface Props {
+ connectionId: string;
+ initialSql?: string;
+ onSqlChange?: (sql: string) => void;
+ onResult?: (result: QueryResult | null, error: string | null) => void;
+}
+
+export function WorkspacePanel({
+ connectionId,
+ initialSql = "",
+ onSqlChange,
+ onResult,
+}: Props) {
+ const [sqlValue, setSqlValue] = useState(initialSql);
+ const [result, setResult] = useState(null);
+ const [error, setError] = useState(null);
+ const queryMutation = useQueryExecution();
+
+ const handleChange = useCallback(
+ (val: string) => {
+ setSqlValue(val);
+ onSqlChange?.(val);
+ },
+ [onSqlChange]
+ );
+
+ const handleExecute = useCallback(() => {
+ if (!sqlValue.trim() || !connectionId) return;
+ setError(null);
+ queryMutation.mutate(
+ { connectionId, sql: sqlValue },
+ {
+ onSuccess: (data) => {
+ setResult(data);
+ setError(null);
+ onResult?.(data, null);
+ },
+ onError: (err) => {
+ setResult(null);
+ setError(String(err));
+ onResult?.(null, String(err));
+ },
+ }
+ );
+ }, [connectionId, sqlValue, queryMutation, onResult]);
+
+ return (
+
+
+
+
+
+
+ Ctrl+Enter to execute
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}