diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index fa2275fd687..6a2f5d0142f 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -2,10 +2,12 @@ import { dialog } from "electron"; import type { BrowserWindow } from "electron"; import { basename } from "node:path"; import { nanoid } from "nanoid"; +import { z } from "zod"; import { publicProcedure, router } from "../.."; import { db } from "../../../../main/lib/db"; import type { Project } from "../../../../main/lib/db/schemas"; import { getGitRoot } from "../workspaces/utils/git"; +import { assignRandomColor } from "./utils/colors"; export const createProjectsRouter = (window: BrowserWindow) => { return router({ @@ -55,6 +57,8 @@ export const createProjectsRouter = (window: BrowserWindow) => { id: nanoid(), mainRepoPath, name, + color: assignRandomColor(), + tabOrder: null, lastOpenedAt: Date.now(), createdAt: Date.now(), }; @@ -69,6 +73,44 @@ export const createProjectsRouter = (window: BrowserWindow) => { project, }; }), + + reorder: publicProcedure + .input( + z.object({ + fromIndex: z.number(), + toIndex: z.number(), + }), + ) + .mutation(async ({ input }) => { + await db.update((data) => { + const { fromIndex, toIndex } = input; + + const activeProjects = data.projects + .filter((p) => p.tabOrder !== null) + .sort((a, b) => a.tabOrder! - b.tabOrder!); + + if ( + fromIndex < 0 || + fromIndex >= activeProjects.length || + toIndex < 0 || + toIndex >= activeProjects.length + ) { + throw new Error("Invalid fromIndex or toIndex"); + } + + const [removed] = activeProjects.splice(fromIndex, 1); + activeProjects.splice(toIndex, 0, removed); + + activeProjects.forEach((project, index) => { + const p = data.projects.find((p) => p.id === project.id); + if (p) { + p.tabOrder = index; + } + }); + }); + + return { success: true }; + }), }); }; diff --git a/apps/desktop/src/lib/trpc/routers/projects/utils/colors/colors.ts b/apps/desktop/src/lib/trpc/routers/projects/utils/colors/colors.ts new file mode 100644 index 00000000000..82671b70f41 --- /dev/null +++ b/apps/desktop/src/lib/trpc/routers/projects/utils/colors/colors.ts @@ -0,0 +1,16 @@ +import colors from "tailwindcss/colors"; + +const PROJECT_COLORS = [ + colors.blue[500], + colors.green[500], + colors.yellow[500], + colors.red[500], + colors.purple[500], + colors.cyan[500], + colors.orange[500], + colors.slate[500], +] as const; + +export function assignRandomColor(): string { + return PROJECT_COLORS[Math.floor(Math.random() * PROJECT_COLORS.length)]; +} diff --git a/apps/desktop/src/lib/trpc/routers/projects/utils/colors/index.ts b/apps/desktop/src/lib/trpc/routers/projects/utils/colors/index.ts new file mode 100644 index 00000000000..71f1ed4900e --- /dev/null +++ b/apps/desktop/src/lib/trpc/routers/projects/utils/colors/index.ts @@ -0,0 +1 @@ +export { assignRandomColor } from "./colors"; diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts index 312c6ca0a20..5495deb5348 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts @@ -19,7 +19,6 @@ export const createWorkspacesRouter = () => { }), ) .mutation(async ({ input }) => { - // Find the project const project = db.data.projects.find((p) => p.id === input.projectId); if (!project) { throw new Error(`Project ${input.projectId} not found`); @@ -33,10 +32,8 @@ export const createWorkspacesRouter = () => { branch, ); - // Create git worktree await createWorktree(project.mainRepoPath, branch, worktreePath); - // Create worktree record const worktree = { id: nanoid(), projectId: input.projectId, @@ -45,10 +42,12 @@ export const createWorkspacesRouter = () => { createdAt: Date.now(), }; - // Set order to be at the end of the list - const maxOrder = - db.data.workspaces.length > 0 - ? Math.max(...db.data.workspaces.map((w) => w.order)) + const projectWorkspaces = db.data.workspaces.filter( + (w) => w.projectId === input.projectId, + ); + const maxTabOrder = + projectWorkspaces.length > 0 + ? Math.max(...projectWorkspaces.map((w) => w.tabOrder)) : -1; const workspace = { @@ -56,32 +55,37 @@ export const createWorkspacesRouter = () => { projectId: input.projectId, worktreeId: worktree.id, name: input.name ?? branch, - order: maxOrder + 1, + tabOrder: maxTabOrder + 1, createdAt: Date.now(), updatedAt: Date.now(), lastOpenedAt: Date.now(), }; - // Save to database await db.update((data) => { data.worktrees.push(worktree); data.workspaces.push(workspace); data.settings.lastActiveWorkspaceId = workspace.id; - // Update project lastOpenedAt const p = data.projects.find((p) => p.id === input.projectId); if (p) { p.lastOpenedAt = Date.now(); + + if (p.tabOrder === null) { + const activeProjects = data.projects.filter( + (proj) => proj.tabOrder !== null, + ); + const maxProjectTabOrder = + activeProjects.length > 0 + ? Math.max(...activeProjects.map((proj) => proj.tabOrder!)) + : -1; + p.tabOrder = maxProjectTabOrder + 1; + } } }); return workspace; }), - /** - * Get a workspace by ID - * Throws if workspace not found - */ get: publicProcedure .input(z.object({ id: z.string() })) .query(({ input }) => { @@ -92,20 +96,61 @@ export const createWorkspacesRouter = () => { return workspace; }), - /** - * Get all workspaces sorted by order - */ getAll: publicProcedure.query(() => { return db.data.workspaces .slice() - .sort((a, b) => a.order - b.order); + .sort((a, b) => a.tabOrder - b.tabOrder); + }), + + getAllGrouped: publicProcedure.query(() => { + const activeProjects = db.data.projects.filter( + (p) => p.tabOrder !== null, + ); + + const groupsMap = new Map< + string, + { + project: { id: string; name: string; color: string; tabOrder: number }; + workspaces: Array<{ + id: string; + projectId: string; + worktreeId: string; + name: string; + tabOrder: number; + createdAt: number; + updatedAt: number; + lastOpenedAt: number; + }>; + } + >(); + + for (const project of activeProjects) { + groupsMap.set(project.id, { + project: { + id: project.id, + name: project.name, + color: project.color, + tabOrder: project.tabOrder!, + }, + workspaces: [], + }); + } + + const workspaces = db.data.workspaces + .slice() + .sort((a, b) => a.tabOrder - b.tabOrder); + + for (const workspace of workspaces) { + if (groupsMap.has(workspace.projectId)) { + groupsMap.get(workspace.projectId)!.workspaces.push(workspace); + } + } + + return Array.from(groupsMap.values()).sort( + (a, b) => a.project.tabOrder - b.project.tabOrder, + ); }), - /** - * Get the last active workspace - * Returns null if no active workspace set (valid state) - * Throws if active workspace ID exists but workspace not found (data inconsistency) - */ getActive: publicProcedure.query(() => { const { lastActiveWorkspaceId } = db.data.settings; @@ -125,10 +170,6 @@ export const createWorkspacesRouter = () => { return workspace; }), - /** - * Update a workspace - * Supports partial updates to workspace properties - */ update: publicProcedure .input( z.object({ @@ -145,12 +186,10 @@ export const createWorkspacesRouter = () => { throw new Error(`Workspace ${input.id} not found`); } - // Apply patches if (input.patch.name !== undefined) { workspace.name = input.patch.name; } - // Update timestamps workspace.updatedAt = Date.now(); workspace.lastOpenedAt = Date.now(); }); @@ -158,9 +197,6 @@ export const createWorkspacesRouter = () => { return { success: true }; }), - /** - * Delete a workspace and its associated worktree - */ delete: publicProcedure .input(z.object({ id: z.string() })) .mutation(async ({ input }) => { @@ -170,7 +206,6 @@ export const createWorkspacesRouter = () => { return { success: false, error: "Workspace not found" }; } - // Find associated worktree and project const worktree = db.data.worktrees.find( (wt) => wt.id === workspace.worktreeId, ); @@ -178,31 +213,36 @@ export const createWorkspacesRouter = () => { (p) => p.id === workspace.projectId, ); - // Remove git worktree if it exists if (worktree && project) { try { await removeWorktree(project.mainRepoPath, worktree.path); } catch (error) { console.error("Failed to remove worktree:", error); - // Continue with database cleanup even if git operation fails } } - // Remove from database await db.update((data) => { - // Remove workspace data.workspaces = data.workspaces.filter((w) => w.id !== input.id); - // Remove worktree if (worktree) { data.worktrees = data.worktrees.filter( (wt) => wt.id !== worktree.id, ); } - // Update last active workspace if needed + if (project) { + const remainingWorkspaces = data.workspaces.filter( + (w) => w.projectId === workspace.projectId, + ); + if (remainingWorkspaces.length === 0) { + const p = data.projects.find((p) => p.id === workspace.projectId); + if (p) { + p.tabOrder = null; + } + } + } + if (data.settings.lastActiveWorkspaceId === input.id) { - // Set to the most recently opened workspace, if any const sorted = data.workspaces .slice() .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt); @@ -213,9 +253,6 @@ export const createWorkspacesRouter = () => { return { success: true }; }), - /** - * Set active workspace - */ setActive: publicProcedure .input(z.object({ id: z.string() })) .mutation(async ({ input }) => { @@ -233,34 +270,38 @@ export const createWorkspacesRouter = () => { return { success: true }; }), - /** - * Reorder workspaces - */ reorder: publicProcedure .input( z.object({ + projectId: z.string(), fromIndex: z.number(), toIndex: z.number(), }), ) .mutation(async ({ input }) => { await db.update((data) => { - const { fromIndex, toIndex } = input; - - // Get all workspaces sorted by order - const workspaces = data.workspaces - .slice() - .sort((a, b) => a.order - b.order); + const { projectId, fromIndex, toIndex } = input; + + const projectWorkspaces = data.workspaces + .filter((w) => w.projectId === projectId) + .sort((a, b) => a.tabOrder - b.tabOrder); + + if ( + fromIndex < 0 || + fromIndex >= projectWorkspaces.length || + toIndex < 0 || + toIndex >= projectWorkspaces.length + ) { + throw new Error("Invalid fromIndex or toIndex"); + } - // Move workspace from fromIndex to toIndex - const [removed] = workspaces.splice(fromIndex, 1); - workspaces.splice(toIndex, 0, removed); + const [removed] = projectWorkspaces.splice(fromIndex, 1); + projectWorkspaces.splice(toIndex, 0, removed); - // Update order fields to reflect new positions - workspaces.forEach((workspace, index) => { + projectWorkspaces.forEach((workspace, index) => { const ws = data.workspaces.find((w) => w.id === workspace.id); if (ws) { - ws.order = index; + ws.tabOrder = index; } }); }); diff --git a/apps/desktop/src/main/lib/db/schemas.ts b/apps/desktop/src/main/lib/db/schemas.ts index 2a5df8ebf7a..91da642c8c5 100644 --- a/apps/desktop/src/main/lib/db/schemas.ts +++ b/apps/desktop/src/main/lib/db/schemas.ts @@ -1,46 +1,34 @@ -/** - * Database schemas for local-first storage - * These types define the structure of data stored in lowdb - */ - -/** - * Project represents a main git repository - */ export interface Project { - id: string; // nanoid - mainRepoPath: string; // Absolute path to the main git repo - name: string; // Project name (derived from folder name) - lastOpenedAt: number; // Timestamp of last access + id: string; + mainRepoPath: string; + name: string; + color: string; + tabOrder: number | null; + lastOpenedAt: number; createdAt: number; } -/** - * Worktree represents a git worktree - */ export interface Worktree { - id: string; // nanoid - projectId: string; // References Project.id - path: string; // Absolute path to the worktree - branch: string; // Git branch name - source of truth for git operations + id: string; + projectId: string; + path: string; + branch: string; createdAt: number; } -/** - * Workspace represents a UI tab (1:1 with Worktree) - */ export interface Workspace { - id: string; // nanoid - projectId: string; // References Project.id - worktreeId: string; // References Worktree.id - name: string; // User-facing workspace name - order: number; // Explicit order in the workspace tabs (0 = first, 1 = second, etc.) + id: string; + projectId: string; + worktreeId: string; + name: string; + tabOrder: number; createdAt: number; updatedAt: number; lastOpenedAt: number; } export interface Tab { - id: string; // nanoid + id: string; title: string; terminalId?: string; type: "single" | "group"; @@ -59,9 +47,6 @@ export interface Database { settings: Settings; } -/** - * Default database state - */ export const defaultDatabase: Database = { projects: [], worktrees: [], diff --git a/apps/desktop/src/renderer/react-query/projects/index.ts b/apps/desktop/src/renderer/react-query/projects/index.ts index 8f1df6c0302..e5f015078cb 100644 --- a/apps/desktop/src/renderer/react-query/projects/index.ts +++ b/apps/desktop/src/renderer/react-query/projects/index.ts @@ -1 +1,2 @@ export { useOpenNew } from "./useOpenNew"; +export { useReorderProjects } from "./useReorderProjects"; diff --git a/apps/desktop/src/renderer/react-query/projects/useReorderProjects.ts b/apps/desktop/src/renderer/react-query/projects/useReorderProjects.ts new file mode 100644 index 00000000000..03b39ed32b3 --- /dev/null +++ b/apps/desktop/src/renderer/react-query/projects/useReorderProjects.ts @@ -0,0 +1,11 @@ +import { trpc } from "renderer/lib/trpc"; + +export function useReorderProjects() { + const utils = trpc.useUtils(); + + return trpc.projects.reorder.useMutation({ + onSuccess: () => { + utils.workspaces.getAllGrouped.invalidate(); + }, + }); +} diff --git a/apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspaces.ts b/apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspaces.ts index 3e9e6e5027b..35bbe675ee4 100644 --- a/apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspaces.ts +++ b/apps/desktop/src/renderer/react-query/workspaces/useReorderWorkspaces.ts @@ -2,7 +2,7 @@ import { trpc } from "renderer/lib/trpc"; /** * Mutation hook for reordering workspaces - * Automatically invalidates getAll query on success + * Automatically invalidates workspace queries on success */ export function useReorderWorkspaces( options?: Parameters[0], @@ -12,10 +12,8 @@ export function useReorderWorkspaces( return trpc.workspaces.reorder.useMutation({ ...options, onSuccess: async (...args) => { - // Auto-invalidate workspaces list await utils.workspaces.getAll.invalidate(); - - // Call user's onSuccess if provided + await utils.workspaces.getAllGrouped.invalidate(); await options?.onSuccess?.(...args); }, }); diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceGroup.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceGroup.tsx new file mode 100644 index 00000000000..49f2b8e1510 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceGroup.tsx @@ -0,0 +1,87 @@ +import { useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { WorkspaceGroupHeader } from "./WorkspaceGroupHeader"; +import { WorkspaceItem } from "./WorkspaceItem"; + +interface Workspace { + id: string; + projectId: string; + name: string; + tabOrder: number; +} + +interface WorkspaceGroupProps { + projectId: string; + projectName: string; + projectColor: string; + projectIndex: number; + workspaces: Workspace[]; + activeWorkspaceId: string | null; + workspaceWidth: number; + hoveredWorkspaceId: string | null; + onWorkspaceHover: (id: string | null) => void; +} + +export function WorkspaceGroup({ + projectId, + projectName, + projectColor, + projectIndex, + workspaces, + activeWorkspaceId, + workspaceWidth, + hoveredWorkspaceId, + onWorkspaceHover, +}: WorkspaceGroupProps) { + const [isCollapsed, setIsCollapsed] = useState(false); + + return ( +
+ {/* Project group badge */} + setIsCollapsed(!isCollapsed)} + /> + + {/* Workspaces with colored line (collapsed shows only active tab) */} +
+ + {(isCollapsed + ? workspaces.filter((w) => w.id === activeWorkspaceId) + : workspaces + ).map((workspace, index) => ( + + onWorkspaceHover(workspace.id)} + onMouseLeave={() => onWorkspaceHover(null)} + /> + + ))} + +
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceGroupHeader.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceGroupHeader.tsx new file mode 100644 index 00000000000..9e35ccb0e8c --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceGroupHeader.tsx @@ -0,0 +1,79 @@ +import { useDrag, useDrop } from "react-dnd"; +import { useReorderProjects } from "renderer/react-query/projects"; + +const PROJECT_GROUP_TYPE = "PROJECT_GROUP"; + +interface WorkspaceGroupHeaderProps { + projectId: string; + projectName: string; + projectColor: string; + isCollapsed: boolean; + index: number; + onToggleCollapse: () => void; +} + +export function WorkspaceGroupHeader({ + projectId, + projectName, + projectColor, + isCollapsed, + index, + onToggleCollapse, +}: WorkspaceGroupHeaderProps) { + const reorderProjects = useReorderProjects(); + + const [{ isDragging }, drag] = useDrag( + () => ({ + type: PROJECT_GROUP_TYPE, + item: { projectId, index }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }), + [projectId, index], + ); + + const [{ isOver }, drop] = useDrop( + () => ({ + accept: PROJECT_GROUP_TYPE, + hover: (item: { projectId: string; index: number }) => { + if (item.index !== index) { + reorderProjects.mutate({ + fromIndex: item.index, + toIndex: index, + }); + item.index = index; + } + }, + collect: (monitor) => ({ + isOver: monitor.isOver(), + }), + }), + [index, reorderProjects], + ); + + return ( + + ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx index 6e720cd311d..66248082930 100644 --- a/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/WorkspaceItem.tsx @@ -12,6 +12,7 @@ const WORKSPACE_TYPE = "WORKSPACE"; interface WorkspaceItemProps { id: string; + projectId: string; title: string; isActive: boolean; index: number; @@ -22,6 +23,7 @@ interface WorkspaceItemProps { export function WorkspaceItem({ id, + projectId, title, isActive, index, @@ -36,19 +38,21 @@ export function WorkspaceItem({ const [{ isDragging }, drag] = useDrag( () => ({ type: WORKSPACE_TYPE, - item: { id, index }, + item: { id, projectId, index }, collect: (monitor) => ({ isDragging: monitor.isDragging(), }), }), - [id, index], + [id, projectId, index], ); const [, drop] = useDrop({ accept: WORKSPACE_TYPE, - hover: (item: { id: string; index: number }) => { - if (item.index !== index) { + hover: (item: { id: string; projectId: string; index: number }) => { + // Only allow reordering within the same project + if (item.projectId === projectId && item.index !== index) { reorderWorkspaces.mutate({ + projectId, fromIndex: item.index, toIndex: index, }); @@ -68,11 +72,11 @@ export function WorkspaceItem({ ref={(node) => { drag(drop(node)); }} - onClick={() => setActive.mutate({ id })} + onMouseDown={() => setActive.mutate({ id })} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} className={` - flex items-center gap-0.5 rounded-t-md transition-all w-full shrink-0 pr-6 pl-3 h-[80%] + flex items-center gap-0.5 rounded-t-md transition-all w-full shrink-0 pr-6 pl-3 h-full ${ isActive ? "text-foreground bg-sidebar" diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/index.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/index.tsx index 3e8160eb87a..690ee9d2fdf 100644 --- a/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/index.tsx @@ -1,17 +1,16 @@ -import { Separator } from "@superset/ui/separator"; import { Fragment, useEffect, useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { trpc } from "renderer/lib/trpc"; import { useSetActiveWorkspace } from "renderer/react-query/workspaces"; import { WorkspaceDropdown } from "./WorkspaceDropdown"; -import { WorkspaceItem } from "./WorkspaceItem"; +import { WorkspaceGroup } from "./WorkspaceGroup"; const MIN_WORKSPACE_WIDTH = 60; -const MAX_WORKSPACE_WIDTH = 240; +const MAX_WORKSPACE_WIDTH = 160; const ADD_BUTTON_WIDTH = 48; export function WorkspacesTabs() { - const { data: workspaces = [] } = trpc.workspaces.getAll.useQuery(); + const { data: groups = [] } = trpc.workspaces.getAllGrouped.useQuery(); const { data: activeWorkspace } = trpc.workspaces.getActive.useQuery(); const activeWorkspaceId = activeWorkspace?.id || null; const setActiveWorkspace = useSetActiveWorkspace(); @@ -24,22 +23,33 @@ export function WorkspacesTabs() { null, ); - // Workspace switching shortcuts - useHotkeys('meta+alt+left', () => { - if (!activeWorkspaceId) return; - const index = workspaces.findIndex((w) => w.id === activeWorkspaceId); - if (index > 0) { - setActiveWorkspace.mutate({ id: workspaces[index - 1].id }); - } - }, [activeWorkspaceId, workspaces, setActiveWorkspace]); + // Flatten workspaces for keyboard navigation + const allWorkspaces = groups.flatMap((group) => group.workspaces); + + // Workspace switching shortcuts (work across groups) + useHotkeys( + "meta+alt+left", + () => { + if (!activeWorkspaceId) return; + const index = allWorkspaces.findIndex((w) => w.id === activeWorkspaceId); + if (index > 0) { + setActiveWorkspace.mutate({ id: allWorkspaces[index - 1].id }); + } + }, + [activeWorkspaceId, allWorkspaces, setActiveWorkspace], + ); - useHotkeys('meta+alt+right', () => { - if (!activeWorkspaceId) return; - const index = workspaces.findIndex((w) => w.id === activeWorkspaceId); - if (index < workspaces.length - 1) { - setActiveWorkspace.mutate({ id: workspaces[index + 1].id }); - } - }, [activeWorkspaceId, workspaces, setActiveWorkspace]); + useHotkeys( + "meta+alt+right", + () => { + if (!activeWorkspaceId) return; + const index = allWorkspaces.findIndex((w) => w.id === activeWorkspaceId); + if (index < allWorkspaces.length - 1) { + setActiveWorkspace.mutate({ id: allWorkspaces[index + 1].id }); + } + }, + [activeWorkspaceId, allWorkspaces, setActiveWorkspace], + ); useEffect(() => { const checkScroll = () => { @@ -59,7 +69,7 @@ export function WorkspacesTabs() { // Calculate width: fill available space but respect min/max const calculatedWidth = Math.max( MIN_WORKSPACE_WIDTH, - Math.min(MAX_WORKSPACE_WIDTH, availableWidth / workspaces.length), + Math.min(MAX_WORKSPACE_WIDTH, availableWidth / allWorkspaces.length), ); setWorkspaceWidth(calculatedWidth); }; @@ -80,53 +90,35 @@ export function WorkspacesTabs() { } window.removeEventListener("resize", updateWorkspaceWidth); }; - }, [workspaces]); + }, [allWorkspaces]); return ( -
+
- {workspaces.map((workspace, index) => { - const nextWorkspace = workspaces[index + 1]; - const isActive = workspace.id === activeWorkspaceId; - const isNextActive = nextWorkspace?.id === activeWorkspaceId; - const isHovered = workspace.id === hoveredWorkspaceId; - const isNextHovered = nextWorkspace?.id === hoveredWorkspaceId; - const separatorOpacity = - !isActive && !isNextActive && !isHovered && !isNextHovered - ? 100 - : 0; - - return ( - -
- setHoveredWorkspaceId(workspace.id)} - onMouseLeave={() => setHoveredWorkspaceId(null)} - /> + {groups.map((group, groupIndex) => ( + + + {groupIndex < groups.length - 1 && ( +
+
- {index < workspaces.length - 1 && ( -
- -
- )} - - ); - })} + )} + + ))}
{/* Fade effects for scroll indication */} @@ -138,7 +130,6 @@ export function WorkspacesTabs() { )}
-
); }