diff --git a/package-lock.json b/package-lock.json index c6bc7087..3ac24627 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,16 @@ { "name": "libsql-studio", - "version": "0.2.1", + "version": "0.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "libsql-studio", - "version": "0.2.1", + "version": "0.2.2", "dependencies": { "@codemirror/lang-sql": "^6.5.5", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", "@lezer/common": "^1.2.1", "@lezer/lr": "^1.4.0", "@libsql/client": "^0.5.3", @@ -927,6 +929,55 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz", + "integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz", + "integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.0", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz", + "integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.1.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@drizzle-team/studio": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@drizzle-team/studio/-/studio-0.0.39.tgz", diff --git a/package.json b/package.json index bf2e4bfb..5d6570af 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ }, "dependencies": { "@codemirror/lang-sql": "^6.5.5", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", "@lezer/common": "^1.2.1", "@lezer/lr": "^1.4.0", "@libsql/client": "^0.5.3", diff --git a/src/components/database-gui.tsx b/src/components/database-gui.tsx index c890e7c7..a0add200 100644 --- a/src/components/database-gui.tsx +++ b/src/components/database-gui.tsx @@ -13,6 +13,7 @@ import { MessageChannelName } from "@/messages/const"; import { OpenTabsProps } from "@/messages/openTabs"; import QueryWindow from "@/components/tabs/query-tab"; import SchemaEditorTab from "@/components/tabs/schema-editor-tab"; +import { LucideCode, LucideTable, LucideTableProperties } from "lucide-react"; export default function DatabaseGui() { const DEFAULT_WIDTH = 300; @@ -25,7 +26,12 @@ export default function DatabaseGui() { const [selectedTabIndex, setSelectedTabIndex] = useState(0); const [tabs, setTabs] = useState(() => [ - { title: "Query", key: "query", component: }, + { + title: "Query", + key: "query", + component: , + icon: LucideCode, + }, ]); useMessageListener( @@ -45,6 +51,7 @@ export default function DatabaseGui() { return [ ...prev, { + icon: LucideTable, title: newTab.name, key: newTab.key, component: , @@ -57,6 +64,7 @@ export default function DatabaseGui() { return [ ...prev, { + icon: LucideCode, title: newTab.name, key: newTab.key, component: , @@ -74,6 +82,7 @@ export default function DatabaseGui() { return [ ...prev, { + icon: LucideTableProperties, title: newTab.name, key: newTab.key, component: , diff --git a/src/components/sortable-tab.module.css b/src/components/sortable-tab.module.css new file mode 100644 index 00000000..5623c96e --- /dev/null +++ b/src/components/sortable-tab.module.css @@ -0,0 +1,7 @@ +.tab .close { + visibility: hidden; +} + +.tab:hover .close { + visibility: visible; +} diff --git a/src/components/sortable-tab.tsx b/src/components/sortable-tab.tsx new file mode 100644 index 00000000..962f4b36 --- /dev/null +++ b/src/components/sortable-tab.tsx @@ -0,0 +1,76 @@ +import { LucideIcon, LucideX } from "lucide-react"; +import { useSortable } from "@dnd-kit/sortable"; +import styles from "./sortable-tab.module.css"; +import { WindowTabItemProps } from "./windows-tab"; +import { cn } from "@/lib/utils"; +import { forwardRef } from "react"; +import { ButtonProps } from "./ui/button"; + +interface SortableTabProps { + tab: WindowTabItemProps; + selected: boolean; + onSelectChange: () => void; + onClose: () => void; +} + +type WindowTabItemButtonProps = ButtonProps & { + selected?: boolean; + title: string; + icon: LucideIcon; + onClose?: () => void; +}; + +export const WindowTabItemButton = forwardRef< + HTMLButtonElement, + WindowTabItemButtonProps +>(function WindowTabItemButton(props: WindowTabItemButtonProps, ref) { + const { icon: Icon, selected, title, onClose, ...rest } = props; + + const className = cn( + "h-9 flex items-center text-left text-xs font-semibold px-2 w-max-[150px]", + styles.tab, + selected + ? "border-x border-t bg-background border-b-background rounded-t" + : "border-b border-t border-t-secondary border-x-secondary opacity-65 hover:opacity-100" + ); + + return ( + + ); +}); + +export function SortableTab({ + tab, + selected, + onSelectChange, + onClose, +}: SortableTabProps) { + const { attributes, listeners, setNodeRef } = useSortable({ id: tab.key }); + + return ( + + ); +} diff --git a/src/components/tabs/table-data-tab.tsx b/src/components/tabs/table-data-tab.tsx index 132c1c50..77e65b0f 100644 --- a/src/components/tabs/table-data-tab.tsx +++ b/src/components/tabs/table-data-tab.tsx @@ -153,9 +153,9 @@ export default function TableDataWindow({ tableName }: TableDataContentProps) { )}
-
+
- - - {tabs.map((tab, idx) => { - return ( - + tab.key)} + strategy={verticalListSortingStrategy} + > + {tabs.map((tab, idx) => ( + onSelectChange(idx)} + onClose={() => + onTabsChange(tabs.filter((t) => t.key !== tab.key)) + } /> - - ); - })} + ))} + +
+
- -
-
- {tabs.map((tab, tabIndex) => { - return ( +
+ {tabs.map((tab, tabIndex) => (
{tab.component}
- ); - })} + ))} +
- + + {dragTab ? ( +
+ +
+ ) : null} +
+ ); }