From 87ddef4dff06c42093273292c3192dd346b3c7b5 Mon Sep 17 00:00:00 2001 From: anuphilipp3 Date: Fri, 3 Jul 2026 07:21:02 +0530 Subject: [PATCH] Org-switch+ few tweaks --- apps/web/src/app/dashboard/page.tsx | 2 +- .../src/components/global-command-palette.tsx | 79 +++++++++++-------- apps/web/src/components/nav-bar.tsx | 17 ++-- .../src/components/org-switcher-dropdown.tsx | 4 +- .../src/components/sidebar/app-sidebar.tsx | 4 - .../src/components/sidebar/client-layout.tsx | 21 ++++- apps/web/src/components/tab-bar/tab-bar.tsx | 61 +++++++++++++- apps/web/src/lib/workspace-scope-reset.ts | 42 ++++++++++ 8 files changed, 176 insertions(+), 54 deletions(-) create mode 100644 apps/web/src/lib/workspace-scope-reset.ts diff --git a/apps/web/src/app/dashboard/page.tsx b/apps/web/src/app/dashboard/page.tsx index 3c40b718..56ec2053 100644 --- a/apps/web/src/app/dashboard/page.tsx +++ b/apps/web/src/app/dashboard/page.tsx @@ -273,7 +273,7 @@ const DashboardPage: React.FC = () => { mobileOnly /> -
+
{/* ── Desktop Hero (inside padded container for alignment) ── */} diff --git a/apps/web/src/components/global-command-palette.tsx b/apps/web/src/components/global-command-palette.tsx index 78b3378e..584982af 100644 --- a/apps/web/src/components/global-command-palette.tsx +++ b/apps/web/src/components/global-command-palette.tsx @@ -154,7 +154,13 @@ export function GlobalCommandPalette() { ) const commandSurfaceClass = - '[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5' + '[&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:pt-2.5 [&_[cmdk-group-heading]]:pb-1 [&_[cmdk-group-heading]]:text-[10px] [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-[0.14em] [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-14 [&_[cmdk-input]]:text-base' + + // Shared row treatment — gradient selection sweep + icon chip (Raycast-style). + const itemClass = + 'group mx-1 flex items-center gap-3 rounded-xl px-2 py-2 transition-colors aria-selected:bg-gradient-to-r aria-selected:from-primary/[0.14] aria-selected:via-primary/[0.06] aria-selected:to-transparent aria-selected:ring-1 aria-selected:ring-inset aria-selected:ring-primary/20' + const chipClass = + 'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted/60 text-muted-foreground ring-1 ring-inset ring-border/50 transition-colors group-aria-selected:bg-gradient-to-br group-aria-selected:from-primary/20 group-aria-selected:to-violet-500/10 group-aria-selected:text-primary group-aria-selected:ring-primary/20' return ( @@ -162,10 +168,17 @@ export function GlobalCommandPalette() { showCloseButton={false} className={cn( 'overflow-hidden p-0 gap-0 max-w-2xl', - 'max-h-[min(85vh,640px)] flex flex-col' + 'max-h-[min(85vh,640px)] flex flex-col', + 'rounded-2xl border-border/60 bg-popover/90 backdrop-blur-2xl', + 'shadow-2xl shadow-primary/10 ring-1 ring-inset ring-white/[0.04]' )} > Search tools and pages + {/* Gradient hairline — the palette's signature accent */} +
No results found. @@ -191,15 +204,17 @@ export function GlobalCommandPalette() { key={`recent-${entry.url}`} value={`recent ${entry.searchValue} ${entry.url}`.toLowerCase()} onSelect={() => run(entry)} - className="mx-1 flex items-start gap-3 rounded-lg py-2.5 transition-colors aria-selected:bg-gradient-to-r aria-selected:from-primary/[0.12] aria-selected:to-primary/[0.04] aria-selected:ring-1 aria-selected:ring-inset aria-selected:ring-primary/15" + className={itemClass} > - + + + {entry.title} {entry.description ? ( - + {entry.description} ) : null} @@ -221,24 +236,23 @@ export function GlobalCommandPalette() { key={`pinned-${entry.url}`} value={`pinned ${entry.searchValue} ${entry.url}`.toLowerCase()} onSelect={() => run(entry)} - className="mx-1 flex items-start gap-3 rounded-lg py-2.5 transition-colors aria-selected:bg-gradient-to-r aria-selected:from-primary/[0.12] aria-selected:to-primary/[0.04] aria-selected:ring-1 aria-selected:ring-inset aria-selected:ring-primary/15" + className={itemClass} > - + + + {entry.title} {entry.description ? ( - + {entry.description} ) : null} - @@ -259,18 +273,17 @@ export function GlobalCommandPalette() { key={entry.url} value={`${entry.searchValue} ${entry.url}`.toLowerCase()} onSelect={() => run(entry)} - className="flex items-start gap-3 py-2.5 aria-selected:bg-accent" + className={itemClass} > - + + + {entry.title} {entry.description ? ( - + {entry.description} ) : null} @@ -282,29 +295,29 @@ export function GlobalCommandPalette() { ))} -
- - +
+ + ↑↓ - {' '} + navigate - - + + ↵ - {' '} + open - - + + esc - {' '} + close - - + + {modLabel}K - {' '} + toggle
diff --git a/apps/web/src/components/nav-bar.tsx b/apps/web/src/components/nav-bar.tsx index faa0078c..2997a7c2 100644 --- a/apps/web/src/components/nav-bar.tsx +++ b/apps/web/src/components/nav-bar.tsx @@ -7,8 +7,8 @@ import { ModeToggle } from "@/components/modeToggle"; import { getToolMessageKey } from "@/lib/tool-i18n"; import { routeConfig } from "@/lib/route-config"; import { WorkspaceSwitcherDropdown } from "@/components/workspace-switcher-dropdown"; +import { OrgSwitcherDropdown } from "@/components/org-switcher-dropdown"; import { WorkspaceQuickActions } from "@/components/workspace-quick-actions"; -import { WorkspaceBreadcrumb } from "@/components/workspace-breadcrumb"; import { NotificationsBell } from "@/components/notifications-bell"; function openCommandPalette() { @@ -27,7 +27,7 @@ function CommandTrigger() { > Search or jump to… - + ⌘K @@ -63,19 +63,23 @@ export function NavBar() { const config = match?.[1]; const matchedRoute = match?.[0]; - if (!config) { + // The dashboard needs no page title — the sidebar highlights it and the page + // greets the user. A "Dashboard" label beside the org switcher is noise. + if (!config || matchedRoute === '/dashboard') { // Minimal bar everywhere with a sidebar so chrome (workspace, bell, theme) // is reachable on /dashboard, /settings, etc. — not just /app routes. return (
-
+
+
+ +
-
); } @@ -94,6 +98,8 @@ export function NavBar() {
+ +
@@ -116,7 +122,6 @@ export function NavBar() {
-
); } diff --git a/apps/web/src/components/org-switcher-dropdown.tsx b/apps/web/src/components/org-switcher-dropdown.tsx index 2110d593..1bb03a3f 100644 --- a/apps/web/src/components/org-switcher-dropdown.tsx +++ b/apps/web/src/components/org-switcher-dropdown.tsx @@ -10,7 +10,7 @@ import { useActiveOrg, useWorkspaceStore } from "@/store/workspace-store" import { CreateOrgDialog } from "@/components/create-org-dialog" /** - * Organization switcher — top-left of the sidebar header, beside the brand. + * Organization switcher — lives in the top nav, beside the page title. * Switching org cascades to its default workspace via `setActiveOrg`. */ export function OrgSwitcherDropdown() { @@ -28,7 +28,7 @@ export function OrgSwitcherDropdown() {
-
- -
diff --git a/apps/web/src/components/sidebar/client-layout.tsx b/apps/web/src/components/sidebar/client-layout.tsx index 1419ca7b..d836f5d8 100644 --- a/apps/web/src/components/sidebar/client-layout.tsx +++ b/apps/web/src/components/sidebar/client-layout.tsx @@ -5,7 +5,6 @@ import { SidebarProvider, useSidebar } from "@/components/ui/sidebar"; import { AppSidebar } from "./app-sidebar"; import { NavBar } from '@/components/nav-bar'; import { MobileNav } from '@/components/mobile-nav'; -import { GlobalCommandPalette } from '@/components/global-command-palette'; import { TabBar } from '@/components/tab-bar/tab-bar'; import { useTabStore } from '@/store/tab-store'; import { isTabRoute } from '@/lib/route-config'; @@ -14,6 +13,12 @@ import { MigrationBanner } from '@/components/migration-banner'; import { MobileDesktopHint } from '@/components/mobile-desktop-hint'; import { WorkspaceSwitcherDropdown } from '@/components/workspace-switcher-dropdown'; import { NotificationsBell } from '@/components/notifications-bell'; +import { useWorkspaceStore } from '@/store/workspace-store'; +import { initWorkspaceScopeReset } from '@/lib/workspace-scope-reset'; + +// Reset workspace-scoped stores whenever the active workspace changes +// (module-level, mirrors workspace-store's own subscribeOnce pattern). +initWorkspaceScopeReset(); // Renders all open tool tabs simultaneously. The active tab is visible; // inactive tabs use display:none to stay mounted (preserving their state). @@ -59,6 +64,10 @@ function Layout({ children }: { children: React.ReactNode }) { const { state } = useSidebar(); const pathname = usePathname(); const { tabs } = useTabStore(); + // Keying tool content by workspace remounts every mounted tab (and page) on + // switch, so each tool refetches under the new scope instead of showing the + // previous workspace's data. + const activeWorkspaceId = useWorkspaceStore((s) => s.activeWorkspaceId); // Open command palette for new tab via "+" button const openCommandPalette = useCallback(() => { @@ -104,9 +113,12 @@ function Layout({ children }: { children: React.ReactNode }) {
{inTabMode ? ( - + ) : ( -
+
{children}
@@ -116,7 +128,8 @@ function Layout({ children }: { children: React.ReactNode }) {
- + {/* Command palette is mounted once globally in ClientShell (root layout) — + a second mount here caused stacked overlays needing two clicks to dismiss. */}
); } diff --git a/apps/web/src/components/tab-bar/tab-bar.tsx b/apps/web/src/components/tab-bar/tab-bar.tsx index 20e962dd..1a451c7d 100644 --- a/apps/web/src/components/tab-bar/tab-bar.tsx +++ b/apps/web/src/components/tab-bar/tab-bar.tsx @@ -6,6 +6,11 @@ import { X, Plus, ChevronLeft, ChevronRight } from 'lucide-react' import { useTabStore } from '@/store/tab-store' import { getRouteConfig } from '@/lib/route-config' import { cn } from '@/lib/utils' +import { OrgSwitcherDropdown } from '@/components/org-switcher-dropdown' +import { WorkspaceSwitcherDropdown } from '@/components/workspace-switcher-dropdown' +import { WorkspaceQuickActions } from '@/components/workspace-quick-actions' +import { NotificationsBell } from '@/components/notifications-bell' +import { ModeToggle } from '@/components/modeToggle' import { Tooltip, TooltipContent, @@ -90,8 +95,7 @@ export function TabBar({ onNewTab }: TabBarProps) { if (path !== pathname) router.push(path) } - function handleClose(e: React.MouseEvent, path: string) { - e.stopPropagation() + const closeTabAndNavigate = useCallback((path: string) => { const { tabs, activeTabPath } = useTabStore.getState() const idx = tabs.findIndex(t => t.path === path) const newTabs = tabs.filter(t => t.path !== path) @@ -100,11 +104,44 @@ export function TabBar({ onNewTab }: TabBarProps) { router.push(next ?? '/dashboard') } closeTab(path) + }, [router, closeTab]) + + function handleClose(e: React.MouseEvent, path: string) { + e.stopPropagation() + closeTabAndNavigate(path) } + // Keyboard shortcuts: ⌥1–9 jump to tab, ⌥W close active tab. + // (⌘W / ⌘1–9 are browser-reserved and cannot be intercepted.) + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (!e.altKey || e.metaKey || e.ctrlKey || e.shiftKey) return + const t = e.target as HTMLElement | null + if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return + // e.code is layout-independent — on macOS ⌥ changes e.key ('w' → '∑'). + if (e.code === 'KeyW') { + const { activeTabPath } = useTabStore.getState() + if (!activeTabPath) return + e.preventDefault() + closeTabAndNavigate(activeTabPath) + return + } + const digit = /^Digit([1-9])$/.exec(e.code) + if (digit) { + const { tabs, activeTabPath } = useTabStore.getState() + const tab = tabs[Number(digit[1]) - 1] + if (!tab) return + e.preventDefault() + if (tab.path !== activeTabPath) router.push(tab.path) + } + } + window.addEventListener('keydown', onKeyDown) + return () => window.removeEventListener('keydown', onKeyDown) + }, [router, closeTabAndNavigate]) + return ( -
+
{/* Left scroll arrow */} - + {title} + {i < 9 && ( + + ⌥{i + 1} + + )} ) @@ -211,6 +253,17 @@ export function TabBar({ onNewTab }: TabBarProps) { Open tool (⌘K) + + {/* Global context chrome — same set & order as the NavBar so tab mode + never hides which org/workspace the open tools are scoped to. */} +
+
+ + + + + +
) diff --git a/apps/web/src/lib/workspace-scope-reset.ts b/apps/web/src/lib/workspace-scope-reset.ts new file mode 100644 index 00000000..b3bdad60 --- /dev/null +++ b/apps/web/src/lib/workspace-scope-reset.ts @@ -0,0 +1,42 @@ +// Clears in-memory copies of workspace-scoped data whenever the active +// workspace changes, so no tool ever shows the previous workspace's data. +// IN-MEMORY ONLY — must never call actions that mutate backend data. +// +// Works together with the `key={activeWorkspaceId}` remount in client-layout: +// the remount re-runs every tool's load effect; these resets make sure +// load-once guards (e.g. bookmarks' `hasSynced`) actually refetch. +import { useWorkspaceStore } from "@/store/workspace-store" +import { useBookmarkStore } from "@/store/bookmark-store" +import { usePasswordStore } from "@/store/password-store" +import { useEnvironmentManagerStore } from "@/store/environment-manager-store" +import { useApiKeyVaultStore } from "@/store/api-key-vault-store" + +let initialized = false +let prevWorkspaceId: string | null = null + +export function initWorkspaceScopeReset() { + if (initialized) return + initialized = true + + prevWorkspaceId = useWorkspaceStore.getState().activeWorkspaceId + + useWorkspaceStore.subscribe((state) => { + const next = state.activeWorkspaceId + if (next === prevWorkspaceId) return + const isRealSwitch = prevWorkspaceId !== null && next !== null + prevWorkspaceId = next + if (!isRealSwitch) return // initial hydration, not a switch + + // Bookmarks: persisted + load-once guard — reset both so it refetches. + useBookmarkStore.setState({ + bookmarks: [], + folders: [], + selectedFolderId: null, + hasSynced: false, + }) + // Secret tools: in-memory decrypted data — drop it at the scope boundary. + usePasswordStore.getState().clearPasswords() + useEnvironmentManagerStore.getState().clearSets() + useApiKeyVaultStore.getState().clearEntries() + }) +}